O corpo da tarefa
O corpo do SKILL.md são as instruções que o agente segue. Numa execução, ele
roda dentro do wrapper /kadu:agent-run-task, que já preparou tudo:
helpers de instrumentação, data de referência, registro das chamadas HTTP e a
finalização. Não refaça nada disso.
Os helpers
Os helpers ficam em /tmp/agent_state.sh. Cada bloco de shell do corpo começa
com source /tmp/agent_state.sh.
| Helper | Alimenta | Para quê |
|---|---|---|
emit_event '<json>' | aba Histórico | um evento por marco da execução, não em laço apertado |
append_log '<texto>' | aba Logs | linhas de log |
log_http_call '<texto>' | aba Logs | toda chamada a API de sistema externo, com os secrets mascarados |
create_todo '<json>' | aba Pendências | pendência que sobrevive à execução |
mark_body_started | — | sentinela obrigatória (abaixo) |
:::danger Não redefina os helpers Redefinir qualquer um deles no corpo da skill tira o envio do caminho que mascara os secrets. Use os que o wrapper gravou. :::
A sentinela
No fim da execução, o wrapper confere se o corpo marcou que começou. Sem a
marca, ele registra execution.body_skipped e reprova a execução (exit 2).
Então o corpo começa assim:
source /tmp/agent_state.sh
emit_event "$(jq -nc '{type:"step.started",level:"info",step:"1",message:"Buscando os pagamentos de ontem"}')"
mark_body_started
Eventos: type e message são obrigatórios
Todo evento precisa de type e message; level, step e payload são
opcionais. Um evento sem message é recusado (400) e o envio ignora o erro: o
evento some do histórico, sem nada no log. Se um passo sumiu do histórico,
suspeite disso primeiro.
| Chave | Convenção |
|---|---|
type | step.started e step.completed por passo |
step | número do passo como string ("1", "2") |
level | thinking, info, success, warning ou error |
message | o que está acontecendo, em uma frase, no idioma da execução |
payload | dados estruturados do passo (contagens, ids) |
Logs e chamadas HTTP
log_http_call é para chamadas de API de integração (Fintera, Pagar.me,
Linear, Slack), uma entrada por chamada:
source /tmp/agent_state.sh
RESP=$(curl -sS -w '\n%{http_code}' -u "$FINTERA_TOKEN:X" \
"https://financeiro.fintera.com.br/entities/$FINTERA_ENTITY_ID/receivable_accounts?per_page=50")
log_http_call "$(printf 'GET /receivable_accounts?per_page=50\n→ %s' "$(tail -n1 <<<"$RESP")")"
Ferramenta de alto volume (scanner, crawler, carga em lote) faz centenas ou
milhares de requisições: logar cada uma afoga a aba Logs e esconde as chamadas
que importam. Resuma numa linha de append_log
(nuclei: 1.240 requisições → 3 achados) e reserve log_http_call para as
APIs.
Não há usuário na execução
A execução é automática. Toda pergunta é um travamento silencioso: a tarefa fica parada até estourar o limite de 60 minutos. Onde uma skill interativa perguntaria, a tarefa decide e registra:
| Situação | O que fazer |
|---|---|
| Dado ambíguo | escolha a leitura mais provável, diga qual escolheu e crie uma pendência com a dúvida |
| Um item falhou | crie uma pendência com o erro real e siga para os próximos |
| Volume acima do esperado | processe até o teto e crie uma pendência listando o excedente |
| Falta um pré-requisito absoluto | grave o resultado dizendo onde parou e saia com código diferente de zero |
source /tmp/agent_state.sh
create_todo "$(jq -nc --arg doc "$CNPJ" \
'{title:"Pagamento sem conta a receber correspondente",description:"Nenhuma conta em aberto para o CNPJ no valor pago.",payload:{cnpj:$doc,valor:1520.00}}')"
Escreva a skill para os dois modos quando ela também for útil no Claude Code, com uma pessoa do outro lado:
if [ -n "${GITHUB_ACTIONS:-}" ]; then MODE=task; else MODE=local; fi
Entradas
| Variável | O que traz |
|---|---|
$PARAMETERS | os parâmetros do disparo, em JSON, já validados contra o parameters_schema |
$REFERENCE_DATE | a data de referência (AAAA-MM-DD): o "hoje" da execução |
$LOCALE | o idioma de quem disparou; escreva eventos e resultado nele |
$INPUT_FILES | um caminho por linha, só com accepts_input_files: true. Os arquivos já estão no disco. |
$OUTPUT_FILES_DIR | só com generates_output_files: true: tudo o que for salvo aqui é publicado |
| secrets | cada chave de required_secrets chega como variável de ambiente ($FINTERA_TOKEN) |
source /tmp/agent_state.sh
PARAMS="${PARAMETERS:-}"; [ -z "$PARAMS" ] && PARAMS='{}'
REPO_URL=$(jq -r '.repo_url // empty' <<<"$PARAMS")
Não invente valor ausente. Parâmetro essencial vazio é falha honesta, e não motivo para adivinhar.
A finalização: você grava o arquivo, o wrapper envia
No fim, grave ./.execution-result.json com result e summary:
source /tmp/agent_state.sh
RESULT=$(cat <<'MD'
## Resumo
12 pagamentos baixados, 2 pulados (ver pendências).
MD
)
SUMMARY='{"processados":12,"pulados":2}'
jq -n --arg result "$RESULT" --argjson summary "$SUMMARY" \
'{result:$result, summary:$summary}' > ./.execution-result.json
resultnão é status. É o Markdown final que a aba Resultado mostra, no idioma de$LOCALE.{"result":"success"}faz a aba exibir a palavra "success".summaryé um objeto de contadores, e não uma frase.- Quem envia o resultado ao console é o wrapper, a partir desse arquivo. Não chame a finalização no corpo e nunca reenvie os logs no fim: a aba Logs é alimentada aos poucos, e um reenvio a sobrescreve.
Erro fatal no meio: grave mesmo assim o .execution-result.json dizendo
onde parou, e saia com código diferente de zero. O status de falha vem do
código de saída; não tente marcar a falha por conta própria.
Tarefas conversacionais
Com conversational: true, a pessoa pode pedir ajustes depois que a execução
termina (card Conversa no console, ou POST /v1/executions/{id}/continue).
Cada pedido cria uma execução filha, e a rodada filha recebe:
- os mesmos parâmetros, data de referência e ambiente da anterior. O
pedido chega em
$ADDITIONAL_INSTRUCTIONS; - a sessão do agente da anterior: o modelo vê o que leu, rodou e produziu;
- os arquivos da anterior, nos mesmos caminhos:
.execution-files/input/,.execution-files/output/($OUTPUT_FILES_DIR) e.execution-files/state/. O resto do diretório de trabalho é descartado entre as rodadas.
O wrapper e o corpo rodam de novo, do começo: o modelo decide, pelo pedido e pelo histórico, o que refazer. Isso muda como o corpo é escrito:
| Regra | Por quê |
|---|---|
Guarde em .execution-files/state/ o que valha reaproveitar (roteiro, assets, projeto de edição) | É o único diretório de trabalho preservado, além das entradas e saídas. |
Grave as saídas em $OUTPUT_FILES_DIR como sempre, sobrescrevendo o arquivo que muda | Saída restaurada que volta sem mudança não é publicada de novo; a alterada é publicada como saída da nova rodada. |
| Torne idempotente todo efeito em sistema externo | O corpo roda de novo a cada rodada. Sem conferir antes de criar, uma rodada que só ajusta um texto duplica a cobrança, a conta ou o e-mail da primeira. |
Escreva o result de cada rodada como o estado atual completo | A aba Resultado de cada execução mostra só o result dela. |
Não dependa de nada fora de .execution-files/ | Pacotes instalados, /tmp e o resto do diretório de trabalho não sobrevivem. |
Secret no histórico aparece como [secret NOME]: leia de novo do ambiente | Valor de secret nunca entra na sessão salva. O histórico da rodada anterior traz o nome da variável; arquivos de .execution-files/ voltam com o valor atual da secret. |
| Evite intermediários grandes | Sessão, entradas, saídas e estado somam no máximo 1 GiB. Acima disso a execução termina normalmente, mas não pode ser continuada. |
Os dois executores continuam conversas: o claude_code retoma o transcript, o
goose a sessão dele (goose run --resume). A sessão é do executor que a
gravou: se o nível ou o override de executor da tarefa mudar entre as rodadas,
a próxima é recusada (409 EXECUTOR_CHANGED) e a tarefa precisa ser disparada
de novo. No goose, os modelos mais baratos dos níveis 1 e 2 recebem um
histórico que cresce a cada rodada: confira se o resultado se sustenta depois
de algumas rodadas.
Auxiliar que roda dentro de uma tarefa
Uma auxiliar pequena (enviar uma mensagem no Slack) não precisa de instrumentação própria: a tarefa que a chama registra o passo. Mas, quando a tarefa orquestra auxiliares que fazem o grosso do trabalho, auxiliares mudas deixam o histórico e os logs vazios durante quase toda a execução.
A auxiliar chamada dentro de uma execução roda no mesmo processo e enxerga os helpers. Então ela instrumenta os próprios passos:
| Aba | Quem alimenta |
|---|---|
| Histórico | a tarefa abre e fecha cada fase; a auxiliar emite step.started/step.completed dos passos dela, com contagens (o item individual vai para arquivo, não para evento) |
| Logs | a auxiliar registra uma linha por ferramenta ou lote |
| Pendências | a auxiliar cria as dela |
| Resultado | só a tarefa grava o .execution-result.json; a auxiliar devolve arquivo e resumo |
Documente isso numa seção "Instrumentação do Kadu" da auxiliar, para quem a lê saber em que aba procurar.
Modelo de corpo
# Olá, Kadu
Tarefa de exemplo: não acessa nenhum sistema externo e não faz perguntas.
## Passo 1 — início
```bash
source /tmp/agent_state.sh
emit_event "$(jq -nc '{type:"step.started",level:"info",step:"1",message:"Preparando o cumprimento"}')"
mark_body_started
PARAMS="${PARAMETERS:-}"; [ -z "$PARAMS" ] && PARAMS='{}'
NOME=$(jq -r '.nome // "time"' <<<"$PARAMS")
append_log "Parâmetros recebidos: $PARAMS"
```
## Passo 2 — resultado
Escreva um cumprimento curto para `$NOME`, no idioma de `$LOCALE`, citando a
data de referência (`$REFERENCE_DATE`). Grave o resultado:
```bash
source /tmp/agent_state.sh
RESULT="## Olá, $NOME!"
jq -n --arg result "$RESULT" '{result:$result, summary:{cumprimentos:1}}' > ./.execution-result.json
emit_event "$(jq -nc '{type:"step.completed",level:"info",step:"2",message:"Cumprimento gravado"}')"
```