Files
gstack/docs/state-root.md
T
f30b7b788a v1.91.13.0 fix: severe bug wave (autoplan guard, land-and-deploy CI gate, project identity, gbrain, review gates) (#3013)
* fix(land-and-deploy): never merge over red, pending or missing CI (#2995)

/land-and-deploy gated only on required checks, so a repo with none
(every GitHub Free private repo, any repo without branch protection)
skipped the CI wait and could merge over red CI. The verdict now comes
from bin/gstack-ci-gate, a tested helper bound to the head SHA: all
checks by default, cancel/unknown buckets fail, NO_CHECKS needs explicit
per-head approval, and both merge commands (including --auto) run only
after a PASS for the same SHA they pin with --match-head-commit.

Co-authored-by: harshitgavita-07 <harshit.gavita@gmail.com>

* fix(slug): give nested-group projects their own state bucket and verify checkpoint identity (#3003, #2901)

gstack-slug kept only the last two remote path segments, so
customer-a/product/repo and customer-b/product/repo shared one bucket
and /context-restore could present another project's checkpoint.

- One canonical-remote rule (bash + TS, golden vectors) used by all six
  resolvers: lowercased host, no userinfo/port/.git, Azure DevOps and
  Bitbucket Server normalized. 3+ segment remotes get
  <last-two>-<16 hex>; two-segment remotes (all of GitHub) are unchanged.
- Versioned sticky cache (v2:) keeps cache hits git-free.
- context-save stamps the credential-free remote and root;
  context-restore only treats this project's checkpoints as latest,
  reports foreign/unverified ones first, and treats a different root
  with the same remote (Conductor handoff) as information.
- gstack-slug --adopt-legacy copies only files the user confirms.
- question-preference-hook reads the same versioned cache, so never-ask
  preferences fire; unknown identity asks instead of auto-deciding.

* fix(gbrain): brain context loads, managed sync works, memory sync stops duplicating (#2883, #2985, #2922)

- brain-context-load called 'gbrain list_pages' (an MCP tool name), so
  every skill's brain context was 0 bytes. It now uses 'gbrain list'
  with gbrain's real filter flags, and prints one line when queries fail.
- The code-stage sync always passes --no-pull (managed gbrain >= 0.51
  refuses without it); a gbrain that rejects the flag fails with an
  upgrade message and is never retried without it. stderr is streamed
  with a bounded tail and quoted in failure summaries.
- /sync-gbrain gains --sources (and GSTACK_MEMORY_INGEST_SOURCES); by
  default it skips types already synced through a federated curated
  source, so curated pages stop being re-duplicated.

Co-authored-by: Vitor Cepeda Lopes <vitor@vitorcepedalopes.com>
Co-authored-by: harshitgavita-07 <harshit.gavita@gmail.com>

* fix(codex,cso): clean Codex reviews stop failing; semgrep and trivy SARIF results import (#2769, #3011)

- Codex review gate is PASS / FAIL / UNVERIFIED. Untagged output (what
  a clean 'codex review --base' prints) is UNVERIFIED, not FAIL, and
  never a false PASS; P2/P3-only output passes.
- import-sarif resolves semgrep's undeclared %SRCROOT% and trivy's
  ROOTPATH (including git-URL targets) through explicit, captured
  mappings; declared bases must sit inside the source root or the
  run's checkout, URIs are percent-decoded, and rejected results are
  counted with rule, base and path instead of silently dropped.

* fix: version strings are not IPs, failed update checks are not 'up to date', private security reporting (#2856, #2786, #2984)

- The pre-push redact hook exempts a root VERSION file that is only a
  version string and the version token in CHANGELOG release headings;
  the engine allowlist is untouched.
- gstack-update-check caches an unreadable remote as CHECK_FAILED with
  a short TTL instead of UP_TO_DATE; /gstack-upgrade never reports
  'latest' after a failed check.
- SECURITY.md points reporters at GitHub private vulnerability
  reporting.
- pr-title-sync (pull_request_target, write token) pins actions/checkout
  to a reviewed SHA.

* fix(autoplan): publication guard accepts real Claude Code journals (#3007, #3009, #2968, #2977, #2986, #3006)

The guard required a session journal to start with a user message, but
Claude Code starts it with SessionStart hook attachments (and a forked
compacted session starts with a compact_boundary), so every phase was
denied for anyone with a SessionStart hook. Windows path spellings
(C:/, /c/, drive case) were rejected too.

- Reader accepts the captured startup, compact, clear and fork shapes
  (claude 2.1.284 fixture), computes root uniqueness over every
  candidate, and returns typed reason codes instead of one string.
- Hard denials (competing root, foreign cwd, sidechain, agent, cycle,
  malformed, identity) say what happened and how to run the reviews by
  hand; unflushed journals still retry.
- A well-formed, stable journal whose root shape is unrecognized now
  warns ('phase publication was NOT verified') and continues, without
  overriding Claude Code's own permission check; no bypass switch.
- Windows paths fold before validation, so '..' is still rejected.
- Periodic drift canary runs the reader on fresh journals from the
  pinned claude.

Co-authored-by: Charles M Berg <77517285+CharlesBerg85@users.noreply.github.com>
Co-authored-by: laurenbdaniels-coder <264343335+laurenbdaniels-coder@users.noreply.github.com>
Co-authored-by: Jordan Mayer <176445995+maydaycyber@users.noreply.github.com>
Co-authored-by: elmesery-dev <elmesery.dev@gmail.com>

* v1.91.13.0: severe bug fix wave release notes, stale gbrain verbs in docs

* test: seed the versioned slug cache in the memory-injection deny case (#2901)

The preference hook now resolves the project bucket from gstack-slug's
v2 slug-cache entry instead of basename(cwd); this fixture still wrote
preferences under the basename bucket without the entry the skill
preamble always writes first.

* fix: full-suite follow-ups for the fix wave

- gstack-slug's --help heredoc is over 512B, so it gets the BASH_COMPAT=50
  pipe-deadlock guard like the other bins.
- Refresh the codex/factory ship goldens for the plan-gates slug change.
- The periodic lane gains the autoplan journal drift canary (107 paid
  files); raise the eval-slices cap to 25 so every slice still starts
  at once until its recorded duration lets the planner pack it.

* fix(plan-eng-review): clearer Step 0 sequencing, write routes and section outputs

The workflow judge scored plan-eng-review's instructions at
actionability 3.67 (bar 4) on text unchanged from main. Resolve the
ambiguities it named together, without changing any gate or output:
Step 0 names the Read's order and keeps one STOP; the pending-call,
read-only, forbidden-write and failed-save rules become one Write
routes block; header/label limits and grid checks are concrete;
Performance gets a checklist; each section has a stated output shape;
Lake Score is defined. Same total length (parity cap).

---------

Co-authored-by: harshitgavita-07 <harshit.gavita@gmail.com>
Co-authored-by: Vitor Cepeda Lopes <vitor@vitorcepedalopes.com>
Co-authored-by: Charles M Berg <77517285+CharlesBerg85@users.noreply.github.com>
Co-authored-by: laurenbdaniels-coder <264343335+laurenbdaniels-coder@users.noreply.github.com>
Co-authored-by: Jordan Mayer <176445995+maydaycyber@users.noreply.github.com>
Co-authored-by: elmesery-dev <elmesery.dev@gmail.com>
2026-10-02 11:11:00 -07:00

188 lines
8.6 KiB
Markdown

# Where gstack keeps its state
gstack keeps everything it remembers — config, analytics, sessions, projects,
learnings, review logs, the egress ledger, the trust-policy store, hook error
logs — under one directory, the **state root**. By default that is
`~/.gstack`. Set `GSTACK_HOME` to put it somewhere else.
One rule picks the root, and every gstack script, hook and skill uses it. The
rule lives in two owner files that a parity test keeps identical:
`bin/gstack-state-root.sh` (bash) and `lib/state-root.ts` (TypeScript).
## How the root is chosen
The first variable in this list with a non-empty value wins. Empty values count
as unset.
| Order | Source | When to use it |
|------:|--------|----------------|
| 1 | `GSTACK_STATE_ROOT` | `gstack-paths`' own output. It is also honored as input, so re-reading it in the same environment returns the same root. Do not set it by hand. |
| 2 | `GSTACK_HOME` | **The one variable to set** when you want your state somewhere other than `~/.gstack`. |
| 3 | `GSTACK_STATE_DIR` | Legacy alias, honored for compatibility. |
| 4 | `CLAUDE_PLUGIN_DATA` | Only when `CLAUDE_PLUGIN_ROOT` contains `gstack` (any case), so another plugin's data directory never captures gstack state. |
| 5 | `$HOME/.gstack` | The default. On Windows shells, `USERPROFILE` stands in when `HOME` is unset. |
| 6 | `.gstack` | Last resort when there is no home directory at all (some containers). |
## See which root is in use
```bash
~/.claude/skills/gstack/bin/gstack-paths --explain
```
Real output with `GSTACK_HOME` set while `~/.gstack` still holds older state:
```
state root: /home/you/work-state (selected by GSTACK_HOME)
chain (first non-empty wins):
GSTACK_STATE_ROOT unset
GSTACK_HOME /home/you/work-state selected
GSTACK_STATE_DIR unset
CLAUDE_PLUGIN_DATA unset
default /home/you/.gstack ignored
default root /home/you/.gstack also holds gstack state: yes
merged privacy keys (most restrictive value across roots wins):
telemetry: off (from /home/you/.gstack/config.yaml)
memorable_recall: not set (default applies)
codex_reviews: not set (default applies)
update_check: not set (default applies)
docs: https://github.com/garrytan/gstack/blob/main/docs/state-root.md
```
`--explain` only reads; it writes nothing and exits 0. `gstack-config list`
also prints a one-line note when `GSTACK_STATE_ROOT`, `GSTACK_HOME` and
`GSTACK_STATE_DIR` name different directories.
## Move your state
gstack never moves state for you. To relocate it:
```bash
cp -a ~/.gstack /new/place/gstack-state
export GSTACK_HOME=/new/place/gstack-state # add this to your shell profile
~/.claude/skills/gstack/bin/gstack-paths --explain # confirm: selected by GSTACK_HOME
```
Keep or delete the old `~/.gstack` once you have checked the new root works.
Until you delete it, its privacy settings still count (next section).
## Privacy settings never get looser when the root moves
Four config keys are opt-outs: `telemetry` (off < anonymous < community),
`memorable_recall` (off < on), `codex_reviews` (disabled < enabled) and
`update_check` (false < true). gstack reads each of them from the resolved root
**and** from `~/.gstack`, and uses the most restrictive value. So turning
telemetry off once in `~/.gstack` keeps it off after you set `GSTACK_HOME`.
Every other key, including `proactive` and `founder_resources`, reads the
resolved root only.
When `gstack-config set` writes a merged key but the other root still holds a
more restrictive value, it tells you which root overrides it and prints the
command that changes that root, for example:
```bash
GSTACK_STATE_ROOT='/home/you/.gstack' ~/.claude/skills/gstack/bin/gstack-config set codex_reviews enabled
```
`gstack-config list` shows the winning root for merged keys, for example
`telemetry: off (set, /home/you/.gstack)`.
Trust-policy deny tiers merge the same way: a `deny` or `read-only` entry in
`~/.gstack/gbrain-repo-policy.json` still applies when your resolved root is
elsewhere. Nothing is ever written to the other root.
## Project buckets
Per-project state (checkpoints, learnings, timeline, question preferences)
lives in `projects/<slug>/`. `bin/gstack-slug` picks the slug from the git
`origin` remote:
- A 2-segment remote (`github.com/owner/repo`) files under `owner-repo`.
- A remote with 3 or more path segments (GitLab nested groups such as
`gitlab.com/customer-a/product/repo`, Azure DevOps) files under
`product-repo-<16 hex>`. The hex is a digest of the canonical remote, so
`customer-a/product/repo` and `customer-b/product/repo` get separate buckets,
and the ssh, https and ssh-with-port spellings of one repository share one.
- Without a remote, the project root's directory name is used.
The first slug gstack resolves in a directory sticks (it is cached under
`slug-cache/`), so a project keeps its bucket when it adds a remote later.
**Upgrading from a version that used one bucket for nested groups.** Earlier
versions filed every `<group>/product/repo` under `product-repo`. That bucket
stays where it is; gstack neither moves nor deletes it. The first time gstack
resolves the new slug it prints one notice naming both buckets, and
`/context-restore` reports the old bucket while it still holds checkpoints. To
bring earlier files across, run this in the project:
```bash
~/.claude/skills/gstack/bin/gstack-slug --adopt-legacy
```
It lists every file in the old bucket. Checkpoints are marked as matching this
project, another project (never copied), or unknown (saved before checkpoints
recorded their project). Files with no project identity (question
preferences, learnings, timeline) may belong to any project that shared the
bucket, so they are listed for you to decide. On a terminal it asks per file;
otherwise copy chosen files with
`gstack-slug --adopt-legacy --copy <path> [<path>...]`. Nothing is copied
without your confirmation, and nothing in the new bucket is overwritten.
**Pinning a slug.** Set `GSTACK_PROJECT_SLUG=<name>` to choose the bucket
yourself, for example in the project's `.claude/settings.local.json`
(`"env": {"GSTACK_PROJECT_SLUG": "<name>"}`). Use it when an ssh host alias
(`git@work-gitlab:group/sub/repo`) gives a different host than the https
remote, or for a vendored sub-repo that should be its own project. The pin is
applied per run and never cached.
## Uninstall
`gstack-uninstall` deletes state only at `~/.gstack`.
- It first refuses (exit 2) when `~/.gstack` resolves — following symlinks —
to `/`, your home directory or one of its parents, the gstack checkout, the
current git repository, or a parent of either. The message names the reason
and a `fix:` line.
- When the resolved root is somewhere else (you set `GSTACK_HOME`, for
example), uninstall leaves it in place and prints
`left in place: <path> (selected by <VAR>=<value>)` plus the exact
`rm -rf -- '<path>'` command to run after you have checked it.
- `--keep-state` never deletes any state. `--force` only skips the prompt.
## If gstack-paths is missing
Skill blocks resolve the root with
```bash
eval "$(~/.claude/skills/gstack/bin/gstack-paths)"; : "${GSTACK_STATE_ROOT:?gstack-paths failed; reinstall with ./setup or /gstack-upgrade}"
```
The second half stops the block instead of writing under `/` if the resolver
is broken. If you see `gstack-paths failed` or
`cannot resolve the gstack state root`, reinstall: run `./setup` in the gstack
checkout, or `/gstack-upgrade`.
## Claude Code plugin installs
gstack does not ship an official Claude Code plugin distribution today (no
`.claude-plugin` manifest has ever been on `main`; some forks publish one), and
telemetry does not report plugin-mode installs. When gstack does run as a
plugin, `CLAUDE_PLUGIN_ROOT` names gstack and state lives in
`CLAUDE_PLUGIN_DATA`, while a terminal run outside the plugin session uses
`~/.gstack`. Those are two different roots; run `gstack-paths --explain` in
each environment to see which one is active. Cross-environment support (a
pointer from `~/.gstack` to the plugin root) is deferred until a plugin
distribution exists.
## For contributors
- Bash: executables source `bin/gstack-state-root.sh` beside them and call
`gstack_state_root_select` (sets `$_gstack_sr_root`), or `eval` gstack-paths
with the guard above. Hooks must source the twin; never spawn gstack-paths
from a hook.
- TypeScript: `resolveStateRoot()` and `readConfigKey()` from
`lib/state-root.ts`.
- `test/state-root-ratchet.test.ts` fails on a hand-rolled chain, an
executable `~/.gstack` in a template bash block, an unguarded gstack-paths
eval or `export GSTACK_STATE_ROOT` in prose. Its allowlist
(`test/state-root-ratchet.allowlist.json`) needs a reason per entry.