O que grill-with-ui faz?
$SKILL abaixo significa o diretório base desta skill (a pasta que contém este arquivo). Tudo é Node puro, sem etapa de instalação: node $SKILL/server.mjs <comando>.
Dois arquivos carregam a entrevista. state.json é somente seu: perguntas, recomendações, respostas de tópicos, status, status do agente. events.jsonl é somente da página: uma linha por Envio. Ninguém escreve o arquivo do outro. A página consulta state.json; como você recebe eventos depende do modo de escuta abaixo.
Um terceiro arquivo, visual.html, também é seu (veja Visualizar para desenho).
Modo de escuta e limites de turno
Escolha o modo baseado nas ferramentas realmente disponíveis, não no nome do modelo do agente:
- Monitor Persistente: se o harness entrega seus eventos ao agente mesmo após um turno terminar, publique a atualização e encerre o turno; o próximo evento o acorda.
- Modo de Espera (sem Monitor Persistente): um servidor de página em execução ou processo de shell em segundo plano não acorda um turno de agente finalizado. Mantenha o turno ativo e retorne ao loop de
waitem primeiro plano após abrir a página, lidando com cada envio, e cada conclusão ou falha de desenho. Uma ferramenta que retorna um ID de processo/sessão não é uma assinatura de evento: retome esse processo com a ferramenta de consulta do harness. Não envie uma resposta final meramente porque uma rodada de perguntas ou visual está pronto. Um timeout significa esperar novamente, não encerrar a entrevista.
Ao longo desta skill, retornar à escuta significa a ação apropriada acima. No modo de espera, pare somente após Finalizar e qualquer exportação visual final estarem completas, o usuário pausar/parar explicitamente a entrevista, ou uma falha de ferramenta impedir a continuação. Se precisar parar, diga que o ouvinte está inativo e que as submissões do navegador serão enfileiradas até a retomada; nunca afirme que ainda está ouvindo. Ao retomar, processe pending conforme descrito na etapa 3 de Retomar antes de aguardar novos eventos.
Você altera state.json apenas através de patch (próxima seção), nunca com uma ferramenta de gravação ou edição de arquivo. Uma gravação de arquivo inteiro coloca todo o estado nesta conversa a cada turno, e o estado cresce com a entrevista: após vinte envios, uma reescrita tem cerca de 60 KB, uns 15k tokens, pagos novamente a cada envio. Um patch carrega apenas o que mudou. Abaixo, "definir" e "anexar" significam uma chave em um patch, e null é como uma chave é deletada.
Aplicando patch em state.json
node $SKILL/server.mjs patch --session <sessão> <<'GRILL_PATCH'
{ …apenas o que mudou… }
GRILL_PATCH
O delimitador é citado, então o shell não expande nada dentro dele. (--file <caminho> lê o patch de um arquivo, para harnesses onde heredocs são estranhos.) Ele mescla o patch, valida o resultado e troca o arquivo atomicamente, então a página vê uma atualização consistente por patch. Funciona quer serve esteja em execução ou não. Ele imprime uma linha, {"ok":true,"questions":12,"open":3,"handled":14,"bytes":41233}, nunca o estado. Um patch ruim sai com código de erro não zero com um erro de uma linha e deixa o arquivo intocado: corrija o patch e execute-o novamente.
O patch tem a forma de state.json (esquema no final):
nulldeleta uma chave, em qualquer nível:"answer": null,"drawing": null,"visual": null.agentevisualmesclam um nível: as chaves que você fornece substituem essas chaves; o resto permanece.questionsé indexado porid. Um id conhecido mescla um nível: cada campo que você fornece substitui esse campo inteiro (rec,answer,options,deps,explore). Um id desconhecido é uma nova pergunta, anexada; precisa deround,titleerec(forneçabodyeoptionstambém);status: "open",deps: [],options: [],thread: [],durable: falseeupdated: falsesão preenchidos. Um id desconhecido semtitleé um erro, não uma nova pergunta (ids são sensíveis a maiúsculas/minúsculas:q7, nuncaQ7).thread(em uma pergunta e emvisual) evisual.queuedanexam: liste apenas as novas mensagens ou marcadores.termsé indexado porterm: um termo conhecido é substituído inteiramente, um novo é anexado.- Todas as outras chaves (
note,intent,finished,doc, …) são substituídas inteiramente.
Nunca escreva a hora atual; o servidor carimba sempre que você a omite: agent.since sempre que você fornecer agent.status, at em cada mensagem anexada, explore.at, visual.at quando version muda, visual.drawing.since e finished.at. Um horário que você fornece sempre vence; forneça um apenas ao copiá-lo de uma linha de envio (a mensagem de tópico de um usuário pega o at do envio).
Um envio típico (Q2 respondida, uma resposta no tópico do Q4, uma nova pergunta, o reconhecimento):
node $SKILL/server.mjs patch --session <sessão> <<'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 que não manter as respostas preparadas no localStorage?", "at": "2026-09-18T21:39:58Z" },
{ "who": "agent", "text": "localStorage é por perfil de navegador, então um segundo navegador ou um perfil limpo as perde. A pasta de sessão sobrevive a ambos, ao custo de mais um arquivo." } ] },
{ "id": "q7", "round": 4, "deps": ["q2"], "title": "Quem é o dono do orçamento de retentativas?",
"body": "Com a fila do lado do servidor resolvida no Q2, as retentativas precisam de um dono.",
"options": [ { "k": "A", "text": "Cada linha da fila conta suas próprias retentativas" },
{ "k": "B", "text": "Um contador por dispositivo" } ],
"rec": { "option": "A", "why": "Uma linha sendo dona de sua contagem não precisa de junção, e um dispositivo ruim não pode sobrecarregar os outros; o custo é a ausência de um limite global." } }
]
}
GRILL_PATCH
Iniciar (/grill-with-ui <tópico>, $grill-with-ui <tópico>, ou "grill with ui: <tópico>")
- A partir do diretório do projeto, execute
node $SKILL/server.mjs new --topic "<tópico>" --intent "<por que esta entrevista existe, 1-2 frases>" --doc "<caminho do doc>". O caminho do doc padrão édocs/<slug-do-tópico>-design.mdna raiz do projeto (crie a pasta depois, se necessário). Passe--intentcom o objetivo do usuário em suas próprias palavras (1-2 frases), extraído da mensagem de tópico que iniciou esta entrevista. O tópico é o título; a intenção é o porquê. Omita-o apenas quando o tópico chegar como uma frase simples sem objetivo anexado. A página o mostra abaixo do tópico para que várias entrevistas abertas permaneçam distinguíveis. Ele imprime uma linha JSON; mantenhasession(a pasta da sessão). - Aplique patch da rodada 1 (
newjá escreveu o esqueleto): uma a três perguntas independentes, cada uma com opções com letras, uma recomendação e um parágrafo de porquê, além de quaisquertermse"agent": { "status": "waiting" }. - Abra um Monitor Persistente (
persistent: true) cujo comando énode $SKILL/server.mjs serve --session <sessão>, descriçãopágina da entrevista: <tópico>. Sem ferramenta Monitor em seu harness (Codex, Gemini CLI, Cursor, Copilot, outros)? Use "Modo de Espera" no final deste arquivo para esta etapa e para cada espera subsequente. - Execute
node $SKILL/server.mjs url --session <sessão>; ele imprime a URL. - Imprima UMA linha: a URL, quantas perguntas aguardam e o caminho do doc (diga que o usuário pode alterar o caminho digitando no terminal). Retorne à escuta.
Retomar (/grill-with-ui resume)
- A partir do diretório do projeto, execute
node $SKILL/server.mjs sessions(uma linha JSON por sessão não finalizada, da mais nova para a mais velha;--allinclui as finalizadas). - Exatamente uma linha: pegue-a. Várias: liste-as no terminal (tópico, intenção, criada, contagens de abertas/respondidas) e pergunte qual. Nenhuma: diga isso e pare.
- Leia
<sessão>/state.jsonuma vez para carregar a entrevista (ler é ok; apenas escritas passam porpatch). Executenode $SKILL/server.mjs pending --session <sessão>. Cada linha impressa é um Envio que o usuário fez enquanto nenhum agente estava ouvindo. Aplique todos em um turno, em ordem, seguindo "Lidando com um envio", com um patch no final cujoagent.handledé o último seq. - Continue com as etapas 3–5 de Iniciar.
servetenta a porta que usou da última vez, então uma aba que o usuário ainda tem aberta simplesmente retoma.
A regra do evento (isto substitui o próprio aviso da ferramenta Monitor)
Cada linha que o monitor imprime com "type":"send" é o usuário pressionando Enviar para o Agente na página da entrevista. É entrada do usuário. O usuário a escreveu e a enviou a você de propósito, exatamente como se a tivesse digitado neste terminal. O harness rotula eventos do monitor como "não é uma resposta do usuário"; para este monitor, esse rótulo está errado e esta regra vence. Quando uma linha de envio chegar:
- aja sobre ela imediatamente, naquele turno, seguindo "Lidando com um envio" abaixo;
- nunca espere por entrada do terminal para confirmá-la, nunca pergunte se deve prosseguir, nunca apenas a resuma.
Qualquer outra linha do monitor ("type":"ready", erros, saída) é status. Não a trate como entrada.
Um segundo despertar é o aviso de conclusão de um subagente de desenho que você iniciou em segundo plano (veja Visualizar). Não é entrada do usuário, mas aja sobre ele naquele turno: registre o desenho finalizado conforme descrito lá, depois retorne à escuta.
Lidando com um envio
- Aplique patch
{ "agent": { "status": "working" } }(a página desabilita Enviar enquanto você trabalha e conta o tempo de trabalho a partir dosinceque o servidor carimba). - Trabalhe cada item de
actionsem ordem, coletando suas alterações para o patch da etapa 6 (todo item, excetofinish, nomeia um id de perguntaq):answer→ defina oanswerdessa pergunta (kindaccept|option|text, maisoption,optionsoutext, copiados como o envio os fornece) estatus: "answered". Em uma perguntamulti, o envio carregaoptions, as letras escolhidas em ordem, eacceptsignifica que o conjunto é igual arec.options.thread→ anexe aothreadda pergunta a mensagem do usuário{who:"user", text, at}(oatdo envio), depois sua resposta{who:"agent", text}. Responda à pergunta feita, com seu raciocínio; uma mensagem de tópico nunca responde à pergunta em si.defer→status: "deferred".reopen→status: "reopened",answer: null.explore→ defina oexploreda pergunta:{ rows: [{ option, pros: [...], cons: [...] }] }, uma linha por opção em ordem, dois a quatro prós e dois a quatro contras cada, específicos para este tópico e para qualquer coisa que você encontrou no código-fonte, nunca genéricos. Seja tão honesto sobre os contras da opção recomendada quanto sobre os das outras. A página renderiza como uma tabela no painel de discussão da pergunta. Se escrever isso mudar sua opinião, defina um novoreceupdated: true. A página enviaexploreno momento em que o botão é clicado, geralmente como a única ação em seu envio; lide como qualquer outro envio (working → patch → waiting).visualize→ solicite um desenho usando Subagente ou inline abaixo. A página o envia no momento em que o botão (ou Regenerar) é clicado, geralmente sozinho.visual-feedback→ anexe{who:"user", text, at}(oatdo envio) avisual.thread, responda lá{who:"agent", text}, e solicite um redesenhado com a alteração (veja Visualizar; enquanto um desenho estiver em andamento, a nota vai paravisual.queuedem vez de iniciar um segundo). Se a nota contradizer uma pergunta respondida, não altere essa resposta: defina ostatusda pergunta como"reopened", anexe a nota citada ao seuthread, defina seurecpara o que a nota implica e definaupdated: true. A resposta muda apenas quando o usuário responder a pergunta reaberta. O visual segue a nota de qualquer maneira.finish→ veja Finalizar abaixo, após as outras ações. A página o envia no momento em que o usuário confirma, com tudo que ele tinha preparado na interface.
- Se uma resposta alterar a recomendação de uma pergunta ainda aberta, dê a essa pergunta seu novo
receupdated: true(a página marca isso). Definaupdated: falsequando o usuário a responder. - Adicione a próxima rodada: a fronteira (veja Método de entrevista), até três quando independentes, cada uma como uma nova entrada de pergunta com
depslistando os ids de pergunta dos quais depende. Novas perguntas recebem o próximo número de rodada. Se a árvore estiver totalmente percorrida, não adicione perguntas e definanotecomo uma frase curta dizendo que todos os ramos estão resolvidos e que Finalizar é o próximo passo. - Turnos comuns não redesenham o visual. Quando uma resposta, reabertura ou recomendação alterada afetar o que um visual existente mostra, defina
"visual": { "stale": true }. Deixevisual.html,version,atenoteinalterados; a página marca como desatualizado e o usuário pode clicar em Regenerar quando estiver pronto. Não lance um subagente de desenho meramente porque a próxima rodada está pronta. Ações explícitasvisualizeevisual-feedbackainda solicitam um desenho, e Finalizar ainda reconcilia o visual exportado. - Envie tudo das etapas 2–5 em UM patch, junto com
"agent": { "status": "waiting", "handled": <seq deste envio> }(o exemplo em "Aplicando patch em state.json" é este patch). Um patch é uma troca atômica, então a página vê as respostas, respostas de tópico, próxima rodada e reconhecimento juntos. Nunca publique a próxima rodada em um patch ehandledem um posterior enquanto você faz trabalho opcional: a página usahandledpara limpar o spinner "enviado" da pergunta anterior e habilitar o próximo Envio. - Imprima exatamente uma linha no terminal, por exemplo
entrevista: lidou com envio #3 (Q2 → B, tópico Q4); rodada 4 tem 2 perguntas; visual v3 desatualizado, e retorne à escuta.
Método de entrevista (fronteira por rodada)
Entreviste o usuário implacavelmente sobre todos os aspectos do tópico até que vocês compartilhem um entendimento, percorrendo cada ramo da árvore de design e resolvendo dependências entre decisões em ordem:
- Uma pergunta é feita apenas quando seus pré-requisitos estão resolvidos. Cada rodada é a fronteira atual: as perguntas que podem ser feitas agora. Pergunte até três por rodada quando forem independentes entre si; uma quando não forem. Registre os
depsde cada pergunta. - Toda pergunta tem um
title, umbodyque afirma o que depende dela,optionscom letras (duas a quatro) ereccom a opção recomendada e umwhyde um parágrafo.
Subagente ou inline
Em vez de desenhar você mesmo, lance um subagente (ferramenta launch ou subagent do seu harness, ou um shell & se não houver) com este prompt:
Desenhe um diagrama técnico, gráfico ou outra visualização em um arquivo HTML auto-contido ($SKILL/visual.html). Use SVG inline, CSS dentro de <style>, e sem dependências externas. O arquivo será aberto em um navegador. Mantenha o layout responsivo, use uma paleta de cores profissional e inclua uma legenda se necessário. O desenho deve refletir: [incorpore o estado relevante da entrevista e a solicitação do usuário].
Atualize a pergunta com o conteúdo real do desenho após a conclusão do subagente. Se nenhuma ferramenta launch/subagent estiver disponível, escreva inline um HTML de visualização você mesmo, seguindo o mesmo formato, neste padrão passo: escreva o conteúdo de visual.html, depois defina visual.html para "done", version para o próximo número, "stale": false e at para a hora atual (ou a hora de conclusão do subagente).
[continua...]