# Gate reference

Every gate, what it advances, and what makes it refuse.

You normally type none of this: the agent runs the gates and you answer words in chat. `/flow <sub>` is
your **manual surface** — read state when the agent is confused, or advance a gate yourself when it is
stuck or claims a result it never ran. Same gates either way; an interactive run asks you in a dialog,
so it needs no `--confirmed`.

| Gate | Advances | Refuses unless |
|---|---|---|
| `start <slug> [--ui] [--patch|--docs --confirmed] [--merge=squash\|merge\|rebase\|any] [--check-upstream]` | → plan | slug is new (binds the session); relaxed modes are operator-granted in chat, anchored by `--confirmed`: `--patch` = chore release, no plan document, every gate unchanged; `--docs` = docs-only, no plan document and no tier-1 checks (worktree and release intact). Start prints the dev-flow build and stamps it into the feature state (`flow status` shows `flow=<ver>`), warns when **this session runs an older build than the checkout** (`--check-upstream` also asks npm for the published one), prints the playbook gaps `flow doctor` would report — and **refuses while the playbook has no `merge` policy**: it names the question (squash / merge / rebase / any), the style its own history suggests, and the re-run `--merge=<style> --confirmed`, which writes the answer into `## release` (single-line insert, validated, `.bak` kept, committed as a playbook chore only when the playbook is the only dirty path) and never asks again |
| `use <slug>` | — (binds this session to a feature) | slug unfinished — the binding is persisted to disk as a claim (`flow version` lists every dev-flow copy registered in the process, and a copy whose in-memory bindings never saw the bind attributes commands from the claim, so a two-sources setup does not misjudge your git) |
| `approve [--yolo]` | plan → implement (creates worktree + branch) | `plan/*.md` exists; if `--ui`, rendered mockup screenshots (per `mockupGlobs`, default `*.png`) exist under `plan/` — headless chromium renders embedded in plan.md; **one question** — or `--confirmed` after you answered in chat; **yolo** (your chat answer at **any** checkpoint question) → run that gate with `--yolo --confirmed` (`approve`/`verify`/`release`/`finish`): that question is answered and every later one (Verified?/Release?/Finish?) is pre-confirmed — the agent then runs the remaining gates back to back, honoring guidance attached to the yolo word (e.g. "yolo, create a patch version release") — checks and guardrails stay fully enforced. `commands.setup` runs in the worktree and is retried until it succeeds once (a worktree left behind by a failed setup is set up on the next `approve`, never silently reused); a `--docs` feature runs no project command at all and says `setup skipped (docs mode)` |
| `test` | records a check | — (runs the `test` command/check, records pass/fail) |
| `check` | records all tier-1 checks | — (runs every `flow-check`/test/health from project.md `## implement`); a `--docs` feature runs none of them and says `docs mode: project commands skipped` — a docs flow builds nothing, so a fresh checkout must not read as a failed deploy (#96) |
| `health` | records a check | — (runs `seedData` + the health command, retrying up to `healthWait` seconds; app must be up locally with real data) |
| `verify [--yolo]` | implement → pre-release | all tier-1 checks green — a check with no passing record is **run by the gate itself** (`checks ran at the gate (test)`) rather than refused and pointed at `flow check`, since entering the stage or editing the worktree is what cleared it — and so is a record whose worktree fingerprint differs from the tree now (`stale record(s) rerun after a worktree change: …`): a change made through bash fires no edit-tool hook, the fingerprint is what notices the tree moved — and a failure there prints the run output, **the feature branch holds at least one commit** (verify refuses a branch with nothing committed — the checks describe the working tree, so an uncommitted state has never been recorded — and prints `commits: N ahead of <default>, M uncommitted path(s)` with what it ran; uncommitted paths are reported, never gated), build (if configured) green, `dev` deploy (if configured) runs first so you can review the running app **and the health probe (if configured) re-runs against what it just deployed, retrying every 2s up to `healthWait` seconds** (a failing probe refuses the gate and reports how many attempts it took: the `flow check` PASS predates the deploy, and a service that needs seconds after its container starts is not a broken deploy; with no probe the output says `health: none configured`), tier-2 checklist (ui-only items excluded for non-UI features) confirmed as a set; a `--docs` feature runs none of it (`docs mode: project commands skipped`) and only asks you; **one question** — or `--confirmed`; `--yolo --confirmed` = you answered 'yolo' at Verified? — answers it and pre-confirms the rest |
| `release [--yolo|--no-version|--no-fold-check]` | pre-release → release | checks re-greened after the prep edits; with `versioning.file`: the manifest version parses and is **not tagged yet** (the bump must be ahead of the last release); with `release.changelog` the gate **proves the entry mechanically** (#97): a **heading names the version** (prose that mentions it, or a `1.2.0-beta` section when releasing `1.2.0`, is not an entry), **this branch touched the file** (an inherited changelog is not release prep), **every entry that was pending before the release survives inside the new section** — compared on normalized lines, so re-indenting a fold is free while rewording or deleting one is refused by naming the line — and **nothing is left under `Unreleased`**. The pending baseline is the newest tag's changelog (what actually shipped), else the branch's merge base; when neither resolves, the two git-dependent proofs are skipped **with a printed advisory** rather than refusing a legal release for a topology reason, and the content proofs (the heading, its body) always run; tier-2 `## pre-release` checklist confirmed; the manifest version it validates is **stamped as the release's version of record** (`release-done` publishes that, whatever the checkouts say later); **one question** (Release?) — or `--confirmed`; `--no-version` (your call at Release?) ships this flow as **merge + changelog** — the bump/tag requirements and the tag checks at `release-done` are skipped for it, but the branch must still add its **own line under `Unreleased`** (editing an older section releases nothing) and it rides there until the next versioned release folds it (default: versioned); `--no-fold-check` is the operator-granted escape from the line-level fold proofs — one flow, stamped in the gate output, the heading and touched proofs stay; **stale session build** (loaded build behind the checkout): the gate prints both builds, lists what shipped between them (those proofs will not run this release) and folds *update (/reload, then re-run) or proceed with the old release?* into the Release? question — the operator's word, recorded in the release output, **never a block**: publishing under an older gate is a legitimate choice (hotfix, offline reload) |
| `release-done` | records the release (the stage stays `release`) | **no question — the Release? answer covered the publication**; verify approval; the version published is the one `release` **stamped** from the manifest it validated (a main checkout still behind cannot republish the previous version), and it must not be older than the newest matching tag **on origin** (tag lists come from `git ls-remote`, so an unfetched clone does not decide what exists; when an origin exists but cannot be asked (offline, no ssh agent) **both release gates refuse** rather than answering with local tags); with `versioning.file`: that tag exists, is reachable from the default branch **and is on origin** (a local-only tag is not a release); with `release.artifact`: the published-artifact check runs (`{version}`/`{tag}` placeholders — the line it prints names the version it verified; skip once with `--no-artifact-check`, operator-instructed only) · creates the forge release from the tag, its body **the `release.changelog` entry for that version** — the changelog is the source of truth, and the tag's annotation stands in only when the changelog has no entry for the version, else nothing (and it says why — a body that is really a shell error is dropped, never shipped) — `gh` for GitHub, `glab` for GitLab) and then **proves the release exists**: it asks the forge (`gh release view <tag>` / `glab release view <tag>`) and refuses when nothing is there, because #51, #60 and #63 were three ways of ending `release recorded as complete` with nothing published — and when the changelog was the body it **reads the stored body back** and refuses a mismatch, so the release page and the changelog cannot drift (#97; normalized before comparing, skipped with a printed note on another forge). Skipping is deliberate and loud — `--no-release-create`, `release.releases = false` or `forge = self` each print that no forge release was created, and the view check is skipped for them; **with a `merge` policy** it also proves the merge that shipped matches it — branch ancestry plus what the forge says (`gh pr view` / `glab api`), so a squash under `merge = merge` (or a merge commit under `merge = squash`) refuses the release record, while a shape it cannot see prints an advisory instead (#97's split: an invisible topology never blocks a legal release); when the release went out on a stale build the operator acknowledged at the release gate, it prints that acknowledgement in place of the after-the-fact version-skew warning, and that warning prints the session vs disk version pair plus the duplicate-copy note when a second copy is registered (#117) |
| `finish [--yolo] [slug]` | release → done (removes worktree + branch, deletes `.dev-flow/<slug>/`, and deletes `origin/<branch>` **only** when its merged state is verified — ancestor of the default branch or a merged PR/MR via `gh`/`glab`; unverifiable branches stay with a note) | a release is recorded (`release-done` ran), the main checkout is clean **and** pushed to `origin/<default>`, and the worktree has no uncommitted changes; when the clean checkout trails `origin/<default>` the gate **fast-forwards local main** as it closes out (never a merge — anything that cannot fast-forward prints the operator's exact `git pull --ff-only` command); **one question** (Finish? — mentions unmerged-looking commits with the playbook's `merge` policy in mind, and warns when the forge release create failed at `release-done` and no retry succeeded) — or `--confirmed`; under `merge = merge`, commits that never reached the default branch are a **policy violation**: `yolo` does not answer it, only your explicit answer does; to abandon an unreleased feature use `abort` |
| `reflect ["<note>"|list|tidy|move <n> --plugin|--playbook]` | — | `reflect "<note>"` queues a suggestion in `reflections.md` (never gates; gate flags such as `--confirmed` are ignored, they never become part of the note). `--playbook` / `--plugin` files it in that section — the agent knows what its own note is about, so the wording guess is the fallback, and the echoed line prints the reason it landed where it did (`"gate" names dev-flow itself`, `playbook or project wording`, `nothing plugin-shaped in the note …`); a note is filed under **one** section, never both. `reflect list` numbers the open items with their section; `reflect move <n> --plugin|--playbook` re-files one of them. `reflect tidy` drops the closed (`- [x]`) items — the preamble and open items stay verbatim and a `## section` heading left empty goes with its items; every surface that queues or reviews reflections (`reflect "<note>"`, bare `reflect`, `doctor`) nudges with the closed count when the checked-off pile grows. Bare `reflect` prints the full **session-reflection protocol** for the agent (from the `/flow reflect` bar it additionally wakes the model, so the protocol executes immediately): review the whole session log, re-check every open item against the current playbook and **newer dev-flow versions** (`npm view pi-dev-flow version` vs the running version — dev-flow ships from npm; recommend `pi install npm:pi-dev-flow` + `/reload`, the operator performs updates), dedupe new findings against the open ones, then report playbook vs plugin items to the operator and ask whether to fix now — plugin items: search upstream issues first (`gh issue list --repo <upstream> --state all --search …`; an author-addressed match is reported back, no match → `gh issue create` with a detailed description and a suggested fix), playbook items: `flow doctor` routes each to the docs flow that implements it |
| `roadmap [add "<feature>" \| pick <n> <slug> \| done <n> \| tidy]` | — | manages `.dev-flow/roadmap.md`, the gitignored backlog of future features (`- [ ]` checklist): bare prints the numbered open items, `add` appends one, `pick <n> <slug>` stamps open item n with `(slug)` so `flow start <slug>` can take it — **`finish` checks the stamped item off when that feature ships**; `done <n>` checks off by hand (items shipped outside a flow), `tidy` drops the checked lines. Never gates; the open count is surfaced in `status` and session context |
| `doctor ["<issue note>"]` | — | read-only findings: playbook drift, dead commands/scripts, untracked playbook, ghost feature dirs, orphan worktrees/branches — **plus a review of every open reflection, each routed to the flow that should fix it** (playbook item → docs flow; plugin item → upstream issue, or a patch/normal flow inside the dev-flow repo) — every item is labelled from the `## section` it is filed under (the file is the record, so the table cannot disagree with it) and items sitting outside both sections are named rather than silently bucketed. It writes nothing and asks nothing, safe for agents and CI at any stage; the agent investigates, you confirm, the routed flow implements. `doctor "<note>"` prints an investigation brief and a fix route for a fresh observation, including the section it would be filed under and why |
| `status [slug]` / `abort [slug]` | — | — |
| `help` (`--help`, `-h`) | — | — (prints the gate help — answerable in any repo, at any stage, with no `.dev-flow/`) |
| `version [--check-upstream]` | — | — (prints dev-flow's **own** version and the directory it was loaded from — which copy this session runs, this checkout vs an installed `pi-dev-flow`; when the checkout has moved past the build this session loaded it says so and names both, because the gates execute the loaded code — `/reload` to run the newer one; `--check-upstream` adds the published npm version; answerable in any repo, at any stage, with no `.dev-flow/`) |

`--confirmed` is the operator-presence flag: pass it only after the operator literally answered the stage question in chat — one checkpoint per stage, no second dialog.
