ganbatte-os 0.2.20 → 0.2.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gos/README.md +67 -0
- package/.gos/docs/slack-notifications.md +219 -0
- package/.gos/plans/tasks/T-084-api-diagnostico-negocio.md +405 -0
- package/.gos/plans/tasks/T-085-api-diagnostico-individual.md +291 -0
- package/.gos/plans/tasks/T-086-api-ativacao-manual-batch.md +287 -0
- package/.gos/plans/tasks/T-104-hook-storybook-branch-context.md +218 -0
- package/.gos/plans/tasks/T-110-drift-typescript-pre-commit-hook.md +355 -0
- package/.gos/scripts/tools/text-sanitize.js +236 -0
- package/.gos/skills/registry.json +2 -1
- package/.gos/skills/slack-review/SKILL.md +91 -0
- package/.gos/skills/weekly-update/CHANGELOG.md +84 -0
- package/.gos/tests/hooks/storybook-branch-check.test.sh +42 -0
- package/package.json +1 -1
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// text-sanitize.js — Sanitizador de texto pt-BR determinístico
|
|
4
|
+
// Zero-dep. Aplica subset de .gos/libraries/content/ai-writing-patterns.md + acentos.
|
|
5
|
+
//
|
|
6
|
+
// Uso:
|
|
7
|
+
// node text-sanitize.js --text "texto aqui"
|
|
8
|
+
// node text-sanitize.js --file path.md
|
|
9
|
+
// node text-sanitize.js --text "..." --json
|
|
10
|
+
//
|
|
11
|
+
// Export:
|
|
12
|
+
// const { sanitize } = require('./text-sanitize.js')
|
|
13
|
+
// const { text, changes } = sanitize(input)
|
|
14
|
+
|
|
15
|
+
'use strict'
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Dicionários (word-boundary, case preservado no prefixo)
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
// P07 — vocabulário IA → equivalente natural pt-BR
|
|
22
|
+
const AI_VOCAB = {
|
|
23
|
+
'aprimorar': 'melhorar',
|
|
24
|
+
'aprimorando': 'melhorando',
|
|
25
|
+
'aprimorada': 'melhorada',
|
|
26
|
+
'aprimorado': 'melhorado',
|
|
27
|
+
'fomentar': 'estimular',
|
|
28
|
+
'fomentando': 'estimulando',
|
|
29
|
+
'intrincado': 'complexo',
|
|
30
|
+
'intrincada': 'complexa',
|
|
31
|
+
'adicionalmente': 'além disso',
|
|
32
|
+
'aprofundar': 'detalhar',
|
|
33
|
+
'aprofundando': 'detalhando',
|
|
34
|
+
'crucial': 'importante',
|
|
35
|
+
'cruciais': 'importantes',
|
|
36
|
+
'fundamental': 'essencial',
|
|
37
|
+
'fundamentais': 'essenciais',
|
|
38
|
+
'duradouro': 'permanente',
|
|
39
|
+
'duradoura': 'permanente',
|
|
40
|
+
'vibrante': 'ativo',
|
|
41
|
+
'vibrantes': 'ativos',
|
|
42
|
+
'renomado': 'conhecido',
|
|
43
|
+
'renomada': 'conhecida',
|
|
44
|
+
'deslumbrante': 'impressionante',
|
|
45
|
+
'imperdivel': 'recomendado',
|
|
46
|
+
'imperdível': 'recomendado',
|
|
47
|
+
'testemunho': 'exemplo',
|
|
48
|
+
'tapecaria': 'conjunto',
|
|
49
|
+
'tapeçaria': 'conjunto',
|
|
50
|
+
'sublimar': 'destacar',
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Acentos faltantes pt-BR — apenas adiciona acento, não muda palavra
|
|
54
|
+
const MISSING_ACCENTS = {
|
|
55
|
+
'concluida': 'concluída', 'concluidas': 'concluídas', 'concluido': 'concluído', 'concluidos': 'concluídos',
|
|
56
|
+
'atualizacao': 'atualização', 'atualizacoes': 'atualizações',
|
|
57
|
+
'implementacao': 'implementação', 'implementacoes': 'implementações',
|
|
58
|
+
'validacao': 'validação', 'validacoes': 'validações',
|
|
59
|
+
'notificacao': 'notificação', 'notificacoes': 'notificações',
|
|
60
|
+
'configuracao': 'configuração', 'configuracoes': 'configurações',
|
|
61
|
+
'integracao': 'integração', 'integracoes': 'integrações',
|
|
62
|
+
'migracao': 'migração', 'migracoes': 'migrações',
|
|
63
|
+
'correcao': 'correção', 'correcoes': 'correções',
|
|
64
|
+
'execucao': 'execução',
|
|
65
|
+
'sessao': 'sessão', 'sessoes': 'sessões',
|
|
66
|
+
'descricao': 'descrição', 'descricoes': 'descrições',
|
|
67
|
+
'operacao': 'operação', 'operacoes': 'operações',
|
|
68
|
+
'geracao': 'geração',
|
|
69
|
+
'criacao': 'criação',
|
|
70
|
+
'remocao': 'remoção',
|
|
71
|
+
'revisao': 'revisão',
|
|
72
|
+
'versao': 'versão', 'versoes': 'versões',
|
|
73
|
+
'decisao': 'decisão', 'decisoes': 'decisões',
|
|
74
|
+
'conexao': 'conexão', 'conexoes': 'conexões',
|
|
75
|
+
'expressao': 'expressão', 'expressoes': 'expressões',
|
|
76
|
+
'apos': 'após',
|
|
77
|
+
'pos': 'pós',
|
|
78
|
+
'ate': 'até',
|
|
79
|
+
'tambem': 'também',
|
|
80
|
+
'alem': 'além',
|
|
81
|
+
'porem': 'porém',
|
|
82
|
+
'ja': 'já',
|
|
83
|
+
'nao': 'não',
|
|
84
|
+
'sao': 'são',
|
|
85
|
+
'pre': 'pré',
|
|
86
|
+
'nivel': 'nível', 'niveis': 'níveis',
|
|
87
|
+
'codigo': 'código', 'codigos': 'códigos',
|
|
88
|
+
'usuario': 'usuário', 'usuarios': 'usuários',
|
|
89
|
+
'relatorio': 'relatório', 'relatorios': 'relatórios',
|
|
90
|
+
'diretorio': 'diretório', 'diretorios': 'diretórios',
|
|
91
|
+
'arquivo': 'arquivo', // no-op control
|
|
92
|
+
'servico': 'serviço', 'servicos': 'serviços',
|
|
93
|
+
'pratica': 'prática', 'praticas': 'práticas',
|
|
94
|
+
'basico': 'básico', 'basicos': 'básicos',
|
|
95
|
+
'automatico': 'automático', 'automaticos': 'automáticos',
|
|
96
|
+
'automatica': 'automática', 'automaticas': 'automáticas',
|
|
97
|
+
'dinamico': 'dinâmico', 'dinamica': 'dinâmica',
|
|
98
|
+
'estatico': 'estático', 'estatica': 'estática',
|
|
99
|
+
'historico': 'histórico', 'historica': 'histórica',
|
|
100
|
+
'pagina': 'página', 'paginas': 'páginas',
|
|
101
|
+
'duvida': 'dúvida', 'duvidas': 'dúvidas',
|
|
102
|
+
'area': 'área', 'areas': 'áreas',
|
|
103
|
+
'apos': 'após',
|
|
104
|
+
'proximo': 'próximo', 'proxima': 'próxima', 'proximos': 'próximos', 'proximas': 'próximas',
|
|
105
|
+
'unico': 'único', 'unica': 'única', 'unicos': 'únicos', 'unicas': 'únicas',
|
|
106
|
+
'publico': 'público', 'publica': 'pública',
|
|
107
|
+
'pratico': 'prático', 'pratica': 'prática',
|
|
108
|
+
'possivel': 'possível', 'possiveis': 'possíveis',
|
|
109
|
+
'disponivel': 'disponível', 'disponiveis': 'disponíveis',
|
|
110
|
+
'responsavel': 'responsável', 'responsaveis': 'responsáveis',
|
|
111
|
+
'util': 'útil', 'uteis': 'úteis',
|
|
112
|
+
'tres': 'três',
|
|
113
|
+
'seculo': 'século', 'seculos': 'séculos',
|
|
114
|
+
'hifen': 'hífen', 'hifens': 'hífens',
|
|
115
|
+
'voce': 'você', 'voces': 'vocês',
|
|
116
|
+
'portugues': 'português',
|
|
117
|
+
'ingles': 'inglês',
|
|
118
|
+
'contem': 'contém',
|
|
119
|
+
'atencao': 'atenção',
|
|
120
|
+
'instalacao': 'instalação',
|
|
121
|
+
'organizacao': 'organização', 'organizacoes': 'organizações',
|
|
122
|
+
'informacao': 'informação', 'informacoes': 'informações',
|
|
123
|
+
'documentacao': 'documentação',
|
|
124
|
+
'autenticacao': 'autenticação',
|
|
125
|
+
'aplicacao': 'aplicação', 'aplicacoes': 'aplicações',
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// Helpers
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
const CODE_FENCE = /```[\s\S]*?```/g
|
|
133
|
+
const INLINE_CODE = /`[^`\n]+`/g
|
|
134
|
+
const COMMIT_HASH = /\b[0-9a-f]{7,40}\b/g
|
|
135
|
+
const TASK_ID = /\bT-\d{3}\b/g
|
|
136
|
+
|
|
137
|
+
function protectSpans(text) {
|
|
138
|
+
const spans = []
|
|
139
|
+
const replaced = text
|
|
140
|
+
.replace(CODE_FENCE, (m) => { spans.push(m); return `\u0000${spans.length - 1}\u0000` })
|
|
141
|
+
.replace(INLINE_CODE, (m) => { spans.push(m); return `\u0000${spans.length - 1}\u0000` })
|
|
142
|
+
return { text: replaced, spans }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function restoreSpans(text, spans) {
|
|
146
|
+
return text.replace(/\u0000(\d+)\u0000/g, (_, idx) => spans[Number(idx)])
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function matchCase(original, replacement) {
|
|
150
|
+
if (original === original.toUpperCase() && original.length > 1) return replacement.toUpperCase()
|
|
151
|
+
if (original[0] === original[0].toUpperCase()) return replacement[0].toUpperCase() + replacement.slice(1)
|
|
152
|
+
return replacement
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function applyDict(text, dict, ruleName, changes) {
|
|
156
|
+
// ordena por tamanho desc para evitar que "concluida" case com "concluidas"
|
|
157
|
+
const keys = Object.keys(dict).sort((a, b) => b.length - a.length)
|
|
158
|
+
for (const from of keys) {
|
|
159
|
+
const to = dict[from]
|
|
160
|
+
const re = new RegExp(`\\b${from.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'gi')
|
|
161
|
+
text = text.replace(re, (m) => {
|
|
162
|
+
const out = matchCase(m, to)
|
|
163
|
+
changes.push({ rule: ruleName, before: m, after: out })
|
|
164
|
+
return out
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
return text
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
// Transformações
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
function stripLeadingEmojis(text, changes) {
|
|
175
|
+
const emojiAtLineStart = /^[ \t]*([\u2705\u274C\u26A0\u{1F680}\u2B50\u{1F3AF}\u{1F4A1}\u{1F527}\u{1F389}\u{1F525}\u{1F44D}\u{1F44C}])\s+/gmu
|
|
176
|
+
return text.replace(emojiAtLineStart, (m, emoji) => {
|
|
177
|
+
changes.push({ rule: 'P17-emoji-bullet', before: emoji, after: '' })
|
|
178
|
+
return m.replace(emojiAtLineStart, '').replace(/^[ \t]+/, '') || ''
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function normalizeDashes(text, changes) {
|
|
183
|
+
// em-dash entre palavras sem espaço de travessão legítimo → hyphen
|
|
184
|
+
// Padrão travessão: " — " (espaço-dash-espaço) é legítimo; "word—word" não
|
|
185
|
+
const re = /(\w)—(\w)/g
|
|
186
|
+
return text.replace(re, (m, a, b) => {
|
|
187
|
+
changes.push({ rule: 'P14-dash', before: '—', after: '-' })
|
|
188
|
+
return `${a}-${b}`
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
// API pública
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
function sanitize(input) {
|
|
197
|
+
if (!input || typeof input !== 'string') return { text: input || '', changes: [] }
|
|
198
|
+
const changes = []
|
|
199
|
+
const { text: protectedText, spans } = protectSpans(input)
|
|
200
|
+
let out = protectedText
|
|
201
|
+
out = stripLeadingEmojis(out, changes)
|
|
202
|
+
out = normalizeDashes(out, changes)
|
|
203
|
+
out = applyDict(out, MISSING_ACCENTS, 'spell-accent', changes)
|
|
204
|
+
out = applyDict(out, AI_VOCAB, 'P07-ai-vocab', changes)
|
|
205
|
+
out = restoreSpans(out, spans)
|
|
206
|
+
return { text: out, changes }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
module.exports = { sanitize, AI_VOCAB, MISSING_ACCENTS }
|
|
210
|
+
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
// CLI
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
if (require.main === module) {
|
|
216
|
+
const args = process.argv.slice(2)
|
|
217
|
+
let input = ''
|
|
218
|
+
let json = false
|
|
219
|
+
for (let i = 0; i < args.length; i++) {
|
|
220
|
+
const a = args[i]
|
|
221
|
+
if (a === '--text') input = args[++i] || ''
|
|
222
|
+
else if (a === '--file') {
|
|
223
|
+
const fs = require('node:fs')
|
|
224
|
+
input = fs.readFileSync(args[++i], 'utf8')
|
|
225
|
+
} else if (a === '--json') json = true
|
|
226
|
+
else if (a === '--stdin') {
|
|
227
|
+
input = require('node:fs').readFileSync(0, 'utf8')
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (!input) {
|
|
231
|
+
try { input = require('node:fs').readFileSync(0, 'utf8') } catch { /* empty */ }
|
|
232
|
+
}
|
|
233
|
+
const result = sanitize(input)
|
|
234
|
+
if (json) process.stdout.write(JSON.stringify(result, null, 2) + '\n')
|
|
235
|
+
else process.stdout.write(result.text)
|
|
236
|
+
}
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
{ "slug": "agent-teams", "path": "skills/agent-teams/SKILL.md" },
|
|
19
19
|
{ "slug": "git-ssh-setup", "path": "skills/git-ssh-setup/SKILL.md" },
|
|
20
20
|
{ "slug": "humanizer", "path": "skills/humanizer/SKILL.md" },
|
|
21
|
-
{ "slug": "weekly-update", "path": "skills/weekly-update/SKILL.md" }
|
|
21
|
+
{ "slug": "weekly-update", "path": "skills/weekly-update/SKILL.md" },
|
|
22
|
+
{ "slug": "slack-review", "path": "skills/slack-review/SKILL.md" }
|
|
22
23
|
]
|
|
23
24
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: slack-review
|
|
3
|
+
description: Revisar, editar e aprovar drafts de notificações Slack pendentes em .gos/slack-queue/. Use quando o usuário pedir para "revisar notificações", "aprovar Slack", "enviar notificações pendentes" ou após commits que dispararam o hook post-commit-sync.
|
|
4
|
+
argument-hint: "[opcional: id específico, --flush, --reject-all]"
|
|
5
|
+
use-when:
|
|
6
|
+
- usuário pediu para revisar/aprovar notificações Slack pendentes
|
|
7
|
+
- após commit em branch com hook post-commit-sync instalado
|
|
8
|
+
- fila .gos/slack-queue/ tem drafts pendentes
|
|
9
|
+
- preciso enviar atualização de task para o Slack com aprovação humana
|
|
10
|
+
do-not-use-for:
|
|
11
|
+
- envio imediato sem revisão (use `node slack-notify.js --send-now`)
|
|
12
|
+
- textos longos (propostas, emails) — use /humanizer primeiro
|
|
13
|
+
- drafts em canal custom sem webhook configurado
|
|
14
|
+
metadata:
|
|
15
|
+
category: workflow
|
|
16
|
+
version: "1.0.0"
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# Skill: /slack-review — Aprovar notificações Slack
|
|
20
|
+
|
|
21
|
+
Revisar drafts enfileirados por `post-commit-sync.js` antes de enviar ao Slack. Aprovar, editar inline ou rejeitar cada um.
|
|
22
|
+
|
|
23
|
+
## Quando usar
|
|
24
|
+
|
|
25
|
+
- Depois de um commit com `T-NNN` no message — hook enfileira draft em `.gos/slack-queue/`.
|
|
26
|
+
- Usuário pediu para revisar ou aprovar notificações pendentes.
|
|
27
|
+
- Antes de push/deploy quando vários commits foram feitos e é preciso consolidar notificações.
|
|
28
|
+
|
|
29
|
+
## Pré-requisitos
|
|
30
|
+
|
|
31
|
+
- `SLACK_WEBHOOK_URL` configurado em `.env` na raiz.
|
|
32
|
+
- Fila em `.gos/slack-queue/` existe (criada automaticamente pelo hook).
|
|
33
|
+
|
|
34
|
+
## Fluxo
|
|
35
|
+
|
|
36
|
+
### 1. Listar pendentes
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
node .gos/scripts/tools/slack-queue.js list
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Saída esperada: lista de drafts com id, task, commit, author e primeira linha do payload.
|
|
43
|
+
|
|
44
|
+
Se vazio → informar usuário e encerrar.
|
|
45
|
+
|
|
46
|
+
### 2. Iterar cada draft
|
|
47
|
+
|
|
48
|
+
Para cada draft pendente:
|
|
49
|
+
|
|
50
|
+
1. Mostrar preview via `node .gos/scripts/tools/slack-queue.js show <id>`.
|
|
51
|
+
2. Renderizar o `payload.text` em bloco markdown para o usuário ver.
|
|
52
|
+
3. Via `AskUserQuestion`, perguntar: **aprovar · editar · rejeitar · pular**.
|
|
53
|
+
|
|
54
|
+
### 3. Ações
|
|
55
|
+
|
|
56
|
+
- **Aprovar**: `node .gos/scripts/tools/slack-queue.js approve <id>` — envia e move para `sent/`.
|
|
57
|
+
- **Editar**: propor reescrita do texto (aplicar princípios do `/humanizer` mentalmente se o texto tiver padrões IA residuais). Mostrar diff, pedir confirmação. Salvar via editar o JSON direto ou chamar `slack-queue.js edit <id>` (abre $EDITOR).
|
|
58
|
+
- **Rejeitar**: `node .gos/scripts/tools/slack-queue.js reject <id> [--reason "..."]` — move para `rejected/`.
|
|
59
|
+
- **Pular**: deixa pendente para próxima run.
|
|
60
|
+
|
|
61
|
+
### 4. Atalhos
|
|
62
|
+
|
|
63
|
+
- `node .gos/scripts/tools/slack-queue.js flush` — aprova todos pendentes de uma vez (só usar quando já revisou visualmente e tem certeza).
|
|
64
|
+
- `node .gos/scripts/tools/slack-queue.js list --json` — saída JSON para processamento programático.
|
|
65
|
+
|
|
66
|
+
## Exemplos
|
|
67
|
+
|
|
68
|
+
### Exemplo 1 — Revisão simples
|
|
69
|
+
|
|
70
|
+
Usuário: "Revisa as notificações Slack pendentes"
|
|
71
|
+
|
|
72
|
+
1. `slack-queue.js list` → 2 drafts (T-110, T-082).
|
|
73
|
+
2. Para T-110: mostrar preview, perguntar. Usuário aprova → `approve`.
|
|
74
|
+
3. Para T-082: mostrar preview, usuário pede para editar "Commit: wip" → propor "Commit: progresso inicial", confirmar, aplicar, approve.
|
|
75
|
+
|
|
76
|
+
### Exemplo 2 — Edição com humanização leve
|
|
77
|
+
|
|
78
|
+
Preview traz "Task concluida aprimorando a validacao". O `text-sanitize.js` já teria corrigido isso, mas se escapou (ex: texto veio via `send --text` sem pipeline), propor reescrita: "Task concluída: validação melhorada". Confirmar com usuário.
|
|
79
|
+
|
|
80
|
+
## Troubleshooting
|
|
81
|
+
|
|
82
|
+
- **"SLACK_WEBHOOK_URL não configurado"** no approve: checar `.env` na raiz, recarregar env.
|
|
83
|
+
- **Draft preso em pending após approve**: verificar se `sent/` foi criado e o arquivo foi movido. Se falhou no POST, ver status no output JSON.
|
|
84
|
+
- **Lista vazia mas esperava drafts**: confirmar que commit usou `T-NNN` e hook rodou (`ls .git/hooks/post-commit`). Ver [docs/slack-notifications.md](../docs/slack-notifications.md).
|
|
85
|
+
|
|
86
|
+
## Referências
|
|
87
|
+
|
|
88
|
+
- Pipeline completo: `.gos/docs/slack-notifications.md`
|
|
89
|
+
- CLI: `.gos/scripts/tools/slack-queue.js`
|
|
90
|
+
- Sanitização: `.gos/scripts/tools/text-sanitize.js`
|
|
91
|
+
- Payload builders: `.gos/scripts/tools/slack-notify.js`
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# weekly-update — Changelog
|
|
2
|
+
|
|
3
|
+
## 2026-04-17 — v2.0 (Web API, thread obrigatória, auto-descoberta)
|
|
4
|
+
|
|
5
|
+
Mudanças estruturais motivadas por incidente em produção: o envio via incoming webhook caiu fora da thread "Conversa semanal" e gerou mensagem isolada no canal #cnpq-tech.
|
|
6
|
+
|
|
7
|
+
### Incidente
|
|
8
|
+
|
|
9
|
+
- **O quê:** weekly-update postou como mensagem nova no canal em vez de dentro da thread.
|
|
10
|
+
- **Causa raiz:** incoming webhook do Slack não suporta `thread_ts`. O skill (v1.x) tentava passar `--thread-ts` para `slack-notify.js send`, mas o script ignorava (webhook path).
|
|
11
|
+
- **Impacto:** poluição do canal + confusão visual; mensagem correta teve que ser reenviada e a errada deletada manualmente (API rejeitou `chat.delete` com `cant_delete_message` porque webhook é identidade separada mesmo dentro do mesmo app).
|
|
12
|
+
|
|
13
|
+
### Migração obrigatória: Web API (chat.postMessage)
|
|
14
|
+
|
|
15
|
+
- Weekly-update **não usa mais webhook**. Passa a usar `chat.postMessage` via Web API com bot token.
|
|
16
|
+
- Env vars novas (ambas obrigatórias):
|
|
17
|
+
- `SLACK_BOT_TOKEN=xoxb-...` — Bot User OAuth Token.
|
|
18
|
+
- `SLACK_CHANNEL_ID_WEEKLY=C0ADYLL6W0K` — ID do canal #cnpq-tech.
|
|
19
|
+
- Env var antiga `SLACK_WEBHOOK_CSPO_TECH` **não é mais usada pelo weekly-update** (continua viva para hooks de commit/task, sem impacto).
|
|
20
|
+
|
|
21
|
+
### Scopes necessários no bot (app Slack)
|
|
22
|
+
|
|
23
|
+
- `chat:write` — postar mensagens.
|
|
24
|
+
- `chat:write.public` — postar em canais públicos sem invite (opcional).
|
|
25
|
+
- `groups:history` — ler histórico de canais privados (necessário para auto-descoberta de thread).
|
|
26
|
+
- Após adicionar qualquer scope: **Reinstall to Ganbatte** no painel OAuth (sem reinstall o token não ganha o scope).
|
|
27
|
+
- Bot precisa ser convidado no canal privado: `/invite @autocommit` em `#cnpq-tech`.
|
|
28
|
+
|
|
29
|
+
### Thread obrigatória
|
|
30
|
+
|
|
31
|
+
- Skill cancela o envio se não conseguir resolver `thread_ts`. Nunca mais cai em "mensagem nova no canal".
|
|
32
|
+
- Ordem de resolução do `thread_ts` (Phase 5):
|
|
33
|
+
1. Link do Slack em `$ARGUMENTS` → extrai ts.
|
|
34
|
+
2. **Auto-descoberta** via `slack-notify.js find-thread` (novo) — busca mensagem mais recente que bata com o regex.
|
|
35
|
+
3. Fallback: `AskUserQuestion` pedindo o link.
|
|
36
|
+
|
|
37
|
+
### Auto-descoberta de thread
|
|
38
|
+
|
|
39
|
+
Novo comando no CLI:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
node .gos/scripts/tools/slack-notify.js find-thread \
|
|
43
|
+
--channel-id "$SLACK_CHANNEL_ID_WEEKLY" \
|
|
44
|
+
--pattern "CHECK-POINT ASSÍNCRONO|Conversa semanal"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Retorno:
|
|
48
|
+
```json
|
|
49
|
+
{ "found": true, "ts": "1776445206.005489", "text_preview": "..." }
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Resolve o `thread_ts` da última mensagem que bate com o pattern — dispensa cole manual de link a cada semana.
|
|
53
|
+
|
|
54
|
+
### Anti-sobreposição entre updates (Phase 1)
|
|
55
|
+
|
|
56
|
+
Dois mecanismos cumulativos:
|
|
57
|
+
|
|
58
|
+
1. **Arquivo de estado** `.gos/.weekly-update-last-run.json` — armazena `last_run_date` (YYYY-MM-DD). Skill usa como piso do filtro ClickUp (`date_done_from`). Gravado apenas após `sent: true`.
|
|
59
|
+
2. **Arquivo do update anterior** `.gos/weekly-updates/YYYY-MM-DD.md` — texto íntegro do último envio. Skill lê o mais recente em Phase 1 e cruza contra o draft em Phase 2/4, descartando menções duplicadas (cobre casos em que uma task foi reportada proativamente e só fechou no ClickUp na janela seguinte).
|
|
60
|
+
|
|
61
|
+
### Janela de busca default
|
|
62
|
+
|
|
63
|
+
- Era 10 dias. Agora 7 (fallback quando não há `last_run_date`). Alinha com cadência semanal.
|
|
64
|
+
|
|
65
|
+
### Template — quebra visual entre título e corpo
|
|
66
|
+
|
|
67
|
+
- Todas as seções (`*O que foi feito*`, `*Desafios encontrados*`, etc.) agora têm linha em branco entre título e primeiro parágrafo. Slack renderia colado antes.
|
|
68
|
+
|
|
69
|
+
### Novos comandos no `slack-notify.js`
|
|
70
|
+
|
|
71
|
+
| Comando | Uso | Scope necessário |
|
|
72
|
+
|---|---|---|
|
|
73
|
+
| `send --channel-id C... --thread-ts TS` | Post via Web API em thread | `chat:write` |
|
|
74
|
+
| `delete --channel-id C... --ts TS` | Apagar mensagem do próprio bot | `chat:write` (do app autor) |
|
|
75
|
+
| `find-thread --channel-id C... --pattern REGEX` | Achar ts mais recente que bata com regex | `groups:history` (privado) |
|
|
76
|
+
|
|
77
|
+
### Limitações conhecidas
|
|
78
|
+
|
|
79
|
+
- `chat.delete` só funciona para mensagens postadas pelo **mesmo token**. Webhook conta como identidade separada — mensagens antigas postadas via webhook não podem ser deletadas via API mesmo que o app seja o mesmo. Delete manual no Slack UI é o único caminho nesse caso.
|
|
80
|
+
|
|
81
|
+
### Coexistência com hooks de commit
|
|
82
|
+
|
|
83
|
+
- Hooks pós-commit (`task-done`, `task-update`, `sprint-summary`) continuam em `SLACK_WEBHOOK_URL` via `sendWebhook`. Zero impacto.
|
|
84
|
+
- Weekly-update é o único fluxo que migrou para Web API.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Smoke test do hook storybook-branch-check.sh (T-104)
|
|
3
|
+
set -e
|
|
4
|
+
|
|
5
|
+
HOOK=".claude/hooks/storybook-branch-check.sh"
|
|
6
|
+
export CLAUDE_PROJECT_DIR="$(pwd)"
|
|
7
|
+
|
|
8
|
+
fail() { echo "FAIL: $1"; exit 1; }
|
|
9
|
+
|
|
10
|
+
echo "=== Test 1: UserPromptSubmit com 'storybook' ==="
|
|
11
|
+
OUT=$(echo '{"user_prompt":"edite a story do Button no storybook"}' | bash "$HOOK")
|
|
12
|
+
current=$(git -C packages/fractus branch --show-current 2>/dev/null || echo unknown)
|
|
13
|
+
if [ "$current" != "feat/storybook" ]; then
|
|
14
|
+
[[ "$OUT" == *"STORYBOOK BRANCH CONTEXT"* ]] || fail "warning nao injetado (branch=$current). Output: $OUT"
|
|
15
|
+
echo "PASS: warning injetado (branch=$current)"
|
|
16
|
+
else
|
|
17
|
+
[[ -z "$OUT" ]] || fail "deveria ser silencioso em feat/storybook. Output: $OUT"
|
|
18
|
+
echo "PASS: silencioso em feat/storybook"
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
echo "=== Test 2: prompt sem storybook (silencioso) ==="
|
|
22
|
+
OUT=$(echo '{"user_prompt":"liste as tasks abertas"}' | bash "$HOOK")
|
|
23
|
+
[[ -z "$OUT" ]] || fail "deveria ser silencioso. Output: $OUT"
|
|
24
|
+
echo "PASS: silencioso"
|
|
25
|
+
|
|
26
|
+
echo "=== Test 3: PreToolUse em .stories.tsx ==="
|
|
27
|
+
OUT=$(echo '{"tool_input":{"file_path":"/e/Github/Ganbatte/packages/fractus/src/components/Button.stories.tsx"}}' | bash "$HOOK")
|
|
28
|
+
if [ "$current" != "feat/storybook" ]; then
|
|
29
|
+
[[ "$OUT" == *"STORYBOOK BRANCH CONTEXT"* ]] || fail "warning nao injetado por file_path. Output: $OUT"
|
|
30
|
+
echo "PASS: warning por file_path"
|
|
31
|
+
else
|
|
32
|
+
echo "PASS: silencioso em feat/storybook"
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
echo "=== Test 4: exit code sempre 0 ==="
|
|
36
|
+
echo '{}' | bash "$HOOK"
|
|
37
|
+
code=$?
|
|
38
|
+
[ "$code" -eq 0 ] || fail "exit code=$code"
|
|
39
|
+
echo "PASS: exit=0"
|
|
40
|
+
|
|
41
|
+
echo ""
|
|
42
|
+
echo "Todos os testes passaram"
|
package/package.json
CHANGED