whiphand runs workflows against a working folder: an ordered set of steps — an LLM CLI runner
(claude, copilot, opencode) with its own model and tool policy, a shell command, or a stop to
ask a human — which can be wrapped in a cycle that repeats until a check passes. The
canonical workflow plans a feature interactively with one model, then implements, tests and
reviews it in a loop until the review comes back clean.
Requires a Rust toolchain for the whiphand CLI, Node ≥ 24 to build the desktop
app's UI and run the test scripts, and at least one of
claude / copilot / opencode on PATH.
npm install
cargo build --release -p whiphand-cli # target/release/whiphand
target/release/whiphand --help # or: cargo install --path crates/whiphand-cli && whiphand --helpwhiphand doctor # check the tools whiphand needs are installed
whiphand init # scaffold .whiphand/ (config + starter workflows) here
whiphand new-workflow review-pr # scaffold .whiphand/workflows/review-pr.yaml
whiphand run examples/cycle.yaml --dry-run --input feature=demo # print every argv, spend nothing
whiphand run examples/feature.yaml --input feature="oauth support" # the real thing
whiphand run examples/cycle.yaml --input feature=x --max-iterations 5 --yes # override the loop budget; don't ask
whiphand run --resume 20260907-141233-a3f1 --extra-iterations 2 # continue a stopped run; grant 2 more iterations to any exhausted loop
whiphand run iterate --input feature="oauth support" --name "OAuth support" # label the run
whiphand run iterate --input feature="fix login" --attach ./bug.png --attach ./server.log # hand files to the plan step
whiphand rename-run 20260907-141233-a3f1 "Something better" # relabel it later ('' clears)
whiphand run examples/feature.yaml --input feature=x --worktree # run on a new branch in its own git worktree
whiphand run develop --input feature=x --no-worktree # run in the working folder even if the workflow asks for a worktree
whiphand run develop --input feature=y --in-worktree 20260907-141233-a3f1 # run again in that run's worktree, on its branch (refused while another run is using it)
whiphand worktree remove 20260907-141233-a3f1 # remove a run's worktree (--force discards uncommitted changes); the branch stays
whiphand tui -C ~/src/shop # experimental: follow a workspace's runs in the terminalWorkflows live in .whiphand/workflows/<name>.yaml (so whiphand run iterate works) or anywhere as a
path. Artifacts land in .whiphand/runs/<run-id>/ as plain markdown. Workspace defaults live in
.whiphand/config.yaml.
Terminal UI (experimental). whiphand tui is a keyboard-only front end for terminals, tmux and
SSH, beside the desktop app. It opens -C, else the current folder if it has .whiphand/, else a
list of recent workspaces. For now it is read-only: the runs list (/ filters, o shows what is
running in every recent workspace), a run's step tree beside its log, events, artifacts and diff
(1-4), and whiphand doctor. o opens an artifact in $PAGER, D runs git diff in the run's
worktree, and ? lists every key. A run another whiphand process is driving (the desktop, the CLI)
shows as foreign: you can follow it, and the process that started it answers its prompts. The
terminal rings and sets its title when a run needs you or ends; WHIPHAND_TUI_NOTIFY=off (or
bell) quiets that, and NO_COLOR is honoured.
whiphand init ships three starter workflows. The two that build something end the same way: a
human gate that can send the work back with comments for another cycle, not just ship it or kill
it. iterate works on the branch you are on, in your own checkout: it plans once, interactively,
then implements and reviews in a cycle until the sign-off approves it, and commits the signed-off
work with a message it writes from the diff. It never switches or creates a branch, so run it again
for the next small step on the same branch. Anything already uncommitted when it starts ends up in
its commit; a first step records it, so the review does not count it against the run. develop is for a larger feature: on feature/<run slug> in its own git worktree,
brought up to your trunk's remote state, it cuts the plan into stages and builds, reviews, accepts
and commits them one at a time, then pushes the branch (leave the push command blank to skip that).
whiphand new-workflow scaffolds from iterate.
Running workflows side by side. develop declares worktree:, so each run gets its own checkout
under .whiphand/worktrees/<run-id>-<slug> (just <run-id> for an unnamed run) on a new branch, and your main checkout is never touched: start
two features at once and they cannot collide.
--worktree / --no-worktree (or the New Run dialog's switch) override the workflow's choice for one
run; --in-worktree <run-id> instead continues in the worktree an earlier run made, on its branch;
--resume always returns to the tree the run started in. Deleting a run removes its worktree
(refusing if it has uncommitted changes) but never its branch; whiphand worktree remove does it by
hand. A worktree starts from the base's fetched upstream (worktree.sync, on by default; set it to
false to start from the local base). whiphand init writes templates to new workspaces only, so an
existing workspace keeps its old copies until you replace them. See docs/design.md, Worktrees.
research builds nothing. You settle the question with the agent in a live chat, then a headless
agent investigates and writes a report whose every claim carries a file:line or a URL, a second
agent checks it against your question and spot-checks its sources, and you read the result: accept
it, or send it round again with a comment.
A run can carry a name (--name, the desktop's New run dialog, or later with
whiphand rename-run, where '' clears it), shown in place of its id in the UI and
notifications. See docs/design.md for how a step sees its run's name and how auto-naming
works.
whiphand doctor reports what this machine has, in two groups — the same data the desktop's
Doctor page renders, so the two can never disagree:
AI harnesses
✔ claude 2.1.263
· not logged in — run `claude` and use /login
✔ copilot 1.0.83
· copilot will not signal when it needs you; set "beep": true in ~/.copilot/settings.json
✔ opencode 1.17.13
Support tools
✔ git 2.53.0
✔ node 24.16.0
○ fd not installed
✔ installed · ✘ missing and required · ○ missing but optional. The harness group lists
exactly the registered runners, so any harness in it can be a workflow's runner:; doctor.yaml
may override a harness entry but not add one. The support group also lists tools that make agents
more effective (gh, ast-grep, yq, uv, ctags, scc); all of them are optional, so a machine
without them shows ○, never ✘.
An installed tool that has no login gets a note under its row, with the fix. The row stays ✔
(the binary is there); the note is what will fail the first step:
| Tool | Note | Counts as logged in |
|---|---|---|
| claude | not logged in — run `claude` and use /login |
~/.claude/.credentials.json (or under CLAUDE_CONFIG_DIR), ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN, CLAUDE_CODE_OAUTH_TOKEN, Bedrock/Vertex/Foundry, or an apiKeyHelper / key in ~/.claude/settings.json. Not checked on macOS (the login is in the Keychain). |
| copilot | not logged in — run `copilot login` |
COPILOT_GITHUB_TOKEN, GH_TOKEN or GITHUB_TOKEN; COPILOT_PROVIDER_BASE_URL (bring your own key); or a login recorded in ~/.copilot/config.json |
| opencode | no provider credentials — run `opencode auth login` |
a credential in auth.json under $XDG_DATA_HOME/opencode, or a provider key opencode auth list finds in the environment |
| gh | not logged in — run `gh auth login` |
gh auth token --hostname github.com finds a token (keyring, hosts.yml or GH_TOKEN) |
These checks are local: nothing is sent to a server and no token is read into the output, so an offline machine is not reported as logged out. A note appears only when doctor is sure — a timeout, an unreadable file or an answer it does not recognise gives no note, so a missing note is not proof of a login.
Two more notes cover an install that is present but not doing its job:
- rtk hook — when claude and rtk are both installed and neither
~/.claude/settings.jsonnorsettings.local.json(underCLAUDE_CONFIG_DIRif set) has a hook whose command invokesrtk, the rtk row saysrtk is installed but no Claude Code hook calls it — run `rtk init -g` to set it up. A settings file that cannot be read or parsed gives no note; project-level settings are not consulted. - Minimum version — a harness older than the oldest version whiphand's adapter was verified against (claude
2.1.260, copilot 1.0.83, opencode 1.17.13) gets
older than <version>, the oldest version whiphand is tested with — update it. A version doctor cannot read as plain numbers gets no note.
Run inside a project (or with -C <dir>; the desktop passes the open workspace), doctor also checks
that folder and adds a row only when something is wrong with it: git refusing the repository
(detected dubious ownership — the row carries the git config --global --add safe.directory … fix,
and steps that need the write-guard fail until it is applied) and, on Windows, a path too long to leave
headroom under the 260-character limit (open it through subst instead).
Doctor's built-in table lives in crates/whiphand-core/src/doctor/tools.rs. To extend it on your own
machine, hand-write doctor.yaml beside your global config.yaml:
| Platform | Path |
|---|---|
| Linux | ~/.config/whiphand/doctor.yaml |
| macOS | ~/Library/Application Support/whiphand/doctor.yaml |
| Windows | %APPDATA%\whiphand\doctor.yaml |
(WHIPHAND_CONFIG_HOME overrides the directory.)
tools:
- id: bun # a new id is appended to its group
label: Bun
group: support # 'harness' or 'support'
argv: [bun, --version]
url: https://bun.sh
- id: rtk # an existing id REPLACES that built-in, in place
label: rtk
group: support
argv: [rtk, --version]
version_pattern: 'v?(\d+\.\d+\.\d+)' # capture group 1 is the version
optional: false # missing is then an error, not a shrug
aliases: [rtk-bin] # other binary names to try, in order
hide: [fd, jq] # drop built-ins you do not care aboutEvery key is validated and unknown ones are rejected, so a typo tells you rather than
silently doing nothing. Overriding a built-in changes its label, group, url and
optional, but detection for claude, copilot and opencode always goes through their
adapters — that is where their setup advice (copilot's beep note, opencode's PATH note)
comes from.
It is a separate file from config.yaml on purpose: config.yaml is rewritten
wholesale whenever settings are saved, so anything unrecognized in it would be lost.
Nothing writes doctor.yaml.
See docs/design.md for the full reference. The short version:
name: cycle
inputs:
feature: { required: true }
test_command: { required: false, default: npm test }
steps:
- id: plan # live terminal chat; artifact harvested from the session afterwards
runner: claude
model: opus
mode: interactive
writes: false
output: plan.md
prompt: |
We are planning: {{ inputs.feature }}. Work with me on a plan. Do not modify files.
- id: human-review # repeats fix-cycle until `sign-off` approves
kind: loop
until: sign-off
max_iterations: 5
steps:
- id: fix-cycle # repeat the body until `review` returns VERDICT: PASS
kind: loop
until: review
max_iterations: 3
steps:
- id: test-fix # repeats until the tests pass, before `review` ever runs
kind: loop
until: tests
max_iterations: 3
steps:
- id: execute # headless; may write
runner: copilot
model: gpt-5.5
mode: headless
writes: true
# `tests`, `review` and `sign-off` are forward references: the
# previous iteration's/round's log, findings and feedback,
# each dropped when its own loop has none yet to read.
inputs: [plan, tests, review, sign-off]
output: execute-report.md
prompt: |
Implement the attached plan. If a tests log marked VERDICT: FAIL
is attached, fix every failure it shows first.
- id: tests # a shell command: no runner, no tokens
kind: command
run: eval "{{ inputs.test_command }}"
verdict: true # non-zero exit sends test-fix round again
output: tests.log
- id: review # headless, read-only, must emit VERDICT: PASS|FAIL
runner: claude
model: haiku
mode: headless
writes: false
verdict: true
inputs: [plan, execute, tests, sign-off]
output: findings.md
prompt: |
Review the diff against the plan. If sign-off feedback is
attached, FAIL unless every requested change is addressed.
- id: sign-off # stops and asks you, showing the diff
kind: approval
verdict: true # required: this is what the outer loop's `until` reads
title: Ship it?
instructions: Approve, or request changes with comments on the whole change or on individual files.
show_diff: true
capture: review # per-file comments; required to answer "request changes"
inputs: [review]
output: feedback.mdexecute reads tests, review and sign-off as forward references, each resolving to its
own loop's previous pass and dropped when that loop has none yet — so a round that requests
changes, or a run that just failed its tests, sends fresh feedback back into the next attempt.
That's the shape every starter workflow ships with — see docs/design.md's "Sending work
back" section for the nested-loop details.
Step kinds. agent (the default), command, manual, approval, and loop — a cycle
over its own steps that repeats until its until step passes, bounded by
max_iterations. verdict: true makes any step report pass/fail (an agent's VERDICT:
line, a command's exit code, a human's answer) instead of failing the run outright — that is
what a loop's until watches, and what on_findings (below) reacts to outside a loop.
Human steps. manual and approval stop the run and ask — on a terminal whiphand
prompts you; without one it fails naming the step, unless --yes takes the step's default.
The desktop app gives the decision the whole page: show_diff: true shows the working tree
side by side, and capture: review turns the screen into a place to leave per-file feedback
that feeds back into the next loop iteration on retry.
Stages. kind: stages runs its own steps once per file in a directory instead of once
over one input — build stage 1, get it reviewed and accepted, commit it, then move to stage
2 — so a large feature never needs one sign-off over the whole diff at the end. items is a
templated glob ({{ run.dir }}/plans/*.md, re-globbed before every stage, so an added file
is picked up and a deleted pending one is skipped). {{ run.dir }} is the run folder — an
absolute path with forward slashes, the same value as $WHIPHAND_RUN_DIR — so the shipped
workflow's planner writes its stage files there, not into the repository: they are not
committed to the branch or the PR, and go when runs.max_retained prunes the run. (A glob
metacharacter — [, *, ?, { — in the workdir path would break that glob.) A stage's
id is its file's basename
without extension, and renaming or renumbering an already-completed stage file makes it run
again under its new id. {{ stage.index }}, {{ stage.total }}, {{ stage.id }},
{{ stage.title }} (and $WHIPHAND_STAGE_ID/_TITLE/_INDEX/_TOTAL/_PATH for a
command step) read the stage currently running, and inputs: [stage] attaches its file —
all four only exist inside a stages body. Every verdict: true step in a stage's body
(a loop's until included) must be followed by a gate placed directly in the body, not
inside a loop. A loop that runs out inside a stage ends there and hands over to that gate.
Rejecting at the gate re-runs the whole stage, with the rejection handed to the last
writes: true step before the gate, up to max_retries (default 2) before it hands the
stage to you in a live session. allow_paths on a writes: true step fails it, naming the
file, if it touched anything outside the given globs. See the shipped
develop workflow and docs/design.md's "Stages" section for the rest.
That workflow's commit steps are POSIX shell lines; command steps run through a POSIX shell on every
OS (see Windows below), so it runs unchanged on Linux, macOS and Windows.
Attachments. --attach <path> (repeatable — or the desktop's New Run dialog: pick,
drop, or paste an image) copies a file into the run before step one; a step reads them by
naming the reserved ref attachments in its inputs:.
See docs/design.md for the full reference: cycles and resuming an exhausted loop, stages,
disabling a step, on_findings (what happens when a review outside a loop finds problems),
and every rule above in detail.
apps/desktop is a Tauri + Fluent UI shell over the same Rust engine
(crates/whiphand-core) the CLI uses — workflows, runs, and cancellation behave identically in both; the desktop app
just adds a GUI (workflow picker, live run view, xterm-backed interactive handoff, and a
Files tab: a tree of the opened workspace with rendered-markdown preview and in-place
editing).
The sidebar is grouped by scope: a workspace switcher on top, then the pages that act on the open workspace (Runs, Workflows, Files, Settings), then the ones that don't (Activity, Doctor, Preferences). Activity lists runs across every recent workspace; workspaces can be pinned, and Ctrl/Cmd+K switches between them.
Above Activity, an "Ongoing runs" section lists live jobs — running, or blocked on you — as clickable rows with a status pill, so switching to the one that needs attention doesn't require a detour through the Activity grid; waiting jobs sort to the top. It's capped at 5 rows, with a "+N more" row into Activity, and can be turned off from Preferences.
The Files tab reaches the filesystem through Tauri's fs plugin, whose scope starts empty and is extended at runtime to directories you explicitly open, pick in a folder dialog, or drop on the window; the agent's RPC surface has no file methods.
Prerequisites: Node ≥ 24, a Rust toolchain (cargo), and on Linux the webkit2gtk
dev packages (libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev).
npm run tauri dev -w desktop # launch the desktop app in dev mode
npm run verify # local CI-equivalent gate — see belownpm run verify runs the same checks as CI in one fail-fast chain: root
typecheck/tests, the CLI/desktop parity suite (npm run test:parity — verifies the
desktop app's surface and behavior match the CLI, and that the agent still answers
as recorded, see parity/), the desktop app's own tests and build,
and finally the Rust checks: fmt, clippy and tests for the Cargo workspace at the
repo root (crates/*), and cargo check against apps/desktop/src-tauri. If
cargo isn't installed, the Rust checks are skipped with a warning instead of failing.
The core parity corpus (parity/fixtures/core/) is a set of JSON op lists and the
results the TypeScript implementation gave for them before it was removed; cargo test
compares against them. After an intended change in behavior (a template edit, say), run
WHIPHAND_UPDATE_GOLDEN=1 cargo test -p whiphand-core --test parity, which rewrites only
the results that changed, and review the diff.
The agent's regression check (parity/agent.test.ts) plays a scenario corpus that
calls every protocol method against target/release/whiphand-agent and compares the
normalized transcripts with the ones recorded for the platform in
parity/fixtures/agent/{posix,win32}/. PARITY_RECORD=1 re-records them; review the
diff, since a recorded change is a change in behavior.
Wayland troubleshooting: if the desktop window opens with a blank webview under
Wayland/webkitgtk, set WEBKIT_DISABLE_COMPOSITING_MODE=1 and
WEBKIT_DISABLE_DMABUF_RENDERER=1 in the environment before launching.
The desktop app can serve the same UI to a browser on another computer on your network, so you can watch a run, read its logs and diff, answer an approval, or start and cancel work from a laptop in another room. Turn it on in Preferences → Remote access, then open the URL it shows (or scan the QR code).
Read this before turning it on.
- Anyone with the link can run commands on the host machine. That is what Whiphand does:
startRunexecutes a workflow,ptyInputtypes into a live session, and the browser can edit workflow YAML — which is the list of commands to run. Treat the link exactly as you would an open terminal on that machine.- The connection is plain HTTP, with no TLS. The token and everything the run prints cross the network in the clear. Anyone who can capture your network traffic — shared Wi-Fi without client isolation, a compromised device on the LAN — gets the token, and with it the machine.
Use it on a network you control. It is off by default and stays off until you explicitly enable it.
How it works: the agent built into the desktop app also listens on a TCP
port (61338 by default), serving the browser bundle over HTTP and the same NDJSON
JSON-RPC protocol over a WebSocket. Access is gated on a 256-bit token carried in the
URL fragment — never sent to the server, so it stays out of access logs — and stored
per-origin in the browser. Because a WebSocket upgrade is not subject to CORS, the
agent additionally validates the Host and Origin headers on every request, which
is what stops a DNS-rebinding attack from a page you visit elsewhere. Rotating the
token disconnects every device using the old link.
Two differences from the desktop app:
- No Files tab. Browsing the workspace reads the local disk directly, which a browser on another machine cannot do. Run artifacts and diffs still work, because those go through the agent's RPC rather than the filesystem.
- No folder picker. Recent workspaces are one click away; anything else is opened by typing its path.
Runs already in progress replay in full when a browser attaches — the agent keeps a per-job transcript of terminal and log output, so checking in halfway through a run shows what already happened rather than an empty pane.
whiphand treats Windows as one more platform with one way to do paths and processes, not a special case.
- A POSIX shell is required for command steps.
commandsteps run through/bin/shon POSIX and, on Windows, through thesh.exe/bash.exethat comes with Git for Windows — found by deriving it from thegiton yourPATH(never a barebashlookup, which on a WSL machine findsC:\Windows\System32\bash.exe, a launcher into the wrong filesystem).whiphand doctorshows the shell it resolved. With none installed, doctor is red and command steps refuse to run with the fix named; agent steps still work. Write command steps for/bin/sh. - Breaking changes (Windows and, for 2–3, everywhere): (1) the default Windows shell is no longer
cmd.exe; (2){{ }}inrun:expands to a shell variable reference (${WHIPHAND_RUN_NAME}), so single-quoted uses stop expanding — quote it as you would any shell variable (an input that is itself a command, like the shipped templates'test_command, is run aseval "{{ inputs.test_command }}";evalonly an input whose whole purpose is to be a command, never a run name or stage title); (3) an explicitshell:ofcmd/cmd.exe/PowerShell is rejected at parse time. - Portable names. Anything that becomes a file or directory name — a workflow name, step/loop/stage
id, a step's
output(a relative path:reports/plan.mdis fine,.., absolute paths and\are not), an attachment name — is rejected at parse time on every platform if Windows would reject it:\ / : * ? " < > |, control characters, reserved device names (CON,NUL,COM1…, also with an extension), and a trailing dot or space. A workflow authored on Linux withoutput: report:v1.mdis refused where you write it. - Prompts are not on the command line. An agent's prompt travels in a file in the run directory (piped
on stdin to
claude -p, or named by a one-line pointer for the other runners), and Claude's settings are passed by path — so a%VAR%, a newline or a long prompt can no longer be eaten bycmd.exe. - One path style. Everything whiphand shows a model or writes to a manifest is workspace-relative
with forward slashes;
run.jsonstores paths relative to the run directory, so a run directory that moves still resumes. - Nothing outlives the run. A Windows Job Object per run (held by the CLI itself, and by the desktop app) ends the whole process tree on cancel, timeout or crash. Liveness is a heartbeat lease (renewed every 30 s, stale at 5 min), cut short when the owning process provably no longer exists on this machine, so a run whose process was killed is marked crashed at next start. A host suspended for more than five minutes loses its run on wake, and a step that deliberately left a process running does not keep it past the run.
- What is out of scope. A workspace on a typed UNC path (
\\server\share\proj) is refused with a clear message — map the share to a drive letter instead. Paths over 260 characters are not guaranteed, but a workspace deep enough to hit the limit warns when it is opened (subst X: <folder>is the escape hatch). App state and run directories on redirected or network paths are best-effort and not tested in CI. The remote-access token file is0666on Windows (file permissions there only toggle read-only);doctorsays so. WSL is neither supported nor blocked — it is Linux as far as whiphand is concerned.
Both artifacts can be built as self-contained executables that need no repo, no
npm install, and no Node on the target machine.
npm run package:cli # dist/whiphand (dist/whiphand.exe on Windows) — the CLI, a native Rust binary
npm run package:desktop # dist/*.deb + dist/*.AppImage on Linux, an NSIS installer on Windows
npm run package # both
npm run reinstall # Ubuntu: build the .deb, then apt-remove and reinstall itdist/whiphand is the Rust CLI (crates/whiphand-cli), about 3.5 MB with the starter
workflows compiled in. Copy it anywhere on PATH and run whiphand as usual. claude / copilot / opencode are still runtime prerequisites;
whiphand doctor reports them, along with everything else this machine needs.
npm run package:desktop builds the browser UI that remote access serves as a Tauri
resource, then has Tauri's bundler produce a .deb to install and an .AppImage to run
from anywhere (an NSIS installer on Windows). The agent is compiled into the app, so
there is no Node runtime or sidecar to ship. npm run package:cli finishes by smoke-testing
the binary it built (scripts/package/smoke.mjs), so a broken CLI is not produced silently.
npm run reinstall is the Ubuntu edit/install/verify loop in one command: it builds the
.deb, removes the installed package, then installs what it just built (sudo is used
for the two apt steps, so expect a password prompt). The removal is not just tidiness —
the version is pinned across builds, so apt-get install over an already-installed
0.1.0 is a no-op and the new bundle would silently not land.
These are built natively and link this machine's glibc, so they run on the Ubuntu release that built them, not on older ones. Building for wider reach means building in an older-glibc container.
Releases are started by hand from GitHub Actions: bump the version
(npm run bump -- 0.1.4) in a PR and merge it, then on GitHub go to
Actions → Release → Run workflow on main and enter 0.1.4. Releases are immutable, so
a version can only be released once. See docs/design.md for the full release,
auto-update, and updater-signing-key process, and for how to roll back a bad release.
Windows downloads are unsigned. There is no code-signing certificate yet — the seam for
one exists (WHIPHAND_SIGN_COMMAND for whiphand.exe, Tauri's bundle.windows.signCommand
for the installer) but is unset. WHIPHAND_SIGN_COMMAND must name a single executable, not
a full command line with its own arguments — it is resolved and spawned directly, never
through a shell. Expect Windows SmartScreen's "Windows protected your PC" prompt on first
run, and don't be surprised if antivirus software flags either binary: a ~110 MB
postject-modified node.exe and an installer with no publisher signature are both shapes
heuristic scanners dislike. Neither is a defect in the build; both go away once a real
certificate is in place.
docs/design.md— why this exists, the architecture, and every workflow-format detail this README only summarizes.docs/superpowers/specs/2026-09-08-staged-plans-design.md— design for the not-yet-built staged/multi-commit plan feature.docs/review-backlog.md— known issues and larger refactors, not yet scheduled.