Files
gstack/docs/capy-install.md
T
Garry Tanandcapy-ai[bot] ada251bbc0 v1.91.96.0 fix: honest install and self-upgrade on Capy (field wave A, items 1-5) (#3164)
* docs(designs): gstack field wave plan (2026-10-12) and the shared simulated Capy host fixture

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* feat(doctor): --min-revision, a launch Bun found off PATH, planning-only pins SKIP

gstack-doctor --check gains --min-revision <v>|unknown: below it the revision
row FAILs with REVISION_BELOW_MINIMUM naming the Capy wrapper or
bin/gstack-upgrade; unknown says so and never claims the minimum. The runtime
and pins rows name a supported Bun found in $BUN_INSTALL/bin (not on PATH)
with the export/GSTACK_LAUNCH fix (gstack_bun_launch_dir, one helper in
bin/gstack-bun-version.sh). --for a planning-only workflow (autoplan, the plan
reviews, office-hours) reports a repo test-pin mismatch as SKIP pins
(planning-only; ...). New result codes UPGRADE_REFUSED, UPGRADE_FETCH_FAILED,
REVISION_BELOW_MINIMUM, CAPY_WRAPPER_STALE, INSTALL_UNVERIFIED with
troubleshooting anchors; REVISION_UNMET's fix names bin/gstack-upgrade.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* feat(install): install verdict and install admission at skill start

bin/gstack-install-verdict.sh (Bash) and lib/install-verdict.ts (TypeScript)
are the one reader of the install verdict gstack-capy-install writes to
<state root>/install-verdicts/<sha256(checkout realpath)>.json (checkout,
HEAD, workflow and required components, planning-only pins, trailer). The
source checkout and capy/support.env resolve through gstack_source_root /
sourceRoot, which follow an installed root's .source-path.

gstack-skill-start, after SESSION_KIND: an unattended session (any host)
refuses on a PATH Bun below the supported minimum; with GSTACK_CAPY=1 an
unattended or explicit spawned session also refuses without a passing
verdict for this checkout, HEAD and workflow (INSTALL_UNVERIFIED). A refusal
prints the INSTALL: row, an install-refused instruction block bound to its
SESSION_ID, GSTACK_RESULT ... status=refused run=- and exits 3. Ambient
spawned sessions warn; GSTACK_ALLOW_UNSUPPORTED_RUNTIME=1 admits and says so.
capy/support.env declares GSTACK_MIN_REVISION and GSTACK_CAPY_MIN_WRAPPER.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* feat(capy-install): launch PATH, Bun reuse and CI pins, setup skip on HEAD, verdict, --check

The GSTACK_LAUNCH line carries PATH=<dir>:$PATH when the certified Bun is not
first on PATH, plus GSTACK_CAPY=1 and GSTACK_INSTALL_VERDICT, and
GSTACK_LAUNCH_PATH: <dir>; the doctor runs with that PATH. A supported Bun in
$BUN_INSTALL/bin is reused instead of downloaded again. Project pins are read
with bin/gstack-runtime-pins.ts (CI lanes included): when every exact pin is
gstack's verified version the archive is installed alongside a different PATH
Bun; any other exact pin is RUNTIME_PIN_CONFLICT. Both ./setup runs are
skipped when this HEAD was set up with the same hosts, model and browser flag
and the check passes (never after a fast-forward); a failed setup ends with
gstack: fail setup and is retried next run. --check certifies only;
GSTACK_CAPY_LOCK_HELD=1 accepts the bootstrapper's held lock; result anchors
come from the generated components table; REV_NOTE names bin/gstack-upgrade.

Intentional pin change: test/gstack-capy-install.test.ts --revision trailer
now names <root>/bin/gstack-upgrade (the old text named a command that never
existed).

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* feat(upgrade): bin/gstack-upgrade, the unattended fast-forward every fix line names

--check fetches and prints current|behind <installed> -> <remote> (<n>
commits); the default fast-forwards to the remote default branch tip and
reinstalls (bin/gstack-capy-install on a Capy install, else ./setup
--refresh-registered), completing a pending reinstall on the next run. It
refuses (UPGRADE_REFUSED, exit 3, nothing changed) a dirty tracked byte, a
diverged commit, a pinned checkout, the incoming Bun floor and a held
.setup-lock; never stashes or resets; an unreachable remote is
UPGRADE_FETCH_FAILED. Fetches are receipted (fail-open). The unattended
skill-end UPGRADE_AVAILABLE line is followed by UPGRADE_COMMAND naming it.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* feat(capy): vendored Capy skill, frozen bootstrapper and full wrapper bin/gstack-capy-session

capy/skill/gstack/ is the Drive skill's source of truth. scripts/install.sh
is a frozen bootstrapper: it prints gstack-capy-skill: wrapper <n> (<path>),
clones or fetches under .gstack-capy-install.lock, reads the remote
capy/support.env (FETCH_HEAD), fast-forwards a clean unpinned checkout below
GSTACK_MIN_REVISION (refusing dirty, diverged or pinned checkouts with the
exact commands; GSTACK_CAPY_AUTO_UPGRADE=0 disables), and execs the
checkout's own bin/gstack-capy-session with GSTACK_CAPY_LOCK_HELD=1 so the
held lock is never taken twice. Offline it says the remote minimum is
unknown. A Drive copy older than GSTACK_CAPY_MIN_WRAPPER prints an owner
action (CAPY_WRAPPER_STALE). SKILL.md: check at session start, install
lazily before the first workflow, the GSTACK_LAUNCH line, the trailer rule,
the subagent first-report line, gstack-pregate sealed before any push, and
the autoplan/DRIVER.md pointer. The context-budget fixture gains the skill's
entry.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* 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>

* refactor(capy): the stale-copy owner action moves from the frozen bootstrapper to bin/gstack-capy-session

The checkout-owned full wrapper reads GSTACK_CAPY_MIN_WRAPPER through
gstack_support_env_get(gstack_source_root) and compares it with the
bootstrapper revision it was handed, so the rule can change without
recopying the Drive skill. The bootstrapper keeps only what a stale checkout
cannot do: clone, fetch, read the remote minimum, fast-forward, exec.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* v1.91.96.0: honest install and self-upgrade on Capy; supported minimum 1.91.96.0

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: drop the helper-only Capy host fixture test (test-of-test ratchet); lane tests exercise the helper

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: bound the new sync spawns in the install-verdict and wrapper tests (spawnSync tripwire)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* ci(paid-pr-profile): the vendored Capy skill is free-only; free-only files are not prompts needing paid coverage

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* chore(harness): non-behavioral HARNESS_VERSION decision for the PR-profile free-only rule

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test(doctor): lane A runtime and pins cases are platform-neutral (bash paths; a lane-free .bun-version pin)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs(designs): field wave plan with amendments and the gate rev 2-3 review record (ARTIFACT_VALID, 3 gate revisions)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(capy-install): follow a project's exact Bun pin above the minimum when gstack has its verified archive (adds Bun 1.4.3)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: the installer follows a verified exact project Bun pin (capy-install, troubleshooting, changelog)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(capy-install): the install reason names the project's pin and gstack's real minimum

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>
2026-10-13 14:41:58 +00:00

12 KiB

Installing gstack on a Capy cloud machine

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:

git clone --single-branch --depth 1 https://github.com/garrytan/gstack.git ~/.local/share/gstack
~/.local/share/gstack/bin/gstack-capy-install --for autoplan

Session start

  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). 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 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, the installer downloads one 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). That archive is the supported minimum (1.4.2), or the project's exact pin when it is above the minimum and gstack carries a verified archive for it (GSTACK_BUN_VERIFIED: 1.4.2, 1.4.3), so a repo whose CI pins bun-version: 1.4.3 gets 1.4.3. With an exact pin, only that version satisfies it. Any other exact pin (below the minimum, two different ones, or one without a verified archive), or a range gstack's minimum does not satisfy, stops with both constraints (RUNTIME_PIN_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. 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), 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 trailer:

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:

