A lightweight harness above OpenSpec and Superpowers that keeps a Claude Code project on track — across detours, context compaction, and however many epics are in flight at once.
Full documentation lives at pm-plugin.dev. For AI agents: the site
publishes llms.txt (a lightweight, ~7KB index of every doc
page) and llms-full.txt (the entire site as one
document, ~200KB — use sparingly, not as a default).
It answers the three questions you lose the moment context gets compacted or an interrupt derails the session:
- What were we working on before the detour?
- What work is currently outstanding?
- What is the next highest-priority item?
It does this without becoming a second task tracker. Stories stay wherever they already
live — OpenSpec tasks.md, a Superpowers plan, an external issue tracker. PM owns only what
none of those own: cross-epic priority/ordering, an explicit detour stack, and epic
links — including the reconcile relationship where a detour can invalidate the proposal it
interrupted.
When an epic has children, PM doubles as a multi-agent harness: a parent epic's children
run as worktree-isolated, unattended agents converging their work back through sequential
merge — not a metaphor, an actual dispatch-and-converge framework (see /pm:hierarchy under
Commands below).
Not a benchmark — real numbers pulled straight from this repo's own history, verifiable in
git log:
- Every multi-agent hierarchy dispatch runs worktree-isolated, unattended, converging back through sequential merge with zero data loss — every conflict seen so far has been mechanical (a shared CHANGELOG header, a usage string), never a real logic collision.
- 43 releases shipped end-to-end (spec → build → test → changelog → version bump → release) with the plugin managing its own backlog the entire time.
- 603 tests, 0 dependencies — the entire engine is Node 18+ built-ins only, ~6,000 lines
across a 174-line
scripts/conductor.mjsentry point and 32scripts/lib/*.mjsmodules, nothing tonpm install. The suite itself is split across 18 files so it runs in parallel. - Caught its own bugs mid-flight, live: a stale-cache silent fallback, an archived-child leak in
hierarchy planning, a false-positive auto-detour heuristic — each found by using the tool on
itself, logged as a
DF-finding, and fixed in the same session it was discovered.
If you're managing more than one epic at a time, resuming work after a context compaction, or running unattended multi-epic batches — this is the layer that remembers what OpenSpec and Superpowers can't.
Important
0.14.0 — /pm:feedback (file a bug/feature request as a GitHub issue directly from a
session), github-issues tracker inward sync (pull open issues in as untriaged epics), and a
CI workflow gating every PR.
0.13.0 — Worktree-isolated epic-hierarchy dispatch matures: category-based
--preauthorize shorthand, per-epic review-mode escalation, auto-detected minimal detours
from commit shape, dependency-aware top-level queue ordering, and a reconciler that writes
its verdict back durably instead of leaving it in the transcript.
See CHANGELOG.md for the full history.
PM's design choices aren't novel in isolation — they're borrowed, deliberately, from patterns that already work at scale elsewhere and adapted to an agentic coding session:
- Policy-as-code, not policy-as-prose —
/pm:gate-guard's reconcile-owed check is a mechanicalPreToolUsehook, not a rule an agent might forget to re-read after compaction. Same idea as an admission controller: the guard blocks the write, it doesn't just ask nicely. - Worktree isolation for parallel execution — epic-hierarchy dispatch runs each child in its own git worktree/branch, merges sequentially, and treats the orchestrator as the sole writer of shared state — the same shape as isolating parallel CI jobs so they can't stomp on each other's output, applied to parallel agent execution instead.
- An explicit interrupt stack, not implicit memory — the detour stack (PUSH/POP + a mandatory reconcile gate on resume) is the same discipline as saving and restoring context around a hardware interrupt: the thing that got interrupted doesn't get to just "remember" — it gets re-validated before it resumes.
- Instruction layer, never integration layer — the engine never opens a network connection or calls an external system itself (the one documented exception is the opt-in gate-guard hook). External work — GitHub, Jira, Honcho — is always the interactive agent's job; the engine only shapes the instructions it emits. Same separation of concerns as a scheduler that never touches the resources it schedules.
- How to make a detour genuinely resumable — not "remember to come back to this," but a structured stack frame with a reason, a link, and a mandatory reconcile gate that a fresh agent can re-run cold.
- How to run multiple epics unattended without a shared-state race — worktree isolation + sole-writer state transitions, discovered as a real gap during the plugin's own first live dogfood run (see the 0.12.0 changelog entry) and fixed the way you'd want any real bug fixed: in the open, with a design doc, not silently patched over.
- How to keep a hard architectural law honest — "the engine never calls an external system" is enforced by review discipline, not a technical sandbox, and the CLAUDE.md constraints spell out exactly the one documented exception and why.
- How to turn a preflight scan into a real safety mechanism — epic-level autonomy's decision rule (pre-authorized → proceed; no backup path → hard stop; destructive-but- restorable → warn and log; genuine unknown → stop) is designed to be followed by an agent mid-task, not just read once at the start.
- How doc drift actually gets caught — not by discipline alone (that already failed twice this session), but by treating a mismatch between a dispatch table and its own docs as a bug with a filed epic, the same as any other bug.
Requirements: Node 18+ (already present via OpenSpec/Superpowers). No npm install, no other
dependencies — the engine is zero-dependency by hard rule.
This plugin is distributed via the cfdude-plugins marketplace:
/plugin marketplace add cfdude/cfdude-plugins
/plugin install pm@cfdude-pluginsPM works completely on its own — install it and /pm:init gives you cross-epic priority
ordering, an explicit detour stack, and the reconcile gate, with no other plugin required.
That said, PM was designed to sit above three companions, and each one adds something PM doesn't do itself:
- OpenSpec — the
openspeclane's spec-driven proposal workflow (proposal.md/design.md/tasks.md). PM tracks the epic; OpenSpec owns what gets built and its durable spec record. - Superpowers — the
superpowerslane's execution discipline (brainstorming, TDD, subagent-driven development, code review). PM tracks when and in what order; Superpowers drives how well each epic gets built. - Honcho — durable memory that survives outside any single repo. PM's detour stack and reconcile gate are the live working set for one project; Honcho is where a PUSH/POP memory line goes so the relationship between projects — and between sessions of the same project — survives a context compaction, a new machine, or a week away. Genuinely useful the moment you're juggling more than one repo PM manages.
Install OpenSpec:
npm install -g @fission-ai/openspec # or: brew install openspec
cd your-project
openspec initInstall Superpowers (available on the official Anthropic marketplace):
/plugin marketplace add anthropics/claude-plugins-official
/plugin install superpowers@claude-plugins-officialTip
Honcho is the one companion that's genuinely out of scope for a quick install block here — it's a full memory service, not a Claude Code plugin. Plastic Labs offers both a hosted option and self-hosted deployment; start at docs.honcho.dev for current setup instructions, or go straight to the source at github.com/plastic-labs/honcho. Not required for PM to work — only recommended once you want a project's detour history to outlive that project's own context window.
cd your-project
/pm:init/pm:init scaffolds .conductor/state.json, registers any existing OpenSpec proposals and
Superpowers plans as epics, writes the managed rules block into your project's CLAUDE.md
(or the equivalent file for a declared non-Claude-Code platform — see Supported Platforms),
and renders PROJECT.md. From there:
/pm:status # see the current briefing
/pm:next # decide what to work onThe host declares which platform it is via --platform <claude-code|hermes|codex> in the hook
command pm authors for that platform (unrecognized values are rejected, not silently defaulted).
The declared platform is recorded in .conductor/state.json by /pm:init and write-rules —
the commands that accept the flag and (re)write the block. The read-only hooks pass it through
without persisting, and /pm:upgrade does not read it either (its 0.24.0 migration stamps
claude-code on repos that predate the field, rather than adopting a declared value). The
recorded platform shapes two things: the command
form written into the managed rules block (/pm:status on Claude Code and Hermes, /pm-status
on Codex — Hermes keeps the pm: namespace because it silently skips a plugin command that
collides with one of its built-ins, and it ships a built-in status), and which file the block
is written to, first-match-wins over that platform's own project-context precedence chain
(CLAUDE.md for Claude Code; HERMES.md > AGENTS.md > CLAUDE.md for Hermes; AGENTS.md for
Codex, which cannot read CLAUDE.md at all).
| Platform | Status | Notes |
|---|---|---|
| Claude Code | ✅ Supported | The only platform PM actually runs on today — plugin commands, hooks, and skills all target it directly. |
| Hermes | 🗺️ Rules block only | pm renders a correctly-targeted, correctly-worded rules block (--platform hermes) but ships no Hermes commands/hooks yet. Tracked under multi-platform-agent-support. |
| Codex | 🗺️ Rules block only | Same — --platform codex writes AGENTS.md with the flat /pm-status command form, but no Codex prompt files ship yet. Tracked under multi-platform-agent-support. |
| Gemini CLI | 🗺️ Planned | Tracked under multi-platform-agent-support. |
| Grok Build (xAI) | 🗺️ Planned | Tracked under multi-platform-agent-support. |
AGENTS.md-based platforms (generic) |
🗺️ Planned | Most non-Claude-Code tools use AGENTS.md instead of CLAUDE.md for project instructions — supporting that format is the shared unlock for all of the above. |
PM can make a project aware that its epics mirror to an external issue tracker, without the
engine ever calling that tracker itself — same instruction-layer law as everything else. Works
generically with any tracker name (--system isn't an enum), so Jira, Linear, and
GitHub Issues are all supported today.
github-issues was hardcoded inward-only and every other system got the outward mirror. It is
now an explicit direction on the tracker entry, and that recorded value is what every emitter
reads:
direction |
The rules block and the brief tell the agent to… |
|---|---|
inward |
list open items in the tracker's scope and register the new ones with add-epic --status untriaged (deduped by externalUrl), using the deterministic --id the emitted recipe supplies |
outward |
create a tracker issue for any local epic lacking externalId, then keep its status transitioning in step with the linked issue |
both |
do both |
A NEW primary tracker registered without --direction now defaults to inward, whatever the
vendor. This is a deliberate reversal: set-tracker --system jira --project JOB produced the
outward mirror in 0.26.0 and produces the inward pull in 0.27.0. Outward creation of issues in
someone else's tracker is the consequential default and has to be chosen, not inherited. The
one-line remedy, if outward is what you want:
node "${CLAUDE_PLUGIN_ROOT}/scripts/conductor.mjs" set-tracker --system jira --direction outwardExisting repos are unaffected. /pm:upgrade stamps each tracker with the direction it
already behaves with — github-issues primary → inward, any other primary → outward, every
secondary → inward — and an explicitly set value is never overwritten, so a second upgrade is a
no-op. A secondary tracker is pinned to inward; any other value is rejected, because a secondary
is pull-plus-writeback by definition and has never created anything outward.
Direction alone does not turn a section on: an inward section is emitted only when the tracker
also names what to read. A github-issues tracker with no repo still emits neither section,
because pm may not emit a command line with an unfilled placeholder.
Real shape, from a project actually running this in production:
statusIntent maps PM's lifecycle to a semantic target, never a literal workflow-transition
name — the interactive agent resolves the actual transition. An epic mirrored to that tracker
looks like:
{
"id": "job-504",
"title": "[JOB-504] Investigation: matching pipeline audit",
"status": "archived",
"lane": "external",
"externalId": "JOB-504",
"externalUrl": "https://your-instance.atlassian.net/browse/JOB-504"
}Configure with /pm:tracker — it detects signals in your project, confirms with you, and
calls set-tracker. The briefing's TRACKER SYNC line only ever lists honestly-computable
drift (an active-work epic missing externalId); it never fabricates transition state the
engine can't actually see.
Primary + secondary trackers: a repo has exactly one primary tracker (everything above)
plus, optionally, one or more secondary trackers — for when your real dev tracker is Jira but
you also want to watch a GitHub repo for inbound issues, e.g. from outside contributors, or from
another internal repo publishing cross-project notifications (a service filing a GitHub issue in
a downstream repo to flag a breaking change). A secondary tracker gets inward pull (deduped by
externalUrl, which is globally unique, rather than bare externalId, which only has to be
unique within one tracker/repo — two secondary trackers can each have an issue numbered #42
without colliding) plus completion status writeback: when an epic sourced from a secondary
tracker reaches archived, the agent closes the linked issue there too. It never gets
outward-created issues — that stays exclusive to the primary tracker.
node "${CLAUDE_PLUGIN_ROOT}/scripts/conductor.mjs" set-tracker --system jira --project JOB
node "${CLAUDE_PLUGIN_ROOT}/scripts/conductor.mjs" set-tracker --role secondary \
--system github-issues --repo acme/market-intelligenceSee commands/tracker.md for the full --role/--remove contract.
Resyncing after completion: where at least one configured tracker has an emittable inward
procedure — direction includes inward and it names a scope to read — the rules block
instructs the agent to re-sync (/pm:sync) right after closing/transitioning a linked issue as
part of completing an epic. The phrasing stays tracker-count-agnostic regardless of how many are
configured. The SessionStart brief nudges toward /pm:sync on the same condition; it is a
non-blocking reminder, never an automatic sync.
Changed in 0.27.0. Both of those used to fire more widely: the reminder appeared in every rules block with a tracker (citing "the writeback steps above" that the same block never emitted), and the brief's nudge appeared whenever any tracker existed. An outward-only repo now gets neither. If you are diffing 0.27.0's emitted output against 0.26.0's, these are two of exactly three expected differences — the third is progress rendering
· N lifecycle. Treat a fourth as a regression.
Freshness and the refresh gate: a tracker-linked epic carries externalUpdatedAt, the
tracker's own updated timestamp as of the last time the agent actually read that item. Before
an epic becomes the active piece of work its source of truth is re-read, and the obligation is
cleared by record-tracker-refresh. The gate keys on provenance — does this epic have an
external origin — never on direction: an issue filed by a third party and an epic born from a
local spec have different sources of truth in the same repo on the same day.
The brief's freshness line — ⚠ N tracker-linked epic(s) never re-read since mirroring — counts
only epics that can still become work: every archived epic is excluded, because the archive
disposition discharges the refresh obligation outright, whatever the outcome. There is no
terminal watermark to record. The closed half stays the agent's: the engine cannot know a tracker
item is closed without integration it must never do, so the inward-sync step that already reads
the open list is what proposes a disposition for an epic whose item is no longer open — and that
disposition is what clears the count. Measured on this repository when the old behavior was filed
as a bug: 59 counted, 29 of them already ended or closed, and /pm:sync — the action the line
names — could not clear those 29, because an epic that ended has no open item to read.
/pm:init — Initialize the PM conductor in this repo
Scaffolds .conductor/state.json, registers any existing OpenSpec proposals and Superpowers
plans as epics, writes the managed rules block into CLAUDE.md (or the file the declared
--platform actually reads — see Supported Platforms), and renders PROJECT.md.
Safe to run once per repo; re-running is a no-op if already initialized.
The rules block carries a gate procedure of five numbered, required task items — the call-site completeness sweep, commit-based verification, the lifecycle-marker declaration, commit attribution, and ending work by recording a disposition. They are numbered items rather than prose bullets on purpose: measured across an audit of 8 repositories, a rule carried by a mandatory task section reached 14/14 adoption in subsequent changes, and the same rule as a prose bullet reached 3/15.
/pm:status — Show the current conductor briefing
The active epic (with lane), the detour stack, the top-priority epics next up, and per-lane
counts. Re-renders PROJECT.md from .conductor/state.json first. Also surfaces, when present:
ungated archives (archived with no Gate 2 review from anyone — a standing condition, not an
episode, so every session sees it until a real verdict supersedes it), handoffs from both
ends, and each release's N epics, M deferred.
Progress excludes lifecycle bookkeeping. A task that is bookkeeping about the change's own
lifecycle — above all the task that archives the change itself, which cannot be ticked before the
thing that ticks it — carries the literal marker <!-- pm:lifecycle --> on its own task line.
The engine infers this from nothing else: not the wording, not the commands the text names, not
the position in the file. Marked tasks render as · N lifecycle alongside the count (and
0/0 · N lifecycle where every task is excluded), and no longer count as outstanding work at
archive time. Mark it when the task source is authored or amended — a source written before
0.27.0 gets the marker the first time you touch it.
openspec validate --archivedcollides with the marker. Do NOT wire it into a pm-managed repo. The marker is pm's alone: that lint counts raw checkboxes, knows nothing about the marker, and therefore fails every correctly archived pm change — reporting1 incomplete taskagainst the same file pm reports complete with· N lifecycle. Its own help text offers it for pre-commit linting, and nothing clears the failure: ticking the archive task would be a false record, and dropping the marker would break pm's archive gate. Ignoring a marked line upstream is the clean fix and it is not pm's to make. Tracked as #154.
/pm:next — Decide what to work on next
Resumes the top of the detour stack if non-empty; otherwise picks the highest-priority
queued epic (P0→P3), skipping anything starved on an unresolved depends-on link and
naming the blocker when it does.
Reads ## Dependency warnings first. A blocker does not have to be in the queue to be
named: an epic's effective priority is the best of its own and every epic that
transitively depends-on it, so a planned P2 that a queued P1 needs renders P2 → P1
and is called out by name. Computed, never stored — the merit priority stays legible, and
deprioritising the dependent drops the lift with it. An epic waiting on something outside the
queue is reported as unworkable rather than offered, which forces the decision (pull the
dependency forward, or descope the epic waiting on it) that otherwise just stalls.
/pm:detour [what came up] — Handle a mid-build interruption
Classifies the interruption as minimal or substantial before doing anything else.
| Flag | Behavior |
|---|---|
--minimal "<what you fixed>" |
Fast-path: calls log-detour to append to .conductor/detours.log and resume. No proposal, no stack entry. |
| (none) | Substantial: register a new epic in the appropriate lane for the detour, then PUSH the current one onto the detour stack with push-detour. |
push-detour and pop-detour are verbs, not hand-edits. Until 0.35.0 the substantial
detour's PUSH and POP were a documented hand-edit of .conductor/state.json — the state of
record's most consequential transition performed by the one mechanism the rules block tells
every agent never to use, with none of the engine's guarantees applied to it.
push-detour <parent-epic-id> --detour <detour-epic-id> --reason "<why>" (--reconcile | --no-reconcile)
pop-detour [<paused-epic-id>]push-detour validates that both epics exist and neither has ended, requires a non-empty reason,
writes the frame with reconcileOnResume, records both protocol links (detour
resolves-blocker-for parent; parent may-invalidate detour), activates the detour, and emits
the Honcho line — all in ONE guarded write, so it inherits the write-conflict guard and the
read-back verification. Exactly one of --reconcile / --no-reconcile is required and there
is no default: whether the detour can invalidate the paused epic's plan is a judgment, and a
default would make an absent decision look like a considered one.
pop-detour removes the top frame, resumes the epic, and writes reconcileNeeded in the same
write — the frame is gone before reconciliation runs, so a second write would let the
archive-drift self-heal clear the obligation. The optional epic id is an assertion, not a
selector: the stack is LIFO, so naming an epic that is not on top is refused. It emits the POP
Honcho line only when nothing needs reconciling, because "reconciled vs X" is not true until the
verdict exists.
honcho-memory <push\|pop> <epicId> "<reason>" formats the exact ready-to-copy Honcho memory
line for a PUSH/POP and appends a timestamped copy to .conductor/honcho-memories.log — the
engine only formats and logs the string, it never calls Honcho itself. The detour verbs call it
for you; it remains available for a pivot recorded after the fact, and for the POP line after a
reconcile verdict.
/pm:resume — Resume a paused epic after a detour
Pops the detour stack and runs the mandatory reconcile gate: a fresh-context reconciler
agent re-validates the paused epic against what the detour actually shipped, then writes its
verdict back durably via record-reconcile (not just into the conversation transcript).
record-tracker-refresh <epicId> --verdict unchanged|material-change --external-updated-at <iso> [--summary "<what>"] — Record a tracker-refresh verdict
Before an epic becomes the active piece of work, its source of truth gets re-read. For an epic
linked to an external item that means the item itself (body, comments, labels, state); this verb
records what you found. Both arguments are required, so a verdict can never be recorded without
advancing the epic's externalUpdatedAt watermark — <iso> is the tracker's own updated
timestamp, never a local clock reading. Recording clears the epic's outstanding refresh
obligation. An epic with no externalId is refused by name: it re-reads its LOCAL source (plan
document, or OpenSpec proposal plus tasks), and nothing about that is recorded in state.
record-gate-review <epicId> --gate 1|2 --verdict pass|fail --base-sha <sha> --head-sha <sha> [--reviewer "<who>"] — Record an OpenSpec gate review
Writes a fresh-context reviewer's verdict durably onto an openspec-lane epic
(gateReview.gate1/gate2). update-epic --status archived rejects the transition for any
openspec-lane epic that doesn't already have a recorded gateReview.gate2.verdict === "pass" —
Gate 2 (implementation review, before docs) is mechanically required to archive, not just
narrated. Scoped strictly to the openspec lane; superpowers/claude-code/decision/external
epics are completely unaffected.
The verdict carries its evidence as data. --base-sha/--head-sha record the range that was
actually reviewed and --reviewer records who reviewed it, so a verdict can be checked and can go
stale: if the epic later attributes commits the recorded head does not reach, the archive is
refused by name until the range is re-reviewed or the attribution is corrected. Before 0.27.0 a
review of a..b on an epic that then shipped b..c was byte-identical to one that covered
everything.
An archive that reached archived without any review at all now records verdict: "ungated"
instead of nothing. That is a standing condition, reported by the brief and by integrity until a
real passing verdict supersedes it — not an episode that a single session's briefing consumes.
record-cross-spec-review <releaseId> --verdict pass|fail [--reviewer "<who>"] — Record the release-scope cross-spec review
Gate 1 and Gate 2 each take one change as their unit, so nothing above them asked whether a
release's specs agree with each other. /pm:cross-spec-review runs that review — the six
questions (contradiction, double ownership, unmeetable requirements, gaps, vocabulary forks,
shared chokepoints), with BLOCKS/POLISH adjudication — and this verb records its verdict on the
release.
The engine derives the evidence. It enumerates the release's spec set from disk across its
member changes and stores a SHA-256 per file it read; an agent never supplies the list, because a
list typed by the party being reviewed goes stale in exactly the way this gate exists to catch. A
spec added to the release afterwards, or a reviewed spec amended, marks the verdict
⚠ stale on PROJECT.md and the session brief; a spec the engine cannot read reads
⚠ unverifiable and a pass is refused rather than recorded against absent evidence. The record
is keyed change-relative, so the /opsx:archive move never reads as staleness, and re-recording
supersedes the prior verdict while keeping it readable.
The gate applies at two or more spec files counted flat across the release, so one change
carrying six specs qualifies exactly as six changes carrying one each do; below that the verb
refuses, because Gate 1 covers a single spec completely. A multi-spec release with no verdict
renders ⚠ no cross-spec review (N specs) — silence and "reviewed and clean" must not look the
same.
Measured on this plugin's own 0.27.0: six specs that had each passed openspec validate --strict and would each have passed Gate 1 alone returned 5 Critical and 10 Important when
reviewed as a set.
/pm:sync — Register new proposals and plans
Picks up any new OpenSpec proposals or Superpowers plans not yet tracked as epics, and
reconciles openspec/changes/archive/ — an archived change the conductor never knew about is
registered as an epic already in archived status. That backfill is announced once and marked by
archiveBackfilledAt, never a silent side effect. Where a tracker's direction includes inward
and it names a scope to read, /pm:sync also pulls open items in as untriaged epics,
deduplicated by externalUrl (globally unique) rather than bare externalId.
A plan file is matched to its epic by association, not by filename. Plan filenames carry a
date prefix and epic ids do not, so a filename match fired only by luck and every other epic's
plan came back as a fresh untriaged epic on every sync, forever. /pm:sync now skips a plan some
epic's planPath claims — whatever that epic's id, lane or status, so a shipped plan stops being
re-offered without inferring completion from anything — and where a plan matches an existing epic
id minus its date prefix, it registers nothing and prints both exits (associate it, or register
it as distinct work). remove-epic leaves a syncIgnore tombstone so a removal survives the next
sync; attaching that plan to an epic clears it. Attach the plan
(/pm:epic update <id> --plan <path>) and the association does the rest. specPath (--spec)
is the same family one artifact over — the design document an epic came from — and a removal
tombstones it identically, naming --spec in the un-ignore instruction.
/pm:epic — Register or manage an epic directly
| Subcommand | Does |
|---|---|
add --id X --title "…" --lane L --priority P [--status S] [--parent ID] [--external-id KEY] [--add-story "<milestone>" …] |
Register any epic in any lane; optionally nest under a parent or link a tracker issue. --add-story is repeatable, so a plan's milestones land in the same write as the epic instead of one update-epic call at a time afterwards. |
add-many --from <path|-> |
Atomically bulk-create a parent + children from a JSON batch. Each entry may carry a stories array — plain titles, or {"title": "…", "done": true} — validated in the same up-front pass, so a blank title refuses the whole batch. |
update-epic <id> [--title …] [--status …] [--lane …] [--priority …] [--parent …] [--plan …] [--link …] [--clear-links] [--description "…"] [--notes "…"] [--external-id …] [--external-url …] [--external-updated-at <iso>] [--review-mode …] [--add-story "<title>"] [--story <n> --done|--wont-do "<reason>"] |
Write-back path — title corrections, status/lane/priority changes, links, free-text annotation, tracker linkage, per-epic review-mode escalation, inline story mutation (see below). |
update-epic <id> --attribute-commit <sha> |
Record a commit as this epic's work. Repeatable, append-only, in landing order. The engine infers attribution from nothing — not the files a commit touches, not an epic id in a message — so an unattributed commit is one the epic's Gate 2 cannot be checked against. Do not attribute the commit that moves openspec/changes/<id>/ under archive/: it lands after the reviewed range by construction and makes the epic's own Gate 2 stale at the instant the archive gate reads it. |
update-epic <id> --status archived --outcome delivered|killed|superseded|abandoned --reason "<why>" --no-deferrals |
How work ends — a terminal disposition with its reason, never deletion. Every outcome except delivered requires the reason. The deferral assertion is required in the same invocation: swap --no-deferrals for --deferral "<epicId>:<section>" where work is now held by a registered epic, or --declined-deferral "<what>:<why not>" where you are deliberately not doing it. Add --carried-to <epicId> --reason "<which tasks moved>" to hand off unfinished work. |
remove-epic <id> [--cascade] |
Hard-delete; blocked by default if it has children (--cascade removes descendants too). Strips dangling links elsewhere. |
reorder <id> <id> … |
Manual rank — place the epics of ONE priority band, top to bottom, in the order given. Ranks are rewritten dense 1..N on every call, and this is the only thing that writes rank. Takes the whole band and refuses a partial one, so the numbering stays contiguous by construction; unranked epics sort after every ranked one. Rank is the LAST sort key (dependencies → priority → rank) — it breaks ties that today fall through to alphabetical order, and never outranks a dependency or a priority. update-epic --priority clears an epic's rank, since a placement among one band's peers means nothing among another's. |
set-active <id> / clear-active |
Set/clear the top-level active epic. |
Link types are a closed vocabulary, and only two of them do anything. --link "<type>:<epic>[:<reason>]" validates both halves — an unknown <type> is refused with the
valid set, because a typo used to be stored silently and then copied as precedent by the next
agent reading state.json.
| Type | What it does |
|---|---|
depends-on |
Read by the engine — orders the queue so a blocker is listed, and picked by /pm:next, before the epic waiting on it. |
supersedes |
Read by the engine — marks the named epic replaced, for /pm:triage and for integrity. Lives on the epic that replaces. |
may-invalidate |
Protocol state written at a detour PUSH; carries the reconcile verdict. Nothing switches on the type. |
relates-to · blocks · resolves-blocker-for |
Annotation only — recorded for a reader, read by no behaviour. |
resolves-blocker-for is not a synonym for depends-on: the detour protocol puts it on the
detour naming the parent, so the equivalent depends-on edge points the other way. Links already
stored under some other type still load and still render — validation is on write only — and
integrity's link-of-unknown-type check reports each one instead of guessing a repair.
A flag with no value is refused, on every command that accepts it. --clear-links,
--no-deferrals and --done are the three flags that legitimately carry none; every other flag
exits non-zero when its value is missing or blank, rather than writing the epic with the field
quietly absent. The rule lives on the shared flag registry rather than in each command — so
add-epic, update-epic, add-many, record-gate-review, record-cross-spec-review and
release cannot answer it three different ways, which is exactly what they used to do:
add-epic --plan with no value exited 0 and created the epic with no plan attached, while
update-epic --plan refused the identical mistake a minute later.
Inline story mutation — --add-story "<title>" (repeatable, and available on add-epic
and add-many too) appends { title, done: false } to the epic's inline stories[];
--story <n> --done marks the n-th story done, where n is 1-indexed (--story 1 is
the first story). Closes a recurring hand-edit-of-state.json risk — a naive JSON re-escape of
an em dash has corrupted the file before. Every flag rejects out-of-range/empty input and
writes nothing.
--story <n> --wont-do "<reason>" — the third state a checklist needs. A story's done
boolean holds two states and the record needs three: open, completed, and deliberately not
being done. Deletion is not the third state — removing the row destroys the evidence that the
work was ever projected — so the row and its title always survive and only the terminal state
differs. The reason is required: a terminal state with no recorded why reproduces the
original problem one level down. A disposed story leaves both sides of the progress ratio,
exactly as a <!-- pm:lifecycle --> task does (3/3 stories · 2 disposed), and a recorded
disposition is never silently replaced.
This adds no new archive refusal. The archive gate already refuses --outcome delivered
while any work is outstanding, and inline stories are the first progress source it reads — so
an epic with an unticked story has been blocked since that gate shipped. What was missing was an
honest way past it: the refusal's other remedy, the <!-- pm:lifecycle --> marker, cannot be
written on an inline story at all (there is no task source), which left only --carried-to —
naming a receiving epic for work that was dropped rather than moved, i.e. the fabricated
record that refusal itself warns against. On a stories epic the refusal now names the
outstanding stories first and offers --wont-do second, because the block is the reminder.
/pm:hierarchy — Run a parent epic's children as a batched, unattended multi-agent harness
plan-hierarchy --parent <id> computes execution batches from priority + sibling
depends-on links (topological sort, cycle-rejecting). Dispatch is worktree-isolated: each
child runs as its own agent in its own git worktree/branch, never writes
.conductor/state.json itself, and converges back sequentially with the orchestrator as sole
writer of state transitions. An ordinary merge conflict is never a hard stop — it resolves via
a tiered ladder before ever reaching "ask the human."
Discovery is dispatched too, not just the work. The preflight scan is a full read of each child's entire source, so running it inline for N children spends N whole documents of the orchestrator's context before the first child is dispatched — and a subagent's transcript never enters the parent's, only its report does. One subagent per child; same rule for any open-ended search the run needs. The full-read requirement is unchanged: it is about depth, not about who performs it.
set-autonomy <id> — Grant an epic broad execution trust
Only after a mandatory preflight risk-scan (full read of the epic's source, not a keyword
grep). See the conductor skill's "Epic-level autonomy" section for the full process.
| Flag | Does |
|---|---|
--level off|autonomous |
The trust level itself. |
--preauthorize "<action>:<reason>" |
Pre-approve one specific action (repeatable). |
--preauthorize "category:<name>:<reason>" |
Pre-approve a whole class of routine actions (filesystem, network, schema, external-api) without enumerating each one. |
--context "<note>" |
Record background/decisions supplied during preflight (repeatable). |
--notify "<what>" |
Durably record a WARN-class decision as it happens, not just for an end-of-epic report. |
/pm:review-mode — Set this repo's review-intensity dial
set-review-mode --mode off|standard|thorough: off (self-review only) · standard
(default — one fresh-context reviewer per gate) · thorough (two independent reviewers,
adjudicated). A single epic can escalate above the repo's dial via update-epic <id> --review-mode, but never de-escalate below it.
/pm:gate-guard — Inspect the reconcile-gate guard
A hard PreToolUse guard blocking Edit/Write/NotebookEdit while the active epic still
owes a reconcile — on by default and unconditional for that specific case; set-gate-guard off no longer bypasses it.
/pm:triage — Screen an incoming ask against the whole backlog
triage "<the ask>" runs BEFORE add-epic. The dedup the conductor already had is
identity-based — same id, or the same externalUrl — which stops /pm:sync mirroring an issue
twice and does nothing about the same ask arriving under a different name. This returns the
existing epics that share distinctive vocabulary with the ask (each with the shared words
that put it there, and a flag for the ones already superseded), the lane this repo's routing
picks, and the backlog's current shape.
It emits verdict: null and means it: the engine computes what is worth READING and never
decides whether two asks are the same — that is judgment, and judgment is the agent's. Record
what you conclude with --link "supersedes:<id>:<why>", or end an ask you are turning down with
update-epic <id> --status archived --outcome declined --reason "<why not>" --no-deferrals —
because declining by never registering the ask destroys the record that anyone considered it.
/pm:lane-routing — Per-repo lane-routing overrides
set-lane-routing --add "<match>:<lane>" [--add …] | --remove "<match>" | --clear defines
keyword/glob rules checked before the generic lane heuristic — for when "anything touching
billing always goes through openspec" needs to be a rule, not a CLAUDE.md carve-out.
suggest-lane "<free text>" looks one up.
It is an input, not an answer. Routing reads THE ASK — words, size, the overrides recorded
here — and nothing else; it cannot ask whether the work serves something the project already
committed to, because pm holds no milestone or product context to weigh. So the tie-break is
asymmetric: claude-code means no spec, no plan, no gate and no stories, over-processing costs
hours and under-processing costs the record permanently, and an unresolved routing question
therefore resolves away from claude-code, never into it. Departing from the suggestion is
expected — record why on the epic (update-epic <id> --notes "lane: <chosen> not <routed> — <why>"), which every path that registers an epic now asks for, not just the mirrored ones.
/pm:tracker — Make the conductor aware of an external issue tracker
Detects signals, confirms with you, and records the tracker (Jira/GitHub/Linear) — the engine
never calls the tracker itself; it only shapes the instructions it emits for you to act on.
set-tracker --direction inward|outward|both sets which instructions those are; a new primary
with no --direction defaults to inward regardless of vendor (see
Direction is configuration, not a vendor rule).
/pm:feedback — File a bug report or feature request
/pm:feedback [bug|feature] "<summary>" gets a bug report or feature request to cfdude/pm
through whichever of three channels the machine can actually use. All calls are agent-invoked;
the engine itself never touches GitHub.
gh and an authenticated GitHub account are an OPTIONAL dependency, not a requirement (#105).
The report is written to .conductor/feedback/ first, on every path, so nothing is lost when
a channel fails. Then, in order of least friction:
| # | Channel | Needs | Notes |
|---|---|---|---|
| 1 | gh issue create |
gh installed and gh auth status clean |
Preferred whenever available — no browser, no login, and it dedups against open issues first |
| 2 | Prefilled issues/new?title=…&body=… URL |
a browser | No credential touches the plugin, and the issue is attributed to whoever hit the bug. ~6 KB URL ceiling ≈ ~3 KB of raw body; longer reports are truncated with a pointer to the local file |
| 3 | bugs@pm-plugin.dev |
an email client | For users who don't have — or don't want — a GitHub account, so declining GitHub never means losing the feedback |
Both checks are run before any gh call, and a missing one is "this channel is unavailable"
rather than an error. curl is not a substitute for channel 1: anonymous issue creation returns
HTTP 401, and a PAT is strictly worse than gh (same account requirement, hand-managed token).
The dependency was never on the CLI — it is on holding a GitHub credential at all.
The inward tracker sync (/pm:sync against a github-issues tracker) also needs gh plus
an authenticated account, and unlike feedback it has no credential-free fallback: listing issues
is a read, and anonymous listing is not available. The emitted rules block now states that
preflight and tells the agent to STOP that section rather than report a sync it could not
perform.
The CLAUDE.md rules block includes an unconditional "Feedback" section encouraging the agent
to use this proactively — file a bug/limitation/friction point (or ask "want me to file this
as feedback?") instead of silently working around it. Recurring friction that never gets
reported is a product failure the same way a crash is; this section exists because that
happened here for real (see df-update-epic-no-story-toggle-verb).
/pm:changelog — Show what changed in the plugin
The changelog delta between this repo's stamped pmVersion and the currently installed
version.
/pm:upgrade — Upgrade this repo's conductor state/rules
Refreshes the managed rules block (write-rules) in whichever file the recorded platform
reads, runs any pending migrations — including stamping platform: "claude-code" on a
pre-0.24.0 state file that predates the field — re-renders PROJECT.md, and stamps the new
pmVersion. Idempotent — safe to run more than once. Requires /reload-plugins first if you
just updated the plugin (the SessionStart briefing tells you when).
The engine also exposes rules-target, a read-only query printing the absolute path of the file
the recorded platform's rules block belongs in (resolving that platform's first-match-wins
chain). It exists so tooling around pm never has to mirror the chain — a second copy of platform
knowledge is drift waiting to happen. Unlike write-rules, it records nothing.
After showing the changelog delta ("What's new in pm"), the agent reviews each Added
headline and recommends adopting any opt-in capability that's relevant to this repo's current
.conductor/state.json (e.g. secondary trackers, thorough review mode) — one line, one
reason, the command to run. It never enables anything itself.
If an upgrade goes wrong, git is the rollback. .conductor/state.json is git-tracked, so
commit state.json before upgrading (a restore discards every uncommitted state change since
the last commit, not only the migration's), then git restore .conductor/state.json and
/pm:status to re-render PROJECT.md from the restored file.
Rolling back state does not require rolling back the engine — every added field has a
documented absent-value default, so the current engine behaves identically on an older state
file. Rolling back the engine is a separate plugin-level operation: pin the marketplace
source to the prior ref and /reload-plugins.
release <id> --intent "<what>" [--target <date>] [--member <epicId>] [--defer <epicId> --reason "<why>"] — Plan a release as a first-class object
state.releases[] holds {id, intent, target, deferred[]}. Membership is recorded one-way as
epic.release, so the release and the epic can never disagree about whether an epic is in it, and
the engine proposes membership for nothing.
--defer <epicId> --reason "<why>" records a deliberate exclusion: the epic stays in the
backlog rather than being ended, and the reason survives the release closing. That is the same
reason-bearing disposition record used at the other three scopes (an epic ending, a deferral in a
design doc, a handoff), applied to release scope. PROJECT.md and the briefing render
<release>: N epics, M deferred, with every deferral's reason reachable from state.json.
Without it, "we deliberately cut X because Y" survives only in a conversation transcript — which is exactly the failure this release exists to fix.
integrity — Read-only audit of the record itself
Reports records that cannot be true: an archived epic whose task source exists with nothing
ticked, one change registered under two lanes (keyed on the date-prefix-stripped id), a gate
verdict whose recorded range does not reach the commits its 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 Gate 1, an epic archived with an ungated Gate 2
(no review from anyone), 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, a recorded commit sha this repository can no longer resolve, an epic still
open in a release that has already delivered, and an epic another epic declares it supersedes
that never ended.
The release one closes a real loop. A release object carries no delivery marker, so "this
release delivered" is read from its members: at least one holds a delivered disposition, and
none is active or paused — a staged release still in flight stays silent. Every member left
non-terminal that the release's own deferred[] does not name is reported, because the record
says neither that it shipped nor that it was cut. That is the shape of #137: 0.27.0 shipped, all
twenty of its member epics stayed queued, and next recommended two P0s that had shipped hours
earlier. Both new checks report only epics whose status is non-terminal, so neither can add a
finding for work that has already ended.
That last one has a deadline. The shas in attributedCommits and in a gate verdict's
baseSha/headSha are the evidence — "a reviewer read this range" is only checkable while the
range exists. A squash-merge leaves every commit on the merged branch reachable from no ref, and
the next git gc deletes them (default gc.pruneExpire: two weeks); measured on this repo right
after a release, 36 recorded shas were reachable from nothing while every check stayed green. The
check separates orphaned — still in the object store, recoverable now with git tag — from
already gone, and reports nothing at all in a clone that resolves none of the record, because
a fresh, shallow or single-ref clone lacks that history rather than having destroyed it.
Expect a burst of heal-archived-epic-passed-gate-2 on your first run after upgrading. Every
repo that followed the documented /opsx:archive → 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 — so they miss it by one step. That is expected, not a bug; the finding names
the exact remedy (update-epic <id> --status archived --outcome delivered --no-deferrals), and
the archive gate lets an agent replace an engine stamp, so nothing is frozen at unknown.
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.
claim / unclaim / owners — Who owns this epic, and is this repo quiescent?
For multi-session and multi-repo work. An orchestrator dispatching epics into a sibling repo's
conductor needs to know whether a session is mid-operation there before it writes, and needs to
tell in progress from abandoned when a session died. state.json could answer neither.
node scripts/conductor.mjs claim <epic-id> --session <name> [--ttl <minutes>] [--steal]
node scripts/conductor.mjs claim --repo --session <name> [--ttl <minutes>] [--steal]
node scripts/conductor.mjs unclaim <epic-id> --session <name> [--steal]
node scripts/conductor.mjs owners [--json]--session is required; PM_SESSION supplies it, and an explicit flag outranks the environment.
It is advisory, and that word is load-bearing. claim and unclaim are the only verbs that
refuse because of a claim — everything else writes to a claimed epic exactly as before. When two
sessions both claim, the second is told who holds it and until when and exits non-zero having
written nothing; there is no silent-corruption reading because it is not a write. The real
write race is handled separately, by state.json's revision guard, which --steal does not
touch. A marker nobody honours is worse than none, so exactly one surface refuses: the one whose
whole job is coordination.
What expires a claim is its own stated TTL, recorded on the claim (default 120 min for an
epic, 30 for the repo marker). Deliberately a TTL rather than a heartbeat: a heartbeat nothing
beats is claimedAt in a costume, and it makes staleness wrong in both directions. Re-claiming as
the same session extends it. Claiming over an expired claim needs no --steal and reports the
takeover; archiving an epic clears its claim.
What reports a stale one: owners when asked, and integrity's advisory-claim-shape check
when nobody thinks to ask — which matters, because a stale claim is by construction left by a
session that is no longer there to ask. owners is behaviourally verified read-only, so an
orchestrator can point it at a repo it does not own.
The repo-level "some session is mid-operation here" marker lives in a git-ignored sidecar,
.conductor/session-claim.json, not in state.json — it answers is it safe to write to
state.json, so putting it in the file it is worried about would invert its purpose. Release it
when you are done touching the conductor, which is strictly later than when the work is done.
activity / set-activity-log / purge-logs — What did this conductor actually do?
state.json is a snapshot of the present. It cannot answer a single question about how a
project got there. Off by default; on in the maintainer's repos.
node scripts/conductor.mjs set-activity-log on|off
node scripts/conductor.mjs activity [--since <iso>] [--epic <id>] [--json]
node scripts/conductor.mjs purge-logs [--kind activity|conflicts|detours|all] [--keep <n>] \
[--over <size>] [--older-than <days>] [--dry-run] [--yes]The reader ships with the writer — that was the condition, not a nicety. A log nothing reads
is a data graveyard: write-path complexity, rotation, retention and a purge CLI all paid
immediately, for a benefit that stays speculative. So every section activity prints answers a
question the feature was filed to answer — time to pickup, detour frequency, lane distribution and
re-routes, gate verdicts in sequence, and out-of-band writes.
That last one is why it earns its overhead. Every event carries the state.json revision range
it covers, so a revision the file reached that no event accounts for is a write the engine did not
make — a hand-edit. That is a gate defeated silently, previously discoverable only by jq across
15 repos, turned into a query. (The range, not a single number, is load-bearing: update-epic
saves twice, and a single-number event would report every ordinary write as a hand-edit.)
Events are derived by diffing state.json across one invocation, from a single point in the
engine's dispatch — not emitted by hand from each of the ~25 verbs that write state. A list of emit
sites typed from memory goes stale the moment a verb is added, and a verb that forgot to emit would
be invisible in exactly the log built to find invisible things. It is registered on
process.on("exit"), because one mutating verb writes state and then calls process.exit().
Sized rather than guessed: 128 KB segments (≈680 events ≈ 37k tokens — a segment must be fully
readable in one pass), named activity-<ISO start>.log so retention and --since are answerable
from the filename without opening a file, and 1 GB total per project, oldest first, pruned at
the rotation boundary only. .conductor/activity/ is git-ignored by init and by upgrade.
purge-logs refuses to guess twice: with no selector it removes nothing, and without --yes it
prints the plan and removes nothing.
verify-specs — Which design documents have no epics?
An epic can record the design document its work was drawn from: --spec <path> →
specPath, on add-epic, update-epic and as a specPath key in an add-many batch. It is
provenance only — nothing reads progress from it and no scan registers epics from it — and it is
deliberately many-to-one, which is the concept that was missing. A Tier-2 design enumerating
six implementation chunks produced exactly one epic, because only that chunk also got a plan
file; the other five were found by hand 11 days later, having blocked every release in between.
Scanning a specs directory would not have helped: that yields one epic per document, which closes
when chunk 1 ships.
node scripts/conductor.mjs verify-specs [--root <path>] [--headers]Prints, for every .md file under the root (recursively, minus README/INDEX/CONTRIBUTING),
how many epics claim it and which ones — then the other half of the set difference, the epics
whose specPath names a document that is not on disk. Coverage is status-blind: an archived epic
for chunk 1 is coverage of chunk 1.
It is not an integrity check, on purpose. integrity reports shapes that cannot be true; a
document with no epic can be true and usually is — a note, a reference, an abandoned sketch.
Filing every one as a finding is the noise that got the plan-freshness warning cut back (wrong 7
times out of 8), so this prints an inventory with none of integrity's finding vocabulary,
always exits 0, and stays out of the session brief. An absent root says no spec root rather
than reporting zero uncovered — silence and clean must never look the same.
Enumerating what a design implies stays with the agent, which is reading the document anyway;
add-many is the atomic fan-out (one write, N chunks, all naming the same specPath) and the
engine computes only the difference.
--headers — the epics a document already names. Most design documents open with a header
that already says which epics they are for (**Epic:** \gh-82-…` · Relates: `…`). verify-specs --headersreads those headers and prints, for every **uncovered** document, the ids it names and theupdate-epic --spec line that would attach each one. It **proposes and never applies** — the same splittriageuses for asks: mechanical candidate set, your verdict. A header id that names no epic in the record is reported as a finding, from covered documents too. The parse is label-agnostic and region-bounded: anyAnything:line in the leading metadata block opens it, the first blank line ends it, and backtick-quoted tokens matching the epic-id format are the candidates, each carrying its label verbatim. It exists because deriving the association from FILENAMES placed 8 of this repository's 10 documents and structurally could not place the other two — the many-to-one casesspecPath` is for.
verify-state — Detect an undetected hand-edit of state.json
Compares state.json's filesystem mtime against the timestamp recorded at the last
render(). Fails loudly (non-zero exit) if state.json was modified after the last render —
mechanical evidence of a hand-edit, which is against the rules (state.json should only
change through the engine's own subcommands).
render --diff-summary — Mechanically check whether a PROJECT.md diff is epic-relevant
node scripts/conductor.mjs render --diff-summary prints epic-relevant: yes or
epic-relevant: no to stdout in addition to rendering as usual. Two things change PROJECT.md
on nearly every render even when nothing about the epics themselves changed — the
"Last rendered" timestamp line, and the "Recent detours" table (which rotates as new entries
land, oldest falling off its 8-row window). Eyeballing a raw git diff PROJECT.md to decide
whether those are the only changes is error-prone; --diff-summary normalizes both away and
reports whether anything else differs, so the "safe to discard this diff as noise" call
becomes mechanical instead of a manual read. A PROJECT.md that has never been rendered before
always reports yes (there's no baseline to compare against).
changesets — List pending CHANGELOG fragment files
Lists every .changesets/*.md fragment as { changesets: [{ id, path, body }] }, sorted by
epic id. Hierarchy children write their changelog entry to .changesets/<epic-id>.md instead
of editing CHANGELOG.md's shared [Unreleased] section directly — eliminating the merge
conflict that section otherwise guarantees across parallel batches. The orchestrator remains
the sole writer of CHANGELOG.md, consolidating pending fragments into it once at release
time, then deleting the consumed fragment files.
--help is per-verb. Name a verb and you get that verb's real flag surface; name none and you
get the verb list.
node "$ENGINE" update-epic --helpconductor.mjs update-epic — 27 flags.
--title <a value>
--lane <openspec|superpowers|claude-code|decision|external>
--priority <P0|P1|P2|P3>
--status <untriaged|queued|active|paused|later|blocked|planned|archived>
--parent <a value>
--external-id <a value>
…
--wont-do <a reason>
Docs: https://pm-plugin.dev/llms.txt
That block is captured output, not a hand-written illustration — an earlier draft of this section composed one from memory and got the flag order wrong, in a section about help that cannot lie.
Every line is projected from the same registry rows the unknown-flag guards read, so help
cannot advertise a flag the parser refuses — adding a flag grows both in one edit, and a
hand-written help table could not make that promise. (no value) and (repeatable) are called
out because they change the shape of a correct invocation: a valueless flag given a value is
refused, and repeating a non-repeatable flag silently keeps only the last one. A flag with a closed set of legal values names them — --outcome, --status, --lane, --priority, --platform and the rest — so the answer does not live in scripts/lib/ any more. Flags without a closed set still render <a value>; that gap is visible rather than papered over.
A verb that takes no flags says so rather than printing an empty list. 21 of the 48 are in that group, and "takes none" must not look like "nobody declared this yet".
add-many is the one verb whose registry rows are not all flags — its parser takes only
--from, and the rest describe keys inside the batch JSON. Its help says both, so a reader does
not conclude the verb is impoverished and go back to the source.
Help never touches state. A help flag reaches no subcommand, which is why log-detour --help
does not write a detour entry described as --help — the bug that put the short-circuit there.
Where the two channels differ. The installed engine is the authority on what it accepts and
is version-exact by construction. pm-plugin.dev is authoritative
for procedure, concepts and rationale, and documents the latest release — so a flag the site
shows that your engine refuses is a version gap, not a bug. /pm:changelog tells you which.
The docs also expose a free, no-auth MCP server at https://pm-plugin.dev/mcp.
Every managed repo receives this same routing in its CLAUDE.md rules block on /pm:upgrade.
Reach for a render to "just look at" another repo's backlog and you dirty it. There is now a
stated contract, declared in scripts/lib/verb-effects.mjs and enforced by the test suite —
completeness against the engine's own dispatch table, plus a real run of every read-only verb
with the whole repo hashed by content and mtime before and after. A write added to a verb
that used to be safe fails CI rather than someone else's checkout.
| Effect | Verbs |
|---|---|
| read-only — safe against a repo you do not own | activity · brief · changelog · changesets · gate-guard · integrity · lesson-advice · owners · plan-hierarchy · rules · rules-target · suggest-lane · triage · verify-specs · verify-state · verify-worktrees |
mutates — writes state.json, PROJECT.md, CLAUDE.md, or a .conductor/ log |
everything else, including render, snapshot, sync, commit-nudge, upgrade, write-rules, and every add-/update-/set-/record- verb |
Want the current state without touching anything? Use brief, not render.
render is mutates even though it is idempotent when nothing changed: since the PROJECT.md
-is-never-clean fix it skips both writes when the content would be identical, but with anything
to render it rewrites PROJECT.md and .conductor/render-stamp.json. Idempotent-when-nothing-
changed is not read-only.
A --read-only enforcement flag was considered and declined: it would have to be threaded
through or sniffed from argv at forty verbs, and it asks a caller to trust that the flag was
wired up. The CI-time behavioural check gives the same guarantee — a doc that cannot drift —
without shipping anything.
Installed to skills/ on /pm:init:
| Skill | Description |
|---|---|
conductor |
The full discipline — detour classification, PUSH/POP, the reconcile gate, epic-level autonomy's preflight scan, epic-hierarchy orchestration. Triggers on "what were we working on," "this is broken, fix it first," "park this," "resume." |
cross-spec-review |
The release-scope gate: do this release's specs AGREE with each other? Gate 1 and Gate 2 each take one change as their unit and structurally cannot find a contradiction between two of them. Triggers on "cross-spec review," "do the specs agree," "review the specs together." |
lessons |
Keeping a repository's PROCESS knowledge where it fires on the situation rather than on recall — the docs/lessons/ shape, the frontmatter contract, and the capture half. Its detect: matchers are what the lesson-advice PreToolUse hook fires on. pm ships the mechanism, never the corpus. Triggers on "lessons learned," "have we hit this before," "that cost us." |
dogfooding |
Routing what the work taught you instead of leaving it in the transcript: a practice you adopted becomes a registered candidate with its evidence attached, and a workaround you invented for tooling friction becomes a filed bug. Triggers on "should this be in the product," "I had to work around," "there's no verb for this." |
You configure verbosity and format deliberately, once, globally — in an output style, or as a communication contract in your CLAUDE.md. A plugin re-imposing its own defeats that, so pm's reporting shapes are split three ways and only one of them is pm's to keep.
| Band | What it is | Follows your contract? |
|---|---|---|
| Recorded | --outcome/--reason, --no-deferrals, gate verdicts, --attribute-commit, --notify, record-reconcile |
No — these are writes to .conductor/state.json, not sentences. A communication preference applied to a record is data loss, not brevity. |
| Parsed | hierarchy-child-executor's STATUS/DONE/DECISIONS/CONCERNS, merge-conflict-resolver's, reconciler's VERDICT/AMENDMENTS/NOTES |
No — a wire format between agents. The orchestrator branches on STATUS; a reshaped field name is a report nobody reads back. The prose inside a field does follow your contract. |
| Narrated | the consolidated end-of-hierarchy report, the end-of-epic autonomy report, the preflight question batch, gate summaries, /pm:status narration, /pm:next's recommendation |
Yes. pm's headings are a default for a user who has configured none — not a style that outranks one. |
Reshaping is always allowed; omitting never is. Where your shape has no slot for something pm
requires — the notifications[] read-back, the "are you OK with these?" checkpoint, the deferral
list — pm's instruction is to add a slot, not to drop the element.
One inheritance detail decides what actually reaches a dispatched child. A subagent inherits
every level of the CLAUDE.md hierarchy the main conversation loads, ~/.claude/CLAUDE.md
included; an output style applies to the main conversation only and does not reach one. So a
contract that lives only in an output style never arrives at a child executor or the reconciler —
put it in CLAUDE.md if you want the whole hierarchy to honour it.
View hooks and agents
Hooks (hooks/hooks.json) — dormant until /pm:init runs in a project:
| Hook | Purpose |
|---|---|
| SessionStart (startup / resume / compact) | Injects the briefing via additionalContext — the index comes back the moment context is summarized away. |
| PreCompact | Calls snapshot (render + .conductor/brief.txt) right before the context window collapses. |
PostToolUse (every Bash call) |
Calls commit-nudge. It OBSERVES the repository rather than reading the command text: it keeps a HEAD watermark (.conductor/commit-watch.json, git-ignored) and speaks only when HEAD has moved AND git reflog says the move was a commit. So -m, -am, -F, an editor commit and a commit made inside a script are all noticed, while a command that merely mentions git commit — a grep, a heredoc, an echo — a rejected commit, a commit that landed in another repo, and a checkout/reset are all silent. Then it nudges a state update, and auto-detects an unlogged minimal detour from commit shape (only while an epic is active, and excluding routine conductor bookkeeping commits). On an observed commit it also names the exact update-epic <id> --attribute-commit <sha> for the epic that commit belongs to — the detour epic while a detour is live, never the paused parent — so the per-commit attribution obligation is prompted while it is still actionable rather than only checked at the archive gate. The prompt is louder while the epic's attributedCommits is still empty (the last moment the catch-up-in-order rule is available) and one line thereafter, and it is absent entirely where the engine would be guessing: no active epic, an epic with no attribution array, or an unobserved commit. |
| PreToolUse (gate-guard) | Hard-blocks Edit/Write/NotebookEdit while the active epic owes a reconcile — on by default, unconditional for that case. |
| PreToolUse (lesson advisor) | Calls lesson-advice on Bash/Edit/Write/NotebookEdit. Matches the pending tool call against every docs/lessons/*.md entry that declares a detect: matcher in its frontmatter, and injects that lesson's rule before the mistake. Advisory only — it never blocks and always exits 0, which is why it is a separate entry from the gate guard. Silent in a project with no docs/lessons/, and dormant until /pm:init. Precision is the constraint, not coverage: a lesson that cannot be matched with near-certainty carries no detect: and stays retrieval-only, and only the command's first line is matched, so a heredoc body or an echo that merely names a command is data rather than a trigger. Adding a matcher is a frontmatter edit, never a code change. |
Tool currency. pm and superpowers are plugins that update themselves, but OpenSpec is a
CLI you upgrade by hand — and openspec update, which regenerates the per-project instruction
files, slash commands and skills the whole OpenSpec lane runs on, is a separate manual
per-project step. So a machine can sit on OpenSpec 1.10 while a project still runs 1.6's generated
artifacts, indefinitely, with nothing saying so. (Measured in this repo: four minor versions
stale, while actively running an OpenSpec change.)
The session brief and /pm:upgrade now both report that drift, from one shared emitter so the two
can never disagree. Three things about how it behaves:
pmnever runsopenspec update. It emits the instruction and you run the terminal command, exactly as withopenspec init— a source scan in the test suite fails the build if any engine file ever passes theopenspecbinary an argv other than--version.- It holds rather than suppressing itself mid-change.
openspec updaterewrites the instruction files an in-flight change is being authored against, so with an active change the drift is still reported but the imperative becomes hold until<change>is archived. Suppressing outright would silence it permanently in any repo that usually has a change open — which is precisely how the drift above accumulated unseen. - It tells you whether a diff will exist. Where the generated files are git-tracked,
git diffafter the run is your review; where they are not, it says to copy them aside first, because the rewrite destroys local edits to.claude/skills/openspec-…with no diff and no trace.
Either version being undeterminable is reported as cannot tell, never as stale, and nothing is
spawned at all unless the repo has an openspec/ directory and a readable generated stamp.
Agents (agents/) — dispatched by name, run in a clean context:
| Agent | Purpose |
|---|---|
reconciler |
Fresh-context re-validation of a paused epic against what a detour actually shipped, at the reconcile gate. |
hierarchy-child-executor |
Executes one child epic from a hierarchy batch, front-loaded with its autonomy grant, in its own worktree. |
merge-conflict-resolver |
Second rung of the tiered conflict-resolution ladder — resolves a worktree-merge conflict after a normal git merge fails. |
/pm:init → /pm:status → /pm:next → (build the epic in its own lane) → /pm:sync
↑ │
└────── /pm:detour ────────────┤
(minimal: fix→commit→push→log, resume)
(substantial: PUSH → build detour → /pm:resume → RECONCILE GATE → POP)
Multi-epic batches: plan-hierarchy --parent <id> → dispatch each child (worktree-isolated) →
merge sequentially → orchestrator applies all state transitions as sole writer →
verify-worktrees for hygiene.
your-project/
├── .conductor/
│ ├── state.json # state of record — epics, detour stack, links, autonomy grants
│ ├── detours.log # append-only trail: timestamp · SHA · kind · epic · note
│ # — one row per commit: a SHA already logged under a kind is not
│ # given a second row, and a commit touching only pm's own
│ # generated files is bookkeeping, not detour work. MINIMAL rows
│ # are exempt — they record what you declared, not what git saw.
│ └── honcho-memories.log # ready-to-copy Honcho memory lines, timestamped
├── CLAUDE.md # managed rules block (idempotent; delete to opt out)
│ # — AGENTS.md instead, on a platform that reads that file (see
│ # Supported Platforms below); pm targets whichever file the
│ # declared platform actually resolves first
└── PROJECT.md # generated view — never hand-edited
pm/ (this repo)
├── .claude-plugin/plugin.json manifest
├── CHANGELOG.md release history (Keep a Changelog + SemVer)
├── commands/ /pm:init /pm:status /pm:next /pm:detour /pm:resume /pm:sync
│ /pm:epic /pm:hierarchy /pm:tracker /pm:feedback /pm:lane-routing
│ /pm:review-mode /pm:gate-guard /pm:changelog /pm:upgrade
├── skills/conductor/SKILL.md the discipline
├── agents/ reconciler.md · hierarchy-child-executor.md · merge-conflict-resolver.md
├── hooks/hooks.json SessionStart · PreCompact · PostToolUse · PreToolUse
└── scripts/conductor.mjs the engine (zero dependencies)
See CONTRIBUTING.md for the dev/main branch workflow, PR requirements, and CI gate. See CHANGELOG.md for version history.
Contributing to pm itself requires one-time setup that pm's own users do not need. This
repository is managed by the plugin it ships, so pm's hooks would otherwise run the installed
engine against your checkout — rewriting the tracked PROJECT.md with output a release out of
date, on every commit. Export PM_ENGINE_DELEGATION naming your checkout, in your shell profile:
export PM_ENGINE_DELEGATION="$HOME/path/to/your/pm/checkout"It lives outside the repository because a plugin cannot write your shell profile — which is precisely what makes it a trustworthy authorization. Full explanation, and how to verify it took, in CONTRIBUTING.md.
If you are a pm user, there is nothing here for you to configure. Unset is the default and the correct state; the installed plugin runs its own engine, as it should.
Which repository did that command just write? The engine resolves everything it touches —
.conductor/, PROJECT.md, the managed rules block — from CLAUDE_PROJECT_DIR when that
variable is set, falling back to the working directory. That is deliberate and is how a session
targets a sibling repo's conductor. But when the variable resolves to a different directory
than your cwd and your cwd has a .conductor/ of its own, every invocation now warns on stderr
and names both repositories, so a redirected write never looks like a local one. It is a warning,
never a refusal, and nothing silences it — not PM_QUIET_ENGINE_BANNER, and not the presence of
CLAUDE_PROJECT_DIR itself. Running the engine from a subdirectory, or pointing it at a project
from a directory with no conductor, stays silent.
Tracked in this repo's own conductor backlog (PROJECT.md) rather than a separate board —
pm manages its own development. Notable planned items: multi-platform agent support
(Codex, Gemini CLI, Grok Build, generic AGENTS.md), an AI feedback loop closing the
/pm:feedback ↔ issue-sync cycle, and portfolio-level architecture-consistency scanning
across the backlog.
MIT © Rob Sherman
