mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 14:06:50 +01:00
fix(capy): a --for review-voice install never self-upgrades the shared checkout (GST-290) (#3667)
Also: the Captain digest drops an archived thread from In progress in the same turn (GST-371). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
b6cb70aea1
commit
99706a47bb
13 files changed
+110
-16
No files matched your search
@@ -1,5 +1,16 @@
|
||||
# Changelog
|
||||
|
||||
## [1.91.356.0] - 2026-10-31
|
||||
|
||||
**A reviewer's install no longer upgrades the gstack checkout a lane shares.** On a Capy machine, an /autoplan reviewer runs `install.sh --for review-voice` against the same checkout as the lane that dispatched it. Once the lane's workflow lease was more than 120 minutes old, that install fast-forwarded the checkout and rewrote the install verdict as `for=review-voice`. The lane's next workflow was then refused with `INSTALL_UNVERIFIED`.
|
||||
|
||||
### Fixed
|
||||
- **`--for review-voice` never self-upgrades the checkout.** When origin is ahead, the session prints `SELF_UPGRADE: skipped (review-voice never self-upgrades a shared checkout; fix: <root>/bin/gstack-upgrade when no other session uses this checkout (UPGRADE_SKIPPED))`, leaves HEAD where it was, and the trailer stays ok with a `behind=` note. This holds with or without a lease, and `GSTACK_UPGRADE_FORCE=1` does not override it. If `bin/gstack-doctor-components.sh` cannot be read, the check still treats `review-voice` as a reviewer install. The bootstrapper still fast-forwards a checkout below the supported minimum.
|
||||
- **`--check --for review-voice` names `bin/gstack-upgrade` as the fix** instead of the install rerun, which would not move a reviewer checkout.
|
||||
|
||||
### Changed
|
||||
- **The Captain's digest drops an archived thread from In progress in the same turn** (docs/captain.md, the DIGEST.md template and /captain).
|
||||
|
||||
## [1.91.355.0] - 2026-10-31
|
||||
|
||||
**A driven `/ship` on Capy no longer pays one agent turn per finished reviewer, and it can count its own calls.** Replays of gstack-evals #34 and #36 (GSTA-167) made 30 to about 92 parent calls against a target of 12. In the 9-reviewer replay, 23 of 25 waits were the parent waking once per finished reviewer, because the driver told it to end its turn on `wait` while reviewers ran. What reviewers see, which reviewers run, their depth and the blocking floor are unchanged.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# gstack digest v1.91.355.0 — regenerate/re-copy after upgrading gstack
|
||||
# gstack digest v1.91.356.0 — regenerate/re-copy after upgrading gstack
|
||||
|
||||
Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed
|
||||
for agent hosts without a full skill install. The full skills add workflows,
|
||||
|
||||
+13
-2
@@ -29,7 +29,10 @@
|
||||
# inside a workflow (/ship's docs step) must not change gstack under it. The age
|
||||
# bound matters on Capy, where the leased harness pid is the machine's
|
||||
# long-lived capy-server and a live lease alone would never end the deferral;
|
||||
# each gstack-skill-start renews it (UPGRADE_BUSY; GSTACK_UPGRADE_FORCE=1 overrides). Lines: `SELF_UPGRADE: done <old> -> <new>` | `skipped (<reason>)`
|
||||
# each gstack-skill-start renews it (UPGRADE_BUSY; GSTACK_UPGRADE_FORCE=1 overrides).
|
||||
# An install profile (`--for review-voice`, a dispatched reviewer on a machine
|
||||
# whose checkout a lane shares) never self-upgrades, lease or not, and
|
||||
# GSTACK_UPGRADE_FORCE does not override it (UPGRADE_SKIPPED, GST-290). Lines: `SELF_UPGRADE: done <old> -> <new>` | `skipped (<reason>)`
|
||||
# | `none (current)` | `disabled (<the variable: GSTACK_CAPY_SELF_UPGRADE=off or
|
||||
# GSTACK_CAPY_AUTO_UPGRADE=0>)`.
|
||||
# --check never moves the checkout; behind origin it prints
|
||||
@@ -70,6 +73,8 @@ fi
|
||||
[ "${GSTACK_CAPY_REMOTE_MIN:-}" = unknown ] && echo "gstack-capy-session: remote minimum unknown (fetch failed); the revision row is local only"
|
||||
# shellcheck source=bin/gstack-install-verdict.sh
|
||||
. "$ROOT/bin/gstack-install-verdict.sh"
|
||||
# shellcheck source=bin/gstack-doctor-components.sh
|
||||
. "$ROOT/bin/gstack-doctor-components.sh" 2>/dev/null || gstack_doctor_install_profile() { [ "$1" = review-voice ]; }
|
||||
MIN_WRAPPER=$(gstack_support_env_get "$(gstack_source_root "$ROOT/bin")" GSTACK_CAPY_MIN_WRAPPER)
|
||||
if [ -n "$MIN_WRAPPER" ] && [ "${GSTACK_CAPY_WRAPPER_REV:-0}" -lt "$MIN_WRAPPER" ] 2>/dev/null; then
|
||||
echo "gstack-capy-skill: owner action — Drive copy is wrapper ${GSTACK_CAPY_WRAPPER_REV:-0}, minimum $MIN_WRAPPER; copy $ROOT/capy/skill/gstack/ (CAPY_WRAPPER_STALE)"
|
||||
@@ -108,7 +113,8 @@ if [ "$CHECK" -eq 1 ]; then
|
||||
if behind; then
|
||||
# The rerun fast-forwards only what gstack-upgrade would move; otherwise
|
||||
# gstack-upgrade itself names the refusal and its fix.
|
||||
if [ "${GSTACK_CAPY_SELF_UPGRADE:-}" = off ]; then report_behind "unset GSTACK_CAPY_SELF_UPGRADE, then $RERUN"
|
||||
if gstack_doctor_install_profile "$SKILL"; then report_behind "$ROOT/bin/gstack-upgrade"
|
||||
elif [ "${GSTACK_CAPY_SELF_UPGRADE:-}" = off ]; then report_behind "unset GSTACK_CAPY_SELF_UPGRADE, then $RERUN"
|
||||
elif [ "${GSTACK_CAPY_AUTO_UPGRADE:-1}" = 0 ]; then report_behind "unset GSTACK_CAPY_AUTO_UPGRADE, then $RERUN"
|
||||
elif [ "$(git -C "$ROOT" symbolic-ref -q --short HEAD 2>/dev/null)" != "$(sed -n "1s/.*branch '\([^']*\)' of .*/\1/p" "$(git -C "$ROOT" rev-parse --absolute-git-dir 2>/dev/null)/FETCH_HEAD" 2>/dev/null)" ] \
|
||||
|| [ -n "$(git -C "$ROOT" status --porcelain --untracked-files=no 2>/dev/null)" ] \
|
||||
@@ -123,6 +129,11 @@ elif [ "${GSTACK_CAPY_SELF_UPGRADED:-}" != 1 ]; then
|
||||
elif [ "${GSTACK_CAPY_AUTO_UPGRADE:-1}" = 0 ]; then disabled GSTACK_CAPY_AUTO_UPGRADE
|
||||
elif [ -z "${GSTACK_CAPY_REMOTE_MIN:-}" ] || [ "$GSTACK_CAPY_REMOTE_MIN" = unknown ]; then echo "SELF_UPGRADE: none (remote minimum unknown)"
|
||||
elif ! behind; then echo "SELF_UPGRADE: none (current)"
|
||||
elif gstack_doctor_install_profile "$SKILL"; then
|
||||
REASON="$SKILL never self-upgrades a shared checkout; fix: $ROOT/bin/gstack-upgrade when no other session uses this checkout (UPGRADE_SKIPPED)"
|
||||
echo "SELF_UPGRADE: skipped ($REASON)"
|
||||
report_behind "$ROOT/bin/gstack-upgrade"
|
||||
export GSTACK_CAPY_BEHIND_BLOCKED="$REASON"
|
||||
elif [ ! -x "$ROOT/bin/gstack-upgrade" ]; then echo "SELF_UPGRADE: skipped ($ROOT/bin/gstack-upgrade is missing)"; report_behind "$RERUN"
|
||||
export GSTACK_CAPY_BEHIND_BLOCKED="$ROOT/bin/gstack-upgrade is missing; fix: $RERUN (UPGRADE_REFUSED)"
|
||||
elif [ "${GSTACK_UPGRADE_FORCE:-}" != 1 ] && [ -x "$ROOT/bin/gstack-lease" ] \
|
||||
|
||||
+2
-1
@@ -329,7 +329,8 @@ turn, three sections in this order, each row with its thread, one line, and an a
|
||||
2. **New**: what landed or changed since the last digest (merges with versions, READYs
|
||||
received, trains built, incidents), newest first.
|
||||
3. **In progress**: every working lane, what it owns, its state (building, READY, in
|
||||
train slot i, blocked on …) and how long since it last reported.
|
||||
train slot i, blocked on …) and how long since it last reported. A thread the Captain
|
||||
archives leaves this list in the same turn.
|
||||
|
||||
Ages make stalls visible: a lane in progress with no report for hours, or a READY waiting
|
||||
longer than `FILL_HOURS`, is the controller's next action.
|
||||
|
||||
@@ -257,7 +257,8 @@ turn, three sections in this order, each row with its thread, one line, and an a
|
||||
2. **New**: what landed or changed since the last digest (merges with versions, READYs
|
||||
received, trains built, incidents), newest first.
|
||||
3. **In progress**: every working lane, what it owns, its state (building, READY, in
|
||||
train slot i, blocked on …) and how long since it last reported.
|
||||
train slot i, blocked on …) and how long since it last reported. A thread the Captain
|
||||
archives leaves this list in the same turn.
|
||||
|
||||
Ages make stalls visible: a lane in progress with no report for hours, or a READY waiting
|
||||
longer than `FILL_HOURS`, is the controller's next action.
|
||||
|
||||
@@ -27,5 +27,7 @@ ask format is gstack docs/captain.md, The ask format.
|
||||
|---|---|---|---|
|
||||
| <code> | <files or area> | <building / READY / train slot i / blocked on …> | <age> |
|
||||
|
||||
A thread the controller archives leaves this table in the same turn.
|
||||
|
||||
Stalls to act on: a lane with no report for longer than <N hours>, a READY older than
|
||||
`FILL_HOURS` with no train, a slot call unanswered for longer than <N minutes>.
|
||||
+2
-1
@@ -377,7 +377,8 @@ field").
|
||||
`DIGEST.md` is rewritten every controller turn: **Needs you** (decisions only the owner
|
||||
can make, in [the ask format](#the-ask-format); empty is the goal), **New** (what landed or
|
||||
changed, newest first), **In progress** (every lane, what it owns, its state, the age of
|
||||
its last report). Ages turn stalls into the controller's next action.
|
||||
its last report). Ages turn stalls into the controller's next action. When the controller
|
||||
archives a thread, it drops that thread from **In progress** in the same turn.
|
||||
|
||||
## The daily flaky pass
|
||||
|
||||
|
||||
+12
-1
@@ -47,7 +47,13 @@ upgrade: a reviewer later asked to run a workflow runs the same installer
|
||||
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
|
||||
cloned twice. A review-voice session never self-upgrades the checkout, because
|
||||
the lane on the same machine may be in the middle of a workflow on it: when
|
||||
origin is ahead it prints `SELF_UPGRADE: skipped (review-voice never
|
||||
self-upgrades a shared checkout; …)`, certifies the installed revision and
|
||||
stays ok with `behind=<n> … (UPGRADE_SKIPPED); fix: <root>/bin/gstack-upgrade`.
|
||||
The bootstrapper still fast-forwards a checkout below the supported minimum.
|
||||
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`.
|
||||
|
||||
@@ -101,6 +107,11 @@ there is refused with the usual `--for <skill>` fix.
|
||||
note, and a project that needs the newer gstack says so with a floor
|
||||
(next section).
|
||||
`SELF_UPGRADE: none (current)` when nothing is ahead;
|
||||
`--for review-voice` prints `SELF_UPGRADE: skipped (review-voice never
|
||||
self-upgrades a shared checkout; fix: <root>/bin/gstack-upgrade when no other
|
||||
session uses this checkout (UPGRADE_SKIPPED))` whenever origin is ahead,
|
||||
with or without a lease, and `GSTACK_UPGRADE_FORCE=1` does not change that
|
||||
([reviewer machines](#reviewer-machines---for-review-voice));
|
||||
`GSTACK_CAPY_SELF_UPGRADE=off` (or `GSTACK_CAPY_AUTO_UPGRADE=0`) prints
|
||||
`SELF_UPGRADE: disabled (<variable>)`. `--check` never moves the checkout;
|
||||
behind origin it prints `gstack-capy-session: behind origin/<branch>:
|
||||
|
||||
@@ -1525,7 +1525,8 @@ yourself; or change the pin in the project.
|
||||
**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
|
||||
lease (`UPGRADE_BUSY`), `bin/gstack-upgrade` is missing, or the session is a
|
||||
`--for review-voice` reviewer, which never moves a checkout a lane may share
|
||||
(`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
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gstack",
|
||||
"version": "1.91.355",
|
||||
"version": "1.91.356",
|
||||
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
||||
@@ -150,16 +150,19 @@ describe('--for review-voice through the no-Drive brief', () => {
|
||||
expect(warm.ms).toBeLessThan(WARM_BUDGET_MS);
|
||||
}, 300_000);
|
||||
|
||||
test('origin moves: the same brief fast-forwards the checkout before it certifies', () => {
|
||||
// GST-290: the reviewer's machine may share this checkout with a lane mid-workflow.
|
||||
test('origin moves: the same brief never moves the checkout; it certifies the installed revision and reports behind', () => {
|
||||
const head = git(['rev-parse', 'HEAD'], root);
|
||||
fs.writeFileSync(path.join(origin, 'REVIEW_VOICE_FIXTURE.md'), 'moved\n');
|
||||
git(['add', '-A'], origin);
|
||||
git(['commit', '-qm', 'origin moves'], origin);
|
||||
const tip = git(['rev-parse', 'HEAD'], origin);
|
||||
const moved = brief();
|
||||
expect(moved.status, moved.out + moved.stderr).toBe(0);
|
||||
expect(moved.out).toContain(`SELF_UPGRADE: done ${VERSION} -> ${VERSION}`);
|
||||
expect(git(['rev-parse', 'HEAD'], root)).toBe(tip);
|
||||
expect(trailer(moved.out)).toBe(OK_TRAILER);
|
||||
expect(moved.out).toContain(`SELF_UPGRADE: skipped (review-voice never self-upgrades a shared checkout; fix: ${root}/bin/gstack-upgrade when no other session uses this checkout (UPGRADE_SKIPPED))`);
|
||||
expect(moved.out).not.toContain('SELF_UPGRADE: done');
|
||||
expect(git(['rev-parse', 'HEAD'], root)).toBe(head);
|
||||
expect(trailer(moved.out)).toStartWith(`gstack: ok ${VERSION} for=review-voice (pins: skipped (helper runtime); behind=1 `);
|
||||
expect(trailer(moved.out)).toContain('(UPGRADE_SKIPPED)');
|
||||
}, 300_000);
|
||||
|
||||
test('a review-voice check never narrows an ok verdict a fuller install wrote for this HEAD and project', () => {
|
||||
|
||||
@@ -85,7 +85,7 @@ function fixture(opts: { minWrapper?: number; cloneAt?: 'A' | 'B' } = {}) {
|
||||
if ((opts.cloneAt ?? 'A') === 'A') git(['clone', '-q', '--single-branch', '--depth', '1', `file://${bare}`, root], host.base);
|
||||
write(path.join(seed, 'VERSION'), '1.0.1\n');
|
||||
write(path.join(seed, 'capy/support.env'), `GSTACK_MIN_REVISION=1.0.1\nGSTACK_CAPY_MIN_WRAPPER=${opts.minWrapper ?? 1}\n`);
|
||||
for (const rel of ['capy/skill/gstack/scripts/install.sh', 'capy/skill/gstack/SKILL.md', 'bin/gstack-capy-session', 'bin/gstack-install-verdict.sh', ...UPGRADE_DEPS]) {
|
||||
for (const rel of ['capy/skill/gstack/scripts/install.sh', 'capy/skill/gstack/SKILL.md', 'bin/gstack-capy-session', 'bin/gstack-install-verdict.sh', 'bin/gstack-doctor-components.sh', ...UPGRADE_DEPS]) {
|
||||
if (fs.existsSync(path.join(REPO, rel))) write(path.join(seed, rel), fs.readFileSync(path.join(REPO, rel), 'utf8'), 0o755);
|
||||
}
|
||||
write(path.join(seed, 'bin/gstack-capy-install'), STUB_INSTALLER, 0o755);
|
||||
@@ -420,6 +420,58 @@ describe('capy skill: the session hands the installer GSTACK_CAPY_BEHIND', () =>
|
||||
expect(f.git(['rev-parse', 'HEAD'])).toBe(f.c);
|
||||
});
|
||||
|
||||
// GST-290: a dispatched reviewer's install ran with no lease of its own, fast-forwarded the
|
||||
// shared checkout and rewrote the verdict to for=review-voice, so the lane's next autoplan
|
||||
// admission refused (INSTALL_UNVERIFIED). A review-voice install never moves the checkout.
|
||||
describe('--for review-voice never self-upgrades a shared checkout', () => {
|
||||
const blocked = (root: string) => `review-voice never self-upgrades a shared checkout; fix: ${root}/bin/gstack-upgrade when no other session uses this checkout (UPGRADE_SKIPPED)`;
|
||||
const behindFixture = () => {
|
||||
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']);
|
||||
return f;
|
||||
};
|
||||
|
||||
test('install mode behind origin with no lease: skipped with the reason, HEAD stays, and the installer gets behind, gstack-upgrade as the fix and the blocked reason', () => {
|
||||
const f = behindFixture();
|
||||
const r = f.run(['--for', 'review-voice']);
|
||||
expect(r.status, r.stdout + r.stderr).toBe(0);
|
||||
expect(r.stdout).toContain(`SELF_UPGRADE: skipped (${blocked(f.root)})\n`);
|
||||
expect(r.stdout).not.toContain('SELF_UPGRADE: done');
|
||||
expect(r.last).toBe(`gstack: ok 1.0.1 for=autoplan behind=1 to=1.0.2 fix=${f.root}/bin/gstack-upgrade blocked=${blocked(f.root)}`);
|
||||
expect(f.git(['rev-parse', 'HEAD'])).toBe(f.b);
|
||||
expect(f.run(['--for', 'review-voice'], { GSTACK_UPGRADE_FORCE: '1' }).stdout).toContain(`SELF_UPGRADE: skipped (${blocked(f.root)})\n`);
|
||||
expect(f.git(['rev-parse', 'HEAD'])).toBe(f.b);
|
||||
});
|
||||
|
||||
test('install mode behind origin with bin/gstack-doctor-components.sh missing: still skipped (the profile check fails closed for review-voice)', () => {
|
||||
const f = behindFixture();
|
||||
f.git(['update-index', '--assume-unchanged', 'bin/gstack-doctor-components.sh']);
|
||||
fs.rmSync(path.join(f.root, 'bin/gstack-doctor-components.sh'));
|
||||
const r = f.run(['--for', 'review-voice']);
|
||||
expect(r.stdout).toContain(`SELF_UPGRADE: skipped (${blocked(f.root)})\n`);
|
||||
expect(f.git(['rev-parse', 'HEAD'])).toBe(f.b);
|
||||
});
|
||||
|
||||
test('install mode on a current checkout: none (current), and no blocked reason', () => {
|
||||
const f = fixture({ cloneAt: 'B' });
|
||||
f.git(['fetch', '-q', 'origin', 'main']);
|
||||
f.git(['merge', '--ff-only', '-q', 'FETCH_HEAD']);
|
||||
write(path.join(f.root, 'bin/gstack-capy-install'), ENV_INSTALLER, 0o755);
|
||||
f.git(['update-index', '--assume-unchanged', 'bin/gstack-capy-install']);
|
||||
const r = f.run(['--for', 'review-voice']);
|
||||
expect(r.stdout).toMatch(/^SELF_UPGRADE: none \(current\)$/m);
|
||||
expect(r.last).toBe('gstack: ok 1.0.2 for=autoplan behind=none to=none fix=none');
|
||||
});
|
||||
|
||||
test('--check behind origin: the fix is gstack-upgrade, not the install rerun (which would not move a review-voice checkout)', () => {
|
||||
const f = behindFixture();
|
||||
const r = f.run(['--check', '--for', 'review-voice']);
|
||||
expect(r.last).toBe(`gstack: ok 1.0.1 for=autoplan behind=1 to=1.0.2 fix=${f.root}/bin/gstack-upgrade`);
|
||||
expect(f.git(['rev-parse', 'HEAD'])).toBe(f.b);
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
Reference in new issue
Block a user