# The playbook — `.dev-flow/project.md`

The operator-owned file that says how *your* project is tested, run, versioned and guarded. Written once per repo with `flow init`, edited by you.

`.dev-flow/` ignores itself — its own `.dev-flow/.gitignore` is `*` with the single exception
`!project.md`, so no state, plan or worktree path is ever tracked by accident. Tracking the playbook
in a repo that ignores the folder is a deliberate one-off:
`git add -f .dev-flow/project.md && DEV_FLOW_ALLOW_MAIN=1 git commit -m "track project.md"` — the
env prefix is for dev-flow's `pre-commit` hook, which allows a main commit whose only change is the
playbook.

It is **committed to git**; you edit it (the agent is blocked from writing the main-checkout copy
and may edit it only inside a docs/patch flow's worktree, or apply it through `flow init --write`
on your word, which validates). Content is organized per stage
(`## plan`, `## implement`, `## pre-release`, `## release`…) in **three tiers** — tier 1 splits into
executable checks and hard safety rules:

| Tier | Notation | Enforcement |
|---|---|---|
| 1 — executable | ` ```flow` (`key = value`) config blocks; ` ```flow-check <name>` command blocks | gates run every check, record pass/fail, worktree edits invalidate — hard |
| 1 — guard | ` ```flow-guard ` (`deny`/`confirm`/`protect`/`forbid`) rules in `## global` | enforced on every bash command and every edit/write path, at every stage — hard block or operator confirmation |
| 2 — checklist | `- [ ] item` lines (`- [ ] (ui) item` = only for `--ui` features) | gate confirms cover them as a set (hashed, invalidated on edits); `(ui)` items never appear for non-UI features |
| 3 — prose | any other text | injected to the agent when its stage begins — advisory |

Example `## implement` section:

````markdown
## implement
```flow
test = npm test
run = npm run dev
health = curl -fs localhost:3000/healthz
healthWait = 30
```
```flow-check overflow
node scripts/check-overflow.mjs
```
- [ ] visual pass at 390px and 1280px: no overflows, no stray scrollbars
Prefer feature flags for risky paths.
````

`healthWait` (default 30) is the seconds the post-deploy probe may keep retrying, every 2s — a service
whose readiness lags its container start needs a wait, not a poll loop hand-written into the playbook.
`healthWait = 0` restores the single shot.

Your project's own safety rules are a `flow-guard` block in `## global` — the reason a project
adopting dev-flow can delete its own guard extension:

````markdown
## global
```flow-guard
deny    \bgit\b[^\n]*\bpush\b[^\n]*--force(?!\w) :: history stays linear — the playbook sets the merge style
deny    \bgit\b[^\n]*\bworktree\s+prune\b :: it prunes other sessions' worktrees
confirm \bgit\b[^\n]*\bbranch\s+(?:-D\b|--delete\b) :: deletes a merged branch git cannot see as merged
confirm \bdocker\b[^\n]*\bcompose\b[^\n]*\brestart\b :: drops a permanent upstream connection
protect docker/.env*                 :: gitignored live secrets — and any backup copied beside them
forbid  docker/secrets               :: gitignored secret storage
```
````

| Verb | Applies to | What happens |
|---|---|---|
| `deny` | one segment of a bash command | hard block — no confirmation offered |
| `confirm` | one segment of a bash command | asks the operator; **blocked when no UI is present** — a hidden confirmation is not a confirmation |
| `protect` | an edit/write path | asks the operator, same no-UI rule |
| `forbid` | an edit/write path | hard block |

