---
name: install-agent
description: Installs a Claude Code subagent on-demand from the FTS5 catalog (`~/.claude/cache/maxv-orchestration/index.db`) into `~/.claude/agents/<name>.md` (user scope) or `.claude/agents/<name>.md` (project scope). Twin of `install-skill` for agents. Writes `.maxv-source-<name>.json` sidecar for version tracking. Used by `orchestrate` Phase 7.3 when an agent is needed but not present locally, enabling lazy-load model that bypasses the cumulative-description token budget.
when_to_use: |
  Trigger phrases: "instale o subagente <name>", "install agent <name>",
  "/maxvision-orchestration:install-agent <name>".
  Auto-invoked by `orchestrate` Phase 7.3 when `resolve_local` returns NOT_FOUND.
disable-model-invocation: true
allowed-tools: Read Bash(test *) Bash(jq *) Bash(gh api *) Bash(gh auth status) Bash(git clone *) Bash(git -C * sparse-checkout *) Bash(git -C * fetch *) Bash(git -C * rev-parse *) Bash(git -C * checkout *) Bash(sqlite3 *) Bash(date *) Bash(mkdir -p *) Bash(cp *) Bash(rm -rf *) Bash(cat *) Bash(test -f *) Bash(test -d *) Bash(sha256sum *) Bash(mktemp -d)
---

# Install agent

Argument: `$ARGUMENTS` — `<agent-name> [--scope=user|project] [--force] [--method=sparse_clone|raw_url|from_cache]`.

> **Hard rule:** this skill writes to disk. Confirm with the user before executing any install (unless invoked by `orchestrate` with batch approval).
>
> **Lazy-load model:** the goal of this skill is to keep `enabledPlugins` minimal so the cumulative agent description budget (15k token warning) stays low. Only agents the user actually invokes get materialized to disk; everything else lives in the FTS5 catalog (zero session-token cost).

## Workflow

### 1. Preflight

```bash
set -euo pipefail
INDEX_DB=~/.claude/cache/maxv-orchestration/index.db
test -f "$INDEX_DB" || {
  printf '%s\n' '{"error":"index missing","suggestion":"Run /maxvision-orchestration:index-catalog first"}'
  exit 1
}
gh auth status >/dev/null 2>&1 || {
  printf '%s\n' '{"error":"gh not authenticated","suggestion":"Run gh auth login"}'
  exit 1
}
```

### 2. Parse arguments

```bash
set -euo pipefail
AGENT_NAME=""
SCOPE="user"
FORCE=0
METHOD=""
for arg in $ARGUMENTS; do
  case "$arg" in
    --scope=*) SCOPE="${arg#--scope=}" ;;
    --force) FORCE=1 ;;
    --method=*) METHOD="${arg#--method=}" ;;
    --*) printf '%s\n' "{\"error\":\"unknown flag: $arg\"}"; exit 2 ;;
    *) AGENT_NAME="$arg" ;;
  esac
done
test -n "$AGENT_NAME" || { printf '%s\n' '{"error":"agent name required"}'; exit 2; }
test "$SCOPE" = "user" -o "$SCOPE" = "project" || { printf '%s\n' '{"error":"scope must be user or project"}'; exit 2; }
```

### 3. Look up source in FTS5

```bash
set -euo pipefail
META=$(sqlite3 -json "$INDEX_DB" "SELECT i.name, i.path, i.last_commit_sha, i.model, s.id as source_id, s.url, s.default_branch, s.tier, s.license FROM item i JOIN source s ON i.source_id = s.id WHERE i.kind='agent' AND i.name='$AGENT_NAME' LIMIT 1;")
test "$META" != "[]" -a -n "$META" || {
  printf '%s\n' "{\"error\":\"agent not in catalog\",\"agent\":\"$AGENT_NAME\",\"suggestion\":\"Run /maxvision-orchestration:discover-agent <keyword> to find candidates\"}"
  exit 3
}

REPO_URL=$(echo "$META" | jq -r '.[0].url')
BRANCH=$(echo "$META" | jq -r '.[0].default_branch // "main"')
SOURCE_PATH=$(echo "$META" | jq -r '.[0].path')
INDEXED_SHA=$(echo "$META" | jq -r '.[0].last_commit_sha')
TIER=$(echo "$META" | jq -r '.[0].tier')
LICENSE=$(echo "$META" | jq -r '.[0].license // "unknown"')
SOURCE_ID=$(echo "$META" | jq -r '.[0].source_id')
```

### 4. Resolve destination

```bash
set -euo pipefail
if [ "$SCOPE" = "user" ]; then
  DEST_DIR=~/.claude/agents
else
  DEST_DIR=.claude/agents
fi
DEST_FILE="$DEST_DIR/$AGENT_NAME.md"
SIDECAR="$DEST_DIR/.maxv-source-$AGENT_NAME.json"
mkdir -p "$DEST_DIR"

# Collision check
if [ -f "$DEST_FILE" ] && [ "$FORCE" -ne 1 ]; then
  printf '%s\n' "{\"error\":\"already installed\",\"path\":\"$DEST_FILE\",\"suggestion\":\"Use update-component to refresh, or pass --force to overwrite\"}"
  exit 4
fi
```

