# Plano de Implementação: MCP v4 — Deep Agent Tools

## Visão Geral

Redesenhar o MCP TecJustiça de 5 tools genéricas para 12 tools focadas seguindo padrões Deep Agent (ls, read, grep, glob). Inclui 6 migrations SQL para Full-Text Search com PostgreSQL tsvector/GIN e 4 RPCs para queries complexas.

**Spec completa**: `docs/tecjustica-mcp-v4-spec.md`

---

## Fase 0: Migrations SQL no Supabase

Aplicar 6 migrations via Supabase MCP (`apply_migration`). As RPCs 3-6 dependem das colunas FTS das migrations 1-2.

**Projeto Supabase**: `pglghyznirxkewcmuhvp`

### Tarefas

- [x] Migration 1: `add_fts_documents` — coluna `fts tsvector GENERATED` + índice GIN em `documents`
- [x] Migration 2: `add_fts_movimentacoes` — coluna `fts tsvector GENERATED` + índice GIN em `movimentacoes`
- [x] Migration 3: `create_rpc_buscar_documentos` — RPC multi-modo (fulltext/regex/ilike) com snippets
- [x] Migration 4: `create_rpc_buscar_movimentacoes` — RPC full-text em movimentações
- [x] Migration 5: `create_rpc_stats_documentos` — RPC agregação por tipo/sigilo/período
- [x] Migration 6: `create_rpc_localizar_no_documento` — RPC posições + snippets de termo em 1 doc
- [x] Verificar migrations com queries de teste via `execute_sql`

### Detalhes Técnicos

**Migration 1 — FTS Documents**:
```sql
ALTER TABLE public.documents
ADD COLUMN IF NOT EXISTS fts tsvector
GENERATED ALWAYS AS (
  to_tsvector('portuguese',
    coalesce(tipo_nome, '') || ' ' ||
    coalesce(nome, '') || ' ' ||
    coalesce(conteudo, '')
  )
) STORED;

CREATE INDEX IF NOT EXISTS idx_documents_fts
ON public.documents USING gin(fts);

CREATE EXTENSION IF NOT EXISTS pg_trgm;
```

**Migration 2 — FTS Movimentações**:
```sql
ALTER TABLE public.movimentacoes
ADD COLUMN IF NOT EXISTS fts tsvector
GENERATED ALWAYS AS (
  to_tsvector('portuguese',
    coalesce(descricao, '') || ' ' ||
    coalesce(tipo_nome, '') || ' ' ||
    coalesce(tipo_descricao, '')
  )
) STORED;

CREATE INDEX IF NOT EXISTS idx_movimentacoes_fts
ON public.movimentacoes USING gin(fts);
```

