From 2b4ba61578dc2d180383c307339ea80a4580e1e5 Mon Sep 17 00:00:00 2001 From: garrytan Date: Thu, 29 Oct 2026 01:17:06 +0000 Subject: [PATCH 1/5] test(capy-session): an install inside a leased run must not upgrade (#3571) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- test/capy-skill-wrapper.test.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test/capy-skill-wrapper.test.ts b/test/capy-skill-wrapper.test.ts index a2f9d0506..c5c3fc1ff 100644 --- a/test/capy-skill-wrapper.test.ts +++ b/test/capy-skill-wrapper.test.ts @@ -393,6 +393,25 @@ describe('capy skill: the session hands the installer GSTACK_CAPY_BEHIND', () => } }); + // #3571: /ship's docs step ran install.sh --for document-release from inside the run that + // holds the lease, the upgrader excluded its own ancestry, and the checkout moved mid-/ship. + test('install mode inside a run that holds the workflow lease: the self-upgrade is deferred, HEAD stays and the trailer stays ok; after the run the same install upgrades', () => { + 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']); + const leaseDir = path.join(f.host.base, 'leases'); + const lease = path.join(f.root, 'bin/gstack-lease'); + const inRun = spawnSync('bash', ['-c', 'bash "$1" touch --pid $$ --root "$2" || exit 9; bash "$3" --for document-release; echo "exit=$?"', 'run', lease, f.root, BOOT], { cwd: f.host.home, encoding: 'utf8', env: { ...f.host.env, GSTACK_CAPY_ROOT: f.root, GSTACK_CAPY_REPO: `file://${f.bare}`, WRAP_LOG: path.join(f.host.base, 'wrap.log'), GSTACK_LEASE_DIR: leaseDir }, timeout: 120_000 }); + expect(inRun.stdout, inRun.stderr).toMatch(/^SELF_UPGRADE: skipped \(this session's own gstack run holds a workflow lease on this installation: pid \d+ \(\d+m bash\), so the upgrade is deferred until the run ends; fix: .+bin\/gstack-upgrade \(UPGRADE_BUSY\)\)$/m); + expect(inRun.stdout).not.toContain('SELF_UPGRADE: done'); + expect(inRun.stdout).toMatch(/^exit=0$/m); + expect(inRun.stdout).toMatch(/blocked=this session's own gstack run holds a workflow lease .+ \(UPGRADE_BUSY\)$/m); + expect(f.git(['rev-parse', 'HEAD'])).toBe(f.b); + const after = f.run(['--for', 'document-release'], { GSTACK_LEASE_DIR: leaseDir }); + expect(after.stdout).toMatch(/^SELF_UPGRADE: done 1\.0\.1 -> 1\.0\.2$/m); + expect(f.git(['rev-parse', 'HEAD'])).toBe(f.c); + }); + 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); From 568a7f4a3d5909ac7fb200da51bea7b35ff607b7 Mon Sep 17 00:00:00 2001 From: garrytan Date: Thu, 29 Oct 2026 01:17:07 +0000 Subject: [PATCH 2/5] fix(capy-session): defer the self-upgrade while this session's own run holds the lease (#3571) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- bin/gstack-capy-session | 17 ++++++++++++++++- docs/troubleshooting.md | 11 +++++++++-- lib/result-codes.ts | 2 +- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/bin/gstack-capy-session b/bin/gstack-capy-session index 4296bdec8..c65ae4721 100755 --- a/bin/gstack-capy-session +++ b/bin/gstack-capy-session @@ -23,7 +23,11 @@ # applies: dirty, diverged, pinned, Bun hold, a live workflow lease) and on # success re-execs the NEW checkout's copy of this script with the same # arguments (GSTACK_CAPY_SELF_UPGRADED=1 stops a second attempt) before the -# installer runs. Lines: `SELF_UPGRADE: done -> ` | `skipped ()` +# installer runs. A live lease held by this session's own process chain also +# skips it (#3571): gstack-upgrade never counts the caller's own session, but an +# install run from inside a workflow (/ship's docs step) must not change gstack +# under that workflow, so the upgrade is deferred to a run with no live lease +# of its own (UPGRADE_BUSY; GSTACK_UPGRADE_FORCE=1 overrides). Lines: `SELF_UPGRADE: done -> ` | `skipped ()` # | `none (current)` | `disabled ()`. # --check never moves the checkout; behind origin it prints @@ -107,6 +111,17 @@ elif [ "${GSTACK_CAPY_SELF_UPGRADED:-}" != 1 ]; then elif ! behind; then echo "SELF_UPGRADE: none (current)" 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" ] \ + && OWN=$("$ROOT/bin/gstack-lease" live --root "$ROOT" 2>/dev/null) \ + && ! "$ROOT/bin/gstack-lease" live --root "$ROOT" --exclude-ancestry-of $$ >/dev/null 2>&1; then + HOLDERS="" + while read -r _pid _age; do HOLDERS="$HOLDERS${HOLDERS:+, }pid $_pid ($_age)"; done </dev/null) UP_OUT=$("$ROOT/bin/gstack-upgrade" --ff-only-no-reinstall 2>&1); UP_RC=$? diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 1ffa23542..3c5ce795e 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -1566,11 +1566,18 @@ process has exited never blocks, and neither does a stale one: each lease records its process's start identity, so a pid reused after a reboot or a machine wake (GBRA-105's lease named pid 379, which had become dbus-daemon) is reclaimed instead of blocking. The upgrader's own session (the pid chain it -runs under) never counts. +runs under) never counts for `bin/gstack-upgrade` run by hand. A Capy install +in install mode (`bin/gstack-capy-session`) counts it, though: an install run from inside a +workflow that holds the lease (/ship's docs step, #3571) prints +`SELF_UPGRADE: skipped (this session's own gstack run holds a workflow lease on +this installation: pid

(m ), so the upgrade is deferred until +the run ends; fix: /bin/gstack-upgrade (UPGRADE_BUSY))` and leaves the +checkout where the run started. **Fix.** Wait for those sessions to finish (`bin/gstack-lease list` shows every lease with its age and `alive|dead|stale`; a holder is named as -`pid

