Clique nas respostas; o prompt se monta ao lado

Quem vai pedir um plano a um agente não quer escrever XML na mão nem lembrar de todas as seções que costumam faltar. Aqui as perguntas são o formulário, o prompt é a saída, e o arquivo continua sendo um só — sem build, sem servidor, sem dependência nova.

Bootstrap 5.3.8 highlight.js 11.11.1 sem npm contrato v1

Prompt de planejamento

Responda à esquerda; o prompt à direita é remontado a cada clique e fica pronto para colar no agente. As respostas ficam salvas neste navegador — voltar ao arquivo devolve a última combinação. Restaurar padrões volta ao estado de fábrica.

prompt padrão · 38 linhas
<task type="planning">

  <objective>
    Trocar o parser de configuração de YAML para TOML em toda a aplicação
  </objective>

  <context>
    Monorepo Node 20 com quatro pacotes; a configuração é lida em onze lugares e dois deles rodam em CI.
  </context>

  <depth>
    <level name="padrao">Etapas na ordem de execução, com os arquivos que cada uma toca e o risco que ela carrega.</level>
    <budget max-lines="120">Detalhe o que muda uma decisão e resuma o resto.</budget>
  </depth>

  <deliverable>
    <format name="markdown-checklist">Markdown com uma checklist em ordem de execução. Cada item começa com um verbo e cabe em uma linha.</format>
  </deliverable>

  <plan-sections>
    <section id="riscos">O que pode dar errado, o sinal que denuncia cada caso e o que fazer quando ele aparecer.</section>
    <section id="criterios-de-aceite">Como saber que terminou: um critério verificável por etapa, sem adjetivo.</section>
    <section id="plano-de-testes">O que testar em cada etapa e o comando exato que roda o teste.</section>
  </plan-sections>

  <constraints>
    <rule>Não introduza dependência nova. Se achar inevitável, pare e justifique em vez de assumir.</rule>
    <rule>Nenhuma mudança que quebre a API pública ou o formato de dados já gravado em disco.</rule>
  </constraints>

  <protocol>
    <step n="1">Antes de propor, liste os arquivos que o plano vai tocar e cite de cada um o trecho que sustenta a proposta.</step>
    <step n="2">Não escreva código de produção nesta etapa. O entregável é o plano.</step>
    <step n="3">Onde houver mais de um caminho, mostre as opções e o critério que decide entre elas — não escolha em silêncio.</step>
    <step n="4">Marque explicitamente o que você NÃO verificou e o que depende de informação que só eu tenho.</step>
  </protocol>

</task>

Seis perguntas, 38 linhas de prompt. O limite prático é doze perguntas: acima disso ninguém responde, e o construtor vira um formulário de imposto de renda.

Levar o construtor para um documento seu

São três blocos, e eles já estão marcados por comentário no fonte deste arquivo. Copie nesta ordem — a spec e a casca dentro do .tab-pane, o runtime no fim do <body>, depois do runtime do template.

BlocoVai ondePrecisa mexer?
pb:spec dentro do painel da aba sim — é a sua spec: perguntas, fragmentos e o esqueleto do prompt
pb:shell logo abaixo, no mesmo painel quase nada — só o id e o data-pb-spec
pb:runtime último <script> do arquivo não — copie inteiro, como o runtime do template

1. A spec

Um bloco inerte com a declaração inteira. Cada pergunta vira um <fieldset>; o corpo de cada <option> é o pedaço de prompt que ela injeta.

<script type="application/xml" id="pb-spec-plano">
<prompt-builder id="plano" lang="xml" title="Monte o prompt que pede o plano">

  <question id="profundidade" type="radio" label="Profundidade do plano">
    <option value="padrao" label="Padrão" default="true"><![CDATA[<level name="padrao">Etapas na ordem de execução.</level>]]></option>
    <option value="exaustivo" label="Exaustivo"><![CDATA[<level name="exaustivo">Leia o código antes de propor.</level>]]></option>
  </question>

  <template><![CDATA[
    <task type="planning">
      <depth>
        {{profundidade}}
      </depth>
    </task>
  ]]></template>

