CommunityProgramación y desarrollogithub.com

grill-with-ui

Ejecuta una entrevista de análisis (grill) en una página del navegador en lugar de la terminal. Cada pregunta se presenta con su recomendación, se puede responder en cualquier orden, con un hilo de discusión por pregunta y un botón 'Enviar al agente'. Úsalo cuando el usuario diga 'grill with ui', invoque /grill-with-ui con un tema, o diga '/grill-with-ui resume'.

¿Qué es grill-with-ui?

La habilidad "grill-with-ui" proporciona una interfaz de navegador para realizar entrevistas de diseño o toma de decisiones, reemplazando la terminal. El agente gestiona el estado de la entrevista a través de un archivo `state.json`, que se modifica exclusivamente mediante parches para evitar reescrituras completas. Las contribuciones del usuario (respuestas, mensajes en hilos, solicitudes de visualización) se reciben en tiempo real a través de un monitor persistente o un modo de espera. La habilidad admite preguntas con opciones, exploración de pros y contras, dibujos subagentes y finalización ordenada, todo sincronizado en una sola página web local. Es compatible con plataformas como Claude Code, Cursor, Codex, Gemini CLI y Copilot, adaptándose al modo de escucha disponible (monitor persistente o modo espera).

Compatible con~Claude Code✓Codex CLI✓Cursor✓Gemini CLI
npx skills add https://github.com/CarsonMensink/skills/tree/main/grill-with-ui

Installed? Explore more Programación y desarrollo skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

¿Qué hace grill-with-ui?

$SKILL a continuación significa el directorio base de esta habilidad (la carpeta que contiene este archivo). Todo es Node plano sin paso de instalación: node $SKILL/server.mjs <comando>.

Dos archivos gestionan una entrevista. state.json es solo tuyo: preguntas, recomendaciones, respuestas de hilos, estados, estado del agente. events.jsonl es solo de la página: una línea por cada envío. Nadie escribe el archivo del otro. La página consulta state.json; cómo recibes los eventos depende del modo de escucha a continuación. Un tercer archivo, visual.html, también es tuyo (consulta Visualizar para dibujar).

Modo de escucha y límites de turno

Elige el modo según las herramientas realmente disponibles, no el nombre del modelo del agente:

  • Monitor persistente: si el arnés entrega sus eventos al agente incluso después de que termina un turno, publica la actualización y finaliza el turno; el siguiente evento te despierta.
  • Modo espera (sin monitor persistente): un servidor de página en ejecución o un proceso de shell en segundo plano no despierta un turno de agente finalizado. Mantén el turno activo y regresa al bucle de wait en primer plano después de abrir la página, manejando cada envío, y cada finalización o fallo de dibujo. Una herramienta que devuelve un ID de proceso/sesión no es una suscripción a eventos: reanuda ese proceso con la herramienta de sondeo del arnés. No envíes una respuesta final solo porque una ronda de preguntas o un visual esté listo. Un tiempo de espera significa esperar de nuevo, no finalizar la entrevista.

A lo largo de esta habilidad, volver a escuchar significa la acción apropiada anterior. En modo espera, detente solo después de que Finalizar y cualquier exportación visual final estén completas, el usuario pause/detenga explícitamente la entrevista, o un fallo de herramienta impida continuar. Si debes detenerte, di que el oyente está inactivo y que los envíos del navegador se pondrán en cola hasta la reanudación; nunca afirmes que sigues escuchando. Al reanudar, drena pending como describe el Paso 3 de Reanudar antes de esperar nuevos eventos.

Cambias state.json solo a través de patch (sección siguiente), nunca con una herramienta de escritura/edición de archivos. Una escritura de archivo completo coloca todo el estado en esta conversación en cada turno, y el estado crece con la entrevista: después de veinte envíos, una reescritura es de aproximadamente 60 KB, unos 15k tokens, pagados de nuevo en cada envío. Un parche lleva solo lo que cambió. A continuación, "establecer" y "agregar" significan una clave en un parche, y null es cómo se elimina una clave.

