# AGENTS.md

> **Capa agentica (MCP)**: para uso desde Claude Code u otros clientes LLM, este repo expone un servidor MCP stdio en `mcp_server/` con tools tipadas (defaults dry-run, `confirm_token` para aplicar, rollback vía `script_audit`). Ver [docs/AGENT_TOOLS.md](docs/AGENT_TOOLS.md) para el catálogo y recetas. Manifest navegable en `generated/agent/tools_manifest.json`.

## Commands
- Install deps with `python -m pip install -r requirements.txt`.
- Run the local app with `python main.py`; it serves FastAPI/Uvicorn at `http://127.0.0.1:8000` with reload enabled.
- On Windows, `start.bat` runs `python main.py` in a new window and opens the browser; `stop.bat` kills any process using port `8000`.
- There is no test, lint, or formatter config in this repo. For a syntax-only check, run `python -m py_compile main.py db.py ghl_client.py sync_engine.py script_runner.py` and add specific scripts as needed.
- Run focused utility scripts directly, for example `python scripts\mp_contact_search.py <query>` or `python scripts\mp_opportunity_search.py <query-or-status>`.
- Global dashboard sync parallelism is controlled with `SYNC_ENGINE_MAX_WORKERS`; default is `12`, hard maximum is `20`. It affects the `Sincronizar Todo` button and processes multiple GHL locations in parallel.
- Dashboard batch parallelism is controlled globally with `SCRIPT_RUNNER_MAX_WORKERS`; default is `4`, hard maximum is `20`. Higher values can increase GHL `429`/timeout risk because script subprocesses do not share in-memory rate-limit state.

## App Wiring
- `main.py` is the app entrypoint; API routes call `db.py`, `sync_engine.py`, and `script_runner.py`.
- `templates/index.html` plus `static/js/app.js` and `static/css/style.css` are the single-page dashboard UI.
- `db.py` owns the local SQLite schema at `generated/data/mp_manager.sqlite` (path comes from `paths.DB_PATH`); sync writes replace per-location contacts, pipelines, and opportunities inside transactions.
- `sync_engine.py` reads `Bucéfalo - Mesa de control - API Tokens - MP.csv` at startup/sync time. The CSV must include `Location_ID`, `Nombre`, and `API_token`; tokens are cached in memory and are not stored in SQLite.
- The main brand account is hard-coded as location `GbKkBpCmKu2QmloKFHy3`; all other CSV locations are treated as branches.

## Generated Files
All dynamic outputs live under `generated/` and are sourced from `paths.py`. Never hardcode disk paths in new code — import from `paths` (or via `scripts/common.py`, which re-exports them). Layout: `generated/data/` (SQLite), `generated/reports/` (audit/dup/drift/coverage outputs), `generated/exports/` (downloads served by `/api/exports/`), `generated/logs/` (`errors.jsonl` + `script_runs/`), `generated/migrations/` (pre-destructive snapshots), `generated/browser/` (Playwright session, profile, screenshots), `generated/runtime/` (`server_info.json`, `last_mode`, in-flight bulk batches), `generated/_archive/` (legacy). The whole tree is gitignored and megaignored.

## GHL API Gotchas
- `ghl_client.py` is the executable source for GHL behavior: base URL `https://services.leadconnectorhq.com`, API version header `2021-07-28`, and bearer-token auth.
- Keep the per-token rate limiting in `GHLClient._wait_for_rate_limit`; global sync concurrency must use `SYNC_ENGINE_MAX_WORKERS` instead of hardcoded worker counts.
- Contacts paginate with `meta.nextPageUrl` and `startAfter`; do not replace this with offset pagination.
- Opportunities must be fetched with `POST /opportunities/search`; `GET /opportunities/` is documented here as returning empty/zero results.
- If GHL returns no pipelines but opportunities have pipeline IDs, sync creates synthetic pipelines from opportunity stages instead of failing.

## GHL Pagination & Full Dataset Retrieval
<!-- Obligatorio para todos los scripts de auditoría, búsqueda, sincronización o corrección: nunca asumir que una sola respuesta de GHL contiene todos los contactos u oportunidades. Todo script que necesite un escaneo completo debe paginar, acumular resultados, deduplicar por `id`, y reportar conteos finales. -->

- **Contactos:** usar la paginación cursor-based de GHL con `meta.nextPageUrl`, `startAfter` y, cuando aplique, `startAfterId`. No usar offset pagination. Continuar solicitando páginas hasta que no exista siguiente cursor/URL o hasta alcanzar un límite explícito del script (`--limit`, `--max-contacts`, etc.).
- **Oportunidades:** usar exclusivamente `POST /opportunities/search` por `locationId`. Si se requiere un dataset completo, paginar/iterar la búsqueda hasta agotar resultados o alcanzar un límite explícito. No usar `GET /opportunities/` para escaneos porque puede devolver vacío/cero resultados.
- **Acumulación segura:** cada página debe agregarse a una colección global, deduplicando por `id` para evitar repetidos entre páginas o reintentos.
- **Protección anti-loop:** todo paginador debe controlar cursores/URLs ya vistos, páginas vacías repetidas o límites máximos razonables para evitar ciclos infinitos.
- **Auditorías profundas:** antes de comparar, sincronizar o corregir datos, el script debe haber consolidado todas las páginas necesarias de contactos/oportunidades para no dejar datos fuera del análisis.
- **Resumen final:** los scripts que paginan deben reportar total de páginas consultadas, total recibido, total único procesado, duplicados descartados y si se cortó por límite.

