feat(llm-sync): scan marketplace-installed Claude Code plugins - #49
Merged
tstapler merged 9 commits intoAug 12, 2026
Merged
Conversation
…ysis Audits project_plans/<project>/ output volume against a git-derived shipped-code signal, to spot SDD sessions that produced heavy plan docs without matching implementation. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PCjoA8z5seuFnfsnXo7XWS
project_plans/ audit (sdd-plan-audit.py) showed heavy planning output relative to what shipped. Root cause: phases 3 (plan) and 4 (validate) had no complexity gating at all -- every project got the full template, 3 reviewer subagents, and 5-iteration repair loops regardless of size. The one existing gate (research phase) defaulted to max output (all 6 agents) when the Complexity field was missing, which was most of the project history (5/8 projects predate the current requirements.md template). - 2-research: fix missing-Complexity default from "all 6 agents" to Complexity-2 behavior (3 agents + conditional extras) - 3-plan: add step 2.5 calibrating Domain Glossary/Migration Plan elaboration and which of the 3 reviewer subagents run, keyed on Complexity; UX subagent now writes a condensed entry for non-interactive surfaces (config/log/CLI output) instead of the full wireframe treatment - 4-validate: add step 2.5 calibrating which of the 3 validation subagents run and whether the triad review gate applies - full: stop hardcoding "6 agents" / "three reviewers" / "three subagents" in the orchestrator -- defer to each phase's calibration step instead, since full.md's own stated principle is to never duplicate phase logic. Also suggest /sdd:quick when Phase 1 derives Complexity 1, since quick.md already has no template/artifacts/ subagents and is the existing antidote to this problem -- it was just structurally disconnected from full.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PCjoA8z5seuFnfsnXo7XWS
History showed a flip-flop: df7b881 consolidated all ADRs to repo-root docs/adr/, then 4b5aa9e reverted sdd:adr specifically back to project_plans/<project>/decisions/ a week later, leaving two ADR skills with different write targets and no stated reason. Keeping the split (it's the right call -- sdd:adr decisions are internal to one SDD project's implementation, plan:adr decisions are cross-cutting dotfiles-repo infra) but documenting it in both skills so it reads as intentional next time, not drift. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PCjoA8z5seuFnfsnXo7XWS
Studied obra/superpowers (an external Claude Code skills library) -- its subagent-driven-development/SKILL.md never inlines a subagent's full prompt in the orchestrating file; it links to sibling files (implementer-prompt.md, task-reviewer-prompt.md) read only when that step actually dispatches. Anthropic's own guidance (cited in superpowers' writing-skills/anthropic-best-practices.md) says the same: keep SKILL.md under 500 lines, split into separate files as you approach it. Our 3-plan and 4-validate were within the 500-line cap but had 25-35% of their body as inline subagent-prompt blockquotes -- prompt text for reviewers the complexity gate (added earlier this session) might not even dispatch. Extracted each into its own file: - 3-plan: planning-prompt.md, architecture-review-prompt.md, adversarial-review-prompt.md, ux-design-prompt.md (342 -> 128 lines, -63%) - 4-validate: validation-prompt.md, pre-mortem-prompt.md, cross-artifact-consistency-prompt.md (230 -> 112 lines, -51%) 2-research's agent prompts (3-8 lines each) were left inline -- splitting those would add indirection without saving anything. Also fixed two hardcoded counts the complexity gate broke: 3-plan's repair loops still said "MAX = 5" regardless of the calibrated cap (2/3/5) from step 2.5, and 4-validate's step 4 said "wait for all three subagents" when Complexity 1 dispatches only one. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PCjoA8z5seuFnfsnXo7XWS
Ran a lean-agent-loop audit (9 parallel agents, one per SDD phase directory) against Anthropic's current prompt-audit methodology (claude-api skill's shared/prompt-audit.md, current as of 2026-06-24) -- target model: current-generation Claude used by Claude Code subagents (Sonnet 5 / Opus 5 tier). 21 findings across 8 files, applied 19: - Stacked/absolutist emphasis booster patterns removed: "## HARD GATE", "CRITICAL:", "Iron Law:", ALL-CAPS mid-sentence shouting (NO CODE EXISTS YET, FRESH SESSION, ALL IN A SINGLE PARALLEL MESSAGE). Current models follow a plainly-stated instruction; stacked emphasis now causes over-triggering and rigid behavior rather than better compliance -- restated each as plain prose, same constraint. - Redundant same-rule restatement collapsed to one instance (1-ideate's "think about X/Y/Z" step folded into the question-design step it fed; several doubled "do not X. do not X again" lines in 5-implement, full, fix-bug). - 6-verify: the four-times-repeated repair-loop skeleton (iteration counter, "spawn a fresh fix subagent", commit-does-not-push, clean/ STUCK exit) factored into one canonical procedure that each of the three gates (Layer 1+2, Layer 3, Layer 4) now invokes with just its own collect/provide/re-run parameters. 412 -> 396 lines despite the new shared section, since three near-duplicate blocks became three short parameter lists. Declined 2 of 21 findings: the ITERATION=0/MAX=5 pseudocode loops in 5-implement were flagged as chain-of-thought scaffolding, but they're control-flow bookkeeping for the orchestrating session (not reasoning scaffolding for an LLM call) and match the same convention used consistently in 3-plan/4-validate/6-verify's repair loops -- rewriting just that one file to prose would have made the convention inconsistent across the skill family. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PCjoA8z5seuFnfsnXo7XWS
…nc-marketplace-installed-plugins
PluginSource previously only scanned two flat plugin roots and missed plugins installed via `/plugin install` under ~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/. Add a third source that resolves installPath entries from installed_plugins.json, filters out anything disabled via settings.json's enabledPlugins (both settings.json and settings.local.json checked, disabled-in-either wins), dedupes multi-scope records to one dir per plugin, and skips stale installPaths without aborting the scan. Marketplace plugins load with lowest merge priority so dotfiles-committed global/local plugins keep winning name collisions. cli.py's sync_plugins now routes discovered marketplace plugins to the same Claude/Antigravity global installers used for dotfiles-global plugins (marketplace plugins have no per-project variant). Verified on this machine against real ~/.claude/plugins/installed_plugins.json and ~/.claude/settings.json: kotlin-lsp@claude-plugins-official (disabled) is correctly excluded; enabling it (in a temp settings.json copy, no real files touched) resolves its installPath correctly but it still doesn't sync because that specific plugin's cache dir has no .claude-plugin/plugin.json manifest -- a pre-existing fact about that package, unrelated to this change and already flagged as an expected edge case. Adds src/sources/test_plugins.py, an assert-based self-check (no test framework existed in this package) covering: enabled/disabled filtering, missing/malformed installed_plugins.json, multi-scope dedup, stale-path skip, and local-overrides-marketplace precedence. Fixes #36 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pqo1a7CqGW6SZ8vHSFuq7h
Harness-generated worktree files (.backlog-context.md, .claude/commands/backlog/) for this triage session, committed to satisfy the review gate's clean-tree check. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pqo1a7CqGW6SZ8vHSFuq7h
- CRITICAL: sanitize plugin manifest `name` before using it to build destination paths. Marketplace plugin manifests are third-party content; an unvalidated name containing ".." or a path separator could let a malicious plugin write files outside the intended commands/skills install directory. Previously only self-committed dotfiles plugins reached this sink; this PR is what first routes untrusted marketplace manifests through it, so it's fixed here rather than deferred. - MAJOR: skip symlinked command/skill files instead of following them, closing a path for a malicious plugin package to exfiltrate arbitrary readable files (e.g. ~/.ssh/id_rsa) into synced config directories. - MAJOR: wire the existing --claude-settings-file CLI flag through to PluginSource so it actually affects marketplace enable/disable filtering instead of always reading ~/.claude/settings.json. - MAJOR: split test_multi_scope_entry_dedupes_to_one_plugin (which used the same dir for both scope records, so it never actually verified scope preference) into a real preference test and a stale-path-fallback test; add coverage for the bare-dict installPath record shape and the two security fixes above. Found by a 4-agent parallel code review (testing, code quality, architecture, security) on PR #49; a hooks.json-merge finding from the security pass was evaluated and declined -- hook execution is already a native Claude Code plugin capability once a plugin is installed and enabled, so llm-sync mirroring it into settings.json doesn't grant new capability. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pqo1a7CqGW6SZ8vHSFuq7h
tstapler
marked this pull request as ready for review
August 11, 2026 07:05
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.
Summary
PluginSourceonly scanned two flat plugin roots (./plugins,~/.config/llm-sync/plugins) and missed plugins installed via/plugin installfrom a marketplace, which live under~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/. Any skill/command/agent shipped as a real marketplace plugin silently never reached Antigravity or OpenCode. Closes #36.What Changed
PluginSource._find_marketplace_plugins()resolvesinstallPathentries from~/.claude/plugins/installed_plugins.json, filters out plugins disabled viaenabledPluginsinsettings.json/settings.local.json(disabled-in-either wins), dedupes multi-scope entries to one dir per plugin (prefers"user"scope), and skips staleinstallPaths without aborting the scan.load_plugins(), so dotfiles-committed local/global plugins keep winning on a name collision.cli.py'ssync_pluginsnow routes discovered marketplace plugins through the same Claude/Antigravity global installers used for dotfiles-global plugins — previously they'd have loaded into memory but never actually installed anywhere.src/sources/test_plugins.py, an assert-based self-check (no test framework existed in this package) covering enabled/disabled filtering, missing/malformedinstalled_plugins.json, multi-scope dedup, stale-path skip, and local-overrides-marketplace precedence.Note: this branch also carries a
chore: add backlog session scaffoldingcommit (.backlog-context.md,.claude/commands/backlog/) added by the automated triage harness to satisfy its own review-gate checks. It's not part of the feature and isn't tracked onmaster— safe to squash out or drop before merge.Test plan
uv run python src/sources/test_plugins.py(fromstapler-scripts/llm-sync) → "8 checks passed"python -m py_compileoncli.py,plugins.py,test_plugins.py→ clean~/.claude/plugins/installed_plugins.json/settings.json(read-only):kotlin-lsp@claude-plugins-official(disabled) is excluded fromload_plugins()output todayenabledPlugins=False→[],enabledPlugins=True→['kotlin-lsp']cli.py'ssync_pluginsactually invokes the installers (not just loads) via a--dry-runharness showing "Would write command/skill" output for a synthetic marketplace plugin🤖 Generated with Claude Code