Session Flow
A developer-grade workspace for people who are not developers. Your work saves itself, your history is kept and searchable, several threads of work run side by side without colliding — and nothing reaches the real version of your project until you say yes. You never have to learn git.
1. Where this came from — four ways of working with AI
You are three days into building something with Claude. It is going well. Then the conversation fills up, the model compresses what it remembers, and it starts contradicting decisions you made on day one. You open a fresh conversation — and the new one knows nothing. The reason you chose your approach is in the old one somewhere, and you cannot find it.
Or this. You were told to commit your work, so you did. Sometimes. You were told to push it, so you did. Sometimes. Nobody told you when to merge, or what merging even implies. Two pieces of work land in the wrong order and now there is a "conflict" — a word for a mess you did not create and cannot read.
Session Flow exists because of those two afternoons. Here is the ground it covers, in the order most people travel it.
A few definitions, in case you want to know what the machinery is doing.
- Git — the version-control system professional software teams use.
- Commit — save a snapshot of your work.
- Push — send those snapshots to the online backup copy.
- Merge — combine one line of work into another.
- Conflict — when git sees changes it cannot decide between.
You will meet these words again below. You will never have to type — or learn — any of them.
Just talk to the chatbot
You work in the conversation window and nowhere else. Nothing is version-controlled. Nothing is backed up beyond whatever the app happens to keep. And the model knows only what the current conversation holds — when that fills, it compacts (summarises its own history to make room) and detail goes.
Close the conversation and the reasoning goes with it. You keep the files. You lose the story of why they look like that.
Use git, like a developer
Git is excellent at what it is for: every version kept, several people on the same files without overwriting each other, any change reversible.
It is also complex, unforgiving, and built by and for people who use it daily. It assumes habits — commit early, pull before you push, know which branch you are on — that a business owner building their own tools does not have. Steve, in the conversation that started this project:
The cost of not having those habits is not a warning message. It is lost work.
Automate git in the background
The obvious fix: a background program that commits and pushes for you, so you never have to remember. Fine for one project, one person, one thing at a time.
It breaks the moment several threads of work run at once. An automation that saves everything it finds cannot tell finished work from work another live session is halfway through. An early attempt at exactly this swept up work belonging to running sessions and corrupted the project's official history.
Automation without ownership rules is not safety. It is a faster way to break something.
Session Flow
The full power of a professional developer's environment, and none of its ceremony.
You talk to Claude in plain English. Git happens underneath, correctly, without you learning it. Your work is saved and backed up after every exchange. Several threads run at once without colliding. Every conversation is kept, readable and searchable — so the reasoning survives, not just the files. And nothing reaches the official copy without your yes.
The standard being aimed at is Google Docs: you type, it is saved, it is safe, you can find it again, and you never once think about the mechanism.
2. The core problem with standard AI sessions
Four things go wrong without a system like this. Session Flow is built to solve exactly these.
- Closed conversations have no history. When a session ends, its reasoning ends with it. The files remain; the record of what was tried, rejected and decided does not.
- The history you do get expires. Claude Code — the app you run Claude inside — keeps its record of your conversations for about two weeks, then tidies it away. Anything you want in a month is already gone, and you find out when you go looking.
- Context gets squeezed into a few small files. Your context is everything the model can currently see. The usual workaround for a fading conversation is a summary file. But a summary is a compression, and "why did we do it that way" is the first thing compression throws out — which is exactly what you need six weeks later.
- You are forced to work one thing at a time. Git and Claude Code, used plainly, want one line of work in one folder. Two conversations on the same project collide. Parallel work becomes a bottleneck instead of an advantage.
3. The solution — a human-centric system
Three commitments, which explain nearly every choice below.
Everything is an ordinary text file. No servers, no databases, no dashboard to keep open. The project's state lives in plain text files inside the project folder, so you can read them and edit them yourself in any text editor — Notepad, VS Code, whatever you use. Search them, back them up, treat them like any other document. If Session Flow vanished tomorrow, the files would still make sense.
The commands speak English and tell you what to do next. You are never expected to keep track of where the project has got to, or to remember a step. One command to start work, one to stop. Everything between either happens automatically or arrives as a yes/no question with the consequence spelled out. No command assumes you already know what a branch, a worktree or a session is — each is explained the first time it comes up.
Threads of work run in parallel and talk to each other. Several conversations can work on one project at once without colliding, because each gets its own private copy. They hand work between each other, and each thread has an Inbox where you — or another thread — can leave a note for it to pick up next time.
4. What it is — the core concepts
Eleven terms carry the whole system. Everything else is in the glossary at the end.
| Term | What it means |
|---|---|
| Board | The workstream's high-level plan — what you are working on now and what comes after, split into phases so you can see where you are in the arc of the work. |
| Command | A plain-English instruction you type, starting with a slash — /workon, /wrap. Nine exist; you only need to learn three to run the whole system. |
| Draft copy | A private folder where one session's changes happen, leaving the official copy untouched until you approve the work. Git calls it a worktree. You never have to use either word. |
| Drop | The AI's inbox for passing ideas between workstreams — a note left in a mailbox outside every project, which /workon folds into the right Board's Inbox. |
| Handoff | The summary one session leaves for the next: where things stand, what is done, what is next, what is unresolved. The note you would leave a colleague taking over your desk. |
| Hook | A small background program Claude Code runs automatically at set moments. Hooks are what save your work without you asking. Six exist; you never run one yourself. |
| Inbox | The section at the bottom of the Board where new ideas wait to be triaged: sorted and prioritised against the existing plan. Issues, blockers, things to discuss, a thought you had in the shower. Type into it yourself whenever you like, or ask Claude to add something for you. |
| Official copy | The real, current version of the project. Git keeps it on the main branch — a branch being one line of work, a version of the project's history you can change without disturbing another. |
| Project | The overarching initiative — one folder on your computer, tracked by git. What you would normally call "the thing I am building". |
| Session | One working conversation with Claude, from opening it to closing it down. A workstream is made of many sessions, one after another. |
| Workstream | One strand of work inside a project: a feature, an area, a long-running effort. It has a short name like billing-rework. A project can run several at once. |
How a drop works
- A session working inside a draft copy sees only its own private snapshot of the project — including its own snapshot of every Board. Writing a note onto another workstream's Board from there would edit a stale version, and it would surface days later in the wrong shape.
- So the session files a drop instead: the note goes into a mailbox that sits outside every project.
- Next time you open the workstream it was addressed to with
/workon, Claude collects the drop, folds it into that Board's Inbox, and triages it with you. - The drop deletes itself once delivered. The Board's Inbox is the permanent record.
You never need to look at the queue. And when something cannot wait for the next session, one live session messages another directly with the SendMessage command.
5. How it works — a day in the life
The commands
Nine exist. The three in bold are the ones you actually use.
| Command | What it does | When to use it |
|---|---|---|
/flow-help | Loads this guide | When you need a reminder |
/land | Landing is Session Flow's word for bringing finished work into the official copy — with your approval, after the tests pass | Rarely; /workon offers it for you |
/quickfix <what> | A tiny fix, tested and brought home in one gesture | 30-second fixes that do not deserve a session |
/recall <scope> <question> | Asks a helper "what did we say or decide about X" — it reads the archive in its own memory and hands back just the answer | When you need something from an old session |
/repo-health | A snapshot of the project's state, built by asking git rather than from memory | Any time you want to know what is going on |
/workon <name> | Starts or resumes a workstream: offers to bring home any finished work, sets up your private working space, briefs you on where things stand | Every time you sit down to work |
/wrap | Closes out the session: updates the handoff and Board, saves everything | When you are stopping for now |
/wrap-end | Everything /wrap does, plus tidies the working space away | Parking the project for a while |
/wrap-lite | Quick save, plus a ready-to-paste opener for a fresh conversation | Conversation full, but you are mid-task |
The working day
- You start:
/workon billing-rework. Claude asks git what state the project is in — never its own memory. If an earlier session left finished work, it offers: "Land these first? (recommended — takes about a minute.)" Then it sets up your working folder, reads the handoff and Board, sorts anything in the Inbox with you, and briefs you. It waits for your go-ahead before touching anything. - You work. Nothing to remember. After every exchange your work is committed and pushed to GitHub — the online service holding your project's backup copy. If the computer dies mid-sentence, you lose the sentence.
- Claude watches how full the conversation is getting. At roughly three-quarters full you get a one-time nudge to move to a fresh conversation — early enough that the handoff gets written properly rather than in a panic. A second, more urgent nudge fires if the conversation is about to compact itself.
- You stop, one of three ways. Mid-task with the conversation full →
/wrap-lite, which hands you an opener to paste into a new conversation. Done for now →/wrap, which answers one question: is this finished? Complete and tests pass → ready ("this can be brought home"); otherwise in progress with a one-line status note saying what is left. Parking the project →/wrap-end: all of that, plus the working folder is tidied away, the work still safe on GitHub and still marked ready. - Later — maybe days later — the work comes home. The next
/workonfinds the ready work and offers to land it. Claude shows you a one-line summary of each piece, asks which to bring in, checks each still fits the latest official copy, runs the tests, folds the session into one tidy history entry, updates the handoff and ledger, and clears the draft away. A failed test sets that piece aside with a status note — everything else still lands, and nothing is ever half-landed.
Every wrap ends with two short blocks so you never hunt back through the conversation: what it just did, then a numbered Your next steps.
Tiny fixes skip all of it. /quickfix fix the typo in the welcome email makes the change in a throwaway draft, tests it, shows you a one-line preview, brings it home and cleans up.
What runs by itself — the six hooks
You never invoke these. They are what makes the system work in practice, and all fail safe: an error lets your work continue and says so, rather than blocking you.
| Hook | When it fires | What it does for you |
|---|---|---|
| flow-archive | After every exchange | The archivist. Copies the conversation to a permanent archive folder, keeping a readable version of every conversation forever. |
| flow-checkpoint | After every exchange | The auto-saver. Commits your work and pushes it to GitHub. This is why you cannot lose anything. |
| flow-context-nudge | Conversation ~three-quarters full | The one-time suggestion to move to a fresh conversation. |
| flow-main-guard | Before any of Claude's edits or git commands | The guard. Stops Claude editing the official copy directly while parallel sessions run, and stops a wrapped conversation making further edits. Claude's tools only — your own edits are always free. |
| flow-precompact | Just before a conversation compacts itself | The same suggestion, with urgency — so the handoff is written before detail is lost. |
| flow-report | When a session starts | One line of orientation — e.g. "2 pieces of work READY to land". Read-only. |
Starting a project
A project is a normal folder on your computer, wherever you keep your work — not inside any special Claude folder. The one requirement is that it is tracked by git. If Claude Code already works on the project, it almost certainly is. For a brand-new folder, tell Claude "set up git in this folder and connect it to GitHub" — a one-minute job.
After that there is nothing to configure. /workon <name> creates the working space; the first /wrap writes the handoff and settings file. When Claude writes tests — small automatic checks that the thing still does what it should — /wrap records how to run them, and from then on they run before any work reaches the official copy.
The Board comes later, deliberately: phases only mean something once a plan exists, so the Board is created the moment a plan is approved, with phases matching that plan's milestones. A workstream that is still an open question does not get one yet. Ask for one any time with /dev-project setup.
Bringing an existing project in
Nothing converts until you touch it. The first time /workon or /wrap meets a project whose handoff predates Session Flow, it offers a one-time migration: one yes archives the old handoff, rewrites it to the current format, creates the settings file, and converts any external board into a local Board file — showing you the mapping for approval first. It copies everything as it stands, flagging what looks out of date rather than dropping it silently. Bringing the content up to date happens with you, in the next live session.
6. What you get
- A developer-grade environment without the learning curve. Version control, parallel work, automatic test checks, reversible history — all running properly, none of it requiring you to learn git.
- Work that cannot be lost. Every exchange is saved and backed up. Forgetting to save is not a failure mode here; forgetting to wrap costs you a summary, not work.
- A permanent, searchable history. Every conversation kept in readable form, forever, outside the project.
/recallanswers questions from it — "what did we decide about the pricing tiers?" — without dragging the old conversation into your current one. - Nothing changes without your yes. Claude never merges work on its own. Always a preview, a test run, an approval.
- Parallel threads that coordinate. Several strands at once, each isolated, each able to hand notes to the others.
- A guide, not just a tool. Every command says what it just did and what to do next, in plain English.
Your next step: open a project and run /workon <name>. For a new workstream, use a short lowercase name like billing-rework. Everything else is offered to you as you go.
Appendix A — where things live
your-project/
├── HANDOFF.md ← the handoff (or the index, if several workstreams)
├── HANDOFF-<name>.md ← one handoff per workstream
├── BOARD-<name>.md ← the Board, with the Inbox at the bottom
├── docs/
│ ├── ledger.md ← what changed, why, and what was reversed
│ └── <name>-runbook.md ← how to operate the thing
├── _handoff-archive/ ← retired workstreams' handoffs
└── .claude/
├── flow.yaml ← project settings (test command, parallel mode)
├── branch-state/ ← status cards for work in flight
├── session-reports/ ← per-session summaries when several drafts run at once
└── worktrees/ ← the private working folders themselves
All of it is saved into the project's history except the private working folders.
This tree shows the project after work has been brought home. A workstream still in flight keeps its handoff, Board and work inside its own draft copy under .claude/worktrees/<name>/ until it lands — which is what stops parallel sessions making conflicting edits to your live documents.
The ledger is a dated list of everything ever brought home: what changed, why, and anything reversed. A runbook appears only when an older project is migrated in and its long reference sections move out of the handoff. The fully annotated tree is on the Technical tab.
The conversation archive
One place lives outside the project: the archive — every conversation, past and present, as a readable text file you can open and copy from, beside the raw original. This is where history lives after Claude Code's own list forgets it, and it is what /recall searches. It stays on your computer: never backed up to GitHub, never shared.
C:\ClaudeArchive\ *
└── <project>\ ← one folder per project
└── <workstream>\ ← one folder per workstream
├── billing-rework-v3--a1b2c3d4.md ← readable transcript — kept forever
└── billing-rework-v3--a1b2c3d4.jsonl ← full raw history — kept 180 days
* Example location. The archive folder is a setting (FLOW_ARCHIVE_DIR) — ask Claude to set or change it. The readable version is kept forever because it takes up so little space; the raw version defaults to 180 days (FLOW_ARCHIVE_RAW_DAYS).
Appendix B — common questions
- Do I have to remember to land?
- No.
/workonchecks for finished work every time you start, and offers. Your job is saying yes. Run/landyourself only when you want work in the official copy sooner than your next session — just before putting something live, say. - Can I lose work if I forget to wrap?
- No. The work is already backed up by the auto-saver. Not wrapping means the handoff was not written for that session — nothing more. The next wrap writes it from the saved history. The only cost is that the next session rebuilds context from the Board and the saved work rather than an up-to-date handoff.
- Why is there no handoff file for my new workstream?
- Two reasons, both normal. The handoff is created by the workstream's first
/wrapor/wrap-lite, not by/workon— so a conversation that ended without wrapping leaves none. And until the work lands, the handoff lives inside the draft copy, appearing alongside the others only when the work comes home. To read it meanwhile, open it from the working folder or ask any live session. - What does "ready" actually mean?
- The verdict
/wraprecords: the task is complete and the tests pass. Ready work is what/workonand/landoffer to bring home. You can overrule it either way. - What if the tests fail when landing?
- That piece is set aside untouched, with a status note naming the failed test. Everything else still lands. Nothing is ever half-landed.
- What is the difference between the three wraps?
- Same task continuing right now in a fresh conversation →
/wrap-lite. Stopping for now, even a day →/wrap. Parking the project and tidying up →/wrap-end. When something has shipped or a phase has finished, use/wrapso the Board reflects it immediately. - Why did a command refuse to edit something?
- It only ever refuses Claude's edits, never yours. Either the official copy is locked because parallel sessions are running — use
/workon,/quickfixor/landfor real changes — or you are in a conversation that has already been wrapped, so continue in the new one. The message says which. The handoff and Board are the exception: always writable, even on a locked official copy, because they are coordination notes rather than the project's content. - Can a session working on a different workstream write to my Inbox?
- It depends where that session is working.
The middle row is the trap: without the drop queue it would not look like a failure. The write would succeed — just into the wrong copy. Session Flow warns instead, and hands back the exact drop command to use.
The session doing the writing Can it reach another workstream's Board? How it hands a note over In the main copy of a project Yes — even a locked project, even a different project Writes the Board directly In its own draft copy, still live No Files a drop; /workonfolds it inIn a draft copy and already wrapped No Files a drop; /workonfolds it inInbox notes are things not to be forgotten, not things that cannot wait, so delivery at that workstream's next session is the right speed. For anything genuinely urgent, one live session messages another with
SendMessage. And nothing goes missing meanwhile:/repo-healthreports notes still waiting for a workstream nobody has opened. - What happens to files in a draft copy that git was not tracking — a stray
.envfile, say? - (A
.envfile holds settings, often passwords and access keys; projects deliberately keep it out of version control so it never reaches GitHub.) Before Session Flow cleans up a finished draft copy, it copies out anything left behind that git was not tracking. But do not lean on that: a draft copy should never be the only copy of anything. Passwords, keys and tokens belong in your password manager and in environment variables — settings stored in Windows itself rather than in a file. Anything else irreplaceable belongs outside the working folder too. - Is there protection if Claude's own checks are bypassed?
- Yes. GitHub can be set to refuse two things on the official copy: permanently deleting its history, and rewriting it. That runs on GitHub's servers, so it holds even if something local goes wrong. One-time setup per project; needs a paid GitHub plan if the project is private.
- Why did VS Code warn "Failed to save" when I edited a Board or handoff?
- You had the file open with unsaved edits while Session Flow updated it in the background — usually a landing bringing home changes made in a draft copy. Nothing is lost; your typing is still in the tab. Choose Compare, copy your additions across, save. Avoid Overwrite, which discards the background update. Writing Inbox lines and saving straight away keeps the window for this tiny.
- Does anything remind me to come back?
- No, by design. State lives in files that answer when asked. If you want a reminder, put one wherever you keep your own tasks.
- Can a draft copy get stuck, unable to catch up with the official copy?
- No. If a draft needs the latest official copy folded in mid-work — rare — Claude does it as a plain merge, never by rewriting the draft's history, so nothing can strand it. The one moment history is genuinely rewritten is landing, and that step recovers on its own even if interrupted.
- Why does an old conversation show an error, "read-only", or an "Open Worktree" button?
- All three mean the same harmless thing: that conversation ran in a draft copy that has since been cleaned up after its work came home. Old conversations are read-only archives — scroll and read freely, but messaging one makes VS Code try to restart Claude inside a folder that no longer exists, failing with a misleading "native binary failed to launch" error. Nothing is broken; no work is lost. To ask about an old session, use
/recallin any live conversation —/recall om-pipeline-v34 what did we decide about the OM tile?— or name a whole workstream instead. Deleting an old conversation you do not need is fine.
Appendix C — glossary
Every term this guide defines, in one place.
| Term | Plain meaning |
|---|---|
| auto-saver | The automatic save-and-backup after every exchange (the flow-checkpoint hook) |
| Board | The workstream's high-level plan: its phases, what is waiting, what is only an idea, and the Inbox |
| branch | Git's name for a separate line of work — a version of the project's history you can change without disturbing another |
| Claude Code | The app you run Claude inside |
| command | A plain-English instruction starting with a slash — /workon, /wrap, /land |
| commit | Save a snapshot of your work |
| compact | What the model does when a conversation fills up — summarises its own history to make room, losing detail |
| conflict | When git sees changes it cannot decide between |
| context | Everything the model can currently see: this conversation, plus whatever files it has been shown |
| draft copy | A private folder where one session's changes happen, leaving the official copy untouched |
| drop | The AI's inbox for passing ideas between workstreams — a note left in a mailbox outside every project; /workon folds it into the right Board's Inbox |
.env file | A settings file that often holds passwords and keys, deliberately kept out of version control |
| environment variable | A setting stored in Windows itself rather than in a file — where passwords and keys belong |
| exchange / turn | One message from you plus Claude's reply. The auto-saver runs at the end of every one |
| git | The version-control system under everything — it keeps the project's history as saved snapshots |
| GitHub | The online service holding your project's backup copy, where every save is sent |
| handoff | The summary one session leaves for the next — created and updated only by the wrap commands |
| hook | A small background program Claude Code runs automatically at set moments |
| Inbox | The section at the bottom of a Board where new ideas wait to be triaged at the next /workon |
| ledger | The dated list of everything ever brought into the official copy — what changed, why, what was reversed |
| locked | A project running parallel sessions, where Claude may not edit the official copy directly. It never applies to your own edits |
| merge / land | Combine one line of work into another — here, bringing draft work into the official copy |
| model | Claude itself — the AI doing the work. Distinct from Claude Code, which is the app it runs inside |
| official copy | The real, current version of the project — git's main or default branch |
| phase | One stage of a Board's plan, ending at a milestone |
| project | The overarching initiative — one git-tracked folder on your computer |
| push | Send those snapshots to the online backup copy on GitHub |
| ready | The verdict that work is complete and its tests pass, so it can be brought home |
| runbook | How to operate the thing — commands, addresses, standing rules |
SendMessage | The command one live session uses to speak to another live session directly |
| session | One working conversation with Claude, from opening it to closing it |
| squash | Combining a session's many small snapshots into one tidy history entry |
| status note | The one-line "what is left", or "why it did not land", attached to work in flight |
| tests | Small automatic checks that confirm the project still does what it should |
| triage | Sorting and prioritising new items against the plan that already exists |
| version control | Keeping every past version of a file, so any change can be undone and any old state recovered |
| workstream | One strand of work inside a project, with a short name like billing-rework |
| worktree | Git's name for the private folder a draft copy is edited in |
Session Flow ships as nine slash commands, six hooks and three templates. For exact procedures, file contracts and known limitations, open the Technical tab. Load either version any time with /flow-help (or /flow-help technical).
Session Flow — Technical Reference
The companion to the Plain English guide. This tab names the exact mechanisms: hooks, files, git operations, contracts. Source of truth is the command files, hooks and templates — a coverage test in tests/flow/run-tests.mjs fails if a flow command or registered hook is missing from either document.
Bootstrap requirements
A Session Flow project is any git repository: initialised (git init), with a commit on its default branch and an origin remote for the continuous checkpoints to push to (commands resolve the real default branch via git symbolic-ref refs/remotes/origin/HEAD, falling back to main/master — never assumed). Without a remote, checkpoint commits still land locally and the push reports itself fail-soft, but the "work is always on GitHub" guarantee obviously doesn't hold — add a remote before relying on it. No other setup: the first /workon creates the worktree and branch-state; the first /wrap creates the handoff and .claude/flow.yaml from the templates. Recommended one-time hygiene: add .claude/worktrees/ to .gitignore (/land warns if it's missing).
Architecture in one paragraph
Session Flow is a set of Claude Code slash commands (deliberate procedures), hooks (automatic behaviour), and file contracts (state). Work happens on worktree-<slug>-vN branches in worktrees under .claude/worktrees/; per-branch state travels in .claude/branch-state/*.yaml on the branch itself; the default branch changes only via /land's squash-merge (or a documented external writer). Everything is deterministic reads of git plus markdown/YAML — no daemon, no database.
The hooks
Six registered, all fail-soft (an internal error exits 0 and reports to stderr — never blocks work).
| Hook file | Event | Behaviour |
|---|---|---|
flow-report.js | SessionStart | Read-only orientation report. Counts worktree branches by state: "N READY to land", "N in progress", locked-worktree warnings; suggests parallel mode when multiple worktrees exist with no flow.yaml. Never writes (it replaced a retired auto-sync that did). |
flow-checkpoint.js | Stop | The auto-saver. In a worktree: git add -A → commit checkpoint: <ISO> (auto) → push. Hooks aren't tool calls, so the guard never blocks a checkpoint. |
flow-main-guard.js | PreToolUse | Two denials. (a) Main lock: in a repo whose flow.yaml has parallel_mode: true, when the session's repo is the main checkout on the default branch, denies Edit/Write/NotebookEdit and any Bash containing a history-changing git verb (commit merge rebase cherry-pick revert am reset apply restore stash clean checkout switch; read-only sub-verbs like stash list/stash show are allowed) — unless this session's own unlock flag (.claude/flow-unlock/<session_id>.json, or the deprecated legacy .claude/flow-unlock.json) names it and hasn't expired. Carve-out: Edit/Write targeting any *.json directly inside .claude/flow-unlock/ — or the legacy path — is always allowed; it is the designed admission mechanism, and a flag only admits the session named by both its filename and its contents, so allowing any session to write any flag file admits nobody extra. As of 2026-09-01 only /land (and /quickfix, which invokes the landing procedure) ever write a flag, so this carve-out now exists purely to serve the landing step. Since 2026-08-31 the carve-out emits an explicit allow (permissionDecision: "allow" on stdout) and runs before the unlock-validity check so a mid-land expiry refresh gets the same treatment. Layering caveat (proven same day): the hook-level allow settles only the guard's layer — the auto-mode permission classifier is an independent, nondeterministic layer that can still deny the flag Write. The deterministic fix for that layer is user-settings permission rules for the exact path (note Write(*)/Edit(*) match only a single relative path segment, so it does not cover this). Coordination carve-out (widened in the final review): the same explicit-allow treatment is designed to apply, unconditionally, to Edit/Write on exactly six path shapes regardless of which session or repo is asking: BOARD-*.md, HANDOFF-*.md, HANDOFF.md (repo root), .claude/flow.yaml, docs/*-runbook.md (directly in docs/) and _handoff-archive/*.md (directly in that directory) — the first four give the board's Inbox a real write path; the last two cover what a main-checkout /wrap writes into carved-out paths: MIGRATE MODE's handoff snapshot and its runbook. (docs/decisions.md was a seventh shape until 2026-09-02, when the record-layer consolidation retired that file; the exemption was removed with it.) Commits are not carved out: the guard still denies git verbs on a locked main, so these files sit dirty on disk until /land's Step-0 pre-check auto-commits exactly that list. CLAUDE.md, memory files and other context files are deliberately not carved out — closer to source than coordination, so writes to them stay gated. Worth repeating in plain terms: this guard has never applied to Steve. It is a hook on Claude's own tool calls; Steve's own edits — in VS Code or any other editor, to any file, coordination or otherwise — were always free. (b) Session seal: in a worktree whose branch-state has sealed: true and session_id = this session, the same edit-class actions are denied — the wrapped conversation may not write again. Everything else in a live worktree is unrestricted. |
flow-archive.js | Stop | Transcript archive. Why it exists: the harness prunes ~/.claude/projects after ~2 weeks while workstreams span months — without the archive, older conversation history is simply gone. Per-turn incremental capture to FLOW_ARCHIVE_DIR (this machine: D:\ClaudeArchive; code default ~/claude-archive; both env vars set per-machine in settings.json's env block): delta-append guarded by a 256-byte tail signature (rewrite ⇒ whole re-copy), never-shrink rule against lesser duplicate copies, per-session index files under _index/ (no shared-write contention), title-rename tracking; worktree-run sessions group under <repo>/<workstream>/ named by the worktree (dictation-typo-proof), title-named sessions group by their -vN slug. Throttled scan pass (30 min) renders one idle conversation to .md (FLOW_ARCHIVE_USER_LABEL names the human in the rendered speaker labels, default "User" — cosmetic only; tool_result bodies excluded) and prunes raw older than FLOW_ARCHIVE_RAW_DAYS (default 180; 0 = keep only the rendered md; the prune always renders before deleting) daily — md kept forever. CLI: --backfill (one-off, largest copy per session id wins), --render (render all stale, ignore idle rule). ~0.6ms steady state. |
flow-precompact.js | PreCompact | Nudges a /wrap-lite before compaction, so state reaches the handoff before context is lost. |
flow-context-nudge.js | UserPromptSubmit | One-time "this conversation is about N% full" nudge. Gauges real token usage from the transcript's most recent assistant usage record, read from the file's last 512KB. Window size resolves: FLOW_CONTEXT_LIMIT env → a payload figure if supplied → learned from this transcript (past 200K ⇒ wide) → learned from the 5 most recent sibling transcripts → 200K default. Threshold FLOW_CONTEXT_NUDGE_PCT, default 75. ~0.2s per prompt. |
On disk but deliberately unregistered: flow-worktree-arm.js — WorktreeCreate turned out to be a provider hook (it must create the worktree), not an observer, so auto-arming is owned by /workon. Kept test-pinned in case an observer event appears.
Guard resolution quirks (known, logged): the guard resolves the repo from the edited file's path (Edit/Write) or the session's cwd (Bash). So git -C <locked-repo> … from outside the repo never triggers the lock, while git -C <other-repo> commit from inside a locked repo is denied. Move your cwd in its own Bash call first — an in-command cd doesn't help; the hook fires before the command runs.
The unlock flag. One file per session: .claude/flow-unlock/<session_id>.json = {"session_id","expires"}. Flags are /land-only now. Only the landing step — shared by /land and /quickfix, which invokes it for its own branch — ever writes a flag, because only landing needs to run mutating git verbs (merge/commit/push) on a locked main. /wrap, /wrap-lite, /wrap-end and /workon never write one: their only main-checkout writes were coordination files (BOARD-*.md/HANDOFF-*.md/HANDOFF.md/.claude/flow.yaml/docs/*-runbook.md/_handoff-archive/*.md), which the coordination carve-out below exempts from the lock entirely — a file write needs no commit, so leaving them dirty in main is legal and expected, and /land's Step-0 pre-check commits them. /workon's old arm/disarm dance (write flag → commit + push → delete flag, just to flip parallel_mode) is deleted along with the general-purpose main-write key a wrap used to hand a main-checkout session that turned into real work — that session is now told to move to /quickfix or a worktree instead. A session is admitted only when the file named for it also names it inside and has not expired; each session deletes only its own file, on every exit path (success, abort, error); never committed (the whole directory is excluded by pathspec in /land's staging and by .gitignore). Why per-session: the previous single .claude/flow-unlock.json was one shared path — last writer won, and every exit deleted whatever flag was present, so any session could silently revoke another's admission mid-procedure. The legacy single-file admission is deleted: the guard still allows writing the old .claude/flow-unlock.json path (harmless — a write there no longer grants anything), but the check that once admitted a session on its say-so is gone, closing the cross-session-flag-minting mechanism that path enabled. Housekeeping: flags left by crashed sessions are pruned by the guard on its deny path (parsed-and-expired outright; unparseable only after a 10-minute grace, so a flag caught mid-write by another session is never destroyed). A session id is used verbatim as a filename, so it must match ^[A-Za-z0-9._-]+$ — anything else is refused rather than resolved.
Commands — exact semantics
| Command | Procedure summary |
|---|---|
/workon <slug> | Fetch + prune → enumerate candidates at branch level (union of git worktree list --porcelain and worktree-* refs, local + origin, de-duplicated) → read each branch-state (worktree path, else git show <branch>:…, else git show origin/<branch>:… — the origin form is the only one that works for origin-only branches) → land-first offer for status: ready → Step 2b: drain the drop queue into this workstream's board Inbox and report what other workstreams still have waiting — in the main checkout, before EnterWorktree, since a drain after entry would fold notes into the branch's stale copy → transcript housekeeping (flow-transcript-sweep.mjs: rescues ended worktree sessions' transcripts into the main project dir; --prepare pre-creates the next worktree's project dir as a junction so transcripts land there from the first byte) → resolve the slug's in-progress worktree or the branch to create for <slug>-vN → auto-arm parallel_mode on the 2nd concurrent worktree — writes the main checkout's flow.yaml and leaves it dirty (a coordination file, so no unlock flag is needed either) before EnterWorktree switches into the new worktree; the ordering is load-bearing, since arming after entry would target the wrong checkout's copy of the file — /land's Step-0 pre-check commits it, replacing the old write-flag-then-commit-push-then-delete-flag dance → resume or create the worktree with a full branch-state file → briefing → wait. Never creates a board — boards are born from an approved plan, not from /workon. --help, --enable-parallel, --disable-parallel (set the flag the same dirty-write way). |
/wrap | Orient → MIGRATE MODE if the handoff lacks **Flow:** v2 → brand-check drafts → update handoff to the v2 contract (or a session report if sibling branches of the same workstream are open) + index + decisions recorded in the handoff's own section + surgical board update → commit + push LAST → landability verdict (run test_command; complete + green → ready, else in-progress + note) → seal (sealed: true written, deliberately left uncommitted — committing it would be denied by the very guard it arms; the Stop-hook checkpoint commits it seconds later, which is safe: /land deletes the file and a new session gets a new session_id). Writes no unlock flag — in a main checkout the coordination files it touched (board/handoff) are left dirty for /land's pre-check to commit. |
/wrap-lite | Same handoff quality, same commit+push+seal; skips the decisions lift and the board; prints the /workon opener + next conversation name. Never removes worktrees; never creates a branch-state file for the default branch. Writes no unlock flag, same as /wrap. |
/wrap-end | Everything /wrap does, then asks parked or archived? (archived → handoff moves to _handoff-archive/<slug>-<date>.md, index row dropped), quarantines the worktree's gitignored files before removing it (branch stays, pushed; no seal — nothing left to seal), prints the opener. No unconditional --force on the removal any more, and no unlock flag. |
/land | Main checkout + default branch only. Write your unlock flag (Step 0 — still the one command that does) → dirty-coordination-files pre-check (auto-commits BOARD-*.md/HANDOFF-*.md/HANDOFF.md/.claude/flow.yaml/_handoff-archive/*.md/docs/*-runbook.md if any are dirty — widened set, identical to the guard's carve-out) → fetch + pull --ff-only → branch-level enumeration → preview + ask (all / pick / abort) → per branch: record the pre-rebase ref, then rebase (in its worktree; origin-only branches: merge-base --is-ancestor with strict exit-code reading, temp worktree OUTSIDE the repo if needed, local ref materialised first) — if the land aborts after this point, the branch is restored to the pre-rebase ref or force-with-lease pushed before stopping, never left rewritten and unpushed → git merge --squash → delete branch-state + session-report from disk (guarded git rm -f) → commit → test gate (scratch log outside the repo; red → reset --hard HEAD~1, branch survives with note, same abort-safe rebase rule applies) → fold docs (handoff merge, ledger entry with greppable identity, no hash, index regeneration) → amend with bounded blanket add (excludes .claude/worktrees and the unlock flag) → verify presence AND absence, remediating via git restore --staged --source=HEAD~1 → cleanup: quarantine each branch's worktree before removing it, then worktrees-then-branch (order is load-bearing) + delete remote branch → one push → milestone check → delete unlock flag → plain-English report. |
/quickfix <desc> | Scope check (≤ ~3 files, else redirect) → transcript junction (--prepare, before entering) → temp worktree quickfix-<timestamp> → change → test_command → land THIS branch via the /land procedure (invocation = approval; preview still shown; this is the other command that writes a flag) → cleanup: quarantine, then worktree-first-branch-second. |
/repo-health | Read-only: fetch, status, worktree list (+ lock-PID liveness), branch-level state reads, branch -vv, recent log, flow.yaml, stash list, pending Inbox drops (flow-drop.mjs list --repo) → table + plain-English next actions. Never answers from recall. |
/flow-help | Loads the plain guide; technical argument → the Technical Reference. |
/recall <scope> <question> | Dispatches ONE session-historian agent to answer "what did we say/decide about X" from a named past conversation, a workstream slug, or the whole repo history. Searches distilled layers first (handoff/board/ledger, plus a retired decisions archive where one exists), then the archive's readable .md files, then live transcripts under ~/.claude/projects/ (main + worktree-keyed dirs) — isolated, so the asking conversation receives only the answer, never the transcript. Not for resuming work (that's /workon). |
Layout on disk
<repo>/
├── HANDOFF.md # single-mode handoff, or multi-mode index (one line per workstream)
├── HANDOFF-<slug>.md # per-workstream handoff (multi mode)
├── BOARD-<slug>.md # phased-delivery board (Phases/Active/Backlog/New Ideas/Inbox)
├── docs/
│ ├── ledger.md # append-only: what landed and why (written by /land)
│ └── <slug>-runbook.md # operational reference (deploy rules, IDs, standing rules)
├── _handoff-archive/ # archived workstream handoffs
└── .claude/
├── flow.yaml # parallel_mode · test_command · default_branch · known_writers
├── branch-state/
│ └── <branch>.yaml # per-branch contract — lives ON its branch; deleted at landing
├── session-reports/
│ └── <branch>.md # sibling-session delta — folded + deleted at landing
├── flow-unlock/ # per-session unlock passes — NEVER committed; /land-only now
│ └── <session_id>.json
└── worktrees/
└── <name>/ # the worktrees — gitignore `.claude/worktrees/`
And in ~/.claude/: commands/ (the nine command files), hooks/flow-*.js (+ flow-lib.js), agents/session-historian.md, scripts/flow-transcript-sweep.mjs, scripts/flow-worktree-quarantine.mjs, scripts/flow-drop.mjs, scripts/flow-inbox-migrate.mjs, templates/ (handoff, board, flow.yaml), tests/flow/run-tests.mjs, and both docs. Also flow-drops/ — the drop queue itself, gitignored and machine-local.
Visibility rule (where files are while work is in flight): the tree above is the default branch's view. Everything a workstream's sessions commit — its handoff, board, specs, code, its branch-state file — exists only on that worktree-<slug>-vN branch until landing: readable in its worktree and on origin/<branch>, absent from the main checkout by construction. /land's squash-merge is the one mechanism that makes those files appear in the main checkout. Corollaries: a brand-new workstream's handoff is created by its first wrap, not by /workon; a conversation that ends without any wrap leaves no handoff at all — just checkpoints plus a branch-state with sealed: false, which is how /workon's re-enter path detects and announces the unwrapped takeover.
The branch-state contract
branch: worktree-billing-rework-v3
workstream: billing-rework # sibling detection keys on this
conversation: billing-rework-v3
status: in-progress # in-progress | ready
sealed: false # true after a wrap; guard denies edits from that session
session_id: <uuid>
note: "what's left, one line" # the "status note"; or a land-blocked reason
Filename: the branch name with / and \ replaced by __, plus .yaml. Reading order everywhere: (1) the branch's own worktree, (2) git show <branch>:…, (3) git show origin/<branch>:… for origin-only branches — and never a glob of the main checkout's branch-state/ directory, which is empty by construction.
flow.yaml
parallel_mode: false # true = main checkout locked; auto-armed by /workon at 2nd concurrent worktree
test_command: "" # the single source of truth for the suite; wrap verdict + land gate
default_branch: main # informational; commands resolve the real one via origin/HEAD
known_writers: [] # every legitimate external automation that pushes to this repo
Arming takes effect the moment the file is written (the guard reads the file, not git) — flow.yaml is one of the six coordination files carved out of the lock (see below), so the write itself needs no unlock flag even though it just locked the repo. It is deliberately left dirty afterward: /land's Step-0 pre-check commits it at the next landing, same as any other coordination file (this replaces the earlier immediate-commit-via-unlock-flag dance, which itself had replaced a defer-the-commit model that left flow.yaml dirty in main and aborted the next /land's squash-merge; both retired). known_writers exists so the answer to "who else pushes here?" is a documented list, not an investigation.
Test-gate capture: test_command is populated by /wrap, not by the human. When /wrap finds it empty in a repo that appears testable (test files, a package.json test script, or tests written this session), it determines the suite command, verifies it runs, records it in flow.yaml (creating the file from the template if absent) and announces the change — asking only when more than one plausible command exists. A testable repo is never left with a silently empty gate; if one somehow is, /land's preview surfaces it as "no test gate".
Coordination files: exempt from the lock
Six path shapes are carved out of the main-checkout lock entirely, for Edit/Write, from any session — including one working an unrelated repo:
| Path | Shape |
|---|---|
BOARD-*.md, HANDOFF-*.md, HANDOFF.md | repo root only |
.claude/flow.yaml | exact |
docs/*-runbook.md | basename ends -runbook.md, directly in docs/ |
_handoff-archive/*.md | files directly in that directory, never a subdirectory |
The first four make the board's Inbox genuinely writable. The last two are what a main-checkout wrap writes by contract — MIGRATE MODE snapshots the old handoff into _handoff-archive/ and moves reference blocks into docs/<slug>-runbook.md. (A seventh shape, docs/decisions.md, sat on this list until 2026-09-02 as an exempt path that nothing wrote; the record-layer consolidation retired the file itself, so the exemption went with it and the guard now gates that path like any other doc.) See the hooks table above for the exact mechanism — an explicit-allow carve-out in flow-main-guard.js, the same shape as the unlock-flag carve-out.
Who can actually reach a board — the isolation is tool-shaped, not path-shaped. The carve-out above settles the guard's layer only. The harness's own worktree isolation sits in front of it and is not ours to lift. Measured by probe on 2026-09-02 from an unsealed worktree — six writes, three targets, two tools:
| Target | Write/Edit | Bash redirect |
|---|---|---|
| No git repo at all | allowed | allowed |
| A different repo's main checkout | allowed — unguarded | allowed |
| Its own repo's main checkout | refused | allowed |
Refusal text, verbatim: "This session is isolated in the worktree …. Edit the worktree copy of this file instead of the shared-checkout path." So the isolation guards Write/Edit against a session's own repo's main checkout, and nothing else. Two consequences: a path outside the target repo is reachable with the supported Write tool — which is exactly what the drop queue below uses — and the Bash column is a gap we deliberately do not exploit (undocumented, can close in any release, and routing around a boundary the harness is enforcing is not something this system institutionalises).
The middle row is the dangerous one, because it is not an error. The write succeeds against a copy pinned at the branch point — measured at 41 lines against main's 54 for the same board — and arrives only when that branch lands, appending to the Inbox at end-of-file, which is exactly where every other session appends. A real three-way merge means nothing is lost, but the cost of a hand-over note becomes a merge conflict, deferred.
So the guard emits an explicit allow with a warning when a worktree session edits a BOARD-<slug>.md at the repo root whose slug is not that worktree's own workstream: it names both workstreams, says the edit is going to this branch's stale copy, instructs the session not to report it as delivered, and hands back a ready-to-run flow-drop.mjs add invocation. Advisory only — wrapped in try/catch and falling through to the normal allow on any failure, because a coordination warning must never be able to block a worktree write. The session's own board is deliberately silent (a warning on every board edit trains the reader to ignore it), as is a nested docs/BOARD-*.md.
The supported route is the drop queue. ~/.claude/scripts/flow-drop.mjs add --repo <path> --workstream <slug> --from <slug> --body-file <file> writes one file per drop under ~/.claude/flow-drops/<repo>/<slug>/ — outside every target repo, so the supported Write tool reaches it from anywhere, and two sessions dropping at once cannot collide. Bodies arrive via a file, never argv (non-ASCII in argv crashes node on Windows), and are free text: a body that says "urgent, belongs in Phase F" is honoured by the existing triage, so there is deliberately no structured priority or target-phase field.
/workon's Step 2b drains the queue into the board's Inbox — appending at the end of the ## Inbox section (not EOF; a board may carry later sections), attributing each entry to its source workstream, verifying the board write on disk before deleting any drop, and skipping a drop whose to_repo does not match. Each folded entry carries a trailing <!-- drop:<id> --> marker, which makes an interrupted drain safe to re-run. Step 2b runs in the main checkout, before EnterWorktree — after entry the drain would hit the very wall the queue exists to get around, so the ordering is load-bearing and says so inline. A workstream with no board yet is a no-op and its drops wait, which is correct under the plan-then-board rule. /repo-health reports what is still pending, so a workstream nobody reopens never holds notes invisibly.
Deliberately NOT built: a carve-out that lets a sealed session write coordination files (it would only restore access to the same stale worktree copy — the wrong artefact); any use of the Bash gap in the table above; and SendMessage as the hand-over mechanism, since it needs the target session live, leaves no durable record, and Inbox notes are non-urgent by definition. Relocating the Inbox to its own file inside the repo was also rejected: the block is per-checkout, not per-file, so an in-repo inbox is refused exactly as the board is.
The Inbox heading itself now states the rule it enables, verbatim:
## Inbox
_New ideas, backlog items and topics for discussion. Anything added below is triaged at this workstream's next /workon — moved to Backlog or New Ideas as appropriate, or slotted into the current phase if urgent. Anyone may add here by hand. A session working in a worktree cannot write this file; it drops a note via `~/.claude/scripts/flow-drop.mjs` instead, which /workon folds in here before triage._
The wording names no individual (anyone may write), does not call entries one-liners (they may be paragraphs), states what the Inbox is for, states how triage disposes of it, and names the one route a worktree session must use — explicit at the point it is read. An earlier version of this rewording reached only new boards created from the template, leaving four boards on two older variants for months; scripts/flow-inbox-migrate.mjs <repo>… now exists so the rewrite is repeatable and idempotent, rewriting only an italic blurb and reporting UNRECOGNISED rather than guessing at anything else.
Commits are not part of the carve-out. The guard still denies git verbs on a locked main, so these files sit dirty on disk between writes; /land's Step-0 pre-check auto-commits exactly this list before doing anything else — the two sets are kept identical on purpose, since anything writable-but-uncommittable would sit dirty forever and abort the next squash. Practical consequence: a cross-repo Inbox drop reaches GitHub only at that repo's next land — but /workon always reads straight off disk, so triage is never delayed by that gap.
CLAUDE.md, memory files and other context files are deliberately not carved out — they steer every session that reads them, which makes them closer to source than to coordination; writes to them stay gated like any other file. Note what that means for a /wrap-end run in a locked main checkout: its "capture session learnings" step cannot write ~/.claude/memory/*.md or a project CLAUDE.md there. The command now says so plainly and tells Steve the change belongs in a worktree (or his own editor, which the guard has never applied to) rather than pretending a carve-out covers it.
Say it plainly, because it surprises people: this guard was never a lock on Steve. It is a PreToolUse hook on Claude's own tool calls (Edit/Write/NotebookEdit/mutating Bash). Steve's own edits — in VS Code, in any editor, to any file, coordination or otherwise — were always free; nothing in Session Flow has ever stopped him typing into a file directly.
Worktree quarantine before every removal
scripts/flow-worktree-quarantine.mjs <worktree-path> runs immediately before a worktree is removed, on all three removal paths (/land's per-branch cleanup, /wrap-end, /quickfix). It copies the worktree's gitignored files (git ls-files --others --ignored --exclude-standard) to <FLOW_ARCHIVE_DIR>\_quarantine\<repo>\<worktree>-<date>\, skipping a fixed list (node_modules, dist, .venv, __pycache__, target, any nested .claude/worktrees). Silent when nothing is preserved; one report line otherwise; fails soft on locked files (a Windows-specific risk — a just-closed session can still hold a handle).
Why this exists: git worktree remove silently deletes gitignored files with no confirmation and no --force needed — git has treated ignored files as disposable since 2.17 (documented only in the introducing commit, never the man page; there is no preserve flag). A worktree landing destroyed a gitignored local secrets file, taking live API tokens with it, because the file was gitignored rather than merely untracked. wrap-end.md's previous unconditional --force on worktree removal made this worse (it overrides even git's untracked-file protection) and is deleted along with this fix.
The rule that generalises, worth stating on its own: a worktree must never hold the only copy of anything. Concretely:
- Secrets (API tokens, PATs, passwords) belong in environment variables, with LastPass as the source of truth — never a loose file inside a worktree, gitignored or not.
- Irreplaceable non-secret data (local fixtures, a local database, anything that took real effort to assemble and isn't derivable from the repo) must live outside the worktree.
- Regenerable state (
node_modules,dist, build caches) is fine to lose — that's exactly what the skip-list above exists to leave behind.
The reverse need — copying a gitignored file into a freshly created worktree (e.g. a local .env cache every session needs) — is served by an optional .worktreeinclude file in the repo, not by the quarantine script.
Keeping a branch in sync with main, outside landing
If a worktree's branch ever needs today's main folded in mid-work — rare; most sessions never need this — do it with a plain git merge <default-branch>, never a rebase. Squash-landing already discards branch history at the end, so a merge bubble mid-work costs nothing, and merging can never leave a branch rewritten with no way back if something goes wrong.
/land's own rebase (used to fast-forward a branch cleanly before squashing) is the one place rebasing still happens, and it is built to fail safe: the pre-rebase ref is recorded before the rebase runs, and if the land aborts partway through, the branch is either reset back to that ref or pushed with git push --force-with-lease origin <branch> before the procedure stops — never left rewritten and unpushed. What actually permits that push, stated accurately: there is no worktree-scoped permission rule and there never was one. settings.json carries a broad, pre-existing Bash(git push --force-with-lease:*) rule that does not look at the branch name at all, so a force-with-lease push to any branch — the default branch included — clears the settings layer. The design intent (worktree branches only) is therefore a discipline written into land.md, not a mechanism: what makes it deterministic for the default branch is the GitHub ruleset below, which blocks force pushes server-side and is active on the flow repos. The settings layer alone would not stop you; the ruleset does.
Board creation: born from the plan, not from /workon
BOARD-<slug>.md is no longer created automatically at a workstream's first /workon. Steve's ruling: phases and journey steps can only be chosen meaningfully once a plan exists — an empty shell board at first entry is premature and just sits there unfilled.
Trigger: when a plan is approved for work in a flow repo (ExitPlanMode, or a writing-plans plan being written), the board is created then, in the worktree, with:
- Phases derived from the plan's own structure — each phase ends at one of the plan's own milestones/checkpoints, not an arbitrary split.
- Journey steps as the human-readable division: the logical sequence of events the product/workstream goes through.
At the same moment, in the same worktree: the workstream's row is added to HANDOFF.md's index, and — where the repo's own CLAUDE.md carries a workstream map — its line there too. All of it lands through the normal /land cycle; no carve-out is needed because it happens inside a worktree, not on a locked main. The standing rule this satisfies: the process, not Steve, keeps the handoff index and CLAUDE.md aware of every workstream.
For a plan-less workstream (a question that grew into real work with no formal plan written), the wrap-time "no board is tracking this" nudge is the fallback — it now offers plan-then-board rather than a bare /dev-project setup pointer, and only mentions the Systems Dashboard where that integration actually applies.
The trigger and the board's format are deliberately separable. This decision fixes only when a board is created; the template itself (currently designed for a human reader) may still be revised later — an AI-focused variant with issue lists and blockers has been discussed — without touching this trigger.
GitHub Rulesets — the server-side backstop
Client-side protection (the guard hook, settings permission rules) is advisory: any of it can be bypassed locally (--no-verify, editing settings.json, a classifier quirk), and a distributed plugin cannot ship permission rules or hook registrations into a client's settings.json at all — only the guard/hooks/commands/quarantine script travel with the plugin. A GitHub Ruleset on the repo's default branch is the deterministic backstop: it is enforced server-side, so it holds even if every client-side layer is bypassed or absent.
Scope, and why it must be exactly this narrow: default branch only, two rules — block force pushes (non_fast_forward) and restrict deletions (deletion). Nothing else: no PR requirement, direct pushes stay allowed, so /land's own git push passes untouched. The default-branch scoping is load-bearing, not a preference: /land deletes worktree-* branches on origin as routine cleanup, and the sync escape hatch above force-pushes them with --force-with-lease — a repo-wide ruleset would block both of those legitimate operations.
Plan requirement (checked, not assumed): on GitHub Free, rulesets on a private repo can be created but are not enforced — the repo's Settings → Rules page shows an explicit "not enforced" upgrade banner. GitHub Pro removes this restriction for a personal account's private repos (~£4/month, covers all of them); the flow repos run on Pro, so their rulesets are enforced.
Enrolment call — identical for every flow repo:
gh api repos/drsteveday/aisys/rulesets -X POST --input - <<'JSON'
{ "name": "session-flow main protection", "target": "branch", "enforcement": "active",
"conditions": { "ref_name": { "include": ["~DEFAULT_BRANCH"], "exclude": [] } },
"rules": [ { "type": "non_fast_forward" }, { "type": "deletion" } ] }
JSON
(Repeat per repo, substituting the repos/<owner>/<repo> path. The heredoc has no backslashes in it, so a plain Bash call is safe here — if a hook objects anyway, write the same JSON to a file and pass --input <file> instead.)
Verification is the enforcement banner, not the API response. After the Pro upgrade: gh api repos/<owner>/<repo>/rulesets should show "enforcement":"active", and the repo's Settings → Rules page must show no "not enforced" banner — that banner is ground truth. A live negative test confirms it end-to-end: from a scratch clone, git push --force origin main on a no-op amend must be rejected by the server.
Known-writers check (once per enrolled repo): confirm none of the repo's documented external writers (VPS crons, the KB Sync Bot, etc. — see known_writers in flow.yaml) force-push or delete the default branch; an ordinary push still passes the ruleset untouched.
If enforcement still fails after the Pro upgrade, the client-side guard remains the only protection, exactly as it was before this decision — nothing regresses, the ruleset is purely additive.
Client packaging: because plugins cannot ship permission rules or repo settings, aisys-setup gains two one-time guided steps for a new install: (1) paste the flow permission rules (now just the flag-dir pair plus the broad, pre-existing force-with-lease rule) into the client's settings.json, and (2) run the enrolment call above on their repo — or note the plan limitation if they're not on a plan that enforces it.
The handoff v2 contract
Header: slug · **Flow:** v2 (the migration marker — absence triggers MIGRATE MODE) · latest conversation · last touched (local wall-clock with time) · one-phrase status · working environment · anchor lines carried verbatim. Exactly one On-resume block naming <slug>-v<N+1>. Six core sections (Session focus / What shipped / What's pending / Decisions / Working tree state / Open questions). ## Links by path, never copied content. ## Recent sessions capped at three lines. Claims marked ✅ verified (how) / ⚠ believed. Hygiene on every wrap: stale-fact sweep in place, rolling compression, promote-then-delete durable facts, current-first ordering, soft budget ~200–400 lines.
Ledger entry format
## <YYYY-MM-DD> — <slug> — <headline>
meta: branch <branch> · workstream <slug> · conversation <slug>-vN · squash subject "<first line>"
<paragraph>
Decisions: (1) <chose X over Y because Z>. (2) <…>.
Overturned: (1) <what was held before> → <what replaced it> — <why it changed>. (2) <…>.
The Decisions: and Overturned: lines carry the session's rationale and its reversals, lifted from the handoff's ## Decisions / conventions established this session section and compressed to one clause each. They inherit workstream, date and conversation from the meta: line above, so both are attributed by construction rather than by hand. Each is omitted entirely when there is nothing to record — never written as "none".
An entry written by an unlocked main-checkout wrap takes the same shape, with the two branch-and-squash fields naming their own absence: meta: branch (none — main checkout) · workstream <slug> · conversation <slug>-vN · squash subject "(no squash — direct)". Nothing else differs.
The paragraph stays full detail whether or not either line is present: it is what a reader sees before deciding whether to open the conversation named in the meta: line, so it carries verified facts, IDs and pointers, not a restatement of the squash subject. The Overturned: content is captured earlier than the entry itself — by /wrap's stale-fact sweep, at the moment it corrects a stale claim in the handoff, because that correction is what destroys the prior claim. In the handoff section that holds it, the Overturned: sub-line accumulates where the decisions above it are replaced: every wrap carries forward the items no ledger entry has lifted yet and appends its own, so a branch spanning several conversations delivers all of its reversals rather than the last conversation's only. Lifting it onto an entry is what clears it, and a section holding one is never overwritten with *N/A this session.*.
No commit hash, deliberately: writing the hash into its own commit amends the commit (the hash changes); the branch tip dies with the branch. The meta line is a greppable identity — find the landing with git log --grep "<squash subject>".
Known limitations
- Auto-arming covers only
/workon-created worktrees — not agent-dispatch worktrees, baregit worktree add, or/quickfix//landtemp worktrees. - The guard's cwd-based repo resolution (quirks above).
flow-context-nudgeinfers the context window rather than being told it — setFLOW_CONTEXT_LIMITif a session's window differs from what its project's recent transcripts suggest.- Single-operator design: nothing coordinates two humans landing simultaneously in one repo.
- A repo that has not been enrolled in the GitHub Ruleset has only the client-side guard, which is advisory — enrol each new repo with the call above.
Keeping this current: any change to commands, hooks or templates must update both documentation files (docs/session-flow.md + docs/session-flow-technical.md) — the coverage test in tests/flow/run-tests.mjs enforces the names; the prose is on you. And any edit to those files must rebuild and republish this page in the same session — it does not update itself.