mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-01 06:24:41 +01:00
Also: the Captain digest drops an archived thread from In progress in the same turn (GST-371). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
366 lines
21 KiB
Cheetah
366 lines
21 KiB
Cheetah
---
|
|
name: captain
|
|
preamble-tier: 2
|
|
version: 1.0.0
|
|
description: |
|
|
Run a crew of coding agents from one controller (also /crew): lanes send READY
|
|
blocks and never merge; the controller merges through trains, keeps the digest,
|
|
relays approvals, fixes flakes daily. Use when asked to "run the crew", "set up a
|
|
captain", "build a merge train", "call a slot", "validate a READY", "write the
|
|
digest" or "run the flaky pass". (gstack)
|
|
triggers:
|
|
- captain
|
|
- crew
|
|
- run the crew
|
|
- merge train
|
|
- call the slot
|
|
- validate this ready
|
|
- write the digest
|
|
- flaky pass
|
|
allowed-tools:
|
|
- Bash
|
|
- Read
|
|
- Write
|
|
- Edit
|
|
- Glob
|
|
- Grep
|
|
- AskUserQuestion
|
|
sensitive: false
|
|
---
|
|
|
|
{{PREAMBLE}}
|
|
|
|
# /captain: one controller, many lanes
|
|
|
|
A crew is one coordinating agent (the Captain, or controller) and any number of worker
|
|
threads (lanes), each on its own branch and machine, all reporting to the controller and
|
|
through it to one person (the owner). The operating model below is what keeps dozens of
|
|
lanes merging into one base branch every day without a red base: lanes never merge, a
|
|
READY block is the only merge trigger, every merge runs through a train, every decision
|
|
the owner makes is recorded in their own words, and flakes are fixed daily instead of
|
|
retried away. The guide for a team setting this up on Capy, with the drive layout and
|
|
the templates, is `docs/captain.md` in the gstack checkout; the copy-paste files are in
|
|
`captain/templates/` next to this skill.
|
|
|
|
Usage: `/captain [setup | ready <pr> | train | digest | approvals | flaky-pass]`; `/crew` is
|
|
the same skill. With no argument, read the crew's rules file and the digest, then do whatever the open item at
|
|
the top of the digest needs.
|
|
|
|
## Boundaries
|
|
|
|
- The controller merges; a lane never merges, enables auto-merge, or uses a merge queue
|
|
the controller does not drive. A lane's job ends at an unstamped READY.
|
|
- The controller never commits to a lane's branch. It calls slots; the lane stamps.
|
|
- Nothing merges outside a train; a single PR takes a one-PR `--urgent` train, and a
|
|
repair under a freeze takes `train build --repair`. Every merge outside a train costs a
|
|
backstop gate.
|
|
- The owner's decisions arrive through the controller, in the owner's exact words, and
|
|
are recorded before the work they approve starts.
|
|
- Paid evidence is read, never rejudged: no thresholds, trial counts, case kinds or
|
|
budgets change to turn a red green.
|
|
|
|
## Setup: parameters and the rules file
|
|
|
|
Everything repo-specific is a parameter. Fill them once in the crew rules file
|
|
(`captain/templates/CREW.md`) and never hard-code them in a message:
|
|
|
|
| Parameter | What it names | Example |
|
|
|---|---|---|
|
|
| `REPO` | owner/name the crew ships to | `acme/widgets` |
|
|
| `BASE` | the branch trains merge into | `main` |
|
|
| `REQUIRED_CHECKS` | the check names that must be green at a READY head | `Free Tests`, `typecheck` |
|
|
| `FULL_GATE_CHECK` | the check the train gate publishes on the train head | `gstack/full-gate` |
|
|
| `TRAIN_MAX`, `FILL_HOURS` | the train size and the wait before a short train leaves | 5, 3 |
|
|
| `MERGE_METHOD` | `squash` or `merge`; one per repo, never mixed | `squash` |
|
|
| `STAMP_FILES` | the files a version stamp may touch | `VERSION`, `package.json`, `CHANGELOG.md` |
|
|
| `KNOWN_INFRA_FLAKES` | the named infra failures a lane may rerun once | a job that times out on one runner image |
|
|
| `SPEND` | standing paid-eval spend and its cap per rerun | gates, screens, focused cases; cap per rerun |
|
|
| `CONTROLLER` | the thread or name every ask and READY goes to | the Captain's thread code |
|
|
|
|
The repo's `.gstack/ship-policy.json` carries the `train` block (`gate_workflow`,
|
|
`free_workflow`, `full_gate_check`, `max_size`, `fill_hours`), so the queue tool and the
|
|
rules file read the same names. A repo without a `train` block runs the per-PR READY
|
|
flow: the same block, no slots.
|
|
|
|
The rules file is the lanes' contract, and it is pointers plus parameters: each rule is
|
|
one line that names its home in gstack (`docs/captain.md`, the ship-policy, pregate and
|
|
testing docs, the /ship and /review skills, the repository's own AGENTS.md), so no crew
|
|
carries its own copy of how to do things. The rules: session-start install check,
|
|
validation discipline, untrusted external diffs, file ownership, pre-approved spend, the
|
|
READY contract, reports, one routing path (everything through the controller), no merging
|
|
base into a READY branch, the CHANGELOG entry under `[Unreleased]`, the flaky rule,
|
|
approvals, /ship before READY, review convergence, and plan before build. Every rule that rests on the
|
|
owner's words carries the date and the words. A rule that exists only in a crew's file is
|
|
a gap in gstack: write it there, then point at it.
|
|
|
|
## The READY contract
|
|
|
|
A READY is the only thing that puts a PR in a train. A green list state, a passing PR
|
|
page, a "looks good" in chat and a cheap-tier pass are not READYs. The lane sends, as
|
|
printed, the block from:
|
|
|
|
```bash
|
|
~/.claude/skills/gstack/bin/gstack-ship-queue ready --pr "$PR" --no-screen # or --screen [--broad]
|
|
```
|
|
|
|
and adds the lines the tool cannot know. Validate every field before the pin goes near
|
|
`train build`; a missing or wrong field sends the lane back, never into the train.
|
|
|
|
| Field | Must read | Refuse when |
|
|
|---|---|---|
|
|
| `READY #<n> <sha>` | the PR number and the **full 40-character head SHA**, the pin | a short SHA, a fork head, a head pushed after the block was printed |
|
|
| `STAMP_VERSION` | `unstamped` before a slot call; the allocated version after it | a version chosen by the lane before its slot was called |
|
|
| `base` | `BASE` | any other target branch |
|
|
| `ci` | every `REQUIRED_CHECKS` name green at the pin, with counts (`passed=… failed=0 pending=0`) | a pending or failed required check, or a run on another head |
|
|
| `PREGATE` | `pass` for the pin's tree, remote lanes cleared with that head's own CI runs | `incomplete`, or a pass for another tree |
|
|
| `gate` | the paid evidence the lane offers: its own focused cases (named, with results), or the screen it asks for | "ran the cheap tier" offered as merge evidence |
|
|
| `SHIP_RECEIPT` | `pr=<n> head=<pin's first 12>` written by /ship at the pin | `none`, `invalid`, or a receipt head that is not the pin |
|
|
| `screen` | `--no-screen` with the focused cases that passed, or `--screen` with the reason | no choice, or `--no-screen` with no cases named |
|
|
| `READY_RECEIPT` | the receipt path the tool wrote (local and on the notes ref) | missing |
|
|
| flaky cases | each one already in the ledger, with both runs | a rerun that is not logged, or a second failure called a flake |
|
|
| lane statement | "head frozen from now on" | absent |
|
|
|
|
Optional lines a crew may require in its rules file: the gate run id and tree hash the
|
|
lane's own gate covered (useful where lanes gate on their own machines), the issue the PR
|
|
closes, and the key before/after image for the merge ask. Ship the fields as options and
|
|
let the rules file pick; one crew's habit is not the contract.
|
|
|
|
After READY the head is frozen. A push drops the PR from any train and needs a new READY
|
|
at the new head, /ship rerun included. The one exception is the stamp push below.
|
|
|
|
## Merge trains
|
|
|
|
One full gate on the train head covers up to `TRAIN_MAX` READY PRs. The train merges the
|
|
pins in slot order onto `BASE` in a scratch worktree, records each prefix tree (T0 is
|
|
`BASE`, Tk the train head), runs the free tests, dispatches the gate, then merges one
|
|
PR at a time, each only while `BASE` equals the prefix the gate covered modulo stamps.
|
|
Every queue command ends with `NEXT:` and a `GSTACK_RESULT:` line; do what `NEXT` says
|
|
unless this skill says otherwise.
|
|
|
|
### 1. Build
|
|
|
|
```bash
|
|
~/.claude/skills/gstack/bin/gstack-ship-queue train build --prs "$PINS" --dry-run # ORDER, PREFIX, SLOT, ESTIMATE, no spend
|
|
~/.claude/skills/gstack/bin/gstack-ship-queue train build --prs "$PINS" # writes the record, pushes the train branch, dispatches the gate
|
|
```
|
|
|
|
`PINS` is `41@<sha>,42@<sha>` in READY order. Screened PRs take the first slots, then the
|
|
rest in READY order. Fewer than `TRAIN_MAX` PRs wait until the oldest READY is
|
|
`FILL_HOURS` old; one urgent PR goes alone with `--urgent`. Announce `ORDER` to the lanes.
|
|
Refusals happen before any spend and each names its PR: a moved head (get a new READY),
|
|
no receipt at that head, a fork head, a real-code conflict (rebuild without that PR and
|
|
send it back to merge `BASE`; apart from the slot's stamp commit below, this is the only
|
|
time a lane merges `BASE` into a READY branch), and free tests red on the train branch
|
|
(treat as a red train with no paid spend).
|
|
|
|
### 2. Verdict
|
|
|
|
```bash
|
|
~/.claude/skills/gstack/bin/gstack-ship-queue train status --train "$NONCE" # never spends
|
|
~/.claude/skills/gstack/bin/gstack-ship-queue train verdict --train "$NONCE"
|
|
```
|
|
|
|
`GREEN` means a complete census bound to the nonce, head and tree, with `FULL_GATE_CHECK`
|
|
success on the train head. The gate reruns a case that failed on its own verdict once,
|
|
inside the same run; a pass is `GREEN` with `FLAKY N` and the case names, which the daily
|
|
flaky pass reads from the run's flake-records artifact. `RED` lists cases that already
|
|
failed twice: go to section 4. `INCOMPLETE` (infra or incomplete panels, partial shards,
|
|
no record) is never green.
|
|
|
|
### 3. Slot by slot: stamp on call, verify, merge, verify
|
|
|
|
For each slot in `ORDER`, send the lane the slot-call message
|
|
(`captain/templates/slot-call.md`): the PR, the slot, `BASE`'s current SHA and the
|
|
remaining order. The lane runs the restamp against those expectations, merges `BASE`
|
|
into its pin from slot 2 on (and at slot 1 when `BASE` moved), commits **only** the merge
|
|
and the stamp files, pushes, waits for required CI, and answers
|
|
`READY #<n> <stamped sha> STAMP_VERSION=<X>`. A stamp commit that changes one byte
|
|
beyond `STAMP_FILES` (a release summary line, a wording fix, a reflow, even when a tool
|
|
prompt asks for one) fails verification and freezes the queue for everyone.
|
|
|
|
```bash
|
|
~/.claude/skills/gstack/bin/gstack-ship-queue verify --pr "$PR" --train "$NONCE" # VERIFY_TRAIN: may merge as slot i; MERGE_CONDITION: sha=…
|
|
gh pr merge "$PR" --repo "$REPO" --squash --match-head-commit "$MERGE_SHA" # or --merge, per MERGE_METHOD
|
|
~/.claude/skills/gstack/bin/gstack-ship-queue verify --pr "$PR" --train "$NONCE" # VERIFY_TRAIN: merged; BASE's tree is Ti
|
|
```
|
|
|
|
`--match-head-commit` makes the server reject a merge whose head moved after you read it.
|
|
Never merge out of order, never merge anything else while a train is merging, and never
|
|
skip the second verify: a mismatch there is what writes the freeze. When every slot has
|
|
merged, `train prune --train "$NONCE"` deletes the train branch.
|
|
|
|
**Under `changelog_fragments`** (a stamped train: `train build` printed `S1:`..`Sk:` lines)
|
|
there is no slot call and no lane stamp. Per slot, in `ORDER`, run
|
|
`gstack-ship-queue train land --train "$NONCE" --pr "$PR"` until it exits 0. It pushes the
|
|
stamped prefix S_i as a commit on [pin, `BASE`], waits for the policy's `required_checks`,
|
|
merges with `--match-head-commit` and confirms `BASE` is S_i exactly. A rerun resumes from
|
|
its journal, and any other `BASE` freezes the queue as above (docs/ship-policy.md#train-land).
|
|
|
|
**The tree rule.** A gate covers a tree, not a commit. A head whose diff from the gated
|
|
prefix touches only `STAMP_FILES` reuses the gate; that is what lets the lane stamp after
|
|
the gate ran. Anything else is a new tree and needs a new gate.
|
|
|
|
**Who stamps.** Default: the lane stamps on call, so the controller never pushes to a
|
|
branch it does not own. Option: the controller stamps in its own clone and pushes the
|
|
stamp-only commit; same verification, one fewer round trip, one more agent with push
|
|
rights. Pick one per crew in the rules file.
|
|
|
|
**Gate-ahead (option).** While slot i merges, slot i+1 can gate on its branch plus slot
|
|
i's head, stamped with its future version (`gstack-restamp --after`): if slot i lands as
|
|
expected, the real merge is the same tree and the gate is reused. Use it when slots are
|
|
long and the queue is deep.
|
|
|
|
**Severe race (option).** When several severe fixes are close and the next train will be
|
|
urgent, tell all their lanes at once; the first complete READY takes that train and the
|
|
rest join the following one at their own call. Inside a built train the order is fixed
|
|
(the stamp and verify refuse a reorder), so the race is for a train, never for a slot.
|
|
Never hold a train for a PR that has not sent a READY.
|
|
|
|
### 4. A red train: flaky rule, bisect, rebuild once
|
|
|
|
A `RED` case already failed twice in-run, so it is real; never rerun the whole gate.
|
|
|
|
```bash
|
|
~/.claude/skills/gstack/bin/gstack-ship-queue train bisect --train "$NONCE" --spend-cap "$CAP" --dry-run
|
|
~/.claude/skills/gstack/bin/gstack-ship-queue train bisect --train "$NONCE" --spend-cap "$CAP"
|
|
```
|
|
|
|
The bisect runs only the red cases on every prefix. `ATTRIBUTED #<pr>`: rebuild without
|
|
it and send that lane the case and both runs. Red on every prefix including T0: `BASE`
|
|
is broken; fix forward first, through an urgent one-PR train, or through
|
|
`train build --repair` when the queue is frozen (a freeze refuses `--urgent`). Every prefix green: the
|
|
bisect earns exactly one unchanged `train rebuild`; report both runs, and a second red
|
|
goes to a repair lane. Any other pattern: no rebuild, a repair lane. An infra-only or
|
|
incomplete census earns one re-dispatch (`train rebuild --infra`). Bisect results never
|
|
turn a train green; nothing else is retried.
|
|
|
|
### 5. The backstop, the freeze and the babysit
|
|
|
|
A gate on every push to `BASE` checks whether a green train record covers `BASE`'s tree
|
|
modulo stamps; if none does (a merge outside a train, a drifted slot), it dispatches the
|
|
full gate on `BASE`, and a red result writes a freeze record and opens an issue. While
|
|
frozen, trains and merges refuse. Clear it with one repair PR at its exact head,
|
|
`train build --repair --prs <pin>`, which gates `BASE` plus the repair; after it merges,
|
|
verify prints `FREEZE: cleared` and the next train takes the PRs still waiting.
|
|
|
|
After each merge, watch `BASE`'s required workflows and the release until they publish.
|
|
A red `BASE` freezes the queue until it is root-caused; rerun only with the cause proven
|
|
to be outside the merged PR. Reruns on `BASE` are never a way to get a green.
|
|
|
|
## The digest
|
|
|
|
The controller keeps one digest file (`captain/templates/DIGEST.md`), rewritten at every
|
|
turn, three sections in this order, each row with its thread, one line, and an age:
|
|
|
|
1. **Needs you**: decisions only the owner can make, in the ask format below (the
|
|
Captain's name, one lettered line per decision). Empty means the owner has nothing to do.
|
|
2. **New**: what landed or changed since the last digest (merges with versions, READYs
|
|
received, trains built, incidents), newest first.
|
|
3. **In progress**: every working lane, what it owns, its state (building, READY, in
|
|
train slot i, blocked on …) and how long since it last reported. A thread the Captain
|
|
archives leaves this list in the same turn.
|
|
|
|
Ages make stalls visible: a lane in progress with no report for hours, or a READY waiting
|
|
longer than `FILL_HOURS`, is the controller's next action.
|
|
|
|
## Approvals: three shapes
|
|
|
|
Standing approvals (the ones in the rules file, with date and words) are cited, never
|
|
re-asked. A fresh ask is always needed for a product or behaviour decision, spend over a
|
|
cap, anything published outside the repository, any change to a threshold, budget, trial
|
|
count or case kind, closing someone's contribution without landing it, and anything
|
|
destructive. The lane-side details (record command, mirror, secret scan) are in
|
|
`capy/skill/gstack/captain.md`; the controller enforces the shapes.
|
|
|
|
**A. The per-thread approval record.** Every lane keeps an `## Approvals` table in its
|
|
report and PR body: date, what was asked, the owner's exact words, who relayed them, what
|
|
the answer covers, what is still pending. Render it with `gstack-approvals render`; never
|
|
type rows by hand. A thread with no record says "No approvals recorded"; a lost record
|
|
says so instead of looking empty.
|
|
|
|
**B. The plan ask**, in this order, written for someone outside the project (no gate codes
|
|
in the body): before → after with concrete examples; why it matters, with numbers; what
|
|
caused the work (issue, run, report, or the owner's words quoted); the big decisions
|
|
(options, pick, why, what would flip it); what we need from you in the ask format; and a
|
|
closing **"do it"** reply that takes every recommendation at full scope, follow-ons
|
|
included. `gstack-owner-brief render --captain --captain-name "<Captain name>"` prints this
|
|
shape from a plan; relay it whole. Template: `captain/templates/plan-ask.md`.
|
|
|
|
**The ask format** (every ask to the owner, the digest's Needs you included), explained like
|
|
the owner is five: the Captain's name, then one lettered line per decision, letters unique
|
|
across the list: `- A: <code> hit problem p, which happens when h. We could fix it in these
|
|
ways: 1) x; 2) y; 3) z. Recommendation: x because why. A1 to take recommendation.` The
|
|
recommendation is option 1; "do it" takes every recommendation in the last list, and a named
|
|
option ("A2") takes that option instead. When the owner has to do something, the line
|
|
carries the exact link to the page (never a menu path; a Capy app page gets its exact path
|
|
and field), numbered steps in the simplest words, and the one word to reply when done.
|
|
Home: `docs/captain.md`, The ask format.
|
|
|
|
**Plan before build.** A lane that builds something runs a sized `/autoplan` on its brief as
|
|
its first act after the session-start check and sends the controller the plan ask above; the controller relays it, and the
|
|
lane builds only after the owner's word is answered into the run and it reports `APPROVED`.
|
|
New scope taken on part-way gets its own plan. Read-only lanes, the flaky pass's repairs,
|
|
slot stamps, CHANGELOG resolves, cap re-pins under a grant, S1 emergencies (broken install
|
|
or upgrade, red main, a security bug) and fixes from an already-diagnosed, audited problem
|
|
list are exempt; every other fix gets a one-round, eng-only plan. The brief a
|
|
controller hands a lane, with its `Plan: required | approved | exempt` line, is
|
|
`captain/templates/brief.md`; the rule is `docs/captain.md` (Plan before build).
|
|
|
|
**C. The merge ask.** Every READY the controller brings to the owner, and every PR body:
|
|
before/after images (uploaded with the host's PR asset tool, each captioned with what
|
|
changed and why; a skill or CLI change renders its output with `gstack-pr-shots`), why
|
|
in two or three sentences, the Approvals table, and the evidence (checks at the head,
|
|
gate or train result, flaky cases). The controller's merge report links the PR and shows
|
|
the key image. Template: `captain/templates/merge-ask.md`.
|
|
|
|
The owner decides what needs the owner; the controller decides everything else that
|
|
needs no access, credentials, spend over cap or a cited approval (a default flip, a
|
|
one-way door, a trust posture change go to the owner).
|
|
|
|
## The daily flaky-fix pass
|
|
|
|
Once a day (a scheduled automation; prompt in
|
|
`captain/templates/automation-flaky-pass.md`), one lane reads two sources and fixes
|
|
causes:
|
|
|
|
- the gate runs' flake-records artifacts since the last pass (every `FLAKY N` case, with
|
|
both attempts), and
|
|
- the free suites' retried cases in the same window.
|
|
|
|
Each event is one line in the ledger (`captain/templates/flaky-ledger.md`): date, case,
|
|
tier, PR and head, the failing run, the passing run, a one-line symptom. The pass groups
|
|
by case, fixes the worst repeat first, and moves entries to **Fixed** with the PR or to
|
|
**Won't fix** with a reason. A fix PR shows the failure first: a forced probe or a
|
|
regression test that is red on `BASE` and green with the fix. Weakened assertions, raised
|
|
thresholds, added trials, changed case kinds, skipped cases and longer timeouts are not
|
|
fixes and need the owner's words.
|
|
|
|
Rerun rules the ledger enforces: a case that failed on its own verdict reruns once, in the
|
|
same run or at the same tree; a pass is a flake to log and fix, a second failure is real.
|
|
Harness, infra and incomplete reds are never flakes. One named infra failure from
|
|
`KNOWN_INFRA_FLAKES` may be rerun once per PR and must be stated in the READY; a second
|
|
occurrence escalates. Every rerun's spend is reported.
|
|
|
|
## Reports and waves
|
|
|
|
A lane that finishes writes `reports/<code>.md` (`captain/templates/report.md`): Outcome,
|
|
PR (number, head, version, checks), Needs (what the owner must do), Cost, Lesson, and
|
|
the Approvals table. The controller reads reports before it reads chat, and turns every
|
|
Lesson into a rule in the rules file, a line in the standing-rules file
|
|
(`captain/templates/AGENTS.md`), or a filed tool fix. The controller's own log is
|
|
append-only and timestamped (`captain/templates/NOTES.md`); the digest is derived from it.
|
|
|
|
Serial slots cost tens of minutes each, so small fixes in one area travel together: when
|
|
a lane's diff is small and open PRs touch the same area, fold them into one wave PR with
|
|
every contributor credited (`Contributed by @handle`, `Co-authored-by`, `Supersedes
|
|
#<pr>`), and never merge a contributor's PR directly from its fork.
|
|
|
|
## Where the protocols differ, ship both
|
|
|
|
Crews that run this model well disagree on a few knobs. Each is a line in the rules
|
|
file, not a rule of this skill: squash versus merge commits; lane stamps versus
|
|
controller stamps; a gate run id and tree hash in the READY versus the tool's receipt
|
|
alone; a screen on every PR versus the lane's own focused cases; and whether reruns of a
|
|
named infra flake are allowed at all. Write the crew's pick with a date, and change it
|
|
with the owner's words.
|