aboutsummaryrefslogtreecommitdiffziptar.gz
path: root/docs
diff options
context:
space:
mode:
Diffstat (limited to 'docs')
-rw-r--r--docs/cloud-lane.md195
-rw-r--r--docs/design.md329
-rw-r--r--docs/lanes.md115
-rw-r--r--docs/manual.md239
-rw-r--r--docs/multi-session.md46
-rw-r--r--docs/orchestrator.md152
-rw-r--r--docs/resource-ledger.md304
7 files changed, 1380 insertions, 0 deletions
diff --git a/docs/cloud-lane.md b/docs/cloud-lane.md
new file mode 100644
index 0000000..d48c1ed
--- /dev/null
+++ b/docs/cloud-lane.md
@@ -0,0 +1,195 @@
+# wf cloud — cloud worker lane on promo credits — design
+
+Status: owner decisions 2026-10-06 (design session).
+Tool code goes to `/projects/public/workflow` (public tool repo); this spec was originally developed locally,
+then a copy `docs/cloud-lane.md` in the tool repo becomes the live one (same as resource-ledger).
+
+## 1. Goal
+
+1. Use a promo cloud credit for worker tasks, so the weekly subscription limit goes to design/orchestration/local-only work.
+2. No GitHub, no hosting: code goes up as a `claude --cloud` bundle upload from a local folder, results come
+ back through the session transcript. Push stays local-only.
+3. A local ledger keeps cloud spend under a cap; the orchestrator routes fitting work to the cloud only while
+ the ledger is positive; otherwise everything runs locally as today.
+4. Cloud work never touches TASKS.md / archive: bookkeeping stays local (`wf` is not in the VM).
+
+## 2. Owner decisions (2026-10-06)
+
+| # | Decision |
+|---|----------|
+| C1 | No GitHub (no mirror, no GitHub App). Bundle upload only. |
+| C2 | Cloud ledger with budget set by owner (balance minus margin against overrun). |
+| C3 | The orchestrator may put fitting work in the cloud on its own while the ledger balance is positive; at ≤ 0 it runs locally, no question asked. |
+| C4 | Opus in the cloud is fine (most work is opus). |
+| C5 | Purpose-built snapshot repos (code + the resource files a task needs) are the unit sent up; no hosted git. |
+
+<a id="facts"></a>
+## 3. Facts (measured 2026-10-06, CLI 2.1.290)
+
+| # | Fact |
+|---|------|
+| F1 | `claude --cloud "<prompt>"` in a folder with no git remote uploads a bundle (history + tracked files; untracked and `.env`-like files left out; ≤ 100 MB, else branch-only, else squashed snapshot). It prints `Created cloud session: …`, `View: https://claude.ai/code/session_…`, `Resume with: claude --teleport session_…` and **exits** — scriptable. |
+| F2 | `--cloud` needs a TTY (`-p` refused; piped stdout refused). A pty (tmux, python `pty`) works. |
+| F3 | A never-seen folder shows the trust dialog first (blocks; "Yes" = Down, Enter). Pre-accepting it (`~/.claude.json` `projects[<path>].hasTrustDialogAccepted = true`, atomic rewrite) works — verified 2026-10-06, no dialog. |
+| F4 | Cloud sessions run **Opus 5.5 at effort medium** by default (commit trailer, status line). |
+| F5 | The VM can't reach local networks behind a firewall → it can't push to local git. Custom allowlist = claude.ai environment settings (owner action). |
+| F6 | Large projects with native build tools may require environment setup; e.g., .NET SDK not in the base image → download from package repos (minutes, cached with environment setup script). |
+| F7 | `claude --teleport <sid>` for a bundle session: validates, fetches logs, then **fails silently at "Checking out branch"** (branch only in the VM) — sometimes exits, sometimes still resumes the conversation. It never applies the branch. Teleporting a *running* session does not interrupt it (local copy shows "Interrupted"; cloud kept working). |
+| F8 | After a teleport resumes, `/export <file>` writes the rendered conversation (80-col wrapped; tool output truncated `… +N lines`; **assistant text complete**) with **no model call**. The local `.jsonl` transcript only appears after a local message (= a full-context model call, avoid). |
+| F9 | So the result channel is the session's **final assistant text**: a gzip+base64 patch between markers with a sha256, whitespace-stripped on read. A patch inside tool output is truncated and useless. |
+| F10 | `claude -p "<msg>" --cloud <sid>` queues a follow-up into a running/idle session and exits (no TTY needed). |
+| F12 | The export contains the prompt too: marker parsing must take only assistant lines (`● WF-RESULT …` = bullet prefix) after the prompt, never a bare substring match. |
+| F13 | Environment configuration (owner, 2026-10-06): custom network allowlist for required services; environment variables for telemetry and timeouts; setup script for large dependencies. The exact setup depends on project needs and should be configured in claude.ai environment settings. |
+| F11 | Cloud sessions share the subscription rate limits only after the promo credit is used up; until then they draw the credit (promo terms). No CLI shows the balance: owner reads it on claude.ai. |
+
+Baseline local cost (`wf usage --report`, API-price $, 2026-10-06):
+opus worker ≈ $1.10–1.32 per done task (median $0.58–0.96, n=64), sonnet ≈ $0.14–0.30, orchestrator
+≈ $0.25 per dispatched task. Transcript output counts are estimates.
+
+## 4. Design
+
+<a id="snapshot"></a>
+### 4.1 Unit of work: snapshot repo
+
+`wf cloud send <id>` builds `<project>/out/cloud/<id>/` (git-ignored, deleted by `pull` after apply):
+- `git archive <master HEAD>` of the project → plain files; plus every path in workflow.toml
+ `cloud_include` (git-ignored inputs, e.g. large data files), force-added; one commit "base <sha>".
+- Packed size > 90 MB → refuse (`wf: snapshot <n> MB > 90 MB; trim cloud_include`).
+- Never in the snapshot: `TASKS.md`/archive/`.wf`/`.worktrees`/`out/` except `cloud_include`.
+- Pre-accept the trust dialog for that path (F3), then run `claude --cloud "<prompt>"` under a python pty with
+ a timeout (5 min); parse `session_…` from the output. No session id → exit 1 with the last 5 output lines.
+
+<a id="prompt"></a>
+### 4.2 Prompt (template `templates/cloud-prompt.md`, filled by `send`)
+
+Fixed text (≈ 25 lines) + `wf show <id>` body + the project CLAUDE.md test recipe of the areas the task names
+(`wf ctx` areas section). Rules in the template:
+- `wf` is absent: ignore wf/TASKS/lane/gate rules in CLAUDE.md; never edit TASKS.md/archive.
+- Never ask; blocked → final text `WF-AWAITING: <one-line question>` and stop.
+- Test red → implement → green; commit on `main` (snapshot repo) with one-line messages.
+- Network blocked for a needed source → say so in the report line, continue with what the repo has.
+- **Final message text** (not a tool call), exactly:
+ ```
+ WF-RESULT done|awaiting|handback
+ WF-REPORT <≤2-line archive entry>
+ WF-USAGE in=<n> cw=<n> cr=<n> out=<n> model=<id> (summed from its own transcript, script given in the template)
+ WF-PATCH-BEGIN sha256=<hex of the gzip> bytes=<n>
+ <base64 of `git format-patch --binary <base>..HEAD --stdout | gzip -9`>
+ WF-PATCH-END
+ ```
+- Built (cloud send task): the template writes each marker line in brackets (`[WF-RESULT R]`) so no prompt line
+ starts with a marker; `cloud.final_message(export)` = the F12 parser: lines of the last message starting
+ `● WF-RESULT` (assistant bullet; the prompt renders as `❯ `/` `, never `● `), bullet/indent stripped,
+ ends at the next non-indented line. `pull` parses fields from that list only. Snapshot record
+ `.wf/cloud/<id>.json`: id, sid, project, lane, master (sha for `git am`), base (snapshot commit = patch base),
+ folder, sent, model, bytes. Ledger row reserved before `claude --cloud` (sid `pending:…`), removed on failure.
+
+<a id="pull"></a>
+### 4.3 Return: `wf cloud pull <id> | --all`
+
+- Teleport under a pty in the snapshot folder, `/export <tmp>`, `/exit` (F7, F8); no model call.
+- Parse the export: last line starting `● WF-RESULT` (F12); none → `running` (exit 4, prints age since send).
+ None, but the last assistant message (no non-empty prompt after it) ends in a bare `WF-PATCH-END` line
+ (key words dropped) → handback `bad result: no WF-RESULT key`, patch kept when a `sha256=… bytes=…` header decodes.
+- `done`: join base64 between markers (strip whitespace and the 2-space indent), check sha256 and bytes,
+ gunzip → patch. Apply in the task's lane worktree on branch `<lane>/<id>` from the recorded base:
+ `git am --3way`; files outside the project's tracked tree or in `cloud_include` → refuse.
+ Then `quick_gate`; green → `wf done <id> -m "<WF-REPORT>"` + commit + `wf merge` (the `wf finish` path, run by
+ `pull` itself, so a pull costs the orchestrator one Bash call). Red / am conflict → handback (below).
+- `awaiting` → `wf add -s awaiting "<q>"` + `wf status <id> blocked <a-id>`.
+- `handback` / red gate / bad patch → `wf note <id> "cloud <sid>: <why>"`, `wf status <id> clear`, task goes back
+ to the local queue with `Recovery: cloud attempt <sid> — <why>` in its note (a local worker may take the patch
+ from `out/cloud/<id>.patch`, kept on handback).
+- Every pull that ends the session charges the ledger (4.4) and deletes the snapshot folder.
+- Archive (owner ruling 2026-10-06, built as part of cloud infrastructure): each pull that ends a session then archives it in the
+ app (`POST https://api.anthropic.com/v1/code/sessions/<sid>/archive`, the CLI's internal archiveRemoteSession,
+ CLI 2.1.291; 200/409 ok). Failure → one `archive <sid> failed: …` line, pull exit unchanged; ledger row
+ `archived: true`; `wf cloud archive <sid>|--ended` retries.
+- Built (cloud pull task): sections of the final message open at a key line, other lines continue it (export
+ wraps); the patch header + base64 are joined with all whitespace removed (`sha256=HEXbytes=N` then base64:
+ gzip base64 starts `H4sI`, never a digit). Refused paths: outside the tree / project folder, TASKS, archive,
+ `.wf`, `.worktrees`, `out`, `cloud_include`. One handback note `Recovery: cloud attempt <sid> — <why>[; patch …]`.
+ Lost = no result (or no export) and > 24 h since send. Branch `<lane>/<id>` already there / worktree_setup red →
+ exit 1, session kept (pull again). `--export FILE` parses a given export (tests, manual). Exit 0 ended, 4 running.
+
+<a id="ledger"></a>
+### 4.4 Ledger: `~/.local/state/wf/cloud.json` (same state dir and lock style as `wf res`)
+
+```
+{"budget": 240.0, "spent": 0.0, "reserve_per_task": 4.0, "max_parallel": 3,
+ "entries": [{"id", "project", "sid", "model", "sent", "state": "running|done|awaiting|handback|lost",
+ "usd": null|float, "usd_source": "self|est|owner"}]}
+```
+- balance = budget − spent − reserve_per_task × running. `send` refuses (exit 3, one line) when
+ balance < reserve_per_task or running ≥ max_parallel.
+- Charge on end: `WF-USAGE` × `usage.PRICES` (reuse `wflib/usage.py`), plus a 15 % overhead factor for
+ what the session can't see (title generation, setup); missing WF-USAGE → charge reserve_per_task,
+ `usd_source=est`.
+- `wf cloud ledger` prints budget/spent/balance/running; `--set-balance <usd from claude.ai>` sets
+ spent = budget − balance (owner reconcile, `usd_source=owner` row); `--budget N` changes the cap.
+- A running entry older than 24 h without `WF-RESULT` → `lost` (charged reserve), task back to local.
+
+<a id="fit"></a>
+### 4.5 What fits the cloud (routing)
+
+Project opt-in: workflow.toml `cloud = true` (+ `cloud_include = [...]`, optional `cloud_note = "..."` appended
+to the prompt) only enables the lane; routing is per task (owner ruling 2026-10-06, amended: opus only).
+Whoever adds a task (design session, orchestrator, worker follow-ups) classifies it: `Cloud: yes|no`
+(`wf add --cloud` / `wf set --cloud`); yes = Done checkable from the repo alone (code, synthetic data,
+`cloud_include` data), no GUI/display, live game/server/capture, LAN, Wine or local installs, `wf`/`wf res` steps,
+owner; opus only, prefer larger (1h); unsure → no. Task fits when all hold:
+- project opted in; task runner-ready (Done + Model), not `Sessions: owner|solo`, no `Cloud: no` line;
+- effort ≤ `slice_above` (no slice jobs); Model opus (sonnet/haiku never, also with `Cloud: yes`: overhead ≈ their cost, §5);
+- `Cloud: yes` → fits (regex skipped); no Cloud line → body must not name `wf ` commands as steps
+ (workflow-upgrade / bookkeeping tasks need `wf`), GUI, LAN hosts, `wf res`, or a live server — checked by a small
+ regex list in `wflib/cloud.py`.
+`wf orch pick cloud` order (`cloud.pick_key`): `Cloud: yes` first, then prio, then effort desc (1h before <1h).
+A worker that finds a task unfit for the cloud adds `Cloud: no` (`wf set <id> --cloud no`).
+
+<a id="orch"></a>
+### 4.6 Orchestrator
+
+`/wf-orchestrate` and `wf batch` gain a virtual lane `cloud`:
+- `wf orch pick cloud` = first fitting task across the project's lanes, ledger allows → `wf cloud send`, claim
+ `status progress cloud:<sid>`. Prints `stop lane cloud: <ledger|none fit|max parallel>` otherwise.
+- The orchestrator calls `wf cloud pull --all` on each wake-up (no faster than every 10 min; a teleport
+ takes ~30 s and no tokens). Results feed `wf orch post` like a worker report.
+- Local lanes skip tasks claimed `cloud:`; the cloud lane takes opus tasks only (the expensive ones locally), `Cloud: yes` first (§4.5).
+- Ledger ≤ reserve → the cloud lane stops; the local lanes run as today (C3).
+
+## 5. Economics (measured, see §6)
+
+Value of local work = its API-price $ (what the weekly limit is spent on). Per opus task:
+
+| Item | $ | Source |
+|---|---|---|
+| Local opus worker avoided | 1.20 | Historical API-price per task (range 1.10–1.32, n=64) |
+| Local overhead of a cloud task | 0.10 | send + pull = 2 orchestrator calls at ~150k cached ctx (~0.05 each); poll wakes amortised ~0.03; gate = CPU only |
+| Failure (handback → local redo) | 0.24 | assumed p_fail 20 % × 1.20 |
+| **Local $ saved** | **0.86** | |
+| Cloud credit used | 1.40 | typical replay scenario with setup overhead |
+
+Net per opus task = 0.86 − v × 1.40:
+
+| Credit value v | Net per task | $240 credit → tasks / net |
+|---|---|---|
+| 0 % (use-or-lose, time-limited) | **+0.86** | ~150 tasks / **+$129 local** |
+| 50 % | +0.16 | ~150 / +$24 |
+| 100 % (as paid usage) | −0.54 | loss: only worth it when the local limit is hit |
+
+The analysis shows that using cloud credits for opus-only tasks is economical only when the credit value is low (time-limited or not yet consumed).
+Sonnet tasks: local $0.14–0.30 vs overhead + failure ≈ $0.15 → ≈ 0 net even at v = 0 → **cloud lane takes opus
+only** (sonnet only via `Cloud: yes`). Build cost: 6 slices worth of engineering.
+
+## 6. Validation
+
+Cloud worker send/pull was validated with:
+- Manual pty driver tests against the real CLI on throwaway repos (no project content).
+- Round-trip end-to-end tests from send → export → pull with real snapshot repos.
+- Synthetic test fixtures with the same message shape as real cloud sessions (for unit tests in the tool repo).
+
+Key findings:
+- Marker parsing must handle export word wrapping (spaces, indents).
+- ANSI codes in the TUI output must be stripped before marker matching.
+- Trust dialog pre-acceptance prevents interactive blocks.
+- Export `/exit` may take multiple Enter presses to ensure the file is written.
diff --git a/docs/design.md b/docs/design.md
new file mode 100644
index 0000000..5f691d7
--- /dev/null
+++ b/docs/design.md
@@ -0,0 +1,329 @@
+# wf — design reference
+
+Shared task workflow for many coding agents (Claude Code sessions) working in parallel on the
+projects under one folder (`/projects`). This file is the reference for the TASKS.md format,
+`workflow.toml`, every `wf` command and the release rules. The memory/CPU ledger `wf res` has its
+own document: [resource-ledger.md](resource-ledger.md). Overview: [README](../README.md).
+
+## 1. Goal
+
+One workflow definition and one tool, used by every agent working in a subfolder of
+`/projects`, instead of task scripts and workflow text copied (and drifting) per project.
+Project `CLAUDE.md` files keep project specifics only.
+
+Success:
+- An agent started in any project answers "continue" with the same steps.
+- A workflow fix is made once, in one repo, under revision control.
+- Task references cannot rot silently (`wf check` fails on them).
+- Less always-loaded instruction text per project.
+- A worker that hits a workflow problem reports it in one command and keeps working; fixes
+ reach all workers without breaking one that is mid-task.
+- Routine task upkeep (list, find, add, reprioritise, flag, finish) is one short command
+ each; an agent never reads or rewrites TASKS.md whole to do it.
+
+Non-goals:
+- No runner: the task tool never builds, tests, commits or pushes.
+
+## 2. Layout
+
+```
+/projects/
+ CLAUDE.md -> workflow/shared/CLAUDE.md symlink
+ workflow/ this repo
+ shared/CLAUDE.md shared loop (section 6), loaded by every project
+ CLAUDE.md rules for sessions changing this repo
+ wf.py CLI entry
+ wf_res.py `wf res` (files, /proc, systemd); logic in wflib/res.py
+ wflib/tasks.py pure text: parse, insert, remove, archive
+ wflib/refs.py pure text: ids, links, anchors, doc sections
+ wflib/config.py find project root, load workflow.toml
+ wflib/check.py everything `wf check` reports
+ wflib/ledgers.py in-flight plan ledgers for `wf next`
+ wflib/search.py pure text: ranked search over tasks, archive, docs
+ wflib/migrate.py one-time converter from a numbered task list
+ wflib/res.py pure logic of the resource ledger
+ tests/test_*.py unittest, stdlib
+ inbox.md reports from workers, append-only, not in git (section 7)
+ templates/ workflow.toml, TASKS.md, archive.md for `wf init`
+ CHANGES.md release notes, newest first
+ <group>/<project>/ any depth ≤ 3; a project = a folder with workflow.toml
+```
+
+Loading: Claude Code reads `CLAUDE.md` from the working directory and every parent, so
+sessions in a project or in its `.worktrees/*` get the shared file without a pointer. If a
+symlink is not loaded, a real one-line file `/projects/CLAUDE.md` containing
+`@workflow/shared/CLAUDE.md` does the same. The shared file sits in `shared/` so that a session
+inside `/projects/public/workflow` does not load it twice.
+
+Each project `CLAUDE.md` starts with:
+`Workflow: /projects/CLAUDE.md (shared loop, wf). Below = project specifics; this file wins on conflict.`
+
+The folder scanned by `wf projects` is the parent of this repo, or its grandparent when the parent holds no project (`WF_ROOT` overrides); the inbox
+is `inbox.md` here (`WF_INBOX` overrides). The tasks of the workflow itself can live in a
+separate (private) wf project; nothing in this repo needs them.
+
+Requirements: Linux, Python ≥ 3.11 (stdlib only). `wf res` also needs systemd user units with
+cgroup v2.
+
+## 3. TASKS.md format
+
+```markdown
+# Tasks — <project>
+
+<free prose: commands, pointers>
+
+## Awaiting your decision
+
+- **a-smoke-screens**: needs you present, screen unlocked. ...
+
+## Pending
+
+- **t-cave-seams** [P3] (<1h): Terrain: cave entrance seams. Close the slit beside the lintel.
+ - Steps: ...
+ - Tests (red first): ...
+ - Done: ...
+ - After: [[t-terrain-cave-render]]
+ Ref: DESIGN.md#terrain
+
+## Needs human
+
+- **t-playtest-feel** [P1] (<1h): Play-test feel + tuning. ...
+ - [ ] ...
+
+## Deferred
+
+- **t-android-port** [P3] (10h): ...
+```
+
+Rules:
+- Item = a line `- **<id>**` at column 0 plus every following line that is blank or indented.
+ A flush-left non-item line ends the item list of the section (prose after items is kept).
+- Id grammar: `t-[a-z0-9-]+` for tasks (Pending, Needs human, Deferred), `a-[a-z0-9-]+` for
+ Awaiting items. A task keeps its id when it moves between sections.
+ (Changed from the chat draft, which used `h-` for Needs human: a prefix tied to a section
+ would force an id change on a move.)
+- Ids are unique across TASKS.md and the archive, and never reused.
+- Task header: `- **<id>** [P0-P3] (<effort>) [status]: Title. Goal.` (old `(<effort>, interactive)`
+ still reads as `Sessions: owner`; `check` warns.)
+ Effort is one of `<1h`, `1h`, `5h`, `10h`, `100h`. Awaiting items have no priority or effort.
+- Status, optional, fixed words: `(in progress: <branch or note>)`, `(blocked: [[a-id]])`.
+- `After: [[id]], [[id]]` lists dependencies. `Ref:` lists `path` or `path#anchor`, comma
+ separated, relative to the project root; text in parentheses after a ref is a free note.
+- `Model: haiku|sonnet|opus` (none = opus): the model for the task (docs/lanes.md §0).
+ `Sessions: parallel|solo|owner [— why]` (none = parallel; `- Sessions:` works too):
+ solo = only while no other session is live in the project (lib/submodule bump, repo-wide refactor,
+ shared runner), and while it is in progress no other session picks anything; owner = needs the owner
+ present (real display, playtest, choices): `next` picks it only with `--owner`.
+- Slices of a task over 1h: own items `t-<parent>-1`, `t-<parent>-2` (or any id given with
+ `--id`), each `After:` the previous one. The parent item stays as the summary and lists its
+ slices (`Slices:` line); it is done when its last slice is done, and `next` does not pick it
+ while a slice is open.
+- Pending holds open work only, in pick order.
+- Sections other than the four named ones are prose: the tool keeps them and checks only
+ the `[[id]]` links in them.
+
+Archive file: newest first, one line per finished task:
+`- 2026-09-29 **t-cave-seams** Terrain: cave entrance seams — <entry, ≤2 lines>`.
+Older lines without an id stay as they are.
+
+## 4. workflow.toml
+
+In the project root. Its presence marks the project root for `wf`.
+
+```toml
+format = 1 # required: TASKS format version
+tasks = "TASKS.md" # required
+archive = "tasks/archive.md" # required
+docs = ["DESIGN.md", "docs/"] # files/dirs whose links and anchors `check` validates
+verify = ["dotnet build X.slnx", "dotnet test X.slnx"] # printed by `done`, never run
+done = ["Feature changed -> docs/CATALOG.md"] # printed by `done`
+ledgers = ".superpowers/sdd" # optional: in-flight plan ledgers
+ctx_hint = 100000 # optional: tokens; 0 = off (see below)
+worktree_setup = ["mkdir -p out && ln -sfn \"$WF_MAIN/out/data\" out/data"] # optional: `wf setup` (below)
+quick_gate = ["dotnet test X.slnx --filter ContractTests"] # optional: `wf gate` (below)
+cloud = true # optional: project opts in to the cloud lane (default false)
+cloud_include = ["out/data/"] # optional: git-ignored paths force-added to the cloud snapshot
+cloud_note = "No GUI here." # optional: appended to the cloud prompt
+
+[anchors] # optional: index/spec anchor scheme
+index = "DESIGN.md" # every task Ref anchor must be a heading here
+index_section = "Subsystems"
+specs = "docs/superpowers/specs" # explicit <a id> here needs an index entry
+```
+
+Only `format`, `tasks` and `archive` are required. Unknown keys are an error (catches typos).
+
+`worktree_setup`: shell commands `wf setup` runs inside a lane worktree (cwd = the worktree's project
+folder, env `WF_MAIN` = the main tree's project folder), in order, stopping at the first non-zero exit
+(`wf: …`, exit 1). They provide git-ignored inputs a fresh worktree lacks (e.g. link `out/…` from the
+main tree) and must be idempotent: wf-worker runs it after every worktree create/reuse.
+
+`cloud` fit (cloud-lane spec §4.5): with `cloud = true` a task fits when runner-ready, not `Sessions: owner|solo`,
+no `Cloud: no` line, effort <= `slice_above`, Model opus, and its title/body does not match the unfit regex list
+in `wflib/cloud.py` (`wf` / `wf res` steps, GUI, LAN hosts, live server). Routing is per task: whoever adds it sets
+`Cloud: yes|no` (`wf add/set --cloud`); `Cloud: yes` skips the regex list only (still opus only), `Cloud: no`
+always excludes, no line = the rules above. `wf orch pick cloud` order (`cloud.pick_key`): `Cloud: yes` first,
+then prio, then larger effort. `wf check` accepts the `Cloud:` line (yes|no, one per task).
+
+`quick_gate`: fast regression commands (minutes, not the full slow gate) `wf gate` runs in the current
+tree's project folder (worktree or main; env `WF_MAIN`), stopping at the first red (`wf: …`, exit 1);
+none configured → exit 0. wf-worker runs it before `wf done`, so a break is caught by the task that made
+it, not blamed on a later task by the async slow gate. The project picks the commands.
+
+## 5. wf CLI
+
+Run as `python3 /projects/public/workflow/wf.py <cmd>` from anywhere inside a project. The
+project root is the nearest ancestor of the working directory that holds `workflow.toml`;
+`--project DIR` overrides. Output is plain text, exit 0 on success.
+
+Principle: every routine operation is one command with short output, so agents spend no
+context on reading or rewriting task files. Hand edits are for body prose only.
+
+Read (never write):
+
+| Command | Does |
+|---|---|
+| `next [--brief]` | Awaiting items, Needs-human titles, in-flight ledgers, then the next task with `ctx` of its refs. `--brief`: the task only. Skips blocked items, open `After:`, parents with open slices, tasks held by another live session, `Sessions: owner` (unless `--owner`), `Sessions: solo` while another session is live. A solo task in progress by another live session → picks nothing, exit 1. Exit 1 if nothing is pickable. |
+| `list [filters]` | One line per item: `id P2 1h status Title` (title cut to fit 100 columns). Default: Pending. Filters: `-s pending\|human\|awaiting\|deferred\|all`, `-p 0-3` (that priority or higher), `--ready` (pickable now), `--runner` (ready + Done line, not owner-bound, no open slices), `--blocked`, `--progress`, `--ref <path[#anchor]>`, `-n N`. Last line: counts per section. |
+| `show <id>...` | The raw item(s), nothing resolved. |
+| `ctx <id \| path#anchor>` | The item, its refs resolved, its dependencies and the items that depend on or link to it, then the notes of every area (`## Areas`) the item names (area name, code-map anchor or Paths entry as a whole word). For an anchor: that doc section plus the tasks that reference it. |
+| `search <words>... [filters]` | Ranked hits over tasks, archive and `docs`: one line each, `id` or `path:line heading`, plus the matching line. Filters: `--tasks`, `--archive`, `--docs`, `-n N` (default 15). |
+| `log [-n N] [words]` | Newest archive lines (default 10), optionally only those matching the words. |
+| `projects` | Every project under the folder that holds the workflow repo (`/projects`) with a `workflow.toml` (depth ≤ 3): counts per section, next task id + title, number of `check` errors. |
+| `check` | Validates; prints every problem; exit 1 on any error. |
+
+Write (each changes only the items named, and prints the changed header lines):
+
+| Command | Does |
+|---|---|
+| `add "<Title. Goal.>" -p N -e EFFORT [opts]` | New item. Id made from the title unless `--id` given or the text starts with `<id>: `; printed. Opts: `-s SECTION`, `--after ID,…`, `--ref REF,…`, `--parent ID` (slice: id `t-<parent>-N` or `--id`, `After:` the previous slice, parent's slice list updated), `--model M`, `--sessions solo|owner`, `-b` (body lines from stdin; never read unasked, a harness may hold stdin open). `add -` reads a whole item block from stdin. |
+| `done <id>... [-m "<entry>"]` | Removes the task(s), puts the dated line(s) on top of the archive, prints the `verify` and `done` lists once. `-m` is required for a single task and applies to it; several ids without `-m` use each task's goal sentence. |
+| `prio <id> <0-3>` | Sets the priority and moves the item to its place by the insert rule. |
+| `move <id> <section>` or `move <id> --before\|--after <id>` | Moves between sections, or reorders inside one (refused if it breaks priority or `After:` order, unless `--force`). |
+| `status <id> progress "<note>" \| blocked <a-id> \| clear` | Sets or clears the status words. |
+| `set <id> [--title T] [--effort E] [--after IDS] [--ref REFS] [--model M] [--sessions S] [--done TEXT]` | Changes header fields and the `After:` / `Ref:` / `Model:` / `Sessions:` / `Done:` lines (`""` removes). `--interactive` = old spelling of `--sessions owner`. `--after ""` removes the line. `--title` keeps the goal, unless it has its own (`Title. Goal.`) or ends in `?`/`!`: then it replaces the whole text. |
+| `rename <id> <new>` | Changes an open item's id (same kind, unused, not archived) and every `[[id]]` link in TASKS.md. |
+| `note <id> "<line>"` | Appends one `- <line>` to the item body. |
+| `body <id>` | Replaces the item body with stdin (header, `After:`, `Ref:` kept). |
+| `tick <id> <n \| text>` | Checks the n-th (or the matching) `- [ ]` box of a Needs-human item. |
+| `report "<what happened>" [--kind bug\|idea\|friction] [--cmd "<command>"]` | Appends one entry to the workflow `inbox.md` (section 7). Touches nothing in the project. |
+| `batch N [--lanes L,…] [--prep] [--model M] [--for 6h] [--mem 4G] [--dry-run]` · `batch --status` | Starts an unattended batch orchestrator (`claude -p`, auto mode, no prompts, prompt `templates/batch-prompt.md`) as a `wf res run` job with `CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS` = `--for`; it writes `out/wf-batch-<stamp>.md`. `--status` = this project's newest batch job + newest summary file. `--prep` = prep batch (`templates/prep-prompt.md`): sonnet workers write Done (+Model) for up to N tasks without Done, unclear → awaiting; none → nothing started. See `docs/orchestrator.md`. |
+| `init` | In a folder without `workflow.toml`: writes it, TASKS.md and the archive from the templates. |
+| `migrate [--write]` | One-time converter from the numbered format. Without `--write`: prints the diff, the id map and notes, changes nothing. `--write` also raises `format` in `workflow.toml`. |
+
+Every write command accepts `--dry-run` (prints a unified diff, writes nothing) and ends by
+comparing the full check before and after: a change that adds a problem is refused and
+nothing is written; problems that were there before do not block other edits.
+
+Details:
+
+- **Pick rule (`next`)**: first Pending item, in file order, that has no `(blocked: …)` and
+ whose `After:` ids are all in the archive. Items it skipped are listed in one line each
+ with the reason.
+- **Ref resolution (`ctx`, `next`)**: `path#anchor` → the section under the heading whose
+ slug or explicit `<a id>` equals the anchor, up to the next heading of the same or higher
+ level, cut at 80 lines with a `… (N more lines: <path>:<line>)` note. `path` alone → the
+ file's first heading and its `**Goal:**` line if it has one. With `[anchors]` set, an
+ index anchor also prints the spec section that carries the same `<a id>`.
+- **Context hint (`next`, `done`)**: with `CLAUDE_CODE_SESSION_ID` set, the last request's prompt size
+ (input + cache write + cache read) from the last 1 MB of `<CLAUDE_CONFIG_DIR|~/.claude>/projects/*/<id>.jsonl`;
+ over `ctx_hint` → last line `context ~Nk tokens (> Mk): ask the owner to /clear, then continue`. No id,
+ no transcript → nothing. Subagents share the id, so the line says it is about the main session.
+- **Ledgers (`next`)**: for each `<ledgers>/*/progress.md`: plan path from its first line,
+ done tasks, where to resume, count of `Ruling:` lines.
+- **Insert rule (`add`, `prio`, `move <section>`)**: position in the section = after the
+ last item of the same or higher priority, and after every item named in its `After:`.
+ Body is re-indented to two spaces. A block given to `add -` must carry an id and, for
+ tasks, a priority; a leading `N.` from the old format is refused with a message.
+- **Id from title (`add`)**: `t-` (`a-` for Awaiting) + slug of the title (the text up to
+ the first `. `), at most 40 characters, cut at a word; if taken (TASKS.md or archive), `-2`, `-3`, … is appended.
+- **Search ranking**: words are matched case-insensitively as whole words or word prefixes.
+ Score per hit = words matched × weight of where (id or title 5, doc heading 4, header
+ goal 3, `Ref:`/body/doc text 1); all words matched ranks before some. Ties: open tasks,
+ then docs, then archive. No index file; everything is read per call.
+- **Exact ids only** in write commands; a unique id prefix is accepted by read commands.
+ An unknown id prints the three nearest ids.
+- **`done`** refuses when the id is unknown or is a parent with open slices. On an `a-` id
+ it removes the item without an archive line (`-m` is optional there) and clears the
+ `(blocked: [[a-id]])` status of the tasks that waited on it, listing them.
+- **`check`** errors:
+ - duplicate id; id reused from the archive; bad id grammar; id prefix not allowed in its section
+ - task without priority or with an effort outside the vocabulary
+ - `[[id]]` that is neither in TASKS.md nor in the archive
+ - `(blocked: [[x]])` where `x` is not an open `a-` item
+ - `After:` cycle; Pending item placed before an open item it is `After:`
+ - `Ref:` path missing; anchor missing in that file
+ - old-format leftovers: numbered items
+ - item after a flush-left prose line inside a task section; malformed item header
+ - with `[anchors]`: task anchor without index heading; two index headings with the same
+ slug; index link to a missing file or anchor; explicit spec `<a id>` without index entry
+ - config: missing required key, unknown key, configured path that does not exist
+ In `docs` only `[[t-…]]` / `[[a-…]]` links are validated (other `[[x]]` may be the
+ project's own wiki links).
+ Warnings (exit stays 0): `#N` number refs in TASKS.md; task in progress with no branch/note; Awaiting item that no task
+ references and that is older than 30 days by `git blame`.
+- **Writes** are whole-file: read, change in memory, write to a temp file in the same
+ directory, rename. If the file changed on disk between read and write (mtime + size), the
+ command stops without writing. `done` writes the archive first, then TASKS.md; if the
+ second write fails it reports that the archive line must be removed.
+- **Errors** go to stderr as one line each, naming file and id; no tracebacks for expected
+ failures.
+
+## 6. Shared CLAUDE.md
+
+Text: `shared/CLAUDE.md` (the file is the authority). Sections: Style (concise everywhere;
+compaction summaries, specs, plans stay full and exact), Cold start, Loop, Done, TASKS.md,
+Context economy, Memory / CPU, Workflow problems.
+
+## 7. Improving the workflow without disturbing workers
+
+Problem: `wf.py` and the shared CLAUDE.md are live for every worker the moment they change.
+A worker that edits them mid-task, or a half-finished change, would hit all the others.
+
+**Report (any worker, any time)**
+- `wf report` appends to `inbox.md` with a single `O_APPEND` write, so parallel workers
+ never clash and nobody waits. Entry: date, project, kind, the text, the command if given,
+ wf version (git short hash).
+ `- 2026-09-29 proj-a friction: prio change needs two commands (cmd: wf prio …) @84decd1`
+- The worker then works around the problem and continues its own task. It does not edit
+ anything under `/projects/public/workflow` and does not wait for a fix.
+- `wf next` in any project prints one line when the inbox has entries, for the user's eyes:
+ `workflow inbox: 3 reports (triage: workflow session)`.
+
+**Triage and fix (a workflow session, started by the owner)**
+- The workflow's own tasks live in a wf project of their own (e.g. a private
+ `/projects/workflow`), whose CLAUDE.md holds the rules of this section. Cold start there:
+ each inbox entry becomes a task (`wf add`, duplicates merged) or is rejected with a line in the
+ archive; the entry is then deleted from `inbox.md`. `inbox.md` is git-ignored; tasks and
+ archive carry the record. Bookkeeping there changes nothing a worker runs.
+- Changes to `wf.py`, `wflib/`, `shared/CLAUDE.md` or templates happen in a git worktree
+ `/projects/public/workflow/.worktrees/<topic>` on a branch, never in the main tree. Workers keep
+ running the main tree's `master` meanwhile.
+- Release = fast-forward merge to `master` in the main tree after: tests green, and
+ `wf check` run with the new code against every project listed by `wf projects` gives the
+ same result as before the change (or differences that are the intended ones).
+- The user starts workflow sessions and sees the release; no worker does it on its own.
+
+**Compatibility rules for a release**
+- Commands and options are only added. Renaming or removing one keeps the old spelling
+ working for one release, printing a one-line notice with the new spelling.
+- A change to the TASKS.md format raises `format` and ships with a `wf migrate` step.
+ `wf` run in a project with an older `format` refuses write commands with
+ `TASKS format 1, wf needs 2: run wf migrate --write (idle project, one commit)`; read
+ commands keep working on the old format.
+- Shared CLAUDE.md text changes reach a worker at its next session start; a rule change
+ must therefore not make files written under the old rule invalid.
+- Every release adds a line to `CHANGES.md` (newest first): date, what changed, what a
+ project has to do, if anything.
+
+## 8. Testing
+
+- `wflib` functions are pure (text in, text out); unit tests use literal before/after texts
+ written by hand, never output of the code under test.
+- CLI tests run `wf.py` with `--project <tempdir>` on fixture projects: pick rule, skip
+ reasons, insert position with priority and `After:`, `done` refusals, every `check` error
+ once, write-conflict stop, `list` filters, `prio`/`move` placement and refusals, `status`,
+ `set`, `note`, `body`, `tick`, search ranking order on a fixed corpus, `report` from two
+ processes at once (both entries whole), `format` gate, migrate on a fixture with the known oddities (stray `N.`,
+ `N/M` titles, `After:` by title, prose after items).
+- Each new test is seen failing first, and again with the tested line broken by hand.
diff --git a/docs/lanes.md b/docs/lanes.md
new file mode 100644
index 0000000..2fc6833
--- /dev/null
+++ b/docs/lanes.md
@@ -0,0 +1,115 @@
+# Lanes, Model line, slice jobs, area maps
+
+Reference (released 2026-10-05). Lanes split work by task **size** and **pick policy**: a fast lane clears
+small, unblocking work and slices big tasks; a slow lane takes 1h slices; more lanes by config. The model is
+a per-task attribute (`Model:` line): interactive sessions take tasks with Model ≤ their own, orchestrated
+workers run on the task's Model. Area maps (code map + test recipe per area) are checked and refreshed by
+auto-added tasks.
+
+## 0. The Model line
+- Task body line ` Model: haiku|sonnet|opus` (2-space indent like `Ref:`; `- Model:` also parsed), in the
+ body tail before `After:` / `Ref:`. No line → `opus`. Order haiku < sonnet < opus.
+- Set by the task's writer at `wf add --model M`; `wf set ID --model M`; `--model ""` removes it (→ opus).
+ - `haiku`: mechanical edit, exact instructions, no judgement.
+ - `sonnet`: exact Done criteria + an existing pattern to copy + a real-data pass/fail test.
+ - `opus`: design forks, debugging, reverse engineering / oracles, cross-source analysis, anything unclear.
+- Wrong level found mid-task: `wf set ID --model <higher>` + `wf note ID "<why>"` + `wf status ID clear`.
+- `wf check`: unknown value or two Model lines → error; extra words after the value → warning; missing → fine.
+
+
+## 1. Lane config
+`workflow.toml`, optional; absent → built-in default equal to:
+```toml
+slice_above = "1h" # effort above this → slice job (§3); one of EFFORTS
+
+[lanes.fast]
+efforts = ["<1h"] # efforts this lane implements
+order = "unblock" # pick policy (§2)
+slices = true # this lane also takes slice jobs (exactly one lane may set it)
+
+[lanes.slow]
+efforts = ["1h"]
+order = "priority"
+fallback = "fast" # nothing pickable here → pick from that lane
+```
+- Lane name: `[a-z][a-z0-9-]*`; used in worktree `.worktrees/<lane>`, branch `<lane>/<id>`, registry file.
+- `efforts`: subset of `EFFORTS`, ≤ `slice_above`; lanes' efforts are disjoint; every effort ≤ `slice_above`
+ belongs to some lane. Tasks without effort → the lane holding `<1h`.
+- `order`: `unblock` | `priority`. `fallback`: another lane name, optional, no cycles.
+- `slices = true` on exactly one lane; default: the lane holding `<1h`.
+- Validation errors in `config.load` (unknown keys are errors). Defining `[lanes.*]` replaces the
+ whole default set (no merge).
+- Extra lanes: add tables (e.g. `[lanes.long] efforts = ["5h"]` with `slice_above = "5h"`). Several workers in
+ one lane = existing `<lane>-2`, `-3` worktrees.
+
+## 2. Pick order
+Pickable set (Pending, not blocked, no open slices, all `After:` archived, no live foreign claim), filtered
+to the lane's efforts (plus slice jobs on the `slices` lane) and to Model ≤ session model (interactive only).
+Effective priority = best (lowest P) of the task's own and that of every open task waiting on it,
+transitively ("waits on" = `After: [[x]]`, or a parent waiting on its open slices). Keys:
+- `unblock`: (1) waited-on count > 0 first, (2) more open tasks waiting on it (transitively) first,
+ (3) effective priority, (4) file order.
+- `priority`: (1) effective priority, (2) waited on by another lane first, (3) waited-on count,
+ (4) file order (section order, then position).
+
+## 3. Slice jobs
+- A Pending task with effort > `slice_above` and no open slices = slice job. It is not pickable as an
+ implementation task in any lane; it is pickable in the `slices` lane, ranked by that lane's order.
+- Shown with `slice:` prefix in `wf next`, `wf list --runner`, `wf lanes` counts (`2 pickable (1 slice)`).
+- `wf next` prints for it: `Slice job: wf add "<title>" -e <1h|1h> --parent <id> --model M` per slice, each
+ with Steps/Done/Ref; no code; `wf note <id> "sliced into …"`; `wf status <id> clear`. Not `wf done` (parent
+ stays open until its slices are done).
+- Model of a slice job = the task's Model (default opus; slicing is judgement).
+- A task with open slices is never a slice job (already sliced). All slices done → parent pickable
+ only if its effort ≤ `slice_above`; otherwise `wf done` of the last slice prints `parent <id>: all slices done
+ → wf done <id> or add slices` (it is not offered as a slice job again while it has done slices: rule = no
+ open slices AND no archived slices).
+
+## 4. Sessions, registry, model
+- `wf next [--lane L] [--as M]`: `--as` = the session's model (haiku|sonnet|opus); none → haiku, with a
+ hint line. `--lane` = lane name; no `--lane` → all lanes merged, `priority` order, slice jobs included.
+- Registry `.wf/sessions/<lane>.json` (`{"lane", "model", "socket", "pid", "session", "at"}`, from env
+ `CLAUDE_CODE_MESSAGING_SOCKET`, `CLAUDE_PID`, `CLAUDE_CODE_SESSION_ID`; skipped without them); no `--lane`
+ → `all.json` (`all` is reserved). `.wf/.gitignore` = `*`. Newest wins per lane unless another live session
+ holds it: then kept, and `wf next` first prints `another live <lane> session holds this lane: uds:…`.
+ Alive = pid and socket file exist. `wf lanes --unregister` drops this session's records.
+- Claims: `wf status ID progress …` writes `.wf/claims/ID.json` (`{"id", "lane", "model", "socket", "pid",
+ "session", "at"}`); `status clear|blocked` and `wf done` remove it. `wf next` skips a task whose claim is
+ alive, not mine and still `in progress`.
+- `wf lanes [--lane L]`: one line per configured lane:
+ `fast: 3 pickable (1 slice) · 1 waiting · session uds:… (alive)` / `… · no session`.
+- Waiting block: `t-y waits on t-x (slow lane) → message uds:…: "t-x blocks my t-y, please take it"` /
+ `… → no slow session: tell the owner` — lane of a task = lane by effort (slice lane for slice jobs).
+- `wf done`: tasks of other lanes that became pickable → `notify <lane> uds:…: now pickable t-y` /
+ `<lane> work now pickable: t-y (no session: tell the owner)`.
+- Messages go with the agents' SendMessage tool, `to` = `uds:<socket>`.
+- `wf list`: model and lane columns; filters `--model M`, `--lane L`; `--runner --lane L` = ids in pick order
+ for that lane (no session model filter; the orchestrator spawns per task model).
+
+## 5. Orchestrator, batch, worker
+- `docs/orchestrator.md`, `shared/skills/wf-orchestrate`: lanes from `wf lanes`; per lane
+ `wf list --runner --lane <lane>` → first id; spawn `wf-worker` with `model: <task's Model>`; worktree
+ `.worktrees/<lane>`, branch `<lane>/<id>`. At most one worker per lane worktree; lanes in parallel.
+- `wf batch N [--lanes fast,slow]` (default: all configured lanes).
+- `shared/agents/wf-worker.md`: prompt line `Lane: <lane> Model: <model>`; slice job → slice only, no code.
+- `wf usage --log … --lane L`: cost line per worker; `--report` rows by lane × model × effort.
+
+## 6. Area maps
+- Notes file: config `areas = "<path>"`, default project `CLAUDE.md`. Section `## Areas`, one `### <area>` each
+ with lines (all optional):
+ - `- Code map: …` — anchors = backticked tokens (`` `parse_item` ``).
+ - `- Test recipe: …`
+ - `- Paths: <globs, space-separated>` — default: whole repo.
+ - `- Checked: <short sha>` — last refresh; set by `wf areas --mark`.
+- `wf areas [AREA]`: per area: missing anchors (`git grep -F -q <token> -- <paths>` fails), commits touching
+ Paths since Checked (`git rev-list --count <sha>..HEAD -- <paths>`), stale yes/no. Exit 0.
+- `wf areas --mark AREA`: write `Checked: <HEAD short sha>` into the section (add the line if missing).
+- Stale = any missing anchor, or Checked set and commits ≥ `area_stale_commits` (config, default 20).
+ No Checked → only the anchor test.
+- `wf check`: missing anchor → warning `area <a>: anchor <tok> not found`. Unknown Checked sha → warning.
+- `wf done`: after bookkeeping, for each stale area with no open task whose id is `t-map-<slug>` → add
+ `t-map-<slug>` P2 `<1h` Model: sonnet, title `Refresh <area> area map`, Steps: `wf areas <area>`; fix missing
+ anchors and test recipe from changed files (`git log --stat <Checked>..HEAD -- <paths>`); `wf areas --mark
+ <area>`. Done: `wf areas <area>` shows not stale. Printed `added t-map-<slug> (area map stale)`.
+ Id taken by an archived task → `t-map-<slug>-2`, … (ids never reused).
+- No `## Areas` section → feature silent. Template `templates/CLAUDE.md` area block has `Paths:`/`Checked:`.
diff --git a/docs/manual.md b/docs/manual.md
new file mode 100644
index 0000000..2b4594b
--- /dev/null
+++ b/docs/manual.md
@@ -0,0 +1,239 @@
+# wf — owner's manual
+
+How to set up and run `wf` day to day. The README tells you what it is and why it exists. The
+reference docs give the details: [design.md](design.md) (format, config, every command),
+[orchestrator.md](orchestrator.md), [lanes.md](lanes.md), [multi-session.md](multi-session.md),
+[resource-ledger.md](resource-ledger.md). Every command below also has `wf <command> -h`.
+
+This is still a personal setup. The manual describes how it runs on the author's machine. It is
+not a supported product.
+
+## 1. Setup
+
+### Requirements
+- Linux with systemd. `wf res` needs systemd user units and cgroup v2. Everything else only needs a shell.
+- Python ≥ 3.11 (standard library only, nothing to `pip install`).
+- git, and [Claude Code](https://claude.com/claude-code) (`claude` on `PATH`).
+- Optional: the `superpowers` Claude Code plugin. The shared rules refer to its brainstorming,
+ debugging and plan-writing skills. Without it, agents just follow the plain rules.
+
+### One root folder
+All projects live under one folder. The shipped rules, skills and agent file use the absolute
+path `/projects` (for example `python3 /projects/public/workflow/wf.py`). Keep that path: create it once
+and own it, or point it at a folder in your home directory.
+
+```sh
+sudo mkdir /projects && sudo chown "$USER": /projects # or: sudo ln -s ~/projects /projects
+git clone <this repo> /projects/public/workflow
+ln -s workflow/shared/CLAUDE.md /projects/CLAUDE.md # every agent under /projects loads it
+echo "alias wf='python3 /projects/public/workflow/wf.py'" >> ~/.bashrc
+```
+
+Claude Code reads `CLAUDE.md` from the working directory and from every parent folder, so every
+session in `/projects/<anything>` gets the shared rules. If the symlink is not picked up, use a
+one-line `/projects/CLAUDE.md` with the content `@workflow/shared/CLAUDE.md` instead.
+`wf projects` scans the parent folder of the tool repo, or its grandparent when the parent holds no project (tool under e.g. `public/`; override: `WF_ROOT`). Workflow reports
+go to `/projects/public/workflow/inbox.md` (override: `WF_INBOX`).
+
+### Agent and skills
+The orchestrator spawns the worker subagent. The skills are the session modes in section 2.
+Install them as symlinks, so that `git pull` updates them:
+
+```sh
+mkdir -p ~/.claude/agents ~/.claude/skills
+ln -s /projects/public/workflow/shared/agents/wf-worker.md ~/.claude/agents/wf-worker.md
+for s in wf-orchestrate wf-pilot wf-design; do ln -s /projects/public/workflow/shared/skills/$s ~/.claude/skills/; done
+```
+
+Agents and skills appear only in sessions started after the install, so restart any running session.
+
+### A project
+```sh
+cd /projects/games/mygame # any depth ≤ 3 under /projects; a git repo is recommended
+wf init # writes workflow.toml, TASKS.md, tasks/archive.md, CLAUDE.md (keeps existing files)
+wf check # 0 errors
+```
+If the project already has a numbered task list, `wf migrate` shows the conversion (dry run) and
+`wf migrate --write` applies it.
+
+Edit the generated `CLAUDE.md`: layout, verify command, gotchas and one `### <area>` block per
+code area (code map with grep anchors, test recipe, paths). Agents read these area notes before
+they read code.
+
+`workflow.toml` (the template lists every key with a comment; unknown keys are errors):
+
+| Key | What it does |
+|---|---|
+| `tasks`, `archive` | file names (defaults `TASKS.md`, `tasks/archive.md`) |
+| `docs` | files/folders whose `[[id]]` links `wf check` validates |
+| `verify` | commands `wf done` reminds of (it never runs them) and `wf ctx` prints as Verify |
+| `done` | extra checklist lines `wf done` prints |
+| `quick_gate` | fast regression check; `wf gate` and `wf finish` run it and refuse when it is red |
+| `worktree_setup` | idempotent commands `wf setup` runs in a fresh lane worktree (link git-ignored data, venv…; env `WF_MAIN` = main tree) |
+| `slice_above`, `[lanes.*]` | task sizes per lane and when a task must be sliced (default lanes: fast `<1h`, slow `1h`); see lanes.md |
+| `areas`, `code_root`, `area_stale_commits`, `area_ignore` | where the area notes live and when they count as stale |
+| `ledgers`, `ctx_hint`, `[anchors]` | in-flight plan ledgers, `/clear` hint, required Ref anchors |
+| `cloud`, `cloud_include`, `cloud_note` | cloud lane opt-in (below) |
+
+Add `out/` to the project's `.gitignore`: logs, batch summaries and scratch go there.
+
+### Resource ledger (`wf res`)
+Optional, but needed once several agents build or test at the same time.
+
+```sh
+wf res shell-init # prints an alias that starts claude inside agents.slice: add it to ~/.bashrc
+wf res timer on # 1-minute timer: starts queued jobs, ends game mode, cleans scratch, adopts sessions
+wf res status # budget, reservations, warnings
+```
+Reserves and limits go in `~/.config/wf/resources.toml` (all keys optional: `user_reserve_gb`,
+`user_reserve_cpus`, `game_reserve_gb`, `game_reserve_cpus`, `game_hours`, `small_headroom_gb`,
+`scratch_hours`; see resource-ledger.md §3.3). Optional hook that hides the display from agent
+commands while game mode is on, in `~/.claude/settings.json`:
+
+```json
+"hooks": {"PreToolUse": [{"matcher": "Bash", "hooks": [{"type": "command",
+ "command": "python3 /projects/public/workflow/wf_res.py hook"}]}]}
+```
+
+`wf batch` (and `/wf-pilot`) need one allow rule in `~/.claude/settings.json`, because auto mode
+does not let an agent start another unattended agent: `Bash(python3 /projects/public/workflow/wf.py batch:*)`.
+
+### Cloud lane (optional)
+Tasks can also run as `claude --cloud` sessions, which are billed to your cloud budget, not your
+machine. You need a Claude Code version with `--cloud` / `--teleport` and a claude.ai login.
+Per project: `cloud = true` in `workflow.toml`, plus `cloud_include` for git-ignored inputs the
+task needs. Set the spending cap once with `wf cloud ledger --budget 100` and check it with
+`wf cloud ledger`. Only opus tasks marked `Cloud: yes` (or passing the fit rules) are sent. See
+orchestrator.md "Cloud lane".
+
+## 2. Daily use
+
+There are four kinds of session. Start each one in the project folder with `claude` (the
+`agents.slice` alias if you set it up).
+
+### Worker session: "continue"
+Type `continue` (or "next task"). The agent runs `wf next --as <its model>`. It shows open
+questions, items that need you, plans in flight and the next task, then works on that task:
+test red → implement → green → `wf done` → one commit. It does not ask before it starts.
+Several sessions in one project: give each a lane ("continue in the slow lane"). `wf next` then
+switches them to worktree mode (one branch per task, `wf finish` merges it). One session per lane.
+
+### Orchestrator: `/wf-orchestrate`
+After `/clear`, type `/wf-orchestrate` (`go` = start the proposed run without asking,
+`batch N` = prepare an unattended batch). The orchestrator holds no lane and never reads code.
+It spawns one fresh `wf-worker` per task (`wf orch pick` / `wf orch post`), one per lane in
+parallel, on the task's model, and stops a lane on the first awaiting, handback or red
+post-check. Use it when the backlog is runner-ready (section 3) and you want to steer.
+
+### Overnight: `/wf-pilot`
+`/wf-pilot [--batch 4] [--lanes a,b] [--for 8h]` in an idle session, which can be one you opened
+from the phone. The pilot starts headless `wf batch` runs one after another until the deadline.
+The session stays small, and you get push notifications on new questions, on stops and at the
+end. Steering: `wf batch --stop` (graceful, running workers finish), `wf move <id> deferred`
+(skip a task), `wf batch --status` (newest batch and its summary). A one-off short run without a
+pilot is `wf batch N [--lanes …]`. `wf batch N --prep` makes tasks without a Done line
+runner-ready instead.
+
+### Design: `/wf-design`
+`/wf-design [topic | id]` is for brainstorming, specs and rulings with you. Flow: brainstorm →
+spec (committed) → your approval → `wf add` slices ≤ 1h with Done, Model and Ref → the
+orchestrator picks them up. It answers open questions with you and records each ruling where it
+binds.
+
+### Your side
+- **Awaiting** (`wf list -s awaiting`): questions agents could not decide. Answer them in a
+ design or orchestrator session. The session records the ruling and runs `wf done <a-id>`,
+ which unblocks the waiting tasks.
+- **Needs human** (`wf list -s human`): things only you can do (play-test, hardware, accounts).
+ Check boxes with `wf tick <id> <n>`. Done → tell any session ("t-x is done") or run `wf done`.
+ Tasks marked `Sessions: owner` are picked only when you say you are present (`wf next --owner`).
+- **Stop file**: `wf batch --stop` writes `out/wf-batch.stop`. Batches, pilots and
+ `wf lanes --wait` stop before the next spawn.
+- **Game mode**: `wf res game on [--for 4h]` before you play. Agents keep running but get less
+ CPU and IO, and big jobs that do not fit wait. `wf res game off` when you are done.
+- **Overview**: `wf projects` (all projects), `wf lanes`, `wf log -n 10`, `wf res status`.
+- **Workflow inbox**: agents file problems with `wf report`. Triage `/projects/public/workflow/inbox.md`
+ now and then: fix the tool, or turn entries into tasks.
+
+## 3. Writing good tasks
+
+```sh
+wf add "Cave seams. Close the slit beside the lintel." -p 1 -e '<1h' --model sonnet \
+ --done "tests/test_cave.py::test_seam passes; render of fixture shows no gap" --ref DESIGN.md#terrain
+```
+
+- **Title. Goal.** One line: the title, then the outcome in one sentence.
+- **Done line** (`--done`): a check a worker can run without you, such as a file, a test result
+ or an artifact. Never "owner confirms". Only tasks with a Done line are runner-ready: the
+ orchestrator, batch and pilot skip the others. (`wf batch --prep` drafts them.)
+- **Model line** (`--model`): the cheapest model that fits. haiku = mechanical, exact steps.
+ sonnet = exact Done, an existing pattern to copy, and a real-data pass/fail. opus (the default
+ when the line is missing) = design, debugging, reverse engineering, anything unclear. A worker
+ that finds the task too hard raises it itself and hands it back.
+- **Effort** (`-e`): `<1h`, `1h`, `5h`, `10h`, `100h`. Anything above `slice_above` (1h) becomes a
+ slice job: an agent splits it into `--parent` slices, each with its own Done/Model, and never
+ implements it whole. Slice big work yourself when you already know the steps.
+- **Code anchors**: when you know them, add body lines like
+ `Code: src/terrain.py build_cave; test to copy: tests/test_cave.py::test_floor`. Write anchors
+ (function or string names), not line numbers. They save the worker most of its exploring.
+- **Refs and dependencies**: `--ref doc.md#heading` makes `wf ctx` print that section;
+ `--after t-other` keeps it unpicked until `t-other` is done.
+- **Areas**: keep each area's code map and test recipe in the project `CLAUDE.md`. `wf ctx`
+ prints the areas a task names. When code drifts, `wf done` adds a `t-map-…` refresh task
+ automatically.
+- **Cloud** (cloud projects): `--cloud yes` only when the Done line can be checked from the repo
+ alone (no GUI, live server, LAN, local installs). When unsure, `no`.
+- **Sessions**: `--sessions solo` for repo-wide changes (nothing else runs meanwhile),
+ `--sessions owner` when you must be there.
+
+## 4. Best practices and pitfalls
+
+- **One lane per session**, at most one worker per lane worktree. Parallel lanes are safe
+ (project lock, branch per task). Two sessions in one lane pick against each other.
+- **Fresh context per task.** Cost is mostly cache reads, which grow with context. Short-lived
+ workers on the cheapest fitting model are cheaper than one long session. `/clear` the
+ orchestrator after a big design talk, but only when no worker is running (a `/clear` kills its
+ running command).
+- **Big jobs through `wf res`**: anything over 2 GB, 4 cores or 10 minutes goes through
+ `wf res run --mem … --for … --title … -- cmd`. Exit 3 = busy: do other work and retry later,
+ or use `--queue`. `wf res hist` suggests sizes from past runs.
+- **Never hand-edit TASKS.md** while agents run. Use `wf add/set/note/move/done`. If you must
+ edit by hand, run `wf check` afterwards.
+- **Commit ≠ merge ≠ push.** Agents commit their own task. Workers ff-merge only their own task
+ branch. Pushing goes only to a remote named `home` (`git push home --all`), and nothing else is
+ pushed unless you ask.
+- **Close the loop on problems.** Agents work around a workflow problem and `wf report` it.
+ Triaging the inbox is how the rules get better. Put project-only needs in the project's
+ `CLAUDE.md` or `workflow.toml`, not in the shared rules.
+- **Watch cost**: `wf usage` (this session per agent), `wf usage --report` (per lane, model and
+ effort across every project's `out/wf-cost.log`). Expensive lanes usually mean vague Done lines
+ or a model that is too large.
+- **Model choice**: run the orchestrator and design sessions on opus. Tasks get the cheapest
+ model that fits. A sonnet task without an exact Done line comes back as a handback.
+- **Updating the tool**: `git -C /projects/public/workflow pull`. Read the top of `CHANGES.md`: each line
+ ends with "Projects: …", which says what a project must do (usually nothing).
+- **Publishing a public copy**: keep the tool repo private (its history names your projects) and
+ share a fresh-history snapshot instead. Put your private words (project names, home paths), one
+ per line, in `publish-denylist.local` in the tool repo (git-ignored), then run
+ `python3 scripts/publish_snapshot.py [DEST]` (default `~/src/wf-public`). It copies master's
+ tree without `inbox.md`, `.worktrees/`, `__pycache__/`, `out/`, refuses if any denylist word is
+ in a path or file, and commits once per run (first: "initial public snapshot"; later: the new
+ `CHANGES.md` lines). It never pushes and never adds a remote: review DEST, then add the public
+ remote and `git push` there yourself.
+
+## 5. Troubleshooting
+
+| Symptom | Cause / fix |
+|---|---|
+| `busy: …` and exit 3 from `wf res run/note` | Not enough memory or CPUs beyond your reserve. Retry after the printed time, add `--queue`, or check `wf res status` for stale entries (`wf res release r-N`). If memory really is free (unused reservations), `--force` fits against real free memory. |
+| `wf orch post` says post-check red | The worker said done, but the archive line is missing, the branch is still there, the worktree is dirty, or `wf check` has errors. The message names the problem. Fix it in the worktree (`wf merge`, commit or clean), then post again. |
+| Task stuck `in progress` though no one works on it | A session or worker died. `wf list --stale` shows them, and `wf status --clear-stale` clears every claim without a live session (or `wf status <id> clear`). Re-run the worker with `wf orch pick <lane> --id <id> --recovery "<why>"` to keep its WIP. |
+| `wf start` refuses: worktree has uncommitted changes | A dead worker left WIP. `--recovery` keeps it and shows it. Otherwise commit or reset it in that worktree. |
+| `branch <lane>/<id> exists (local WIP?)` (cloud pull) | A local branch for that task already exists. Merge or delete it, then `wf cloud pull <id>` again. The cloud session is kept. |
+| `wf cloud pull` exit 4 | The session is still running. Pull again later; after 24 h without a result it counts as lost. |
+| `wf cloud pull` handback / bad patch | The task goes back to its local lane with a `Recovery: cloud attempt …` note. The patch is kept in `out/cloud/<id>.patch`. |
+| `wf merge`: rebase conflict | The branch is untouched. `git rebase master`, resolve, run verify, `wf merge` again. |
+| `wf finish` refused | The quick gate is red, files outside the given paths are uncommitted, or a path is outside the repo, missing or unchanged. Fix it and rerun. If it fails after `done:`, a rerun resumes. |
+| A worker or batch does nothing | `wf list --runner` is empty: tasks lack a Done line (`wf batch --prep`), are blocked on an awaiting item, or wait on `After:`. `wf lanes` shows counts per lane. |
+| `wf check` errors after a hand edit | Read the message (duplicate id, broken `[[link]]`, bad section) and fix the item. Ids are never reused. |
+| `project format N is newer than this wf` | Update the tool: `git -C /projects/public/workflow pull`. |
diff --git a/docs/multi-session.md b/docs/multi-session.md
new file mode 100644
index 0000000..628aad9
--- /dev/null
+++ b/docs/multi-session.md
@@ -0,0 +1,46 @@
+# Multi-session: several sessions in one project
+
+Approved 2026-10-04. Problem: one work tree + `master` shared by sessions → a commit sweeps the
+other's edits (`git add -A`), half-done code breaks the other's tests, two sessions of a lane pick
+the same task.
+
+## 1. Claims
+- `wf status ID progress …` writes `.wf/claims/ID.json` (who: socket, pid, lane, model); details in
+ `lanes.md` §4. `wf next` skips a task claimed by another live session.
+- A second live session of a lane: `wf next` warns on its first line; the registry keeps the first one.
+
+- Task line `Sessions: solo` → picked only while no other session is live; while it is in progress
+ (claimed) every other session's `wf next` picks nothing (exit 1, names the holder); its `wf done`
+ prints `notify <lane> uds:…: solo <id> done, run wf next` for each other live session.
+ `Sessions: owner` → picked only by `wf next --owner` (owner present).
+
+## 2. Worktree mode (only while > 1 session is live, git projects)
+- `wf next` prints `===== Multi-session =====`: from the main tree
+ `git worktree add .worktrees/<lane> -b <lane>/<task> master` (or `cd .worktrees/<lane> && git switch -c <lane>/<task> master`
+ when it exists), `&& wf setup` appended when workflow.toml has `worktree_setup`; inside a worktree a
+ reminder of where you are.
+- `wf setup` (in a worktree) runs `worktree_setup` (cwd = worktree, env `WF_MAIN` = main tree): links /
+ copies git-ignored inputs (`out/…`). Idempotent; run after every worktree create/reuse.
+- wf run inside a linked git worktree uses the main tree's project (same relative folder, when it
+ has `workflow.toml`): TASKS.md, archive, docs, `.wf/` are the main tree's. Bookkeeping never
+ conflicts; branches never touch TASKS.md. `wf check` there checks the main tree.
+- One session alone: as before, on `master`.
+
+## 3. Done in worktree mode
+`wf done` run inside a linked worktree prints, after verify/checklist: commit code (explicit paths), then
+`wf merge`. `wf merge` (under the project lock, `.wf/lock`): worktree must be clean → `git rebase master`
+(conflict → abort, branch untouched: rebase by hand, resolve, verify, `wf merge` again) → `merge --ff-only`
+into the main tree → commit TASKS.md + archive there (`<id> done`, id from the branch `<lane>/<id>`) →
+detach the worktree, delete the branch → push home if the remote exists (`--no-push` skips).
+The ff-merge of one's own task branch is a standing owner permission (shared CLAUDE.md); never other branches.
+`wf finish <id> -m ENTRY [--commit MSG PATH…]` does it in one call (workers): refuses first if files outside
+PATHS are uncommitted or quick_gate is red (task stays open) → `wf done` → commit PATHS → `wf merge`. In a main tree
+(no worktree) TASKS.md + archive join that commit and nothing is merged. Verify runs before it (`wf ctx` prints it).
+Next task: new branch from master in the same worktree.
+
+## 3a. Project lock
+Every wf write command and `wf merge` run under an exclusive flock on `.wf/lock` (main tree), so parallel
+sessions/workers never lose a TASKS.md edit or race a merge-back.
+
+## 4. Commits
+Shared rule: stage explicit paths, never `git add -A` / `git commit -a`.
diff --git a/docs/orchestrator.md b/docs/orchestrator.md
new file mode 100644
index 0000000..845c527
--- /dev/null
+++ b/docs/orchestrator.md
@@ -0,0 +1,152 @@
+# Orchestrator: one session talks to the owner, fresh workers do the tasks
+
+Why: cost is dominated by cache reads, which grow with context size. A fresh worker per task costs about
+as much less as a cheaper model does; researching in one context and implementing in another is NOT cheaper
+(rediscovery, handbacks). So: the orchestrator slices and rules, and one fresh worker per task researches
+and implements.
+
+## Sessions
+- **Orchestrator** (interactive, opus, `claude --autocompact 120k`): owner talk about runs, Awaiting rulings,
+ slicing, spawning workers, post-checks, unattended batches. Reads `wf` output and worker reports, never
+ code. Holds no lane: never `wf next --as` (use `wf lanes`, `wf list --runner --lane <lane>`).
+- **Design session** (optional, big window): brainstorming, specs, hard rulings. Ends each topic with a
+ committed spec/task; the orchestrator picks it up from TASKS.md.
+- **Workers**: the `wf-worker` subagent (`shared/agents/wf-worker.md`; install:
+ `ln -s /projects/public/workflow/shared/agents/wf-worker.md ~/.claude/agents/wf-worker.md`), one task each.
+
+## Starting a session
+Skills (install once: `ln -s /projects/public/workflow/shared/skills/wf-orchestrate ~/.claude/skills/` and the same for
+`wf-design` and `wf-pilot`): after `/clear`, type `/wf-orchestrate [go | batch N]`, `/wf-pilot [--batch 4] [--lanes a,b] [--for 8h]` or `/wf-design [topic | id]` (not
+"continue": that is the normal worker cold start). Agents and skills installed while a session runs appear
+only in sessions started afterwards: restart it.
+
+## Runner-ready tasks (slicing rule)
+- `Model:` line set to the cheapest lane that fits (sonnet: exact Done + a pattern to copy; haiku: mechanical).
+- Done checkable headless: a file, test result or artifact, plus `wf add` for follow-ups. Never "report to
+ owner" / "owner confirms".
+- `After:` satisfied, no `Sessions: owner`. `Sessions: solo` → run it alone.
+
+## One worker
+Two commands do steps 1-3 and 5-7 (one call each): `wf orch pick <lane> [--id ID] [--recovery WHY]` (stop file,
+pick, claim, free worktree, recorded in `.wf/orch/<id>.json`, prints the spawn line and the prompt below) and
+`wf orch post <id> <lane> --result "<report result line>" [--commit SHA] [--agent A] [--duration S] [--no-pick]`
+(post-check, merge if needed, leftover commit, orch + cost log lines, then the next pick or `stop lane <lane>: <why>`).
+The steps they automate:
+
+1. Pick: `wf list --runner --lane <lane>` → first id (lanes: `wf lanes`). Claim: `wf status <id> progress "worker"`. No commit of
+ its own: `wf merge` or any later bookkeeping commit takes TASKS.md (and the claim) along; harmless.
+2. Worktree: `<main>/.worktrees/<lane>`; if a live session or another worker uses it, `<lane>-2`, `-3`, …
+ (live interactive sessions: `~/.claude/sessions/*.json` field `cwd`). Branch `<lane>/<id>`.
+3. Spawn: Agent tool, `subagent_type: wf-worker`, `model: <task Model, none = opus>`, background, no worktree isolation
+ (it blocks `wf merge` on the main checkout). Prompt:
+
+ ```
+ Task: <id> Lane: <lane> Model: <model>
+ Main tree: <main> Worktree: <path> Branch: <lane>/<id>
+ Final message: the 4 report lines only.
+ ```
+ (the last line is needed: the agent rule alone did not stop prose before the report.)
+ Slice job (row starts `slice:` / `wf show` effort > slice_above): same spawn; the worker only slices.
+ (no `wf ctx` output in the prompt: the worker fetches it; the prompt stays in your context forever.)
+4. At most one worker per lane at a time; lanes in parallel. `wf` writes and `wf merge` take the project
+ lock (`.wf/lock`), so parallel workers are safe.
+5. On completion read only the 4-line report (`followups` = ids the worker added). Post-check:
+ - `done`: `wf log -n 3` shows the id; `git -C <main> branch --list <lane>/<id>` empty; `wf check` 0 errors;
+ `git -C <path> status --porcelain` empty; `git -C <path> merge-base --is-ancestor HEAD <master>` exit 0
+ (worktree HEAD in master: no impl commit left on a detached HEAD; else `cd <path> && wf merge`).
+ - Everything else: commit leftover bookkeeping
+ `git -C <main> commit -m "<id> <outcome> (orchestrator)" -- TASKS.md <archive>` if they changed.
+6. Outcome:
+ | Report / state | Action |
+ |---|---|
+ | done + post-check ok | next task in the lane |
+ | model raised by the worker | continue; next pick spawns it on the new model |
+ | sliced | next task in the lane |
+ | done+gate-red <culprit> <fix-id> (async gate red, earlier task's break) | post-check as done; next pick in the lane = the P0 fix (no Done/Model → add them first); never stop the lane |
+ | awaiting / handback / post-check red / no report | stop the lane, tell the owner |
+ | wip (after a wrap-up) | stop the lane; reslice or rerun later |
+ | crashed / killed (no report) | one fresh worker, prompt plus `Recovery: <why>` (it inspects git status, git log master..HEAD, `git diff -- TASKS.md` — often just the claim — and `wf show <id>`), then stop |
+7. Log one line per task to `out/wf-orch.log` (git-ignored): time, lane, model, id, outcome, commit, duration.
+ Cost: `wf usage --agent <agent id> --log <id> <outcome> --effort <task estimate: <1h|1h|5h|10h|100h> --lane <lane>` appends one key=value line to
+ `out/wf-cost.log` (tokens + API-price $ from the subagent transcript). `--duration <s>` adds dur=; `wf usage --report` (med_dur) = per lane/model and
+ effort: n, done, median/total $, $ per done task, median turns, over every project's log.
+ The completion notice's token count is NOT the cost (one worker: notice 67k, transcript 5.0M cache reads);
+ real usage is in the subagent transcript `~/.claude/projects/<project>/<session>/subagents/agent-*.jsonl`
+ (`wf usage` per session/agent; one request is several transcript entries: counted once;
+ subagent transcripts lack final output counts → out estimated from content, shown `~`, log `est=N`).
+
+## Cloud lane
+Virtual lane `cloud` (projects with `cloud = true`; design: cloud-lane spec §4.6): tasks run as
+`claude --cloud` sessions billed to the cloud budget, not as local workers.
+- `wf orch pick cloud [--id ID]` = stop file → ledger (`wf cloud ledger`) → first fitting task across all lanes
+ (runner-ready, not in progress, `wf cloud` fit rules; opus only, `Cloud: yes` first, then prio, then larger effort) → `wf cloud send`
+ (claim `in progress: cloud:<sid>`, record `.wf/orch/<id>.json` lane cloud). Nothing to spawn: the session runs
+ remotely. Otherwise `stop lane cloud: ledger (…)` (balance < reserve, or send refused) | `none fit (…)` |
+ `max parallel (…)`. A stop of the cloud lane never stops the local lanes.
+- Local lanes skip tasks claimed `cloud:` (every `in progress` task is skipped by `wf orch pick <lane>`).
+- On each wake-up, at most every 10 min: `wf cloud pull --all` (teleport ~30 s, no tokens). Per ended task it prints
+ `<id>: <state> (<sid>, $… )` (state done | awaiting | handback | lost; done also `report: commit <sha>`) →
+ `wf orch post <id> cloud --result <state> [--commit <sha>]` = the worker-report path (done: archive line, branch
+ `<task lane>/<id>` gone, `wf check`; else leftover commit of the note/claim pull left), log line, next cloud
+ pick. `running` (exit 4) → nothing; pull again next wake. awaiting / handback / lost → `stop lane cloud`, tell the
+ owner; the task is back in its local lane (handback note `Recovery: cloud attempt …`).
+- Fill: one `wf orch pick cloud` per free slot (up to `max_parallel`), each in its own call.
+- Batch (`wf batch` on a `cloud = true` project) pulls itself: the job runs `wf_res.py batch-sidecar` around
+ `claude -p`; every 10 min while it lives (until `out/wf-batch.stop`) it runs `wf cloud pull --all` and per ended
+ task `wf orch post <id> cloud --result <state> [--commit <sha>] --no-pick` + a `cloud <id> <state> <sha> (sidecar pull)`
+ line in the summary, so pulls never starve while the batch waits on foreground workers. The batch orchestrator
+ only picks cloud, never pulls. Two pulls of one id at once: the second prints `<id>: pulled by another wf cloud
+ pull, skipped` (rc 0; flock on `.wf/cloud/<id>.json`).
+ Tail: the orchestrator exited with `.wf/cloud/*.json` records left (sent at batch end) → the sidecar keeps
+ pulling every 10 min (no picks, no sends) until no record is left, the stop file appears or 24 h passed; the job's
+ ledger reservation shrinks to 0.2 GB / 1 cpu for it (ledger only, the unit's MemoryMax stays); the summary ends
+ with `cloud tail ended: <why>; <n> pulls; left: <ids|none> (sidecar)`. A next batch's sidecar may overlap it (record
+ flock).
+
+## Unattended batch (nightly, long runs)
+The orchestrator does not babysit long runs; a headless batch orchestrator (separate `claude -p` process,
+fresh context per batch) does. The owner starts it (auto mode denies an agent launching another unattended agent): `!` + the command, or
+once an allow rule `Bash(python3 /projects/public/workflow/wf.py batch:*)` in `~/.claude/settings.json`.
+
+```
+wf batch N [--lanes sonnet,haiku] [--model opus] [--for 6h] [--mem 4G] # --dry-run prints the command; default mem/for: wf res hist
+wf batch --status # newest job + out/wf-batch-*.md
+wf batch K --prep [--lanes fast] # prep batch (below)
+```
+= `wf res run --mem 4G --for 6h --title wf-batch -- claude -p --model opus --permission-mode auto
+--permission-prompts none "<templates/batch-prompt.md>"` with `CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS` = `--for`.
+The prompt: follow 'One worker' for up to N tasks, never implement; each round one wf-worker per lane in ONE
+message, foreground, post-check, one line per task to `out/wf-batch-<stamp>.md`; stop a lane on its first stop
+outcome; never ask (follow-ups → `wf add -p 2`, decisions → `wf add -s awaiting`); end with the ids added.
+
+Why foreground: `claude -p` ends when the orchestrator's turn ends and kills background workers after a
+wait ceiling (default 10 min; the env above raises it as a backstop). Workers spawned in one message run in
+parallel and the turn waits for all. Background subagents cannot spawn subagents, so the batch
+orchestrator is never a subagent. The interactive orchestrator checks `wf batch --status`.
+
+Headless batch = short fire-and-forget runs (a few tasks, no steering). Long runs: pilot below.
+
+Prep batch (`--prep`): makes the backlog runner-ready, never implements. Targets = up to K Pending tasks without
+Done, not `Sessions: owner`, no status, no open slices (pickable first, then priority); none → prints `prep: …
+nothing started`, starts nothing. Prompt `templates/prep-prompt.md`: sonnet subagents (≤ 3 tasks each) read the
+task text, `wf set <id> --done "…"` (+ `--model` if missing); unclear → `wf add -s awaiting` + `wf status <id>
+blocked <a-id>`. Summary lines `<id> → Done: …` | `<id> → awaiting <a-id>`; then it commits the task file.
+Pilot runs one when idle with `not runner-ready` tasks (once per idle spell).
+
+## Pilot (nested)
+`/wf-pilot [--batch 4] [--lanes a,b] [--for 8h]`, typed into an idle project session (phone via Remote Control),
+keeps that session tiny: subagents cannot spawn subagents, so it pilots headless batch orchestrators instead of
+workers. Loop: `wf batch K --left <time to deadline>` (a wf res job, returns at once) → background Bash `wf res wait <rid>` → its exit
+re-invokes the pilot → `wf batch --status` → one line to its context and `out/wf-orch.log` → next batch. A batch =
+up to K tasks, rounds of one worker per lane in parallel (2 lanes, K=4 → two rounds). Nothing pickable →
+prep batch if tasks are not runner-ready (once per idle spell), then background `wf lanes --wait 1800`, until `--for`. Deadline fit from history: task p90 = p90 of done/handback durations in `out/wf-orch.log` (local lanes, > 0; < 3 runs → 30 min); `--left` launches K' = min(K, floor(left / p90)) tasks with `--for` = left (K' = 0 → `fit: 0 …; nothing started`, the pilot stops); the batch prompt runs `wf batch --time-left <deadline>` before each round and spawns nothing more on `: stop` (left < p90). Steering: the owner talks to the pilot only; stop →
+`wf batch --stop` (stop file, checked before every spawn: latency ≤ the running tasks), skip → `wf move <id> deferred`. Batch rc≠0 or no summary →
+retry once, then stop. PushNotification on new awaiting items, on a stop and at run end; each alert also an `ALERT <text>` line in `out/wf-orch.log` and in the summary (push may be off). Phone push needs `agentPushNotifEnabled` (settings) / app notifications on. Needs the allow rule
+`Bash(python3 /projects/public/workflow/wf.py batch:*)`.
+
+## Context
+Orchestrator large (after design talk) → finish the topic, commit, ask the owner to `/clear`. State lives in
+TASKS.md, the log and the batch summaries.
+Safe to clear only with no background worker running. Tested (2026-10-05): `/clear` leaves a background
+subagent alive and its hand-back + completion notice reach the new context, but SIGKILLs its running Bash
+command (exit 137): a worker mid-test or mid-commit loses that step and reports a false failure or half state.
diff --git a/docs/resource-ledger.md b/docs/resource-ledger.md
new file mode 100644
index 0000000..17a0443
--- /dev/null
+++ b/docs/resource-ledger.md
@@ -0,0 +1,304 @@
+# wf res — shared resource ledger for agents — design
+
+Status: approved and implemented 2026-10-01.
+
+## 1. Goal
+
+1. No agent job is OOM-killed because another agent started a big job at the same time.
+2. The user keeps a guaranteed share of RAM and CPU (gaming), switchable on demand.
+3. An agent that cannot get resources now gets one clear line (who holds them, until when) and works on
+ something else instead of waiting or retrying blindly.
+4. Agent-made waste (tmpfs scratch, failed units) is cleaned routinely.
+5. Python ≥ 3.11 stdlib, Linux systemd user manager, no daemon. Works in any directory, wf project or not.
+
+Machine facts (2026-10-01): 16 cores, 30 GB RAM, 7 GB swap, `/tmp` is tmpfs (16 GB), user manager
+delegates `cpu io memory pids`; `systemd-run --user --scope --slice=agents.slice` and transient units with
+`MemoryMax`/`CPUWeight` verified working.
+
+## 2. User decisions (2026-10-01)
+
+| # | Decision |
+|---|----------|
+| D1 | Default user reserve 6 GB RAM, 4 CPUs. |
+| D2 | Gaming reserve 12 GB RAM, 8 CPUs. |
+| D3 | Gaming mode slows agents; never freezes or kills them. |
+| D4 | Scratch under `/tmp/claude-<uid>/` untouched for 2 h (newest mtime in the entry) may be deleted automatically by any agent, whoever made it. Except a live session's own dir (`<project>/<session-id>`, live = `~/.claude/sessions/<pid>.json` with that `sessionId`, pid alive, `procStart` matching; added 2026-10-02 after an idle session lost its scratchpad). Same automatic clean sweeps top-level litter in `/tmp` and `/var/tmp` (own entries only): empty dirs idle 2 h (`scratch_hours`) and `clr-debug-pipe-<pid>-*` / `dotnet-diagnostic-<pid>-*` of dead pids; entries with content are never swept. Per-project `out/` patterns only list unless `--yes`. |
+| D5 | v1 = everything in the proposal: run, status, wait, release, note, queue, game, clean, timer, shared rule. |
+| D6 | `game on [--for D]`, default 4 h, turns itself off. |
+| D7 | Install a 1-minute user timer via an explicit `wf res timer on`. |
+| D8 | `game on` never fails and never squeezes running jobs; it switches budget and weights at once and prints the shortfall and the estimated time until the full reserve is free. |
+| D9 | Small unreserved work: headroom (2 GB) in the budget **and** all agent processes inside `agents.slice`: running Claude sessions are moved there live by `wf res adopt` (run by every timer tick; no restart), new ones optionally start there via the `shell-init` alias. |
+
+## 3. Architecture
+
+### 3.1 Cgroup layout
+
+```
+user@<uid>.service
+└── agents.slice ← MemoryHigh, CPUWeight, IOWeight (normal / gaming)
+ ├── run-…scope ← each Claude session (started via the alias), its shells, small jobs
+ └── agents-jobs.slice
+ └── wf-r-N.service ← each `wf res run` job: MemoryMax, MemoryHigh, MemorySwapMax=0, Nice=10
+```
+
+- `agents.slice` caps the sum of everything agents do. The ledger decides who may start big work; the slice
+ is the kernel backstop for wrong estimates and unreserved small work.
+- Slice properties are set with `systemctl --user set-property --runtime agents.slice …`. The slice unit file
+ `~/.config/systemd/user/agents.slice` is written on first use (then `daemon-reload`), so the slice exists
+ before any set-property.
+- Normal: `CPUWeight=20`, `IOWeight=20`, `MemoryHigh = RAM − user_reserve_gb`.
+- Gaming: `CPUWeight=5`, `IOWeight=5`, `MemoryHigh = max(RAM − game_reserve_gb, agents.slice MemoryCurrent)`;
+ re-lowered towards the target on every `wf res` call / tick (D8: no squeezing below current use).
+- Weights only matter under contention; on an idle machine agents get full speed.
+
+### 3.2 Code layout
+
+- `wflib/res.py` — pure, text in / data out: parse `/proc/meminfo`, parse `systemctl show` output, durations
+ and sizes (`10G`, `40m`, `4h`), prune, capacity rule, queue start order, game slice values and shortfall,
+ stale-scratch selection (from a given list of (path, newest mtime)), all output lines.
+- `wf_res.py` — I/O: lock, atomic ledger write, `/proc` and `/sys/fs/cgroup` reads, filesystem walk, the
+ runner (`subprocess.run` wrapper, injectable for tests), unit/timer file writing, printing.
+- `wf.py` — only registers `wf res …` and dispatches to `wf_res.main(argv)`. `wf res` needs no
+ `workflow.toml`.
+
+### 3.3 Files
+
+- State dir `~/.local/state/wf/` (`$XDG_STATE_HOME/wf` if set): `resources.json`, `resources.lock`,
+ `logs/r-N.log`, `logs/r-N.rc`, `logs/r-N.peak`, `resources-history.jsonl` (finished rc=0 runs with a peak,
+ appended when pruned from the ledger after 24 h; newest 2000 kept; read by `wf res hist`).
+- Config `~/.config/wf/resources.toml` (`$XDG_CONFIG_HOME`), all keys optional:
+ ```toml
+ user_reserve_gb = 6
+ user_reserve_cpus = 4
+ game_reserve_gb = 12
+ game_reserve_cpus = 8
+ game_hours = 4
+ small_headroom_gb = 2
+ scratch_hours = 2
+ ```
+- Unit files under `~/.config/systemd/user/`: `agents.slice`, and with `timer on`: `wf-res.service`,
+ `wf-res.timer`.
+
+### 3.4 Ledger
+
+`resources.json`: `{"next": N, "game_until": iso|null, "last_clean": iso|null, "entries": [...]}`.
+
+Entry fields: `id` (`r-N`, never reused), `project` (git toplevel basename, else cwd basename), `owner` (pid of
+the nearest ancestor process named `claude`, else the parent pid of `wf`), `title`, `mem_gb`, `cpus`
+(default 1), `est_min`, `state` (`queued` | `running` | `note` | `done`), `unit` (`wf-r-N.service`, run only),
+`cmd` (argv list), `cwd`, `log`, `queued` (iso), `started` (iso), `expires` (iso = started + 2 × est_min),
+and for `done`: `ended`, `rc` (int or null), `peak_gb`, `why` (`exited` | `expired` | `owner gone` | `released` | a kill reason, e.g.
+`killed: oom-kill by systemd-oomd, limit 4.0 GB; raise --mem`).
+`env` (caller variables the user manager lacks; `WF_RES_ID` and secrets never), `by` (who to tell, run/note;
+keys dropped when unknown): `name` (`--by`, else `WF_SESSION_NAME`, else `<lane> session` from the wf lane
+record `.wf/sessions/*.json` with this `CLAUDE_PID`, main tree or toplevel), `task` (`WF_TASK`, else the
+`<lane>/<id>` branch of cwd), `batch` (`WF_RES_ID`: every job unit gets `WF_RES_ID=r-N`, so a `wf batch`
+worker's entries name the batch), `address` (`uds:$CLAUDE_CODE_MESSAGING_SOCKET`; a batch worker's = its
+orchestrator's, reachable via SendMessage). Status lines of live entries end `[by <name> <task> batch r-N,
+message uds:…]`; the throttle warning ends `; started by …`.
+
+Every command takes the lock (`fcntl.flock` exclusive on `resources.lock`), reads, then **prunes**, acts,
+writes atomically (temp file in the same dir + `os.replace`), releases.
+
+Prune (pure, given unit states, live pids, now):
+- `running` whose unit is inactive/failed/unknown → `done` (`rc` from `logs/r-N.rc`, `peak_gb` from
+ `logs/r-N.peak`). No `.rc` (the sh wrapper died with the job) → `rc` null and `why`/`peak_gb` from
+ `journalctl --user -u wf-r-N.service --since @started` (`Failed with result '…'`, `systemd-oomd killed`,
+ `… memory peak`), shown in `status` and `wait`; journal silent → `why` points at that journalctl command.
+- `running` past `expires` → stays running (job not killed; `run` says so on start, status shows
+ `ETA overdue (still running, not killed)`), flagged `overdue` in status, and counted at
+ `max(reserved, used)`.
+- `note` whose owner pid is gone or past `expires` → `done`.
+- `done` older than 24 h → removed.
+- `game_until` in the past → game off (slice back to normal values).
+
+### 3.5 Capacity rule
+
+```
+budget_mem = MemAvailable − reserve_gb − small_headroom_gb
+ − Σ over running: max(0, mem_gb − MemoryCurrent(unit))
+ − Σ over notes: mem_gb
+budget_cpus = nproc − reserve_cpus − Σ over running and notes: cpus
+fits(req) = req.mem_gb ≤ budget_mem and req.cpus ≤ budget_cpus
+```
+
+- `reserve_*` is the user or the gaming value depending on game mode.
+- A note counts its full reservation (its memory cannot be measured; overcounting briefly is safe).
+- A running job's memory already in use is inside `MemAvailable`, so only its unused part is subtracted.
+
+## 4. Commands
+
+All print short lines; `--json` where stated. Exit: 0 ok · 3 busy (refused) · 1 `wf: …` one line (unknown
+id, systemd failure) · 2 usage.
+
+### 4.1 `wf res run --mem 10G [--cpus N] --for 40m --title "…" [--queue] -- <cmd …>`
+
+- Fits → `systemd-run --user --slice=agents-jobs.slice --unit=wf-r-N --collect
+ -p MemoryMax=<mem> -p MemoryHigh=<0.9·mem> -p MemorySwapMax=0 -p Nice=10
+ -p WorkingDirectory=<cwd> -p StandardOutput=append:<log> -p StandardError=append:<log>
+ /bin/sh -c '"$@"; rc=$?; cat /sys/fs/cgroup$(cut -d: -f3 /proc/self/cgroup)/memory.peak > <peak>; echo $rc > <rc>' sh <cmd …>`
+ (the unit is collected on exit, so the wrapper records exit code and peak bytes itself). Prints `r-N started; log <path>; ETA ~HH:MM`. Exit 0.
+- Environment: a user unit starts from the user manager's environment, so the caller's variables that differ
+ from `systemctl --user show-environment` go along as `--setenv=NAME=VALUE` (stored in the entry, so a queued
+ job gets them too); skipped: `PWD OLDPWD SHLVL _` and names with TOKEN/SECRET/PASSWORD/PASSWD/CREDENTIAL.
+- Does not fit, no `--queue` → exit 3, one line:
+ `busy: 7.5 GB held by proj-a "dotnet e2e" (r-4) until ~14:40; 2.1 GB free for agents; retry after ~14:40 or work on something else`
+ (names the holders whose release would make it fit, earliest ETA first; CPU shortage named the same way).
+- Does not fit, `--queue` → `queued`, prints `r-N queued, position P; est. start ~HH:MM; cancel: wf res release r-N`. Exit 0.
+- A request larger than the whole agent budget on an empty machine → exit 1 `wf: 20 GB can never fit (max …)`.
+- `--force` (memory really free though the ledger says busy: unused reservations, stale notes): fits against
+ MemAvailable − user reserve (game reserve in game mode) − headroom only; ledger claims and the queue are ignored;
+ the entry is still recorded. Does not fit even so → exit 3 `busy even with --force: only X GB really free beyond
+ the reserve; …`. The normal busy line ends with `; X GB really free beyond the reserve: --force starts it past
+ the ledger (only if the holders will not use what they reserved)` when `--force` would fit.
+- `systemd-run` failure → entry removed, exit 1 with its first stderr line.
+- `--lock KEY`: one queued/running job per KEY and main tree (git common dir's parent: lane worktrees share it)
+ at a time, e.g. jobs sharing one checkout (a-game-project-builder `../.<project>-gate`). A title starting with the word
+ `gate` locks `gate` unasked (2026-10-06: two hand-started gates overlapped in one gate checkout). Held → exit 3
+ `busy: lock 'KEY' held by r-N "title" (ETA ~HH:MM|queued): …; --queue waits for it (--force does not override
+ a lock)`; `--queue` → queued `(lock held by r-N)`. Ledger field `lock` = `KEY@<main tree>`.
+- The command reaches the job verbatim (no systemd `%` specifier expansion: `--format='%h %s'` is safe).
+
+### 4.2 Queue
+
+Strict FIFO by `queued`: the head starts when it fits; entries behind a waiting head wait (a big job is never
+starved); an entry whose lock a running job holds is skipped (it neither starts nor blocks the rest). Starting happens inside every `wf res` call (after prune) and every timer tick. A started queued job
+runs with the cwd and argv it was queued with; its ETA counts from the actual start.
+
+### 4.3 `wf res status [r-N] [--json]`
+
+Without id: one line per entry (id, project, title, state, reserved / used / peak GB, cpus, started, ETA or
+`overdue`), then the budget line (`agents may use X GB, Y cpus now; reserve 6 GB/4 cpus`), game mode with
+time left and shortfall line (§4.6), unreserved agent memory (`agents.slice` MemoryCurrent − jobs; `(X GB of it file cache, reclaimable)` =
+slice `memory.stat` `file` − `shmem`, capped at that figure), warnings:
+- running job throttled at its own limit: used ≥ 0.85 × `--mem` **and** its cgroup `memory.pressure`
+ `some avg60` ≥ 20 % → `r-N throttled at its memory limit (…): likely too small; release --stop, re-run
+ with a bigger --mem` (page cache alone at the limit does not stall, so no false alarm; the timer tick
+ has no reader, so it does not warn);
+- tmpfs `/tmp` above 25 % of RAM;
+- `N claude sessions outside agents.slice (wf res adopt)`.
+- `N jobs outside wf res (bare systemd-run): <unit> <used> GB, …`: running transient user services outside
+ `agents*` slices, except desktop ones (`app-*`, `dbus-*`). Warning only: a service cannot change slice live.
+
+With id: that entry in full, including `rc`, `peak_gb`, `why` for done entries.
+
+### 4.4 `wf res wait r-N [--timeout 2h]`
+
+Polls every 15 s (each poll is a normal locked call, so it also prunes and starts queued work) until the entry
+is `done`; prints `r-N done rc=0 peak 9.4 GB in 37 min`. The throttle warning (§4.3) goes to stderr once. Exit 0 when done (whatever `rc`), 1 on timeout. If
+reaped, `status r-N` answers from the ledger.
+
+### 4.5 `wf res release r-N [--stop]`
+
+- `queued` / `note` → removed. Running unit → refused with `wf: r-N is running; --stop to kill it` unless
+ `--stop`, then `systemctl --user stop`, entry → done (`why=released`).
+
+### 4.6 `wf res game on [--for 4h] | off`
+
+- `on`: `game_until = now + for`; set gaming slice values (§3.1); prints
+ ```
+ game on until 22:40 (4h); CPU/IO now yours
+ short 3.1 GB of 12 GB: r-4 proj-a "dotnet e2e" 2.5 GB ~21:05, r-7 proj-b "r5 rebuild" 9 GB ~21:30
+ full reserve free ~21:30 (est.); free now: wf res release r-7 --stop
+ ```
+ The shortfall/estimate lines appear only when the reserve is not free now. Estimate = ETAs of the running
+ jobs that must end, in order (jobs are never stopped by game mode). New requests that do not fit are refused
+ or queued as usual. `on` while on → extends `game_until`.
+- `off` or expiry: normal slice values, `game_until = null`.
+- No windows over the game: `wf res hook` (Claude Code `PreToolUse` hook, matcher `Bash`, in
+ `~/.claude/settings.json`; the user adds it, wf never edits dotfiles) prefixes every agent Bash command with
+ `unset DISPLAY WAYLAND_DISPLAY;` while `game_until > now`, and adds context telling the agent why GUI apps
+ fail (run headless/offscreen or later). Reads the ledger only (no lock, no systemctl, ~60 ms); off, expired,
+ other tools or any error → no output, command unchanged. Live for running sessions: state is read per call.
+ ```json
+ "hooks": {"PreToolUse": [{"matcher": "Bash", "hooks": [{"type": "command",
+ "command": "python3 /projects/public/workflow/wf_res.py hook"}]}]}
+ ```
+
+### 4.7 `wf res note --mem 3G [--cpus N] --for 20m [--force] "title"`
+
+Reserves for foreground work (no unit). Freed when `owner` exits, at `expires`, or by `release`. Refused like
+`run` (exit 3) when it does not fit; `--force` as in `run`.
+
+### 4.8 `wf res clean [--yes]`
+
+- Automatic part (no `--yes` needed; also run by any `wf res` call when `last_clean` > 10 min ago, and by every
+ tick):
+ - every direct and second-level entry under `/tmp/claude-<uid>/` whose newest mtime (recursive) is older than
+ `scratch_hours` → deleted; lines `freed 1.2 GB: /tmp/claude-1000/-projects-x/<session>` (only printed by
+ `clean` itself; silent from other commands);
+ - `systemctl --user reset-failed 'wf-r-*'`.
+- Listed only, deleted with `--yes`: per-project patterns from the current project's `workflow.toml`
+ `cleanup = ["out/prof", "out/history-logs/*.log:30d"]` (glob relative to the project root, optional `:Nd`
+ minimum age by mtime). Never follows symlinks; never leaves the project root.
+
+### 4.9 `wf res timer on | off`, `wf res tick`
+
+- `timer on` writes `wf-res.service` (`ExecStart=python3 /projects/public/workflow/wf.py res tick`) and
+ `wf-res.timer` (`OnBootSec=1min`, `OnUnitActiveSec=1min`), `daemon-reload`, `enable --now wf-res.timer`.
+ `off` disables and removes them.
+- `tick` = lock, prune, start queued, game expiry and slice re-lowering, automatic clean; then adopt., then
+ session caps: every running scope in `agents.slice` (a Claude session) gets `MemoryHigh = session_mem_gb`
+ (config, default 6, 0 = off → `infinity`) via `systemctl --user set-property --runtime`, only where it differs.
+ MemoryHigh only: a session over it is throttled, never killed (a MemoryMax OOM kill could pick `claude`, whose
+ oom_score_adj is 200). `status` warns `claude session N at its memory cap (…)` at ≥ 0.9 × cap and ≥ 20 % stall.
+ `wf res run` jobs live in `agents-jobs.slice`, not in a session, so the cap never limits them. Silent.
+
+### 4.10 `wf res adopt`
+
+Moves running Claude sessions into `agents.slice` without a restart (verified 2026-10-01: a live process moved
+by `StartTransientUnit` with `PIDs`). Session roots = processes named `claude` whose parent is not `claude`,
+outside the slice; each root and its descendants outside the slice go into a new scope
+`wf-claude-<root pid>.scope` via
+`busctl --user call org.freedesktop.systemd1 /org/freedesktop/systemd1 org.freedesktop.systemd1.Manager
+StartTransientUnit 'ssa(sv)a(sa(sv))' wf-claude-<pid>.scope fail 2 PIDs au <n> <pids…> Slice s agents.slice 0`.
+Children forked later inherit the scope. Prints one line per session; errors (process gone) are reported, and
+the timer retries next minute.
+
+### 4.11 `wf res shell-init`
+
+Prints the alias line for `~/.bashrc` (the user adds it; wf never edits dotfiles):
+`alias claude='systemd-run --user --scope --quiet --slice=agents.slice claude'`.
+
+### 4.12 `wf res hist [--project P]`
+
+Reservation sizes from history: history file + the ledger's finished runs (rc=0, peak known). Grouped by
+project and title kind = title minus trailing `(…)` and trailing sha/hex words (7–40 hex chars, ≥ 1 digit):
+`gate 2c91cac7 (fix x)` → `gate`. One line per kind: `n`, median request, peak p50/p95, median estimate,
+duration p50/p90, suggestion. Suggestion (≥ 3 runs): mem = p95 peak ×1.15 (up to 0.1 GB, ≥ 0.2 GB), for = p90
+duration ×1.5 (≥ 5 min); percentiles nearest-rank. `run`/`note` asking > 2× the suggestion (mem or for) print
+`hint: history says ~X GB / Y min (n runs)` on stderr; nothing is resized. `wf batch` without `--mem`/`--for`
+uses the `wf-batch` suggestion of the project (not `--prep`; the bg-wait ceiling stays 6h unless `--for`).
+
+## 5. Shared rule (`shared/CLAUDE.md`, ~4 lines)
+
+- Job expected > 2 GB RAM, > 4 cores or > 10 min → `wf res run` (never bare `systemd-run`, never a long
+ foreground command); big foreground step → `wf res note`.
+- Exit 3 = busy: note it on the task, do other work, retry after the printed time. No tight polling.
+ The line names `--force` when the memory is really free (claims unused): rerun with it if the holders will not grow.
+- No big scratch in `/tmp` (RAM); use the project's git-ignored `out/`.
+- Session end: release your own entries (`wf res status`).
+
+## 6. Errors
+
+- Not Linux / no systemd user manager / no cgroup v2 → `wf: wf res needs a systemd user session` exit 1.
+- Corrupt ledger → renamed to `resources.json.bad-<ts>`, start empty (id counter salvaged from its text: ids never reused), one warning line. Unknown entry keys (written by a newer version) are ignored, never corrupt: a `wf res wait` started before a release must not wipe the ledger. Starting a job deletes stale `logs/<id>.rc/.peak`.
+- Lock is held at most for one command's critical section (no subprocess wait longer than a `systemctl`
+ call inside the lock; `wait` polls with the lock released between polls).
+
+## 7. Tests
+
+- Pure (`tests/test_res.py`, hand-written literals): meminfo parse; size/duration parse; capacity with mixed
+ running/note entries, used figures, headroom and game reserve; prune (unit gone, pid gone, expired note,
+ overdue run, 24 h done removal, game expiry); FIFO start order incl. blocked head; busy line holder choice and
+ wording; game slice values and shortfall/estimate lines; stale scratch selection; cleanup pattern + age.
+- I/O (`tests/test_res_io.py`): fake runner records argv for run/stop/set-property/timer; atomic write; corrupt
+ ledger recovery; two real processes reserving at once under the real lock never overbook (budget faked).
+- Smoke (manual, release step): real `wf res run --mem 100M --for 1m -- true`, `wait`, `status`.
+
+## 8. Release
+
+Slices (≈1 h each): pure core · ledger+lock+run/status/release · wait/rc/peak · queue · note · game+slice ·
+clean · timer+tick+shell-init · shared rule + CHANGES + release. Release rules of the workflow repo apply;
+after release the workflow session runs `wf res timer on` once and tells the user to add the
+`shell-init` alias.