` :: ` separates the matcher from the reason, so reasons are free text and show up verbatim when a
rule fires. `deny`/`confirm` matchers are JS regex sources — an unparseable regex is a hard finding,
so `flow init --write` refuses the file (an unknown verb is a warning). Anchor a matcher on the **verb**
you mean, not on a flag that can appear anywhere after `git`: `\bgit\b[^\n]*--force` also refuses
`git filter-branch --force` (a local rewrite) and `git show -f`, and a rule that blocks unrelated
commands gets routed around. You do not need a rule for forced pushes at all — dev-flow confirms every
`git push --force` / `-f` with the operator itself, and prints the one prefix that answers for it
(`DEV_FLOW_ALLOW_REWRITE=1`). `protect`/`forbid` take a repo-relative path; a rule on a directory covers
its subtree, including files that do not exist yet. A rule whose **last segment** globs
(`protect docker/.env*`) covers the siblings in that directory whose name matches — the backup copied
beside a secret is the same secret, and the commit that adds it never names the file, so no `deny` regex
ever sees it. A glob anywhere else in the path warns at `flow init`: it names one literal file called
`.env*` and guards nothing.
The block belongs to `## global` — these rules are never stage-scoped, and one under a stage warns and
applies nothing.

Matching is per shell segment (the guard splits on `;`, `|`, `&&` and newlines, honouring `cd … &&` and
`git -C`), and a segment whose head only reads (`grep`, `cat`, `ls`, `git log`, `jq`, …) is skipped — so
`grep "git push --force" docs/` searches, it does not push. A read-only head that can still write
(`find -delete`, `find -exec …`, `sort -o FILE`, `awk 'BEGIN{system(…)}'`) is judged like a writer —
your `deny` rules do not have a back door through the read-only list.

`## pre-release` prose is where rules like “follow semver: minor for user-facing features, patch
otherwise; confirm the number before tagging” live — the agent sees them at stage entry, and
`flow release` / `flow release-done` hard-check the manifest side (bumped past the last tag, then
manifest↔tag agreement) and the changelog side (a heading for the version, an entry this branch
prepared, and every entry that was pending before the release folded into it — see
[gates.md](gates.md)). `## global` holds
`defaultBranch`, `branchPattern`, `worktreeDir`, `mainHook`, `mockupGlobs`, `bashTimeout = require`
(default: every shell call must state its own timeout) `| inject`, `release.review`,
`release.changelog` (default `CHANGELOG.md`; set it empty to opt out), `release.releases` and `release.artifact = <cmd>` (verify the published artifact — image registry probe etc. — before release-done completes); the implement stage takes `setup`/`test`/`build`/`run`/`seedData`/`health`/
`dev`. Of those, `setup` runs once inside a freshly created worktree at `approve` (where `cp .env` +
`npm install` belong — a failing setup fails the gate), and `run` is advisory: the agent starts the app
with it for the running-and-healthy walk, dev-flow never runs it itself. `dev` is the dev deploy run by
`flow verify` before the operator question — it must background and exit,
e.g. `docker compose up -d .`; the gate names what it ran (`dev ran (<cmd>)`, or
`dev: none configured`) so minutes of deploys never read as "nothing happened". After a deploy that
ran, verify re-runs the health probe (`commands.health` or a `flow-check` block named `health`, with
`seedData` first when set) in the worktree and refuses the gate when it fails — the pre-deploy
`flow check` PASS describes a container that no longer exists. What "healthy and serving this
build" means stays the project's own assertion: the probe runs with the worktree as cwd, so it can
wait for readiness and compare the served build stamp with `git rev-parse HEAD`. No probe → the
gate prints `health: none configured` and `flow doctor` flags the combination. The pre-release and release stages take
`versionFile`/`versionTag` (defaults `v{version}`; accepted under either section) and
`merge = squash | merge | rebase | any` — how a merged PR lands on the default branch (see below). `## global` also takes
`forge = github | gitlab | self` — the release machinery's target; when unset it is detected from the
`origin` URL at the moment of use (GitHub gets `gh`, GitLab gets `glab`, self-hosted stays forge-neutral:
no automatic release creation, merge proof by ancestry only), and `release.releases = false` skips
automatic release creation entirely (the legacy `github` key still parses, with a rename warning).
A leftover `project.json` is no longer read — the doctor tells you to fold it in and delete it.