out=$(bash <skill>/scripts/install.sh --check --for autoplan 2>&1); rc=$?
printf '%s\n' "$out" | grep '^gstack: '
[ "$rc" -eq 0 ]

Never pipe --check (or the installer) through tail, head or grep alone: the pipe's exit status replaces the doctor's, and a silent failure is the incident this installer exists to end. Every FAIL <id> row carries a fix: clause and a reason code with its troubleshooting anchor. A parent agent confirms each subagent machine with this grep; one machine's 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_…) in the message, never the short display code: the code is a label the app renders, the id is what another agent can address and what the evidence bundle records. UBI_OWNER and lane ownership records take the same full id.

Browser

The default install prints SKIP browser (lazy; gstack-browser-ensure installs on first use) and downloads nothing. The first browser skill run (/browse, /qa, /design-review, ...) calls bin/gstack-browser-ensure from its preflight, which installs Chromium (~115 MiB, about 86 s on a Capy machine) under setup's lock and prints BROWSER_OK or BROWSER_UNAVAILABLE <reason>. --with-browser installs it during the install instead, and the installer then prints the planning-only install time and the browser bootstrap time separately.

The launch line

Every successful run, --check included, prints before the trailer:

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>

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 never writes to the Capy drive. See docs/state-root.md.

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). 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

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).

Exit codes

Exit Meaning
0 gstack: ok — every required component passed
1 gstack: fail <ids> — a required component failed; each row has a fix
2 usage error
3 preflight stopped before ./setup (INSTALL_PREFLIGHT_FAILED, RUNTIME_PIN_CONFLICT); nothing was installed