From bfb5f32f06252cf3ca3f103ab2616ce8be7850b1 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 22 Aug 2026 19:34:47 -0700 Subject: [PATCH 1/2] Check clean and fast-forward before resetting local main, set a finite release-watch timeout Two real findings from the promotion PR's own review round: the hub Skills refresh force-reset local main with git checkout -B main origin/main without first checking the checkout was clean or the update was a fast-forward, risking a silent discard of local work under this fleet's no-discard rule. Now checks both first and stops rather than reconciling either problem. The release-watch bound was a literal timeout placeholder rather than a defined value, now set to 2700 seconds, matching the pr_review.py script's own default, adjustable when the maintainer states a different bound for a specific release. --- .agents/skills/merge-and-release/SKILL.md | 31 ++++++++++++------- .claude-plugin/fleet-skills/.source-digest | 2 +- .../skills/merge-and-release/SKILL.md | 31 ++++++++++++------- .github/skills/merge-and-release/SKILL.md | 31 ++++++++++++------- 4 files changed, 58 insertions(+), 37 deletions(-) diff --git a/.agents/skills/merge-and-release/SKILL.md b/.agents/skills/merge-and-release/SKILL.md index bc979dca..d4ed1e78 100644 --- a/.agents/skills/merge-and-release/SKILL.md +++ b/.agents/skills/merge-and-release/SKILL.md @@ -93,18 +93,25 @@ skill covers all of it, scoped down by what the maintainer actually asks for. distinctly, the ref changed mid-dispatch, rather than folding it into an ordinary absent-run timeout. Report and stop rather than guessing once the interval elapses with zero or more than one candidate still matching. Only once exactly one candidate is confirmed, poll that one run - id to completion in - one further bounded background wait with an explicit timeout, `timeout gh run - watch --repo owner/repo --exit-status` on a host with GNU `timeout`, or the equivalent - bounded-wait mechanism on a host without it (macOS without coreutils, native Windows), and - report a timeout separately from a completed run's own conclusion, the tag or version it - produced. A run that fails, times out, or never starts is reported, never silently retried. -7. In the hub, when the chosen scope includes a release, bring this checkout to the merged - content: `git fetch origin main`, then `git checkout -B main origin/main` to force the local - `main` to the fetched tip regardless of what it pointed to before. `skills_install.py` stamps - and installs from whatever this checkout's HEAD already is, so a plain `git checkout main` - would leave a local `main` that already existed pointing at its old, pre-fetch commit, and - skip the refresh silently. Only then run `python3 scripts/skills_install.py --report`, then + id to completion in one further bounded background wait with an explicit, finite timeout, + 2700 seconds (45 minutes, matching `scripts/pr_review.py`'s own default) unless the maintainer + states a different bound for this specific release, `timeout 2700 gh run watch --repo + owner/repo --exit-status` on a host with GNU `timeout`, or the equivalent bounded-wait + mechanism enforcing the same bound on a host without it (macOS without coreutils, native + Windows), and report a timeout separately from a completed run's own conclusion, the tag or + version it produced. A run that fails, times out, or never starts is reported, never silently + retried. +7. In the hub, when the chosen scope includes a release, first confirm this checkout is clean, + `git status --porcelain` empty, a dirty checkout stops here and is reported rather than being + switched or reconciled, per this fleet's no-discard-work rule. `git fetch origin main`. When a + local `main` already exists, confirm the fetch is a fast-forward for it, `git merge-base + --is-ancestor main origin/main`, stop and report a diverged local `main` rather than + force-resetting it, the same rule applies to a diverged branch as to a dirty tree. Only once + both checks pass, `git checkout -B main origin/main` to bring the local `main` to the fetched + tip. `skills_install.py` stamps and installs from whatever this checkout's HEAD already is, so + a plain `git checkout main` would leave a local `main` that already existed pointing at its + old, pre-fetch commit, and skip the refresh silently. Only then run `python3 + scripts/skills_install.py --report`, then `python3 scripts/skills_install.py` to install, and confirm `--report` now reads current, regardless of whether step 5 or 6 dispatched, skipped, or failed a release, this step is gated only on the chosen scope, never on the release outcome. This refreshes only the machine running diff --git a/.claude-plugin/fleet-skills/.source-digest b/.claude-plugin/fleet-skills/.source-digest index 687e0260..42e65b95 100644 --- a/.claude-plugin/fleet-skills/.source-digest +++ b/.claude-plugin/fleet-skills/.source-digest @@ -1 +1 @@ -c766251ed9f8b42d +38fa9346389f203d diff --git a/.claude-plugin/fleet-skills/skills/merge-and-release/SKILL.md b/.claude-plugin/fleet-skills/skills/merge-and-release/SKILL.md index bc979dca..d4ed1e78 100644 --- a/.claude-plugin/fleet-skills/skills/merge-and-release/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/merge-and-release/SKILL.md @@ -93,18 +93,25 @@ skill covers all of it, scoped down by what the maintainer actually asks for. distinctly, the ref changed mid-dispatch, rather than folding it into an ordinary absent-run timeout. Report and stop rather than guessing once the interval elapses with zero or more than one candidate still matching. Only once exactly one candidate is confirmed, poll that one run - id to completion in - one further bounded background wait with an explicit timeout, `timeout gh run - watch --repo owner/repo --exit-status` on a host with GNU `timeout`, or the equivalent - bounded-wait mechanism on a host without it (macOS without coreutils, native Windows), and - report a timeout separately from a completed run's own conclusion, the tag or version it - produced. A run that fails, times out, or never starts is reported, never silently retried. -7. In the hub, when the chosen scope includes a release, bring this checkout to the merged - content: `git fetch origin main`, then `git checkout -B main origin/main` to force the local - `main` to the fetched tip regardless of what it pointed to before. `skills_install.py` stamps - and installs from whatever this checkout's HEAD already is, so a plain `git checkout main` - would leave a local `main` that already existed pointing at its old, pre-fetch commit, and - skip the refresh silently. Only then run `python3 scripts/skills_install.py --report`, then + id to completion in one further bounded background wait with an explicit, finite timeout, + 2700 seconds (45 minutes, matching `scripts/pr_review.py`'s own default) unless the maintainer + states a different bound for this specific release, `timeout 2700 gh run watch --repo + owner/repo --exit-status` on a host with GNU `timeout`, or the equivalent bounded-wait + mechanism enforcing the same bound on a host without it (macOS without coreutils, native + Windows), and report a timeout separately from a completed run's own conclusion, the tag or + version it produced. A run that fails, times out, or never starts is reported, never silently + retried. +7. In the hub, when the chosen scope includes a release, first confirm this checkout is clean, + `git status --porcelain` empty, a dirty checkout stops here and is reported rather than being + switched or reconciled, per this fleet's no-discard-work rule. `git fetch origin main`. When a + local `main` already exists, confirm the fetch is a fast-forward for it, `git merge-base + --is-ancestor main origin/main`, stop and report a diverged local `main` rather than + force-resetting it, the same rule applies to a diverged branch as to a dirty tree. Only once + both checks pass, `git checkout -B main origin/main` to bring the local `main` to the fetched + tip. `skills_install.py` stamps and installs from whatever this checkout's HEAD already is, so + a plain `git checkout main` would leave a local `main` that already existed pointing at its + old, pre-fetch commit, and skip the refresh silently. Only then run `python3 + scripts/skills_install.py --report`, then `python3 scripts/skills_install.py` to install, and confirm `--report` now reads current, regardless of whether step 5 or 6 dispatched, skipped, or failed a release, this step is gated only on the chosen scope, never on the release outcome. This refreshes only the machine running diff --git a/.github/skills/merge-and-release/SKILL.md b/.github/skills/merge-and-release/SKILL.md index bc979dca..d4ed1e78 100644 --- a/.github/skills/merge-and-release/SKILL.md +++ b/.github/skills/merge-and-release/SKILL.md @@ -93,18 +93,25 @@ skill covers all of it, scoped down by what the maintainer actually asks for. distinctly, the ref changed mid-dispatch, rather than folding it into an ordinary absent-run timeout. Report and stop rather than guessing once the interval elapses with zero or more than one candidate still matching. Only once exactly one candidate is confirmed, poll that one run - id to completion in - one further bounded background wait with an explicit timeout, `timeout gh run - watch --repo owner/repo --exit-status` on a host with GNU `timeout`, or the equivalent - bounded-wait mechanism on a host without it (macOS without coreutils, native Windows), and - report a timeout separately from a completed run's own conclusion, the tag or version it - produced. A run that fails, times out, or never starts is reported, never silently retried. -7. In the hub, when the chosen scope includes a release, bring this checkout to the merged - content: `git fetch origin main`, then `git checkout -B main origin/main` to force the local - `main` to the fetched tip regardless of what it pointed to before. `skills_install.py` stamps - and installs from whatever this checkout's HEAD already is, so a plain `git checkout main` - would leave a local `main` that already existed pointing at its old, pre-fetch commit, and - skip the refresh silently. Only then run `python3 scripts/skills_install.py --report`, then + id to completion in one further bounded background wait with an explicit, finite timeout, + 2700 seconds (45 minutes, matching `scripts/pr_review.py`'s own default) unless the maintainer + states a different bound for this specific release, `timeout 2700 gh run watch --repo + owner/repo --exit-status` on a host with GNU `timeout`, or the equivalent bounded-wait + mechanism enforcing the same bound on a host without it (macOS without coreutils, native + Windows), and report a timeout separately from a completed run's own conclusion, the tag or + version it produced. A run that fails, times out, or never starts is reported, never silently + retried. +7. In the hub, when the chosen scope includes a release, first confirm this checkout is clean, + `git status --porcelain` empty, a dirty checkout stops here and is reported rather than being + switched or reconciled, per this fleet's no-discard-work rule. `git fetch origin main`. When a + local `main` already exists, confirm the fetch is a fast-forward for it, `git merge-base + --is-ancestor main origin/main`, stop and report a diverged local `main` rather than + force-resetting it, the same rule applies to a diverged branch as to a dirty tree. Only once + both checks pass, `git checkout -B main origin/main` to bring the local `main` to the fetched + tip. `skills_install.py` stamps and installs from whatever this checkout's HEAD already is, so + a plain `git checkout main` would leave a local `main` that already existed pointing at its + old, pre-fetch commit, and skip the refresh silently. Only then run `python3 + scripts/skills_install.py --report`, then `python3 scripts/skills_install.py` to install, and confirm `--report` now reads current, regardless of whether step 5 or 6 dispatched, skipped, or failed a release, this step is gated only on the chosen scope, never on the release outcome. This refreshes only the machine running From 3fab2a1bd2a3cb9c04b3dd7e6fd5474fd731b838 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 22 Aug 2026 19:41:29 -0700 Subject: [PATCH 2/2] Keep the skills_install.py --report command on one physical line Distinct from the earlier-disproven CommonMark rendering claim, qodo caught a real risk here: this repo's SKILL.md is also read as raw text by Codex and opencode, not only rendered. A naive raw-text extraction of a backtick-wrapped command that itself contains a real newline can split into two shell commands instead of one. Kept this specific command on a single physical source line. --- .agents/skills/merge-and-release/SKILL.md | 5 ++--- .claude-plugin/fleet-skills/.source-digest | 2 +- .../fleet-skills/skills/merge-and-release/SKILL.md | 5 ++--- .github/skills/merge-and-release/SKILL.md | 5 ++--- 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/.agents/skills/merge-and-release/SKILL.md b/.agents/skills/merge-and-release/SKILL.md index d4ed1e78..d2045d8e 100644 --- a/.agents/skills/merge-and-release/SKILL.md +++ b/.agents/skills/merge-and-release/SKILL.md @@ -110,9 +110,8 @@ skill covers all of it, scoped down by what the maintainer actually asks for. both checks pass, `git checkout -B main origin/main` to bring the local `main` to the fetched tip. `skills_install.py` stamps and installs from whatever this checkout's HEAD already is, so a plain `git checkout main` would leave a local `main` that already existed pointing at its - old, pre-fetch commit, and skip the refresh silently. Only then run `python3 - scripts/skills_install.py --report`, then - `python3 scripts/skills_install.py` to install, and confirm `--report` now reads current, + old, pre-fetch commit, and skip the refresh silently. Only then run `python3 scripts/skills_install.py --report`, + then `python3 scripts/skills_install.py` to install, and confirm `--report` now reads current, regardless of whether step 5 or 6 dispatched, skipped, or failed a release, this step is gated only on the chosen scope, never on the release outcome. This refreshes only the machine running this session, per skill-lifecycle, every other machine still refreshes on its own next run or diff --git a/.claude-plugin/fleet-skills/.source-digest b/.claude-plugin/fleet-skills/.source-digest index 42e65b95..c33ce29e 100644 --- a/.claude-plugin/fleet-skills/.source-digest +++ b/.claude-plugin/fleet-skills/.source-digest @@ -1 +1 @@ -38fa9346389f203d +d7a408c14ed1a52c diff --git a/.claude-plugin/fleet-skills/skills/merge-and-release/SKILL.md b/.claude-plugin/fleet-skills/skills/merge-and-release/SKILL.md index d4ed1e78..d2045d8e 100644 --- a/.claude-plugin/fleet-skills/skills/merge-and-release/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/merge-and-release/SKILL.md @@ -110,9 +110,8 @@ skill covers all of it, scoped down by what the maintainer actually asks for. both checks pass, `git checkout -B main origin/main` to bring the local `main` to the fetched tip. `skills_install.py` stamps and installs from whatever this checkout's HEAD already is, so a plain `git checkout main` would leave a local `main` that already existed pointing at its - old, pre-fetch commit, and skip the refresh silently. Only then run `python3 - scripts/skills_install.py --report`, then - `python3 scripts/skills_install.py` to install, and confirm `--report` now reads current, + old, pre-fetch commit, and skip the refresh silently. Only then run `python3 scripts/skills_install.py --report`, + then `python3 scripts/skills_install.py` to install, and confirm `--report` now reads current, regardless of whether step 5 or 6 dispatched, skipped, or failed a release, this step is gated only on the chosen scope, never on the release outcome. This refreshes only the machine running this session, per skill-lifecycle, every other machine still refreshes on its own next run or diff --git a/.github/skills/merge-and-release/SKILL.md b/.github/skills/merge-and-release/SKILL.md index d4ed1e78..d2045d8e 100644 --- a/.github/skills/merge-and-release/SKILL.md +++ b/.github/skills/merge-and-release/SKILL.md @@ -110,9 +110,8 @@ skill covers all of it, scoped down by what the maintainer actually asks for. both checks pass, `git checkout -B main origin/main` to bring the local `main` to the fetched tip. `skills_install.py` stamps and installs from whatever this checkout's HEAD already is, so a plain `git checkout main` would leave a local `main` that already existed pointing at its - old, pre-fetch commit, and skip the refresh silently. Only then run `python3 - scripts/skills_install.py --report`, then - `python3 scripts/skills_install.py` to install, and confirm `--report` now reads current, + old, pre-fetch commit, and skip the refresh silently. Only then run `python3 scripts/skills_install.py --report`, + then `python3 scripts/skills_install.py` to install, and confirm `--report` now reads current, regardless of whether step 5 or 6 dispatched, skipped, or failed a release, this step is gated only on the chosen scope, never on the release outcome. This refreshes only the machine running this session, per skill-lifecycle, every other machine still refreshes on its own next run or