Merge PR A (install) under the autoplan wave; gstack-autoplan admission uses lib/install-verdict.ts (INSTALL_UNVERIFIED)

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:37:10 +00:00
commit 79ee56af91
34 files changed
+2067 -214

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 |
+77 -16
View File
@@ -1126,10 +1126,11 @@ constraint with its source), or change the project's pin.
revision (a VERSION string, tag, branch or commit) and the checkout is at
another. An unmet revision is never `ok`.
**Fix.** Move the checkout: `/gstack-upgrade`, or
**Fix.** Move the checkout: `<checkout>/bin/gstack-upgrade` (the tip of the default
branch; `/gstack-upgrade` interactively), or
`git -C <checkout> fetch origin && git -C <checkout> checkout <ref> && ./setup`.
`bin/gstack-capy-install --revision <ref>` never moves an existing checkout; it
reports `gstack: ok <installed> (requested <ref>; run gstack-upgrade to move)`.
reports `gstack: ok <installed> (requested <ref>; run <checkout>/bin/gstack-upgrade to move)`.
<a id="capy-install-runtime-conflict"></a>
### `gstack-capy-install: Bun conflict: gstack needs >=<minimum>, <project> pins <constraint> (RUNTIME_PIN_CONFLICT; ...)`
@@ -1170,6 +1171,80 @@ or nvm), then re-run `bin/gstack-capy-install`. A checksum mismatch means the
upstream installer changed: update `GSTACK_BUN_INSTALL_SHA` only after
reviewing the new script.
<a id="upgrade-refused"></a>
### `gstack-upgrade: <cause>; nothing was changed; fix: <command> (UPGRADE_REFUSED)`
**Meaning.** `bin/gstack-upgrade` moves a checkout only by fast-forward to the
tip of its remote default branch. It refused before changing anything: the
directory is not a gstack checkout, HEAD is detached or on another branch (a
pin), a tracked file is modified, a local commit is not on the remote branch,
the incoming release's Bun floor is above the Bun on PATH, or another updater
holds `<state root>/.setup-lock`. It never stashes, resets or discards.
**Fix.** Resolve the named cause with the printed command (commit or move the
listed changes, check out the default branch, move local commits to another
branch), then re-run `bin/gstack-upgrade`. A reinstall that failed after the
fast-forward is completed by the next run (`reinstall pending from <version>`).
<a id="upgrade-fetch-failed"></a>
### `gstack-upgrade: could not reach origin (...) (UPGRADE_FETCH_FAILED)`
**Meaning.** The remote default branch could not be listed or fetched (offline,
proxy, credentials), so `--check` cannot say whether the checkout is current.
It never reports `current` without the fetch.
**Fix.** Check network access to the checkout's origin
(`git -C <checkout> remote -v`), then re-run.
<a id="revision-below-minimum"></a>
### `gstack: fail revision — installed <v> is below the supported minimum <m> (<cause>); fix: ... (REVISION_BELOW_MINIMUM)`
**Meaning.** `capy/support.env` on the remote default branch declares the
oldest revision whose installer, trailer and fix lines are honest
(`GSTACK_MIN_REVISION`), and this checkout is older. The Capy skill
bootstrapper reads it from `FETCH_HEAD`, so even a checkout that predates the
file learns it. `--check` only reports (`fast-forward available`); install
mode fast-forwards a clean, unpinned checkout, reinstalls and rechecks under
one lock. The cause names why nothing moved: `pinned by --revision`, `pinned:
HEAD is detached`, `tracked files changed: <files>`, `diverged: ...` or
`auto-upgrade disabled` (`GSTACK_CAPY_AUTO_UPGRADE=0`). `gstack-doctor --check
--min-revision <m>` fails the revision row the same way.
**Fix.** Run the fix clause: the bootstrapper without `--check`, or
`<checkout>/bin/gstack-upgrade`. For a pin, drop `--revision` or pin a
revision at or above the minimum. An interrupted install after the
fast-forward leaves the trailer `fail`; the next install run completes it.
<a id="capy-wrapper-stale"></a>
### `gstack-capy-skill: owner action — Drive copy is wrapper <n>, minimum <m>; copy <root>/capy/skill/gstack/ (CAPY_WRAPPER_STALE)`
**Meaning.** The Capy skill folder that ran (the bootstrapper's
`GSTACK_CAPY_WRAPPER`) is older than the checkout's `GSTACK_CAPY_MIN_WRAPPER`.
The run continues through the checkout's own wrapper (`bin/gstack-capy-session`);
only a checkout with no such wrapper refuses (`gstack: fail wrapper`).
**Fix.** The owner copies `<checkout>/capy/skill/gstack/` over the skill folder
(on Capy, the user-scope Drive `skills/gstack/`). Agents never write it.
<a id="install-unverified"></a>
### `INSTALL: fail verdict — <detail>; fix: ... (INSTALL_UNVERIFIED)`
**Meaning.** On a Capy machine (`GSTACK_CAPY=1`, set by the `GSTACK_LAUNCH`
line), an unattended or spawned workflow starts only with a passing install
verdict (`<state root>/install-verdicts/<sha256 of the checkout path>.json`,
named as `GSTACK_INSTALL_VERDICT`) for this checkout, its current HEAD and a
workflow at least as wide as the one starting: a verdict for `--for autoplan`
does not cover `qa`'s browser, and a planning-only pins SKIP does not cover a
workflow that runs the repo's tests. `gstack-skill-start` printed this row and
`GSTACK_RESULT: skill=<s> status=refused run=-`, exit 3. The same start refuses
an unattended run whose Bun on PATH is below the supported minimum
(`INSTALL: fail runtime ... (RUNTIME_BELOW_MINIMUM)`) on any host.
**Fix.** Run the Capy skill installer with `--for <workflow>` on this machine
and launch with the `GSTACK_LAUNCH` line it prints.
`GSTACK_ALLOW_UNSUPPORTED_RUNTIME=1` admits anyway (printed as
`INSTALL_VERDICT: overridden`); record it as a deviation.
---
## Browser
@@ -2508,20 +2583,6 @@ older runner cannot know the newer state.
**Fix.** Nothing for the notice. For the refusal, upgrade gstack
(`bin/gstack-upgrade`) on this machine and rerun the command.
<a id="install-verdict-failed"></a>
### `INSTALL_VERDICT_FAILED`
**Meaning.** On a Capy host (`GSTACK_CAPY=1`) `gstack-autoplan`'s mutating
verbs (`next`, `submit`, `answer`, `revise`, `export`) need the install verdict
the Capy wrapper writes (`GSTACK_INSTALL_VERDICT`) to say `gstack: ok` for this
checkout and HEAD. Missing, failing, or written for another checkout or HEAD
refuses before anything is written. `status` and `resume` stay available.
**Fix.** Run the Capy gstack skill wrapper until its trailer is `gstack: ok`,
then launch commands with its `GSTACK_LAUNCH` line. To proceed anyway, set
`GSTACK_ALLOW_UNSUPPORTED_RUNTIME=1`; the override is recorded in
`run.json.deviations` and printed as `ADMISSION: override`.
<a id="artifact-gate-unsettled"></a>
### `ARTIFACT_GATE_UNSETTLED`
+17 -3
View File
@@ -278,9 +278,10 @@ by its runner. Result codes: `EXECUTION_UNKNOWN`, `RUN_LOCKED`,
the missing heading added. `resume` completes an interrupted revise.
- **Admission and upgrades.** `next`, `submit`, `answer`, `revise`, `export`
and `run` refuse on a Capy host (`GSTACK_CAPY=1`) without an ok install
verdict for this checkout at `$GSTACK_INSTALL_VERDICT`
(`INSTALL_VERDICT_FAILED`), and in any unattended session when the Bun on
PATH is below `engines.bun` (`RUNTIME_BELOW_MINIMUM`);
verdict for this checkout, HEAD and workflow at `$GSTACK_INSTALL_VERDICT`
(`INSTALL_UNVERIFIED`), and in any unattended session when the running Bun
is below gstack's supported minimum (`RUNTIME_BELOW_MINIMUM`); the decision
is the same one `gstack-skill-start` makes (see Install admission);
`GSTACK_ALLOW_UNSUPPORTED_RUNTIME=1` overrides and records a run deviation.
`status` and `resume` are never gated. A run directory written by an older
gstack prints `GSTACK_VERSION_CHANGED` once and moves forward (legacy
@@ -328,3 +329,16 @@ line, on an ephemeral state root.
`/autoplan` run (the one that produced this wave's plan) and validates clean.
`test/fixtures/multi-agent-wave/INCIDENTS.md` records what went wrong while it
was produced by hand and which validator rule now catches each incident.
## Install admission
An unattended run, and a `GSTACK_SESSION_KIND=spawned` run with `GSTACK_CAPY=1`,
starts only on a supported Bun and, on a Capy machine (`GSTACK_CAPY=1`, set by
the installer's `GSTACK_LAUNCH` line), with a passing install verdict for this
checkout, its HEAD and the workflow. Otherwise `gstack-skill-start` prints
`INSTALL: fail runtime|verdict — <detail>; fix: <command> (<CODE>)` and
`GSTACK_RESULT: skill=<s> status=refused run=-`, and exits 3; stop and run the
fix. `GSTACK_ALLOW_UNSUPPORTED_RUNTIME=1` admits anyway (`INSTALL_VERDICT:
overridden`), recorded as a deviation. TypeScript entrypoints use
`checkInstallAdmission` from `lib/install-verdict.ts`; the Bash side is
`bin/gstack-install-verdict.sh`. See [capy-install.md](capy-install.md#the-launch-line).