---
name: query-index
description: Internal helper. Runs a BM25 FTS5 query against ~/.claude/cache/maxv-orchestration/index.db and returns top-N candidates ranked by relevance. Called by route-task when the cheatsheet has no match. Not user-invocable directly.
when_to_use: Called programmatically by route-task. Users should invoke discover-skill or discover-agent instead.
user-invocable: false
allowed-tools: Read Bash(test *) Bash(sqlite3 *)
---

# Query index

Argument: `$ARGUMENTS` — `<query> [--kind=skill|agent] [--limit=N]`

## Workflow

### 1. Resolve cache path + preflight
```bash
set -euo pipefail
INDEX_DB=~/.claude/cache/maxv-orchestration/index.db
test -f "$INDEX_DB" || {
  echo '{"matches":[],"error":"index missing","suggestion":"Run /maxvision-orchestration:index-catalog first"}'
  exit 1
}
```

### 2. Parse arguments

Extract from `$ARGUMENTS`:
- `query` — required, the FTS5 MATCH expression (positional, first non-flag token)
- `--kind=skill|agent` — optional filter; default both kinds (empty string)
- `--limit=N` — optional integer, default 10

Assign to shell variables `QUERY`, `KIND` (default `''`), `LIMIT` (default `10`). LIMIT must be a positive integer; step 4 enforces this with a regex guard before issuing the SQL.

### 3. Build BM25 SQL with column weights

Column weights (highest first): `name(1.5) > description(1.0) > when_to_use(1.0) > body(0.8) > allowed_tools(0.5)`. The `bm25()` weights argument order matches FTS5 column order in `scripts/schema.sql`: `name, description, when_to_use, body, allowed_tools`.

```sql
SELECT
  item.kind, item.name, item.source_id, item.path,
  substr(item.description, 1, 200) AS description,
  item.last_commit_iso, item.schema_valid,
  bm25(item_fts, 1.5, 1.0, 1.0, 0.8, 0.5) AS score
FROM item_fts
JOIN item ON item.id = item_fts.rowid
WHERE item_fts MATCH :q
  AND (:k = '' OR item.kind = :k)
ORDER BY score
LIMIT :n;
```

### 4. Execute and emit JSON (heredoc + SQL-literal escaping)

Bash interpolates variables before sqlite3 receives them. To make values safe as SQL string literals, double up any embedded single quotes (`'` → `''`) in shell BEFORE feeding them into the heredoc. `LIMIT` is validated as integer.

```bash
set -euo pipefail
# Validate LIMIT is a positive integer (rejects "10; DROP", "10 OR 1=1", etc.)
[[ "$LIMIT" =~ ^[0-9]+$ ]] || {
  echo '{"matches":[],"error":"invalid limit","suggestion":"--limit must be a positive integer"}'
  exit 1
}

# SQL literal escape: ' → ''
Q_ESC=${QUERY//\'/\'\'}
K_ESC=${KIND//\'/\'\'}

RESULT=$(sqlite3 -json "$INDEX_DB" <<SQL
SELECT item.kind, item.name, item.source_id, item.path,
       substr(item.description, 1, 200) AS description,
       item.last_commit_iso, item.schema_valid,
       bm25(item_fts, 1.5, 1.0, 1.0, 0.8, 0.5) AS score
FROM item_fts JOIN item ON item.id = item_fts.rowid
WHERE item_fts MATCH '$Q_ESC'
  AND ('$K_ESC' = '' OR item.kind = '$K_ESC')
ORDER BY score
LIMIT $LIMIT;
SQL
)
```

(`sqlite3 -json` returns a JSON array; one object per row.)

### 5. Normalize output shape

Always emit a single JSON object with a `matches` array (never a bare array). This gives `route-task` a stable shape to consume:

```bash
set -euo pipefail
if [[ -z "${RESULT:-}" || "$RESULT" == "[]" ]]; then
  printf '%s\n' '{"matches":[],"suggestion":"try broader keyword or run /maxvision-orchestration:discover-skill <kw>"}'
else
  printf '{"matches":%s}\n' "$RESULT"
fi
```

Each element of `matches` has the shape:
```json
{"kind":"skill","name":"shadcn-ui","source_id":"shadcn/ui","path":"skills/shadcn-ui/SKILL.md","description":"...","last_commit_iso":"...","schema_valid":1,"score":-3.2}
```

(Lower/more-negative `score` = higher BM25 relevance; callers should sort ascending on `score`.)

## Guardrails

- **Read-only.** Never write to `index.db` or any disk path. `allowed-tools` excludes `Bash(python *)`, `Bash(gh *)`, `Bash(rm *)`, `Bash(mv *)`, and all write paths.
- **Index missing.** Hard-fail at step 1 with a JSON error object and `exit 1`, including the suggestion to run `index-catalog`. Do not proceed with an empty result set.
- **SQL-literal escaping.** Bash interpolates variables before sqlite3 sees them, so `.parameter set :q '$QUERY'` is NOT real parameter binding (the quoting happens in shell, not sqlite3). Step 4 escapes single quotes (`'` → `''`) in `QUERY` and `KIND` and validates `LIMIT` as a positive integer regex. Anything else (semicolons, comments, ATTACH attempts) is then a literal inside the SQL string, not executable SQL.
- **`set -euo pipefail` scope.** Each bash code block is treated as an independent shell session by some interpreters; if executing blocks separately, repeat `set -euo pipefail` at the top of each.
- **`user-invocable: false`.** This skill is an internal helper for `route-task`. Users should invoke `discover-skill` (for skills) or `discover-agent` (for subagents) directly.