**Migration 3 — RPC buscar_documentos**:
```sql
CREATE OR REPLACE FUNCTION public.buscar_documentos(
  p_termo text,
  p_modo text DEFAULT 'fulltext',
  p_numero_processo text DEFAULT NULL,
  p_tipo_nome text DEFAULT NULL,
  p_limite int DEFAULT 20,
  p_offset int DEFAULT 0
)
RETURNS TABLE(
  id uuid,
  numero_processo varchar,
  tipo_nome varchar,
  nome varchar,
  sequencia int,
  data_juntada timestamptz,
  nivel_sigilo varchar,
  tamanho_texto int,
  relevancia float4,
  snippet text,
  total_count bigint
)
LANGUAGE plpgsql
SECURITY DEFINER
AS $func$
BEGIN
  RETURN QUERY
  WITH resultados AS (
    SELECT
      d.id,
      d.numero_processo,
      d.tipo_nome,
      d.nome,
      d.sequencia,
      d.data_juntada,
      d.nivel_sigilo,
      d.tamanho_texto,
      CASE p_modo
        WHEN 'fulltext' THEN
          ts_rank(d.fts, websearch_to_tsquery('portuguese', p_termo))
        ELSE 0.0
      END::float4 AS relevancia,
      CASE p_modo
        WHEN 'fulltext' THEN
          ts_headline('portuguese', d.conteudo,
            websearch_to_tsquery('portuguese', p_termo),
            'StartSel=>>>, StopSel=<<<, MaxWords=35, MinWords=15, MaxFragments=2')
        WHEN 'regex' THEN
          substring(d.conteudo FROM
            greatest(1, position(
              substring(d.conteudo FROM p_termo) IN d.conteudo
            ) - 100)
            FOR 300)
        ELSE
          substring(d.conteudo FROM
            greatest(1, position(lower(p_termo) IN lower(d.conteudo)) - 100)
            FOR 300)
      END AS snippet,
      count(*) OVER() AS total_count
    FROM public.documents d
    WHERE
      (p_numero_processo IS NULL OR d.numero_processo = p_numero_processo)
      AND (p_tipo_nome IS NULL OR d.tipo_nome ILIKE '%' || p_tipo_nome || '%')
      AND CASE p_modo
        WHEN 'fulltext' THEN
          d.fts @@ websearch_to_tsquery('portuguese', p_termo)
        WHEN 'regex' THEN
          d.conteudo ~* p_termo
        ELSE
          d.conteudo ILIKE '%' || p_termo || '%'
      END
      AND d.conteudo IS NOT NULL
      AND d.conteudo != ''
  )
  SELECT * FROM resultados
  ORDER BY
    CASE WHEN p_modo = 'fulltext' THEN resultados.relevancia ELSE 0 END DESC,
    resultados.sequencia DESC NULLS LAST
  LIMIT p_limite
  OFFSET p_offset;
END;
$func$;

GRANT EXECUTE ON FUNCTION public.buscar_documentos TO authenticated;
```

**Migration 4 — RPC buscar_movimentacoes**:
```sql
CREATE OR REPLACE FUNCTION public.buscar_movimentacoes(
  p_termo text,
  p_numero_processo text DEFAULT NULL,
  p_tipo_nome text DEFAULT NULL,
  p_limite int DEFAULT 30,
  p_offset int DEFAULT 0
)
RETURNS TABLE(
  id uuid,
  numero_processo varchar,
  sequencia int,
  data_hora timestamptz,
  descricao text,
  tipo_nome varchar,
  magistrado_nome varchar,
  orgao_julgador_nome varchar,
  snippet text,
  total_count bigint
)
LANGUAGE plpgsql
SECURITY DEFINER
AS $func$
BEGIN
  RETURN QUERY
  SELECT
    m.id,
    m.numero_processo,
    m.sequencia,
    m.data_hora,
    m.descricao,
    m.tipo_nome,
    m.magistrado_nome,
    m.orgao_julgador_nome,
    ts_headline('portuguese', m.descricao,
      websearch_to_tsquery('portuguese', p_termo),
      'StartSel=>>>, StopSel=<<<, MaxWords=35, MinWords=15'
    ) AS snippet,
    count(*) OVER() AS total_count
  FROM public.movimentacoes m
  WHERE
    m.fts @@ websearch_to_tsquery('portuguese', p_termo)
    AND (p_numero_processo IS NULL OR m.numero_processo = p_numero_processo)
    AND (p_tipo_nome IS NULL OR m.tipo_nome ILIKE '%' || p_tipo_nome || '%')
  ORDER BY m.data_hora DESC
  LIMIT p_limite
  OFFSET p_offset;
END;
$func$;

GRANT EXECUTE ON FUNCTION public.buscar_movimentacoes TO authenticated;
```

