docs: Capy install layers, lazy install, snapshot bake, forward-only recovery, raising the minimum

docs/capy-install.md describes the three layers (Drive bootstrapper, full
wrapper, installer), the lazy install, the GSTACK_LAUNCH line, install
admission, baking gstack into a Capy snapshot, and that recovery after an
interrupted fast-forward completes or refuses the pending install stages
(checking out an older commit is not a rollback). docs/CHANGELOG_STYLE.md
(the release doc; there is no docs/RELEASING.md) says when
GSTACK_MIN_REVISION may be raised.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-10-13 01:24:25 +00:00
1 parent f63ed5f7bd
commit 6e10ca3c32
2 files changed
+155 -30

No files matched your search

+9
View File
@@ -125,6 +125,15 @@ sentence: "Version bump for branch-ahead discipline. No user-facing changes yet.
there. Do not pad. Do not explain the plan that will ship eventually. Do not narrate
the branch's history. When real work lands, the entry will replace this at /ship time.
### Raising the supported minimum (`capy/support.env`)
Release stamping can also raise `GSTACK_MIN_REVISION` in `capy/support.env`.
Every Capy machine below it is fast-forwarded at its next session start, so
raise it only for install-honesty or security fixes, and set it at stamp time
to the VERSION being released. It never exceeds VERSION
(test/capy-skill-wrapper.test.ts pins this). Details:
[capy-install.md](capy-install.md#raising-the-supported-minimum).
## Entry format
### Release-summary format (every `## [X.Y.Z]` entry)
+146 -30
View File
@@ -1,47 +1,110 @@
# Installing gstack on a Capy cloud machine
`bin/gstack-capy-install` installs gstack on a fresh Capy cloud machine
(Ubuntu) and ends with one line a parent agent greps. It is the repo-owned
installer the user-scope Capy skill runs after cloning; nothing in the skill
decides what "installed" means.
Three layers, each owned by gstack:
| Layer | Where | Job |
|---|---|---|
| Bootstrapper | `capy/skill/gstack/scripts/install.sh` (copied once into the owner's Drive skill folder; frozen) | clone when absent; fetch origin's default branch and read the supported minimum from `FETCH_HEAD:capy/support.env`; in install mode fast-forward a clean, unpinned checkout below it; exec the full wrapper |
| Full wrapper | `bin/gstack-capy-session` (moves with the checkout) | map the session start onto the installer: `--check` certifies, otherwise install or repair |
| Installer | `bin/gstack-capy-install` | Bun, both host profiles, the doctor, the install verdict, the launch line, the trailer |
The vendored skill (`capy/skill/gstack/SKILL.md`) tells a Capy agent how to use
them. The Drive folder is a mirror: the owner copies `capy/skill/gstack/` there
when asked; nothing writes the Drive automatically.
Direct use, without the skill:
```bash
git clone --single-branch --depth 1 https://github.com/garrytan/gstack.git ~/.local/share/gstack
~/.local/share/gstack/bin/gstack-capy-install --for autoplan
```
What it does, in order:
## Session start
1. Reuses the checkout it lives in (or clones into `--root <dir>`), under a
lock. It never resets or pulls an existing checkout. `--revision <ref>`
pins a fresh clone; an existing checkout at another revision is reused and
the trailer says `(requested <ref>; run gstack-upgrade to move)`.
2. Preflight: `git`, `curl`, `node`, `jq`, then Bun. The default Capy image
ships Bun 1.3.14, below gstack's supported minimum (1.4.2), so the installer
downloads the exact release archive, verifies its SHA-256 against
`bin/gstack-bun-version.sh` and installs it to `$BUN_INSTALL` (default
`~/.bun`). It does that only when gstack's minimum and the target project's
Bun pin (`--project <path>`, default the git toplevel of the current
directory) agree; a conflict prints both constraints and stops
(`RUNTIME_PIN_CONFLICT`, exit 3) before anything is installed.
1. `bash <skill>/scripts/install.sh --check --for <skill>` (under a second when
a checkout exists). It fetches refs only and never moves the checkout. With
no checkout it prints `gstack: fail not-installed — ...` and the session
records that line and continues when it runs no gstack workflow; install
right before the first workflow.
2. When the trailer is `gstack: fail revision ...` or `gstack: fail runtime ...`
and a checkout exists, run the same command without `--check`. Below the
supported minimum, a clean checkout on the remote default branch is
fast-forwarded (`gstack-capy-skill: upgraded <old> -> <new>`), reinstalled
and rechecked as one transaction under `.gstack-capy-install.lock` (the
bootstrapper holds it and hands it over with `GSTACK_CAPY_LOCK_HELD=1`).
A pinned (`--revision`, detached HEAD, another branch), dirty or diverged
checkout is never moved; the trailer names the cause and the exact commands
([REVISION_BELOW_MINIMUM](troubleshooting.md#revision-below-minimum)).
`GSTACK_CAPY_AUTO_UPGRADE=0` disables the fast-forward.
3. When the fetch fails (offline), the bootstrapper says `remote minimum
unknown (fetch failed)` and checks the local install only; it never claims
the minimum.
The upgrade target is always the tip of the default branch: every commit there
is a stamped release that passed CI. Recovery runs forward, never back: an
install interrupted after a fast-forward leaves the trailer `fail`, and the
next install or `bin/gstack-upgrade` run completes the pending install stages
(setup is never skipped after a fast-forward) or refuses and names the cause.
Checking out an older commit is not a supported rollback; it does not undo
setup migrations that already ran.
Between minimums, `bin/gstack-upgrade --check` says `current <v>` or `behind
<installed> -> <remote> (<n> commits)`, and `bin/gstack-upgrade` fast-forwards
and reinstalls (`bin/gstack-capy-install` on a Capy install, else `./setup
--refresh-registered`). It refuses a dirty, diverged or pinned checkout with
[UPGRADE_REFUSED](troubleshooting.md#upgrade-refused) and never stashes or
resets.
## What the installer does
1. Reuses the checkout it lives in (or clones into `--root <dir>`), under the
lock. It never resets or pulls; `--revision <ref>` pins a fresh clone, and an
existing checkout at another revision is reused with the trailer note
`(requested <ref>; run <checkout>/bin/gstack-upgrade to move)`.
2. Preflight: `git`, `curl`, `node`, `jq`, then Bun. A supported Bun already in
`$BUN_INSTALL/bin` (default `~/.bun/bin`) is reused. The project's pins
(`--project <path>`, default the git toplevel of the current directory) are
read with the doctor's own parser (`bin/gstack-runtime-pins.ts`: engines,
packageManager, `.tool-versions`, `.bun-version` and CI `bun-version:` lanes
for this platform). When neither the Bun on PATH nor the installed one
satisfies gstack's minimum and the pins, and every exact pin equals the
version gstack carries a verified archive for (1.4.2), the installer
downloads that exact release archive, verifies its SHA-256 against
`bin/gstack-bun-version.sh` and installs it into `$BUN_INSTALL/bin`, beside
any different system Bun (no symlink into a shared bin directory). Any other
exact pin, or a range gstack's minimum does not satisfy, stops with both
constraints ([RUNTIME_PIN_CONFLICT](troubleshooting.md#capy-install-runtime-conflict),
exit 3) before `./setup`. With no Bun at or above the security floor, the
verified one is installed first so the pins can be read.
3. `./setup --host claude --no-prefix`, then `./setup --host codex --model
gpt-6-astra`, both with `--no-browser`: Chromium is lazy by default.
gpt-6-astra`, both with `--no-browser`. Skipped when this exact HEAD was set
up with the same hosts, model and browser flag (recorded in the git dir
after a successful setup) and `gstack-doctor --check` passes; a warm rerun
takes about a second.
4. `gstack-doctor --check` for the requested workflow (`--for <skill>`,
`--only <skill>`, or `--require <id,id>`; default `claude,codex,patch`).
`--only <skill>`, or `--require <id,id>`; default `claude,codex,patch`),
run with the launch PATH, so the trailer certifies the Bun workflows run.
5. The install verdict, written atomically to
`<state root>/install-verdicts/<sha256 of the checkout realpath>.json`:
checkout, HEAD, the workflow and required components it certified, whether
pins were skipped as planning-only, the trailer, the remote minimum, the
launch PATH and the time.
`--check` runs only steps 4 and 5 (no clone, Bun install or setup).
## The one line to grep
The last line is the doctor's trailer:
The last line is the trailer:
```
gstack: ok 1.91.70.0 for=autoplan
gstack: ok 1.91.96.0 for=autoplan
gstack: fail runtime,pins
```
Exit 0 only with `gstack: ok`. Read the exit status and the trailer directly:
```bash
out=$(~/.local/share/gstack/bin/gstack-capy-install --for autoplan 2>&1); rc=$?
out=$(bash <skill>/scripts/install.sh --check --for autoplan 2>&1); rc=$?
printf '%s\n' "$out" | grep '^gstack: '
[ "$rc" -eq 0 ]
```
@@ -52,7 +115,13 @@ the incident this installer exists to end. Every `FAIL <id>` row carries a
`fix:` clause and a reason code with its [troubleshooting
anchor](troubleshooting.md#install-check-gstack-doctor---check-and-the-capy-installer).
A parent agent confirms each subagent machine with this grep; one machine's
install never covers another.
install never covers another, and a subagent's first report carries its own
`gstack:` line.
`SKIP pins (planning-only; repo pins differ: ..., launch bun <v>)` is not a
failure: planning workflows (`--for autoplan`, the plan reviews,
`office-hours`) never run the target repo's tests. Workflows that do (`review`,
`ship`, `qa`) and `--require` keep `pins` required.
When a lane reports to another thread (a handoff, a `gstack-lane-check`
result, a coordinator contract), store and use the full thread id (`jam_…`)
@@ -73,23 +142,70 @@ time separately.
## The launch line
The installer's last lines include:
Every successful run, `--check` included, prints before the trailer:
```
GSTACK_LAUNCH: GSTACK_SESSION_KIND=unattended GSTACK_STATE_ROOT=<path> GSTACK_EPHEMERAL=1
GSTACK_LAUNCH: PATH=<dir>:$PATH GSTACK_SESSION_KIND=unattended GSTACK_STATE_ROOT=<path> GSTACK_EPHEMERAL=1 GSTACK_CAPY=1 GSTACK_INSTALL_VERDICT=<file>
GSTACK_LAUNCH_PATH: <dir>
```
A child shell's `export` cannot configure the parent's later commands, so the
parent supplies these assignments to every gstack command it launches (prefix
the command, or export them once in the shell that runs it).
`PATH=<dir>:$PATH` and `GSTACK_LAUNCH_PATH` appear only when the certified Bun
is not first on PATH (the Capy image's `/usr/local/bin/bun` is 1.3.14; 57 gstack
commands start with `#!/usr/bin/env bun`). A child shell's `export` cannot
configure the parent's later commands, so the parent supplies these
assignments to every gstack command it launches (prefix the command, or
export them once in the shell that runs it). A fresh shell without them keeps
reporting the Bun it would really run: `FAIL runtime — bun 1.3.14 at
/usr/local/bin/bun ...; verified bun 1.4.2 found at ~/.bun/bin (not on PATH);
fix: export PATH="$HOME/.bun/bin:$PATH" or prefix commands with the
GSTACK_LAUNCH line`.
`GSTACK_EPHEMERAL=1` tells gstack the disk vanishes with the machine, so the
doctor's `state root` row reads `durable=no` and unattended runs say
`learnings: skipped` instead of writing silently. A durable root is opt-in:
set `GSTACK_STATE_ROOT` to a directory that outlives the machine. The
installer only prints the option and never writes to the Capy drive (its
policy excludes memory and verification evidence). See
installer never writes to the Capy drive. See
[docs/state-root.md](state-root.md#ephemeral-machines).
`GSTACK_CAPY=1` turns on install admission. An unattended session (any host)
refuses to start a workflow on a Bun below the supported minimum; with
`GSTACK_CAPY=1`, an unattended or `GSTACK_SESSION_KIND=spawned` session also
needs a passing verdict for this checkout, its current HEAD and a workflow at
least as wide as the one starting. A refusal prints `INSTALL: fail ...` and
`GSTACK_RESULT: skill=<s> status=refused run=-` and exits 3
([INSTALL_UNVERIFIED](troubleshooting.md#install-unverified)). Ambient
spawned sessions (OpenClaw) only warn. `GSTACK_ALLOW_UNSUPPORTED_RUNTIME=1`
admits anyway and says so.
## Bake into a Capy snapshot
Machines that never run a workflow should not pay the install (about 30 s
cold). A project that runs gstack on most machines can bake it instead: the
project Setup's initialize step runs
```bash
git clone --single-branch --depth 1 https://github.com/garrytan/gstack.git ~/.local/share/gstack
~/.local/share/gstack/bin/gstack-capy-install --require claude,codex,patch
```
and snapshots capture `$HOME` (the checkout, `~/.bun/bin`, both host
profiles). Each session then pays only the `--check`. A baked revision still
moves forward at session start when it falls below the supported minimum, and
the baked `~/.bun/bin` reaches workflows through the `GSTACK_LAUNCH` line. The
project Setup is shared by every thread in the project, so changing it is the
project's decision.
## Raising the supported minimum
`capy/support.env` holds `GSTACK_MIN_REVISION` and `GSTACK_CAPY_MIN_WRAPPER`.
Raise `GSTACK_MIN_REVISION` only for install-honesty or security fixes, at
stamp time, to the VERSION being released: every machine below it is
fast-forwarded at its next session start. It never exceeds VERSION
(test/capy-skill-wrapper.test.ts). Raise `GSTACK_CAPY_MIN_WRAPPER` together with
`GSTACK_CAPY_WRAPPER` in the bootstrapper when the bootstrapper itself
changes; older Drive copies keep working and print an owner action
([CAPY_WRAPPER_STALE](troubleshooting.md#capy-wrapper-stale)).
## Exit codes
| Exit | Meaning |