Skip to content

0.24.0: platform-aware rules block, auto-detour fix (#65, #68), and a 2.6x faster test suite - #72

Merged
cfdude merged 30 commits into
mainfrom
dev
Jul 30, 2026
Merged

0.24.0: platform-aware rules block, auto-detour fix (#65, #68), and a 2.6x faster test suite#72
cfdude merged 30 commits into
mainfrom
dev

Conversation

@cfdude

@cfdude cfdude commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Ships 0.24.0 plus two pieces of groundwork. Three separable efforts, one PR because they interlock: the test split changed the command every later commit runs, and the platform work depends on it.

1. Auto-detour false positives — closes #65 and #68

PostToolUse fires when the Bash tool returns, which is not the same as "a commit landed in this repo." Three divergences, each writing a detours.log line attributed to this repo's stale HEAD:

Trigger Issue
pre-commit rejected the commit — HEAD never advanced #65
commit backgrounded, still running — HEAD not advanced yet #68
commit landed in a different repo (paired repo, submodule, git -C) #65

All three reduce to one question: does HEAD hold the commit whose subject we parsed? Worth noting an exit-code check would have closed #65 but not #68 — a backgrounded commit has no exit code yet. That's why two independently-reported issues share one fix.

#68 was filed from this session and #65 from another; #68 was closed as a duplicate since #65 is the superset (it also carries a repo-scope defect).

2. Test suite: ~118s → ~46s

Measured, not guessed: one engine spawn costs ~73ms and the suite makes 676 of them — ~49s of pure node startup, serialized in a single 3,202-line file on a 16-core machine. node --test parallelizes across files only (proven with a 4-file × 3s probe finishing in 3.3s, not 12s).

Split verbatim by a script that refuses to emit unless every test is accounted for by name-set equality (255 → 255). The file already exported 8 helpers that nothing imported — it was built to be split and never was.

Tuning notes: 18 files was worse than 11 (more runner startup, no gain), and --test-concurrency=16 changed nothing — the residual gap to the 21s floor is tmpdir/spawn contention. Rejected the larger win of in-process testing: it would stop exercising the real CLI contract, which is exactly where this PR's bugs live.

New guard: the glob makes a partial-suite pass possible in a way one file never did. .githooks/pre-commit now cross-checks the ran count against grep -Hc '^test(' and aborts on a drop — self-maintaining, no constant to bump.

3. Platform-aware rules block (0.24.0)

pm never runs on its own — a host agent triggers it through a hook whose command string pm itself authored. So the platform is declared via --platform <id>, not detected. No markers, no filesystem archaeology.

The finding that reshaped the design: Hermes resolves project context through a first-match-wins chain — HERMES.mdAGENTS.mdCLAUDE.md.cursorrules. So Hermes does read CLAUDE.md, but only when the first two are absent. In a repo already carrying an AGENTS.md, pm's rules block becomes silently invisible. Writing to a file the platform can read isn't enough — it must win that platform's chain.

Platform Chain (first existing wins) Command form
claude-code CLAUDE.md /pm:status
hermes HERMES.mdAGENTS.mdCLAUDE.md /pm:status
codex AGENTS.md /pm-status

Every value verified against the installed CLIs, not documentation. Hermes preserves : in plugin command names and silently skips a command colliding with a built-in — and it ships a built-in status, so the pm: namespace prevents real loss, not just ugliness. Codex derives command names from prompt-file stems, so it's flat.

The block body stays a single platform-neutral source of text; only command strings vary. Asserted by a test that normalises the command form and requires the three platforms' blocks to be otherwise identical.

Four defects caught before merge

Defect Impact
Marker wording change broke block detection every existing repo would grow a second rules block on upgrade (verified: 1 in, 2 out)
resolveAndRecordPlatform persisted unconditionally write-rules created state in an uninitialized repo, ending pm's dormancy and activating every hook
The HEAD guard suppressed every commit with a body %s is first-line-only while a -m capture spans newlines — the release headlined "fix commit-nudge" had turned it off for the common case, on 24/24 commits here
A README claim about /pm:upgrade specifically false; disproved by running the engine

The third is the one worth reading. Five task reviews missed it because every positive control used a single-line -m; the suite had no multi-line or heredoc case at all. It also didn't show up live, because hooks run the installed engine, not the working tree.

Test rigour

Mutation testing found 4 of 5 new guard tests were vacuousgitRepo()'s baseline commit touches 4 files and the rule rejects >3, so they passed on the file-count rule and never reached the guard. Four tests that read as coverage measured nothing. Every guard added here is now mutation-tested, and a nudgeAndReadLog helper makes absence assertions non-vacuous by construction (detourLog() returns "" for a missing file, so a bare doesNotMatch also passes when the hook never ran).

Verification

  • 282/282 passing, exit code 0 (checked directly — a pipe into rg returns rg's status and hides failures).
  • Syntax-checked exactly as CI does.
  • Migration verified on real history: this repo was itself an untouched 0.23.1 install with an old-wording marker. 1 block → 1 block, marker migrated, 0.23.1 → 0.24.0, platform stamped. Idempotent on a second run.

Known gaps, filed not hidden

https://claude.ai/code/session_01WBqyLnBbYEi5hp2noc3cMC

cfdude added 29 commits July 30, 2026 12:26
Every headless `claude` session the harness spawns inherited the user's
environment, so the user-scope honcho memory plugin loaded and wrote
machine-generated eval text into a personal memory workspace -- content with no
value to a human reader.

HONCHO_ENABLED=false is that plugin's hard off switch (config.ts sets
enabled=false on exactly this string, and saveConfig explicitly preserves the
on-disk value while the override is active, so it is never persisted). Passed
explicitly rather than inherited.

Set in one _child_env() helper rather than at the call site: runners.py's
contract is that adding a platform means adding a function plus a RUNNERS entry,
and the hermes/codex runners are queued -- this way they inherit it by
construction. Nested processes inherit it normally.

Two guards: one asserts PATH survives (passing env= replaces the environment
wholesale, so a bare dict would break `claude`), and one iterates the whole
RUNNERS registry so a runner added later cannot silently reintroduce the leak.

Claude-Session: https://claude.ai/code/session_01WBqyLnBbYEi5hp2noc3cMC
- init-detects-foreign-agent-instruction-files: pm init should read a
  pre-existing AGENTS.md / sol.md left by a prior agent before initializing
- rules-block-cli-fallback: deferred as a planned epic rather than a code TODO,
  because a comment resurfaces to nobody while a planned epic shows in PROJECT.md

Also re-sequences edd-observe-hardcodes-claude-md as depends-on the rules-block
epic: observe.py correctly reflects what the engine writes, and the engine
hardcodes CLAUDE.md, so parameterizing the observer first would abstract over a
filename nothing produces.

Claude-Session: https://claude.ai/code/session_01WBqyLnBbYEi5hp2noc3cMC
Key finding that reshaped it: Hermes resolves project context through a
first-match-wins chain (HERMES.md > AGENTS.md > CLAUDE.md > .cursorrules), so it
DOES read CLAUDE.md -- but only when the first two are absent. In a repo with a
pre-existing AGENTS.md, pm's rules block becomes silently invisible on Hermes.
Writing a file the platform can read is not enough; it must win the chain.

Also records the verified command facts: ':' survives Hermes' plugin command
registration and lookup, and a name colliding with a built-in is SILENTLY
skipped -- so the pm: namespace prevents real loss, not just ugliness. Codex is
flat/hyphenated, evidenced by the maintainer's own opsx-*.md prompt files.

Claude-Session: https://claude.ai/code/session_01WBqyLnBbYEi5hp2noc3cMC
Self-review caught an unresolved either/or in section 3. Decided: write the head
of the detected platform's chain and report the choice. Refusing was rejected --
it leaves pm non-functional in the repo, trading a silent failure for a loud one
rather than fixing it. Also clarified the rule is per-platform: Claude Code has
no chain, so a stray AGENTS.md is irrelevant there.

Claude-Session: https://claude.ai/code/session_01WBqyLnBbYEi5hp2noc3cMC
Q1 reframed the mechanism entirely. pm never runs on its own -- every engine
invocation is triggered by a host agent -- and pm already authors a separate hook
config per platform. So the host declares itself via --platform in the hook
command pm wrote, and there is no detection at all: no markers, no filesystem
archaeology, no precedence to get wrong.

Env sniffing is rejected to a fallback rung: CLAUDECODE=1 is a reliable Claude
Code marker (verified live), but Hermes' shell-hook runner calls subprocess.run
with no env= (agent/shell_hooks.py:463), so hooks inherit whatever launched it and
carry no guaranteed HERMES_* marker -- it could never separate Hermes from Codex.

Q2: active platform recorded in state.json, written and read once per session
start, with a MIGRATIONS entry. Enables the platform-switch case.
Q3: body stays neutral; only command strings vary.

Also renumbered the Design subsections after the insert collided on 3.

Claude-Session: https://claude.ai/code/session_01WBqyLnBbYEi5hp2noc3cMC
Five TDD tasks. Written against the real tree, and verifying it caught four things
that would each have sent an implementer down a dead end:

- the dispatch table calls every subcommand with NO arguments, and init/upgrade/
  writeRules never read argv -- so --platform on init or write-rules would have
  been a silent no-op and the tests would fail in the confusing direction
- platform.mjs must NOT import parseFlags from add-epic.mjs: that pulls it into the
  render/briefing/active-pointer circular cluster, and since rules.mjs imports
  platform.mjs a future edge back would loop around the rules writer. Kept a leaf
  with a three-line flag scan instead
- the obvious `rg writeRules\(\)` verification can never return empty (two
  explanatory comments match), so it is statement-anchored instead
- test run() spreads process.env, so CLAUDECODE=1 leaks in; default-rung tests must
  blank it or they pass via the wrong rung

Claude-Session: https://claude.ai/code/session_01WBqyLnBbYEi5hp2noc3cMC
…r pair

/pm:sync reproduced #69 live three times, including a duplicate of the ACTIVE
epic -- removed all three phantoms. Registered 64/65/66/69/70 as epics.

#68 (mine, backgrounded commit logs the previous SHA) closed as a duplicate of
#65, which is the superset: same missing-success-check defect plus a second
repo-scope defect. Recorded on #65 that the HEAD-advanced guard covers both
triggers while an exit-code check would only cover one.

Claude-Session: https://claude.ai/code/session_01WBqyLnBbYEi5hp2noc3cMC
#68)

PostToolUse fires when the Bash tool RETURNS, which is not the same as "a commit
landed in this repo". Three observed divergences each wrote a false detours.log
line attributed to this repo's STALE HEAD:

  * pre-commit rejected the commit -> HEAD never advanced          (#65 bug 1)
  * the commit was backgrounded, still running -> not advanced yet (#68)
  * the commit landed in ANOTHER repo (paired repo, submodule, git -C) -> our
    HEAD is untouched, but gitShortSha()/headChangedFiles() both read ROOT and
    so attribute that commit to this repo                          (#65 bug 2)

All three reduce to one question: does HEAD in ROOT hold the commit we parsed?
Comparing SHAs would need a stored baseline; the subject is already in hand.
Note an exit-code check would NOT cover the backgrounded case -- there is no
exit code yet -- which is why the HEAD comparison closes both issues rather
than just one.

Three-state on purpose. Only CONTRADICTED (subject parsed, git works, HEAD
disagrees) goes silent. UNVERIFIABLE (no -m to parse, or git unusable here)
keeps the previous behaviour, because the archived-epic self-heal must still run
in a repo with no git -- the existing test at conductor.test.mjs:996 has no git
repo at all, so a two-state guard would have broken it.

Claude-Session: https://claude.ai/code/session_01WBqyLnBbYEi5hp2noc3cMC
CHANGELOG [Unreleased] entry for the #65/#68 fix, including why an exit-code
check would not have covered both issues and why the guard is three-state.

Files test-suite-perf-and-scaffolding (P1) with the measurements behind it:
73ms per engine spawn x 676 run() call sites = ~49s of pure node startup, all in
ONE file, on 16 cores. node parallelizes across FILES only -- proven with a
4-file x 3s probe that finished in 3.3s rather than 12s. The file already exports
8 helpers that nothing imports, so it was built to be split and never was.

Also records a silent-pass hole found while reviewing my own new tests:
detourLog() returns "" for a missing file, so assert.doesNotMatch(detourLog(...))
passes even when commit-nudge never ran -- and it exits 0 on every early return,
so run() does not throw either. Same class as the INDETERMINATE-exits-0 bug the
EDD review caught.

Claude-Session: https://claude.ai/code/session_01WBqyLnBbYEi5hp2noc3cMC
…ad of removal

I reported remove-epic as the workaround for the three sync phantoms. It is not:
the identical three came back on the very next sync about an hour later. Another
session independently saw the same in agent-dm across a two-day gap, so this is
not repo-specific and removal buys only until the next sync.

Found a workaround that does stick, with no code change: set the phantom to
--status archived rather than removing it. The id then still exists, so sync's
own "id already exists" guard fires. Verified durable -- repeated syncs add 0.
That is effectively the syncIgnore tombstone another session proposed, reachable
today, since the dedup keys on id existence rather than status.

Applied to all three phantoms here. Also recorded that the tombstone fix should
lead the plan-field-matching fix, because matching is blocked by #66 (update-epic
has no --plan, so the field cannot be populated) while the tombstone is not.

Claude-Session: https://claude.ai/code/session_01WBqyLnBbYEi5hp2noc3cMC
…~46s)

MEASURED, not guessed: one engine spawn costs ~73ms and the suite makes 676 of
them, so ~49s was pure `node` startup -- serialized in a single 3202-line file on
a 16-core machine. node --test parallelizes across FILES only (proven with a
4-file x 3s probe finishing in 3.3s, not 12s).

Split mechanically by a script that refuses to emit unless every test is
accounted for; verified 255 source tests -> 255 emitted, by name-set equality.
The file already exported 8 helpers that nothing imported, so it was built to be
split and never was. helpers.mjs now carries all 21 helpers plus the consts --
FIXTURE_CHANGELOG was a plain `const` and had to be exported or tests
referencing it threw ReferenceError.

Tuning: 11 files at ~46s. 18 files was WORSE (46s) -- more runner startup for no
gain. The floor is the slowest single file (21s); the remaining gap is
tmpdir/process-spawn contention, NOT concurrency (--test-concurrency=16 was
identical). Rejected the bigger win of in-process testing: it would stop
exercising the real CLI contract, and that contract is where today's bugs live.

NEW SILENT-LOSS GUARD. The glob makes a partial-suite pass possible in a way the
single file never did: if a file stops matching, the suite still passes on a
subset. The hook now cross-checks the ran count against `grep -c '^test('` across
the files and aborts on a drop -- self-maintaining, no constant to bump. Covered
by its own test, and the hook fixture was moved to the real scripts/test layout
and de-indented so it actually exercises the guard.

Also documented a landmine: `node --test scripts/test` (a directory) does NOT
work on Node 26 -- it treats the arg as a module, dies MODULE_NOT_FOUND and
reports "1 test". It exits nonzero so it cannot pass silently, but the hook says
plainly not to "simplify" the glob into a directory.

256/256 passing, exit code 0.

Claude-Session: https://claude.ai/code/session_01WBqyLnBbYEi5hp2noc3cMC
detourLog() returns "" for a missing file, so assert.doesNotMatch(detourLog(cwd),
/AUTO-DETOUR/) also passes when the hook never ran at all -- and commit-nudge
exits 0 on every early return, so run() does not throw either. The assertion
could not distinguish "the guard fired" from "nothing happened."

MUTATION TEST, which is the only reason this got caught: disabling the guard left
4 of the 5 guard tests still PASSING. Root cause -- gitRepo()'s baseline commit
touches FOUR files (.conductor/render-stamp.json, .conductor/state.json,
CLAUDE.md, PROJECT.md) and looksLikeUnloggedMinimalDetour() rejects anything over
three. Every negative assertion was passing on the file-count rule, never
reaching the guard at all. Four tests that looked like coverage were measuring
nothing.

Fix: each guard test now lands a small, differently-subjected commit FIRST so
HEAD is genuinely auto-log-eligible, then asserts suppression, then lands the real
subject and asserts the entry appears. Re-mutated: 4 of 5 now fail with the guard
off, and the 5th is the positive-control test that must pass either way.

Also split the self-heal test into a pair rather than converting it. The original
has no git repo, which makes it the ONLY coverage of the unverifiable branch --
and it is what caught the two-state guard bug in the first place, so deleting that
coverage to "fix" it would have been backwards. Renamed to say what it covers and
added a verified-path twin with a real repo and a landed commit.

257/257 passing, exit code 0.

Claude-Session: https://claude.ai/code/session_01WBqyLnBbYEi5hp2noc3cMC
Audit of all 61 doesNotMatch assertions for the shape found in my own gh#65
tests. Results:

  * projectMd()/claudeMd() THROW on a missing file, so assertions reading through
    them cannot pass vacuously. No change needed.
  * detourLog() returns "" -- 4 pre-existing bare assertions in conductor-08
    ("does not auto-log a large commit", "...without a fix/chore prefix",
    "...naming the active epic", "...pm's own state files") could each pass if the
    hook never ran at all.
  * parseBrief() returns "" when brief output is empty, so 3 assertions carry the
    same lower-severity risk. Recorded on the epic, not fixed here.

Added nudgeAndReadLog(cwd, command): runs commit-nudge, asserts it emitted its
PostToolUse context payload (which it does on every path where it ran to
completion), and only then reads the log. That pins the difference between "the
rule under test rejected this commit" and "nothing happened" -- commit-nudge exits
0 on every early return, so run() never throws and the bare assertion could not
tell them apart.

MUTATION-TESTED the helper itself rather than assuming: with the hook forced to
early-return before emitting, 7 of 20 tests in that file now fail, including all
4 that previously would have passed silently.

Deliberately NOT used for the gh#65/gh#68 suppression tests -- those legitimately
emit nothing, and prove non-vacuity via assertSuppressedThenLands instead.

257/257 passing.

Claude-Session: https://claude.ai/code/session_01WBqyLnBbYEi5hp2noc3cMC
Contributor-facing: the test command changed to node --test scripts/test/*.test.mjs,
and the pre-commit hook gained a ran-vs-declared count guard. Records why
in-process testing was rejected and the directory-argument landmine.

Claude-Session: https://claude.ai/code/session_01WBqyLnBbYEi5hp2noc3cMC
The plan was written against scripts/conductor.test.mjs, which no longer exists --
it would have sent every implementer at a deleted file. Retargeted to
scripts/test/platform.test.mjs (a new capability gets its own file, which also
keeps the parallel split balanced), refreshed the baseline from 250 to 257 tests,
and fixed the helpers.mjs reference in the CLAUDECODE isolation hazard note.

Claude-Session: https://claude.ai/code/session_01WBqyLnBbYEi5hp2noc3cMC
…-platform command form

Combines Task 1 (platform constants + declared-platform resolution) and Task 2
(per-platform command strings in the rules block) into one commit: Task 1 alone
has no independently-green state, since rulesBlock() doesn't accept a platform
until Task 2 lands, so its test can't turn green on its own.

pm never runs on its own -- a host agent always triggers it through a hook whose
command string pm authored -- so the platform is declared via --platform rather
than detected. Resolution ends in a hard claude-code default because resolving to
nothing would write no rules block at all: pm appearing installed while
contributing nothing.

The rules block body stays a single platform-neutral source of text; only the
slash-command form varies via the new pmCmd(platform, name) helper. The pm:
namespace is retained wherever supported because Hermes SILENTLY skips a plugin
command that collides with a built-in, and it ships a built-in `status` -- so a
bare /status would be dropped with no signal.

Also fixes a site the brief's "14 lines in rules.mjs" enumeration missed:
RULES_BEGIN (constants.mjs) hardcoded "/pm:init" and leaked into every rendered
block regardless of platform. RULES_BEGIN/RULES_END are the literal anchors
writeRules() uses to detect and replace an existing block, so they must stay one
invariant string across all platforms -- the fix drops the platform-specific
command reference from the anchor comment entirely rather than making the anchor
itself platform-dependent, which would have broken idempotent block replacement
for non-claude-code platforms.
…ll marker

SHIPPING-BREAKING REGRESSION, caught before push. Task 2 made RULES_BEGIN's
parenthetical platform-neutral -- "(managed by /pm:init …)" became
"(managed by pm …)" -- which was the right call, since a platform-dependent
anchor would make a block written under one platform invisible to another.

But detection keyed on the FULL decorated string, so a block written by any
earlier pm version no longer matched, and writeRules() fell through to its APPEND
branch. Every existing repo would have grown a SECOND rules block on the next
/pm:upgrade. Verified live before the fix:

    blocks before: 1
    conductor: appended rules block to CLAUDE.md
    blocks AFTER write-rules: 2

Fix: detect via RULES_BEGIN_PREFIX ("<!-- BEGIN pm-conductor rules"), and build
the replace regex from it too. An old block is now found, replaced in place, and
silently upgraded to the current wording -- verified: 1 block in, 1 block out,
wording migrated from "/pm:init" to "pm". Decoration is now free to change; the
anchor is not.

Regression test added and MUTATION-TESTED: reverting detection to the full string
makes it fail, so it genuinely pins the behaviour rather than decorating it.

264/264 passing.

Claude-Session: https://claude.ai/code/session_01WBqyLnBbYEi5hp2noc3cMC
Hermes resolves project context first-match-wins over HERMES.md > AGENTS.md >
CLAUDE.md. Writing CLAUDE.md in a repo that already has an AGENTS.md means the
block is SILENTLY invisible -- no error, just an agent with no conductor rules.
writeRules now targets the first existing file in the platform's chain, and
reports which file and platform it chose.

Also wires --platform through the init/write-rules CLI dispatch (conductor.mjs)
and init()'s writeRules() call (subcommands.mjs) -- required for the platform
to actually reach writeRules() from those entry points, beyond what the task
brief's file list named.
Written and read once per session start, so the choice is auditable in the state
of record and a platform SWITCH is detectable -- the project changed hands and the
new host's artifacts may need recreating. Additive 0.24.0 migration defaults
existing repos to claude-code.
Second regression caught before push, same family as the marker one.

resolveAndRecordPlatform() persists the resolved platform, but loadState() returns
defaultState() for a MISSING file rather than null. So recordPlatform() always
reports a change on a fresh repo, and the unguarded saveState() CREATED
.conductor/state.json in a project that never ran /pm:init.

That silently ends pm's dormancy, which is a documented design property: once
state.json exists, isInitialized() is true and every hook activates. Verified live
-- `write-rules` in an empty directory produced a state file, after which
commit-nudge started firing in a project that had never opted in.

Fix: persist only when isInitialized(). Re-verified live: an empty directory now
stays empty of conductor state.

Three regression tests added -- write-rules dormancy, commit-nudge dormancy, and a
positive control proving an INITIALIZED repo still records a platform switch (so
the guard cannot fail closed). Mutation-tested: removing the guard fails exactly
the write-rules test, while the other two are a separate property and the control,
which must pass either way.

275/275 passing.

Claude-Session: https://claude.ai/code/session_01WBqyLnBbYEi5hp2noc3cMC
Closes the loop: without this the declared-platform mechanism exists but nothing
exercises it, and every real session would fall through to the env/default rung.

The test's own regex (JSON.stringify(hooks).match(/conductor\.mjs" .../)) undercounted
against the real file: JSON.stringify re-escapes embedded quotes as \", so the pattern
needed the backslash accounted for -- verified live by dropping to 0 matches before the
fix and confirmed the corrected version actually detects a missing flag (vacuity check:
removed --platform from gate-guard, suite went red with an explicit assertion message,
restored it, green again).

Docs: README.md (Quick Start, Supported Platforms table, /pm:init and /pm:upgrade command
docs, project-structure tree), commands/upgrade.md (platform-aware rules target + the
pre-0.24.0 migration stamp), skills/conductor/SKILL.md (write-rules and the installed-rules
description). Mintlify site (pm-plugin.dev) intentionally left out of scope for this task.
Task 5 (hooks declare --platform, docs updated) was the final task in the
2026-07-29-platform-aware-rules-block plan implementing this epic. All five
tasks are done and committed; mark the epic archived and clear active.
276 tests (was 250 -- the count I corrected earlier this session is already stale),
38 CHANGELOG releases, ~2,900 engine lines across 26 lib modules. Also notes the
suite is split across 11 files so it runs in parallel, since that is the visible
consequence of the perf work.

Claude-Session: https://claude.ai/code/session_01WBqyLnBbYEi5hp2noc3cMC
…fixes)

This repo was itself an untouched 0.23.1 install with an old-wording marker, so
running the real upgrade here is the strongest available end-to-end check of the
two regressions caught during this epic:

  rules blocks in CLAUDE.md: 1 -> 1        (no duplicate block)
  marker wording: "managed by /pm:init" -> "managed by pm"   (upgraded in place)
  pmVersion: 0.23.1 -> 0.24.0
  platform: (absent) -> claude-code        (migration stamped it)
  command form: /pm:status, zero hyphenated forms

Claude-Session: https://claude.ai/code/session_01WBqyLnBbYEi5hp2noc3cMC
…ge BODY

CRITICAL, caught by the whole-branch review. `git log -1 --format=%s` returns only
the FIRST line, but the `-m` capture uses [^"]* which spans newlines and swallows
the entire message. Comparing those directly can never match for a commit with a
body -- and this repo mandates one (the Claude-Session footer), so the guard
returned early on 24 of 24 commits on this branch.

Silently lost on every one: the DETOUR-COMMIT trail during a detour, AUTO-DETOUR
detection, the archived-epic self-heal (so /pm:next stops advancing after an
archive), the re-render, and the nudge itself. The release whose headline is "fix
commit-nudge" had turned commit-nudge off for the common case. Verified live: a
genuinely landed multi-line commit produced no output and no log entry.

Two-part fix, both halves needed:
  * compare first line with first line, trimmed (as %s is)
  * treat a shell-assembled message -- `-m "$(...)"`, a heredoc, `-m "$MSG"` -- as
    UNVERIFIABLE, since the command string holds the shell SOURCE, not the text git
    received. "Cannot tell" must not be reported as "does not match".

WHY FIVE TASK REVIEWS MISSED IT: every positive control used a single-line -m; the
suite had no multi-line or heredoc case. Added four, and mutation-tested both
halves -- reverting the first-line split fails 2 tests, dropping the shell-source
escape fails 1.

Also from the same review:
  * .githooks/pre-commit: `grep -c` omits the filename: prefix for a SINGLE file, so
    awk -F: summed a bare number and got 0 -- the ran-vs-declared guard went blind in
    exactly the case it exists to catch. Now `grep -Hc`, with its assertion in
    conductor-09 updated to require the -H rather than any grep.
  * README overstated that the declared platform is recorded in state.json; the four
    read-only hooks pass --platform through but never persist it.

280/280 passing.

Claude-Session: https://claude.ai/code/session_01WBqyLnBbYEi5hp2noc3cMC
…mmetrically

Both from the whole-branch review.

The CLAUDECODE rung was dead code -- both branches returned "claude-code", so it
could never change an outcome, and the test that claimed to distinguish "the env
rung" from "the terminal default" could not fail for the reason it stated. Removed,
with a note that a future env-based rung must return something DIFFERENT rather
than being a no-op that reads like coverage.

resolvePlatform now ignores an unrecognised flag value symmetrically with how it
already ignored an unrecognised recorded value. Callers still assertKnownPlatform()
first so a typo fails loudly; this is the second line of defence so a caller that
forgets cannot leak a garbage id into rulesTarget()/pmCmd(), both of which would
fall back silently. New test covers a hand-edited/future-version state.json.

Also files the review's two follow-ups rather than leaving them in a transcript:
autodetour-parser-covers-am-and-F (P2 -- #65 is only closed for -m; -am still
writes a false DETOUR-COMMIT) and platform-switch-orphans-old-rules-block (P2,
blocks hermes -- a switch leaves the old platform's block live but frozen).

281/281 passing.

Claude-Session: https://claude.ai/code/session_01WBqyLnBbYEi5hp2noc3cMC
The id was rejected on the first attempt (uppercase F violates
^[a-z0-9][a-z0-9._-]$) -- refiled lowercase.

Claude-Session: https://claude.ai/code/session_01WBqyLnBbYEi5hp2noc3cMC
…r fix

Shipping 'we fixed commit-nudge' without noting that %s is first-line-only while a
-m capture spans newlines would be an incomplete claim -- that mismatch is what
silently disabled the hook for every commit with a body.

Claude-Session: https://claude.ai/code/session_01WBqyLnBbYEi5hp2noc3cMC
Scoped re-review of the C1 fix round. Four of five findings were closed; two
things it caught here:

MY README "FIX" WAS WORSE THAN THE OVER-CLAIM IT REPLACED. I wrote that the
declared platform is recorded by "/pm:init, /pm:upgrade, and write-rules".
Disproved by running the engine: `upgrade --platform codex` leaves platform =
claude-code. upgrade never reads the flag -- its 0.24.0 migration stamps
claude-code on repos predating the field, which is not "adopting a declared
value". A vague over-claim became a specific false promise to exactly the
Codex/Hermes user the paragraph was written for. Corrected.

A NEW C1 TEST WAS MEASURING THE WRONG RUNG. "treats a shell-assembled message as
UNVERIFIABLE" used `-F -`, which has no -m at all, so it reached the unverifiable
rung via the EMPTY-SUBJECT branch and never touched shellBuilt. Proof: the
shellBuilt mutation failed only the $(...) test. Renamed it for what it actually
covers and added a real heredoc-in--m case; the mutation now fails 2, not 1.

Also: platform.mjs's docstring still listed the CLAUDECODE rung 14 lines above the
note explaining it was removed. And documented that shellBuilt is deliberately
broad -- a literal ${VAR} in a real subject also lands on the unverifiable rung,
so gh#65 can still false-positive for that shape. That is the correct direction to
fail: a false log line is reviewable, a false SUPPRESSION silently disables the
hook, which already shipped once.

Filed the remaining residual (an escaped quote inside a double-quoted -m truncates
the capture and wrongly contradicts) on autodetour-parser-misses-am-and-f.

282/282 passing.

Claude-Session: https://claude.ai/code/session_01WBqyLnBbYEi5hp2noc3cMC
…Numbers

The auto-detour fix and the test-suite split were sitting under [Unreleased]
while 0.24.0 had its own section -- they ship in this release, so leaving them
stranded would have released the content without releasing its changelog entry.
Merged into one 0.24.0 section ordered Added / Changed / Fixed / Migration
(the naive merge left two ### Fixed headings).

Date taken from the datetimeday MCP per the release checklist, not guessed:
2026-07-30. The pre-existing 2026-07-29 heading was a day stale.

Real Numbers recomputed rather than estimated, as that section's own claim
requires: 282 tests (was 276 -- six added during the review rounds), 38 releases,
130-line entry point, 26 lib modules, 0 dependencies.

Claude-Session: https://claude.ai/code/session_01WBqyLnBbYEi5hp2noc3cMC
@cfdude
cfdude merged commit 7be1c4f into main Jul 30, 2026
7 checks passed
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.

Auto-detour post-commit hook writes false-positive entries (no success check + no repo awareness)

1 participant