**Migration 5 — RPC stats_documentos**:
```sql
CREATE OR REPLACE FUNCTION public.stats_documentos(
  p_numero_processo text
)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
AS $func$
DECLARE
  resultado jsonb;
BEGIN
  SELECT jsonb_build_object(
    'numero_processo', p_numero_processo,
    'total_documentos', count(*),
    'com_conteudo', count(*) FILTER (WHERE conteudo IS NOT NULL AND conteudo != ''),
    'sem_conteudo', count(*) FILTER (WHERE conteudo IS NULL OR conteudo = ''),
    'tamanho_total_chars', coalesce(sum(tamanho_texto), 0),
    'tamanho_total_bytes', coalesce(sum(tamanho_bytes), 0),
    'por_tipo', (
      SELECT jsonb_object_agg(
        coalesce(sub.tipo_nome, 'Sem tipo'),
        jsonb_build_object('quantidade', sub.qtd, 'chars_total', sub.chars)
      )
      FROM (
        SELECT d2.tipo_nome, count(*) AS qtd, coalesce(sum(d2.tamanho_texto), 0) AS chars
        FROM public.documents d2
        WHERE d2.numero_processo = p_numero_processo
        GROUP BY d2.tipo_nome
        ORDER BY qtd DESC
      ) sub
    ),
    'por_sigilo', (
      SELECT jsonb_object_agg(
        coalesce(sub2.nivel_sigilo, 'NAO_INFORMADO'),
        sub2.qtd
      )
      FROM (
        SELECT d3.nivel_sigilo, count(*) AS qtd
        FROM public.documents d3
        WHERE d3.numero_processo = p_numero_processo
        GROUP BY d3.nivel_sigilo
      ) sub2
    ),
    'periodo', jsonb_build_object(
      'primeiro_documento', min(data_juntada),
      'ultimo_documento', max(data_juntada)
    )
  ) INTO resultado
  FROM public.documents
  WHERE numero_processo = p_numero_processo;

  RETURN resultado;
END;
$func$;

GRANT EXECUTE ON FUNCTION public.stats_documentos TO authenticated;
```

**Migration 6 — RPC localizar_no_documento**:
```sql
CREATE OR REPLACE FUNCTION public.localizar_no_documento(
  p_document_id uuid,
  p_termo text,
  p_contexto_chars int DEFAULT 100,
  p_max_ocorrencias int DEFAULT 30
)
RETURNS jsonb
LANGUAGE sql
SECURITY DEFINER
AS $func$
  WITH doc AS (
    SELECT
      conteudo,
      lower(conteudo) AS conteudo_lower,
      coalesce(tamanho_texto, length(conteudo)) AS tamanho
    FROM public.documents
    WHERE id = p_document_id
      AND conteudo IS NOT NULL
      AND conteudo != ''
  ),
  posicoes AS (
    SELECT DISTINCT ON (pos)
      bloco - 1 + position(
        lower(p_termo) IN substring(doc.conteudo_lower FROM bloco)
      ) - 1 AS pos
    FROM doc,
         generate_series(1, doc.tamanho, greatest(1, length(p_termo))) AS bloco
    WHERE position(lower(p_termo) IN substring(doc.conteudo_lower FROM bloco)) > 0
    ORDER BY pos
    LIMIT p_max_ocorrencias
  ),
  com_snippet AS (
    SELECT
      p.pos AS posicao,
      substring(
        (SELECT conteudo FROM doc)
        FROM greatest(1, p.pos + 1 - p_contexto_chars)
        FOR (p_contexto_chars * 2 + length(p_termo))
      ) AS snippet
    FROM posicoes p
    ORDER BY p.pos
  )
  SELECT jsonb_build_object(
    'document_id', p_document_id,
    'termo', p_termo,
    'total_ocorrencias', (SELECT count(*) FROM com_snippet),
    'tamanho_documento', (SELECT tamanho FROM doc),
    'ocorrencias', coalesce(
      (SELECT jsonb_agg(
        jsonb_build_object('posicao', posicao, 'snippet', snippet)
        ORDER BY posicao
      ) FROM com_snippet),
      '[]'::jsonb
    )
  );
$func$;

GRANT EXECUTE ON FUNCTION public.localizar_no_documento TO authenticated;
```

