Skip to main content
/pm:status runs the conductor engine’s render command, then reads both the output render just printed and PROJECT.md, and summarizes the project’s position: what’s actively being built, what’s paused and why, what’s queued next, and how many epics exist per lane. It’s the fastest way to re-orient yourself at the start of a session or after any state-changing operation.
Read the render output, not only PROJECT.md (0.50.0). The render output can carry a block that PROJECT.md never does — SPEC DELTAS ABSENT FROM THE MAIN SPECS, below. It depends on git’s index rather than on the record, so it is printed, not written into a tracked file. Reading only PROJECT.md misses it.

What the briefing shows

NOW

The active epic with its lane icon, priority badge, and live progress: its inline stories and its checkbox source (plan file or tasks.md) counted together. If no epic is active, this section prompts you to triage.

DETOUR STACK

All paused epics in LIFO order, each annotated with the reason it was pushed onto the stack. Any frame with a pending reconcile-on-resume gate is flagged with ⚠ to remind you to reconcile before resuming.

NEXT UP

The top-priority queued epics, ordered by effective priority — an epic inherits the best priority of everything that transitively depends on it, so a P1 blocked by a backlog P2 lifts that P2 into view instead of starving behind it. Renders as P2 → P1; never written to state. reorder sets a manual rank as the final tie-break.

DEPENDENCY WARNINGS

An epic that is blocked with no live depends-on edge, or a dependency inversion — naming both endpoints, both statuses, and the lift. Rendered in PROJECT.md as well as the brief, because /pm:next reads PROJECT.md and would otherwise never see them.

LANE COUNTS

A snapshot of how many epics exist across each lane (openspec, superpowers, claude-code, decision, external) broken down by status, so you can see the overall shape of the backlog at a glance.

TRACKER SYNC

Where a tracker’s direction calls for it, this section lists any currently active epics that are missing an externalId. These are candidates for /pm:sync or manual tracker linkage.

UNGATED ARCHIVES

Epics archived with no Gate 2 review from anyone. A standing condition, not an episode: it is recomputed from state.json at every composition and never consumed, so every session sees it until a real passing verdict supersedes it.

WITHDRAWN GATE 2 ARCHIVES

The same standing condition’s other kind (0.43.0): archived openspec-lane epics whose Gate 2 verdict was withdrawn (update-epic --withdraw-gate-review) and not recorded again. Its own heading, never under UNGATED ARCHIVES, because “no review” is false of a review that was recorded and taken back. Each entry quotes the withdrawal reason, and says so where the epic was archived ungated before the withdrawn review. Cleared the same way: record a real verdict.

SPEC DELTAS ABSENT FROM THE MAIN SPECS

New in 0.50.0, and read from the render output, never from PROJECT.md: a delivered openspec epic whose archived change’s spec deltas the main specs in git’s index do not hold (an ADDED/MODIFIED/RENAMED-TO header missing, or a REMOVED/RENAMED-FROM header still there). The same set integrity’s delivered-epic-spec-deltas-absent reports, and the SessionStart briefing carries it under the same heading. A standing condition: it clears only when the index holds what the deltas require. Run integrity for each finding’s remedy.

HANDOFFS

Work carried out of an archived epic, shown from both ends — the epic that carried it out and the epic that inherited it. A relationship visible from one side only is how a remainder disappears.

RELEASES

Each release renders as N epics, M deferred, with every deliberate exclusion’s reason reachable from state.json — so “what is in this release, and what did we cut and why” survives the session that decided it. A release holding two or more spec files also carries its cross-spec review state: fresh, ⚠ stale, ⚠ unverifiable, or ⚠ no cross-spec review (N specs).

TOOL CURRENCY

Where the project uses OpenSpec, whether the installed CLI and this project’s generated artifacts agree. pm and superpowers update themselves; OpenSpec is upgraded by hand and openspec update is a separate per-project step — see /pm:upgrade.
A Gate 1 verdict now goes stale when a reviewed artifact is amended (0.51.0, #198). The Gate 1 cell on this briefing and on the SessionStart brief reads ⚠ stale once a recorded artifact changes, and ⚠ unverifiable for an artifact that could not be read when the verdict was recorded. A verdict recorded before this release carries no digests and renders exactly as before. A live change’s tasks.md is never digested, and staleness never blocks an archive. See /pm:epic.

Progress counts stories and tasks together (0.50.0)

Progress is the UNION of two parts, and an archived change is still read. An epic’s count sums its inline stories (a story recorded --wont-do leaves both sides of the ratio) and its checkbox source — its plan file, or for an openspec-lane epic the change’s tasks.md. Neither hides the other: adding a story to an epic that has a tasks.md no longer makes the tasks unread, and the ratio then counts items (N/M items). Once /opsx:archive has moved openspec/changes/<id>/, the checkbox source is the ARCHIVED tasks.md (openspec/changes/archive/<YYYY-MM-DD>-<id>/, the latest date winning where one id was archived twice) — for every epic, not only a backfilled one — so an archived epic renders its real counts instead of 0/0, and the archive gate’s handoff demand reads the same count. A missing tasks.md or plan still warns while the epic is not archived, whether or not it has stories.
An unrelated old archive directory does not end a live epic (0.50.0). A name is not an identity. An archive directory dated (YYYY-MM-DD-<id>) more than a day before a LIVE epic’s createdAt is some older, unrelated change that happens to share the name, so it neither ends the epic nor clears its active pointer, and set-active still accepts the epic. A live epic with no createdAt is never ended by a bare name match (recover-created-at dates it). An epic that is already archived still finds its archive by name. sync names each directory it set aside — see /pm:sync.

Progress excludes lifecycle bookkeeping

A task that is bookkeeping about a change’s own lifecycle — above all the task that archives the change itself — carries the literal marker {/* pm:lifecycle */} on its own task line, and is excluded from progress. It renders as · N lifecycle beside the ratio, or 0/0 · N lifecycle where every task is excluded. The engine infers this from nothing else: not the wording, not the commands the text names, not the position in the file. Mark it when the task source is authored or amended. Without the marker, the archive task counts as outstanding work forever — it cannot be ticked before the thing that ticks it — and the archive gate would demand a handoff for a change that delivered everything.

Auditing the record itself — integrity

integrity is a read-only audit of the conductor’s own record: shapes that cannot be true. An archived epic whose task source exists with nothing ticked. One change registered under two lanes. A recorded commit sha the repository can no longer resolve — orphaned by a squash-merge and due for deletion at the next gc, or already gone. An epic left open in a release that already delivered, and not among the epics that release cut on purpose. An epic something else declares it supersedes, still sitting queued or active. A gate verdict whose recorded range does not reach the commits its own note cites. A gate recorded as bookkeeping rather than review. A delivered epic that attributed no commits. An archived openspec-lane epic with a passing Gate 2 and no (or a withdrawn) Gate 1. An epic archived with an ungated Gate 2. An archived openspec-lane epic whose Gate 2 was withdrawn and not recorded again (archived-with-withdrawn-gate-2, 0.43.0). An epic the archive-drift heal flipped that reads outcome: unknown while carrying a passing Gate 2. A dangling epic reference. An archive directory no epic corresponds to. An epic in a status the engine does not define. A github-issues tracker whose recorded repo is not owner/name or HOST/owner/name (tracker-repo-not-a-github-repository, 0.45.0) — such a repo gets no literal gh step until it is re-recorded, and the finding carries the re-record command, plus, for a secondary, its --remove printed shell-quoted. A delivered openspec epic whose archived spec deltas never reached the main specs (delivered-epic-spec-deltas-absent, 0.50.0, below). 0.51.0 adds four more kinds of drift, each named in the next section.

Four checks new in 0.51.0

  • tracker-item-held-by-two-epics names two epics holding one tracker item (the pre-0.50.0 duplicates the writers now refuse). It names both holders and the remedy, update-epic <one> --clear external-url (#231).
  • archived-change-also-live names a change present both live and under archive/ with identical content, the shape upstream openspec archive leaves because it copies a tree rather than moving it (#215). The remedy is git rm -r of the live copy. A live change that differs from its archived namesake is a re-proposal and is not reported.
  • late-failing-gate-review reports a failing gate verdict recorded after the epic’s merge as the real retrospective review it is, and gate-recorded-as-bookkeeping no longer reports it (#201).
  • archived-delivered-fails-delivered-obligation names an archived epic recorded delivered that no longer meets what delivered requires, by a path no gate sees: a Gate 2 flipped to fail after the archive, the heal re-archiving a changed epic, a stripped carriedTo, or tasks that changed on disk. Epics whose Gate 2 is withdrawn or ungated are left to their own checks.
archive-directory-has-no-epic also reports an archive directory the 0.50.0 date rule set aside, in the same words sync uses.

delivered-release-epic-left-open — and the marker that settles it (0.51.0)

A release carrying the delivered marker (release <id> --deliver, below) is delivered, full stop: every non-archived member not in its deferred[] is reported, active or not. A release with no marker keeps the member-derived reading (at least one member delivered and none active or paused, so a staged release in flight stays silent), and that finding now says the reading is inferred and prints release <id> --deliver as the way to replace the guess with the fact. That replaces the --status active workaround the check’s earlier wording suggested. Archiving the last active member of a release that is still in flight no longer reads as the release being delivered. Every check is reported with its count including the ones that found nothing, so a check that measured nothing is visibly a check that ran. It writes no state, blocks no command, and repairs nothing — each finding names the epic, and the remediation is a command you run.
On the first run after upgrading to 0.27.0, expect a burst of heal-archived-epic-passed-gate-2. Every repo that followed the documented /opsx:archive then heal flow lands on outcome: unknown rather than delivered: the migration only stamps epics already archived in state, and the heal flips the rest afterwards. Expected, not a bug — each finding carries the exact remedy.

delivered-epic-spec-deltas-absent — the check that reads git’s index (0.50.0)

For every delivered openspec-lane epic whose change sits under openspec/changes/archive/, it reads each delta specs/<capability>/spec.md there and checks the main spec openspec/specs/<capability>/spec.md: every ADDED and MODIFIED requirement header, and each RENAMED TO, must be under its ## Requirements; every REMOVED header, and each RENAMED FROM, must not be. Headers only, never bodies — a later legitimate amendment changes a body, and a check that fired on routine work would be ignored. A later archived change that touched the same header the other way discharges it. An unpaired RENAMED line in an archived delta is reported too, since nobody can check it. Each finding names the epic, the change, the capability and each header. The main spec is read from the index, not the working tree and not HEAD, in ONE git cat-file --batch, relative to the conductor root. The working tree would pass the exact loss this exists for — 0.48.0’s archive rewrote two main specs, the commit staged only openspec/changes, and a later hard reset discarded four ADDED requirements for two days.
The stated cost: from openspec archive until git add, the index still holds the old main specs, so a CORRECT archive is reported in that window too — stage openspec/ whole and it clears. That window is why the condition is never written into PROJECT.md: integrity, the SessionStart briefing and the render verb’s printed output carry it. It is a standing condition, never a refusal — no archive path is refused on it.
A change deliberately archived with --skip-specs is waived, not fixed (0.51.0). update-epic <id> --spec-deltas-waived "<why>" takes that epic out of this report (the reason is the value and cannot be blank), and update-epic <id> --clear spec-deltas-waived puts it back. The flag is refused for an epic outside the check’s scope (a delivered, openspec-lane epic whose change is archived), and a waiver is never silent: the briefing and the render verb’s output print SPEC DELTAS WAIVED (N) with the epic ids, so a waived epic whose deltas really are missing is distinguishable from a clean one. A waived epic’s archived change still discharges other epics’ obligations. Where git cannot answer at all (no repository), no presence or absence finding is made, and each surface prints one line instead, the same way on all three:
The remedy it prints runs against a change that is already archived (neither openspec archive nor /opsx:sync acts on one): edit the main spec’s ## Requirements so it holds what the archived delta requires — copy each block reported absent from that delta, delete each block reported present, rename a RENAMED FROM header to its TO — then git -C <conductor root> add openspec/.

epic-in-undefined-status — the record no terminal rule can reach

New in 0.40.0. An epic whose status is outside the set the engine defines is now a reported finding, mirroring the existing link-of-unknown-type check. The finding names the consequence a reader would otherwise have to work out:
  • The epic is exempt from every rule that tests for the archived status — so it is invisible to precisely the checks that would surface it.
  • Any depends-on edge pointing at it reads unsatisfied forever, and it permanently absorbs the effective priority of everything waiting on it.
  • The archive walker below cannot reach it either, because it is not archived.
It reports; it never repairs. Which status the epic should be in is a judgment about what happened to the work, and the engine will not guess.
Measured across 27 distinct upstreams before this shipped: 26 epics sitting in status: "done" across five repositories — a value KNOWN_STATUSES does not contain, written before the status set was closed, and which 0.27.0’s migration never reached because it looked only at epics already reading archived.

Which archived epics did nobody decide about — unconsidered-outcomes

New in 0.40.0. A read-only list of every archived epic whose disposition is an engine stamp carrying unknown — that is, the engine recorded that nobody was asked.
Each row names who stamped it (the 0.27.0 migration and the archive-drift heal are different histories, and which one it was changes how much of the epic’s story is still recoverable) and carries the exact update-epic … --status archived --outcome … --reason … --no-deferrals invocation that would record a real disposition.

deliveredBlockedBy — what stands between this epic and delivered (0.45.0)

Every entry now carries deliveredBlockedBy, always present and [] when nothing blocks. Each element is {kind, detail, remedy} — one per obligation standing in the way of a delivered archive, in the order to run them (Gate 2 before the handoff). A checkbox-source epic’s handoff remedy is the delivered archive itself carrying --carried-to <epicId> --reason "<which tasks moved>", because the gate refuses a delivered archive over open tasks unless the work is handed to a registered epic. That closes a measured papercut: unconsidered-outcomes and integrity’s epic-in-undefined-status used to offer a delivered choice the archive gate would then refuse — 12 of 20 entries in one run printed a command that exited 1 — and one checkbox handoff blocker printed an empty remedy. delivered is now offered only where the gate would accept it. Both halves of the predicate are load-bearing. An evidence-derived stamp — a delivered written from a passing Gate 2 — is excluded, because re-deciding it would ask you to re-derive what the record already got right. So is an agent-recorded outcome of any value, because somebody was asked. A stamp alone or an unknown alone would each sweep in the wrong population. Where a record genuinely cannot be reconstructed, --outcome unreconstructable says so with its required reason — distinct from unknown, which says nobody looked. The set shrinks only by somebody deciding; the engine never guesses.
Measured across 27 distinct upstreams: 310 archived epics carrying outcome: unknown, written by 0.27.0’s migration with no reason text. In PM’s own repository that was 66 of 144 archived epics — 46% of the archive.

A write that changes nothing says so

New in 0.40.0. A command that found nothing to change used to print the same success line as one that rewrote the record, so “did that land?” could only be answered by reading state.json or git status. Every write now reports through a shared reporter across the whole write surface rather than one verb: 27 saveState call sites are bound to it, four carry a declared exemption naming its reason, and a per-call-site source scan fails the build on a fifth that does neither. That last part is the point — the same rule shipped at one verb first, and both Gate 2 reviewers independently found four sibling sites still printing success on a no-op save.

Design-document coverage — verify-specs

The command has its own page as of 0.50.0: /pm:verify-specs. Once epics can name the design document they came from (--spec), “which design documents produced no work?” becomes mechanically answerable.
It lists every design document under the root with the epics drawn from it, and names the ones with none. A document counts as any .md under the root, recursively, minus README/INDEX/CONTRIBUTING — the same named-exclusion rule the plan scan uses, so the two cannot disagree. Point --root at a plans directory and it answers the same question about plans.
This is a listing verb, not an integrity check, and the distinction is deliberate. integrity reports shapes that cannot be true. A design document with no epic can be true and often is — a note, a reference, an abandoned sketch. Filing every one as a finding is exactly the noise that made the freshness warning ignorable before 0.31.0 scoped it. So verify-specs reports only when asked, always exits 0, and never speaks on its own.An absent root prints “no spec root” rather than “0 uncovered” — a repository that keeps its designs elsewhere should not get a confidently empty report.
Coverage is status-blind: an archived epic for one chunk of a design is coverage of that chunk.

--headers — let the documents propose their own epics

New in 0.34.0. Design documents usually name their epics in a header. verify-specs --headers reads the leading metadata block of each document and reports the epic ids it finds, so populating specPath on a repository that already has designs is a confirmation rather than a hand-backfill.
It proposes; you confirm. The engine must not decide that a document and an epic belong together, and a header is prose an author can typo — so this is the same split /pm:triage uses for asks: a mechanical candidate set, a human verdict.
The parse is label-agnostic and region-bounded — any **Anything:** line opens the block, the first blank line closes it, and backtick-quoted tokens matching the epic-id format are candidates. That was chosen empirically rather than guessed: a label allowlist dropped three genuine ids filed under **Depends on (all three must land first):**, and matching bare backticks anywhere would read state.json out of any document opening with prose.Measured against ten real documents: 26 of 26 ids, zero false candidates, including both cases where one design produced three epics — which is exactly what deriving the mapping from filenames could not do.
A header id that names no epic in the record is reported as a finding rather than inventory — a stale header, a removed epic, or a typo. Covered documents are checked too: a document whose first named epic shipped can still name a second that never existed.

This shape is a default, not a house style

New in 0.36.0. The layout above is what PM emits for someone who has configured nothing. If you have an output style or a communication contract in your project file, yours wins — PM’s shape is a default, and a plugin’s preference does not outrank your instructions. What does not bend is anything PM records: a disposition’s reason, a gate verdict’s range, an attribution. Those are writes to the state file rather than sentences, so no communication contract reaches them — applying “be brief” to a recorded reason is data loss, not brevity. Nor do the field names agents exchange between themselves, which are a wire format another agent branches on; the prose inside those fields is ordinary writing and does follow you.
Deference has a limit, and it is stated in the emitted rules: your contract governs how you report, never what PM instructs you to do. A brevity preference does not excuse a required task item, a gate, or a recorded reason. And where your shape has no slot for something required — a notification read-back, a deferral list, a blocked child — the rule is reshape, never drop: add a slot.

Which verbs write — verb-effects

render writes PROJECT.md. brief does not. Until 0.33.0 nothing said so, and the only way to find out was to run one and check git status. Every dispatched verb now carries a declared effect — mutates or read-only — and the declaration is verified rather than asserted: the suite checks it covers every verb the dispatcher actually has, then runs each read-only verb against a repository with a render pending and hashes the tree by content and mtime.
That measurement corrected two claims while it was being written. render is content-conditional — it skips the rewrite when only the timestamp would change — so a content-only hash reported it clean, which would have shipped a wrong declaration. It is still mutates: idempotent-when-nothing-changed is not the same as read-only, and both facts are now asserted so they cannot be conflated. Adding mtime then caught three more verbs writing on every call.

Release planning — release

A release is a named grouping of epics the agent declares: an id, intent prose, an optional target, and the epics deliberately cut from it. It exists because a release’s scoping judgments otherwise survive only in the session that made them.
  • Intent prose is required to create one — an id with no statement of what it is for is unreadable later, which is the failure this records against.
  • A new release id is validated first (0.45.0) — it must match ^[a-z0-9][a-z0-9._-]*$, checked before the missing-intent refusal (which prints the id inside a command) and before any write. An existing release with a legacy id is still updated and shown.
  • Membership is one-way. It lives on the epic as epic.release, at most one, and the release object carries no member list to fall out of step with it. Re-associating an epic MOVES it — and since 0.50.0 the move is RECORDED on the release it leaves: release r2 --member e1, with e1 in r1, appends {op: "unmember", epic: "e1", via: "member", to: "r2"} to r1’s amendments[] and says so on stderr, so release show r1 reads “moved to r2” rather than silently holding one epic fewer. Re-adding an epic to the release it is already in writes nothing.
  • The engine proposes nothing. No epic is auto-assigned; adding, re-prioritizing, or archiving epics changes membership for none of them. Grouping is a scope judgment, and the scope judgment is the thing being preserved.
  • An exclusion is not an ending. A deferred epic keeps its status and gets no disposition of its own — it stays in the backlog because it is still work someone may do. What it loses is membership of that release.
  • Re-deferring with a NEW reason keeps the old one (0.50.0). deferred[] holds the current reason, and {op: "redefer", reason, was, wasRecordedAt} goes to the release’s amendments[], where release show renders both. The same reason again writes nothing.

Recording that a release shipped — --deliver, and its inverse --undeliver (0.51.0)

  • --deliver sets release.delivered = {recordedAt}. It is a marker, not a judgment, so no reason is demanded. integrity’s delivered-release-epic-left-open reads it instead of guessing from the members. Marking an already-marked release changes nothing.
  • --undeliver removes the marker (the release returns to the member-derived reading). It is refused when there is no marker to remove, and --deliver together with --undeliver is refused. Nothing is written in either refusal.
  • A release recorded before this existed has no marker and nothing needs migrating: absent means “derive it from the members”, exactly as before. release show renders the marker.

The derived candidate review: line (0.51.0)

release show also prints a candidate review: line, computed from the candidate members’ existing Gate 2 records with nothing stored. A candidate member is a release member that is not archived and has at least one attributed commit. The line reads converged at <sha> when every candidate member carries a passing Gate 2 verdict over one shared range (the same base and the same head). Otherwise it reads NOT converged, listing each distinct range with its members, each member with no Gate 2 verdict (a withdrawn Gate 2, or one with no recorded head, counts as no verdict) and each member whose verdict is fail. A release with no candidate members prints no such line. The procedure behind it is the shipped release-candidate skill; the budget rule is on /pm:review-mode.

Reading one back, and taking it back — release show, --unmember, --undefer

New in 0.41.0. A release could be written and never read. release show [<id>] renders intent, target, derived members, deferrals and the cross-spec verdict; with no id it lists every release. release show takes at most one further positional; a second is refused before anything runs (0.44.0). release show --force is refused by the read form itself: --force passes the engine’s command-line check because release mutates, and the read form declines every flag. That read-back matters more than it sounds, because of the storage asymmetry above: membership lives on the epic and is derived, while deferred[] lives on the release object. So a reader who opened that object saw exclusions populated and members absent — reading as “this release has cuts and no content”, the exact inverse of the truth. --member and --defer also shipped with no inverse, which is the defect class 0.40.0’s own call-site rule exists to catch, found by turning that rule on a surface older than it:
Each requires a reason, and those reasons land in the record — a new amendments[] on the release object, which release show renders. --member’s long-standing implicit undefer (adding a cut epic back silently cleared its exclusion) now records the same way instead of happening invisibly.
amendments[].epic holds another record’s id, so it joins the reference sweep remove-epic runs. Without that, removing an epic would have left a dangling pointer that release show faithfully rendered — found by applying the data-reference half of the call-site rule, which asks where a field holding an id is written, read, and removed.

When to run it

Run /pm:status at the start of every session to confirm where you left off, after /pm:resume to verify the detour stack contracted correctly, after /pm:sync to see newly registered epics, and any time you want to re-orient mid-session. The SessionStart hook fires it automatically when Claude Code opens your project — running it manually just forces a fresh render at any point you choose. Story counts are derived live from each change’s tasks.md — openspec/changes/<id>/tasks.md while the change is in flight, and its archived copy under openspec/changes/archive/ once /opsx:archive has moved it — counted together with any inline stories. If the counts look stale, the tasks.md checkboxes are the source of truth — /pm:status will recount on the next render.

Sample PROJECT.md output

The brief is capped, and what it costs (0.52.0)

The SessionStart brief is capped at 16384 bytes. It was uncapped and grew with a repository’s history: 78,275 bytes, about 19.5K tokens, on pm’s own repo. A brief under the cap is sent unchanged. Over it, only RELEASES (the 3 most recent), DISPOSITIONS, GATE REVIEWS, HANDOFFS and EPIC LINKS (the first 5 each) are bounded, each item line is trimmed to 240 characters and each list ends in (+N more — see PROJECT.md "<section>"); those lists move after every obligation block. Warnings and obligation blocks are never cut: if they alone exceed the cap they are sent whole and the marker says so. On pm’s own record the brief is now 8,242 bytes. The PreCompact snapshot and PROJECT.md’s “Briefing” are unchanged and uncapped. render (this command) and upgrade now end with two lines: the managed rules block as it sits on disk, and the SessionStart brief (bytes and estimated tokens, bytes/4). rules --sections prints the rules block’s per-section sizes, ranked. The brief carries one STOP: spend guard — line for a stuck active epic; see /pm:spend-guard. Hooks drain their payload (0.52.0). brief and snapshot read stdin first, and gate-guard, lesson-advice and commit-nudge do the same where they used to return before reading, so a payload past the pipe buffer (about 64 KiB) can no longer fail the writer with EPIPE. Allow and deny decisions are unchanged.