(m )`), then re-run `bin/gstack-upgrade`. +`pid

(m )`), then re-run `bin/gstack-upgrade`. For the +session's own run, run the printed `bin/gstack-upgrade` after the run ends. 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: ...)`, diff --git a/lib/result-codes.ts b/lib/result-codes.ts index d4e1cf5b4..46ca94bd7 100644 --- a/lib/result-codes.ts +++ b/lib/result-codes.ts @@ -597,7 +597,7 @@ export const RESULT_CODES = { }, UPGRADE_BUSY: { anchor: 'upgrade-busy', - summary: 'bin/gstack-upgrade changed nothing: another live session holds a workflow lease on this installation (bin/gstack-lease live), so moving the checkout would change gstack under it', + summary: 'bin/gstack-upgrade changed nothing: another live session holds a workflow lease on this installation (bin/gstack-lease live), or, at a Capy install, the caller\'s own run does, so moving the checkout would change gstack under it', fix: 'wait for those sessions to finish (bin/gstack-lease list names each pid and age), then re-run bin/gstack-upgrade; GSTACK_UPGRADE_FORCE=1 overrides when you know they are idle', }, UPGRADE_FETCH_FAILED: { From f3a130c760f8b4539f131c35aabb6ce80eaf394b Mon Sep 17 00:00:00 2001 From: garrytan Date: Thu, 29 Oct 2026 01:17:07 +0000 Subject: [PATCH 3/5] docs(changelog): an install inside /ship no longer upgrades gstack under it (#3571) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08a99c739..39ee47d14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [Unreleased] + +**An install check run from inside /ship no longer upgrades gstack under it.** /ship's docs step runs `scripts/install.sh --for document-release`. That run fast-forwarded the checkout from 1.91.298 to 1.91.299 in the middle of a lane's /ship, so the receipt writer's rules changed partway through the run (#3571). + +### Fixed +- **`bin/gstack-capy-session`**: in install mode, a live workflow lease held by the caller's own process chain now skips the self-upgrade: `SELF_UPGRADE: skipped (this session's own gstack run holds a workflow lease on this installation: pid

(m ), so the upgrade is deferred until the run ends; …(UPGRADE_BUSY))`. The install still certifies the installed revision and the trailer stays ok with `behind=`. `bin/gstack-upgrade` deliberately ignores the caller's own lease, and an install started from inside the run passed that check. A run with no live lease of its own upgrades as before, and `GSTACK_UPGRADE_FORCE=1` still overrides. + +### Tests +- `test/capy-skill-wrapper.test.ts`: an install run under a shell that holds the lease leaves HEAD where it was and stays ok. The same install after that shell exits upgrades. Before this change the first run printed `SELF_UPGRADE: done 1.0.1 -> 1.0.2`. + ## [1.91.301.0] - 2026-10-29 **Four paid gate cases that went red on 2026-10-28 without a product defect now pass on the runs that failed them.** Each repair replays the failing run as a free test that fails without it. From 86dc45156b40c236e807f0be5671a76a95ac4f4c Mon Sep 17 00:00:00 2001 From: garrytan Date: Thu, 29 Oct 2026 01:25:24 +0000 Subject: [PATCH 4/5] fix(capy-session): bound the own-run deferral by the lease's age so Capy machines still upgrade (#3571) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- CHANGELOG.md | 4 ++-- bin/gstack-capy-session | 17 ++++++++++------- docs/capy-install.md | 11 ++++++++++- docs/troubleshooting.md | 13 +++++++------ test/capy-skill-wrapper.test.ts | 27 +++++++++++++++++---------- 5 files changed, 46 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39ee47d14..c366d528c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,10 +5,10 @@ **An install check run from inside /ship no longer upgrades gstack under it.** /ship's docs step runs `scripts/install.sh --for document-release`. That run fast-forwarded the checkout from 1.91.298 to 1.91.299 in the middle of a lane's /ship, so the receipt writer's rules changed partway through the run (#3571). ### Fixed -- **`bin/gstack-capy-session`**: in install mode, a live workflow lease held by the caller's own process chain now skips the self-upgrade: `SELF_UPGRADE: skipped (this session's own gstack run holds a workflow lease on this installation: pid

(m ), so the upgrade is deferred until the run ends; …(UPGRADE_BUSY))`. The install still certifies the installed revision and the trailer stays ok with `behind=`. `bin/gstack-upgrade` deliberately ignores the caller's own lease, and an install started from inside the run passed that check. A run with no live lease of its own upgrades as before, and `GSTACK_UPGRADE_FORCE=1` still overrides. +- **`bin/gstack-capy-session`**: in install mode, a live workflow lease under 120 minutes old held by the caller's own process chain now skips the self-upgrade: `SELF_UPGRADE: skipped (a gstack run in this session holds a workflow lease on this installation: pid

(m ), so the upgrade waits until the lease is 120 minutes old; …(UPGRADE_BUSY))`. The install still certifies the installed revision and the trailer stays ok with `behind=`. `bin/gstack-upgrade` deliberately ignores the caller's own lease, and an install started from inside the run passed that check. Each `gstack-skill-start` renews the lease. The age bound matters on Capy: there the leased pid is the machine's long-lived `capy-server`, so a lease is never released by its process exiting. An older lease, or none, upgrades as before, and `GSTACK_UPGRADE_FORCE=1` or a hand-run `bin/gstack-upgrade` still moves the checkout. ### Tests -- `test/capy-skill-wrapper.test.ts`: an install run under a shell that holds the lease leaves HEAD where it was and stays ok. The same install after that shell exits upgrades. Before this change the first run printed `SELF_UPGRADE: done 1.0.1 -> 1.0.2`. +- `test/capy-skill-wrapper.test.ts`: an install run under a shell that holds a fresh lease leaves HEAD where it was and stays ok. Once that lease is 121 minutes old, with its holder still alive, the same install upgrades. Before this change the first run printed `SELF_UPGRADE: done 1.0.1 -> 1.0.2`. ## [1.91.301.0] - 2026-10-29 diff --git a/bin/gstack-capy-session b/bin/gstack-capy-session index c65ae4721..3df25b12e 100755 --- a/bin/gstack-capy-session +++ b/bin/gstack-capy-session @@ -23,11 +23,13 @@ # applies: dirty, diverged, pinned, Bun hold, a live workflow lease) and on # success re-execs the NEW checkout's copy of this script with the same # arguments (GSTACK_CAPY_SELF_UPGRADED=1 stops a second attempt) before the -# installer runs. A live lease held by this session's own process chain also -# skips it (#3571): gstack-upgrade never counts the caller's own session, but an -# install run from inside a workflow (/ship's docs step) must not change gstack -# under that workflow, so the upgrade is deferred to a run with no live lease -# of its own (UPGRADE_BUSY; GSTACK_UPGRADE_FORCE=1 overrides). Lines: `SELF_UPGRADE: done -> ` | `skipped ()` +# installer runs. A live lease under 120 minutes old (the lease prune window) +# held by this session's own process chain also skips it (#3571): +# gstack-upgrade never counts the caller's own session, but an install run from +# 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 -> ` | `skipped ()` # | `none (current)` | `disabled ()`. # --check never moves the checkout; behind origin it prints @@ -112,13 +114,14 @@ elif [ "${GSTACK_CAPY_SELF_UPGRADED:-}" != 1 ]; then 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" ] \ - && OWN=$("$ROOT/bin/gstack-lease" live --root "$ROOT" 2>/dev/null) \ + && OWN=$("$ROOT/bin/gstack-lease" live --root "$ROOT" 2>/dev/null | awk '{ a = $2; sub(/m$/, "", a); if (a + 0 < 120) print }') \ + && [ -n "$OWN" ] \ && ! "$ROOT/bin/gstack-lease" live --root "$ROOT" --exclude-ancestry-of $$ >/dev/null 2>&1; then HOLDERS="" while read -r _pid _age; do HOLDERS="$HOLDERS${HOLDERS:+, }pid $_pid ($_age)"; done < (m )`. The team-mode SessionStart hook +holder as `pid

(m )`. The install-mode self-upgrade also +defers while its own chain holds a live lease under 120 minutes old. An install +run from inside a workflow (/ship's docs step running `install.sh --for +document-release`, #3571) prints `SELF_UPGRADE: skipped (a gstack run in this +session holds a workflow lease on this installation: ... (UPGRADE_BUSY))`, +leaves the checkout where the run started and stays ok with `behind=`. Each +`gstack-skill-start` renews the lease. On Capy the leased pid is the machine's +long-lived `capy-server`, so the deferral ends when the lease turns 120 +minutes old, not when the process exits; `bin/gstack-upgrade` run by hand +moves the checkout sooner. The team-mode SessionStart hook (`bin/gstack-session-update`) honors the same leases: it logs `SKIP leased pid=

/m` to `analytics/session-update.log` and tries again at the next session start, without writing its hourly throttle stamp. A lease whose diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 3c5ce795e..7e5d610f3 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -1567,12 +1567,13 @@ records its process's start identity, so a pid reused after a reboot or a machine wake (GBRA-105's lease named pid 379, which had become dbus-daemon) is reclaimed instead of blocking. The upgrader's own session (the pid chain it runs under) never counts for `bin/gstack-upgrade` run by hand. A Capy install -in install mode (`bin/gstack-capy-session`) counts it, though: an install run from inside a -workflow that holds the lease (/ship's docs step, #3571) prints -`SELF_UPGRADE: skipped (this session's own gstack run holds a workflow lease on -this installation: pid

(m ), so the upgrade is deferred until -the run ends; fix: /bin/gstack-upgrade (UPGRADE_BUSY))` and leaves the -checkout where the run started. +in install mode (`bin/gstack-capy-session`) counts it while that lease is under +120 minutes old: an install run from inside a workflow that holds the lease +(/ship's docs step, #3571) prints `SELF_UPGRADE: skipped (a gstack run in this +session holds a workflow lease on this installation: pid

(m +), so the upgrade waits until the lease is 120 minutes old; fix: +/bin/gstack-upgrade once the run ends (UPGRADE_BUSY))` and leaves the +checkout where the run started. Each `gstack-skill-start` renews the lease. **Fix.** Wait for those sessions to finish (`bin/gstack-lease list` shows every lease with its age and `alive|dead|stale`; a holder is named as diff --git a/test/capy-skill-wrapper.test.ts b/test/capy-skill-wrapper.test.ts index c5c3fc1ff..9b041ba7d 100644 --- a/test/capy-skill-wrapper.test.ts +++ b/test/capy-skill-wrapper.test.ts @@ -395,20 +395,27 @@ describe('capy skill: the session hands the installer GSTACK_CAPY_BEHIND', () => // #3571: /ship's docs step ran install.sh --for document-release from inside the run that // holds the lease, the upgrader excluded its own ancestry, and the checkout moved mid-/ship. - test('install mode inside a run that holds the workflow lease: the self-upgrade is deferred, HEAD stays and the trailer stays ok; after the run the same install upgrades', () => { + // On Capy the leased pid is the long-lived capy-server, so the deferral ends by the lease's age. + test('install mode inside a run that holds a fresh workflow lease: the self-upgrade is deferred, HEAD stays and the trailer stays ok; once that lease is 120 minutes old the same install upgrades', () => { 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']); const leaseDir = path.join(f.host.base, 'leases'); - const lease = path.join(f.root, 'bin/gstack-lease'); - const inRun = spawnSync('bash', ['-c', 'bash "$1" touch --pid $$ --root "$2" || exit 9; bash "$3" --for document-release; echo "exit=$?"', 'run', lease, f.root, BOOT], { cwd: f.host.home, encoding: 'utf8', env: { ...f.host.env, GSTACK_CAPY_ROOT: f.root, GSTACK_CAPY_REPO: `file://${f.bare}`, WRAP_LOG: path.join(f.host.base, 'wrap.log'), GSTACK_LEASE_DIR: leaseDir }, timeout: 120_000 }); - expect(inRun.stdout, inRun.stderr).toMatch(/^SELF_UPGRADE: skipped \(this session's own gstack run holds a workflow lease on this installation: pid \d+ \(\d+m bash\), so the upgrade is deferred until the run ends; fix: .+bin\/gstack-upgrade \(UPGRADE_BUSY\)\)$/m); - expect(inRun.stdout).not.toContain('SELF_UPGRADE: done'); - expect(inRun.stdout).toMatch(/^exit=0$/m); - expect(inRun.stdout).toMatch(/blocked=this session's own gstack run holds a workflow lease .+ \(UPGRADE_BUSY\)$/m); - expect(f.git(['rev-parse', 'HEAD'])).toBe(f.b); - const after = f.run(['--for', 'document-release'], { GSTACK_LEASE_DIR: leaseDir }); - expect(after.stdout).toMatch(/^SELF_UPGRADE: done 1\.0\.1 -> 1\.0\.2$/m); + const script = [ + 'bash "$1" touch --pid $$ --root "$2" || exit 9', + 'bash "$3" --for document-release; echo "exit=$?"', + 'git -C "$2" rev-parse HEAD', + '"$5" -e "const t = Date.now() / 1000 - 121 * 60; require(\'fs\').utimesSync(process.argv[1], t, t)" "$4/$$"', + 'bash "$3" --for document-release; echo "exit=$?"', + ].join('\n'); + const r = spawnSync('bash', ['-c', script, 'run', path.join(f.root, 'bin/gstack-lease'), f.root, BOOT, leaseDir, process.execPath], { cwd: f.host.home, encoding: 'utf8', env: { ...f.host.env, GSTACK_CAPY_ROOT: f.root, GSTACK_CAPY_REPO: `file://${f.bare}`, WRAP_LOG: path.join(f.host.base, 'wrap.log'), GSTACK_LEASE_DIR: leaseDir }, timeout: 120_000 }); + const [inRun, aged] = r.stdout.split(/^exit=0$/m); + expect(r.stdout.match(/^exit=0$/gm), r.stdout + r.stderr).toHaveLength(2); + expect(inRun).toMatch(/^SELF_UPGRADE: skipped \(a gstack run in this session holds a workflow lease on this installation: pid \d+ \(0m bash\), so the upgrade waits until the lease is 120 minutes old; fix: .+bin\/gstack-upgrade once the run ends \(UPGRADE_BUSY\)\)$/m); + expect(inRun).not.toContain('SELF_UPGRADE: done'); + expect(inRun).toMatch(/blocked=a gstack run in this session holds a workflow lease .+ \(UPGRADE_BUSY\)$/m); + expect(aged).toContain(f.b); + expect(aged).toMatch(/^SELF_UPGRADE: done 1\.0\.1 -> 1\.0\.2$/m); expect(f.git(['rev-parse', 'HEAD'])).toBe(f.c); }); From 35be63ac1458f3583fc8f8a6cb1a9b5d690d33a7 Mon Sep 17 00:00:00 2001 From: garrytan Date: Thu, 29 Oct 2026 01:26:55 +0000 Subject: [PATCH 5/5] docs(changelog): prose lint, no mid-sentence colon (#3571) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c366d528c..6250f527e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ **An install check run from inside /ship no longer upgrades gstack under it.** /ship's docs step runs `scripts/install.sh --for document-release`. That run fast-forwarded the checkout from 1.91.298 to 1.91.299 in the middle of a lane's /ship, so the receipt writer's rules changed partway through the run (#3571). ### Fixed -- **`bin/gstack-capy-session`**: in install mode, a live workflow lease under 120 minutes old held by the caller's own process chain now skips the self-upgrade: `SELF_UPGRADE: skipped (a gstack run in this session holds a workflow lease on this installation: pid

(m ), so the upgrade waits until the lease is 120 minutes old; …(UPGRADE_BUSY))`. The install still certifies the installed revision and the trailer stays ok with `behind=`. `bin/gstack-upgrade` deliberately ignores the caller's own lease, and an install started from inside the run passed that check. Each `gstack-skill-start` renews the lease. The age bound matters on Capy: there the leased pid is the machine's long-lived `capy-server`, so a lease is never released by its process exiting. An older lease, or none, upgrades as before, and `GSTACK_UPGRADE_FORCE=1` or a hand-run `bin/gstack-upgrade` still moves the checkout. +- **`bin/gstack-capy-session`**: in install mode, a live workflow lease under 120 minutes old held by the caller's own process chain now skips the self-upgrade: `SELF_UPGRADE: skipped (a gstack run in this session holds a workflow lease on this installation: pid

(m ), so the upgrade waits until the lease is 120 minutes old; …(UPGRADE_BUSY))`. The install still certifies the installed revision and the trailer stays ok with `behind=`. `bin/gstack-upgrade` deliberately ignores the caller's own lease, and an install started from inside the run passed that check. Each `gstack-skill-start` renews the lease. The age bound matters on Capy, where the leased pid is the machine's long-lived `capy-server`, so a lease is never released by its process exiting. An older lease, or none, upgrades as before, and `GSTACK_UPGRADE_FORCE=1` or a hand-run `bin/gstack-upgrade` still moves the checkout. ### Tests - `test/capy-skill-wrapper.test.ts`: an install run under a shell that holds a fresh lease leaves HEAD where it was and stays ok. Once that lease is 121 minutes old, with its holder still alive, the same install upgrades. Before this change the first run printed `SELF_UPGRADE: done 1.0.1 -> 1.0.2`.