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. Overview: README.
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
# 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.
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 whoseAfter: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.pathalone → 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): withCLAUDE_CODE_SESSION_IDset, the last request's prompt size (input + cache write + cache read) from the last 1 MB of<CLAUDE_CONFIG_DIR|~/.claude>/projects/*/<id>.jsonl; overctx_hint→ last linecontext ~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 ofRuling: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 itsAfter:. Body is re-indented to two spaces. A block given toadd -must carry an id and, for tasks, a priority; a leadingN.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.
donerefuses when the id is unknown or is a parent with open slices. On ana-id it removes the item without an archive line (-mis optional there) and clears the(blocked: [[a-id]])status of the tasks that waited on it, listing them.checkerrors:- 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]])wherexis not an opena-itemAfter:cycle; Pending item placed before an open item it isAfter: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
docsonly[[t-…]]/[[a-…]]links are validated (other[[x]]may be the project's own wiki links). Warnings (exit stays 0):#Nnumber refs in TASKS.md; task in progress with no branch/note; Awaiting item that no task references and that is older than 30 days bygit 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.
donewrites 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
wflibfunctions 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.pywith--project <tempdir>on fixture projects: pick rule, skip reasons, insert position with priority andAfter:,donerefusals, everycheckerror once, write-conflict stop,listfilters,prio/moveplacement and refusals,status,set,note,body,tick, search ranking order on a fixed corpus,reportfrom two processes at once (both entries whole),formatgate, migrate on a fixture with the known oddities (strayN.,N/Mtitles,After:by title, prose after items). - Each new test is seen failing first, and again with the tested line broken by hand.