agentsclimarketplace

Wave setup

Skill formtrieb/flotilla/.claude/skills/wave-setup

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".From its SKILL.md

Install
npx -y skills add formtrieb/flotilla --skill wave-setup

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

70.7 KB, as published. Nobody here has run it

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 — 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 three concerns. Interview them one round-trip each — don't ask field-by-field. A fourth 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 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? (claim-rung → workflow-state-name overrides; default {"queued": "Todo", "inFlight": "In Progress", "inReview": "In Review"}), 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. 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 and connected to the code 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 configured claim state (Todo/In Progress/In Review + Backlog/Canceled + doneState if set) — else the first setState mid-wave throws.

Confirm with the consumer — not engine-checkable:

  1. 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.
  2. 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.
  3. Team convention: humans park new/backlog work in Backlog; Todo means wave-claimed. 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.

Run the preflight (step 6) to prove 1–2 for real, and confirm 3–5 by hand — a config validate PASS only proves the JSON is well-formed, and the preflight cannot see the human conventions.

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. 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 below for the exact entries and the full command surface.

  • 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) the Coordinator embeds as that row's dependency-install step (workflow-driver.md's depsSetup) — without it, a fresh worktree has no dependencies and the verify gate cannot run at all, for the Worker or the Reviewer. If nothing is gitignored here, record that explicitly (no install step needed). For a non-Node consumer, this is also where the engine.cli non-Node manifest's install command gets recorded (Engine invocation above) — prepended ahead of whatever else this answer already names, so engine.cli resolves before the row's first engine call.

    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"). 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, not WaveConfig fields; the engine has no verb to check them.

  • 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 — 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. A tracked, un-sandboxed docker entry grants a host escape to every future agent of the repo, not just this wave's Workers. 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.

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 below — but land in the identical .claude/settings.json.

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, 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.

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.

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.

The consumer default: pin the published package as a devDependency, bind to the local binary it installs.

npm install --save-dev @formtrieb/flotilla-engine
{ "engine": { "cli": "./node_modules/.bin/flotilla-engine" } }

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).

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:

{ "engine": { "cli": "./tools/wave/node_modules/.bin/tsx tools/wave/src/cli.ts" } }

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 prepend that install command to the row's depsSetup (the same per-row dependency-install input the "Worktree-brief inputs" precondition below already asks you to record) — ahead of the consumer's own build install, so the worktree's very first step resolves engine.cli before anything else needs it. Exact manifest shape and commands: reference/setup-mechanics.md.

Once written, config validate reports the bound value verbatim (engine.cli: <value>) — read that line to confirm the repo is bound to the form you think it is, not merely that a binding exists. A present-but-malformed binding (a shell metacharacter, an empty string, a ~-rooted path) fails config validate loud rather than being read as unbound — fix the field it names and re-run.

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) 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 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:

VariableSecret or identifierApplies whenToken flavorWhere it livesVerify step
GITHUB_TOKENsecretcode host is githubclassic PAT (repo scope), or fine-grained (Issues + Pull requests + Contents, each Read and write) — GitHub token permissionsGITHUB_TOKEN_CMD lookup command in the tracked env blockhost-pr preflight — pr-merge-token
BITBUCKET_TOKENsecretcode host is bitbucketAtlassian account API token (Basic auth, paired with BITBUCKET_EMAIL) or a workspace/repository access token (Bearer, alone) — Bitbucket token belowBITBUCKET_TOKEN_CMD lookup command in the tracked env blockhost-pr preflight — pr-merge-token
BITBUCKET_EMAILplain identifier — no _CMD counterpart, no permissions.deny anchorcode 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 aboveplain value in the tracked env blockhost-pr preflight — create-credentials (advisory)
LINEAR_API_KEYsecretstore 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 GitHubLINEAR_API_KEY_CMD lookup command in the tracked env blockstore-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 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 — 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 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 below — one file, not a second one.

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:

{{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:

# .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; 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 both the system keychain and a proxying sandbox). 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:

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:

{
  "env": {
    "NODE_USE_ENV_PROXY": "1",
    "BITBUCKET_TOKEN_CMD": "security find-generic-password -a $USER -s flotilla-bitbucket-token -w",
    "BITBUCKET_EMAIL": "[email protected]"
  }
}

Scope and the create-credentials preflight check that grades this precondition: 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 (the team + repo pair, confirmed under 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 three concerns. One round-trip per concern (store kind → eligibility → verify). 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. 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, item 3) rather than composing the profile from the description alone. Don't ask field-by-field — one structured question per concern.

  2. Compose the JSON. From the answers, assemble the WaveConfig. The exact shapes are in reference/setup-mechanics.md.

  3. Author engine.cli. 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 the bound value into the JSON you are about to write — ./node_modules/.bin/flotilla-engine for the ordinary case, the vendored value only for this repo's own dogfood config. See Engine invocation above for the judgment and 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 one-line .flotilla/tmp/ entry to the consumer's .gitignore — see .gitignore scaffold — the Scribe scratch path 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). 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) — 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.

  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. 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 above for the judgment, reference/setup-mechanics.md 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. Write/extend the tracked .claude/settings.json per Scaffolding the tracked permission allowlist and env block above: 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 both scaffolded paths, .claude/settings.json and the Echo-Guard hook copy at .claude/hooks/echo-guard.cjs (the copy 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 echo-guard. Narrow the consumer's .gitignore — never widen this scaffold to route around it — then re-run both checks; cross-check with git add --dry-run .claude, which must list exactly the two scaffolded paths. Narrowing recipe: reference/setup-mechanics.md.

  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), and 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). 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:

  12. 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 above.

  13. Code-host token scope — on github, checked against the table in reference/setup-mechanics.md (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. 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.

  14. 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 failed check means wave-plan/wave-create would be handed a config that cannot actually run a wave to completion.

  15. 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.

  16. 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.

  17. 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 Backlog, not Todo; 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 checklist above) — state it as what to go do, not as the fact alone.

  18. 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.

  19. Next step — wave-plan (issue breakdown + wave roster) or wave-create (if a breakdown is already approved).

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.

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, 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).
  • 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 fails 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, 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).
  • 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. 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).
  • 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, or whether the team parks work in Backlog. Walk through those three 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.

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, read as a file beside this skill's own directory — no skill invocation, no namespace to guess.

Keep looking

Skills are one crate of 627,801. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.