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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ganbatte-os",
3
- "version": "0.2.20",
3
+ "version": "0.2.22",
4
4
  "description": "Framework operacional para design-to-code, squads de entrega e sprint sync com ClickUp.",
5
5
  "bin": {
6
6
  "ganbatte-os": ".gos/scripts/cli/gos-cli.js",