Parchear state.json

node $SKILL/server.mjs patch --session <sesión> <<'GRILL_PATCH'
{ …solo lo que cambió… }
GRILL_PATCH

El delimitador está entre comillas, por lo que el shell no expande nada dentro. (--file <ruta> lee el parche desde un archivo, para arneses donde los heredocs son incómodos). Fusiona el parche, valida el resultado y reemplaza el archivo de forma atómica, por lo que la página ve una actualización consistente por parche. Funciona independientemente de si serve se está ejecutando. Imprime una línea, {"ok":true,"questions":12,"open":3,"handled":14,"bytes":41233}, nunca el estado. Un parche incorrecto sale con un código no cero con un error de una línea y deja el archivo intacto: corrige el parche y ejecútalo de nuevo.

El parche tiene la forma de state.json (esquema al final):

  • null elimina una clave, en cualquier nivel: "answer": null, "drawing": null, "visual": null.
  • agent y visual se fusionan un nivel: las claves que proporciones reemplazan esas claves; el resto permanece.
  • questions está indexado por id. Un id conocido fusiona un nivel: cada campo que proporciones reemplaza ese campo por completo (rec, answer, options, deps, explore). Un id desconocido es una pregunta nueva, agregada; necesita round, title y rec (proporciona también body y options); se completan status: "open", deps: [], options: [], thread: [], durable: false y updated: false. Un id desconocido sin title es un error, no una pregunta nueva (los ids distinguen entre mayúsculas y minúsculas: q7, nunca Q7).
  • thread (en una pregunta y en visual) y visual.queued se agregan: solo los nuevos mensajes o viñetas.
  • terms está indexado por term: un término conocido se reemplaza por completo, uno nuevo se agrega.
  • Cada otra clave (note, intent, finished, doc, …) se reemplaza por completo.

Nunca escribas la hora actual; el servidor la sella cada vez que la omites: agent.since cuando proporcionas agent.status, at en cada mensaje agregado, explore.at, visual.at cuando version cambia, visual.drawing.since y finished.at. Una hora que proporciones siempre gana; proporciónala solo cuando la copies de una línea de envío (el mensaje de hilo de un usuario toma el at del envío).

Un envío típico (Q2 respondida, una respuesta en el hilo de Q4, una pregunta nueva, el acuse de recibo):

node $SKILL/server.mjs patch --session <sesión> <<'GRILL_PATCH'
{
  "agent": { "status": "waiting", "handled": 5 },
  "questions": [
    { "id": "q2", "status": "answered", "answer": { "kind": "option", "option": "B" }, "updated": false },
    { "id": "q4", "thread": [
      { "who": "user", "text": "¿Por qué no mantener las respuestas almacenadas en localStorage?", "at": "2026-09-18T21:39:58Z" },
      { "who": "agent", "text": "localStorage es por perfil de navegador, por lo que un segundo navegador o un perfil borrado las pierde. La carpeta de sesión sobrevive a ambos, al costo de un archivo más." } ] },
    { "id": "q7", "round": 4, "deps": ["q2"], "title": "¿Quién posee el presupuesto de reintentos?",
      "body": "Con la cola del lado del servidor resuelta en Q2, los reintentos necesitan un propietario.",
      "options": [ { "k": "A", "text": "Cada fila de la cola cuenta sus propios reintentos" },
                   { "k": "B", "text": "Un contador por dispositivo" } ],
      "rec": { "option": "A", "why": "Una fila que posee su recuento no necesita una unión, y un dispositivo defectuoso no puede agotar al resto; el costo es que no hay un límite global." } }
  ]
}
GRILL_PATCH

