| name | hiivmind-corpus-navigate |
| description | Navigate and query documentation from registered corpora. Use when users ask documentation questions, want to look up API references, or mention keywords matching registered corpora. Triggers: documentation, docs, lookup, search corpus, what does, how to, API reference. Auto-triggers when query contains keywords from any registered corpus (flyio, polars, etc.).
|
Corpus Navigate Skill
Search and retrieve documentation from registered corpora. This skill handles the READ side
of the corpus ecosystem - finding and presenting documentation based on user queries.
When This Skill Activates
- User asks a question matching corpus keywords (e.g., "how do I deploy to fly.io?")
- User explicitly invokes:
/hiivmind-corpus navigate [corpus] [query]
- Gateway routes a documentation query to this skill
Prerequisites
Registry required: .hiivmind/corpus/registry.yaml must exist with at least one corpus.
If no registry exists:
No corpus registry found. Register a corpus first:
/hiivmind-corpus register github:hiivmind/hiivmind-corpus-flyio
Workflow
Phase 1: Load Registry
Pattern reference: ${CLAUDE_PLUGIN_ROOT}/lib/corpus/patterns/registry-loading.md
- Read
.hiivmind/corpus/registry.yaml
- Parse registered corpora
- Build in-memory corpus index
Embedded corpora: If .hiivmind/corpus/config.yaml exists in the current repo, it is available for navigation without registry registration. Treat it as an additional corpus alongside registry entries.
Cross-corpus bridges: Also attempt to load .hiivmind/corpus/registry-graph.yaml. If found, extract the aliases section for use in Phase 2 routing. If missing or malformed, skip silently — bridges are optional.
corpora:
- id: flyio
source:
type: github
repo: hiivmind/hiivmind-corpus-flyio
ref: main
Phase 2: Route Query to Corpus
Pattern reference: ${CLAUDE_PLUGIN_ROOT}/lib/corpus/patterns/corpus-routing.md
If corpus specified explicitly:
Arguments: "flyio how to deploy"
→ corpus = flyio, query = "how to deploy"
If corpus not specified:
- Check aliases (if registry-graph.yaml was loaded in Phase 1): match query against alias keys (exact or substring). If an alias matches, add its target corpora/concepts to routing candidates.
- Embedding routing (if fastembed available):
- For each registered corpus, resolve lance_path:
- Local/embedded corpus:
lance_path = {corpus_path}/index-embeddings.lance/
- Remote (GitHub) corpus:
lance_path = .hiivmind/corpus/cache/{corpus_id}/index-embeddings.lance/
Run cache freshness check (see § Remote Embedding Cache below). If no cache available, skip this corpus.
- For each corpus with a valid lance_path:
- Run:
uv run ${CLAUDE_PLUGIN_ROOT}/lib/corpus/scripts/search.py {lance_path} "{query}" --top-k 3 --json
- Corpus score = max score across returned entries
- If top corpus score > 0.6 and > second corpus score + 0.15: use that corpus
- If top score > 0.6 but within 0.15 of second: present top 2-3 corpora to user
- If top score <= 0.6: fall through to keyword scoring
- If search.py exits non-zero for all corpora: fall through to keyword scoring
- Load keywords from each corpus config
- Score query against keywords (alias matches count as additional keyword hits)
- If single match → use that corpus
- If multiple matches → ask user to clarify
- If no matches → list available corpora
Phase 3: Fetch Corpus Index
Pattern reference: ${CLAUDE_PLUGIN_ROOT}/lib/corpus/patterns/index-fetching.md
Step 3a: Attempt index.yaml fetch (v2)
Try to fetch index.yaml first. If it exists, the v2 flow (yq pre-filtering) is used.
From GitHub source:
gh api repos/{owner}/{repo}/contents/index.yaml --jq '.content' | base64 -d
gh api repos/{owner}/{repo}/contents/{path}/index.yaml --jq '.content' | base64 -d
gh api repos/{owner}/{repo}/contents/index.yaml?ref={ref} --jq '.content' | base64 -d
From local source:
Read: {source.path}/index.yaml
If index.yaml is found → continue to Step 3b (freshness check) then Phase 4 (v2 search).
If index.yaml is NOT found → fall back to Step 3c (index.md fetch, v1 flow).
Step 3b: Freshness check (v2 only)
Pattern reference: ${CLAUDE_PLUGIN_ROOT}/lib/corpus/patterns/freshness.md
After fetching config.yaml (which navigate already does to resolve source IDs), compare the stored SHA against the live repo:
SOURCE_REPO=$(yq '.sources[0].repo_owner + "/" + .sources[0].repo_name' config.yaml)
SOURCE_BRANCH=$(yq '.sources[0].branch' config.yaml)
INDEXED_SHA=$(yq '.sources[0].last_commit_sha' config.yaml)
CURRENT_SHA=$(gh api "repos/${SOURCE_REPO}/commits/${SOURCE_BRANCH}" --jq '.sha')
If INDEXED_SHA differs from CURRENT_SHA, include a note in the response:
Note: this corpus was indexed at {short_sha}, source is now at {current_short_sha}. Consider running /hiivmind-corpus refresh.
If the check fails (network error, permissions, non-git source) → skip silently and proceed.
Self sources: Use local git log instead of gh api:
DOCS_ROOT=$(yq '.sources[] | select(.type == "self") | .docs_root // "."' config.yaml)
[ "$DOCS_ROOT" = "." ] && DOCS_ROOT=""
if [ -n "$DOCS_ROOT" ]; then
CURRENT_SHA=$(git log -1 --format=%H -- "$DOCS_ROOT")
else
CURRENT_SHA=$(git log -1 --format=%H)
fi
Multi-source corpora: The example above checks sources[0] only. For multi-source corpora, repeat for each source or check only the primary source.
Step 3c: Fetch index.md (v1 fallback)
From GitHub source (using gh api - preferred):
gh api repos/{owner}/{repo}/contents/index.md --jq '.content' | base64 -d
gh api repos/{owner}/{repo}/contents/{path}/index.md --jq '.content' | base64 -d
gh api repos/{owner}/{repo}/contents/index.md?ref={ref} --jq '.content' | base64 -d
Fallback (WebFetch):
WebFetch: https://raw.githubusercontent.com/{owner}/{repo}/{ref}/index.md
prompt: "Return the full markdown content"
From local source:
Read: {source.path}/index.md
Phase 4: Search Index
Step 4-pre: Query Expansion (if chunks-embeddings exists)
Check if chunks-embeddings.lance/ exists for the selected corpus:
- Local/embedded:
{corpus_path}/chunks-embeddings.lance/
- Remote:
.hiivmind/corpus/cache/{corpus_id}/chunks-embeddings.lance/
If it exists:
- Generate 2 query variants:
- Lexical variant: Reformulate the query using specific keywords, function names, identifiers
- Conceptual variant: Reformulate using synonyms, broader concepts, related terminology
- Store all 3 queries:
[original, lexical_variant, conceptual_variant]
Example:
- Original: "how do I filter by date"
- Lexical: "date filter timestamp column"
- Conceptual: "temporal predicates datetime expressions"
If chunks-embeddings.lance/ does not exist: skip, use only the original query.
Can be disabled with query_expansion: false in the corpus config.yaml.
If index.yaml was fetched (v2 flow):
Step 4a: Embedding pre-filter (if index-embeddings.lance/ exists)
If fastembed is available:
Step 0: Resolve embedding path
Determine the local path to index-embeddings.lance/ for the selected corpus:
- Local/embedded corpus:
lance_path = {corpus_path}/index-embeddings.lance/. If it doesn't exist → fall through to yq pre-filter.
- Remote (GitHub) corpus:
lance_path = .hiivmind/corpus/cache/{corpus_id}/index-embeddings.lance/. Run cache freshness check (see § Remote Embedding Cache below). If no cache available → fall through to yq pre-filter.
Use lance_path in all subsequent search.py invocations in this phase.
- LLM extracts search terms and optionally constructs SQL predicate:
- Tag matches:
"array_has_any(tags, ['term1', 'term2'])"
- Title matches:
"title LIKE '%term%'"
- Or no predicate (pure semantic search)
- Run:
uv run ${CLAUDE_PLUGIN_ROOT}/lib/corpus/scripts/search.py {lance_path} "{query}" --top-k 15 --where "{predicate}" --select "concepts" --json
- Reranking decision: If top 3 scores are within 0.05 of each other OR query is ambiguous, re-run with
--rerank for better precision. Do NOT rerank if top score > 0.8 or during Phase 2 cross-corpus routing.
- If search.py exits 0 with results:
- Parse ranked entry IDs with cosine scores
- Graph-boost (if graph.yaml exists):
- Read
concepts from each result entry (returned by --select "concepts")
- For each concept, look up relationships in graph.yaml
- Entries in related concepts get +0.05 score boost (once per entry, capped at 1.0, no duplicates)
- Re-sort by boosted scores
- Feed top 15 entries to Step 4b (LLM semantic judgment)
- Skip the yq pre-filter below
- If search.py exits non-zero or no results: fall through to yq pre-filter
Step 4a-chunks: Chunk search (if chunks-embeddings.lance/ exists)
Resolve chunk lance path:
- Local/embedded:
chunks_lance_path = {corpus_path}/chunks-embeddings.lance/
- Remote:
chunks_lance_path = .hiivmind/corpus/cache/{corpus_id}/chunks-embeddings.lance/
If path exists:
For each query (original + variants from Step 4-pre):
uv run ${CLAUDE_PLUGIN_ROOT}/lib/corpus/scripts/search.py {chunks_lance_path} "{query}" \
--table chunks --hybrid --text-column chunk_text --top-k 15 \
--select "parent,chunk_text,line_range" --json
Merge results across queries: deduplicate by id, keep highest score per chunk,
weight original query results 2x.
Step 4a-fusion: Fuse results across tiers
If both index-embeddings and chunks-embeddings returned results:
-
Normalize scores per tier: Divide each score by the max score in its tier
(so both tiers have scores in 0-1 range).
-
Parent-child deduplication: For each chunk hit, check if its parent field
matches any file/section hit's id:
- If match: boost the parent's normalized score by +0.1, attach the chunk's
line_range to the parent entry (so the LLM knows which section is relevant).
Remove the chunk from the results.
- If no match: keep the chunk as a standalone result with its
parent and
line_range for context.
-
Merge: Combine remaining results from both tiers, sort by normalized score,
take top 15.
-
Feed merged results to Step 4b (LLM semantic judgment).
If only one tier returned results: skip fusion, use that tier's results directly.
See: ${CLAUDE_PLUGIN_ROOT}/lib/corpus/patterns/embeddings.md § Graph-Boost, Reranking
Step 4a fallback: yq pre-filter
Extract 2-5 search terms from the user's query. Construct yq filters:
yq '.entries[] | select(
(.tags[] | test("term1|term2"; "i")) or
(.keywords[] | test("term1|term2"; "i")) or
(.summary | test("term1.*term2|term2.*term1"; "i"))
) | {id, title, summary, tags, category, stale}' index.yaml
This returns a candidate set (typically 5-20 entries) with enough metadata for semantic judgment.
Step 4b: LLM semantic judgment
Review the pre-filtered candidates. Select the 2-5 entries that best answer the user's query. Consider:
- Summary relevance to the question
- Tag/keyword alignment
- Category appropriateness (e.g., prefer
tutorial for "how to" questions)
- Stale status (include but note if entry is stale)
If index.md was fetched (v1 fallback):
Search the index for relevant entries:
- Extract search terms from user query
- Search index.md for matching entries
- If tiered index, search relevant sub-index
- Extract
source:path references from matches
Index entry format:
- **Install flyctl** `flyio:flyctl/install.html.markerb` - Description
Search approach:
- Use Grep with search terms against index content
- Look for entries with backtick-wrapped paths
- Rank by relevance (keyword match > description match)
Phase 4c: Graph Enrichment
Pattern reference: ${CLAUDE_PLUGIN_ROOT}/lib/corpus/patterns/graph.md
Optional — skip this phase if no graph.yaml exists in the same location as the index.
Check: After fetching the index, attempt to load graph.yaml from the same location (same repo path or local directory). If missing or fetch fails → skip phase, proceed to Phase 5 with only Tier 1 results.
When graph.yaml exists:
-
Load graph.yaml — parse concepts and relationships
-
Tier 2: Concept membership
For each Tier 1 matched entry, find which concepts it belongs to.
v2 mode (yq queries):
yq ".concepts | to_entries[] | select(.value.entries[] == \"${ENTRY_ID}\") | .key" graph.yaml
yq '.concepts["{concept_id}"].entries[]' graph.yaml
v1 mode: For each Tier 1 matched entry (source_id:path), find which concepts it belongs to by reading graph.yaml directly. Collect all other entries in those same concepts as Tier 2 candidates.
Limit: up to 5 additional entries per matched concept.
-
Tier 3: Relationship traversal (1 hop)
For each concept matched in Tier 2, follow typed relationships in graph.yaml to find related concepts (1 hop only — do not recurse).
v2 mode (yq queries):
yq '.relationships[] | select(.from == "{concept}") | .to' graph.yaml
yq '.concepts["{related_concept}"].entries[]' graph.yaml
v1 mode: Read graph.yaml directly and traverse relationships manually.
Add entries from related concepts as Tier 3 candidates, ranked by relationship type:
includes / extends → high relevance
depends-on → medium relevance
see-also / contrast-with → lower relevance
Limit: up to 3 entries per related concept, up to 2 relationship types considered as traversal candidates (1 hop only — do not recurse).
-
Tier 4: Cross-corpus bridge traversal
Check for cross-corpus bridges via registry-graph.yaml (loaded in Phase 1). If not loaded → skip Tier 4.
For each concept matched in Tiers 2-3, check if it participates in any bridge:
Remote Embedding Cache
LanceDB cannot open Lance datasets over HTTPS — it only supports local paths, S3, GCS, and Azure. For remote GitHub corpora, the navigate skill caches the Lance directory locally.
Cache location: .hiivmind/corpus/cache/{corpus_id}/index-embeddings.lance/
Freshness check algorithm:
1. cache_path = .hiivmind/corpus/cache/{corpus_id}/index-embeddings.lance/
2. If cache_path exists:
a. Read .hiivmind/corpus/cache/{corpus_id}/_cache_meta.json
b. If cached_at + ttl_days > now → FRESH, use cache
c. Else (stale):
- current_sha = gh api repos/{owner}/{repo}/commits/{ref} --jq '.sha'
- If current_sha == cached_commit_sha → rewrite _cache_meta.json with updated cached_at, use cache
- If current_sha != cached_commit_sha → re-clone Lance directory, update _cache_meta.json
- If gh api fails → use cached version silently
3. If cache_path does not exist:
a. Attempt sparse clone (see below)
b. If clone succeeds and index-embeddings.lance/ exists → copy to cache, write _cache_meta.json
c. If clone fails or no Lance directory → fall through to yq pre-filter
Sparse clone procedure:
TTL_DAYS=$(yq ".corpora[] | select(.id == \"{corpus_id}\") | .cache.ttl" .hiivmind/corpus/registry.yaml 2>/dev/null | sed 's/d$//')
TTL_DAYS=${TTL_DAYS:-7}
TMPDIR=$(mktemp -d)
git clone --depth 1 --filter=blob:none --sparse \
"https://github.com/{owner}/{repo}.git" "$TMPDIR" 2>/dev/null
cd "$TMPDIR" && git sparse-checkout set index-embeddings.lance chunks-embeddings.lance
if [ ! -d "$TMPDIR/index-embeddings.lance" ]; then
rm -rf "$TMPDIR"
fi
COMMIT_SHA=$(git -C "$TMPDIR" rev-parse HEAD)
CACHE_DIR=".hiivmind/corpus/cache/{corpus_id}"
mkdir -p "$CACHE_DIR"
rm -rf "$CACHE_DIR/index-embeddings.lance.tmp"
cp -r "$TMPDIR/index-embeddings.lance" "$CACHE_DIR/index-embeddings.lance.tmp"
rm -rf "$CACHE_DIR/index-embeddings.lance"
mv "$CACHE_DIR/index-embeddings.lance.tmp" "$CACHE_DIR/index-embeddings.lance"
[ -d ];
-rf
-r
-rf
> <<
-rf
_cache_meta.json format:
{
"corpus_id": "obsidian",
"source_repo": "hiivmind/hiivmind-corpus-obsidian",
"source_ref": "main",
"cached_at": "2026-03-29T09:00:00Z",
"cached_commit_sha": "81cfc93c3e3f92696dec7a40814d8b404f94f3aa",
"ttl_days": 7
}
Graceful Degradation
| Condition | Behavior |
|---|
| No index.yaml | Fall back to index.md prose scanning (v1) |
| No graph.yaml | Skip concept enrichment and relationship traversal |
| yq not available | LLM reads index.yaml directly as structured YAML |
| Freshness check fails | Skip silently, proceed with cached index |
| Stale entries in results | Include them but note "this entry may be outdated" |
| No registry-graph.yaml | Skip cross-corpus bridges and aliases |
| No index-embeddings.lance/ for corpus | Skip embedding pre-filter and cross-corpus routing, use yq/keyword approach |
| No fastembed but index-embeddings.lance/ exists | Skip embedding search, fall back to keywords |
| Stale embeddings (index newer than embeddings) | Use stale embeddings, note in output |
| Remote corpus, no cached embeddings | Sparse clone on first query (~5s), then cached |
| Remote corpus, cache stale but SHA unchanged | Touch cached_at, use cache |
| Remote corpus, cache stale and SHA changed | Re-clone Lance directory (~5s) |
| Remote corpus, network unavailable | Use existing cache if present, else yq pre-filter |
| git not available | Skip sparse clone, fall through to yq pre-filter |
| No chunks-embeddings.lance/ | Skip chunk search, use metadata embeddings only |
| chunks-embeddings.lance/ but no fastembed | Skip chunk search, fall back to metadata-only |
| Stale chunk embeddings | Use them, note in output |
| query_expansion: false in config | Skip query expansion, search with original query only |
Phase 5: Fetch Documentation
For matched entry source_id:relative_path:
- Read source config from corpus config.yaml: CRITICAL. You MUST read config.yaml FIRST to resolve source_id: prefixes to actual repository URLs. Never guess repository names.
- Build gh api command or local path to documentation
- Fetch content
From GitHub (using gh api - preferred):
gh api repos/{source_owner}/{source_repo}/contents/{docs_root}/{path}?ref={branch} --jq '.content' | base64 -d
Fallback (WebFetch):
WebFetch: https://raw.githubusercontent.com/{source_owner}/{source_repo}/{branch}/{docs_root}/{path}
prompt: "Return the documentation content"
From local cache:
Read: .corpus-cache/{corpus_id}/.source/{source_id}/{path}
Self source (embedded corpus):
For type: self sources, read files directly from the repo:
# Resolve path
docs_root = source.docs_root (normalize "." to "")
file_path = {repo_root}/{docs_root}/{relative_path}
# Read directly
Read: {file_path}
No cloning, no remote fetch, no gh api call needed. The file is already local.
Phase 6: Present Answer
Format the response with:
- Documentation content
- Source citation
- Related documentation suggestions
## {Topic Name}
**Source:** {corpus}:{path}
---
{documentation content}
---
**Related:**
- [Related Topic 1](related-path-1)
- [Related Topic 2](related-path-2)
Clarification Flow
When no exact match is found in the index:
I searched for '{terms}' in the {corpus} corpus but didn't find an exact match.
I found these related entries:
- Entry 1
- Entry 2
How would you like to proceed?
1. Rephrase my question
2. Show available sections
3. This topic is missing
Arguments
| Argument | Description | Example |
|---|
{corpus} | Optional corpus ID | flyio |
{query} | Search query | "postgres configuration" |
Usage examples:
/hiivmind-corpus navigate flyio "postgres setup"
/hiivmind-corpus navigate "how to deploy"
"How do I set up Postgres on Fly.io?" (auto-triggers via keywords)
Error Handling
Registry not found:
No corpus registry found at .hiivmind/corpus/registry.yaml
To register a corpus:
/hiivmind-corpus register github:hiivmind/hiivmind-corpus-flyio
Corpus not registered:
Corpus '{corpus}' is not registered.
Available corpora: flyio, polars
Register with: /hiivmind-corpus register github:owner/repo
Index fetch failed:
Could not fetch index for corpus '{corpus}'.
The corpus may be offline or the URL has changed.
Try: /hiivmind-corpus status {corpus}
Document not found:
Document not found: {source}:{path}
The documentation may have moved. Try:
/hiivmind-corpus refresh {corpus}
Pattern Documentation
- Graph enrichment:
${CLAUDE_PLUGIN_ROOT}/lib/corpus/patterns/graph.md
- Index v2 schema:
${CLAUDE_PLUGIN_ROOT}/lib/corpus/patterns/index-format-v2.md
- Freshness checks:
${CLAUDE_PLUGIN_ROOT}/lib/corpus/patterns/freshness.md
- Index rendering:
${CLAUDE_PLUGIN_ROOT}/lib/corpus/patterns/index-rendering.md
- Registry graph:
${CLAUDE_PLUGIN_ROOT}/lib/corpus/patterns/registry-graph.md
- Embeddings:
${CLAUDE_PLUGIN_ROOT}/lib/corpus/patterns/embeddings.md
Related Skills
- Migrate v1→v2 (headless):
${CLAUDE_PLUGIN_ROOT}/skills/hiivmind-corpus-migrate/SKILL.md
- Headless status (pipelines):
${CLAUDE_PLUGIN_ROOT}/skills/hiivmind-corpus-status-headless/SKILL.md
- Headless rebuild (pipelines):
${CLAUDE_PLUGIN_ROOT}/skills/hiivmind-corpus-build-headless/SKILL.md
- Register:
hiivmind-corpus-register - Add corpora to the registry
- Status:
hiivmind-corpus-status - Check corpus health
- Discover:
hiivmind-corpus-discover - Find available corpora
- Refresh:
hiivmind-corpus-refresh - Update corpus from upstream
${CLAUDE_PLUGIN_ROOT}/skills/hiivmind-corpus-graph/SKILL.md — View, validate, edit concept graphs
${CLAUDE_PLUGIN_ROOT}/skills/hiivmind-corpus-bridge/SKILL.md — Cross-corpus concept bridges and aliases