product-update-logger
Informe esta skill sobre o que seu produto lançou. Ela escreve uma entrada de changelog refinada em docs/changelog.md (um registro vivo, com a entrada mais recente primeiro) e, simultaneamente, produz um pacote de conteúdo: thread de tweets, post no LinkedIn, trecho de e-mail e uma frase de efeito.
Fontes de entrada: texto livre de sua mensagem, commits do git lidos automaticamente do repositório local ou PRs do GitHub se você fornecer um repositório. Qualquer combinação funciona.
Arquivos de Referência
Leia estes arquivos antes de cada execução:
cat references/changelog-format.md
cat references/content-rules.md
cat references/noise-filter.md
Etapa 1: Verificação de Configuração
echo "GITHUB_TOKEN: ${GITHUB_TOKEN:-não definido -- busca de PRs do GitHub desabilitada}"
echo "Git: $(git rev-parse --is-inside-work-tree 2>/dev/null && echo 'repositório detectado' || echo 'não é um repositório git')"
echo "Changelog: $(ls docs/changelog.md 2>/dev/null && echo 'existe' || echo 'será criado')"
Observe se o git está disponível e se um changelog já existe. Isso determina o formato do rótulo de versão.
Etapa 2: Analisar Entrada
Colete da conversa:
items-- descrição em texto livre do que foi lançado (separado por pipe se múltiplos). Opcional se o git estiver disponível.since-- quantos dias para trás olhar. Padrão: 7. O usuário pode dizer "últimas 2 semanas" (14) ou "desde o último lançamento".repo-- "owner/repo" do GitHub para busca de PRs. Opcional.version_label-- rótulo personalizado como "v2.1.0" ou "A Atualização de Velocidade". Opcional; o padrão é baseado na data.
Se o usuário não disse nada sobre itens E não há repositório git: Pergunte "O que você lançou? Liste os recursos, correções ou melhorias -- um por linha."
Se o git estiver disponível e o usuário não disse nada específico: Prossiga com a leitura automática do git na Etapa 3. Mostre ao usuário o que foi encontrado e confirme antes de transformar.
Escreva a entrada analisada:
python3 << 'PYEOF'
import json, os, re
inp = {
"items": "", # PREENCHER: texto livre separado por pipe, ou "" se nenhum
"since": 7, # PREENCHER: número inteiro de dias
"repo": "", # PREENCHER: "owner/repo" ou ""
"version_label": "" # PREENCHER: "" significa automático (baseado na data), ou string personalizada
}
with open("/tmp/pul-input.json", "w") as f:
json.dump(inp, f, indent=2)
print(f"Desde: {inp['since']} dias")
print(f"Itens de texto livre: {inp['items'] or 'nenhum (usará git/GitHub)'}")
print(f"Repositório GitHub: {inp['repo'] or 'nenhum'}")
print(f"Rótulo de versão: {inp['version_label'] or 'automático (baseado na data)'}")
PYEOF
Etapa 3: Executar o Script de Coleta
ls scripts/gather.py 2>/dev/null && echo "script encontrado" || echo "ERRO: scripts/gather.py não encontrado"
GITHUB_TOKEN="${GITHUB_TOKEN:-}" python3 scripts/gather.py \
--since "$(python3 -c "import json; print(json.load(open('/tmp/pul-input.json'))['since'])")" \
--repo "$(python3 -c "import json; print(json.load(open('/tmp/pul-input.json'))['repo'])")" \
--items "$(python3 -c "import json; print(json.load(open('/tmp/pul-input.json'))['items'])")" \
--output /tmp/pul-raw.json
Verifique a saída:
python3 -c "
import json
with open('/tmp/pul-raw.json') as f:
d = json.load(f)
print(f'Itens encontrados: {d[\"total_items\"]}')
print(f'Ruído filtrado: {d[\"noise_filtered\"]}')
print(f'Git disponível: {d[\"git_available\"]}')
print(f'GitHub disponível: {d[\"github_available\"]}')
print(f'Origens: git={sum(1 for i in d[\"items\"] if i[\"source\"]==\"git_commit\")}, '
f'prs={sum(1 for i in d[\"items\"] if i[\"source\"]==\"github_pr\")}, '
f'texto={sum(1 for i in d[\"items\"] if i[\"source\"]==\"free_text\")}')
print()
print('Itens:')
for item in d['items']:
print(f' [{item[\"source\"]}] {item[\"subject\"]}')
"
Se total_items == 0: Pare. Informe ao usuário: "Nenhum item lançado encontrado. Descreva o que você lançou, aponte-me para um repositório git com commits recentes ou adicione um repositório GitHub com repo: owner/repo e um GITHUB_TOKEN."
Mostre a lista de itens ao usuário e pergunte: "Estes são os itens que encontrei. Algo para adicionar ou remover antes de eu escrever o changelog?"
Aguarde confirmação ou edições. Se o usuário disser "parece bom", "prossiga", ou não fizer alterações, continue. Se o usuário adicionar ou remover itens, atualize /tmp/pul-raw.json de acordo antes da Etapa 4.
Etapa 4: Gerar Entrada do Changelog
Imprima itens para contexto:
python3 -c "
import json
with open('/tmp/pul-raw.json') as f:
d = json.load(f)
print(json.dumps(d['items'], indent=2))
print()
print(f'Formato do changelog existente: {d[\"existing_changelog\"][\"format\"]}')
print(f'Último rótulo: {d[\"existing_changelog\"][\"last_label\"]}')
print(f'Hoje: {d[\"date\"]}')
"
Instruções para IA: Transforme cada item bruto de linguagem técnica para linguagem de benefícios voltada ao usuário. Siga references/changelog-format.md para regras de transformação e exemplos.
Regras:
- NÃO invente resultados ou métricas. "40% mais rápido" deve vir dos dados de origem. Se nenhum número estiver no commit ou PR, não adicione um.
- Use pretérito perfeito: "Adicionado", "Corrigido", "Melhorado" -- não "Adiciona", "Corrige"
- Atribua exatamente uma categoria a cada item: Novo, Melhorado, Corrigido ou Nos bastidores
- Nos bastidores: Inclua apenas se for relevante para desenvolvedores (mudanças de API, mudanças que quebram compatibilidade). Omita seções vazias.
- Omita qualquer coisa que se refira a: alterações de teste, alterações de CI, commits apenas de documentação
Determine o rótulo da versão:
- Se o usuário especificou um: use exatamente
- Se
existing_changelog.format == "semver": incremente com base nas alterações (patch para apenas correções, minor para qualquer novo recurso) - Padrão:
Semana de [Mês Dia, Ano]usando a data de hoje
Escreva a entrada em /tmp/pul-entry.json:
{
"label": "Semana de 23 de Abril de 2026",
"date": "2026-04-23",
"new": [
{"title": "Modo escuro", "description": "Ative em Configurações > Aparência. Funciona em todas as visualizações."}
],
"improved": [
{"title": "Tempo de resposta da API", "description": "40% mais rápido em média. O painel agora carrega em menos de 1 segundo."}
],
"fixed": [
{"title": "Exportação CSV", "description": "As exportações não perdem mais a última linha."}
],
"under_the_hood": []
}
Verifique a entrada:
python3 -c "
import json
with open('/tmp/pul-entry.json') as f:
e = json.load(f)
print(f'Rótulo: {e[\"label\"]}')
total = 0
for cat in ['new', 'improved', 'fixed', 'under_the_hood']:
items = e.get(cat, [])
if items:
print(f'{cat.replace(\"_\", \" \").title()} ({len(items)}):')
for item in items:
print(f' - {item[\"title\"]}: {item[\"description\"]}')
total += len(items)
print(f'Total: {total} itens')
"
Etapa 5: Gerar Pacote de Conteúdo
Usando a entrada do changelog da Etapa 4, gere todas as quatro peças de conteúdo. Siga references/content-rules.md estritamente.
Frase de efeito (máx. 20 palavras): Uma frase cobrindo os 1-2 maiores itens. Linguagem simples, sem jargão.
Thread de tweets (3-5 tweets):
- Tweet 1: Gancho -- "Lançamos [N] coisas esta semana." ou comece com o maior recurso
- Tweets 2-N: Um item por tweet, máx. 1-2 frases
- Último tweet: "Changelog: [link]" ou "Mais na próxima semana." (opcional)
- Cada tweet estritamente com menos de 280 caracteres
- Sem hashtags. Sem travessões. Voz ativa.
Post no LinkedIn:
- Sem markdown (asteriscos são renderizados como literais no LinkedIn)
- Sem hashtags
- Voz de fundador: "Lançamos", não "Estamos animados em anunciar"
- Parágrafos curtos (1-2 frases cada), linhas em branco entre eles
- Termine com uma pergunta ou observação, não com uma chamada para ação
- Total de 150-400 palavras
Trecho de e-mail:
- Assunto: "O que lançamos esta semana: [maior item] + [mais 1]"
- Corpo: 50-100 palavras. "Aqui está o que lançamos esta semana:" então marcadores.
Escreva em /tmp/pul-content.json:
{
"one_liner": "Modo escuro, API mais rápida e um bug de exportação corrigido.",
"tweet_thread": [
"Lançamos 3 coisas esta semana.",
"O modo escuro está no ar. Ative em Configurações > Aparência. Funciona em todos os lugares.",
"O tempo de resposta da API agora é 40% mais rápido. O painel carrega em menos de um segundo.",
"Corrigido: as exportações CSV estavam perdendo a última linha. Isso acabou.",
"Changelog: [link]"
],
"linkedin_post": "Lançamos 3 atualizações esta semana.\n\nO modo escuro está no ar. Ative em Configurações, em Aparência. Funciona em todas as visualizações.\n\nO tempo de resposta da API está 40% mais rápido em média. O painel agora carrega em menos de um segundo para a maioria dos usuários.\n\nTambém corrigimos um bug onde as exportações CSV estavam silenciosamente perdendo a última linha. Se você enfrentou isso e parou de exportar, vale a pena tentar novamente.\n\nQual recurso você está esperando?",
"email_snippet": {
"subject": "O que lançamos esta semana: modo escuro + API mais rápida",
"body": "Aqui está o que lançamos esta semana:\n\n- Modo escuro: ative em Configurações > Aparência\n- Tempo de resposta da API: 40% mais rápido, painel carrega em menos de 1 segundo\n- Corrigido: exportações CSV não perdem mais a última linha\n\nChangelog completo abaixo."
}
}
Etapa 6: Auto-QA
python3 -c "
import json, re
with open('/tmp/pul-raw.json') as f:
raw = json.load(f)
with open('/tmp/pul-entry.json') as f:
entry = json.load(f)
with open('/tmp/pul-content.json') as f:
content = json.load(f)
full_text = json.dumps(entry) + json.dumps(content)
fails = 0
# Verificação 1: Sem travessões
if chr(8212) in full_text:
print('FALHA: travessão encontrado -- substitua por hífen')
fails += 1
else:
print('PASSOU: sem travessões')
# Verificação 2: Palavras proibidas
banned = ['poderoso', 'robusto', 'perfeito', 'inovador', 'revolucionário',
'otimizar', 'alavancar', 'transformar', 'revolucionar', 'animados em anunciar',
'contentes em anunciar', 'estamos entusiasmados', 'de ponta', 'melhor da classe',
'classe mundial', 'desbloquear', 'encantador']
found = [w for w in banned if w.lower() in full_text.lower()]
if found:
print(f'FALHA: palavras proibidas encontradas: {found}')
fails += 1
else:
print('PASSOU: sem palavras proibidas')
# Verificação 3: Comprimento do tweet
thread = content.get('tweet_thread', [])
long_tweets = [(i+1, len(t)) for i, t in enumerate(thread) if len(t) > 280]
if long_tweets:
print(f'FALHA: tweets com mais de 280 caracteres: {long_tweets}')
fails += 1
else:
print(f'PASSOU: todos os {len(thread)} tweets com menos de 280 caracteres')
# Verificação 4: LinkedIn sem hashtags
li = content.get('linkedin_post', '')
if re.search(r'#[A-Za-z]', li):
print('FALHA: hashtags encontradas no post do LinkedIn')
fails += 1
else:
print('PASSOU: sem hashtags no LinkedIn')
# Verificação 5: Sem markdown no LinkedIn
if '**' in li or '__' in li:
print('FALHA: formatação markdown no LinkedIn (renderiza como asteriscos literais)')
fails += 1
else:
print('PASSOU: sem markdown no LinkedIn')
# Verificação 6: Contagem de palavras da frase de efeito
one_liner = content.get('one_liner', '')
word_count = len(one_liner.split())
if word_count > 20:
print(f'FALHA: frase de efeito tem {word_count} palavras (máx. 20)')
fails += 1
else:
print(f'PASSOU: frase de efeito tem {word_count} palavras')
# Verificação 7: Contagem de itens
entry_items = (len(entry.get('new', [])) + len(entry.get('improved', [])) +
len(entry.get('fixed', [])) + len(entry.get('under_the_hood', [])))
raw_total = raw['total_items']
print(f'INFO: {entry_items} itens no changelog de {raw_total} itens brutos')
print()
print(f'Resultado: {"PASSOU" if fails == 0 else f"FALHOU ({fails} problemas)"}')
"
Se alguma verificação falhar: Corrija o problema no arquivo temporário relevante antes de prosseguir para a Etapa 7. Reexecute a verificação após corrigir.
Etapa 7: Anexar ao Changelog + Salvar Conteúdo
python3 << 'PYEOF'
import json, os, re
with open('/tmp/pul-entry.json') as f:
entry = json.load(f)
with open('/tmp/pul-content.json') as f:
content = json.load(f)
# Construir a nova seção do changelog
lines = [f"## {entry['label']}", ""]
CAT_HEADERS = {
"new": "### Novo",
"improved": "### Melhorado",
"fixed": "### Corrigido",
"under_the_hood": "### Nos bastidores",
}
for cat, header in CAT_HEADERS.items():
items = entry.get(cat, [])
if items:
lines.append(header)
for item in items:
lines.append(f"- **{item['title']}** -- {item['description']}")
lines.append("")
lines.append("---")
lines.append("")
new_section = "\n".join(lines)
# Inserir no início de docs/changelog.md
os.makedirs("docs", exist_ok=True)
changelog_path = "docs/changelog.md"
if os.path.exists(changelog_path):
existing = open(changelog_path).read()
# Inserir após o cabeçalho de nível superior (se houver) ou no topo
if existing.startswith("# "):
end_of_heading = existing.index("\n") + 1
updated = existing[:end_of_heading] + "\n" + new_section + existing[end_of_heading:]
else:
updated = new_section + existing
else:
updated = "# Changelog\n\n" + new_section
with open(changelog_path, "w") as f:
f.write(updated)
print(f"Changelog atualizado: {changelog_path}")
# Salvar pacote de conteúdo
date = entry['date']
content_dir = "docs/product-updates"
os.makedirs(content_dir, exist_ok=True)
content_path = f"{content_dir}/{date}-content.md"
content_lines = [
f"# Pacote de Conteúdo: {entry['label']}",
"",
"## Frase de Efeito",
content.get('one_liner', ''),
"",
"## Thread de Tweets",
"",
]
thread = content.get('tweet_thread', [])
for i, tweet in enumerate(thread, 1):
content_lines.append(f"[{i}/{len(thread)}] {tweet}")
content_lines.append("")
content_lines += [
"## Post no LinkedIn",
"",
content.get('linkedin_post', ''),
"",
"## Trecho de E-mail",
"",
f"Assunto: {content.get('email_snippet', {}).get('subject', '')}",
"",
content.get('email_snippet', {}).get('body', ''),
"",
]
with open(content_path, "w") as f:
f.write("\n".join(content_lines))
print(f"Pacote de conteúdo: {content_path}")
PYEOF
Etapa 8: Limpeza e Apresentação
rm -f /tmp/pul-input.json /tmp/pul-raw.json /tmp/pul-entry.json /tmp/pul-content.json
echo "Concluído."
Apresente ao usuário nesta ordem:
1. Entrada do changelog (markdown formatado, não JSON bruto):
## Semana de 23 de Abril de 2026
### Novo
- **Modo escuro** -- Ative em Configurações > Aparência. Funciona em todas as visualizações.
### Melhorado
- **Tempo de resposta da API** -- 40% mais rápido em média. O painel agora carrega em menos de 1 segundo.
### Corrigido
- **Exportação CSV** -- As exportações não perdem mais a última linha.
---
2. Frase de efeito (texto simples)
3. Thread de tweets (marcadores numerados, cada tweet entre 280 caracteres)
4. Post no LinkedIn (texto simples, parágrafos curtos)
5. Trecho de e-mail (assunto + corpo)
Sempre pergunte ao usuário se há mais alguma coisa que ele gostaria de ajustar antes de finalizar.