Skip to main content
/pm:upgrade brings your project up to date after installing a new version of PM. It refreshes the managed rules block in CLAUDE.md, runs any pending migrations on .conductor/state.json, re-renders PROJECT.md, and stamps the new pmVersion — all without touching your epic history or any non-PM content. Use it whenever the SessionStart briefing shows an upgrade nudge like pm 0.4.0 → 0.4.1 available.

When to run it

The SessionStart briefing tells you when an upgrade is needed. The sequence is always:
  1. Install the new PM version via the Claude Code plugin marketplace.
  2. Run /reload-plugins (or restart Claude Code) to load the new plugin code into the running session.
  3. Run /pm:upgrade to migrate your project’s state and refresh the rules block.
If you skip step 2, Claude Code is still executing the old plugin code and the upgrade will not apply the correct migrations. The briefing message "this is pm 0.4.0 but 0.4.1 is installed" is the reload reminder — it means the reload has not happened yet.

What it does

1

Refreshes the managed rules block

The managed PM section is replaced with the version shipped by the new plugin. Any content outside the managed delimiters — your own notes, other tool rules, project context — is left completely untouched.As of 0.24.0 the target file is platform-aware: CLAUDE.md on Claude Code, and on a platform that resolves project context through a precedence chain, whichever file that platform will actually read. Upgrading from a pre-0.24.0 release also migrates the block’s own delimiter wording in place — detection keys on a stable prefix, so an older block is found and refreshed rather than duplicated.
2

Runs pending state migrations

The engine applies any schema migrations needed to bring state.json up to the new version’s format. Migrations are idempotent: if your state is already at the target schema, the migration step is skipped with no changes written. The 0.24.0 migration stamps a platform field on repos that predate it, defaulting to claude-code.
3

Re-renders PROJECT.md

PROJECT.md is regenerated from the now-migrated state.json, picking up any new fields or formatting changes introduced in the new version.
4

Stamps pmVersion

The pmVersion field in state.json is updated to the newly installed version, so future SessionStart checks know the upgrade has been applied and stop showing the nudge.

How the rules block is found (0.44.0)

By whole-line markers. A BEGIN line is one that starts with {/* BEGIN pm-conductor rules and ends with */}; an END line is exactly {/* END pm-conductor rules */}. A marker string anywhere else — in prose, in inline code, after indentation — is ordinary text. Exactly one BEGIN line followed by one END line → the block between them is replaced and every byte outside it is left unchanged; no marker lines → the block is appended; any other arrangement is refused (below). The block is spliced in literally, so a recorded value containing $& or $`` lands verbatim, and it takes the file's line endings: a CRLF file stays CRLF. Before this, markers were matched as substrings — a prose line quoting the BEGIN marker above a hand-written section made a refresh delete that section and print refreshed`. Six more verbs declare --platform: init and the five hook verbs (brief, snapshot, commit-nudge, gate-guard, lesson-advice), because hooks/hooks.json passes --platform claude-code to each. Every one refuses a valueless or unknown value by name, and init does so before it creates .conductor/state.json — init --platform bogus used to create it and then exit 1, ending pm’s dormancy in a repo whose init had failed. Any flag a verb does not declare is refused before anything is written, on these verbs as on every other; in a repo without pm the hook verbs refuse nothing.

When upgrade refuses — exit 11, and nothing is written (0.44.0)