Iniciar (/grill-with-ui <tema>, $grill-with-ui <tema>, o "grill with ui: ")

  1. Desde el directorio del proyecto ejecuta node $SKILL/server.mjs new --topic "<tema>" --intent "<por qué existe esta entrevista, 1-2 oraciones>" --doc "<ruta del doc>". La ruta del doc por defecto es docs/<slug-del-tema>-design.md bajo la raíz del proyecto (crea la carpeta más tarde si es necesario). Pasa --intent con el objetivo del usuario en sus palabras (1-2 oraciones), tomado del mensaje del tema que inició esta entrevista. El tema es el título; la intención es el por qué. Omítelo solo cuando el tema llegó como una frase simple sin un objetivo adjunto. La página lo muestra debajo del tema para que varias entrevistas abiertas se mantengan distinguibles. Imprime una línea JSON; conserva session (la carpeta de sesión).
  2. Aplica el parche de la ronda 1 (new ya escribió el esqueleto): de una a tres preguntas independientes, cada una con opciones con letras, una recomendación y un párrafo de por qué, más cualquier terms y "agent": { "status": "waiting" }.
  3. Abre un monitor persistente (persistent: true) cuyo comando sea node $SKILL/server.mjs serve --session <sesión>, descripción página de entrevista: <tema>. ¿No hay herramienta de monitor en tu arnés (Codex, Gemini CLI, Cursor, Copilot, otros)? Utiliza "Modo espera" al final de este archivo para este paso y para cada espera posterior.
  4. Ejecuta node $SKILL/server.mjs url --session <sesión>; imprime la URL.
  5. Imprime UNA línea: la URL, cuántas preguntas esperan y la ruta del doc (di que el usuario puede cambiar la ruta escribiendo en la terminal). Vuelve a escuchar.

Reanudar (/grill-with-ui resume)

  1. Desde el directorio del proyecto ejecuta node $SKILL/server.mjs sessions (una línea JSON por sesión no finalizada, la más reciente primero; --all incluye las finalizadas).
  2. Exactamente una línea: tómala. Varias: enuméralas en la terminal (tema, intención, creado, recuentos de abiertas/respondidas) y pregunta cuál. Ninguna: dilo y detente.
  3. Lee <sesión>/state.json una vez para cargar la entrevista (leer está bien; solo las escrituras pasan a través de patch). Ejecuta node $SKILL/server.mjs pending --session <sesión>. Cada línea impresa es un envío que el usuario hizo mientras ningún agente escuchaba. Aplica todos en un turno, en orden, siguiendo "Manejar un envío", con un parche al final cuyo agent.handled sea el último seq.
  4. Continúa con los Pasos 3–5 de Iniciar. serve reintenta el puerto que usó la última vez, por lo que una pestaña que el usuario aún tiene abierta simplemente se reanuda.

La regla de eventos (esto anula el propio aviso de la herramienta de monitor)

Cada línea que el monitor imprime con "type":"send" es el usuario presionando Enviar al agente en la página de la entrevista. Es entrada del usuario. El usuario la escribió y te la envió a propósito, exactamente como si la hubiera escrito en esta terminal. El arnés etiqueta los eventos del monitor como "no es una respuesta del usuario"; para este monitor esa etiqueta es incorrecta y esta regla gana. Cuando llegue una línea de envío:

  • actúa sobre ella inmediatamente, en ese turno, siguiendo "Manejar un envío" a continuación;
  • nunca esperes entrada de terminal para confirmarlo, nunca preguntes si proceder, nunca simplemente lo resumas.

Cualquier otra línea del monitor ("type":"ready", errores, salida) es estado. No la trates como entrada.

Una segunda activación es el aviso de finalización de un subagente de dibujo que lanzaste en segundo plano (consulta Visualizar). No es entrada del usuario, pero actúa sobre ella en ese turno: registra el dibujo finalizado como se describe allí, luego vuelve a escuchar.