### The merge policy (`merge = squash | merge | rebase | any`)

The one knob a repo can silently lack: a squash and a merge commit look identical right up until someone
needs to know which one the release contains. Three things read the key:

| policy | `main` gains | provable with git alone |
|---|---|---|
| `merge` | a merge commit plus the branch's own commits | **yes** — the branch is an ancestor of `origin/<default>` |
| `squash` | one new commit | no — ask the forge (`gh`/`glab`) |
| `rebase` | the branch's commits, replayed with new SHAs | no — same |

- **`flow init` asks** with evidence from your own history (`merge history: 6 merge commit(s), 3 squash(es)
  — the most recent PR landed as squash`), and **`flow start` refuses until it is set**: the agent asks you
  in chat, re-runs with `--merge=<style> --confirmed`, and the answer is inserted as a single line under
  `## release` (validated before the write, `.bak` kept, committed as a playbook chore when the playbook is
  the only dirty path). `any` is the explicit opt-out: no policy, no asking.
- **the guard enforces it where the decision is made** — `gh pr merge --squash` against `merge = merge` is
  blocked outright, and a merge that carries no style flag (so the forge default decides) needs your
  confirmation; a local `git merge --squash` only ever confirms, because merging main into a branch is
  ordinary work.
- **`release-done` proves what actually landed** matched the policy (branch ancestry plus what the forge
  reports), and `flow finish` says whether commits missing from the default branch are expected (squash,
  rebase) or a violation (merge — where `yolo` does not answer for you). A shape dev-flow cannot see —
  `forge = self`, no CLI, an unfetched branch — prints an advisory instead of refusing a legal release.

What it cannot do is bind a human clicking the merge button in the Web UI: only the forge's own
require-a-merge-strategy setting does that, and setting it stays your step in the forge, not dev-flow's.

