feat(capy-install): project floor, --for none, WARN for behind and Node-only pins, in-place review-voice upgrade, /cso env hygiene (harness lane F)

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-26 14:30:22 +00:00
1 parent 5755601dac
commit 4564de2ddb
21 files changed
+495 -78

No files matched your search

+69 -13
View File
@@ -41,17 +41,39 @@ no host profile, so a workflow started there is refused (`INSTALL: fail verdict
— … (review-voice certifies only the read-only helpers); fix: run
GSTACK_CAPY_ROOT=<root> bash <root>/capy/skill/gstack/scripts/install.sh --for
<skill> …`). A review-voice check never replaces an ok verdict that a fuller
install wrote for the same HEAD and project. Every /autoplan reviewer prompt
install wrote for the same HEAD and project. The reverse is an in-place
upgrade: a reviewer later asked to run a workflow runs the same installer
`--for <skill>`, which reuses the checkout (`upgrading the review-voice
install in place to --for <skill> (same checkout ...; no second clone)`),
applies the project's pins, runs `./setup` and both host profiles, and writes
the workflow's verdict over the reviewer's. Nothing is removed and nothing is
cloned twice. Every /autoplan reviewer prompt
ends with this command (`GSTACK TRAILER`); the warm run's budget, under 20 s,
is measured by `test/capy-review-voice.test.ts`.
## Machines that run no workflow: `--for none`
A coordinator, liaison or Captain thread runs no gstack workflow on its own
machine, and `gstack: fail not-installed` there read as a broken install.
`bash <skill>/scripts/install.sh --check --for none` is that machine's session
check: it prints `gstack: ok not-needed for=none (no checkout at <root>; before
the first workflow run: bash <skill>/scripts/install.sh --for <skill>)` (or
`checkout <v> at <root>, not certified; latest: <ver>; ...` when one exists)
and exits 0 without cloning, moving, installing or certifying anything;
install mode with `--for none` behaves the same. The bootstrapper answers it
itself, so it is honest for any checkout revision, and `bin/gstack-capy-install
--for none` answers the same way for a machine without the Drive skill. That
ok admits no workflow: no install verdict is written, so a workflow started
there is refused with the usual `--for <skill>` fix.
## 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.
right before the first workflow. A machine that will run none uses
`--check --for none` and records its `gstack: ok not-needed` line instead.
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
@@ -68,10 +90,16 @@ is measured by `test/capy-review-voice.test.ts`.
refusal of `gstack-upgrade` applies), prints `SELF_UPGRADE: done <old> ->
<new>` and re-execs the new checkout's wrapper, which installs from the
new revision (`GSTACK_CAPY_UPGRADED_FROM` set, so setup is never skipped).
A refusal prints `SELF_UPGRADE: skipped (<reason>)`, and the session is
never certified ok: the trailer is `gstack: fail behind — installed <rev> is
<n> commits behind origin (latest <ver>) and the session-start upgrade was
skipped: <reason>; fix: <command> (UPGRADE_BUSY|UPGRADE_REFUSED)`, exit 1.
A refusal prints `SELF_UPGRADE: skipped (<reason>)` and a `WARN behind —
installed <rev> is <n> commits behind origin (latest <ver>) and the
session-start upgrade was skipped: <reason>; fix: <command>
(UPGRADE_BUSY|UPGRADE_REFUSED|UPGRADE_SKIPPED)` row; the doctor certifies
the installed revision and the trailer stays ok with `behind=<n> (latest:
<ver>); upgrade skipped: <reason> (<CODE>); fix: <command>` notes
([UPGRADE_SKIPPED](troubleshooting.md#upgrade-skipped)). A gap above the
supported minimum is never a hard fail; the lane reports the `behind=`
note, and a project that needs the newer gstack says so with a floor
(next section).
`SELF_UPGRADE: none (current)` when nothing is ahead;
`GSTACK_CAPY_SELF_UPGRADE=off` (or `GSTACK_CAPY_AUTO_UPGRADE=0`) prints
`SELF_UPGRADE: disabled (<variable>)`. `--check` never moves the checkout;
@@ -85,6 +113,24 @@ is measured by `test/capy-review-voice.test.ts`.
4. When the fetch fails (offline), the bootstrapper says `remote minimum
unknown (fetch failed)` and checks the local install only; it never claims
the minimum, and nothing is fast-forwarded.
5. The project's floor. A consuming repository names the oldest gstack it wants
its crew on as `min_gstack_version` in its `.gstack/ship-policy.json` (the
key `gstack-ship-queue` already validates; a file holding only that key is
valid). The doctor's `floor` row reads it from the project the check runs in
(`--project`, else the git toplevel): `PASS floor <f> declared by <file>`,
`SKIP floor (... declares none)`, or `WARN floor — installed <v> is below the
floor <f> that <file> declares (min_gstack_version); fix: bash
<skill>/scripts/install.sh --for <skill> (fast-forwards ...), or
bin/gstack-upgrade (PROJECT_FLOOR_UNMET)`. The trailer stays ok and carries
`floor=<f> (<file>)` and `fix: <command>`; the lane runs the fix before its
first workflow. Raising the floor is one commit in the project instead of a
message to every thread ([PROJECT_FLOOR_UNMET](troubleshooting.md#project-floor-unmet)).
6. The project's Node pin. When only the Node pin is unmet (the image's Node
behind the repo's pin while every Bun pin is met), `pins` is `WARN pins —
... (node pin only; bun pins pass); fix: ... (PROJECT_NODE_PIN_UNMET)`, not a
failure: gstack runs on Bun and the project's tests choose their Node. A
Bun pin that is unmet stays `FAIL pins` (`PROJECT_PIN_MISMATCH`)
([PROJECT_NODE_PIN_UNMET](troubleshooting.md#project-node-pin-unmet)).
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
@@ -283,19 +329,26 @@ 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: ok 1.91.101.0 for=autoplan (behind=12 (latest: 1.91.107.0); fix: bash <skill>/scripts/install.sh --for autoplan)
gstack: ok 1.91.240.0 for=ship (behind=5 (latest: 1.91.245.0); floor=1.91.245.0 (<project>/.gstack/ship-policy.json); fix: bash <skill>/scripts/install.sh --for ship)
gstack: ok 1.91.240.0 for=ship (behind=5 (latest: 1.91.245.0); upgrade skipped: tracked files changed: README.md (UPGRADE_REFUSED); fix: <checkout>/bin/gstack-upgrade)
gstack: ok not-needed for=none (no checkout at <root>; before the first workflow run: bash <skill>/scripts/install.sh --for <skill>)
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|helper runtime)`, then either
`behind=<n> (latest: <ver>)` followed by `fix: <command>` (the session ended
<n> commits behind origin's default branch after a fetch that succeeded in this
run; `bin/gstack-capy-session` exports `GSTACK_CAPY_BEHIND`, `_BEHIND_TO` and
`_BEHIND_FIX`, so this works with every bootstrapper revision), or `latest:
`behind=<n> (latest: <ver>)` (the session ended <n> commits behind origin's
default branch after a fetch that succeeded in this run; `bin/gstack-capy-session`
exports `GSTACK_CAPY_BEHIND`, `_BEHIND_TO` and `_BEHIND_FIX`, so this works
with every bootstrapper revision), optionally followed by `upgrade skipped:
<reason> (<CODE>)` when an install-mode fast-forward was refused, or `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
in this run); then `floor=<f> (<file>)` when the project's floor is unmet; and
last exactly one `fix: <command>` whenever `behind=` or `floor=` is present.
A fail trailer never carries notes. `gstack: ok not-needed for=none (...)` is
the `--for none` outcome: no revision, nothing certified, no workflow admitted. 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:
@@ -455,8 +508,11 @@ working and print an owner action
([CAPY_WRAPPER_STALE](troubleshooting.md#capy-wrapper-stale)), and the
doctor's `wrapper` row warns (an active copy below the minimum is a WARN,
never a FAIL; `bin/gstack-upgrade` never leaves a pending reinstall for it).
Wrapper 2 only adds optional exports (the trailer's `latest:` note and the
`cloned` line), so the minimum stays 1.
Wrapper 2 only added optional exports (the trailer's `latest:` note and the
`cloned` line). Wrapper 3 answers `--check --for none` itself (a copy at 1 or 2
prints `fail not-installed` for a coordinator machine and still works for every
other call), so the minimum is 3: every older Drive copy prints the owner
action until the owner recopies `capy/skill/gstack/`.
## Exit codes
+53
View File
@@ -1484,6 +1484,59 @@ moved into place.
**Fix.** Make the verdict directory writable, or set `GSTACK_STATE_ROOT` to a
writable directory, then re-run `bin/gstack-capy-install`.
<a id="project-floor-unmet"></a>
### `WARN floor — installed <v> is below the floor <f> that <project>/.gstack/ship-policy.json declares (min_gstack_version); fix: ... (PROJECT_FLOOR_UNMET)` / trailer note `floor=<f> (<file>)`
**Meaning.** The project you work in declares the oldest gstack it wants its
crew on, as `min_gstack_version` in its `.gstack/ship-policy.json` (the same
key `gstack-ship-queue` validates; a file holding only that key is valid), and
this machine's checkout is older. One commit in the project replaces a
crew-wide "upgrade to <v>" broadcast: every machine's next session check says
so and names the command. It is a project rule, not a security minimum, so
the row is a WARN and the trailer stays `gstack: ok` with a `floor=<f>
(<file>)` note and a `fix:` note; only the supported minimum
(`REVISION_BELOW_MINIMUM`) fails a session for being old.
**Fix.** Run the fix before the first workflow: on Capy, `bash
<skill>/scripts/install.sh --for <skill>` fast-forwards a clean checkout to
origin and reinstalls in one step (`SELF_UPGRADE: done`); elsewhere
`bin/gstack-upgrade`. If the floor is above gstack's latest release, lower it
in the project.
<a id="project-node-pin-unmet"></a>
### `WARN pins — node <v> outside <pin> (<file>); node read from <path> (node pin only; bun pins pass); fix: ... (PROJECT_NODE_PIN_UNMET)`
**Meaning.** The project's Node pin is not met by the Node first on PATH (a
Capy image's Node behind the pin, say), while every Bun pin is met. gstack's
own commands run on Bun, and the project's tests usually select their Node
themselves (nvm, the CI matrix), so this is advisory: the trailer stays ok. A
Bun pin that is unmet, with or without a Node gap, is still `FAIL pins`
(`PROJECT_PIN_MISMATCH`).
**Fix.** When this machine will run the project's tests, run the row's fix: the
Capy skill installer installs an exact pin into gstack's runtime store and
launches it through the `GSTACK_LAUNCH` PATH; otherwise `nvm install <pin>`
and put it first on PATH. A range pin (`>=24.22.1`) needs a Node you install
yourself; or change the pin in the project.
<a id="upgrade-skipped"></a>
### `WARN behind — installed <v> is <n> commits behind origin (latest <ver>) and the session-start upgrade was skipped: <reason>; fix: <command> (UPGRADE_SKIPPED|UPGRADE_BUSY|UPGRADE_REFUSED)` / trailer note `behind=<n> (latest: <ver>); upgrade skipped: <reason> (<CODE>); fix: <command>`
**Meaning.** An install-mode session start above the supported minimum found
the checkout behind origin and could not fast-forward it: the checkout is
dirty, diverged or pinned (`UPGRADE_REFUSED`), another live session holds a
lease (`UPGRADE_BUSY`), or `bin/gstack-upgrade` is missing
(`UPGRADE_SKIPPED`). Nothing was changed, the doctor certified the installed
revision, and the trailer stays `gstack: ok` with the notes above: a gap
above the minimum is never a hard fail. The verdict records the installed
revision, so a lane that is behind reports it (`behind=` rides every report).
**Fix.** When the lane needs the newer gstack (a `floor=` note, or a fix that
landed upstream), clear the named cause and run the fix: `bin/gstack-upgrade`
after the refusal is resolved, or the Capy skill installer again once the
lease holder finishes. Below the supported minimum the bootstrapper fails
`revision` instead (`REVISION_BELOW_MINIMUM`).
<a id="upgrade-refused"></a>
### `gstack-upgrade: <cause>; nothing was changed; fix: <command> (UPGRADE_REFUSED)`