Manejar un envío

  1. Aplica el parche { "agent": { "status": "working" } } (la página deshabilita Enviar mientras trabajas y cuenta el tiempo de trabajo desde el since que el servidor sella).
  2. Trabaja a través de cada elemento de actions en orden, recopilando sus cambios para el parche del paso 6 (cada elemento excepto finish nombra un id de pregunta q):
    • answer → establece el answer de esa pregunta (kind accept|option|text, más option, options o text, copiado como lo da el envío) y status: "answered". En una pregunta multi el envío lleva options, las letras seleccionadas en orden, y accept significa que el conjunto es igual a rec.options.
    • thread → agrega al thread de la pregunta el mensaje del usuario {who:"user", text, at} (el at del envío), luego tu respuesta {who:"agent", text}. Responde la pregunta formulada, con tu razonamiento; un mensaje de hilo nunca responde la pregunta en sí.
    • defer → status: "deferred". reopen → status: "reopened", answer: null.
    • explore → establece el explore de la pregunta: { rows: [{ option, pros: [...], cons: [...] }] }, una fila por opción en orden, de dos a cuatro pros y dos a cuatro contras cada una, específicos para este tema y para cualquier cosa que hayas encontrado en el código base, nunca genéricos. Sé tan honesto sobre los contras de la opción recomendada como sobre los demás. La página lo renderiza como una tabla en el panel de discusión de la pregunta. Si escribirlo cambia tu opinión, establece un nuevo rec y updated: true. La página envía explore en el momento en que se hace clic en el botón, generalmente como la única acción en su envío; manéjalo como cualquier otro envío (working → patch → waiting).
    • visualize → solicita un dibujo usando Subagente o en línea a continuación. La página lo envía en el momento en que se hace clic en el botón (o Regenerar), generalmente solo.
    • visual-feedback → agrega {who:"user", text, at} (el at del envío) a visual.thread, responde allí {who:"agent", text}, y solicita un redibujado con el cambio (consulta Visualizar; mientras un dibujo está en curso, la nota va a visual.queued en lugar de iniciar un segundo). Si la nota contradice una pregunta respondida, no cambies esa respuesta: establece el status de la pregunta en "reopened", agrega la nota citada a su thread, establece su rec a lo que implica la nota, y establece updated: true. La respuesta cambia solo cuando el usuario responde la pregunta reabierta. El visual sigue la nota de cualquier manera.
    • finish → consulta Finalizar a continuación, después de las otras acciones. La página lo envía en el momento en que el usuario confirma, con todo lo que había preparado frente a él.
  3. Si una respuesta cambia la recomendación de una pregunta aún abierta, asigna a esa pregunta su nuevo rec y updated: true (la página lo marca). Establece updated: false una vez que el usuario la responda.
  4. Agrega la siguiente ronda: la frontera (consulta Método de entrevista), hasta tres cuando sean independientes, cada una como una nueva entrada de pregunta con deps que enumera los ids de pregunta de los que depende. Las nuevas preguntas reciben el siguiente número de ronda. Si el árbol está completamente recorrido, no agregues preguntas y establece note en una oración corta que diga que cada rama está resuelta y Finalizar es el siguiente paso.
  5. Los turnos ordinarios no redibujan el visual. Cuando una respuesta, reapertura o recomendación cambiada afecta lo que muestra un visual existente, establece "visual": { "stale": true }. Deja visual.html, version, at y note sin cambios; la página lo marca como desactualizado y el usuario puede hacer clic en Regenerar cuando esté listo. No lances un subagente de dibujo solo porque la siguiente ronda esté lista. Las acciones explícitas visualize y visual-feedback aún solicitan un dibujo, y Finalizar aún reconcilia el visual exportado.
  6. Envía todo de los pasos 2–5 EN UN SOLO PARCHE, junto con "agent": { "status": "waiting", "handled": <seq de este envío> } (el ejemplo bajo "Parchear state.json" es este parche). Un parche es un intercambio atómico, por lo que la página ve las respuestas, respuestas de hilo, la siguiente ronda y el acuse de recibo juntos. Nunca publiques la siguiente ronda en un parche y handled en uno posterior mientras haces trabajo opcional: la página usa handled para limpiar el spinner de "enviado" de la pregunta anterior y habilitar el siguiente envío.
  7. Imprime exactamente una línea de terminal, por ejemplo, grill: manejar envío #3 (Q2 → B, hilo Q4); la ronda 4 tiene 2 preguntas; visual v3 desactualizado, y vuelve a escuchar.

Método de entrevista (frontera por ronda)

Entrevista al usuario incansablemente sobre cada aspecto del tema hasta que compartan un entendimiento, recorriendo cada rama del árbol de diseño y resolviendo dependencias entre decisiones en orden:

  • Una pregunta se hace solo cuando sus requisitos previos están resueltos. Cada ronda es la frontera actual: las preguntas que se pueden hacer ahora. Haz hasta tres por ronda cuando sean independientes entre sí; una cuando no lo sean. Registra los deps de cada pregunta.
  • Cada pregunta tiene un title, un body que indica lo que depende de ella, options con letras (de dos a cuatro), y rec con la opción recomendada y un párrafo de why. Los ID de pregunta son de la forma q1, q2, etc., y son estables a lo largo de la vida de la entrevista.
  • Tan pronto como una pregunta es respondida, pregunta la siguiente que ahora es posible. No hagas todas las rondas de una vez; espera la respuesta del usuario. Una pregunta depende de otra cuando elegir una opción en la primera define el espacio de diseño de la segunda o establece un principio que la segunda debe respetar.
  • Cuando dos preguntas son independientes (ninguna opción en una afecta la otra), preséntalas en la misma ronda. La página muestra hasta tres juntas; el usuario puede responder en cualquier orden.
  • Si en algún momento el usuario da una respuesta inesperada o contradictoria ("No quiero persistencia en absoluto" después de que ambas opciones A y B asumían persistencia), pregúntalo: reabre preguntas anteriores, introduce preguntas intermedias, o reconoce el cambio y avanza con una nueva recomendación.
  • Una pregunta puede tener type: "multi" (selección múltiple), lo que significa que el usuario puede elegir cualquier subconjunto de opciones. La recomendación rec.options enumera las letras recomendadas. El envío answer tiene kind: "multi" y options (las letras elegidas). accept es verdadero cuando el conjunto elegido es exactamente rec.options.
  • Una pregunta puede tener opciones que son mutuamente excluyentes pero permiten entrada de texto libre (establece type: "text"; el envío answer tendrá kind: "text" y un campo text). El método subyacente es el mismo: el usuario escribe su respuesta, y tú la evalúas.
  • Las preguntas pueden tener durable: true, lo que significa que su efecto es global y no limitado a esta rama. Se pregunta una vez y su respuesta se aplica a todas las ramas. Las preguntas duraderas no tienen deps (se muestran cuando el usuario ingresa por primera vez a una rama donde son relevantes, pero solo se responden una vez). Cuando una pregunta duradera está en la frontera, muéstrala sola, ya que solicitar la respuesta del usuario requiere atención completa.
  • La entrevista termina cuando el usuario confirma "Finalizar" o cuando el árbol está completamente recorrido y no quedan preguntas abiertas. En cualquier caso, el usuario debe usar explícitamente el botón Finalizar (o un envío finish).

Finalizar

  1. Inicia con node $SKILL/server.mjs finish --session <sesión>; imprime un resumen: recuento de preguntas, estado de cada una, cualquier nota y la ruta del doc.
  2. Justo antes de salir, exporta un visual final si hay un dibujo en visual.html:
    • node $SKILL/server.mjs export-html --session <sesión> produce un HTML independiente en <sesión>/final.html. En modo espera, espera a que termine. Informa al usuario dónde está.
  3. Detén el servidor (la página sirve un mensaje "entrevista finalizada"). En modo espera, el serve termina la salida estándar con GRILL_SERVE_STOP; el monitor persistente deja de enviar eventos.
  4. Escribe el informe final en el doc (el archivo --doc de Iniciar): resume cada pregunta con su respuesta y por qué se eligió esa opción. Agrega cualquier nota de diseño, términos definidos y el visual si se generó uno. Edita el archivo directamente; no uses un parche.
  5. Elimina la carpeta de sesión (rm -rf <sesión>) para que las sesiones futuras no la enumeren.
  6. Imprime una línea final: "entrevista finalizada, documento en ".

