Merged
Conversation
…re save; tick 0.49.0 task 7.6 0.49.0's docs sync saw one edit_page re-serialize the changelog page, mangling older entries; the sync shipped as a plain git PR (cfdude/pm-docs#55). Task 7.6 (docs-site Node claims) verified live. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
openspec update 1.13.0 -> 1.13.2 rewrote the six opsx commands and six openspec-* skills. Upstream changes only: a Project check preamble (root:null handling, never create openspec/ as a side effect), trigger phrases in skill descriptions, archive now counts tasks via openspec list --json totalTasks/completedTasks instead of reading tasks.md checkboxes, new-capability sync rules for missing main specs (ADDED-only, retire_capabilities), apply handles missingArtifacts when blocked, update-change drops /opsx:continue and /opsx:new references and moves all writes to its confirm step, explore hands off to /opsx:propose. git log shows every prior edit to these files was an openspec regeneration; no local edit was lost, so none was restored. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
…ctions from the 0.43.0 findings inventory Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
…est-split Tracked since 407e4a9 and byte-identical (diff -rq) to archive/2026-09-21-functional-assertion-test-split; openspec list reported the archived change as live at 47/48. Found by the 0.50.0 findings inventory; the class is #222. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
…0.0 pending a measurement Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
…e-defer in amendments[] Decision (a): a cross-release --member is recorded as an unmember amendment on the old release (via member, to <new>), not refused — the move is documented behaviour and the implicit-undefer precedent already records the same class. Decision (b): re-defer history lives in the existing amendments[] trail as op redefer with was; no nested history[], no MIGRATIONS entry. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
One plan for lesson-detect-matcher-hardening and gh-cfdude-pm-194. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
…ts evidence One plan for commit-gate-tests-working-tree-not-index and gh-cfdude-pm-219. Compares stash --keep-index, a checkout-index snapshot, a patch dance and an index worktree, with measured overheads, and picks the snapshot. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
…ions once
frameEpic read f.epic||f.epicId||f.id; push-detour writes pausedEpic, so every
detour event carried epic:null, DETOURS byEpic was always empty and
`activity --epic` dropped detours. Detour events are now derived by frame
identity (pausedEpic + pausedAt, multiset difference) rather than by depth,
and carry {epic: pausedEpic, detour: spawnedDetour, depth}. byEpic counts
pushes only (one interruption = one push; counting pops doubled it), and
readEvents' --epic filter also matches the `detour` field.
The fake-frame unit test ({epic:'e1'}) is replaced by one driven by real
push-detour/pop-detour verbs.
RED: red-task1.txt (real frames -> epic null/undefined; --epic d1 -> []).
Mutation: old keys -> real-frame test fails; pops counted -> {e1:2} fails;
detour clause dropped -> file-rung --epic test fails.
Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
Baseline measured in a hermetic TMPDIR: 436 leftover dirs per assertion-half run, 1,517 per functional-half run. Plan reuses temp-dir.mjs's removeAtExit. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
`release r2 --member e1` with e1 in r1 moved the pointer and left r1 with no
trace: `release show r1` read members (0), no amendment, nothing on stderr.
The move is kept (documented behaviour) and is now recorded on the OLD
release as {op: "unmember", epic, via: "member", to: "<new>"}, announced on
stderr naming both releases, and rendered by `release show` as "moved to
`<new>`". Same-release re-member writes nothing; a pointer to a missing
release is replaced with a stderr line instead of throwing.
RED: docs/superpowers/plans/2026-09-25-release-member-moves-silently/red-task1.txt
Mutations M1, M2: .../mutation-evidence.txt
Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
One change for handoff-demand-blind-spots (0.43.0 review A1/A2) and gh-cfdude-pm-222 (#222): - conductor-record MODIFIED "Outstanding work is a defined quantity": progress is the union of inline stories and the checkbox source, and an archived openspec change's checkbox source is its archived tasks.md for every epic, not only a backfilled one. - gate-integrity MODIFIED "The interactive archive verb accepts an epic that is already archived": drops the clause that let outstanding work read zero once the source had moved. - gate-integrity ADDED: a delivered epic whose archived spec deltas are absent from the main specs (read from git's index) is reported as a standing condition. Header presence only, and a later archived change discharges it. It is not an archive refusal, which would deadlock pm's own closeout (design D3). Measured 2026-09-25: 17 archived epics render 0/0 today, and 2 of them are delivered with a real task still open (53/54, 46/47). The spec-sync comparison finds 0 of 186 headers at HEAD, and replayed at 3256cc2^ it finds exactly 0.48.0's 4 lost ADDED headers. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
drop-detour shrinks the stack, so the depth comparison logged it as detour-pop (the 0.46.0 sweep's third instance). A removed frame's kind is now chosen by verb: pop-detour -> detour-pop, drop-detour -> detour-drop, any other verb -> detour-removed. The diff alone cannot tell them apart: set-active accepts a paused epic, after which a pop and a --no-reconcile drop make identical changes. The report counts drops (and removals by another verb, printed only when non-zero) separately; commands/activity.md's DETOURS row says so. RED: red-task2.txt (buried drop -> detour-pop; other verb -> detour-pop). Mutation: every removal mapped to detour-pop -> both new tests fail. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
…rd it tmpRepo, fixtureCache, fixturePluginRoot and addHierarchyWorktree (helpers.mjs) and the git-gateway fixture's three roots plus its linked worktree now go through removeAtExit() from temp-dir.mjs — 0.49.0's one mechanism, reused. Guard, id temp-dir-cleanup: - functional/: a nested node --test run in a hermetic TMPDIR over a probe that calls every directory-making helper; asserts exit 0, pass > 0, every probe path under the hermetic TMPDIR, and zero pm-* entries left after exit. - assert/ (twin): helpers' dirs are in scheduledForRemoval(); every mkdtempSync call site in scripts/test is scheduled on its line or enrolled in KNOWN with how it is removed (sites later tasks fix are marked pending); and every KNOWN entry still names a live site. RED: docs/superpowers/plans/2026-09-25-gh-cfdude-pm-224-evidence/red-1.txt (twin: 4 unenrolled helper sites, tmpRepo unscheduled; functional: 8 dirs left). Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
…ndoff-demand-blind-spots Resolves task 0.3 by re-key; 0.4 tracker refresh for #222 recorded (unchanged, 2026-09-25T03:54:58Z). Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
A lesson's detect: now either works or is rejected with a reason. The
classifier keeps rejected matchers apart from lessons that have no matcher.
Before this change a malformed matcher was discarded silently.
- checkDetect(raw) is a pure validator. It enforces the key allowlist
(C2: a typo'd key matched every tool call), string values (a tool array),
a tool the hook is wired to, a positive predicate, and tool/predicate
coherence. Each regex must compile, contain no control character (JSON
\b decodes to U+0008), and nest no unbounded quantifier (C1).
- classifyLessons(dir) returns {matchable, rejected, retrievalOnly}.
matchableLessons() is its projection, so the hook and any reporter read
the same verdict. The hook stays silent about rejects.
- Frontmatter is found after CRLF is normalised (C1).
- pathEndsWith reads notebook_path for NotebookEdit.
The output sweep now judges the new reason strings as not-output. They
are returned as data and nothing in the engine prints them. Any surface
that later prints one must escape it at the print site, where the sweep
will see it.
RED:docs/superpowers/plans/red-task2.txt (the new exports do not exist).
Mutation proofs: 11 of 11 mutants were killed. They cover CRLF, the key
allowlist, nesting, the control-character check, the positive predicate,
the tool allowlist, value types, coherence, notebook_path, absent vs
malformed, and the compile error.
Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
parity (pm-parity-, 6 per run), engine-resolution (pm-gh139-, 2 per run) and drift-script (pm-cert-record-/-covers-, removed inline so leaked only when an assertion failed) now wrap their mkdtempSync in removeAtExit(). The guard grows with them: their four pending KNOWN entries are gone from the twin, and the functional guard runs parity and engine-resolution in hermetic TMPDIRs and asserts nothing survives. RED: docs/superpowers/plans/2026-09-25-gh-cfdude-pm-224-evidence/red-2.txt (twin: 5 unenrolled sites; functional: 6 pm-parity-* and 2 pm-gh139-* left). Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
The hook ran the assertion half over the checkout, so a failing test staged with a passing copy left unstaged committed green while HEAD held the failure. It now exports the commit's index with `git checkout-index -a --prefix=` into a private $TMPDIR snapshot and runs the half there; the floor's `declared` reads the snapshot's bytes. It also captures GIT_INDEX_FILE before its env scrub and hands it to the three index readers (drift, the export, `declared`) but never to the runner: under `git commit -a` and `git commit <path>` git uses a different index and `.git/index` is stale, so the drift script and the floor were reading the wrong one. LOCKDIR is absolute (the cleanup now runs from the snapshot), and one EXIT cleanup, reached from INT/TERM/HUP, removes the snapshot, the output file and the lock. The hook writes neither the working tree nor the index. Fixtures IX-a..IX-g (functional) + the IX shape test (assert twin). RED: docs/superpowers/plans/2026-09-25-commit-gate-evidence/red-1.txt Mutation proofs: .../mutation-1.txt (7 mutants, all killed). Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
…ishes only when clean (#219) The recipe piped the combined four-bucket run through `grep -m1 '^ℹ tests '`: a run with failures still produced a count, the STOP fired only on no output or `tests 0`, and the output was discarded, which is why #219's `fail 1` could not be diagnosed. It now writes the whole run to `<git-common-dir>/pm-real-numbers/<UTC>.txt`, names the file, and prints PUBLISH only when the runner exited 0 with fail 0, cancelled 0 and tests > 0; anything else prints STOP. A failed run is recorded against #219 before a re-run. The logs' inverse (removal) is a named one-liner in the recipe. Guard: scripts/test/assert/release-checklist-recipe.test.mjs (file rung). RED: docs/superpowers/plans/2026-09-25-commit-gate-evidence/red-2.txt Decision block executed on clean/fail/cancelled/no-summary/zero logs and two mutants (fail clause dropped; pipe restored), both killed: .../recipe-decision-2.txt. The flaky test itself is still unidentified. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
…oval head-attachment (pm-head-, pm-nogit-), hermetic-git (pm-hermetic-, pm-fake-template-), state-write-verification (pm-persist-), commit-resolution (pm-optinject-) and emitted-invocations (pm-emitted-docs-, and pm-templates-, which was removed inline and so leaked only on failure) now wrap their mkdtempSync in removeAtExit(). The twin's pending KNOWN entries are all gone; the only remaining KNOWN leak is the conductor-33 tail. The functional guard now also runs these five files (commit-resolution and emitted-invocations name-filtered to the tests that reach the site). Each edited functional file's assertion twin carries a header note in the same commit, as drift check 3 requires. RED: docs/superpowers/plans/2026-09-25-gh-cfdude-pm-224-evidence/red-3.txt (twin: 7 unenrolled sites; functional: 17 dirs left across the five files). Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
…call Code review 0.43.0, C1. The lesson-advice hook runs before every Bash, Edit, Write and NotebookEdit call, and hooks.json gives it no timeout of its own. A detect of ^(a+)+$ therefore held each tool call until Claude Code's 60 s hook timeout killed it. - Every regex of one invocation now runs inside a single node:vm context under one shared deadline, REGEX_BUDGET_MS = 100. The vm timeout is the only built-in way to interrupt a backtracking regex. It was measured interrupting one on Node 20.19.4 and 26.10.0. - When the budget is spent, a regex counts as NOT matched. That holds for both commandMatches and commandLacks: a suppression half that could not finish never lets its lesson fire. An exhausted budget costs advice, never time. - The command's first line is capped at MATCH_TEXT_CAP = 4096 characters. - The static nesting check from the previous commit is a heuristic. ^(a|a)*$ nests nothing and still takes 6.7 s on 24 characters, and so does pm's own filter-at-read-time matcher on a crafted line. The budget is what makes the bound real. node:vm joins the engine's built-ins. It is not a dependency, and no test pins the import list. The timing tests use 25 characters, which takes about 13 s unguarded, and bounds of 2 s (unit) and 4 s (hook). That leaves a wide margin in both directions on a loaded machine. RED: docs/superpowers/plans/red-task3.txt (the hook took 2454 ms at 23 chars, and the new exports do not exist). Mutation proofs: 3 of 3 mutants were killed. They cover the vm budget, the input cap, and a timed-out commandLacks. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
…ems 1 and 7; changeset Assertion half after: 3 leftover dirs (436 before), all the conductor-33 pm-seg* tail. Functional half not re-measured (orchestrator load note); per-file guard runs leave 0. Mutation proofs: evidence/mutation-proofs.txt. Friction filed as #227. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
The lessons-index test now reports rejected matchers. It calls the engine's classifier, so it cannot disagree with what the hook fires on. It fails on the commit that writes a detect: that cannot work, and names the lesson and the reason. The closed INERT_PENDING_194 grandfather list and its companion test are gone. The README's 🔔 column is now held equal to classifyLessons().matchable. ADVISED_TOOLS is held equal to the lesson-advice matcher in hooks/hooks.json. The six bare-regex lessons were each judged on precision: - a-silent-noop-edit-reports-success now fires on an in-place sed -i. The .replace( half is source text, so it stays a habit. - stacked-background-commits-collide-on-the-lock now fires on a git commit backgrounded with &. run_in_background is a tool-input field, so it stays a habit. - cite-a-symbol-not-a-line-number now fires on a --reason, --notes or --description value that cites file.ext:N. - an-unused-active-pointer-turns-a-true-check-into-noise now fires on the engine command, conductor.mjs or "$ENGINE", that adds a release member or archives an epic. - second-resolution-timestamps-collide-on-fast-machines and a-fixture-reconstructed-from-live-data-dies-when-the-data-improves matched source text being written. No detect key can see that, so their matchers were removed and both are retrieval-only by design. Each converted matcher was probed. It fires on its intended command, and it stays silent on ls, on `echo sed -i`, on `git commit && …`, and on --notes that cite a symbol. RED: docs/superpowers/plans/red-task4.txt (the six rejects, with reasons). Mutation proofs: 3 of 3 mutants were killed. One reverts a lesson to a bare regex, one drops a README bell, and one drifts ADVISED_TOOLS from hooks.json. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
…s revision verify-state compared only state.json's mtime to the render stamp, so a verb that saves without rendering (set-activity-log, claim, platform recording) made it exit 1 claiming an undetected hand-edit. It now reads the stamp's stateRevision: revision ahead = engine save (exit 0, "PROJECT.md may be stale"); revision behind = rewound file (exit 1); equal revision with a newer mtime = hand-edit (exit 1). A stamp with no stateRevision keeps the mtime check. Docs that promised "reports a hand-edit" after a refused block write (commands/review-mode.md, commands/tracker.md) now say "stale". RED: scratchpad/wt-50/code-review-batch/red-verify-state.txt (file rung, conductor-07: engine-write test exits 1 with the hand-edit message; rewound revision test reports the generic mtime message). Mutation: disabling the equal-revision mtime branch fails "verify-state fails loudly when state.json is hand-edited after the last render". Item 1 (call-site sweep, rg 'verifyState|stateRevision|writeRenderStamp'): verifyState has one caller (conductor.mjs dispatch). The stamp's only writer is render.mjs writeRenderStamp, which already records stateRevision; its only reader is verifyState. The revision is advanced only by store writeRecord (file and memory stores). Inverse: none — verify-state is read-only. Limitation, documented in README: a hand-edit followed by an engine save before verify-state runs is invisible (the save advances the revision). Item 7: nothing new to route — the finding was already registered. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
User-facing documentation for lesson-detect-matcher-hardening and #194: - skills/lessons/SKILL.md, section detect: the rejection rules, the JSON doubled-backslash trap, CRLF, notebook_path, where a reject is reported (the hook stays silent; classifyLessons() and pm's per-commit index test report it), and the 100 ms / 4096-character regex budget. - README.md, lesson-advisor hook row: rejects are refused rather than guessed at, and the regex phase is bounded. - CLAUDE.md: node:vm joins the engine's built-in list. - .changesets/lesson-detect-matcher-hardening.md and .changesets/gh-cfdude-pm-194.md. - Plan: item-1 sibling regex sites and item-7 routing are recorded. #228 is filed for the consumer-facing report surface. Not done here: the Mintlify site (pm-plugin.dev) mirror of the lessons contract belongs to the release's doc-sync step. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
Re-running `--defer <epic>:<new reason>` replaced the deferral record outright,
so "depends on X landing" became "cut for scope" with no history. deferred[]
keeps its shape and holds the current reason; the replaced one is appended to
the release's amendments[] as {op: "redefer", reason, was, wasRecordedAt} and
named on stderr; `release show` renders both through the existing renderer.
An identical-reason re-run is now a no-op (recordedAt no longer refreshes).
RED: docs/superpowers/plans/2026-09-25-release-member-moves-silently/red-task2.txt
Mutations M3, M4: .../mutation-evidence.txt
Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
…med; snapshot drops the block (Gate 2 C1, C2, I3) C1 — a failure in the check no longer kills the hooks. briefing.mjs specSyncBlock() catches it and returns ONE line, `spec-sync check unavailable: <escaped reason>`, so `brief` (SessionStart) and the `render` verb exit 0 and emit everything else. integrity's runIntegrity() reports a check that throws as UNAVAILABLE with its reason, still runs every other check, prints no stack, and integrity() exits non-zero. gate-integrity's ADDED requirement states the degrade rule, with two new scenarios. C2 — exit 128 is no longer "no repository" on its own (git exits 128 for every fatal error: a corrupt .git/index made integrity print 0 findings and exit 0). indexFileContents() confirms a 128 with the existing `gitPath` operation (rev-parse --git-path index, locale-independent, reads no index): null only when that also fails 128 (or git is absent); otherwise the original failure is rethrown. I3 — snapshot() no longer passes specSync: .conductor/brief.txt is tracked in 14 of 24 fleet repos. design.md D6, tasks 5.2/6.1 wording and call-site-sweep-6.1.txt follow; the output-sweep judgment follows. Tests (functional files with their twins, edited in this commit): - functional/spec-sync-surfaces: a `git` shim on PATH failing `cat-file --batch` (exit 1) per surface — brief, render verb, integrity; a corrupt-index integrity case; snapshot's stdout and brief.txt carry no block. Twin assert/spec-sync-surfaces: the same through a double whose index read fails, and the snapshot source/brief.txt check. - functional/spec-sync-index: a corrupt index is rethrown. Twin assert/spec-sync-index: 128 is null only when gitPath also fails 128; a 128 with gitPath answering is rethrown. RED: red-gate2-C1-C2-I3.txt (11 fail against the pre-fix engine). Mutations: mutation-gate2-C1-C2.txt (re-throw instead of degrade fails all six C1 cases; 128-as-no-repository fails all three C2 cases). Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
…ve resolver (Gate 2 I4)
changeSpecRoot() took the FIRST stripped match in directory order (the
oldest), so a change archived twice was reviewed against its stale
specs. It now calls archivedChangeDir(changeId, <changes>/archive), the
same resolver isArchived(), archivedTasksPath() and spec-sync use; its
comment says so.
The 6.1 sweep only enumerated the NAMED resolvers, which is how this
site was missed. call-site-sweep-6.1.txt now records the widened
derivation (rg "archivedChanges\(|strippedChangeId\(") and classifies
every site it finds: this was the one per-id resolution; the rest are
set enumerations asking a different question.
RED: red-gate2-I4.txt. Mutation: mutation-gate2-I4.txt (the oldest-match
rule restored fails the case with 2026-08-01-twice).
Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
…nors) - I5: unit/spec-sync — a MODIFIED header missing from the main spec is reported absent (dropping `...delta.modified` passed every test before). - "; AND": assert/progress-union asserts the per-part remedies are joined by `; AND` and never `; or`. - Overflow pointer: assert/spec-sync-surfaces — six findings end in `(+1 more — see \`integrity\`)`, never PROJECT.md. - maxBuffer: assert/spec-sync-index pins indexBlobs' 256 MiB. - Refusal wording: gate-integrity's scenario and task 1.3 now read the engine's form, `2 task(s) outstanding (1/3 done)`; archived-progress accepts only that form. - `N/M items`: one sentence in the conductor-record delta, and a unit assertion that a two-part source renders `items`. - parseCatFileBatch's two pure cases moved from assert/spec-sync-index to the unit rung (unit/spec-sync). - commands/status.md: story counts come from the archived tasks.md once the change has moved, not only openspec/changes/<id>/tasks.md. Each new guard passed on arrival; mutation-gate2-I5-minors.txt proves each in a scratch copy (MODIFIED dropped; the overflow pointing at PROJECT.md; maxBuffer removed; `; or` in place of `; AND`). Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
…clusion is in the spec (confirmation review I, M) [I] runIntegrity() caught ANY check that threw, which was unspecified and turned a crashing check into an empty finding list that a `deepEqual(…, [])` test reads as a pass. The catch is narrowed to DEGRADABLE_CHECKS (delivered-epic-spec-deltas-absent); every other check that throws fails the run as before. gate-integrity's degrade rule says it covers this check only. Hardening: every findingsFor helper (functional/conductor-15, functional/conductor-18, functional/gate-artifact-evidence, assert/gate-artifact-evidence, assert/unknown-status-integrity) asserts `!c.unavailable`. Twins edited in this commit: assert/conductor-15 (a non-spec-sync check made to throw fails the run; DEGRADABLE_CHECKS is exactly spec-sync), unit/conductor-18 (no check reports UNAVAILABLE on an ordinary record), assert/gate-artifact-evidence (its helper asserts the check ran). [M] gate-integrity's ADDED requirement now states that the PreCompact snapshot does not carry the block, because .conductor/brief.txt is tracked in many repositories. RED: red-gate2-narrow.txt. Mutation: mutation-gate2-narrow.txt (the catch widened back to every check — the throwing non-spec-sync check is swallowed and the assert case fails). Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
…c, stories and tasks counted together, and delivered specs checked against the main specs (#222)
… archive older than the epic is not its archive Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
…om one validator STORABLE_EPIC_ID is now EPIC_ID_FORMAT exactly, and add-epic, add-many, sync's change and plan rungs, the archive backfill and pushEpic() all call it. `x|y`, `.hidden` and uppercase plan names are no longer registered under ids add-epic refuses; each skip is named, a plan whose lowercased stem is valid gets a runnable `add-epic --id <lower> --plan`, and sync's final line counts the skips instead of reading "synced". The 0.45.0 uppercase allowance in output-text-integrity is superseded (spec + functional 6.5/6.6a/6.6c and its assertion twin edited). Stored legacy ids still load and update. Task 2 of docs/superpowers/plans/2026-09-25-sync-registers-ids-add-epic-refuses.md RED: docs/superpowers/plans/2026-09-25-sync-registers-ids-add-epic-refuses-evidence/red-2.txt Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
An unrelated archive/2025-01-01-add-auth ended an ACTIVE epic add-auth by bare name: the drift heal archived it (outcome unknown), cleared the active pointer, and sync still printed "synced". The one resolver, archivedChangeDir(), now takes the epic RECORD and sets aside a dated directory more than a day before the epic's createdAt (the slack covers openspec's local date against UTC). Exempt: epics registered by the archive backfill. Undatable records: a live epic is never ended by a bare name; an ended one still locates its files. Every consumer passes the record (heal, active-pointer clear, set-active, resolveEpics, missing(), archived task counts, update-epic's regression check, integrity, spec-sync, cross-spec-review). sync names each set-aside directory every run and counts them in its final line. Fixtures that registered an epic and then archived under a fixed PAST date, or hand-wrote a live epic with no createdAt, described an impossible history: they now use archiveDay() or an earlier createdAt; assertions unchanged; functional twins edited. Task 3 of docs/superpowers/plans/2026-09-25-sync-registers-ids-add-epic-refuses.md RED: docs/superpowers/plans/2026-09-25-sync-registers-ids-add-epic-refuses-evidence/red-3.txt Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
Changeset fragment for both defects; the plan's required item 1 (every registration path, every writer of status archived, every resolver consumer, the two name-keyed held sets that still see a set-aside directory, and the unshipped override inverse) and item 7 (a process lesson on fixed-date fixtures, with README index rows). Mutation proof (copy of the tree, assert/sync-registration-ids.test.mjs): validator weakened to the 0.45.0 rule -> 3 fail; date rule disabled -> 2; backfill exemption dropped -> 1; undatable fallback lenient -> 1; no day of slack -> 2; heal clears the pointer by bare id -> 2; skip count dropped -> 1; set-aside line dropped -> 1; plan remedy dropped -> 1. All nine killed. Task 4 of docs/superpowers/plans/2026-09-25-sync-registers-ids-add-epic-refuses.md (no RED: documentation only) Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
…pped rule The lesson and the plan's item-7 line quoted counts from a run with the lenient fallback, which did not ship. Replaced with the strict-rule runs: 14 assertion-half and 42 functional failures over 14 test files plus the shared withArchivedChange fixture. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
…ate rule binds live epics only Review [I]: canBeArchiveOf set aside a dated archive older than createdAt even for an ALREADY-archived epic. pm's own 0.40.0 createdAt recovery dates an epic from the first commit that held it, which can postdate its archive (knowledge-store: bidirectional-sync-api and schema-source-reconciliation, createdAt 07-09 against archives 07-01 / 07-06): their 26/26 and 14/14 rendered as a dash, every sync advised renaming the directory, and they left spec-sync and cross-spec scope. The rule now decides only whether LIVE work is ended (heal, active pointer, set-active); an ended epic resolves by name, so the set-aside line cannot print for it. deliveredRegression asks about the record as it will be written. conductor-15 8.3 restored to its fixed 2026-08-05 date (assert + functional) as the regression guard. Hygiene: the pushEpic refusal moves to the unit rung; set-active's own isArchived(t) gets an isolated case; the bare-id scan widens (any receiver, ["id"], subcommands.mjs) and declares its limits. Mutation proof (copy of the tree): archived exemption dropped -> 3 fail (8.3, the ended-epic test, the resolver test); set-active isArchived(t) removed -> 1; set-active by t["id"] -> 2; pushEpic sink weakened -> 1. Task 5 of docs/superpowers/plans/2026-09-25-sync-registers-ids-add-epic-refuses.md RED: docs/superpowers/plans/2026-09-25-sync-registers-ids-add-epic-refuses-evidence/red-5.txt Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
…-blind-spots, not a main-spec edit Review [I] governance: 3c36c05 edited openspec/specs/output-text-integrity directly, bypassing the delta process. Reverted to its 7138f67 text. The change is now a REMOVED + ADDED pair in handoff-demand-blind-spots (a MODIFIED block cannot drop the superseded uppercase scenario; precedent: 2026-09-24-node-support-policy). The archive date rule joins that change's conductor-record delta: the one-resolver paragraph is amended, the rule is stated to bind LIVE epics only, and four scenarios cover the live collision, a same-day heal, the already-archived epic dated after its archive, and the undatable live epic. tasks.md and gate records untouched. openspec validate handoff-demand-blind-spots --strict: valid. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
…al counter-example Review [I]: the lesson claimed the fixed-date fixtures described histories that cannot happen. One could: conductor-15 8.3's already-archived epic dated after its archive is what pm's own 0.40.0 createdAt recovery writes in the field (knowledge-store), and editing that fixture hid the regression both review lenses found. Renamed to a-failing-fixture-may-be-the-rules-first-real-counterexample, with the measured cost and the classify-before-editing rule; README rows and the plan's item 7 updated. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
…c; the date rule's edges pinned Gate 1 / cross-spec minors. When a set-aside directory is really the epic's OWN archive (the epic was registered after its change was archived), the line now also prints the archive gate's dispositionInvocation, so the operator can end it deliberately. canBeArchiveOf is exported and unit-tested as a pure predicate: the one-day slack edge (day-1 matches, day-2 set aside), the backfill exemption on a REOPENED backfilled epic, ended records and undated directories. The undated-directory name match is kept on purpose, with the reason written beside it. Mutation proof (copy of the tree): no slack -> 4 fail; two days of slack -> 2; backfill exemption dropped -> 1; end-the-epic remedy dropped -> 1. Task 6 of docs/superpowers/plans/2026-09-25-sync-registers-ids-add-epic-refuses.md RED: docs/superpowers/plans/2026-09-25-sync-registers-ids-add-epic-refuses-evidence/red-8.txt Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
… and its edges become scenarios Gate 1 / cross-spec review of 72979f7, both Important: - MODIFIED 'Archive registration cannot produce duplicate epics': registration identity stays NAME-only; the date rule decides whether a directory is a live epic's archive, never whether its name is held; the flip scenario narrowed to a datable epic, plus a no-createdAt scenario. - MODIFIED 'sync reconciles the archive directory': a set-aside directory is held by name and reported every run, not registered. Minors: the resolver paragraph states the rule's PURPOSE (it also governs a live epic's progress source, missing-source warning and changeSpecRoot); why an undated directory still matches by name; the backfill exemption holds for a reopened epic; scenarios for the exact one-day slack, the reopened backfilled epic and the late-registered remedy line; gate-integrity's spec-sync scope no longer says 'whatever the stored status says'; output-text-integrity names the line requirement instead of 'above'. tasks.md and gate records untouched. openspec validate handoff-demand-blind-spots --strict: valid. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
…add-epic accepts; an older unrelated archive cannot end a live epic
…easurement (8.1), orchestrator tasks ticked; attribute sync-registers Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
…eltas to the main specs Lifecycle bookkeeping — not attributed. openspec/ staged whole (the archive rewrites openspec/specs too). Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
This was referenced Sep 26, 2026
cfdude
added a commit
to cfdude/pm-docs
that referenced
this pull request
Sep 26, 2026
… command pages (#56) Mirrors pm 0.50.0 (cfdude/pm#234, 32ccf8b): changelog entry; Real Numbers 66 releases / 2,775 tests / 21,357 engine LOC / 0 deps; new verify-state, verify-worktrees and verify-specs pages with nav entries; status, epic, sync, tracker, triage, changelog, activity, gate-guard, review-mode, hierarchy command pages; state-and-project, daily-workflow, external-trackers and multi-agent-hierarchy. Claude-Session: https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
0.50.0 closes the open findings from the 0.43.0 code review, tightens the archive gate, and hardens the per-commit gate.
What changed
tasks.mdthrough one resolver, and stories and tasks now count together. A new integrity check,delivered-epic-spec-deltas-absent, reads git's index and would have caught 0.48.0's lost main-spec sync. It caught this release's own archive untilopenspec/was staged. It degrades rather than crashing the SessionStart briefing (pm's archive gate does not notice when an archived change's spec deltas never reached openspec/specs/ #222).commit -aandcommit -- <path>. Drift now judges what is staged and runs from the snapshot. The Real Numbers recipe publishes only a clean run (A combined-invocation bucket run reported fail 1 non-reproducibly — the Real Numbers recipe needs its output captured #219).add-manynow persists each input or refuses it by name.add-epicaccepts, and names every skip. An unrelated archive dated before a live epic'screatedAtcan no longer end that epic.detect:either works or is rejected with a reason. Each regex runs under a time budget and cannot stall a tool call (A malformed lessondetect:is discarded silently — half of pm's own matchers are inert and nothing says so #194).Gates: Gate 1, the cross-spec review (5 specs) and Gate 2 are recorded for the openspec change. Every superpowers or claude-code branch passed a two-lens branch review plus a confirmation before merging. Fleet-wide resolver scan: 0 unintended differences across 28 repos and 1,167 epics.
Suite: assertion half 1523/1523, functional 1227/1227. No
state.jsonschema change and no migration. Requires/reload-plugins. The managed rules block changed (sync dedup step), so the fleet pass rewrites each repo's CLAUDE.md.https://claude.ai/code/session_01BoqzgrFwRC6QTUKuBkw8kM