mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 22:14:25 +01:00
* fix(capy): the launch environment is sourced from a 0600 file and no skill fence may print the environment (GSTA-104) During GSTA-96's unattended /ship on #3425, the Step 14.5 docs-audit child re-typed the GSTACK_LAUNCH assignments as `export $(...)`; the substitution came back empty, the bare `export` printed the whole environment, and three API key values landed in its tool output. - bin/gstack-capy-install writes the assignments to <verdict>.env (mode 0600, idempotent PATH prepend) and prints GSTACK_LAUNCH_FILE: <path>. - ship Step 14.5 prompt, document-release's spawned note, autoplan/DRIVER.md and gstack-skill-start's capy-asks block say: source the file; never export $(...), env, printenv or bare set in a tool fence. - test/launch-env-echo-ratchet.test.ts greps every generated SKILL.md, sections, DRIVER.md, the Capy skill and bin/ shell helpers, with negative controls under test/fixtures/launch-env-echo/. - lib/redact-engine.ts: env.value (HIGH) finds the value of any *_KEY, *_TOKEN, *_SECRET, *_PASSWORD variable in the writer's environment, so a leaked value cannot reach a PR body, receipt, report or Drive file. - scripts/lib/shard-engine.ts scrubs the Capy launch markers from shard environments (GSTA-99's finding). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(ci): DRIVER.md back to 159 lines; shard-engine recorded non-behavioral at HARNESS_VERSION 6; verdict-count test lists .json only Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: register test/fixtures/launch-env-echo as a free fixture Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(redact): env-value fixtures stay out of pregate's env.kv net A credential-named assignment with an entropic literal on one line is what the repo's own pregate secrets scan refuses in added lines, so the fixture values move into plainly named constants and the scanned text is built from them. The scan under test still sees the same values through processEnv. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(windows): the late-completion remote-caller case gives its second attempt a 2s budget (run 40263984171) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(redact): env.value trims the value, keeps a prefix value's longer match, hides the preview and skips PUBLIC names /ship Step 9.4 review fixes for the GSTA-104 env-value scan: - a value whose environment copy ends in a newline (a key read from a file) is searched for trimmed, so its leaked form still matches (the length check already trimmed; the search used the raw value); - when one credential value is a prefix of another, the dedup key carries the span length, so the longer value is redacted whole in either order instead of leaving its tail in the text and dropping its finding; - the env.value preview is a fixed mask instead of the first characters of a live credential; - framework-public names (NEXT_PUBLIC_*_KEY, *_PUBLIC_KEY) are not scanned, since those values are committed on purpose; - the header and the processEnv docstring state the name rule (KEY, TOKEN, SECRET, PASSWORD, PASSWD, any case) and that process.env is the engine's one ambient input. Step 7 coverage extensions and a regression test for each fix in test/redact-env-value.test.ts; docs/capy-install.md names the full rule. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * refactor(capy-install): write_launch_file quotes through shq; header names the launch file Review finding: _sq duplicated shq() from the same file. The launch file's assignments now go through shq, and the header comment describes the GSTACK_LAUNCH_PATH and GSTACK_LAUNCH_FILE lines beside GSTACK_LAUNCH. Step 7 coverage: the verdict test sources the launch file and runs --check; the single-quote path test sources the file too; a new case makes the launch-file directory unwritable and checks the run says so, prints no GSTACK_LAUNCH_FILE line, leaves no temp file and keeps its exit code and trailer (linux, non-root). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(preload): strip the Capy launch markers so a direct bun test matches the shard sandbox Red Team finding: the launch-marker scrub lived only in createShardSandbox, so a direct `bun test <file>` run under an unattended Capy session inherited GSTACK_SESSION_KIND, GSTACK_CAPY, GSTACK_EPHEMERAL and GSTACK_INSTALL_VERDICT (GSTA-99's symptom). The preload now deletes the same four names, and test/shard-engine.test.ts imports CAPY_LAUNCH_MARKERS instead of re-typing them. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(ratchet): the launch-env echo scan catches backtick and $VAR exports, typeset -p, env flags and a set in any segment Review findings 11 and 12: export-subst also trips on `export \`...\`` and an unquoted `export $VAR` (both splat a NAME=value file; `export "$name"` is one word and stays allowed); the export rule adds `typeset -p`; env trips with only flags (`env -0`) and as `/usr/bin/env`; set is judged per segment and with redirections, allowed only when piped straight into an anchored grep filter. Quoted strings are blanked offset-preserving so a `#` inside quotes no longer hides the rest of the line. The negative control covers each new form on both sides. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(ship): Step 14.5 names where the launch-file placeholder comes from The /document-release prompt line read `. <file>` with no word on what fills it. It now says the placeholder is the GSTACK_LAUNCH_FILE: path the parent's own install check printed. Worded to stay under the ship skill's pinned parity size (313,279 of 313,281 union bytes). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(redact): env.value admits *_PASS, *_PAT, *_APIKEY and *_PWD, and skips path values and gstack's lock token /ship Step 11 adversarial findings. The name rule also covers DB_PASS, GITHUB_PAT, *_APIKEY and *_PWD (the shell's own PWD and OLDPWD stay out). A credential-named variable whose value is a filesystem path (SSL_KEY=/etc/ssl/ private/x.key) names where the credential lives, so the path is no finding, and GSTACK_BRAIN_LOCK_TOKEN (pid-random-epoch, written to the lock owner file) is not a secret. Without these, an ordinary mention of that path or token in a PR body, receipt or CSO report was a HIGH fail-closed blocker. The adversarial pass also recommended an entropy floor so dev defaults such as DB_PASSWORD=postgres stop matching prose; declined here on purpose, because the scan's promise is to catch a leaked value whatever its shape, and a weak real password is still a credential. Recorded as an open decision in the ship report. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(capy-install): a failed launch-file write removes only its own temp file The failure path ran rm -f "$LAUNCH_FILE".tmp.*, which also deleted the temp file a concurrent installer run (two threads on one machine, same verdict path) was about to mv, so that run lost its launch file too. write_launch_file now cleans up its own $tmp and returns 1; the caller only reports. The file's comment no longer embeds the state-root path (a newline in GSTACK_STATE_ROOT could have ended the comment). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: the launch markers come from one list, and the echo ratchet scans setup and scripts/*.sh The preload imports CAPY_LAUNCH_MARKERS instead of re-typing the four names, and the ratchet asserts the installer exports every name on that list, so a fifth marker cannot be added to one place only. The shell scan also covers ./setup and the scripts/*.sh install scripts callers run first. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(changelog): the Unreleased entry states the review-fixed env.value rules, the ratchet forms and the one test-budget change Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(redact): the env.value path exemption asks whether the file exists instead of matching a shape Second review cycle: a path-shaped rule also matched a base64 secret that starts with "/" and holds another "/" (about 1 in 150 AWS-shaped keys), which then went unscanned. The exemption now fires only when the value names a file that exists on the scanning machine (SSL_KEY=/etc/ssl/private/x.key). The module header names that stat as the engine's one filesystem read. NON_SECRET_ENV_NAMES is exported so the test can prove bin/gstack-brain-lock.sh still exports every name in it; the PWD/OLDPWD fixtures hold plain words so only the name rule keeps them out. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: the installer's own-temp-file cleanup is exercised, and the ratchet coverage test names setup and scripts An mv shim first on PATH refuses only the launch-file target, so the temp file exists when the move fails: the run removes that file alone (a sibling from a concurrent run survives), says so, prints no GSTACK_LAUNCH_FILE line and keeps the earlier file, exit code and trailer. The ratchet coverage test asserts setup, scripts/build.sh and scripts/sandbox-doctor.sh are scanned. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(redact): env.value has no path exemption and matches the value in normalized form Second adversarial pass: the existence check let the scanned agent exempt any "/"- or "~/"-prefixed secret by creating a path of that name, a silent bypass of the one guard this branch adds, so the exemption is gone; a path-valued SSL_KEY that must appear in a text goes through the allowlist. The engine is back to no I/O. The value is now normalized the way the text is before indexOf, so a credential NFKC rewrites (fullwidth digits) still finds its leaked ASCII form. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(capy-install): the launch file is written only on a passing run; the ratchet checks the marker list both ways A gstack: fail run no longer writes or announces a sourceable file whose PATH prepend points at an unverified Bun. The ratchet also asserts every variable the launch file exports is a CAPY_LAUNCH_MARKERS entry or GSTACK_STATE_ROOT, so a sixth export cannot reach fixture children unstripped. The Capy skill's prose no longer offers re-typing the assignments as an alternative to sourcing. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(capy-install): the launch file is part of the ok verdict Third adversarial pass: a passing run whose launch file could not be written still said gstack: ok with exit 0 and printed only the GSTACK_LAUNCH line, leaving a shell-evaluating caller with the re-typing path the branch exists to remove. The file is now written before the verdict is recorded; a write failure on a passing run turns the run into "gstack: fail launch-file — could not write the launch file <path>; fix: … (LAUNCH_FILE_UNWRITABLE)" with exit 1 and an ok:false verdict, and every failing run removes the launch file an earlier passing run left beside the verdict. The two failure tests assert the new contract, the doctor-FAIL tests assert no launch file is announced or left, and the docs, Capy skill and CHANGELOG say the same. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(redact): the env.value allowlist and placeholder rules see the normalized value too An allowlisted value in NFC did not suppress a match on its NFKC-composed form, and a value that is a placeholder only after normalization was still searched for. Fixture nits from the third review cycle: fixed directory string instead of process.cwd(), the fullwidth assertion guards its own vacuity, a zero-width-padded value under the floor is pinned, and the ratchet comment names the one kept export and the PATH case line. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(capy-install): LAUNCH_FILE_UNWRITABLE is a registered result code; a kept profile verdict keeps its launch file Fourth review cycle. The new trailer code now has its row in lib/result-codes-capy.ts (the owner module is at its size cap) and its docs/troubleshooting.md anchor, so the install-side code scan resolves it. A failing profile check (--for review-voice) that keeps a fuller ok verdict for this HEAD no longer deletes that verdict's launch file: the fail branch removes the file only when the run rewrites the verdict, and setup_failed removes it too. The docs say "a failing run that rewrites the verdict", and that an unwritable directory keeps both the old file and the old verdict. A new test runs a passing install, then a failing profile check, and asserts the file and the ok verdict both survive. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(redact): the normalized allowlist and placeholder rules each have an assertion that fails without them Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(capy-install): a setup failure after a passing install removes the earlier launch file Fifth review cycle: setup_failed's cleanup had no assertion. The setup failure test now installs once (launch file present), commits a new HEAD so setup runs again, fails that setup, and asserts the launch file is gone and the verdict says ok:false before the next run sets up again and ends ok. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: state-root, contributor reference and capy-install follow the launch-file contract (Step 14.5 audit) The /document-release audit (docs-3449-1792992068) corrected three authored docs: docs/state-root.md shows the GSTACK_LAUNCH_FILE line and says to apply it with ". <that file>" instead of prefixing or exporting the assignments; docs/CONTRIBUTOR_REFERENCE.md names scan()'s one ambient input instead of calling it pure; docs/capy-install.md's bun-shim bullet and exit table name the launch file and the fail launch-file trailer. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
235 lines
11 KiB
Markdown
235 lines
11 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.
|
|
|
|
A bucket can also hold a user-owned `pregate.json` policy, for a repo that keeps
|
|
`.gstack/` out of git (see [pregate v1](pregate.md#pregate-v1)).
|
|
|
|
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.
|
|
|
|
## Ephemeral machines
|
|
|
|
A Capy cloud machine, a CI runner or a throwaway VM loses its disk when the
|
|
run ends, and with it every learning, decision, review-log row and timing
|
|
record gstack wrote under the default root. gstack does not guess this from
|
|
the path (a tmpfs and a home directory look alike); the host says so with one
|
|
environment marker:
|
|
|
|
```
|
|
GSTACK_EPHEMERAL=1
|
|
```
|
|
|
|
What the marker changes:
|
|
|
|
- `gstack-doctor` and `gstack-doctor --check` report the `state root` row as
|
|
`durable=no` (the dashboard warns, the check still passes: an ephemeral root
|
|
is a fact, not a broken install).
|
|
- Unattended sessions print `learnings: skipped (state root is ephemeral; set
|
|
GSTACK_STATE_ROOT)` instead of writing silently to a disk that will vanish.
|
|
|
|
A durable root is opt-in: the owner sets `GSTACK_STATE_ROOT` (or `GSTACK_HOME`)
|
|
to a directory that outlives the machine. The installer never picks one for
|
|
you. On Capy the drive is governed by Capy's own drive policy, which excludes
|
|
memory and verification evidence, so `bin/gstack-capy-install` only prints the
|
|
option and never writes there.
|
|
|
|
A child shell's `export` cannot configure the parent's later commands
|
|
(`lib/state-root.ts` reads each process's own environment), so the installer
|
|
ends with one machine-readable line the parent supplies to every gstack
|
|
command it launches, and the same assignments as a sourceable file:
|
|
|
|
```
|
|
GSTACK_LAUNCH: GSTACK_SESSION_KIND=unattended GSTACK_STATE_ROOT=<path> GSTACK_EPHEMERAL=1
|
|
GSTACK_LAUNCH_FILE: <verdict file without .json>.env
|
|
```
|
|
|
|
Apply them with `. <that file>` at the start of the shell that runs each
|
|
command; never re-type them as `export $(...)`, and never run `env`, `printenv`,
|
|
a bare `set` or a bare `export` in a tool fence (a bare `export` prints every
|
|
variable, credentials included). See
|
|
[docs/capy-install.md](capy-install.md#the-launch-line).
|
|
|
|
## 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
|
|
GSTACK_STATE_ROOT=$(~/.claude/skills/gstack/bin/gstack-paths --get GSTACK_STATE_ROOT); : "${GSTACK_STATE_ROOT:?gstack-paths failed; reinstall with ./setup or /gstack-upgrade}"
|
|
```
|
|
|
|
(No `eval`: worktree-isolated Claude Code sessions refuse it.)
|
|
|
|
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.
|