### 5. Show install plan + confirmation

If invoked by `orchestrate` with batch approval, skip the prompt. Otherwise:

```
About to install agent: <name>
  Source:   <repo URL>
  Branch:   <branch>
  Path:     <source_path>
  Commit:   <sha (short)>
  Tier:     <tier>
  License:  <license>
  Method:   <sparse_clone|raw_url>
  Target:   <DEST_FILE>
  Sidecar:  <SIDECAR>

Proceed? [sim/skip]
```

Wait for `sim`. On `skip`, exit 0 with `{"status":"skipped"}`.

### 6. Execute install

#### Method A — sparse_clone (default)

Best for repos already used (cached pack) and for full commit-sha capture.

```bash
set -euo pipefail
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT

git clone --depth 1 --filter=blob:none --sparse --branch "$BRANCH" "$REPO_URL" "$TMP/repo"
# CRITICAL: sparse-checkout requires --no-cone for single-file paths.
# Per empirical test on git for Windows 2.47, leading slash breaks the resolution
# despite docs recommending it. Use the path without leading slash and suppress
# the NON-CONE warning to stderr/null.
git -C "$TMP/repo" sparse-checkout set --no-cone "$SOURCE_PATH" 2>/dev/null
test -f "$TMP/repo/$SOURCE_PATH" || {
  printf '%s\n' "{\"error\":\"source path not in repo after sparse-checkout\",\"path\":\"$SOURCE_PATH\"}"
  exit 5
}
COMMIT_SHA=$(git -C "$TMP/repo" rev-parse HEAD)
cp "$TMP/repo/$SOURCE_PATH" "$DEST_FILE"
```

#### Method B — raw_url (fallback when sparse_clone fails)

Single-file fetch via `gh api`. No git history.

```bash
set -euo pipefail
OWNER_REPO="${REPO_URL#https://github.com/}"
OWNER_REPO="${OWNER_REPO%.git}"
COMMIT_SHA=$(gh api "repos/$OWNER_REPO/commits/$BRANCH" --jq '.sha')
gh api "repos/$OWNER_REPO/contents/$SOURCE_PATH?ref=$COMMIT_SHA" --jq '.content' | base64 -d > "$DEST_FILE"
test -s "$DEST_FILE" || {
  printf '%s\n' "{\"error\":\"empty download\"}"
  exit 5
}
```

#### Method C — from_cache (NEW v0.4.0 — fastest path)

When the agent file exists in `~/.claude/plugins/cache/<marketplace>/<plugin>/<v>/<name>.md` (plugin was previously installed but is now disabled in `enabledPlugins`), copy directly from disk. Zero network calls. Commit SHA pulled from FTS5 catalog (which was indexed from the same source).

```bash
set -euo pipefail
# Find the cached file. Two layouts: <plugin>/<v>/agents/<name>.md OR <plugin>/<v>/<name>.md (voltagent flat)
CANDIDATES=$(find ~/.claude/plugins/cache -name "$AGENT_NAME.md" -type f 2>/dev/null)
test -n "$CANDIDATES" || {
  printf '%s\n' "{\"error\":\"agent not in any plugin cache\",\"agent\":\"$AGENT_NAME\"}"
  exit 5
}
# Filter by frontmatter `name:` (disambiguate dupes; some plugins ship same name in agents/ and root)
CACHE_PATH=$(grep -lE "^name:[ \t]*${AGENT_NAME}([ \t]|$)" $CANDIDATES 2>/dev/null | head -1)
test -n "$CACHE_PATH" || {
  printf '%s\n' "{\"error\":\"name match failed in cache files\",\"candidates_count\":$(echo "$CANDIDATES" | wc -l)}"
  exit 5
}

# Use FTS5 commit_sha as authoritative version
COMMIT_SHA="$INDEXED_SHA"
cp "$CACHE_PATH" "$DEST_FILE"
```

#### Method selection (with auto-prefer for from_cache)

```bash
set -euo pipefail
if [ -z "$METHOD" ]; then
  # Auto: if file exists in any plugin cache, use from_cache (fast). Otherwise sparse_clone.
  CACHE_HIT=$(find ~/.claude/plugins/cache -name "$AGENT_NAME.md" -type f 2>/dev/null | head -1)
  if [ -n "$CACHE_HIT" ]; then
    EFFECTIVE_METHOD="from_cache"
  else
    EFFECTIVE_METHOD="sparse_clone"
  fi
else
  EFFECTIVE_METHOD="$METHOD"
fi

case "$EFFECTIVE_METHOD" in
  sparse_clone) ;;  # method A (above)
  raw_url) ;;       # method B (above)
  from_cache) ;;    # method C (above)
  *) printf '%s\n' "{\"error\":\"unknown method: $EFFECTIVE_METHOD\"}"; exit 2 ;;
esac
```

**Order of preference** (when `--method` not specified):
1. `from_cache` — if plugin cache has the file (zero network, fastest, exact match with FTS5-indexed commit)
2. `sparse_clone` — fresh install from upstream (default for novel agents)
3. `raw_url` — fallback when sparse_clone fails (e.g., shallow clone unsupported)