upgrade checks two files before its first write and refuses, exit 11, if either is in a state the engine will not guess about. A refused upgrade writes nothing: state.json (its pmVersion included), PROJECT.md, .gitignore and the rules file stay byte-identical, so the repository keeps reading as not upgraded — which is what the fleet procedure should go on reporting until a human fixes it. A malformed rules block. An orphan BEGIN or END marker line, or two blocks. With the END line deleted from a repo’s CLAUDE.md and pmVersion at 0.42.0:
pmVersion still read 0.42.0 afterwards. The engine refuses rather than repairs because repairing an orphan marker means guessing where the managed text ends, and that guess is how hand-written text was lost. Two well-formed blocks have an unambiguous extent but not an unambiguous intent. Delete the stray lines — or a whole duplicate block — from the shell, highest line number first, then re-run /pm:upgrade. A marker line inside a fenced code example counts; the outcome is a refusal with line numbers, never a deletion. An unreadable .conductor/state.json. Conflict markers, truncation or the wrong shape: the refusal names the reason and the git remedies (the full message is in /pm:gate-guard). Fix the file, then re-run. --force does not override it. upgrade also back-fills .gitignore through the ensureGitignore() it already runs, adding any of pm’s per-checkout files the file lacks: .conductor/brief.txt (the SessionStart/PreCompact brief), .conductor/detours.log, .conductor/write-conflicts.log and its rotated .conductor/write-conflicts.log.prev, .conductor/write-conflicts.latch, the Honcho outbox .conductor/honcho-memories.log (written by name, so a repo does not depend on a global *.log rule to keep it out of git), .conductor/commit-watch.json, .conductor/commit-observe.json*, .conductor/session-claim.json* (appended alongside any older exact session-claim.json entry, which is left in place, harmless), .conductor/state.json.lock*, .conductor/state.json.tmp* and .conductor/activity/. It never removes a line. An ignore line does nothing for a file git already tracks (0.51.0). 14 of 24 pm-managed repos on one machine had committed .conductor/brief.txt, so every snapshot left a changed tracked file. When any of those paths is in the index, upgrade (and init) print an ⚠ UNTRACK PM'S SESSION FILES block naming exactly the tracked entries:
Run it from the project root. --cached removes the files from git only; they stay on disk. pm never runs it itself. The block repeats on every upgrade until the files are untracked, and is silent wherever git cannot answer (no repository, or no index). There is no state.json change and no migration.

What 0.52.0 changes — rules-block text and a capped brief, no migration

