fix: pre-landing review fixes

Red team (/ship Step 9): a Drive copy at a bootstrapper revision below GSTACK_CAPY_MIN_WRAPPER is a WARN with the owner action in a direct (no-provenance) run too; only a pre-bootstrapper copy fails. A --for none session never self-upgrades. Stale fail-behind comment in gstack-capy-session; SKILL.md says ok not-needed is not an install verdict. Tests: UPGRADE_SKIPPED collapse, silent in-place message, direct-run WARN, wrapper-2 --for none handover.

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 16:05:39 +00:00
1 parent 6a0f9e47bb
commit fc69d3ff0b
10 files changed
+72 -14

No files matched your search

+5 -2
View File
@@ -5,7 +5,7 @@
**The session check stops failing for things that are not failures, and a consuming repository can name the gstack it wants without broadcasting to every thread.** Five Captains running gstack on Capy hit the same install friction this week: eight crew-wide "upgrade to <version>" messages since Oct 16, `gstack: fail not-installed` on coordinator machines that run no workflow, `fail pins` from the image's Node while the tests run on the pinned one, reviewers told to "reinstall" when asked to run a workflow, and an environment dump from a broken `/cso` shell line. Each of these is now a product behaviour instead of a crew rule.
### Added
- **Project floor** (`floor` row in `gstack-doctor --check`, `lib/doctor-components.ts`, `bin/gstack-doctor-check.sh`): a consuming repository declares 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 reads it from the project the check runs in: `PASS floor <f> declared by <file>`, `SKIP floor (... declares none)`, or `WARN floor — installed <v> is below the floor <f> ...; fix: <install command>` (`PROJECT_FLOOR_UNMET`). The Capy installer adds `floor=<f> (<file>)` and a `fix:` note to its ok trailer. One commit in the project replaces a message to every thread; the fix on Capy is the install command, which fast-forwards a clean checkout to origin in one step.
- **Project floor** (`floor` row in `gstack-doctor --check`, `lib/doctor-components.ts`, `bin/gstack-doctor-check.sh`): a consuming repository declares the oldest gstack it wants its crew on as `min_gstack_version` in its `.gstack/ship-policy.json` (the field `gstack-ship-queue` already validates; a file holding only that field is valid). The doctor reads it from the project the check runs in: `PASS floor <f> declared by <file>`, `SKIP floor (... declares none)`, or `WARN floor — installed <v> is below the floor <f> ...; fix: <install command>` (`PROJECT_FLOOR_UNMET`). The Capy installer adds `floor=<f> (<file>)` and a `fix:` note to its ok trailer. One commit in the project replaces a message to every thread; the fix on Capy is the install command, which fast-forwards a clean checkout to origin in one step.
- **`--for none`** (`capy/skill/gstack/scripts/install.sh` wrapper 3, `bin/gstack-capy-install`): a machine that runs no gstack workflow (a coordinator, a liaison, a Captain) prints `gstack: ok not-needed for=none (<notes>)` and exits 0, installing, moving and certifying nothing, with or without a checkout. The bootstrapper answers it itself, so the line is honest for any checkout revision. That ok admits no workflow: no install verdict is written, so a workflow started there is refused with the usual `--for <skill>` fix. `GSTACK_CAPY_MIN_WRAPPER` is 3; older Drive copies keep working and print the owner action.
- **Review-voice machines upgrade in place** (`bin/gstack-capy-install`): running `--for <skill>` on a machine installed `--for review-voice` says `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. The admission refusal's fix now says so (`upgrades this checkout in place: ./setup and host profiles, no second clone`) instead of reading like a reinstall.
@@ -13,6 +13,8 @@
- **A gap above the supported minimum is never a hard fail** (`bin/gstack-capy-install`): an install-mode session whose session-start fast-forward was skipped (dirty, diverged or pinned checkout, a live lease, `gstack-upgrade` missing) prints `WARN behind — ... and the session-start upgrade was skipped: <reason>; fix: <command> (UPGRADE_BUSY|UPGRADE_REFUSED|UPGRADE_SKIPPED)` and keeps its ok trailer with `behind=<n> (latest: <ver>); upgrade skipped: <reason> (<CODE>); fix: <command>` notes, exit 0. It used to be `gstack: fail behind`, exit 1. Below the supported minimum the bootstrapper still fails `revision` (`REVISION_BELOW_MINIMUM`).
- **A Node-only pin mismatch is advisory** (`bin/gstack-doctor-check.sh`): when the project's Node pin is unmet by the Node on PATH while every Bun pin is met, `pins` is `WARN pins — ... (node pin only; bun pins pass); fix: ... (PROJECT_NODE_PIN_UNMET)` and the trailer stays ok. gstack runs on Bun and the project's tests choose their Node (nvm, the CI matrix). A Bun pin that is unmet stays `FAIL pins` (`PROJECT_PIN_MISMATCH`).
- **WARN rows print their action as given** (`_dc_row`): `owner action: ...` for the Capy skill copy, `fix: ...` for the floor, a skipped upgrade and a Node pin, so a lane knows which WARNs it can clear itself.
- **A Drive copy below the wrapper minimum is a WARN in a direct run too** (`bin/gstack-doctor-check.sh`): with no bootstrapper provenance (`bin/gstack-upgrade`'s installer run, a direct `gstack-doctor --check`), every Drive copy at a bootstrapper revision below `GSTACK_CAPY_MIN_WRAPPER` is `WARN wrapper — every Capy skill copy is below the minimum wrapper 3 (it still works): ...` with the owner action and an ok trailer; only a pre-bootstrapper copy (no `GSTACK_CAPY_WRAPPER=` line) still fails. Found by /ship's red team: raising the minimum to 3 would otherwise have failed every direct run on a machine whose Drive copy is the deployed wrapper 2 until the owner recopied the skill.
- **A `--for none` session never self-upgrades** (`bin/gstack-capy-session`): an older bootstrapper (wrapper 1 or 2) hands `--for none` to the session in install mode; the session now treats it like `--check` (reports `behind=`, moves nothing) before the installer prints `ok not-needed`. Wrapper 3 answers `--for none` before the session. The session's header comment describes the `WARN behind` outcome (it said `fail behind`).
- **/cso** (`cso/SKILL.md.tmpl`): the always-loaded startup block gains two rules. Environment hygiene: prerequisite checks read a variable as set or unset only, and `env`, `printenv`, `set -x`, a bare `set` or `export`, `export $(...)` and `echo "$NAME"` never appear in a tool fence. Tooling errors fail loudly: a non-zero exit with no helper status is retried at most twice (5 s, 30 s) and then ends **not assessed** for whatever the helper never recorded, quoting the command, exit code and stderr tail; never a quiet partial, never another review offered as the missing coverage.
- **Capy skill and docs** (`capy/skill/gstack/SKILL.md`, `docs/capy-install.md`): the session-start steps cover `--for none`, `floor=`, `behind=` as informational, the Node WARN and the in-place review-voice upgrade; run directories and scratch on Capy live under `~/.capy/work/<name>`, never `/tmp`, which Capy wipes when a machine sleeps.
- **Result codes** (`lib/result-codes-capy.ts`): `PROJECT_FLOOR_UNMET`, `PROJECT_NODE_PIN_UNMET` and `UPGRADE_SKIPPED`, each with a `docs/troubleshooting.md` anchor.
@@ -20,7 +22,8 @@
### Tests
- `test/gstack-doctor.test.ts`: the `floor` row's four outcomes (absent file, null key, met, unmet with the wrapper fix and without it) and the Node-only WARN beside a Bun-plus-Node FAIL; every required-row list gains `floor`.
- `test/gstack-capy-install.test.ts`: a skipped session-start upgrade is an ok trailer with the `upgrade skipped` note and a `WARN behind` row (replacing the `fail behind` pin); a `WARN floor` row becomes the `floor=` and `fix:` notes, with one `fix:` when `behind=` is also present; `--for none` with and without a checkout; a review-voice verdict upgraded in place by `--for autoplan` (setup runs, the verdict becomes the workflow's).
- `test/capy-skill-wrapper.test.ts`: `--for none` through the bootstrapper (checkout present, install mode, no checkout), wrapper 3 in the handover.
- `test/capy-skill-wrapper.test.ts`: `--for none` through the bootstrapper (checkout present, install mode, no checkout), wrapper 3 in the handover; a wrapper-2 handover of `--for none` in install mode leaves HEAD where it was.
- `test/gstack-doctor.test.ts`: a direct run with every Drive copy a bootstrapper below the minimum is a WARN with an ok trailer, and a pre-bootstrapper copy beside it keeps the FAIL. `test/gstack-capy-install.test.ts`: an unknown skip code collapses to `UPGRADE_SKIPPED` with its anchor; the in-place upgrade message is silent for a same-profile rerun, `--check` and a second review-voice install.
- `test/helpers/carve-guards.ts`: the /cso skeleton pin moves to 18,900 (measured 18,843) for the two startup paragraphs; `test/fixtures/context-budget.json` pins `capy/skill/gstack` and `cso` at their new eager sizes.
### Gate integrity
+5 -2
View File
@@ -35,8 +35,10 @@
# "; " (the note separator): after a refused self-upgrade it is bin/gstack-upgrade,
# which prints its own refusal and fix. A skipped self-upgrade (refused, busy,
# or gstack-upgrade missing) also exports GSTACK_CAPY_BEHIND_BLOCKED=<reason; fix: …
# (CODE)>, and the installer turns an ok trailer into `gstack: fail behind`: a
# session that meant to upgrade and could not is never certified ok.
# (CODE)>, and the installer adds a `WARN behind` row with that reason and code:
# the trailer stays ok (the installed revision is above the supported minimum),
# and the note names the fix. A `--for none` session certifies nothing and moves
# nothing, so it never self-upgrades (it behaves like --check).
#
# Usage: gstack-capy-session [--check] --root <checkout> [gstack-capy-install arguments]
# --check certify only (bin/gstack-capy-install --check: the doctor with the
@@ -53,6 +55,7 @@ for a in "$@"; do
[ "$a" = --check ] && CHECK=1
prev="$a"
done
[ "$SKILL" = none ] && CHECK=1
INSTALLER="$ROOT/bin/gstack-capy-install"
if [ ! -x "$INSTALLER" ]; then
echo "gstack: fail installer — $INSTALLER is missing; fix: bash ${GSTACK_CAPY_WRAPPER_PATH:-<skill>/scripts/install.sh} (re-clones or fast-forwards)"
+7 -1
View File
@@ -527,7 +527,7 @@ _dc_wrapper_rev() {
# ~/.capy/drive/*/skills/gstack/scripts/install.sh, failing only when each is
# stale. The workaround runs the checkout's own copy, which certifies.
_dc_check_wrapper() {
local min copy rev stale="" current="" active="${GSTACK_CAPY_WRAPPER_PATH:-}" workaround owner
local min copy rev stale="" current="" pre="" active="${GSTACK_CAPY_WRAPPER_PATH:-}" workaround owner
min=$(sed -n 's/^GSTACK_CAPY_MIN_WRAPPER=\([0-9][0-9]*\)[[:space:]]*$/\1/p' "$SRC/capy/support.env" 2>/dev/null | head -1); min="${min:-1}"
workaround="bash $SRC/capy/skill/gstack/scripts/install.sh --check${CHECK_FOR:+ --for $CHECK_FOR} (this session)"
owner="owner action: copy $SRC/capy/skill/gstack/ over the Drive skill folder, or have Capy re-sync it; this recurs every session until the Drive copy changes"
@@ -536,10 +536,12 @@ _dc_check_wrapper() {
[ -n "${GSTACK_CAPY_WRAPPER_REV:-}" ] && [ "$copy" = "$active" ] && continue
[ -z "${GSTACK_CAPY_WRAPPER_REV:-}" ] && [ -n "$active" ] && [ -f "$active" ] && [ "$copy" != "$active" ] && continue
rev=$(_dc_wrapper_rev "$copy")
[ "$rev" -eq 0 ] 2>/dev/null && pre=1
if [ "$rev" -lt "$min" ] 2>/dev/null; then stale="${stale:+$stale, }$copy (wrapper $rev)"; else current="${current:+$current, }$copy (wrapper $rev)"; fi
done
if [ -z "${GSTACK_CAPY_WRAPPER_REV:-}" ] && [ -n "$active" ] && [ -f "$active" ] && [ -z "$stale$current" ]; then
rev=$(_dc_wrapper_rev "$active")
[ "$rev" -eq 0 ] 2>/dev/null && pre=1
if [ "$rev" -lt "$min" ] 2>/dev/null; then stale="$active (wrapper $rev)"; else current="$active (wrapper $rev)"; fi
fi
if [ -n "${GSTACK_CAPY_WRAPPER_REV:-}" ]; then
@@ -552,6 +554,10 @@ _dc_check_wrapper() {
fi
elif [ -n "$current" ]; then
_dc_row PASS wrapper "Drive copy $current (minimum $min)${stale:+; stale copies: $stale}"
elif [ -n "$stale" ] && [ -z "$pre" ]; then
# Every copy is a bootstrapper below the minimum: it still works (a direct
# run has no provenance to say so), so the row is the owner action, not a FAIL.
_dc_row WARN wrapper "every Capy skill copy is below the minimum wrapper $min (it still works): $stale" "$owner" CAPY_WRAPPER_STALE
elif [ -n "$stale" ]; then
_dc_row FAIL wrapper "every Capy skill copy is stale (minimum wrapper $min): $stale" "$workaround; owner: ${owner#owner action: }" CAPY_WRAPPER_STALE
else
+1 -1
View File
@@ -9,7 +9,7 @@ description: Install and use gstack in Capy and Claude Code. Load at session sta
Resolve `scripts/install.sh` relative to this SKILL.md, not the working repository. It is a frozen bootstrapper: it prints `gstack-capy-skill: wrapper <n> (<path>)` first, learns the supported minimum from gstack's remote, and hands over to the checkout's own wrapper (`bin/gstack-capy-session`).
1. At session start run `bash scripts/install.sh --check`, adding `--for <skill>` when you know the workflow (`--for autoplan`, `--for review`, `--for qa`). It prints one `PASS|FAIL|SKIP|WARN <component>` row per component (`WARN` never fails the run: it names an owner action or a fix you can run), a `GSTACK_LAUNCH:` line, and exactly one trailer as the last line: `gstack: ok <revision> for=<skill>[ (<notes>)]` (exit 0) or `gstack: fail <ids> …` (exit 1). Read the exit status and grep `^gstack: ` directly; never pipe the check through `tail`, `head` or `grep` alone. `--check` never moves the checkout. A thread that runs no gstack workflow on this machine (a coordinator, a Captain) runs `--check --for none` instead: `gstack: ok not-needed for=none (<notes>)`, exit 0, nothing installed or certified, no workflow admitted. Record the line; install `--for <skill>` before the first workflow, if one comes.
1. At session start run `bash scripts/install.sh --check`, adding `--for <skill>` when you know the workflow (`--for autoplan`, `--for review`, `--for qa`). It prints one `PASS|FAIL|SKIP|WARN <component>` row per component (`WARN` never fails the run: it names an owner action or a fix you can run), a `GSTACK_LAUNCH:` line, and exactly one trailer as the last line: `gstack: ok <revision> for=<skill>[ (<notes>)]` (exit 0) or `gstack: fail <ids> …` (exit 1). Read the exit status and grep `^gstack: ` directly; never pipe the check through `tail`, `head` or `grep` alone. `--check` never moves the checkout. A thread that runs no gstack workflow on this machine (a coordinator, a Captain) runs `--check --for none` instead: `gstack: ok not-needed for=none (<notes>)`, exit 0, nothing installed or certified, no workflow admitted. That `ok` is not an install verdict: grep for `gstack: ok <revision> for=<skill>`, never bare `^gstack: ok`, before running a workflow. Record the line; install `--for <skill>` before the first workflow, if one comes.
2. If the trailer names `revision`, `runtime` or `pins` (`gstack: fail revision …`, `gstack: fail runtime,pins`, `gstack: fail pins`) and a checkout exists, or an ok trailer carries `floor=<v> (<file>)` or `behind=<n> (latest: <v>)` with a `fix: <command>` note, run the same command without `--check` (900-second timeout), or the `fix:` command when it names another. `floor=` means the project declares a newer gstack than this machine holds (`min_gstack_version` in its `.gstack/ship-policy.json`; the `WARN floor` row): run the fix before the first workflow, every time. `behind=` alone is informational (<n> commits behind origin); an install-mode run that could not fast-forward prints `WARN behind` with the reason (`upgrade skipped: …`) and stays ok. Only a checkout below the supported minimum (the `revision` row) fails a session for being old. Report the trailer verbatim, `behind=` and `floor=` included. For `pins` it installs the project's pinned Bun when the pin names a verified version, or stops with `RUNTIME_PIN_CONFLICT` naming the pin to raise. Any clean, unpinned checkout behind origin is fast-forwarded in install mode (below the `GSTACK_MIN_REVISION` floor by the bootstrapper, `gstack-capy-skill: upgraded <old> -> <new>`; above it by the checkout, `SELF_UPGRADE: done <old> -> <new>`), reinstalled and rechecked in one locked step. A pinned (`--revision`, detached), dirty or diverged checkout, or one another live session holds a lease on (`UPGRADE_BUSY`), is never moved: the trailer names the cause and the exact commands (above the floor it stays ok with `WARN behind` after `SELF_UPGRADE: skipped (<reason>)`, carrying `UPGRADE_BUSY` or `UPGRADE_REFUSED`). A lease whose pid was reused after a wake is reclaimed on its own. `GSTACK_CAPY_AUTO_UPGRADE=0` disables every fast-forward, `GSTACK_CAPY_SELF_UPGRADE=off` only the above-floor one.
3. If the trailer is `gstack: fail not-installed …` and this session runs no gstack workflow, record that line and continue; nothing is installed. Install right before the first workflow with `bash scripts/install.sh --for <skill>` (about 30 s cold: clone, a SHA-256-verified Bun when none installed fits, both host profiles without Chromium, and a `~/.local/bin/bun` shim so fresh shells get the recorded Bun).
4. Any other `FAIL` row carries a `fix:` command and a reason code with a troubleshooting anchor; run the fix, then `--check` again. `SKIP codex-cli (optional; …)`, `SKIP browser (lazy; …)`, `SKIP pins (planning-only; …)`, `SKIP pins (helper runtime; …)` and `SKIP floor (…)` are not failures. `WARN pins … (node pin only; bun pins pass)` is not one either: the Node on PATH misses the project's pin while Bun is right; run its fix only when this machine will run the project's tests.
+4 -2
View File
@@ -506,8 +506,10 @@ floor is not what keeps machines current). It never exceeds VERSION
changes in a way older copies cannot work with; older Drive copies keep
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).
doctor's `wrapper` row warns (a bootstrapper copy below the minimum is a WARN,
active or found in the Drive by a direct run, never a FAIL; only a
pre-bootstrapper copy with no `GSTACK_CAPY_WRAPPER=` line fails;
`bin/gstack-upgrade` never leaves a pending reinstall for it).
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
+10 -5
View File
@@ -1574,8 +1574,10 @@ every lease with its age and `alive|dead|stale`; a holder is named as
When you know they are idle, `GSTACK_UPGRADE_FORCE=1 bin/gstack-upgrade`
proceeds. At a Capy session start the same refusal appears as
`SELF_UPGRADE: skipped (live gstack sessions hold this installation: ...)`,
and the trailer is `gstack: fail behind — … (UPGRADE_BUSY)`: a session that
could not upgrade is never certified ok.
the doctor adds `WARN behind — … (UPGRADE_BUSY)` and the trailer stays
`gstack: ok … (behind=<n> (latest: <v>); upgrade skipped: … (UPGRADE_BUSY); fix: …)`:
the installed revision is above the supported minimum, so the session is
certified and the note names the fix.
<a id="upgrade-fetch-failed"></a>
### `gstack-upgrade: could not reach origin (...) (UPGRADE_FETCH_FAILED)`
@@ -1618,9 +1620,12 @@ action that never fails the run. Without provenance (a direct
`gstack-doctor --check`, or a pre-bootstrapper copy with no
`GSTACK_CAPY_WRAPPER=` line, which reads as wrapper 0) it reads the Drive
copies (`~/.capy/drive/*/skills/gstack/scripts/install.sh`, or only
`GSTACK_CAPY_WRAPPER_PATH` when set) and fails only when every copy is stale,
listing each with its revision. A stale active wrapper is a `WARN` too: an
older copy still works (`capy/support.env`). `bin/gstack-upgrade` runs the
`GSTACK_CAPY_WRAPPER_PATH` when set), listing each with its revision: every
copy a bootstrapper below the minimum is `WARN wrapper — every Capy skill copy
is below the minimum wrapper <m> (it still works): ...` with the owner action,
and it fails only when a copy is pre-bootstrapper (wrapper 0), which cannot
hand over at all. A stale active wrapper is a `WARN` too: an older copy still
works (`capy/support.env`). `bin/gstack-upgrade` runs the
installer with the checkout's own copy as the active wrapper and never leaves
a pending reinstall when the wrapper row is the only failure.
+2 -1
View File
@@ -41,6 +41,7 @@
*/
import { RESULT_CODES } from './result-codes';
import { SHIP_POLICY_PATH } from './ship-policy';
export type ComponentKind = 'core' | 'host' | 'component' | 'optional';
@@ -100,7 +101,7 @@ export const NOT_NEEDED_PROFILE = 'none';
export const NOT_NEEDED_REVISION = 'not-needed';
/** The project file and key the `floor` row reads (the same key gstack-ship-queue validates). */
export const PROJECT_FLOOR_FILE = '.gstack/ship-policy.json';
export const PROJECT_FLOOR_FILE = SHIP_POLICY_PATH;
export const PROJECT_FLOOR_FIELD = 'min_gstack_version';
/** The read-only helpers the `helpers` row launches (`<bin> --help` loads each module graph with the launch Bun). */
+12
View File
@@ -393,6 +393,18 @@ describe('capy skill: the session hands the installer GSTACK_CAPY_BEHIND', () =>
}
});
test('an older bootstrapper handing --for none to the session in install mode: the session certifies nothing and moves nothing (no self-upgrade, HEAD unchanged), and reports behind like --check', () => {
const f = fixture({ cloneAt: 'B' });
write(path.join(f.root, 'bin/gstack-capy-install'), ENV_INSTALLER, 0o755);
f.git(['update-index', '--assume-unchanged', 'bin/gstack-capy-install']);
f.git(['fetch', '-q', 'origin', 'main']);
const r = spawnSync('bash', [path.join(f.root, 'bin/gstack-capy-session'), '--root', f.root, '--for', 'none'], { cwd: f.host.home, encoding: 'utf8', env: { ...f.host.env, GSTACK_CAPY_REMOTE_MIN: '1.0.1', GSTACK_CAPY_WRAPPER_REV: '2', GSTACK_CAPY_WRAPPER_PATH: BOOT }, timeout: 120_000 });
expect(r.status, r.stdout + r.stderr).toBe(0);
expect(r.stdout).not.toMatch(/^SELF_UPGRADE:/m);
expect(r.stdout.trim().split('\n').at(-1)).toBe(`gstack: ok 1.0.1 for=autoplan behind=1 to=1.0.2 fix=bash ${BOOT} --for none`);
expect(f.git(['rev-parse', 'HEAD'])).toBe(f.b);
});
test('current, or the fetch failed: nothing is exported, and values inherited from a parent session are dropped', () => {
const f = fixture({ cloneAt: 'B' });
f.git(['fetch', '-q', 'origin', 'main']);
+10
View File
@@ -293,6 +293,11 @@ describe('gstack-capy-install', () => {
const bare = run(f, [], { ...busy, GSTACK_CAPY_BEHIND_BLOCKED: 'gstack-upgrade exit 9' });
expect(bare.status).toBe(0);
expect(lastLine(bare.stdout)).toBe(`gstack: ok 1.2.3 for=autoplan (behind=3 (latest: 1.2.6); upgrade skipped: gstack-upgrade exit 9 (UPGRADE_REFUSED); fix: ${f.root}/bin/gstack-upgrade)`);
// A code the installer does not know collapses to UPGRADE_SKIPPED, whose anchor exists; the reason and fix are kept.
const other = run(f, [], { ...busy, GSTACK_CAPY_BEHIND_BLOCKED: 'origin unreachable; fix: retry when online (UPGRADE_FETCH_FAILED)' });
expect(other.status).toBe(0);
expect(lastLine(other.stdout)).toBe('gstack: ok 1.2.3 for=autoplan (behind=3 (latest: 1.2.6); upgrade skipped: origin unreachable (UPGRADE_SKIPPED); fix: retry when online)');
expect(other.stdout).toMatch(/^WARN behind — .*; fix: retry when online \(UPGRADE_SKIPPED; .*#upgrade-skipped\)$/m);
expect(lastLine(run(f, [], { ...busy, STUB_DOCTOR_FAIL: '1' }).stdout)).toBe('gstack: fail codex');
});
@@ -338,6 +343,11 @@ describe('gstack-capy-install', () => {
const verdict = full.stdout.match(/GSTACK_INSTALL_VERDICT=(\S+)/)![1]!;
expect(JSON.parse(fs.readFileSync(verdict, 'utf8'))).toMatchObject({ for: 'autoplan', ok: true });
expect(lastLine(full.stdout)).toBe('gstack: ok 1.2.3 for=autoplan');
// The message is for the review-voice -> workflow step only: a rerun of the same profile, a --check and a second review-voice install stay silent.
const upgrading = /upgrading the review-voice install in place/;
expect(run(f, ['--for', 'autoplan', '--project', project]).stdout).not.toMatch(upgrading);
expect(run(f, ['--check', '--for', 'autoplan', '--project', project]).stdout).not.toMatch(upgrading);
expect(run(f, ['--for', 'review-voice', '--project', project]).stdout).not.toMatch(upgrading);
});
test('--root clones when the directory is absent and refuses a non-gstack directory', () => {
+16
View File
@@ -885,6 +885,22 @@ describe('gstack-doctor --check: wrapper and runtime drift', () => {
expect(fresh.status, fresh.out).toBe(0);
});
posixPathTest('direct doctor run: a bootstrapper Drive copy below the minimum still works, so it is an owner-action WARN with an ok trailer (only a pre-bootstrapper copy fails)', () => {
const f = makeFixture();
withSupport(f, Number(currentRev) + 1);
const old = drive(f, 'user-garry-tan', current);
const r = parse(f, []);
expect(r.status, r.out).toBe(0);
expect(r.trailer).toMatch(/^gstack: ok /);
expect(r.line('wrapper')).toBe(`WARN wrapper — every Capy skill copy is below the minimum wrapper ${Number(currentRev) + 1} (it still works): ${old} (wrapper ${currentRev}); owner action: copy ${f.root}/capy/skill/gstack/ over the Drive skill folder, or have Capy re-sync it; this recurs every session until the Drive copy changes (CAPY_WRAPPER_STALE; https://github.com/garrytan/gstack/blob/main/docs/troubleshooting.md#capy-wrapper-stale)`);
const json = JSON.parse(doctor(f, ['--check', '--json']).out);
expect(json.ok).toBe(true);
expect(json.rows.find((x: { id: string }) => x.id === 'wrapper')).toMatchObject({ status: 'WARN', code: 'CAPY_WRAPPER_STALE' });
// A pre-bootstrapper copy beside it keeps the FAIL: that copy cannot hand over at all.
drive(f, 'org', PRE_BOOTSTRAPPER);
expect(parse(f, []).trailer).toBe('gstack: fail wrapper');
});
posixPathTest('two Drive copies, one current: PASS naming the stale one; GSTACK_CAPY_WRAPPER_PATH alone decides when set without a revision', () => {
const f = makeFixture();
withSupport(f);