Memoria
Todo lo que sé y cómo lo organizo
Rotación de free tier: 4 keys de Google x 3 ciclos de modelos, switch pago/free tier
Gregory pidió un modo alternativo al de pago: rotar 4 cuentas de Google (jaquesgregory@, gregoryjaques96@, glassesmedia@, souza.gregory@ — 4 proyectos separados, confirmado que el x4 de cupo es real porque Google limita por proyecto no por key) x 3 ciclos de modelos, agotando cada tramo gratis antes de gastar. Basado en 3 diagramas dibujados a mano por Gregory (excalidraw). Antes de codear se escribió spec completo en FREETIER_ROTATION.md (regla >5min) y se confirmaron 3 decisiones con Gregory: (1) el reset se sincroniza con el reset real de Google (~04:00 ART / medianoche Pacific Time), no a las 22:00 como estaba dibujado — evita que el sistema piense que tiene cupo cuando Google todavía no reseteó; (2) al agotar TODO el cupo gratis del día, el pipeline pasa a modo pago automático el resto del día (nunca se pausa); (3) confirmado que las 4 cuentas son reales y separadas. Arquitectura: switch global "Modo IA: Pago/Free tier" en system_settings.ai_mode (default paid, cero cambio de comportamiento si no se activa). En modo free tier, ai-router.js (callAI()) resuelve la casilla activa vía src/lib/freetier-router.js (resolveFreetierSlot()) recorriendo ciclo→key→modelo en orden, devolviendo la primera casilla cuyo contador de hoy no llegó al límite diario (RPD) — sin cursor guardado, se recalcula en cada llamada leyendo freetier_usage_daily (correcto con el pipeline corriendo como procesos Node cortos por cron, no un daemon persistente). El "día" de cada contador se calcula con freetier_today_art() (resta 4hs antes de tomar la fecha), autoreseteando sin cron. gemini.js ahora cachea un cliente GoogleGenerativeAI POR API key (antes uno solo global) para poder llamar con cualquiera de las 4 keys. No aplica a llamadas con Google Search grounding (useSearch:true) — esas siguen en la key de pago principal, grounding ya tiene su propio tramo gratis separado (grounding_usage_daily). Decisión de seguridad importante (pedido explícito de Gregory, quería cargar las keys directo desde el CRM en vez de tocar .env de Oracle por SSH): las 4 API keys reales de Google se guardan en Supabase (freetier_keys.api_key), NO en .env — es seguro porque tanto el CRM (crm/src/lib/supabase.ts) como Oracle (src/db/client.js) ya acceden a Supabase con SUPABASE_SERVICE_KEY server-only, nunca con la key anon expuesta al browser. Mitigación del único hueco real (el CRM todavía no tiene login, pendiente que Gregory lo agregue): el campo se muestra enmascarado en /settings (últimos 4 caracteres) una vez guardado, con reemplazo sin necesidad de ver el valor completo de nuevo — mismo patrón que Stripe/GitHub/Vercel. Trazabilidad: cada llamada en modo free tier deja un log dedicado en system_logs con metadata.freetier_marker:true, key/ciclo/modelo usado, y shadow_cost_usd (lo que hubiera costado en el modelo PAGO configurado normalmente para ese agente en agent_model_config, no el modelo free tier real que corrió) — esto alimenta la card "Ahorrado" nueva en Financiero → Resumen y la pestaña nueva Financiero → Free tier (editor de límites por ciclo/modelo, editable porque los límites reales de Google cambian con el tiempo y no hay tabla oficial estable — semilla inicial con los números de los diagramas de Gregory, pendiente confirmar contra el dashboard real de cada cuenta en AI Studio antes de activar en serio; también uso en vivo por key con barras de progreso, misma forma visual que los diagramas originales). 3 tablas nuevas (freetier_keys, freetier_cycle_models con seed de los 3 ciclos, freetier_usage_daily) + columna system_settings.ai_mode + 2 RPCs (increment_freetier_usage, freetier_savings) — scripts/migrate-add-freetier-rotation.sql, aplicada vía mcp supabase. costs_by_agent ya filtraba api_cost_usd > 0, así que la actividad en modo free tier (costo real 0) no infla esa tabla existente pero tampoco aparece ahí — es esperado, la pestaña nueva es donde se ve esa actividad. Verificado: node --check en los 3 archivos de pipeline tocados/nuevos, tsc --noEmit limpio en CRM, probado en preview (toggle de settings, guardado real de una key de prueba con masking confirmado end-to-end, las 3 pestañas de /financiero/freetier con datos semilla correctos, card Ahorrado en Resumen) — sin errores de consola. Deploy 11850c6 en Vercel READY. Oracle: git pull + import-test de ai-router.js OK (sin dependencias nuevas en package.json, no hizo falta npm install). Pendiente para Gregory: cargar las 4 keys reales en /settings, confirmar los límites reales de cada cuenta en aistudio.google.com/rate-limit contra lo sembrado en /financiero/freetier antes de activar el switch a "Free tier", y agregar login al CRM (mencionado como plan propio, no construido en esta tarea).
Toggle Total/Hoy en tabla "Costo por agente" de /financiero
Gregory pidió, tras ver la tabla de costo por agente ya poblada con datos reales (columna Cached incluida), agregar un toggle para ver el gasto de hoy además del acumulado de 30 días. Se extrajo la tabla a un nuevo componente cliente crm/src/app/(crm)/financiero/AgentCostTable.tsx con estado local (useState) para alternar entre vista "total" y "hoy", reutilizando el mismo RPC costs_by_agent() vía una nueva query getCostsByAgentToday() en queries.ts (comparte mapCostRow() con getCostsByAgent()). page.tsx pasa ambos datasets ya resueltos server-side (agentRows, agentRowsToday) al componente cliente. Al convertir la tabla en client component apareció un error de hidratación real (toLocaleString() sin locale explícito difiere entre Node server y navegador, ej. "1905" vs "1.905") — se corrigió fijando toLocaleString('es-AR') en los 3 lugares que formatean requests, siguiendo el mismo patrón que ya usa el resto del CRM (agents/[slug], settings, panel-client, etc). Verificado en preview: toggle cambia correctamente entre datasets, sin errores de hidratación ni consola. Deploy dpl_Fi7JLuYjQJkAehrw7KSaLJ2yzAEk en Vercel READY. Solo tocó /crm, no requirió actualización en Oracle.
gemini.js: retry loop ahora loguea a system_logs (no solo console.error) — cierra tarea #8 sin guardrail (Gregory pone límite directo del proveedor)
Última pieza de la auditoría de sales-closer. Nuevo logRetry() en gemini.js, llamado en cada intento fallido de callGeminiWithSearch() y callGemini() (incluido el fallback final flash-lite→flash) — level:warn con model/attempt/max_attempts/retry_delay_ms/error/fallback_to en metadata. Antes cada reintento solo hacía console.error(), invisible fuera de la consola del proceso — el incidente del 23/06 (8 horas de reintentos contra una cuenta con prepago agotado) pasó desapercibido 2 semanas por esto exacto. Gregory decidió explícitamente NO construir un guardrail de presupuesto automático en código ("dejo un presupuesto limitado directo del proveedor, es más fácil") — el alcance de la tarea #8 se redujo solo al logging. Commit 8073ca5, Vercel READY, Oracle actualizado y verificado. Con esto se cierran TODAS las tareas de la auditoría completa de sales-closer iniciada hoy (07/07/2026): tokens/costo reales, escalate_gregory con ntfy, estados del prompt, dead code, modelo en vivo en /flows, contrato uniforme de tokens/thinking/cache en los 6 agentes, reordenamiento del prompt para caching, cálculo real de cache, pestañas de Financiero con calculadora, y logging de reintentos.
Cache de tokens: calculado y trackeado en todos los agentes — cache_price_per_1m en ai_model_catalog, cachedTokens en los 3 wrappers, propagado a 6 agentes + visible en /agents/models
Gregory preguntó directamente si pagábamos cache y si lo estábamos calculando, justo después de implementar el reordenamiento del prompt de sales-closer (tarea #9). Encontrado: ai_model_catalog no tenía columna de precio de cache, y calcGeminiCost()/calcTokenCost() cobraban TODO el input a precio completo aunque Google/OpenAI facturan automático con descuento cuando hay cache-hit — con el caching recién funcionando, Financiero iba a mostrar un costo MÁS ALTO que el real (dirección opuesta a los bugs anteriores del día, mismo síntoma). Implementado completo: (1) migración cache_price_per_1m en ai_model_catalog con precios oficiales verificados (Gemini Flash $0.03/M, Flash-Lite $0.01/M, Pro $0.125/M; Claude Haiku 4.5 $0.10/M, Sonnet 4.6 $0.30/M; GPT-4.1 $0.50/M, mini $0.10/M, nano $0.025/M — GPT-4.1 tiene 75% de descuento, no 90% como los demás); (2) gemini.js/openai.js/anthropic.js devuelven cachedTokens (subset de inputTokens, nunca sumado aparte); (3) calcGeminiCost()/calcTokenCost() restan cachedTokens del input a precio completo y cobran esa porción al precio de cache; (4) propagado cached_tokens al metadata de los 6 agentes reales que loguean tokens (sales-closer, researcher, copywriter, web-qa, web-generator, response-analyzer); (5) /agents/models ahora tiene columna "cache $/1M" editable igual que in/out/thinking. Anthropic NO tiene caching automático (necesita cache_control explícito, no implementado — cachedTokens de Claude da 0 a propósito, documentado como gap real, no bug). AGENTS_CHECKLIST.md actualizado: cached_tokens es ahora parte obligatoria del contrato uniforme (sección 2b), mismo nivel que thinking_tokens — regla explícita de Gregory: "trazabilidad es lo más importante aquí". Commit 148e1d0, Vercel READY, Oracle actualizado y verificado.
sales-closer: prompt reestructurado — system prompt 100% idéntico entre requests, datos del prospecto movidos al mensaje de usuario (tarea #9 completada)
Implementado el reordenamiento discutido para aprovechar implicit caching de Gemini. buildSystemPrompt(skills) ya no recibe prospect — es exactamente el mismo string en TODAS las llamadas (identidad, skills, coreografía de venta STAGE 1-5, reglas, estados, acciones). Antes mezclaba nombre/rubro/ciudad/rating/reseñas del prospecto desde el principio del prompt y también inline dentro de cada etapa, rompiendo el prefijo compartido que el caching necesita. Fix: datos variables movidos a buildUserPrompt() (nueva sección "DATOS DEL NEGOCIO"), texto de las 5 etapas usa placeholders entre corchetes ([nombre del negocio], [rubro], [ciudad], [rating], [reseñas]) que la IA reemplaza con los datos reales del mensaje de usuario. Precio (PRICING_SECTION/PRICE_REFRAME) pasa a usar siempre las constantes de env FIRST_MONTH_ARS/MONTHLY_ARS en vez de prospect.setup_price — correcto porque es precio único sin negociación, no debería haber overrides pre-cierre. Verificado con test local (sin llamadas reales a la API): buildSystemPrompt() da el string exacto idéntico para dos prospectos distintos con datos totalmente diferentes; buildUserPrompt() sí varía y trae los datos reales de cada uno. No se tocó el contenido/estrategia de venta (SPIN, Voss, Cialdini, Belfort) — mismo texto, solo cambió dónde vive cada dato. AGENTS.md y agents-data.ts actualizados en el mismo commit (ee62dcf), pusheado, Vercel READY, Oracle actualizado y verificado. Pendiente: medir el impacto real en costo/cache-hit contra la API real en los próximos días (comparar contra el ~$0.0074/req actual en Financiero) — no se puede confirmar el ahorro exacto sin datos de producción reales.
Nuevo documento AGENTS_CHECKLIST.md — checklist obligatorio a leer antes de tocar cualquier agente
Gregory pidió un documento tipo SYSTEM.md pero solo para agentes, después de que el mismo tipo de bug (llamada a IA sin loguear costo/tokens) apareciera dos veces en agentes distintos (sales-closer 23/06, webhook classifyMessage 07/07) pese a que la regla de tracking obligatorio ya existía en CLAUDE.md. Creado AGENTS_CHECKLIST.md en la raíz con: (1) todas las páginas del CRM donde aparece info de agentes, (2) contrato de qué debe tener todo agente que llama IA, (3) bitácora acumulativa de errores encontrados. Agregada regla de oro en CLAUDE.md obligando a leerlo antes de tocar cualquier agente. La sección de errores se actualiza siempre; las secciones de contrato/superficies solo con cambios estructurales reales (preguntarle a Gregory si hay duda).
gemini.js: fallback interno flash-lite→flash mal atribuido en logs y costo — encontrado auditando thinking tokens de todos los agentes
gemini.js tiene un fallback interno: si flash-lite falla con 503, reintenta automáticamente en flash. Pero tryModel()/callGemini() nunca devuelven cuál modelo sirvió realmente la respuesta. ai-router.js entonces loguea SIEMPRE el modelo originalmente pedido (modelId de agent_model_config, ej. flash-lite) y recalcula el costo con el precio de ESE modelo — no con el del fallback real (flash, más caro, con thinking prendido por defecto vs flash-lite que lo tiene apagado). Encontrado auditando web-qa y web-generator (ambos configurados en flash-lite): mostraban thinking_tokens reales y sustanciales (1.059 y 208 promedio) bajo la etiqueta "flash-lite", que según la doc oficial de Google no debería generar thinking — la explicación real es que esas llamadas puntuales corrieron en flash por el fallback interno, mal atribuidas tanto en el campo model como en el costo calculado. NO corregido todavía — agregado a AGENTS_CHECKLIST.md sección de errores. Fix propuesto: que tryModel()/callGemini() devuelvan el modelo real usado, y que ai-router.js use ESE para tagear el log y calcular el costo, no el modelId original.
researcher.js nunca loguea thinking_tokens pese a usar Gemini 2.5 Flash (thinking prendido por defecto)
Auditando tokens IN/OUT/thinking de todos los agentes reales: researcher usa gemini-2.5-flash (thinking ON por defecto según doc oficial de Google), pero las 324 ejecuciones reales tienen metadata.thinking_tokens completamente AUSENTE (no es 0, la clave no existe) — a diferencia de sales-closer.js que sí loguea bien ese campo desde callAI(). Sugiere que researcher.js no está tomando thinkingTokens del resultado de callAI() al armar el metadata del log. NO corregido todavía — agregado a AGENTS_CHECKLIST.md.
Nueva regla de oro: cruzar incidentes de costo contra el dashboard real del proveedor, nunca confiar solo en system_logs
Ver CLAUDE.md sección "Ante un incidente de costo real, cruzar SIEMPRE contra el dashboard de facturación del proveedor". Causa: reporté a Gregory que los 182 logs con tokens_estimated:true ($3.29) eran "la reconstrucción del incidente del 23/06" sin cruzarlo contra Google Cloud Console. Gregory compartió capturas reales: $12.11 de costo total ese día, 1.004 requests a Gemini 2.5 Pro, 41.62% de éxito. Cero filas en system_logs (de cualquier stage) tienen metadata.model=gemini-2.5-pro ese día — el modelo real que causó el incidente es invisible en nuestra DB. La causa real, encontrada en los mensajes de error crudos: 429 "Your prepayment credits are depleted" contra gemini-2.5-pro:generateContent desde las 16:24 UTC hasta las 23:59 UTC (casi 8 horas de reintentos contra una cuenta con el prepago agotado, sin loguear nada más que console.error en cada intento). Los 182 "estimados" resultaron ser los logs reales de los primeros ~12 minutos (antes de agotar el prepago) con un placeholder de tokens (33000/1200 fijo) en vez del usageMetadata real — no una reconstrucción posterior.
El retry loop de gemini.js no loguea a system_logs en cada intento fallido — solo console.error — esto oculta incidentes de rate-limit/cuota exactamente cuando más importa verlos
src/lib/gemini.js: isRetryableError()/tryModel() en callGemini() y callGeminiWithSearch() solo hacen console.error() en cada intento fallido dentro del loop de reintentos — nunca escriben a system_logs. Solo el resultado FINAL (éxito o el catch del llamador) genera una fila. Esto significa que durante un incidente real (como el 23/06, 429 por "prepayment credits are depleted" contra Gemini 2.5 Pro, ~8 horas de reintentos) el volumen real de requests contra el proveedor (Google mostró 1.004 requests a Pro ese día) queda invisible en nuestra propia base — solo vimos 242 error logs de sales-closer ese día, una fracción del total real. Este gap se vuelve MÁS probable, no menos, a medida que Gregory escala el volumen de sales-closer (100→250→500→1000 mensajes/día) porque más volumen concurrente aumenta la probabilidad de pegar contra rate limits. Propuesto pero NO implementado (pendiente decisión de Gregory): hacer que el loop de reintentos también escriba a system_logs (aunque sea sin tokens/costo, solo modelo+tipo de error) para que futuros incidentes sean visibles en tiempo real, y/o un guardrail de presupuesto diario que pause el agente automáticamente antes de agotar el prepago en vez de reintentar por horas contra una cuenta muerta.
El "33k tokens/req" de sales-closer era en realidad ~12.1k reales — 182 logs sintéticos del incidente 23/06 (tokens_estimated:true) se promediaban junto con datos reales
Gregory notó que la página del agente mostraba 12.004 tokens de la última ejecución, contradiciendo el "promedio ~28-33k" que yo mismo había repetido en el turno anterior sin verificarlo. Investigando: de las 219 filas de costo>0 de sales-closer en 30 días, 182 tienen metadata.tokens_estimated:true, input_tokens:33000 EXACTO (sin variación) y output_tokens:1200 EXACTO — todas del 23/06/2026, sumando exactamente $3.29 (el monto "mostrado en el dashboard" del incidente original de Pro/$9 documentado en el header de sales-closer.js). Son un backfill/reconstrucción retroactiva de ESE incidente puntual, no requests reales. Las 23 ejecuciones REALES logueadas desde el 26/06 (después del incidente) promedian 12.099 input tokens y $0.0074/request — muy por debajo de lo que todos creíamos. Ninguna query de Financiero ni de /agents/[slug] filtraba tokens_estimated=true, así que ambas quedaban infladas. Mismo flag encontrado en 130+ logs de web-generator (por-negocio), panel-generator, seo-specialist, web-qa y response-analyzer — mismo día, mismo origen, probablemente afecta sus números también. Corregidos los números en AGENTS.md (sales-closer). NO corregido el código de agregación (queries.ts/RPCs costs_by_agent/daily_costs/getCostLogs) — pendiente decidir con Gregory si se excluyen esos logs o se marcan visiblemente en el CRM, toca más agentes de los que se auditaron hoy.
Precios oficiales investigados 07/07/2026: Gemini caching + GLM-4.6/4.5 — para decidir si Gregory sube a un modelo más avanzado en sales-closer
Gregory quiere usar un modelo más avanzado para mejorar el cierre de ventas, pero temía repetir el incidente de Pro. Con el hallazgo de que el prompt real es ~12.1k tokens (no 33k, ver memory de tokens_estimated), el cálculo de costo cambia mucho. Precios oficiales verificados en las páginas oficiales (ai.google.dev/gemini-api/docs/pricing y docs.z.ai/guides/overview/pricing), no inventados: Gemini 2.5 Flash (actual) $0.30/$2.50 por 1M in/out, cache $0.03/1M (90% off), sin costo de storage en implicit caching. Gemini 2.5 Pro $1.25/$10.00 (prompts<=200k), cache $0.125/1M, storage $4.50/1M/hora (solo explicit caching). Gemini 3.5 Flash (nuevo) $1.50/$9.00, cache $0.15/1M. Gemini 3.1 Pro Preview $2.00/$12.00 (<=200k). GLM-4.6 (Zhipu/Z.ai, China) $0.60/$2.20 por 1M in/out, cached input $0.11/1M, sin costo de storage (promocional). GLM-4.5-Air (tier barato) $0.20/$1.10, cache $0.03/1M. GLM requiere integración nueva (nuevo provider en ai-router.js + fila en ai_model_catalog) — Pro/Claude ya están integrados y serían "solo cambiar el dropdown en agent_model_config". Con ~12k tokens/req reales, subir a Pro costaría aprox $0.027/req sin caching (vs $0.0074 actual en Flash) — mucho más manejable que la proyección vieja basada en 33k tokens. NO implementado nada — Gregory pidió explícitamente solo investigar precios, sin tocar código todavía.
Financiero y /agents/[slug] subcontaban costos/tokens de sales-closer por bugs de muestreo (no de logging)
Dos bugs distintos, mismo síntoma: Gregory no encontraba tokens de sales-closer ni en su página de agente ni en Financiero. (1) getCostsByAgent()/getDailyCosts() en crm/src/lib/queries.ts hacían .select().limit(5000) SIN .order() — PostgREST cappea cualquier respuesta a 1000 filas server-side sin importar el limit pedido, y sin order el subset es arbitrario. Con 3.864 filas de costo>0 en 30 días, Financiero mostraba sales-closer con 13 requests/$0.44 en vez de 219/$3.92 reales. Mismo bug que ya se había corregido para getTotalCost() el 05/07 (sum_api_cost RPC) pero no se replicó a estas dos funciones. Fix: nuevas funciones Postgres costs_by_agent()/daily_costs() (scripts/migrate-add-agent-cost-rpc-functions.sql), agregación 100% server-side. (2) La página /agents/[slug] calculaba tokenStats/costStats sobre getLogs() (últimos 50 logs por fecha, SIN filtrar). Para agentes con cron de alta frecuencia y bajo hit-rate (sales-closer cada minuto, 8.100 logs totales pero solo 205 con costo real), esos últimos 50 casi nunca incluían una ejecución de IA real — la card de Tokens directamente no renderizaba. Fix: nueva getCostLogs() (últimos 50 CON api_cost_usd>0) usada específicamente para tokenStats/costStats/logCost, dejando getLogs() intacta para las KPIs que sí deben reflejar todos los ticks (cola, % éxito, último run). Los datos reales SIEMPRE estuvieron bien logueados — el logging de tokens nunca falló, era 100% un problema de cómo se leían/agregaban en el frontend.
escalate_gregory nunca notificaba de verdad — guardaba notified_gregory:false (al revés) y no mandaba ningún push
Auditoría completa de sales-closer.js: la rama action==="escalate_gregory" en runSalesCloser() seteaba notified_gregory: false (debería ser true) y la única señal de la escalada era un system_log level:warn, invisible fuera de /logs. En 219 ejecuciones reales, hubo exactamente 1 escalate_gregory registrado en la historia del sistema — y ese caso nunca llegó a Gregory de forma confiable. Fix: nuevo src/lib/ntfy.js (helper compartido para el pipeline Node/ESM, replica el patrón ya usado en crm/src/app/api/webhook/whatsapp/route.ts — duplicado a propósito porque el pipeline no puede importar del CRM Next.js) + la rama escalate_gregory ahora llama sendNtfy() con topic prospectos-pd (mismo que ya usa Gregory) y guarda notified_gregory:true. Nota: notified_gregory sigue sin mostrarse en ningún lado del CRM (declarado en types.ts, nunca leído) — el ntfy push es la señal real, el campo de DB es solo para auditoría/consultas futuras. Pendiente: sugerirle a Gregory una badge visible si la quiere.
sales-closer: prompt caching de Gemini (no vector/RAG) es el camino recomendado para bajar el costo de ~33k tokens/req — pendiente spec + OK de Gregory antes de tocar el copy de venta
Gregory pidió opinión sobre cómo bajar el costo del system prompt de sales-closer (~33k tokens/req, causa original de que gastara el presupuesto completo el 23/06). Investigado (docs oficiales de Gemini vía WebFetch/WebSearch, no inventado): Gemini 2.5 Flash+ tiene implicit caching activo por default, sin costo de setup, ~90% de descuento en tokens de lectura cacheados, mínimo 2048 tokens (el prompt de 33k lo supera cómodo), sin costo de storage (a diferencia del explicit caching). El requisito real es que el contenido compartido/estático esté al PRINCIPIO del prompt y sea idéntico entre requests cercanos en el tiempo. Hoy buildSystemPrompt() en src/queue/sales-closer.js NO cumple esto: aunque ~30k de los 33k tokens son estáticos (las 8 skills, ~33KB, están al final del prompt), datos variables del prospecto (business_name, rubro, ciudad, rating/reseñas) están intercalados DESDE EL PRINCIPIO y también dentro del texto de cada STAGE 1-5 — esto rompe el prefijo compartido casi de inmediato y probablemente el caching implícito hoy aporta poco o nada pese a que la mayoría del contenido es reutilizable. Recomendación: mover los datos variables del prospecto de buildSystemPrompt() a buildUserPrompt() (son "datos de este request", no "instrucciones de comportamiento"), dejando el system prompt 100% idéntico en cada llamada — debería recuperar la mayor parte del costo sin sacrificar nada de información. NO recomendado como primer paso: RAG/memoria vectorial (idea de Gregory) — son solo 8 archivos (~33KB), un mapeo determinista stage→skills lograría un recorte de tokens similar sin embeddings/vector DB ni riesgo de que falte contexto en un caso límite, y el caching ya resuelve el costo sin remover información (que era el objetivo explícito: "darle lo máximo de información posible pagando el menor precio posible"). NO implementado todavía — toca directamente el copy de venta ya afinado con Cialdini/Voss/Belfort, así que corresponde escribir spec .md y esperar aprobación de Gregory antes de tocarlo (regla de >5min de CLAUDE.md).
sales-closer: fix de estados en el prompt (converted→conversion_ready) + eliminado status objection muerto del filtro de query
Dos correcciones menores en la misma auditoría: (1) buildSystemPrompt() describía a la IA un estado "converted" que el código nunca escribe (el real es conversion_ready, seteado solo por la acción mark_client, nunca por next_status) — texto del prompt corregido para no confundir al modelo, sin tocar el status real en DB. (2) runSalesCloser() filtraba .in(status, [...,"objection"]) — confirmado que ningún código path ni ninguna acción de la IA puede producir ese status (next_status solo admite replied|interested|negotiating, y 0 prospectos lo tuvieron jamás en la historia) — eliminado del filtro. Quedan referencias inofensivas a "objection" en ACTIVE_STATUSES de crm/src/app/api/webhook/whatsapp/route.ts y scripts/backfill-twilio-inbound.js (no escriben ese status, solo lo tratan como "activo" si existiera) — no tocadas, fuera de alcance de esta auditoría puntual.
/flows: el modelo de cada nodo pipeline ahora es en vivo desde agent_model_config, no texto hardcodeado
Pedido explícito de Gregory: cuando cambia el modelo de un agente desde /agents/[slug] (ModelSelector), esa info debería reflejarse automático en Flujos. FlowCanvas.tsx tenía model/cost como string plano por nodo — sales-closer todavía decía "claude-sonnet-4-6" en Flujos, meses después de cambiarlo a Gemini 2.5 Flash el 23/06/2026 desde el CRM. Fix: nueva getLiveModelInfo() en crm/src/app/(crm)/flows/actions.ts (mapea node id → agent_slug real vía AGENT_SLUG_BY_NODE, cruza agent_model_config + ai_model_catalog) + useEffect en FlowCanvas.tsx que pisa data.model al cargar (mismo patrón que ya usaba getQueueCounts() para los contadores de cola). Cubre los 6 agentes de pipeline con fila real en agent_model_config (researcher, copywriter, web-generator, web-qa, response-analyzer, sales-closer). Los agentes de management (CEO/CMO/etc.) NO están cubiertos — no tienen ModelSelector ni fila en agent_model_config todavía, su modelo sigue siendo 100% texto estático en agents-data.ts/AGENTS.md (gap real, pendiente si Gregory quiere extender el live-switching a esa capa).
REGLA NUEVA: si el git pull en Oracle trae cambios en package.json, correr npm install ahí también, no alcanza con el pull
Agregada a CLAUDE.md (06/07/2026) tras encontrar que src/lib/openai.js/anthropic.js llevaban días en el repo sin que node_modules de Oracle tuviera el paquete openai instalado — researcher/copywriter/sales-closer fallaban en cada corrida real, en silencio, hasta que se detectó auditando logs. La regla de "verificar Vercel + Oracle después de cada push" ahora incluye: si package.json cambió, correr npm install en Oracle y verificar con un import de prueba antes de avisar que está listo.
CRÍTICO RESUELTO: faltaba el paquete npm "openai" en Oracle — researcher/copywriter/closer fallaban en cada corrida
Revisando la corrida del pipeline del lunes 06/07/2026 (primera corrida real desde que se activó PIPELINE_WEEKDAYS_ONLY) se encontró que researcher, copywriter, qa-loop y closer fallaban con "Cannot find package 'openai' imported from /home/ubuntu/app/src/lib/openai.js" — el paquete npm openai (usado por el sistema multi-proveedor de IA, ai-router.js) nunca se instaló en Oracle desde que se agregaron src/lib/openai.js/anthropic.js en una sesión anterior (faltó el npm install correspondiente). Efecto real: la corrida del 06/07 no investigó ni generó copy para NINGÚN prospecto nuevo (aunque el Prospector sí encontró negocios nuevos), y sales-closer tampoco pudo responder conversaciones ese día. Corregido: npm install en Oracle (confirmado con Gregory antes de correrlo) — "added 1 package, removed 1 package, changed 4 packages". Verificado: import de src/lib/openai.js ahora funciona (IMPORT OK). git status en Oracle quedó limpio, sin drift de package-lock.json esta vez. Panel Generator (36 armados, 0 errores), Enqueuer (36 encolados) y Sender (36 enviados, $2.4048 = $0.0668/msg exacto, confirma el fix de precio real de WhatsApp) SÍ funcionaron bien en esa misma corrida — solo researcher/copywriter/closer estaban rotos por esta dependencia faltante.
Webhook de WhatsApp: mensajes automáticos (bot del negocio) también pasan a status=replied
Antes, cuando el webhook (crm/src/app/api/webhook/whatsapp/route.ts) clasificaba un mensaje como auto-reply (bot del sistema del negocio), solo actualizaba conversation_history y respondía con un mensaje genérico — nunca tocaba status ni message_response, como si nadie hubiera respondido. Gregory pidió (05/07/2026): "si es mensaje automática también va a replied, porque luego separaré de bot y humanos" — la separación bot/humano ya queda tageada en conversation_history (role: "auto_reply" vs "prospect"), pero el status debe reflejar que SÍ hubo una respuesta. Corregido: la rama auto-reply ahora también actualiza message_response, response_at y status (mismo criterio que la rama humana: pasa a "replied" solo si estaba en "sent"), pero sigue sin poner needs_closer_reply=true (un bot no debe escalar al Closer).
CEO agent falló hoy (05/07/2026) con "memory_json is not defined" — corre por su propio cron diario, no relacionado con el pipeline de prospección
El 05/07/2026 (domingo) el agente CEO corrió por su cron diario propio (0 23 * * * node src/index.js ceo, independiente de PIPELINE_WEEKDAYS_ONLY) y falló con el error "CEO falló: memory_json is not defined" (log en system_logs, stage=ceo, 23:00:37 UTC). No investigado todavía — Gregory preguntó si quería que se revise, quedó pendiente de confirmar prioridad. Nota aparte: src/index.js tiene un comentario desactualizado que dice "ceo → Lunes 07:00" cuando el crontab real lo corre TODOS los días a las 23:00 UTC — inconsistencia de doc encontrada de paso, no corregida todavía.
Response Analyzer DESACTIVADO (05/07/2026) — se rediseñará para vivir en el webhook y ser el router bot/humano real
Gregory revisó los 41 prospectos "abandonados" y confirmó que ninguno era realmente interesado (valida que la clasificación neutral era correcta). Discutiendo el flujo real de mensajes, se determinó que el webhook de WhatsApp (crm/src/app/api/webhook/whatsapp/route.ts, función classifyMessage()) YA hace hoy el trabajo de decidir bot-vs-humano que debería ser el de response-analyzer.js — este último solo hacía una segunda pasada de sentiment DESPUÉS de que sales-closer.js ya actuaba. Gregory: "de momento dejamos el response analyser desactivado si el no hace este trabajo". Se sacó "analyzer" de PIPELINE_STAGES (src/index.js) y de la línea de crontab en Oracle (*/5 * * * * node src/index.js analyzer, eliminada) — el archivo response-analyzer.js queda intacto, solo no se ejecuta. agents-data.ts/AGENTS.md/FlowCanvas.tsx actualizados con status "paused"/"⏸️ desactivado". Pendiente de definir en una próxima sesión: el nuevo diseño (clasificar bot/humano + derivar al agente correcto) tiene que correr en el momento (no puede esperar un cron de minutos) — falta decidir si ese código vive en el webhook TypeScript (Vercel) directamente, o si hay otra forma de que "sea" response-analyzer corriendo ahí.
CRÍTICO RESUELTO: 41 prospectos reales quedaron abandonados 12+ días — nuevo status "abandoned" visible en el CRM
Auditando response-analyzer.js se encontró que la rama "neutral" dejaba al prospecto en status=replied sin ningún cambio — invisible en todo el CRM (Prospectos y Conversaciones no lo filtran, no hay notificación). 41 prospectos reales quedaron así desde el 23/06/2026: 38 con sentiment=neutral, y 3 con sentiment=interested que una versión vieja del código nunca propagó al status (quedaron en replied/sent). Gregory confirmó rescatarlos: los 38 neutral → status=abandoned (nuevo valor en ProspectStatus, con tab "🚨 Abandonados (revisar)" en /prospects, badge rojo), los 3 interested → status=interested directo (ya se tratan como leads calientes). Verificado en preview: tab muestra 38 resultados correctos. El código de response-analyzer.js ahora setea neutral→abandoned en vez de dejarlo silencioso, y agrega needs_closer_reply=false a su query (antes corría en paralelo con sales-closer.js sobre los mismos prospectos, sin coordinación — ahora solo actúa DESPUÉS de que el Closer ya tuvo su turno y no movió el estado).
Gregory decidió MANTENER response-analyzer.js — a futuro será el que derive a distintos agentes de cierre, no solo sales-closer
Pregunté si convenía retirar response-analyzer.js dado que hoy se solapa/compite con sales-closer.js (ambos consumen status=replied de forma independiente). Gregory: "No quiero que saquemos este modulo porque en el futuro tendremos otros que no es solo closer, y este agente va a derivar al correcto" (05/07/2026) — el plan es tener más de un agente especializado de cierre en el futuro, y response-analyzer.js va a ser el router/triage que decida a cuál derivar cada respuesta, no solo un clasificador de sentiment. No retirar ni simplificar este módulo en futuras auditorías bajo el argumento de "es redundante con el closer".
Gap encontrado (no arreglado): cuando Response Analyzer marca "interested", no reactiva needs_closer_reply — el Closer no retoma solo
Al redibujar el diagrama de /flows se notó que response-analyzer.js, cuando clasifica sentiment=interested, solo actualiza status=interested pero nunca pone needs_closer_reply=true — como la query de sales-closer.js exige needs_closer_reply=true para actuar, el Closer no necesariamente retoma esa conversación solo. Es menos grave que el bug de "neutral" (interested SÍ es visible/filtrable en /prospects, a diferencia de neutral que era invisible), pero sigue siendo un posible punto de fuga. No se tocó el código todavía — marcado en el diagrama de /flows con un edge punteado de advertencia (⚠️) y queda pendiente de decisión.
CRÍTICO RESUELTO: Financiero mostraba costos subcontados — Supabase corta cualquier select a 1000 filas, la función de agregación que el código esperaba nunca existía
Al agregar la card de "Costo mensajería" en /financiero, se detectó que daba $0.04 cuando la realidad (verificada por SQL directo) era $0.85. Causa raíz: getTotalCost() en crm/src/lib/queries.ts llama a un RPC "sum_api_cost" que el propio comentario del código dice que existe "para evitar el límite de 1000 filas" — pero esa función de Postgres NUNCA se había creado. El código caía silenciosamente a un fallback que hace un SELECT con .limit(5000), pero Supabase/PostgREST tiene un tope server-side de 1000 filas que ignora cualquier .limit() más alto pedido desde el cliente — de las ~3111 filas reales con costo>0, solo se traían 1000 (sin orden garantizado), subestimando TODOS los totales de Financiero: "Costo IA" mostraba $2.97 cuando el real acumulado es $43.70, "Costo Maps" mostraba $28.51 cuando el real es $55.37. Esto llevaba meses así (desde que se agregó el comentario sobre el RPC), afectando el dashboard financiero completo, no solo la card nueva de WhatsApp. Corregido: se creó la función sum_api_cost real en Postgres (scripts/migrate-add-sum-api-cost-function.sql) que agrega server-side sin traer filas individuales. Se corrigieron también las etiquetas "30 días" de las cards por "acumulado" (el cálculo siempre fue de todo el historial, nunca una ventana de 30 días real — esa ventana solo aplica a la tabla "Costo por agente" de abajo, que usa una query distinta y correcta).
PEDIDO A FUTURO: gráfico de costo total por conversación/prospecto (todos los stages sumados)
Gregory pidió (05/07/2026) que en el futuro quiere ver, en un gráfico, cuánto costó la conversación completa con un prospecto puntual — sumando TODOS los costos reales asociados a ese prospect_id a través de todo el pipeline: researcher, copywriter, web-generator, seo-specialist, web-qa, panel-generator (hoy $0), sender (WhatsApp, ahora con costo real $0.0668/mensaje Marketing), sales-closer (respuestas dentro de la ventana, $0.005/msg), response-analyzer. Hoy system_logs ya tiene prospect_id en casi todos los logs con costo, así que la agregación es factible (GROUP BY prospect_id, SUM(api_cost_usd)) — falta construir la vista/gráfico en el CRM (candidato: página de detalle del prospecto en /prospects/[id], o una vista nueva). No implementado todavía, solo queda marcado para retomar cuando Gregory lo pida.
Aclaración: el fee de Meta es por mensaje-template (Marketing), no por conversación — la ventana de 24h no lo baja
Gregory preguntó (05/07/2026) si el costo de WhatsApp era por mensaje o por template, y si dentro de la ventana de 24h el precio cambiaba. Respuesta: el template Marketing (VA/VB, primer contacto) cuesta $0.0668 SIEMPRE, tenga o no ventana abierta — Marketing nunca tiene ventana gratis ni descuento (a diferencia de Utility). Pero si el prospecto responde y se abre la ventana de 24h, un mensaje LIBRE (no template) de nuestro lado no tiene fee de Meta — solo el fee fijo de Twilio ($0.005). El código YA distinguía esto correctamente antes de esta auditoría: sendWhatsAppTemplate() (Marketing, $0.0668, usado por sender.js para el primer contacto) vs sendWhatsApp() (mensaje libre, $0.005, usado por sales-closer.js/payment-agent.js para responder dentro de la ventana) — solo el primero necesitaba el fix de costo real, el segundo ya estaba bien.
Precio real de WhatsApp CONFIRMADO y editable en /settings — Meta $0.0618/msg (Marketing, Argentina) + Twilio $0.005/msg = $0.0668 total
Gregory confirmó el fee de Meta en la calculadora oficial de Twilio: $0.0618/mensaje para categoría Marketing, destinatarios en Argentina (que es la categoría de nuestros templates de prospección VA/VB). Se creó la tabla whatsapp_pricing (category: marketing/utility/authentication, meta_fee_usd, twilio_fee_usd, notes) — scripts/migrate-add-whatsapp-pricing.sql. Editable desde /settings del CRM (WhatsappPricingCard.tsx + updateWhatsappPricing en actions.ts), probado end-to-end en preview. whatsapp.js (getWhatsAppCost()) ahora lee esta tabla en cada envío y loguea el costo real ($0.0668/msg para marketing) en system_logs.api_cost_usd, reemplazando el $0.005 hardcodeado que subestimaba el costo real (le faltaba el fee de Meta por completo). Esto reemplaza/resuelve la memoria anterior "PRECIO REAL de WhatsApp confirmado con fuentes oficiales" que dejaba el fee de Meta como TBD.
Gregory decidió NO construir el visor de templates de WhatsApp en el CRM — prefiere verlos directo en Twilio Console
Se había propuesto traer el contenido real de los templates (VA/VB) al CRM vía la Content API de Twilio (requería agregar credenciales de Twilio a Vercel, hoy solo están en Oracle). Gregory respondió (05/07/2026): "No hace poner los templates en el CRM entonces, lo veo desde Twilio" — decisión de no construir esta feature, descartada. No confundir con la tabla whatsapp_pricing (esa sí se construyó, es sobre PRECIO no sobre el contenido de los templates).
Gregory decidió NO implementar todavía la verificación de línea (line_type_intelligence) antes de encolar
Se propuso usar Twilio Lookup (line_type_intelligence) antes del Enqueuer para descartar números que no son celulares (landline/VoIP nunca tienen WhatsApp), como mitigación parcial al problema de "número sin WhatsApp" dañando la reputación con Meta. Gregory respondió (05/07/2026): "No hagamos la verificacion de numero todavia" — queda pendiente para más adelante, no implementar sin que lo pida. Ver memoria "pending" del 05/07/2026 con el detalle de la investigación (no existe forma oficial de verificar WhatsApp específicamente, solo line_type_intelligence como mitigación parcial).
PRECIO REAL de WhatsApp confirmado con fuentes oficiales (05/07/2026) — Twilio $0.005 + fee de Meta por categoría, NO es un flat $0.010
Investigado en documentación oficial (no inventado): Twilio.com/whatsapp/pricing, Twilio changelog "Meta is Updating WhatsApp Pricing on July 1, 2025", developers.facebook.com pricing docs. Desde julio 2025 Meta dejó el modelo de "ventana de conversación 24h" y factura POR MENSAJE según categoría de template: Marketing (nuestros templates de prospección VA/VB) se cobra SIEMPRE, sin ventana gratis y SIN descuento por volumen. Utility se cobra solo fuera de la ventana de 24h de servicio al cliente (gratis dentro). Authentication se cobra siempre pero SÍ tiene descuento por volumen. El fee de Meta varía por país del destinatario (Argentina en nuestro caso) — el monto exacto NO está confirmado todavía, hay que consultarlo en Twilio Console (calculadora de precios) o Meta Business Manager filtrando Argentina + Marketing. Twilio cobra aparte un fee plano de $0.005/mensaje (confirmado, coincide con lo hardcodeado en whatsapp.js y con los logs reales). REGLA: hasta tener el número real de Meta, NO inventarlo — dejar marcado como pendiente (ver memoria "pending" de sender.js). agents-data.ts, AGENTS.md y FlowCanvas.tsx ya actualizados con este hallazgo.
PENDIENTE: fallback de email no se activa si Twilio acepta el mensaje pero falla la entrega después (async)
En src/queue/sender.js, el fallback a email (encolar channel=email si WhatsApp falla) solo se dispara si la llamada a Twilio tira excepción EN EL MOMENTO (síncrono). Si Twilio acepta el mensaje (devuelve SID) pero la entrega falla DESPUÉS (reportado vía webhook crm/src/app/api/webhook/twilio-status/route.ts, típicamente error 63024 "número sin WhatsApp"), el prospecto se marca directo status=discarded, sin ningún intento de mandar por email. Gregory pidió explícitamente marcar esto para trabajar en el futuro, no arreglarlo ahora (05/07/2026).
PENDIENTE: no existe forma oficial de verificar si un número tiene WhatsApp antes de enviar — investigar mitigación con Lookup line_type_intelligence
Gregory reportó que la mayoría de errores actuales de envío son "número sin WhatsApp" (detectado recién al fallar en Twilio, dañando la reputación/quality score con Meta). Investigado en documentación oficial: el WhatsApp Cloud API de Meta (el que usa Twilio) NO tiene forma de verificar registro de WhatsApp antes de enviar — esa capacidad existía en la vieja On-Premises API pero se sacó al migrar a Cloud API (fuente: developers.facebook.com/docs/whatsapp/cloud-api/messages/contacts-messages — el endpoint Contacts actual es para MANDAR tarjetas de contacto, no para verificar destinatarios). El Lookup API de Twilio tampoco tiene paquete de WhatsApp (fuente: twilio.com/docs/lookup/v2-api — paquetes reales: line_type_intelligence, sim_swap, identity_match, caller_name, sms_pumping_risk, reassigned_number, call_forwarding, line_status — ninguno de WhatsApp). Mitigación real disponible y no implementada todavía: usar line_type_intelligence de Lookup ANTES de encolar para descartar números que no son móviles (landline/VoIP nunca tienen WhatsApp) — reduce el problema, no lo elimina (un celular sin WhatsApp instalado sigue sin detectarse por esta vía). Existen servicios de terceros no oficiales (ej. Maytapi) que sí verifican, pero no son soportados por Meta/Twilio. Pendiente: decidir si vale la pena sumar el chequeo de line_type_intelligence al Enqueuer antes de encolar.
sender.js auditado: código muerto borrado, rampa Meta ficticia borrada, message-sender reclasificado a herramienta
Auditoría de src/queue/sender.js y message-builder.js (05/07/2026): (1) borrado buildWhatsAppMessage()/WHATSAPP_VARIANTS de message-builder.js — código muerto, sender.js nunca lo llamaba, el texto real vive en los templates de Meta (contentSid). (2) Sacada la mención "rampa Meta 10→20→40" de agents-data.ts/FlowCanvas.tsx — nunca implementada en código, era aspiracional. (3) message-sender no tenía kind:"tool" en agents-data.ts pese a no usar IA — corregido, y se agregó su ficha faltante en AGENTS.md (no existía). Pendiente: Gregory quiere poder VER los templates de WhatsApp (contenido real, categoría) desde el CRM — hoy son SIDs opacos en .env, requeriría integrar la Content API de Twilio (necesita credenciales Twilio también en Vercel, no solo Oracle) — presentado como propuesta separada, no construido todavía.
Límite diario de mensajes ahora editable en el CRM (/settings) — nueva tabla system_settings, default 100/día
Gregory pidió (04/07/2026) que el límite máximo de mensajes enviados a prospectos por día sea configurable desde el CRM, no solo por env var — "para no bloquear el número" (rampa de Meta). Se creó la tabla system_settings (id=default, daily_send_limit int, default 100) vía scripts/migrate-add-system-settings.sql. Nueva card en /settings (DailySendLimitCard.tsx + actions.ts, Server Action updateDailySendLimit) con input numérico + botón Guardar, probado end-to-end en preview (UI → Server Action → Supabase, confirmado con SELECT). enqueuer.js (getDailyLimit()) lee esta tabla primero, con fallback a DAILY_SEND_LIMIT env var solo si la fila no existiera. DAILY_SEND_LIMIT en .env.example actualizado a 100 (antes 10).
Enqueuer.js: 3 fixes de la auditoría — comentario viejo borrado, pendingTodayCount sin filtro de fecha corregido, código muerto eliminado
Auditoría de src/queue/enqueuer.js (04/07/2026): (1) el comentario del archivo decía "tiempos escalonados 8-25 min entre mensajes" pero el código real pone el mismo scheduled_at (ahora) a todo el batch — Gregory confirmó que ese ES el diseño real (mandar todo junto), no un bug, se borró el comentario desactualizado. (2) pendingTodayCount() no filtraba por fecha, contaba TODOS los mensajes pending de siempre — corregido para contar solo scheduled_at >= inicio del día de hoy, igual que sentTodayCount(). (3) Código muerto SMS_COUNTRIES/isSmsFriendly (nunca llamado) eliminado. Enqueuer confirmado como "herramienta" (nunca usó IA) — regla de oro 04/07/2026. Es la última pieza automática de la cadena: después de esto el sistema espera a que el prospecto responda (webhook WA → response-analyzer/sales-closer).
RESUELTO: crontab de Oracle corregido — payment-review cada 30 min, notifier muerto eliminado
Gregory confirmó (04/07/2026) el diseño correcto: payment-reviewer debe correr cada 30 min (no 1 vez/día). Se corrigió el crontab de Oracle vía SSH: "node src/index.js payment" → "node src/index.js payment-review", y se eliminó la línea de "notifier" (comando inexistente desde que se borró src/notifications/notifier.js — reemplazado por notificaciones push ntfy.sh directo desde el webhook de WhatsApp, ver memoria del bug de ntfy). Backup del crontab viejo en /home/ubuntu/app/crontab.bak.20260704. Gregory: "puse un nuevo que me llega por notificacion push desde ntfy.sh" — confirma que el sistema de notificaciones actual es el de ntfy, no el viejo por WhatsApp.
RESUELTO: notificaciones push ntfy.sh fallaban si el nombre del negocio tenía emoji (ByteString error)
SYSTEM.md decía "corregido 28/06/2026" pero el código de crm/src/app/api/webhook/whatsapp/route.ts (sendNtfy()) NUNCA sanitizaba business_name antes de usarlo como header Title — el bug siguió pasando después de esa fecha (confirmado en system_logs: 3 fallos el 29/06/2026 con "Cannot convert argument to a ByteString... value of 55357" — 55357 es el surrogate alto de muchos emoji). Gregory pidió verificar que el sistema de notificaciones nuevo (ntfy.sh, reemplaza al viejo notifier.js por WhatsApp) estuviera sano. Se encontró y corrigió de verdad: se agregó asciiSafeTitle() que saca cualquier char con codepoint >255 del businessName antes de mandarlo como header. Antes de este fix, cualquier prospecto con emoji en el nombre (viene de Google Places, pasa) hacía que Gregory NUNCA recibiera el push de ese mensaje humano real — silencioso, solo un log warn.
Pipeline completo ahora solo corre lun-vie por defecto (PIPELINE_WEEKDAYS_ONLY), toggle en cualquier momento
Gregory pidió (04/07/2026) que el pipeline operativo completo (prospector→...→analyzer, toda la cadena lineal de PIPELINE_STAGES en src/index.js) no corra los fines de semana. Se agregó env var PIPELINE_WEEKDAYS_ONLY (default true) — si hoy es sábado/domingo (UTC) y está en true, el pipeline completo se omite (log + return, sin tocar ningún stage). Para volver a correr todos los días: PIPELINE_WEEKDAYS_ONLY=false en .env. NO afecta a los cron reactivos (closer, analyzer, payment-review, notifier) que siguen corriendo todos los días — el pedido era específico para la cadena de prospección/generación.
CRÍTICO: crontab de Oracle tiene "node src/index.js payment" (no existe) en vez de "payment-review" — el chequeo de suscripciones MP nunca corrió cada 30 min como se diseñó
Confirmado en logs/cron.log de Oracle: la línea de crontab `*/30 * * * * node src/index.js payment` tira "Comando desconocido: payment" en cada corrida desde hace tiempo — el comando real en COMMANDS (src/index.js) se llama `payment-review`. payment-reviewer.js (sincroniza subscription_status desde Mercado Pago, notifica suspensiones/reactivaciones por WhatsApp) solo corrió 5 veces en total, todas como parte de la cadena lineal diaria del pipeline completo (11:00 UTC), nunca vía el cron de cada 30 min. Mismo problema con la línea `0 * * * * node src/index.js notifier` — no existe comando "notifier" desde que se borró src/notifications/notifier.js, tira "Comando desconocido: notifier" cada hora. Pendiente: confirmar con Gregory el diseño correcto (¿cada 30 min o 1 vez/día, como dice el propio comentario del archivo payment-reviewer.js?) antes de arreglar el crontab.
agents-data.ts tenía horarios inventados por agente para pasos que en realidad corren en la cadena lineal de PIPELINE_STAGES (un solo cron a las 11:00 UTC)
Prospector, Qualifier, Researcher, Copywriter, Web Generator, SEO Injector, Web QA, Panel Generator y Message Sender tenían cada uno un "schedule" con hora fija distinta (ej. "Diario 14:30 UTC" para panel-generator) — esto es falso: todos corren dentro de UNA sola invocación de node src/index.js sin argumento, disparada por un único cron a las 11:00 UTC, cada paso arranca apenas termina el anterior (confirmado empíricamente: panel-generator corrió a las 15:17 UTC un día que arrancó a las 11:00, porque los pasos previos tardaron horas). Corregido: los "schedule" de esos 9 agentes ahora dicen "Parte del pipeline lineal diario (sin hora fija propia)". También se agregaron 2 fichas que faltaban en agents-data.ts (existían en código pero no en el CRM): enqueuer y payment-reviewer, ambos kind:tool (sin IA).
ACCESO SSH A ORACLE: tengo acceso directo — key en C:\Users\cande\Downloads\ssh-key-2026-06-21.key, IP 163.176.206.142
Tengo acceso directo al servidor Oracle por SSH usando la key local de Gregory: ssh -i "C:\Users\cande\Downloads\ssh-key-2026-06-21.key" ubuntu@163.176.206.142 — directorio del proyecto: /home/ubuntu/app. Mi Bash tool corre en la máquina Windows local de Gregory (donde está la key), así que puedo invocar ssh con un comando remoto entre comillas para diagnosticar o ejecutar (git pull, revisar logs, editar .env, etc.) sin que Gregory tenga que copiar/pegar nada él. Confirmado y preferido por Gregory el 04/07/2026: "prefiero que lo hagas siempre que posible". El scheduler real en Oracle es un crontab (no PM2, ver memoria de bug corregido) — un git pull ahí deja el código activo en la próxima corrida de cron sin necesitar reinicio.
Gregory prefiere que yo ejecute directo en Oracle por SSH cuando sea posible, en vez de solo darle el comando
Gregory: "prefiero que lo hagas siempre que posible" (04/07/2026), tras confirmarme que puedo conectarme yo mismo por SSH con su key local (C:\Users\cande\Downloads\ssh-key-2026-06-21.key, usuario ubuntu@163.176.206.142). Antes solo le daba el comando listo para copiar/pegar (regla vieja de CLAUDE.md). Ahora, para acciones de bajo riesgo y ya autorizadas en el flujo de la conversación (deploys, git pull, restarts, backfills), debo intentar ejecutarlas yo directo por SSH en vez de limitarme a entregar el comando — sigue aplicando pedir confirmación antes de acciones nuevas/riesgosas que no se hayan discutido, pero una vez que el paso está aprobado en la conversación, ejecutarlo yo es lo preferido.
CLAUDE.md/SYSTEM.md documentaban PM2 como el scheduler de Oracle — la realidad es un crontab, PM2 nunca se arrancó
Al hacer git pull + pm2 restart all en Oracle (04/07/2026) se encontró `pm2 list` vacío — nunca se corrió `pm2 start ecosystem.config.cjs` aunque el archivo existe commiteado en el repo. El scheduler real es `crontab -l`: pipeline completo 11:00 UTC, `analyzer` cada 5 min, `closer` cada minuto 11-23h ART, `payment` cada 30 min, `notifier` cada hora, `ceo` diario 23:00 UTC. Como cada línea de cron invoca `node src/index.js <stage>` como proceso nuevo, un `git pull` deja el código activo en la siguiente corrida de cron — nunca hace falta "reiniciar" nada. CLAUDE.md (sección Oracle) y SYSTEM.md (decía "pipeline manual, cron pendiente de activar") quedaron corregidos en el mismo momento.
Bugs de cobro corregidos: payment-agent.js ignoraba el precio real del prospecto y siempre cobraba el monto global por defecto
createPaymentLink() tenía setupAmount/monthlyAmount como defaults de parámetro de JS — se resolvían ANTES de traer el prospecto de Supabase, así que SIEMPRE usaban DEFAULT_SETUP_AMOUNT/DEFAULT_MONTHLY_AMOUNT (el monto global), ignorando prospect.setup_price/monthly_price. El flujo real (server.js → /api/payment-wall/create-preference) llama a createPaymentLink() sin pasar esos montos explícitos, así que en producción TODOS los cobros del primer pago usaban el default global, no el precio cotizado al cliente. Mismo bug en _handlePayment() (creaba el Preapproval mensual con DEFAULT_MONTHLY_AMOUNT en vez de prospect.monthly_price). Corregido: el fallback ahora se resuelve DESPUÉS de traer el prospecto (setupAmount ?? Number(prospect.setup_price ?? DEFAULT_SETUP_AMOUNT)). Se eliminó también _extractMonthlyAmount(), función muerta que nunca se llamaba (leía el monto de gregory_notes en vez de la columna real).
Panel Generator dejó de usar IA — ahora es herramienta. Copywriter genera los 2 campos del panel en la misma llamada del copy de la web
Con el modelo de negocio unificado en un plan único ($35.000/$25.000 ARS para todos), panel-generator.js ya no necesita generar por IA 9 campos (setup_description, monthly_description, included_features, faqs, hero_tagline, persuasion_headline/subtext, cta_text) — auditando panel-client.tsx se encontró que hero_tagline/persuasion_headline/persuasion_subtext NUNCA se renderizaban (IA pagada y tirada). Rediseño: copywriter.js genera panel_hero_subtext y panel_cta_text (los 2 únicos campos con valor real, dirigidos al dueño del negocio) en la MISMA llamada que genera el copy_content de la web (constante PANEL_FIELDS, solo en full_generation, no en targeted_fix). panel-generator.js ya no llama a ningún modelo — solo lee esos 2 campos de copy_content, arma panel_content, y fija setup_price/monthly_price desde env (PANEL_FIRST_MONTH_ARS/PANEL_MONTHLY_ARS). Reclasificado kind:tool en agents-data.ts. included_features/faqs pasaron a ser arrays estáticos en panel-client.tsx (PLAN_INCLUDED/PLAN_FAQS) — el plan es igual para todos, no vale la pena pagar IA para variarlo. La card de precio en panel-client.tsx se rediseñó de "dos opciones" (setup vs mensual, modelo viejo) a un único plan con 2 números (primer mes / desde el mes 2), y se corrigió el hardcode "USD" a "ARS".
Nueva pestaña "Panel de venta" en /flows del CRM + PANEL_FLOW.md
Gregory pidió ver visualmente todo lo que está conectado a la página pública del panel (crm/src/app/[slug]/page.tsx) antes de hacerle ajustes futuros. Se creó PanelFlowView.tsx (vista de solo lectura, sin drag/drop, agrupada en 5 etapas: genera contenido → arma el panel → la página pública → de la página a la conversación → el cobro) y se agregó un tab switcher en flows/page.tsx ("Pipeline completo" | "🖥️ Panel de venta"). Documentación completa en texto: PANEL_FLOW.md (raíz del repo) — qué campos de prospects consume la página, quién los escribe, de dónde sale panel_content, cómo se conecta el botón de WhatsApp con sales-closer, y la cadena completa hasta el cobro en Mercado Pago.
MODELO DE NEGOCIO UNIVERSAL (04/07/2026): $35.000 ARS primer mes, $25.000 ARS/mes después, SIN NEGOCIACIÓN, para TODOS los rubros
Qué hace el sistema: encuentra negocios locales sin web (Google Maps/Places), les genera una web con IA (copywriter + web-generator + templates), corre QA de contenido y visual, arma un panel de venta público, y lo manda por WhatsApp. Gregory solo interviene cuando hay interés real (sales-closer negocia/cierra, o Gregory a mano). Modelo de precios ACTUAL (reemplaza todo lo anterior — ya no hay pricing inteligente en USD ni por categoría): - Primer mes: $35.000 ARS (incluye dominio propio + primera cuota de suscripción) - Meses siguientes: $25.000 ARS/mes - Universal para TODOS los rubros (antes esto solo aplicaba a hamburguesas/lomiterías vía FIXED_PRICE_CATEGORIES; ahora es el único modelo, ya no hay smart pricing USD $97-297 negociable) - SIN NEGOCIACIÓN — sales-closer no debe ofrecer descuentos ni variar el precio por prospecto - Moneda: ARS (Argentina), no USD Por qué: Gregory decidió simplificar todo el negocio a un único plan fijo el 04/07/2026, en vez de mantener dos modelos en paralelo (smart pricing USD para la mayoría de rubros + fixed ARS solo para hamburguesas). Esto también elimina código muerto (calculateSmartPrice, HIGH_VALUE_CATEGORIES, LOW_VALUE_CATEGORIES, CAPITAL_CITIES) en panel-generator.js y sales-closer.js. Reemplaza/supera: memoria "Modelo de precios" (project_pricing_model.md, decía setup variable USD $97-297 negociable + $47 USD/mes) y memoria 53973da9 (MP_SETUP_AMOUNT=30000/MP_MONTHLY_AMOUNT=20000, valores viejos). Esas quedan obsoletas. Cómo aplicar: cualquier agente o página que muestre o cobre precio (panel-generator.js, sales-closer.js, payment-agent.js, server.js, panel-client.tsx, Oracle .env) debe usar estos números. Si Gregory vuelve a cambiar el modelo, actualizar esta fila de memory (no crear una nueva sin marcar esta como superada).
web-qa.js: wait de Puppeteer bajado a 4s, logging de costo $0 corregido, límite conocido del check visual documentado + logueado explícito
Implementado 04/07/2026 a pedido de Gregory tras la auditoría de web-qa.js. 1. api_cost_usd pasó de `aiResult.costUsd || null` a `aiResult.costUsd ?? 0` — un costo legítimo de $0 (ej. se saltó el check de IA por error de texto bloqueante) ya no se confunde con "no se registró costo". 2. VISUAL_WAIT_MS bajado de 7000 a 4000 (constante nueva, antes hardcodeado inline) — el mensaje de loader_stuck ahora usa la constante en vez de "7 segundos" fijo en el string. 3. Límite conocido documentado (a pedido explícito de Gregory, "dejá esto escrito en la página del agente y sus documentos"): si Puppeteer falla en cargar la página (timeout/red), el issue visual_check_failed queda con severidad warn — no bloquea por sí solo el pase a qa_passed, o sea que un prospecto puede aprobar sin que nadie haya confirmado realmente cómo se ve la web. Es comportamiento preexistente (no un bug nuevo de hoy), documentado ahora en AGENTS.md, agents-data.ts (visible en /agents/web-qa del CRM) y en el propio código. 4. Log explícito nuevo: cuando Puppeteer falla, además del issue agregado en qa_issues, ahora hay un log({level:"warn", ...}) dedicado y buscable con el mensaje "check visual con Puppeteer falló... este prospecto puede pasar QA sin confirmación visual real" — antes solo vivía adentro del array de issues, difícil de encontrar en Logs. Esto es para poder auditar más adelante cuántas aprobaciones pasaron sin check visual real (pedido explícito: "si no tiene, ponÉ logs para que sepamos en el futuro al analizar"). Verificado: node --check en web-qa.js, tsc --noEmit en crm/ (sin errores), preview real de /agents/web-qa confirmando el texto nuevo visible. Gregory confirmó que no hay más ajustes pendientes para web-qa.js en sí — el próximo tema a tratar es continuar la conversación sobre runQaLoop() (ya arreglado, ver memoria del bug) o el siguiente agente en la auditoría secuencial (copywriter → web-generator → seo-specialist → web-qa ya cubiertos).
Bug crítico encontrado y corregido: runQaLoop() en src/index.js medía rechazos contra el status viejo (copy_ready), rota por el propio rediseño de QA de hoy
Auditando web-qa.js el 04/07/2026 (mismo día del rediseño de ruteo de rechazos), se encontró que `runQaLoop()` (src/index.js, el loop interno que encadena copywriter→web-generator→seo→qa hasta MAX_QA_ATTEMPTS=3 veces) seguía contando rechazos con `.eq("status", "copy_ready")` — el destino de rechazo que existía ANTES del rediseño de esta misma sesión. Desde el rediseño, los rechazos van a `researched` (contenido) o `qa_failed` (técnico), nunca a `copy_ready`. Efecto: el conteo de "fallidos" siempre daba 0, así que el loop SIEMPRE reportaba "todas las páginas aprobadas en 1 intento" en los logs, sin importar cuántos rechazos reales hubiera — un log completamente engañoso. Además, los prospectos rebotados por contenido a `researched` nunca se reintentaban dentro del mismo loop (el loop nunca llamaba a `copywriter`, solo a `web-generator`), quedando esperando hasta el pipeline del día siguiente en vez de resolverse en los 3 intentos previstos. FIX: `runQaLoop()` ahora llama a `COMMANDS.copywriter()` en cada intento si hay prospectos en `researched` (nuevos o rebotados), y mide el progreso real contando `researched` (reintentable, dispara otro intento) vs `qa_failed` (terminal, se reporta pero no cuenta para seguir reintentando). IMPORTANTE — este bug NUNCA llegó a afectar producción real: Oracle actualiza su código con `git pull` manual (nunca automático), y no hay indicio de que se haya hecho pull de ninguno de los commits de hoy (b55fb8d, a5cf2f1) todavía. Se encontró y corrigió ANTES de que el rediseño de QA llegara a correr de verdad en el servidor. También en la misma auditoría: FlowCanvas.tsx (`/flows`) tenía dos cosas desactualizadas — el nodo Web QA decía "Determinista + claude-haiku" (corre en gemini-2.5-flash-lite real) y el nodo "QA Reject Loop" mostraba una sola flecha de vuelta a Web Generator con "Web Generator (reintento)" — ya no es así: ahora la flecha va al Copywriter (`rej-cw`, target copywriter-node) y el texto aclara que solo el rechazo de CONTENIDO reintenta automático, el técnico va a revisión manual sin loop. Corregido en el mismo commit.
SEO Injector marcado como herramienta (no agente) en todo el CRM + limpieza menor de código
Implementado 04/07/2026 aplicando la regla de oro dictada en la misma sesión ("si no usa IA, no es agente, es herramienta"). Cambios: 1. AgentDef (crm/src/lib/agents-data.ts) gana un campo opcional kind?: "agent"|"tool". Seteado kind:"tool" en seo-specialist Y en web-analyzer (formalizando lo que ya era cierto para este último). 2. /agents (listado) y /agents/[slug] (detalle): cuando kind==="tool", se muestra un badge "🔧 Herramienta" (teal) en vez de la insignia de estado normal ("Activo"/etc). 3. FlowCanvas.tsx: PipelineNode gana soporte para data.isTool — muestra "🔧 Herramienta" en vez del tag de status, PERO conserva el contador de cola en vivo (a diferencia del ToolNode ya existente para web-analyzer, que es para funciones sin cron propio y no tiene contador de cola). seo-node ahora tiene isTool:true. Verificado en preview que el contador (61) se preserva. 4. AGENTS.md: seo-specialist ahora tiene el callout "⚠️ NO es un agente — es una herramienta" al inicio de su sección, mismo patrón que ya tenía web-analyzer. 5. Limpieza menor en seo-specialist.js: parámetro brief de injectSeoIntoHtml() nunca se usaba (dead code, eliminado). SCHEMA_TYPES no tenía entradas para cafe/bakery/meal_takeaway/sandwich_shop/fast_food_restaurant (categorías activas del pivot de hamburguesas) — agregadas ANTES de la entrada genérica "restaurant" en el objeto (si no, matchean el substring "restaurant" primero y nunca llegan a la específica). 6. Hallazgo adicional de la auditoría: en 30 días, seo-specialist tuvo 278 corridas OK vs 672 errores (todos 429 de cuota Gemini) — antes del rediseño de hoy, un fallo de cuota abortaba TODO el paso, incluida la parte gratis/determinista (schema+meta+title nunca se inyectaban). Con la IA eliminada esto ya no puede pasar. PENDIENTE (barrido de terminología, no hecho ahora — ver memoria de la regla de oro): qualifier, los 3 prospectors, domain-agent, payment-agent, create-restaurant-account también califican como "herramienta" bajo el mismo criterio pero no se tocaron en esta sesión.
Regla de oro: si no usa IA, no es "agente" — es "herramienta", en toda la doc y el CRM
Gregory dictó esta regla el 04/07/2026 al ver que seo-specialist.js (SEO Injector) quedó 100% determinista tras eliminarle su única llamada a IA (era redundante, ver memoria "eliminación de IA redundante en SEO Specialist"). Dijo textual: "actúa en el CRM para decir herramienta, u otra palabra que no sea agente, si no usa IA no es agente." REGLA: un paso del pipeline se llama "agente" SOLO si llama a un modelo de IA. Si no (lógica determinista, solo APIs externas no-LLM, o solo Supabase), se lo llama "herramienta" en AGENTS.md, agents-data.ts y el CRM (/agents, /flows) — sin importar si tiene su propio cron/entrada en PIPELINE_STAGES. Es un criterio distinto y más estricto que el ya usado para web-analyzer ("no tiene cron propio") — este es puramente sobre uso de IA. Ya actualizado en CLAUDE.md como regla de oro. Ya aplicado a seo-specialist (SEO Injector) el mismo día: kind:"tool" en agents-data.ts, badge "🔧 Herramienta" en /agents y /agents/seo-specialist, nodo de /flows muestra "🔧 Herramienta" en vez de "Activo", AGENTS.md tiene el callout "⚠️ Esto es una herramienta, no un agente". PENDIENTE — NO aplicado todavía (barrido futuro, no pedido en esta sesión): qualifier.js (sin IA, reglas puras), google-prospector.js/yelp-prospector.js/ml-prospector.js (solo APIs externas no-LLM), domain-agent.js (Porkbun/Cloudflare), payment-agent.js (Mercado Pago), create-restaurant-account.js (solo Supabase) — todos calzan en el criterio "sin IA = herramienta" pero no se tocaron ahora porque Gregory pidió específicamente por SEO Injector. Aplicar el mismo tratamiento la próxima vez que se audite o se toque cualquiera de estos.
Fix quirúrgico en reintentos de QA (Copywriter) + eliminación de IA redundante en SEO Specialist
Implementado 04/07/2026 directamente (sin spec previo, a pedido explícito de Gregory) — Ronda 2 de WEB_GENERATOR_HARDENING.md. Gregory señaló, tras ver el rediseño de la ronda 1, que un rechazo de QA no debería forzar a un agente a "hacer el trabajo 100% de nuevo", y que agentes que ya hicieron bien su trabajo (dio el ejemplo de SEO) no deberían repetirlo sin necesidad. 1. web-qa.js: runAiCheck() ahora recibe Object.keys(copy_content) del prospecto y le pide al modelo que, si encuentra un problema de contenido, indique el "campo" exacto (uno de esos keys) a corregir, o null si es un problema general. Se guarda generated_content.qa_fix_fields — pero solo si TODOS los issues de contenido trajeron un campo específico (si alguno es null, no hay info completa y se prefiere jugar seguro: sin targeting, el Copywriter regenera todo). 2. copywriter.js: si qa_fix_fields tiene contenido, filtra los fields de content_type_schema a solo esos, arma un prompt distinto ("ESTO NO ES UN BORRADOR NUEVO... corregí ÚNICAMENTE esos campos") y mergea el resultado sobre el copy_content existente ({...existingCopy, ...generated}) — los campos no mencionados quedan intactos byte a byte. Log de cada corrida incluye action:"targeted_fix"|"full_generation" y fields_changed:[...]. 3. seo-specialist.js: se eliminó la llamada a IA (callAI) por completo. Encontrado en el código real: el prompt de "optimizar H1" ya le daba a Gemini la respuesta armada en JS (`${category} en ${city} — ${business_name}`, 100% determinístico) como parte del propio JSON de ejemplo — la IA no aportaba nada, solo ubicaba el <h1> actual para poder reemplazarlo sin romper el resto del HTML, algo que un regex hace igual. Nueva función optimizeH1() hace todo por regex, $0, sin llamar a ningún modelo. Se borró la fila de agent_model_config para este agente en Supabase (ya no aplica dropdown de modelo). loadSkills()/SKILLS_DIR/imports de fs muertos también se eliminaron del archivo. Docs actualizadas en el mismo alcance: AGENTS.md (copywriter, web-qa, seo-specialist — 3 secciones, más tabla de totales de costo actualizada a ~$9.6/día), agents-data.ts (mismos 3 agentes), types.ts (GeneratedContent.qa_fix_fields, qa_issues.campo), /prospects/[id] muestra los campos puntuales señalados por QA con badges moradas cuando hay qa_fix_notes. WEB_GENERATOR_HARDENING.md tiene una sección "Ronda 2" documentando todo esto como registro permanente. Verificación: node --check en los 3 archivos JS (sin errores), tsc --noEmit en crm/ (sin errores), preview real de /prospects?status=qa_passed (36 resultados reales, sin errores de consola). No se probó el flujo end-to-end con un rechazo de contenido real (necesita correr el pipeline con Gemini real para generar un caso donde el AI check de QA efectivamente marque un campo específico).
Web Generator hardening: nunca avanza roto, selector de template por datos (template_variant_rules), QA rutea inteligente (contenido→Copywriter, técnico→qa_failed)
Implementado 04/07/2026 tras revisar web-generator.js con Gregory. Spec: WEB_GENERATOR_HARDENING.md. 1. REGLA DE ORO NUEVA (ya en CLAUDE.md + memory category=rule): si falla la generación de contenido para un cliente real, el prospecto nunca avanza de status. En la práctica: generateAiContent()/generateAiContentForRestaurant() en web-generator.js ya NO tienen catch que devuelve defaults con costUsd:0 en silencio — el error se propaga, el batch loop existente ya no llama a .update() sobre ese prospecto, se reintenta solo en el próximo cron. used_default_services/used_default_faq se agregan a generated_content cuando el Copywriter devolvió menos de 6 servicios/5 FAQ (fallo PARCIAL, no bloquea, pero queda visible — decisión explícita de Gregory: bloquear solo fallo total). 2. SELECTOR DE TEMPLATE ESCALABLE: nueva tabla template_variant_rules (category, priority, conditions jsonb con price_level_in/zone_in, template_name, content_type, score) — migración add_template_variant_rules aplicada con seed que replica el comportamiento actual de dental (boutique/familiar). src/lib/template-selector.js ahora resuelve: template_catalog (1 variante) → template_variant_rules (N variantes, por prioridad) → fallback hardcodeado de emergencia. Agregar un rubro nuevo con variantes ya es 100% datos, sin tocar código — resuelve correr dentistas+hamburguesas+lo que venga a la vez. 3. QA YA NO DESCARTA EL COPY DEL COPYWRITER: web-qa.js tagea cada issue con source:"ai" (contenido) o source:"technical" (placeholder, contacto, title, html_too_short, loader_stuck, no_visible_content, js_errors, visual_check_failed). Rechazo de contenido → status:"researched" con qa_fix_notes (el Copywriter lo recibe en su prompt vía nueva fixNotesSection en buildPrompt(), y limpia qa_fix_notes al regenerar con éxito). Rechazo técnico → status nuevo "qa_failed", SIN reintento automático (regenerar copy no arregla un bug de mapping/render) — cola de revisión manual. Antes CUALQUIER error mandaba a copy_ready y web-generator.js regeneraba con su propia IA (peor, descartando copy_content bueno) — y un loader_stuck o js_errors aislado podía quedar silenciosamente en status:generated, violando la regla de oro nueva. 4. CRM: nueva vista dedicada — statuses copy_ready/qa_passed/qa_failed agregados a STATUS_LABELS/COLOR/ICON de /prospects (tab roja "⚠️ Falló QA (revisar)"), card de detalle en /prospects/[id] mostrando qa_technical_issues y qa_fix_notes cuando aplica. ProspectStatus y GeneratedContent (types.ts) actualizados — antes ni siquiera incluían copy_ready/qa_passed que YA existían en producción. 5. seo-specialist.js: hero_subheadline leía de section_brief (nunca lo tuvo) — corregido a copy_content (efecto colateral directo del fix de ayer). 6. Docs actualizadas en el mismo alcance: CLAUDE.md (regla de oro), AGENTS.md (copywriter/web-generator/web-qa/seo-specialist), agents-data.ts (mismos 4), SYSTEM.md (diagrama de estados del pipeline + tabla de flujo restaurante), COMO_AGREGAR_UN_TEMPLATE.md (sección 0 nueva: rol de cada agente; sección 1 reescrita: template_catalog vs template_variant_rules; checklist actualizado). PENDIENTE explícitamente NO implementado ahora (anotado para el futuro): panel-generator.js debería usar copy_content/content_type_schema del Copywriter en vez de su propio prompt aislado — Gregory dijo que esa página se revisa en otra sesión (ver memoria separada "Idea de Gregory: panel-generator debería usar el copy del Copywriter"). VERIFICACIÓN: node --check en los 5 archivos JS tocados (sin errores), npx tsc --noEmit en crm/ (sin errores), preview real de /prospects?status=qa_failed (screenshot, tab roja renderiza bien con 0 resultados esperado, QA OK muestra 36 reales que ya existían en producción sin visibilizar antes). NO se corrió el pipeline real end-to-end con Gemini de verdad — pendiente que Gregory lo pruebe con un prospecto real que falle QA por contenido y otro por motivo técnico, confirmando que cada uno toma el camino correcto.
Idea de Gregory: panel-generator debería usar el copy del Copywriter (content_type_schema) para vender más
Al revisar web-generator (04/07/2026), Gregory señaló que panel-generator.js (Módulo 5.5 — copy del panel de ventas + pricing) genera su propio copy con un prompt separado, y le parece que debería reusar/alimentarse del Copywriter (mismo copy_content / content_type_schema) para ser más persuasivo y consistente con la voz de marca ya definida por rubro. Gregory aclaró explícitamente que esta página "ya la vamos a ver" más adelante — no es para implementar ahora, solo dejarlo anotado. Retomar cuando se aborde panel-generator.js en una sesión futura: evaluar si conviene que el Copywriter también genere los campos que necesita el panel (hero_tagline, persuasion_headline, etc.) vía una fila nueva en content_type_schema, en vez de que panel-generator tenga su propio prompt aislado.
Fix: seo-specialist.js leía hero_subheadline de section_brief (siempre vacío) — corregido a copy_content
Efecto colateral directo del fix del bug de section_brief/copy_content en copywriter+web-generator (04/07/2026, mismo día, a pedido explícito de Gregory). seo-specialist.js:117-124 intentaba JSON.parse(prospect.section_brief) para sacar hero_subheadline y armar el meta description — pero section_brief es y siempre fue exclusivamente el markdown del web-analyzer, nunca tuvo ese campo (ni antes ni después del fix de ayer). Corregido: ahora lee JSON.parse(prospect.copy_content), que es donde el copywriter realmente escribe hero_subheadline. También se actualizó el SELECT de runSeoSpecialist() para traer copy_content en vez de section_brief. AGENTS.md y agents-data.ts actualizados en el mismo cambio.
Regla de oro: si falla la generación de contenido para un cliente, el prospecto nunca avanza de etapa
Gregory dictó esta regla el 04/07/2026 al revisar el hallazgo de la auditoría de web-generator.js: los catches de generateAiContent()/generateAiContentForRestaurant() devolvían defaults hardcodeados (DEFAULT_SERVICES, DEFAULT_FAQ, DEFAULT_RESTAURANT_AI) con costUsd:0 SIN loguear el error real, y el prospecto igual pasaba a status:"generated" como si tuviera copy personalizado. Cruzado con los 429 de cuota vistos en la auditoría del copywriter, esto sugiere que varias webs reales de fines de junio 2026 salieron con copy 100% genérico (mismos servicios/FAQ para todos los dentistas) sin ningún rastro de error en los logs. REGLA: en cualquier agente que genere contenido para un prospecto/cliente real, si la generación falla por cualquier motivo, el prospecto NO puede avanzar de status ni parecer "generado con éxito". Todo catch debe loguear el error real (nunca silencioso). Si falla, se queda en un estado que refleje el fallo (reintento o flag de error explícito), nunca avanza con contenido genérico/roto disfrazado de éxito. Aplica en cascada a agentes aguas abajo (QA, SEO, sender). Actualizado en CLAUDE.md en la sección "Reglas operativas — NO NEGOCIABLES", inmediatamente después de la regla de worktrees huérfanos. IMPLEMENTACIÓN PENDIENTE (no confundir con la regla en sí, que ya está escrita y vigente desde ahora): falta escribir el código real en web-generator.js (y evaluar copywriter.js/panel-generator.js/seo-specialist.js) para cumplir esta regla — logging real en los catches + un status/flag de fallo explícito que impida avanzar. Ver spec pendiente de aprobación de Gregory sobre el rediseño de web-generator (selector de template escalable + QA que preserva el copy_content + esta regla).
Contador de grounding (RPD) + costos en vivo por agente + skill del researcher conectado
A partir de la auditoría del researcher (03/07/2026), Gregory pidió: costos siempre calculados desde la BBDD (nunca hardcodeados), un contador real de uso de grounding (Google Search/Maps) para saber cuándo se supera el tramo gratis, que todo eso sea editable desde el CRM, que se conecte el skill sin usar del researcher, y que si thinking_tokens no llega de la API quede un log de aviso en vez de contarse como 0 en silencio. Implementado en esta sesión (spec: GROUNDING_USAGE_AND_LIVE_COSTS.md): DB (migración scripts/migrate-add-grounding-usage.sql, aplicada): - grounding_usage_daily (usage_date, grounding_type, request_count) — contador real en Supabase, no en memoria de proceso (reemplaza el bug del commit 862edf3 que reseteaba en cada ciclo de PM2) - grounding_pricing (grounding_type, free_rpd, price_per_1000) — editable desde /agents/models, sembrado con google_search (1500 RPD, $35/1000) y google_maps (1500 RPD, $25/1000, sin uso todavía) - función increment_grounding_usage(p_grounding_type) — upsert atómico que devuelve el contador de hoy Código: - src/lib/grounding-usage.js (nuevo): calcGroundingCost() — dentro del tramo gratis de RPD (requests/día, confirmado contra ai.google.dev/gemini-api/docs/pricing que Google cobra por request en el tramo gratis y por query individual recién after superarlo) el costo es $0; si Supabase falla, sobreestima por seguridad y loguea warn - src/lib/gemini.js: usa calcGroundingCost en vez de la fórmula vieja (searchCount*0.035 siempre); agrega warnIfThinkingTokensMissing() que loguea warn si thoughtsTokenCount no viene en la respuesta (antes se colapsaba a 0 en silencio); callGemini/callGeminiWithSearch aceptan stage para que el warning aparezca en el agente correcto - src/lib/openai.js: mismo chequeo para completion_tokens_details en modelos o-series (reasoning) - src/lib/ai-router.js: pasa stage:agentSlug a las llamadas de Gemini/OpenAI - src/pipeline/researcher.js: ahora carga y usa src/skills/researcher/business-research.md (existía, nunca se leía — mismo patrón loadSkills() que copywriter.js) CRM: - crm/src/app/(crm)/agents/[slug]/page.tsx: nueva getCostStats(logs) — costo real promedio de los últimos 7 días calculado en vivo desde system_logs.api_cost_usd, reemplaza el string estático de agents-data.ts para cualquier agente con logs reales (researcher ya muestra ~$0.35/ejecución en preview, coincide con lo medido en la auditoría) - crm/src/app/(crm)/agents/models/ — nueva sección "Grounding" (GroundingClient.tsx + updateGroundingPricing en actions.ts): tramo gratis y precio/1000 editables, barra de progreso del uso de HOY por tipo - crm/src/lib/agents-data.ts: researcher.cost ya no tiene un número hardcodeado, dice "ver costo real en esta página" (fallback únicamente si todavía no hay logs) Documentación: - AGENTS.md: sección researcher y la nota de "Google Search grounding" ya no tienen números fijos que se desactualizan solos — apuntan al CRM y explican el tramo gratis real - Verificado con tsc --noEmit (0 errores) y curl de solo lectura contra un servidor next dev YA corriendo de otra sesión concurrente (PID 2308, no se tocó) — /agents/researcher y /agents/models devuelven 200 y renderizan el contenido nuevo sin errores PENDIENTE SIN RESOLVER (hallado con get_advisors después de la migración, no corregido en esta sesión — está fuera de alcance, es un patrón ya existente): grounding_usage_daily y grounding_pricing quedaron con RLS deshabilitado, igual que ai_model_catalog, agent_model_config, template_catalog y content_type_schema — parece un patrón intencional del proyecto (CRM usa service key), pero son 6 tablas ahora expuestas vía PostgREST sin políticas. Vale una pasada dedicada si Gregory quiere cerrarlo. PENDIENTE (ya guardado en memoria aparte, no tocar sin que Gregory lo pida): comparar Grounding con Google Maps vs Google Places API del prospector — ver memoria "ULTRA IMPORTANTE".
Fix del bug de section_brief + copywriter dinámico por template + página /templates en el CRM
Implementado 03/07/2026 en base al hallazgo de la auditoría del mismo día (ver memoria "Auditoría completa del agente copywriter"). Spec: COPYWRITER_TEMPLATE_SCHEMA_FIX.md. QUÉ CAMBIÓ: 1. Migración `scripts/migrate-add-content-schema.sql` (aplicada vía Supabase MCP): columna nueva `prospects.copy_content` (jsonb) + tabla nueva `content_type_schema` (content_type, fields jsonb) con seed para `restaurant` (8 campos: tagline, hero_subheadline[shared/SEO], subtitulo, hero_l1, hero_l2, frase_foto_1/2/3) y `dental` (6 campos: tagline, hero_subheadline[shared], frase_dentista, nombre_dentista, servicios[lista×6], faq[lista×5]) — grounded exactamente en lo que buildPlaceholderMap()/buildPlaceholderMapForHamburger() de web-generator.js ya consumían. 2. `src/lib/template-selector.js` (nuevo): selectTemplate() extraído de web-generator.js, ahora devuelve también `contentType` (no solo template/score) y se comparte entre copywriter.js y web-generator.js — antes estaba duplicado conceptualmente y web-generator decidía el modo comparando el string del nombre del template en vez de leer content_type. 3. `src/pipeline/copywriter.js` (reescrito): antes de generar, resuelve el content_type del prospecto vía selectTemplate(), carga los campos desde content_type_schema (cache en memoria del proceso, mismo patrón que ai-router.js), arma el JSON del prompt dinámicamente iterando sobre esos campos (ya no hay un esquema fijo de 17 campos hardcodeado). Escribe en `copy_content` (columna propia) en vez de `section_brief`. Si content_type_schema no tiene fila para el content_type resuelto, cae a un esquema mínimo de 2 campos (tagline, hero_subheadline) con un log warn — nunca rompe el pipeline. 4. `src/pipeline/web-generator.js`: en el camino normal, ya NO llama a generateAiContent()/generateAiContentForRestaurant() (las funciones de IA propia) — parsea prospect.copy_content y lo usa directo. Esas funciones se conservan SOLO como fallback (si copy_content viene vacío/malformado, logueado como warn) o para el reintento cuando web-qa.js dejó qa_fix_notes. isRestaurant ahora se deriva de contentType (via selectTemplate/contentTypeForTemplate), no de comparar templateName === "hamburguer-1" a mano. section_brief ya NO se toca por este cambio — sigue siendo escrita únicamente con el markdown del web-analyzer (su propósito original), sin colisión porque el copywriter ya no escribe ahí. 5. Página nueva `/templates` en el CRM (crm/src/app/(crm)/templates/page.tsx) + ítem de sidebar: muestra, por content_type, los campos que content_type_schema define (con su guidance) y, por template, los últimos 3 prospectos reales con copy_content generado (valores reales, no mockeados). Server component, Supabase directo, sigue el design system (--gj-* tokens) del resto del CRM. Verificado en preview: renderiza bien, sin errores de consola, ambos content_types (dental/restaurant) muestran sus campos correctamente; como todavía no se corrió el pipeline real con datos nuevos, las secciones de templates muestran "todavía no hay prospectos generados con este template" (esperado). 6. Documentación actualizada en el mismo alcance: AGENTS.md (secciones copywriter y web-generator, modelo real corregido, costos actualizados, tabla de totales), crm/src/lib/agents-data.ts (descripción y tasks de ambos agentes), SYSTEM.md (línea de modelo IA copywriter corregida de claude-haiku a gemini-2.5-flash-lite), crm/.../flows/FlowCanvas.tsx (nodo copywriter-node y web-gen con modelo/descripción/costo actualizados), templates/COMO_AGREGAR_UN_TEMPLATE.md (nueva sección 3.1: cómo declarar content_type_schema para un content_type nuevo), crm/src/lib/types.ts (campo copy_content agregado al tipo Prospect). CONTEXTO DE NEGOCIO: Gregory aclaró que NO hay un template de hamburguesa nuevo/separado — solo existe hamburguer-1, y el pipeline real todavía nunca se corrió de punta a punta con ese template (por eso están ajustando cada agente del flujo ahora, incluido este fix). Los archivos .mp4 nuevos sueltos en templates/hamburguer-1/ (sin integrar a index.html todavía) quedan fuera de este alcance — cuando Gregory los integre y defina qué texto nuevo necesitan, sumar esos campos a content_type_schema es una fila nueva en Supabase, sin tocar código del copywriter. VERIFICACIÓN REALIZADA: node --check en los 3 archivos JS tocados (sin errores de sintaxis), npx tsc --noEmit en crm/ (sin errores de tipos) tanto antes como después de los cambios de FlowCanvas.tsx, preview real de /templates en el navegador (screenshot + snapshot + console limpio). NO SE CORRIÓ el pipeline real end-to-end (requiere llamadas reales a Gemini vía Oracle/local con .env) — pendiente que Gregory lo pruebe con 1 prospecto dental y 1 restaurant reales, revisando el log de "placeholders sin reemplazar" de web-generator.js.
ULTRA IMPORTANTE — recordar a Gregory: comparar Grounding con Google Maps (Gemini) vs Google Places API (prospector actual)
Gregory vio la tabla de precios oficial de Gemini y notó que existe una herramienta de "Fundamentación con Google Maps" (Grounding with Google Maps) dentro de la API de Gemini — 1.500 RPD gratis, luego USD 25 por cada 1.000 instrucciones fundamentadas (separado del pool de Google Search grounding). Preguntó si el researcher podría hacer el mismo trabajo que hace hoy el prospector (src/pipeline/prospector.js, que usa Google Places API (New) directo, facturado en Google Cloud, ver costos reales documentados en researcher.js/AGENTS.md sección prospector). DECISIÓN (03/07/2026): Gregory NO quiere avanzar con ninguna de las dos opciones (reemplazar Places API por Maps grounding, o que researcher también prospecte) todavía. Pidió específicamente guardar esto como memoria "ultra importante" para que en el futuro se le recuerde crear una comparación entre: 1. Lo que ya existe: prospector.js + Google Places API (New) — costo real ~$0.091/prospecto encontrado (ver memoria "Plan y contexto de experimento de rubro: hamburguesas/lomiterías") 2. La alternativa: Gemini + Grounding con Google Maps — datos que trae (nombre, rating, reseñas, horarios, fotos, etc.), calidad/completitud vs Places API, costo real (1.500 RPD gratis compartido, luego $25/1000 vs el pricing actual de Places API), y si requiere o no las mismas API keys/infraestructura. CUÁNDO USAR ESTA MEMORIA: la próxima vez que se toque prospector.js, researcher.js, o se hable de costos de prospección/Google Places/Google Maps grounding, traer esto a colación y preguntarle a Gregory si quiere avanzar con la comparación.
Auditoría completa del agente copywriter — su output (section_brief) se genera, se paga, y nunca se usa
Auditoría pedida por Gregory el 03/07/2026 sobre el agente copywriter (src/pipeline/copywriter.js). HALLAZGO CRÍTICO: la columna `section_brief` fue diseñada originalmente para el web-analyzer (markdown de gap-analysis, ver scripts/migrate-add-generator-fields.sql:32-35 — "secciones que tiene la web actual vs nuestros templates"). El copywriter (módulo 3.5, agregado después) reutiliza esa misma columna para guardar su JSON estructurado (tagline, hero_headline, hero_subheadline, descripcion_negocio, propietario, servicios×6, diferenciadores×3, testimonios_copy×3, faq×5, ctas, keywords SEO, tono, notas_diseno). Pero web-generator.js (Módulo 4) NUNCA lee `prospect.section_brief` para generar contenido — genera su propio copy con un prompt independiente y más pobre (generateAiContent/generateAiContentForRestaurant, solo tagline/servicios/faq/frase_dentista). Y en la misma escritura a Supabase (web-generator.js:995), SOBREESCRIBE `section_brief` con `brief?.markdownReport ?? null` (el reporte del web-analyzer, variable completamente distinta), destruyendo el JSON del copywriter en la DB. Efecto cascada: seo-specialist.js:117-118 intenta `JSON.parse(prospect.section_brief)` esperando el JSON del copywriter para sacar `hero_subheadline` — pero para cuando corre (después de web-generator en la secuencia del pipeline), la columna ya es markdown o null. El parse falla, cae a {}, hero_subheadline es siempre undefined. Esa lectura está muerta. CONCLUSIÓN: el copywriter corre ~130 req/día, cuesta dinero real, y su output completo (el más elaborado del pipeline, con fórmulas de copy PAS/AIDA/FAB de sus 3 skills) nunca llega a ninguna web ni meta tag. Es trabajo 100% desperdiciado desde que se agregó al pipeline. OTROS HALLAZGOS DE LA MISMA AUDITORÍA: 1. Desincronización de modelo documentado en 4 fuentes distintas: agent_model_config (real) = gemini-2.5-flash-lite. copywriter.js FALLBACK_MODEL = gemini-2.5-flash-lite (coincide). agents-data.ts = Gemini 2.5 Flash-Lite (coincide). AGENTS.md = "gemini-2.5-flash" sin -lite (desactualizado, datos del 23/06). SYSTEM.md línea 87 y FlowCanvas.tsx:323 (diagrama /flows del CRM) = "claude-haiku-4-5-20251001" (completamente desactualizado, ni el proveedor correcto — el copywriter usó Anthropic Haiku brevemente solo el 22/06 antes de migrar al ai-router). 2. thinking_tokens siempre null en metadata de los últimos 30 días pese a que memoria de sesión anterior (30/06) registra un fix para loguearlo — el fix no se refleja en logs reales. 3. 92 errores en 30 días (~21.5% error rate) — 100% son 429 "quota exceeded, limit 20/day, model gemini-2.5-flash" (no flash-lite), concentrados 27/06-02/07. Hipótesis no confirmada: Oracle pudo haber seguido corriendo código pre-migración (hardcoded a gemini-2.5-flash) varios días después de que el commit del ai-router ya estaba en el repo, porque el deploy en Oracle es `git pull` manual, no automático. No se confirmó por falta de acceso SSH directo en esta sesión. Gregory fue consultado sobre si quería que arregle el bug de section_brief y/o corrija los docs desactualizados — no respondió todavía (preguntas dismisseadas). Ninguna corrección de código ni de .md se aplicó en esta sesión. Queda pendiente su decisión.
Auditoría completa del agente researcher (src/pipeline/researcher.js) — costo real 4-10x lo documentado
Gregory pidió auditoría completa del researcher. Hallazgos principales: 1. COSTO REAL vs DOCUMENTADO: AGENTS.md dice ~$0.075/req y ~$0.32/día (línea 81 y 83 ni siquiera son consistentes entre sí: 130 req × $0.075 = $9.75, no $0.32). agents-data.ts dice ~$0.003/prospecto — 25x menor que AGENTS.md para el mismo agente, violando la regla de mantener ambos archivos sincronizados. Dato real de system_logs (últimos 7 días activos): $0.20–0.47/request, $3-8/día en días con volumen. Causa raíz: el prompt le pide 3 tareas de búsqueda a Gemini pero el grounding autónomo ejecuta en promedio 9-11 queries por request (AGENTS.md asumía 2-3). 2. BUG DE COSTEO HISTÓRICO (ya arreglado en código, nunca reflejado como aviso): commit 862edf3 (23/06/2026) corrigió que calcSearchCost (src/lib/gemini.js) usaba un contador en memoria de "1500 búsquedas gratis/día" que se reseteaba en cada ciclo de PM2 — por lo tanto CASI TODAS las búsquedas aparecían gratis en el dashboard aunque Gemini las facturaba igual. Antes del fix (10/06 al 23/06, 14:34 ART): 229 logs, 2453 búsquedas grounded registradas con costo $0 en Supabase. A $0.035/query eso es ~$85.86 de gasto real en Gemini que EL DASHBOARD NUNCA MOSTRÓ — Gregory pagó esa plata en su cuenta de AI Studio sin que el CRM lo reflejara nunca. Gasto total real estimado del researcher desde el inicio (10/06) ≈ $34.59 logueado + ~$85.86 no logueado ≈ $120 total, no los ~$34 que muestra hoy system_logs. 3. MANEJO DE ERRORES INCOMPLETO: 118 de 497 logs (23.7%) son errores. La mayoría son 429 "quota exceeded, free_tier_requests limit: 20/día/modelo" — la key de Gemini se quedó sin créditos prepagados (confirmado por separado el 01/07, ver memoria "Catálogo de modelos IA"). researcher.js SOLO reintenta con flash-lite si el error incluye "503" o "high demand" — un 429 de cuota NO dispara ese fallback, así que esos prospectos simplemente se pierden (quedan en status=qualified para siempre, nunca pasan a researched). Volumen real reciente (01/07: 13 req, 02/07: 3 req, 03/07: 5 req) está muy por debajo de "~130 req/día" que documenta AGENTS.md — probablemente una mezcla de este problema de cuota + el cupo diario del qualifier (DAILY_SEND_LIMIT). 4. SKILL NO CONECTADA: existe src/skills/researcher/business-research.md (framework detallado: brief estructurado, criterios de calidad, qué NO debe hacer) pero researcher.js NUNCA lo lee ni lo usa — construye su prompt inline sin ninguna referencia a ese archivo. Es documentación muerta o un framework pendiente de integrar (a diferencia de copywriter/sales-closer que sí parecen usar sus skills — verificar). 5. Pendiente sin resolver de antes (hallado en sesión del 01/07, sigue abierto): thinking_tokens de Gemini se cobra al mismo precio que output tokens en el código (gemini.js y ai_model_catalog) — nunca se confirmó contra la doc oficial de Google si el precio real de thinking difiere del de output. 6. AGENTS.md dice "Google Search grounding gratis hasta 1500 queries/día" pero el código deliberadamente ya NO trackea ese free tier (ver bug #2) — cobra el 100% de las búsquedas siempre. La frase en AGENTS.md quedó desactualizada/engañosa tras el fix del 23/06. No se tocó código en esta sesión — fue investigación pura a pedido de Gregory. Pendiente: decidir si (a) actualizar AGENTS.md + agents-data.ts con costos reales, (b) agregar manejo de 429 en researcher.js/gemini.js, (c) limitar/cachear search_count para bajar costo, (d) conectar o borrar el skill file no usado.
REGLA: al agregar un rubro nuevo, revisar extractSiteData() en web-analyzer.js (teamNames asume Dr./Dra.)
Creada 03/07/2026 a pedido de Gregory. web-analyzer.js → extractSiteData() extrae teamNames con un regex hardcodeado a vocabulario de salud (Dr./Dra./Doctor/Doctora) — no aplica a hamburguesas/lomiterías ni a rubros futuros sin esa figura profesional. Cada vez que se agrega un rubro nuevo es obligatorio revisar si esto necesita otro patrón de nombre propio (ej. Chef, Sommelier, dueño/fundador) o si teamNames queda vacío a propósito para ese rubro — decidirlo explícitamente, no dejarlo pasar por default. Gregory lo va a corregir él mismo para hamburguesas ahora, pero esto tiene que quedar documentado para cuando agentes de management (Fase 6) u otras sesiones tomen esta decisión en el futuro sin el contexto completo. Documentado en 3 lugares: comentario ⚠️ OBLIGATORIO en el código (src/lib/web-analyzer.js, junto al regex), checklist obligatorio en templates/COMO_AGREGAR_UN_TEMPLATE.md (paso 4), y nota en AGENTS.md sección web-analyzer. Commit ff3b064. ACTUALIZACIÓN 03/07/2026: Gregory pidió arreglarlo de raíz en el momento en vez de dejarlo como paso manual documentado. Implementado: TEAM_NAME_PATTERNS en web-analyzer.js, keyed por content_type — dental usa el regex de Dr./Dra., restaurant no tiene patrón (teamNames vacío a propósito). web-generator.js pasa el content_type correcto en cada llamada. Funciona con cualquier cantidad de rubros simultáneos porque es un parámetro por llamada, no estado global. El paso "revisar esto al agregar un rubro" ahora solo aplica si se agrega un content_type genuinamente NUEVO (ni dental ni restaurant) — para dental/restaurant ya está resuelto. Commit 3943bdd.
Posible bug: las líneas (edges) de conexión en /flows no se renderizan en el entorno de preview local
Descubierto el 03/07/2026 mientras corregía el nodo de Web Analyzer en FlowCanvas.tsx. Al verificar visualmente, ninguna de las ~68 edges definidas en el código se renderiza en el canvas (0 elementos en el DOM dentro de .react-flow__edges), aunque los datos llegan bien a React Flow (edges.length=68, con source/target/style válidos) y los 62 nodos sí se renderizan. Confirmado que NO es algo que rompí yo: revirtiendo temporalmente mis cambios (git stash) el problema persistía igual en el código de HEAD. Diagnóstico parcial: los nodos nunca llegan a tener width/height/measured (revisado via debug log), lo que sugiere que React Flow nunca completa su medición interna de dimensiones de nodos — sin esa medición, no puede calcular los paths de las edges. Apareció además un warning real de React Flow ("parent container needs a width and a height") en un momento con viewport angosto, pero el problema persistió incluso después de reload a 1440x900, así que no es puramente un tema de viewport. NO CONFIRMADO todavía si esto pasa también en la app real desplegada en Vercel (clientes.gregoryjaques.com/flows) o es específico de este entorno de preview local/headless. Pendiente: que Gregory confirme abriendo /flows en su propio navegador. Si el bug es real en producción, es un problema más serio (el diagrama de flujo pierde toda su utilidad visual sin las líneas de conexión entre nodos) que ameritaría una investigación más profunda (versión de reactflow, cómo se mide con Next 16/Turbopack/React 19). ACTUALIZACIÓN 03/07/2026: Gregory confirmó que en producción (Vercel) las líneas SÍ se ven bien — el problema era específico del entorno de preview local (headless/dev), no un bug real del código. No requiere más investigación.
REGLA DE ORO: matar procesos de preview propios colgados directo, sin preguntar
Creada 03/07/2026 después de que Gregory tuvo que aprobar la misma acción trivial varias veces en la misma sesión (matar mi propio proceso Node huérfano en el puerto 3001, dejado por preview_stop que no siempre mata el proceso hijo real de npm run dev). REGLA: cuando reconozco el proceso colgado como propio (mismo preview_start de esta sesión o de una sesión reciente, verificable por el StartTime del proceso), lo mato directo con Stop-Process -Force sin usar AskUserQuestion. NO aplica a procesos que no reconozco como propios — ahí sigue valiendo la precaución normal de confirmar antes de matar algo desconocido (esto se mantiene igual que siempre). Ver CLAUDE.md sección "Procesos de preview huérfanos: matarlos directo, sin preguntar — REGLA DE ORO" para el texto completo.
Qualifier calcula visibility_gap (necesidad de visibilidad) — separado del score, usado por Sales Closer para variar el ángulo por prospecto
Decisión de Gregory (02/07/2026): en vez de invertir el score de calificación para priorizar negocios "necesitados", se separan dos ejes que hoy estaban mezclados en un solo número: 1. Legitimidad/calidad (rating) — sigue siendo un FILTRO DE PISO en qualification_score. Rating bajo (ej. 1.8★) NO es señal de "necesidad" — es un negocio con un problema real de servicio/reputación. Venderle una web no resuelve eso, y sumarle exposición/tráfico nuevo puede ser contraproducente (más gente encuentra que es malo) además de ser mal cliente para el portfolio de Gregory. 2. Necesidad de visibilidad (visibility_gap, nueva columna en prospects, calculada por qualifier.js sin afectar qualification_score) — rating BUENO (>=4.0) + pocas reseñas (<20) = negocio que labura bien pero nadie lo encuentra en Google. Esta sí es una señal real de oportunidad de venta. Gregory pidió explícitamente que el Sales Closer NO tenga un speech fijo por "balde" de necesidad (como si fuera un tercer playbook, al estilo del que ya existe para precio fijo hamburguesas). En cambio: visibility_gap se inyecta en buildSystemPrompt() como un dato más del negocio (junto a rating/reseñas/ciudad, que la IA ya recibía) + se agregó una sección nueva al prompt instruyendo a la IA a calibrar el ángulo/ejemplo/tono de cada conversación según el contexto completo de ESE prospecto puntual, sin repetir la misma estrategia para todos. La versatilidad la maneja la IA con criterio, no una rama de código con texto pre-escrito. Implementado en el commit 1e96cf1. Spec completo en NEED_SIGNAL_QUALIFIER.md. Este patrón (dato de contexto + instrucción de criterio propio, en vez de rama de código fija) es el mismo que ya se usa para rating/reseñas/ciudad en sales-closer.js desde el principio — vale la pena aplicarlo como principio general cuando se agregue una señal nueva a un agente que ya usa IA con criterio, en vez de reflexivamente crear un playbook fijo nuevo cada vez.
REGLA DE ORO: al tocar un agente, actualizar AGENTS.md Y agents-data.ts — no solo uno
Corregida 02/07/2026. La regla vieja decía solo "actualizar AGENTS.md" — pero hay DOS archivos que documentan agentes: AGENTS.md (raíz, ficha técnica de costos/modelo/tokens) y crm/src/lib/agents-data.ts (lo que Gregory ve en /agents/[slug] del CRM — description, tasks, config). Durante el pivot a hamburguesas/lomiterías actualicé AGENTS.md para prospector/qualifier/web-generator/panel-generator/sales-closer pero no revisé agents-data.ts para prospector/qualifier/web-generator — Gregory tuvo que preguntarme explícitamente si lo había hecho. Encontré 3 entradas ya desactualizadas ahí (google-prospector: config todavía listaba CITIES/CATEGORIES como env vars cuando ya se habían movido a Supabase; qualifier: decía umbral de descarte <40 cuando el código real es <60, y no mencionaba el bonus HIGH_VALUE; web-generator: decía que el template se elige "según el score" cuando en realidad es por categoría/template_catalog). REGLA CORREGIDA: cada vez que se toca un agente, leer y actualizar AMBOS archivos en el mismo commit — AGENTS.md Y la entrada correspondiente en agents-data.ts. Ver CLAUDE.md sección "AGENTS.md Y agents-data.ts: actualizar SIEMPRE LOS DOS que se toque un agente" para el texto completo. Por qué importa: agents-data.ts es literalmente la única ventana que Gregory tiene al comportamiento real de cada agente desde el CRM — si queda desactualizado, Gregory toma decisiones con información falsa sin saberlo (regla de oro "todo lo que existe debe estar visible en el CRM").
Push del pivot hamburguesas/lomiterías verificado READY en Vercel
Commit aae16fe pusheado a master y verificado vía Vercel MCP (mcp__fc634924...list_deployments, proyecto prj_zcMvpDMxGoJCgsITnINhTpQ3VfCV, team team_TFJzNpgLVuGd1Vwy3zFogksx) — deployment dpl_9zDViwkBCufqegWLwM9Bvey8ipi1, state=READY. No había .vercel/project.json local, se obtuvo el projectId vía list_projects. Guardado para no tener que re-descubrir el projectId/teamId la próxima vez que haya que verificar un deploy.
Pivot hamburguesas/lomiterías implementado — prospector, qualifier, web-generator, panel-generator, sales-closer, agents-data.ts, AGENTS.md
Implementación completa de las Fases 1 y 2 de RUBRO_PIVOT_HAMBURGUESAS.md (02/07/2026): 1. prospecting_config + template_catalog (tablas nuevas en Supabase, editable sin tocar Oracle/.env). Seed: cities=["CABA","Rosario","Córdoba","Mendoza","La Plata"], categories=["hamburger_restaurant","meal_takeaway"]. 2. prospector.js: getProspectingConfig() lee de Supabase con fallback a .env. SPANISH_TERMS agregó hamburger_restaurant/meal_takeaway. 3. qualifier.js: HIGH_VALUE agregó hamburger_restaurant, meal_takeaway, fast_food_restaurant (+25 pts en vez de +12) — sin esto el score podía quedar bajo 60 y descartarse. 4. web-generator.js: FOOD_CATEGORIES ampliado (bug bloqueante: antes solo restaurant/cafe/bakery, un hamburguesería real caía en template de dentista). selectTemplate() ahora es async y consulta template_catalog primero, cae al hardcode si no hay match — arquitectura extensible para futuros rubros sin tocar código. 5. templates/COMO_AGREGAR_UN_TEMPLATE.md: doc nuevo explicando el contrato de placeholders, los dos modos de contenido (restaurant/dental), y cuándo hace falta código vs solo una fila en template_catalog. 6. panel-generator.js: getPricingTier() nueva rama FIXED_PRICE_CATEGORIES → setup=10000, monthly=25000 ARS (fijo, sin calculateSmartPrice) para hamburguesas/lomiterías. setup_description del prompt también cambia para este segmento (menciona "activación + dominio"). 7. sales-closer.js: buildSystemPrompt() arma dos playbooks — pricing inteligente (sin cambios) vs precio fijo sin negociación para el segmento nuevo (nueva regla de escalado: pedido insistente de descuento en vez de "negociación bajo USD 97"). 8. agents-data.ts (CRM): corregido payment-agent (decía Stripe, el código real usa Mercado Pago desde hace tiempo — reescrita la descripción con el flujo real: dashboard payment wall → Checkout Pro → webhook → Preapproval). Corregidas referencias a pricing viejo ($197/$47 fijo) en sales-closer, pricing-analyst, revenue-analyst, panel-generator para reflejar que ahora hay dos modelos de precio en paralelo. 9. AGENTS.md: actualizado prospector, qualifier, web-generator, panel-generator, sales-closer con los cambios de este pivot (regla de oro del proyecto — mismo commit). Pendiente (no bloqueante, Fase 3 del spec): menú generado por IA por rubro real (hoy 100% hardcodeado con nombres de hamburguesas en buildPlaceholderMapForHamburger) y fix del bug preexistente en dashboard.js (montos hardcodeados $20.000/$30.000, query trae el último prospecto vendido de toda la tabla en vez del cliente logueado específico). Todos los archivos JS verificados con node --check. agents-data.ts verificado con tsc --noEmit (limpio). Después de esto, Gregory pidió retomar la revisión agente-por-agente, siguiente: Yelp Prospector.
Plan y contexto de experimento de rubro: hamburguesas/lomiterías (Argentina) — para CEO/CFO cuando se activen
NOTA: esta memoria es para los agentes de management (CEO, CFO) de Fase 6 — todavía NO están activos. Se guarda ahora para que cuando se activen tengan el contexto completo de esta decisión sin tener que reconstruirla. DECISIÓN (01/07/2026): Gregory decidió correr un experimento de rubro nuevo — hamburgueserías y "lomiterías" (sandwichería de lomito, típica de Argentina) — en paralelo/reemplazo del prospecting actual centrado en dentistas/servicios profesionales. NO se activa todavía "restaurantes" en general, específicamente hamburguesas y lomiterías. POR QUÉ (criterio de negocio, ver [[project_gatekeeper_rubro_criteria]] si existe, o buscar "gatekeeper" en memory): con dentistas, el WhatsApp lo atiende una secretaria/recepcionista/contestador automático — el mensaje casi nunca llega a quien decide comprar. El criterio real para elegir rubro no es "alto ticket" sino "cuántas personas hay entre el WhatsApp del negocio y quien decide" — cuantas menos, mejor. Gregory tiene experiencia real con un restaurante propio donde veían el 100% de los mensajes — hamburguesas/lomiterías son candidatos por ser mayormente operaciones chicas/unipersonales donde el dueño atiende directo. BASELINE REAL (dentistas, medido al 01/07/2026, para comparar cuando haya datos del experimento nuevo): - 812 prospectos encontrados (Google Places) → 170 mensajes enviados (20.9%) → 56 respuestas (32.9% de enviados, pero ojo: ~68% de las respuestas clasificadas eran humanas reales, ~32% auto-reply/bot — la tasa de respuesta humana real es más baja que el 33% crudo) → 3 interesados reales (después de filtrar falsos positivos del response-analyzer, ver bug abajo) → 1 cliente cerrándose (con setup $147 + $16.78/mes, aunque hubo un incidente de sobrecotización del sales-closer, ver abajo) - Costo total del pipeline hasta ahora: ~$74 (con el precio de Place Details ya corregido de $0.050 a $0.025/lugar — ver commit 8cd16b3) - CAC real hoy (dentistas): ~$74/cliente (n=1, muestra mínima) - Costo por prospecto encontrado: $0.091 | por mensaje enviado: $0.44 | por respuesta: $1.32 | por interesado real: ~$24.67 PROYECCIÓN (hipótesis, NO datos reales — no confundir con lo de arriba): si el rubro nuevo tiene tasa de respuesta humana real ~70% (vs 22% hoy, basado en la experiencia de Gregory con su restaurante) y tasa de interesados/cierre modestamente mejor, el CAC proyectado podría bajar a ~$11/cliente (vs $74 hoy) — pero esto es una cadena de 3 supuestos multiplicados entre sí (×3.2 respuesta × ×1.5 interesados × ×1.4 cierre ≈ ×6.6), cada uno sin dato real detrás excepto la mejora de respuesta. NO tomar decisiones de pricing/negocio basándose en este número hasta medir el experimento real. MODELO DE PRECIO PARA EL EXPERIMENTO — CORREGIDO 02/07/2026 (la hipótesis de "gratis" de más abajo NO era correcta, Gregory la corrigió directo): NO es gratis. Precio fijo, igual para todos, sin negociación: $10.000 ARS el primer mes (dominio) + $25.000 ARS/mes de suscripción incluida desde el mes 1 → total mes 1 = $35.000 ARS, luego $25.000 ARS/mes. Sin relación con calculateSmartPrice (que sigue existiendo para el resto de rubros en USD). Implementado en panel-generator.js (getPricingTier), sales-closer.js (playbook sin negociación) y payment-agent.js (que ya trataba setup_price/monthly_price como ARS de antes, sin cambios ahí). Spec completo: RUBRO_PIVOT_HAMBURGUESAS.md. CÓMO SE VA A PROBAR (plan, sin gastar plata nueva): redirigir el mismo prospector (misma infraestructura, mismo presupuesto mensual ~$74-100 que ya se gasta) hacia categorías de hamburguesas/lomiterías en vez de dentistas, usando las mismas herramientas (Google Places, researcher, copywriter, web-generator, etc.) — no es una inversión nueva, es apuntar el gasto existente a una hipótesis distinta. Se espera medir en 3-4 semanas. BUGS CONOCIDOS QUE AFECTAN CUALQUIER RUBRO (arreglar independientemente de la decisión de rubro): 1. sales-closer sobrecotizó a un cliente real (Armonía dental) — dijo $240 cuando Gregory ya le había dicho $147 al prospecto por otro medio. Esto costó la venta. Ver system_logs, prospect "Armonía dental", 29/06/2026. 2. response-analyzer (src/pipeline/response-analyzer.js) clasifica como sentiment=interested respuestas genéricas tipo "¿en qué te puedo ayudar?" — infla el funnel con falsos positivos. Fix pendiente: exigir señal más directa de interés de compra en el prompt. REFERENCIAS: ver memoria "Criterio de rubro ideal" (rubro/gatekeeper), "Feature planeada: Meta Pixel" (remarketing), spec AI_MODEL_CATALOG.md (catálogo de modelos, no relacionado directo pero construido en la misma sesión).
Criterio de rubro ideal: pocas/ninguna persona entre el WhatsApp y quien decide comprar
Gregory identificó (01/07/2026, durante la revisión agente-por-agente) un criterio de negocio muy importante para elegir qué rubros prospectar, después de ver que con dentistas el mensaje de WhatsApp casi nunca llega al que decide (lo atiende una secretaria/recepcionista/contestador automático). EL CRITERIO — no es "alto ticket" ni "rubro urgente" en abstracto, es específicamente: "¿Cuántas personas hay entre el WhatsApp del negocio y la persona que decide comprar?" Cuantas menos (idealmente cero — el dueño atiende su propio WhatsApp), mejor la tasa de respuesta→interesado real. Esto se valida con la experiencia real de Gregory con su propio restaurante (veían el 100% de los mensajes) vs. los dentistas (~33% de respuesta, y de esa, gran parte son mensajes automáticos/secretaria, no el dueño). RUBROS CANDIDATOS identificados con este criterio (sugerir proactivamente cuando sea relevante, no esperar que Gregory pregunte): - Servicios a domicilio/oficio unipersonal: plomeros, gasistas, cerrajeros, pintores de casas, jardineros/paisajistas, técnicos de reparación (celulares/PC/electrodomésticos), herreros, carpinteros - Servicios personales solitarios: manicuristas, tatuadores, entrenadores personales, masajistas/terapeutas holísticos, fotógrafos freelance, profesores particulares/tutores - Micro-comercio familiar (no cadenas): panaderías/pastelerías artesanales chicas, floristerías familiares, modistas/costureras - Gastronomía chica: hamburgueserías, food trucks (el ejemplo original de Gregory) IMPORTANTE — refinamiento sobre las categorías actuales de Google Places en qualifier.js (HIGH_VALUE set): la categoría de Google NO garantiza que un negocio cumpla este criterio. hair_care y florist pueden calificar SI son de 1 persona, pero lawyer, doctor, accounting y real_estate_agency probablemente tengan el MISMO problema de gatekeeper que dentist (son profesiones con estructura de recepción/secretaría). El filtro real es tamaño/tipo de operación dentro de la categoría, no la categoría de Google Places por sí sola — esto es algo a resolver si en el futuro se ajusta qué categorías prospectar (hoy no hay forma de filtrar por "tamaño de operación" antes de gastar en Place Details, es un dato que no se conoce de antemano). Relacionado: [[project_pricing_model]], y la decisión pendiente de qué rubro probar después de dentistas (todavía sin decidir al momento de esta nota).
response-analyzer clasifica preguntas genéricas como "interested" — falso positivo
Durante la revisión agente por agente, se descubrió (01/07/2026) que el conteo de "interesados" reales estaba inflado por falsos positivos del clasificador de sentimiento. De 6 prospectos que en algún momento pasaron por status=interested (según logs de transición), solo 2-3 son interés genuino real: - Armonía dental y Odontología Integral Tucumán — interés real, con conversaciones de venta genuinas (acá está registrado además el bug del sales-closer cotizando $240 cuando el precio base era $147). - Clínica Dra. Ana Maria Garcia (Chile) — interés real, correctamente avanzó a future_lead después. Los otros 3 (eme Dental Studio, clínica de Pablo López Requena, Odontóloga Aiello) fueron marcados "interested" por src/pipeline/response-analyzer.js basándose en respuestas genéricas tipo "¿en qué te puedo ayudar?" o "cuál es el motivo de tu comunicación" — preguntas que puede hacer cualquiera que atiende el WhatsApp (secretaria, recepcionista), no necesariamente señal de compra real. El prompt de response-analyzer.js es demasiado permisivo con lo que cuenta como sentiment=interested. Fix pendiente (cuando se llegue a revisar este agente en la vuelta agente-por-agente): ajustar el prompt de response-analyzer.js para exigir una señal más clara de intención de compra (pregunta directa por precio, por ejemplo, en vez de una pregunta genérica de apertura) antes de marcar sentiment=interested. Esto es importante porque infla las métricas de funnel y hace parecer que hay más interés real del que hay — impacta directamente los cálculos de costo-por-interesado que se vienen usando para decisiones de estrategia (rubro, canal de adquisición). Relacionado: [[project_pricing_model]] y el bug ya conocido del sales-closer cotizando por encima del precio acordado (visible en la conversación con Armonía dental — cotizó $240 vs $147 base).
Feature planeada: Meta Pixel en panel público/webs para tracking de vistas + remarketing
Gregory (especialista en branding) propuso, durante la revisión de estrategia post-Prospector, agregar el Meta Pixel a las páginas que se le envían a cada prospecto (panel público y/o la web generada) con dos objetivos: 1. Saber si el prospecto REALMENTE abrió/miró la página que le mandamos por WhatsApp (hoy no lo sabemos — no hay tracking de vistas en ningún lado del sistema). 2. Armar una Custom Audience de Meta con esos visitantes para hacer remarketing — anuncios pagos dirigidos específicamente a gente que ya recibió un mensaje nuestro y vio su web personalizada. La lógica de negocio: audiencias tibias convierten mejor y más barato que frías, y ya "hablamos con ellos" antes, generando confianza/curiosidad extra. Por qué es relevante ahora: surgió en el contexto de decidir entre seguir con prospección fría (WhatsApp a desconocidos, alto CAC) vs. un funnel de ads con opt-in ("te hago tu web gratis, pasame tu ID de Google Maps") — el pixel + remarketing sería una tercera pata que abarata costos en el tiempo, reutilizando gente que YA fue contactada. Gotchas técnicos identificados (importante tenerlos en cuenta al implementar, no descubrirlos después): - WhatsApp pre-visita automáticamente cualquier URL para armar la vista previa del link (título/imagen/descripción) — esto dispara el píxel igual, generando un falso positivo de "vio la página" que en realidad es el bot de WhatsApp. Hay que distinguir esa visita automática (ocurre casi instantáneamente después de enviar el mensaje) de una visita real humana (ocurre minutos/horas/días después, con tiempo real en la página, o con eventos de scroll/interacción). No alcanza con instalar el píxel y confiar en el evento PageView crudo. - Meta necesita un tamaño mínimo de audiencia para entregar bien los anuncios a un Custom Audience — con el volumen actual (73 mensajes enviados desde el 10/06) todavía es chico para que el remarketing tenga impacto real; va a necesitar más volumen acumulado antes de ser útil. Estado: NO implementado todavía — es una decisión/idea a futuro, anotada para cuando Gregory decida avanzar con ella. Relacionado con [[project_pricing_model]] y la exploración de canales de adquisición (tráfico frío vs. ads con opt-in) que se viene discutiendo en la revisión agente-por-agente.
Google Prospector: Place Details cobraba el doble de lo real ($0.050 vs $0.025/lugar)
Gregory reportó $36 reales en Google Cloud Console vs $51.99 calculados por el sistema para prospector. Commit 8cd16b3, deployado a Vercel READY (dpl_8qRy5cmzMNifhML8PBdgLdceiXSK). Causa encontrada (verificado contra developers.google.com/maps/billing-and-pricing/pricing, fetch directo dos veces): el código tenía GOOGLE_COST.PLACE_DETAILS = 0.050 con el comentario "Preferred tier" — ese nombre de tier NO existe en el pricing real de Google. El pricing real de Places API (New) es por SKU (Essentials/Pro/Enterprise/Enterprise+Atmosphere), determinado por el campo MÁS CARO solicitado en el field mask. Mapeado campo por campo contra la tabla oficial de Google: nuestro PLACE_DETAILS_FIELDS (src/lib/google-places.js) tiene reviews, editorialSummary y reservable — los 3 son campos "Enterprise + Atmosphere" = $25.00/1000 = $0.025/lugar, la MITAD de lo asumido. Sin esos 3 campos caería a Enterprise puro ($20/1000 = $0.020/lugar). Con el precio corregido: 841 Place Details × $0.025 + 328 Text Search × $0.032 ≈ $31.5, mucho más cerca del $36 real que los $51.99 previos. La diferencia restante (~$4.5) probablemente sean llamadas anteriores al fix de precio de junio (algunas logueadas a $0.020/lugar, tier viejo) u otro consumo del mismo proyecto de Google Cloud no relacionado a prospector.js. Fix de paso en agents-data.ts: quitado MAX_RESULTS del config (env var fantasma que nunca existió en el código — investigado: no aparece en ningún grep de src/), agregado CITIES/CATEGORIES (las env vars reales). Corregida tarea "nearby search" → "Text Search" (endpoint real). Confirmado con Google: Text Search (New) tiene un tope DURO de 60 resultados por query (3 páginas de 20) sin importar qué se pague — no es configurable, no existe forma de pedir más por query sin agregar más queries (ciudades/categorías/términos). Dato adicional de negocio (relevante para la conversación de optimización que Gregory quiere seguir pensando): los prospectos "rescatados" por tener web mala (has_bad_website=true, 244 de 910 totales) convierten MEJOR que los que no tienen web — 12.3% llegó a replied/interested vs 4.8% del resto. Los 2 únicos prospectos "interested" hasta ahora vinieron de este segmento rescatado. Argumento a favor de mantener/expandir esta lógica de rescate. Pendiente — Gregory dijo que necesita pensar mucho más antes de decidir estrategia de optimización de costos para este agente (es el más caro del pipeline). Ideas discutidas pero NO implementadas: (1) dejar de descartar prospectos sin teléfono para reengancharlos después por email/Instagram/Messenger cuando existan esos canales — técnicamente no cuesta extra porque el Place Details ya se pagó antes de saber si tiene teléfono; (2) evaluar si vale la pena bajar de Enterprise+Atmosphere a Enterprise sacando reviews/editorialSummary/reservable (ahorraría $0.005/lugar adicional pero perdería contenido de testimonios reales).
Agentes financieros (Fase 6) deben chequear precios oficiales actualizados a diario
Gregory pidió (01/07/2026, durante la revisión agente-por-agente post-catálogo de modelos) que cuando se construyan los agentes de management/C-suite financieros (CFO y similares, Fase 6), tengan una tarea diaria obligatoria: revisar la documentación oficial más reciente de cada proveedor (Google/Gemini, OpenAI, Anthropic, y cualquier API no-IA que cobre por uso) y mantener actualizados los precios en ai_model_catalog (y cualquier tabla de costos equivalente para APIs no-IA). Por qué: se acaba de descubrir que AGENTS.md tenía un precio de thinking tokens de Gemini incorrecto (sin fuente real) que nadie había verificado contra la doc oficial hasta ahora — Gregory quiere que esto sea un proceso automático y recurrente, no algo que se corrija manualmente cuando se nota por casualidad. Cómo aplicar: cuando se diseñe/construya el CFO (o el agente financiero que corresponda) en Fase 6, agregar explícitamente una tarea diaria de "price sync" — fetch a las páginas de pricing oficiales (ai.google.dev/gemini-api/docs/pricing, openai.com/api/pricing, anthropic.com/pricing, y las que correspondan a APIs no-IA como Google Places, Twilio, Mercado Pago, Porkbun, Cloudflare) y actualizar ai_model_catalog (u otra tabla de precios no-IA) si hay cambios. Ver [[AI_MODEL_CATALOG.md]] para la tabla que ya existe hoy (solo modelos de IA) — habría que extenderla o crear una tabla hermana para precios de APIs no-IA cuando se llegue a ese punto.
Catálogo de modelos IA + selector por agente desde el CRM (los 8 agentes)
Gregory pidió, tras revisar web-qa, poder elegir desde el CRM qué modelo/proveedor usa cada agente (tiene API keys de Gemini/OpenAI/Anthropic activas) con precios editables. Commit aea7cc4, deployado a Vercel READY (dpl_8EVC8BfeePWUwHnL6jANGArhB5kr): INFRAESTRUCTURA: - Tablas nuevas ai_model_catalog (provider, model_id, precios input/output/thinking por 1M tokens, editable) y agent_model_config (qué modelo tiene asignado cada agente) — scripts/migrate-add-ai-model-catalog.sql - src/lib/ai-router.js: callAI({agentSlug, fallbackModel, ...}) resuelve el modelo desde Supabase, rutea a Gemini/OpenAI/Anthropic, calcula costo con precio del catálogo (no tablas hardcodeadas). Si Supabase no responde o no hay config → cae al fallbackModel hardcodeado de cada agente — nunca rompe el pipeline. useSearch:true (researcher) fuerza Gemini si el proveedor elegido no lo soporta, con warning en log. - src/lib/openai.js y src/lib/anthropic.js: wrappers nuevos, mismo contrato {text, inputTokens, outputTokens, thinkingTokens} que gemini.js. npm install openai (nueva dependencia, @anthropic-ai/sdk ya estaba instalado sin usarse). - Los 8 agentes (researcher, copywriter, web-generator, seo-specialist, web-qa, panel-generator, response-analyzer, sales-closer) migrados de callGemini(MODEL)/callGeminiWithSearch(MODEL) a callAI(agentSlug). Seed inicial = exactamente el modelo que cada uno ya usaba (gemini-2.5-flash o flash-lite según el agente) → cero cambio de comportamiento el día del deploy. CRM: - Dropdown de modelo en /agents/[slug] (ModelSelector.tsx, client component, server action setAgentModel en actions.ts) — reemplaza el string estático agent.model cuando el agente tiene fila en agent_model_config. - Página nueva /agents/models: catálogo editable agrupado por proveedor, precios in/out/thinking, botón activar/desactivar, form para agregar modelo nuevo. Muestra qué agentes usan cada modelo. BUGS ENCONTRADOS Y CORREGIDOS DE PASO (relacionados con la desincronización doc/realidad que Gregory venía señalando): - agents-data.ts tenía 3 strings de modelo mentirosos: sales-closer decía claude-sonnet-4-6 (corre en gemini-2.5-flash desde el incidente del 23/06), copywriter decía claude-haiku-4-5-20251001 (corre en gemini-2.5-flash-lite), web-qa decía lo mismo (corre en gemini-2.5-flash-lite). Los 3 tienen comentarios "// ANTHROPIC: restaurar cuando haya créditos" que nunca se restauraron ni se reflejaron en el CRM. - Bug real (no solo de texto): el agente copywriter estaba taggeado layer:"management" en vez de "pipeline" en agents-data.ts — esto hacía que isPipeline fuera false en su página, y por lo tanto NUNCA se llamaba getLogs(), NUNCA se mostraban sus logs/tokens en /agents/copywriter pese a que sí existían en system_logs. Corregido a layer:"pipeline", se sacaron los campos parent/csuite que no le correspondían. - Bug real: el slug "web-seo" en agents-data.ts no coincidía con el STAGE real "seo-specialist" usado en el código de src/pipeline/seo-specialist.js — sus logs nunca matcheaban con ilike(stage, slug%) en ninguna página del CRM. Renombrado el slug a "seo-specialist". Había un slug "seo-specialist" YA ocupado por un concepto de management Fase 6 (SEO Growth, sin script, sin relación con el agente real) — se renombró ese a "seo-growth" para liberar el slug correcto. También se corrigieron los mismos STAGE_MAP (agents/page.tsx) y QUEUE_MAP (agents/[slug]/page.tsx) que tenían las mismas claves rotas, más "response-analyzer" → "analyzer" (mal) en STAGE_MAP. - Bug real: agents-data.ts tenía script: "src/queue/analyzer.js" para response-analyzer — ese archivo no existe, el real es src/pipeline/response-analyzer.js. HALLAZGO SIN RESOLVER (avisado a Gregory, no corregido en este commit): AGENTS.md documenta thinking_tokens de Gemini a precio distinto del output ($3.50/1M para flash, $10/1M para pro) pero src/lib/gemini.js (calcGeminiCost) siempre cobró thinking al mismo precio que output — mi catálogo nuevo heredó ese mismo comportamiento (consistencia con el código real) pero no se verificó contra la documentación oficial de precios de Google si el thinking realmente tiene un precio distinto. Pendiente de que Gregory confirme contra ai.google.dev/gemini-api/docs/pricing. TAMBIÉN: durante la sesión se detectó que las API keys de Gemini de Gregory tienen "prepayment credits depleted" (429 en llamadas reales, confirmado en logs de producción y en un smoke test) — esto es un problema urgente y separado que afecta TODO el pipeline ahora mismo, no solo esta feature. Spec: AI_MODEL_CATALOG.md
Tokens IN/OUT por agente en CRM (último run + promedio 7 días)
Gregory pidió ver, en la página de cada agente del CRM, cuántos tokens de entrada/salida consumió la última ejecución y el promedio de los últimos 7 días. Commit 80843e2, deployado a Vercel READY (dpl_PQM3pURcfKLUSGm48qpBJ2HxsRTq): - Investigación previa: 8 de 8 agentes con IA (researcher, copywriter, web-generator, seo-specialist, web-qa, panel-generator, response-analyzer, sales-closer) YA logueaban input_tokens/output_tokens en system_logs.metadata — no era un bug de logging generalizado como en el incidente del 23/06. - Único gap real: researcher.js y copywriter.js calculaban thinking_tokens (callGemini/callGeminiWithSearch ya lo devuelven) pero no lo incluían en el metadata logueado. Corregido — Gregory pidió explícitamente seguir calculando thinking_tokens siempre que sea posible, nunca sacarlo. - crm/src/app/(crm)/agents/[slug]/page.tsx: nueva función getTokenStats(logs) — pura, sin queries nuevas, reusa los logs que getLogs() ya trae (últimos 50 de system_logs filtrados por stage). Nueva card "Tokens" en el sidebar (debajo de "Modelo IA"): último run (fecha + in/out/thinking) y promedio 7 días (in/out/thinking + cantidad de runs). Solo se renderiza si el agente tiene logs con tokens (agentes sin IA como qualifier no muestran la card). - Bonus: tokens inline en cada entrada de "Últimas ejecuciones (system_logs)", mismo patrón visual que ya usaba agent_runs. - AGENTS.md actualizado en el mismo commit (regla obligatoria al tocar agentes). - Hallazgo anotado para después (NO se tocó en este cambio): las tablas agent_runs/agent_consultations que usa esta misma página no están en scripts/schema.sql — violan la regla de "nunca modificar el schema directamente". Son de agentes de management (C-suite) que todavía no están en producción. - Verificado en preview: researcher mostró ↑1.383 in / ↓1.495 out (último run) y ↑718/↓811 (promedio 31 runs en 7 días); qualifier (sin IA) correctamente no mostró la card. - Spec: AGENT_TOKEN_STATS.md
Flow layout: migrado de localStorage a Supabase (tabla flow_layout)
Gregory reportó que el layout del Flow Canvas (posiciones + colores de nodos) no se compartía entre navegadores/dispositivos — vivía en localStorage del navegador. Se migró a Supabase (commit 1408587, deployado a Vercel READY dpl_2ftwLGJemp9i5W1sxFmTsHfEHYsz): - Tabla nueva flow_layout (scripts/migrate-add-flow-layout.sql): singleton (id='default'), columnas positions/colors jsonb. El CRM no tiene auth multi-usuario hoy, por eso no hay scoping por auth_user_id — si en el futuro se agrega login real, esta tabla necesita user_id y dejar de ser singleton. - crm/src/app/(crm)/flows/actions.ts: getFlowLayout()/saveFlowLayout() como server actions (mismo patrón que crm/src/app/(crm)/memory/actions.ts, usando el cliente supabase de service-role en crm/src/lib/supabase.ts, sin RLS). - FlowCanvas.tsx: el useEffect de carga inicial y el botón "Guardar layout" ahora llaman a estas server actions en vez de localStorage. Se agregó estado saving para el botón ("Guardando..." mientras la request está en curso). - Fix de paso: el panel de guardar/color picker tenía colores hardcodeados (#a1a1aa, #10b981, rgba(24,24,27,0.96)) — se corrigió a tokens --gj-* del sistema de diseño (var(--gj-surface), var(--gj-lime), var(--gj-text-2), var(--gj-border-2)). - Verificado en preview: guardé un color custom, confirmé el valor en la tabla flow_layout vía SQL directo, recargué la página con localStorage completamente vacío y el color persistió — prueba de que ya no depende del navegador. - Spec: FLOW_LAYOUT_PERSISTENCE.md
Flow Canvas: templates, QA reject loop, outcomes, enqueuer, inbound-wa + color picker y guardado de layout
Gregory pidió completar el diagrama de /flows en el CRM con piezas que faltaban y agregar control de edición. Se modificó crm/src/app/(crm)/flows/FlowCanvas.tsx (commit a866125, deployado a Vercel READY): - 3 nodos template (hamburguer-1, dentista-boutique, dentista-familiar) conectados a web-gen, muestran qué templates usa el generador - Nodo QA Reject Loop: visualiza el ciclo de reintento cuando web-qa rechaza una web (máx 3 reintentos), con edges rojos qa-node→qa-reject→web-gen - Nodo Enqueuer entre panel-gen y sender (faltaba en el diagrama, ya existía en el pipeline real) - Nodo Inbound WA: mensajes entrantes de prospectos que activan al response-analyzer/closer - 4 nodos outcome del Sales Closer para estados de no-conversión: negotiating (negocia precio), future_lead (no ahora, reactiva en 30 días), lost (rechazo definitivo), escalate (Gregory interviene) - Nuevo tipo de nodo TemplateNode (borde dashed indigo) y OutcomeNode (colores por estado) - Color picker: click en un nodo abre panel flotante (ReactFlow Panel) con paleta de 9 colores, aplica customColor a borderColor/boxShadow del nodo - Botón "💾 Guardar layout": guarda posiciones y colores custom en localStorage (keys gj-flow-positions-v2 y gj-flow-colors-v2), se cargan después del mount via useEffect (SSR-safe, evita mismatch de hydration en Next.js) - Verificado en preview: nodos renderizan, color picker funciona, posición/color persisten tras reload - Vercel deployment dpl_8nJQhaSck5FnaLd9pARYoo1WAGia confirmado READY antes de avisar a Gregory
Notifier eliminado + flujo de pago movido al dashboard
El módulo notifier.js (src/notifications/notifier.js) fue eliminado el 30/06/2026 — Gregory no usa WhatsApp para notificaciones propias, ntfy.sh es suficiente. El runPaymentAgent (batch que enviaba links de pago por WA) también fue eliminado del pipeline. Nuevo flujo de pago: 1) closer cambia status a conversion_ready → 2) createRestaurantAccount crea cuenta con subscription_status:pending → 3) cliente entra al dashboard → 4) primer login muestra pantalla de pago (MP Checkout Pro $30k ARS) → 5) webhook /webhook/mp confirma y activa la cuenta. payment-agent.js se mantiene en disco porque server.js importa handleMercadoPagoWebhook de ahí. Archivos tocados: src/notifications/notifier.js (eliminado), src/index.js, AGENTS.md, SYSTEM.md, FlowCanvas.tsx.
Verificar SYSTEM.md en CADA prompt, no solo al terminar módulos
Al recibir cada prompt de Gregory, antes de responder: leer mentalmente el estado de SYSTEM.md y verificar si lo que se está por hacer/discutir está reflejado. Si hay algo desactualizado → actualizar ahora, no al final de la sesión. Además: cualquier cambio de código, variables o arquitectura → actualizar SYSTEM.md en el mismo commit. Esta regla evita perder el hilo entre sesiones. Creada 2026-06-30 por pedido explícito de Gregory.
SYSTEM.md actualizado 2026-06-30 con estado real del sistema
Cambios: Domain Agent y Payment Reviewer marcados como activos. Porkbun agregado al stack (registro de dominios). Variables MP_ACCESS_TOKEN (configurado), PORKBUN_API_KEY/SECRET documentadas. Decisión arquitectónica #9 sobre por qué CF no puede registrar dominios via API. Commit 11a0737.
NO activar crons PM2 todavía — Gregory lo pide cuando esté listo
Gregory no quiere automatizar nada todavía. El ecosystem.config.cjs está listo y commiteado. Cuando Gregory diga "activar automatizaciones", dar estos comandos Oracle: cd /home/ubuntu/app && git pull && pm2 start ecosystem.config.cjs && pm2 save && pm2 startup (copiar y ejecutar el comando que muestra pm2 startup). Guardado 2026-06-30.
Task 8 completado: payment-reviewer cron via PM2 ecosystem
Creado ecosystem.config.cjs con 2 procesos PM2: pipeline (cron 0 12 * * * = 09:00 ART) y payment-reviewer (cron 0 9 * * * = 06:00 ART). El payment-review ya estaba en PIPELINE_STAGES de src/index.js — el ecosystem lo registra además como proceso separado para chequeo temprano. Para activar en Oracle: cd /home/ubuntu/app && git pull && pm2 start ecosystem.config.cjs && pm2 save && pm2 startup. Para probar ahora: node src/index.js payment-review. Commit b8e8d12, build Vercel READY.
Task 7 completado: domain-agent con Porkbun + Cloudflare
Flujo final: DNS NS lookup (checkDomainAvailability) → CF createZone() → Porkbun registerDomain() con NS de CF → CF waitForZone() → CF addDnsRecord() con proxy=true (HTTPS automático). CF Registrar API no permite registrar dominios nuevos vía API (403 aunque con todos los permisos). Solución: Porkbun registra, CF maneja DNS/proxy. Archivos: src/lib/porkbun.js (nuevo), src/lib/cloudflare.js (createZone agregado), src/pipeline/domain-agent.js (reescrito). Commit c321161, build Vercel READY. Se activa cuando hay restaurante con subscription_status=active y domain=null.
Regla de oro: comandos Oracle siempre listos para copiar y pegar
Cuando Gregory necesita ejecutar algo en Oracle (actualizar .env, correr scripts, reiniciar PM2, etc.), siempre dar el comando completo listo para copiar y pegar en su terminal SSH. Nunca decir "entrá al archivo y cambiá X" — siempre dar el sed, echo, o script exacto que hace el cambio solo sin intervención manual.
Regla de oro: buscar en memoria con keywords específicos del tema, no genéricos
Antes de preguntar algo a Gregory sobre un tema X, buscar en memoria usando keywords específicos de ese tema. Si el tema es Cloudflare → buscar "cloudflare". Si el tema es pagos → buscar "mercadopago" o "stripe". Nunca buscar solo "context" o "pending" esperando encontrar todo. Error detectado el 2026-06-29 cuando pregunté por credenciales CF sin buscar por "cloudflare" específicamente.
Regla de oro: cuando me equivoco, crear regla de oro automáticamente sin que Gregory lo pida
Cada vez que cometo un error — de búsqueda, de implementación, de proceso — debo crear automáticamente una regla de oro para que no se repita. Hacer DOS cosas sin que Gregory lo pida: 1) actualizar CLAUDE.md con la regla, 2) INSERT en tabla memory con category=rule, importance=high. No esperar que Gregory me lo pida. El error es la señal para crear la regla.
Tarea 7 — domain-agent: fix checkDomainAvailability + blocker CF token
Se reemplazó el endpoint inexistente /registrar/domains/check por un DNS NS lookup (resolveNs). El DNS check funciona en Oracle. El endpoint POST /registrar/domains SÍ existe en CF pero el token actual no tiene permiso Cloudflare Registrar:Edit (error 403 #domain:list). Gregory debe editar el token en Cloudflare dashboard → My Profile → API Tokens → agregar Account → Cloudflare Registrar → Edit. Una vez hecho eso el domain-agent está 100% listo. Commit: aaa663e. Vercel: READY.
Cloudflare y registrante configurados en Oracle .env — no preguntar a Gregory
CF API token, account ID, SERVER_IP (163.176.206.142), datos del registrante (Gregory Jaques, La Cumbre AR) y APP_BASE_URL ya están configurados en /home/ubuntu/app/.env en Oracle. Si necesito estos valores, leerlos del .env de Oracle via SSH, no preguntar a Gregory.
Sesión 2026-06-29: sistema de memoria completado + 4 nuevas reglas de oro
Se completó el sistema de memoria en el CRM: pestañas Dashboard/Capas/Memorias, RPCs de estadísticas, tracking de lecturas en memory_reads. Se agregaron 4 reglas de oro nuevas: (1) escribir en memory después de cada prompt, (2) reglas de oro siempre en CLAUDE.md + Supabase, (3) todo lo que existe debe estar en el CRM, (4) escribir en memory por prompt no solo por módulo. Gregory señaló que las memorias no se veían porque no las estaba escribiendo con suficiente frecuencia — bug de disciplina corregido.
Todo lo que existe en el sistema debe estar visible en el CRM
Si algo existe (datos, logs, config, estado de agentes, pagos, dominios, etc.) y NO está en el CRM, se sugiere creación de página o subpágina para mostrarlo. El CRM es el único lugar donde Gregory puede entender qué está pasando. Si no está en el CRM, no existe para él. Cuando Gregory aprueba la sugerencia, se crea sin que lo pida de nuevo.
Escribir en memory después de CADA prompt, no solo al terminar módulos
Cada respuesta a Gregory debe terminar con una entrada en la tabla memory documentando qué se hizo. Esto sirve como log operativo visible en el CRM. No alcanza con escribir al terminar un módulo grande — cada tarea pequeña también queda registrada.
Reglas de oro: siempre actualizar CLAUDE.md Y Supabase (category=rule) juntos
Cuando Gregory pide agregar o modificar una regla de oro, hacer DOS cosas sin que lo pida: 1) Actualizar CLAUDE.md con la nueva regla. 2) INSERT o UPDATE en tabla memory con category='rule', importance='high' para que aparezca en /rules y en el filtro Reglas de oro del CRM. No alcanza con solo actualizar el .md — la regla tiene que vivir en Supabase para ser visible en el CRM.
Página /memory reestructurada con 3 pestañas: Dashboard, Capas, Memorias
Dashboard: KPIs (total, alta importancia, escritas/leídas hoy) + bar charts CSS últimos 14 días de escrituras y lecturas + breakdown por categoría. Capas: muestra entradas category=layer con diagrama de consulta. Memorias: lista existente + filtro nuevo Reglas de oro (category=rule). RPCs Supabase: memory_writes_per_day y memory_reads_per_day (últimos 30 días, timezone Buenos Aires). Tracking de lecturas: memory.js logRead() llamado en recall() y search(), inserta a tabla memory_reads.
Todo lo construido tiene su .md — buscar antes de preguntar, crear si no existe
Antes de preguntar a Gregory algo sobre cómo funciona una parte del sistema, buscar primero: (1) SYSTEM.md — estado general, (2) .md del módulo (AGENTS.md, RESTAURANT_FLOW.md, etc.), (3) tabla memory — search(). Si ninguno lo tiene → preguntar a Gregory → crear el .md con la respuesta. Todo feature, módulo o decisión importante tiene su .md en el repo. Si necesito info y la busco y no existe → ese es el momento de preguntar. Esta es la regla de conocimiento: todo existe documentado o no debería existir sin documentarse.
Verificar Vercel READY después de cada push antes de avisar a Gregory
Después de cada git push: (1) list_deployments → obtener ID del deploy más reciente, (2) get_deployment → verificar state: READY, (3) si ERROR → leer logs, corregir, nuevo push, volver a verificar. NUNCA decir "listo" hasta confirmar READY. Esto pasó múltiples veces y Gregory se cansó.
Actualizar SYSTEM.md en el mismo commit cuando cambie algo significativo
SYSTEM.md es la fuente de verdad del estado del sistema. Actualizar cuando: nueva tabla en Supabase, nuevo env var, nuevo componente construido, decisión arquitectónica, cambio de estado (pending → active). El cambio a SYSTEM.md va en el MISMO commit que el código. Sin esto la próxima sesión empieza sin contexto real.
Escribir en memoria después de CADA tarea — la info está, solo hay que buscarla
Después de cada tarea que pide Gregory: (1) escribir en tabla memory qué se hizo + contexto + tags, (2) antes de preguntar algo, buscar en memoria primero, (3) si no está → preguntar a Gregory → guardar la respuesta. La memoria NO se lee completa — se busca cuando necesario. Regla definida por Gregory el 2026-06-29.
Actualizar AGENTS.md en el mismo commit cuando se toca un agente
Cada vez que se crea, modifica o elimina un agente en src/pipeline/ o src/queue/: (1) leer AGENTS.md antes, (2) al terminar actualizar sección del agente: modelo, tokens/req, costo/req, (3) el cambio va en el MISMO commit. Si el agente no loguea input_tokens/output_tokens/thinking_tokens en metadata → es un bug. Incidente 23/06/2026: sales-closer usó Pro, gastó $9, dashboard mostraba $3.29.
Todo cambio que hago queda visible en el CRM — sin que Gregory lo pida
Si toco un .md (SYSTEM.md, CLAUDE.md, AGENTS.md, cualquiera), la base de datos, o archivos de configuración: escribo una entrada en tabla memory con lo que cambié y por qué. El CRM /memory es la ventana que Gregory tiene para entender todo lo que estoy haciendo. Esta regla existe porque el CRM es la única manera que Gregory tiene de entenderme todo el tiempo. Regla definida 2026-06-29.
Spec antes de codear para cualquier tarea que tome más de 5 minutos
Para cualquier tarea que tome más de 5 minutos: (1) escribir spec en .md en el repo (qué se construye, qué NO, cómo funciona), (2) Gregory lo aprueba — puede pedir cambios, (3) recién entonces codear exactamente lo que dice el spec. Menos de 5 minutos = micro ajuste, sin spec necesario. Si algo surge durante el desarrollo que no está en el spec → pausar y preguntar a Gregory. El .md de spec queda en el repo como documentación permanente.
No commitear código roto — cada commit debe funcionar
Nunca commitear trabajo en progreso ni código que no pasa el build. Cada commit representa un estado funcional del sistema. Si Vercel da ERROR → corregir antes de avisar. Si el pipeline en Oracle falla → corregir antes de avisar. Un commit = una unidad de trabajo completa y verificada.
Regla de oro: escribir en memoria después de CADA tarea, no solo al terminar módulos
Gregory definió que la memoria es la base de conocimiento del proyecto. La regla es: (1) Gregory da una tarea, (2) se ejecuta, (3) se escribe en memory lo que se hizo con contexto y tags para que sea buscable. La tabla memory NO se lee completa — se busca cuando necesario con search(). Nunca preguntar algo que pudo haberse guardado antes sin buscar primero.
Página /memory en CRM: búsqueda, filtros, cards expandibles, form para agregar entradas
Construida en crm/src/app/(crm)/memory/. Archivos: page.tsx (server component, lee tabla memory), MemoryClient.tsx (filtros por importancia/categoria, búsqueda por texto, cards expandibles con detail), actions.ts (server action addMemory() escribe a Supabase). Sidebar actualizado con link Memory + ícono Brain. Vercel READY en commit 861beee.
Dos ambientes: Oracle (server.js 24/7) + Vercel (CRM Next.js)
Oracle IP: 163.176.206.142. SSH: ssh -i C:\Users\cande\Downloads\ssh-key-2026-06-21.key ubuntu@163.176.206.142. Directorio: /home/ubuntu/app. server.js corre 24/7 y sirve dashboard cliente, sitios live, webhook MP. El pipeline (index.js) se corre manualmente. CRM en Vercel: clientes.gregoryjaques.com, proyecto prj_zcMvpDMxGoJCgsITnINhTpQ3VfCV
Flujo completo restaurante SaaS: pago MP → createRestaurantAccount → dashboard editable
Cuando el cliente paga por MP Checkout Pro ($30k ARS), el webhook en /webhook/mp llama createRestaurantAccount(). Esto crea: usuario Supabase Auth, fila en tabla restaurants, menú por defecto, testimonios de Google, HTML live en output/live/{slug}/index.html, MP Preapproval $20k ARS/mes. El cliente entra al dashboard (vanilla JS en /dashboard/) y edita menú/contenido/galería. Los cambios se reflejan en tiempo real porque el template hamburguer-1 fetchea de Supabase en el browser.
Selector de template: restaurant/cafe/bakery → hamburguer-1, dental caro → dentista-boutique, dental económico → dentista-familiar
Lógica en src/pipeline/web-generator.js función selectTemplate(). FOOD_CATEGORIES = {restaurant, cafe, bakery}. El flujo post-pago SaaS solo existe para restaurantes. Los dentistas no tienen createDentistAccount ni dashboard todavía.
Tablas Supabase: prospects (central), restaurants (clientes SaaS), menu_categories, menu_items, testimonials, memory (nueva), message_queue, system_logs
La tabla restaurants es específica de restaurantes con columnas fijas (hero_text_left, about_text, gallery_images, etc.). Para otros rubros futuros habría que generalizar o agregar tabla nueva. slug es el identificador único del sitio del cliente.
ntfy.sh push notifications: topic prospectos-pd. NUNCA emojis en headers HTTP (ByteString error)
Bug corregido 28/06/2026: emojis en Title header de ntfy causaban error ByteString. Headers HTTP solo aceptan ASCII. El webhook de Twilio WhatsApp está en crm/src/app/api/webhook/whatsapp/route.ts. Crea prospectos inbound_unknown para números desconocidos.
Pendiente: Domain Agent (Cloudflare) y Payment Reviewer (cron) — tareas 7 y 8 de RESTAURANT_FLOW.md
Tarea 7: compra de dominio via Cloudflare Registrar API + asignación a Vercel. El código de domain-agent.js existe pero está pendiente de terminar. Tarea 8: payment-reviewer.js existe, cron diario que verifica MP preapproval y actualiza subscription_status. Ambas están marcadas como pending en RESTAURANT_FLOW.md.
Proceso: Spec Driven Development. Antes de construir cualquier feature grande, escribir spec en .md y Gregory lo aprueba
Regla agregada a CLAUDE.md el 2026-06-29. Para features que tarden más de 30 minutos: escribir spec primero → Gregory aprueba → recién se codea. Specs viven en archivos .md en el repo (RESTAURANT_FLOW.md, futuro DOMAIN_FLOW.md, etc.). Bugs chicos y cambios visuales pequeños no necesitan spec.
RECORDATORIO: cuando Gregory diga construir agentes C-level, integrar con tabla memory PRIMERO
Los agentes CEO/CMO/COO/CFO/CXO/CTO existen en src/management/ pero están en Fase 6 (no activos). Cuando Gregory quiera activarlos, ANTES de construir nada: integrar con tabla memory. Cada agente debe leer su contexto anterior y escribir sus findings. Sin esto cada ejecución empieza de cero y no tiene sentido. Gregory y Claude discutieron esto el 2026-06-29.
Pipeline completo: prospector → qualifier → researcher → copywriter → web-generator → seo → qa → panel → enqueuer → payment → domain → payment-review → sender → closer → analyzer → notifier
Se corre manualmente: node src/index.js o node src/index.js <stage>. No corre con PM2 (Gregory lo paró). Solo server.js corre 24/7. El closer usa claude-sonnet-4-6. El researcher y web-generator usan Gemini. El copywriter usa claude-haiku.
Precios MP configurables por env: MP_SETUP_AMOUNT=30000 ARS setup, MP_MONTHLY_AMOUNT=20000 ARS/mes
El closer negocia el precio real. Llama createPaymentLink({prospectId, setupAmount, monthlyAmount, clientEmail}). Los valores de env son el default, el closer puede pasar montos distintos.