v1.91.254.0 pregate: starter policy, benign-finding allowlist, environment vs diff failures, per-run scratch

Merge origin/main (4d48d39e51, v1.91.253.0) and restamp at slot 1.

FILES: CHANGELOG.md VERSION agents-digest/gstack-AGENTS.md package.json
MERGE_CONDITION: sha=c3c96b3ad0af6a486958872020ace795e83a8f2d
NEW_VERSION=1.91.254.0

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-10-26 20:05:22 +00:00
commit eb6bebf363
133 files changed
+17316 -91

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,194 today; each new skill ratchets it)
`CATALOG_BUDGET_TOKEN_EQUIVALENTS` (1,251 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
+5 -4
View File
@@ -55,6 +55,7 @@ gstack/
├── deslop-shared-libs/ # Recommendations-only audit for worthwhile shared-code extractions
├── 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
├── 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)
@@ -70,7 +71,7 @@ gstack/
├── freeze/ # /freeze skill; bin/check-freeze.sh (PreToolUse edit-boundary hook; sources careful/bin/hook-extract.sh, fails closed)
├── guard/, unfreeze/ # /guard (careful + freeze in one), /unfreeze
├── gstack-upgrade/ # /gstack-upgrade skill + migrations/ (run after ./setup during an upgrade)
├── bin/ # CLI utilities (gstack-render.ts = render a local HTML file through Aside or the engine, gstack-design-detect.ts = probe/scan through a user-installed impeccable engine; gstack-design-md.ts = open DESIGN.md check/convert/tokens/mark; gstack-repo-mode, gstack-slug, gstack-config, gstack-wtree, gstack-evidence, gstack-issue-guard, gstack-relink, gstack-memorable, etc.)
├── bin/ # CLI utilities (gstack-render.ts = render a local HTML file through Aside or the engine, gstack-design-detect.ts = probe/scan through a user-installed impeccable engine; gstack-design-md.ts = open DESIGN.md check/convert/tokens/mark; gstack-design-lint = the free deterministic design lint over HTML/CSS/components, --render measures in the browser (docs/design-lint.md); gstack-repo-mode, gstack-slug, gstack-config, gstack-wtree, gstack-evidence, gstack-issue-guard, gstack-relink, gstack-memorable, etc.)
├── document-release/ # /document-release skill (every-ship pre-verification audit; standalone doc updates + Diataxis coverage map)
├── document-generate/ # /document-generate skill (Diataxis doc generator: tutorial/how-to/reference/explanation)
├── cso/ # /cso skill (OWASP Top 10 + STRIDE security audit)
@@ -88,7 +89,7 @@ gstack/
│ └── dist/ # Compiled binary
├── agents-digest/ # Committed 2KB instruction-tier rules digest (gstack-AGENTS.md) for rules-reading hosts
├── extension/ # Chrome extension (side panel + activity feed + CSS inspector)
├── lib/ # Shared libraries (aside-render.ts = local-HTML rendering, Aside first, engine fallback; design-catalog.ts = the typed design anti-pattern catalog every design skill renders from; design-detect-contract.ts = detector sentinel vocabulary; design-md.ts = open DESIGN.md reader/writer; dom-dump-script.ts + generated dom-dump.js = rendered-DOM dump for the detector; review-evidence.ts = review-start receipt binding, computed freshness and shared-code snapshot eligibility; frontend-scope.ts; claude-bin.ts, error-handling.ts, worktree.ts, egress-receipt.ts, context-bill.ts, redact-engine.ts, tracker-guard.ts, version-source.ts, code-intelligence/)
├── lib/ # Shared libraries (aside-render.ts = local-HTML rendering, Aside first, engine fallback; design-catalog.ts = the typed design anti-pattern catalog every design skill renders from; design-lint/ = the static and render tiers behind bin/gstack-design-lint, ported from hallmark's sloplint; design-detect-contract.ts = detector sentinel vocabulary; design-md.ts = open DESIGN.md reader/writer; dom-dump-script.ts + generated dom-dump.js = rendered-DOM dump for the detector; review-evidence.ts = review-start receipt binding, computed freshness and shared-code snapshot eligibility; frontend-scope.ts; claude-bin.ts, error-handling.ts, worktree.ts, egress-receipt.ts, context-bill.ts, redact-engine.ts, tracker-guard.ts, version-source.ts, code-intelligence/)
│ └── diagram-render/ # Vendored mermaid + excalidraw runtimes, built into one offline bundle the renderer loads
├── patches/ # bun `patchedDependencies` patches (playwright-core windowsHide)
├── docs/designs/ # Design documents (incl. IMPECCABLE_INTEROP.md = the design detector / catalog / open DESIGN.md record, and fork-port-residual-2026-09/ evaluation evidence)
@@ -102,7 +103,7 @@ gstack/
├── SKILL.md # Generated from SKILL.md.tmpl (don't edit directly)
├── SKILL.md.tmpl # Template: edit this, run gen:skill-docs
├── ETHOS.md # Builder philosophy (Boil the Ocean, Search Before Building)
├── NOTICE.md # Third-party notices: material derived from impeccable and the DESIGN.md spec (both Apache-2.0)
├── licenses/ # Verbatim license texts for the notices above (Apache-2.0.txt)
├── NOTICE.md # Third-party notices: material derived from impeccable and the DESIGN.md spec (Apache-2.0), Elaya's design skills and hallmark's sloplint (MIT)
├── licenses/ # Verbatim license texts for the notices above (Apache-2.0.txt, MIT-elaya.txt, MIT-hallmark.txt)
└── package.json # Build scripts for browse
```
+291
View File
@@ -0,0 +1,291 @@
# Running a Captain crew on Capy with gstack
This page is for a team that wants one coordinating agent (a Capy Captain, or any
controller agent) to run many worker threads against one repository, with gstack as the
engineering harness. It covers the drive layout, the crew rules file, the READY contract,
merge trains, approvals, the digest and the daily flaky pass. The skill that teaches the
controller the same model at run time is [`/captain`](skills.md#captain) (it also answers
to `/crew`); the lane-side rules for a thread working under a Captain are in
[`capy/skill/gstack/captain.md`](../capy/skill/gstack/captain.md) and
[capy-install.md](capy-install.md#working-under-a-captain). Every file named below has a
copy-paste template under [`captain/templates/`](../captain/templates/).
## The model in one paragraph
Lanes build on their own branches and machines. A lane's job ends at an **unstamped
READY**: a block printed by `gstack-ship-queue ready` plus the full head SHA, the screen
choice and the flaky cases. The controller validates the block, batches READY pins into a
**merge train**, reads one full gate on the train head, then merges slot by slot: it calls
the slot, the lane stamps the version on call, the controller verifies the tree, merges
with the expected head, verifies again. A red train is bisected on its red cases and
rebuilt at most once. Every decision the owner makes travels through the controller in a
fixed shape and is recorded in the owner's words. Flakes are logged in a ledger and fixed
by a daily pass; nothing is retried into green.
## Roles
- **The owner** decides what only the owner can decide: product and behaviour, spend over
a cap, anything published outside the repository, thresholds and budgets, closing a
contribution, anything destructive. Every answer is recorded in the owner's words.
- **The controller** (the Captain) runs the crew: starts lanes on disjoint files, keeps the
rules file, the digest and the notes, validates READYs, builds and merges trains, calls
slots, relays asks to the owner in the three shapes, runs the flaky pass, and decides
everything that needs no access, credentials, spend over cap or a cited approval.
- **A lane** (a crew thread) owns named files, builds on its own branch and machine,
runs /ship, sends one READY block, stamps on call (merging the base into its pin only
as part of that stamp commit, or when the controller names a real-code conflict),
writes one report, and never merges a PR.
- **The controller never commits to a lane's branch**, and a lane never pushes to another
lane's. One committer per shared tree.
## Drive layout
The Captain's drive is mounted read-write on every crew thread, so it is the handoff
surface. Keep it small and predictable:
| Path | Who writes | What it holds |
|---|---|---|
| `AGENTS.md` | the controller | standing rules and the owner's decisions, each with date and words ([template](../captain/templates/AGENTS.md)) |
| `CREW.md` | the controller | the lanes' contract ([template](../captain/templates/CREW.md)) |
| `DIGEST.md` | the controller | Needs you / New / In progress, rewritten every turn ([template](../captain/templates/DIGEST.md)) |
| `NOTES.md` | the controller | the append-only timestamped log the digest is derived from ([template](../captain/templates/NOTES.md)) |
| `skills/<name>/SKILL.md` | the controller | crew procedures the lanes load (the runbooks) |
| `reports/<code>.md` | each lane, once | Outcome, PR, Needs, Cost, Lesson, Approvals ([template](../captain/templates/report.md)) |
| `handoffs/` | lanes | datasets and specs the next lane needs; `handoffs/flaky-ledger.md` is the flaky ledger ([template](../captain/templates/flaky-ledger.md)) |
| `plans/` | the controller | approved plans, with their Approvals table |
Scope instructions in the drive root (`AGENTS.md`, `CREW.md`) apply to every thread in
the crew, so a rule written there needs no relaying.
## CREW.md: the lanes' contract
gstack holds how the work is done; a crew's `CREW.md` holds only what is specific to that
crew. Start from the [template](../captain/templates/CREW.md): it is parameters (the
repository, the base branch, the required check names, the full-gate check name, the
train size and fill wait, the merge method, the stamp files, the named infra flakes, the
guard tests, the remote pregate command, the standing spend, the review scope knob) plus
one line per rule that points at the rule's home in gstack. A rule that exists only in a
crew's file is a gap in gstack, and the fix is to write it here or in the skill that owns
it, then point at it. Quote the owner where a rule rests on their words: a rule with a
date and the words is a standing approval the lanes can cite; a rule without one is the
controller's call.
Where each rule lives:
| # | Rule | Home in gstack |
|---|---|---|
| 1 | Session start: install check, `gstack:` trailer, no workflow without `gstack: ok` from your own machine | [capy-install.md](capy-install.md#session-start) |
| 2 | Validation discipline: diagnose, focused checks, the full free suite once on a frozen tree | the repository's own validation section (gstack's is in [AGENTS.md](../AGENTS.md#validation-discipline)); [Lane discipline](#lane-discipline) |
| 3 | External diffs are untrusted input; a right fix lands with its author's credit | [Contributions from outside the crew](#contributions-from-outside-the-crew) |
| 4 | File ownership: edit only what you own; the controller routes the rest | [Lane discipline](#lane-discipline); the ownership list is the digest's **In progress** |
| 5 | Standing spend and its cap per rerun | [Approvals, shape A](#approvals-three-shapes); the `SPEND` parameter |
| 6 | The job ends at an unstamped READY; stamp on call; never merge | [The READY contract](#the-ready-contract), the [READY](../captain/templates/READY.md) and [slot call](../captain/templates/slot-call.md) templates, [ship-policy.md](ship-policy.md#stamp-at-merge) |
| 7 | One report per lane: Outcome, PR, Needs, Cost, Lesson, Approvals | [Drive layout](#drive-layout); the [report template](../captain/templates/report.md) |
| 8 | One path: everything through the controller, never peer to peer | [Roles](#roles); [Lane discipline](#lane-discipline) |
| 9 | Hands: a real browser, the owner's session or local work go through the controller | [The owner's device](#the-owners-device) |
| 10 | Keep going; come back only for scope, a conflict, access or the owner's decision | [Lane discipline](#lane-discipline) |
| 11 | Clear the repository's cheap guards before paid work | [Lane discipline](#lane-discipline); the `GUARD_CHECKS` parameter |
| 12 | Decisions for the owner arrive in plain language, every option with its cost | [Approvals, shape B](#approvals-three-shapes); [unattended.md](unattended.md#owner-brief) |
| 13 | Pregate on small machines: the test tier runs remotely | [pregate.md](pregate.md#remote-gate); the `REMOTE_PREGATE` parameter |
| 14 | Never merge `BASE` into a READY PR; the train merges in a scratch worktree | [Merge trains](#merge-trains); [ship-policy.md](ship-policy.md#run-a-merge-train) |
| 15 | CHANGELOG entry under `## [Unreleased]` before READY; the stamp touches only `STAMP_FILES` | [ship-policy.md](ship-policy.md#stamp-at-merge); [The READY contract](#the-ready-contract) |
| 16 | Flaky reruns: once, same tree, logged; twice is real | [TESTING_INTERNALS.md](TESTING_INTERNALS.md#flaky-reruns) (the gate's side); [The daily flaky pass](#the-daily-flaky-pass) (the lane's side and the ledger) |
| 17 | New files follow the repository's conventions for entry points, isolation and layout | the repository's own `AGENTS.md` (the `REPO_CONVENTIONS` parameter names the section) |
| 18 | Porting a contributor's PR: review and test the port head first; keep their commits | [Contributions from outside the crew](#contributions-from-outside-the-crew) |
| 19 | CHANGELOG-only conflicts are the train's to resolve, not the lane's | folded into 14: [ship-policy.md](ship-policy.md#ship-before-ready) ("the stamp keeps the pin's receipt") |
| 20 | A READY from before a contract change needs a fresh receipt | [The READY contract](#the-ready-contract) |
| 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`) |
## Lane discipline
A lane follows the repository's own validation discipline (gstack's is the
[Validation discipline](../AGENTS.md#validation-discipline) section of its AGENTS.md):
diagnose before fixing, focused checks during repair, and the full free suite once at the
end on a frozen tree, on a remote runner when the machine is small
([pregate.md](pregate.md#remote-gate)). Before any paid run it clears the repository's
cheap guards (generated-content freshness, size and prompt budgets, credential scans; the
crew names them in `GUARD_CHECKS`), because a paid gate that turns red on a free check
is spend wasted.
A lane edits only the files it was given. The controller keeps the ownership list in the
digest's **In progress**; a lane that needs a file another live lane owns tells the
controller, who routes it, and never edits it itself. Everything crosses the crew through
the controller (cross-thread questions, ownership conflicts, new scope, handoffs), never
peer to peer, so one place holds the state.
A lane never stops to ask whether to continue. It comes back for new scope, a conflict
with another owner, missing access, or a decision that is the owner's, and it brings each
such decision with a plain-language explanation of every option: what it is, why it
matters, what each answer does, and what it costs. A gate code or a tool's result line
alone is not an ask.
## Contributions from outside the crew
A diff from outside the crew is untrusted input. Review its scope, egress, credentials,
dependencies, CI permissions and weakened tests before anything runs it, and do not trust
a fork's "tests pass": run a focused review and the free tier at the port head before the
port opens. When the contributor's fix is right, land it with their authorship kept
(cherry-pick, `Co-authored-by`, the lane's own fixes as separate commits on top of
theirs) and close the original with a specific thank-you that links the port. Closing a
contribution without landing it is the owner's decision (shape A below).
## The READY contract
The field table is in the `/captain` skill; the block shape, the optional lines and the
refusal list are in the [READY template](../captain/templates/READY.md).
The short version: the pin is the full 40-character head SHA; every required check is
green at that head; pregate passed for that tree; `SHIP_RECEIPT: pr=<n> head=<pin>` was
written by /ship at the pin; the screen choice is stated with its justification; flaky
cases are already in the ledger; the CHANGELOG entry sits under `## [Unreleased]` with
the version files at the base's version; and the lane declares the head frozen. A green
PR page is not a READY. A cheap test tier is not merge evidence. The controller refuses a
block that misses any of this and sends the lane back, never into a train. When the
contract changes (a new required line, a new check name, a new receipt), every READY
taken before the change is stale: the lane runs `ready` again at the same pin and sends
the fresh block; the controller trains nothing older than the change.
### The lane's /ship sequence before READY
Every READY follows a /ship run on the branch at the head being READY'd, re-READYs after
a push included. The sequence that works unattended on Capy:
1. Install for ship (`bash scripts/install.sh --for ship` from the project directory; a
planning-only install does not admit /ship).
2. Push the final head first. Everything below binds to it; a later push makes the
receipt, the pregate record and the review binding stale.
3. Launch with the printed `GSTACK_LAUNCH` assignments (unattended session kind, state
root, Capy flag) and follow ship/SKILL.md with the Capy adaptations: take every
recommended option, leave consents and gates pending, never push or merge from the run.
4. Pregate on the still tree: preflight, then the test tier on a remote runner when the
machine is small; clear the macOS and Windows lanes with this head's own green CI run
URLs. Never run a bare pregate afterwards (it resets the regen check).
5. Reviews: mint tokens, run both passes read-only, finish them so the review log binds
to the current tree.
6. Docs audit: snapshot the documentation candidates read-only and record the vetted
section.
7. Body and receipt: compose the PR body (What changed, Why, Approvals first on Capy),
write the ship receipt into it, post body and title.
8. Terminal line: `gstack-ship-receipt result --pr <n>` prints
`GSTACK_RESULT: skill=ship status=complete`.
9. READY: `gstack-ship-queue ready --pr <n> --pregate <pregate.json> --screen|--no-screen`;
send the printed block.
10. After the slot call, the stamp commit keeps the pin's receipt; re-READY with the same
receipt, no second /ship.
## Merge trains
gstack's queue tool drives trains when the repository's `.gstack/ship-policy.json` carries
a `train` block ([ship-policy.md](ship-policy.md#run-a-merge-train)). The controller's
loop, per train:
1. `train build --prs <pins> --dry-run`, then without `--dry-run`: the record, the train
branch, free tests, one full gate.
2. `train verdict`: GREEN (with `FLAKY N` when an in-run rerun turned a case green), RED
(cases that failed twice), or INCOMPLETE (never green).
3. Per slot, in order: the [slot call](../captain/templates/slot-call.md) to the lane; the
lane stamps and pushes only the base merge and the stamp files; `verify --train`;
`gh pr merge --match-head-commit`; `verify --train` again.
4. `train prune` when every slot has merged.
A red train: `train bisect` on the red cases over every prefix. An attributed PR leaves
the train and goes back to its lane; red on the base branch itself is fixed forward
through an urgent one-PR train (or `train build --repair` while the queue is frozen, which
refuses `--urgent`); all prefixes green earns exactly one unchanged rebuild;
anything else goes to a repair lane. An infra-only census earns one re-dispatch. A
backstop gate runs on every push to the base branch and freezes the queue when it finds
a tree no green train covered; one repair PR at its exact head clears the freeze.
**Slot 2 and later.** The base branch already carries the earlier slots' stamps, so a
stamp on the bare pin leaves the PR conflicting on the CHANGELOG. The lane merges the base
into its pin first (expect a CHANGELOG-only conflict; resolve it with `mergeChangelogs`
from `lib/changelog-check.ts`, the `bun -e` one-liner in
[ship-policy.md](ship-policy.md#run-a-merge-train), never by hand, and grep for conflict
markers before stamping), then restamps, then commits only the merge and the stamp
files. In practice slot 1 needs the same merge whenever the base moved after the pin was
taken. A stamp commit with one extra byte freezes the queue for every
lane behind it.
Options the controller writes into CREW.md rather than inheriting: squash versus merge
commits; lane stamps versus controller stamps; gate-ahead (slot i+1 gates on slot i's
head while i merges); the severe race (several severe lanes told at once that the next
urgent train is theirs, first complete READY takes it; never a race for a slot inside a
built train); whether a READY also carries a gate run id and tree hash.
## Approvals: three shapes
**A. The record.** Every lane keeps an `## Approvals` table in its report and PR body,
rendered by `gstack-approvals render`: date, what was asked, the owner's exact words, who
relayed them, what the answer covers, what is pending. Standing approvals are cited with
their rule; a fresh ask is always needed for a product or behaviour decision, spend over
a cap, anything published outside the repository, a threshold, budget, trial-count or
case-kind change, closing a contribution without landing it, and anything destructive.
Standing spend is the commonest standing approval: `SPEND` in CREW.md names what the
owner pre-approved (train gates, screens, bisects, the backstop, focused paid cases) with
the owner's date and words and the cap per rerun; anything over the cap is a fresh ask.
**B. The plan ask** ([template](../captain/templates/plan-ask.md)): before → after, why it
matters, what caused this work, the big decisions, what we need from you with the
recommended one-word reply as the default, and a closing "do it" reply that takes every
recommendation at full scope. `gstack-owner-brief render --captain` prints it from an
autoplan run.
**C. The merge ask** ([template](../captain/templates/merge-ask.md)): captioned
before/after images, why, the Approvals table, and the evidence. /ship writes the PR
body in this shape on Capy; the controller's merge report shows the key image inline.
## The digest
`DIGEST.md` is rewritten every controller turn: **Needs you** (decisions only the owner
can make, each with its one-word default; empty is the goal), **New** (what landed or
changed, newest first), **In progress** (every lane, what it owns, its state, the age of
its last report). Ages turn stalls into the controller's next action.
## The daily flaky pass
A scheduled automation starts one lane a day with the
[flaky-pass prompt](../captain/templates/automation-flaky-pass.md). It reads the gate
runs' flake-records artifacts and the free suites' retried cases, writes one ledger line
per event, groups by case, and fixes the worst repeat first with a PR that shows the
failure red before green. It never reruns a gate and never touches a threshold, trial
count or case kind. On Capy, create it with the automation tool, scheduled in the owner's
timezone, running as a thread in the crew so its report lands in `reports/`.
The lane's side of the same rule: in CI the gate reruns a case that failed on its own
verdict once, in the same run ([TESTING_INTERNALS.md](TESTING_INTERNALS.md#flaky-reruns));
a pass is green with `FLAKY N`, and a case that failed twice is a real red that nobody
reruns by hand. In a lane's own focused paid runs it reruns only the failed case, once, at
the same tree: a pass goes into the ledger (case, PR, head, both runs, a one-line symptom)
and is named in the READY; a second failure is real. A harness, infra or incomplete red is
never a flake. One named failure from `KNOWN_INFRA_FLAKES` may be rerun once per PR and
is stated in the READY. A red train is the controller's (bisect, rebuild once), and every
rerun reports its spend.
## The owner's device
Some actions exist only in the owner's logged-in browser or on the owner's machine:
approving a deployment review, switching the GitHub account a run used, a local-only
build. A lane that needs one asks the controller; the controller does it through the
owner's device (on Capy, a paired device and the owner's browser) and records what it
did. Before approving a deployment, check the run's pending deployments list; approve
only the run the lane named.
## The lessons loop
Every report ends with one Lesson. The controller reads lessons before chat, and each
one becomes a rule in CREW.md, a line in AGENTS.md, or a tool fix filed as an issue;
nothing stays a lesson for long. Rules carry the thread that produced them, so a new lane
can read the incident behind the rule.
## Setting it up on Capy
1. Create the Captain and give it the drive layout above; paste the filled `CREW.md`
and an empty `DIGEST.md`.
2. Add the `train` block to the repository's `.gstack/ship-policy.json` and name the
same check names in `CREW.md`.
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.
+4 -1
View File
@@ -438,7 +438,10 @@ with `gstack-owner-brief render --captain`, answers are recorded with
`gstack-approvals`, and /ship's PR body opens with before/after images
(`gstack-pr-shots`), Why and Approvals. The guide is
[`capy/skill/gstack/captain.md`](../capy/skill/gstack/captain.md);
`GSTACK_CAPY_ASKS=0` drops the block for one session.
`GSTACK_CAPY_ASKS=0` drops the block for one session. The controller's side (the
READY contract, merge trains, the digest, the daily flaky pass, with templates for the
crew rules file and the Captain's drive) is the `/captain` skill and
[captain.md](captain.md).
## Raising the supported minimum
+391
View File
@@ -0,0 +1,391 @@
# design-lint: the free, deterministic design check
`bin/gstack-design-lint` reads a page (or a directory of pages, stylesheets
and components) and prints every machine-checkable design tell it finds, with
a file and line, in under a second. With `--render` it also measures the page
in gstack's own browser: the fold, the word budget per screen, contrast
against the painted pixels, focus rings by tabbing, crowded headings, text
painted under other elements, the share of the page that is photographs.
Nothing is installed and nothing is paid for: the static tier is TypeScript
over the files, and the render tier drives the same engine `gstack-render`
picks (Aside when it is running, otherwise the browse daemon `./setup` built).
The rules are ported from [Nutlope/hallmark](https://github.com/Nutlope/hallmark)'s
`sloplint` (MIT; see NOTICE.md and licenses/MIT-hallmark.txt), rewritten in
gstack's voice and mapped onto gstack's severity words. The copy tier is
`gstack-design-craft`'s content-realism and ship-floor check under the same
contract. Phase 1 (this document) is the tool alone; /design-html and
/design-review call it in Phase 2.
## Usage
```
gstack-design-lint <file-or-dir ...> [--render] [--url <http://…>] [--scope page|component] [--job <job>] [--json] [--rules]
```
- Reads `.html`, `.css`, `.jsx`, `.tsx`, `.vue`, `.svelte` and `.astro`.
Tailwind class strings in components are read too, including `cn()`,
`clsx()`, `cva()` and their kin. `node_modules`, dot folders and build
output below the top level are skipped; a symlink is followed once and lint
roots and the working directory are read through their real paths; a file
over 2 MB is a bundle, not a page, and is skipped with a warning on stderr.
A page or stylesheet the lint skipped (over the cap, unreadable,
unparseable; a linked sheet or an inline `<style>` block included) is also
one `internal-error` finding at ask, so a page whose CSS the lint could not
read is not a pass; only a linked stylesheet that does not exist is warned
about and otherwise ignored (its tokens are simply absent). A
relative stylesheet href may not climb out of the project (the nearest
directory holding a `package.json` or `.git`), by its path and by its real
path, so a symlink inside the project that points outside it is not read
either; outside any project it is bounded by the working directory the
lint runs in, and only `.css`
files are read.
- Markup inside an HTML comment is dead for every rule, a commented-out
`<style>` or `<script>` included; a comment that never closes runs to the
end of the file, as it does in the browser. A tag start with no `>` (a stray
`<img `) and any `<` that starts no tag (`<1`, `a < b`) are read as text
and dropped from the markup, so a page whose tail is ten thousand of them
costs its length; a `<!doctype>` or `<?xml?>` declaration stays. A tree nests at most 512 elements deep
(the browser's own cap); a start tag deeper than that is read as empty. An
open heading is closed by the next heading start tag, an open `<p>` by the
next block start, and a repeated `<li>`, `<button>`, `<a>`, `<label>`,
`<select>`, `<form>` or table cell by its next start tag, as the browser
does, so a page of unclosed elements costs its length to lint. A nested
CSS selector is expanded against its parent (`&` replaced, a bare selector
descendant-joined) up to 8192 characters, and a sheet may store at most
four times its length plus 64 KB of expanded selectors; past either bound
the nested selector stands as written, so a sheet costs its length too.
Nesting is read 64 levels deep; a rule nested below that is dropped, not
parsed.
- A page is linted with the stylesheets it links (`<link rel=stylesheet>`,
`@import`, and anything they reach); a directory of components shares the
project's tokens and global state styles, so a focus ring declared once in
`globals.css` is not reported missing on every component.
- `--render` measures every HTML page found; `--url` measures a running dev
server (and implies `--render`). When no browser is available the render
tier is skipped and the summary says why.
- `--scope component` skips the page-only rules (the nav, the footer, the
eyebrow, section gaps, the wireframe count) when linting a component alone.
- `--job <job>` names what the visitor came to do (`book`, `buy`,
`subscribe`, `attend`, `visit`, `hire`, `listen`, `join`, `try`,
`sign up`): an email sign-up as the page's close is only a finding when the
job is not one where a mailing list is native.
- `--json` prints `{ summary, files, findings }`; `--rules` prints the table below.
## Output
One line per finding, then one summary line:
```
LINT: <blocks|ask|polish|waived> <rule-id> <file>:<line> <evidence> | fix: <fix>
LINT_SUMMARY: blocks=N ask=N waived=N polish=N render=<ran|skipped: reason>
```
Page-supplied text in a finding never holds the line's own ` | fix: `
separator (it is printed as ` / fix: `), so splitting a line on its first
` | fix: ` always reads the fix; a program reading many fields should take
`--json`.
Exit **2** when anything blocks, **1** when an ask is unanswered, **0**
otherwise (**3** on a usage error). Findings sort blocks, ask, waived, polish,
then by rule, file and line.
## Tiers
The lint uses gstack's severity words, not a fourth scale:
| Tier | Meaning | hallmark's word |
|---|---|---|
| **blocks** | Never waivable. Correctness, access, honesty and the bans: a card in a card, a purple-blue gradient, a missing focus ring, contrast under threshold, a reveal that hides content until JavaScript runs. | Floor |
| **ask** | A default a model falls into. Fix it, or waive it on the record with a reason about this page. | Reflex |
| **polish** | Craft detail invisible at arm's length. Printed, never blocks. | Finish |
`waived` marks an ask or polish finding a waiver answered. **Blocking and
waived are never summed**: `blocks=0` is always a true sentence about the
Floor. Three house rules hallmark holds at Floor stay at ask here on purpose:
the eyebrow (`kicker-above-heading`; a figure caption or a plan name above a
heading is often right), the 80-word screen (`screen-word-budget`; a Read-mode
page legitimately exceeds it) and the dash (`em-dash-overuse`; gstack's rule is
overuse, not a ban). A blocking rule the lint can only *suspect* (an invented
metric it cannot prove invented) reports at ask so a person confirms it.
## Waivers
A finding is answered in a comment that names the rule and gives a reason
about this page:
```html
<!-- gstack-lint-disable kicker-above-heading: the chapter number is the reader's bookmark through a forty-page spec -->
```
```css
/* gstack-lint-disable section-gap: the ledger separates its rows with rules, not air */
```
```tsx
// gstack-lint-disable template-nav: the three links are the product's three modes
```
`impeccable-disable <rule>: <reason>` is accepted as an alias, so a comment
that already silences the impeccable engine answers the same tell here. The
lint refuses, each as its own `waiver` finding at ask: a rule it does not
know; the lint's own rules (`render`, `waiver`, `house-style`,
`internal-error`: a fallback face or a failed measurement is never signed off);
a **blocking** rule (the Floor is never waivable; the one exception is a
blocking rule the lint could only *suspect* on this page, which it reports at
ask and which a waiver answers like any ask); a reason under 24 characters,
boilerplate ("by design", "intentional", "looks better"), reused word for
word on another waiver, or sharing no word of four letters or more with the
page's own text; a second waiver of one rule. More than three waivers
on one page is a house style and belongs in DESIGN.md: reported once at polish
as `house-style`.
A claim written in a stylesheet answers for that stylesheet on every page
that links it, and only for findings located in it: the shared-word check does
not apply (a stylesheet has no prose), so the claim cannot reach a finding in
the page's own markup. A claim written in the page answers for that page,
including a finding in a stylesheet it links; a second page linking the same
sheet without answering still reports it, so the sheet's finding may print
once waived and once at ask, each from its own page. Which of these a context
gets follows its shape, not the `--scope` flag: one html page keeps page
semantics under either scope. In a directory of components, or a lone
component file beside a stylesheet (under either scope), each file's claim answers for that file alone,
weighed against that file's own text and its own suspected rules, so a sibling
component is never covered and a sibling that measures a blocking rule does
not turn this file's suspected finding into the Floor; a stylesheet beside the
components answers for itself as on a page. Two claims on one line are both read. A blocking copy
tell (`generic-hero-copy`) is the Floor inside the hero, the section or header
that holds the h1; the same line, or a "Meet <Name>" or three-word triad
heading, in body copy further down is suspected and reported at ask, so a
waiver can answer it.
## Rules
| Rule | Tier | Source gate | The tell | The fix |
|---|---|---|---|---|
| `purple-blue-gradient` | blocks | 2a | purple-to-blue, purple-to-pink or cyan-on-dark gradient | one anchor hue and solid ink |
| `gradient-text` | ask | 2b | gradient text | solid ink; carry emphasis with weight or a display face |
| `pure-black-white` | blocks | 7a | #000 or #fff as a colour outside the token block | tint toward the anchor hue: chroma 0.005 or more |
| `absolute-surface-token` | ask | 7b | a surface token that resolves to pure black or white | tint the paper |
| `zero-chroma-neutrals` | ask | 22 | zero-chroma oklch() neutrals | give the neutrals chroma 0.005 or more |
| `accent-footprint` | ask | 23 | accent colour past about 5% of a viewport | retreat the accent to actions and one signal |
| `glow-wash` | ask | 29 | glow and wash: blurred orbs, a radial halo or a zero-offset coloured glow | a solid or grained surface; elevation by lightness, shadows with offset and blur |
| `cream-palette` | ask | 65 | the cream paper, serif display and terracotta accent look | move one axis: the accent first, the paper second, the display last |
| `framework-palette` | ask | 74 | a framework default palette (Tailwind indigo-500, slate-900 and kin) as a token or utility | a colour mixed for the page in OKLCH |
| `overused-font` | ask | 1 | a training-data display face (Inter, Roboto, Fraunces, Playfair Display, Space Grotesk and the rest of gstack's catalog) or a system stack as the display | a display face chosen for the page |
| `banned-font` | ask | catalog | a face gstack bans in any role (Papyrus, Comic Sans, Impact, Lobster, Raleway, Courier New and the rest of lib/design-catalog.ts BANNED_FONTS) | a face chosen for the page; the body face for body and labels |
| `line-length` | ask | 25 | running prose outside 45 to 75 characters a line | max-width: 65ch |
| `font-family-count` | ask | 37 | more than three font families | display, body and one outlier |
| `italic-word-in-heading` | blocks | 38i | one italic word inside an otherwise roman heading | a line break, or the whole head set one way |
| `italic-serif-display` | ask | 38ii | an italic display system | roman display, or declare it on --font-display with font-synthesis: none |
| `accented-word` | ask | 38iii | one word in a heading set apart by colour, weight, a highlight or an underline | the whole head one way; a line break; a second line at a second size |
| `caps-leading` | blocks | 55a | an uppercase display head that can wrap with line-height under 1.0 | line-height 1.0 at least (1.02 to 1.08 recommended) |
| `caps-lockup-leading` | ask | 55b | a single-line caps lockup under 1.02 | line-height 1.02, or make sure it cannot wrap at 320px |
| `monospace-costume` | ask | 64 | monospace on labels, captions or small type when the content is not code | labels in the body face, a step smaller, normal case |
| `tiny-text` | ask | 75 | running text under 12px, functional text under 11px, or prose leaded under 1.3 (render) | body 16px at 1.5 to 1.65; labels one step smaller |
| `wide-tracking` | blocks | 67 | the tracked-caps label: small text set uppercase or letter-spaced past 0.03em | the body face, a step smaller, sentence case, untracked |
| `template-cards` | ask | 3 | three equal icon-above-heading tiles, an icon in a rounded tile, or a thick coloured side stripe | vary widths and heights, inline icons, hairline or no border |
| `nested-cards` | blocks | 4 | a card inside a card | one containment layer |
| `vh-height` | blocks | 6a | a hero sized in vh | dvh or svh |
| `raw-spacing` | blocks | 24a | a raw padding, margin or gap length where a --space-* token exists | the token |
| `off-scale-spacing` | ask | 24b | spacing off the page's scale | the scale; three optical nudges at most |
| `overflow-x-unclipped` | ask | 34 | overflow-x missing on html and body, or a 100vw width | overflow-x: clip on both; width: 100% |
| `horizontal-scroll` | blocks | 34 | horizontal scroll at a width between 320 and 1920 (render) | find the element wider than its viewport and cap it |
| `hero-balance` | ask | 44a/44c | the hero's content sits off centre for its screen, or its visual stands apart from the text (render) | centre the content in its screen; put the visual beside the text |
| `hero-fold` | blocks | 44b | the headline's bottom or the hero's first action below the 800px fold at 1280 wide; the lede and the visual are not measured (render) | pull the display clamp down, tighten leading, a two-line lede, trim padding |
| `wide-container` | blocks | 44d | at 1920x1080 the container has no max-width, gutters under 120px, or a grid gains columns | cap 1360 to 1520px and centre; hold column counts; cap vw visuals |
| `bare-fr-track` | blocks | 50 | an image-bearing grid track on bare 1fr | minmax(0, 1fr) |
| `display-head-unwrapped` | blocks | 50 | a display head without overflow-wrap: break-word and min-width: 0 | overflow-wrap: break-word; min-width: 0 (never anywhere) |
| `radio-tab-jump` | blocks | 53 | CSS-only radio tabs at position: absolute; top: 0 with no JS guard | radios in flow at zero size, or a handler with preventScroll |
| `double-sticky` | blocks | 56 | two position: sticky; top: 0 elements | top: var(--banner-height) on the second |
| `screen-word-budget` | ask | 58 | more than about 80 words in one 900px window, or a hero with more than two text blocks before its actions | headline, one sentence, actions; a section is a head block, then its composition |
| `section-gap` | ask | 59 | a gap under 120px between sections at 1440, every gap the same, or more than nine sections (render) | 120px at least, varied; six to nine sections a page |
| `heading-rhythm` | ask | 71 | a heading nearer what precedes it than what it heads (render) | at least twice the space above a head as below it |
| `cramped-padding` | ask | 72 | text closer than 8px (or half an em) to the side of its filled box (render) | pad the box; a 16px gutter |
| `thin-section` | ask | 77 | a section below the hero with no mass: a narrow body on a wide canvas (render) | widen the body, add a visual, set a statement, or merge the section |
| `small-section-heads` | ask | 78 | the median h2 below the hero renders under 40px at 1440 (render) | h2 at the section size, item titles at the title size |
| `dead-band` | ask | 79 | more than 160px of nothing between the hero's lowest content and the fold (render) | centre the content and grow the visual or the head; never fill with copy |
| `hand-drawn-svg` | ask | 80 | an inline SVG of 120px or more with three curves or fills and no text: a drawn illustration (render) | a photograph, the product built, or type at scale |
| `empty-picture` | ask | 81 | a slot or gradient block of 300x200px or more showing nothing but its wash (render) | the user's image, a credited photograph, or the product at full fidelity |
| `counting-marks` | ask | 82 | 24 or more identical marks standing for a quantity (render) | set the number in type |
| `photo-wall` | ask | 83 | photographs cover more than a third of the page below the hero (render) | keep the strongest pictures, set big; carry the rest in type, figures, the product, a table |
| `email-band-close` | ask | 85 | the page ends on an email sign-up form (one or two fields, one of them the email; never a form with a password or a sign-in button) on a page whose job is not subscribe, join, attend, listen or visit | end on what the visitor came to do, or on the last strong section |
| `aphoristic-cadence` | blocks | 19 | the rebuttal cadence three or more times ("Not a feature. A platform.") | say what the product does, in its own nouns |
| `generic-hero-copy` | blocks | 19 | a filler opener ("Built for the modern team", "In today's landscape"), "quietly trusted by", or the catalog's "Welcome to" and "all-in-one solution" | say what the product does, in its own nouns |
| `generic-cta-copy` | ask | 19/42 | a stock second action beside the primary (Learn more, Get started, Book a demo) | one action, or a second that names where it goes |
| `em-dash-overuse` | ask | 19 | em or en dashes carrying the prose | a comma, a colon, a full stop or a line break |
| `template-footer-line` | ask | 19 | a stock footer line (All rights reserved) | the page's own line, or nothing |
| `template-nav` | ask | 42 | the reflex nav: wordmark left, inline links, a button right, hairline under | decide the nav and build another shape, or keep it on the record |
| `template-footer` | ask | 42 | the reflex footer: four link columns, a social row, a copyright | decide the footer and build another shape, or keep it on the record |
| `invented-proof` | blocks | 46a | an invented metric, testimonial, logo wall or case-study count | the user's number, a text slot, or nothing |
| `drawn-browser-chrome` | blocks | 47a | a hand-drawn browser bar (traffic lights, URL pill) or OS window title bar | a real screenshot in a <figure>, or no chrome |
| `device-frame` | ask | 47b | a phone, laptop, terminal or editor frame | one frame with real content inside, or none |
| `imitation-material` | ask | 47c | styled divs standing in for a product, a photo edge or a physical object | the product built inside a slot, a real asset, or nothing |
| `kicker-above-heading` | ask | 54 | the eyebrow: short inert type before a heading that only announces it | open the section another way; where the heading is figurative, the label becomes the heading |
| `template-punctuation` | blocks | 63 | template punctuation: middle-dot meta strings, an arrow glued to link text, decorative status dots | line breaks, columns, hairlines; a verb as the label |
| `wireframe-slots` | ask | 68 | more than five empty slots on one page, or four or more in one row | set the set in type; a slot that stays holds a real photo or the product |
| `repeated-fact` | ask | 69 | one fact (a price, a time, a count) printed in four or more sections | say each fact once, where it does work |
| `fake-send` | blocks | 70 | a form that prints success with no request behind it | validate, post nowhere and say so plainly until the endpoint exists |
| `layout-transition` | blocks | 10a | transition: all, or animating width, height, top or left | list the properties; transform and opacity |
| `spacing-transition` | ask | 10b | transitioning margin or padding | transform |
| `hover-tells` | ask | 11 | the same lift on every card, two effects on one element, an image scaling on hover, or a hover on something inert | one signal per element, on the container, not the image |
| `bounce-easing` | ask | 12 | bounce or overshoot easing on UI state | an exponential ease-out |
| `focus-ring-fade` | blocks | 15 | a focus ring that fades in | instant: exclude outline and box-shadow from the transition |
| `tooltip-focus-delay` | blocks | 17 | a tooltip whose focus delay equals its hover delay | hover 800 to 1000ms, focus 0 |
| `autorotate-no-pause` | blocks | 18 | auto-rotating content with no pause on hover and focus | pause on hover and focus; a visible pause control past 5 seconds |
| `missing-focus-visible` | blocks | 26a | an interactive element with no :focus-visible ring, or an outline removed with nothing in its place | a ring at 3:1 or better, instant |
| `missing-states` | ask | 26b | no :active or :disabled state | all the states a control can be in |
| `missing-reduced-motion` | blocks | 27 | a keyframe or transform transition without a prefers-reduced-motion fallback | add @media (prefers-reduced-motion: reduce) |
| `autoplay-unmuted` | blocks | 28a | a video autoplaying without muted | autoplay muted loop playsinline |
| `lazy-lcp` | blocks | 28a | loading="lazy" on the first image or the hero video | fetchpriority="high" on the LCP image |
| `video-without-poster` | ask | 28b | a video without poster or a priority hint | add them |
| `input-state-shift` | blocks | 39a | input states that break: a border that changes width, a focus ring built from border, disabled by opacity alone | constant border, outline, shared height, three disabled channels |
| `control-height` | ask | 39b | control height outside 44 to 52px on a marketing page | the number |
| `content-hidden-at-rest` | blocks | 62 | a reveal class hiding content until JavaScript runs | visible by default; hide only under a .js class, or animate from visible with @starting-style |
| `emoji-decoration` | blocks | 30a | an emoji standing in for an icon | one icon library, custom SVG, or typography |
| `two-icon-libraries` | ask | 30b | two icon libraries on one page | one library |
| `unlabeled-decoration` | blocks | 33 | a decorative <svg>, <canvas> or CSS-art element with neither aria-hidden="true" nor a label | hide it or name it |
| `broken-image` | blocks | 76 | an <img> with no source or one that does not load | the real file |
| `stock-photo-hotlink` | blocks | 76 | a stock-site photo hotlink | the user's image, or a library photograph credited on the page |
| `text-occlusion` | blocks | 73 | text painted under another element (render) | give the text its own space, or its own surface above the cover |
| `low-contrast` | blocks | 40 | text, an icon, a focus ring or a field edge under its contrast threshold | fix the pair; body 4.5:1, large text and edges 3:1 |
| `gray-on-color` | blocks | 41 | button text within a step of its fill, or grey text on a coloured surface | the paired ink token |
| `literal-outside-tokens` | blocks | 48 | a colour or font-family literal outside the token block | lift it into the tokens |
| `button-label-wraps` | blocks | 49a | a button label or primary nav link that wraps | shorten it; white-space: nowrap |
| `link-wraps` | ask | 49b | a footer link, tab or breadcrumb that wraps | shorten it |
| `straight-quotes` | polish | F1 | straight quotes in prose | curly quotes |
| `three-dot-ellipsis` | polish | F2 | three dots for an ellipsis | the ellipsis character |
| `balanced-display-head` | polish | F3 | a display head without text-wrap: balance | text-wrap: balance |
| `two-primary-buttons` | polish | F4 | two filled primary buttons on one page, or side by side | one filled primary; the second as a text or outline action |
| `icon-coin` | polish | F5 | an icon in a coloured coin | the icon inline, bare |
| `display-figures-tabular` | polish | F6 | display-size figures without tabular numerals | font-variant-numeric: tabular-nums |
| `stamped-recipe` | polish | F7 | one container recipe stamped on six or more selectors, or the pill radius on five or more non-controls | vary the recipe; pills on controls, tags and badges only |
| `scroll-choreography-mix` | polish | F8 | more than one scroll-choreography pattern | one pattern a page |
| `fake-avatar` | blocks | F9 | a fake-human avatar hotlink | the user's people, initials, or no portrait |
| `stock-person-photo` | polish | F9 | a person-keyword stock photo URL | the user's photograph |
| `page-invariants` | polish | F15 | page invariants broken: primary button style, container width, column gap or radius drifting | one recipe for each |
| `unthemed-browser-surfaces` | polish | F16 | no ::selection, caret-color or scrollbar-color on a page with a committed palette | theme the browser surfaces |
| `text-sliver` | polish | F17 | a text block squeezed into a sliver at 1440 (render) | give the block room or merge the columns |
| `tiny-slot` | polish | F18 | a slot rendering under 40px in either dimension (render) | size the slot for what it will hold |
| `font-weight-count` | polish | F19 | more than three font weights in running use (render) | three weights |
| `lonely-grid-item` | polish | F20 | a grid or wrapping row leaving one item alone on its last row (render) | change the count or the columns |
| `skipped-heading` | polish | F21 | a heading level skipped | step the levels one at a time |
| `justified-text` | polish | F22 | justified text without hyphens: auto | hyphens: auto, or left-aligned |
| `missing-lang` | polish | F23 | a document without lang | lang on <html> |
| `hairline-under-shadow` | polish | F24 | a hairline border under a wide soft shadow on one surface | the shadow or the border, not both |
| `placeholder-name` | blocks | craft | a placeholder name (John Doe) | use a supplied name, or one that fits the product in sample data |
| `placeholder-brand` | blocks | craft | a placeholder brand (Acme, Nexus) | use a supplied customer, or a fitting name in sample data |
| `lorem` | blocks | craft | lorem ipsum placeholder text | write real copy |
| `marketing-buzzword` | ask | craft | a marketing buzzword | say the specific thing |
| `round-number` | blocks | craft | a round invented number | use the supplied figure, or drop the claim |
| `needs-proof` | blocks | craft | a [Needs proof] marker left in the page | supply the proof or remove the claim before publishing |
| `oops` | ask | craft | "oops" as an error message | say what happened and what to do |
| `scroll-listener` | ask | craft | a scroll event listener | use IntersectionObserver; read scroll inside requestAnimationFrame for a scroll-linked effect |
| `dead-link` | ask | craft | a dead link (href="#") | link it, or render the control disabled |
| `alert` | ask | craft | alert() for a message | show the message inline |
| `img-no-alt` | blocks | craft | an <img> without alt | add alt text (alt="" when decorative) |
| `missing-title` | blocks | craft | a document without a <title> | add a <title> |
| `missing-meta-description` | ask | craft | no meta description | add <meta name="description"> |
| `missing-favicon` | polish | craft | a document without a favicon | add a favicon <link rel="icon"> |
| `missing-skip-link` | ask | craft | no skip link reaching <main> | add a "Skip to content" link first in <body> whose #id exists on <main> |
| `missing-aria-current` | ask | craft | route navigation without aria-current | mark the current nav link aria-current="page" |
Source gates are Nutlope/hallmark `references/gates.md` at 7ec8682; `craft`
marks a rule carried over from `gstack-design-craft` (Elaya-derived). Four
ids are the lint's own: `waiver` (a refused waiver, ask), `house-style` (more
than three waivers, polish), `render` (a page that did not render, blocks:
an unrendered page is never a pass) and `internal-error` (a page or stylesheet the lint could not read, or a rule family that
threw on a page, ask: the page is not a pass until the lint has read it).
## Token names the rules read
Several rules key on hallmark's token names, which are also the names
`/design-html` writes: `--color-paper` (the ground; `cream-palette`,
`cyan-on-dark` and the dark-paper checks read it), `--color-accent`
(`accent-footprint`, `cream-palette`), `--font-display` / `--font-heading` /
`--font-title` / `--font-hero` (`overused-font`), `--space-*` (`raw-spacing`,
`off-scale-spacing`) and any `--color-*` or `--font-*` token
(`literal-outside-tokens` fires only on a page that declares one, since a
literal on a page with no tokens bypasses nothing; its Tailwind half, an
arbitrary value like `bg-[#0b0b12]` or `font-['Inter']`, fires on any page,
since Tailwind's theme is the token block it bypasses). A project that names its
tokens `--bg` or `--brand` gets none of those checks and no signal that they
did not run; Phase 2 reads the roles from DESIGN.md.
The token block is `:root`, `html`, a `[data-theme|mode|scheme]` scope, `@theme`,
`@font-face` and anything in a `tokens.css`. Its values are read in one mode:
the base block's last declaration wins, and a mode override (a token rule under
`@media (prefers-color-scheme: …)` or a `[data-theme=…]` scope) only supplies
tokens the base block never declares, so a page that carries both a light and
a dark palette is measured in the mode it declares first, never a mix of the
two. A custom property declared in any other scope (`.dark { --color-paper: … }`,
a component-local `--accent`) is a token definition, not a literal, so
`literal-outside-tokens` does not report it; the dark values of a class-scoped
theme are not read either.
## What the lint does not cover
A clean `LINT_SUMMARY` is not the whole design checklist. These checklist and
catalog items stay with the LLM pass in `/design-review` and are not rules
here: `!important` in new CSS (the parser drops the flag), an h1 past 6rem
(`oversized-h1`), tracking below -0.04em (`extreme-negative-tracking`), body
text under 16px in a static stylesheet (`tiny-text` here is hallmark's 12px
running-text floor, measured by the render tier), the `theater-slop-phrase`
list beyond "built for the modern", the pulsing dot, a text container with
no `max-width` (`line-length` reads a declared `ch` value), an uppercase
paragraph at body size (`wide-tracking` reads caps at label size or tracked;
a 16px untracked uppercase `<p>` passes), and a fixed px `width` on a
container with no responsive handling (`wide-container` reads the opposite
case). The copy tier
reports at the document's first line with a count, because
`gstack-design-craft` counts matches without positions; Phase 2 folds those
patterns into the lint with lines. It reads the page with every open
`<script>`, `<style>`, `<template>` or `<nav>` that nothing closes after it
dropped, so a page of unclosed openers costs its length there too.
`tooltip-focus-delay` reads the delay slot of a tooltip's `transition`
(the second time in each comma part) or its `transition-delay`, 100ms and
over; a `0.2s` or `200ms` fade is a duration, not a delay. Where a rule id is shared with
`lib/design-catalog.ts`, the tier here follows hallmark's gate (several
catalog `ask` rows are `blocks` here); Phase 2 reconciles the two before a
catalog row is marked `detect: lint`. Also outside the lint: `system-ui` or
`-apple-system` as the body's first face (`system-font-primary`; the
`overused-font` rule reads display tokens and heading selectors, as hallmark's
gate does), a gradient fill on the primary button (`gradient-cta`;
`purple-blue-gradient` reports the banned hue pairs only, and an
indigo-to-violet ramp needs stops 12° or more apart), `text-align: center`
density, one large radius on everything, and backdrop blur on more than one
container (`glow-wash` reports a blur over a translucent near-white or
near-black fill; the zero-offset coloured glow is read from 8px in a Tailwind
class and past 20px in a stylesheet, hallmark's two thresholds). The render
tier's probes run in the page's own JavaScript, so a page that redefines them
is caught only when it hands back a rule, grade or shape the lint does not
know (each such answer is one `render` finding naming the probe and width,
and every well-formed finding beside it is kept); a page that answers every
probe with nothing passes the render tier, which is why the static tier stands
on its own and a URL target is measured, never trusted. A stylesheet shared by
many pages is parsed once but its stylesheet-only rules run per page and are
deduplicated at the end, so forty pages on one bundle cost forty passes over
it. `overused-font` and `banned-font` match a catalog stem to the whole
family name or its first words (`Cormorant Garamond`, `DM Serif Display`,
`Comic Sans MS`), never to a longer word (`Interstate` is not `Inter`).
Four checklist items are read more narrowly here than the LLM pass reads
them: `zero-chroma-neutrals` reports every opaque `oklch(L 0 H)` literal
whether or not the page has an anchor hue (hallmark's stamp carried the hue;
a deliberately monochrome page waives it on the record); `missing-focus-visible` reports a removed outline only when no
`:focus` rule anywhere on the page draws a ring (the per-element check is the
render tier's focus walk); `off-scale-spacing` holds every px length to the
4px scale whether or not DESIGN.md declares one (`raw-spacing` is the rule
gated on `--space-*` tokens); and `double-sticky` pairs any two `top: 0`
sticky selectors without reading their scroll containers, so a sticky table
header inside an `overflow: auto` wrapper under a sticky nav reports and
wants a waiver on the record. As in hallmark, a selector the heading pattern
matches (`__title`, `__headline`, `display`) is read as display type unless
its name is also a label's or a control's, so a 12px uppercase tracked
`.card__title` is caught by the markup half only when its text is typed in
capitals. `em-dash-overuse` does not count an en dash between two digits (a
range).
## What is deliberately not ported
hallmark's theme stamp (the CSS comment that declares a theme, a posture and a
hero shape) and every guard that reads it; the genre flag; the judged gates
(the AI template shape, section rhythm, hover by judgement, toasts, the
reviews); the `--before` / `--kept` redesign record (Phase 2); the photo index.
+430
View File
@@ -0,0 +1,430 @@
# The recommended harness: gstack + Capy, from zero to the first merged train
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
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
check name below is configurable; nothing here depends on gstack's own
repository.
Two layers, kept apart on purpose:
- **Harness-generic** (works in Claude Code, Codex and any host that reads the
SKILL.md files): `/ship`, `gstack-ship-queue`, `gstack-restamp`,
`gstack-pregate`, the ship receipt, the train protocol, the flaky-rerun
rule, `release-cut`, `gstack-factory-scorecard`, `/retro`.
- **Capy-specific**: the user-scope skill folder and its install check, the
Captain and its Drive, crew threads, automations, `tools.pr.create`, the
approvals relay. Each one is marked **Capy** below; the section at the end
lists what is only Capy.
```mermaid
flowchart LR
P[Person] -->|plain-language asks and one-word answers| C[Captain thread]
C -->|briefs, slot calls| L1[Crew thread: branch A]
C --> L2[Crew thread: branch B]
C --> L3[Crew thread: branch C]
L1 -->|/ship, READY block with SHIP_RECEIPT| C
L2 --> C
L3 --> C
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]
G -.->|flake-records artifacts| F[Daily flaky pass]
M -.->|free-suite retries| F
M -.->|/retro Step 7.5| S[Factory scorecard]
```
## Day 0: install
### 1. The user-scope gstack skill (Capy)
Copy `capy/skill/gstack/` from a gstack checkout into the person's user-scope
Drive as `skills/gstack/` (SKILL.md, `captain.md`, `scripts/install.sh`).
Add to the user-scope `AGENTS.md`:
> At the start of each session on a Capy cloud machine, load the user-scoped
> `gstack` skill and perform its installation check before starting work. If
> the check fails, follow the skill's session-start steps. Treat a session with
> no `gstack: ok` line from this machine as having no gstack: run no gstack
> workflow, claim none works, and report any blocker. Repeat this check on each
> separate task machine.
What that buys you: every thread runs `bash scripts/install.sh --check
[--for <skill>]` first and reports exactly one trailer, `gstack: ok <revision>
for=<skill>` or `gstack: fail <ids>`. The install is about 30 seconds cold
and prints the `GSTACK_LAUNCH:` assignments every later command needs
([docs/capy-install.md](capy-install.md)). A thread that runs `/ship` installs
with `--for ship`; a planning-only verdict (`SKIP pins (planning-only)`) does
not admit `/ship`, `/review` or `/qa`.
Two things the first week taught every crew:
- **Most threads never install.** The rule installs right before the first
workflow, so a thread that does not plan one never gets `/review` or
`/investigate` either. If most threads in a project run a workflow, bake
gstack into the project's Capy Setup snapshot instead
([capy-install.md#bake-into-a-capy-snapshot](capy-install.md#bake-into-a-capy-snapshot)):
each session then pays only the check.
- **One revision per thread.** Subagents on fresh machines install
separately and can land one patch apart. Put the minimum revision your crew
needs in the crew rules and have every first report quote the trailer, so a
lane that is behind says so.
### 2. The repository
The harness reads four files from the repository. A team can adopt them one at
a time; each is independent.
| File | Why | Without it |
|---|---|---|
| `.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. |
| `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
requirement for trains: a workflow that accepts `workflow_dispatch` with the
train nonce and publishes a check (`full_gate_check`) and a record artifact on
the train head. gstack's own `evals.yml` is the reference; a repository with a
plain test workflow sets `gate_workflow` to it and gets a train without paid
cases.
### 3. The Captain and its Drive (Capy)
Create a Captain from the Capy app (New Captain) or with
`tools.captain.create`. Its Drive is mounted read-write on every crew thread;
this layout is what the crews converged on:
```
captain-<name>/
AGENTS.md the Captain's standing rules: mandate, severity bar, standing approvals, every decision quoted verbatim with its date
CREW.md the crew rules every thread reads at start (install check, validation discipline, READY shape, stamp-on-call, flaky rule, approvals)
DIGEST.md the state of the crew, "Needs you" first; rewritten, never appended
NOTES.md the Captain's running log
skills/ procedures the crew runs: merge-train/, captain-approvals/, and any repo-specific runbook
reports/<code>.md one per thread: Outcome, PR (with head SHAs), Needs, Cost, Lesson, Approvals
handoffs/ datasets and files the next thread needs (the flaky ledger lives here)
plans/ plans brought for approval
```
The phase-1 templates for these files (`AGENTS.md`, `CREW.md`, `DIGEST.md`, a
report, a READY block, an approvals table, a slot call) ship under
`captain/templates/` with the `/captain` skill, and `docs/captain.md`
explains how the Captain runs a day. Drop the templates in, replace the
placeholders (repository, crew login list, gate workflow name, minimum
revision, who may approve what), and the crew has its rules before its first
thread starts. The approvals shape itself (plan asks with before → after, why,
cause, big decisions and a one-word default; a `## Approvals` table in every
report and PR body) is the harness-generic part and is documented in
[capy/skill/gstack/captain.md](../capy/skill/gstack/captain.md).
## The first train
A lane (one crew thread, one branch) does this, in order. Every command is
harness-generic; only the thread and the PR tool are Capy.
1. **Branch from the default branch, build, test with focused checks.** Write
the CHANGELOG entry under `## [Unreleased]`; leave VERSION at main's value.
Never pre-pick a version.
2. **Push the final head.** Everything below binds to the pushed head; a later
push invalidates the receipt, the pregate record and the review binding.
3. **Open the PR** with the host's PR tool (Capy: `tools.pr.create`, never
`gh pr create`, so CI and review callbacks reach the thread), base the
default branch.
4. **Run `/ship` unattended** with the `GSTACK_LAUNCH` assignments
exported: it runs the pregate (on a machine under 8 cores the test tier
needs `gstack-pregate --stage tests --remote`, about $0.06 on a Ubicloud VM
or free locally for small selections), both reviews, the gate-integrity
list, the docs audit, and writes the ship receipt into the PR body
(`gstack-ship-receipt write --pr <n>`). With `queue_mode:
stamp-at-merge`, `/ship` does not bump the version. `/ship` never merges
and never merges main into the branch.
5. **READY.** `gstack-ship-queue ready --pr <n> --no-screen` (after running the
skill's own paid cases focused) or `--screen` (one paid screen on the merge
SHA). Send the printed block as is: `READY #<n> <head>
STAMP_VERSION=unstamped`, CI at that head, `PREGATE: pass`, the screen line
and `SHIP_RECEIPT: pr=<n> head=<head>`. A block with `SHIP_RECEIPT: none` is
not READY. From now on the head is frozen: a push drops the PR from any
train (`QUEUE_TRAIN_MOVED`) and the lane must READY again.
6. **Post the READY block as a PR comment too.** The Captain reads it in the
thread; the scorecard (below) measures READY → merge from the first PR
comment that carries it. A crew that only reports in-thread has no
READY-to-merge number.
The controller (the Captain, or a thread it designates) does this; the
Captain's `skills/merge-train/` runbook holds the crew's copy and
[ship-policy.md#run-a-merge-train](ship-policy.md#run-a-merge-train) is the
reference:
1. `gstack-ship-queue train build --prs 41@<sha>,42@<sha> --dry-run`, then
without `--dry-run`. Up to 5 READY pins, screened first; with fewer than 5
it waits until the oldest READY is `fill_hours` old, `--urgent` for one PR.
Refusals happen before any spend: a moved head, a missing receipt, a fork
head, a real conflict (the line names the PR and files; rebuild without it
and send it back to its lane to merge main).
2. `train verdict`: GREEN, possibly with `FLAKY N` (a case failed once and
passed its one in-run rerun; that is green, and the flake record goes to the
daily pass), RED (cases that failed twice: bisect on them, drop the attributed
PR, or rebuild once after an all-green bisect, never more), or INCOMPLETE
(one re-dispatch for an INFRA-only census).
3. **Per slot, in order:** call the lane ("#41 slot <version>, base <sha>,
order <…>"). The lane merges main if needed (CHANGELOG by section), runs
`gstack-restamp --next --base main --pr 41 --expect-base <sha> --expect-order
<…> --allow-repo-commands`, commits **only** the stamp files, pushes, waits
for required CI, and reports `READY #41 <stamped sha> STAMP_VERSION=<X>`.
The controller runs `gstack-ship-queue verify --pr 41 --train <nonce>`,
merges with the exact head (`gh pr merge 41 --squash --match-head-commit
<sha>`, or `--merge` where the repository keeps merge commits), and
verifies again. A stamp commit that holds anything beyond the stamp files
fails `verify --train`, freezes the queue and costs a backstop gate.
4. `train prune` when every slot has merged.
**Who merges.** One controller, never the lane. Both crews that ran this
learned it the same way: a PR merged off a green list state whose gate never
passed, and a stamp commit with one extra line that froze the queue.
## The daily flaky pass
The rule (set once by the owner and quoted in the crew rules): a case that
fails on its own verdict is rerun once at the same tree, inside the same run.
A pass is green with a flake record; a second failure is a real red; harness,
INFRA and INCOMPLETE reds are never flakes. Nothing is retried twice, and no
threshold, budget or trial count moves to manufacture a pass.
The pass reads two sources, the same two the scorecard counts:
- **Paid:** the gate workflow's `flake-records-a<attempt>` artifact
(`flake-records.jsonl`, one record per flaky case: case, kind, PR, head,
run, both attempts, symptom, rerun cost).
- **Free:** the free workflow's `flake-ledger-*` artifacts (one `flaky-pass`
line per file that failed and passed on the serial retry).
It appends each event to a ledger the crew can read (the crews keep it at
`handoffs/flaky-ledger.md` in the Captain's Drive: date, case, tier, PR and
head, both runs, a one-line symptom), ranks repeats first, and opens one fix
thread per day with the ledger's Open section as its brief. The fix thread
moves entries to Fixed (with the PR) or Won't fix (with the reason), never
deletes them. `bun run test:health` is the free weekly check on the free lane
(a file that flakes in more than 5% of main runs over at least 20 runs fails
it).
**As a Capy automation** (Capy): one automation, one schedule trigger a day in
the owner's timezone, prompt along the lines of "read the newest gate runs'
`flake-records-a*` artifacts and the free runs' `flake-ledger-*` artifacts
since the last pass, append new events to `handoffs/flaky-ledger.md`, rank
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
`.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
(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
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)).
Watch for release starvation: when trains merge faster than the backstop
settles, most main commits are superseded before they are gated and the week
can end with no candidate. The fix is in tooling (accept a commit a green train
record covers), not in holding slots by hand.
## The factory scorecard
`bin/gstack-factory-scorecard` reads one window (default 7 days) from GitHub
through `gh` and prints a Markdown table plus a CSV; `/retro` runs it as
Step 7.5 when `gh` is authenticated. Read-only, no paid work; it downloads the
small verdict and flake artifacts, so a busy repository takes a few minutes
(`--max-artifact-runs` caps that). Every line prints its counting rule, and a
line whose input could not be read says `unavailable (<reason>)` rather than 0.
```bash
gstack-factory-scorecard --repo owner/name --since-days 7 \
[--crew login,login] [--gate-workflow evals.yml] [--free-workflow free-tests.yml] \
[--train-prefix train/] [--ready-marker 'READY #\d+|SHIP_RECEIPT:'] [--pass-marker '"passes": (\d+)|passes=(\d+)'] \
[--pass-cap 3] [--out-dir .context/factory-scorecard]
```
`--repo` defaults to the origin remote's owner/name on github.com (or
`GH_HOST`) and is required without one. Each run writes this window's `scorecard.md` and `scorecard.csv`
to `--out-dir` and appends the same rows to `trend.csv` there, so a weekly
retro accumulates its own trend without renaming files.
| Line | How it is counted | What one of the best software factories would show |
|---|---|---|
| Merged PRs | `merged_at` in the window, base = default branch; crew by `--crew` logins, else author association OWNER/MEMBER/COLLABORATOR | several merges a day, every day; outside contributions land in the same week |
| Lead time, created → merged | PR `created_at` to `merged_at`, median and p90 | median under 24h, p90 under 3 days (DORA "elite" change lead time is under a day) |
| Lead time, READY → merged | first PR comment matching `--ready-marker` at or before `merged_at`; PRs with no such comment are left out and counted | under 2h; every merged PR carries a READY comment |
| Review passes per PR | the largest count `--pass-marker` captures in the PR body (the ship receipt's `"passes": N` once /ship writes it) or a PR comment (`passes=N`, `REVIEW_PASSES: N`), median and p90, plus how many PRs sat at `--pass-cap` (3: pass 1 reviews the whole diff, later passes only the delta, a pass that finds nothing new is the exit) | median 2 passes; none at the cap |
| Gate runs green / red / cancelled | completed runs of the gate workflow (`workflow_dispatch` by default; `--gate-event any`); cost from the run's `report-verdict-a*` artifact (`collector-outcomes.json` `.verdict.costUsd`), else the `**$N** rule/judge cost` or `cost=$N` line of the PR's verdict comment | green on 95%+ of runs, cancelled near 0, cost known for every run |
| Known gate cost per merged PR | known cost / merged PRs; unknown-cost runs make it a floor | known, stable or falling week over week |
| Red-train rate | runs whose head branch starts with `--train-prefix`, cancelled excluded; red = conclusion other than success; red backstops and screens shown beside it | under 5% (each red train is a bisect plus a rebuild, about two gates) |
| Flaky events | `flake-records-a*` records of the newest gate runs (count, known rerun spend) and `flake-ledger-*` entries of free runs on the default branch; a run whose artifacts cannot be read is counted and named, never scored 0 | 0 repeat cases; under 1 flaky case per 10 gates; free retries under 1% of runs |
| Issues opened / closed | GitHub search, created and closed in the window | closed ≥ opened; nothing severe older than a week |
| Releases cut | releases published in the window, drafts excluded | at least one tagged release a week, on schedule |
The third column is a bar, not a boast: it is what DORA's elite band and the
best-run open source projects show, and it is in the table so a reader sees
the gap next to the number.
### Where gstack's own numbers stand
Measured on gstack's repository with this tool for 2026-10-19 to 2026-10-26
(the first full week of trains and `/ship`-before-READY; 103 merged PRs, 77
gate runs, artifacts read for the newest 60):
| Line | gstack, that week | Against the bar |
|---|---|---|
| Merged PRs | 103 (crew 100, outside 3) | volume is there; outside contributions are few and are ported by the crew rather than merged from forks |
| Lead time, created → merged | median 7.9h, p90 23.7h | inside the bar on both |
| Lead time, READY → merged | no number: 0 of 103 PRs had a READY comment | the crew reports READY in-thread; step 6 above (post it on the PR) is what makes this measurable |
| Review passes per PR | no number: 0 of 103 PRs carried a pass count | the receipt's `passes` field lands with /ship's convergence rule; until then crews record the count in the READY block, and the marker reads it from a PR comment |
| Gate runs | 73 green / 4 red / 0 cancelled; known cost $1,628 on 59 of 77 runs | green rate 95%; cost unknown on 18 runs (screens that planned no cases, and runs older than the artifact cap) |
| Known gate cost per merged PR | $15.81 (a floor) | about $24 per train of up to 5 PRs, plus screens; one gate per PR would have been about $33 each |
| Red-train rate | 4% (1 red of 28 trains; 0 red backstop, 3 red screens) | inside the bar; the three red screens were one judge reading a moved file |
| Flaky events | 21 paid cases passed on their one rerun (rerun spend $10.72); 17 free-suite retries in 17 of 103 runs | above the bar on both: 0.27 flaky cases per gate against 0.1, and 17% of free runs retried against 1%. One family (`ship-docsync-*`) repeated three times in two days |
| Issues opened / closed | 50 / 56 | inside the bar |
| Releases cut | 1 (the first weekly release) | on schedule, with the starvation caveat above |
Read the gap honestly: lead time and throughput are where they should be, the
gate is green and its cost is mostly known, and flakiness is the line that is
not. The daily pass exists for exactly that line; the scorecard's job is to
show next week whether it moved. The inventory lane's per-PR baseline CSV in
the Captain's Drive (142 merged PRs over the eleven days before this window
closed) holds the longer history behind these numbers; from here on
`trend.csv` in `--out-dir` carries the weeks.
## What is Capy-specific and what is not
| Concern | Harness-generic (any host) | Capy-specific |
|---|---|---|
| Install and health | `./setup`, `gstack-doctor`, `gstack-upgrade` | the user-scope skill folder, `scripts/install.sh --check` and its trailer, the project Setup snapshot |
| Running a workflow | the SKILL.md files, `GSTACK_SESSION_KIND=unattended`, `GSTACK_RESULT:` lines | the `GSTACK_LAUNCH` assignments, `GSTACK_CAPY=1` and the `capy-asks` block, no enforced hooks (safety skills are advisory) |
| Approvals | `gstack-approvals`, `gstack-owner-brief render --captain`, the `## Approvals` table | the Captain relay (`needs_you`), the private Drive mirror |
| Pull requests | `/ship`, the ship receipt, `gstack-pr-shots` | `tools.pr.create` (callbacks), `tools.pr.uploadAsset` (before/after images) |
| Merging | `gstack-ship-queue` (ready, train build, verdict, verify), `gstack-restamp`, `gh pr merge --match-head-commit` | the controller is a Captain thread; slot calls are thread messages |
| Flaky pass | the rerun rule, `flake-records-a*` and `flake-ledger-*` artifacts, `test:health` | the daily automation and the ledger in the Captain's Drive |
| Release | `release-cut`, `weekly-release.yml`, the `stable` channel | nothing |
| Scorecard | `gstack-factory-scorecard`, `/retro` Step 7.5 | nothing (the CSV can be kept in the Captain's Drive) |
| Memory | `reports/<code>.md`, `DIGEST.md`, the flaky ledger | the Drive scopes they live in |
## Options where crews differ
Two crews ran this a week on different repositories and kept different habits
on a few points. Ship the one that fits the repository; both are supported.
| Point | Option A | Option B |
|---|---|---|
| Merge method | squash with `--match-head-commit` (one commit per PR; CHANGELOG merges by section) | merge commit with `--match-head-commit` (keeps the lane's commits; `history: merge` in the policy) |
| Paid evidence per PR | one train gate covers up to 5 PRs; lanes choose `--screen` or focused own cases | a full gate per PR at handoff, plus "gate-ahead": PR N+1 gates on (its branch + N's head) so N's merge needs no second gate |
| Reusing a gate | tree rule: a head whose diff from the gated tree touches only stamp files reuses the gate (`gstack-tree-receipt`) | the same rule, checked by the controller with `git diff <gated> <head>` before every merge |
| Severe fixes racing for a slot | the next train, `--urgent` for one PR | call the slot for all; the first complete READY wins and the rest restamp to the next patch |
| Cheap CI on PR pushes | free tests on every push; paid work only in trains and screens | a cheap tier on pushes, a `full-ci` label that dispatches the full suites on the exact head; the cheap tier is never merge evidence |
| Infra flakes | never rerun a harness/INFRA red; one re-dispatch for an INFRA-only census | one named infra flake (for example a known runner wedge) may be rerun once and must be stated in the READY; a second wedge escalates |
| Small fixes | one PR each, batched by the train | batched into wave PRs, since serial slots cost about 30 minutes each |
| After the merge | the main backstop gates any push a train did not cover, and freezes the queue on red | the controller watches main's test, e2e and release runs until the release publishes; a red main freezes the queue until root-caused |
| Severe triage | the Captain watches issues and PRs and starts a thread per unowned severe item | a scheduled severe sweep (every 4 to 8 hours) with a fixed severity bar, dedupe against owned lanes, one hotfix thread per batch |
| Contributor PRs | ported onto a crew branch with the author's commits kept and `Co-authored-by`; the fork PR is closed with a thank-you that links the port | landed through a wave PR with "Contributed by @handle", `Co-Authored-By` and `Supersedes` lines |
| Privacy | the pregate's sealed-output and secret scans before every push | plus a per-user denylist grep (names, hostnames, identifying counts from private sources) before any public artifact |
## Known Capy limits
Collected from the issue files five Captains wrote on 2026-10-26 after a week
of running this harness on five repositories. Each is a limit of the platform or of the
harness on it as of that date, with the workaround the crews use.
**Install and versions**
- Every machine, subagents included, needs its own install and trailer; a
fresh machine spends 30 seconds to a few minutes on it, and crews with nine
shard subagents per run pay it nine times. Workaround: bake gstack into the
project Setup snapshot.
- Threads that plan no workflow never install, so `/review` and
`/investigate` go unused. Workaround: a crew rule that names the workflow
each thread will run, or the snapshot.
- gstack versions drift between machines in one thread (`behind=1` on one,
current on another); every minimum bump was a crew-wide broadcast (eight in
ten days for one crew). Workaround: the minimum revision in the crew rules,
quoted in every first report.
- A reviewer installed `--for review-voice` is refused by every workflow and
must reinstall `--for <skill>` when later asked to run one.
**Machines and long work**
- Machine sleep and restarts kill detached servers, local drivers and long
verify loops; `/tmp` does not survive a wake. Workaround: watched background
operations (they keep the machine awake), run directories under
`~/.capy/work`, long drivers on a remote VM, one worktree per counted run.
- Commits on a shared tree sweep up another subagent's uncommitted hunks.
Workaround: one committer per tree.
- Capy's git wrapper forces the linked identity, so tests that set
`GIT_AUTHOR_NAME` and assert on it fail on every Capy machine and pass in CI
(gstack's own `test/gstack-retro-metrics.test.ts` is one). Workaround: read
those failures as environment, and run the free suite on a remote VM for
acceptance.
- A broken shell line can print the environment, keys included, into a tool
log. Workaround: never run `env`, `printenv`, `set` or `export` bare;
check a secret as set or unset only (gstack's launch-env ratchet enforces
this in its own files).
**Pull requests and review**
- `tools.pr.create` without `index` or `head` publishes the primary
checkout's branch, not a worktree's, and targets the configured base, which
on a project that works off a non-default branch must be passed every time.
- Capy's per-push automatic review plus `/ship`'s two review passes double the
review time, and a review loop can run five or more cycles on a P0 as each
push surfaces one new finding. Workaround: run the review voices before the
first push; on a P0 fix only findings in the P0 path and defer the rest to a
follow-up.
- A clean review is not carried forward to a tree-identical merge head, which
can sit "running" for ten minutes or more.
- PR watches and CI callbacks fire only for repositories in a Capy project;
a thread waiting on a repository outside one sleeps until a timer wakes it.
- No per-subagent cost is visible, so `/ship` and plan reports cannot state
what the host subagents spent ("six host subagents, spend unknown").
- A merge rule keyed on "ship receipt on the head" has no machine-checkable
form on the platform side: the controller greps the PR body. `gstack-ship-queue
ready` and `train build` check it on the harness side.
**Approvals**
- An autoplan gate inside a crew thread costs a Captain round trip even when
the owner's standing answer is "take every recommendation"; record the
standing answer once with `gstack-approvals` and cite it. Approval rows
mirrored to the project-user Drive are not readable by other crew threads,
so the Captain carries the text into PR bodies.
**Harness gaps the same files name (fixed or owned in gstack, listed so the
reader knows the date):** the pregate failing every touched file in a
repository without `.gstack/pregate.json`; `/ship` printing no
`GSTACK_RESULT: skill=ship` line in unattended mode; the receipt going stale
on a late push (now refused by `ready`); the receipt-write step truncating a
PR body; `/ship`'s docs audit blocking on a risk the owner must accept by
hand; the pregate's secret scan refusing benign per-trial dummy tokens with no
allowlist; no cost ceiling surfaced at autoplan gates.
## Where things live
| What | Path |
|---|---|
| The user-scope skill and the Captain guide | `capy/skill/gstack/SKILL.md`, `capy/skill/gstack/captain.md`, `capy/skill/gstack/scripts/install.sh` |
| Captain templates and the Captain's day | `captain/templates/`, `docs/captain.md` (ship with the `/captain` skill) |
| Install on Capy, snapshots, the launch line | `docs/capy-install.md` |
| 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` |
| Scorecard | `bin/gstack-factory-scorecard`, `lib/factory-scorecard.ts`, `retro/SKILL.md.tmpl` Step 7.5, `test/factory-scorecard.test.ts` |
+34
View File
@@ -41,6 +41,7 @@ Detailed guides for every gstack skill — philosophy, workflow, and examples.
| [`/deslop-shared-libs`](#deslop-shared-libs) | **Shared Code Reviewer** | Find worthwhile shared-code extractions in recent work. Recommendations only. |
| [`/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`. |
| [`/benchmark-models`](#benchmark-models) | **Model Benchmark** | Side-by-side cross-model benchmark for skills (Claude vs GPT vs Gemini). Latency, tokens, cost, optional LLM-judged quality. |
| [`/eval-plan`](#eval-plan) | **Eval Planner** | Preregister a paid eval (bars, arms, decoys, stop rules), dry-run it for $0 with the stub model, pilot three priced items, then approve the spend from the extrapolation table. |
| | | |
@@ -1291,6 +1292,39 @@ Remove the `/freeze` boundary, allowing edits everywhere again. The hooks stay r
---
## `/captain`
One controller, many lanes. `/captain` (also `/crew`) teaches a coordinating agent the
operating model that keeps dozens of worker threads merging into one base branch without
turning it red.
```text
You: /captain
You: /captain ready 42 # validate a lane's READY block before it enters a train
You: /captain train # build, read the verdict, call slots, verify, merge, verify
You: /captain digest # rewrite Needs you / New / In progress
```
The rules: lanes never merge and their job ends at an unstamped **READY** (the block
`gstack-ship-queue ready` prints, plus the full head SHA, the screen choice, the ship
receipt at the pin and the logged flaky cases); a green PR page or a cheap test tier is
never merge evidence. The controller batches READY pins into a **merge train**, reads one
full gate on the train head (an in-run flaky rerun once; a case that failed twice is
real), then merges slot by slot: slot call, lane stamps on call with a commit that holds
only tool output, `verify --train`, `gh pr merge --match-head-commit`, verify again. A red
train is bisected on its red cases and rebuilt at most once; a backstop gate on every
base push freezes the queue when a tree no train covered lands. Approvals have three
shapes (the per-thread record, the plan ask with before → after and a "do it" reply, the
merge ask with captioned before/after images). The digest is Needs you / New / In
progress with ages. A daily flaky-fix pass reads the gate runs' flake records and the free
suites' retries into a ledger and fixes causes, never thresholds. Where crews that run
this model disagree (squash or merge, who stamps, gate-ahead, the severe race, a gate run
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)
live in `captain/templates/`; the team setup guide for Capy is [docs/captain.md](captain.md).
## `/gstack-upgrade`
Keep gstack current with one command. It detects your install type (global at `~/.claude/skills/gstack` vs vendored in your project at `.claude/skills/gstack`), runs the upgrade, syncs both copies if you have dual installs, and shows you what changed.
+1
View File
@@ -6,6 +6,7 @@ Beyond the slash-command skills, gstack ships standalone CLIs for workflows that
|---------|-------------|
| `gstack-model-benchmark` | **Cross-model benchmark** — run the same prompt through Claude, GPT (via Codex CLI), and Gemini; compare latency, tokens, cost, and (optionally) LLM-judge quality score. Auth detected per provider, unavailable providers skip cleanly. Output as table, JSON, or markdown. `--dry-run` validates flags + auth without spending API calls. |
| `gstack-taste-update` | **Design taste learning** — writes approvals and rejections from `/design-shotgun` into a persistent per-project taste profile. Decays 5%/week. Feeds back into future variant generation so the system learns what you actually pick. |
| `gstack-factory-scorecard` | **Factory scorecard** — one window (default 7 days) of a repository's shipping numbers, read from GitHub through `gh`: merged PRs (crew vs outside), lead time (created → merged, first READY comment → merged), gate runs with their known cost, red-train rate, flaky events (paid reruns and free-suite retries), issues opened vs closed, releases cut. Markdown table plus CSV, every line with its counting rule and what one of the best factories would show; `/retro` Step 7.5 runs it. Read-only, no paid work. [Guide](recommended-harness.md#the-factory-scorecard). |
| `gstack-egress` | **Egress receipt auditor** — every gstack-initiated off-machine send writes a tamper-evident, hash-chained receipt to `~/.gstack/security/egress.jsonl` before the send. `list` shows what gstack attempted to send and to which host, `grants` shows the standing consent settings plus the exact command that revokes each, `verify` recomputes the hash chain and exits 3 on tamper (catches edits, reordering, and mid-chain deletion; truncating or deleting the ledger itself is out of scope — it's a forensic log, not tamper-proof storage). |
| `gstack-context-bill` | **Token bill-of-materials** — read-only, offline audit of what an installed skills tree costs in tokens: always-on frontmatter every session pays vs per-invocation SKILL.md + forced references. `--diff` compares two trees, `--budget` enforces a ceiling, `--exact` opts into Anthropic `count_tokens` (sends the text of the billed `.md` files inside the tree, the linked gstack checkout or gstack's render folder off-machine, names and estimates anything else a symlink reaches, labels a part-estimated bill `mixed`; writes an egress receipt first, degrades to the offline estimate if the receipt can't be written). |
| `gstack-code-intelligence` | **Code-intelligence provider picker** — wraps GBrain, Sourcebot, and Graphify behind one interface: `options`/`status` to see what's available, `select` to pick one, `index`/`search` to use it, `suggest` to check whether the one-time indexing offer should fire here. The offer triggers on large repos (1,000+ tracked files; a decline is persisted). Non-local providers refuse to index *or search* until you record per-repo consent (`consent <repo> yes\|no` — the query text is repo-derived content), the per-repo trust policy's deny and read-only tiers veto write-class operations regardless of consent, and every off-machine send writes an egress receipt. Fully optional — with nothing selected, gstack falls back to grep. |