</prompt-builder>
</script>

2. A casca

O autor escreve só isto; o runtime preenche o [data-pb-form]. O que não pode faltar: data-pb-spec apontando para o id do bloco acima, o <pre data-live> em volta da saída, e o <noscript>.

<div class="row g-4 prompt-builder" id="pb-plano" data-pb-spec="pb-spec-plano">
  <div class="col-lg-5">
    <div data-pb-form><noscript>…aviso…</noscript></div>
  </div>
  <div class="col-lg-7">
    <div class="position-sticky" style="top: 5rem">
      <button type="button" class="btn btn-primary" data-pb-copy>Copiar prompt</button>
      <button type="button" class="btn btn-outline-secondary" data-pb-reset>Restaurar padrões</button>
      <span data-pb-status aria-live="polite"></span>
      <pre data-live><code class="language-xml" data-pb-output>…prompt padrão, escapado…</code></pre>
    </div>
  </div>
</div>

3. As duas linhas do template.html

O runtime "congelado" do template muda em exatamente dois pontos. Nada mais.

a) o laço que guarda a fonte crua passa a pular blocos vivos:

   document.querySelectorAll('pre > code').forEach(function (code) {
+    if (code.closest('[data-live]')) return;   // texto muda em runtime: leia na hora do clique
     sources.set(code, code.textContent.replace(/\n$/, ''));
   });

Sem essa linha o cache é tirado no load e nunca mais atualizado: o botãozinho de hover do bloco copiaria o prompt inicial para sempre, mesmo depois de vinte cliques. O sintoma é cruel porque a tela mostra o texto novo e a área de transferência traz o velho.

b) a função de copiar vira reutilizável:

+  // Exposto para quem estende o documento (ex.: o construtor de prompt) reusar o
+  // mesmo caminho de cópia em vez de duplicar o fallback de file://.
+  window.__explainerCopy = copyText;

O botão grande do construtor usa esse gancho quando ele existe e cai num execCommand('copy') próprio quando não existe — então o bloco pb:runtime também funciona colado num HTML que não veio desta skill.

Antes de entregar

  • Rode check-doc.mjs: ele confere a casca, a spec e cada {{marcador}}.
  • Clique em todas as opções e olhe o prompt: fragmento multi-linha desalinhado aparece na hora.
  • Desmarque uma pergunta inteira de checkbox — o elemento vazio que sobra ainda precisa ser XML válido.
  • Copie e cole num agente de verdade. Prompt que ninguém testou é prompt que não existe.

Quatro elementos, e acabou

Não há condicional, laço nem aninhamento. Fragmento é a única forma de condicionar — se um pedaço do prompt só faz sentido em certo caso, ele é o corpo de uma <option>. Essa limitação é deliberada: um mini-motor de template dentro do documento seria mais código do que o construtor inteiro.

<prompt-builder> — a raiz

AtributoObrig.Valor
idsim [a-z][a-z0-9-]*. Prefixa todo id gerado e é a chave do localStorage
langnão linguagem do bloco de saída para o destaque. Padrão xml
titlenão título exibido acima do formulário

<question> — uma por fieldset

AtributoObrig.Valor
idsim [a-z][a-z0-9-]*, único no construtor. É o nome usado em {{id}}
typesim radio · checkbox · text · textarea
labelsim vira a <legend> do <fieldset>
helpnãouma linha de ajuda sob o rótulo
joinnão só em checkbox: newline (padrão) · blank-line · comma · space
placeholdernãosó em text/textarea
defaultnãosó em text/textarea: o valor inicial

<option> — o fragmento vive no corpo

value e label são obrigatórios; default="true" marca a inicial — exatamente uma em radio, zero ou mais em checkbox. O corpo, sempre em CDATA, é o que entra no prompt. Corpo vazio significa "o fragmento é o próprio value".

<template> — exatamente um

O esqueleto, também em CDATA, com os marcadores {{id-da-pergunta}}. A indentação comum é removida na leitura, então dá para indentar o CDATA dentro da spec sem sujar a saída.