## Dynamic Schema & Custom Fields Mapping
Para consultar y mapear campos personalizados cuyos IDs varían entre diferentes sucursales de GHL, se debe utilizar el siguiente flujo de API (ambos usando la cabecera `Version: 2021-04-15`):

<!-- Obligatorio para todos los scripts: antes de leer, comparar, sincronizar o actualizar campos personalizados de contactos u oportunidades en cualquier location, se debe consultar este flujo de endpoints y resolver los IDs dinámicos por nombre de campo. No se deben hardcodear IDs de custom fields entre sucursales. -->

1. **Obtener Catálogo de Objetos:**
   - **Endpoint:** `GET https://services.leadconnectorhq.com/objects/?locationId={locationId}`
   - **Detalle:** Requerido para inicializar y autorizar la sesión de consulta de esquemas de metadatos en GHL para esa ubicación.
2. **Obtener Esquema por Objeto (Schema by Key):**
   - **Endpoint:** `GET https://services.leadconnectorhq.com/objects/{objectKey}?locationId={locationId}`
   - **Parámetro `{objectKey}`:** Usar `contact` o `opportunity`.
   - **Detalle:** Devuelve el listado completo de campos (estándar y personalizados). Permite mapear de forma dinámica campos por su nombre (ej. `"Canal de Origen"` o `"Fuente de Prospecto"`) a su correspondiente ID dinámico en esa sucursal para lectura o actualización segura.

## Business Logic & Field Correlations (Monte Providencia Rules)
Derivado de `DOCUMENTACIÓN de Monte Providencia.md`, estas reglas rigen el comportamiento de los scripts, la integridad de los datos y el mapeo en GHL:

### 1. Flujo de Sincronización y Prioridades
- **Sincronización de Contactos:** Bidireccional entre la cuenta de Marca principal (`GbKkBpCmKu2QmloKFHy3`) y las sucursales. Cualquier creación/modificación en un lado se sincroniza al otro.
- **Sincronización de Oportunidades:** Unidireccional de Sucursal a Marca (la sucursal tiene prioridad). Las modificaciones de oportunidades en la cuenta de marca no se sincronizan hacia las sucursales.
- **Multi-Oportunidades:** Un mismo contacto puede tener más de una oportunidad (múltiples intentos de empeño).

### 2. Campos Requeridos para Business Intelligence (BI)
Cada contacto u oportunidad debe poblar consistentemente:
- `Sucursal` (y campo `TIENDA`) alineados con el "verificador de sucursales".
- `Canal de Origen` (e.g., FORMULARIO, FACEBOOK, WHATSAPP, LLAMADA, INSTAGRAM, SUCURSAL).
- `Fuente de Prospecto` (e.g., CLIENTE CONOCIDO, SUCURSAL, PROSPECCIÓN, REFERIDO, ALIANZA, EVENTO ESPECIAL, LEAD DIGITAL).

### 3. Reglas de Validación de Origen/Fuente
- `LEAD DIGITAL` siempre proviene de un canal digital (Canal de Origen: Formulario, Facebook, etc.).
- Si el `Canal de Origen` es `SUCURSAL`, la `Fuente de Prospecto` **no puede ser** `LEAD DIGITAL`.
- El tag `"sucursal"` en contactos implica creación manual en sucursal, a menos que el campo `source` nativo de GHL indique lo contrario (un origen digital).

### 4. Correlación de Datos de Vehículo
- Para prospectos con fuente `LEAD DIGITAL`, el campo `Vehículo` de la oportunidad debe construirse concatenando los campos personalizados del contacto: `"Marca del Vehículo" + "Versión del Vehículo" + "Año del Vehículo"`.
- En oportunidades creadas manualmente, el campo de vehículo de la oportunidad puede diferir de los datos del contacto.

### 5. Pipelines y Oportunidades Huérfanas
- Toda oportunidad debe residir en un pipeline activo válido (comúnmente llamado `"Standar"`).
- Oportunidades huérfanas (sin pipeline válido) se deben reubicar según coincidencia de nombre de etapa (stage) o marcar para revisión.

## Scripts And Data Safety
- `script_runner.py` exposes only scripts listed in `SCRIPTS_METADATA`; add metadata there when adding a dashboard-runnable script.
- Most scripts read `mp_manager.sqlite` from the repo root and expect a prior dashboard sync. Some scripts are live GHL mutators (`fix_*`, `migrate_*`, `move_*`, `update_*`, `sync_contact_*`); prefer read-only audit/search scripts first unless the user explicitly asks to modify GHL data.
- Treat `Bucéfalo - Mesa de control - API Tokens - MP.csv` and any printed API tokens as secrets.

## Case log (registro de casos)
- `docs/casos/` is the chronological log of real operations/investigations on Bucéfalo, written **for agent recall** (dense, exact commands, literal ids/errors). It complements the `docs/PLAYBOOK_*` files (timeless theory) and the agent memory (atomic facts).
- **When to add a case:** after any Bucéfalo mutation (with `run_id`/snapshot) OR after closing a non-trivial investigation that reached a root cause (even with no mutation).
- **How:** copy `docs/casos/_PLANTILLA.md` → `docs/casos/YYYY-MM-DD-<slug>.md`, fill it, add the row to `docs/casos/INDEX.md`, and link the related memory with `[[slug]]`.
- **Before** investigating a new symptom, `grep` `docs/casos/` for the error/symptom — it may already be solved.