0.52.0 ships no state.json schema change and no migration: the newest MIGRATIONS entry is still 0.44.0, and every field the release adds (state.spendTracking, epic.gateRounds, the spend-guard limits, gate1Waived, a story’s corrections[]) is optional. It does change what the rules block says and how large the SessionStart brief is, so /pm:upgrade is still the step that delivers it: the managed rules block now names the six role defaults (see /pm:profile), plus the spend and spend-guard sections only while those are on, and the brief it sends is capped (see /pm:status). Run /reload-plugins first, then /pm:upgrade in each repo. upgrade lists the opt-ins a repo has never adopted, on every run (#210). The changelog delta only names what a release added, so an opt-in skipped at an earlier release was invisible forever. After the delta, upgrade prints one opt-ins line of what is adopted and one NOT SET line per opt-in that is not, each naming the command that adopts it: set-review-mode --mode …, set-lane-routing --add …, set-activity-log on, set-tracker --system …, set-spend-tracking on and set-spend-guard …. It reads a registry in scripts/lib/opt-ins.mjs and state.json, and enables nothing. There is no claim row: a claim is an epic’s temporary hold, not a repo setting. Every printed command is run against the engine by a test, so it runs as printed. Spend tracking is a developer feature and stays off until you run set-spend-tracking on; see /pm:spend.

What 0.51.0 changes — rules-block text, no migration

0.51.0 ships no state.json schema change and no migration: the newest MIGRATIONS entry is still 0.44.0, and every field the release adds is optional. It does change what the rules block says, so /pm:upgrade is still the step that delivers it: a paragraph under “Re-read the source before an epic becomes the work” directing the agent to confirm an OpenSpec epic’s planning is complete (every artifact the project’s OpenSpec schema requires exists, and they agree) before treating it as ready to apply, the execution profile section (/pm:profile), and the release candidate section. Run /reload-plugins first, then /pm:upgrade in each repo.

What 0.45.0 changes — rules-block text, no migration

0.45.0 ships no state.json schema change and no migration, but it does rewrite what the rules block says, so /pm:upgrade is still the step that delivers it. Every tracker-configured repo’s tracker sections change: the listing step (now gh issue list … --limit 1000 --json …,updatedAt,…, with a truncation stop), the quoting sentence (fill item values as one single-quoted word; --title=, --external-url=, suggest-lane --ask= stay attached), the secondary watermark step (previously absent), the non-numeric issue-key placeholder, and the completion-sync reminder’s wording. Both inward sections also stop pointing at a /pm:epic list that does not exist. A repo whose recorded github-issues repo is not owner/name or HOST/owner/name stops receiving a literal gh line after this upgrade — it gets the vendor-neutral “list open items with your own tooling” step instead — until it re-runs set-tracker, and integrity’s tracker-repo-not-a-github-repository names it. init’s stderr, the commit nudge and /pm:init also stop telling the agent to edit state.json and name the verb instead.

What 0.44.0’s migration does

One entry, additive and idempotent: the link arming stamp. It gives every may-invalidate link without a key an explicit reconcileOnResume — true where its epic owes a reconcile, the link has no verdict, and it targets another epic that exists; false otherwise. A keyed link is never touched, and upgrade runs the same stamp on every run, so a keyless link written later by an unreloaded older session is stamped too. A 0.43.0 state file loads; until /pm:upgrade runs, every record-reconcile on an epic holding a keyless link is refused naming /pm:upgrade. The stamp cannot tell an old --no-reconcile link from an old --reconcile one, so an owing epic can come out owing a verdict against both — see /pm:resume.

What 0.27.0’s migration does

One entry, additive, idempotent, and backward-compatible — a state.json written by 0.26.0 still loads, and a second run changes nothing. It reads nothing outside state, because a migration that consulted disk would produce a different result on a machine whose checkout sits at a different commit. both would be wrong for an existing non-github-issues primary in the direction that costs something: a Jira tracker receives only the outward section today, so both would grant an inward pull no repo has ever had, and /pm:sync would start registering an untriaged epic per open issue. Every archived epic also gets a terminal outcome, regardless of lane — delivered only where a passing Gate 2 exists, unknown everywhere else, both carrying recordedBy: "migration" so a later rule can key on which path wrote the stamp. Lane-scoping this would be wrong on measured data: of PM’s own 69 archived epics only 3 are openspec-lane, so stamping one lane would leave 66 with no outcome and the outcome invariant would fail on its own repository the instant the migration ran. An existing disposition is never overwritten.
Expect a burst of heal-archived-epic-passed-gate-2 on your first integrity run after this upgrade (node "$ENGINE" integrity — there is no /pm:integrity slash command; see /pm:status). The migration only stamps epics already archived in state; the archive-drift heal flips the rest to archived afterwards and stamps them unknown at that moment. So every repo that followed the documented /opsx:archive then heal flow lands on unknown rather than delivered, by one step. That is the expected shape, not a bug. Each finding names the exact remedy — update-epic <id> --status archived --outcome delivered --no-deferrals — and the archive gate lets an agent replace an engine-written stamp, so nothing is stuck at unknown.
Three things about PM’s emitted output also differ from 0.26.0, deliberately: the “Sync after completing tracker-linked work” reminder now leaves the rules block where no inward procedure is emitted; the briefing’s consider /pm:sync nudge now leaves outward-only repos; and progress renders · N lifecycle where tasks carry the {/* pm:lifecycle */} marker. Treat a fourth as a regression.

What 0.40.0’s migration does — and the verb that finishes it

0.40.0 gives every epic a registration date (createdAt) and a last-touched date (touchedAt). New epics get both for free: createdAt is written by pushEpic(), the single sink every creation surface routes through, and touchedAt advances inside saveState() on any write that genuinely changes the epic’s stored content. Epics that already existed are the problem the migration solves — and it cannot solve it alone.
Absence means UNKNOWN. It never means today. The migration does not stamp upgrade day onto every pre-existing epic. A date that says “registered the day you upgraded” is worse than no date, because a staleness question would then read every legacy epic as brand new.
recover-created-at is the re-runnable half. It sweeps every dated-less epic and takes its createdAt from the commit that first introduced that id into .conductor/state.json, reading local git history only. As of 0.51.0 the date is the commit’s AUTHOR date, not the committer date: a rewrite (rebase, amend, cherry-pick) moves the committer date later than the authoring, and one epic was stamped 07-09 when its only introducing commit was authored 06-30. It keeps full history and takes the earliest match by date. Dates already stamped are never overwritten; clear one with update-epic <id> --clear created-at and re-run the verb.
The 0.40.0 upgrade invokes it once for you. You can run it again at any time, and that re-runnability is the design, not a convenience:
  • It is not implemented inside MIGRATIONS. A one-shot transformation that reads disk produces a different result per checkout — which the migration framework forbids by name. Two checkouts of one remote on the development machine differed by two commits touching state.json, so a one-shot recovery would have frozen a wrong answer in one of them permanently.
  • Where this checkout holds no evidence, the date is LEFT ABSENT rather than invented — no git, an untracked state file, a shallow graft, or an id older than the history you have fetched. Re-run after fetching more history and it recovers what it could not see before.
  • It never overwrites a date already present, and it repairs nothing else. An epic sitting in an undefined status is dated like any other and left in that status, because which status it should be is a judgment about what happened to the work.
A shallow clone would have fabricated a date for every epic and recorded it as fact. At a graft point, git diffs the boundary commit against nothing and reports every id as introduced there — so a shallow checkout would have stamped all of them with the graft date and looked entirely successful. The verb reads git’s own shallow file and refuses to date across a boundary.
Recovering a registration date is not a touch. The sweep writes createdAt without advancing touchedAt, because the last-touched stamp answers when the epic’s own content last changed, and a backfill of its registration date is not that.
Measured across 27 distinct upstreams before this shipped: 18 of 20 open epics in PM’s own repository carried no date of any kind. The schema already had startedAt, completedAt, externalUpdatedAt and disposition.recordedAt — and nothing recording when an epic was registered, which is exactly the population a staleness question is about, since an epic registered and never started has no other date.The recovery costs roughly 25ms per epic, so a few seconds on a large repository. /pm:upgrade is user-invoked and not hooked, so that cost lands where you can see it.

Running pm in a deployed checkout — a detached HEAD

New in 0.42.0. If your repository deploys by checking itself out — git checkout --detach --force <tag> into a serving directory, with development in a separate clone or worktree — then the deployed copy contains .conductor/state.json too, because that file is git-tracked by design: it is the backup, and git restore .conductor/state.json is the documented undo. PM’s dormancy guard asks whether that file exists. So until 0.42.0 one file was answering two different questions — is this repository pm-managed and is this tree a place to work — and in a self-deploying repository those diverge. Two things followed, and both were silent:
  • Session bookkeeping landed in the deployed tree. Measured in a real production checkout: a commit watermark, a brief snapshot and an activity log — all git-ignored, so git status showed nothing and the owner noticed independently.
  • A /pm:upgrade run there reported success and then vanished at the next release, because the deploy’s git checkout --force discards it.

What 0.42.0 does

Session bookkeeping is not written when HEAD is detached. The commit watermark, the detour log, the brief snapshot, the session claim and the activity log all record work in progress, and a tree nobody is working in has none. Suppression is silent — a file that does not appear asserts nothing, where a line on every hook invocation would be noise. A verb that writes says the tree is detached. It still writes; PM reports rather than deciding. The message names the tag when HEAD is exactly at one, because detached at v2.11.0 identifies a deployment where detached alone does not:
Read-only verbs stay silent — reading a deployed checkout’s record is a legitimate thing to want, and crying wolf on them is the defect 0.40.0 removed from the sibling warning this one sits beside.
Detachment is the whole signal — the tag is context, not a condition. Requiring a tag match would be more precise about “deployment” and would miss every deploy that checks out a sha. The cases detachment over-catches — a bisect, reviewing an old release, and every CI run, since actions/checkout leaves HEAD detached at the sha — are ones where suppressing session bookkeeping is right anyway: those trees are ephemeral or read-only by intent.
What this does not do. It does not change the dormancy guard, and it does not stop a mutating verb writing. /pm:upgrade in a deployed checkout still stamps state.json, rewrites the rules block and re-renders PROJECT.md — all tracked files — and the next deploy still discards them. The warning is the mitigation, not a refusal. If you see it and did not mean to be there, run the command again in the checkout that is on a branch.

Is OpenSpec current, and is this project current with it?

pm and superpowers are plugins — they update themselves, and PM already nudges when a repo’s stamped version lags the installed one. 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 anywhere saying so. Measured in PM’s own repository: four minor versions stale, while actively running an OpenSpec change, with two upstream slash commands its agents could not know existed. As of 0.28.0 both /pm:upgrade and the SessionStart briefing report that drift, from one shared emitter so the two surfaces can never disagree.
1

PM's engine never runs openspec update — but you or your agent can

PM’s engine never runs it for you: pm instructs, it does not integrate. That is the instruction-layer law, and it is enforced mechanically — a source scan in the test suite fails the build if any engine file ever passes the openspec binary an argv other than --version.The negation names PM’s engine as the non-actor, not a human as the required one. The safety requirement was always review the diff before committing, never a human must type it.
Until 0.40.0 this read “you run the terminal command — exactly as with openspec init”. openspec init genuinely is reserved to the human in some estates’ global agent instructions, so the analogy transferred that reservation onto update, which has no such reservation. Measured cost: an agent session read the nudge, refused to run the command, and asked its user to run it instead — one full round trip — and the same misread had already left four repositories on one machine stale at 1.9.0. The test that pins the new wording asserts the old wording is absent as well, so a revert cannot pass unnoticed.
2

It holds mid-change instead of suppressing itself

openspec update rewrites the very 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 that change is archived, naming it. Suppressing the nudge outright would silence it permanently in any repo that usually has a change open — which is precisely how the four-version drift above accumulated unseen.
3

It tells you whether a diff will exist

Where the generated files are git-tracked, git diff after the run is your review. Where they are not tracked — or where that cannot be confirmed — the nudge tells you to copy them aside first, because the rewrite destroys local edits to your generated OpenSpec skills with no diff and no trace. Tracked-ness is scoped to this project even when it sits inside a larger repository.
Either version being undeterminable is reported as cannot tell, never as stale — the same third answer PM’s ancestry checks give. And nothing is spawned at all unless the repo has an openspec/ directory and a readable generated stamp, so a project that does not use OpenSpec never pays for the check.

Idempotency

Running /pm:upgrade more than once is always safe. If your project is already at the latest version, every step is a no-op — nothing is written, nothing is changed, and the command exits cleanly.
Always run /reload-plugins before /pm:upgrade when you’ve just installed a new version. If you skip the reload, Claude Code is still running the old plugin code, and the upgrade command will apply the old engine’s logic rather than the new one. Reload first, then upgrade.

Recommending adoption of new capabilities

After printing the changelog delta, PM doesn’t just leave you to read it — the agent reviews each Added headline in the “What’s new in pm” output and judges whether it describes an opt-in capability (a new flag, a new set-* subcommand, a new tracker/autonomy/review-mode behavior) rather than a bug fix or a change that applies automatically with no action needed. For anything opt-in that looks relevant to your project’s current .conductor/state.json — say, you already have a primary tracker configured but haven’t added a secondary one, or you’re touching schema-sensitive files without thorough review mode turned on — the agent recommends it in one line: what it is, why it fits, and the exact command to enable it. It never enables anything on its own; adoption is always your call. If nothing in the delta is opt-in or relevant to your project, the agent says so rather than silently skipping the review.

/pm:changelog — Review what changed between versions

/pm:changelog surfaces the PM plugin’s own changelog entries that are newer than a given version, so you know exactly what a release brought rather than just that an upgrade happened. By default, the floor is the version stamped in your project’s .conductor/state.json (pmVersion) — so running it after an upgrade shows you everything that version introduced. /pm:upgrade automatically prints this changelog delta for the versions it crosses, so you see what changed as part of every upgrade. Use /pm:changelog directly to review the delta again or to inspect a different version range on demand.