Skip to content

fix: resolve render_treatment_report.py across all three install roots (v2.4.0) - #13

Merged
wan-huiyan merged 1 commit into
mainfrom
fix/plugin-install-path-resolution
Aug 6, 2026
Merged

fix: resolve render_treatment_report.py across all three install roots (v2.4.0)#13
wan-huiyan merged 1 commit into
mainfrom
fix/plugin-install-path-resolution

Conversation

@wan-huiyan

Copy link
Copy Markdown
Owner

The bug

SKILL.md:341 and the README's recap section both told the agent to run:

python3 ~/.claude/skills/context-police/scripts/render_treatment_report.py ...

A /plugin install never creates that path. The script lands at:

~/.claude/plugins/cache/<marketplace>/context-police/<version>/skills/context-police/scripts/render_treatment_report.py

(confirmed against a real install on this machine — four cached versions, all at that layout).

So on the plugin install path the command just fails. The usual response is a log line plus "continue", which means the recap step silently does nothing while the summary still reads clean. This is the same class of defect that was fixed in session-handoff v1.17.0 (wan-huiyan/session-handoff#13), where it made a user believe an installed skill was absent.

CLAUDE_PLUGIN_ROOT does not rescue this on its own: it is frequently unset in the shell a step actually runs in, and it points at the calling plugin's own root, so it can never reach a sibling plugin.

The fix

Both call sites now resolve across all three install roots before invoking, and guard the call:

# Resolve across all three install roots. A plugin install creates neither of the first two.
S="${CLAUDE_PLUGIN_ROOT:+${CLAUDE_PLUGIN_ROOT}/skills/context-police/scripts/render_treatment_report.py}"
[ -f "$S" ] || S="$HOME/.claude/skills/context-police/scripts/render_treatment_report.py"
[ -f "$S" ] || S="$(find -L "$HOME/.claude/plugins/cache" -mindepth 7 -maxdepth 7 \
    -path '*/context-police/*/skills/context-police/scripts/render_treatment_report.py' 2>/dev/null \
  | awk -F/ '{print $(NF-4)"\t"$0}' | sort -V -k1,1 | tail -1 | cut -f2-)"

if [ -f "$S" ]; then
  python3 "$S" ...
else
  echo "render_treatment_report.py: not found - tried \$CLAUDE_PLUGIN_ROOT/skills/context-police/scripts/, ~/.claude/skills/context-police/scripts/, and the plugin cache"
fi

Four details, each of which has caused a real defect elsewhere:

  1. Rank on the version segment alone ($(NF-4)). The marketplace segment precedes the version in the path, so a plain sort -V over whole paths ranks by marketplace name and lets aaa-mkt/2.5.0 lose to zzz-mkt/1.0.0. This repo's depth is 7 (not the usual 5) because the plugin nests its scripts under skills/context-police/, which is why the version sits at NF-4 rather than NF-2.
  2. find, not a shell glob. zsh's nomatch fails a non-matching glob at expansion time, before 2>/dev/null can apply, printing a raw shell error.
  3. Guard before the call. No > "$OUT" redirect here (the script takes --out), but the if [ -f "$S" ] guard is still what keeps an unresolved lookup from producing a truncated artifact.
  4. "not found - tried <paths>", never a bare "not installed". A failed lookup is not evidence about install state, and a bare "not installed" has already been misread by a human as proof a skill was absent.

Regression test

New tests/plugin-path-resolution.test.mjs (zero deps, node --test) extracts the resolver verbatim out of SKILL.md and README.md and executes it against fixture caches, so the documented snippet is the thing under test rather than a copy that could drift. Per file it asserts:

  • resolves a plugin-cache install when ~/.claude/skills/ does not exist at all
  • ranks on the version segment, not the marketplace name (aaa-marketplace/2.5.0 beats zzz-marketplace/1.0.0)
  • orders versions numerically (2.10.0 beats 2.9.0)
  • prefers ~/.claude/skills/ over the cache; CLAUDE_PLUGIN_ROOT wins over both when it holds the script
  • an unset CLAUDE_PLUGIN_ROOT collapses to empty rather than becoming /skills/...
  • a home with no install at all resolves to nothing and writes nothing to stderr (the glob trap)

Plus static checks on both files: no python3 ~/.claude/skills/... invocation survives, the not-found message names its paths, and the two rendered surfaces ship an identical resolver.

Negative controls run, both confirmed red:

Injected regression Result
$(NF-4)$(NF-2) 4 red (version-ranking tests + resolver-parity + plugin-copy-sync)
restore the single-root python3 ~/.claude/skills/... line 2 red (static check + plugin-copy-sync)

Test suite

npm testgreen before the change (23 pass / 0 fail / 1 skipped) and green after (42 pass / 0 fail / 1 skipped). The skip is the pre-existing "VERSION file (if present)" test; this repo has no VERSION file.

Version

2.3.02.4.0 (minor — behavior changes). Synced in plugin.json, .claude-plugin/marketplace.json, and the SKILL.md frontmatter. package.json has no version field, so nothing to sync there. README Version history has a v2.4.0 entry. Plugin copy regenerated with scripts/dev/sync_plugin_copy.sh.

Deliberately not changed

  • --skills-dir ~/.claude/skills stays as-is everywhere, and so do the SK = ~/.claude/skills defaults in render_treatment_report.py, apply_disable_model_invocation.py, pilot/retrieve.py, and pilot/phase8_embeddings_probe.py. These are the audit target — the skills universe being measured — not an executable lookup. (Worth noting separately: on a plugin-heavy install that universe is arguably incomplete, since plugin-supplied skills live in the cache. That is a different bug class and out of scope here.)
  • README's install instruction git clone ... ~/.claude/skills/context-police — that is a manual-install command that creates the directory, not a lookup of an existing one.
  • docs/workstream/** references to ~/.claude/skills/... — archived handoff records of what was run at the time; rewriting them would falsify the record.
  • No dead documentation links found. The "Related skills" entries in both files already use the canonical safe form (bare skill name, or a GitHub URL). Zero doc-link fixes were needed.

…s (v2.4.0)

SKILL.md and README told the agent to run
`python3 ~/.claude/skills/context-police/scripts/render_treatment_report.py`.
A `/plugin install` never creates that path -- the script lands under
`~/.claude/plugins/cache/<marketplace>/context-police/<version>/skills/context-police/scripts/`
-- so on the plugin install path the command just failed. The usual response
(log it and continue) means the recap step silently does nothing while the run
still reads clean.

Both call sites now resolve across `$CLAUDE_PLUGIN_ROOT`, `~/.claude/skills/`,
and a version-ranked `find` over the plugin cache, and print
`not found - tried <paths>` rather than a bare "not installed".

CLAUDE_PLUGIN_ROOT alone does not fix this: it is frequently unset in the shell
a step runs in, and it points at the calling plugin's own root so it can never
reach a sibling.

The version is ranked on its own path segment (awk '{print $(NF-4)}'), not the
whole path -- the marketplace segment precedes the version, so a plain `sort -V`
over full paths would let aaa-mkt/2.5.0 lose to zzz-mkt/1.0.0. `find` is used
rather than a glob because zsh's nomatch fails a non-matching glob before
2>/dev/null can apply.

tests/plugin-path-resolution.test.mjs extracts the resolver verbatim out of
SKILL.md and README.md and executes it against fixture caches, so the documented
snippet is the thing under test. Negative controls confirmed: changing $(NF-4)
to $(NF-2) turns the version-ranking tests red, and restoring the single-root
invocation turns the static check red.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@wan-huiyan
wan-huiyan merged commit 261d013 into main Aug 6, 2026
4 checks passed
@wan-huiyan
wan-huiyan deleted the fix/plugin-install-path-resolution branch August 6, 2026 14:05
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