پرش به محتوا

What you control

Every knob — the launch defaults and the ladder's caps and toggles — plus the full command reference for the engine and its helpers.

مستندات به انگلیسی نوشته شده است. بقیهٔ سایت به فارسی خوانده می‌شود.

This is the part most people miss. The plan is a plain markdown file, and a handful of lines in it are read by the engine. Change a line, change the behaviour. You can ask Claude for any of these in plain language at plan time, or edit the file yourself afterwards.

The control surface at a glance

You want to…Put this in the planWhere
Choose the executing model**Target model:** claude-opus-5## Session budget
Change how much work fits a session**Budget:** ~200K weight/session## Session budget
Use a different model for one phase- **Model:** haikuthat ### Phase N block
Say how big a phase is- **Size:** S | M | Lthat ### Phase N block
Turn QA on**QA gate:** on## Session budget
Turn QA off explicitly**QA gate:** off## Session budget
Commit to a specific branch**Branch:** feature/checkout## Session budget
Force skills into every session**Skills (every session):** `design-system` ## Session budget
Name the MCP servers every session needs**MCP servers (every session):** `context7` ## Session budget
Add a server only one phase needs (UNIONED with the plan's)- **MCP:** `github` that ### Phase N block
Park rather than continue when a server will not connect**MCP policy:** require## Session budget
Carve one phase out of a plan-wide require- **MCP policy:** continuethat ### Phase N block
Turn QA off (or on) for ONE phase- **QA:** off | on — the phase's word beats the plan'sthat ### Phase N block
Say which repos a phase touches (its scope — what decides concurrency)the Repos column; empty means all, which makes the phase run alone## Phase graph table
Say a phase depends on othersthe Depends on column## Phase graph table
Block a phase behind something external*(GATED)* + - **Gates (must clear first):** … (numbered steps for human gates)that ### Phase N heading
Say who can clear that gate- **Gate-check:** ai <check> (a session — prefer) | manual <who> (a person) | date 2026-09-01 (itself)that ### Phase N block
Clear / approve a gate (any kind)the phase page's Gate card, or gate-approve.sh <slug> <N> --by <who>the console · docs/handoffs/<slug>/gate-status.md
Retire a plan nobody will finishstatus: abandoned + a reason — set it with close-plan.shthe plan's frontmatter
Bring a retired plan backclose-plan.sh <slug> --reopenthe plan's frontmatter
Put a console run on one work branchSettings ▸ Automation ▸ Branch (or the launch form)the console
Give a run its own checkout, so two plans in one repo drive at oncethe launch form ▸ Give this run its own checkout (needs the work branch)the console
Say what a finished branch becomesthe launch form ▸ When the plan completes — PR · merge queue · integration · keepthe console
Bring a stack up before a phase can be proved- **Setup:** in that ### Phase N block (or one **Setup (every phase):** line in §Session budget)the plan
How long a session may sit on its OWN background job before the console parks itSettings ▸ Automation ▸ stallLocalJobMs (45 min shipped)the console
Bound the console's own checkouts (cap, setup command, .env copy, branch reclaim)Settings ▸ Automation (shown only while isolation is on)the console
Delete a run's pe/* branches once their PR has mergedSettings ▸ Automationthe console
Open a PR when the plan completesSettings ▸ Automation ▸ Open a PR (needs the work branch)the console
Queue runs whose repos overlapSettings ▸ Automation ▸ Repository guardthe console
Let the console heal a stopped run by itselfSettings ▸ Automation ▸ Auto-recover halted runs (and the ladder card's toggles)the console
Bound what it may spend by itselfSettings ▸ Automation · the ladder ▸ Caps (rungs and dollars per phase / run / day)the console
See who is in the repository, and queue behind themSettings ▸ Automation ▸ Session presence ▸ Install (or phase-console install-hooks)the console · ~/.claude/settings.json
Send every announcement to a chat channelSettings ▸ Notifications ▸ Channels (needs --allow-webhooks)the console · docs/webhooks.md

Console automation defaults

The console keeps its automation preferences in Settings ▸ Automation (stored per instance under ~/.config/phase-console/). The first eight are the opening values for every launch surface — the run form, the phase launcher, the recovery and QA dialogs; each launch can override them for itself, and the preferences are where "for all plans" is said once. The rest — the ladder card — say what the autopilot may do by itself once a phase stops short, and how much of it (The loop is the specification). Every one round-trips through POST /api/prefs.

PreferenceDefaultWhat it does
Attach default skillsoffSeed the machine's --default-skills list into new runs, and pre-tick it in launch dialogs.
QA by defaultoffLaunch surfaces open with the QA gate ticked, so starting a run activates QA for the plan (needs --allow-writes; earlier finished phases are backfilled waived).
Branchcurrent branchWork branch per run puts every console-minted session of a run on one plan-wide branch, pe/<slug> — created from the default branch if missing, reused by later phases.
Open a PR at completiononWork-branch runs only: the plan's last phase is told to push pe/<slug> and open a PR per scoped repo. For that run — and only that run — bare git push moves from the deny wall to an approval card, and gh pr create stays a card even under the trusted profile; force-pushes and --delete stay denied outright.
Repository guardonThe scheduler queues runs whose repository scopes overlap. Off: overlapping runs may start together, and a work-branch run sharing a repo with a live one is told to work in a linked git worktree instead of switching the shared checkout.
Take the run branch back from a clean checkoutClean onlyIsolated runs only: a checkout sitting on pe/<slug> with nothing uncommitted in it is switched to the default branch so the run can have its own tree. A checkout holding work — including a file nothing ever added — is never touched, and the run refuses branch-in-use naming those files. No branch is deleted and no ref moves. Never turns it off.
Delete merged run branchesonAfter a pr settle, a run's pe/<slug> and pe/<slug>-p<N> are deleted once the trunk contains them. Always git branch -d — git's own refusal for an unmerged branch is the safety, and -D exists nowhere in this codebase — so a squash-merge the local trunk has not fetched keeps its branch until the next drive.
When an MCP server is unavailableContinue and warnThe phase boards without the servers that would not answer, its prompt names them and tells it to record the gap under Outstanding as an errand, and you are told once per run per server. Park the phase is the older behaviour — use it when the work genuinely cannot proceed, remembering that a run whose ready phases have all parked has nothing left to start. Settable per run in the launch dialog and per phase in the run's phase matrix; a plan's own **MCP policy:** require outranks both the run choice and this preference.
Auto-recover halted runsonNew runs opt into the ladder (autoRecoverByDefault): a stopped phase is classified and climbed by itself, within the caps below; off, every stop is yours.
Continue runs a recovery fixedonWhen a recovery leaves the board reading fixed, the run resumes by itself (autoContinueRecovery).
Rungs per phase · Spend per phase3 · $100The ladder's per-phase caps (ladderPerPhaseRungs, ladderPerPhaseUsd) — how many rungs one phase may climb and what they may cost before its errand is written.
Rungs per run · Spend per run10 · $400The per-run caps (ladderPerRunRungs, ladderPerRunUsd); the one automatic budget raise stays inside the USD cap.
Spend per day$600Across every run this console drives in a day (ladderPerDayUsd).
Sweep every5 minHow often the convergence loop re-reads every open plan even when nothing happened (convergeEveryMs; 0 turns the timer off — boot, a docs change and the minute after a stop always run a pass).
Park on a required MCP server for30 minA phase parked by the require policy continues without the server after this long, an errand recorded (mcpRequireTimeoutMs; 0 waits for it to heal, however long).
Raise a spent run budget once by25 %The resource ladder's one budget raise, within the per-run cap (budgetAutoRaisePct; 0 never raises).
Unblock attemptsonA handoff marked blocked for a reason no machine category fits gets ONE bounded session allowed to do the unblocking work — then an errand (unblockAttempts).
Take over stale claimsonAn expired foreign lock over unfinished work is taken over and the work continued; a live session's claim is never touched (staleClaimTakeover).
Resume killed lanes at bootaskWhat this console does about the runs its own restart stopped (resumeAtBoot). Ask — the shipped default — starts nothing and puts the question in front of whoever opens the app next, naming the runs and phases; Always resumes each killed lane's own session without asking, at most 3 restarts in a row per phase, then an errand; Never writes the errand straight away. The question is asked once per console start, and answering it is not stored — a restart is the event that makes it new again.
Switch accounts at a wallonA signed-out run account, or a usage window too far out to sleep on, switches to a registered account that can pay (autoAccountSwitch); off, the wall halts with an errand.
Delegate human gatesoffA manual gate is briefed to the phase's own session to VERIFY its conditions against citable evidence and record the clearance as by: ai-session-delegated, instead of stopping the run for a person (delegateHumanGates). Off by default and deliberately so — the plan author wrote human, and "the owner approves the visual result" is not a thing a session can judge. What makes it safe is not trust: the brief requires cited evidence per condition and STOPS with the condition named when it has none. Turn it on for a plan whose gates are machine-verifiable in practice.
Board a phase that states no verificationoffA phase whose plan omits the §Verification bullet boards anyway and passes on its handoff alone (allowUnverifiedPhases), instead of parking at boarding with "add a §Verification command, then Retry". The record says phase.verify-waived, so nobody reads "0 commands green" as proof, and a bullet the runner cannot read still parks — that is a formatting fault the author should hear about.
One more rung while the work is movingoffWhen a phase has spent its rungs but the newest settled rung landed commits, the ladder is granted ONE extra rung for that phase (ladderExtendOnProgress, journalled phase.ladder-extended). Once per phase; the dollar caps stand.
Boarding scheduleoffWhen this console is willing to START phases (boardingSchedule). See below.

The boarding schedule

Every other clock on this page is the machine's — the session cap, the account's usage window, the scopes. This one is yours: is now a time I want sessions starting at all. Nothing could infer it, and a laptop is perfectly capable of boarding an eleven-phase plan at 03:40.

Three rules, composed in this order and no other:

RuleKindWhat it means
Boarding windowsallowfromto local time, optionally restricted to certain days. With none set, every hour is allowed. A window whose to is not after its from runs past midnight and belongs to the day it STARTED on — 22:00→06:00 on Friday is Friday night, and covers Saturday 02:00.
Cron openingsallowFive-field expressions in local time; each match opens boarding for cronMinutes (60 by default). The same allow-list, said in the vocabulary a crontab already uses — including Vixie's day-of-month/day-of-week union, so 0 0 1 * 1 means the 1st and every Monday.
Quiet hoursdenyNothing boards inside these, whatever the windows say. Quiet hours win; that is what makes them worth setting. (A device's quiet hours — when a phone stays silent — are a different, per-device setting: docs/phone.md, Step 6.)

Outside the schedule a ready phase queues rather than failing: the queue names the boarding window as what it waits on and says when that opens, and the phase boards on the very next scan after it does. The console wakes at the opening rather than polling for it.

Two deliberate exemptions:

  • A recovery you ask for is never held. The schedule governs what the AUTOPILOT starts. Re-check, Finish in its own session, Resume with an instruction, Retry and Fix with a new agent all run immediately, because pressing a button at 02:00 is asking for it.
  • A live phase is never interrupted. The schedule decides whether a session may START; a session already running when quiet hours begin runs to its own end. Stopping work mid-edit to keep a timetable would leave a working tree nobody owns.

An unreadable window or a malformed cron expression is dropped, never defaulted — a schedule nobody can parse must never silently become "board at any hour". The card refuses a bad expression out loud rather than swallowing it.

Beside the preferences, every launch surface offers two per-run choices when accounts are registered (--allow-accounts): Account — which Claude login the run's sessions spend, including auto (most 5-hour headroom) — and On usage limitswitch (checkpoint and continue at once under the account with headroom; the dialogs' default), wait (sleep to the reset and resume by itself, restart-safe), or pause (checkpoint and stop for a person). A model-specific limit keeps switching models, not accounts — and files its wall under the model's own bucket, so auto skips that account only for runs of that model. Switch account on a live run acts immediately and lists every account with the current one marked; the scheduler throttles only the limited account. Accounts rename (display name only) and remove from Settings; an expired login raises a Sign in again alert and a run pinned to it is refused at preflight instead of burning sessions.

What it does by itself, and what it asks you

Since 2.3.0 a stopped phase is not a dead end. The console classifies it — never started, work in progress, done but unrecorded, verification red, declared blocked (lock · credential · gate · external · unknown), a resource wall (usage · auth · budget · model), an unreachable MCP server, a broken plan, a stale or a live foreign claim, a manual gate, a QA verdict — and climbs that situation's ladder: its own session first (--resume), a fresh briefed session next, an account or model switch at a wall, one bounded unblock session on a declared blocker, a takeover of a stale claim. The convergence loop runs it at boot, on a docs change, every sweep, a minute after any stop, and on Recover & continue. When every rung is spent — or the situation was yours from the start — it leaves one errand: what is needed, how to give it, what was already tried. You see the ladder on every Ways forward group (the situation chip, the rungs tried, the next rung, or the errand card), the errands and nothing else under the dashboard's Waiting on you, each plan's last pass on the Pulse's Converge line, and the caps and toggles above. Sessions see each other through the session-presence hook (Settings ▸ Automation ▸ Session presence, phase-console install-hooks): a hand-run claude in the repository shows on the Pulse, its lock is a queue to wait in while it lives and debris the moment it ends, and its phase-outcome.sh declarations drive the same machinery as a lane's. What stays yours: a sign-in, a manual gate, a credential, a blocker no category fits, a QA verdict the ladder tried for and could not produce, anything destructive or published. The loop has every word of it.

A session that boots and then says nothing

The ladder above is for a phase that stopped. A session that is still running and has simply gone quiet is a different animal, and the console used to be able to do nothing but put a card up about it: one lane in a measured incident sat silent for seventy minutes and the whole cost of that was one notification an hour earlier.

Now the console does what the operator did, on a clock. At ten minutes of total silence it writes to the session itself (journal: phase.auto-nudged) — the same thing a Steer does, and the only thing that has ever recovered one of these by hand. Five minutes later, if the lane is still silent, it recycles the session (phase.auto-recycled): the child is ended, the phase goes back to pending with its session id kept, and the ordinary drive loop re-boards it as a resume — same conversation, same lock, same lease. One nudge and one recycle per phase, ever: if it wedges again with both spent, the console stops trying and parks the phase with one errand (phase.stall-parked) naming what was already tried, while the run keeps driving its other lanes. Pressing Retry clears the ledger and lets it try once more. A session that answers the nudge and then wedges again still gets its recycle — the bound counts what was actually done, not how many times the lane went quiet.

It only ever fires before a session's first turn — no tool call has ever opened, no turn has ended, nothing has been spent on this attempt, no task list published, nothing committed and the tree clean. That is the one class where ending a process provably loses nothing, and every one of those facts is about this attempt, not the phase's history: a phase that committed last time round is still helped this time. A silent session with any work in it is left alone and stays a card for you. It never touches a frozen, pausing or fleet-held lane either: those are silent because the console made them so.

Underneath it, and independent of all of the above, every session now carries a first-event bound — a session that produces no output at all for twenty minutes is ended through the normal teardown, even with no console driving it. Twenty and not fifteen so that it lands strictly after the recycle rather than level with it: the two clocks must never race over whether the session is kept. Output that cannot be parsed is logged and counted rather than dropped, so "it said nothing" stays a claim you can trust.

Stopping things, at three sizes

Three surfaces carry the same verbs, scoped differently. The run controls act on the whole run: Pause after this phase (boundary), Freeze now (SIGSTOP every session, reversible), Stop now (SIGTERM everything; phases record interrupted, never failed). Each session tab — on the autopilot page, the Runs page's lanes, and the session console's own toolbar — carries Freeze/ Continue and Stop for that one session: the rest of the run keeps scheduling, a stopped phase keeps its session id for Retry, and a queued phase's Stop takes it out of the admission line before anything spawns. The fleet rows on the Runs page carry the run-level Freeze/Continue and Stop, so a live run is never a row you can only link away from. None of these touch the consecutive-failure budget, and pressing Start/Continue resets it — a resumed run never inherits a spent one. The fourth and largest size is the whole console at once — the next section.

Freezing the whole console

The fourth size, and the one to reach for when you are stepping away or something is going wrong faster than you can read it. Freeze all (Settings ▸ Automation, and the orchestration board's header on #/runs) stops every running session where it stands and starts nothing new — no queued phase, no elapsed wait, no recovery, no convergence pass. Thaw all puts the whole fleet back exactly where it was.

Four things are worth knowing, because each is the opposite of what a stop button usually does:

  • Nothing is lost, and nothing becomes a checkpoint. A frozen session is SIGSTOPped, so it thaws mid-token in the same process. Unlike a per-lane Freeze now, a fleet freeze carries no fifteen-minute conversion: it is a standing freeze, so a console left frozen over a weekend is still exactly frozen on Monday.
  • No clock is moved. Every timer keeps running and simply finds the console frozen when its moment comes. A wait whose window passed during the freeze fires once, at the thaw; a queued entry stays queued, names the freeze as what it waits on, and never ages into a reservation.
  • One case needs a press. A run whose phase happened to be between its session ending and its closeout when you froze settles paused and attributed to you — and a run a person paused is deliberately one the console will not resume by itself. Thaw-all wakes everything and unholds the queue; that run wants Continue. Nothing is lost (its phase is pending and keeps the session id, so Continue resumes rather than re-runs), but it is the one place "exactly where it was" means "plus one press".
  • It survives a restart. The freeze is a marker file in the console's instance state, so a console that dies — or a laptop that closes — does not quietly hand the fleet back. A console that boots under one re-adopts nothing and spawns nothing.
  • Your own hands still work. Freeze/Thaw and Stop on a single run, and Recover & continue, are operator acts rather than auto-starts, so they go through. What stops is the console acting by itself.

The gate is one predicate, consulted by every mechanism that could begin work, and the list of those mechanisms is pinned in viewer/test/fleet-freeze.test.ts: a new one cannot be added without declaring where its freeze gate lives.

POST /api/fleet/freeze and /api/fleet/thaw (both --allow-run) are the endpoints — each answers 409 with a reason when there is nothing to do, so pressing Freeze twice never re-stamps the moment you froze it. GET /api/fleet and /api/state's fleet field carry the state, which is what the app-wide banner renders on every page.

Two plans in one repository, at the same time

The scheduler's rule has always been never two live sessions whose scopes intersect, and the only way it could keep that promise was to make the second run wait. Isolation is the other way to keep it: give the run a checkout of its own, and the two are no longer in one working tree.

Turn it on for a run with Give this run its own checkout on the launch form. It rides the work-branch git strategy — a run with no branch of its own has nothing to check out — and on a live run the switch only turns off: a run's commits are on the branch in the checkout it started in, and there is no honest moment to move them.

Asked-for and got are two different facts, and the console keeps them apart. A run that asks to isolate can be refused — the repository is a superproject (git worktree add on a repo with submodules gives EMPTY submodule directories), the disk cannot take another tree, the cap is reached. Every refusal degrades to the shared checkout with queue semantics — exactly the behaviour that existed before the feature — and says which impossibility it hit, in the run journal (run.isolation, run.isolation-kept) and on the run page. Nothing fails.

Four settings under Settings ▸ Automation govern the console's own trees, and they are shown only while isolation is on: Worktrees at once (a cap, shipped at 3 — each is a full checkout on disk), a setup command run once inside a fresh tree (npm ci, a symlink, nothing), whether to copy the source checkout's ignored .env files into it — off by default, because copying secrets into a second directory is a decision, not a discovery — and whether the console may take the run branch back from a clean checkout.

The console may switch your clean checkout off pe/<slug> (Settings ▸ Automation ▸ Take the run branch back from a clean checkout, shipped ON). The wedge it ends is one the console itself creates: the work-branch strategy tells every session to check pe/<slug> out, so your own checkout ends up holding it — and git allows a branch exactly one working tree, so from then on every run of that plan meets branch-in-use and silently shares that tree. With the setting on, a checkout sitting on the run branch with nothing uncommitted in it is switched to the default branch and the run takes its own; the journal says which tree moved and where to (run.isolation-reclaimed), and the isolation preflight says "will reclaim <path>" before you launch. A checkout holding anything — a tracked edit or a file nothing ever added — is never touched: the run refuses branch-in-use and names the files that stopped it. No branch is deleted and no ref moves; the branch is exactly where it was, with one fewer working tree on it. The reclaim also consults the lock table (read off the lock files themselves, so a claim made a second ago counts): a checkout a live lock names in its worktree= line — any plan's, unexpired, its session not ended — is never moved, and a live lock that names no tree (the ordinary hand claim, claim N --scope … --git with no --here) holds every checkout of the repository its scope touches, because a claim that never said where its work rides may be riding this one. Either way the refusal names the holder. (Until console-parallel-repaint P1 only dirtiness protected a live session's tree, which held most of the time.) Never turns it off.

Post-merge phases board a detached mirror. A run whose pull request has merged and whose pe/<slug> has been deleted still has phases to drive — and re-creating that branch would re-open work the merge just closed. Such a run boards a checkout detached at the default branch's head instead: it owns no ref, so any number of them may stand beside each other and beside whoever holds the branch. Its lock is qualified branch=detached@<sha12>, which contends with nobody holding a real ref, and with another detached claim only when the two name the same working tree — two runs detached at the same commit in two console-managed trees run side by side (they were serialised before console-parallel-repaint P1). A plan can ask for it deliberately with - **Checkout:** main on a phase (main, master or the word default; anything else is documentation the console does not act on). The journal line is run.isolation-detached.

Merged pe/* branches are deleted after a pr settle (Settings ▸ Automation ▸ Delete merged run branches, shipped ON). Once the trunk contains a run's commits the branch is a name for something that already happened, and a console that drives thirty runs otherwise leaves thirty of them behind. Evidence, not assumption: the deletion is always git branch -d, git's own refusal to delete a branch whose commits are reachable from nothing else, so a squash-merge the local trunk has not fetched simply reads as unmerged and the branch survives until the next drive. -D does not exist anywhere in this codebase. A branch still checked out somewhere is kept and named (run.branches-pruned reports what went).

A worktree this console did not make is reported, never removed. Every boot and every drive sweeps the repository's registrations: one whose directory is gone is pruned (it was holding a branch for nothing), and a live hand-made checkout on a pe/* branch is named in the journal (run.worktrees-unmanaged) and left exactly as it is — it may hold work, and every sweep here keeps a checkout that does. A checkout on your own feature branch is none of the console's business and is not mentioned.

Lock leases default to 2 h; the runner claims 90 min. A lapsed lease is takeable by anyone — that is the cooperative design — so the number has to outlast the work it protects, and 30 minutes was shorter than a phase. The console's own sessions never noticed, because the runner refreshes on a timer, which is exactly why the wrong default survived: it only ever hurt a session driven by hand, which has nothing refreshing anything. The runner still states its own (--lease 5400RUNNER_LEASE_S, a literal in seconds: 90 minutes, nine times its 10-minute refresh cadence, so eight ticks may be missed before a claim it is actively holding can lapse).

What two live branches are doing to each other is measured, not guessed. The run page's Git card carries ahead/behind against the base, the files the branch has changed, the tree's disk footprint, and a radar verdict for every pair of live branches — clean (no common file), overlap (common files, still merges), conflicted (a real merge conflict is already sitting there) and unknown (the probe could not answer, which is deliberately not clean). The verdicts come from git merge-tree; the whole view arrives on one run:git event, and phase_console_branch_conflicted_files (metrics) is the same number for a scraper. A pair that turns conflicted mints one inbox item whose Serialize action patches the other run back to queue — one press, and the overlap is gone.

A branch has to end up somewhere, and you choose whereWhen the plan completes, on the same form, for any work-branch run whether or not it isolated:

StrategyWhat happens when the run finishes
Pull request (default)Push the branch and open a PR — the only ending with a person reading the diff
Merge queueOne more session rebases on whatever landed while the run drove, re-runs the plan's §End-to-end verification, and pushes only if that passes
IntegrationThe console merges the branch into its own staging checkout (pe/integration, one per console) and stops — no remote touched, no session spent
KeepNothing. The branch and its checkout stay exactly where they are

Only the two that end at a remote may open the push carve-out; integration merges locally and keep does nothing, so neither gets it. A clean tree is removed when the run settles; a dirty one is kept and journalled, because uncommitted work is never the console's to throw away.

The board

The fleet table on #/runs is a record — every run there has ever been, sorted and filtered, one row each. It is the right shape for what happened and the wrong one for what is happening: a row cannot say that this run is queued behind that one, and every verb that changes what happens next used to live on some other page — priority on the launch form, hold and bump on an endpoint with no button, Freeze all in Settings, isolation in a settings sheet.

The board is the other reading of the same fleet, and it is what #/runs shows by default (runsView, one toggle away from the table — never both at once). Four columns — running · queued · waiting · frozen — a card per live run carrying its plan, its phase strip, its spend, its branch chip and the facts about what it is waiting on; and every verb inline on the card it acts on: freeze, continue, pause, resume, hold, release, stop, priority, bump, and the repair actions. Freeze all and Thaw all are in the header. Alongside the queued column it shows the same ordering advice GET /api/queue carries — remaining weight and an ETA per queued plan — as a suggested order, computed when you ask and never acted on.

Nothing on it is a new control: the columns, the lane rows, the holder facts, the branch chip, the meters and every lifecycle door are the same primitives the run and Now pages use, which is why a verb cannot behave one way here and another way there.

Steering which plan goes first

Four controls, and none of them is a heuristic — the scheduler still runs the same deterministic first-fit scan it always has, and these are inputs to it.

Queue priority (high · normal · low, on the launch dialog and on a live run's settings) decides which class an admission is scanned in. First come, first served still decides within a class, and the starvation bound is untouched: an entry that has been bypassed enough times, or has simply waited ten minutes, reserves its tokens against everything behind it including a higher class. A low plan that starves outranks a high one that just arrived. A class is a preference; starvation is a bug.

Hold and Release (beside Pause/Resume on the run controls) stop a run boarding anything new while its running phases finish and write their handoffs. That is the whole difference from Pause: a pause waits for the next phase boundary and then settles the run, so bringing it back is a Start; a hold refuses the next admission and nothing else, so a Release puts it straight back in the queue. Held entries name who held them on the queue page, and never age into a reservation — standing aside must not turn a plan into an obstacle.

Bump moves one queued entry to the front of its class. It never crosses a class boundary (the priority control is what says that out loud), and it is one-shot: the mark lives on that queue entry and dies with it, so a phase that queues again later starts from its class's tail.

Start after <slug> (launch only) chains a run behind another plan: it boards nothing until that plan's latest run ends — finished, paused, parked, halted or stopped, whichever comes — and the chain survives a console restart because it lives on the run's checkpoint. The queue page shows it as waiting for <slug> to settle. The slug is not validated: one that names no plan settles at once, rather than refusing to start a run whose predecessor you are about to create.

GET /api/queue carries ordering advice alongside the entries — remaining plan weight and an ETA per queued plan, computed when you ask and never stored. Nothing on the server reads it back: it is the figure to look at before deciding to use one of the four controls above.

Run-level beats plan prose for console-minted sessions. When a run uses the work branch and the plan's own **Branch:** line names a different branch, the session is told to use the run's branch and to record the discrepancy in its handoff — the console never silently rewrites the plan. Hand-driven sessions (copy-paste boot prompts) keep following the plan's line.

Command reference

Run these from the repository that owns docs/, or set DOCS_ROOT.

The engine

scripts/phase-graph.sh <slug>                    # the board (default)
scripts/phase-graph.sh <slug> --lint             # structural validation; non-zero on a problem
scripts/phase-graph.sh <slug> --ready            # ready phase numbers
scripts/phase-graph.sh <slug> --ready-after N    # the ready set assuming N just completed
scripts/phase-graph.sh <slug> --deps N           # N's prerequisites
scripts/phase-graph.sh <slug> --dependents N     # phases N blocks
scripts/phase-graph.sh <slug> --size N           # S | M | L
scripts/phase-graph.sh <slug> --repos N          # N's SCOPE as a csv — the input to every lock check
scripts/phase-graph.sh <slug> --mcp [N]          # the MCP servers the plan (or phase N) needs
scripts/phase-graph.sh <slug> --mcp-policy [N]   # continue | require
scripts/phase-graph.sh <slug> --gated N          # yes | no
scripts/phase-graph.sh <slug> --gate-kind N      # human | ai | auto | none
scripts/phase-graph.sh <slug> --gate-status N    # evaluate the gate (approval clears any kind)
scripts/gate-approve.sh <slug> N --by <who>      # record a clearance (--revoke restores the gate)
scripts/phase-graph.sh <slug> --boot-prompt N    # the copy-paste prompt for phase N
scripts/phase-graph.sh <slug> --session-plan opus   # proposed session grouping
scripts/phase-graph.sh <slug> --qa-mode          # off | on <reason> | waived <reason>
scripts/phase-graph.sh <slug> --qa-result N      # the recorded verdict
scripts/phase-graph.sh <slug> --qa-prompt N      # the QA subagent's brief
scripts/phase-graph.sh <slug> --plan-status      # active | complete | abandoned | superseded
scripts/phase-graph.sh <slug> --closed           # exit 0 if the plan is closed, 1 if open
scripts/phase-graph.sh <slug> --memory-block     # done/ready/waiting, for the memory entry

A closed plan answers differently on purpose: --ready and --ready-after come back empty, --session-plan returns a notice instead of groups, --lint still lints but exits 0, and the board prints a 🔒 CLOSED banner in place of the ready/waiting/batching lines. validate.sh skips it and next-phase-prompt.sh offers no boot prompts. See the artifacts.

The helpers

ScriptWhat it does
new-plan.sh <slug>Scaffold docs/plans/<slug>.md from the template.
new-handoff.sh <slug> <N> <title> [status] [--qa] [--force]Scaffold the phase handoff, update INDEX.md, auto-fill dependencies and generate a boot prompt per unblocked phase.
handoff-status.sh <slug>The INDEX, per-file status, and the live board.
next-phase-prompt.sh <slug> <N|none>End-of-phase banner, board, batching advice, and a boot prompt for every newly ready phase.
phase-lock.sh <slug> claim|release|status|list|conflicts <N> [--owner <id>] [--scope <csv>] [--session <id>] [--force] [--git]Claim, release or inspect a phase lock; conflicts asks across every plan whether a live session shares your scope. --session (default $PE_SESSION_ID, else $CLAUDE_CODE_SESSION_ID) names the session in the lock, so the console can release it the moment that session ends.
phase-lane.sh <slug> create|merge|remove <N> [--qa <round>] [--detach] [--repo <token>] [--owner <id>] [--force] · phase-lane.sh list [<slug>]A hand session's own checkout, made where the console keeps its own: a locked worktree of the phase's repository under <root>/.worktrees/hand/<slug>/p<N>[-qa<r>] on pe/<slug>-p<N>[-qa<r>] (or detached), folded back with ff-then---no-ff, removed with worktree remove + branch -d — never a sibling folder of the project.
phase-outcome.sh <slug> <N> complete|blocked|needs-human|waiting-external|partial [--reason …] [--wait-minutes M] [--watch ref]Declare how a session ended, machine-readably — the runner's channel (PE_OUTCOME_FILE); unsupervised, it lands in the console's inbox and is picked up the same way.
phase-outcome.sh <slug> <N> ruling --what "…" [--why "…"] [--kind ambiguity|deviation|deferral] [--cost-if-wrong "…"]The same script's second shape: what a session decided, as opposed to how it ended. Appends one NDJSON line to the plan's ruling ledger (PE_RULINGS_FILE, else runs/<instance>/<slug>/rulings.ndjson). A ruling is never an outcome — nothing acts on it, and declaring one does not declare the other.
session-hook.shThe user-scope Claude Code hook (SessionStart · Stop · SessionEnd) that reports a session to the console owning its directory; installed by phase-console install-hooks or Settings ▸ Automation ▸ Session presence. Fail-open; PHASE_CONSOLE_HOOK_OFF=1 silences it.
qa-record.sh <slug> <N> <pass|fail|waived|pending> --report <path>Record a QA verdict.
close-plan.sh <slug> [--status abandoned|superseded|complete] [--reason "…"] [--reopen] [--force]Close a plan that will never finish, or --reopen one. Sets status:, closed: and closed_reason:, and releases the plan's own phase locks. Idempotent; never touches git.
validate.sh <slug>Full validation — plan structure and handoff consistency.