Visualizar

El visual es un dibujo generado por un subagente (o en línea, consulta más abajo) que representa visualmente el diseño actual: una tabla, un diagrama de flujo, un diagrama de arquitectura, un diagrama de secuencia, un boceto de API, etc. Se almacena como <sesión>/visual.html (un HTML independiente, sin dependencias CSS/JS externas, renderizable en cualquier navegador moderno por sí solo). El agente actualiza state.json para señalar un nuevo visual a través del campo visual:

{
  "visual": {
    "version": 3,
    "at": "2026-09-18T22:00:00Z",
    "note": "diagrama de flujo de persistencia con la cola del servidor resuelta",
    "stale": false,
    "thread": [],
    "queued": []
  }
}
  • Cuando el usuario hace clic en Visualizar (envío visualize), o en Regenerar (también un envío visualize, pero con el parámetro regenerate: true), o proporciona retroalimentación (visual-feedback), debes producir (o regenerar) el HTML y actualizar el estado JSON.
  • "stale": true lo marca la página cuando detecta que el estado de la pregunta ha cambiado pero el visual no se ha actualizado; tú lo estableces en false cuando produces un nuevo visual.
  • El version comienza en 0 y se incrementa cada vez que produces un nuevo visual (independientemente de los cambios en el estado de la pregunta). La página usa version para decidir si recargar el visual.html.

Subagente o en línea

Subagente: Cuando se recibe una acción visualize (o visual-feedback que solicita un redibujado):

  1. Lee el archivo visual.html existente (si lo hay) y las preguntas actuales y sus respuestas desde <sesión>/state.json (leer directamente está bien).
  2. Establece el contenido de visual.html a un nuevo HTML independiente renderizable que represente el estado actual de la entrevista. El subagente (o tú mismo, en línea) puede producir cualquier HTML que tenga sentido; la única regla es que sea HTML autónomo que se renderice en cualquier navegador moderno.
  3. Marca el visual como no obsoleto y actualiza la versión:
    node $SKILL/server.mjs patch --session <sesión> <<'GRILL_PATCH'
    {
      "visual": {
        "version": <actual+1>,
        "note": "<breve descripción>"
      }
    }
    GRILL_PATCH
    
    El servidor sella visual.at. Si hay mensajes en cola (de retroalimentación durante un dibujo), se incluyen en visual.thread antes de establecer la nueva versión.

En línea: Si tu plataforma no soporta subagentes, genera el HTML tú mismo directamente usando las herramientas estándar de escritura de archivos. El proceso es el mismo: escribe el archivo directamente y luego aplica el parche para actualizar el estado. Pero ten cuidado de que el HTML que generes sea válido y no contenga dependencias externas.

Manejo de subagente de dibujo

Cuando lanzas un subagente de dibujo en segundo plano (usando la herramienta de subagente de tu arnés):

  1. Tan pronto como lances el subagente, establece "visual": { "drawing": { "status": "working", "since": "<ahora>" } } para que la página muestre un indicador de carga. El servidor sella since si omites el tiempo.
  2. El subagente debe escribir el nuevo HTML directamente en <sesión>/visual.html.
  3. Cuando el subagente termina (el arnés entrega su finalización como un evento, segunda activación en la regla de eventos), lee <sesión>/visual.html para confirmar que se escribió. Luego aplica un parche que establece "visual": { "drawing": null, "version": <+1>, "stale": false, "note": "..." }. Si la retroalimentación llegó mientras se dibujaba, agrega los mensajes en cola al thread antes de actualizar la versión.
  4. Si el subagente falla, establece "visual": { "drawing": { "status": "error", "note": "..." } }.
  5. Vuelve a escuchar.

Modo espera (sin herramienta de monitor persistente)