`flow init` is the dialogue around this file: **fresh repo** → prints the setup brief (detection report
plus one question per undetected knob — forge, version convention, releases, artifacts, checks, prose),
the agent asks you each question, fills the draft and writes your answers back validated
(`--write <content>`; `--draft` prints the draft or current file whenever you'd rather read raw — an
interactive init may also confirm the draft with you on the spot and write it then); **existing file** → doctor mode (drift, dead commands/scripts,
unparseable manifest, dual configs, **ghost feature dirs** (a `.dev-flow/<slug>/` whose `state.json`
is gone — it offers to delete those, on your confirm, and says so instead when no UI is present) —
it never runs your commands) offering a refresh — which it
**refuses** whenever your file has lines a regenerated draft cannot reproduce (prose, your own
checklist items, hand-set keys): it prints the findings and the lines it would drop instead of
overwriting them. `--force` remains the deliberate overwrite — with your word (`--confirmed`, or one
confirm dialog) and a `project.md.bak` of what was there. At any time
you can just edit `.dev-flow/project.md` yourself — the next `flow init` (doctor) validates and
flags problems, and `flow doctor` runs the same probes on demand without offering to change
anything (findings are also printed whenever `flow init` runs without a UI, e.g. in CI). `--force`
rewrites from fresh detection. Improvement
ideas the agent notices along the way queue up in `.dev-flow/reflections.md` under `## playbook`
(what to change in *your* playbook) and `## plugin` (what to change in dev-flow itself) —
`flow reflect "<note>"` appends them (`--playbook` / `--plugin` says which section, no flag guesses
from the wording and prints why), `flow reflect list` numbers the open ones, `flow reflect move <n>
--plugin|--playbook` re-files one, `flow reflect tidy` drops the closed (`- [x]`) ones,
plain `flow doctor` reviews the open ones and routes
each fix to the flow that implements it (docs flow for playbook items — those edits only pass a
docs/patch flow's worktree), bare `flow reflect` hands the agent the full end-of-session
reflection protocol (review the session log, re-check open items against fixes that already
shipped, dedupe new findings, put the fix decision to the operator) — and nothing there ever gates.
Future features have their own file: `.dev-flow/roadmap.md`, a gitignored backlog `flow roadmap`
manages (add / pick → `flow start` / done / tidy) — a picked item is checked off by `finish` when
its feature ships, and it never gates either.

### Structured stage reports (a prose convention, not a gate)

The gates enforce *state*; how the agent *talks* about that state at each stage is a tier-3 convention —
type `prose.implement`-style guidance into the stage sections and every session inherits it. The contract
this repo runs on has two fixed shapes, both **plain markdown inline lines — never fenced** (a code block
renders monospace with a horizontal scrollbar in web UIs; fences stay for diffs and command output).

**The stage line.** Every stage report opens with one inline line: a `◆` marker, the stage and slug, then
the *decision record* as `·`-separated `key value` pairs with backticked values. The core — stage · slug ·
mode (+ grant mark: `✓` operator-granted, `?` proposed) · ui — repeats at every stage; stage-specific
fields append:

| Stage | Fields after the core | Example |
|---|---|---|
| pre-flow | mode proposal + why, detected knob defaults, merge-policy state | **◆ pre-flow · new work** — mode `normal` (proposed: touches behavior) · ui on · forge `github` ✓ · merge policy **missing** |
| plan | plan.md status, mockups `rendered/total`, open questions | **◆ plan · `dark-mode`** — `normal` ✓ · ui on · plan `created` · mockups `3/3` · open questions `0` |
| implement | commits `N ahead, M uncommitted`, tier-1, tier-2, deploy/health, branch@sha | **◆ implement · `dark-mode`** — `normal` ✓ · ui on · commits `2 ahead, 0 uncommitted` · tier-1 `5/5` · deploy `health PASS` · `feat/dark-mode@a1b2c3` |
| pre-release | **version decision** — `pending` / `bump <patch\|minor\|major> → vX.Y.Z (pending your word)` / `no-version [your word]` — tag, changelog, fold, review tally, merge style | **◆ pre-release · `dark-mode`** — `normal` ✓ · ui on · version **`bump minor → 0.56.0 (pending your word)`** · tag `v0.56.0` · changelog `ready` · review `0 must / 0 should` · merge `merge` |
| release | PR + state, merge style used, tag state, artifact proof, forge release state (one line per step — this stage has no checkpoint) | **◆ release · `dark-mode`** — PR `#131 merged` · merge `--merge (playbook)` · tag `v0.56.0 pushed` · artifact `npm view OK` · forge release `created` |
| finish | release recorded, main state, unmerged commits, reflections open, release-checklist open | **◆ finish · `dark-mode`** — release `recorded` · main `clean, pushed` · unmerged `0` · reflections `2 open` |

A decision that was *not* made is recorded as `pending` — never left unstated, never silently invented
(a version bump needs the operator's word; `no-version` too).

**The checkpoint.** Exactly one per stage, as a plain chat message (a structured-question tool is
not chat and never licenses `--confirmed`), three markdown bullets and the single verb, **yolo** named
as an available answer:

> **CHECKPOINT — Release?**
> - **context** — checks green; manifest ahead of last tag; `Unreleased` folded.
> - **options** — *release* → bump minor 0.56.0, tag `v0.56.0`, forge release · *no-version* → merge only, entry rides under `Unreleased` · *edit* — what to change · *yolo* → release with the parameters above, remaining gates run back to back
> - **recommend** — *release* — user-facing feature, minor per playbook semver
>
> Answer in chat: **Release?** (or yolo)

After each gate that advances a stage, one short echo line confirms the new state before new work
(`▶ stage: implement — 2 commits ahead, tier-1 5/5`). Mid-stage additions amend `plan.md` with an
`**Added mid-stage:**` line instead of opening a second checkpoint — the verb count stays one per
stage. The contract is advisory like all tier-3 prose: gates refuse what gates refuse regardless of
how neatly the report is formatted.