### 7. Verify installed file is a valid agent

```bash
set -euo pipefail
head -1 "$DEST_FILE" | grep -q '^---$' || {
  printf '%s\n' "{\"error\":\"installed file lacks YAML frontmatter\"}"
  rm -f "$DEST_FILE"
  exit 5
}
grep -q '^name:[ \t]*'"$AGENT_NAME"'\b' "$DEST_FILE" || {
  printf '%s\n' "{\"warning\":\"frontmatter name does not match arg name\",\"file\":\"$DEST_FILE\"}"
  # Continue anyway — upstream may have renamed; user can override.
fi
```

### 8. Write sidecar

```bash
set -euo pipefail
NOW_ISO=$(date -u +%Y-%m-%dT%H:%M:%SZ)
SHA256_FILE=$(sha256sum "$DEST_FILE" | awk '{print $1}')

cat > "$SIDECAR" <<EOF
{
  "\$schema_version": "1.0.0",
  "kind": "agent",
  "agent_name": "$AGENT_NAME",
  "repo": "$REPO_URL",
  "branch": "$BRANCH",
  "source_path": "$SOURCE_PATH",
  "commit_sha": "$COMMIT_SHA",
  "indexed_sha": "$INDEXED_SHA",
  "file_sha256": "$SHA256_FILE",
  "tier": "$TIER",
  "license": "$LICENSE",
  "source_id": "$SOURCE_ID",
  "install_method": "$EFFECTIVE_METHOD",
  "scope": "$SCOPE",
  "installed_via": "maxvision-orchestration",
  "installed_at": "$NOW_ISO",
  "last_check": "$NOW_ISO"
}
EOF
```

### 9. Update version-check cache

```bash
set -euo pipefail
CACHE=~/.claude/cache/maxv-orchestration/version-check.json
mkdir -p "$(dirname "$CACHE")"
test -f "$CACHE" || echo '{}' > "$CACHE"
TMP_CACHE=$(mktemp)
jq --arg name "$AGENT_NAME" --arg sha "$COMMIT_SHA" --arg now "$NOW_ISO" \
   '.["agent:"+$name] = {kind:"agent", local_sha:$sha, remote_sha:$sha, last_check_iso:$now, status:"up_to_date", ttl_seconds:3600}' \
   "$CACHE" > "$TMP_CACHE" && mv "$TMP_CACHE" "$CACHE"
```

### 10. Report

```
✓ Installed agent: <AGENT_NAME>
  Source:    <REPO_URL>@<BRANCH>
  Path:      <SOURCE_PATH>
  Commit:    <COMMIT_SHA>
  Target:    <DEST_FILE>
  Sidecar:   <SIDECAR>
  Sha256:    <SHA256_FILE>
  Method:    <EFFECTIVE_METHOD>
  Scope:     <SCOPE>

⚠️  Hot-reload not supported in Claude Code 2.1.x.
   The agent is now on disk and will be available natively in the NEXT session.
   For SAME-session use, the orchestrator falls back to general-purpose-injection
   (Phase 7.5 dispatch dual-path).
```

Append to log `~/.claude/projects/<workspace>/maxv-orchestration.log`:

```
2026-05-04T22:30:00Z  install-agent  python-pro  sparse_clone  VoltAgent/...@6f804f0  ok  scope=user
```

### 11. Output JSON for orchestrator consumption

```json
{
  "status": "installed",
  "agent_name": "<name>",
  "path": "<DEST_FILE>",
  "sidecar": "<SIDECAR>",
  "commit_sha": "<COMMIT_SHA>",
  "scope": "<SCOPE>",
  "hot_reload_supported": false,
  "next_session_native": true
}
```

## Guardrails

- **Single confirmation per install.** Even if `orchestrate` passed batch approval, tier_4 sources still require explicit per-line `sim`.
- **Never overwrite without `--force`.** If `<DEST_FILE>` exists, abort with status code 4 and recommend `update-component`.
- **Never write outside `~/.claude/agents/` or `<project>/.claude/agents/`.** Path traversal in `$AGENT_NAME` rejected at step 2 (validate `[a-zA-Z0-9_-]+` only).
- **Agent name validation.** Reject names containing `/`, `..`, or shell metacharacters.
- **License surface.** If `tier=tier_4` or `license=source-available`, show explicit warning at step 5.
- **Atomic write.** `cp` the file in a single op; if any subsequent step fails, leave file in place but mark sidecar `partial: true`. (Orchestrator can retry.)
- **No silent overwrite of sidecar.** If `<SIDECAR>` exists with different `commit_sha`, that's the upgrade path → caller should use `update-component`, not `install-agent --force`.
- **Path validation:** `AGENT_NAME` must match `^[a-zA-Z][a-zA-Z0-9_-]*$`; reject otherwise.

## Error codes

| Exit | Meaning |
| ---: | --- |
| 0 | success or user-skipped |
| 1 | preflight failed (no index, no gh auth) |
| 2 | bad args |
| 3 | agent not in catalog |
| 4 | already installed (use update-component or --force) |
| 5 | install execution failed (clone/fetch/verify) |
