From 9f81c8a9db345140f6570b6aa4e4197ace3bd0f2 Mon Sep 17 00:00:00 2001 From: DAVID AMID Date: Sun, 30 Aug 2026 17:52:34 +0300 Subject: [PATCH 1/5] docs(spec): propose local distribution for a one-command Claude Code trial MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Someone who finds the repo today has to install Go 1.26 and a C toolchain, build with the right tags, run a binary, work out ANTHROPIC_BASE_URL, and trust that routing their coding agent through an unknown local proxy will not break it. Most evaluators stop at step one. The value we want them to feel first is KV-cache, which requires being on the wire, so this proposes making the PROXY trivial to install rather than avoiding it. Five pieces, ordered by what they unblock. Pure-Go release binaries. The quickstart tells everyone to set CGO_ENABLED=1, and the dependency tree suggests that is unnecessary for the default build: the tiktoken tokenizer is pure Go and says so in internal/tokens, modernc.org/sqlite is pure Go, and go-tree-sitter is the only cgo dep and is already gated behind cg_skeleton with a stub for the negative case. If CGO_ENABLED=0 builds, we get a static binary, a plain GOOS/GOARCH matrix in one CI job and ~20 lines of GoReleaser; if not, four platforms need per-platform runners. That check is Gate A and everything else in the piece scales off it. Homebrew tap as the primary path because it avoids the macOS Gatekeeper dialog that a curl download triggers. An --idle-exit watchdog so nothing outlives use, reusing the graceful-shutdown path already in main.go. Off by default, because a gateway deployment must never self-terminate. Two constraints are load-bearing: the threshold has a floor at store.ttl_seconds, since exit wipes frozen decisions and a freeze dying mid-session is the 11.5x cache-write bug FrozenLost exists to catch; and the keepalive inverts what idle means, because pinging is what it does while there is no client traffic, so a naive watchdog kills the feature it ships. A `cache` preset — cachesplit alone. Fully lossless: no content dropped, no markers, no expand tool, no LLM calls, so the loudest objection to a context proxy does not apply and that is verifiable from one config line. It is also the best-evidenced component we have (-34.1%, 0% -> 96.7% hit). A plugin that installs it. A plugin cannot set ANTHROPIC_BASE_URL — plugin settings.json accepts only agent and subagentStatusLine — so it is the installer and operator surface, not the transport, and it closes the loop by having Claude perform the settings edit. scripts/ does the deterministic steps; the skill does the part a script does badly, which is merging one key into a settings.json that already holds the user's theme, model, permission rules and possibly their own base URL. Lifecycle via a SessionStart hook rather than launchd: nothing left on the machine, no privileged install. Documents the four details that make that hook correct — never async, idempotent because the event also fires on clear/compact/resume/fork, fixed port because the env block and the hook must agree, and the clone-time trust prompt. Gateway conformance, five items, because this funnel puts us on the wire and a broken trial is a lost adopter. Two can make the demo read as negative: we buffer some SSE responses to look for an expand call, and Claude Code aborts a stream silent for 300s; and a rejected cache_control makes Claude Code disable prompt caching for the rest of the conversation, which is a silent switch-off of the thing we are selling — the strongest argument for shipping `cache` rather than a placement preset. Also records the adoption fact missing from our docs: setting ANTHROPIC_BASE_URL without a credential variable keeps the claude.ai subscription active, so an evaluator can trial context-guru with no API key at all. Non-goals are explicit: not a proxy replacement, no offloaders in the funnel default, no measurement-only mode (rejected — it diagnoses a problem it cannot fix without pings), and not the DAM integration. Five open questions for reviewers, and staging where stage 1 ships alone and carries most of the adoption win. Docs only — no code, no behavior change. Signed-off-by: DAVID AMID Assisted-By: Claude Opus 5 (1M context) --- .../2026-08-30-local-distribution-design.md | 264 ++++++++++++++++++ 1 file changed, 264 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-30-local-distribution-design.md diff --git a/docs/superpowers/specs/2026-08-30-local-distribution-design.md b/docs/superpowers/specs/2026-08-30-local-distribution-design.md new file mode 100644 index 00000000..52df6c57 --- /dev/null +++ b/docs/superpowers/specs/2026-08-30-local-distribution-design.md @@ -0,0 +1,264 @@ +# Local distribution: get context-guru running on a stranger's Claude Code in one command + +## Problem + +Someone finds the repo, wants to know whether it saves them money on their own Claude +Code sessions, and today has to: install Go 1.26, install a C toolchain, build with the +right tags, run a binary, work out `ANTHROPIC_BASE_URL`, and trust that routing their +coding agent through an unknown local proxy will not break it. Most evaluators stop at +step one. + +The value we want them to feel first is **KV-cache**, not the offloaders. That decides +almost everything below, because cache work requires being on the wire — so this proposal +is about making *the proxy* trivial to install, not about avoiding it. + +Target: **two commands, no toolchain, no guide, reversible.** + +``` +/plugin marketplace add rossoctl/context-guru +/context-guru:install +``` + +## What we ship + +Five pieces. Each is independently useful; the order is the order they unblock each other. + +| # | Piece | Why | +|---|---|---| +| 1 | Pure-Go release binaries + Homebrew tap | Removes the toolchain gate entirely | +| 2 | `--idle-exit` self-terminating proxy | Nothing left running on the machine | +| 3 | `cache` preset | The KV-cache-only pitch, fully lossless | +| 4 | A plugin: install skill + `scripts/` + `SessionStart` hook | One command, and an agent that can make the judgment calls | +| 5 | Gateway conformance fixes | Routing must not break their agent | + +### 1. Pure-Go binaries + +`docs/get-started/quickstart-proxy.md` tells every evaluator to set `CGO_ENABLED=1` and +install a C toolchain. Reading the dependency tree, that looks unnecessary for the default +build: + +| Dependency | cgo? | +|---|---| +| `tiktoken-go/tokenizer` | **No.** `internal/tokens/tokens.go` says it outright: *"o200k_base is embedded in the binary (pure-Go, offline, no CGO)"* | +| `modernc.org/sqlite` (dashboard) | **No** — pure Go by design | +| `tree-sitter/go-tree-sitter` | Yes — but gated behind `//go:build cg_skeleton`, with `internal/treesitter/stub.go` for `!cg_skeleton` | + +So without `-tags cg_skeleton` there should be no cgo dependency at all. + +**Gate A — verify before anything else** (30 seconds, and it decides the shape of all of +piece 1): + +```sh +CGO_ENABLED=0 go build -o /tmp/cg ./cmd/context-guru-proxy +``` + +If it passes we get a **static single binary**: a plain `GOOS`/`GOARCH` matrix in one CI +job, no C cross-toolchains, no zig, no libc coupling, ~20 lines of GoReleaser. If it +fails, cross-compiling four platforms needs per-platform runners and the binaries carry +libc requirements — still doable, several times the work. + +`skeleton` is the only casualty, and it costs nothing here: it is not in `codesmart` (the +default) and not in the cache story at all. Ship pure-Go binaries for everyone; a +`-skeleton` variant can follow per-platform, or not at all initially. + +**Distribution, ranked by friction:** + +1. **Homebrew tap** — `brew install rossoctl/tap/context-guru`. One command, **no Gatekeeper + dialog**, plus updates and clean uninstall. The primary path for macOS and Linux. +2. `go install` — one command, but puts the toolchain back. +3. `curl … | sh` — one command, but macOS quarantines an unsigned download ("cannot be + verified"). Fixable with `xattr -d com.apple.quarantine` in the installer, though + `curl | sh` reads badly to this repo's audience. Notarization needs a paid Apple account. +4. Plugin `bin/` — committing per-platform binaries to git is ugly, and `bin/` is barred + from claude.ai org distribution. + +Decision: **tap first, `curl | sh` as fallback, `go install` documented for Go users.** + +### 2. `--idle-exit` — the proxy cleans itself up + +Lifecycle is tied to sessions (piece 4 starts it), so nothing should outlive use. The +machinery already exists in `cmd/context-guru-proxy/main.go`: SIGINT/SIGTERM handling, +`srv.Shutdown`, and `armShutdown` to release the dashboard's SSE connections. An idle +watchdog just triggers the same path — atomic last-request timestamp, ticker, exit 0. + +Three constraints, all load-bearing: + +- **Off by default.** A gateway or eval-containers deployment must never self-terminate. + `--idle-exit=24h` is opt-in and set only by the installer. +- **The threshold has a floor: `store.ttl_seconds`.** Exit wipes the in-memory Store — + rewind stashes, frozen decisions, `cg:len:`. A frozen decision dying mid-session is the + 11.5× cache-write bug that `FrozenLost` exists to detect. The default TTL is 10000s + (≈2.8h), so 24h is safely past it and nothing live is lost — but a *short* threshold (10 + or 30 minutes) would be actively destructive. **Enforce the floor in config validation, + not in a doc comment.** +- **The keepalive inverts the meaning of idle.** If pings are the point, the proxy has to be + alive precisely when there is no client traffic — idle is when it works (X=280s → ping). A + naive watchdog kills the feature it ships. The idle clock must also reset on keepalive + activity, and exit only when no session has a pending ping schedule. 24h ≫ the keepalive + window (K=2 ≈ 10 min) so they coexist, but that has to be explicit rather than lucky. + +Self-kill alone is a footgun; self-kill **plus** `SessionStart` resurrection is coherent. +They ship together or not at all. Longer term a SQLite-backed store makes self-kill free at +any threshold — `store` already has the interface seam for it. + +### 3. A `cache` preset + +`pipeline: [cachesplit]` and nothing else. This is the funnel's default. + +Why it is the right first taste, and not a watered-down one: + +- **Fully lossless.** No content dropped, no `<>` markers, no `context_guru_expand` + tool injected, no LLM calls. The loudest objection to a context proxy — *you are editing + my agent's context* — does not apply, and that is verifiable by reading one config line. +- **It is the best-evidenced component we have**: −34.1% cost, 0% → 96.7% cache hit in an + isolated A/B, and it already ships in every preset. +- **The win is visible in the dashboard's four billed token tiers**, which come from the + provider's usage block. + +`safe` (`format` → `cachesplit`) is close but still rewrites JSON; `cache` makes the claim +exact. Note `cachesplit` and `cacheinject` are both no-ops on implicit prefix-cache backends +(vLLM, llm-d) — `apply/prefixsplit.go` says so — so the funnel's pitch is Anthropic-family +specific and the docs should say that plainly. + +### 4. The plugin: skill + `scripts/` + hook + +A plugin **cannot** set `ANTHROPIC_BASE_URL` — plugin `settings.json` accepts only `agent` +and `subagentStatusLine`. So the plugin is not the transport; it is the **installer and +operator surface**, and it closes the loop by having Claude do the file edit. + +``` +context-guru-plugin/ + .claude-plugin/plugin.json # userConfig: preset, port, scope + skills/install/SKILL.md # /context-guru:install — drives scripts/, makes the judgment calls + skills/status/SKILL.md # /context-guru:status — reads /stats, explains the numbers + skills/uninstall/SKILL.md # /context-guru:uninstall — removes the env key, stops the proxy + scripts/install.sh # brew → curl fallback; idempotent; prints machine-readable result + scripts/settings.py # merge/remove ONE key in a settings.json, with backup + scripts/start-proxy.sh # healthz probe, start only if absent, fixed port + hooks/hooks.json # SessionStart → start-proxy.sh +``` + +**Why a skill rather than a shell script alone.** A skill does not reduce permission +prompts, it *concentrates* them — an eight-step guide becomes eight prompts unless the skill +runs one command. The real reason for the skill is the part a script does badly: + +`init` must merge into an existing `~/.claude/settings.json` that already holds the user's +theme, model, permission rules, and possibly their own `ANTHROPIC_BASE_URL`. That is +judgment work — read it, back it up, add exactly one key, detect a conflicting base URL +already present, choose a scope, verify, and report. An agent handles that; `jq` gymnastics +against an unknown file does not. Every maintainer's own settings file is the proof: at +least one of ours already sets a base URL plus a benchmark override that must survive. + +So: **`scripts/` for the deterministic steps, the skill for the judgment and the verify.** + +**Scope choice, which the skill must ask about.** Precedence is managed → `--settings` → +`.claude/settings.local.json` → `.claude/settings.json` → `~/.claude/settings.json`, so user +scope is the *lowest*. + +| Write target | Reaches | Blast radius if the proxy is down | +|---|---|---| +| `.claude/settings.local.json` | you, this repo, gitignored | one repo — **the install default** | +| `.claude/settings.json` | everyone who clones the repo | one repo, whole team — team adoption | +| `~/.claude/settings.json` | you, every project on the machine | **every Claude Code session you have** | + +A global base URL pointing at `localhost` means a dead proxy breaks Claude Code everywhere, +including repos the evaluator never meant to experiment in. For a trial aimed at strangers +that blast radius is the main risk in this proposal — bigger than the build. Hence: default +to project-local, `--global` as an explicit opt-in, and `--idle-exit` paired with the +`SessionStart` hook so "is it running?" stops being the user's problem. + +**Why `SessionStart` and not a launchd/systemd service.** Session-scoped lifetime leaves +nothing running on the machine and needs no privileged install. Mechanics check out: +`SessionStart` supports `type: "command"`, hooks are awaited unless `async: true` is set, and +the default timeout is 600s — so a hook that starts the proxy and polls `/healthz` before +returning closes the race with the first API request. Four details: + +- **Never set `async: true`** on this hook; it reintroduces the race. +- **`SessionStart` also fires on `clear`, `compact`, `resume` and `fork`**, not just + `startup`. The starter must be idempotent — probe `/healthz`, start only if absent. + Compaction alone re-fires it repeatedly in a long session. +- **Fixed port.** The env block and the hook have to agree, and you cannot negotiate a + dynamic port after the env value is already written. Configurable at install time, written + to both places. Not 4000 — that collides with litellm. +- **Trust gate.** A hook in a committed `.claude/settings.json` that launches a binary + prompts on clone. Correct behaviour, but it means "clone and go" is really "clone, approve, + go", and the README should say so rather than surprise people. + +Note the plugin also gives us `SessionEnd` and `PreCompact`/`PostCompact` as explicit +signals, which is strictly better than `proxy/agentcompaction.go` inferring compaction from a +string match against a specific Claude Code build. Out of scope here; worth recording. + +### 5. Gateway conformance + +This funnel puts us on the wire, so a broken trial is a lost adopter. Checked against the +gateway protocol reference; each needs a test, and #1 and #2 are the ones that can make the +demo look *negative*: + +1. **Do not buffer SSE.** *"A gateway that buffers complete responses before relaying them + stalls the client"*, and Claude Code aborts a stream silent for 300s. We have + `sse_buffered` / `sse_buffered_pct` counters, so we do buffer some responses to inspect + for an expand call. Under the `cache` preset nothing injects the expand tool, so this + should be inert — verify it against a real session rather than assume. +2. **A rejected `cache_control` disables prompt caching for the rest of the conversation.** + Claude Code retries and turns the capability off. So a breakpoint-budget mistake is not an + error the user sees — it silently switches off the thing we are selling. This is the + strongest argument for shipping `cache` (no `cacheinject`) rather than a placement preset. +3. **The attribution block is stripped positionally.** Claude Code prepends it as the first + `system` block and the API strips it only if the array arrives unchanged. `cachesplit` + reshapes that array. Either prove the interaction is safe (our split has a + `minSplitTokens` floor of 1024, so it should never touch the small attribution block) or + ship `CLAUDE_CODE_ATTRIBUTION_HEADER=0` in the install. +4. **Forward error bodies byte-unmodified.** Claude Code's capability-rejection retry matches + on the upstream's error *wording*; wrapping errors breaks the recovery path. +5. **Serve `/v1/messages/count_tokens`.** Absent, Claude Code counts context by issuing + inference requests. Cheap to add. + +Confirmed already correct: `copyHeaders` forwards `anthropic-beta` (which carries the OAuth +capability — stripping it is a `401`), and auth headers are dropped only when a real API key +is configured. + +## Subscription auth works, and it is the headline + +Setting `ANTHROPIC_BASE_URL` **without** a credential variable keeps the user's claude.ai +login active: their Pro/Max usage limits and billing continue to apply. Combined with the +above, an evaluator can trial context-guru **with no API key at all.** + +That is the single most important adoption fact about this project and it appears nowhere in +our docs. One caveat to state honestly in the docs: on subscription billing the saving lands +in usage limits rather than dollars, so `/stats` cost figures are list-price estimates that +do not match a subscriber's bill. + +## Non-goals + +- **Not a replacement for the proxy.** This is packaging for the proxy, and the proxy stays + the only place cache work happens. +- **No offloaders in the funnel default.** They are a separate story (see the plugin-transport + analysis, PR #129) and lossy-by-design, which is the wrong first impression for a cache pitch. +- **No measurement-only mode.** A plugin that merely reports what cache expiry costs was + considered and rejected: without the ability to ping, it diagnoses a problem it cannot fix. +- **Not the DAM integration.** DAM is harness-plural with its own gateway; a Claude Code + plugin covers one harness. Separate proposal. + +## Open questions for reviewers + +1. **Gate A** — does `CGO_ENABLED=0` build? Everything in piece 1 scales off this answer. +2. **Does an `env` block merge per-key across settings files, or is the whole object + replaced by the highest-precedence file?** The docs say *lists* merge and call `env` "an + ordinary key". If it is replaced wholesale, a user-scope install silently stops working in + any repo that ships its own `env` block — i.e. exactly the most-configured repos. Needs a + 10-minute test. +3. **Default scope: project-local or global?** Local is safer, global is the better demo. The + proposal picks local; happy to be overruled. +4. **Idle-exit default: 24h?** And should the floor be `max(2 × store.ttl_seconds, 1h)`? +5. **Homebrew tap: `rossoctl/homebrew-tap` as a new repo, and who owns release signing?** + +## Staging + +- **Stage 0** — Gate A + the `env` merge test. Half a day. Decides the rest. +- **Stage 1** — GoReleaser + tap + `cache` preset + fix the CGO claim in the quickstart. The + funnel already works here for anyone willing to run two commands by hand. +- **Stage 2** — `--idle-exit` + the plugin (skill, scripts, `SessionStart` hook). +- **Stage 3** — conformance items 1–5 with tests. + +Stage 1 is shippable alone and is most of the adoption win; stages 2–3 make it pleasant and +safe. From 9f7e221291945ed1b20dc493d7866451997e2173 Mon Sep 17 00:00:00 2001 From: DAVID AMID Date: Sun, 30 Aug 2026 18:35:11 +0300 Subject: [PATCH 2/5] docs(spec): resolve both distribution gates with reproducible checks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The proposal left two questions open that could each have reshaped it. Both are now measured, with the scripts committed so a reviewer can re-run them. Gate A — can we ship a static binary with no C toolchain? Yes. scripts/gate-a-purego.sh runs in a golang:1.26 container, so it needs no local Go: default tags, CGO_ENABLED=0 PASS -tags cg_skeleton, CGO_ENABLED=0 fails, as intended GOOS/GOARCH x {linux,darwin} x {amd64,arm64} PASS, all four artifact 30.5 MB stripped, static The cg_skeleton failure is "build constraints exclude all Go files in .../go-sitter-forest/typescript" — the cgo-disabled signature — which confirms tree-sitter is the only C dependency and that the gating build tag is doing its job. ldd reports "not a dynamic executable", so there is no libc coupling either. The first run of this check reported a failure for the wrong reason (proxy.golang.org timeouts, not cgo), so the script now fetches the grammar modules first and says so: a network error must not be mistakable for the error we are testing for. The practical consequence is bigger than the release tooling. docs/get-started/quickstart-proxy.md tells every evaluator to set CGO_ENABLED=1 and install a C toolchain, and that is only true for a cg_skeleton build. Fixing that paragraph removes the largest onboarding gate in the project and depends on nothing else in this proposal. Gate B — does an `env` block merge per key across settings files, or does the highest-precedence file replace the whole object? It merges. scripts/gate-b-envmerge.sh relocates the config dir with CLAUDE_CONFIG_DIR so it never touches a real ~/.claude, and reads the result from a SessionStart hook that dumps the environment it was handed. With CG_USER+CG_BOTH in user scope and CG_BOTH+CG_PROJ in project scope, the hook saw CG_BOTH=project, CG_PROJ=project and CG_USER=user — so a higher-precedence file wins only the keys it actually sets, and a user-scope install survives a repo shipping its own env block. That removes the failure mode the gate existed to catch: a user-scope install silently dying in the most-configured repos. It does not change the default-scope recommendation, because per-key merge was only one argument for project-local and the blast-radius argument stands on its own. It does mean --global needs no per-repo caveat, and that init has one conflict to reason about rather than two: an ANTHROPIC_BASE_URL the user already set themselves. Open questions are down from five to five, but they are now all judgment calls (default scope, idle-exit floor, tap ownership, artifact size, whether to ship skeleton in v1) rather than unknowns that could invalidate the plan. Docs and test scripts only — no code, no behavior change. Signed-off-by: DAVID AMID Assisted-By: Claude Opus 5 (1M context) --- .../2026-08-30-local-distribution-design.md | 90 +++++++++++++----- scripts/gate-a-purego.sh | 92 ++++++++++++++++++ scripts/gate-b-envmerge.sh | 95 +++++++++++++++++++ 3 files changed, 255 insertions(+), 22 deletions(-) create mode 100755 scripts/gate-a-purego.sh create mode 100755 scripts/gate-b-envmerge.sh diff --git a/docs/superpowers/specs/2026-08-30-local-distribution-design.md b/docs/superpowers/specs/2026-08-30-local-distribution-design.md index 52df6c57..b55026d7 100644 --- a/docs/superpowers/specs/2026-08-30-local-distribution-design.md +++ b/docs/superpowers/specs/2026-08-30-local-distribution-design.md @@ -45,17 +45,26 @@ build: So without `-tags cg_skeleton` there should be no cgo dependency at all. -**Gate A — verify before anything else** (30 seconds, and it decides the shape of all of -piece 1): +**Gate A — measured, 2026-08-30. It passes.** Reproduce with `scripts/gate-a-purego.sh` +(runs in a `golang:1.26` container, so no local Go needed): -```sh -CGO_ENABLED=0 go build -o /tmp/cg ./cmd/context-guru-proxy -``` - -If it passes we get a **static single binary**: a plain `GOOS`/`GOARCH` matrix in one CI -job, no C cross-toolchains, no zig, no libc coupling, ~20 lines of GoReleaser. If it -fails, cross-compiling four platforms needs per-platform runners and the binaries carry -libc requirements — still doable, several times the work. +| Check | Result | +|---|---| +| default tags, `CGO_ENABLED=0` | **PASS** — builds clean | +| `-tags cg_skeleton`, `CGO_ENABLED=0` | **PASS (fails as intended)** — `build constraints exclude all Go files in …/go-sitter-forest/typescript`, the cgo-disabled signature. Confirms tree-sitter is the only C dependency | +| `GOOS`/`GOARCH` × `linux,darwin` × `amd64,arm64` | **PASS, all four** | +| artifact | **30.5 MB stripped** (`-ldflags="-s -w"`), 38.3 MB unstripped, `ldd` → *"not a dynamic executable"* | + +So we get a **fully static single binary**: a plain `GOOS`/`GOARCH` matrix in one CI job, +no C cross-toolchains, no zig, no libc coupling, ~20 lines of GoReleaser. 30 MB is on the +large side — the o200k_base tokenizer tables, the embedded dashboard UI and +`modernc.org/sqlite` account for most of it — but it is a fine download for `brew` or +`curl`, and it needs no runtime dependencies at all. + +**Consequence for the docs:** `docs/get-started/quickstart-proxy.md` currently instructs +every evaluator to set `CGO_ENABLED=1` and install a C toolchain. That is only true for a +`cg_skeleton` build. Fixing that paragraph is a one-line change that removes the largest +onboarding gate we have, independent of everything else in this proposal. `skeleton` is the only casualty, and it costs nothing here: it is not in `codesmart` (the default) and not in the cache story at all. Ship pure-Go binaries for everyone; a @@ -167,6 +176,27 @@ that blast radius is the main risk in this proposal — bigger than the build. H to project-local, `--global` as an explicit opt-in, and `--idle-exit` paired with the `SessionStart` hook so "is it running?" stops being the user's problem. +**Gate B — measured, 2026-08-30: `env` blocks MERGE per key.** Reproduce with +`scripts/gate-b-envmerge.sh` (relocates the config dir via `CLAUDE_CONFIG_DIR`, so it never +touches a real `~/.claude`; a `SessionStart` hook dumps the environment it was handed). + +With `CG_USER`+`CG_BOTH` in user scope and `CG_BOTH`+`CG_PROJ` in project scope, the hook saw: + +``` +CG_BOTH=project <- project won the key it sets +CG_PROJ=project +CG_USER=user <- survived; the user object was NOT replaced +``` + +So a higher-precedence file wins only the keys it actually sets, and **a user-scope install +survives a repo that ships its own `env` block.** That removes the failure mode this gate +existed to catch. + +It does *not* change the default-scope recommendation. Per-key merge was one argument for +project-local; the blast-radius argument above stands on its own and is the stronger one. It +does mean `--global` is safe to offer without a per-repo caveat, and that `init` only has to +reason about one conflict: an `ANTHROPIC_BASE_URL` the user already set themselves. + **Why `SessionStart` and not a launchd/systemd service.** Session-scoped lifetime leaves nothing running on the machine and needs no privileged install. Mechanics check out: `SessionStart` supports `type: "command"`, hooks are awaited unless `async: true` is set, and @@ -239,26 +269,42 @@ do not match a subscriber's bill. - **Not the DAM integration.** DAM is harness-plural with its own gateway; a Claude Code plugin covers one harness. Separate proposal. +## Resolved before review + +Both gates that would have reshaped the plan were run rather than left open. Scripts are in +this PR so a reviewer can re-run them. + +| Gate | Question | Answer | +|---|---|---| +| A | Does `CGO_ENABLED=0` build? | **Yes** — static 30.5 MB binary, all four targets, no C toolchain. `scripts/gate-a-purego.sh` | +| B | Does `env` merge per key or get replaced wholesale? | **Merges per key** — a user-scope install survives a repo's own `env` block. `scripts/gate-b-envmerge.sh` | + +Neither answer changes a decision in this proposal; both remove a way it could have been +wrong. Gate A is the load-bearing one — it is what makes piece 1 twenty lines of GoReleaser +instead of a per-platform CI build. + ## Open questions for reviewers -1. **Gate A** — does `CGO_ENABLED=0` build? Everything in piece 1 scales off this answer. -2. **Does an `env` block merge per-key across settings files, or is the whole object - replaced by the highest-precedence file?** The docs say *lists* merge and call `env` "an - ordinary key". If it is replaced wholesale, a user-scope install silently stops working in - any repo that ships its own `env` block — i.e. exactly the most-configured repos. Needs a - 10-minute test. -3. **Default scope: project-local or global?** Local is safer, global is the better demo. The - proposal picks local; happy to be overruled. -4. **Idle-exit default: 24h?** And should the floor be `max(2 × store.ttl_seconds, 1h)`? -5. **Homebrew tap: `rossoctl/homebrew-tap` as a new repo, and who owns release signing?** +1. **Default scope: project-local or global?** Local is safer (a dead proxy costs one repo), + global is the better demo. The proposal picks local; happy to be overruled. Gate B means + global carries no per-repo caveat. +2. **Idle-exit default: 24h?** And should the floor be `max(2 × store.ttl_seconds, 1h)`? +3. **Homebrew tap: `rossoctl/homebrew-tap` as a new repo, and who owns release signing?** +4. **Does the 30 MB artifact bother anyone?** It is mostly tokenizer tables, the embedded + dashboard UI and `modernc.org/sqlite`. A `-slim` build without the dashboard is possible if + it matters, but it costs the demo its best surface. +5. **Ship `skeleton` at all in v1?** It is the only thing needing cgo, so a `-skeleton` + variant means per-platform CI for one component that is not in `codesmart` and not in the + cache story. Proposal: omit it from the first release and document the source build. ## Staging -- **Stage 0** — Gate A + the `env` merge test. Half a day. Decides the rest. +- ~~**Stage 0** — Gate A + the `env` merge test.~~ **Done** (see Resolved above). - **Stage 1** — GoReleaser + tap + `cache` preset + fix the CGO claim in the quickstart. The funnel already works here for anyone willing to run two commands by hand. - **Stage 2** — `--idle-exit` + the plugin (skill, scripts, `SessionStart` hook). - **Stage 3** — conformance items 1–5 with tests. Stage 1 is shippable alone and is most of the adoption win; stages 2–3 make it pleasant and -safe. +safe. With Gate A answered, the smallest useful change in the whole proposal is a one-line +quickstart fix telling people they do not need a C compiler. diff --git a/scripts/gate-a-purego.sh b/scripts/gate-a-purego.sh new file mode 100755 index 00000000..5fbf1e50 --- /dev/null +++ b/scripts/gate-a-purego.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash +# Gate A: can context-guru ship as a static binary with no C toolchain? +# +# Runs in a golang container, so it needs no local Go install. A named volume holds the +# module cache so the matrix downloads once rather than once per target. +# +# 1. default tags, CGO_ENABLED=0 -> must SUCCEED (the binary we want to ship) +# 2. -tags cg_skeleton, CGO_ENABLED=0 -> must FAIL (tree-sitter is the only cgo dep) +# 3. GOOS/GOARCH matrix, CGO_ENABLED=0 -> must SUCCEED for all four release targets +# 4. artifact size and linkage +# +# Results as of 2026-08-30 (go 1.26.4, module github.com/rossoctl/context-guru): +# 1 PASS · 2 PASS (fails with "build constraints exclude all Go files", the +# cgo-disabled signature) · 3 PASS on all four · 4 30.5 MB stripped, "not a dynamic +# executable". +# +# Note: `go mod download all` is flaky against proxy.golang.org from here and may report +# failures while the builds still succeed — `go build` fetches only what it needs. +set -uo pipefail +cd "$(dirname "$0")/.." + +IMG="${IMG:-golang:1.26}" +VOL="${VOL:-cg-gomodcache}" +LOG="${LOG:-/tmp/gate-a.log}" +: >"$LOG" + +run() { docker run --rm -v "$PWD":/src -v "$VOL":/go/pkg/mod -w /src \ + -e GOFLAGS=-buildvcs=false "$@"; } + +result() { # name, expected(ok|fail), exit code + local name="$1" want="$2" code="$3" verdict + if [[ "$want" == ok ]]; then + [[ "$code" -eq 0 ]] && verdict="PASS" || verdict="FAIL" + else + [[ "$code" -ne 0 ]] && verdict="PASS (failed as expected)" || verdict="UNEXPECTED SUCCESS" + fi + printf '%-38s exit=%-3s %s\n' "$name" "$code" "$verdict" | tee -a "$LOG" +} + +echo "== prewarming module cache (best effort, up to 3 attempts) ==" | tee -a "$LOG" +for i in 1 2 3; do + run -e CGO_ENABLED=0 "$IMG" go mod download all >>"$LOG" 2>&1 && { echo " ok on attempt $i" | tee -a "$LOG"; break; } + echo " attempt $i failed (builds may still succeed)" | tee -a "$LOG" +done + +echo | tee -a "$LOG" +echo "== 1. default tags, CGO_ENABLED=0 (want SUCCESS) ==" | tee -a "$LOG" +run -e CGO_ENABLED=0 "$IMG" go build -o /tmp/cg ./cmd/context-guru-proxy >>"$LOG" 2>&1 +result "default tags / CGO=0" ok $? + +echo | tee -a "$LOG" +echo "== 2. -tags cg_skeleton, CGO_ENABLED=0 (want FAILURE) ==" | tee -a "$LOG" +echo "-- fetching tree-sitter grammars first, so a network error cannot be mistaken" | tee -a "$LOG" +echo "-- for the cgo error we are actually testing for" | tee -a "$LOG" +grammars=no +for i in 1 2 3 4 5; do + run -e CGO_ENABLED=0 "$IMG" go mod download github.com/alexaandru/go-sitter-forest/c >/dev/null 2>&1 \ + && { grammars=yes; echo " fetched on attempt $i" | tee -a "$LOG"; break; } + echo " attempt $i failed" | tee -a "$LOG" +done +if [[ "$grammars" == yes ]]; then + run -e CGO_ENABLED=0 "$IMG" go build -tags cg_skeleton -o /tmp/cg-skel ./cmd/context-guru-proxy >>"$LOG" 2>&1 + result "cg_skeleton / CGO=0" fail $? + grep -m1 "build constraints exclude" "$LOG" >/dev/null \ + && echo " reason: \"build constraints exclude all Go files\" = cgo disabled" | tee -a "$LOG" +else + echo "cg_skeleton / CGO=0 INCONCLUSIVE (could not fetch grammars)" | tee -a "$LOG" +fi + +echo | tee -a "$LOG" +echo "== 3. cross-compile matrix, CGO_ENABLED=0 ==" | tee -a "$LOG" +for t in linux/amd64 linux/arm64 darwin/amd64 darwin/arm64; do + os="${t%/*}"; arch="${t#*/}" + run -e CGO_ENABLED=0 -e GOOS="$os" -e GOARCH="$arch" "$IMG" \ + go build -o "/tmp/cg-$os-$arch" ./cmd/context-guru-proxy >>"$LOG" 2>&1 + result "$t" ok $? +done + +echo | tee -a "$LOG" +echo "== 4. artifact size and linkage (linux/amd64) ==" | tee -a "$LOG" +# Inspected inside the container: Rancher Desktop does not share /tmp with the host. +run -e CGO_ENABLED=0 -e GOOS=linux -e GOARCH=amd64 "$IMG" sh -c ' + set -e + go build -ldflags="-s -w" -o /tmp/cg ./cmd/context-guru-proxy + ls -l /tmp/cg | awk "{printf \"stripped: %.1f MB\n\", \$5/1048576}" + go build -o /tmp/cg-plain ./cmd/context-guru-proxy + ls -l /tmp/cg-plain | awk "{printf \"unstripped: %.1f MB\n\", \$5/1048576}" + printf "linkage: "; ldd /tmp/cg 2>&1 | head -1 +' 2>&1 | tee -a "$LOG" + +echo | tee -a "$LOG" +echo "full log: $LOG" diff --git a/scripts/gate-b-envmerge.sh b/scripts/gate-b-envmerge.sh new file mode 100755 index 00000000..ac302595 --- /dev/null +++ b/scripts/gate-b-envmerge.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +# Gate B: does an `env` block MERGE per-key across settings files, or does the +# highest-precedence file replace the whole object? +# +# Why it matters for distribution: `context-guru init` writes ANTHROPIC_BASE_URL into a +# settings file. If a lower-precedence file's `env` object is replaced wholesale by a +# higher one, then a user-scope install (~/.claude/settings.json, the LOWEST precedence) +# silently stops working in any repo that ships its own `env` block — i.e. exactly the +# most-configured repos, and with no error. +# +# Method: no model call, no network. A SessionStart hook dumps the environment it was +# given to a file. Claude Code runs SessionStart hooks before the first request, so the +# session is killed immediately afterwards and the dump is still written. +# +# user scope (CLAUDE_CONFIG_DIR) : CG_USER=user CG_BOTH=user +# project scope (.claude/…json) : CG_BOTH=project CG_PROJ=project +# +# MERGE => CG_USER=user, CG_BOTH=project, CG_PROJ=project (union, project wins the clash) +# REPLACE => CG_USER unset, CG_BOTH=project, CG_PROJ=project (user object discarded) +# +# The real user's ~/.claude is never touched: CLAUDE_CONFIG_DIR relocates it. +set -uo pipefail + +LAB="$(mktemp -d /tmp/cg-envmerge.XXXXXX)" +CFG="$LAB/cfgdir" +PROJ="$LAB/proj" +DUMP="$LAB/env.txt" +mkdir -p "$CFG" "$PROJ/.claude" + +cat >"$CFG/settings.json" < $DUMP" } + ] + } + ] + } +} +JSON + +cat >"$PROJ/.claude/settings.json" <"$LAB/claude.out" 2>&1 +echo "claude exit=$? (output: $LAB/claude.out)" +echo + +if [[ ! -s "$DUMP" ]]; then + echo "RESULT: INCONCLUSIVE — hook produced no dump." + echo " Check $LAB/claude.out; the hook may not have run (trust gate?) or CG_ vars" + echo " were not exported to hook processes." + exit 2 +fi + +echo "=== environment the hook saw ===" +cat "$DUMP" +echo + +got_user=$(grep -c '^CG_USER=user$' "$DUMP" || true) +got_both=$(grep -c '^CG_BOTH=project$' "$DUMP" || true) +got_proj=$(grep -c '^CG_PROJ=project$' "$DUMP" || true) + +echo "=== verdict ===" +if [[ "$got_user" -eq 1 && "$got_both" -eq 1 && "$got_proj" -eq 1 ]]; then + echo "MERGE — env objects union across scopes; the higher-precedence file wins only" + echo " the keys it actually sets. A user-scope install survives a project env block." +elif [[ "$got_user" -eq 0 && "$got_both" -eq 1 && "$got_proj" -eq 1 ]]; then + echo "REPLACE — the project env object replaced the user one wholesale (CG_USER lost)." + echo " A user-scope install silently dies in any repo shipping its own env block." + echo " => init must write project-local scope, or verify per-repo." +else + echo "UNEXPECTED — CG_USER=$got_user CG_BOTH=$got_both CG_PROJ=$got_proj" + echo " Inspect the dump above before drawing a conclusion." +fi From 3c59d452d6ee2bdaae17e56f04862c9e72c0b51b Mon Sep 17 00:00:00 2001 From: DAVID AMID Date: Sun, 30 Aug 2026 18:50:20 +0300 Subject: [PATCH 3/5] docs(spec): lead the distribution spec with the measured findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The gate results were appended near the end, so a reviewer met the hedged version of the argument first and the evidence several screens later. Three changes, no new claims. A "Findings up front" table now sits directly after the Problem, giving both answers, what each rules out, and the script that reproduces it. The "What we ship" table gains a "Gated on" column so it is visible at a glance that pieces 1 and 4 are unblocked rather than speculative. The trailing "Resolved before review" section is removed as a duplicate of the new one, and Open Questions now opens by saying the remaining five items are judgment calls rather than unknowns. Piece 1 and the Problem statement also stop hedging. "Reading the dependency tree, that looks unnecessary" and "there should be no cgo dependency at all" were written before the check ran; both now state the measured result and the dependency table names go-tree-sitter as the only cgo path rather than implying there might be others. The Problem section says plainly that step one of the current onboarding is avoidable and that our own quickstart is wrong about it, because that is the finding most likely to be acted on independently of the rest of the proposal. Docs only — no code, no behavior change. Signed-off-by: DAVID AMID Assisted-By: Claude Opus 5 (1M context) --- .../2026-08-30-local-distribution-design.md | 58 ++++++++++--------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/docs/superpowers/specs/2026-08-30-local-distribution-design.md b/docs/superpowers/specs/2026-08-30-local-distribution-design.md index b55026d7..cdf92552 100644 --- a/docs/superpowers/specs/2026-08-30-local-distribution-design.md +++ b/docs/superpowers/specs/2026-08-30-local-distribution-design.md @@ -8,6 +8,10 @@ right tags, run a binary, work out `ANTHROPIC_BASE_URL`, and trust that routing coding agent through an unknown local proxy will not break it. Most evaluators stop at step one. +Step one turns out to be avoidable. **The C toolchain is not required** — measured, not +assumed (Gate A below): `CGO_ENABLED=0` produces a fully static binary for all four release +targets, and our own quickstart is telling people otherwise. + The value we want them to feel first is **KV-cache**, not the offloaders. That decides almost everything below, because cache work requires being on the wire — so this proposal is about making *the proxy* trivial to install, not about avoiding it. @@ -19,31 +23,44 @@ Target: **two commands, no toolchain, no guide, reversible.** /context-guru:install ``` +## Findings up front + +Two questions could each have reshaped this proposal. Both were run before review rather +than left open; the scripts ship alongside so a reviewer can re-run them. + +| Gate | Question | Answer | Reproduce | +|---|---|---|---| +| **A** | Does `CGO_ENABLED=0` build? | **Yes.** Static 30.5 MB binary, all four release targets, no C toolchain. `cg_skeleton` is the only cgo path. | `scripts/gate-a-purego.sh` | +| **B** | Does an `env` block merge per key, or does the highest-precedence file replace the whole object? | **Merges per key.** A user-scope install survives a repo shipping its own `env` block. | `scripts/gate-b-envmerge.sh` | + +Neither answer changes a decision below; both remove a way this could have been wrong. Gate +A is the load-bearing one — it turns piece 1 from a per-platform CI build into a plain +`GOOS`/`GOARCH` matrix, and it means **the single smallest useful change in this whole +proposal is a one-line quickstart fix telling people they do not need a C compiler.** + ## What we ship Five pieces. Each is independently useful; the order is the order they unblock each other. -| # | Piece | Why | -|---|---|---| -| 1 | Pure-Go release binaries + Homebrew tap | Removes the toolchain gate entirely | -| 2 | `--idle-exit` self-terminating proxy | Nothing left running on the machine | -| 3 | `cache` preset | The KV-cache-only pitch, fully lossless | -| 4 | A plugin: install skill + `scripts/` + `SessionStart` hook | One command, and an agent that can make the judgment calls | -| 5 | Gateway conformance fixes | Routing must not break their agent | +| # | Piece | Why | Gated on | +|---|---|---|---| +| 1 | Pure-Go release binaries + Homebrew tap | Removes the toolchain gate entirely | Gate A ✅ | +| 2 | `--idle-exit` self-terminating proxy | Nothing left running on the machine | — | +| 3 | `cache` preset | The KV-cache-only pitch, fully lossless | — | +| 4 | A plugin: install skill + `scripts/` + `SessionStart` hook | One command, and an agent that can make the judgment calls | Gate B ✅ | +| 5 | Gateway conformance fixes | Routing must not break their agent | — | ### 1. Pure-Go binaries `docs/get-started/quickstart-proxy.md` tells every evaluator to set `CGO_ENABLED=1` and -install a C toolchain. Reading the dependency tree, that looks unnecessary for the default -build: +install a C toolchain. The dependency tree says that is unnecessary for the default build, +and Gate A confirms it: | Dependency | cgo? | |---|---| | `tiktoken-go/tokenizer` | **No.** `internal/tokens/tokens.go` says it outright: *"o200k_base is embedded in the binary (pure-Go, offline, no CGO)"* | | `modernc.org/sqlite` (dashboard) | **No** — pure Go by design | -| `tree-sitter/go-tree-sitter` | Yes — but gated behind `//go:build cg_skeleton`, with `internal/treesitter/stub.go` for `!cg_skeleton` | - -So without `-tags cg_skeleton` there should be no cgo dependency at all. +| `tree-sitter/go-tree-sitter` | Yes — the only one. Gated behind `//go:build cg_skeleton`, with `internal/treesitter/stub.go` for `!cg_skeleton` | **Gate A — measured, 2026-08-30. It passes.** Reproduce with `scripts/gate-a-purego.sh` (runs in a `golang:1.26` container, so no local Go needed): @@ -269,22 +286,11 @@ do not match a subscriber's bill. - **Not the DAM integration.** DAM is harness-plural with its own gateway; a Claude Code plugin covers one harness. Separate proposal. -## Resolved before review - -Both gates that would have reshaped the plan were run rather than left open. Scripts are in -this PR so a reviewer can re-run them. - -| Gate | Question | Answer | -|---|---|---| -| A | Does `CGO_ENABLED=0` build? | **Yes** — static 30.5 MB binary, all four targets, no C toolchain. `scripts/gate-a-purego.sh` | -| B | Does `env` merge per key or get replaced wholesale? | **Merges per key** — a user-scope install survives a repo's own `env` block. `scripts/gate-b-envmerge.sh` | - -Neither answer changes a decision in this proposal; both remove a way it could have been -wrong. Gate A is the load-bearing one — it is what makes piece 1 twenty lines of GoReleaser -instead of a per-platform CI build. - ## Open questions for reviewers +Both gates are answered (see [Findings up front](#findings-up-front)), so everything below is +a judgment call rather than an unknown that could invalidate the plan. + 1. **Default scope: project-local or global?** Local is safer (a dead proxy costs one repo), global is the better demo. The proposal picks local; happy to be overruled. Gate B means global carries no per-repo caveat. From 3dedb9768847a162f3cbbb3b105ce4f38b293be8 Mon Sep 17 00:00:00 2001 From: DAVID AMID Date: Sun, 30 Aug 2026 18:58:49 +0300 Subject: [PATCH 4/5] docs(spec): move the verification detail to an appendix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit put both checks at the top of the spec, which was the wrong call. They were de-risking, not findings: each came back the way the proposal already assumed, so neither changes a decision. Opening with them made a reviewer read what we had been worried about before reading what is being proposed. The checks now live in an "Appendix: Verification" at the end, with the reproduce commands and the honest note about the cg_skeleton check first failing for the wrong reason. The body keeps only what is material where it is material: piece 1 states that CGO_ENABLED=0 yields a static 30.5 MB binary for all four targets and that the quickstart's C-toolchain instruction is wrong, and piece 4 states in one sentence that env blocks merge per key so a user-scope install is not clobbered. Both point at the appendix rather than carrying the evidence inline. Also drops the "Gated on" column from the ship table and the surviving "Gate A/Gate B" references in prose, which only made sense while the gates were the frame. Net 10 lines shorter and the proposal reads as a proposal. Docs only — no code, no behavior change. Signed-off-by: DAVID AMID Assisted-By: Claude Opus 5 (1M context) --- .../2026-08-30-local-distribution-design.md | 136 ++++++++---------- 1 file changed, 63 insertions(+), 73 deletions(-) diff --git a/docs/superpowers/specs/2026-08-30-local-distribution-design.md b/docs/superpowers/specs/2026-08-30-local-distribution-design.md index cdf92552..877271b2 100644 --- a/docs/superpowers/specs/2026-08-30-local-distribution-design.md +++ b/docs/superpowers/specs/2026-08-30-local-distribution-design.md @@ -8,9 +8,9 @@ right tags, run a binary, work out `ANTHROPIC_BASE_URL`, and trust that routing coding agent through an unknown local proxy will not break it. Most evaluators stop at step one. -Step one turns out to be avoidable. **The C toolchain is not required** — measured, not -assumed (Gate A below): `CGO_ENABLED=0` produces a fully static binary for all four release -targets, and our own quickstart is telling people otherwise. +Step one turns out to be avoidable. **The C toolchain is not required** — `CGO_ENABLED=0` +produces a fully static binary for all four release targets, and our own quickstart is +telling people otherwise. The value we want them to feel first is **KV-cache**, not the offloaders. That decides almost everything below, because cache work requires being on the wire — so this proposal @@ -23,38 +23,22 @@ Target: **two commands, no toolchain, no guide, reversible.** /context-guru:install ``` -## Findings up front - -Two questions could each have reshaped this proposal. Both were run before review rather -than left open; the scripts ship alongside so a reviewer can re-run them. - -| Gate | Question | Answer | Reproduce | -|---|---|---|---| -| **A** | Does `CGO_ENABLED=0` build? | **Yes.** Static 30.5 MB binary, all four release targets, no C toolchain. `cg_skeleton` is the only cgo path. | `scripts/gate-a-purego.sh` | -| **B** | Does an `env` block merge per key, or does the highest-precedence file replace the whole object? | **Merges per key.** A user-scope install survives a repo shipping its own `env` block. | `scripts/gate-b-envmerge.sh` | - -Neither answer changes a decision below; both remove a way this could have been wrong. Gate -A is the load-bearing one — it turns piece 1 from a per-platform CI build into a plain -`GOOS`/`GOARCH` matrix, and it means **the single smallest useful change in this whole -proposal is a one-line quickstart fix telling people they do not need a C compiler.** - ## What we ship Five pieces. Each is independently useful; the order is the order they unblock each other. -| # | Piece | Why | Gated on | -|---|---|---|---| -| 1 | Pure-Go release binaries + Homebrew tap | Removes the toolchain gate entirely | Gate A ✅ | -| 2 | `--idle-exit` self-terminating proxy | Nothing left running on the machine | — | -| 3 | `cache` preset | The KV-cache-only pitch, fully lossless | — | -| 4 | A plugin: install skill + `scripts/` + `SessionStart` hook | One command, and an agent that can make the judgment calls | Gate B ✅ | -| 5 | Gateway conformance fixes | Routing must not break their agent | — | +| # | Piece | Why | +|---|---|---| +| 1 | Pure-Go release binaries + Homebrew tap | Removes the toolchain gate entirely | +| 2 | `--idle-exit` self-terminating proxy | Nothing left running on the machine | +| 3 | `cache` preset | The KV-cache-only pitch, fully lossless | +| 4 | A plugin: install skill + `scripts/` + `SessionStart` hook | One command, and an agent that can make the judgment calls | +| 5 | Gateway conformance fixes | Routing must not break their agent | ### 1. Pure-Go binaries `docs/get-started/quickstart-proxy.md` tells every evaluator to set `CGO_ENABLED=1` and -install a C toolchain. The dependency tree says that is unnecessary for the default build, -and Gate A confirms it: +install a C toolchain. Only one dependency needs it, and it is already behind a build tag: | Dependency | cgo? | |---|---| @@ -62,26 +46,18 @@ and Gate A confirms it: | `modernc.org/sqlite` (dashboard) | **No** — pure Go by design | | `tree-sitter/go-tree-sitter` | Yes — the only one. Gated behind `//go:build cg_skeleton`, with `internal/treesitter/stub.go` for `!cg_skeleton` | -**Gate A — measured, 2026-08-30. It passes.** Reproduce with `scripts/gate-a-purego.sh` -(runs in a `golang:1.26` container, so no local Go needed): +Confirmed by building it (see [Verification](#verification)): `CGO_ENABLED=0` yields a +**fully static single binary** — 30.5 MB stripped, `ldd` reports *"not a dynamic +executable"* — for all four release targets. So releases are a plain `GOOS`/`GOARCH` matrix +in one CI job: no C cross-toolchains, no zig, no libc coupling, ~20 lines of GoReleaser. +30 MB is on the large side (tokenizer tables, the embedded dashboard UI and +`modernc.org/sqlite`) but it is a fine `brew` or `curl` download with no runtime deps. -| Check | Result | -|---|---| -| default tags, `CGO_ENABLED=0` | **PASS** — builds clean | -| `-tags cg_skeleton`, `CGO_ENABLED=0` | **PASS (fails as intended)** — `build constraints exclude all Go files in …/go-sitter-forest/typescript`, the cgo-disabled signature. Confirms tree-sitter is the only C dependency | -| `GOOS`/`GOARCH` × `linux,darwin` × `amd64,arm64` | **PASS, all four** | -| artifact | **30.5 MB stripped** (`-ldflags="-s -w"`), 38.3 MB unstripped, `ldd` → *"not a dynamic executable"* | - -So we get a **fully static single binary**: a plain `GOOS`/`GOARCH` matrix in one CI job, -no C cross-toolchains, no zig, no libc coupling, ~20 lines of GoReleaser. 30 MB is on the -large side — the o200k_base tokenizer tables, the embedded dashboard UI and -`modernc.org/sqlite` account for most of it — but it is a fine download for `brew` or -`curl`, and it needs no runtime dependencies at all. - -**Consequence for the docs:** `docs/get-started/quickstart-proxy.md` currently instructs -every evaluator to set `CGO_ENABLED=1` and install a C toolchain. That is only true for a -`cg_skeleton` build. Fixing that paragraph is a one-line change that removes the largest -onboarding gate we have, independent of everything else in this proposal. +**The one thing here that is worth acting on immediately:** +`docs/get-started/quickstart-proxy.md` instructs every evaluator to set `CGO_ENABLED=1` and +install a C toolchain. That is only true for a `cg_skeleton` build. Fixing that paragraph is +a one-line change, it removes the largest onboarding gate we have, and it depends on nothing +else in this proposal. `skeleton` is the only casualty, and it costs nothing here: it is not in `codesmart` (the default) and not in the cache story at all. Ship pure-Go binaries for everyone; a @@ -193,26 +169,10 @@ that blast radius is the main risk in this proposal — bigger than the build. H to project-local, `--global` as an explicit opt-in, and `--idle-exit` paired with the `SessionStart` hook so "is it running?" stops being the user's problem. -**Gate B — measured, 2026-08-30: `env` blocks MERGE per key.** Reproduce with -`scripts/gate-b-envmerge.sh` (relocates the config dir via `CLAUDE_CONFIG_DIR`, so it never -touches a real `~/.claude`; a `SessionStart` hook dumps the environment it was handed). - -With `CG_USER`+`CG_BOTH` in user scope and `CG_BOTH`+`CG_PROJ` in project scope, the hook saw: - -``` -CG_BOTH=project <- project won the key it sets -CG_PROJ=project -CG_USER=user <- survived; the user object was NOT replaced -``` - -So a higher-precedence file wins only the keys it actually sets, and **a user-scope install -survives a repo that ships its own `env` block.** That removes the failure mode this gate -existed to catch. - -It does *not* change the default-scope recommendation. Per-key merge was one argument for -project-local; the blast-radius argument above stands on its own and is the stronger one. It -does mean `--global` is safe to offer without a per-repo caveat, and that `init` only has to -reason about one conflict: an `ANTHROPIC_BASE_URL` the user already set themselves. +`env` blocks merge **per key** across scopes, so a user-scope install is not clobbered by a +repo that ships its own `env` block, and `--global` needs no per-repo caveat. Verified; +see [Verification](#verification). That leaves `init` one conflict to reason about rather +than two: an `ANTHROPIC_BASE_URL` the user already set themselves. **Why `SessionStart` and not a launchd/systemd service.** Session-scoped lifetime leaves nothing running on the machine and needs no privileged install. Mechanics check out: @@ -288,12 +248,10 @@ do not match a subscriber's bill. ## Open questions for reviewers -Both gates are answered (see [Findings up front](#findings-up-front)), so everything below is -a judgment call rather than an unknown that could invalidate the plan. +All judgment calls — nothing below is an unknown that could invalidate the plan. 1. **Default scope: project-local or global?** Local is safer (a dead proxy costs one repo), - global is the better demo. The proposal picks local; happy to be overruled. Gate B means - global carries no per-repo caveat. + global is the better demo. The proposal picks local; happy to be overruled. 2. **Idle-exit default: 24h?** And should the floor be `max(2 × store.ttl_seconds, 1h)`? 3. **Homebrew tap: `rossoctl/homebrew-tap` as a new repo, and who owns release signing?** 4. **Does the 30 MB artifact bother anyone?** It is mostly tokenizer tables, the embedded @@ -305,12 +263,44 @@ a judgment call rather than an unknown that could invalidate the plan. ## Staging -- ~~**Stage 0** — Gate A + the `env` merge test.~~ **Done** (see Resolved above). - **Stage 1** — GoReleaser + tap + `cache` preset + fix the CGO claim in the quickstart. The funnel already works here for anyone willing to run two commands by hand. - **Stage 2** — `--idle-exit` + the plugin (skill, scripts, `SessionStart` hook). - **Stage 3** — conformance items 1–5 with tests. Stage 1 is shippable alone and is most of the adoption win; stages 2–3 make it pleasant and -safe. With Gate A answered, the smallest useful change in the whole proposal is a one-line -quickstart fix telling people they do not need a C compiler. +safe. The smallest useful change in the whole proposal is the one-line quickstart fix. + +--- + +## Appendix: Verification + +Two assumptions the plan rests on were checked rather than trusted. Both came back the way +the proposal assumed, so neither changes a decision above — they are recorded so a reviewer +does not have to take the claims on faith, and the scripts are committed so they can be +re-run. + +**The binary needs no C toolchain** — `scripts/gate-a-purego.sh`, run 2026-08-30 against +`go 1.26.4` in a `golang:1.26` container (so it needs no local Go install): + +| Check | Result | +|---|---| +| default tags, `CGO_ENABLED=0` | PASS | +| `-tags cg_skeleton`, `CGO_ENABLED=0` | fails, as intended — `build constraints exclude all Go files in …/go-sitter-forest/typescript`, the cgo-disabled signature. Confirms tree-sitter is the only C dependency | +| `GOOS`/`GOARCH` × {linux,darwin} × {amd64,arm64} | PASS, all four | +| artifact | 30.5 MB stripped (`-ldflags="-s -w"`), 38.3 MB unstripped, `ldd` → *"not a dynamic executable"* | + +The first run of the `cg_skeleton` check failed on `proxy.golang.org` timeouts rather than on +cgo, which would have been recorded as a confirmation it was not. The script now fetches the +grammar modules first and reports `INCONCLUSIVE` if it cannot, so a network error cannot be +mistaken for the error being tested for. + +**`env` blocks merge per key** — `scripts/gate-b-envmerge.sh`. It relocates the config dir +via `CLAUDE_CONFIG_DIR` so it never touches a real `~/.claude`, and reads the result from a +`SessionStart` hook that dumps the environment it was handed rather than asking a model to +report it. With `CG_USER`+`CG_BOTH` in user scope and `CG_BOTH`+`CG_PROJ` in project scope, +the hook saw `CG_BOTH=project`, `CG_PROJ=project`, `CG_USER=user` — the higher-precedence +file won only the key it sets, and the user-scope object survived. + +Still unverified, and called out where it matters above: the five gateway conformance items +in piece 5. From 1e1e57064db4cf3f057b804d2969762d03b76e6d Mon Sep 17 00:00:00 2001 From: DAVID AMID Date: Sun, 30 Aug 2026 21:12:25 +0300 Subject: [PATCH 5/5] docs(spec): add the user journey, and make the SessionStart hook self-gating MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two gaps, one of them a correctness bug in the design. The proposal described what we build but never what the evaluator actually does, which is the part a reviewer will judge. A "What the user does" section now answers the question it kept raising: nothing is copied into their project and the skill is not something they place. /plugin install writes enabledPlugins to ~/.claude/settings.json, so the plugin and its skills are installed once per machine and reachable from any repo; only the routing decision is per-repo, deliberately, because that is the one with blast radius. It lists the three commands, what the install skill does at each step, the realistic count of approval prompts (three to four, of which two happen once ever), and a table of what is global versus per-project. The correctness bug: the spec described the SessionStart hook as unconditional, but the plugin is installed at user scope, so its hooks run in EVERY project — including ones the user never routed, where starting a proxy is pure waste. The hook now self-gates on ANTHROPIC_BASE_URL. Settings `env` values are written into the process environment and hook processes inherit it, so the hook exits immediately unless the variable names our port. That needs no extra configuration, keeps no second copy of the port to drift, and degrades correctly: remove the env key by hand and the hook stops firing on its own. Also moves the new section above "What we ship" so the ### pieces stay nested under it rather than under the journey, and removes a duplicated paragraph in piece 1 that stated the quickstart problem twice. Docs only — no code, no behavior change. Signed-off-by: DAVID AMID Assisted-By: Claude Opus 5 (1M context) --- .../2026-08-30-local-distribution-design.md | 59 +++++++++++++++++-- 1 file changed, 53 insertions(+), 6 deletions(-) diff --git a/docs/superpowers/specs/2026-08-30-local-distribution-design.md b/docs/superpowers/specs/2026-08-30-local-distribution-design.md index 877271b2..1487404d 100644 --- a/docs/superpowers/specs/2026-08-30-local-distribution-design.md +++ b/docs/superpowers/specs/2026-08-30-local-distribution-design.md @@ -23,6 +23,46 @@ Target: **two commands, no toolchain, no guide, reversible.** /context-guru:install ``` +## What the user does + +Nothing is copied into their project, and the skill is not something they have to place. +`/plugin install` writes to `enabledPlugins` in `~/.claude/settings.json` (user scope is the +default and means "available across all projects"), so the plugin and its skills are +installed **once per machine** and are then reachable from any repo. + +``` +1. /plugin marketplace add rossoctl/context-guru # once per machine; prompts to trust the source +2. /plugin install context-guru@context-guru # once per machine; may ask for /reload-plugins +3. /context-guru:install # once per repo (or once, with --global) +``` + +Step 3 is the whole reason a skill exists rather than a shell script. It: + +1. detects the platform and installs the binary — `brew` if present, else the release + tarball (one Bash approval); +2. asks which scope to route (defaults to this project only); +3. reads the target settings file, backs it up, and adds **one** key — + `env.ANTHROPIC_BASE_URL` (one file-edit approval); +4. notices if the user already has an `ANTHROPIC_BASE_URL` and asks what to do rather than + overwriting it; +5. starts the proxy, verifies `/healthz`, and prints the dashboard URL. + +Realistically **three to four approval prompts**, of which steps 1–2 happen once, ever. +Afterwards `/context-guru:status` reads `/stats` and explains the numbers, and +`/context-guru:uninstall` removes the key and stops the proxy. + +What is global and what is not: + +| Thing | Scope | Frequency | +|---|---|---| +| Plugin + its skills | global — user settings | once per machine | +| Binary | global — `brew` / `~/.local/bin` | once per machine | +| **Routing (the `env` block)** | **per-project by default**, `--global` opt-in | **once per repo** | +| Proxy process | per-machine, started on demand | automatic | + +Only the *routing decision* is per-repo, and deliberately so — it is the one with blast +radius. Everything else is install-once. + ## What we ship Five pieces. Each is independently useful; the order is the order they unblock each other. @@ -53,10 +93,9 @@ in one CI job: no C cross-toolchains, no zig, no libc coupling, ~20 lines of GoR 30 MB is on the large side (tokenizer tables, the embedded dashboard UI and `modernc.org/sqlite`) but it is a fine `brew` or `curl` download with no runtime deps. -**The one thing here that is worth acting on immediately:** -`docs/get-started/quickstart-proxy.md` instructs every evaluator to set `CGO_ENABLED=1` and -install a C toolchain. That is only true for a `cg_skeleton` build. Fixing that paragraph is -a one-line change, it removes the largest onboarding gate we have, and it depends on nothing +**The one thing here worth acting on immediately** is that quickstart paragraph. Its +`CGO_ENABLED=1` instruction is true only for a `cg_skeleton` build, so fixing it is a +one-line change that removes the largest onboarding gate we have — and it depends on nothing else in this proposal. `skeleton` is the only casualty, and it costs nothing here: it is not in `codesmart` (the @@ -178,8 +217,16 @@ than two: an `ANTHROPIC_BASE_URL` the user already set themselves. nothing running on the machine and needs no privileged install. Mechanics check out: `SessionStart` supports `type: "command"`, hooks are awaited unless `async: true` is set, and the default timeout is 600s — so a hook that starts the proxy and polls `/healthz` before -returning closes the race with the first API request. Four details: - +returning closes the race with the first API request. Five details: + +- **The hook must self-gate on `ANTHROPIC_BASE_URL`.** The plugin is installed at user scope, + so its hooks run in **every** project — including the ones the user never routed. Starting a + proxy there is pure waste. The fix needs no extra configuration: settings `env` values are + written into the process environment and hook processes inherit it, so the hook reads + `$ANTHROPIC_BASE_URL` and exits immediately unless it names our port. That makes the hook + run in exactly the projects where routing is configured, with no per-project enablement and + no second copy of the port to keep in sync. It also degrades correctly: if the user removes + the env key by hand, the hook stops firing on its own. - **Never set `async: true`** on this hook; it reintroduces the race. - **`SessionStart` also fires on `clear`, `compact`, `resume` and `fork`**, not just `startup`. The starter must be idempotent — probe `/healthz`, start only if absent.