Conversation
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
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.
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
PostToolUsefires when the Bash tool returns, which is not the same as "a commit landed in this repo." Three divergences, each writing adetours.logline attributed to this repo's stale HEAD:pre-commitrejected the commit — HEAD never advancedgit -C)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
nodestartup, serialized in a single 3,202-line file on a 16-core machine.node --testparallelizes 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=16changed 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-commitnow cross-checks the ran count againstgrep -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.md→AGENTS.md→CLAUDE.md→.cursorrules. So Hermes does readCLAUDE.md, but only when the first two are absent. In a repo already carrying anAGENTS.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.CLAUDE.md/pm:statusHERMES.md→AGENTS.md→CLAUDE.md/pm:statusAGENTS.md/pm-statusEvery 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-instatus, so thepm: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
resolveAndRecordPlatformpersisted unconditionallywrite-rulescreated state in an uninitialized repo, ending pm's dormancy and activating every hook%sis first-line-only while a-mcapture spans newlines — the release headlined "fix commit-nudge" had turned it off for the common case, on 24/24 commits here/pm:upgradeThe 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 vacuous —
gitRepo()'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 anudgeAndReadLoghelper makes absence assertions non-vacuous by construction (detourLog()returns""for a missing file, so a baredoesNotMatchalso passes when the hook never ran).Verification
rgreturns rg's status and hides failures).1 block → 1 block, marker migrated,0.23.1 → 0.24.0, platform stamped. Idempotent on a second run.Known gaps, filed not hidden
autodetour-parser-misses-am-and-f— Auto-detour post-commit hook writes false-positive entries (no success check + no repo awareness) #65 is only closed for-m.git commit -amparses to an empty subject and still writes a falseDETOUR-COMMITduring a detour; an escaped quote inside a double-quoted-mtruncates the capture. Worth resolving before those issues are closed upstream.platform-switch-orphans-old-rules-block(blocks Hermes) — a switch leaves the previous platform's block live but frozen: a stale-instructions hazard.shellBuiltis deliberately broad — a literal${VAR}in a real subject also lands on the unverifiable rung, so Auto-detour post-commit hook writes false-positive entries (no success check + no repo awareness) #65 can still false-positive for that shape. That's the correct direction to fail: a false log line is reviewable, a false suppression silently disables the hook.https://claude.ai/code/session_01WBqyLnBbYEi5hp2noc3cMC