fix(install-channel): a Codex plugin install was told it was "unknown" - #207
Merged
Conversation
Reported from a real Codex session:
❌ memesh update does not support this install method (unknown).
Update this installation from the tool or workflow that installed MeMesh.
on an install memesh fully supports. `memesh doctor` carried the same
non-answer into its summary.
Codex CLI adopted Claude Code's plugin manifest format and its cache layout,
one directory over — ~/.codex/plugins/cache/<marketplace>/<plugin>/<version>
against ~/.claude/plugins/cache/… . detectInstallChannel matched only
`.claude`, so a Codex-hosted copy fell past every branch to `unknown`.
Confirmed on disk: ~/.codex/plugins/cache/pcircle-memesh/memesh/4.7.1 exists
and mirrors the Claude Code layout exactly, and ~/.codex/config.toml carries
[hooks.state."memesh@pcircle-memesh:hooks/hooks.json:…"] entries for every
event — Codex is running memesh's own hook scripts, not just its MCP server.
Both runtimes now classify as `plugin-marketplace`. That is deliberate rather
than lazy: all three runtime consumers of the value ask the same host-agnostic
question — "is this wired by a plugin runtime" — and the answer is yes for
both. Splitting the enum would have required editing all three conditions and
still left the host-specific wording to fix.
What genuinely differs is the remediation, so that is now derived rather than
assumed. Verified against the real Codex CLI on this machine rather than
guessed — `codex plugin marketplace upgrade --help` takes an optional
MARKETPLACE_NAME, `codex plugin add` takes PLUGIN@MARKETPLACE:
Codex codex plugin marketplace upgrade pcircle-memesh
codex plugin add memesh@pcircle-memesh
Claude Code memesh upgrade-plugin (unchanged)
A Codex user is deliberately NOT sent to `memesh upgrade-plugin`. That script
reads $HOME/.claude/plugins/marketplaces/pcircle-memesh and patches
$HOME/.claude/plugins/installed_plugins.json; neither exists under ~/.codex
(checked — Codex keeps plugin state in config.toml and stages marketplaces
under ~/.codex/.tmp), so it aborts with "marketplace cache not found". A
confidently wrong instruction is worse than the generic one it replaces.
Three places that said "Claude Code" to everyone now name the runtime they
found: the doctor's hook-wiring row, its shell-CLI row, and the session-start
update banner. AGENTS.md's claim that Codex "has no hooks" is corrected — true
of an MCP-only wiring, false of a plugin install.
getInstallChannelSupport's second parameter is required, not optional. Every
caller already holds that path — it is what produced the channel — so an
optional one only bought the ability to be answered with a guess, and the
"host unknown" branch that served it was code for a case that cannot occur.
Evidence:
node scripts/run-tests-isolated.mjs exit=0
Test Files 185 passed (185)
Tests 2534 passed (2534)
npm run typecheck exit=0
node scripts/check-doc-claims.mjs exit=0 (all 18 gates)
npm run build exit=0
end-to-end against the real paths on this machine:
~/.codex/plugins/cache/… -> plugin-marketplace / codex /
"Codex CLI plugin marketplace" / codex plugin marketplace upgrade …
~/.claude/plugins/cache/… -> plugin-marketplace / claude-code /
"Claude Code plugin marketplace" / memesh upgrade-plugin
break-test — removed '.codex' from PLUGIN_HOST_DIRS:
exit=1, 3 failed | 12 passed
detects Codex CLI plugin-marketplace cache paths
does NOT prescribe upgrade-plugin to a Codex-hosted install
tells the two runtimes apart
restored and re-verified byte-identical
…annel had resolved
Caught in review before merge. `detectInstallChannel` matches against
`path.resolve(packageRoot)`; the three new consumers of `detectPluginHost`
pass the raw value. The old `isPluginMarketplacePath` was only ever called
with the already-normalised path, so the asymmetry arrived with this branch.
That made the comment I wrote a promise the code did not keep:
// channel === 'plugin-marketplace' is derived from this same path, so
// detectPluginHost cannot miss here.
It could. `runDoctor` takes `packageRoot` from its caller, and `path.resolve`
collapses `..`, makes relative absolute, and on Windows rewrites `/` to `\`.
`C:/Users/a/.codex/plugins/cache/…` would classify as `plugin-marketplace`
while an unresolved search for `\.codex\plugins\cache\` found nothing — and
that silent null hands a Codex user `memesh upgrade-plugin`, the one command
this branch exists to keep away from them.
The first test I wrote for this was a false guard: it built the input with
`path.join`, which collapses `..` itself, so the argument arrived normalised
and the assertion passed with or without the fix. The mutation run is what
exposed it — the mutant survived. The input is now a raw unnormalised literal.
break-test, `path.resolve` removed:
first attempt (path.join input) exit=0, 16 passed ← survived, bad test
after fixing the input exit=1, 1 failed | 15 passed
plugin host detection > normalises the path the same way
detectInstallChannel does
restored and re-verified byte-identical
node scripts/run-tests-isolated.mjs tests/install-channel.test.ts exit=0
Test Files 1 passed (1), Tests 16 passed (16)
npm run typecheck exit=0
…-host branch
The audit baseline is keyed by line number, so adding 7 lines above
`verifySkillsManifest` in this branch moved a triaged entry from
doctor.ts:1792 to :1799 and the gate reported it as a new untriaged hit
plus a stale entry to prune. That is a re-key, not a re-triage.
Verified before moving it, rather than assuming:
working tree 1799: ` const entries = manifest.entries ?? [];`
origin/main 1792: ` const entries = manifest.entries ?? [];`
byte-identical — classification and reason unchanged.
The first attempt rewrote the file with json.dumps and silently unescaped
every — in the file: 69 lines changed for a 1-line move. Reverted and
done as a text substitution — the diff is 2 lines.
npm run verify:release exit=0
✓ C5: denominator=109 hits=77 new=0
✓ Every hit is triaged; every detector saw a non-empty candidate set.
…all-channel # Conflicts: # dist/core/doctor.d.ts.map # dist/core/doctor.js.map # dist/skills-manifest.json # dist/transports/cli/cli.js.map # dist/transports/http/server.js.map # scripts/audit/baseline.json
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.
The report
From a real Codex CLI session:
…on an install memesh fully supports.
memesh doctorcarried the same non-answer into its summary (PASS_WITH_CONCERNS, "install method unknown").Cause
Codex CLI adopted Claude Code's plugin manifest format and its cache layout, one directory over:
~/.claude/plugins/cache/<marketplace>/<plugin>/<version>~/.codex/plugins/cache/<marketplace>/<plugin>/<version>detectInstallChannelmatched only.claude, so a Codex-hosted copy fell past every branch tounknown.Confirmed on disk rather than assumed:
~/.codex/plugins/cache/pcircle-memesh/memesh/4.7.1exists and mirrors the Claude Code layout exactly, and~/.codex/config.tomlcarries[hooks.state."memesh@pcircle-memesh:hooks/hooks.json:…"]entries for every event — Codex runs memesh's own hook scripts, not just its MCP server.The fix
Both runtimes classify as
plugin-marketplace. That is deliberate: all three runtime consumers of the value ask the same host-agnostic question — "is this wired by a plugin runtime" — and the answer is yes for both. Splitting the enum would have meant editing all three conditions and still leaving the host-specific wording to fix.What genuinely differs is the remediation, so that is derived, not assumed:
codex plugin marketplace upgrade pcircle-memesh, thencodex plugin add memesh@pcircle-memeshmemesh upgrade-plugin(unchanged)A Codex user is deliberately not sent to
memesh upgrade-plugin. That script reads$HOME/.claude/plugins/marketplaces/pcircle-memeshand patches$HOME/.claude/plugins/installed_plugins.json; neither exists under~/.codex(checked — Codex keeps plugin state inconfig.tomland stages marketplaces under~/.codex/.tmp), so it aborts withERROR: marketplace cache not found. A confidently wrong instruction is worse than the generic one it replaces.Codex's syntax was read off
codex plugin marketplace upgrade --helpandcodex plugin add --helpon this machine, not recalled.Three places that said "Claude Code" to everyone now name the runtime they found: the doctor's hook-wiring row, its shell-CLI row, and the session-start update banner.
getInstallChannelSupport's second parameter is required, not optional. Every caller already holds that path — it is what produced the channel — so an optional one only bought the ability to be answered with a guess, and the "host unknown" branch that served it was code for a case that cannot occur.Docs
AGENTS.mdclaimed Codex CLI "has no hooks". True of an MCP-only wiring, false of a plugin install — corrected to distinguish the two.Evidence
End-to-end against the real paths on this machine:
Break-test — removed
'.codex'fromPLUGIN_HOST_DIRS:Restored and re-verified byte-identical.