@dieulc/workflow
Plan↔Build mode with an independent Review Mode reviewer, questionnaire, subagent explore, todos and shadow-checkpoint rewind for Pi.
Package details
Install @dieulc/workflow from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@dieulc/workflow- Package
@dieulc/workflow- Version
0.5.2- Published
- Oct 11, 2026
- Downloads
- 745/mo · 201/wk
- Author
- dieulc
- License
- MIT
- Types
- extension
- Size
- 672.6 KB
- Dependencies
- 0 dependencies · 5 peers
Pi manifest JSON
{
"extensions": [
"./index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
@dieulc/workflow
Plan↔Build mode with an independent Review Mode reviewer, questionnaire, subagent explore, todos and shadow-checkpoint rewind for Pi.
Features
- Plan / Build duality —
Plantool gating (edit/writeblocked in plan),Buildrestores full tools; tab +CtrlAlt+pshortcuts,--workflow-planflag - Review Mode — an independent reviewer agent that runs after Plan Mode finishes (sequential — never in parallel), in its own in-process session. It audits the plan, rewrites it, streams its work into the main transcript, and hands the result to Plannotator. Off by default; see Review Mode below.
- Questionnaire — 1–4 questions with headers/options, dedup freeform aliases, inline
↳ Your answer:editor - Explore (subagents) — parallel
pi --mode jsonsubagents (max 8, concurrency 4),session_shutdownkill,activeSubagents - Todos — plan-faithful todo list (global step refs + statuses + phase groups) that persists across Plan/Build/Default modes.
workflow_todo(list/update/done/pending/add/toggle/sync/clear) with industry-style wholesaleupdate(send the COMPLETE list; omitted items are removed) plus idempotentdone/pending, lenient[DONE:<ref>]markers, throttled Claude-Code-style reminders, a tool-result footer, and a stale ⚠ notice — but no forced turns and no heuristic auto-completion (reference: Claude Code / OpenCode / Codex practice). - Rewind — shadow bare-repo checkpoints per cwd (
agent/checkpoints/checkpoint-<slug>.git),isDirty+createSafetySnapshot+restoreCode - Plannotator bridge —
plannotator:request/review-result/plan-approvedevents, inline markdown handoff (ScrollView→ inline scrollback),getUtcDatePrefix/normalizePlanPathfor.pi/plans/<UTC-date>-<slug>.md - Model roles — Planner/Explorer/Builder roles with one model + thinking level each (
roles.ts,~/.pi/agent/roles.jsonv2)./planswitches to the planner model,/buildto the builder model,exploresubagents use the explorer model./roleopens a tabbed picker (scoped models first,Ctrl+Ofor all). - Model profiles — named combinations of role models (
profiles.ts,~/.pi/agent/profiles.jsonv1). Create a profile per provider or per budget, switch with/profile— activation is session-scoped, so two concurrent sessions can run different profiles. With no profile active the roles.json flow applies unchanged. See Model Profiles.
Install
pi install npm:@dieulc/workflow # latest
pi install npm:@dieulc/workflow@0.1.0 # pinned
pi install -l npm:@dieulc/workflow # project-local (.pi/settings.json)
pi -e npm:@dieulc/workflow # try without installing
Manage it with pi list, pi update --extensions, pi remove npm:@dieulc/workflow. Requires Pi on Node ≥ 22.19; the Pi core packages (@earendil-works/*, typebox) are provided by Pi at runtime and declared as peer dependencies.
Commands
| Command | Description |
|---|---|
/plan / /workflow |
Enter plan mode (tool gating + switches session model to planner model high thinking) |
/build |
Enter build mode (full tools + switches session model to builder model medium thinking) |
/rewind |
Pick checkpoint to restore |
/todos |
Show todo list |
/btw |
Queue note for next turn |
/role |
Config screen: one model + thinking per role (planner/explorer/builder/reviewer tabs, custom roles included). CLI: list, set <role> <provider/model> [thinking], use <role|auto>, add/remove/reset |
/profile |
Switch or manage model profiles (named role-model combinations). Picker: activate / edit / rename / delete / New profile… / Default…. CLI: list, show <name>, use <name|none>, create <name>, edit <name>, rename <old> <new>, delete <name>, default [name|none] [global|project] |
/review-mode [on|off] [global|project] |
Enable/disable Review Mode (default off). status prints the effective config, naming the settings file that supplied the timeout |
/review-mode timeout <seconds|unlimited> [global|project] |
Set or show the per-pass time cap (unlimited/0 = no cap). Writes to global settings by default |
/review-prompt [global] |
Edit the reviewer's guidance (defaults to "align with the existing framework and industry best practice") |
/review-status |
Show review phase, model, plan hash, findings and any error |
/review |
Run one review round against the current plan file now |
/commit |
Commit staged changes with an AI-summarized message (-a: stage tracked changes first). Confirmation-gated; the confirmation arms the commit exemption for that run |
/commit-policy [never|ask|allow] [global|project] |
View or set the commit policy (default never: the agent cannot git add/commit/push — you commit, or use /commit) |
Model Profiles
A profile is a named combination of role model settings — one {provider, id, thinking} each for planner, explorer, builder and reviewer. Profiles layer on top of Model Roles: role identity (tools, system prompts, custom roles) stays global in roles.json; a profile only overrides models while it is active.
/profile # picker: activate / edit / rename / delete / New profile… / Default…
/profile create budget # name it, then pick model + thinking per role (same flow as /role)
/profile use budget # activate for THIS session (none = back to the roles.json flow)
/profile edit budget # re-open the tabbed model editor
/profile rename budget cheap
/profile delete budget # confirm dialog; deleting the active profile falls back to base roles
/profile default budget # workflow.defaultProfile — new sessions start on it (global|project)
Semantics
- What a profile overrides: model + thinking for the four built-in roles only. Tools,
systemPromptAddendumand custom roles are global (roles.json) and are never touched by a profile. Partial profiles are allowed in hand-edited files — roles a profile does not name fall back to the base role model. - Activation is session-scoped.
/profile usechanges only the current session and rides the session'sworkflowentry, so/resumerestores it and two concurrent sessions can run different profiles — session 1 on a cheap provider, session 2 on a premium one. There is deliberately no global "active profile" file. - Optional default:
workflow.defaultProfile(see Settings) seeds sessions that never made an explicit choice; an explicit/profile use <name|none>always wins for that session. - Effect:
/planswitches to the profile's planner,/buildto the profile's builder,exploresubagents run on the profile's explorer, and Review Mode's reviewer session uses the profile's reviewer (planner fallback preserved). Activating while in plan/build re-applies the mode model immediately; in Default mode nothing switches until the next plan/build/explore/review use — the manual chat model is never touched by activation. /rolekeeps editing the base roles. With a profile active, the/rolepicker shows a notice and/role listprints the profile's overrides;/profile edit <name>is the way to change what actually runs.- Fallback: no profiles (or none active) → the roles.json flow, identical to before profiles existed. A profile referencing a model missing from the registry is warned about at save time and refused at switch time ("staying on current model"), same as
/role.
Why: mix models from different providers in one workflow (frontier planner + cheap explorer), keep one profile per provider to control budget per provider, run concurrent sessions on different providers to work around per-provider concurrency limits, and exploit cheaper starter plans across several providers.
Profile config lives in ~/.pi/agent/profiles.json v1 (user) with optional per-project override at <cwd>/.pi/profiles.json (per-profile replace, project wins). Names are case-insensitively unique, ≤ 40 chars, no whitespace (keep them CLI-addressable), and none/auto/off are reserved (off is a /profile use deactivate keyword, so a profile can never be named it). A missing file is the normal empty state; a corrupt or wrong-version file is never rewritten (warning + in-memory empty config).
Argument completion follows pi's full-argument contract (the same one pi's /mcp uses): accepting a suggestion replaces the entire typed argument text, so completing /profile use Syn-␣ inserts /profile use Syn-Deepseek-… with the subcommand intact — it never collapses to /profile <name>. The dropdown closes (and the first Enter submits) once the typed token is already an exact, unique match. This applies positionally to /profile (use|edit|show|delete|rename <old>|default <name> [global|project]), /commit-policy (never|ask|allow [global|project]) and /role (set <role> <provider/model> [thinking], use <role|auto>, remove <name>).
Review Mode
An independent second agent supervises the plan before you see it. The flow is strictly sequential: Plan Mode finishes, then the reviewer runs, then the reviewed plan goes to Plannotator.
Plan Mode (main session) ──plan file──▶ Review Mode (child session, sequential)
│ rewrites the plan
▼
Plannotator webview ◀── reviewed plan + `## Review changes`
│ approve ──▶ Build Mode
└─ feedback ──▶ Plan Mode revises ──▶ Review re-reviews
- Approve in Plannotator → Build mode; both Plan and Review shut down.
- Feedback → Plan revises, Review re-reviews the revision, Plannotator reopens.
- Any failure (missing model, unauth'd provider, abort, error) → the author's plan is handed over unchanged with a warning. Review Mode never blocks the plan. A pass whose run ends without a
review_submit_plansubmission fails immediately (the settle race), so a silent reviewer cannot hang the handoff even with no time cap.timeoutMsis off by default (0= unlimited); set a positive value to re-enable the outer safety net, and a pass that outlives it is aborted — though a plan submitted before the deadline is always kept.
Enabling
/review-mode on # global (~/.pi/agent/settings.json)
/review-mode on project # this repo only (<cwd>/.pi/settings.json)
/review-mode status
/review-mode timeout unlimited # remove the per-pass cap (global)
/review-mode timeout 600 project # 10-minute cap for this repo only
Turning the toggle off stops the reviewer and restores the single-agent plan/build flow exactly as it was. Nothing in Review Mode runs while it is disabled.
/review-mode status reflects what the current process loaded, so /role changes and extension edits are picked up on the next /reload (or a new session). After /reload, the reviewer model, the effective timeout and its settings file (and the absence of the old flow row) all reflect the current files on disk.
The reviewer's model
Review Mode runs in its own session with its own model, taken from the reviewer role (/role). The default seed is deliberately a different model from the planner so the review brings an independent perspective — a reviewer that shares the planner's model and thinking tends to share its blind spots.
/role set reviewer opencode-go/muse-spark-1.3-contributor xhigh
If the reviewer role has no model, Review Mode falls back to the planner model and warns rather than silently doing nothing.
Roles load from ~/.pi/agent/roles.json (with a <cwd>/.pi/roles.json project override) at session start and again whenever either file changes on disk, so a /role change made in another window is picked up without /reload. A missing, unreadable, or legacy roles.json is never overwritten: the built-in defaults are used in memory and a warning naming the file is shown, leaving it intact. /review-mode status, the mode band and the reviewer run all read through the same loader.
Reviewer guidance
.pi/review-prompt.md (project) wins over ~/.pi/agent/review-prompt.md (global), which wins over the built-in default. Edit it with /review-prompt.
The built-in default asks the reviewer to, in priority order: align with the existing project framework, follow industry best practice, verify every requirement maps to a plan step, check the plan is executable without guessing, and prefer simplicity. Repository context files (AGENTS.md, CLAUDE.md, …) also load into the reviewer's system prompt, which is where most framework alignment actually comes from.
This guidance lives only in the reviewer's session. Plan Mode receives a single neutral line ("a reviewer will audit this plan") and can never see the reviewer's prompt, findings, or transcript.
Permissions
Plan Mode and Review Mode share one role-based command policy (permissions.ts). Review Mode has Plan Mode's permissions: read-only inspection plus test/lint/typecheck runs, and a single plan-write path.
| Tool | Purpose |
|---|---|
read, grep, find, ls |
Inspect the repository |
review_bash |
Shell access on the same role policy Plan Mode uses: read-only inspection plus test/lint/typecheck runners (npm test, npm run lint/typecheck, eslint, tsc --noEmit, node --test, pytest, cargo clippy, …). No file redirects (2>/dev/null and 2>&1 are allowed), no sed/awk, no rm/mv/cp/mkdir/touch, no sudo, no package installs, no builds, no git add/commit/push |
review_explore |
Read-only reconnaissance subagents on the explorer role's model, capped by exploreBudget per round |
review_submit_plan |
The only write path: validates and saves the revised plan to Plan Mode's own .pi/plans/<UTC-date>-<slug>.md |
review_pass_done |
Ends a pass with a verdict (refused if nothing was submitted) |
The policy classifies commands into three classes and maps them to roles:
| Role | Command class | Write tools | Used by |
|---|---|---|---|
| planner | read-only + verify | .pi/plans/ only |
Plan Mode |
| reviewer | read-only + verify | review_submit_plan only |
Review Mode |
| explorer | read-only | none | explore / review_explore subagents |
| builder | full (unrestricted) | all | Build Mode / normal sessions |
- read-only — inspection only (
ls,cat,rg,git log/blame/ls-files/show-ref,npm run/yarn runwith no script argument (lists them),docker logs,journalctl,base64,ss,netstat,lsof, …).sed/awkare deliberately not read-only — their programs are code — so they are Builder-only. - verify — the above plus
npm test,npm run lint|typecheck|check,eslint,tsc --noEmit,node --test,npm run/node --runwith an allowlisted script,pytest,mypy,go vet,cargo clippy, … Runners that rewrite (--fix,--write,-u/-w/--update,--test-update-snapshots,--watch) — including through a wrapper such asnpm test -- -u— and arbitrary output-path flags (eslint -o,jest --outputFile) stay blocked, as do build commands (npm run build). - full — no gate (Builder and normal sessions only).
Compound commands are checked segment-by-segment with quote awareness, so git status && npm test and npm test 2>/dev/null | tail -50 work while curl … | sh, $(…), backticks, heredocs, sed/awk and file redirects to anything other than /dev/null are refused. Refusals name the rule that failed (explainCommandRefusal), so a blocked command explains what to change. Explorer subagents run as separate pi processes and carry their role in PI_WORKFLOW_ROLE, so the same policy gates their bash too. The powershell tool is refused for every non-builder role, since its syntax cannot be classified by the POSIX allowlist.
The built-in bash, edit and write tools are never given to the reviewer at all.
The child session is created with an explicit tool allowlist: the read-only built-ins plus every reviewer tool above. That union is load-bearing — Pi's createAgentSession({ tools }) option is a global allowlist that filters custom tools too, so a built-ins-only list silently removes review_submit_plan and makes every review end in a fallback. The runtime therefore reads getActiveToolNames() straight after creation and refuses the session (naming the missing tools) rather than starting a review that cannot succeed; the active tool set is also logged once per session.
Two passes
- Review + rewrite — audits the plan against the repository and the guidance, records findings, submits the revised plan.
- Self-verify — re-reads the original and the rewrite, confirms every accepted finding was actually addressed, and reports anything that was silently dropped. This pass exists because a rewrite can lose a requirement the author cared about.
Pass 2 is skipped when verify is false or when pass 1 found nothing at severity ≥ 5.
Every change is visible
A ## Review changes appendix is appended to the plan automatically (the reviewer is told not to write it), grouped by tier with file:line, severity, confidence and rationale:
## Review changes
reviewer `opencode-go/muse-spark-1.3-contributor` · verdict **revise**
**Findings:** 2 addressed, 1 rejected, 1 deferred
### Addressed
**Critical**
- 🔴 `src/export/csv.ts:1` — Plan ignored the Result<T,E> convention _(severity 9, confidence 85%, framework-alignment)_
- Every sibling exporter returns Result; the plan returned a bare string.
### Deferred
- ⏸ No rollback path _(severity 7, confidence 80%)_
Review in the main transcript
Because the two agents run sequentially — Plan Mode finishes before the reviewer starts — their transcripts can never interleave, and there is no overlay or second pane to navigate. Everything shows up in Pi's normal transcript.
Mode changes are reported by Pi's footer status (⏸ plan / ▶ build, plus todo progress): switching with Tab or Ctrl+Alt+P is silent and adds nothing to the transcript, while /plan, /build and /default print a single confirmation line. Only the review phase adds bands, and the reviewer's stream is rendered in the review color:
| Entry | Color | Meaning |
|---|---|---|
▌ REVIEW MODE |
accent | A review round is running (round + model shown) |
| action headers | muted | One line per reviewer tool call with its key argument (▸ read src/app.ts, ▸ review_bash: git log -5). Assistant prose and thinking are not streamed, so the transcript stays close to Plan Mode's; status lines are muted |
▌ REVIEW DONE |
success / warning | Round finished, with the verdict or the fallback reason |
▌ REVIEW SUMMARY |
success / warning | The submitted plan's verdict, plan path and findings (severity-colored) |
These are display-only custom entries (pi.appendEntry + pi.registerEntryRenderer): they are durable in the transcript and survive /reload, but they never enter the LLM context, so Plan Mode and Build Mode cannot see the reviewer's work. Consecutive identical bands are suppressed (the gate is seeded from the newest band on reload), so cycling modes or re-running a review cannot grow the transcript. Long reviews are coalesced (flushed roughly every 400 ms) and capped, with a one-line notice pointing at /review-status when output is truncated.
Where your next message goes
Input follows the active mode, decided automatically — there is nothing to focus or toggle:
- Plan Mode → the message goes to the plan agent.
- Review Mode (a review round is in flight) → the message is delivered to the reviewer's session (steered if it is mid-pass) and echoed in the transcript as
you → reviewer. The plan agent never sees it. - Neither (including while the plan sits in Plannotator) → the message goes to the plan agent, so you can still refine the plan.
Delivery to the reviewer is best-effort: if it fails, the message falls through to the plan agent rather than being swallowed.
Status line. While a review is running, the status widget above the editor shows the live phase (🔍 review · pass 2/2 · 3 findings · ● verifying).
Isolation
The reviewer is an in-process child session created with createAgentSession, and it is isolated on purpose:
DefaultResourceLoader({ noExtensions: true })— the child cannot load this (or any other) extension, so Review Mode cannot recurse.SessionManager.inMemory(...)— no session file is written; the reviewer never touches the parent's history.- Its own
ModelRuntime— no contention with the parent's model state. - Plan Mode's conversation is passed as pruned prompt context (oversized tool results elided, workflow-injected markers dropped, nothing after the plan write), not by sharing the session.
An environment marker (PI_WORKFLOW_REVIEW_CHILD) is set only for the duration of session creation and restored immediately — the child runs in the same process, so leaking it would affect a later /reload.
Provider seeding
The isolation above has one consequence: a provider that exists only because an extension registered it in the parent session (pi.registerProvider(...)) does not exist in the reviewer's freshly created runtime — every review round would fail with No API key found for <provider>. even though /login had stored a valid credential.
Pi composes providers from three sources: the built-in catalog, the static ~/.pi/agent/models.json, and extension registrations on the parent runtime. Only the last one is parent-only. Review Mode bridges it with a present-probe, not layer names (review-providers.ts):
- Probe the parent for a provider-registration surface: the session's model registry (
ctx.modelRegistry) first, then the underlying runtime (ctx.modelRuntime). Both expose the same three accessors; a future Pi surface is picked up by adding one entry to the probe list. - Compare with the child. Providers the child already composes are skipped. The built-in catalog and
~/.pi/agent/models.jsonare loaded by every runtime by construction, so for them the seeder is a guaranteed no-op — and any future globally-composed provider source is skipped automatically, with zero layer-name drift. - Seed the difference, native-first. For every provider the parent knows and the child does not, the seeder registers the parent's provider object when there is one, otherwise its config — each in its own try/catch, so a broken unrelated provider can never block a review. Idempotent: already-seeded ids are skipped on a second pass.
- Preflight auth, provider-agnostically. After seeding and before the session is created, the reviewer runtime is asked
hasConfiguredAuth(provider) || checkAuth(provider)for the reviewer model's own provider. On absence the review fails fast with the provider name, the seed report, and the universal escape hatch below — never the bare mid-review error.
Every bridged method is feature-detected: on a Pi version without a given accessor the seeder degrades to "nothing to seed" and the preflight reports the escape hatch instead of crashing.
The universal escape hatch (works for any provider, even without this fix): ~/.pi/agent/models.json is loaded by every runtime, including the reviewer's:
{
"providers": {
"<provider>": {
"baseUrl": "https://…/v1",
"apiKey": "$MY_API_KEY",
"api": "openai-completions",
"models": [
{
"id": "<model-id>",
"name": "<display name>",
"reasoning": true,
"input": ["text"],
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 },
"contextWindow": 200000,
"maxTokens": 65536
}
]
}
}
}
A stored credential (/login <provider> → auth.json) always wins over the apiKey template, so $MY_API_KEY needs no environment variable while you are logged in. Extensions that register the same provider at runtime keep working: their live model list replaces the static one in the parent, so nothing regresses there.
Configuration
Settings live under workflow.reviewMode in ~/.pi/agent/settings.json (global) or <cwd>/.pi/settings.json (project). Project values win field-by-field over global. Writing them preserves every other key in the file.
Commit policy
By default the agent cannot touch your git history: the tool gate deterministically blocks git add, git commit and git push — including compound commands (git add -A && git commit -m x), global-option forms (git -C path add) and powershell wrappers. You commit manually, or run /commit for an AI-summarized, confirmation-gated commit. Read-only git (status, log, diff, branch -a, config --get, …) stays free, and plan mode keeps its stricter read-only rules regardless.
The default exists because prompt-only rules are soft: models habitually commit per plan step, and "never commit unless asked" instructions in CLAUDE.md-style files are famously ignored (anthropics/claude-code#58883, #68724, #7029). Claude Code, Codex CLI and OpenCode all treat agent-initiated commits as something to gate or approve — so the policy here is enforced where commands execute, not in prose:
| Agent | Default commit behavior | Control |
|---|---|---|
| Claude Code | never auto-commits | CLAUDE.md rules (soft, documentedly ignored) + PreToolUse hooks (hard) |
| Codex CLI | approval-gated git mutations | sandbox + --approval-mode |
| OpenCode | no auto-commit | ordered permission rules (git push * → deny), global + project config |
| Aider | auto-commits every edit | --no-auto-commits, --commit-prompt (the configurable-commit pole) |
Modes
| Mode | Meaning |
|---|---|
never (default) |
git add / git commit / git push are blocked with a policy reason that tells the model to leave changes uncommitted for review |
ask |
The agent may commit; every git commit / git push pops an Allow once / Deny dialog (headless sessions fail closed). git add stays free — staging alone pollutes nothing |
allow |
Automatic per-step commits. While a plan todo list is active the extension itself makes exactly one commit per completed step (at the workflow_todo completion boundary, before the model starts the next step; subject step N: <step text>), and the model cannot run git add/commit/push at all. Outside a plan (no todos) the model may commit as before. /commit always works |
The status line always shows the active policy (▶ build 2/5 · commits:never), so you can see at a glance what the agent may do to history.
/commit under any policy
/commit works under never and allow-during-a-plan because it is the user's own request. The flow is multi-turn (read the staged diff → confirm the message in a questionnaire → commit), so the exemption is armed at the user's confirmation, not when the flow starts:
/commitrecords a pending flow and injects its agent turn. Nothing is exempted yet.- When the confirmation questionnaire is answered positively (✅ Commit, or a custom message), the extension arms a scoped commit authorization. Choosing ❌ Cancel clears the flow and arms nothing.
- The authorization exempts only
git commit—git addandgit pushstay blocked — and is valid until the run settles (agent_settled), additionally bounded by a 10-minute TTL and a 10-commit cap. It is also dropped onsession_shutdownand on rewind (session_tree).
The confirmation step is required: with no positive answer no authorization exists and the policy keeps blocking. -a staging happens extension-side before the flow, outside the tool gate.
/commit-policy # status: mode, which scope supplied it, both settings paths
/commit-policy never # set globally (~/.pi/agent/settings.json)
/commit-policy allow project # this repo only (<cwd>/.pi/settings.json)
Known limits (accepted, same trade-off OpenCode/Claude Code make): a configured git alias (e.g. ci → commit) is not recognized, and exotic obfuscations (sh -c 'git commit') are caught only when the plain text survives. Plan-mode and subagent read-only gating are unaffected by the policy; the shadow-checkpoint rewind commits live in a separate bare repo and never appear in your history.
Settings
Role config lives in ~/.pi/agent/roles.json v2 (user) with optional per-project override at <cwd>/.pi/roles.json (per-role replace). Each role holds exactly one {provider, id, thinking}. v1 modelPool files are discarded and reseeded on load. Seeded with defaults on first run. Respects theme, modelRegistry, and plannotator phases from agent/plannotator.json.
Profile config lives in ~/.pi/agent/profiles.json v1 (user) with optional per-project override at <cwd>/.pi/profiles.json (per-profile replace). Each profile holds {name, description, roles} where roles maps any of the four built-in role names to one {provider, id, thinking} — see Model Profiles.
The default profile lives under workflow.defaultProfile (global/project precedence like the other workflow settings):
{
"workflow": {
"defaultProfile": "cheap"
}
}
/profile default <name|none> [global|project] writes this key read-modify-write, preserving every other settings key. Sessions that never picked explicitly (fresh or resumed) re-read it at every start, so changing the default applies to them; an explicit /profile use in a session pins that session until changed.
Review Mode settings live under workflow.reviewMode:
| Key | Default | Meaning |
|---|---|---|
enabled |
false |
Master toggle. When false, every Review Mode code path is a no-op |
rounds |
1 |
Max Plan↔Review rounds per user-feedback cycle. Reached the cap → the plan is handed over with an ## Unresolved findings appendix |
passes |
2 |
1 = review+rewrite only; 2 = review, then self-verify |
verify |
true |
Run the self-verification pass |
fallbackOnError |
"skip" |
skip hands over the author's plan on any failure (block is reserved) |
exploreBudget |
3 |
Max review_explore subagents per round (cost ceiling) |
timeoutMs |
0 |
Optional wall-clock cap for a whole pass, run included. 0 = unlimited (default); a pass then ends on review_pass_done, run settle, abort or error. A silent reviewer is still caught immediately by the settle race. /review-mode status prints the value and the settings file that set it, and /review-mode timeout <seconds|unlimited> changes it without editing JSON |
{
"workflow": {
"reviewMode": { "enabled": true, "rounds": 1, "passes": 2, "exploreBudget": 3 }
}
}
"reviewMode": true is accepted as shorthand for { "enabled": true }.
Commit Policy settings live under workflow.commitPolicy (same global/project precedence, project wins field-by-field):
| Key | Default | Meaning |
|---|---|---|
mode |
"never" |
"never" = gate blocks git add/commit/push; "ask" = each git commit/push asks first; "allow" = automatic per-step commits (extension-driven, one per completed step; the model cannot commit during a plan) |
{
"workflow": {
"commitPolicy": { "mode": "ask" }
}
}
"commitPolicy": "ask" (a bare mode string) is accepted as shorthand for { "mode": "ask" }. Booleans, numbers and unknown modes fall back to the default. /commit-policy writes this key read-modify-write, preserving every other settings key.
How it works
See index.ts, checkpoint.ts (shadow bare-repo), commit-policy.ts (commit policy config, coercion and write — no runtime imports, unit-testable), utils.ts (safe-command gating, todo/plan extraction, plan hashing), roles.ts (role registry, one model per role), profiles.ts (profile registry, the model overlay and the workflow.defaultProfile setting — no Pi runtime needed, unit-testable), and the Review Mode modules: review.ts (config, prompt, state, transcript, child session — no runtime imports, so it is unit-testable), review-providers.ts (parent→reviewer provider seeding and the provider-agnostic auth preflight), review-tools.ts (the reviewer's tool set), review-runtime.ts (round orchestration), review-ui.ts (the main-transcript entries: line buffer, formatters, renderers). Hot-reload via /reload when placed in ~/.pi/agent/extensions/workflow/ or loaded via pi.extensions manifest.
Todo protocol (industry-aligned)
- Canonical refs: every todo row is numbered globally
1..N; those numbers are the only refs the model ever needs. Refs are canonical positions — they are recomputed as1..Nafter every mutation (plan merge,update, session restore), so the pinned widget, handoff messages, tool output andworkflow_todorefs always show the same numbers (no 11–20 drift). The plan's per-phase labels (e.g.Phase B: 1..3) are preserved only as display metadata ((plan label B2)). Integer refs resolve by canonicalref→ positionalstep→ uniquelabel; the resolver also acceptsGroup/Label,Label@Group,#Nand unique text (≥8 chars). - States:
pending→in_progress→completed(+cancelled). Exactly onein_progressat a time (the harness normalizes extras).completedis kept as a derived legacy field for old sessions/external readers. - Update:
workflow_todo {action:"update", todos:[{ref?, text?, status?}]}replaces the whole list — TodoWrite/todowrite/update_plan semantics. Arefis resolved by canonical ref, then normalized text, then leniently by plan label / group-qualified ref / unique text; a ref that had to be resolved leniently (or matched nothing and was added) is reported underWarnings:in the tool result.done/pendingare idempotent single-item sets;toggleis a convenience flip;addappends an ad-hoc item;syncre-extracts the plan file, matches steps by exact text then by plan label (so model-rewritten wording reunites instead of duplicating), preserves agent-added items (reported askept N agent-added) and never un-completes (status rank merge);clearempties. - Reminder cadence (Claude Code-style throttle): a hidden
[TODO LIST]context block is injected only whenturnsSinceLastTodoWrite ≥ TURNS_SINCE_WRITEandturnsSinceLastReminder ≥ TURNS_BETWEEN_REMINDERS(=3). Unresolved[DONE:…]refs from the previous turn bypass the throttle once so the model learns the right numbers. A one-line[TODO …]footer is appended to the first successfuledit/write/bashresult of each turn. - Stale notice (no forced turns): if a run mutated files but recorded no todo progress, the user gets one
⚠status marker + actx.ui.notify(30 s dedupe); plan-file- [x]ticks are reconciled atagent_endin all modes, skipped when the plan content is unchanged (path+hash guard). No auto-completion — the list is never marked done based on intent. Legacy lists that already contain duplicate rows are not auto-merged; run/todos clear(or approve a new plan) to reset.
Troubleshooting
No API key found for <provider>. in a Review Mode round
The transcript shows Review Mode: … (could not prompt the reviewer: No API key found for <provider>.). This means the reviewer session could not compose auth for the provider — your login itself is fine (Plan/Build keep working off the same auth.json).
The reviewer runs in an isolated session without ambient extensions by design (see Provider seeding), so a provider registered only by an extension in the parent session is invisible to it unless seeded, and its auth must resolve on its own. Since 0.3.2 the provider is seeded automatically and a missing auth fails fast with a message naming the provider, what the seed found, and the escape hatch.
Fixes, in order of preference:
/login <provider>— a stored credential in~/.pi/agent/auth.jsonis read by every runtime, including the reviewer's. No environment variable needed.- Static provider in
~/.pi/agent/models.json— the universal workaround (works even on an installed version without the seeding fix): add the provider under"providers"withbaseUrl,apiKey: "$MY_API_KEY",apiand itsmodels(template in Provider seeding). export MY_API_KEY=…— only needed when the provider config references"$MY_API_KEY"and no credential is stored.
After editing models.json, run /reload or restart pi so every runtime picks it up.
the reviewer's provider failed after N retry attempt(s): … in a Review Mode round
The warning reads review skipped (the reviewer's provider failed after 3 retry attempt(s): 429 Too Many Requests …) and the reviewer pane shows the provider's error text live. This is the reviewer's model provider failing mid-run — its API returned errors until Pi's auto-retry budget (settings.retry.maxRetries, default 3) ran out, so the run settled without ever reaching review_submit_plan. It is not a Review Mode fault and nothing was written; the author's plan is handed over unchanged.
Earlier versions mislabeled this as the reviewer finished without submitting a plan — it has no write path…; since 0.3.3 the real provider error is captured from the reviewer session and reported here, in the pane, and via /review-status (the old message now appears only when the reviewer genuinely settled without calling review_submit_plan).
What to do:
- Check the provider — the truncated message is the provider's own reply (rate limit, quota, overloaded, auth). Retry the review with
/reviewonce the provider recovers; a review that dies this way can simply be re-run. - A long review shares one API key with the parent — planner + reviewer often hit the same key, so a long review can exhaust rate limits. Stagger heavy Plan Mode work or raise the provider's quota.
- Switch the reviewer model if the failures recur —
/role set reviewer <provider/model>(see Review Mode); pick a model whose provider is currently healthy.
Development
cd agent/extensions/workflow
npm install
npm run typecheck # tsc --noEmit (covers every source file, including the review modules)
npm test # node --test utils.todo.test.ts permissions.test.ts roles.test.ts profiles.test.ts review.test.ts review-ui.test.ts commit-policy.test.ts
Plain TypeScript, no build step — Pi loads index.ts directly (via jiti). From the repo root, npm run verify checks this package's manifest, tarball contents, tests and load path before a release. Live reload: edit the .ts file and run /reload in Pi.
License
MIT — see LICENSE.