09 — Plan de ejecución: Ada v2 en un solo PR
2026-09-11. Estado: plan para aprobación del fundador. Sin código detrás. Alcance: todo lo acordado en los docs 07 y 08 en un PR contrastaging: prerrequisitos del harness, sistema de skills con diez recetas, conducta (proactiva, aclara, saldo, límites), orquestación de campañas con tarjeta de proceso y pasos en segundo plano, tarjetas de resultado sobre DataArtifactRenderer, y el bench con medidor de utilidad como condición de merge.
Un PR, pero commits ordenados por paquete (A→E) para que sea revisable y para que el bench se pueda correr en cada corte. Todo detrás de una bandera LUNA_V2 (env, default on en staging, off en producción hasta que el bench pase), de modo que el merge no cambie producción hasta que tú lo decidas.
Estimación honesta: 12 a 14 días hábiles de trabajo continuo. Lo que no cabe en eso se dice en §7.
Paquete A — El harness deja de castigar (días 1-3)
A1. Razonamiento por turno
- Qué ve la persona: Ada replantea cuando una búsqueda da cero; deja de repetir la misma llamada tres veces.
- Cambio: el esfuerzo de razonamiento viaja del skill activo al proveedor.
metadata.reasoning: bajo | medio→runStreaming({ reasoning })enluna-turn.service.ts→ModelInvocation.thinking = { enabled: true, effort }enmastra-model.adapter.ts:323(hoy fuerzaenabled:falsesiempre que el modelo lo permite) →reasoning_effort: 'low' | 'medium'enlibs/model-gateway/src/adapters/openai-compatible.ts:158. Sin skill activo:bajo. La pasada de forma (toolChoice:'none') se queda sin razonamiento. - Riesgo: latencia y costo. Se mide en A7 antes y después sobre los 20 briefs; si p50 sube más de 2× con
bajo, el default baja anoney solo los skills de análisis pidenbajo.
A2. El workspace en el prompt
- Qué ve la persona: “cómo va mi pipeline” responde a la primera; Ada nunca vuelve a decir “no hay CRM”.
- Cambio:
luna-input.compiler.tscompila una sección## Tu workspace hoy(≤ 400 caracteres, caché 60 s por workspace): colecciones conrecord_countystage_counts(CollectionsService.stageCountsByCollection), secuencias con estado yready, buzón (sender.status), y los tres saldos (créditos de contacto, consultas del mes, tope de inferencia) leídos por el mismo camino queusage. Entra al manual antes de## El estado de este turno. - Spec:
luna-input.compiler.spec.ts— la sección existe, cabe, degrada a vacío si una lectura falla, nunca lleva un UUID (regla de fuga existente).
A3. Intents reordenados
- Qué ve la persona: Ada intenta antes de rendirse.
- Cambio:
INTENT_GUIDEenluna-manual.ts:responder→cierre→proponer_decision→encuadre→limite.limitesolo cuando un paso del skill lo declara o falta un conector. Bump de manual a 3.0.0.luna-manual.spec.tsy las expectativas de intent de los 20 briefs se revisan una por una (bench 6 ya las reescribió por precedencia; se vuelven a escribir por la nueva).
A4. Argumentos persistidos
- Qué ve la persona: al expandir “Ver el detalle”: “buscó: bombas hidráulicas · empresas · todo México”.
- Cambio:
tool.finishedllevaargs(crudo) yargs_label(lenguaje de producto, construido pordescribeScopeextendido enluna-mcp-tools.ts).board.runsse llena encollecting.exec(luna-turn.service.ts:700-768):{run_id, tool, args, ref_label}. Sin migración:payloades jsonb libre. Cliente:lunaRuntime.tsconservaargs_label;ToolRow.tsxlo muestra al expandir.
A5. Payload limpio
- Qué ve la persona: se acaba “no es un censo ni una prueba de capacidad” en cada respuesta.
- Cambio: en
luna-mcp-tools.ts, al proyectar un resultado de mercado,warnings[](los tres fijos) ynext_actionsalen del objeto que ve el modelo y pasan a_footnote(una frase en español), que solo lee el proyector de tarjetas (D1). Regla nueva del manual: “la fuente va al pie de la tarjeta; no la repitas”.
A6. El validador permite la pregunta legítima
- Qué ve la persona: Ada pregunta el comprador cuando no lo sabe, una vez, con default.
- Cambio:
LunaIntentValidator(luna.contracts.ts:502) recibeallowedQuestionKeysdel turno =requieredel skill activo que no estén en el libro de hechos. Unaencuadre.preguntasobre una de esas llaves no es defectopregunta. Se mantiene el bloqueo de preguntar lo ya sabido.
A7. Bench de control
- Correr los 20 briefs con A1-A6 y sin ellos. Tabla: intent, refs, ruta, costo, p50, herramientas por turno. Es la línea base del PR.
Paquete B — Sistema de skills y diez recetas (días 3-7)
B1. Loader y contrato
- Nuevo
apps/api/src/luna/skills/conluna-skills.ts: lee*/SKILL.md, parsea frontmatter (name,description,metadata: {version, reasoning, tools, cierra_con, requiere, siguiente, costo, gatillos, excluye}), valida en carga de módulo (cuerpo ≤ 2 500, descripción ≤ 1 024,toolsexisten en el catálogo del MCP, sin<>), y exponeSKILLScongelado. Mismo patrón quecognitive/skills.ts. references/se leen a demanda por B4. Referencia compartida_shared/limites.md(lo que Ada no puede hacer, cuánto por llamada, a dónde mandar a la persona).
B2. Índice en el manual
luna-manual.ts: nueva sección fija## Lo que sabes hacer=name · descriptionde cada skill (≈ 1 400 caracteres). SalenTOOL_ROUTINGyCRM_RULES; deERROR_HANDLINGsalen las líneas por herramienta. Presupuesto resultante ≈ 14 700 de 15 000 (§2 del doc 08).luna-manual.spec.tsse actualiza: las secciones eliminadas dejan de exigirse; el índice se exige en toda etapa.
B3. Router de dos etapas
luna-skill-router.ts: determinista. Entradas: mensaje normalizado, etapa, tablero (selección, guardado, plan pendiente), libro de hechos. Reglas:gatillosyexcluyede cada skill (frases), más señales de estado. Salida: hasta 2 candidatos conconfianza: alta | baja.- Confianza alta → el cuerpo se inyecta en el system prompt del turno y el cinturón se recorta (B5). Baja → solo “probable: X” en el estado; el modelo decide.
turn.acceptedllevaskills: [{name, version, by:'router'|'model', confianza}](payload jsonb, sin migración).diagnostics.skills_useden el resultado del turno para el bench.- Spec:
luna-skill-router.spec.tscon los 10 × 20 mensajes de disparo (E1) como tabla: es la prueba de disparo de la guía, a costo cero.
B4. skill_open y skill_reference
- Herramientas de sesión en
luna-mcp-tools.ts(junto aboard_read):skill_open {name}devuelve el cuerpo;skill_reference {skill, ref}devuelve la referencia. Presupuesto 4 500. Registranby:'model'enskills_used.
B5. Cinturón por skill
luna-stage.ts:toolsForTurn(stage, catalog, skills)= unión demetadata.toolsde los skills activos + herramientas de sesión; sin skill activo, el catálogo completo menos los retenidos (como hoy).luna-boundary.spec.tssigue garantizando quepeople_revealnunca entra.
B6. Las diez recetas
Cada una con la plantilla del doc 08 §1 (antes de empezar, pasos con plan B, cómo cierras, ejemplo, errores frecuentes) y el frontmatter completo. Orden de escritura, por impacto en las sesiones reales de ayer:como-va-mi-pipeline(incluye “análisis de mi CRM”)quien-le-ha-vendido(reusacognitive/skills/market-investigation/references/procurement-intelligence.md, enlazada, no copiada)cuantos-haybuscar-empresas-y-proveedores(reusasupplier-sourcing.md)senales-y-oportunidadestrabajar-una-cuentaconseguir-contactosarmar-secuenciaque-sabe-el-equipoguardar-en-crm
B7. Conducta cableada
siguiente: la pasada de cierre recibe la lista y el gate suavesiguiente_ofrecidomarca si elcierre/respondertrae una salida de esa lista (comosalidasancladas o decisión). No bloquea; mide.requiere: alimenta A6.costo: regla del manual “todo paso con costo se propone con precio, saldo y lo que queda; si no alcanza, el lote que cabe”. El saldo viene de A2.limites.md: cargada por todo skill activo como referencia de nivel 3 en el primer turno de la sesión y a demanda después.
Paquete C — Orquestación de campañas (días 7-10)
C1. El Plan en el tablero
luna.contracts.ts:BoardProjection.plan: LunaPlan | nullcon{plan_id, objetivo, skill:'campana', pasos: [{id, skill, titulo, status: pendiente|corriendo|esperando_toque|listo|fallido|omitido, conteo?: {hecho,total}, costo?: {creditos, saldo_despues}, decision_id?, run_id?}], costo_estimado, creado, actualizado}. Se pliega porboard.patchedcon path/plan(sin migración). Espejo enapps/dashboard/src/luna/contracts.ts.- Herramientas de sesión:
plan_write {objetivo, pasos}yplan_advance {paso_id, status, conteo?, decision_id?}. Solo el skillcampanalas lleva entools. stageFor: plan con pasos pendientes → etapaen_campana(nueva), que el router usa para reabrircampanaen “sigue”.
C2. La tarjeta de proceso
apps/dashboard/src/luna/cards/PlanCard.tsx+ partedata-luna-plan-card, insertada como hoy la tarjeta de tablero (lunaRuntime.ts:747-780) en el turno que creó o avanzó el plan. Pasos con estado, conteos, costo estimado y saldo; el pasoesperando_toqueincrusta laDecisionCardexistente pordecision_id; el pasocorriendomuestra progreso de C3. Precedente visual:InvestigationLane.tsx.
C3. Pasos en segundo plano
luna-plan-runner.service.ts: ejecuta un paso cuya receta lo marcasegundo_plano: true(hoy: descubrir personas por cuenta, buscar empresas por lotes) fuera del turno. Usa el mismoLunaToolBackingy el mismoHarvest, con unrun_idpropio, y publicarun.progress {run_id, status, phase, detail}+board.patched /plan/pasos/<i>/conteoa través deLunaStreamHubyappendEvent(eventos conturn_id = run_id;run.progressya está en el CHECK de la migración 185).- Exclusión: un paso corriendo bloquea
beginTurnconconflict(mismo camino que un turno en curso) salvo mensajes de control (“para”, “sigue”). El turno del usuario que dispara el paso cierra concierreparcial: “voy por las personas de 50 cuentas; te aviso aquí”. - Cliente:
lunaRuntime.ts:362(run.progress, hoy vacía) actualiza la tarjeta de proceso; reconexión porsinceSeqya existe. - Límites: máximo 60 llamadas por paso, 10 minutos por paso, parada por saldo (C5).
C4. La receta campana
requiere: [cuantas, criterio, buzon]. Pasos: empresas (lotes de 25 con variación de criterio, dedupe por dominio) → guardar (decisión) → personas (segundo plano;people_searchpor cuenta) → cotizar (people_quotehasta 50) → revelar (decisión con precio y saldo; si no alcanza, lote que cabe) → secuencia (borrador,sequence_check) → revisar y activar (decisión, se resuelve en el dashboard). Escribe el plan antes del paso 1 con costo estimado. “Sigue con la campaña” reanuda en el primer paso no listo.- No llama herramientas de negocio directamente: abre las recetas de cada paso (
skill_open) y usaplan_write/plan_advance.
C5. Topes de producto
- Borrador de secuencia:
contacts.maxItems5 → 50 enapps/mcp/src/tools/families/secuencias.ts:146y en el DTO del API;max_recipientsse respeta. Spec de secuencias actualizado. company_search: el proveedor no expone cursor reutilizable (empresas.ts:124). No hay paginación posible en este PR. El plan B es el del skill: varias búsquedas con criterio distinto y dedupe por dominio. Se documenta enlimites.md.
C6. Decisiones que cargan lo que la persona editó
onResolve(outcome, payload)enslots.ts/cards/*;POST :id/decisions/:decisionId/resolveaceptapayload(filas elegidas, texto editado, colección elegida) yluna-decisions.service.tslo usa enguardar_en_crm,dejar_nota,cambios_masivos.- Los cuatro kinds en
UNAVAILABLE_KINDS(accion_externa,fusionar_conocimiento,sugerir_edicion,investigar) salen del esquemaDecisionDraftque ve el modelo: no más botones muertos. Sus tarjetas quedan en el código para cuando se habiliten.
Paquete D — Tarjetas de resultado (días 10-12)
D1. Proyector y evento
data-artifact.contract.ts:datasetRef.kindadmite'luna_run'con{run_id, turn_id}.luna-board.ts:cardFromToolResult(name, parsed, harvest): DataArtifactSpec | nullpara las seis tarjetas (Lista →DataTableView; Conteo →MetricGridView; Pipeline →BarChartView+ métricas; Cuenta →EvidenceListView+ métricas; Secuencia →DataTableView; Contexto →EvidenceListView), validado porvalidateDataArtifactSpec. MismosRefque el gate.- Evento
card.shown {spec, payload, footnote}: migración que amplía el CHECK deluna_session_events.kind(migración 185) yluna-boundary.spec.ts. Cliente: partedata-luna-result-card→DataArtifactRenderer+ barra de acciones (por fila: las herramientas del skill; en lote: la decisión decierra_con). Dentro de un plan activo, la tarjeta se incrusta en el paso.
D2. Tarjeta Análisis
- Solo para
como-va-mi-pipelineen este PR. El modelo emite unDataArtifactSpecconjson_schemaestricto (strict:trueencendido enmastra-model.adapter.ts:297para este esquema únicamente, porque el contrato de tarjeta sí cumple el dialecto estricto). Pasa por el mismo validador; si falla, tabla canónica.
D3. Manual
- Regla “tarjeta y una frase”: cuando el turno produjo
card.shown, la narración es una o dos frases que la leen; no repite cifras ni filas. Medidornarracion_corta_con_tarjeta.
Paquete E — Evals como condición de merge (transversal, cierra el día 12-14)
E1. Briefs y disparo
briefs.json: los 20 actuales revisados por precedencia nueva + 20 nuevos (2 por skill), conexpect.skill,expect.must_deliver, y los casos de conducta:must_ask(3),saldo-insuficiente-lote-parcial,limite-con-salidapor cada tope de producto,campana-50-personas(conseedde saldo y buzón).trigger-sets/<skill>.json: 10 sí / 10 no por skill, escritos como habla la gente. Se ejecutan enluna-skill-router.spec.ts(router) y en unbench --trigger(modelo con índice, sin router) para medir la autoselección.
E2. Medidores nuevos en run-bench.ts
utilidad: juez modelo (el fallback caro,gpt-5.6-luna, con razonamiento medio) con rúbrica de tres puntos: respondió lo pedido / dio salidas concretas / no inventó. Se guarda la justificación.skill_trigger,siguiente_ofrecido,pregunta_correcta,costo_declarado,limite_con_salida,narracion_corta_con_tarjeta.- Brazo
sin skill(el que ya existe enluna-manual.ts:690) como baseline en cada corrida.
E3. Condición de merge
- Sobre los 40 briefs:
utilidadmedia ≥ 2.4/3 y ≥ +0.8 contra el brazo sin skill;intent≥ 16/20 en los originales;unresolved_refs = 0;skill_trigger≥ 90 % en router y ≥ 80 % en autoselección; p50 ≤ 30 s; costo medio ≤ US$0.05 por turno. - Si no se cumple, no se mergea; se itera sobre las recetas con los transcripts (el bucle de la guía).
Orden de commits dentro del PR
feat(luna): razonamiento por turno y bench de control(A1, A7)feat(luna): el workspace en el prompt y los tres saldos(A2)feat(luna): intents por utilidad, argumentos persistidos, payload limpio(A3, A4, A5)feat(luna): el validador permite la pregunta legítima(A6)feat(luna): loader, índice y router de skills(B1-B5)feat(luna): diez recetas y la referencia de límites(B6, B7)feat(luna): el Plan y la tarjeta de proceso(C1, C2)feat(luna): pasos en segundo plano y la receta campana(C3, C4)feat(luna): topes de producto y decisiones con payload(C5, C6)feat(luna): tarjetas de resultado sobre DataArtifactRenderer(D1-D3)test(luna): briefs, disparo, juez de utilidad y condición de merge(E1-E3)
LUNA_V2 envuelve B en adelante; A entra sin bandera porque son correcciones.
Verificación antes de pedir revisión
apps/api:vitest run src/luna src/luna-decisions src/cognitive,tsc --noEmit,luna-boundary.spec.ts(nunca cobra sola),luna-manual.spec.ts(presupuesto y secciones).apps/dashboard:vitest run src/luna,tsc --noEmit, y una captura real en staging de: conteo → muéstramelos → guardar; cómo va mi pipeline; campaña de 50 hasta la decisión de revelar (sin cobrar).apps/mcp: specs desecuenciasysurface-parity(el catálogo público no cambia de tamaño:skill_open,skill_reference,plan_*son de sesión, no del MCP).- Migración de
card.shownaplicada en staging conlist_migrations. - Bench completo (E3) adjunto al PR como
bench-7.md.
7. Lo que este PR no incluye, y por qué
- Paginación real de
company_search. El proveedor no da cursor. Queda el plan B por lotes. investigar,accion_externa,fusionar_conocimiento,sugerir_edicion. Se ocultan al modelo; habilitarlas es trabajo de sus propios servicios.- Modo automático con tope de inferencia sobre el Plan. El Plan lo prepara (costo estimado, saldos); el corte automático por tope entra cuando el modo automático exista en planes de pago.
- Agents API de OpenAI para “Investigar”. Evaluación aparte.
- Streaming de narración. Con la tarjeta ya en pantalla, la frase corta llega en menos de dos segundos; el streaming deja de ser urgente.
8. Riesgos que pueden mover la fecha
- El razonamiento encendido cambia el comportamiento del gate. Más texto, más cifras, más defectos
cifra. Mitigación: A7 primero; la regla de cifras derivadas (pendiente desde bench 6) puede tener que entrar aquí. - Concurrencia entre pasos en segundo plano y turnos. El bloqueo por sesión en
beginTurnes simple; “para” y “sigue” como mensajes de control necesitan una ruta que no pase por el modelo. - Presupuesto del manual. Dos cuerpos de 2 500 más el índice dejan 300 de holgura. Si un skill crece, se recorta el cuerpo y se pasa a
references/, no se sube el techo. - El juez de utilidad no es determinista. Se corre tres veces por brief y se toma la mediana; la rúbrica pide justificación citable.
9. Lo que necesito de ti para arrancar
- Tu sí a este alcance y a la bandera
LUNA_V2. - Los tres casos de prueba de la campaña con datos reales de tu workspace de staging (criterio, buzón conectado, saldo) para el brief
campana-50-personas. - Que revises los 20 mensajes de disparo por skill cuando estén (B3): son la definición de “cuándo” de cada receta, y eso es criterio de producto, no de código.
