Knowledge gaps
Claude Code plugin marketplace + plugin that turns Basic Memory into an actively maintained knowledge graph
npx -y skills add voxpelli/vp-claude --skill knowledge-gapsAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 3 stars3 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
This skill should be used when the user asks about 'knowledge gaps', 'tool coverage', 'undocumented dependencies', 'undocumented tools', 'concept gaps', 'installed plugins', 'plugin coverage', 'undocumented skills', 'globally installed plugins/skills', 'what is installed on this machine', 'stale/outdated/drifted notes', 'version drift', or 'which tools/packages need updating'. Audits project dependency and tool manifests — and installed Claude Code plugins + skills.sh bundles — against Basic Memory coverage, and detects concept-level hub gaps. Two flag modes: `--stale [brew|npm|cask|crate|vscode|plugin]` checks version drift instead of coverage; `--global` audits what is installed on this machine — Claude Code plugins + skills.sh bundles today — against coverage. Supported ecosystems and full flag mechanics are documented in the skill body.
SKILL.md
40.5 KB, as published. Nobody here has run it
Knowledge Gap Detection
Analyze the current project's dependencies against Basic Memory coverage to identify packages that should be documented but aren't. Supports npm, Rust crates, Go modules, PHP Composer, PyPI, and RubyGems.
When invoked with --stale [<ecosystem>], the skill instead runs an
alternative version-drift workflow (Mode A below) that flags documented
brew/npm/cask/crate/vscode/plugin notes whose recorded version has fallen
behind upstream — not a coverage audit.
Edge Cases
- No manifest files found — if none of the Step 0 globs match, report "No package manifest files detected" and skip Steps 1–5. Still proceed to Steps 6–9 to check for tool manifests.
- No tool manifests found — if Steps 6 finds nothing, report "No tool manifests detected" and skip Steps 7–9. Still run Step 10 for dead wiki-links.
- Ecosystem directory missing in BM — if
list_directoryreturns nothing fornpm/,crates/, etc., treat coverage as 0 documented for that ecosystem. Do not error. - Empty manifest — if
package.jsonhas nodependenciesordevDependencies, orCargo.tomlhas no[dependencies]tables, report "No dependencies found in <manifest>" and skip that ecosystem. - Brewfile with only comments or taps — if no
brew "...",cask "...", orvscode "..."lines exist after filtering, report "No tools found in Brewfile". - Workflow file with no
uses:lines — if a.github/workflows/*.ymlfile exists but has nouses:lines matching the pattern, skip it silently. - RETRO-*.md not committed — retro files are gitignored;
findstill detects them locally. This is expected behavior. - Readwise not available — if
readwise_search_highlightsorreader_search_documentsfails or returns no results, skip the reading- signal analysis in Step 14b and report only graph-based hub gaps in Step 15. bmCLI not available — if thebm project infocommand in Step 10 fails, skip the quick-exit gate and proceed directly with the relation index queries.brewCLI not available — ifbrew leavesin Step 7b fails (non-zero exit, command not found, or empty output on a non-macOS host), skip the Brewfile-vs-installed reconciliation silently and report brew coverage in Brewfile-only mode. This is the expected fallback for auditing a remote machine's declared deps from a different host.--globalmanifest absent — if~/.claude/plugins/installed_plugins.json(plugins) or~/.agents/.skill-lock.json(skills) is missing/unreadable in Step 7c, skip that population silently and note it; the other still runs. These are user-global, so a CI host without~/.claudesimply yields nothing.--global+--staletogether — reject; they are separate modes (coverage vs drift). Run one at a time.--fullwith a scope flag — reject--fullcombined with any of--limit/--since/--sample("--fullcan't be combined with--limit/--since/--sample— pick one").--fullis the opt-out from the scope preflight, not a narrowing flag, so pairing it with one is contradictory.- Scope preflight is top-level-only — the
--stalescope preflight (AskUserQuestion) fires only in the Mode A skill body, never inside a wave subagent. It must never migrate intoreferences/staleness-detection.md(loaded verbatim into subagents), whereAskUserQuestionauto-approves and returns empty. A sub-40 largest cohort, or any typed scope flag /--full, skips the prompt. recent_activityunavailable or empty (Step 10 recency-scoped sweep) — if the call errors, report "Recency sweep unavailable — could not confirm recent-note coverage" in the gap report rather than treating an empty or failed result as "zero recently active notes." If it returns zero results (no MCP error, empty list), that IS a valid clean state — distinguish the two explicitly in the report; do not conflate them.read_notefails mid-sweep (Step 10 recency-scoped sweep, step 3) — skip that note, name it in the report ("N of M recently-active notes could not be read — sweep is partial"), and continue with the remaining notes. Do not silently report the partial result as if it were the full recency sweep.
Workflow
This skill has two mutually exclusive modes. Pick exactly one based on the user's invocation, then run only that mode's steps. Never combine them.
Mode A — Stale mode (alternative entry point)
Triggered when: the user invoked the skill with the --stale flag (e.g.,
/knowledge-gaps --stale, /knowledge-gaps --stale npm), or used trigger
phrases like "stale notes", "outdated formulae", "drifted notes", "which tools
need updating".
Argument parsing: --stale takes an optional ecosystem token —
--stale [brew|npm|cask|crate|vscode|plugin]:
- bare
--stale→ check all six supported cohorts. --stale <token>where the token ∈brew|npm|cask|crate|vscode|plugin→ check only that cohort.- any other token (e.g.
action,gh,go,docker,skill,pypi,gem,composer) → reject with: "--stalesupports brew, npm, cask, crate, vscode, plugin.action/gh/go/dockerhave no single canonical comparable version;skillis unsupported (no comparable version);pypi/gem/composerare deferred." Do NOT silently fall back to all.
Scope modifiers (additive): after the ecosystem token, --stale also
accepts three optional scoping flags, in any order —
--stale [<eco>] [--limit N] [--since <date>] [--sample N]. These exist for
large cohorts (e.g. a 388-note npm directory) where the default full sweep
is impractical in one turn. Applied per cohort — a bare --stale --limit 50 run against all six cohorts checks up to 50 notes in EACH of
brew/npm/cask/crate/vscode/plugin, not 50 total.
--since <date>— the highest-leverage flag: it restricts the cohort to notes NOT touched since<date>(ISOYYYY-MM-DD), and does so BEFORE the upstream fetch (S3) and before S2's per-note read storm — it shrinks N, it doesn't just filter the rendered report. Resolved via onerecent_activity(timeframe="<date>")call, not Nread_notecalls. See the staleness-detection reference's S1 for the exact mechanics.--limit N— caps the (optionally--since-narrowed) cohort to the first N notes in listing order. Deterministic: the same input always yields the same slice — which is exactly why it is not safe to tile a large cohort into waves on its own (a second--limit Ncall re-fetches the identical first N notes rather than advancing). Only safe to layer on top of an already date-disjoint--sinceslice; partitioning itself uses successive--sincecutoffs, not--limit(see "Large-cohort strategy" in the reference file for the actual mechanism).--sample N— takes a random N-note sample instead of the first N — an unbiased bounded slice for a cohort too large to sweep in full, its N notes checked the same way as any other scope (it estimates no drift rate and extrapolates nothing). Not partition-safe (overlapping/uncovered draws across separate runs) — use it for a single bounded run, never to tile a full sweep.
N must be a positive integer and <date> a valid ISO date; reject a
malformed value by name rather than silently ignoring it. --limit and
--sample both set a cohort size, so they're mutually exclusive — if both
appear, reject with: "--limit and --sample can't both be set — pick one."
--full— skips the interactive scope preflight below and sweeps the full cohort unscoped. It is the power-user opt-out from the preflight prompt, not a scope narrower — so it is mutually exclusive with the three scope flags. If--fullappears alongside any of--limit/--since/--sample, reject with: "--fullcan't be combined with--limit/--since/--sample— pick one." (--fullalready implies the widest scope; a narrowing flag contradicts it.)
Scope preflight (interactive, top-level only). Before delegating to the
staleness-detection workflow, and only in this top-level skill body (a human
is present at every /knowledge-gaps invocation), run a one-question scope
preflight for large unscoped sweeps. This mitigates a filed, still-open upstream
limitation — Basic Memory has no bulk metadata/version projection, so the drift
sweep must read_note every entity in a cohort (UPSTREAM-basic-memory.md,
No bulk metadata/version extraction); the preflight lets a human shrink N
before that O(N) read storm rather than after. It is a UX layer over the open
gap, not a reopening of automated wave orchestration (spike vp-claude-4g53,
DEFER verdict) — it automates nothing, it only helps a human pick an existing
scope flag.
Never place this preflight in staleness-detection.md. That file is loaded
verbatim into wave subagents, and AskUserQuestion auto-approves / returns empty
answers inside a subagent — so an interactive gate there silently no-ops. The
gate is safe by construction only here, in Mode A's top-level body.
Run the preflight when both conditions hold:
- No scope modifier was typed — none of
--limit/--since/--sample, and--fullwas not passed (either flag skips the preflight: a typed scope flag already answers the question;--fullis the explicit opt-out). - The largest selected cohort exceeds 40 notes. Learn each selected
cohort's size from one cheap
list_directory(dir_name="<cohort-dir>", depth=1)per selected cohort (the same listing S1 needs anyway) — a count only, noread_note. For bare--stalethat is one listing per cohort; for--stale <eco>it is a single listing. Compare the largest count to 40.
The threshold 40 is an honest heuristic (≈5 concurrent reads/turn; a serialized 1s/call crate cohort; well below the 122-note brew incident this preflight was built for) with no config surface by design — if it fires too eagerly or too late in practice, it is a one-literal edit here.
When both conditions hold, ask once with a single AskUserQuestion — one
question object regardless of how many cohorts qualify (fold the per-cohort
counts into the question body), neutral framing. The menu is three options, or
four when the single-ecosystem sample option below applies.
Scope question (always asked):
- Full sweep (all N) — check every documented note in the selected cohort(s).
- Untouched in the last 90 days (recommended) — resolves to
--since <date>where<date>is today minus 90 days in ISOYYYY-MM-DD. This is the broader narrowing: it keeps every note not refreshed in roughly the last three months. Recommended because it gives the widest coverage short of a full sweep, and because--since's result set nests cleanly across repeat runs (per this skill's own S1 mechanics). - Untouched in the last 180 days — resolves to
--since <date>where<date>is today minus 180 days, ISO. This is a tighter subset of option 2, not a wider one: a note untouched for 180 days is necessarily also untouched for 90, so option 3 ⊆ option 2 ⊆ full. Pick it to check only the most-neglected notes (not refreshed in roughly six months) — a smaller cohort than option 2, not a larger one. When framing the options to the user (labels and question body), state this direction explicitly so a longer window is never mistaken for a bigger cohort. - Random sample of 30, excluding the last 7 days (recency-floored, unbiased
slice) — offered only for a single-ecosystem
--stale <eco>run whose one cohort is very large (> 150 notes); never for a bare--stale(see below for why). Resolves to--since <today−7d> --sample 30— compute the 7-day cutoff at runtime, the same way options 2/3 compute theirs — a random 30-note draw from the notes NOT touched in the last week, checked in full, the same per-note enumeration as any other scope. It computes no drift rate and extrapolates nothing; it is simply the only narrowing that stays representative when a cohort is both huge and actively touched, so neither--sincewindow is tractable on its own: the 90-day narrowing still leaves more than one turn can process (dogfood: ~70 on a ~500-note npm cohort) while the 180-day option resolves to ≈0 (nothing sits untouched that long on a maintained graph), and--limit's alphabetical slice (brew starts ataarch64…) would be a biased subset. The 7-day floor is a narrow guard, not a staleness-narrowing strategy: it excludes only notes an upgrade-haul (or any refresh) may have stamped with today's date moments earlier, so the draw stays representative of the wider cohort — unlike options 2/3, whose 90/180-day floors deliberately bias toward the most-neglected notes as their whole strategy. Its non-partition-safety (overlapping draws across runs) does not matter here — the preflight is a single-shot pick, not a tiled sweep. Below the 150 bar this option is not shown (a 90-day narrowing suffices — brew's 122-note cohort collapsed to 2), keeping the menu at three options for the common case. A different cutoff or sample size stays available as a typed--since <date> --sample Ncombo (already valid — only--limit+--sampleare mutually exclusive, not--since+--sample; a typed combo bypasses this preflight).
Compute all cutoff dates at runtime — never hardcode them. --limit is never a
preflight option (it stays a typed flag): its alphabetical slice is the very
bias the sample option above is chosen to avoid, so it belongs layered on a typed
--since, not offered here.
The sample option is single-ecosystem-only. A bare --stale --sample 30
would draw 30 notes from each selected cohort and silently truncate the smaller
ones (a 45-note cask cohort → a random 30), so the > 150 gate is scoped to a
single --stale <eco> run by construction — never offered for a bare --stale.
The 150-note bar (like the 40-note trigger above) is a fixed heuristic with no
config surface — a one-literal edit if it fires too eagerly or too rarely.
On no response (timeout): default to option 2 (--since <today−90d>) —
never the full sweep, and never the sample even when it was on the menu.
The full sweep is the costliest option, so defaulting to it on silence would
reintroduce the exact blow-up this preflight exists to prevent. The sample,
though cheaper than the 90-day narrowing, is a random draw — a silent automatic
default must be deterministic and reproducible, which a stochastic sample is not
— so the deterministic 90-day --since is always the fallback, regardless of
whether the sample was offered. How long to wait before defaulting is the
executing session's own timeout config, not a number hardcoded here.
One caveat where the sample option was on the menu (a single >150 cohort whose
90-day narrowing is itself known to leave more than one turn can process): the
90-day timeout fallback keeps determinism but does not by itself make that
cohort one-turn-tractable. Flag the resulting default sweep as possibly partial
and prefer the Large-cohort wave-tiling strategy (see staleness-detection.md)
over silently attempting one oversized turn — a truncated single-turn sweep would
under-report drift with no signal that it stopped early.
Once the scope is resolved (a typed flag, an interactive pick, or the timeout
default), carry it forward as if it had been typed and proceed to What to do
below. A natural-language trigger with no flags is treated exactly like a bare
--stale — the same threshold check applies (the gate lives in these shared
Mode A instructions, not in slash-command parsing, so it fires identically on
both routes).
What to do: load and follow the staleness-detection reference file in full — do NOT execute any of the Mode B steps below in the same session:
${CLAUDE_PLUGIN_ROOT}/skills/knowledge-gaps/references/staleness-detection.md
Pass the parsed ecosystem scope (one cohort, or all six) AND the resolved scope
value — any typed --limit/--since/--sample, or what the scope preflight
resolved to: a --since <date> (90d/180d pick or timeout default) or a
--since <today−7d> --sample 30 (the large single-cohort recency-floored sample option) — to that workflow; it runs the
per-cohort drift check and renders one ### Version Drift — <eco> section per
checked cohort. Also pass how the scope was chosen (typed flag / interactive pick
/ timeout default) so S8 can stamp its provenance footnote.
Mode B — Standard mode (manifest-driven coverage)
Triggered when: the user invoked the skill without the --stale flag.
The remaining steps below (numbered 0 through 15) describe this mode only.
Skip them entirely when --stale is present — Mode A is a complete
alternative workflow.
The --global flag is a Mode B addition: when present, it activates Step 7c
(user-global installed-plugin / skill coverage) alongside the project-manifest
steps. --global and --stale are mutually exclusive — if both appear, reject
with "--global (coverage) and --stale (drift) are separate modes; run one."
0. Detect project ecosystems
Before parsing dependencies, scan for manifest files using the Read tool
(not Bash). Check for the following in the current working directory:
| Manifest file | Ecosystem | BM directory |
|---|---|---|
package.json | npm | npm/ |
Cargo.toml | Rust / crates | crates/ |
go.mod | Go modules | go/ |
composer.json | PHP / Composer | composer/ |
requirements.txt or pyproject.toml | Python / PyPI | pypi/ |
Gemfile or Gemfile.lock | Ruby / RubyGems | gems/ |
A project may have multiple manifest files (e.g., a monorepo with both
package.json and Cargo.toml). Process each detected ecosystem separately
and combine results in the final report.
Check for root-level manifest files using Read — do not use Glob for
root manifests, as it recurses into node_modules/ and similar directories:
Read("./package.json")
Read("./Cargo.toml")
Read("./go.mod")
Read("./composer.json")
Read("./pyproject.toml")
Read("./requirements.txt")
Read("./Gemfile")
If Read succeeds, the ecosystem is present and the content is already loaded for Step 1 (no re-reading needed). If Read returns "file not found", skip that ecosystem.
1. Parse dependencies
For each detected ecosystem, read the manifest and extract dependencies:
npm (package.json):
Read package.json and extract all dependencies and devDependencies keys.
Exclude workspace packages (check workspaces field and skip matching entries).
Rust (Cargo.toml):
Read Cargo.toml and extract [dependencies], [dev-dependencies], and
[build-dependencies] table keys. Exclude workspace members.
Go (go.mod):
Read go.mod and extract all require directives. Ignore indirect
dependencies (// indirect comments) unless explicitly requested.
PHP Composer (composer.json):
Read composer.json and extract require and require-dev keys. Skip
php and ext-* entries (platform requirements, not packages).
Python PyPI (pyproject.toml or requirements.txt):
pyproject.toml: extract[project].dependenciesarray and[project.optional-dependencies]entriesrequirements.txt: extract package names (strip version specifiers)
Ruby (Gemfile):
Read Gemfile and extract all gem '<name>' lines. Group by Bundler groups
(group :development, etc.).
2. Check Basic Memory coverage
For each ecosystem, get all documented packages in one lightweight call:
list_directory(dir_name="<ecosystem-dir>", depth=1)
This returns all <prefix>-* note titles without loading content.
Cross-reference against the dependency list to classify each package:
- Documented — a
<prefix>-<package-name>note exists - Undocumented — no dedicated note
For undocumented packages that land in Tier 1 (after step 3), check if they're mentioned in engineering notes:
search_notes(search_type="text", query="<package-name>", page_size=3)
Classify matches as:
- Mentioned — appears in a note but isn't the primary subject
Limit this fallback to Tier 1 candidates to avoid excessive API calls.
3. Tier by import frequency
For undocumented packages, count imports using the Grep tool. Ripgrep
automatically respects .gitignore, skipping node_modules, .git, etc.
npm:
Grep(pattern="from ['\"]<package-name>['\"/]", glob="**/*.{js,ts,mjs,cjs}", output_mode="count")
Grep(pattern="require\\(['\"]<package-name>['\"/]", glob="**/*.{js,ts,mjs,cjs}", output_mode="count")
Rust:
Grep(pattern="use <crate_name>::", glob="**/*.rs", output_mode="count")
Grep(pattern="extern crate <crate_name>", glob="**/*.rs", output_mode="count")
(Replace hyphens with underscores for the crate name in use statements.)
Go:
Grep(pattern="\"<module/path>\"", glob="**/*.go", output_mode="count")
Grep(pattern="\"<module/path>/", glob="**/*.go", output_mode="count")
PHP Composer:
Grep(pattern="use <Vendor>\\\\<Package>", glob="**/*.php", output_mode="count")
Python:
Grep(pattern="import <package_name>", glob="**/*.py", output_mode="count")
Grep(pattern="from <package_name>", glob="**/*.py", output_mode="count")
(Replace hyphens with underscores for import names.)
Ruby:
Grep(pattern="require ['\"]<gem_name>['\"]", glob="**/*.rb", output_mode="count")
For scoped npm packages (e.g., @fastify/postgres), match the full name —
the @ and / don't need escaping in the pattern.
Classify:
- Tier 1 (3+ files import it): Must document — core dependency
- Tier 2 (1-2 files): Should document — used but limited scope
- Tier 3 (devDependencies/dev only, 0 runtime imports): Optional — tooling
4. Generate gap report
Present a structured report with a section per ecosystem. See
${CLAUDE_PLUGIN_ROOT}/skills/knowledge-gaps/references/report-templates.md
("Package coverage report") for the full format: a
## Knowledge Gap Report — <project> header, one ### <eco> Coverage: X/Y (Z%)
section per ecosystem (each with Tier 1 / Tier 2 / Tier 3 / Already Documented
subsections, packages ranked by import count), then an ### Overall Summary.
5. Offer enrichment
For the top 3-5 undocumented Tier 1 packages across all ecosystems, offer to
run /intel with the appropriate prefixed invocation:
- npm:
/intel fastify - crate:
/intel crate:serde - go:
/intel go:github.com/gin-gonic/gin - composer:
/intel composer:laravel/framework - pypi:
/intel pypi:requests - gem:
/intel gem:rails
Present packages ranked by import count.
6. Detect tool manifests
Glob for tool manifest files in the current working directory:
Read("./Brewfile")
Glob(pattern=".github/workflows/*.yml")
Glob(pattern=".github/workflows/*.yaml")
Read("./Dockerfile")
Glob(pattern="*.dockerfile")
Glob(pattern="Dockerfile.*")
Read("./.vscode/extensions.json")
Announce which manifests are found. If none are found AND --global was not
passed, skip Steps 7–9 and note "No tool manifests detected" in the report. If
--global was passed, still run Step 7c — it reads user-global manifests
independent of any project tool manifest.
7. Parse tool manifests
For each detected manifest, read and extract tool identifiers:
Brewfile: Read the file and extract entries by line pattern:
brew "<name>"orbrew '<name>'→brew:<name>cask "<name>"orcask '<name>'→cask:<name>vscode "<publisher>.<ext>"orvscode '<publisher>.<ext>'→vscode:<publisher>.<ext>
Skip comment lines (#) and other directive types (tap, mas, whalebrew).
Keep the parsed brew: set as the Brewfile-declared set. Step 7b reconciles
it against actual installed leaves before coverage is computed.
.github/workflows/*.yml / *.yaml:
Read each workflow file. Grep for uses: lines:
Grep(pattern="uses:\\s+[^./]", glob=".github/workflows/*.{yml,yaml}", output_mode="content")
From each match, extract the action reference:
uses: actions/checkout@v4→action:actions/checkoutuses: docker://alpine:3.18→ skip (docker:// protocol, not an action)uses: ./.github/actions/my-action→ skip (local action,./prefix)
Strip @version suffix. Deduplicate across all workflow files.
Dockerfile / *.dockerfile / Dockerfile.*:
Read each Dockerfile. Extract FROM lines:
Grep(pattern="^FROM\\s+", glob="{Dockerfile,*.dockerfile,Dockerfile.*}", output_mode="content")
From each match, extract the image identifier:
FROM node:22-alpine→docker:node(strip:tag)FROM node:22-alpine AS builder→docker:node(stripAS aliasand tag)FROM gcr.io/distroless/node→ skip (non-Docker-Hub registry)FROM ghcr.io/owner/image→ skip (non-Docker-Hub registry)FROM quay.io/org/image→ skip (non-Docker-Hub registry)
Skip registries with a . or / prefix that indicates non-Docker-Hub. Strip
version tags (:tag) and AS aliases. Deduplicate across files.
.vscode/extensions.json:
Read the file and extract the recommendations array. Each entry is a
vscode:<publisher>.<extension-id> identifier. Example:
{ "recommendations": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint"] }
→ vscode:esbenp.prettier-vscode, vscode:dbaeumer.vscode-eslint
7b. Reconcile Brewfile against brew leaves (ground truth)
The Brewfile is aspirational (what the user declared they want installed)
— brew leaves is actual (formulae installed and not pulled in as a
dependency of something else). The two diverge in two directions worth
surfacing:
- Installed but not Brewfile-declared — silent leaves that snuck in via
brew installoutside the Brewfile. These are real usage signals worth documenting, even though no manifest declares them. - Brewfile-declared but not installed — dead declarations the user intends but never realised, or formulae they have since uninstalled.
When to run: only if Step 7 parsed at least one brew:<name> entry from
a Brewfile (no Brewfile → nothing to reconcile, skip this step).
Detection: check whether the brew CLI is available and runnable:
Bash: command -v brew >/dev/null 2>&1 && brew leaves 2>/dev/null
If the command succeeds, treat its stdout (one formula name per line) as the installed-leaves set. If it fails (non-zero exit, command not found, or empty output on a non-macOS host), skip the reconciliation silently and proceed to Step 8 with the Brewfile-declared set unchanged — this is the expected fallback when auditing a remote machine's declared deps from a different host.
Why brew leaves and not brew list or the Homebrew MCP:
brew listincludes transitive dependencies — the audit would flood with formulae the user never asked for (e.g.,openssl@3,libffi).- The Homebrew MCP's list endpoint returns plain text without
installed_on_requestmetadata, so leaves cannot be distinguished from transitive deps. - Per-formula Homebrew MCP info calls would need ~190 invocations at
2-5s each — minutes of latency for one audit.
brew leavesis the canonical leaf-finder and returns in ~200ms.
Compute the diff between brewfile_declared and installed_leaves:
installed_unlisted = installed_leaves − brewfile_declareddeclared_uninstalled = brewfile_declared − installed_leavesinstalled_and_declared = brewfile_declared ∩ installed_leaves
Update the brew set for Step 8: the canonical installed-formulae set
used for BM coverage classification becomes
installed_leaves ∪ brewfile_declared (union — document anything the user
either declared or actually installed as a leaf). The two diff buckets are
carried forward for Step 9 to surface in the report.
If brew leaves was unavailable, the set used in Step 8 is just
brewfile_declared, and Step 9 reports brew coverage in Brewfile-only mode
without the diff buckets.
7c. Detect host-installed sources (--global; plugins + skills today)
When to run: only when invoked with --global. --global audits what is
INSTALLED ON THIS MACHINE (vs. what the project declares) against coverage —
it reads USER-GLOBAL manifests in $HOME (~/.claude/plugins/*,
~/.agents/.skill-lock.json), not the project. That is why it is opt-in (the
result varies by machine and is not CI-reproducible) and why the Step 9 report
section is labelled "user-global". Today it covers Claude Code plugins +
skills.sh bundles; other host-installed sources (e.g. brew leaves, installed
VSCode/gh extensions — bd vp-claude-1u1 et al.) are candidate sub-steps under
the same flag. This inverts the default's gh: handling: gh: has no project
manifest, but it does have an installed list (gh extension list) — detecting
that is a future --global sub-step, not a permanent exclusion.
The resolution (the <name>@<marketplace> → owner/repo join across
installed_plugins.json + known_marketplaces.json + each marketplace's
marketplace.json, the four source shapes, and skill grouping-by-source) is
deterministic, so it lives in a script — not in this prose:
Bash: node ${CLAUDE_PLUGIN_ROOT}/scripts/list-installed-plugins.mjs
It reads no stdin and emits one NDJSON record per installed plugin / skill-bundle:
{"identifier":"plugin:voxpelli/vp-claude#vp-knowledge","title":"plugin-voxpelli-vp-claude-vp-knowledge","installedAt":"…","members":[],"sourceResolved":true}
{"identifier":"skill:basicmachines-co/basic-memory-skills","title":"skill-basicmachines-co-basic-memory-skills","installedAt":"…","members":["memory-notes","memory-research"],"sourceResolved":true}
identifieris the/intel plugin:/skill:address;titleis the pre-normalized BM-note title Step 8 matches on;membersis the grouped skill-dir roster (the report's "Skills installed" column);installedAtdrives the Step 9 recency cap.sourceResolved: falsemeans owner/repo could not be determined (nomarketplace.json/ unknown source shape) — always Undocumented, shown as "resolve manually".- A non-zero exit or empty output → skip that population silently and note it
(a fresh machine / CI host with no
~/.claudeis normal — mirrors thecommand -v brewfallback in Step 7b). Do NOT abort the audit.
Carry the parsed records forward to Step 8.
8. Check Basic Memory coverage for tools
For each tool type with detected entries, get all documented tools in one call:
list_directory(dir_name="brew", depth=1)
list_directory(dir_name="casks", depth=1)
list_directory(dir_name="actions", depth=1)
list_directory(dir_name="docker", depth=1)
list_directory(dir_name="vscode", depth=1)
Only query directories for tool types that had manifest entries detected. Cross-reference against the parsed identifiers to classify each tool:
- Documented — a
<prefix>-<name>note exists - Undocumented — no dedicated note
When Step 7c ran (--global), get the documented claude_plugin set — matched by
NOTE TYPE, not directory, since legacy notes may live outside plugins/:
list_directory(dir_name="plugins", depth=1)
search_notes(query="plugin skill", note_types=["claude_plugin"], page_size=100)
Match each Step 7c record's title against a claude_plugin note. For a miss,
fall back to search_notes(query="<bare-name>", note_types=["claude_plugin"], page_size=5)
where <bare-name> is the last /- or #-segment of the identifier — this catches
legacy-titled notes (e.g. a note titled "beads-marketplace" for an installed beads
plugin). Classify Documented / Undocumented as above; sourceResolved:false records
are always Undocumented.
9. Add tools section to gap report
Append a tools section to the gap report after the package sections. See
${CLAUDE_PLUGIN_ROOT}/skills/knowledge-gaps/references/report-templates.md
("Tool coverage report") for the full format: one ### <type>: X/Y documented
section per tool type (no import-count tiering — group by type, documented vs
undocumented), with a Brewfile ↔ installed reconciliation sub-section under
Homebrew Formulae only when Step 7b ran (brew leaves available; otherwise the
"Brewfile-only mode" note), then a ### Tool Summary.
No import-count tiering for tools — all manifest entries are equally "used". Group by type, show documented vs undocumented count per type.
For the top undocumented tools, offer /intel invocations:
- brew:
/intel brew:ripgrep - cask:
/intel cask:warp - action:
/intel action:actions/checkout - docker:
/intel docker:node - vscode:
/intel vscode:esbenp.prettier-vscode
When Step 7c ran, append a Plugin/Skill Coverage section (template in
report-templates.md, labelled user-global). The coverage TABLE lists ALL
installed plugins/skills (the X/Y documented count needs the full denominator;
first dedup the records by title — the same plugin installed from two
marketplaces resolves to one title and would otherwise inflate Y),
but the actionable /intel OFFER below it is capped to the top 5
undocumented by installedAt (most recent first), with "…and N more — re-run to
see all" when truncated. When 0 claude_plugin notes match, lead the section
with "No plugin/skill notes yet — here are the 5 most-recently-installed to start"
rather than an all-red wall. Offer (use each record's identifier verbatim):
- plugin:
/intel plugin:<owner>/<repo>(with#<name>when the record has it) - skill:
/intel skill:<owner>/<repo>
10. Detect dead wiki-links
Check the graph for wiki-links referencing non-existent notes — organic documentation debt surfaced by the graph itself.
Quick-exit gate: Check the unresolved count first:
Bash: bm project info main --json | jq '.statistics.total_unresolved_relations'
If the count is 0, skip this step. If the CLI is unavailable, proceed with the search.
Query the relation index for each ecosystem detected in Steps 0–9.
Use entity_types=["relation"] to search the relation index directly —
this returns relation objects with from_entity and to_entity fields:
search_notes(query="npm-", entity_types=["relation"], output_format="json", page_size=50)
search_notes(query="crate-", entity_types=["relation"], output_format="json", page_size=50)
search_notes(query="go-", entity_types=["relation"], output_format="json", page_size=50)
search_notes(query="composer-", entity_types=["relation"], output_format="json", page_size=50)
search_notes(query="pypi-", entity_types=["relation"], output_format="json", page_size=50)
search_notes(query="gem-", entity_types=["relation"], output_format="json", page_size=50)
search_notes(query="brew-", entity_types=["relation"], output_format="json", page_size=50)
search_notes(query="action-", entity_types=["relation"], output_format="json", page_size=50)
search_notes(query="docker-", entity_types=["relation"], output_format="json", page_size=50)
search_notes(query="vscode-", entity_types=["relation"], output_format="json", page_size=50)
Only query prefixes for ecosystems detected in Steps 0–9.
Identify unresolved relations: For each result, check whether to_entity
is present in the JSON response. Relations missing to_entity are unresolved
— the wiki-link target has no corresponding note.
Extract the target name from the relation title (format:
"source → target") or matched_chunk. Cross-reference against the
list_directory results from Steps 2 and 8 to confirm.
Deduplicate: If a dead-linked package already appears in Tier 1/2/3 from manifest parsing, add "(also wiki-linked)" annotation rather than listing it twice.
Recency-scoped sweep (large-directory blind spot): the relevance-ranked
query above is biased toward old, frequently-linked notes — in a large,
active ecosystem directory (e.g. brew/, ~190 notes) the top 50
relevance-ranked rows can be entirely old resolved links, never reaching
notes created that same session. Confirmed empirically: a brew- prefix
relation query returned only resolved old links, while 8 brew-* notes
created that day carried 11 dangling edges that were only found by reading
those notes directly — the relevance sweep reported a false-clean for the
largest, most-active directory in the graph. Run this sweep in addition to
the relevance-ranked query above, not instead of it:
- Get recently-active ecosystem/tool notes in one call:
recent_activity(timeframe="7d", type="entity", output_format="json") - Filter the result to titles matching a detected ecosystem/tool prefix
(
npm-,crate-,go-,composer-,pypi-,gem-,brew-,action-,docker-,vscode-) — the same prefixes queried above. - For each surviving note,
read_noteit and extract every[[Target]]wiki-link in its## Relationssection. - For each target that itself matches an ecosystem/tool prefix, check it
against the
list_directoryresults already gathered in Steps 2 and 8 — a target absent from that title set is a dangling edge the relevance sweep may have missed.
This is O(recently-active notes), not O(corpus) — it stays cheap because it is bounded by the 7-day window rather than directory size, and it is recency-complete regardless of how large or old-link-dominated the directory is. Merge findings from both sweeps into one table, deduplicating by target.
Add dead-link findings to the gap report:
#### Referenced but not documented (dead wiki-links)
| Link | Referenced in | Detected via |
|------|--------------|--------------|
| [[npm-some-pkg]] | npm-fastify, engineering/patterns/http | relevance |
| [[brew-some-tool]] | brew-newly-added | recency |
Add dead-link counts to the Overall Summary:
- Dead wiki-links: Q (across R unique notes)
When offering enrichment (Steps 5, 9), include dead-link targets annotated with "(wiki-linked in N notes)" to show organic graph momentum.
Limitation: page_size=50 per prefix on the relevance-ranked query is a
sample, not exhaustive — the graph may have more unresolved relations than
one page returns, and in a large directory the sample skews toward old,
frequently-linked notes rather than recent ones (see the recency-scoped
sweep above, which exists specifically to cover that blind spot). Even with
both sweeps combined this remains a bounded gap-detection pass, not a full
audit — a note edited outside the 7-day window whose dangling edge also
falls outside the top-50 relevance-ranked sample is still missed by design
(the gardener handles comprehensive auditing). The highest-scored results
surface the most commonly referenced dead links; the recency sweep surfaces
the most recently introduced ones.
11–13. Domain standard detection
Read the standard detection reference file for Steps 11–13:
${CLAUDE_PLUGIN_ROOT}/skills/knowledge-gaps/references/standard-detection.md
This covers detecting domain standards in Basic Memory, classifying them by codebase reference count, and adding a standards section to the gap report.
14–15. Concept-level gap detection
Read the concept detection reference file for Steps 14–15:
${CLAUDE_PLUGIN_ROOT}/skills/knowledge-gaps/references/concept-detection.md
This covers mining the relation graph for implicit hub gaps, checking Readwise for reading signals, and adding a concept coverage section to the gap report.