**Queries de verificação** (rodar após cada migration):
```sql
-- Verificar FTS documents
SELECT count(*) FROM documents WHERE fts IS NOT NULL;

-- Verificar FTS movimentacoes
SELECT count(*) FROM movimentacoes WHERE fts IS NOT NULL;

-- Testar buscar_documentos
SELECT * FROM buscar_documentos('sentença', 'fulltext', NULL, NULL, 3, 0);

-- Testar buscar_movimentacoes
SELECT * FROM buscar_movimentacoes('audiência', NULL, NULL, 3, 0);

-- Testar stats_documentos (usar numero real)
SELECT stats_documentos((SELECT numero_processo FROM processos LIMIT 1));

-- Testar localizar_no_documento (usar doc real)
SELECT localizar_no_documento(
  (SELECT id FROM documents WHERE conteudo IS NOT NULL LIMIT 1),
  'processo', 50, 5
);
```

---

## Fase 1: Criar novas tools de Navegação

5 tools no arquivo `tools/navegacao.py`: listar_processos, visao_geral_processo, ls_documentos, ls_movimentacoes, read_documento.

### Tarefas

- [x] Criar `tools/navegacao.py` com `register_navegacao_tools(mcp)` [complexo]
  - [x] Implementar `listar_processos(busca?, limite=20, offset=0)`
  - [x] Implementar `visao_geral_processo(numero_processo)`
  - [x] Implementar `ls_documentos(numero_processo, limite=30, offset=0, ordem="recentes")`
  - [x] Implementar `ls_movimentacoes(numero_processo, limite=30, offset=0, ordem="recentes")`
  - [x] Implementar `read_documento(document_id, max_chars=10000, offset=0)`

### Detalhes Técnicos

**Arquivo**: `tools/navegacao.py`

**Padrão base de cada tool**:
```python
from fastmcp import Context, FastMCP
import asyncio
import logging
import time
from typing import Any, Literal

from services.supabase_client import get_supabase_client
from services.usage_logger import get_usage_logger, ms_since
from tools._helpers import _get_current_user

logger = logging.getLogger("tecjustica-mcp.navegacao")

def register_navegacao_tools(mcp: FastMCP) -> None:
    @mcp.tool(annotations={"readOnlyHint": True, "openWorldHint": False})
    async def nome_tool(..., ctx: Context) -> dict[str, Any]:
        """Docstring com QUANDO USAR / QUANDO NÃO USAR."""
        _start = time.perf_counter()
        user_id, user_email = _get_current_user()
        try:
            sb = get_supabase_client().client
            # ... queries
            resultado = ...
            try:
                get_usage_logger().log_tool_call(
                    tool_name="nome_tool", duration_ms=ms_since(_start),
                    user_id=user_id, user_email=user_email, success=True,
                    params={...}
                )
            except Exception:
                pass
            return resultado
        except Exception as e:
            try:
                get_usage_logger().log_tool_call(
                    tool_name="nome_tool", duration_ms=ms_since(_start),
                    user_id=user_id, user_email=user_email, success=False,
                    error_message=str(e)
                )
            except Exception:
                pass
            return {"erro": str(e), "dica": "..."}
```

**`listar_processos`**:
- Se `busca` informado: chamar RPC `buscar_processos_por_parte` (já existe)
- Se não: query em `jobs` table (status completed) + join `processos` para metadados
- Retornar: numero_processo, classe_descricao, sigla_tribunal, data_ajuizamento, valor_causa, partes (resumo 2-3 nomes)
- Paginação: `{total, limit, offset, has_more}`

**`visao_geral_processo`**:
- 3 queries paralelas via `asyncio.gather()`:
  1. `processos` table: `select("*").eq("numero_processo", ...).maybe_single()`
  2. RPC `stats_documentos`: `sb.rpc("stats_documentos", {"p_numero_processo": ...})`
  3. `movimentacoes` table: `select("sequencia, data_hora, tipo_nome, descricao, magistrado_nome").eq("numero_processo", ...).order("data_hora", desc=True).limit(10)`
