mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-13 03:48:06 +01:00
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:
commit
20d92ebdda
76 files changed
+4143
-488
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
@@ -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.
|
||||
@@ -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
@@ -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.
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
|
||||
Reference in new issue
Block a user