/pm:sync scans your project for work that should be tracked as epics but isn’t yet. It picks up new OpenSpec proposals and Superpowers plans found on disk, reconciles openspec/changes/archive/, and — where a tracker’s direction includes inward and it names a scope to read — pulls open items in as untriaged epics. After sync completes, PM prompts you to triage any newly registered epics: assign priorities, set statuses, and add any dependency links.
The archive backfill
New in 0.27.0.
sync also walks openspec/changes/archive/. An archived change the conductor holds no epic for is registered as an epic already in archived status, so the record covers what a repository actually shipped rather than only what it happened to register while the work was in flight. The audit behind 0.27.0 measured the gap: the conductor saw 49 of 87 archived changes across 8 repositories.The backfill is visible, one-time, and announced — never a silent side effect. A state-level archiveBackfilledAt marker records that it happened. A backfilled epic carries recordedBy: "archive-backfill" on its disposition and no gate verdict at all: it never passed through the conductor while in flight, so writing one would assert a permanent, unclearable condition against every change archived before the conductor existed.What it registers
OpenSpec proposals
Any
openspec/changes/<id>/ directory not yet tracked becomes an openspec-lane epic registered with status: untriaged and priority: P?.Superpowers plans
Any Superpowers plan file not yet tracked becomes a
superpowers-lane epic registered with status: untriaged and priority: P?.Tracker items (inward)
Where a tracker’s
direction includes inward and it names a scope, open items without a matching externalUrl become epics whose lane comes from lane routing. Default priority is P2; a P0, P1, P2, or P3 label overrides it.Deduplication
Changed in 0.31.0 for plan files. Deduplication used to key on an id derived from the plan’s filename, so an epic whose plan was renamed — or whose id was simply chosen differently — was re-registered forever, and a finished plan came back as a fresh untriaged epic. Sync now keys on a real epic↔plan association (
planPath), and walks a five-rung ladder per plan file:- Claimed — some epic’s
planPathnames this file. Status- and lane-blind on purpose: that is the done-signal, and it needs no completion heuristic. - Literal id match — an epic already carries the plan’s derived id.
- Tombstone — the removal verb recorded that this artifact must not be re-registered, so a removal survives the next sync. Attaching the plan to an epic (
update-epic <id> --plan <path>) clears it. - Name match, unclaimed only — the epic id equals the plan id minus its date prefix and that epic claims no plan already. Sync skips and prints both exits rather than silently repointing an epic’s progress source.
- Otherwise, register.
update-epic <id> --plan <path>, after which rung 1 does the rest forever.state.json, the candidate is skipped silently. For tracker items, deduplication is by externalUrl, which is globally unique; a bare externalId is only unique within one tracker/repo, so two trackers can each hold an issue numbered #42 without colliding. The emitted registration recipe also supplies a derived epic id (<system>-<scope>-<number>), so the same item yields the same id in every repo and session and a re-run is refused as a duplicate rather than landing under an invented slug. Re-running /pm:sync as many times as you like is always safe.
When to run it
Run/pm:sync after pulling changes from another branch that may have introduced new OpenSpec proposals, after generating new proposals in the current session, at the start of a session when you know new work was filed in your issue tracker since you last worked, or any time you suspect the epic index is out of date relative to disk.
Freshness: register what is new, re-read what moved
Registration is only half of an inward sync. For every epic already linked to a tracker item, compare itsexternalUpdatedAt watermark against the item’s tracker-side updated timestamp and read the ones that moved. Seeing an item in a list response is not reading it — listing must never advance a watermark, or sync erases the very drift it exists to find.
The warning counts only work that can still happen (0.31.0). An epic that has ended owes no refresh — its item is closed, and /pm:sync could never clear it, so counting it named an action that could not reduce the number it reported. On PM’s own record that was 60 counted where 31 were real. The archive disposition discharges the obligation; there is no separate terminal watermark, because a record that ended does not need a second ending.
The other direction: an item that is no longer open
New in 0.30.0. Sync reads tracker state anyway, so it now also looks the other way: for an epic whose linked item the tracker reports closed while the epic itself is still non-terminal, sync proposes the disposition. Proposing, never writing. The engine cannot know why work ended, and a reason is precisely what a disposition exists to preserve — so you get the exact command with the reason left to you:queued — after which the next-work recommendation confidently names work that shipped hours ago. An epic already archived is excluded by name, so this never adds noise to a backlog that is already correct.
The engine itself only scans local files and never calls an external system directly. What you do externally is decided by each tracker’s
direction, and the rules block in your project-instruction file already carries the exact steps for whichever branch applies. An outward-only tracker (or one that names no scope) means sync reads nothing external — it registers local OpenSpec/Superpowers sources and stops, and its confirmation line says so.