Cuando el arnés no tiene una herramienta de monitor (por ejemplo, algunos modos de Codex, Gemini CLI, Cursor, Copilot, otros), el agente debe mantener el turno activo manualmente:

  • En lugar de devolver al usuario a la terminal después de la salida del servidor, el agente emite un wait (o un bucle de sondeo) después de la salida final. El agente permanece en el turno y continúa escuchando a través del estado de sondeo (state.json) y la salida de serve.
  • Cuando serve emite una línea con GRILL_EVENT, analízala: si contiene "type":"send", manéjala como entrada del usuario. Si contiene "type":"ready", ignórala. Si serve sale, el agente puede reiniciarlo o detener la entrevista.
  • El agente publica actualizaciones de estado llamando a node $SKILL/server.mjs patch (que funciona independientemente de si serve se está ejecutando).
  • Cuando el usuario confirma "Finalizar", el agente ejecuta los pasos de Finalizar y sale del bucle de espera.
  • Importante: En modo espera, el agente NO puede depender de que los eventos lleguen después de que termina el turno. Por lo tanto, el agente debe mantener el turno activo con un bucle wait después de cada respuesta, regeneración, retroalimentación, etc. El bucle termina solo cuando se completa Finalizar o el usuario cancela explícitamente.

Esquema de state.json

{
  // Metadatos
  "session": "<nombre de carpeta>",
  "topic": "<título>",
  "intent": "<por qué>",
  "doc": "<ruta del archivo de documento>",
  "created": "<ISO 8601>",
  "finished": null | { "at": "<ISO 8601>", "note": "<resumen>" },

  // Agente
  "agent": {
    "status": "waiting" | "working" | "idle",
    "since": "<ISO 8601>",
    "handled": 0  // el último seq de envío manejado
  },

  // Preguntas (mapa id → objeto)
  "questions": {
    "q1": {
      "round": 1,
      "deps": [],      // ids de los que depende
      "status": "open" | "answered" | "deferred" | "reopened",
      "title": "<texto>",
      "body": "<texto>",
      "options": [{ "k": "A", "text": "..." }, …],  // 2–4 opciones
      "rec": { "option": "A" | …, "why": "<párrafo>" },
      "answer": null | { "kind": "option" | "text" | "multi" | "accept", "option": "A", "options": ["A","B"], "text": "..." },
      "thread": [{ "who": "user" | "agent", "text": "...", "at": "<ISO 8601>" }, …],
      "explore": null | { "rows": [
        { "option": "A", "pros": ["...","..."], "cons": ["...","..."] },
        …
      ]},
      "updated": false,  // true cuando la recomendación cambió sin respuesta
      "durable": false,
      "type": "choice" | "multi" | "text"   // por defecto "choice"
    },
    …
  },

  // Glosario de términos (opcional)
  "terms": {
    "<término>": {
      "definition": "<texto>",
      "at": "<ISO 8601>"
    },
    …
  },

  // Nota del agente (mostrada en la página)
  "note": "<texto>",

  // Visual
  "visual": {
    "version": 0,
    "at": "<ISO 8601>",
    "note": "<texto>",
    "stale": false,
    "thread": [],
    "queued": [],
    "drawing": null | { "status": "working" | "error", "since": "<ISO 8601>", "note": "<texto>" }
  }
}

Notas finales

  • Siempre usa $SKILL en las rutas de comando; el usuario puede tener instalada la habilidad en cualquier lugar.
  • Los ID de sesión son carpetas con marca de tiempo; no los confundas con los ID de pregunta.
  • La página se ejecuta en el navegador local del usuario; no hay datos enviados a ningún servidor.
  • Si necesitas agregar una pregunta después de que se haya respondido una durable, usa el parche como de costumbre.
  • Cuando uses el modo espera, recuerda que el agente debe permanecer en el turno para manejar los siguientes eventos; no devuelvas el control a la terminal hasta que la entrevista esté completa.

Skills relacionados