docs/capy-install: resolver, versioned store, bun shim, trailer notes; wrapper minimum 2

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-15 00:41:43 +00:00
1 parent db9fdbd0e6
commit ef8dce11df
3 files changed
+85 -25

No files matched your search

+1 -1
View File
@@ -16,4 +16,4 @@ GSTACK_MIN_REVISION=1.91.96.0
# GSTACK_CAPY_MIN_WRAPPER: the oldest bootstrapper revision (GSTACK_CAPY_WRAPPER in
# capy/skill/gstack/scripts/install.sh) the owner should run. An older copy still
# works and prints an owner action (CAPY_WRAPPER_STALE).
GSTACK_CAPY_MIN_WRAPPER=1
GSTACK_CAPY_MIN_WRAPPER=2
+81 -22
View File
@@ -82,26 +82,39 @@ whose pid is alive and outside its own process chain exists;
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
`requested: <ref> (run <checkout>/bin/gstack-upgrade to move)`. A fresh
clone prints `cloned <repo> at <sha12> (<version>)` (the bootstrapper's
clone too, through `GSTACK_CAPY_CLONED`).
2. Preflight: `git`, `curl`, `node`, `jq`, then Bun. 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
for this platform). One resolver picks the Bun (`gstack_bun_resolve` in
`bin/gstack-bun-version.sh`; `resolveBun` in `lib/pregate/runtime.ts` is
its TypeScript twin), and the installer, `--check`, the doctor, the bun
shim and install admission all use it. Candidates, first eligible wins:
the store copy of the project's exact pin (rule `pin`), the store copy of
the install default (`default`), `$BUN_INSTALL/bin/bun`, `~/.bun/bin/bun`
(`home`), then every `bun` on PATH. Eligible means it runs, reports a
version at or above the supported minimum (1.4.2) and satisfies the
project's pins. When nothing qualifies, 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
and unpacks it into the versioned store
`<state root>/runtimes/bun/<version>/bun`, under `~/.gstack-runtime.lock`.
A store entry is never replaced in place, and `~/.bun/bin` is never
written. The archive is the project's exact pin when gstack carries a
verified archive for it (`GSTACK_BUN_VERIFIED`: 1.4.2, 1.4.3), else the
install default `GSTACK_BUN_DEFAULT` (1.4.3, above the 1.4.2 minimum; one
CI job runs the quick free tier on it).
`GSTACK_BUN_INSTALL_VERSION=<v>` overrides the default; a version without a
verified archive or below the minimum stops the install
([BUN_INSTALL_VERSION_INVALID](troubleshooting.md#bun-install-version-invalid)).
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](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.
default 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
@@ -110,13 +123,46 @@ whose pid is alive and outside its own process chain exists;
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.
5. The install verdict (schema 2), written atomically to
`<state root>/install-verdicts/<sha256 of the checkout realpath>/<sha256 of the project realpath, or "none">.json`:
checkout, project, HEAD, the workflow and required components it
certified, whether pins were skipped as planning-only, the pin it
honoured, the resolved Bun (`bun_path`, `bun_version`, `bun_rule`, and
`bun_install` when this run downloaded it), the trailer, the remote
minimum, the launch PATH and the time. Admission refuses a workflow whose
running Bun is not the one the verdict recorded
([BUN_RESOLUTION_DRIFT](troubleshooting.md#bun-resolution-drift)); the
doctor's `runtime` row reports the same drift. A schema 1 verdict (keyed
by checkout only) is still read.
6. On a Capy machine (`CAPY_MACHINE_CONFIG` set or `~/.capy/system` present),
install mode writes the bun shims described below.
`--check` runs only steps 4 and 5 (no clone, Bun install or setup).
`--check` runs only steps 4 and 5 (no clone, Bun install, setup or shim).
## The bun shim
A fresh shell on Capy finds the image's `/usr/local/bin/bun` (1.3.14) first,
so a `bun test` typed without the `GSTACK_LAUNCH` prefix used to run the wrong
Bun. Install mode therefore writes `~/.local/bin/bun` and `~/.local/bin/bunx`
(`~/.local/bin` precedes `/usr/local/bin` on the Capy image's PATH). Each is a
small POSIX script whose second line is the marker
`# gstack-bun-shim: gstack-owned; delete this file to remove it`. At every
call the `bun` shim finds the git toplevel around the working directory and
runs, in order: the Bun that project's install verdict recorded, the store
copy of the project's exact pin, the store copy of the install default, then
the first non-shim `bun` on PATH. A project with no verdict prints
`gstack bun shim: <root> has no recorded Bun; run ... --check here` to stderr
and falls through. `bunx` runs `bun x` through the `bun` shim beside it.
- `GSTACK_BUN_SHIM_EXPLAIN=1 bun --version` prints
`gstack bun shim: project=<root> record=<file> chose=<bun> rule=<rule>`.
- `GSTACK_BUN_SHIM=0` at install time writes no shim.
- Removing it: delete `~/.local/bin/bun` and `~/.local/bin/bunx`. gstack
replaces only a missing file or a file carrying the marker; a foreign file
or a dangling link there is left untouched with a note, and the
`GSTACK_LAUNCH` prefix stays the way to reach the certified Bun.
- The doctor's `runtime` row notes the shim; the resolver never treats a
shim-marked file as a Bun candidate.
## The one line to grep
@@ -124,9 +170,19 @@ The last line is the trailer:
```
gstack: ok 1.91.96.0 for=autoplan
gstack: ok 1.91.96.0 for=review (requested: v1.91.90.0 (run <checkout>/bin/gstack-upgrade to move); latest: 1.91.101.0 available)
gstack: ok 1.91.96.0 for=autoplan (pins: skipped (planning-only))
gstack: fail runtime,pins
```
An ok trailer carries at most one parenthesised note group, `; `-separated,
in this order: `requested: …` (an existing checkout at another revision than
`--revision`), `pins: skipped (planning-only|no project)`, and `latest: <ver>
available` (origin's default branch has a newer VERSION; the bootstrapper
exports it as `GSTACK_CAPY_REMOTE_VERSION` only after a fetch that succeeded
in this run). A fail trailer never carries notes. Match `^gstack: ok ` or
`^gstack: fail `; never parse past the first space after the version.
Exit 0 only with `gstack: ok`. Read the exit status and the trailer directly:
```bash
@@ -144,7 +200,8 @@ 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
`SKIP pins (planning-only; repo pins differ: ..., launch bun <v>)` (trailer
note `pins: skipped (planning-only)`) 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.
@@ -232,7 +289,9 @@ floor is not what keeps machines current). 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)).
([CAPY_WRAPPER_STALE](troubleshooting.md#capy-wrapper-stale)), and the
doctor's `wrapper` row warns. Wrapper 2 adds the trailer's `latest:` note
and the `cloned` line.
## Exit codes
+3 -2
View File
@@ -801,6 +801,7 @@ describe('gstack-doctor --check: wrapper and runtime drift', () => {
const drive = (f: Fixture, scope: string, text: string) => { const file = path.join(f.home, '.capy/drive', scope, 'skills/gstack/scripts/install.sh'); write(file, text); return file; };
const withSupport = (f: Fixture, min = 1) => write(path.join(f.root, 'capy/support.env'), `GSTACK_MIN_REVISION=1.0.0.0\nGSTACK_CAPY_MIN_WRAPPER=${min}\n`);
const current = fs.readFileSync(path.join(REPO, 'capy/skill/gstack/scripts/install.sh'), 'utf8');
const currentRev = current.match(/^GSTACK_CAPY_WRAPPER=(\d+)$/m)![1];
test('direct doctor run: a pre-bootstrapper Drive copy (no revision line) fails wrapper with both fixes; a current copy is not flagged', () => {
const f = makeFixture();
@@ -814,7 +815,7 @@ describe('gstack-doctor --check: wrapper and runtime drift', () => {
expect(row).toContain('(CAPY_WRAPPER_STALE; https://github.com/garrytan/gstack/blob/main/docs/troubleshooting.md#capy-wrapper-stale)');
write(stale, current);
const fresh = parse(f, []);
expect(fresh.line('wrapper')).toBe(`PASS wrapper Drive copy ${stale} (wrapper 1) (minimum 1)`);
expect(fresh.line('wrapper')).toBe(`PASS wrapper Drive copy ${stale} (wrapper ${currentRev}) (minimum 1)`);
expect(fresh.status, fresh.out).toBe(0);
});
@@ -824,7 +825,7 @@ describe('gstack-doctor --check: wrapper and runtime drift', () => {
const stale = drive(f, 'org', PRE_BOOTSTRAPPER);
const good = drive(f, 'user', current);
const r = parse(f, []);
expect(r.line('wrapper')).toBe(`PASS wrapper Drive copy ${good} (wrapper 1) (minimum 1); stale copies: ${stale} (wrapper 0)`);
expect(r.line('wrapper')).toBe(`PASS wrapper Drive copy ${good} (wrapper ${currentRev}) (minimum 1); stale copies: ${stale} (wrapper 0)`);
expect(parse(f, [], { GSTACK_CAPY_WRAPPER_PATH: stale }).trailer).toBe('gstack: fail wrapper');
});