- Retornar: metadados, partes, assuntos, stats_documentos, ultimas_movimentacoes, dica

**`ls_documentos`**:
- Query SEM `conteudo`: `select("id, numero_processo, tipo_nome, nome, sequencia, data_juntada, tamanho_texto, tamanho_bytes, nivel_sigilo, metodo_extracao, signatarios, tipo_arquivo", count="exact")`
- `limite = min(limite, 100)`
- Ordem: `sequencia` desc/asc
- Retornar: documentos, paginacao, dica

**`ls_movimentacoes`**:
- Query: `select("id, sequencia, data_hora, tipo_nome, descricao, magistrado_nome, orgao_julgador_nome, documento_id", count="exact")`
- Truncar descricao > 300 chars
- Retornar: movimentacoes, paginacao, dica

**`read_documento`**:
- Query COM `conteudo`: `select("id, numero_processo, tipo_nome, nome, sequencia, data_juntada, nivel_sigilo, tamanho_texto, metodo_extracao, signatarios, conteudo").eq("id", ...).maybe_single()`
- Slice no Python: `conteudo[offset:offset + max_chars]`
- **NÃO usa eviction LLM** — agente controla tamanho
- Retornar: metadados (sem conteudo), conteudo (trecho), leitura: {offset, chars_retornados, tamanho_total, tem_mais, proximo_offset}

---

## Fase 2: Criar novas tools de Busca

4 tools no arquivo `tools/busca.py`: grep_documentos, grep_movimentacoes, glob_documentos, localizar_no_documento.

### Tarefas

- [x] Criar `tools/busca.py` com `register_busca_tools(mcp)` [complexo]
  - [x] Implementar `grep_documentos(padrao, modo="fulltext", numero_processo?, tipo_nome?, limite=20, offset=0)`
  - [x] Implementar `grep_movimentacoes(padrao, numero_processo?, tipo_nome?, limite=30, offset=0)`
  - [x] Implementar `glob_documentos(numero_processo, padrao_nome?, padrao_tipo?, nivel_sigilo?, limite=50, offset=0)`
  - [x] Implementar `localizar_no_documento(document_id, termo, contexto_chars=100, max_ocorrencias=30)`

### Detalhes Técnicos

**Arquivo**: `tools/busca.py`

**`grep_documentos`** — Tool mais importante do MCP:
```python
@mcp.tool(annotations={"readOnlyHint": True, "openWorldHint": False})
async def grep_documentos(
    padrao: str,
    modo: Literal["fulltext", "regex", "ilike"] = "fulltext",
    numero_processo: str | None = None,
    tipo_nome: str | None = None,
    limite: int = 20,
    offset: int = 0,
    ctx: Context = None,
) -> dict[str, Any]:
```
- Chama `sb.rpc("buscar_documentos", {"p_termo": padrao, "p_modo": modo, "p_numero_processo": numero_processo, "p_tipo_nome": tipo_nome, "p_limite": limite, "p_offset": offset})`
- Total vem de `dados[0]["total_count"]` se dados existem
- Formatar cada match: id, numero_processo, tipo_nome, nome, sequencia, data_juntada, nivel_sigilo, tamanho_texto, relevancia, snippet
- Retornar: matches, paginacao, dica

**`grep_movimentacoes`**:
- Chama `sb.rpc("buscar_movimentacoes", {...})`
- Formatar: id, numero_processo, sequencia, data_hora, tipo_nome, descricao (truncada 300), magistrado_nome, orgao_julgador_nome, snippet
- Retornar: matches, paginacao

