Use when setting flotilla up in a repo for the first time — writes the one `wave.config.json` every other flotilla skill reads: which issue tracker the repo''s issues live on, which of them agents are allowed to pick up, and the build or test commands that have to pass before anything is proposed for merge. Triggers on "set up flotilla", "configure the wave store", "init wave config", "initialize flotilla for this repo".
Installs into .claude/skills of the current project.
Are you the author of Wave Setup?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/formtrieb-wave-setup)
---
name: wave-setup
description: 'Use when setting flotilla up in a repo for the first time — writes the one `wave.config.json` every other flotilla skill reads: which issue tracker the repo''s issues live on, which of them agents are allowed to pick up, and the build or test commands that have to pass before anything is proposed for merge. Triggers on "set up flotilla", "configure the wave store", "init wave config", "initialize flotilla for this repo".'
---
# wave-setup
Bootstrap flotilla for a consumer repo by producing the `wave.config.json` that every other wave skill reads. This is a one-time setup per consumer repo (re-run to change the store, eligibility set, verify profile, or engine binding). Everything goes through the engine — the skill writes the local config file, proves it loads via `config validate`, then **runs both preflights** — the **store-preflight** (`store-preflight`, tracker facts) and the **host-preflight** (`host-pr preflight`, code-host posture) — to confirm the live preconditions actually hold before any wave is planned (ADR-0023 amendment: tracker facts and code-host facts have separate owners). It also **authors the `engine.cli` invocation binding** (ADR-0032) — the pinned local install for a Node consumer, a minimal tracked manifest for a non-Node one — so every downstream skill reads a single configured command instead of choosing among invocation forms, and **scaffolds the per-project credential lookup command** (ADR-0029) — a keychain item, a `<VAR>_CMD` entry in the tracked settings env block, and the matching `permissions.deny` entry — so a fresh consumer never has to reach for a global shell-profile export to get a wave running.
Your job is the **judgment** — interviewing the three config concerns (store, eligibility, verify), composing the right JSON, and knowing which options apply to the consumer's situation. The CLI plumbing (the `config validate` / `store-preflight` / `host-pr preflight` commands, the exact JSON shapes, the worked examples) lives in [reference/setup-mechanics.md](reference/setup-mechanics.md) — reach for it once you know what to write. You write the file directly (it is local config, not a tracker artifact), then round-trip through the engine to confirm it loads cleanly and that both the store and code-host preconditions hold.
## When to Use
- Setting up flotilla in a new consumer repo for the first time.
- Changing the store kind, eligibility set, or verify profile in an existing consumer repo.
- Confirming a hand-edited `wave.config.json` is valid before running `wave-plan` or `wave-create`.
Do **not** use this for per-wave work — `wave-plan` (breakdown), `wave-create` (spine + worktrees), and `wave-start` (dispatch) are the downstream wave skills. `wave-setup` is the one-time bootstrap; the wave skills are the per-batch operators.
## What the config holds
The config has four concerns. Interview them one round-trip each — don't ask field-by-field. A further field, `engine.cli`, also lives on the config — but it is **scaffolded, not interviewed**: there is no consumer preference to gather, only a repo-shape fact to observe and an install to run. See [Engine invocation](#engine-invocation-enginecli) below.
### 1. Store
Which issue store does this consumer use?
- **`github`** — the M1 target and the right default for any repo that uses GitHub Issues. No `repo` field: the `gh` ambient context (the current working directory's tracked remote) supplies the repo. Carries only `eligibility?`.
- **`linear`** — for consumers whose issue tracker is Linear (ADR-0020). Note what does **not** change: the PR itself is still a GitHub artifact in both known consumers — Linear is the issue tracker, not the code host. Requires `team` (the Linear team key or display name — owns the workflow-state catalog and the label namespace; use the exact key as Linear displays it, e.g. identifiers read `EX-16` → key `EX` — the lookup is case-sensitive). Carries optional `project` (scopes `listOpen` and the PRD Document panel to one project; omit for a whole-team draw — the Document facet then falls back **team-scoped**: `publishDocument` parents the Document on the configured team and `listDocuments` narrows server-side to that team, ADR-0017 as amended. One structural consequence to know: Linear documents `Document.team` as null for any non-team parent, so the team-filtered listing can never return a *project-attached* Document — the team-central convention deliberately accepts this), `eligibility?`, `states?` (workflow-state-name overrides — **six keys**, each optional and each defaulting on its own: `queued` → `Todo`, `inFlight` → `In Progress`, `inReview` → `In Review`, `unclaimTarget` → `Backlog` (where a released claim is parked, and where `applyTriage` clears Linear's `Triage` inbox to), `unplanned` → `Canceled` (Linear's `Canceled` CATEGORY: the rung flotilla calls `unplanned` and GitHub closes as `not_planned`), and `doneState` → *no default*, the opt-in no-integration fallback below. **`inReview`'s default is the one a stock Linear team must CREATE; the other four match Linear's shipped workflow exactly.** Declare only the ones this team names differently — `{"unclaimTarget": "Todo"}` is a complete block; full table in [reference/setup-mechanics.md](reference/setup-mechanics.md#states--all-six-keys-their-defaults-and-the-write-each-one-steers)), and `categoryLabels?` (triage-category → existing label name, e.g. `{"bug": "Bug", "enhancement": "Improvement"}`).
- **`markdown`** — for dev/dogfood on a local filesystem. Requires `repoRoot` (absolute path) and `slug` (a kebab-key identifying the issue set, e.g. `2026-06-18-my-wave`). Carries `eligibility?`.
If the consumer is on GitHub Issues, start with `github`. Reach for `linear` when the consumer's issue tracker is Linear. Reach for `markdown` only when there is no GitHub repo or when the user is working in an isolated local context.
#### Linear operational preconditions
A misconfigured `linear` store fails quietly — the claim ledger or the closing probe silently drifts rather than erroring loudly (ADR-0020). Items 1–2 are **probed for real by `store-preflight`** (step 6) — run it, don't assert it; checks, report shape, exit codes: [reference/setup-mechanics.md](reference/setup-mechanics.md#store-preflight-store-preflight--tracker-facts). Items 3–5 are team conventions the engine cannot see — walk through them with the consumer **before** writing the config.
**Probed by `store-preflight`:**
1. **Linear's GitHub integration is installed** — probed as WORKSPACE-level presence only, never a binding to this team or this repo — else `readClosing` never sees a merged PR and no row resolves to `done` (the opt-in `states.doneState` fallback reports this `not-applicable` instead of failing).
2. **The team's workflow-state catalog covers every state name this config actually resolves to** — the three claim rungs (defaults `Todo`/`In Progress`/`In Review`), the unclaim target (default `Backlog`) and the unplanned target (default `Canceled`), plus `doneState` if set. Read that as the CONFIGURED values, not as those six literal names: all five with defaults are overridable through `states`, and the preflight checks whatever this consumer named — a team whose parking column is `Icebox` needs `Icebox` in the catalog and needs `states.unclaimTarget` pointed at it. Else the first `setState` mid-wave throws.
**Confirm with the consumer — not engine-checkable:**
3. **Wave PRs carry the tracker's magic-word close phrase** in the PR body — for `linear` that is `Fixes <TEAM-NN>` (e.g. `Fixes EX-16`), not GitHub's `Closes #N`. This is the store-kind close-phrase convention (`wave-shared`); it both closes the issue and creates the merged-PR attachment.
4. **PR-route discipline**: wave branches land via PRs against a protected `main`. A fast-forward-only merge mode (no PR, no merge commit) never produces the closing attachment the probe needs.
5. **Team convention**: humans park new/backlog work in whatever `states.unclaimTarget` names (`Backlog` by default); whatever `states.queued` names (`Todo` by default) means wave-claimed. State it to the team in THEIR column names, read off the config you just wrote — a team that overrode `unclaimTarget` will not recognise the default spelling. This is an accepted human-drag hazard (the spine/WAL reconciles the drift, ADR-0002) — make sure the consumer's team knows the convention before turning the wave loop on.
**Read from `store-preflight`'s advisory reading, confirm the residue by hand:**
6. **The team's PR-automation takes no action before the merge — Linear's own default does not.** Configured at Linear's per-team settings path (Settings → Team → Workflows & automations → Pull request and commit automations), separate from the workspace-level integration item 1 probes. Set the four pre-merge events (draft, open, review requested, ready for merge) to "No action", and keep merge on a completed state — or configure `states.doneState`. The rules are team-wide: a pre-merge rule naming a claim state moves any issue a hand-made PR references into a claim no wave holds, and flotilla writes its own claim rungs anyway (ADR-0020, 2026-09-27). Without a completed merge rule or `doneState`, merged pull requests close no issue. `store-preflight` grades the rules into an ADVISORY `gitAutomation` reading (`aligned` / `misaligned` / `abstain` — it never fails the preflight; an event with no rule reads as no action); confirm by hand only what it lists as `undecided`.
Run the preflight (step 6) to prove 1–2 for real, read item 6 off its advisory `gitAutomation` reading, and confirm items 3–5 by hand, plus item 6's residue — the rules it lists as `undecided` (in practice a branch-scoped rule naming a state that is not a claim state). A `config validate` PASS only proves the JSON is well-formed; items 3–5 and item 6's residue are what no engine call can see.
#### Operational preconditions (all stores)
These preconditions apply regardless of store kind:
- **The code-host landing posture** — probed for real by `host-pr preflight` (step 7; store-blind, so it runs identically on `github`/`linear`/`markdown` — landing always happens on the code host, ADR-0023 amendment). It checks that the landing credential can merge PRs and that "Allow auto-merge" is on wherever required checks exist; run it, don't assert either — checks, statuses, and fix instructions: [reference/setup-mechanics.md](reference/setup-mechanics.md#host-preflight-host-pr-preflight--code-host-posture). One fact worth stating up front regardless: GitHub ships "Allow auto-merge" **OFF by default** — tick it (Settings → General → Pull Requests) before a repo's first `--auto` wave if it has required checks.
- **AFK permission posture — scaffold the tracked env block AND allowlist, don't just confirm either** (not engine-checkable). A wave runs unattended: every command a Worker or Reviewer runs passes through the harness permission gate with **no human at the keyboard**, and a worktree inherits tracked files only, so the tracked `.claude/settings.json` is the sole permission/environment source it gets — a command left off the allowlist stalls the agent on a prompt no one answers (measured cost: 2–4 human approvals in one night and a roughly seven-hour stall, docs/retros/2026-07-29-overnight-triple-wave.md, NF-F1). wave-setup **scaffolds** this, rather than leaving it to the operator — see [Scaffolding the tracked permission allowlist and env block](#scaffolding-the-tracked-permission-allowlist-and-env-block) below for the exact entries and the full command surface.
- **Sandbox capability requirements — scaffold the tracked writes/network block, keep `host` operator-local (ADR-0049).** A gate a dispatched Worker or Reviewer cannot run inside its own sandbox is not merely a permission gap the allowlist above closes — some verify commands need to reach a path outside the worktree, a network host, or an un-narrowable host capability (a daemon socket, a simulator, a device) just to run at all, and no `permissions.allow` entry can grant that: a live occurrence took 5h46 of wallclock, of which about five hours were one unattended sandbox-disable prompt (ADR-0049). `wave-setup` asks, per verify command, which of `writes`/`network`/`host` it needs (recorded as that command's `needs` — see [Verify](#3-verify-optional) below), and — **confirmed on this machine before anything is scaffolded** (the carrier itself is measured, ADR-0049 — the per-consumer run below proves it holds *here*, not whether it holds at all) — translates a declared `writes`/`network` need into a tracked `sandbox` block; `host` never lands in a tracked file, for the same host-escape reason `docker` never has. See [Scaffolding the tracked sandbox block](#scaffolding-the-tracked-sandbox-block-writesnetwork--the-capability-requirement-adr-0049) below.
- **Worktree-brief inputs** (not engine-checkable — confirm with the consumer, FOR-32/W4-F4). `wave-start` dispatches Workers and Reviewers into `isolation: 'worktree'` — a fresh checkout of **tracked files only**; anything gitignored is simply absent there. Three consumer answers decide what the Coordinator must fill into every dispatch brief, so gather them now rather than re-deriving them at the first wave's fan-out:
1. **Is the dependency directory gitignored?** (the ordinary case for a lockfile-managed tree — e.g. `node_modules`, `vendor`). If yes, record the exact install command(s) **as `engine.install` in `wave.config.json`**, beside `engine.cli` — not merely in the setup report. Without it, a fresh worktree has no dependencies and the verify gate cannot run at all, for the Worker or the Reviewer; the composer reads the field and fills each row's dependency-install step (`workflow-driver.md`'s `depsSetup`) from it. For a non-Node consumer, this is also where the `engine.cli` non-Node manifest's install command gets recorded ([Engine invocation](#engine-invocation-enginecli) above) — prepended ahead of whatever else this answer already names, so `engine.cli` resolves before the row's first engine call.
**"Nothing is gitignored here — no install step needed" is not a recordable answer wherever `engine.cli` itself resolves through a gitignored path**, which is the ordinary case: a pinned local binary under an ignored `node_modules/`. Check before you accept the answer — `git check-ignore <the first path-shaped word of engine.cli>` exits 0 when the binding is ignored — because that combination is exactly the shape a consumer's live wave hit: two Workers each read a brief asserting nothing was gitignored, each found it false, and each had to install by hand before any engine verb resolved. If the check comes back ignored, the install command is REQUIRED; record it. If it comes back clean and the consumer genuinely installs nothing, record nothing and leave `engine.install` out — an absent field is an honest absence, which the composed brief states as a deferral rather than as a confirmation. (`compose-driver` refuses outright on the third combination — a gitignored binding with no install step from any source — so a wrong answer here stops the compose rather than a Worker five hours in; ADR-0032 amendment 2026-09-04.)
2. **Is the store config (this `wave.config.json`) gitignored?** If yes, a Worker/Reviewer standing inside its own worktree cannot resolve a tracker id against a store it has no config for — record that the Coordinator must embed the **full issue spec** (title, body, acceptance criteria, declared Files globs, risk) into each brief (`issueSpec`) rather than pass a bare tracker reference.
3. **What are the exact verify-command forms that actually resolve inside a fresh worktree — measured, not inferred?** A `VerifyProfile`'s `commands` describes the consumer's build gate; it is not automatically the spelling that resolves once a Worker or Reviewer is standing inside a bare worktree checkout instead of the consumer's own already-populated working copy. **Run each command for real** — from a clean checkout or a scratch worktree — **and observe it resolve the local binary the consumer's own install produced**: the same "proven by execution, not inspection" standard `config validate` and both preflights already hold every other precondition to; a command is not exempt from that standard just because it sits inside a JSON string. This is exactly the trap a live wave hit at compose time (Wave `2026-07-30-arm-and-wiring`, coordinator disclosure 254.3): on a consumer whose dependency directory is **nested** rather than at the repo root — flotilla's own repo, whose engine lives at `tools/wave/` — the bare forms `npx vitest run` / `npx tsc --noEmit` look like the obvious spelling of "run the tests, run the type-gate," find no local binary at the repo root, and reach for the npm registry mid-dispatch instead of failing loud (further measured spellings of this same trap, with call counts, are in [setup-mechanics.md's "2026-07-31 pass"](reference/setup-mechanics.md#2026-07-31-pass--the-used-but-absent-direction-issue-291)). Convention 13 rules out papering over this with a fused `cd tools/wave && npx vitest run` — the same two mechanisms that refuse a fused Worker step (wave-shared Convention 13) refuse a fused verify command too. The measured, resolving forms carry the directory IN the command instead, exactly like `depsSetup` above — `npm ci --prefix tools/wave` (already this consumer's item-1 answer), `./tools/wave/node_modules/.bin/vitest run`, `./tools/wave/node_modules/.bin/tsc -p tools/wave --noEmit` (both confirmed live in this dispatch: 2675 passing tests across 62 files, and a clean `tsc` exit). Record the MEASURED forms, not the plausible-looking one — if the consumer's dependency directory sits at the repo root, the bare forms already named in `verify.profiles[].commands` are typically the measured forms too, but confirm by running them once rather than assuming so.
**The recorded forms are the compose-time source for the driver's verify constants — never restated from `wave.config.json` per wave.** Once measured, they are recorded here the same way `depsSetup` and `engine.cli` are: `workflow-driver.md`'s per-row brief renders the row's Verification-gates step by quoting this recorded shape verbatim. The loop this replaces — a Coordinator opening `wave.config.json`, reading a profile's `commands`, and composing a plausible-looking invocation by hand at every wave's dispatch — **is retired**: the form is proven once, here, at setup, not re-derived wave after wave.
Record all three answers alongside the config (e.g. in the setup report handed to the Coordinator) — they are Coordinator-composition inputs, and only item 1 has a `WaveConfig` field of its own (`engine.install`, which `config validate` echoes back); the engine has no verb to check items 2 and 3.
- **Editor/IDE indexing posture** (not engine-checkable — confirm with the consumer). `wave-start` fans out Workers and Reviewers into worktree-isolated checkouts, and any editor attached to the repo starts indexing and file-watching that fresh checkout and its build output immediately. Live evidence from the server pilot: on a Swift consumer, that load froze the editor's extension host, and the symptom presented as a hung wave / unreachable Coordinator — an orchestration-level red herring for what was purely an editor-configuration gap. Before the first wave, confirm one of two things holds: either the consumer's editor/IDE excludes the wave worktree location and heavy build/dependency directories (e.g. `.build`, `node_modules`) from indexing and file-watching, or `wave-start` runs headless (no editor attached to the repo). Worked example for VS Code: add the worktree path and the build/dependency directories to `files.watcherExclude` and `search.exclude` — the requirement is editor-agnostic, so treat that as one example of the pattern, not a VS Code-specific step.
#### Scaffolding the tracked permission allowlist and env block
Author (or extend) the consumer repo's **tracked** `.claude/settings.json`: an **`env` block** (the proxy flag), a **`permissions.allow`** array naming the wave's full command surface (the one configured `engine.cli` form, the workspace mechanic, the `wave/`-scoped push, the Reviewer's probe-worktree + merge-tree calls, the consumer-tunable deps installer + verify-gate commands), and **`permissions.deny`** entries anchoring the secret-echo class structurally (wave-shared Convention 8). All three live in the same tracked file — the sole permission *and* environment source a worktree-isolated Worker/Reviewer inherits (a worktree carries tracked files only, so the gitignored `.claude/settings.local.json` never arrives). Exact entries, full JSON scaffold, and the per-item rationale: [reference/setup-mechanics.md](reference/setup-mechanics.md#afk-harness-config-scaffold-env-block--permission-allowlist-claudesettingsjson) — that scaffold is what `tools/wave/src/allowlist-scaffold-guard.spec.ts` (issue #291) now enforces stays in sync with this repo's own live file, in both directions (every live allow entry cited; every declared verify command has a matching entry). Reach for the reference doc rather than re-deriving the entries here.
**The `deny` entries are not consumer-specific judgment — scaffold them unconditionally.** Convention 8 binds every role that produces tool output, the Coordinator included, which a brief clause alone can never reach (the Coordinator never receives a brief) — a `permissions.deny` anchor on `Read` and Bash's read-shaped forms (`cat`/`less`/`more`/`head`/`tail`) against the gitignored local settings file and any `.env`-class file is the structural backstop; the brief clause stays as defense-in-depth on top of it. Scaffold this identically for every consumer — the vector is universal, unlike the allowlist which varies by the consumer's own engine-invocation path.
**The env block, not only allowlist-prefix duplication, is the fix for the proxy flag.** Put `NODE_USE_ENV_PROXY=1` in the tracked `env` block so every engine-CLI invocation inherits it — an un-prefixed call otherwise both drops the proxy silently (wave-shared Convention 1) and fails the allowlist's literal prefix match at the same time. Name the allowlist's engine-CLI entry **prefix-free**, alongside its env-prefixed twin (kept for existing briefs and cross-repo habits) — exactly the ONE form `engine.cli` is bound to (ADR-0032 abolished the invocation fallback chain), never the `npx` bootstrap form or a vendored `tools/wave/…` path (this repo's own documented exception only).
**Never widen the wave-branch push entry past `git push origin wave/:*`.** The prefix match is what makes the protected default branch structurally unreachable from a dispatched Worker or Reviewer — a bare `git push:*` reopens exactly that.
**`docker` stays OUT of the tracked file — it is a host-escape, and so is every other `host`-class capability.** A tracked, un-sandboxed `docker` entry grants a host escape to every future agent of the repo, not just this wave's Workers, and that reasoning is the rule for the whole `host` class (ADR-0049 decision 3) — a daemon socket, a simulator, a device, anything a verify command declares as `needs.host: true` — not a `docker`-specific carve-out. Leave it to the operator's untracked `.claude/settings.local.json`, and brief the Worker for graceful degradation instead: the socket-free floor always (`docker compose config`, `bash -n`), the live path only when the socket is reachable, and a precise deferral disclosure otherwise. Full worked pattern: [setup-mechanics.md's host-class rule](reference/setup-mechanics.md#the-host-class-rule--kept-out-of-every-tracked-file-host-escape-docker-is-its-worked-example).
**SSH origin: the sandbox `excludedCommands` needs all three network git verbs.** `git fetch` (the Worker's anchor fetch) and `git pull` (wave-close's `main` pull) are network operations too, not just `push` — an HTTPS origin authenticating through the harness proxy needs none of this.
**The credential lookup-command scaffold merges into this same tracked file.** The keychain item, the `<VAR>_CMD` `env` entry, and its own `permissions.deny` prefix (ADR-0029) are a separate piece of judgment — see [Credentials](#credentials) below — but land in the identical `.claude/settings.json`.
**The sandbox capability scaffold (`writes`/`network`) merges into this same tracked file too — measured first.** See the next subsection.
#### Scaffolding the tracked sandbox block (writes/network) — the capability requirement (ADR-0049)
A verify command's declared `needs` (the [Verify](#3-verify-optional) interview below asks for it, per command) names what that command must reach beyond its own worktree — `writes` (paths outside the worktree), `network` (hosts), or `host` (the class that cannot be narrowed: a daemon socket, a simulator, a device). Declaring a need is not the same as it being *provided* — this subsection is wave-setup's half of making a declared `writes`/`network` need genuinely provided, the way the allowlist above makes a declared *command* genuinely runnable.
**The carrier is measured (ADR-0049, 2026-09-27) — confirm it here, on this machine, before scaffolding anything.** A project-**tracked** `sandbox` block reaches a **dispatched worktree agent** and **merges** with the operator's global sandbox settings, rather than replacing them. It travels through the **dispatching session's own checkout**, not the worktree's copy — so the block must sit on the branch the Coordinator runs from (landed on the default branch first, in practice); a block that lives only on a row's own branch grants nothing. A change takes effect with no session restart. **Still unmeasured:** a headless runner, and `network.allowedDomains` (only `filesystem.allowWrite` was measured). The per-consumer probe below still runs, with real positive and negative controls, before handing off a `sandbox.filesystem.allowWrite` / `sandbox.network.allowedDomains` entry — confirming the carrier holds *here*, not testing whether it exists: [setup-mechanics.md — Measuring whether the tracked sandbox block is honored](reference/setup-mechanics.md#measuring-whether-the-tracked-sandbox-block-is-honored--the-probe-issue-716). A **negative** result STOPs the sandbox portion right here: disclose the alternative carrier plainly (every `writes`/`network` need stays declared-but-unprovided, deferring `capability-gated`, the same line a `host` need always takes) and do not hand off a `sandbox` block on a failed live-gate. A **positive** result — the ordinary case — continues below.
**Compose the block as the union of every verify command's declared needs, path-exact.** `writes[]` → `sandbox.filesystem.allowWrite[]`; `network[]` → `sandbox.network.allowedDomains[]`; nothing resolved, normalized, or reshaped in between — carry the declared string verbatim. `host` needs never appear here, in `excludedCommands`, or in any other tracked shape (the host-class rule above). Exact JSON, the derivation rule, and the guard that holds this in parity with the declared needs: [reference/setup-mechanics.md](reference/setup-mechanics.md#sandbox-capability-scaffold-writesnetwork-from-declared-needs-adr-0049).
**Merge it into the SAME hand-off as the AFK permission scaffold — one staged file, one two-path landing (step 9 below).** This block joins `env`/`permissions`/`hooks` in the file [Scaffolding the tracked permission allowlist and env block](#scaffolding-the-tracked-permission-allowlist-and-env-block) already stages and lands — no separate write path here.
**After the file lands, run the live-gate: every needs-bearing command, once, for real, inside the now-scaffolded sandbox.** Record pass/refused per command. A **refused** command whose `writes`/`network` need was just declared and scaffolded is a **setup STOP** — quote the refusal verbatim and fix the gap here, before any wave dispatches against this config; never a finding left for the first wave to discover unattended. A `host`-needing command refusing is expected, not a STOP — it is the same honest deferral every `host` need takes on every runner. Full procedure: [reference/setup-mechanics.md — the live-gate](reference/setup-mechanics.md#the-live-gate--run-each-needs-bearing-command-once-inside-the-now-scaffolded-sandbox).
**After that one run, ask whether to record what it showed as an environment note on the command (ADR-0049 Amendment 2026-09-27, decision 5b)** — three triggers only (no-output failure, a sandbox-vs-outside difference, a workaround needed), the bound stated aloud, and written only on the operator's explicit yes; setup never writes one unasked. Same reference section for the ask's exact shape and a worked example.
### 2. Eligibility (the Eligibility OR-set)
The set of issue-side triage states that make an issue wave-grabbable. `wave-plan` includes an issue if it carries **any** state in this set (OR semantics). Default: `["ready-for-agent"]`.
This is the **only** coupling between the triage pipeline and the wave pipeline: `triage` flips an issue into a state in this set; `wave-plan` picks it up from there. The value names come from the consumer's triage schema — never hardcode a native tracker label.
For most consumers, the default is correct. Ask only when the consumer's triage vocabulary differs — e.g. they use a differently-named ready label (`["agent-ready"]`) or genuinely have **more than one** wave-eligible state (the OR-set then lists all of them).
> **Never add `ready-for-human` to the eligibility set.** `ready-for-human` is triage's *not-wave-work* terminal — work a human does entirely outside flotilla, which never enters a wave (ADR-0015). Human-gated *wave* work is a different thing: a `ready-for-agent` issue whose **Worker** is `HITL-required`. Such issues are already grabbable under the default `["ready-for-agent"]` eligibility, and `wave-plan` surfaces them flagged human-gated. Adding `ready-for-human` would wrongly pull non-wave work into the roster.
### 3. Verify (optional)
A `VerifyConfig` of build/test profiles that the engine runs against an agent's changed files before a PR is opened. Each profile declares which file globs it applies to and the commands to run.
**There is no `DEFAULT_VERIFY`.** Verify is purely consumer configuration — omit it entirely if the consumer has no build gate. Do not invent a default or guess at a standard command; ask the consumer what their build/test setup is.
**Composing the profile from that description is not the last step — measuring it is.** See [Worktree-brief inputs](#operational-preconditions-all-stores), item 3: a described command is not automatically the one that resolves inside a Worker's or Reviewer's fresh worktree, and a consumer with a nested dependency directory needs the measured, repo-relative form recorded instead of the bare description a consumer's own already-populated working copy would happily run.
**Ask the needs question per command, while you're measuring it (ADR-0049).** For each command, ask which of `writes` (paths outside the worktree — a shared build cache, a derived-data directory), `network` (hosts it must reach), or `host` (a daemon socket, a simulator, a device — the class that cannot be narrowed to a path or a host) it needs, and record the answer as that command's `needs` in the config:
```json
{ "cwd": "ios", "command": "xcodebuild test -scheme App", "needs": { "writes": ["~/Library/Developer/Xcode/DerivedData"] } }
```
This is data about the command, never a harness knob — the engine learns the *need* (`VerifyCommand.needs`, a closed set of exactly `writes`/`network`/`host`; `config validate` refuses any other key or shape), and the skill tier is what translates a `writes`/`network` need into the tracked `sandbox` block below. Most commands need nothing beyond their own worktree — omit `needs` entirely for those; it is additive, so a config that never asks this question loads and composes exactly as it always has. What wave-setup does with a `needs`-bearing answer — measuring first, scaffolding, and the setup-time live-gate — is [Scaffolding the tracked sandbox block](#scaffolding-the-tracked-sandbox-block-writesnetwork--the-capability-requirement-adr-0049) above.
A real consumer's CMS profile (`cms/**` → `composer install` + `vendor/bin/phpunit`) is an example of what a real consumer provides — it is that consumer's own content, not a flotilla default. Do not import it into other consumer configs.
### 4. Models (optional)
Which concrete model this consumer binds to each abstract tier — the optional top-level `models` block, `{ heavy?, standard?, scribe? }` ([ADR-0012](../../../docs/adr/0012-worker-is-brand-free-autonomy-first.md) Amendment 2026-09-21). The engine derives the **tier** from a row's Risk and never knows which model it means.
**Interview it — do not scaffold it.** Unlike `engine.cli` there is no repo-shape fact to observe: which models this consumer will spend, and on what, is a preference only they hold, and a block nobody asked about is a key nobody knows exists. One round-trip: *which model for risky work (`heavy`), which for ordinary work (`standard`), which cheap one for the driver's sidecar-writing stage (`scribe`)?* Record only the keys they answer for; each is independent and an absent one is an honest absence.
**Say what declining costs — one key is not free to omit.** Declaring nothing still runs every wave, since the Coordinator records a model per row at dispatch (`spine set-branch --model`) and that outranks this block. The exception is `scribe`: with neither `models.scribe` nor `models.standard`, the sidecar stage runs on **the row's own model**, so a heavy row pays its expensive model to write one file. Recommend declaring at least `scribe`. Never invent an id — model names are volatile, consumer-owned and spelled nowhere in this toolkit (ADR-0012). Shapes, ladder and refusal: [reference/setup-mechanics.md](reference/setup-mechanics.md#modelsconfig--the-tiermodel-id-binding-adr-0012-amendment-2026-09-21).
### 5. Landing message (optional)
Who writes the commit a landing leaves on the default branch: `landing.commitMessage`, `"pr"` or `"host"` ([ADR-0053](../../../docs/adr/0053-the-landing-verb-authors-the-landing-message-from-the-pr.md)). **The default is `pr`, and absent means `pr`:** flotilla lands the PR's own title and body, on any commit count, and the Worker's commit messages never reach the default branch — except under a merge queue, which composes its own commit, or a rebase landing, which lands the commits themselves. **`host` is for a consumer whose history a tool reads** (semantic-release, commitlint, a changelog generator) and whose commits follow that tool's convention: the repository's own squash setting then writes the message. Omit the key unless you see such a tool; if you do, ask whether it reads the default branch's history, and write `{ "landing": { "commitMessage": "host" } }` only if it does. Shape and refusal: [reference/setup-mechanics.md](reference/setup-mechanics.md#landing--who-writes-the-landed-commit-adr-0053).
## Engine invocation (`engine.cli`)
The invocation form is a **single setup-time binding** (ADR-0032): `wave-setup` authors `engine.cli` once, every downstream skill reads the configured value instead of choosing among invocation forms, and an absent binding is a STOP for the skill that hits it — never a cue to fall back to something else. Unlike store/eligibility/verify there is nothing to interview here: the consumer answer is almost always the same, and the only judgment is Node-vs-not.
**Author `engine.install` in the same pass, for the same reason** (ADR-0032 amendment 2026-09-04). A dispatched worktree carries **tracked files only**, so wherever `engine.cli` resolves through a gitignored path — the ordinary case below, a pinned binary under an ignored `node_modules/` — the binary is *absent there, not merely un-installed*, and the command that makes it exist is as much a setup-time binding as the command that calls it. Write both keys, not just the first.
**The consumer default: pin the published package as a devDependency, bind to the local binary it installs.**
```bash
npm install --save-dev @formtrieb/flotilla-engine
```
```json
{ "engine": { "cli": "./node_modules/.bin/flotilla-engine", "install": "npm ci" } }
```
Pinned by the consumer's own lockfile, so every checkout — including a Worker/Reviewer worktree that carries tracked files only — resolves the identical version. This is the form to reach for on every ordinary Node consumer; it is the whole reason the field exists, not one of several equally-good options (fast and reproducible against the pre-ADR-0032 published-package call — ADR-0032). `install` is the consumer's own dependency install, written **unfused and directory-carrying** (`npm ci`, `npm ci --prefix <depsDir>`, `composer install -d <depsDir>`) — never `cd <dir> && <install>`, which wave-shared Convention 13 forbids and which the same plain-argv rule refuses outright.
**And write that directory REPO-RELATIVE — `tools/wave`, never `/Users/…/tools/wave`.** This is a second rule with a second reason, unrelated to the allowlist rule that makes the *binding* repo-relative: `npm ci --prefix <dir>` exits `EUSAGE` with a corrupt-looking `Missing: <basename>@<version> from lock file` error — naming a package that is in neither the manifest nor the lockfile — whenever that prefix path resolves **through a symlink** (a scratch or temp root reached that way, e.g. macOS's `/tmp` → `/private/tmp`, is the ordinary trigger). A repo-relative prefix cannot reach that state, because it is resolved against the process's working directory, which the OS always reports physically. Do not reach for a different verb when you see it: no `npm ci` flag rescues the symlinked form, and `npm install` survives it only by never comparing the lockfile against the manifest at all. Mechanism and the measured positive/negative control: [evidence/engine-cli-symlink-mechanism.md](evidence/engine-cli-symlink-mechanism.md).
**The one deliberate exception: flotilla's own repo.** This repo binds `engine.cli` to the vendored value instead, because it builds what it runs, not because vendoring is a second consumer-facing choice:
```json
{ "engine": { "cli": "./tools/wave/node_modules/.bin/tsx tools/wave/src/cli.ts", "install": "npm ci --prefix tools/wave" } }
```
Never scaffold this value for a consumer repo — it names a `tools/wave/` checkout that only this repo carries.
**Non-Node consumers get the identical single form, not a second one.** The engine already needs a Node runtime to run at all; a Node consumer's devDependency install already covers it with nothing extra to do. A consumer with no existing Node install step (PHP, Rust, whatever the verify profile actually builds) is missing only the one-line manifest that makes that already-true prerequisite explicit and worktree-resolvable — this is not a new dependency, it is naming one that was always there. Scaffold a minimal tracked `package.json` + lockfile whose only dependency is the engine, run its install once to produce the lockfile and the binary, and **record that install command as `engine.install`** — ahead of whatever the consumer's own build install already needs, so the worktree's very first step resolves `engine.cli` before anything else needs it. The non-Node path writes the field exactly as the Node path does; a consumer whose own build ALSO installs something records the whole install as one plain argv command list, never two fused with `&&`. Exact manifest shape and commands: [reference/setup-mechanics.md](reference/setup-mechanics.md).
Once written, `config validate` reports both bound values verbatim (`engine.cli: <value>, engine.install: <value>`) — read that line to confirm the repo is bound to the forms you think it is, not merely that a binding exists. A present-but-malformed binding of either key (a shell metacharacter, an empty string, a `~`-rooted or absolute path) fails `config validate` loud rather than being read as unbound — fix the field it names and re-run. An **absent** `engine.install` is silent on that line and stays valid; what it is not is a confirmation that no install is needed, and `compose-driver` refuses a compose where that absence would strand a row (a gitignored `engine.cli` path with no install step from any source).
## Credentials
The credential set is **derived**, not fixed to one tracker — it depends on two independent facts: **which code host lands this consumer's PRs**, and **which tracker stores its issues**. Code host (detected the same way `host-pr preflight` detects it, off the git remote — [reference/setup-mechanics.md](reference/setup-mechanics.md#host-preflight-host-pr-preflight--code-host-posture)) supplies the landing credential: `GITHUB_TOKEN` on `github`; the Bitbucket pair — `BITBUCKET_TOKEN` plus the identifier `BITBUCKET_EMAIL` — on `bitbucket` (two token *flavors* are in play there, only one of which the create verb accepts — see [Bitbucket token](#bitbucket-token--the-two-flavors-and-which-one-host-pr-create-needs) below). Store kind adds `LINEAR_API_KEY` on top, for a `linear` store — independent of code host, because a `linear` store's wave PRs still land on whichever code host this repo's remote names, GitHub or Bitbucket. **Never assume `GITHUB_TOKEN` is in play**: a Bitbucket-hosted consumer never sets it and never needs to. Every one of these except `BITBUCKET_EMAIL` resolves through the engine's one credential seam (`credential-resolver.ts`, ADR-0029): a per-project **lookup command**, configured as `<VAR>_CMD` in the tracked settings `env` block, wins over the ambient `<VAR>` and fails loud rather than falling back silently. (`BITBUCKET_EMAIL` is an identifier, not a secret — it never gets a `_CMD` counterpart; see below.) **wave-setup's job is to scaffold this end to end, for whichever credentials this consumer's code host and store actually need** — the keychain item, the `<VAR>_CMD` entry, and the matching `permissions.deny` entry for each secret — not to hand the consumer a choice between placements where only one actually works.
**At a glance — one row per credential the toolkit can require.** This table summarizes the derivation above and points back into it; it is a scan index, not a second authority. A reader on any one host reads only the rows that apply to them:
| Variable | Secret or identifier | Applies when | Token flavor | Where it lives | Verify step |
|---|---|---|---|---|---|
| `GITHUB_TOKEN` | secret | code host is `github` | classic PAT (`repo` scope), or fine-grained (`Issues` + `Pull requests` + `Contents`, each Read and write) — [GitHub token permissions](reference/setup-mechanics.md#github-token-permissions) | `GITHUB_TOKEN_CMD` lookup command in the tracked `env` block | `host-pr preflight` — `pr-merge-token` |
| `BITBUCKET_TOKEN` | secret | code host is `bitbucket` | Atlassian account API token (Basic auth, paired with `BITBUCKET_EMAIL`) or a workspace/repository access token (Bearer, alone) — [Bitbucket token](#bitbucket-token--the-two-flavors-and-which-one-host-pr-create-needs) below | `BITBUCKET_TOKEN_CMD` lookup command in the tracked `env` block | `host-pr preflight` — `pr-merge-token` |
| `BITBUCKET_EMAIL` | plain identifier — no `_CMD` counterpart, no `permissions.deny` anchor | code host is `bitbucket`, **and** this wave will run `host-pr create` (every ordinary wave) | n/a — pairs with the account-API-token flavor of `BITBUCKET_TOKEN` above | plain value in the tracked `env` block | `host-pr preflight` — `create-credentials` (advisory) |
| `LINEAR_API_KEY` | secret | store kind is `linear` (independent of code host) | workspace or personal API key with read/write reach on the team — Linear does not split this as finely as GitHub | `LINEAR_API_KEY_CMD` lookup command in the tracked `env` block | `store-preflight` |
A Bitbucket-hosted consumer reads this table and sees `GITHUB_TOKEN` never applies to them — its row names `github` only. The prose above (and the sections below) remain the authority for the judgment behind each column; this table never replaces them.
### Two first-class paths, chosen by environment lifetime — not a spectrum
- **The lookup-command indirection is the wave-setup default**, for every long-lived interactive machine — an operator's own laptop, a persistent dev box. This section scaffolds it.
- **The ambient variable stays first-class for ephemeral environments** — CI injects a per-job-scoped token into a minutes-lived environment with no keychain to speak of, and that is by design, not a gap waiting to close. There is **no deprecation horizon** for the ambient path: a CI job that never has a keychain cannot ever "graduate" off it, and a deprecation whose end state is unreachable is not a real deprecation. To keep a consumer's CI on the ambient path even after its interactive machines adopt the lookup command, set `<VAR>_CMD=""` for that job — an empty or whitespace-only value reads as "not configured," and the resolver falls through to the ambient variable exactly as designed.
**There is no third rung, and no global shell-profile export.** That doctrine is retired, not one option among several — never hand a consumer a choice between it and the two paths above; every remaining placement question in this section resolves to one of the two.
### What wave-setup scaffolds — all three artifacts, one flow, per credential
Run this once per credential the consumer needs — the code-host landing credential (`GITHUB_TOKEN` on `github`; `BITBUCKET_TOKEN` on `bitbucket`, with `BITBUCKET_EMAIL` alongside it once this wave will run `host-pr create` — see [Bitbucket token](#bitbucket-token--the-two-flavors-and-which-one-host-pr-create-needs) below) plus `LINEAR_API_KEY` for a `linear` store: a keychain item, a `<VAR>_CMD` entry in the tracked `env` block, and its matching `permissions.deny` prefix. Exact JSON for all three, and the mechanical `<VAR>` → `<VAR>_CMD` naming rule: [reference/setup-mechanics.md](reference/setup-mechanics.md#credential-lookup-command-scaffold-adr-0029) — reach for it rather than re-deriving the shapes here.
1. **The keychain item**, under the platform's **default** ACL — never the `-T` restrictive form, which ADR-0029 rejected outright because it prompts on *every* read and breaks AFK dispatch by construction. The default ACL protects the secret **at rest**, not against a live local process in the operator's own session — an accepted trade targeting the accidental-leak class (Convention 8's echo/dump/read vectors), not a hostile-local-process threat model. Whether the item's very **first** value-read prompts once for an explicit Always-Allow click is **environment-conditional** — ADR-0029 no longer asserts either way; the live-gate below settles it per machine, run it and observe rather than assuming. Regenerating a token: `add-generic-password` refuses to overwrite an existing item, so delete the old one before re-adding, or every `_CMD` lookup keeps silently resolving the stale value. GUI cross-check and the exact commands: reference doc above.
2. **The `<VAR>_CMD` entry**, merged into the same `env` block as the proxy flag. `BITBUCKET_EMAIL` is the one exception — an identifier, not a secret, so it lands as a plain `env` value with no `_CMD` counterpart (see [Bitbucket token](#bitbucket-token--the-two-flavors-and-which-one-host-pr-create-needs) below). For every credential that does resolve through `<VAR>_CMD`: a non-blank value always wins over the ambient `<VAR>` and fails loud on a bad exit code, timeout, or empty stdout — never a silent fallback, which would re-open the wrong-token class invisibly (believing you run repo-scoped while actually running over-broad).
3. **The `permissions.deny` entry for the exact lookup-command prefix** — blocks the harness's own Bash tool from running the literal command just written into `<VAR>_CMD`. This closes the residual vector the indirection itself opens (ADR-0029): the tracked environment now carries a *pointer*, not the secret, so the leak class morphs from "expand the variable" to "execute the lookup command directly." Anchors the **direct** form only — a wrapped form (`echo $(security …)`) is a known gap left to a future PreToolUse-hook candidate; it does not touch the engine's own resolution, which runs inside an already-allowlisted CLI invocation, a different actor than an agent's Bash tool reaching for the command directly.
These three merge into the same tracked `.claude/settings.json` scaffold as [Scaffolding the tracked permission allowlist and env block](#scaffolding-the-tracked-permission-allowlist-and-env-block) below — one file, landed edit-tool-first / plain-`cp`-second per step 9 there (never the leading-`!` shortcut; byte-for-byte read-back mandatory).
### The live-gate — one first resolution proves the wiring, and may cost one click
Once all three artifacts exist, run one resolution through the real seam, value-free — re-running the store-preflight already in the procedure below (Procedure step 6) doubles as this:
```bash
{{wave-cli}} store-preflight --config wave.config.json
```
**Whether this needs a click first is a fact about this consumer's own machine, not a fixed property of the scaffold — never an assumed answer either way.** If `store-preflight` fails with the credential unresolved rather than passing straight away, that is not necessarily a broken scaffold: run the reference doc's own step 1 directly, click **Always Allow** (not the one-time "Allow") if a system dialog appears, then re-run `store-preflight`. Read only the exit code here — nothing in this flow ever needs the resolved secret printed (SECRET-SAFE: never execute a configured `_CMD` value directly to "check" it — its stdout *is* the secret).
**Run this re-query through the harness, not a bare external terminal.** `<VAR>_CMD` lives only in the tracked settings `env` block, which a plain external shell never loads — the identical command run there fails with a real-looking "not configured" error that is accurate about *that terminal's* environment and nothing else, not a sign the scaffold is broken.
**Call this a live-gate, whichever way this machine's own first read falls.** ADR-0029 no longer asserts the scaffolded keychain item never prompts by construction — that holds on some environments, not others (the ADR's dated amendment carries the evidence). What the step proves is the same either way: the wiring works end to end, right now, with a human present to click through whatever dialog appears — nothing about a wave's later, unattended reads depends on which case this machine turned out to be.
For a **session-authenticated resolver** (`op`/1Password CLI is the standing example — a command backed by its own login session, not a keychain item), this same first resolution **doubles as the auth exercise**: every later resolution, including the ones a dispatched Worker triggers mid-wave, reuses an already-live session instead of hitting a fresh login prompt with nobody there to answer it.
**Resolvers that require *per-invocation* confirmation are AFK-incompatible — say so, never scaffold one for wave use.** A prompt firing once at setup is fine; one firing on *every* resolution is not — `wave-start`/`wave-close` dispatch unattended, and a `_CMD` blocking on a per-call confirmation mid-wave has no one to answer it (it times out at 60s and fails that row loud, not gracefully). If the only secret-manager CLI a consumer has requires per-invocation confirmation, tell them it does not fit `<VAR>_CMD` and use the ambient path for them instead.
### The interim for non-adopters: direnv + keychain — an explicit half-measure
A consumer not yet ready to adopt the lookup-command indirection should still not fall back to one global shell-profile export — that reopens the exact wrong-scope problem the indirection exists to close (one over-broad token shared across every repo the operator touches, because a shell profile has no notion of "which project"). The interim: **direnv**, scoped to the project directory (a `.envrc` that only loads inside this repo's checkout), pulling the value out of the same kind of keychain item as above and exporting it for that shell only:
```bash
# .envrc — direnv loads this only when your shell cd's into this repo
export GITHUB_TOKEN="$(security find-generic-password -a $USER -s flotilla-github-token -w)"
```
**State this to the consumer as an explicit half-measure, never as a second recommended path.** It solves the *wrong-scope* half — a per-project export instead of one global one — and stops there. The secret still lands in `process.env` for the life of that shell, so the whole echo/dump/read leak class (Convention 8) stays exactly as open as the doctrine it narrows the blast radius of; scope improves, exposure does not. Reach for the lookup-command scaffold above whenever the consumer's environment can support it (a keychain, or a session-authenticated CLI reachable from the wave's own process); reach for direnv+keychain only as the interim for a consumer not yet ready to adopt it, and say so out loud when handing it over.
### GitHub token — what it must be able to DO
`GITHUB_TOKEN` existing is not the precondition — a token that exists but is under-scoped passes a bare presence check and then breaks mid-wave, sometimes in the dangerous direction (an under-scoped token has read a missing branch protection as *absent* rather than reporting the gap, consumer finding IR-F2). Check scope against the engine's actual call surface **before** the first wave. The full endpoint-by-endpoint table (every call the engine makes, and the classic scope / fine-grained permission each needs) is in [reference/setup-mechanics.md — GitHub token permissions](reference/setup-mechanics.md#github-token-permissions); the two points worth stating here directly:
- **Classic PAT: the `repo` scope covers every engine call** (issue CRUD, labels, comments, PR open/read/merge, the auto-merge arm, branch delete, the posture reads). **Fine-grained PATs need three separate repository permissions**, because GitHub splits what `repo` bundles into one scope: `Issues: Read and write`, `Pull requests: Read and write`, `Contents: Read and write`. Naming only the one the arm mutation needs (`Pull requests`) and skipping the other two is the exact way a fine-grained setup passes `store-preflight` and then fails the first issue-labeling call.
- **`workflow` scope (classic) / `Workflows: Read and write` (fine-grained) — only if this wave will touch `.github/workflows/**`.** This is not an engine API permission at all — it gates the `git push` itself: GitHub refuses a pushed commit that adds or modifies a workflow file unless the token carries it, independent of every other scope being correct. This STOPped a wave at the push step once already (W23) because nothing said so beforehand. Check every row's declared Files globs before the wave, not after a Worker's push fails: if none touch `.github/workflows/**`, skip it; if any row's glob does, grant it before dispatch.
### `GITHUB_TOKEN` vs `gh`'s own auth — two different credentials
The engine talks raw HTTP with `GITHUB_TOKEN` for every wave operation — issue CRUD, PR open/merge/arm, both preflights — and never shells out to `gh` (ADR-0019/0023: `gh`'s credentials are sandbox-denied and its TLS has failed under the sandbox in every live run, ADR-0015). `gh` carries its own, separately-authenticated credential (`gh auth login` / the system keyring), used only when *you* run a `gh` command directly, outside the wave pipeline — most commonly `gh repo create` when bootstrapping a brand-new consumer repo, which needs a permission (creating repositories) the wave token deliberately does not carry.
Because these are two independent credentials, a setup can be **half-authenticated in a way that presents as one confusing failure**: `gh auth status` can report an invalid keyring token while `GITHUB_TOKEN` is simply unset (or the reverse) — two distinct problems, easy to misread as one. Check both, separately, before concluding either is fine:
```bash
gh auth status # gh's own credential — only matters if you run gh commands yourself
[ -n "$GITHUB_TOKEN" ] && echo set # value-free presence check — the engine's credential
```
If you need to run `gh repo create` (or any `gh` command needing a permission the wave token lacks) from a shell where `GITHUB_TOKEN` is already exported: `gh` picks up `GITHUB_TOKEN` as an auth override ahead of its own keyring credential, and the call fails with the wave token's narrower scope. The fix is `env -u GITHUB_TOKEN gh repo create …` — unset it for that one invocation so `gh` falls back to its own credential. Document this as the actual mechanism (`gh`'s documented env-override precedence), not as folklore nobody can explain.
### Bitbucket token — the two flavors, and which one `host-pr create` needs
`BITBUCKET_TOKEN` is not one credential shape — **two live consumers can each hold a genuinely different token and both be correctly set up**, because Bitbucket Cloud accepts two distinct flavors and the engine's adapter (`bitbucket-api.ts`) speaks both:
- **An Atlassian account API token**, paired with the account's own email — Bitbucket's Basic-auth pair (`--user '{account_email}:{api_token}'`).
- **A workspace or repository access token** — a Bearer-only credential scoped to one workspace or repo, with no account email attached at all.
Ask which flavor the consumer already holds before wiring `BITBUCKET_TOKEN` — the two are not interchangeable, and only one of them can open a PR. The adapter's own rule is mechanical: `BITBUCKET_EMAIL` set → Basic (`email:token`); unset → Bearer (`token` alone).
**The landing verbs speak both shapes; `host-pr create` speaks only one.** `host-pr status | preflight | arm | merge` — every verb that reads or lands an *already-open* PR — accept either flavor, so **an access token alone (Bearer, no email) is a complete setup** for a wave that only needs to land rows. `host-pr create` — the verb every wave Worker's terminator calls to open the PR in the first place — is **Basic-only, deliberately** (a seam boundary, not an oversight): it refuses loud and typed without `BITBUCKET_EMAIL`, and an access token can never satisfy it regardless of whether an email happens to be set alongside it. Atlassian retired the app-password username:password pairing this call used to accept on 2026-06-09 (removed 2026-07-28); the account-API-token-plus-email pair is its replacement, not an alternative to it.
**So the doctrine is a split, not "email always":**
1. Name which token flavor this consumer holds (Atlassian account API token vs. workspace/repository access token) — the two questions above decide it.
2. Token-only (Bearer) is a complete, correct setup right up until a wave needs to open a PR.
3. The moment `host-pr create` is in play — every ordinary wave — the consumer needs the account API token, `BITBUCKET_EMAIL` alongside it.
**`BITBUCKET_EMAIL` is an identifier, never a secret — keep it out of the keychain/lookup-command path entirely.** It lands as a plain value in the tracked `env` block, the same channel `BITBUCKET_TOKEN_CMD` and the proxy flag already live in, but with no `_CMD` counterpart of its own:
```json
{
"env": {
"NODE_USE_ENV_PROXY": "1",
"BITBUCKET_TOKEN_CMD": "security find-generic-password -a $USER -s flotilla-bitbucket-token -w",
"BITBUCKET_EMAIL": "operator@example.com"
}
}
```
Scope and the `create-credentials` preflight check that grades this precondition: [reference/setup-mechanics.md — Bitbucket token permissions](reference/setup-mechanics.md#bitbucket-token-permissions).
### Linear key — same treatment, plus the integration precondition it cannot substitute for
`LINEAR_API_KEY` needs read/write reach on the team's issues, comments, and labels — a workspace or personal API key with access to the team, since Linear does not split this as finely as GitHub's fine-grained permissions; team membership is the practical gate. Same placement answer as `GITHUB_TOKEN`: scaffold `LINEAR_API_KEY_CMD` through the same three-artifact flow above for a long-lived interactive machine, or leave it deliberately ambient for an ephemeral environment — never the raw key itself in any settings file, tracked or local.
**The key is not a substitute for the Linear↔GitHub integration — it is a separate precondition entirely, and no key can stand in for it.** The key lets the engine read and write Linear issues and workflow states; it has no bearing on whether a merged GitHub PR ever creates the attachment the closing-probe reads. Only the installed integration (probed as WORKSPACE-level presence only — never a team-or-repo binding — confirmed under [Linear operational preconditions](#linear-operational-preconditions) above) does that. A correctly-scoped key on a workspace with no integration installed will run a wave that never resolves any row to `done` — silently, since nothing about the key itself is wrong. `store-preflight` probes the integration for real; the key existing and working is necessary, never sufficient.
## Procedure
1. **Interview the four concerns.** One round-trip per concern (store kind → eligibility → verify → models). For store: ask which tracker the consumer uses; if it's Linear, also gather the team, the optional project, and whether the default workflow-state names or category-label names need overriding (`states`/`categoryLabels`), and walk through the Linear operational preconditions above. **Ask about all five defaulted state names, not just the three rungs** — the two easiest to forget are `unclaimTarget` (which column an unclaimed issue is parked in — `Backlog` by default, but a team that parks in `Todo` or an `Icebox` column has to say so) and `unplanned` (which column a not-planned close lands in — `Canceled` by default). Both are real consumer variance, both fail the state-catalog preflight loudly when the configured name is absent, and one shipped consumer already overrides `unclaimTarget`. For eligibility: confirm the default is right or gather the custom set. For verify: ask whether they have a build gate and, if yes, what files it applies to and what commands to run — then **measure** the resolving form of those commands before recording them (see [Worktree-brief inputs](#operational-preconditions-all-stores), item 3) rather than composing the profile from the description alone, and ask the [needs question](#3-verify-optional) per command in the same pass (`writes`/`network`/`host`, recorded as `needs`). For models: ask which id binds `heavy`, `standard` and the driver's cheap `scribe` stage, record only what they answer, and say what omitting `scribe` costs ([Models](#4-models-optional)). Don't ask field-by-field — one structured question per concern.
2. **Compose the JSON.** From the answers, assemble the `WaveConfig`, adding `landing` only per [Landing message](#5-landing-message-optional). The exact shapes are in [reference/setup-mechanics.md](reference/setup-mechanics.md).
3. **Author `engine.cli` AND `engine.install`.** Install the pinned package as a devDependency (`npm install --save-dev @formtrieb/flotilla-engine`) for a Node consumer, or scaffold the non-Node manifest and run its install once, then set BOTH bound values into the JSON you are about to write — `./node_modules/.bin/flotilla-engine` for the ordinary invocation case (the vendored value only for this repo's own dogfood config), and this consumer's own dependency-install command, unfused and directory-carrying, as `install`. The second key is not optional in practice wherever the first resolves through a gitignored path — the ordinary case; check with `git check-ignore` rather than assuming (ADR-0032 amendment 2026-09-04). See [Engine invocation](#engine-invocation-enginecli) above for the judgment and [reference/setup-mechanics.md](reference/setup-mechanics.md) for the exact commands.
4. **Write the file.** Default path: `wave.config.json` at the consumer repo's root. The consumer commits it alongside their codebase. While the consumer repo root is already in hand for this write, also add the two flotilla-owned entries — `.flotilla/tmp/` and `.claude/worktrees/` — to the consumer's `.gitignore` — see [`.gitignore` scaffold — the Scribe scratch path](reference/setup-mechanics.md#gitignore-scaffold--the-scribe-scratch-path-issue-355) for the exact line and why it exists (it covers the in-flight window a close-time sweep alone does not).
> **In the flotilla toolkit repo itself, no config is committed.** flotilla is the toolkit, not a consumer — it has no wave store of its own. Dogfood use (`wave-plan` / `wave-create` within this repo for testing) uses `--config <path>` to point at a temp config file. Never commit a `wave.config.json` to the flotilla repo root.
5. **Validate.** Run the `config validate` verb (exact command and exit codes: [reference/setup-mechanics.md](reference/setup-mechanics.md)). Exit 0 means valid — and, when `engine.cli` is set, the printed line echoes the bound value verbatim. On a non-zero exit, read the reported field, fix it in the JSON, and re-run. Do not hand the config to downstream skills until it exits 0.
6. **Preflight the store (tracker facts).** Run `store-preflight` (exact command, checks, and exit codes: [reference/setup-mechanics.md](reference/setup-mechanics.md#store-preflight-store-preflight--tracker-facts)) — it probes the *live* tracker integration and workflow-state catalog for real, past what `config validate` can see. On exit 1, fix the gap its `detail` names (in Linear/GitHub or the config) and re-run; do not hand off on an assertion where a run is available. **On a `github` store the gap is almost always the label set** — the `state-catalog` check lists every label the wave reads or writes that the repository lacks (the eligibility label, `risk/*`, `worker/*`, the `wave/*` rungs and `wave/needs-attention`; thirteen on a fresh repo with the defaults). **Re-run the same verb with `--create-missing-labels`** — the primary path: it creates exactly that set through the engine's own credential, then re-probes and names each label it created (idempotent, so a repeat run is safe and says it created nothing). Demote the `gh label create` loop to the fallback, for an operator whose engine credential cannot create labels — an under-scoped token, or a repo where only their own `gh` login has write reach: hand them the exact list one `gh label create` per name (or the repo's label settings page) and re-run the preflight once they report back. Timing evidence for why this flag exists: [evidence/settings-handoff-timing.md](evidence/settings-handoff-timing.md#why---create-missing-labels-exists).
7. **Preflight the host (code-host posture).** Run `host-pr preflight` — store-blind, so it runs identically on `github`/`linear`/`markdown` (landing always happens on the code host, ADR-0023 amendment). Four checks (`pr-merge-token`, `allow-auto-merge`, `required-checks`, Bitbucket-only `create-credentials`), full statuses and fix instructions: [reference/setup-mechanics.md](reference/setup-mechanics.md#host-preflight-host-pr-preflight--code-host-posture). Exit 1 names the gap in the failing check's `detail` — fix it on the code host and re-run. The non-checkable preconditions (magic-word phrase, PR-route discipline, team convention, AFK permission allowlist) still need by-hand confirmation from the checklists above.
8. **Scaffold the credential lookup command, then re-run the live-gate.** For each credential this consumer runs waves with: the keychain item, the `<VAR>_CMD` entry, and the matching `permissions.deny` prefix — per [Credentials](#credentials) above for the judgment, [reference/setup-mechanics.md](reference/setup-mechanics.md#credential-lookup-command-scaffold-adr-0029) for the exact JSON. `BITBUCKET_EMAIL` skips this trio (identifier, not a secret). Re-run `store-preflight` (step 6) once more — a clean re-run **is** the live-gate, proving the `<VAR>_CMD` resolves ahead of any stray ambient variable with you present to fix it if not. Skip the scaffold, deliberately, for a credential staying on the ambient path by design (an ephemeral CI environment, or a not-yet-adopting consumer on the direnv+keychain half-measure) — and say so plainly in your report.
9. **Scaffold the AFK harness config — try the edit tool first; a plain-terminal hand-off is the fallback only.** Compose the tracked `.claude/settings.json` per [Scaffolding the tracked permission allowlist and env block](#scaffolding-the-tracked-permission-allowlist-and-env-block) above, then land it via the two-path rule (full mechanism and reason: [reference/setup-mechanics.md](reference/setup-mechanics.md#afk-harness-config-scaffold-env-block--permission-allowlist-claudesettingsjson)) — **the sandbox structurally refuses a Bash `cp` here; the edit tool is only permission-gated.** **Path 1:** stage the complete, valid file in the session scratchpad, then write it straight to `.claude/settings.json` with the edit tool. **Path 2, only if declined:** hand the operator a plain-terminal `cp <staged path> .claude/settings.json` to run from their own prompt — never the CLI's leading-`!` shortcut. **Either path, read the file back and confirm it matches the staged copy byte for byte before continuing.** Both guard-hook copies land the same two-path way. Timing evidence: [evidence/settings-handoff-timing.md](evidence/settings-handoff-timing.md#cost-of-the-afk-harness-config--guard-hook-hand-offs). The file carries: the `env` block (proxy flag + step 8's `<VAR>_CMD` entries), `permissions.allow` (the one `engine.cli` form + the measured AFK command block), and `permissions.deny` (the secret-echo anchors + step 8's lookup-command entries). A worktree carries tracked files only, so this file — never the gitignored local settings — is the sole permission/environment source a dispatched agent inherits.
**Verify the write actually landed somewhere trackable — a clean `git status` is not proof.** Run `git check-ignore` against all three scaffolded paths — `.claude/settings.json` and BOTH hook copies, `.claude/hooks/echo-guard.cjs` and `.claude/hooks/conv12-guard.cjs` (the copies [reference/setup-mechanics.md](reference/setup-mechanics.md) writes into the same directory): each must find no match (exit 1); a match (exit 0) is a **STOP** — a consumer that gitignores `.claude/` wholesale (an ordinary starter-template line) makes `git add .claude` stage nothing, silently, and every future dispatched agent then runs with no allowlist, no env block, no guard hooks. Narrow the consumer's `.gitignore` — never widen this scaffold to route around it — then re-run all three checks; cross-check with `git add --dry-run .claude`, which must list exactly the three scaffolded paths. Narrowing recipe: [reference/setup-mechanics.md](reference/setup-mechanics.md#trackability-verification-issue-494).
**The `sandbox` block (writes/network) joins the SAME staged file, confirmed first on this machine (ADR-0049 — the carrier itself is measured; see above).** Before this step's composition includes a single `sandbox.filesystem.allowWrite` / `sandbox.network.allowedDomains` entry, run the probe with real positive and negative controls: [reference/setup-mechanics.md — Measuring whether the tracked sandbox block is honored](reference/setup-mechanics.md#measuring-whether-the-tracked-sandbox-block-is-honored--the-probe-issue-716). A negative result stops the sandbox portion here — disclose the alternative carrier in your report and operator checklist, and compose/hand off only the `env`/`permissions`/`hooks` portions above, unchanged. A positive result (the ordinary case) adds the block, derived as the union of every verify command's declared `writes`/`network` needs, to the same staged file and the same two-path landing above (edit tool first, plain `cp` only if declined; byte-for-byte read-back mandatory) — no second write path. Judgment and exact JSON: [Scaffolding the tracked sandbox block](#scaffolding-the-tracked-sandbox-block-writesnetwork--the-capability-requirement-adr-0049) above.
**Once the file lands, run the live-gate: every needs-bearing verify command, once, for real, inside the now-scaffolded sandbox.** Record pass/refused per command. A refused command whose `writes`/`network` need was just declared and scaffolded is a **setup STOP** — quote the refusal, fix it here, never hand it to a wave as a "first-wave finding." A `host`-needing command refusing is expected, not a STOP. Full procedure: [reference/setup-mechanics.md — the live-gate](reference/setup-mechanics.md#the-live-gate--run-each-needs-bearing-command-once-inside-the-now-scaffolded-sandbox).
10. **Report what happened.** A short factual record — where the file landed (absolute path), the `engine.cli` binding you authored (which form, and whether it was the pinned devDependency or the non-Node manifest), both preflight results (store + host), the credential scaffold outcome (which credentials got the lookup-command treatment vs. stayed deliberately ambient, and the live-gate result for each), the tracked harness-config scaffold you wrote (path + confirmation the `env` block sets the proxy flag and carries every `<VAR>_CMD`, that the configured engine-CLI form — prefix-free and env-prefixed — plus the full AFK worker command block (workspace mechanic, scoped wave-branch push, probe-worktree creation, sibling merge-tree prediction, and the consumer-tunable deps installer + verify-gate commands) are all on the allowlist, and that the `permissions.deny` secret-echo and lookup-command anchors are present), and the sandbox capability scaffold outcome (the measurement's result — positive or the negative STOP with the alternative carrier named; which verify commands declared `writes`/`network`/`host` needs; which of those landed in the tracked `sandbox` block; and the live-gate pass/refused per needs-bearing command). This record explains **what was done**. It is not where the operator learns what to do next — that is step 11, and nothing actionable should live only here. If an item would appear in both, cut it from this record and leave it in step 11 — one appearance, not two.
11. **Close with a single ordered operator checklist — the last thing you print, and the only place a first-time operator needs to read.** List every item still required of a human, in the order it must be performed, nowhere else in the report. Each item states, in about one line: **where** (the exact file, setting, or UI location), **what shape** (the literal value, command, or form it takes), and **why** (the concrete consequence of skipping it — not a restated fact). Build the actual list from what this run surfaced (a clean run's checklist is short; a first-time repo or a failed preflight carries more) but hold this order whenever an item applies:
1. **Credentials**, per credential this consumer runs waves with (the code-host landing credential — `GITHUB_TOKEN` on `github`; `BITBUCKET_TOKEN` on `bitbucket`, plus `BITBUCKET_EMAIL` once this wave will run `host-pr create` — every ordinary wave does; `LINEAR_API_KEY` too for a `linear` store) — either (a) the lookup-command scaffold from step 8 is in place (keychain item + `<VAR>_CMD` + deny entry) and its live-gate re-run passed, or (b) this credential is deliberately staying ambient (an ephemeral CI-style environment, or a not-yet-adopting consumer on the direnv+keychain half-measure) and that choice is stated, not defaulted into. Why: the lookup-command indirection is the wave-setup default for a long-lived interactive machine; a credential left on neither path — no scaffold, no stated ambient choice — is exactly the gap between "set up" and "actually wired." See [Credentials](#credentials) above.
2. **Sandbox capability needs**, per verify command that declared `writes` or `network` (ADR-0049) — either (a) the tracked `sandbox` block scaffold is in place and its live-gate (a run of that command, once, inside the sandbox) passed, or (b) the sandbox-block measurement came back negative and every such need stays deliberately declared-but-unprovided, deferring `capability-gated` on every wave, stated plainly rather than left implicit. Why: a declared need nobody provided is the exact five-hours-of-one-permission-prompt failure this precondition exists to close — catching it here costs one setup-time click, not an unattended stall mid-wave. See [Scaffolding the tracked sandbox block](#scaffolding-the-tracked-sandbox-block-writesnetwork--the-capability-requirement-adr-0049) above.
3. **Code-host token scope** — on `github`, checked against the table in [reference/setup-mechanics.md](reference/setup-mechanics.md#github-token-permissions) (call out `workflow` scope (classic) / `Workflows: Read and write` (fine-grained) by name if any row's declared Files glob touches `.github/workflows/**`); on `bitbucket`, checked against [reference/setup-mechanics.md](reference/setup-mechanics.md#bitbucket-token-permissions). Why: an under-scoped token does not always fail loudly — it can pass every preflight and then break mid-wave, or misreport a real protection as absent.
4. **Every `store-preflight` / `host-pr preflight` check that did not `pass`**, fixed at the exact location its `detail` names (e.g. Settings → General → Pull Requests → "Allow auto-merge"; the missing Linear workflow state; the missing Linear↔GitHub integration), then the preflight re-run to confirm it now exits 0. Why: a `fail`ed check means `wave-plan`/`wave-create` would be handed a config that cannot actually run a wave to completion.
5. **Commit `wave.config.json`, the non-Node manifest + lockfile (if scaffolded), and the tracked `.claude/settings.json` scaffold**, at the consumer repo root, in a normal commit. Why: all are tracked-file preconditions the wave pipeline and every dispatched agent depend on — uncommitted, they are invisible to the next session and absent from every worktree.
6. **`gh auth status`** — `github`-hosted consumers only (`gh` has no Bitbucket equivalent in this flow), and even there, only if you intend to run any `gh` command yourself (e.g. `gh repo create`) outside the wave pipeline — checked separately from `GITHUB_TOKEN`, since the two credentials fail independently and a mixed failure reads as one confusing error otherwise. See "`GITHUB_TOKEN` vs `gh`'s own auth" above.
7. **The by-hand confirmations this store kind needs, phrased as the action that closes them** — for `linear`: tell the team new/backlog work goes to the configured `states.unclaimTarget` column (`Backlog` unless overridden), not the configured `states.queued` one (`Todo` unless overridden) — name their actual columns; confirm every wave PR body will carry `Fixes <TEAM-NN>`, not `Closes #N`; confirm the repo lands wave branches via PR against a protected default branch, never a fast-forward-only merge. Each is a `wave-setup`-cannot-see precondition (the [Linear operational preconditions](#linear-operational-preconditions) checklist above) — state it as what to go do, not as the fact alone.
8. **Editor/IDE exclusion**, confirmed once before the first wave: either the consumer's editor excludes the wave worktree location and heavy build/dependency directories (e.g. `.build`, `node_modules`) from indexing and file-watching, or `wave-start` runs headless. Why: an attached editor indexing a fresh worktree checkout has frozen an editor's extension host before, presenting as a hung wave rather than the editor-configuration gap it actually was.
9. **Next step** — `wave-plan` (issue breakdown + wave roster) or `wave-create` (if a breakdown is already approved). Say **"no wave-ready issues yet"** when the eligible pool is empty, never "no issues": an issue that exists but carries no readiness marker is the ordinary starting state, and `triage` is what shapes it — a fresh consumer's seeded issue was reported as absent on a live run because the eligible pool, not the tracker, was read.
A reader who has never set up flotilla before must be able to follow this list top to bottom and finish setup without reading back into the record from step 10.
12. **Offer the consumer orientation block — the last step, and only on an explicit yes.** Nothing above reaches an agent doing ordinary work in this repo between waves: between one wave and the next it inherits none of the standing context that keeps a wave safe — which file binds every flotilla skill, what the stations are called, that the default branch is protected and every change lands through a pull request, that the claim labels/states belong to the wave engine and are never hand-edited, that an issue's triage state is the readiness signal a wave reads, or where to send a flotilla finding. Offer to write a short block naming all of that into the consumer's own house file.
**Target file.** `CLAUDE.md`, or `AGENTS.md` instead when that file already exists in this repo as its house file; default to `CLAUDE.md` when neither exists yet.
**Idempotent by marker.** Check the target file, if it exists, for the line `<!-- flotilla:consumer-orientation -->`. Found anywhere in it → the block is already there: say so, offer nothing, write nothing — this holds even when the operator has since edited the surrounding text, which is left exactly as edited. Not found (including a target file that does not exist yet) → continue.
**Preview, then ask — write only on a clear yes.** Show the operator the block verbatim, then ask whether to add it. On yes: append it (one blank line ahead of it) to the target file, or create the file with just this block as its content if it does not exist yet. On anything else — no, silence, an ambiguous answer — write nothing, note "declined" in your report, and move on: setup is still complete either way.
Exact block text and the append/create mechanics: [reference/setup-mechanics.md](reference/setup-mechanics.md#consumer-orientation-block).
## Validating the quickstart on a throwaway consumer
The README quickstart is measured, not remembered: a throwaway repository is created under the organisation, the quickstart is followed verbatim in installed form through this skill and one one-row wave, every disagreement between the text and the tool is recorded, and the repository is deleted. The create-and-teardown scaffold, the measured run of 2026-09-03 and its numbers live in [reference/throwaway-consumer.md](reference/throwaway-consumer.md) — the first instance of the golden-wave mechanic, to be re-run whenever the quickstart text or this skill changes.
## Common Mistakes
- **Committing a real config into the flotilla toolkit repo.** flotilla is the toolkit — it has no issue store. The `wave.config.json` belongs to the consumer repo, not here.
- **Inventing a `DEFAULT_VERIFY` or guessing a command.** Verify is purely consumer config. If the consumer has no build gate, omit `verify` entirely.
- **Composing `verify.profiles[].commands` straight from the consumer's description, without measuring it.** A described command is not automatically the one that resolves inside a Worker's or Reviewer's fresh worktree — a nested dependency directory (this repo's own `tools/wave/`) is exactly where a bare `npx vitest run` looks right and silently reaches for the npm registry instead of the local binary, with nothing loud to catch it until a dispatched agent's verify gate fails mid-wave. Run each command for real before recording it (see [Worktree-brief inputs](#operational-preconditions-all-stores), item 3, and setup-mechanics.md's worked example) — resolution proven by execution, not inspection.
- **Hardcoding a native label instead of the canonical eligibility role.** Pass canonical role names (e.g. `"ready-for-agent"`); the adapter resolves them to the store's native representation from config.
- **Adding a `schema` or `triageSchema` override.** Vocab file-override is deferred — engine defaults only in M1. Do not invent a config field for it; the engine will reject unknown fields.
- **Skipping `config validate`, `store-preflight`, or `host-pr preflight`.** Writing the file is not enough — `config validate` proves the engine can parse it, `store-preflight` proves the *live* tracker preconditions hold (integration, state catalog), and `host-pr preflight` proves the *live* code-host posture (merge token, allow-auto-merge, required-checks). Round-trip through all three before handing off to downstream skills.
- **Asserting the preconditions instead of running the preflights.** Do not read the checklist and tell the consumer "assuming the integration is installed, the states exist, and auto-merge is on." Run `store-preflight` **and** `host-pr preflight` — they probe for real and name the exact gap on failure. The only preconditions you confirm by hand are the ones the engine genuinely cannot see (magic-word phrase, PR-route discipline, team convention, AFK permission allowlist, and the PR-automation rules the advisory `gitAutomation` reading lists as `undecided`, or all of them when it could not read them).
- **Assuming "Allow auto-merge" is on.** GitHub ships it OFF by default, and `wave-close --auto` cannot arm a checks-pending PR without it. `host-pr preflight` `fail`s on a CI repo where it is off — tick it (Settings → General → Pull Requests) before the first `--auto` wave, or land via the advisory merge-order.
- **Setting a `repo` field on a `github` store config.** The `gh` ambient context supplies the repo; there is no `repo` field on `GitHubStoreConfig`. Adding one will fail validation.
- **Leaving the AFK permission allowlist (or the env block) to the operator instead of scaffolding it.** A worktree inherits **tracked settings only**, so a gitignored `.claude/settings.local.json` never reaches a dispatched Worker/Reviewer; "the operator will add it" is not enough when the tracked file is the sole permission/environment source it gets.
- **Trusting a clean `git status` as proof the AFK scaffold is trackable.** Writing `.claude/settings.json` succeeds even when a consumer's `.gitignore` excludes `.claude/` wholesale — the write never errors, `git add .claude` stages nothing and still exits 0. Run the Procedure step 9 trackability verification every time, not only when a `.gitignore` looks unusual — an ordinary starter-template line is exactly the shape that trips it.
- **Skipping the `permissions.deny` secret-echo anchor because "the brief clause already covers it."** It doesn't, structurally — Convention 8 binds every role that produces tool output, including the Coordinator, which never receives a brief for a deny rule to reach it through. Scaffold both: the deny entries as the structural anchor, the brief clause as defense-in-depth for the roles that do read one.
- **Scaffolding the `npx` or vendored engine-CLI form into a consumer's allowlist (undoes ADR-0032).** A consumer's allowlist carries exactly the configured `engine.cli` form plus its env-prefixed twin, never a second path "just in case" — the invocation fallback chain is abolished. `npx @formtrieb/flotilla-engine` is a pre-setup bootstrap only, and the vendored `tools/wave/…` form is flotilla's own documented exception, never a fresh consumer's fallback.
- **Leaving `engine.cli` unset and letting downstream skills guess.** An absent binding is a STOP for the skill that hits it (ADR-0032) — not a gap wave-setup can leave for later. Author it in the same procedure pass that writes the rest of the config (Procedure step 3), before the file is written.
- **Treating the non-Node manifest as a new dependency instead of an existing one made explicit.** The engine already needs a Node runtime to run at all; the scaffolded `package.json` + lockfile only names that prerequisite so a worktree can resolve it. Presenting it to the consumer as "now you also need Node" invents a requirement that was already there.
- **Scaffolding only the engine-CLI invocation forms and calling the allowlist done.** The engine CLI is not the whole command surface a dispatched Worker or Reviewer runs — the workspace mechanic, the wave-branch push, the Reviewer's probe-worktree + merge-tree calls, and the verify-gate commands all pass through the same permission gate, unattended (docs/retros/2026-07-29-overnight-triple-wave.md, NF-F1). Scaffold the full measured block from [reference/setup-mechanics.md](reference/setup-mechanics.md), not only the engine-CLI entry.
- **Widening the wave-branch push entry to a bare `git push:*`.** The scoped form (`git push origin wave/:*`) is what makes the protected default branch structurally unreachable from a dispatched Worker; a bare `git push:*` undoes that guarantee.
- **Skipping the tracked `env` block and relying on allowlist prefix-duplication alone for the proxy flag.** An un-prefixed call then both silently drops the proxy (wave-shared Convention 1) and fails the allowlist's literal prefix match — two failures from one miss. Scaffold the env block so the flag holds regardless of prefix, and keep both prefix-free and env-prefixed allow entries.
- **Scaffolding `docker` into the tracked `excludedCommands` (host-escape).** A tracked un-sandboxed `docker` grants a host escape to every future agent of the repo, not just this wave's Workers. Keep it in the operator's untracked local settings and brief the Worker for graceful degradation (socket-free floor always — `docker compose config` + `bash -n`; live run only when the socket is reachable; precise deferral disclosure otherwise). This is the rule for the whole `host` class (ADR-0049), not a `docker`-specific one — the next mistake names the general form.
- **Scaffolding a `needs.host: true` command's requirement into ANY tracked file.** The `host` class — a daemon socket, a simulator, a device — cannot be narrowed to a path or a host the way `writes`/`network` can, so there is no exact grant to write; only a blanket, repo-wide one. It is provided operator-local only, always, by design — the same reasoning the `docker` rule above states, generalized to every `needs.host` command, not just docker's.
- **Scaffolding the tracked `sandbox` block before confirming it on THIS machine (ADR-0049).** The carrier is measured — a tracked `sandbox` block reaches a dispatched worktree agent through the *dispatching session's own checkout* and merges with the operator's global settings — but only when the block sits on the Coordinator's own branch; a row-branch-only block grants nothing. Run the per-consumer probe first ([reference/setup-mechanics.md](reference/setup-mechanics.md#measuring-whether-the-tracked-sandbox-block-is-honored--the-probe-issue-716)) to confirm it holds here. A negative result changes the carrier (declared-but-unprovided, deferring `capability-gated`), never silently assumed away.
- **Treating a declared `writes`/`network` need as already provided once it is in `wave.config.json`.** Declaring a need and providing it are two different steps — the config records what a command must reach; only the tracked `sandbox` block, scaffolded and confirmed by the setup-time live-gate, actually provides it. Skipping the live-gate risks the exact live occurrence this design (ADR-0049) exists to close: a refusal discovered unattended, five hours into a wave, instead of at setup with a human present.
- **Excluding only `git push` for an SSH origin.** The Worker's anchor `git fetch` and wave-close's `git pull` are network operations too; on an SSH origin all three (`fetch`, `pull`, `push`) need the sandbox `excludedCommands` guidance, not just `push`.
- **Writing `wave.config.json` at Procedure step 4 without the `.gitignore` line beside it.** Both are one-time writes to the same consumer repo root, at the same point in the sequence — see [`.gitignore` scaffold — the Scribe scratch path](reference/setup-mechanics.md#gitignore-scaffold--the-scribe-scratch-path-issue-355). Skipping it leaves every Scribe hand-off payload an untracked file sitting in the tree for the whole in-flight duration of a consumer's first wave, in the same directory the consumer runs `git add .flotilla` over to commit the spine — the close-time sweep alone does not cover that window.
- **Skipping the worktree-brief questions.** A worktree checkout carries tracked files only — if the dependency dir or the store config is gitignored, the Coordinator needs that answer *before* composing the first wave's dispatch briefs, not after a Worker fails on a missing dependency or a Reviewer can't resolve a bare tracker id. Ask and record both at setup (FOR-32/W4-F4).
- **Accepting "nothing is gitignored — no install step needed" without checking, on a consumer whose `engine.cli` sits under an ignored path.** This is the one worktree-brief answer with a mechanical check available (`git check-ignore` against the binding's own path), and it is the answer that went wrong in the field: a consumer bound to `./node_modules/.bin/flotilla-engine` with `node_modules/` ignored had that false confirmation printed into both of its first wave's dispatch briefs. Run the check, then record `engine.install` — an unchecked answer here is not a shortcut, it is the defect.
- **Recording the install command only in the setup report, not as `engine.install`.** The report is for the operator; the config is what `compose-driver` reads. A command that lives only in prose has to be re-supplied by hand at every wave's compose — which is exactly how it came to be absent when it mattered (ADR-0032 amendment 2026-09-04).
- **Skipping the editor/indexing-excludes precondition.** `wave-start` dispatches worktree-isolated agents whose checkouts and build output an attached editor immediately indexes and file-watches; on the server pilot that load froze an editor's extension host and presented as a hung wave / unreachable Coordinator, not as the editor-configuration gap it actually was. Confirm before the first wave that the consumer's editor excludes the wave worktree location and heavy build/dependency directories (e.g. `.build`, `node_modules`) from indexing and file-watching, or that `wave-start` runs headless. Not engine-checkable; confirm it with the consumer at setup, same as the AFK permission allowlist.
- **Skipping the human-confirmed Linear preconditions.** The preflight covers the integration and the state catalog, but it cannot see whether PRs carry `Fixes <TEAM-NN>`, whether the repo is on the PR route, whether the team parks work in the configured `states.unclaimTarget` column, or the team's PR-automation where its advisory `gitAutomation` reading lists an `undecided` rule or abstains. Walk through those by hand before handing a `linear` config to `wave-plan`/`wave-create`.
- **Treating Linear as the code host.** A `linear` store config does not change where PRs live — they are still GitHub PRs. `team`/`project` identify the Linear issue tracker only.
- **Telling a consumer to `export` a credential in a global shell profile.** That doctrine is retired, not one option among several (ADR-0029) — it is exactly the wrong-scope failure the lookup-command indirection exists to close (one over-broad token shared across every repo the operator's shell touches). Scaffold the `<VAR>_CMD` lookup-command path for a long-lived interactive machine, or state plainly that this credential stays ambient by design for an ephemeral environment (CI). Neither settings file ever carries the raw secret either — `.claude/settings.local.json` is absent from every dispatched worktree, and the tracked `.claude/settings.json` is safe to commit only because it carries the lookup *command*, never the value.
- **Asserting whether the live-gate is an "ACL bind" instead of running it and observing.** ADR-0029's 2026-08-14 amendment found the scaffolded keychain item's first-read prompt behavior is **environment-conditional** — a live determination on one machine found no prompt at all on the first read, while `reference/setup-mechanics.md`'s own field evidence found a required Always-Allow click on another. Whether a given consumer's machine has anything to "bind" is a live-gate question, not a doctrine assertion in either direction. It is always a live-gate (proves the wiring end to end) and, for a session-authenticated resolver like `op`, doubles as the auth exercise.
- **Scaffolding a `<VAR>_CMD` resolver that requires per-invocation confirmation.** A prompt that fires once, at setup, is fine — that is exactly what the live-gate is for. A prompt that fires on every resolution is AFK-incompatible: `wave-start`/`wave-close` dispatch Workers and Reviewers unattended, and there is no one to answer a mid-wave prompt (it times out at 60s and fails that row loud, rather than hanging). If the only secret-manager CLI a consumer has requires per-invocation confirmation, tell them it does not fit `<VAR>_CMD` for a dispatched wave and use the ambient path for them instead.
- **Scaffolding the `<VAR>_CMD` env entry without its matching `permissions.deny` prefix, or vice versa.** All three artifacts — keychain item, `<VAR>_CMD` entry, deny entry — are one flow, not three optional pieces. The deny entry is the residual-vector anchor (ADR-0029): without it, an agent's Bash tool can run the configured lookup command directly and print the secret straight into tool output — exactly the vector the indirection exists to close.
- **Presenting direnv+keychain as a second recommended path instead of an explicit half-measure.** It solves only the *wrong-scope* half (per-project instead of global) — the secret still sits in `process.env` for that shell's life, so the whole echo/dump/read leak class (Convention 8) stays exactly as open as the shell-profile doctrine it narrows. Offer the lookup-command scaffold first; reach for direnv+keychain only as the interim for a consumer not yet ready to adopt it, and say so out loud.
- **Naming only one fine-grained GitHub permission on the checklist (e.g. `Pull requests` alone).** `repo` on a classic PAT bundles issues, PRs, and contents into one scope; a fine-grained PAT needs all three named separately (`Issues`, `Pull requests`, `Contents`, each Read and write) or the setup passes `store-preflight` and then fails the first issue-labeling call.
- **Leaving `gh auth status` and `GITHUB_TOKEN` presence conflated as "one auth problem."** They are two independent credentials (`gh`'s own keyring auth vs. the engine's env var) that fail independently; report and fix each on its own, or a mixed failure reads as one confusing state when it is really two.
- **Ending the report with a scattered summary instead of a single ordered checklist.** An operator should never have to reconstruct the sequence — credentials → token scope → preflight fixes → committing the scaffold → `gh` auth (if used) → by-hand confirmations → editor exclusion → next step — from prose spread across the report. Print it as one ordered list, last, with a place/shape/reason on every item.
- **Writing the consumer orientation block without an explicit yes, or re-asking after a decline.** A "seems fine" reading of an ambiguous answer, or a default-yes when nobody replied, is not consent — write nothing until the operator says yes clearly, and once declined, leave it declined; setup completes either way.
- **Skipping the marker check and duplicating (or rewriting) the consumer orientation block on a re-run.** The marker line is the whole idempotency mechanic — a re-run that reads the surrounding prose instead of grepping for the marker risks stomping edits the consumer made since the block was written, or offering it a second time.
## Operator register (Convention 16)
**Everything you print for the person at this session is operator-directed output, and it holds one register.** Plain language, direct address ("du"/"you"), self-explaining. Translate every internal reference — a decision-record number, a convention number, a finding id, a wave slug, a retro path — into the one-line consequence it carries for them, instead of naming it. Introduce a domain term with a half-sentence the first time it appears in a session, then use it freely. End the run with an operator block: what happened → where it lives → what you do next. Operator-directed text follows the operator's own language; the artifacts you write — issues, PRs, decision records, spine entries — stay English. **Installed form is strict** — no internal token reaches the operator. **Source form**, flotilla's own repo, may append one compact reference pointer after the plain text. Full clause text, the operator mini-glossary, and the mistakes it closes: [wave-shared/reference/convention-16-operator-register.md](../wave-shared/reference/convention-16-operator-register.md), read as a file beside this skill's own directory — no skill invocation, no namespace to guess.