pm plugin, mirroring CHANGELOG.md in the plugin’s repo. Format follows Keep a Changelog; this project adheres to Semantic Versioning.
2026-09-12
A deployed checkout is not a workspace
.conductor/state.json is git-tracked by design — it is the backup, and git restore is the documented undo — so a repository that deploys by checking itself out carries that file in the deployed copy, and PM’s dormancy guard read that tree as a place to work. One file was answering two different questions, and in a self-deploying repository those diverge.Measured in a real production checkout: a commit watermark, a brief snapshot and an activity log had accumulated there, all git-ignored so git status stayed clean. And a fleet upgrade then stamped a new pmVersion and rewrote the managed rules block into it — four tracked files, in a tree whose next git checkout --force discards them.Added
- A detached HEAD suppresses session bookkeeping. 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. Silent — a file that does not appear asserts nothing, where a line on every hook invocation would be noise. The set is defined by a criterion, not a list (per-checkout, engine-owned, a record of work in progress rather than of the project), and all fifteen
.conductor/write sites are settled against it: five suppressed, four deliberately still writing with their reasons recorded, the rest removals needing no guard. - A verb that writes says the tree is detached, naming the files and the tag when HEAD is exactly at one —
detached at v2.11.0identifies a deployment wheredetachedalone does not. It warns and still writes: PM reports, it does not decide. Read-only verbs stay silent, on the sameverb-effectsgate 0.40.0 used to stop the sibling warning crying wolf on the 17 of them.
Fixed
--artifactevidence was invisible togateHasEvidence, so a correctly-run Gate 1 reported asgate-recorded-as-bookkeeping. 0.41.0 added the evidence form and updated the writer and the renderer; two other readers of the same field kept the range-only definition. The predicate is not widened — two of its callers dereferenceentry.headShaon the next line — the two call sites asking it the wrong question are. The arm’s premise inverts for Gate 1: for Gate 2 the loop is commit then review, while a Gate 1 recorded with artifacts necessarily post-dates the commit that fixes what the review found.
Notes
- The probe discriminates the exit status rather than testing for non-zero.
git symbolic-ref --quiet HEADexits 1 when detached, 128 when the directory is not a repository, and throws when git is absent. A blanketcatch → detachedwould make a non-repository suppress writes — the opposite of the safe direction, since a false record is visible and removable while a false suppression silently disables the trail. - Every CI run is a detached tree, because
actions/checkoutleaves HEAD detached at the sha. Accepted rather than carved out: those trees are ephemeral, so suppressing session bookkeeping there is right rather than merely tolerable. It is also why no opt-out ships — the population that would need one wants neither the writes nor the flag. - Gate 2 caught a defect that could not fail locally. The suppression guard probed the frozen root while one of its write paths re-derives its root per call, so in-process the guard asked about one tree and the write landed in another. The CLI fixes one root at startup and the two can never diverge there; CI checks out detached, so PM’s own suite would have gone red on the PR.
- Three verbs reported writes that suppression removed, fixed in the same gate:
claim --repoannounced a marker it never wrote — and with--steal, a takeover that never happened, printing the other session’s expiry so the message read as plausible;log-detourdiscarded the boolean whose own docstring says it exists to stop precisely that;snapshotsaid “snapshot written”. - What this deliberately does not do: it does not change the dormancy guard, and it does not stop a mutating verb writing.
/pm:upgradein a deployed checkout still stamps tracked files the next deploy discards. The warning is the mitigation, not a refusal. - No
state.jsonschema change and no migration.
2026-09-09
Backlog health, and the papercuts 0.40.0’s own closeout surfaced
Twelve items in one release rather than twelve point releases. Shipping 13 versions in 11 days meant a 28-repo fleet pass roughly every 20 hours, and the upgrade is the expensive half.Every item here was found by using 0.40.0 — four of them by the inverse-operation rule 0.40.0 itself added, turned on surfaces that predate it.Added
release show [<id>]— a release object could be written and never read back. Membership is one-way, living on the epic asepic.release, which is the right storage and exactly why a reader who opened the release object sawdeferred[]populated and members absent: it read as “exclusions and no members”, the inverse of the truth. The read-back renders intent, target, derived members, deferrals and the cross-spec verdict.--unmemberand--undeferonrelease, each requiring a reason, and those reasons land in a newamendments[]the read-back renders.--member’s long-standing implicit undefer now records the same way instead of silently.record-gate-review --artifact <path>(repeatable) — Gate 1 reviews spec artifacts by path, before code exists, and was required to supply a commit range for a passing verdict. The only way to satisfy it was a range of artifact commits, which every downstream consumer then reads as an implementation range. A gate-2 pass still requires the sha pair. Making the shas merely optional was rejected: a correct Gate 1 would then render⚠ no checkable evidence, moving the wrong record one step along rather than repairing it.--clear created-at—recover-created-atnever overwrites a date already present, which is correct, and nothing removed one, so a date recovered wrongly was permanent and correctable only by the hand-edit this tool forbids everywhere else. Clear-then-recover is now the correction path.touchedAtstays non-clearable and its registry row says why: the engine re-stamps it on the next real write.--flag=valueon every flag, splitting on the first=so a value may contain one.
Changed
- A flag value may begin with
--. The inward tracker-sync line PM’s own instructions specify verbatim failed on any issue whose title starts with a flag name — and a bug report about a flag usually is titled that way. It failed in the worst available shape: the message said “unknown flag(s)” and listed the title, pointing a reader at the flag spelling rather than the value; the sync run continued; and the item was then silently absent from the backlog with no trace in the record. - Four raw-argv scanners stop disagreeing. Fixing the shared parser fixed 24 modules at once — and
requireKnownFlags, serving five verbs, was still emitting the original bug verbatim afterwards. No existing test could see it, because every one of them puts the unknown flag last. /pm:triageaccepts an ask beginning with--. That call is step 1 of intake — “the ask, in its own words” — and rewording to get past the guard defeats the lexical matching triage exists to do, because the flag names are the distinctive tokens. Narrowed rather than dropped:triage --limit 5supplies no ask, and reading--limitas one returns a confident, meaningless result.--defertakes<epicId:why>inline,--deferralaccepts::as well as:, and--declined-deferral’s placeholder finally says why its separator is doubled. Nothing that worked before was removed.
Fixed
hooks.jsonvalidates clean. It shipped a"comment"key besidematcherandhooksin all five matcher groups, so Claude Code reported unknown keys … ignored at the top of every session, in every repository with the plugin installed. The hooks kept working, which is why it survived: nothing broke, so nothing prompted. The comments were real documentation and moved to a README rather than being deleted — “fires on EVERY Bash call by design” is what stops someone narrowing a matcher and silently killing a hook. Guarded by a closed allowlist, not a blocklist of"comment".- A help token in a value position no longer exits 0 having written nothing.
--helpanywhere in argv short-circuited pre-dispatch, so a command carrying it as a value printed help and reported success. Narrowed to the position a person types it.--title --helpis still refused, correctly; what changed is that it now exits 1 and names the token. - The
gh-137replay test reads only the release it replays. It called itself a replay of a historical moment but computed over the whole live record, so it failed during every release closeout with a bare21 !== 20about an unrelated issue. Fixed by slicing the fixture rather than filtering the findings, and the count is replaced by the identity set. AGENT_OUTCOMESgets an anchor. It derives fromKNOWN_OUTCOMES, which is right, but both existing drift guards anchor their assertions to it too — so emitter and assertion moved together and narrowing it fired nothing. Mutation-tested: of the 124 tests in the two existing guards, exactly one caught a narrowing, and only by accident of the record happening to hold adeclinedepic.
Notes
- PM’s own archive is now fully dispositioned: 66
outcome: unknownrecords → 0. 0.40.0 shippedunconsidered-outcomesto enumerate them; shipping the walker was not walking it. Every verdict cites a commit, a CHANGELOG version, an archived change directory or a source line: 58delivered, 7superseded, 1abandoned. Integrity findings fell from 11 to 6. - Four tests broke because the product worked. A fixture reconstructed the pre-migration record by peeling migration stamps off the live record, so replacing those stamps with real dispositions made the reconstruction permanently impossible. A green suite had quietly come to mean “the archive is still full of undispositioned epics”.
- No
state.jsonschema change and no migration.amendments[]is additive and absent-tolerant.
2026-09-08
The record answers questions about itself
Five defects, one root:.conductor/state.json could not answer questions about its own contents. Measured across 27 distinct upstreams before this shipped — 310 archived epics carrying outcome: unknown, 26 more sitting in status: "done" (a value KNOWN_STATUSES does not contain, which 0.27.0’s migration never reached), and 18 of 20 open epics in PM’s own repository carrying no date of any kind.Added
- Epics record when they were registered and when they were last touched.
createdAtbinds topushEpic()— the single sink every creation routes through — so it inherits the source scan that already forbids bypassing it, rather than an enumeration of creation surfaces that this repository already tried and already watched go stale.touchedAtis stamped insidesaveState()after its no-op early return, against the disk pre-image already read there, excluding both timekeeping fields from the per-record comparison and matching records by epic id, not array position —remove-epicfilters the array, so index-matching would have falsely stamped every record after a removal. recover-created-at, a re-runnable verb the 0.40.0 migration invokes once. Not implemented insideMIGRATIONS: a one-shot transformation that reads disk produces a different result per checkout, which the migration framework forbids by name — and two checkouts of one remote on the development machine differ by two commits touchingstate.json, so a one-shot recovery would have frozen a wrong answer in one of them permanently. 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. Guarded, and absence stays re-attemptable — unshallow the clone, re-run, get the real dates.epic-in-undefined-status, an integrity check reporting any epic whosestatusis outsideKNOWN_STATUSES. The finding names the consequence a reader would otherwise miss: such an epic is non-terminal to every rule testing for the archived status, so it is invisible to precisely the checks that would surface it, and it permanently absorbs the effective priority of everything depending on it. Reports; never repairs.unconsidered-outcomes, a read-only verb enumerating archived epics whose outcome nobody considered — engine-stamped andoutcome: unknown. Both halves matter: a stamp alone sweeps in epics whose outcome a migration correctly derived from a passing Gate 2, and anunknownvalue alone sweeps in epics carrying no disposition at all.unreconstructable, an agent-supplied outcome recording that somebody looked for the evidence of what happened and it does not exist — distinct fromunknown, which says nobody looked.--clear <field>onupdate-epic, deriving its accepted set from a newnullable: truemarker on the flag registry, withsetOnly: "<reason>"on the rows deliberately not clearable. Both directions are tested, so a nullable row added later with no clearing path fails the suite instead of shipping silently. Fields whose clearing costs more than the field carry aclearNote— clearingexternalUrllets the linked item be mirrored again as a new epic; clearingplanlets the nextsyncregister that file as a fresh untriaged epic.- The emitted call-site sweep now obliges the inverse operation — set against unset, add against remove, append against replace, enable against disable, grant against revoke — as a numbered required task item, a declared
mustSayclaim, and in all three mirrored surfaces. /pm:upgradenow tells you to commit what it just rewrote. A sweep of one machine found nine repositories where/pm:upgradeoropenspec updatehad run, succeeded, and been left uncommitted: two sat six days with git recording pm 0.16.0 while disk ran 0.39.0. The failure is silent by construction — every session reads the rewritten files off disk, so nothing is broken and nothing looks wrong. The new line names the changed paths, and is conditional in both directions off a singlegit diff --name-only HEADprobe, so an idempotent re-run stays silent and a git-ignored path is never named.
Changed
--linkappends instead of replacing. A link’s identity is its type and target; the reason is not part of it, so re-supplying an identity updates the reason in place rather than duplicating it or silently discarding the correction.--clear-linksand--linkare no longer mutually exclusive, so the documented repair stays one atomic write. All six sites documenting the old behaviour changed with it, including two the engine emits at runtime.- A write that changes nothing says so, across the whole write surface rather than one verb: 27
saveStatecall sites report through a shared reporter, four carry a declared exemption, and a per-call-site source scan fails the build on a fifth that does neither.
Fixed
declinedhad been added to the outcome enum in an earlier release, reached the engine, and reached none of five documented surfaces — including a test asserting the four-value list that passed by prefix match, so the guard for this drift was blind to it. All five repaired, and the emitted enumerations now render fromAGENT_OUTCOMES.- Two live tests asserted that
--linkreplaces; both would have failed CI on the change above. - A read-only verb no longer warns that it is WRITING a different repository. Running
integritywithCLAUDE_PROJECT_DIRpointed elsewhere printed⚠ WRITING A DIFFERENT REPOSITORYtwo lines aboveintegrity’s own “nothing here writes state”. The warning is worth shouting — which is exactly why crying wolf on the 17 read-only verbs is the defect: it trains a reader to skim past it on the 33 where it is the difference between inspecting another repo and mutating it. Gated onverb-effects.mjs, which already declared every verb’s effect — no new list of verb names to go stale. verb-effects.mjsunder-declared whatupgradewrites — it omittedPROJECT.mdand.conductor/render-stamp.json, and namedCLAUDE.mdwhere the target is whatever the platform’s precedence chain resolves to.- The OpenSpec drift nudge no longer reads as “a human must run this”. It equated
openspec updatewithopenspec init— “a terminal command, exactly likeopenspec init” — andopenspec initgenuinely is reserved to the human in at least one estate’s global agent instructions, so the analogy transferred that reservation ontoupdate, which has no such reservation. Measured cost: an agent read the nudge, refused to run the command, and asked its user instead — one full round trip — and the same misread had already left four repositories stale at 1.9.0. The negation now names pm’s engine as the non-actor rather than a human as the required one, and the test asserts the old wording is absent.
2026-09-05
Things that go missing without anything noticing. Four items under one class: a shipped file that lost its tail, an agent transcript thrown away, a test helper whose sibling silently went stale, and a reported stdout loss that turned out not to exist.
Added
- A structural check over every shipped markdown file. A skill lost 78 lines in 0.31.0 and no check noticed for five releases, because every existing guard asserts things are PRESENT — and a truncation is an absence below the last thing anyone asserts. Five assertions: an unclosed code fence, unterminated frontmatter, a heading whose section is empty, a table header with no rows, an unbalanced
<details>. Each was measured at zero violations across every shipped file before adoption; anything scoring above zero was rejected rather than tuned. The fence check is a tracker, not line parity, and the difference is load-bearing: a four-backtick fence holding a three-backtick example has even parity while well-formed, and truncating inside it keeps parity even — so parity would call the broken file clean. Proven against real history, which is the strongest evidence available: the test readsskills/conductor/SKILL.mdout of git at three release commits and requires the intact one to pass and the two truncated ones to reportunclosed-fence. It would have fired in 0.31.0’s own CI run. And here is its reach, measured rather than implied: excising blocks at every offset in real files, the detection rate on the mid-document shape the original truncation actually took is 9.3% onSKILL.md, 3.4% oncommands/epic.md, 31.0% onREADME.md. A real guard against a real defect — not a general truncation detector. - A guard on write-before-exit output size. A process that writes a large payload to a pipe and then calls
process.exit()truncates at the buffer, because exit skips the flush — measured at 65,536 bytes. Unreachable today (the largest such path is 1,286 bytes), which was a property of current output sizes that nothing held.
Fixed
- The EDD harness threw away the agent’s own output. The runner captured stdout and the adapter copied four metrics out of the result and dropped the rest, so the only way to see what an agent actually said was to run it again — 90 seconds, and a different sample against a non-deterministic surface.
stderrwas captured nowhere. Both are carried now, whole and never excerpted: a cap is the silent-truncation shape this release’s first item is about. A timed-out run keeps its partial transcript too —TimeoutExpiredcarries it, and it was being discarded. - A duplicated test helper that two files had to keep identical, with nothing enforcing it. Hoisted to one definition — a byte-identity test between copies was rejected, because it detects divergence where one definition prevents it.
- 0.35.0 shipped with a heading and no changelog entry at all, and nothing noticed for four releases. Found by the new check — which had been excluding
CHANGELOG.md, and that exclusion is what hid it. Backfilled from the release’s own PR and commit, marked as recovered rather than passed off as original.
Notes
- No migration. Nothing in this release changes the shape of
state.json, and the emitted rules block is byte-identical across all three platforms — managed repos receive no change. - One reported bug was retracted rather than fixed. A claimed stdout loss under
PM_ENGINE_DELEGATIONcould not be reproduced in 34 full-suite runs, and its stated mechanism was impossible: delegation cannot fire from the test harness at all, and the handoff hands the child the parent’s own descriptor. Both facts are now tests, so the mechanism cannot be re-derived from the issue text later.
2026-09-04
The record says what actually happened. Four defects under one invariant: a write must be readable back, and a reported write must have happened. Three were reported from the field within a day of each other; the fourth was found by hitting it while fixing the others.
Added
set-gate-guardwith no argument READS. It wrote and confirmed the write, and nothing anywhere read it back — so “is the guard on?” was answerable only by openingstate.json, which is what a read verb exists to avoid. The reader lives on the toggle, deliberately not ongate-guard: that verb is thePreToolUsehook, which blocks with stderr + exit 2 and allows by returning silently, so its stdout is protocol surface and a report printed there would corrupt it. The reporter read that silence as a broken command. Followingowners, the output states the value and its limits — above all thatonalone does not mean anything is currently blocked.update-epic --withdraw-commit <sha> --withdrawal-reason "<why>"— the exit from an append-only array. Append-only was right and stays: order carries meaning and the last entry is the endpoint a recorded Gate 2headShais compared against. But “cannot be reordered” is a different claim from “can never be corrected”. Agit resetis a normal operation and the gate procedure requires attributing at the moment of each commit, so an attribution can outlive its commit through no error of process — after which every escape was worse than the problem: hand-editstate.json(forbidden), tag the orphan (makes a false record permanent), orremove-epic(destroys the disposition, links and stories). The withdrawal is recorded in a siblingwithdrawnCommitsfield rather than erased.::as the explicit separator for--declined-deferral, and placeholders so--helpteaches the form rather than printing<a value>.
Fixed
--declined-deferralsilently truncated<what>at its first colon. Measured in the wild:"Set alwaysLoad:false to reclaim RAM:declined because X"recordedwhat: "Set alwaysLoad"— which reads as an instruction to do the thing being declined, the opposite of the record’s meaning. Neither split is safe for two free-text halves, so it stops guessing: a single colon behaves as before,::is explicit, and an ambiguous value is refused with the remedy in the message.--deferralis deliberately unchanged — its left half is an epic id, which cannot contain a colon.- A deferral flag outside an archive printed
updatedand wrote nothing. It now refuses and names the correction path, which existed and was merely undiscoverable. - A gate verdict was recordable only on an
openspec-lane epic whileset-review-modeis lane-agnostic and its own table names “a Superpowers task review”. Evidence for every other lane became prose that nothing compares, so a verdict could never read stale, andintegritycould not see it. The archive gate is unchanged and stays openspec-only. - The reconcile-gate refusal told you to bypass it in a way that does not work — that branch is unconditional and
set-gate-guard offdoes not reach it. - A withdrawal could walk an epic past the archive gate — caught at Gate 2, in this release’s own new verb. Withdrawing every sha moved an epic from
staletonone-attributed, and an archive that had been refused succeeded. It now readsattribution-withdrawnand is refused by name. A withdrawal corrects the record; it never discharges the obligation. --declined-deferral ":"recorded{what:"", reason:""}and satisfied the archive gate — an assertion asserting nothing, while the refusal beside it claimed the reason “is not optional”. Both halves must now be non-empty.
Notes
- No migration.
withdrawnCommitsis additive and absent until a withdrawal happens; astate.jsonwritten by 0.37.0 loads unchanged — verified byte-identical throughrender,verify-state,integrityandbriefon a 158-epic file. - The emitted rules block is byte-identical to 0.37.0’s across all three platforms. Managed repos receive no change.
integritygains reach without gaining checks. Its staleness and evidence checks readgateReviewwith no lane filter, so they now cover the lanesrecord-gate-reviewpreviously refused. Expect true findings on non-openspec epics that carry a verdict and attributed no commits.
2026-08-31
Help that answers the question actually asked.
--help was verb-blind and the emitted rules block carried no route to documentation at all, so an agent that needed a verb’s flags read the engine’s source — which is exactly what a session bringing another repo up to date did, and said so. Both halves ship together: a local channel that cannot lie about the installed engine, and a pointer to the docs for everything the engine does not know.Added
<verb> --help— per-verb, projected from the flag registry. Naming a verb prints that verb’s own flag surface: every flag it accepts, which take no value, which repeat, and the phrase each one’s own refusal ends in. Naming none still prints the verb list. Every line is derived from the same registry rows the unknown-flag allowlists read, so a row that grows the allowlist grows the help in the same edit and help cannot advertise a flag the parser refuses. A per-verb help table was refused: that is a rule bound to a list rather than to the function that governs it, and it would be stale the first time a flag was added.- A closed-enum flag names its legal values.
--outcome,--status,--lane,--priority,--platform,--mode,--level,--direction,--gateand--verdictall render their real value space, projected from the same constants the engine validates against. Gate 2 measured the gap this closes: of the 92 value-bearing (verb, flag) pairs, only 21 rendered anything more specific than<a value>, and not one closed enum named its values — so a reader wanting to know what--outcomeaccepts still had to openscripts/lib/, which is the thing this release exists to stop. Flags with no closed set still render<a value>; that remainder is visible rather than papered over. - A verb that takes no flags says so. 21 of the 48 legitimately take none. Printing an empty list would make “takes none” and “nobody declared this verb yet” look identical, which is the ambiguity that declaration exists to remove.
- A “Getting help with pm” section in the emitted rules block. Every managed repo receives it on the next
/pm:upgrade. Measured across all 408 emitted lines and all three platform variants before this release: 0 occurrences of the engine binary, of any URL, ofMCP, of--help, ofpm-plugin.dev, ofSKILL.md, ofREADME— while the block hardcodes eight flags in a singleadd-epicrecipe. It taught flags by worked example and offered no way to enumerate them.
Fixed
add-epic --helpadvertised--clear-links, a flagadd-epicrefuses — caught at Gate 2, and it falsified this release’s own headline invariant. The cause was one field doing double duty:requiresis the tail of a refusal message and, new here, the help signature.--link’s reads “…to empty an epic’s links, say so with--clear-links” — correct in a refusal, a ghost flag in a signature. Rows now carry a separateplaceholderfor the signature.- The guard that found it is now the test. A mechanical sweep asserts that for every one of the 48 verbs, every
--tokenits help prints is a flag that verb accepts. The test it replaces hardcoded four names on one verb — the sibling-miss this repo’s gate procedure names as its dominant defect class, and precisely why a ghost living inside another flag’s placeholder text was invisible to it.
Changed
<verb> --helpand<verb> -hno longer print the global usage line. That is the point of the release rather than a side effect. The short-circuit itself is unchanged and keeps its original property: a help flag still fires before dispatch, reaches no subcommand, and cannot be consumed as data — the bug that put it there waslog-detour --helpwriting a real detour entry described as--help, into an append-only log with no verb to remove it.- Documentation gaps closed —
--descriptiononadd-epicandadd-many,--diff-summary,--platform, and thewrite-rules/rules-targetverbs, which had no command doc at all.
Notes
- No migration. Nothing in this release changes the shape of
state.json. - The skew warning embeds no version number, deliberately. The block a repo reads must be identical before and after an upgrade — a migration records behavior, it does not alter what anyone reads — and the block is static while the plugin is not, so an embedded version is a snapshot that goes stale in exactly the skew case it was meant to warn about. It routes to
/pm:changelog, which computes the answer. - Existing repos will see exactly one addition to the emitted block, the “Getting help with pm” section. Treat a second as a regression.
2026-08-29
PM stops imposing a house style — and a shipped file that had been quietly broken for four releases is whole again.
Added
- Your output style and your project’s communication contract now outrank PM’s reporting shape. PM emitted a prescribed closeout format regardless of what you had configured, which inverted the ordinary precedence: your instructions outrank a plugin’s preference. The split is three bands, not two, and the middle one is the part that matters:
- Delegate discovery, so exploration never fills the orchestrator’s context. Placed as an operating rule rather than a gate item, deliberately: every gate item is a per-change record obligation ticked in a task list, and a per-action habit that fires dozens of times per change would dilute a list whose whole authority is that every member is a real gate. It carries an explicit carve-out — delegating never weakens a full-read requirement, and substituting a keyword grep for a full read is forbidden whoever performs it.
- Lane routing becomes a deliberate decision. The mechanical suggestion is demoted to an input rather than read as a verdict, the judgment half is named as absent, and the tie-break is made directional: the lightest lane leaves no spec, plan, gate or stories behind, so over-processing costs hours while under-processing costs the record permanently — an unresolved routing question therefore resolves away from the lightest lane, never into it.
Partial, and said to be partial. The original complaint — that routing has no product or milestone context to weigh — stays open, because that layer does not exist and the engine cannot invent it without becoming an integration layer. What shipped is a better instruction, not a better algorithm.
Fixed
- A shipped skill had been silently truncated since 0.31.0.
skills/conductor/SKILL.mdlost 78 lines mid-document and went out that way in five consecutive releases. The signature was an odd number of code fences — a markdown file cannot have an unbalanced fence and be intact. That is a mechanical, content-agnostic check nobody was running, and the reason is worth stating: every existing guard checks that the right things are present, and a truncation is an absence below the last thing anyone asserts. The ledger checks the file exists; the drift guards check specific claims appear; none checks the file is whole. Restored, and a structural check for every shipped markdown surface is filed as its own work. - The lifecycle marker’s collision with OpenSpec’s archived lint is documented where the marker itself is documented.
openspec validate --archivedcounts raw checkboxes and fails every correctly-archived change, because the task that archives a change is unticked at archive time by construction. Anyone wiring that command into a pre-commit hook now finds the warning before the confusion.
2026-08-29
The engine is now present at the transition it was absent from, and a guard caught a defect that existed only in the combination of two branches.
Added
push-detourandpop-detour. The substantial-detour PUSH was a documented hand-edit of the state file — the one mechanism this project tells every agent never to use, performed at the most consequential transition it has. None of the engine’s guarantees applied to it: no validation, no conflict guard, no read-back verification, no record that it happened.- POP needed the same treatment, and that was checked rather than assumed — the resume instructions spelled out the identical hand-edit. Fixing PUSH alone would have left the sibling untouched.
- One subtlety the verbs now handle that a hand-edit could not: POP removes the frame before reconciliation runs, so the frame removal, the reconcile obligation and the active-pointer move happen in one state object and one write. Split across two writes, the obligation is cleared by the very heal that should preserve it.
- A defect a diff-scoped review could not have seen: the hand-edit set
role: "detour", andadd-epichas no--role— which is precisely why the hand-edit was documented. Replacing it without that would have registered every detour as a plain epic, silently costing the detour-context fallback and the role column.
- Advisory ownership —
claim,unclaim,owners— records which session is working an epic, and whether a repository’s conductor is quiescent.- Advisory, and exactly one surface refuses: claiming over another session’s live claim exits non-zero having written nothing. So “two sessions both claim” has no silent-corruption reading. Everything else writes to a claimed epic unchanged, because a tool that refused to work over an advisory marker would not be advisory.
- A stated TTL rather than a heartbeat. A heartbeat nothing beats is a timestamp in costume, and it makes staleness wrong in both directions — a live session reads stale after quiet work, a crashed one reads live long after it died.
- The override is
--steal, deliberately not--force: the state writer reads--forceglobally off the argument list, so spelling it that way would have silently disabled optimistic concurrency as a side effect of the cooperative feature.
- An optional activity log that ships with its reader. A log nobody reads is a data graveyard, so the reader landed in the same change. It answers time-to-pickup, detour frequency, lane distribution and re-routes, gate verdicts in sequence — and out-of-band writes, meaning state revisions no event accounts for, which is a hand-edit turned from an archaeology exercise into a query.
- Events carry a revision range rather than a number, because an ordinary update writes twice; single-number events would report every normal write as a hand-edit.
- Retention is bounded by design: segment rotation, a per-project cap pruned oldest-first, and a
purge-logsverb that refuses without both a selector and an explicit confirmation.
Fixed
- The flag-value rule is bound to the dispatch table, not to one registry. It previously covered only the epic write surfaces, so it silently no-opped on a dozen other verbs — and two of them had each independently hand-rolled the same check, which is the tell that a rule was bound to a list rather than to the function it governs. A verb-level guard now reads the dispatcher’s own table and fails if any dispatched verb is unclaimed by a declaration.
That guard immediately earned itself. Two branches merged cleanly and the combination failed: four verbs from one branch were undeclared under the other’s rule. It then refused a shortcut that would have declared their flags as value-bearing when they hand-parsed arguments and therefore never refused anything — a declaration that would have been false. The fix converted three verbs to the shared parser, which surfaced a further defect nothing else had: one verb had no unknown-flag allowlist at all, so a misspelled flag printed the entire report and exited 0.
Not shipped, and worth saying why. Adopting OpenSpec’s CLI as a progress source was investigated and declined on evidence.
openspec validate --archived fails this repository’s own correctly-archived change — it counts raw checkboxes and knows nothing of the lifecycle marker that excludes a change’s own archive task, which is unticked at archive time by construction. The two tools compute different quantities from one file and cannot be reconciled by a precedence rule.2026-08-29
Two practices that only ever reached one repository now ship, a link type is no longer whatever you typed, and a flag with no value stops being accepted on some surfaces and refused on others.
Added
- The dogfooding and lessons disciplines ship as skills. Both were invented while building PM and stayed here — the dogfooding rule appeared twice in this repository’s own project file and zero times in the emitted rules block, so it reached exactly one repo, which is precisely the asymmetry it describes. Both now live in
skills/, the local copies are stubs redirecting there, and one new numbered required task item carries the rule into every conductor-managed project: route what the work taught you — a practice goes to the backlog with its evidence, tooling friction goes to/pm:feedback, a process failure goes to a lesson.- PM ships the mechanism and never the corpus. The lesson shape — the frontmatter contract, the trigger matching, the pre-mistake advisory hook — is a capability any project can use. The lessons themselves are this repository’s own history and stay here.
- A closed vocabulary for link types, in three bands.
--linkvalidated that the epic existed and that the value split correctly; it never checked the type, so a typo was stored silently and then read as precedent by the next agent looking at the record. The known set is now derived mechanically from what the code actually reads, not typed from memory, and a test binds it to the source in both directions — an unknown literal appearing anywhere in the engine fails, and a declared type that stops appearing fails too. verify-specs --headersproposes the epics a design document already names. BackfillingspecPathrevealed that the documents declare their own epics in a header — and that deriving the mapping from filenames placed 8 of 10 while structurally missing exactly the many-to-one cases the field exists for. The parse is label-agnostic and bounded to the leading metadata block, chosen empirically against all ten real documents: 26 of 26 ids, zero false candidates. It proposes; you confirm. A header id naming no epic in the record is reported as a finding, from covered documents too.
Fixed
- A flag with no value is refused on every command that accepts it.
add-epic --planwith no value exited 0 and created the epic with no plan attached, whileupdate-epic --planrefused the identical mistake a minute later. Three flags legitimately carry no value; everything else is value-bearing by default, declared once on the shared registry so six write surfaces cannot answer it three different ways.- The sweep found the defect at a fourth surface nobody had named:
record-gate-reviewchecked no flag for a value at all, so a valueless--reviewerrecorded a gate verdict with the reviewer’s identity simply absent.
- The sweep found the defect at a fourth surface nobody had named:
- Serial deferrals are visible. The stack block now reads
paused 11d · 3rd deferral of this epic, derived from the record rather than a stored counter, and silent on a first deferral.The pushback this issue asked for was declined, for a structural reason rather than a judgment call. There is nopush-detourverb — the substantial-detour PUSH is a documented hand-edit of the state file, so the engine is not running at the moment a deferral is decided and a gate has nowhere to live. Visibility shipped; the gate becomes buildable only once the engine is present at that transition.
2026-08-29
Seven issues, and three of them turned out to be about the guard rather than the code.
Added
- The commit hook names the attribution command at the moment the commit lands. The gate procedure asks for
update-epic <id> --attribute-commit <sha>as each commit is made, and nothing checked until the archive gate read the array — by which time the commits exist, often across sessions, and the append-only ordering rule may already be irrecoverably broken. Since the hook now knows from the repository whether a commit actually landed, it can say so at the only moment the advice still changes the outcome.- It names the detour epic while a detour is live, never the paused parent the active pointer still points at — and resolves the id against the record rather than trusting it, so a placeholder can never produce a command that will not run.
- Louder exactly once. While an epic has attributed nothing, the clause carries the catch-up-in-order rule, because that is the only state in which catching up is still possible. After the first attribution it is one line, forever. The escalation extinguishes itself off state the agent wrote — the hook keeps no bookkeeping of its own.
- Silent wherever the engine would be guessing: no active epic, a sha that is not a sha, a commit already attributed, and every commit on the unverifiable rung.
- A declaration of which verbs mutate the working tree, verified mechanically.
renderwritesPROJECT.md;briefdoes not — and nothing said so. All 39 dispatched verbs now carry a declared effect, checked for completeness against the dispatcher itself and exercised: every read-only verb runs against a repository with a render pending, and the tree is hashed by content and mtime.
Fixed
PROJECT.md’s commit loop is closed. A bookkeeping commit appended a detour row, which changedPROJECT.md, which produced another commit. Two independent causes: the trail now refuses a second row for a(sha, kind)it already holds, and the bookkeeping filter that guarded one branch had never been applied to its sibling — a rule at one call site and not the identical one beside it.- Nothing suppresses a render and nothing freezes the timestamp. The fix is to the cause of change, not the reporting of it: a generated file that never changes is as wrong as one that always does.
- A warning when the conductor is pointed at a different repository.
CLAUDE_PROJECT_DIRsilently overrides the working directory, so a stale value means every read and write lands in another repo with no signal at all. It now warns — naming both repositories and how to undo it — on three required conditions: the variable is set, it resolves throughrealpathto a different directory, and the directory you are standing in has a conductor of its own.- That last condition is the discriminator: running from a subdirectory, or targeting a project from a neutral directory, stays silent. Only when two initialised projects are in play was there a choice that could have gone the other way. It warns and never refuses — cross-repo dispatch is a legitimate pattern, and the defect was that a redirect was indistinguishable in the output, not that it happened.
/pm:feedbackdeclares itsghdependency, and gains two channels that need none — a prefilled issue URL and an email address. The report is written locally first on every path, so it survives regardless of which channel is available.- The retry half of “retry once, then skip” is now provable. It had no test that failed when it was removed — and the claim reproduced at HEAD six releases after it was filed: deleting the retry from both call sites left the suite entirely green. One of the two sites remains unobservable end-to-end (its caller re-runs an idempotent heal two lines later, which is the cover that hid the defect); that site is bound by a source-level check rather than a fabricated injection.
2026-08-27
A record you can correct, a hook that observes instead of guessing, and the association one artifact over.
Added
--correct-disposition "<why the recorded one was wrong>". A recorded disposition used to be unreplaceable, so a mistyped outcome — or one recorded without its reason — had no correction verb and forced a hand-edit ofstate.json. Correcting one is now deliberate, self-describing, and non-destructive:- Deliberate — never reachable by re-running the ordinary verb, and refused outright when there is no agent record to correct. An engine stamp is still replaced the ordinary way; an epic with no disposition has nothing to supersede.
- Self-describing — the flag carries its own justification, so a correction cannot be recorded without saying why.
- Non-destructive — the prior record survives verbatim under
superseded, reusing the same key and one-level cap that gate verdicts already use. Every surface renders· corrected (was <prior outcome>). The engine deliberately does not gate on who: an agent’s disposition carries no identity by construction, so “only the agent who recorded it may correct it” would be unenforceable rather than strict. What it requires instead is that a correction leave evidence it happened.
specPath, andverify-specsto answer what it makes answerable. An epic can now record the design document it came from (--spec), andverify-specsreports which documents have no epics drawn from them. It is its own verb rather than anintegritycheck on purpose — an uncovered design document can legitimately be true (a note, a reference, an abandoned sketch), and filing every one as a finding is exactly the noise 0.31.0 removed from the freshness warning. It reports, never repairs, and never speaks unless asked.- Coverage is family-driven rather than
specPath-only, so pointing it at a plans directory answers the same question about plans. Status-blind: an archived epic for a chunk is coverage of that chunk.
- Coverage is family-driven rather than
Fixed
- The commit hook observes the repository instead of parsing the command. It fired on any Bash command whose text contained
git commit— agrep, a heredoc, a doc edit — asserting a commit that never happened; and it logged a spurious minimal detour when no epic was active. It now reads aHEADwatermark and classifies the move through the reflog.- Flag-form-blind by construction.
-am,-F, editor commits with no message flag, escaped quotes, and forms git has not invented yet are all covered, because the subject comes from the commit itself rather than from a pattern over the command. - It also closes a false negative the text check had: a commit made inside a wrapper script, whose command text never says
git commit, is now noticed. A backgrounded commit is caught on the next call and attributed to its own sha. - Every read is three-valued. No git, not a repository, no reflog, missing or corrupt watermark — all fall back to the previous heuristic rather than erroring, which matters for a hook that fires on every Bash call in every initialised project.
- Flag-form-blind by construction.
- Recording an honest disposition no longer reverts a backfilled epic’s task counts. The root cause was provenance living inside the one field whose entire contract is that an agent overwrites it: registration was recorded on the disposition, and the interactive verb replaces that wholesale. Two lifecycles sharing one host, so recording the truth destroyed the evidence. Registration now lives on the epic and no disposition write touches it.
2026-08-27
Four issues, and two of them ended somewhere other than where they started.
Added
- Sync dedups plan files on a real epic↔plan association, not a filename. Deduplication keyed on an id derived from the plan’s filename, so an epic whose plan was renamed — or whose id was chosen differently — was re-registered forever, and a finished plan came back as a fresh untriaged epic. Sync now walks a five-rung ladder per plan: claimed by some epic’s association (status- and lane-blind, which is the done-signal, with no completion heuristic) · literal id match · a tombstone written by the removal verb so a removal survives the next sync · an id-minus-date-prefix name match, but only if that epic claims nothing · register.
- The field family is declared once, so
specPath— the same missing association one artifact over — adds a single row and inherits all five behaviours rather than growing a second shape. A test fails CI if any row is settable at creation but not on the update surfaces. - Verified on this repository: every plan on disk is now accounted for by a named exit, with zero re-registrations. The four live pairs that were one change registered twice under different lanes are exactly what this prevents.
- The field family is declared once, so
- A terminal disposition for a story that will not be done —
update-epic <id> --story <n> --wont-do "<reason>". The third state a checklist needs: not open, not completed, deliberately not being done. The row and its reason always survive, because deletion would destroy the record that the work was ever projected. A disposed story leaves both sides of the ratio —3/3 stories · 2 disposed— exactly as a lifecycle-marked task does, never5/5(claiming completion nobody earned) and never3/5(a gate that refuses forever).- Stories can now be created atomically —
--add-storyrepeats, and works onadd-epicandadd-many, so an epic arrives with its milestones instead of acquiring them one call at a time or never.
- Stories can now be created atomically —
The measurement changed the answer here, and that is worth recording. The issue asked for a gate on unticked stories. Measuring the live record first showed the gate already existed — the archive gate has refused
--outcome delivered on an epic with an unticked story since dispositions shipped. What was broken was the remedy: it told you to mark the task’s line with a lifecycle marker, which an inline story has no task line to carry. So no new refusal was added; the gate got an honest key instead. Mandatory stories were declined outright on one number — 0 of 148 epics use the free-text carriers shipped in 0.27.0, so the field’s meaning is still migrating and mandating decomposition now would manufacture false positives.Fixed
- The never-re-read warning stops counting work that has ended. It reported every tracker-linked epic without a watermark — including archived ones, whose linked items are closed and which
/pm:synctherefore cannot clear. So the warning named an action that could not clear the number it reported, and at that size it read as a chore nobody starts. On this repository: 60 → 31.- The engine was contradicting text it emits: the inward-sync step already says “an epic that is already
archivedowes nothing here — it ended, and a record that ended does not need a second ending.” A terminal watermark was weighed and rejected as a second mechanism for what the disposition already accomplishes.
- The engine was contradicting text it emits: the inward-sync step already says “an epic that is already
- An epic that has ended no longer blocks writes.
state.activecan legitimately name an archived epic for a stretch — the pointer is cleared on write paths, and thePreToolUsegate-guard hook is read-only. Of the three readers of that pointer, the briefing andPROJECT.mdboth filtered an archived epic and the guard did not — so a stale reconcile obligation on an ended epic could wedgeEdit/Write/NotebookEdit, on a branch thatset-gate-guard offdeliberately cannot reach. Filtered at the point of resolution rather than inside each branch, so a future obligation inherits the rule instead of having to remember it.
2026-08-27
Two things the record and the boundary already knew and could not say.
Added
- A release delivers and its member epics stay open. Three signals sat in
state.jsonat once and nothing consumed them: the epic names a release, that release has a delivered member, and the epic is not in that release’sdeferred[]— the object already distinguishes “cut on purpose” from “shipped”, and only the exclusion half was ever read. Measured: a release shipped, all twenty tracker items closed, all twenty epics stayedqueued, and the next-work recommendation named two P0s that had shipped hours earlier.- A new
integritycheck reports it, and/pm:syncnow proposes the disposition for an epic whose linked item is no longer open. Proposing, never writing — the reason is the agent’s, and a reason is what a disposition exists to preserve. - A batch
--releaseform was deliberately declined. An agent-supplied disposition is currently unreplaceable, so writing twenty terminal dispositions from one invocation is an uncorrectable bulk write of the record’s most load-bearing field. The defect was never the price of twenty calls — it was that nobody knew they were owed, which the check and the sync step both address and a batch verb addresses neither. - A release object carries no parent pointer and no delivery marker, so “did this release deliver” is read member-derived: at least one member delivered, none still in flight. That is a stated trade — a release with any active member goes quiet for the whole release, so the check reports late rather than never. Late-and-quiet was chosen over early-and-noisy on purpose.
- Also new: a superseded epic that never ended — epic A declares it supersedes B, and B is still queued or active.
- A new
Fixed
- Fourteen command docs resolved the engine from the project directory. The “if the plugin cache is empty” fallback tried
$CLAUDE_PROJECT_DIR/scripts/conductor.mjsfirst, and its only test was-f— so any project with a file at that path got it executed bynodewhen a user ran a/pm:*command, with no check that the project was pm.- Deleted rather than gated, and the reasoning is the point: any gate would have to be read from the environment, and a plugin cannot set an environment variable — so it would be a switch nobody could operate. Users never benefited from that arm at all; contributors already have
PM_ENGINE_DELEGATION, which is authorized by real path against something a project cannot write. - A fifteenth site instructed the same resolution in prose rather than a code fence, so a pattern search structurally could not see it — deleting only the fenced blocks would have left an agent to hand-roll the arm back.
- A CI guard now walks every shipped instruction surface — commands, skills, agents, hooks, manifest — and fails on either the original shape or its hand-rolled variant. Its claim is scoped honestly to what it actually matches: an indirection through another variable would pass it.
- Deleted rather than gated, and the reasoning is the point: any gate would have to be read from the environment, and a plugin cannot set an environment variable — so it would be a switch nobody could operate. Users never benefited from that arm at all; contributors already have
2026-08-27
Ordering that can see what blocks it, and an intake that can see what already exists. Both are questions PM could not previously ask about its own backlog.
Added
- Effective priority over the dependency closure.
NEXT UPordered by an epic’s own priority, so a P1 blocked by a backlog P2 was unsurfacable: the P1 showed as next while being unstartable, and the thing that would unblock it appeared nowhere. An epic now inherits the best priority of everything that transitively depends on it, computed over the whole record rather than the queued set, and renders asP2 → P1. It is never written tostate.json. NewDEPENDENCY WARNINGSsections name both endpoints, both statuses, and the lift.reorder <id> <id> …sets manual rank for one whole priority band — the last sort key, after dependencies and priority. It refuses a partial band, naming the omissions, because one-at-a-time ranking is both tedious and racy: two agents each setting one rank produce a numbering neither chose.- Membership is unchanged:
plannedandlaterepics are still excluded fromNEXT UP. Not scheduled is not the same as not nameable. - A finding from the call-site sweep, worth stating plainly:
/pm:nextand the briefing had drifted./pm:nextis a command document with no engine code behind it, so the dependency logic that existed in the brief never applied to it. The warnings therefore render inPROJECT.mdas well — the surface the next-up decision is actually made from.
/pm:triage— screen an ask against the whole backlog before it becomes an epic. Dedup was identity-only (same id, sameexternalUrl), which stops/pm:syncmirroring one issue twice and does nothing about the same ask arriving under a different name. Measured in PM’s own repo: four live pairs are one change registered twice, under different lanes. Identity dedup found none of them.- The engine computes a candidate set and never a verdict —
verdictis alwaysnull. Ranking is IDF over the backlog itself, so house vocabulary self-neutralizes with no curated stoplist, and every candidate carries the tokens that earned its score. Judgment ships as a numbered required task item. - New terminal outcome
declined— an outcome rather than a status, sincearchivedalready means terminal.
- The engine computes a candidate set and never a verdict —
- An integrity check for a recorded sha the repository can no longer resolve.
attributedCommitsand gate verdicts’baseSha/headShaare the evidence a review actually covered a range. A squash-merge orphans them, andgcdeletes them roughly two weeks later — measured here at 36 recorded shas, 0 reachable, with every existing check green throughout, because a three-valued ancestry test correctly answers “unknown” rather than “false”.- It distinguishes orphaned (still in the object store, reachable from nothing — recoverable now, gone at the next
gc, and the finding says so with thegit tagremedy) from absent, and gates the absent arm on a probe so a shallow CI clone is never reported as a disaster.
- It distinguishes orphaned (still in the object store, reachable from nothing — recoverable now, gone at the next
Fixed
- A state write now reads back from disk before reporting success. At two scopes deliberately:
saveState()for the general path, andupdate-epicat the end of the command — becauserender()writes again aftersaveState()returns, so a guard inside it structurally cannot see that write. A newer revision on disk is treated as supersession, not failure.
2026-08-27
Three gaps above the change. 0.27.0 made the conductor’s record true about one change at a time. This release asks the questions that only exist above a change: do a release’s specs agree with each other, is the tooling underneath the project current, and is the engine you are running the one you are editing.
Added
- A release-scope cross-spec review gate. pm’s gate vocabulary was per change — Gate 1 reviews one change’s artifacts, Gate 2 its implementation — so nothing ever asked whether a release’s specs agree with each other. On 0.27.0 that question returned 5 Critical and 10 Important against six specs that had each passed
openspec validate --strictand would each have passed Gate 1 alone, including a flagship scenario that was unreachable and a shared flag allowlist four capabilities all needed to grow.- New subcommand
record-cross-spec-review <releaseId> --verdict pass|fail [--reviewer "<who>"]records the verdict on the release object. The engine enumerates the release’s spec set from disk and stores a SHA-256 per file it read — an agent never supplies the list, because a spec list typed by the party being reviewed goes stale in exactly the way this gate exists to catch. - A spec added after the verdict, or a reviewed spec amended, marks it
⚠ stale; an unreadable spec reads⚠ unverifiableand apassis refused rather than recorded against evidence that does not exist. A multi-spec release with no verdict renders⚠ no cross-spec review (N specs)— silence and “reviewed and clean” must never look the same. - The threshold is two or more spec files counted flat across the release’s member changes, so one change carrying six specs qualifies exactly as six changes carrying one each do.
- New slash command
/pm:cross-spec-reviewcarries the procedure, the six questions (contradiction, double ownership, unmeetable requirements, gaps, vocabulary forks, shared chokepoints) and the BLOCKS/POLISH adjudication. The rules block emits it as a numbered required task item — a form measured at 14/14 adoption against 3/15 for the same rule as a prose bullet.
- New subcommand
- An OpenSpec currency check.
pmandsuperpowersare plugins that update themselves, but OpenSpec is a CLI you upgrade by hand — andopenspec 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 nothing anywhere asked about. 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. The session brief and/pm:upgradenow both report the drift from one shared emitter, so the two surfaces cannot disagree.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, because
openspec updaterewrites the instruction files an in-flight change is being authored against. 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 the review; where they are not, it says to copy them aside first, because the rewrite destroys local edits with no diff and no trace.
Fixed
- Developing
pmno longer runs a stale engine. Every entry point the plugin ships invokes the engine throughCLAUDE_PLUGIN_ROOT, which resolves to the installed plugin rather than the checkout being edited — so thePostToolUsecommit hook re-rendered the trackedPROJECT.mdwith output a release behind the working tree, on every commit. ExportPM_ENGINE_DELEGATIONnaming your checkout and the engine hands the whole invocation off to that checkout’s engine before doing any work. This is contributor-only setup, and pm’s users never touch it. Unset — the default — no handoff is considered at all. It is opt-in and the opt-in names one absolute path, because the decision is evaluated in every project on the machine and it decides whether to execute code the project supplies: nothing readable from inside a repository can enable it. See CONTRIBUTING.md if you are contributing to pm itself.
2026-08-25
The conductor tells the truth. A retrospective delivery audit of 49 archived OpenSpec epics across 8 repositories found that the conductor’s records were wrong and its guards did not fire: 42 of 49 epics archived with
gateReview: null — by following the documented /opsx:archive workflow, because Gate 2 was enforced by update-epic and bypassed by the archive hook — and sync never walked openspec/changes/archive/, so the conductor saw 49 of 87 archived changes (56%). Every number this project has published about its own effectiveness was computed from those records. This release makes them true: 20 issues, 6 specs, 59 requirements, 115 tasks.Added
integrity— a read-only audit of the conductor’s own record. Ten checks overstate.json, each one a shape that cannot be true: an archived epic with zero ticked tasks; a gate verdict whose note cites commits its recorded range does not contain; an epic archived with anungatedGate 2; adeliveredepic with a passing Gate 2 that attributed no commits; an archived openspec epic with no Gate 1; an archive directory the conductor holds no epic for; an epic the heal archived that readsunknownwhile carrying a passing Gate 2; a gate verdict recorded as bookkeeping rather than review; one change id registered under two lanes; a dangling epic reference. It reports and never repairs — a check that repaired would be a second writer racing the paths that produce the records it reads, and one that blocked would turn an audit finding into an outage. Every check that finds nothing still says it ran, because “the check measured nothing” is exactly the failure this release exists to end.release— a release is a first-class object.state.releases[{id, intent, target, deferred[]}], with membership recorded one-way asepic.releaseso the two can never disagree. A deliberate exclusion is recorded with its required reason and the excluded epic stays in the backlog rather than being ended.PROJECT.mdand the briefing render<release>: N epics, M deferred.record-tracker-refresh— records the verdict that clears a tracker-linked epic’s refresh obligation and advances its freshness watermark.- Terminal dispositions with a required reason, at four scopes. An epic ends by recording
--outcome delivered|killed|superseded|abandoned --reason "<why>", never by deletion. The same concept covers a deferral recorded in a design doc, a handoff of unfinished work (--carried-to <epicId>), and a release exclusion. A change dropped at Gate 1 for a good reason used to record byte-identically to one that shipped. - A deferral assertion is required at archive time —
--deferral "<epicId>:<section>",--declined-deferral "<what>:<why not>", or the explicit--no-deferrals. The engine does not read your artifacts and will not guess; an absence you never looked for is indistinguishable from one you confirmed. - Gate verdicts carry checkable evidence.
record-gate-reviewgains--base-sha,--head-shaand--reviewer, stored as fields. A verdict whose recorded head does not reach the commits the epic attributed to itself now reads stale and refuses the archive.gate1acquires a reader — it was previously stored, documented, and consumed by nothing. - Commit attribution —
update-epic <id> --attribute-commit <sha>, repeatable and append-only. The engine infers attribution from nothing: not the files a commit touches, not an epic id in a message. One exclusion: the commit that movesopenspec/changes/<id>/underarchive/must not be attributed, or the epic’s own Gate 2 goes stale at the instant the archive gate reads it. - Epic annotation —
--description(durable rationale, replaced wholesale),--notes(append-only trail that reads as activity), and--clear-linksas the named way to empty the links array. syncreconcilesopenspec/changes/archive/, registering an unknown archived change as an epic already inarchivedstatus. Visible, announced, one-time — never a silent side effect.- The gate procedure PM emits gains five required task items, not prose bullets: the call-site completeness sweep, commit-based verification, the lifecycle-marker declaration, commit attribution, and ending work by recording a disposition. Measured in the audit: a rule carried by a mandatory task section reached 14/14 adoption in subsequent changes; the same rule as a prose bullet reached 3/15.
{/* pm:lifecycle */}— an agent-declared marker on a task line that excludes lifecycle bookkeeping from progress. A task reading “run/opsx:archiveon this change” cannot be ticked before the thing that ticks it, and used to render as outstanding work forever.
Changed
- BREAKING — tracker
directionreplaces direction-by-vendor. Every tracker carriesinward,outwardorboth, set withset-tracker --direction <d>and read by every emitter.rules.mjsused to suppress the outward section with a literalsys !== "github-issues"test whilebriefing.mjsemitted outward drift gated only on a tracker existing, so a repo could receive aCLAUDE.mdwith no outward instructions and a briefing demanding outward action for 29 epics. - BREAKING — a NEW primary tracker with no
--directionnow defaults toinward, regardless of vendor.set-tracker --system jira ...used to produce the outward mirror section and now produces the inward one. Outward creation of issues in someone else’s tracker is the consequential default and must be chosen, not inherited. One-line remedy:set-tracker --system jira --direction outward. Existing repos are unaffected — the migration stamps the direction each tracker already behaves with. - BREAKING — the archive transition is gated on every path, not just
update-epic.reconcileArchived()used to flip an epic toarchivedwith no lane check and no gate check; the rule now binds the function wherever it is invoked —upgrade,render, the commit nudge andsync. An archive that today succeeds silently will either refuse, or record how it bypassed. - BREAKING — an epic that ends carries a disposition. An openspec-lane epic cannot be archived as
deliveredwithout a passing, non-stale Gate 2;killed,supersededandabandonedare exempt by design, because the code was never written or was thrown away. - Three deliberate changes to what PM emits for existing repos. Diffing 0.27.0 output against 0.26.0 will show exactly these three and nothing else; treat a fourth as a regression. (1) The “Sync after completing tracker-linked work” reminder leaves the rules block where no inward procedure is emitted — it used to cite “the writeback steps above” that the same block never emitted. (2) The
consider /pm:syncnudge leaves the briefing for outward-only repos. (3) Progress renders· N lifecyclewhere tasks are lifecycle-excluded. update-epicgains--laneand--plan. A mis-routed epic used to be correctable in exactly one way — remove it and register it again — which discarded its start time, gate verdicts, links and stories.- The emitted inward-sync recipe runs as written. It used to instruct
add-epicwithout the required--id; two independent sessions hit it the same afternoon and each silently invented a slug. The recipe now carries a deterministic derived id, and lane at mirror time comes fromsuggest-laneinstead of a hardcodedclaude-code. - The write-conflict warning latches instead of sampling
count === 3. A burst past the threshold between two briefings warned zero times — so the warning was least likely to fire in exactly the wedged-writer scenario it exists for.
Migration
One entry, additive, idempotent, backward-compatible; astate.json written by 0.26.0 still loads. Tracker direction is stamped with the behavior each tracker already has (github-issues primary to inward, any other primary to outward, every secondary to inward), and every archived epic gets a terminal outcome regardless of lane — delivered only where a passing Gate 2 exists, unknown everywhere else. Lane-scoping that 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.Upgrading — what to expect on the first run
Expect a burst ofheal-archived-epic-passed-gate-2 findings the first time you run /pm:integrity. Every repo that followed the documented /opsx:archive then heal flow lands on outcome: unknown rather than delivered: the migration only stamps epics already archived in state, and the heal flips the rest afterwards, so they miss it by one step. This is expected, not a bug. The check reports each one with the exact remedy — update-epic <id> --status archived --outcome delivered --no-deferrals — and the archive gate’s replacement rule accepts an agent correcting an engine stamp, so the fix is one command per epic and nothing is frozen at unknown.2026-08-18
Added
state.jsonwrites are now guarded against lost updates. Two processes that both read the same state and wrote it back produced a silent lost update: the second write won wholesale and the first one’s change vanished with no error. The atomicrename(2)already guaranteed the write; the unguarded thing was the read-modify-write cycle.loadState()now stamps the on-disk revision onto the object it returns andsaveState()refuses a write whose revision is stale. A lockfile was rejected deliberately — a session killed mid-write leaves a lock held forever, whereas a revision comparison leaves nothing behind.- A no-op save is a no-op. A
saveStatewhose content is unchanged (revision aside) neither writes nor bumps, so re-running a command that changes nothing leavesstate.jsonbyte-identical. This preserves two existing idempotence guarantees the revision bump would otherwise have broken. - A conflict on an interactive verb exits
9, distinct from the1every validation failure already uses, so an agent can tell “someone else wrote, retry” from “you passed a bad flag”. --forceoverwrites deliberately, bypassing the revision check. Without a documented override people learn to hand-editstate.jsonto get around the guard, which is strictly worse than a flag that leaves a trace in the command line. A forced write’s new revision always advances strictly past whatever is currently on disk — otherwise it can land on a revision a later writer already saw, reopening the lost-update window one hop removed.- Hook writes degrade instead of failing. Both hook writes re-run on the next hook, so a conflict is recorded and skipped rather than surfaced as a mid-session error for a write that did not matter. Three consecutive skips warn once in the briefing — and delivering that warning consumes it, rotating the log so the count resets, otherwise the briefing re-warns on every
SessionStartabout a problem that resolved days ago. - Skips are recorded in
.conductor/write-conflicts.log, which rotates at 8 KB keeping one.prev. Size-triggered rather than count-based on purpose: enforcing “keep the last N” means reading and rewriting the file, and this is the failure path of a write guard.
Fixed
initnow git-ignores the conductor’s generated logs..conductor/detours.loghad never been ignored by anything PM ships — it was invisible on the maintainer’s machine only because their personal global gitignore carries*.log, so every other user had carried a permanently untracked file since it shipped. Bothinitandupgradeperform the backfill; wiring it toinitalone would have fixed new repos and missed every existing one.
Compatibility
- A
state.jsonwritten by 0.25.2 has norevision; it loads unchanged and takes revision1on its first write. No migration is required.
2026-08-17
Fixed
- A help flag no longer has a side effect.
--helpwas parsed nowhere in the engine, and the usage string was only reachable from the dispatch fallback that fires for an unknown subcommand — so on a known one,--helpfell through and was consumed as data.log-detour --helpwrote a real row to.conductor/detours.logwith--helpas the detour description. That log is append-only with no verb to remove a row, and it is what a future session reads to reconstruct what happened, so the entry was a false statement in the project’s own history rather than cosmetic noise. Recovery was worse than the bug: the obvious one-liner,head -n -1, is a GNU extension that fails on macOS. Help flags are now handled before dispatch, covering every subcommand rather than the one where the damage happened to be visible —log-detouris simply the verb with a free-text positional in the slot--helplands in. A bare invocation with no subcommand also prints usage and exits 0 instead of erroring. Worth upgrading for even if you have not noticed it: the damage is silent and lands in a file people rarely open.
2026-08-17
Fixed
- A missing progress source now warns instead of rendering an em dash.
bar()rendered—for three different states — “this epic has no progress source”, “the source exists and is empty”, and “the source is missing” — so an openspec epic whosechanges/<id>/tasks.mdhad moved or been deleted was indistinguishable from healthy work with nothing ticked yet. It now renders⚠ tasks.md missing, mirroring the plan lane’s existing⚠ planPath missing. Two claims made while diagnosing this were themselves wrong and are worth recording: the lane did not render0/0(it rendered—, becausebar()returns the em dash whenevertotal === 0), andwarnwas not unconsumed (render.mjsandbriefing.mjsdo not reference it directly, but both render progress throughbar(), which is where the consumer lives). Both were settled by rendering a real fixture rather than reading the source. - Archived epics no longer warn about a missing progress source. Archiving is precisely when a source legitimately goes away: OpenSpec removes
changes/<id>/, and finished plans get moved out ofdocs/superpowers/plans/— which is also how a repo stopssyncre-registering shipped work. Measured on a 108-epic repo, 7 of the 8 epics carrying aplanPathdangled, and all 7 were archived with their plan correctly moved; the single one that resolved wasqueuedand genuinely in flight. A warning wrong seven times out of eight trains the reader to ignore the once it is right. syncno longer registers a plans directory’s own index file as an epic. Every.mdunderdocs/superpowers/plans/was treated as a plan, soREADME.md— the file documenting what the directory is for — became an untriaged epic titled after its H1, observed live as “Superpowers Plans — Active”.readme.md,index.mdandcontributing.mdare excluded by filename; deciding from file contents would make registration depend on heading conventions, which is the fragility this surface already has.
2026-07-30
Added
rules-target— a read-only query printing the absolute path of the file the resolved platform’s rules block belongs in, walking that platform’s first-match-wins chain. It deliberately does not record a platform: a query must not mutate state the waywrite-rulesdoes. It exists so tooling around pm never has to mirrorPLATFORM_RULES_CHAIN.evals/observe.pyhardcodedCLAUDE.md, which 0.24.0 quietly turned into a second platform seam — once the engine began writing a per-platform target, the observer reportedrules_block_present: falsefor any platform whose block lands elsewhere. That is a confident wrong answer, and it would have surfaced on the first Hermes or Codex run as a parity failure that is not real. Copying the chain into Python was rejected: a second copy of platform knowledge only moves the drift instead of removing it.
2026-07-30
Added
- Platform-aware rules block. The managed rules block is no longer Claude-Code-only. The host agent declares itself via
--platform <claude-code|hermes|codex>in the hook command pm authors for that platform, and the active platform is recorded in.conductor/state.json. pm never runs on its own — a host agent always triggers it, through a hook whose command string pm itself wrote — so the platform is declared, not detected: no markers, no filesystem archaeology, no precedence heuristic to get wrong. - Per-platform slash-command form:
/pm:statuson Claude Code and Hermes,/pm-statuson Codex. Thepm:namespace is retained wherever supported for a concrete reason, not aesthetics — Hermes silently skips a plugin command that collides with one of its built-ins, and it ships a built-instatus, so a bare/statuswould be dropped with no signal. Codex derives command names from prompt-file stems, so it is flat and hyphenated. Every value verified against the installed CLIs rather than documentation.
Changed
- The test suite is now
scripts/test/*.test.mjs(11 files) instead of onescripts/conductor.test.mjs, so it runs in parallel. Contributor-facing: the command isnode --test scripts/test/*.test.mjs. Measured rather than guessed — one engine spawn costs ~73ms and the suite makes 676 of them, so ~49s was purenodestartup, serialized in a single file on a 16-core machine;node --testparallelizes across files only. The split is verbatim, verified by test-name-set equality (255 → 255). Notenode --test scripts/test(a directory) does not work — Node treats the argument as a module to execute and dies withMODULE_NOT_FOUNDwhile reporting “1 test”. How much faster depends entirely on free cores, so treat any single headline number with salt. Measured on this project: an otherwise-idle 16-core workstation went ~118s → ~46s (2.6×), but the same machine under load (~12) went ~146s → ~122s — only 1.2×, because parallel execution is exactly what suffers when cores are already contended. On CI, whole-workflow time went from ~52–58s to ~38–48s (~1.3×). On a busy laptop or a few-core runner, expect the low end. .githooks/pre-commitnow aborts if the suite runs fewer tests than are declared. The glob makes a partial-suite pass possible in a way the single file never did: if a file stops matching, everything still goes green, just on a subset. The hook cross-checks the runner’s count againstgrep -Hc '^test('across the files — self-maintaining, with no constant to bump as tests are added.
Fixed
- The rules block is now written to the file the host platform will actually read. Hermes resolves project context first-match-wins over
HERMES.md>AGENTS.md>CLAUDE.md, so in a repo already carrying anAGENTS.md— from a prior Codex attempt, say — the block was silently invisible: no error, no warning, just an agent running without the conductor’s instructions. Writing to a file a platform is capable of reading is not enough; it must win that platform’s precedence chain. - The auto-detour hook no longer writes a
detours.logentry for a commit that did not land in this repo (closes #65 and #68).PostToolUsefires when the Bash tool returns, which is not the same as “a commit landed here.” Three divergences were observed live, each writing a false line attributed to this repo’s stale HEAD: the commit was rejected bypre-commitso HEAD never advanced; the commit was backgrounded and still running; or the commit landed in a different repo — a paired repo, a submodule,git -C elsewhere— leaving our HEAD untouched. An exit-code check would have closed the first but not the second, since a backgrounded commit has no exit code yet; that is why two independently-reported issues share one fix. - Comparing subjects is subtler than it looks, and getting it wrong is worse than the original bug.
git log -1 --format=%syields only the first line, while a-mcapture spans newlines and swallows the whole message — so a naive comparison suppresses every commit that has a body, silently disabling the hook for the common case. The comparison is therefore first-line-to-first-line, and a message the shell assembled (-m "$(…)", a heredoc,-m "$MSG") is treated as unverifiable rather than mismatched, because the command string holds the shell source rather than the text git received.
Migration
0.24.0stampsplatformon existing state files, defaulting toclaude-code. Additive and idempotent; a0.23.1state file still loads.
2026-07-23
Fixed
/pm:upgrade’s own instructions now defend against a real misread: an agent treating the command invocation itself as passive local-command output and skipping it entirely. When/reload-pluginsand/pm:upgradeland in the same turn — the exact sequence this command’s own preamble recommends — the harness wraps that turn’s local command output in a caveat meant for/reload-plugins’s passive stdout, and an agent can over-apply that caveat to/pm:upgradeitself. Self-inflicted by this command’s own documented workflow — fixed by having the command’s own text state plainly: if you’re seeing these instructions, the command was invoked, execute it.
2026-07-21
Added
update-epic <id> --add-story "<title>"andupdate-epic <id> --story <n> --done. Closes a recurring hand-edit-of-state.jsonrisk for inlinestories[]—--add-storyappends{ title, done: false }(creating the array on its first inline story);--story <n>is 1-indexed and currently requires--done(the only supported mutation today) — both reject out-of-range/empty input and write nothing on failure.render --diff-summary— printsepic-relevant: yesorepic-relevant: noafter rendering. The “Last rendered” timestamp and the “Recent detours” table both change PROJECT.md on nearly every render even when nothing about the epics themselves changed, forcing manualgit diffeyeballing before every “is this safe to discard” call.--diff-summarynormalizes both known-trivial sources away and reports whether anything else differs, making the check mechanical.
Fixed
- The
conductor: engine <version> @ <path>banner no longer prints on every single invocation in a self-hosting/dev context. Suppressed by default wheneverCLAUDE_PROJECT_DIRis set — setPM_VERBOSE_ENGINE_BANNER=1to force it back on.PM_QUIET_ENGINE_BANNER=1still works unchanged outside that context. .githooks/pre-commitis now quiet on success — a one-linepre-commit: N/N passingsummary; full output (and a non-zero exit) only surfaces when a test actually fails.verify-worktreesnow also flags a hierarchy-child worktree whose branch is already merged, not just ones whose epic status isarchived. Each flagged worktree’sreasonsarray lists which trigger(s) fired.
Changed
scripts/conductor.mjssplit intoscripts/lib/*.mjsmodules — no behavior change. The engine (2,537 lines, 85 functions) is now a 121-line entry point plus 20 focusedscripts/lib/*.mjsmodules, one per pre-existing section. Motivated by AI-agent token-efficiency, not human-readability tech debt. Purely internal — verified by all 250 pre-existing black-box tests passing unchanged throughout.
2026-07-20
Added
- On-demand AI-agent doc reference:
llms.txt/llms-full.txt. Mintlify auto-publishes both atpm-plugin.dev—llms.txtis a lightweight (~7KB) index of every doc page,llms-full.txtis the entire site as one markdown document (~200KB, tens of thousands of tokens). Referenced from the one-time orientation points that already exist (/pm:init’s step 0, theconductorskill’s new “Further reference” section) rather than the persistent CLAUDE.md rules block, with an explicit size/token warning at every reference point. Also added a docs-site pointer to README.md’s intro.
2026-07-20
Added
- Security scanning via
.github/workflows/security.yml. Calls the two reusableworkflow_callworkflows published incfdude/.github— Semgrep SAST and a Trivy filesystem scan — on push/PR tomainand a weekly schedule, publishing SARIF results to the repo’s Security tab. Both are non-blocking by design. Pinned to a commit SHA (not@main) to avoid a supply-chain gap where a moving branch ref could silently change what runs in CI. SECURITY.md— vulnerability reporting instructions and a short architecture note.
2026-07-20
Added
- CLAUDE.md rules block gains an unconditional “Feedback” section encouraging the agent to proactively use
/pm:feedback [bug|feature] "<summary>"(or ask the user “want me to file this as feedback?”) whenever it hits a bug, a missing CLI verb, or repeated friction — instead of silently working around it. Motivated by a real gap:/pm:feedbackshipped in 0.14.0 and was never used once, while the friction of hand-editing.conductor/state.jsonto flip a story’sdoneflag (no CLI verb exists for it) recurred silently across several sessions before being reported.
2026-07-20
Added
/pm:upgradenow recommends adopting relevant new capabilities. After printing the changelog delta, the command’s instructions tell the agent to review eachAddedheadline, judge whether it’s an opt-in capability (a new flag/subcommand/behavior, not a bug fix or automatic change) relevant to the repo’s current.conductor/state.json, and recommend it — one line, one reason, the command to run — without enabling anything itself. Instruction-only change (commands/upgrade.md,README.md); no engine code or schema touched.
2026-07-20
Added
- Completion-time tracker resync instruction. When an inward-pull-capable tracker is configured (a
github-issuesprimary, or any secondary tracker), the CLAUDE.md rules block now adds a “Sync after completing tracker-linked work” section: after closing/transitioning a tracker-linked issue as part of completing an epic, re-sync with your tracker(s) (/pm:sync) right away, since you’re already doing tracker I/O for that epic. Phrased tracker-count- agnostic (“your tracker(s)”) so it reads correctly whether a repo has one tracker or several (primary + secondary). - Session-start sync nudge. The SessionStart brief now includes a one-line, non-blocking nudge — “N tracker(s) configured (…) — consider
/pm:syncthis session to pull in any new issues” — whenever any tracker (primary or secondary) is configured. This is a reminder only; the engine never calls a tracker itself, and the agent decides whether syncing is worth it. Deliberately does not track a last-synced-at timestamp — the nudge is enough without it.
2026-07-19
Added
- Support a primary tracker plus zero or more secondary trackers.
state.trackeris unchanged and is now, implicitly, the primary tracker — full existing bidirectional behavior (outward issue creation on new epics,statusIntent-driven status transitions), including thegithub-issues-as-primary inward-only special case, byte-for-byte unchanged. New optionalstate.secondaryTrackers[]lets a repo also watch additional trackers — e.g. Jira as the real dev tracker plus a GitHub repo for inbound issues from outside contributors or another internal repo publishing cross-project notifications — viaset-tracker --role secondary --system <sys> --repo <repo>(or--project <key>), removable with--remove. Secondary trackers get inward pull (open issues become untriaged epics) plus a new capability that didn’t exist even for the old inward-onlygithub-issuescase: completion status writeback — when an epic sourced from a secondary tracker reachesarchived, the agent closes the linked issue there too. Secondary trackers never receive outward-created issues; that stays exclusive to the primary tracker. Dedup for both inward pull and writeback now matches onexternalUrl(globally unique) rather than bareexternalId(only unique within one tracker/repo) — fixing a latent cross-tracker collision risk (e.g. issue#42existing in two different secondary-tracker repos) surfaced during this change’s own Gate 1 review, before any code shipped.
2026-07-17
Added
- Mechanical pre-commit hook: the full test suite must pass immediately before every commit, enforced, not just documented. A genuinely failing test was committed once already (0.16.0) because a prose reminder alone wasn’t enough — “run the tests one more time before committing” is exactly the kind of rule that gets skipped under momentum.
.githooks/pre-commitrunsnode --test scripts/conductor.test.mjsand blocks the commit on any failure. One-time setup per clone:git config core.hooksPath .githooks(documented inCONTRIBUTING.md). Found and fixed a real bug on first live use: git setsGIT_DIR/GIT_INDEX_FILE/etc. for hook processes, which leaked into the test suite’s own childgitprocesses (tmp-repo fixtures), causing them to operate against the outer repo’s locked index instead of their own tmp dirs — the hook now unsets those variables before running tests.
2026-07-17
Added
- Added a mechanical test that catches README.md “Commands” drift from the real dispatch table, mirroring the existing SKILL.md drift test. Running it against the current docs caught 8 real gaps (
commit-nudge,log-detour,honcho-memory,set-review-mode,verify-state,write-rules,snapshot,changesets) — several of them genuinely undocumented user-facing subcommands (honcho-memory,verify-state,changesets), fixed in the same pass.agents/hierarchy-child-executor.md’s standing instructions and the conductor skill’s epic-hierarchy preflight section now explicitly require a README.md update (not just SKILL.md) whenever a child epic adds/changes a user-facing command, flag, or behavior — the same class of gap that letrecord-gate-reviewship in 0.16.0 with zero README mention.
2026-07-16
Added
- OpenSpec’s two mandatory gates are now mechanically enforced at archive time, not just narrated. Nothing previously checked that an
openspec-lane epic actually passed Gate 1 (spec review, before code) and Gate 2 (implementation review, before docs) before it was archived — an epic could go straight fromapplytoarchiveon narration alone. A newrecord-gate-review <epicId> --gate 1|2 --verdict pass|fail [--reviewer "<note>"]subcommand writes a fresh-context reviewer’s verdict durably onto the epic (gateReview.gate1/gate2, mirroringrecord-reconcile’s shape), andupdate-epic --status archivednow REJECTS the transition for anyopenspec-lane epic that doesn’t already have a recordedgateReview.gate2.verdict === "pass". Scoped strictly to theopenspeclane —superpowers/claude-code/decision/externalepics are completely unaffected, since they have no two-gate process. - Added a mechanical test that catches SKILL.md “Commands” drift from the real dispatch table.
conductor.test.mjsnow extracts every subcommand key fromconductor.mjs’s dispatch table and asserts each one is mentioned somewhere inskills/conductor/SKILL.md, failing CI the next time a new subcommand ships without a doc mention (the same bug class fixed once by hand in 0.12.0, now enforced instead of relying on someone remembering). Running it against the current docs caught two real gaps —snapshot(the PreCompact-hook-only re-render) andwrite-rules(the/pm:init//pm:upgrade-only CLAUDE.md rules-block refresher) were both real, invoked subcommands with no mention anywhere in SKILL.md — fixed by adding a line for each to the Commands section.
Changed
- The
github-issuestracker no longer tells the agent to auto-create a GitHub issue for every unmirrored local epic.rulesBlock()now suppresses the outward “External tracker sync” section entirely whentracker.system === "github-issues", leaving only the existing inward “GitHub issue sync” section (open issues → untriaged epics) in effect. Filing a public GitHub issue for any local claude-code epic just because agithub-issuestracker is configured is a materially bigger, more consequential default than mirroring toward an internal Jira/Linear instance, sogithub-issuesis now documented and implemented as INWARD-ONLY by design. Jira, Linear, and any other tracker--systemkeep the full bidirectional outward-mirror behavior unchanged.
Fixed
commit-nudge’s auto-detour heuristic no longer false-positives on routine conductor bookkeeping. A commit touching only pm’s own state-output files (.conductor/state.json,PROJECT.md,.conductor/render-stamp.json) is never auto-logged as a stray minimal detour, even if it matches thefix:/chore:+<=3 filesshape — this fired 3 separate times in one session (registering epics, archiving epics, granting autonomy), always on commits that were routine administration, never a real detour.CLAUDE.mdis deliberately excluded from this allowlist: it’s user-authored content, not purely engine-generated output, so a commit touching it could still be a genuine detour.render-stamp.jsonno longer produces a spurious diff on everyrender()call when nothing meaningful changed. Root cause:writeRenderStamp()unconditionally rewrote.conductor/render-stamp.jsonon everyrender()invocation, bumping itsrenderedAttimestamp even whenstate.json(and therefore the renderedPROJECT.mdcontent) hadn’t changed at all — producing a byte-only diff that had to be manually discarded roughly a dozen times across a single dogfooding session.verify-state(the mechanism this stamp exists for) only ever compares the recordedstateMtimeMsagainststate.json’s current mtime; it never readsrenderedAtback for correctness.writeRenderStamp()now skips the rewrite entirely when the existing stamp’sstateMtimeMsalready matchesstate.json’s current mtime, so the sidecar file is only ever touched when something that actually matters changed..conductor/brief.txtwas confirmed already gitignored in this repo (a prior fix); no further action was needed there.
2026-07-15
Added
- Changesets-style fragment files replace direct
CHANGELOG.mdedits for hierarchy children. Every parallel hierarchy-child batch was hitting a 100% collision rate onCHANGELOG.md’s shared## [Unreleased]header — every dispatched child edited the same section, guaranteeing a merge conflict on every multi-child batch. Children now write their changelog entry to.changesets/<epic-id>.mdinstead (same bullet formatCHANGELOG.mdalready uses: a bold one-line summary, then wrapped prose). The orchestrator remains the sole writer ofCHANGELOG.md— consistent with it already being the sole writer of.conductor/state.json— and consolidates all pending fragments into the real[Unreleased]/new-version section once, at release time, then deletes the consumed fragment files. A new zero-dependencychangesetsengine subcommand (node conductor.mjs changesets) lists.changesets/*.mdfragments as{ changesets: [{ id, path, body }] }, sorted by epic id, to make that consolidation step mechanical rather than a manualcat+ guesswork. (First real-world test, this very release: zero CHANGELOG.md conflicts across 3 parallel children, versus a 100% collision rate before.) - Mandatory post-resolution verification for the epic-hierarchy merge-conflict ladder. After ANY conflict resolution (self-resolved by the orchestrator, via
agents/merge-conflict-resolver, or via an escalated model/advisor()opinion), before the merge is committed: grep every touched file for leftover<<<<<<</=======/>>>>>>>markers, and runnode -con every touched.mjs/.jsfile. Either failure means the file is still unresolved. Closes a gap found during this repo’s own 0.14.0 dogfood run, where a resolution removed only the closing conflict markers and left the opening<<<<<<< HEADmarker in place — caught only by a manual re-grep, not by any required step. - Session-continuity check for live external-infra epics. The
hierarchy-child-executoragent now has a required checklist item: before finalizing its report, if the epic’s work made a live change to external infrastructure the orchestrator itself depends on for the rest of the session (branch protection rules, credential/token rotation, webhook/API changes, etc.), it must explicitly answer “does this change affect how the orchestrator itself needs to operate for the rest of this session?” in CONCERNS — even an explicit “no” is required output, not silence. Fixes a real incident:branch-protection-and-pr-workflowapplied live branch- protection settings tomain, and the orchestrator’s very nextgit push origin mainwas rejected — discovered only empirically, not flagged by that epic’s own report. - README.md fully revamped. Replaces the agent-facing, all-over-the-place structure with a Comet-inspired layout: a real banner image, honest badges (CI/version/license only — no DeepWiki/CodeCov/trending until the tooling behind them actually exists), a “Why Use PM?” section built from real, verifiable repo history (not a fabricated benchmark), a genuine “From Industry-Frontier Practice” write-up of the design patterns PM’s architecture actually mirrors, a Supported Platforms table with an honest Status column (Claude Code: Supported; Codex/Gemini CLI/Grok Build/
AGENTS.mdformat: Planned, tracked undermulti-platform-agent-support), collapsible<details>command reference instead of one long flat table, and a Star History chart. Resolvesdf-readme-stale-since-gate-guard(README hadn’t been touched sincef77d774, missing everything shipped since). - Branch protection + PR workflow on
cfdude/pm.mainnow requires pull requests (no direct pushes), thetestjob from.github/workflows/ci.ymlas a required status check, 0 required approving reviews (solo maintainer), and squash-merge-only at the repo level. Day-to-day work moves to a newdevbranch (created frommain’s tip); PRs mergedev→main. SeeCONTRIBUTING.mdfor the full workflow. This is a live GitHub repo settings change, not a code change — nostate.jsonschema impact.
2026-07-15
Added
github-issuestracker: inward pull (open issues → new untriaged epics).set-tracker --system github-issues --repo <owner/name>records a repo alongside the tracker’ssystem. The rules block now gains a “GitHub issue sync” section (in addition to the existing outward “External tracker sync” mirror) telling the interactive agent — as part of/pm:sync— togh issue list --repo <repo> --state open, skip issues already mapped to an epic viaexternalId, and register the rest withadd-epic --status untriaged --external-id <n> --external-url <url> --lane claude-code --priority P2(aP0/P1/P2/P3label on the issue overrides the P2 default). The engine itself never callsgh— same instruction-layer law as every other tracker.add-epicnow also rejects a duplicate--external-idoutright (exits non-zero, writes nothing), so re-running sync can never create a duplicate epic for the same issue even off a stale local view. Seecommands/tracker.md,commands/sync.md, and the conductor skill’s “Hierarchy & external trackers” section..github/workflows/ci.yml. A GitHub Actions CI workflow on push tomainand on every pull request targetingmain, running Node 18.x: anode -csyntax check onscripts/conductor.mjsandscripts/conductor.test.mjs, then the full test suite vianode --test scripts/conductor.test.mjs. This repo is zero-dependency, so “lint” here means the syntax check rather than a third-party linter. The job is namedtest(jobs.test) — a follow-up epic wires this job into required branch-protection status checks./pm:feedback [bug|feature] "<summary>". File a bug report or feature request againstpmitself directly as a GitHub issue oncfdude/pm, from any session using the plugin — replacing the previous workflow of manually copy-pasting details between sessions. Pure command-doc addition: the interactive agent gathers the report, searches open issues oncfdude/pmfor a near-duplicate title (commenting on a match instead of filing a new issue), and otherwise runsgh issue create --repo cfdude/pmwith abug/enhancementlabel, reporting back the issue URL. No engine code involved —scripts/conductor.mjsnever calls GitHub itself; allghcalls are agent-invoked Bash, per the instruction-layer law. Seecommands/feedback.md.
2026-07-15
Added
record-reconcile <epicId> --detour <detourId> --verdict valid|invalidated [--amendments "<a>;<b>"]. The reconciler agent’s verdict at the POP-protocol reconcile gate previously only ever lived in the conversation transcript. This subcommand writes a structured{verdict, amendments, reconciledAt}object onto the paused epic’s link to the detour that triggered reconciliation (creating amay-invalidatelink if none exists yet), and clearsreconcileNeeded— so the judgment is durable in.conductor/state.jsonand visible inPROJECT.md, not just something Claude said once.agents/reconciler.md’s report format,commands/resume.md, and the conductor skill’s POP protocol / rules block now describe this writeback step.- Per-repo lane-routing overrides. New optional
laneRouting.overridesconfig block in.conductor/state.json— keyword/glob rules ({match, lane}) checked BEFORE the generic lane heuristic when an agent decides which lane should build an epic. Set via the newset-lane-routing --add "<match>:<lane>" [--add ...] | --remove "<match>" | --clearsubcommand; looked up via the newsuggest-lane "<free text>"subcommand, which prints{lane, matched}JSON (lane: nullmeans no override matched — fall back to the generic heuristic). Replaces the need for a CLAUDE.md prose carve-out when the generic heuristic is wrong for a repo (e.g. “anything touching billing always goes through openspec”). Seecommands/lane-routing.mdand theconductorskill’s “Lane routing overrides” section. Pure local state write — the engine still never assigns a lane itself;add-epicalways takes an explicit--lane. - Per-epic review-mode override (escalation-only).
update-epic <id> --review-mode off|standard|thoroughsets an epic-level override that can only ESCALATE above the repo-globalset-review-modedial — never de-escalate below it (an attempt to set a lower mode than the current global dial is rejected outright, state unchanged).currentReviewModenow accepts an optionalepicIdand returns the effective mode for that epic: the higher-ranked of the global dial and the epic’s override.rules --epic <id>surfaces the effective per-epic mode in the emitted “Current mode” line. Lets one security-sensitive epic forcethoroughreview without flipping an otherwise-standardrepo’s global dial. - Auto-detected minimal detours from commit diff shape.
commit-nudge(thePostToolUse(Bash)hook that already fires after everygit commit) now recognizes an UNLOGGED minimal detour by its shape — a small commit (<=3 fileschanged) with afix:/chore:conventional-commit subject, made while no detour is active, and not scoped to the currently active epic (afix(<active-epic-id>): ...subject is read as that epic’s own work, not a stray detour) — and appends anAUTO-DETOURentry to.conductor/detours.logautomatically, without waiting for/pm:detour --minimalto be run by hand. Three separate dogfooding sessions converged on “the agent forgets to log the minimal detour” as the #1 pain point; this closes that gap at the mechanism level (hook-driven, not agent-remembered) rather than relying on the agent to recall the rule. SeelooksLikeUnloggedMinimalDetour()/headChangedFileCount()inconductor.mjs. honcho-memory <push|pop> <epicId> "<reason>"subcommand. Formats the exact ready-to-copy one-line Honcho memory string for a detour-stack PUSH/POP (per CLAUDE.md rule 4), prints it to stdout, and appends a timestamped copy to the new.conductor/honcho-memories.log. Previously the interactive agent had to compose that string itself from context on every PUSH/POP, with no engine support and no durable record of what was actually sent — easy to forget or word inconsistently. The engine still never calls Honcho itself (pure string formatting + local logging, staying inside the instruction-layer law);commands/detour.md,commands/resume.md, and theconductorskill’s PUSH/POP protocols now call it and paste its output into the actual Honcho MCP call.- Dependency-aware ordering for the top-level queue, not just hierarchy siblings. The brief’s NEXT UP list (and thus
/pm:next’s recommendation) now applies the samedepends-ontopological orderingplan-hierarchyalready used for one parent’s children to ALL top-level queued/untriaged epics: a higher-priority epic with an unresolveddepends-onlink to another still-queued epic is no longer listed (or picked) ahead of the dependency it’s waiting on, even across otherwise-unrelated epics with no shared parent. When ordering overrides plain priority this way, the brief prints a one-line note naming the blocker, e.g.⚠ epic \high-blocked` ready but waiting on `low-dep`. Unlikeplan-hierarchy`, a dependency cycle among queued epics does not error here — it’s a display/selection helper, not an execution plan, so it falls back to the original priority order for the stuck remainder. - SessionStart upgrade nudge now inlines top Added-bullet headlines. The
pm X.Y.Z → A.B.C availablenudge previously named only the old/new versions, forcing a separate/pm:changeloground trip to judge whether upgrading was worth mid-epic churn. It now inlines up to 3 “Added” bullet headlines (first line only, not the full multi-line body) drawn from every CHANGELOG.md section strictly between the stamped and newest version, so a session can judge upgrade value inline. - Category-based
--preauthorizeshorthand for epic-level autonomy.set-autonomy <id> --preauthorize "category:<filesystem|network|schema|external-api>:<reason>"grants routine actions by category instead of requiring every one enumerated individually. Stored as a distinct{ category, reason?, grantedAt }grant shape alongside existing exact-action{ action, reason?, grantedAt }grants in the samepreAuthorized[]array — exact-action matching is unchanged. Unknown categories are rejected with a non-zero exit and no state write. The matching heuristic each category expands to at decision-rule time (approximate by design) is documented in theconductorskill’s “Epic-level autonomy — the preflight scan” section.
Changed
- Epic-level-autonomy decision rule now says “
--notifyincrementally as it happens,” not “record for the end-of-epic report.” The--notifymechanism already writes durably tostate.json’snotifications[]array; the prior wording implied WARN-class (c) and consequential (e) decisions were only gathered in-memory for a report assembled at the end of the epic, which loses them if the session is compacted or interrupted mid-epic. Fixed in bothCLAUDE.md’s rules block and the identical generated block inscripts/conductor.mjs’srenderRulesBlock-equivalent. The end-of-epic report step now reads backnotifications[]rather than being the primary record. No code change —--notify/notifications[]already worked this way; this is a wording fix so the documented process matches the existing mechanism. - Gate guard is now on by default whenever an epic owes a reconcile.
gateGuardCheck()now blocksEdit/Write/NotebookEditunconditionally when the active epic’sreconcileNeededistrue, regardless of the repo’sgateGuardsetting instate.json—set-gate-guard offno longer bypasses this specific case. Applies retroactively to any epic that already hasreconcileNeeded: true, not just future detour POPs. Reverses the original opt-in design after real-usage feedback (docs/feedback/2026-07-14-pm-plugin-improvement-feedback.md) showed the guard had never actually been turned on across several sessions where it would have caught a real skip. The repo-levelgateGuardflag andset-gate-guard on|offcommand still exist, reserved for any future generalization of the hook to other checks. Seecommands/gate-guard.mdand theconductorskill’s POP protocol.
Fixed
missing()now excludesstatus === "archived"epics. An already-archived openspec epic (proposed, built, and archived — itsopenspec/changes/<id>directory legitimately moved toopenspec/specs/by the archive process) could still render the unresolvable ”⚠ no change on disk” warning forever if its on-disk archive-dir name didn’t matchisArchived()’s dated-prefix convention. Same class of bug already fixed forplanHierarchy()(df-plan-hierarchy-includes-archived-children, 0.12.1), applied here to the missing-change-warning code path.
2026-07-15
Added
startedAt/completedAttimestamps on epics, and a staleness indicator.set-activenow stampsstartedAt(ISO string) the first time an epic goes active (re-activation after a demotion does not reset it);update-epic --status archivedstampscompletedAt. Both fields are purely additive — existing epics simply lack them until touched, so no migration is needed.PROJECT.md’s epic table, its “Now” section, and the brief’sNOW/NEXT UPlines all surface⚠ stale, Nd activefor any epic withstartedAtset, nocompletedAt, and more than 14 days elapsed — supporting velocity tracking and the weekly Ship-Real-Software check.verify-statesubcommand.render()now writes.conductor/render-stamp.json(renderedAt+ the state.json mtime it rendered from) every time it runs.verify-statecompares state.json’s current filesystem mtime against that stamp and fails loudly (non-zero exit, clear stderr) if state.json was modified after the last recorded render — mechanical evidence of an undetected hand-edit, which CLAUDE.md explicitly forbids (state.json/PROJECT.md must only change through the engine’s subcommands). Also fails loudly if no stamp exists yet (state.json has never been rendered).- Engine version+source banner on every invocation.
conductor.mjsnow printsconductor: engine <version> @ <path>to stderr on every run (silenceable viaPM_QUIET_ENGINE_BANNER=1). Discovered live while dogfooding:$ENGINEresolution had silently picked up the installed plugin cache’s0.12.0copy while this repo — the plugin’s own source — was already at0.12.1, with no signal anything was stale.
Fixed
- ENGINE-resolution snippets (skill doc + every command doc) now prefer a repo-local
$CLAUDE_PROJECT_DIR/scripts/conductor.mjsbefore$CLAUDE_PLUGIN_ROOTand the installed-cache fallback. When the repo being worked on IS the pm plugin source (self-hosting), that copy is always the one under active development and should win over a stale cached install.
2026-07-15
Fixed
plan-hierarchyno longer includes already-archived children in a hierarchy plan. Children were filtered byparentonly, with no status check — a done child (e.g. one already merged and archived from a prior dispatch batch) still showed up in the plan, indistinguishable from real pending work. Discovered via the first live dogfood resumption againstpm-plugin-improvements-2026-07-14. Excludingstatus === "archived"from the children filter also correctly makes adepends-onreference to an archived sibling fall outside the hierarchy’s dependency graph — the same existing behavior as a link to any epic outside the hierarchy, since a done dependency imposes no wait.
2026-07-15
Added
verify-worktrees— orphaned hierarchy-dispatch worktree detection. Cross-referencesgit worktree listagainst epic status: any worktree on ahierarchy-child/<epic-id>branch whose epic is already archived (successfully merged and closed out) is flagged. Bakes worktree hygiene into the plugin itself — checkable on any fresh install — rather than depending on a user’s personal CLAUDE.md discipline. Pure read, flags without deleting.- Worktree-isolated epic-hierarchy dispatch, replacing the original “just dispatch in parallel” instructions. Discovered via the first live dogfood attempt against a real hierarchy (every child touched
scripts/conductor.mjs): concurrent children mutating shared files was a real, unaddressed race. Each child now works in its own git worktree; children never write.conductor/state.jsonthemselves (the orchestrator is the sole writer, applied once per batch); worktree branches merge back sequentially. An ordinary merge conflict is never a hard stop — it’s resolved via a tiered ladder (normal merge → dispatch the newagents/merge-conflict-resolver→ escalate to a stronger model/advisor()→ commit best-effort + log a follow-up epic under the same parent) — a direct, consistent application of epic-level autonomy’s existing decision rule, since a git-tracked conflict is always recoverable via history (criterion (c), never the unconditional-stop criterion (b)). agents/merge-conflict-resolver.md— a new packaged agent (mirrorsreconciler.md’s shape) dispatched to resolve a worktree-merge conflict, reportingresolved/uncertain/failedso the orchestrator knows whether to escalate further.
Fixed
- Doc drift in the conductor skill’s Commands line:
remove-epic,plan-hierarchy, andverify-worktreeswere all missing despiteremove-epic/plan-hierarchyalready having shipped in prior releases.
2026-07-15
Added
remove-epic <id> [--cascade]— hard-delete an epic, replacing the rawgit checkoutworkaround that was the only prior recovery from a mis-registered epic. Blocked by default if the epic has children: prints a concise(id, title, lane/priority/status)table of the parent plus every child and exits non-zero, so removing a parent with descendants is always a deliberate, informed choice;--cascaderemoves the epic and all descendants together in one atomic write. Any other epic’slinks[]entries referencing a removed id are stripped automatically, with a warning naming the affected epics. Recoverable only via git history — deliberately no in-app undo, since this verb exists specifically to replace that workaround, not add a softer one next to it.
2026-07-14
Added
plan-hierarchy --parent <id>— batched execution plan for a parent epic’s children. Computes batches from data pm already tracks (no new persistent state):priorityanddepends-onlinks between siblings drive a topological sort — children with no dependency on each other land in the same batch (dispatchable in parallel), children in a dependency chain land in separate, ordered batches. Each child is annotated with whether it already hasautonomy.level: "autonomous"(from epic-level autonomy), so a hierarchy dispatch never fires a child that hasn’t been preflighted. Each child also carriesdependsOn, its sibling dependency ids within the hierarchy, so a blocked-child handler can check whether a later batch depends on it (directly or transitively) rather than guessing from batch order alone. A dependency cycle among children is rejected outright, naming the cycle path, rather than producing a bogus order.agents/hierarchy-child-executor.md— a packaged subagent dispatched once per child epic in a batch: front-loaded with the epic’s full context and its autonomy grant, works the epic to completion using its lane’s normal workflow, follows epic-level autonomy’s decision rule for genuine stops, and returns a fixed report (STATUS/DONE/DECISIONS/CONCERNS).- The
conductorskill documents the full end-to-end process: preflight every child up front (reusing epic-level autonomy’s scan, consolidated into one batch of questions) →plan-hierarchy→ dispatch batch by batch (parallel within a batch, sequential across batches) → one consolidated end-of-hierarchy report flagging anything controversial. - Deferred to a later release: the fuller execution-strategy-selection framework (plain subagents vs. the Workflow tool vs. other execution modes) — this release covers only subagent-per-child dispatch.
2026-07-14
Fixed
add-epic --linkaccepted a malformed value silently instead of erroring. It split the string on:and stored whatever came out with no validation — a typo liketype:related:epic:...parsed successfully as{type:"type", epic:"related"}since nothing checked that"related"was a real epic id.parseLinkFlags()now requires at least two segments and that<epic>references a known, existing epic id, rejecting otherwise with a clear error (shared byadd-epicandupdate-epic).update-epichad no--linkflag, so a malformed link (from before this validation existed, or from a hand-edit) had no CLI path to fix — forcing a directstate.jsonedit, which is what caused a reported em-dash JSON-escaping corruption across unrelated epics.update-epic <id> --link "<type>:<epic>[:<reason>]"now REPLACES the epic’s links wholesale (unlike the other flags, which patch a single field) — the intended fix path.
2026-07-14
Added
set-gate-guard <on|off>— optional, opt-inPreToolUseguard hook. BlocksEdit/Write/NotebookEditwhile the active epic still owes a reconcile after a detour POP (reconcileNeeded). Off by default and dormant until/pm:init. This is the one place pm’s law tolerates mechanical blocking over pure instruction — it protects the single highest-stakes skip (writing source before the reconcile gate runs) as a deliberate, reversible opt-in, never a silent default.
Fixed
- POP protocol never actually told you to SET
reconcileNeeded. The conductor skill documented clearing it after reconciliation, but never setting it true on the paused epic before its detour-stack frame is popped — without that, the flag (and the new gate guard) would never actually trigger. Documented as a hand-edited step, mirroring how the frame itself is already hand-edited. - Doc drift in the conductor skill: the Commands line and
state.jsonreference were missingset-autonomy,set-review-mode,autonomy,reviewMode, andgateGuard— none had been added when those features shipped in 0.8.0/0.9.0.
2026-07-14
Fixed
- Regression from 0.8.4:
reconcileNeededwas cleared on an active epic with no live detour frame, defeating the post-pop reconcile gate. POP protocol removes the detour- stack frame BEFORE reconciliation runs, so deriving the flag purely from live-frame presence wiped it out at exactly the moment it needed to stay true (just-resumed, reconcile not yet done).reconcileArchived()now only recomputes what’s safely derivable from current state: an archived epic always clears it (reconcile is moot); a still-paused epic with a livereconcileOnResumeframe gets it forced true; anything else stale heals to false only if it’s NOT the current active epic, since that’s exactly the legitimate post-pop-pre-reconcile window.
2026-07-14
Added
set-review-mode <off|standard|thorough>— a bounded, repo-level review-count dial. Incorporates Comet’sreview_modeconcept: a single setting (not per-epic) replacing an ad-hoc “how many reviews, when” judgment call with an explicit, dedup’d table.off= self- review only;standard(default when unset) = one fresh-context reviewer per gate;thorough= two independent reviewers per gate with disagreement adjudicated by you. Writesstate.reviewModeand refreshes the CLAUDE.md rules block’s new unconditional ”## Review mode” section, which always shows the currently active mode. Pure instruction-layer — no external calls.
2026-07-14
Fixed
- Recompute-don’t-remember:
.activevalidity andreconcileNeededare re-derived from disk, not trusted as stored flags.reconcileArchived()previously only cleared.activewhen it pointed at an archived epic — a pointer referencing an epic id missing entirely fromstate.epicswas never healed.reconcileNeededwas pure remembered state (set/cleared only by hand-editing per the PUSH/POP protocol), with no recovery if a session lost context mid-detour. Both are now recomputed from ground truth (the epics array, the detour stack’sreconcileOnResumeframes) every timerender()runs — including at the end of/pm:resume— healing stale flags in either direction.brief()stays deliberately read-only, displaying the same recomputed truth in-memory without persisting.
2026-07-14
Fixed
state.jsonwrites are now atomic (tmp+rename).saveState()previously wrote directly viawriteFileSync; a crash or kill mid-write could leave a truncated, unparseablestate.jsonwith no recovery path. Now writes to a.tmp-<pid>-<ts>file in the same directory andrename(2)s over the real path — atomic on the same filesystem, so a crash leaves a truncated tmp file instead of corrupting the system of record.
2026-07-14
Fixed
KNOWN_STATUSESomittedlater/blockeddespite both being documented in the README’s Epic statuses table andcommands/init.md—add-epic/update-epic --status later(orblocked) was rejected outright. Both statuses now validate and persist correctly; NEXT UP already excluded them (onlyqueued/untriagedare included) with no other code change needed, and they correctly still count in the lanes rollup (onlyplannedis excluded from both NEXT UP and the rollup, per the documented distinction).
2026-07-14
Fixed
update-epicsilently no-op’d on an unrecognized flag. A typo’d or unwired flag would parse, runsaveState/render, and printconductor: updated '<id>'even though nothing changed — the only way to catch it was cross-checkinggit diff.update-epicnow validates its flags against a known set and exits non-zero with an “unknown flag” error instead of a false success.update-epichad no--titleflag.add-epicsupports--titleat creation, but correcting a title after an investigation changes what an epic is actually about (a common, legitimate mid-epic event) had no CLI path and required hand-editingstate.json, which the tool explicitly discourages.update-epic <id> --title "..."now works.
2026-07-13
Added
set-autonomy <id>— per-epic autonomy contract. An epic can be granted broad execution trust (autonomy.level: "autonomous", default"off"— unchanged behavior) so it runs through phase transitions without stopping for permission each time. Autonomy is granted only after a preflight risk-scan (documented in theconductorskill) records the user’s pre-authorized actions and supplied context via--preauthorize/--context(repeatable, additive). A five-criteria execution-time decision rule (injected into the CLAUDE.md rules block) still hard-stops for anything with no backup/restore path or no context to act on — autonomy never overrides a genuine safety gate, only removes false ones.PROJECT.mdand the session brief mark an autonomous epic with 🤖. Tracker-linked epics (Jira etc.) get an addendum covering lane-aware source reading, non-authoritative comment-mirroring of approvals, and mid-run drift as its own stop condition.- Development-time scope only — this does not cover actions with irreversible EXTERNAL side effects (sending email/Slack, deploying to production, third-party API calls, pushing to a shared branch); those remain out of scope regardless of autonomy level.
2026-07-08
Added
set-active <id>/clear-active— a CLI verb for the top-level active epic (closes #1). Previously.active— the pointer the briefing’s “NOW” line reads — had no CLI setter, so/pm:next’s “make it active” forced hand-editingstate.json, against the “CLI is the safe interface” model.set-active <id>(positional id) sets the pointer;clear-activedrops it.
Fixed
.activeandstatus: "active"can no longer silently disagree. They were independent fields —update-epic --status activeflipped the status but left.activenull, so the brief reported “no active epic” despite an active epic. Now a single-active invariant is enforced through every CLI path:set-active,update-epic --status active, andadd-epic --status activeall set.activeand the epic’s status together and demote any previously-active epic toqueued; moving the active epic offactive(orclear-active) clears the pointer.set-activerejects an unknown or archived id.
Changed
- Skills/commands resolve the engine version-independently. The
conductorskill and/pm:nextnow prefer$CLAUDE_PLUGIN_ROOTand fall back to the newest installedconductor.mjs(ls -t …/pm/*/… | head -1) instead of embedding a versioned cache path like…/pm/0.6.1/…, which broke on upgrade.set-active/clear-activeare documented in/pm:next,/pm:epic, the skill, and the README.
Upgrade
Minor release — no schema change, no data migration. Update the plugin →/reload-plugins → /pm:upgrade.2026-06-26
Fixed
- Archived OpenSpec epics stayed stuck as the active epic.
isArchived()only matched an archive dir named exactly<id>, but OpenSpec archives a change asopenspec/changes/archive/<YYYY-MM-DD>-<id>. So the engine never detected the archive: the epic kept itsactivestatus,state.activekept pointing at it,/pm:statusshowed a finished epic as NOW,/pm:nextwouldn’t advance, and the epic could even be mis-flagged ”⚠ no change on disk.” Fixed three ways:isArchived()now matches both the exact id and OpenSpec’s date-prefixed dir.- Display honesty:
render/briefno longer present an archived epic as the active one — they show “(no active epic —Xwas archived)”, so/pm:statusand/pm:nextare correct immediately, with no state mutation. - Self-heal: a new
reconcileArchived()clears anactivepointer aimed at an archived epic and stampsstatus: archived. It runs insync,commit-nudge(so the state heals on the same commit that archives the change),init, andupgrade— no more hand-editingstate.jsonafter an archive.
Upgrade
Patch release — no schema change, no data migration. Update the plugin →/reload-plugins → /pm:upgrade.2026-06-25
Added
- Knowledge surfacing — the plugin now teaches the agent at the two moments that matter. Previously an upgrade exposed new commands but never explained what it brought, and a first-time install gave the agent no orientation beyond command descriptions. Closed both:
/pm:upgradeprints a changelog delta. After applying migrations, the engine reads its ownCHANGELOG.mdand prints every entry in(stamped, running]— so the agent and user see exactly what the version added, not just that it happened.- New
changelogsubcommand +/pm:changelog [--since <x.y.z>]. On-demand changelog delta; defaults its floor to the version stamped in this repo’sstate.json. Zero-dependency markdown parsing (sections split on## [x.y.z]headers); graceful when no CHANGELOG ships. /pm:initorients the agent first. Init now instructs the agent to load theconductorskill (the agent-facing how-to) — and points at the shippedREADME.mdfor deeper reference — so even a cold install of a much-later version knows how to drive the plugin. Deep orientation stays a one-time/on-demand load; the persistent CLAUDE.md rules block remains the recurring anchor (no full-orientation injection every session).
Upgrade
Minor release — no schema change, no data migration. Update the plugin →/reload-plugins → /pm:upgrade; the upgrade will now print what this version (and any you skipped) brought.2026-06-25
Fixed
- Multi-version upgrade ordering (hardening).
upgrade()already replayed every migration newer than the stamped version, so a repo several versions behind (e.g.0.2.0 → 0.5.x) was upgraded correctly. This release makes that guarantee robust: migrations are now applied sorted by release (independent of array authoring order), theMIGRATIONSarray is documented as append-only / never-reorder, and a regression test asserts a two-versions-behind repo replays both the 0.3.0 (lane) and 0.5.0 (link-normalize) migrations in order. - Tracker detection no longer over-triggers on Git hosting. The
/pm:tracker,/pm:init, and/pm:upgradedetection guidance previously let the agent infer a tracker from the fact that a repo is hosted on GitHub. Hosting on any Git service (GitHub, GitLab, Bitbucket, …) is not a signal — they all have issues/PRs, but a remote is not evidence that work is managed there. Detection now requires a real signal (an in-use tracker MCP, issue-key conventions, or an explicit statement), frames tracker mirroring as an optional choice, and reassures that declining loses nothing — the conductor always tracks everything locally in.conductor/state.json+PROJECT.md; a tracker only adds an external mirror. Choosing a Git host as the tracker (issues + PRs) remains fully valid.
Upgrade
Patch release — no schema change, no data migration. Update the plugin →/reload-plugins → /pm:upgrade to stamp 0.5.1 and refresh the rules/command docs.2026-06-25
Added
- First-class epic hierarchy. Epics gain an optional
parentfield (single-parent tree, arbitrary depth).add-epic --parent <id>validates the reference (must exist, no self-parent, no cycle) via a sharedparentError()ancestor-walk helper.PROJECT.mdrenders children indented beneath their parent (└─, deepened per level), groups families ordered by parent priority, and shows anX/Y children archivedrollup in the parent’s Progress cell. The briefing’s NEXT UP annotates a child with its parent id. Grouping is render-only — theresolveEpicspriority sort is untouched, so a P0 child of a P2 parent keeps its NEXT UP slot. - External-tracker awareness (instruction layer only). An optional
trackerblock instate.json(system,instance,projectKey,mechanism, and a semanticstatusIntentmap) makes the conductor aware a project mirrors epics to Jira/GitHub/Linear. The engine never calls the tracker — it only shapes the instructions it already emits:- the CLAUDE.md rules block gains an “External tracker sync” section assigning the interactive agent ownership (create issue + record key; transition on status change toward the semantic
statusIntent; parent epic → tracker epic); - the briefing gains a
TRACKER SYNCblock listing only honestly-computable drift — active-work epics (queued/active/paused, excludingmissing()ghosts) with noexternalId. No transition-drift is fabricated (the engine cannot see tracker state). - New
set-trackersubcommand (repeatable--intent <status>:<target>;parseFlagsnow accumulatesintentlikelink) writes the block and refreshes the rules. - New per-epic
externalId/externalUrlfields (onadd-epicandupdate-epic). - New
update-epic <id>write-back subcommand (positional id) mutatesexternalId/externalUrl/parent/status/priorityon an existing epic under the same validation as creation — closing the sync loop after the agent creates an issue. - New
/pm:trackercommand doc;/pm:initand/pm:upgradegain an agent-driven detection step (detect signals → confirm with the user →set-tracker; upgrade only when unset).
- the CLAUDE.md rules block gains an “External tracker sync” section assigning the interactive agent ownership (create issue + record key; transition on status change toward the semantic
- Atomic bulk creation. New
add-many --from <path|->reads a JSON{ parent?, epics[] }batch. Ifparentis present it is created first and children default theirparentto it. Every entry is validated up front (id format, uniqueness vs existing AND within the batch, lane, status, parent refs + intra-batch cycles); on any failure nothing is written and it exits non-zero. A valid batch persists in a single write — removing the race that forced chaining individualadd-epiccalls. JSON only (the engine stays zero-dependency).
Fixed
- Stale-link rendering.
render()and the briefing now emit a link only when both itstypeandepicare strings (sharedvalidLink()helper), so malformed or older-schema link entries no longer render asundefined undefined.
Migration
- 0.5.0 migration (repair-first).
MIGRATIONSgains a0.5.0entry that normalizes storedlinks: valid{type, epic}objects pass through, the documented colon-string encodingtype:epic[:reason]is repaired into an object, and unrecoverable entries are dropped. Additive and idempotent. Defensive rendering (above) is the shape-agnostic durable fix.
Compatibility
All additions are optional and backward-compatible: astate.json written by v0.4.1 loads unchanged, and a 0.5.0-written state remains loadable by the older engine (it ignores the new optional fields).Upgrade
Existing repos: update the plugin →/reload-plugins (or restart) → /pm:upgrade per repo. The upgrade runs the additive, idempotent 0.5.0 migration, refreshes the rules, and stamps pmVersion: 0.5.0. To make a repo tracker-aware, run /pm:tracker (or answer the detection prompt during /pm:upgrade).2026-06-22
Added
/pm:upgradestaleness guard./pm:upgradenow checks whether the running engine version matches the newest installed version before proceeding. If they differ (i.e. the plugin was updated but Claude Code has not been reloaded), it refuses with a clear message — “this is pm<old>but<new>is installed; run/reload-pluginsor restart Claude Code first” — instead of silently re-stamping an old version. From 0.4.1 forward every upgrade is self-guarding.- SessionStart nudge fires from newest installed version. The upgrade nudge in the SessionStart briefing now keys on the newest installed version (from the plugin’s
plugin.json) rather than the running engine version. This means the nudge fires even before you reload Claude Code, and it names the full sequence: (1) reload/restart; (2)/pm:upgradeper repo. - Documented update sequence.
upgrade.mdand README both document the required three-step sequence: update the plugin →/reload-pluginsor restart →/pm:upgradeper project. The upgrade command note now explains why the reload step is mandatory (Claude Code loads the engine at session start).
Limitation
The staleness guard ships inside 0.4.1, so the first upgrade into 0.4.1 still runs the old 0.4.0 engine until you/reload-plugins. From 0.4.1 forward every upgrade is self-guarding.Upgrade
Existing repos: run/pm:upgrade after updating — refreshes rules, stamps 0.4.1 into state.json. Idempotent; safe to run multiple times. No data migration required. Remember to /reload-plugins first (see above).2026-06-18
Added
status: planned— roadmap as ordered backlog. A new epic status for items that are known, sequenced, but not yet ready to start.planned: Nappears as a brief summary line in the briefing; planned epics are excluded from NEXT UP and the lanes rollup, but are shown in the PROJECT.md epics table so the full backlog is visible.syncauto-transitions proposed planned epics → untriaged (openspec lane). Whensync/initdiscovers a new OpenSpec change on disk and an epic with the same id already exists withstatus: planned, it transitions that epic tountriagedautomatically so it enters the normal triage flow without manual state editing.- PROJECT.md stamp-on-content-change only.
rendernow compares the new output to the current file before writing; if the content is identical, the file is not touched. Prevents mtime churn and spurious git diffs when nothing meaningful changed. add-epicvalidates--statusagainst known statuses. Passing an unknown status to/pm:epic addis now an error rather than silently stored. A valueless-flag guard also catches--statuswith no argument (e.g.--status --lane) and reports a clear error instead of treating the next flag as the status value.- Portable
ls -tglob in command docs. Thefind-based file listing insynccommand documentation is replaced with a portablels -tglob, removing a macOS/GNUfindincompatibility. --statusdocumented in/pm:epic. Theaddsub-command now shows all valid status values (includingplanned) in its help text and the commands table.- Roadmap on-ramp guidance. README and SKILL document how to import an existing roadmap into the conductor without parsing: in an interactive session, read the roadmap doc and register each item via
/pm:epic add … --status planned, choosing the appropriate execution lane. The conductor does not parse roadmap files automatically.
Changed
- Rules block wording updated: documents
plannedstatus (roadmap on-ramp), auto-transition of planned epics onsync, and stamp-on-content-change behaviour.
Upgrade
Existing repos: run/pm:upgrade after updating — refreshes rules, stamps 0.4.0 into state.json. Idempotent; safe to run multiple times. No data migration required.2026-06-18
Added
- Lane-agnostic epics. Epics are no longer restricted to OpenSpec proposals. Every epic now carries a
lanetag —openspec | superpowers | claude-code | decision | external— so the conductor tracks the full backlog regardless of how work is executed. - Epic schema fields.
lane(string, optional, backward-compatible): execution lane. Defaults to"openspec"on read so existingstate.jsonfiles are unaffected.planPath(string, optional): repo-relative path to a Superpowers/markdown plan file. Used as a progress source whenstories[]is absent.stories(array, optional): inline{ title, done }story list. Highest-priority progress source.
- Progress precedence resolver.
epicProgress(epic)replacesstoryProgress(id)and resolves progress in order:stories[]→planPathcheckboxes →openspec/changes/<id>/tasks.md→—. A danglingplanPathrenders⚠ planPath missingrather than silent0/0. - Non-OpenSpec epics in the briefing. Non-OpenSpec epics now appear in NEXT UP and the Epics table. Only OpenSpec epics missing their on-disk change are flagged
⚠ no change on disk; other lanes are shown as-is. - Bounded briefing. NEXT UP is capped at top-5 by priority-then-lane, with a per-lane count summary (
lanes: openspec 4 · superpowers 12 · claude-code 9) and a(+N more — see PROJECT.md)overflow line, so the briefing stays compact regardless of backlog size. /pm:epic add. Registers a new epic directly (nostate.jsonedit required):Validates id format (^[a-z0-9][a-z0-9._-]*$), lane, and uniqueness. Optional flags:--plan PATH,--status STATUS,--link "type:id:reason".syncimports Superpowers plans.docs/superpowers/plans/*.mdare scanned onsync/initand registered as lane-superpowersepics (id = filename without.md,planPathset, title from first#heading). Additive and id-collision-safe (colliding ids are skipped with a warning). The plans directory may be absent — the scan returns[]gracefully.- Version-aware upgrade subsystem.
initandupgradestamppmVersion(the running release) intostate.json.brief()compares the stamped version to the running release; if older, prepends a one-line upgrade nudge (re-shown every SessionStart and PreCompact until resolved)./pm:upgraderuns registered migrations (those whosereleaseis newer than the stamped version), then unconditionally refreshes the CLAUDE.md rules block, re-rendersPROJECT.md, and re-stampspmVersion. Idempotent — a second run is a no-op.- 0.3.0 migration: stamps an explicit
lane: "openspec"on any epic lacking one, makingstate.jsonself-describing.
- Lane-agnostic detour rules. A substantial detour becomes its own epic in the appropriate lane (not necessarily an OpenSpec proposal). The
rulesBlock()wording and PUSH/POP templates are updated accordingly.
Changed
- Epics table header changed from
Epic (OpenSpec change)toEpic; a Lane column is added. Epics are sorted by priority rank then lane rank in bothPROJECT.mdand the brief. - NOW line includes the lane tag.
rulesBlock(): “epics = proposals” replaced with “epics are lane-agnostic; OpenSpec is one lane (openspec | superpowers | claude-code | decision | external).”
Upgrade
Existing repos: after updating the plugin, run/pm:upgrade once. It will:- Refresh the CLAUDE.md rules block with lane-agnostic wording.
- Stamp explicit
lane: "openspec"on all pre-0.3.0 epics. - Record
pmVersion: "0.3.0"instate.jsonso the upgrade nudge stops appearing.
2026-06-01
Initial public release. Tracks OpenSpec proposals as epics, maintains an explicit detour stack, and enforces a reconcile gate so nothing is lost when development pivots or context is compacted.

