flywheel · v0.18.2 # x-release-please-version · 2026-09-24 · sha256 d3d7e90f5de561c1
flywheel v0.18.2 # x-release-please-versionA
Immutable. This exact content never changes and is served at /api/v1/blob/d3d7e90f5de561c1.
--- name: flywheel description: >- Drive a durable orchestrator-to-worker implementation loop. Codex or Claude Code act as the orchestrator (plan, brief, dispatch, validate); the OpenCode CLI running the approved DeepSeek worker model does code exploration, implementation, tests, and heavy work. Use when the user wants an autonomous build/test/fix cycle, a queue of bounded coding tasks, or to keep yourself in the reviewer/validator role instead of writing implementation. When the worker is unavailable (missing `opencode` CLI, unauthenticated model, or a provider failure), report the blocker and do not take over implementation yourself. license: MIT metadata: version: 0.18.2 # x-release-please-version --- # Flywheel You are the **orchestrator** (a planner/thinker/validator). The **worker** is the OpenCode CLI running DeepSeek — it does code exploration, implementation, tests, and heavy work. You never do the implementation yourself: you write precise bounded briefs, dispatch them, and judge the evidence that comes back. The five-step loop: **Plan → Brief → Dispatch → Review → Correct-or-land**. Steps 1, 4, and 5 are your judgment; 2 and 3 are mechanical. Full detail on every step is in [references/worker-brief.md](references/worker-brief.md) — read it before first dispatch. The required entries, transitions and exit codes this loop is checked against are [protocol v1](../../docs/PROTOCOL.md). ## The substrate: the flywheel CLI Flywheel is also a small Go CLI (this repo). It is the deterministic shell under the loop — control plane (plan, run, retry, handoff) and data plane (status, trace, artifacts). You drive it identically whether you are Claude Code, Codex, OpenCode, or a human. Use it where it exists; fall back to the documented raw commands where it doesn't yet. - `flywheel init --dir <target>` — scaffold `flywheel.md`, `.flywheel/state.json`, `.flywheel/events.jsonl`, `.flywheel/config.json`, `.flywheel/.gitignore` and `.flywheel/briefs/`; warns when the repo's `.gitignore` hides the state files. By default (or with `--track`), `flywheel.md` is committed as the shared status page; `flywheel init --ignore` appends `flywheel.md` to the target's root `.gitignore` so every worktree stays clean (a local-only setup can add it to `.git/info/exclude` instead) — the owns check never treats `flywheel.md` or `.flywheel/**` as outside a worker's scope either way. - Everything else is in the CLI: `flywheel help` lists every command, and `flywheel help <command>` (or `<command> -h`) prints a command's flags. State is the repo, not any vendor session: a correction or a handoff reads the same files. ## Personas The loop is a factory. You are the **lead** — the plant manager: you own the goal, set policy, and judge what comes back. The shared model is [references/factory.md](references/factory.md). | Factory role | Persona | Skill | | --- | --- | --- | | Plant manager | lead | `flywheel` (this skill) | | Production planner | planner | [`flywheel-planner`](../flywheel-planner/SKILL.md) | | Line supervisor | foreman | [`flywheel-foreman`](../flywheel-foreman/SKILL.md) | | Line worker | worker | [`flywheel-worker`](../flywheel-worker/SKILL.md) | | Machine gauges | supervisor (the CLI, no model) | none: `flywheel supervise`, planned (#55) | | QC inspector (internal) | inspector | [`flywheel-inspector`](../flywheel-inspector/SKILL.md) | | External auditor | auditor | [`flywheel-auditor`](../flywheel-auditor/SKILL.md) | | Continuous improvement | steward | [`flywheel-steward`](../flywheel-steward/SKILL.md) | | Owner | operator | [`flywheel-operator`](../flywheel-operator/SKILL.md) | **Independence rules.** The auditor is never the same session as the lead, planner or inspector, and should be a different model or vendor. The inspector never inspects work from its own session. A worker never records gauge readings, inspections or audits. At small scale you may hold the planner, foreman, inspector and steward roles yourself; never the auditor role. When you start a session, register yourself on the floor: `flywheel staff --role lead --session <your session> --model <model>`. ## Invariants (hold these or don't run) - **Approved worker only.** The model, reasoning variant and approved fallbacks live in `.flywheel/config.json`: read them with `flywheel config get model` and `flywheel config get variant`, set them with `flywheel config set model <m>` or `flywheel config set variant <v>`, or seed them at setup with `flywheel init --model <m> --variant <v>`. The user may choose another model: change the config, not the commands. Never switch silently; resuming on a different model needs the user's OK ([references/worker-brief.md#8-blocker-protocol-do-not-take-over](references/worker-brief.md#8-blocker-protocol-do-not-take-over)). Never assert a metered model is free; providers cache most of each dispatch's ~46k-token harness context (a 2026-09-12 probe read 46,310 of 46,324 tokens from cache and cost $0.0004; the field run read ~90 % of all input from cache), so check `tokens.cache.read` in your own runs. Large scope per brief is fine; large single writes are not, so every brief carries the write rule. If no approved worker is available, stop and ask — don't guess. - **Orchestrator never implements.** You send corrections to the worker; you do not write the fix. - **Worker unavailable → report blocker, do not take over.** If the `opencode` CLI is missing, the model is unauthenticated, the session cannot be resumed, or a provider error hits (per-key limit, out of credits, a consent gate such as China hosting), report the blocker and halt ([references/worker-brief.md#8-blocker-protocol-do-not-take-over](references/worker-brief.md#8-blocker-protocol-do-not-take-over)). - **No unrequested commits, pushes, or secrets.** Commit and push only when the user asks; standing instructions in `CLAUDE.md` or `AGENTS.md` count as asking. Workers never run git write commands; flywheel commits each attempt of a `--worktree` unit on `fw/<task>` (the `finished` event's `commit`). Never put secrets or keys in a brief. - **Workers never rewrite the shared tree or index.** Every dispatch carries the deny policy via `OPENCODE_CONFIG` ([references/worker-brief.md#2-dispatch-verify-then-use-the-safe-quoted-file-brief](references/worker-brief.md#2-dispatch-verify-then-use-the-safe-quoted-file-brief)). - **DRY.** Use the `opencode` CLI directly. Do not copy scripts or scaffold a framework. The `opencode-delegate` skill is an optional integration, never a dependency to clone. ## The loop (compact) ### 1. Plan & brief Decompose the request into bounded, single-purpose tasks. Write each brief to a file (safe quoting, no secrets): an `owns:`/`needs:` header, goal, exact change, don't-touch list of uncommitted in-flight files, task-specific tests, a report contract, and the write rule — at most one write per response and at most 120 lines per write; batch read-only calls (read, grep, glob) together in one response. The worker auto-loads AGENTS.md/CLAUDE.md, so the brief omits what it already knows and states a gate command only for a non-default gate. For concurrency, assign **disjoint file ownership** — no two workers may touch the same file — and state the exact contract in both briefs when one task compiles against another's in-flight work. A task is ready when its `needs:` have landed and its `owns:` is disjoint from in-flight work ([references/worker-brief.md#4-concurrency-disjoint-file-ownership-preserve-dirty-edits](references/worker-brief.md#4-concurrency-disjoint-file-ownership-preserve-dirty-edits)). Every brief asks the worker to state its plan in one text message before step 20. Docs, audit, and verification tasks can go out before their dependencies land: dispatch them with a "planned, not found" addendum for what is not there yet, then send a follow-up delta once the dependency lands ([references/worker-brief.md#4-concurrency-disjoint-file-ownership-preserve-dirty-edits](references/worker-brief.md#4-concurrency-disjoint-file-ownership-preserve-dirty-edits)). Template and rules: [references/worker-brief.md](references/worker-brief.md). When a later unit extends a shared file whose size a previous brief's gate bounded, amend that brief with `flywheel log --task <id> --kind amended --brief <path> --note "<why>"`; the amended event explains the change to verify's T1, so validate no longer fails the stale gate. On a dispatched attempt, owns widen with `--kind amended`; gates change only with `flywheel run <task> --delta <file>`. ### 2. Dispatch (canonical `flywheel run`, raw command as fallback) First choice: `flywheel log --task <id> --kind planned --brief <path> --session <your session> --model <your model> [--goal <goal>]`, then `flywheel run <task>` (attaches the brief with `--file`, applies the deny policy, records every event). `flywheel run` is adapter-agnostic: each worker in `.flywheel/config.json` names its adapter (`opencode`, `claude`, or `sim`), and `flywheel run --worker <name>` picks between several configured workers. For parallel units, `flywheel run --worktree <task>` is the default: each unit builds in its own `.flywheel/worktrees/<task>` on branch `fw/<task>`, so parallel workers never share a tree, and flywheel commands run there use the main ledger. The floor's TREE column shows each unit's worktree and base commit. The hand-built **fresh run** below is the OpenCode-specific fallback (e.g. one increment of a brief): or `sim`), and `flywheel run --worker <name>` picks between several configured workers. The hand-built **fresh run** below is the OpenCode-specific fallback. **Never background a dispatch with a bare `&`**: a shell job nobody tracks finishes unseen (issue #393). Use the host's tracked background mode (one that re-invokes you when the command exits), or block on `flywheel wait <task>... [--timeout D]`: it returns when each named task finishes its current (or first) attempt and prints `<task> <attempt> finished reason=<r>` as each lands (exit 0 all clean, 4 any unclean, 8 timeout). `flywheel run <task> --notify CMD` also runs `CMD` when the run returns on any path, with `FLYWHEEL_FINISHED="<task> <attempt> reason=<r> exit=<code>"` in its environment. The OpenCode fallback (e.g. one increment of a brief): verify flags first (`opencode run --help`), label with `--title`, auto-approve with `--auto`, emit JSON so you capture the session id, and add `--variant low` (the default reasoning variant spends 17-30 k reasoning tokens planning one step and caps with nothing written; `--variant low` keeps ~1 k per step). Every dispatch sets `OPENCODE_CONFIG` to the worker permission policy so the worker cannot rewrite the shared tree (ordering and `--auto` behaviour: [references/worker-brief.md §2](references/worker-brief.md#2-dispatch-verify-then-use-the-safe-quoted-file-brief)): ```bash mkdir -p .flywheel/runs OPENCODE_CONFIG=skills/flywheel/references/worker-permissions.json \ opencode run --pure -m "$(flywheel config get model)" --auto --format json --title "<id>-r1" --variant low \ "Follow the attached brief exactly." --file .flywheel/briefs/<id>.txt < /dev/null > .flywheel/runs/<id>.r1.jsonl; rc=$? ``` Session id (every JSONL event carries it): ```bash grep -o '"sessionID":"[^"]*"' .flywheel/runs/<id>.r1.jsonl | head -1 ``` One run file per attempt — `r1` for the first fresh run, `c1`, `c2`, ... for each correction — so per-attempt steps, tokens, and finish reasons stay separate. `< /dev/null` closes stdin: in a non-TTY shell (an agent's shell tool, CI) `opencode run` waits on an open stdin and writes nothing after startup, which looks exactly like a stall. Dispatch from bash (Git Bash on Windows). `--pure` skips external plugins so the worker always gets OpenCode's default `build` agent; a global plugin once swapped the agent and the worker looped on reads without editing. `--auto` is required for non-interactive dispatch: without it the worker hangs on a permission prompt nobody can answer. Capture the exit status (`rc` above) **and the session id emitted in the JSON output** — both are evidence. `--session` accepts only that emitted id; it never takes an invented string. ### 3. Execute The worker runs tests itself. You do not run the tests for it; you judge its results afterward. Expect the first event within about 30 s. If a run is silent after 60 s or ends early, classify it before retrying ([references/worker-brief.md#3-run-states-and-failures](references/worker-brief.md#3-run-states-and-failures)); check the opencode log for provider errors before calling it a stall. A run reading many distinct files with no edits is `exploring`, not stuck — check its plan message before acting ([references/worker-brief.md#3-run-states-and-failures](references/worker-brief.md#3-run-states-and-failures)). Never kill opencode processes by name. To check on running workers, read the floor with `flywheel factory --once` (or `--json` for machine use) instead of asking workers or reading run files by hand; its andon lists the units that need you (silent, stalled, capped, provider-error). A `git-write` signal (the worker moved HEAD, switched branch or stashed) is not an andon state: `flywheel gate` lists it, and `flywheel land` refuses the unit (T9) until you triage it. ### 4. Review — judge evidence, never trust self-report - **Actual exit status** (`rc`): nonzero means the run failed to execute — investigate, don't proceed. Zero means it ran; it does **not** mean the task is correct. - **`git diff`** against the brief: did it do what was asked, nothing more and nothing less? - Watch the recurring traps: gate failures in files the worker does not own, edits outside `owns:`, tests that pass only as a superuser, debug or test-only surfaces still reachable under the production flag, contract prose that disagrees with its tests, and error paths that send a response without returning ([references/worker-brief.md#6-review-exit-status--diff-and-independent-validation](references/worker-brief.md#6-review-exit-status--diff-and-independent-validation)). - **Gauges (`flywheel validate <task>`):** before an inspect pass, run `flywheel validate <task>`. It runs the brief's `gate:` lines on the exact tree, checks **owns:**, and records a supervisor reading bound to that tree hash — exit 0 means every gate passed and nothing is outside `owns:`, exit 5 means a gate failed or an owned-file violation (recorded in .flywheel/evidence). A passing reading is what vouches for the tree *as it is now*; `flywheel inspect` and `flywheel verify` refuse without one. Never pass on the worker's claim alone — run the gauges and let the reading speak. - **Independent validation when needed:** re-run the gates yourself on sensitive or suspicious changes; treat "tests passed" as a claim to be verified, not a fact. `flywheel validate <task>` is that re-run and its evidence. You may run validation commands independently — but send any implementation change to the worker. #### Validating while other units run When multiple units run in parallel on one checkout, repo-wide gates turn red with each unit's half-written code. To avoid false failures, give each unit its own worktree (as flywheel's own build does), or keep one verify worktree: reset it to main's HEAD, clean it, and copy in only the unit's owned files. Then run `flywheel validate <task> --workdir <tree>` to measure gates on a stable tree, followed by `flywheel inspect <task> --verdict pass --workdir <tree>` using the same tree (so T3 finds a passing supervisor reading on that hash), and commit only the unit's files. A flywheel command run inside a unit's `flywheel run --worktree` worktree (`.flywheel/worktrees/<task>`) uses the main checkout's ledger, not the worktree's stale copy. ### 5. Correct or land - Needs changes → send a **correction** to the worker by resuming the **emitted session id** with a delta brief (never implement it yourself, never invent the session id). The resume carries the same `OPENCODE_CONFIG` policy ([references/worker-brief.md §2](references/worker-brief.md#2-dispatch-verify-then-use-the-safe-quoted-file-brief)): ```bash OPENCODE_CONFIG=skills/flywheel/references/worker-permissions.json \ opencode run --pure -m "$(flywheel config get model)" --auto --format json --title "<id>-c<n>" --variant low --session "<emitted-sessionID>" \ "Apply the attached correction to the same task." --file .flywheel/briefs/<id>.delta.txt < /dev/null > .flywheel/runs/<id>.c<n>.jsonl; rc=$? ``` - Correct and gate-passing → surface the result; commit **only** if the user asked you to. - After merging, run `flywheel land <task> --commit <sha>` to record the landing. Before landing, triage the unit's signals (`flywheel feedback`); `--allow-untriaged <reason>` is for a signal you have read and decided not to turn into a learning, and the reason is recorded. When the gauges cannot run but you verified the unit by hand, land it with `--exception "<what you ran and saw>" --session <your session>`; never use it to skip a failing gate. - When CI already measured the merged commit (the gates passed on the PR before the squash merge), record that evidence rather than an exception: `flywheel attest <task> --commit <sha> --evidence <CI run URL> --session <your session>`, then `flywheel inspect <task> --verdict pass --commit <sha> --session <your session>`, then `flywheel land <task> --commit <sha>`. ## References - [references/worker-brief.md](references/worker-brief.md) — the full brief template, concurrency and dirty-edit rules, process/session handles, run states and failures, provider errors, exit-status and diff review, the correction loop, Windows process handling, and the blocker/do-not-take-over protocol.