Merge remote-tracking branch 'origin/main' into gsta134/skill-diet-pilots

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

# Conflicts:
#	CHANGELOG.md
This commit is contained in:
garrytan committed 2026-10-28 03:42:55 +00:00
commit 20d92ebdda
76 files changed
+4143 -488

No files matched your search

+1 -1
View File
@@ -20,7 +20,7 @@ cuts to the coverage audit, review army, or voice directive have real quality co
A second, harder ceiling guards the DISCOVERY surface: `test/catalog-budget.test.ts`
caps the aggregate frontmatter `name` + `description` across all skills at
`CATALOG_BUDGET_TOKEN_EQUIVALENTS` (1,359 today; each new skill ratchets it)
`CATALOG_BUDGET_TOKEN_EQUIVALENTS` (1,393 today; each new skill ratchets it)
token-equivalents (260-byte per-skill sub-cap), counted through the shared census
in `test/helpers/skill-census.ts`. This one is enforced, not a warning — every
host loads the full catalog every session, so growth here taxes every
+2 -1
View File
@@ -53,9 +53,10 @@ gstack/
├── ship/ # Ship workflow skill
├── review/ # PR review skill (checklist.md is hand-written; design-checklist.md is GENERATED from lib/design-catalog.ts)
├── deslop-shared-libs/ # Recommendations-only audit for worthwhile shared-code extractions
├── write-skill/ # /write-skill skill (edit ladder, levels, gstack-skill-bill, full diet in references/diet.md)
├── test-audit/ # Report-first sweep for low-value tests (test value bar, audit mode)
├── eval-plan/ # Preregistered paid evals: prereg, arms, $0 dry run, priced pilot (bin/gstack-eval-plan)
├── captain/ # /captain (also /crew): one controller, many lanes; templates/ holds CREW.md, AGENTS.md, NOTES.md, DIGEST.md, the flaky ledger, the lane report, the READY block, slot call, asks and the daily automation prompt
├── captain/ # /captain (also /crew): one controller, many lanes; templates/ holds CREW.md, AGENTS.md, NOTES.md, DIGEST.md, the flaky ledger, the lane report, the READY block, slot call, the lane brief, asks and the daily automation prompt
├── plan-ceo-review/ # /plan-ceo-review skill
├── plan-eng-review/ # /plan-eng-review skill
├── autoplan/ # /autoplan skill (auto-review pipeline: CEO → design → DX → eng, eng always last)
+59 -2
View File
@@ -97,6 +97,7 @@ Where each rule lives:
| 21 | Approvals, plan asks and merge asks in the three shapes | [Approvals](#approvals-three-shapes); [capy/skill/gstack/captain.md](../capy/skill/gstack/captain.md) |
| 22 | /ship before every READY; the block carries `SHIP_RECEIPT`; stamp commits hold only tool output | [ship-policy.md](ship-policy.md#ship-before-ready); [the lane's /ship sequence](#the-lanes-ship-sequence-before-ready) |
| 23 | Review loops converge: scope frozen after pass 1, later passes on the delta, a pass cap, a blocking floor | the `/ship` and `/review` skills' own convergence policy ([skills.md](skills.md#ship)); a crew names it in one line and sets only the scope knob (`REVIEW_SCOPE`) |
| 24 | Plan before build: a sized `/autoplan` is a building lane's first act; the plan ask goes up, the build starts after the owner's word is in the run and it reports `APPROVED` | [Plan before build](#plan-before-build); the [brief template](../captain/templates/brief.md) |
## Lane discipline
@@ -121,6 +122,61 @@ such decision with a plain-language explanation of every option: what it is, why
matters, what each answer does, and what it costs. A gate code or a tool's result line
alone is not an ask.
## Plan before build
A lane that builds something plans first. Its first act after the session-start install
check, before any product code, is a `/autoplan` run on its brief, sized to the work. The
phases run in order, CEO, then design, then DX, then eng, with design and DX only when the
scope has them, and a one-file fix gets a short run (`--no-ui --no-dx --max-rounds 1`, with
`--estimate-usd` set so a small `--spend-cap` still seats its attempts), not no run. On Capy the
unattended loop is `autoplan/DRIVER.md` in the gstack checkout. New functionality a lane
takes on part-way, scope its brief did not name, gets its own plan and its own ask before
the lane builds it. In the rules file this is rule 24 of the
[template](../captain/templates/CREW.md); a crew whose own rules already took 24 before
this rule shipped cites this section by name.
**What the lane sends.** The plan and its gate in plain language a person outside the
project can follow: what changes, why now, what the change does, what it costs, the lane's
pick, and the recommended one-word reply. `gstack-owner-brief render --captain --out <run dir>`
prints that from the plan's `## Before → after`, `## Why it matters`,
`## What caused this work` and `## Big decisions` sections, with the cost lines
(`GATE_SPEND:`, `GATE_REC_COST:`) and a last line for the controller; a plan missing a
section exits `CAPTAIN_BRIEF_INCOMPLETE`, so the lane adds the section and renders again
([capy/skill/gstack/captain.md](../capy/skill/gstack/captain.md) § 3 lists them). The lane
sends it whole, as a needs-you report, and builds nothing yet.
**What the controller relays.** One ask to the owner in shape B below: the recommended
reply (every recommendation, plan changes included) as the one-word default and a "do it"
reply that takes every recommendation at full scope, follow-ons included. The plan as
first written is never the default.
**The build gate.** The lane builds only after the owner's word has come back through the
controller and been answered into the run
(`gstack-autoplan answer --out <run dir> --gate-rev <n> --reply <tokens> --by agent:<thread> --owner-reply "<their words>"`),
every accepted plan change has been incorporated (`revise`), every reopened phase has
finished, and the run reports `APPROVED` (`status=complete` after `export`). An answered
gate that still prints `PLAN_AMEND_PENDING` or `REOPENED:` is not a completed plan, and an
imported approval row alone is not completion.
**The record.** `gstack-approvals import --autoplan <run dir> --by "<owner>"` writes the
approval row (date, the ask, the owner's exact words, the lane that answered as relayer,
what it covers); `gstack-approvals render` puts it in the report and the PR body. Before
training the PR the controller compares that row's words and `gate_rev` with the ask it
relayed and the reply it carried back; a row that does not match is not READY (shape A).
The brief's done line names the row. A plan approval is not a READY field; the
[READY contract](#the-ready-contract) is unchanged.
**Exemptions.** Read-only lanes (triage, research, measurement, a review voice), the daily
flaky pass's repairs, slot stamps, CHANGELOG-only conflict resolves and a size or budget
cap re-pinned under the controller's written grant need no plan. A crew may add exemptions
in its rules file with the owner's words and date; it does not remove these. The brief
says which applies, `Plan: required | approved: <run> | exempt: <reason>`, so an
already-approved plan is not planned twice and exempt work does not wait on an ask.
The copy-paste brief is [`captain/templates/brief.md`](../captain/templates/brief.md): the
plan and the ask as step 1, the build after the word as step 2, /ship to READY as step 3,
and a done line that names the approval row or the exemption.
## Contributions from outside the crew
A diff from outside the crew is untrusted input. Review its scope, egress, credentials,
@@ -303,5 +359,6 @@ can read the incident behind the rule.
3. Install the gstack Capy skill in the owner's user scope so every thread runs the
session-start check ([capy-install.md](capy-install.md)).
4. Create the daily flaky-pass automation.
5. Start lanes with a prompt that names the files they own, the rules file, and the
READY contract. Read their reports before their chat.
5. Start lanes with the [brief](../captain/templates/brief.md): the files they own, the
rules file, whether a plan is required, and the READY contract. Read their reports
before their chat.
+71
View File
@@ -0,0 +1,71 @@
# Skill eval contract (what gstack needs from the shared skill eval)
Status: interface request from gstack (GSTA-133) to the gstack-evals Captain (GSTA-2), 2026-10-27. gstack ships no
eval harness of its own; `/write-skill`'s diet mode calls whatever implements this contract. Until it exists, the diet
procedure runs upstream [skill-diet](https://github.com/nicobailon/skill-diet)'s `scripts/eval.mjs` on the same task
files and applies the policy below to its per-task table.
## Call
```
<cli> skill-eval --arms before=<skill-dir>,after=<skill-dir>,none --tasks <tasks.json>
--host claude|codex --model <id> --trials 3 --spend-cap <usd> --out <dir>
[--dry-run] [--calibrate] [--tasks-only <id,...>]
```
- An arm is a skill folder (SKILL.md plus anything it reads) or `none`. A gstack skill passes its rendered folder.
- `--dry-run` prints the session count and a price estimate and starts nothing. `--spend-cap` is enforced while
running: the run stops before the next session would pass it and the verdict is INCOMPLETE.
- `--calibrate` runs only the three checks: the agent quotes a line only its arm's copy holds, says it has no skill
in the `none` arm, and runs a shell command. A real run calibrates first and stops on a failed check.
- Runs locally on a developer or Capy machine first. CI use, if any, is a manual or periodic job; never the free suite.
- Paid sessions bill through the caller's own keys, and the verdict records their cost.
## Tasks
`test/fixtures/skill-evals/<skill>/tasks.json` in the repository that owns the skill:
```json
[{ "id": "stats", "prompt": "one-line request", "setup": "sh \"$SKILL_EVAL_FIXTURES/seed.sh\" full",
"must": ["regex"], "must_not": ["regex"], "check": "shell command that exits 0", "safety": true }]
```
`setup` and `check` run in the session's work dir; `SKILL_EVAL_FIXTURES` is the tasks file's directory. `must`
patterns match the final answer; `must_not` patterns match its code blocks. `safety: true` marks a task whose
failure fails the diet on its own. Tasks are committed before the skill is cut.
## Output
`<out>/verdict.json`:
```json
{ "verdict": "PASS|FAIL|INCOMPLETE", "reasons": ["REGRESSION stats", "SAFETY no-code-change", "WORSE", "UNCOVERED empty"],
"harness_version": "…", "model": "…", "trials": 3,
"calibration": { "skill": true, "none": true, "shell": true },
"arms": { "before": { "pass_rate": 0.9, "tokens_per_run": { "input": 0, "output": 0, "cache_read": 0, "cache_write": 0 },
"turns_per_run": 0, "opened_skill_rate": 1.0, "files_loaded": { "SKILL.md": 30 }, "cost_usd": 0 } },
"tasks": { "stats": { "before": [true, true, true], "after": [true, true, false], "none": [false, false, false] } },
"adjudication": { "blinded": true, "judge_model": "…", "disputed": [] } }
```
Raw transcripts stay under `<out>/runs/`.
## Exit codes
- `0` PASS
- `1` FAIL (a regression, a safety failure, or WORSE)
- `2` INCOMPLETE (spend cap reached, infra failure, timeouts)
- `3` calibration failed
## Policy
- **Per task, not aggregate.** A task whose `before` panel passed (at least 2 of 3) must still pass in `after`
(at least 2 of 3), or the verdict is FAIL with `REGRESSION <id>`.
- **Safety.** Any failed trial of a `safety` task in `after` is FAIL.
- **Uncovered.** A task every arm fails is reported as `UNCOVERED <id>`; it is never dropped silently.
- **WORSE.** `after`'s overall pass rate lower than `before`'s by more than two standard errors is FAIL.
- **Opened the skill.** Under half of the skill-arm runs opening the skill is a warning: the scores then measure
the tool's own text.
- **Trials are fixed before the run.** No trials are added after a result. One same-tree rerun of a red task is
allowed and recorded as flaky; a second failure stands.
- **Blinded adjudication.** A disputed answer goes to a fresh judge that sees the task and the answer, not the arm.
+1 -1
View File
@@ -18,7 +18,7 @@ When qualified CSO runtime images are published, setup gives each automatic prel
From inside your repo, `(cd ~/.claude/skills/gstack && ./setup --team) && ~/.claude/skills/gstack/bin/gstack-team-init required` switches you to team mode and bootstraps the repo so teammates get gstack automatically (the README has the full paste, which also commits the change).
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 [releases.md](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 tagged release cut Monday and Thursday mornings, never more than once every 72 hours except for a security fix, not every merge to main. `gstack-config set update_channel main` follows every merge instead; see [releases.md](releases.md).
Swap `required` for `optional` if you'd rather nudge teammates than block them.
+12 -9
View File
@@ -3,7 +3,7 @@
This is the operating model a team adopts when it runs gstack under a Capy
Captain: one person, one Captain thread that directs a crew, crew threads that
each own a branch and stop at an unstamped READY, one controller that merges
through trains, a daily flaky pass, a weekly release, and a scorecard that says
through trains, a daily flaky pass, a twice-weekly release, and a scorecard that says
whether the factory is getting better. It was written from two crews that ran
it for a week on their own repositories (about 100 merges a week each) and from
the recurring problems four Captains reported while doing so. Every path and
@@ -33,7 +33,7 @@ flowchart LR
C -->|train build: one gate for up to 5 PRs| G[(Gate workflow)]
G -->|GREEN or RED with flake records| C
C -->|slot: stamp, verify, merge, verify| M[(main)]
M -->|Monday| R[Weekly release: tag, notes, stable]
M -->|Mon + Thu, 72 h floor| R[Stable release: tag, notes, stable]
G -.->|flake-records artifacts| F[Daily flaky pass]
M -.->|free-suite retries| F
M -.->|/retro Step 7.5| S[Factory scorecard]
@@ -84,7 +84,7 @@ a time; each is independent.
|---|---|---|
| `.gstack/pregate.json` | Maps changed paths to test lanes, so `/ship` runs the tests that cover the diff and the remote gate selects correctly ([docs/pregate.md](pregate.md)) | `/ship` fails every touched file with `PREGATE_NO_LANE`. Write it once, from one owner, before the crew starts. |
| `.gstack/ship-policy.json` | `queue_mode`, the `train` block (gate and free workflow names, the full-gate check, the train size and fill wait), the stamp files ([docs/ship-policy.md](ship-policy.md)) | Per-PR queue; `train …` refuses with `QUEUE_TRAIN_OFF`. |
| `CHANGELOG.md` + `VERSION` | A stamp per merge and a weekly release with notes ([docs/releases.md](releases.md)) | Skip the version and changelog steps; the train still works on the tree receipt. |
| `CHANGELOG.md` + `VERSION` | A stamp per merge and a twice-weekly release with notes ([docs/releases.md](releases.md)) | Skip the version and changelog steps; the train still works on the tree receipt. |
| `AGENTS.md` validation discipline | Diagnose before fixing, focused checks during repair, one full suite at the end on a frozen tree | Threads retry blindly and report passes from older revisions. |
A `.github/workflows` gate that the train can dispatch is the one hard
@@ -221,17 +221,20 @@ repeats first, start one fix thread with the Open section as its brief, and
report the count to the Captain". Pass `thread: "this"` to keep the history in
one place, and run it once by hand before enabling it.
## The weekly release
## The stable release
`.github/workflows/weekly-release.yml` runs `scripts/release-cut.ts` every
Monday morning: it takes the newest green commit on main's first-parent history
`.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
(its required check succeeded, the backstop check succeeded, every check run
completed and none failed, no active freeze), tags it `v<VERSION>`, publishes a
GitHub Release whose notes are one line per CHANGELOG version since the
previous release, and fast-forwards `stable`. Team-mode installs follow
`stable`; `gstack-config set update_channel main` follows every merge. A
hotfix is `--ref <sha on main>`. Details and the "nothing green in the window"
failure mode: [docs/releases.md](releases.md). A repository adopting this
hotfix is `--ref <sha on main>`, held by the same floor. Details, the floor's
refusal codes and the "nothing green in the window" failure mode:
[docs/releases.md](releases.md). A repository adopting this
copies the workflow, sets its required check and backstop check names
(`REQUIRED_CHECK`, `BACKSTOP_CHECK`), and keeps the CHANGELOG headline
convention ([docs/CHANGELOG_STYLE.md](CHANGELOG_STYLE.md)).
@@ -428,5 +431,5 @@ allowlist; no cost ceiling surfaced at autoplan gates.
| Ship policy, queue and trains | `docs/ship-policy.md`, `lib/ship-queue*.ts`, `bin/gstack-ship-queue`, `bin/gstack-restamp` |
| Pregate and lanes | `docs/pregate.md`, `bin/gstack-pregate` |
| Flaky reruns and records | `docs/TESTING_INTERNALS.md#flaky-reruns`, `scripts/lib/paid-rerun.ts`, `scripts/lib/ci-history.ts` |
| Weekly release | `docs/releases.md`, `scripts/release-cut.ts`, `.github/workflows/weekly-release.yml` |
| Stable release | `docs/releases.md`, `scripts/release-cut.ts`, `.github/workflows/weekly-release.yml` |
| Scorecard | `bin/gstack-factory-scorecard`, `lib/factory-scorecard.ts`, `retro/SKILL.md.tmpl` Step 7.5, `test/factory-scorecard.test.ts` |
+121 -14
View File
@@ -1,14 +1,16 @@
# Releases and update channels
gstack stamps a version at every merge to main, about a dozen a day. Since
late October 2026 there is also a weekly tagged release, and team-mode
installs follow it instead of every merge.
late October 2026 there is also a tagged release, cut twice a week, and
team-mode installs follow it instead of every merge.
## What a release is
Every Monday at 07:00 Pacific (06:00 in winter) the **Weekly release**
workflow (`.github/workflows/weekly-release.yml`, which runs
`scripts/release-cut.ts`) takes the newest green commit on main and:
Every Monday and Thursday at 14:00 UTC (07:00 Pacific, 06:00 in winter) the
**Stable release** workflow (`.github/workflows/weekly-release.yml`, which
runs `scripts/release-cut.ts`; the file kept its old name so its run history
and the `gh workflow run` commands below stay valid) takes the newest green
commit on main and:
1. tags it `v<VERSION>` (the VERSION file at that commit, an annotated tag);
2. publishes a GitHub Release whose notes are one line per CHANGELOG version
@@ -18,7 +20,7 @@ workflow (`.github/workflows/weekly-release.yml`, which runs
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.
forward, and at most once every 72 hours except for a security 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
@@ -60,11 +62,109 @@ finished by the next run, by cron or by hand: `stable` itself stays a
candidate until its release exists, and its notes compare against the release
tag before it.
## The 72-hour floor
`stable` moves at most once every 72 hours (Garry, 2026-10-27). Every cut,
scheduled or by hand, with or without `ref`, checks when `stable` last moved:
the creation time of the annotated `v<VERSION>` tag (VERSION read at `stable`)
pointing at `stable`, or the publish time of that tag's GitHub Release when it
is later (a cut whose `stable` push was retried after the tag was pushed is
dated by its release). Any other tag on that commit is ignored, so a hand-made
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 "<GHSA-…|CVE-…|#n>: why" --ref <fix sha>
NEXT_ALLOWED: 2026-11-05T14:04:51Z for a hand-run cut; 2026-11-05T11:04:51Z for a scheduled run
```
followed by the plan it would have run. Monday to Thursday is exactly 72
hours, so scheduled runs, and only scheduled runs (`GITHUB_EVENT_NAME=schedule`,
which no dispatch input can set), get a 3-hour grace for GitHub's late cron
starts: a Thursday run 69 hours after a late Monday cut still cuts. Two
regular moves are therefore never less than 69 hours apart, and a hand-run cut
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
the floor verdict (`FLOOR:` line) and exits 0.
Two cases are not held. The first cut, before `stable` exists, has nothing to
measure. Finishing a half-done cut (`stable` already at the candidate, release
missing) is not a new move, and a run without `ref` finishes that cut before
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<VERSION>` 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
unpublished (its tag may be days older than the push). A tag dated in the
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.
## Security releases
A security fix is the one exception to the floor. 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:
```bash
gh workflow run weekly-release.yml -f ref=<full sha of the fix> -f security="GHSA-xxxx-xxxx-xxxx: tar path traversal" -f dry_run=false
```
The reason must name a `GHSA-…`, a `CVE-…` or an issue or pull request
`#<n>`, and the tool checks that it is tied to the commits being released
(`stable`..`ref`). One of these must hold:
1. a commit message in that span names the same GHSA or CVE;
2. issue or pull request `#<n>` carries the `security` label and a commit in
the span references `#<n>` (the squash subject `(#<n>)` does);
3. the GitHub Advisory Database entry for the GHSA or CVE (the data OSV
imports) lists an npm package whose own entry changes in the span's
`package.json` or `bun.lock` (a dependency line or a lockfile package key,
never a name listed inside another package's entry): a dependency bump that
fixes a known advisory. The direction of the bump and the patched range are
not checked.
**Verified:** the identifier exists in the reason, the label exists on the
issue, and the link between the identifier and a released commit or bumped
package. **Attested, not verified:** that the change actually fixes the
vulnerability, how severe it is, and that it cannot wait; the `security`
label itself is applied by a person with triage access. A reason the tool
cannot tie to the span is `REFUSED_SECURITY` with what it checked, red on
every trigger, as is `security` without `ref`: a security cut releases up to
the fix, never whatever happens to be newest. Every version between `stable`
and the fix still ships with it (there are no cherry-picks); the plan's
`RIDING_ALONG:` line counts them.
The reason is public. It goes, as one line of at most 300 characters (control
characters become spaces and backticks become apostrophes), into the tag
message and the release notes: ``**Security release:** `<reason>` (verified: <evidence>)``. For an
embargoed fix, use path 2 with a neutral commit subject and a labelled issue
whose title says no more than the fix does, and keep advisory details out of
the reason until disclosure.
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
release, and the next regular cut's notes list only the versions after it.
## Channels
| Channel | Follows | Who |
|---|---|---|
| `stable` | `refs/heads/stable`: the weekly release, plus any hotfix 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 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`):
@@ -72,12 +172,12 @@ The config key is `update_channel` (`bin/gstack-config`):
```bash
gstack-config get update_channel # auto (default) | stable | main
gstack-config set update_channel main # a team install that wants every merge
gstack-config set update_channel stable # a solo install that wants weekly releases
gstack-config set update_channel stable # a solo install that wants stable releases
gstack-config unset update_channel # back to auto
```
`auto` resolves to `stable` when `team_mode` is true and to `main` otherwise,
so existing team installs moved to weekly releases with nothing to migrate and
so existing team installs moved to stable releases with nothing to migrate and
contributor installs did not move at all. `GSTACK_UPDATE_CHANNEL=stable|main`
overrides the key for one command. The resolution lives in one helper,
`bin/gstack-update-channel.sh`, used by:
@@ -111,14 +211,15 @@ still holds and nothing about version stamping changed.
## Cutting a release by hand (hotfixes)
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 Monday, run the
workflow by hand with the merge commit's SHA:
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):
```bash
gh workflow run weekly-release.yml -f ref=<full sha on main> -f dry_run=false
```
or Actions → **Weekly release** → Run workflow, on main (the cut job runs
or open Actions, pick **Stable release** and press Run workflow, on main (the cut job runs
only from `refs/heads/main`, with main's tooling; `ref` must be the full
40-hex SHA). The same green checks apply. The commit is tagged with its VERSION, the release notes say "hotfix release"
and list every version since the previous release (everything merged in
@@ -134,8 +235,8 @@ first, then promote.
Run the workflow with `dry_run=true` (the default for a manual run) to see the
plan without changing anything: the candidate, its VERSION and tag, the stable
move, how many version lines the notes would carry, every skipped commit with
its reason, and the exact commands a real cut would run. The scheduled Monday
run is never a dry run.
its reason, the floor verdict, and the exact commands a real cut would run. The scheduled
runs are never dry runs.
```bash
gh workflow run weekly-release.yml -f dry_run=true
@@ -159,4 +260,10 @@ is exercised against real check runs before it merges.
| `REFUSED: non-fast-forward: ...` | the candidate is behind stable; stable only moves forward |
| `REFUSED: queue frozen since train ...` | the main backstop froze the queue; clear it first (docs/ship-policy.md) |
| `REFUSED: tag v<X> already names <other sha>` | a version was stamped twice; needs a look before anything is cut |
| `REFUSED_FLOOR: stable moved to v<X> at <time>, <h> h ago; ...` | inside the 72-hour floor; nothing changed. Exit 1 by hand, 0 on the schedule or a dry run |
| `NEXT_ALLOWED: <time> for a hand-run cut; <time> for a scheduled run` | follows every `REFUSED_FLOOR` (and `FLOOR_UNKNOWN`, where it says why there is no time) |
| `FLOOR_UNKNOWN: ...` | stable's last move cannot be dated (no annotated `v<VERSION>` tag, an unreadable release, or an unpublished release under a `ref` cut); exit 1 on every trigger |
| `REFUSED_SECURITY: ...` | `security` without `ref`, a reason the tool cannot tie to the released commits, or a security tag that never moved stable run without `security`; exit 1 on every trigger |
| `FLOOR: clear|refused|unknown|none|not applied|bypassed by the security exception (...)` | the floor verdict, in every plan |
| ``SECURITY: `<reason>` (verified: <evidence>)``, `RIDING_ALONG: <n> other versions ship with the fix` | a security cut's plan |
| `TAG:`, `STABLE:`, `RELEASE:` | what the cut did; a re-run reports each step as already done and finishes a step an earlier run did not reach |
+33 -3
View File
@@ -562,6 +562,26 @@ leave it out and report it. The tree receipt refuses such a move after the fact
only while the moved version is older than the base's at the build, so the
restamp, not the receipt, is what keeps the base's key in place.
**Size caps compose; nothing re-pins at the slot.** Ship's union-size cap is a
ledger, `test/size-ledger/ship/`: `base.json` plus one `<branch>.json` per
branch that changed ship's union bytes (`{ "bytes": <signed>, "why": "…" }`),
and the parity suite requires ship's union bytes to equal the base plus every
entry, exactly. A lane records its own change once, before /ship, with `bun
--no-env-file --config=/dev/null scripts/size-ledger.ts record ship --why
"<what changed>"`. The entry is a property of the branch's own diff, so it
stays right across base merges, and each prefix carries the entries of the
slots in it, so T<k> passes with no re-pin and the stamp commit stays the four
files (a cap re-pinned at the slot would be a fifth file on a tree the gate
never ran). A clean merge adds byte deltas exactly; overlapping hunks are a
train conflict anyway, and two branches making an identical edit (git keeps
one copy, both entries count) fail closed with the same record command as the
fix. A branch that grows ship beyond its entry, or shrinks it without a
negative one, fails on its own tree and on every prefix that includes it.
`scripts/size-ledger.ts check ship` prints the ledger; `fold ship
--measured-at <tree>` moves the entries into `base.json` in a tidy-up PR,
never in a stamp commit. Other skills keep their `maxSizeRatio` caps in
`test/helpers/carve-guards.ts`.
**A stacked child whose parent merged earlier.** When the pin was stacked on a
PR that squash-merged in an earlier slot or train, the base merge conflicts
beyond the CHANGELOG wherever the parent's squash and the pin touched the same
@@ -624,9 +644,19 @@ freezes the same way, since main already holds the unverified squash. The
record comes from the one GitHub read that decided to freeze, never a second
one: a slot that read MERGED stays merged even when GitHub stops answering
before the record is written, and a slot unreadable beside a merged one is
left out of `unmerged` and named in the reason; nothing reads it again, so
the `NEXT:` after the repair omits it and the operator seats it in a train by
hand once `gh pr view` answers. When
left out of `unmerged`, recorded under `unreadable` and printed on an
`unreadable:` line; the `NEXT:` after the repair names it (`gh pr view <n>
--json state,headRefOid (unreadable at the freeze: #<n>@<sha>); seat each
that is OPEN in a train`).
When every slot of the train already merged on GitHub and main's SHA is a
prefix another train's verify confirmed (read after the notes ref, so a
machine that never ran that train still sees it), the mismatch says nothing
about this train: verify refuses with `QUEUE_TRAIN_STALE` and `NEXT:
gstack-ship-queue train prune --stale --train <nonce>`, freezing nothing and
dispatching no backstop (train 24's record froze the queue that way on a main
train 28 had verified, #3519). Only a verify that fetched main takes this
exit: under `--no-fetch` the local main may be older than GitHub's, so the
mismatch freezes as before. When
whether a slot merged on GitHub cannot be read, verify refuses
(`QUEUE_PROOF_MISSING`, "whether #<n> merged on GitHub could not be read")
and asks to be rerun once `gh pr view` answers; an unreadable state is never
+30 -2
View File
@@ -42,6 +42,7 @@ Detailed guides for every gstack skill — philosophy, workflow, and examples.
| [`/context-restore`](#context-restore) | **Restore State** | Resume from a saved context, even across Conductor workspace handoffs. |
| [`/health`](#health) | **Code Quality Dashboard** | Wraps type checker, linter, tests, dead code detection. Computes a weighted 0-10 score; tracks trends over time. |
| [`/deslop-shared-libs`](#deslop-shared-libs) | **Shared Code Reviewer** | Find worthwhile shared-code extractions in recent work. Recommendations only. |
| [`/write-skill`](#write-skill) | **Skill Editor** | Write or improve an agent skill without bloat: edit ladder, size per level, skill-to-bin drift check, full diets with a before/after test. |
| [`/test-audit`](#test-audit) | **Test Auditor** | Sweep existing tests for low-value, implementation-coupled or duplicate tests. Report-only unless you approve a batch. |
| [`/landing-report`](#landing-report) | **Ship Queue Dashboard** | Read-only snapshot of the workspace-aware ship queue. Which version slots are claimed, which sibling workspaces have WIP. |
| [`/captain`](#captain) | **Crew Controller** | Run many coding agents from one controller. Lanes build and send a READY block; the controller validates it, merges through trains (stamp on call, verify, merge with the expected head, verify again), keeps the digest, relays approvals in three fixed shapes and runs a daily flaky-fix pass. Also answers to `/crew`. |
@@ -885,6 +886,33 @@ checks do not run the history audit. Optional extractions are advisory and requi
approval; they do not block a clean review or reduce its score. Actual defects
keep their normal fix handling.
## `/write-skill`
Every skill is read in full each time it runs, so every extra line costs tokens on every run, and a skill that names a
command the tool no longer has produces wrong calls that pass CI. `/write-skill` keeps skills lean and correct, for
gstack's own templates and for any skill folder you own.
- **Find the skill.** `gstack-skill-bill <name|folder>` resolves exact names only and says what you are holding: a
folder you own, a gstack checkout template (edit `SKILL.md.tmpl`, then `bun run gen:skill-docs`), or an installed
gstack skill, which it never edits because `gstack-upgrade` overwrites it.
- **Edit ladder.** Stop at the first step that handles the change: fix the tool (a clearer error, a flag, a test);
add nothing a fresh session already does; edit the line that covers the idea instead of appending; put the fact on the
lowest level that reaches the agent in time (description, SKILL.md, `references/`, the tool's `--help`, the tool's
errors); write the rule and put the story in the commit message.
- **Size.** `gstack-skill-bill` prints bytes and estimated tokens per level, and in a gstack checkout the skill's eval
task file (`EVAL TASKS:`). Commits carry
`Skill-Size: <before>→<after> bytes`; a skill more than 10% over its last `Skill-Diet:` commit is DUE, and a red
context-budget ratchet means diet, not a higher ceiling.
- **Drift.** `test/skill-bin-drift.test.ts` fails when a template or rendered skill calls a gstack bin or flag that
does not exist (`bun run scripts/skill-bin-drift.ts --skill <name>` for one skill). Its allowlist only shrinks.
- **Full diet.** Report first, write the eval tasks before any cut
(`test/fixtures/skill-evals/<skill>/tasks.json`), cut, then prove before vs after vs no skill with the shared skill
eval (`docs/designs/SKILL_EVAL_CONTRACT.md`). A diet ships only when no task that passed before fails after.
`/write-skill` is not `/skillify`: `/skillify` codifies a browser scrape into a browser-skill. Adapted from
[nicobailon/skill-diet](https://github.com/nicobailon/skill-diet) (MIT). The first diet, /learn, cut its template from
5,797 to 2,955 bytes.
## `/test-audit`
Find existing tests that cost more than they protect. `/review`, `/ship`, `/qa` and
@@ -1242,7 +1270,7 @@ Claude: 23 learnings for this project (14 high confidence, 6 medium, 3 low)
3 potentially stale (referenced files deleted):
- "auth middleware uses JWT" — auth/middleware.ts was deleted
[Prune these? Y/N]
[Remove, keep, or update each one?]
```
---
@@ -1445,7 +1473,7 @@ this model disagree (squash or merge, who stamps, gate-ahead, the severe race, a
id in the READY), the skill ships both as options for the crew rules file.
Templates (CREW.md, AGENTS.md, NOTES.md, DIGEST.md, the flaky ledger, the lane report,
the READY block, the slot call, the plan and merge asks, the daily automation prompt)
the READY block, the slot call, the lane brief, the plan and merge asks, the daily automation prompt)
live in `captain/templates/`; the team setup guide for Capy is [docs/captain.md](captain.md).
## `/gstack-upgrade`
+20 -2
View File
@@ -4346,6 +4346,23 @@ current main. When whether a slot merged on GitHub cannot be read, verify
refuses with `QUEUE_PROOF_MISSING` instead of either answer; rerun it once
`gh pr view` answers.
<a id="queue-train-stale"></a>
### `QUEUE_TRAIN_STALE`
**Meaning.** Verify found main's tree outside this train's prefixes, but every
slot of the train already merged on GitHub and main's SHA is a prefix another
train's verify confirmed, so the record is stale: its PRs landed and later
trains moved main on. Comparing today's main with its prefixes proves nothing,
so the queue is not frozen and no backstop is dispatched (train 24's record
did both on a main train 28 had verified, #3519). Other trains are read after
the notes ref, so a machine that never ran the later train still sees it.
**Fix.** `gstack-ship-queue train prune --stale --train <nonce>` settles the
record. When some slot is not merged, no train verified main's SHA, or verify
ran with `--no-fetch`, verify freezes as before (`QUEUE_TRAIN_FROZEN`). A queue
another machine already froze stays frozen; the refusal prints its freeze
lines.
<a id="queue-train-frozen"></a>
### `QUEUE_TRAIN_FROZEN`
@@ -4360,8 +4377,9 @@ stops answering a moment later, and a slot whose state could not be read when
another slot had merged is left out of `unmerged` and named in the reason
(`#<n> unreadable on GitHub, left out of unmerged; seat it in a train by hand
once gh pr view answers`), so the rebuild after the repair never seats a PR
that may already be in; nothing reads that slot again, so the `NEXT:` after
the repair omits it and the operator seats it by hand. The
that may already be in; the record keeps it under `unreadable`, an
`unreadable:` line prints it, and the `NEXT:` after the repair names it for a
`gh pr view` check. The
freeze lives in local state and on `refs/notes/gstack-trains`. Ordinary
trains and other trains' merges are refused until a repair lands.