---
name: analyze-candidates
description: Ranks candidate skills/agents by task fit using Sonnet LLM-as-judge AND classifies task complexity (model + effort) in same call. Input is union of cheatsheet + FTS5 candidates with EQUAL weight per D3+D9. Output is task-driven ranking + complexity classification + INSTALL_BEST | INSTALL_MULTIPLE | SYNTHESIZE recommendation. v0.5.0 replaces v0.4.0 BM25+Jaccard ranking with Sonnet-as-judge (Jaccard retained only for INSTALL_BEST→MULTIPLE downgrade cross-check).
when_to_use: Called programmatically by route-task. Receives a JSON list of merged candidates (cheatsheet ∪ FTS5) plus the original user task. Not user-invocable directly.
user-invocable: false
allowed-tools: Read Bash(test *) Bash(python *) Bash(sqlite3 *) Agent
---

# Analyze candidates (Sonnet-as-judge)

Argument: `$ARGUMENTS` — JSON `{"task": "<user task>", "candidates": [{"name": "...", "kind": "skill|agent", "source": "cheatsheet|fts5|both", ...}, ...]}`

## Workflow (v0.5.0 — Sonnet-as-judge)

### 1. Preflight

```bash
set -euo pipefail
INDEX_DB="${MAXV_INDEX_DB:-$HOME/.claude/cache/maxv-orchestration/index.db}"
test -f "$INDEX_DB" || {
    echo '{"matrix":[],"error":"index missing","suggestion":"Run /maxvision-orchestration:index-catalog first"}'
    exit 1
}
```

### 2. Parse input

Extract from `$ARGUMENTS`:
- `task` — the user's task string (verbatim).
- `candidates` — array of `{name, kind, source, source_id?, bm25_score?, priority_hint?}` from route-task's parallel merge.

### 3. Hydrate candidate metadata

For each candidate, hydrate the body+metadata fields needed by the Sonnet prompt. Helper handles SQL escape + JOIN with source table for license:

```bash
set -euo pipefail
python "${CLAUDE_PLUGIN_ROOT}/scripts/analyze_candidates_sonnet.py" hydrate \
    --task "$TASK" --candidates "$CANDIDATES_JSON" > /tmp/hydrated.$$
```

Output: same candidates array but each entry now also has `description`, `when_to_use`, `capabilities[]` (top 5), `license`, `recency_days`, `complexity` from index.db. (Implementation arrives in Task 16; for now the skill assumes route-task already enriched the entries with sufficient metadata.)

### 4. Check classify-cache

```bash
set -euo pipefail
python "${CLAUDE_PLUGIN_ROOT}/scripts/analyze_candidates_sonnet.py" cache-get \
    --task "$TASK" --candidates "$CANDIDATES_JSON" --ttl 86400
```

If cache hit (exit 0, non-empty stdout): return that JSON immediately. Skip step 5-7. (Cache implementation in Task 16.)

### 5. Compose Sonnet prompt + invoke Agent (D12)

> **D12 — direct Agent invocation.** Skills with `Agent` in `allowed-tools`
> invoke Agent during their own execution. There is NO sentinel-resume
> pattern. Claude (the model interpreting this SKILL.md) calls the Agent
> tool directly here.

Compose the prompt via the helper:

```bash
set -euo pipefail
PROMPT="$(python "${CLAUDE_PLUGIN_ROOT}/scripts/analyze_candidates_sonnet.py" compose \
    --task "$TASK" --candidates "$CANDIDATES_JSON")"
```

Then **invoke the Agent tool directly** with these parameters (you, the model, do this):

```
Agent(
    subagent_type="general-purpose",
    model="sonnet",
    description="Rank candidates for: <first 40 chars of task>",
    prompt=<PROMPT from above>
)
```

The subagent receives the composed prompt, runs Sonnet at temperature 0 implicitly (model default), and returns STRICT JSON. Capture the return value as `RAW_OUTPUT`.

