Skip to content

0.50.0 — the 0.43.0 review closed out; the archive gate reads archived work; the commit gate tests the index - #234

Merged
cfdude merged 139 commits into
mainfrom
dev
Sep 26, 2026
Merged

cfdude merged 139 commits into
mainfrom
dev

Conversation

@cfdude

@cfdude cfdude commented Sep 26, 2026

Copy link
Copy Markdown
Owner

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

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.json schema 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

…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
…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
…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)
…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
@cfdude
cfdude merged commit 32ccf8b into main Sep 26, 2026
12 checks passed
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant