diff --git a/.github/workflows/evals-main-backstop.yml b/.github/workflows/evals-main-backstop.yml index da91abc02..2826bf353 100644 --- a/.github/workflows/evals-main-backstop.yml +++ b/.github/workflows/evals-main-backstop.yml @@ -11,7 +11,7 @@ name: Main backstop gate # The verdict is published on the commit as the main-backstop check run # (success when a green train covers the tree or the backstop gated it green, # neutral when the push was superseded or deferred, failure when it froze the -# queue or could not finish); the weekly release reads it (docs/releases.md). +# queue or could not finish); the stable release reads it (docs/releases.md). on: push: branches: [main] @@ -72,7 +72,7 @@ jobs: bun --no-env-file --config=/dev/null --no-install run scripts/train-backstop.ts run --repo "$GITHUB_REPOSITORY" --branch "$GITHUB_REF_NAME" --sha "$GITHUB_SHA" \ --summary "$GITHUB_STEP_SUMMARY" --outputs "$GITHUB_OUTPUT" - # The per-commit verdict the weekly release reads (scripts/release-cut.ts). + # The per-commit verdict the stable release reads (scripts/release-cut.ts). # Its own job with checks: write and no repository code (the same split as # evals.yml's train-check): only the action word the gate step recorded. backstop-check: diff --git a/.github/workflows/weekly-release.yml b/.github/workflows/weekly-release.yml index a5453cb62..c80b70364 100644 --- a/.github/workflows/weekly-release.yml +++ b/.github/workflows/weekly-release.yml @@ -6,9 +6,12 @@ name: Stable release # weekly-release.yml name so its run history and every `gh workflow run` # reference stay intact). stable moves at most once per 72 hours: a cut inside # that floor is refused with REFUSED_FLOOR and NEXT_ALLOWED; a scheduled run -# gets 3 hours of grace for cron start jitter, a hand-run cut none. The only -# exception is a security cut: the security input with a ref, naming a GHSA, -# CVE or # the script ties to the released commits. Run it by hand with a +# gets 3 hours of grace for cron start jitter, a hand-run cut none. Two +# exceptions pass it, one per cut and each with a ref: a security cut (the +# security input, naming a GHSA, CVE or # the script ties to the released +# commits) and a regression cut (the regression input, naming a +# regression-labelled # and the earlier stable release v the behavior +# worked in). Run it by hand with a # ref for a hotfix, or with dry_run to see the plan and change nothing. A pull request that touches # the release tooling runs the dry run against main with a read-only token, # which is the only pre-merge exercise of the real workflow. The candidate's @@ -35,6 +38,10 @@ on: description: 'Security cut (needs ref): the reason, naming the GHSA-…, CVE-… or # it fixes; passes the 72-hour floor once verified and lands in the release notes' type: string default: '' + regression: + description: 'Regression cut (needs ref): the reason, naming the regression-labelled # it fixes and the earlier stable release v it worked in; passes the 72-hour floor once verified and lands in the release notes' + type: string + default: '' pull_request: paths: - '.github/workflows/weekly-release.yml' @@ -59,7 +66,7 @@ jobs: contents: write # Reads the candidate's check runs. checks: read - # A security cut reads the security label on the # its reason names. + # A security or regression cut reads the label on the # its reason names. issues: read pull-requests: read steps: @@ -88,6 +95,7 @@ jobs: DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run && 'true' || 'false' }} RELEASE_REF: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || '' }} SECURITY_REASON: ${{ github.event_name == 'workflow_dispatch' && inputs.security || '' }} + REGRESSION_REASON: ${{ github.event_name == 'workflow_dispatch' && inputs.regression || '' }} run: | set -euo pipefail if [ -n "$RELEASE_REF" ] && ! [[ "$RELEASE_REF" =~ ^[0-9a-f]{40}$ ]]; then @@ -98,10 +106,15 @@ jobs: echo "::error::a security cut needs ref: the full SHA of the fix commit on main" exit 1 fi + if [ -n "$REGRESSION_REASON" ] && [ -z "$RELEASE_REF" ]; then + echo "::error::a regression cut needs ref: the full SHA of the fix commit on main" + exit 1 + fi args=(--repo "$GITHUB_REPOSITORY" --summary "$GITHUB_STEP_SUMMARY") [ "$DRY_RUN" = true ] && args+=(--dry-run) [ -n "$RELEASE_REF" ] && args+=(--ref "$RELEASE_REF") [ -n "$SECURITY_REASON" ] && args+=(--security "$SECURITY_REASON") + [ -n "$REGRESSION_REASON" ] && args+=(--regression "$REGRESSION_REASON") echo "## Stable release ($([ "$DRY_RUN" = true ] && echo 'dry run' || echo 'cut'), $GITHUB_EVENT_NAME)" >> "$GITHUB_STEP_SUMMARY" bun --no-env-file --config=/dev/null --no-install run scripts/release-cut.ts "${args[@]}" diff --git a/CHANGELOG.md b/CHANGELOG.md index a01b6af34..ebce34a93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## [1.91.292.0] - 2026-10-28 + +**A fix for a regression on stable can skip the 72-hour wait, like a security fix.** Garry, asked whether non-security breakage on stable may bypass the twice-weekly cadence and the floor, answered on 2026-10-28: "Regression". Until now a fix for something the last stable release did right, and the current one breaks, waited up to 72 hours on every team install. Now `--regression "#n: worked in v: why"` with `--ref` passes the floor once the tool ties it to the release, and the claim is public in the tag and the notes. + +### Added +- **Regression cuts in `scripts/release-cut.ts`: `--regression ""` with `--ref `** (workflow input `regression`). The reason must name exactly one earlier stable release `v` and at least one `#`. The cut is refused with `REFUSED_REGRESSION`, red on every trigger, unless `v` is an annotated release tag strictly behind the current stable with a published GitHub Release (the current stable itself never counts, nor a tag a half-done cut left behind), `#` carries the `regression` label (a maintainer's declaration; a `security` label does not count, nor the reverse) and a released commit references `#`. That the behavior really worked in `v` stays attested. The record matches a security cut. The sanitized reason and evidence go into the tag (`Regression:` / `Verified:`), the notes (`(regression release)`, `**Regression release:** …`) and the plan (`REGRESSION:`, `RIDING_ALONG:`); a half-done cut finishes from its tag; an unmoved exception tag must verify again with the reason it recorded (this now holds for security cuts too). + +### Changed +- **One exception per cut.** `--security` and `--regression` together are `REFUSED_REGRESSION`. A regression cut passes a dated floor only; `FLOOR_UNKNOWN` (an undatable or unpublished stable) still yields to a security cut alone. The floor counts from any exception release, so the next scheduled cut may be held. `REFUSED_FLOOR` names both ways past it. +- **"Weekly" wording follows the twice-weekly cadence** in README.md, `bin/gstack-update-channel.sh` (its comment and the no-stable-yet message, now "until the first stable release is cut"), the /gstack-upgrade skill's `AHEAD_OF_STABLE` line and the main-backstop workflow comments. docs/releases.md and docs/recommended-harness.md describe the regression exception. + ## [1.91.291.0] - 2026-10-28 **/ship and /autoplan size their review to the diff.** A /ship review used to send the same specialist army, Red Team, adversarial pass and outside voice at every diff and repeat the whole army after any fix, up to three passes. Now `gstack-review-plan` classifies the diff (docs, tests, code or gate) and plans the next step through one transition table, so a README edit gets the core checklist and a gate change still gets everything. Measured on two captured diffs across five loop scenarios with measured reviewer output (`scripts/review-dispatch-compare.ts`), estimated review tokens fall to a median 0.26x of before on docs-only diffs and 0.85x on code diffs (0.52x over all ten runs, range 0.23–0.99). Every reviewer that can block a ship stays at full effort on the lane's model; a paid replay showed a medium-effort fix-check miss a fail-open that the full-effort check caught. diff --git a/README.md b/README.md index d31d6c4fd..a7519ae27 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ From inside your repo, paste this. Switches you to team mode, bootstraps the rep (cd ~/.claude/skills/gstack && ./setup --team) && ~/.claude/skills/gstack/bin/gstack-team-init required && git add .claude/ CLAUDE.md && git commit -m "require gstack for AI-assisted work" ``` -No vendored files in your repo, no version drift, no manual upgrades. Every Claude Code session starts with a fast auto-update check (throttled to once/hour, network-failure-safe, completely silent). Team mode follows the `stable` channel: the weekly tagged release (Monday mornings, plus off-cycle hotfixes), not every merge to main. `gstack-config set update_channel main` follows every merge instead; see [docs/releases.md](docs/releases.md). +No vendored files in your repo, no version drift, no manual upgrades. Every Claude Code session starts with a fast auto-update check (throttled to once/hour, network-failure-safe, completely silent). Team mode follows the `stable` channel: the twice-weekly tagged release (Monday and Thursday, at most once every 72 hours, plus verified security and regression fixes), not every merge to main. `gstack-config set update_channel main` follows every merge instead; see [docs/releases.md](docs/releases.md). Swap `required` for `optional` if you'd rather nudge teammates than block them. @@ -241,7 +241,7 @@ On Windows without Developer Mode (MSYS2 / Git Bash), `setup` falls back to file | Doc | What it covers | |-----|---------------| | [Skill Deep Dives](docs/skills.md) | Philosophy, examples, and workflow for every skill (includes Greptile integration) | -| [The recommended harness](docs/recommended-harness.md) | gstack + Capy from zero to the first merged train: install, the Captain and its Drive, trains, the daily flaky pass, the weekly release, the factory scorecard, what is Capy-specific, known Capy limits | +| [The recommended harness](docs/recommended-harness.md) | gstack + Capy from zero to the first merged train: install, the Captain and its Drive, trains, the daily flaky pass, the twice-weekly release, the factory scorecard, what is Capy-specific, known Capy limits | | [Why gstack](docs/why-gstack.md) | The story, the productivity numbers and their caveats, Karpathy's failure modes, parallel sprints | | [Install details](docs/install.md) | Setup timing, requirements in detail, team mode, the hooks setup registers, agent harness registration | | [Other agents](docs/hosts.md) | Agent harnesses (OpenClaw, Hermes, dots, QM, Muse Code, Grok, any other), OpenClaw and ClawHub, per-host notes, the Codex profile and model tiers, outside-review providers, the design binary's models | diff --git a/VERSION b/VERSION index 946d7387f..62f453c77 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.91.291.0 +1.91.292.0 diff --git a/agents-digest/gstack-AGENTS.md b/agents-digest/gstack-AGENTS.md index 8b9d0100e..96e3cd9ce 100644 --- a/agents-digest/gstack-AGENTS.md +++ b/agents-digest/gstack-AGENTS.md @@ -1,4 +1,4 @@ -# gstack digest v1.91.291.0 — regenerate/re-copy after upgrading gstack +# gstack digest v1.91.292.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, diff --git a/bin/gstack-update-channel.sh b/bin/gstack-update-channel.sh index 033040d6b..89bea702f 100755 --- a/bin/gstack-update-channel.sh +++ b/bin/gstack-update-channel.sh @@ -6,7 +6,7 @@ # # Channels (docs/releases.md): # main every merge to main (a version per merge, a dozen a day) -# stable the weekly tagged release: refs/heads/stable on origin, moved only +# stable the twice-weekly tagged release: refs/heads/stable on origin, moved only # forward along main's first-parent history by the release workflow # # The config key update_channel (bin/gstack-config) is auto | stable | main. @@ -77,7 +77,7 @@ if [ "${BASH_SOURCE[0]}" = "$0" ]; then _uc_err=$(_receipted_git open gstack-upgrade "${_uc_host:-unknown}" gstack-self-update-pull "user ran /gstack-upgrade" git -C "$_uc_root" fetch -q origin "$(gstack_update_channel_refspec stable)" 2>&1 >/dev/null) \ && { printf 'origin/stable\n'; exit 0; } if gstack_update_channel_missing "$_uc_err"; then - echo "gstack-update-channel: origin has no stable release yet; following origin/main until the first weekly release is cut" >&2 + echo "gstack-update-channel: origin has no stable release yet; following origin/main until the first stable release is cut" >&2 else echo "gstack-update-channel: fetching origin/stable failed ($(printf '%s' "$_uc_err" | head -1 | head -c 200)); staying on origin/stable as last fetched" >&2 printf 'origin/stable\n'; exit 0 diff --git a/docs/recommended-harness.md b/docs/recommended-harness.md index a71255010..687ccb0fe 100644 --- a/docs/recommended-harness.md +++ b/docs/recommended-harness.md @@ -225,8 +225,8 @@ one place, and run it once by hand before enabling it. `.github/workflows/weekly-release.yml` ("Stable release") runs `scripts/release-cut.ts` every Monday and Thursday at 14:00 UTC, and never moves -`stable` more than once in 72 hours except for a verified security fix -(`--security` with `--ref`): it takes the newest green commit on main's first-parent history +`stable` more than once in 72 hours except for a verified security or regression +fix (`--security` or `--regression`, with `--ref`): it takes the newest green commit on main's first-parent history (its required check succeeded, the backstop check succeeded, every check run completed and none failed, no active freeze), tags it `v`, publishes a GitHub Release whose notes are one line per CHANGELOG version since the diff --git a/docs/releases.md b/docs/releases.md index 5f8b4eff8..de38b8ff4 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -20,7 +20,8 @@ commit on main and: A tag and its release never change. `stable` is a moving pointer: always a commit on main's first-parent history, moved only by this workflow, only -forward, and at most once every 72 hours except for a security fix (below). +forward, and at most once every 72 hours except for a security or regression +fix (below). **Green** means, on that exact commit: the `free-tests` check run succeeded (the status main requires), the commit was gated (below), every check run on @@ -74,7 +75,7 @@ extra tag can neither date the move nor stall later cuts. A cut inside the floor changes nothing and prints, as its first two lines: ``` -REFUSED_FLOOR: stable moved to v1.91.300.0 at 2026-11-02T14:04:51Z, 47.2 h ago; a hand-run cut needs 72 h; a security fix passes with --security ": why" --ref +REFUSED_FLOOR: stable moved to v1.91.300.0 at 2026-11-02T14:04:51Z, 47.2 h ago; a hand-run cut needs 72 h; a security fix passes with --security ": why" --ref , a regression fix with --regression "#n: worked in v: why" --ref NEXT_ALLOWED: 2026-11-05T14:04:51Z for a hand-run cut; 2026-11-05T11:04:51Z for a scheduled run ``` @@ -87,8 +88,8 @@ always needs the full 72. Thursday to Monday is 96 hours. A refused hand cut exits 1 with an error annotation. A refused scheduled run exits 0 with a notice annotation and the same lines in the step summary: that -is the policy working (a security cut on Wednesday holds Thursday, and -Monday's cut carries everything), not a fault. A dry run prints the plan and +is the policy working (a security or regression cut on Wednesday restarts the +floor and holds Thursday, and Monday's cut carries everything), not a fault. A dry run prints the plan and the floor verdict (`FLOOR:` line) and exits 0. Two cases are not held. The first cut, before `stable` exists, has nothing to @@ -98,7 +99,7 @@ it considers anything newer. The floor fails closed when it cannot date the move. Three cases are `FLOOR_UNKNOWN`, red on every trigger, and only a verified security cut gets -past them, as it gets past the floor. They are a `stable` that no annotated `v` tag dates +past them (a regression cut passes a dated floor only). They are a `stable` that no annotated `v` tag dates (only this tool moves `stable` and it tags first, so that state means someone moved it by hand), a release that cannot be read (a GitHub API error is not "no release"), and a `ref` cut while `stable`'s own release is still @@ -107,14 +108,15 @@ future holds the floor from now, not from that date. The floor binds the release tool, not a writer pushing `stable` directly; that would take a branch ruleset limiting `stable` to the Actions bot. -The floor applies to non-security breakage too. If a cut breaks team -installs, the fix waits for the floor; an affected install can follow main -meanwhile (`gstack-config set update_channel main`) and the next scheduled -cut brings it back. +Breakage that is not a regression of an earlier stable release (a new +feature that misbehaves, say) waits for the floor; an affected install can +follow main meanwhile (`gstack-config set update_channel main`) and the next +scheduled cut brings it back. ## Security releases -A security fix is the one exception to the floor. Run the workflow by hand +A security fix is one of the two exceptions to the floor (the other is a +regression fix, below); a cut carries one exception, never both. Run the workflow by hand with `ref` set to the fix commit on main and `security` set to a reason that names what it fixes: @@ -157,14 +159,60 @@ A security cut that stopped half-way after moving `stable` is finished by the next run with the reason and evidence its tag recorded, without checking them again. A tag that records a security cut but never moved `stable` gets no such pass: the cut is refused (`REFUSED_SECURITY`) until it runs again -with `security` and `ref` and verifies. The floor then counts from the security +with `security`, `ref` and the reason its tag recorded, and verifies. The floor then counts from the security release, and the next regular cut's notes list only the versions after it. +## Regression releases + +A regression fix is the other exception (Garry, 2026-10-28: "Regression"). A +regression is a fix for behavior that worked in an earlier stable release and +is broken on the current one. A maintainer declares it by putting the +`regression` label on the issue or pull request (labelling needs triage +access; create the label once if the repository lacks it), and the person +running the cut names it in the `regression` input, with `ref` set to the +fix commit on main: + +```bash +gh workflow run weekly-release.yml -f ref= -f regression="#3601: worked in v1.91.300.0: the update check stopped running" -f dry_run=false +``` + +The reason must name exactly one release `v` and at least one `#`. +All of these must hold, or the cut is `REFUSED_REGRESSION` with what it +checked: + +1. `v` is an annotated release tag strictly behind the current `stable` + whose GitHub Release is published (not a draft): an earlier stable release + team installs actually ran, which a tag left behind by a half-done cut is + not. The current stable + itself does not count, since a fix for something it still does right needs + no exception. +2. `#` carries the `regression` label (a `security` label does not count, + nor the reverse) and a commit in `stable`..`ref` references it (the squash + subject `(#)` does). + +**Verified:** the release tag, its place behind `stable` and its published +release, the label, and +that the labelled issue or PR is in the released span. **Attested, not +verified:** that the behavior really worked in `v` and that this change +restores it; the label and the public reason carry that claim. + +Everything else matches a security cut. `ref` is required, `regression` and +`security` together are refused, every version between `stable` and the fix +rides along (`RIDING_ALONG:`), and the sanitized reason and evidence go into +the tag message (`Regression:` / `Verified:`) and the notes +(``**Regression release:** `` (verified: )``). A half-done +regression cut finishes from its tag's record, and a regression tag that never +moved `stable` is refused until it runs again with `regression` and the reason +its tag recorded, and verifies. +Unlike a security cut, a regression cut does not pass `FLOOR_UNKNOWN`. The +floor counts from the regression release, so the next scheduled cut may be +held. + ## Channels | Channel | Follows | Who | |---|---|---| -| `stable` | `refs/heads/stable`: the twice-weekly release (at most once per 72 hours), plus any security cut off-cycle | team mode (`./setup --team`) by default | +| `stable` | `refs/heads/stable`: the twice-weekly release (at most once per 72 hours), plus any security or regression cut off-cycle | team mode (`./setup --team`) by default | | `main` | every merge to main | plain `./setup`, contributors, Capy crew machines | The config key is `update_channel` (`bin/gstack-config`): @@ -213,7 +261,7 @@ still holds and nothing about version stamping changed. A hotfix is an ordinary PR: it merges to main through a train and gets its version like any other change. To put it on stable before the next scheduled cut, run the workflow by hand with the merge commit's SHA, once the 72-hour -floor allows it (a security fix uses the security input instead): +floor allows it (a security or regression fix uses its input instead): ```bash gh workflow run weekly-release.yml -f ref= -f dry_run=false @@ -263,7 +311,8 @@ is exercised against real check runs before it merges. | `REFUSED_FLOOR: stable moved to v at