**`glob_documentos`**:
- Query direta na tabela `documents` SEM `conteudo`
- Filtros opcionais ILIKE: `query.ilike("nome", f"%{padrao_nome}%")`, `query.ilike("tipo_nome", f"%{padrao_tipo}%")`
- Filtro eq: `nivel_sigilo`
- `limite = min(limite, 100)`
- Retornar: documentos, paginacao, dica

**`localizar_no_documento`**:
- Chama `sb.rpc("localizar_no_documento", {"p_document_id": document_id, "p_termo": termo, "p_contexto_chars": contexto_chars, "p_max_ocorrencias": max_ocorrencias})`
- Retornar: document_id, termo, total_ocorrencias, tamanho_documento, ocorrencias, dica

---

## Fase 3: Criar tool de Estatísticas

1 tool no arquivo `tools/estatisticas.py`.

### Tarefas

- [x] Criar `tools/estatisticas.py` com `register_estatisticas_tools(mcp)`
  - [x] Implementar `stats_documentos(numero_processo)`

### Detalhes Técnicos

**Arquivo**: `tools/estatisticas.py`

**`stats_documentos`**:
- Chama `sb.rpc("stats_documentos", {"p_numero_processo": numero_processo})`
- Se resultado vazio: retornar `{erro, dica}`
- Se ok: retornar dados do RPC diretamente (já é JSON estruturado)

---

## Fase 4: Atualizar arquivos existentes

### Tarefas

- [x] Atualizar `tools/__init__.py` — novos imports e exports
- [x] Atualizar `tools/_helpers.py` — novo formato de erro, mensagens atualizadas
- [x] Mover `tools/explorar.py` → `tools/_legacy/explorar.py`
- [x] Mover `tools/consultas.py` → `tools/_legacy/consultas.py`
- [x] Atualizar `server.py` — novos imports, registros, prompts, versão [complexo]
  - [x] Atualizar imports e register calls
  - [x] Atualizar 6 prompts com nomes das novas tools
  - [x] Reescrever prompt `guia_ferramentas` com as 12 tools
  - [x] Atualizar versão para `"4.0.0"`

### Detalhes Técnicos

**`tools/__init__.py`**:
```python
from .navegacao import register_navegacao_tools
from .busca import register_busca_tools
from .estatisticas import register_estatisticas_tools
from .analise import register_analise_tools
from .user import register_user_tools

__all__ = [
    "register_navegacao_tools",
    "register_busca_tools",
    "register_estatisticas_tools",
    "register_analise_tools",
    "register_user_tools",
]
```

**`tools/_helpers.py` — Mudanças**:
- Atualizar `MSG_DOCUMENTO_NAO_ENCONTRADO`: trocar `explorar_processo()` por `ls_documentos()`
- Atualizar `_smart_response` nota: trocar `consultar_documentos` por `read_documento`
- Manter tudo mais intacto (usado por `analisar`)

**`server.py` — Imports**:
```python
from tools import (
    register_navegacao_tools,
    register_busca_tools,
    register_estatisticas_tools,
    register_analise_tools,
    register_user_tools,
)
```

**`server.py` — Registros**:
```python
register_navegacao_tools(mcp)
register_busca_tools(mcp)
register_estatisticas_tools(mcp)
register_analise_tools(mcp)
register_user_tools(mcp)
```

**`server.py` — Mapeamento de prompts** (referências antigas → novas):
| Referência antiga | Referência nova |
|---|---|
| `explorar_processo("...")` | `visao_geral_processo("...")` |
| `explorar_processo(busca="...")` | `listar_processos(busca="...")` |
| `explorar_processo()` | `listar_processos()` |
| `consultar_documentos(document_id="...")` | `read_documento("...")` |
| `consultar_documentos(..., busca="...")` | `grep_documentos("...")` |
| `consultar_documentos(..., importantes=True)` | `glob_documentos(..., padrao_tipo="Sentença")` |
| `consultar_movimentacoes(...)` | `ls_movimentacoes(...)` ou `grep_movimentacoes(...)` |