As duas regras de substituição que importam

Indentação preservada. Marcador sozinho na linha aplica a própria indentação a todas as linhas do fragmento. Sem isso, fragmento de duas linhas sai torto:

esqueleto
<depth>
  {{profundidade}}
</depth>
saída
<depth>
  <level name="padrao">Etapas na ordem.</level>
  <budget max-lines="120">Resuma o resto.</budget>
</depth>

Linha vazia some. Marcador sozinho cuja resposta é vazia — nenhum checkbox marcado, campo de texto em branco — leva a linha inteira embora, sem deixar buraco:

se a linha ficasse
<constraints>

</constraints>
o que sai
<constraints>
</constraints>

Um {{x}} sem pergunta x fica literal no texto — e o check-doc.mjs reprova o arquivo. É de propósito: marcador órfão significa pergunta renomeada e esquecida.

Como cada tipo vira fragmento

TipoO que {{id}} devolve
radioo corpo da opção escolhida
checkbox os corpos das marcadas, na ordem da spec (não na ordem do clique), unidos pelo join: newline = \n · blank-line = \n\n · comma = vírgula e espaço · space = um espaço
text / textarea o valor digitado, com as pontas aparadas e sem escape — ver o aviso no topo

Cinco armadilhas do runtime

Ele devolve um documento bem-sucedido contendo um elemento <parsererror>. Um try/catch em volta não pega nada, e o construtor abriria vazio sem uma linha de erro em lugar nenhum.

const doc = new DOMParser().parseFromString(xml, 'application/xml');
const bad = doc.querySelector('parsererror');   // sem namespace: casa em qualquer um
if (bad) return { error: bad.textContent.trim().split('\n')[0] };

O prompt é XML: jogar <task type="planning"> em innerHTML faz o navegador criar um elemento e sumir com a tag da tela. E, como parte do texto vem de campo digitado, seria injeção de HTML de brinde. É textContent, sempre.

Da versão 11.9.0 em diante ele grava data-highlighted="yes" e sai fora na chamada seguinte, escrevendo um aviso no console. O segundo clique deixaria o prompt sem cor nenhuma. A ordem é sempre esta:

code.textContent = prompt;          // textContent, nunca innerHTML
delete code.dataset.highlighted;    // sem isto, o segundo clique sai sem cor
if (window.hljs) hljs.highlightElement(code);

Vale para a remontagem. Na primeira, não: o hljs.highlightAll() do runtime do documento ainda está agendado para o DOMContentLoaded, e destacar antes dele faria o aviso aparecer em toda carga. Por isso a montagem inicial escreve o texto e deixa o destaque para o highlightAll — a menos que o documento já esteja pronto, quando não há mais ninguém para destacar.

Dentro de <script type="application/xml"> o conteúdo é texto cru até a primeira sequência de fechamento — inclusive dentro de CDATA, porque quem corta é o parser de HTML, que não sabe o que é CDATA. Um prompt que ensina a escrever <script> precisa quebrar a sequência com <\/script>. Em compensação, o < comum não precisa de escape nenhum ali dentro: é essa a razão de a spec ser um bloco de script e não um <template> de HTML.

Em navegação privativa e em parte dos file:// o simples acesso lança SecurityError — não só a escrita, a leitura também. Sem try/catch nos dois lados, o handler morre antes de montar o prompt e o construtor fica mudo. A persistência é conveniência; nada pode depender dela.

Acessibilidade: onde o aria-live vai

No [data-pb-status], com uma frase curta do tipo "prompt atualizado · 38 linhas"nunca no bloco de código. Um aria-live no <pre> faria o leitor de tela reler as 38 linhas a cada clique, o que transforma o construtor em algo inutilizável exatamente para quem mais depende de um resumo. Cada pergunta é um <fieldset> com <legend>, e todo controle tem <label for> casado — nos campos de texto o rótulo é visually-hidden, porque a <legend> já diz a mesma coisa na tela.