### 6. Validate Sonnet output (anti-hallucination + schema)

```bash
set -euo pipefail
echo "$RAW_OUTPUT" | python "${CLAUDE_PLUGIN_ROOT}/scripts/analyze_candidates_sonnet.py" validate \
    --candidates "$CANDIDATES_JSON" \
    --schema "${CLAUDE_PLUGIN_ROOT}/schemas/analyze-output.schema.json"
```

Validation rules (Task 16):
1. JSON parse — malformed → retry Sonnet ONCE with stricter prompt; second failure → fallback to BM25+Jaccard v0.4.0 path, emit `"confidence": "low"`.
2. Schema check against `analyze-output.schema.json` — invalid shape → same fallback as #1.
3. Anti-hallucination: every `ranked_skills[*].name` and `ranked_agents[*].name` MUST appear in input `candidates` (subset check). Names not in set → same fallback.

### 7. Jaccard cross-check (BEST → MULTIPLE downgrade)

If Sonnet returned `mode: INSTALL_BEST`, compute Jaccard overlap on top-2 candidate capabilities. If overlap < 0.30 → downgrade to `INSTALL_MULTIPLE` (Sonnet missed that they're complementary). Implementation arrives in Task 17.

```bash
set -euo pipefail
echo "$RAW_OUTPUT" | python "${CLAUDE_PLUGIN_ROOT}/scripts/analyze_candidates_sonnet.py" cross-check \
    --candidates "$CANDIDATES_JSON"
```

### 8. Cache + return

Write to classify-cache.json keyed by sha256(task + sorted(candidate_names)) with TTL 86400s (24h):

```bash
set -euo pipefail
echo "$FINAL_OUTPUT" | python "${CLAUDE_PLUGIN_ROOT}/scripts/analyze_candidates_sonnet.py" cache-put \
    --task "$TASK" --candidates "$CANDIDATES_JSON" --ttl 86400
```

Return `FINAL_OUTPUT` (the validated, possibly-downgraded Sonnet output) to the caller.

## Failure modes

| Scenario | Handling |
| --- | --- |
| Sonnet returns invalid JSON | Retry once with stricter wording. Second failure → fallback BM25+Jaccard, `confidence: "low"`. |
| Sonnet hallucinates name | Schema validation rejects. Trigger same fallback as JSON failure. |
| Sonnet timeout (>30s) | Cancel, fallback. |
| Sonnet picks irrelevant skill #1 | Caught by Jaccard cross-check in step 7. Downgrade BEST→MULTIPLE when top-2 overlap < 0.30. |
| Cache hit | Return cached entry; skip Agent invocation entirely. |

## Guardrails

- **Read-only on index.db.** Only SELECT.
- **Schema-driven anti-hallucination.** Validation is mandatory before Jaccard.
- **Never call Agent in a loop.** Single invocation per orchestrate dispatch; retry is at most once.
- **Honor MAXV_MOCK_SONNET_RESPONSE env var in tests.** If set, the helper short-circuits Agent invocation and returns that JSON (unit tests rely on this).
- **Equal source weight.** Sonnet prompt explicitly tells the model that `source: "cheatsheet"`, `source: "fts5"`, and `source: "both"` carry equal weight. Don't bias rank by `priority_hint` or `bm25_score` — those exist only for traceability in the output.

## Env vars

| Var | Default | Effect |
| --- | --- | --- |
| `MAXV_CLASSIFY_CACHE_TTL` | `86400` | Cache TTL in seconds. |
| `MAXV_SONNET_TIMEOUT` | `30` | Cancel Sonnet call if it exceeds (fallback to BM25+Jaccard). |
| `MAXV_MOCK_SONNET_RESPONSE` | unset | Test-only: helper returns this JSON instead of invoking Agent. |
| `MAXV_INDEX_DB` | `~/.claude/cache/maxv-orchestration/index.db` | Override index path (tests). |