**`server.py` — Prompt `guia_ferramentas` reescrito**:
```
# GUIA DE FERRAMENTAS - TecJustica MCP v4

**12 tools. Padrão Deep Agent: descobrir → navegar → buscar → analisar.**

## NAVEGAÇÃO
1. listar_processos() — Descobrir processos, buscar por parte
2. visao_geral_processo("CNJ") — Entrada: metadados + partes + stats + movs
3. ls_documentos("CNJ") — Listar docs sem conteúdo (ls)
4. ls_movimentacoes("CNJ") — Timeline de movimentações (ls)
5. read_documento("uuid") — Ler conteúdo controlado (cat/read)

## BUSCA
6. grep_documentos("termo") — Full-text/regex/ilike no conteúdo (grep)
7. grep_movimentacoes("termo") — Full-text em movimentações (grep)
8. glob_documentos("CNJ", padrao_tipo="Sentença") — Filtrar por nome/tipo (glob)
9. localizar_no_documento("uuid", "termo") — Posições em 1 doc (bridge grep→read)

## ANÁLISE
10. stats_documentos("CNJ") — Contagens agregadas, zero texto
11. analisar("CNJ", "pergunta") — Análise com LLM
12. whoami — Identidade do usuário

## FLUXO TÍPICO
listar_processos → visao_geral → grep_documentos → localizar → read_documento → analisar
```

**Arquivos a NÃO modificar**: `tools/analise.py`, `tools/user.py`, `services/supabase_client.py`, `services/llm_service.py`, `services/usage_logger.py`, `models/schemas.py`

---

## Fase 5: Lint, Format, Type Check

### Tarefas

- [x] Rodar `ruff check --fix .`
- [x] Rodar `ruff format .`
- [x] Rodar `mypy .`
- [x] Corrigir erros encontrados

### Detalhes Técnicos

```bash
cd /home/marcos/projetos/server-mcp3
ruff check --fix . && ruff format . && mypy .
```

Atenção ao padrão de imports: FastMCP + Context devem ser importados no top-level (NÃO em `TYPE_CHECKING`). Ver `tools/user.py` como referência.

---

## Fase 6: Verificação em Produção

### Tarefas

- [ ] Testar todas as migrations via `execute_sql` com queries reais
- [ ] Testar fluxo completo via curl (token → initialize → tools/list → cada tool)
- [ ] Verificar que `tools/list` retorna exatamente 12 tools
- [ ] Testar o fluxo Deep Agent: listar → visao_geral → grep → localizar → read → analisar

### Detalhes Técnicos

**Fluxo de teste end-to-end**:
```bash
# 1. Token
ANON_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBnbGdoeXpuaXJ4a2V3Y211aHZwIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjYzNjk4MTgsImV4cCI6MjA4MTk0NTgxOH0.hac05fv66miyiRqzwBIZYSA8gjA8dcRFOJMs3wSqWpA"
TOKEN=$(curl -s -X POST "https://pglghyznirxkewcmuhvp.supabase.co/auth/v1/token?grant_type=password" \
  -H "apikey: $ANON_KEY" -H "Content-Type: application/json" \
  -d '{"email":"marcosmarf27@gmail.com","password":"224351*27"}' \
  | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")

# 2-4. Initialize + Notify + tools/list (ver CLAUDE.md para detalhes)

# 5. Testar cada tool nova
# listar_processos, visao_geral_processo, ls_documentos, ls_movimentacoes,
# read_documento, grep_documentos, grep_movimentacoes, glob_documentos,
# localizar_no_documento, stats_documentos
```

**Performance targets**:
- `ls_documentos`, `ls_movimentacoes`, `glob_documentos`: < 500ms
- `grep_documentos` (fulltext): < 2s
- `stats_documentos`: < 1s
- `visao_geral_processo`: < 2s (3 queries paralelas)
