@fontdata/ai-harness 2.0.0 → 2.1.0
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +9 -2
- package/LICENSE +21 -0
- package/README.md +174 -140
- package/{src → dist}/catalog.js +5 -12
- package/dist/cli.js +174 -0
- package/dist/comentarios/command.js +74 -0
- package/dist/comentarios/instalar.js +33 -0
- package/dist/config/config.js +98 -0
- package/dist/contratos/artefatos.js +284 -0
- package/dist/contratos/eventos.js +103 -0
- package/dist/doctor/checks.js +424 -0
- package/dist/init.js +240 -0
- package/dist/install/atomic.js +51 -0
- package/dist/install/flags.js +99 -0
- package/dist/install/hooks.js +47 -0
- package/dist/install/lock.js +39 -0
- package/dist/install/plan.js +209 -0
- package/dist/install/selection.js +77 -0
- package/dist/install/settings.js +108 -0
- package/dist/memoria/indice.js +195 -0
- package/dist/migrar/migrar.js +426 -0
- package/dist/rastro/command.js +46 -0
- package/dist/run.js +86 -0
- package/dist/types.js +1 -0
- package/dist/update/legado.js +74 -0
- package/dist/update/update.js +284 -0
- package/package.json +17 -8
- package/templates/CLAUDE.md +59 -30
- package/templates/agents/auditor-plano.md +42 -0
- package/templates/agents/investigador.md +32 -0
- package/templates/agents/qa.md +44 -0
- package/templates/agents/revisor-testes.md +40 -0
- package/templates/catalog.json +24 -61
- package/templates/commands/atualizar-deps.md +43 -9
- package/templates/commands/diagrama.md +35 -10
- package/templates/commands/gerar-env.md +39 -10
- package/templates/commands/gerar-testes.md +42 -10
- package/templates/commands/git-commit-push.md +0 -0
- package/templates/commands/onboarding.md +44 -15
- package/templates/commands/refatorar.md +50 -10
- package/templates/commands/revisar-pr.md +56 -12
- package/templates/commands/sincronizar-claude-md.md +177 -9
- package/templates/commands/versao.md +0 -0
- package/templates/hooks/comentarios/check.mjs +210 -0
- package/templates/hooks/hooks.json +34 -0
- package/templates/hooks/lembrete.mjs +29 -0
- package/templates/hooks/lib/config.mjs +40 -0
- package/templates/hooks/lib/hook.mjs +85 -0
- package/templates/hooks/lib/rastro.mjs +130 -0
- package/templates/hooks/lib/segredos.mjs +91 -0
- package/templates/hooks/lib/temas.mjs +111 -0
- package/templates/hooks/lib/trabalho.mjs +94 -0
- package/templates/hooks/proteger-contratos.mjs +56 -0
- package/templates/hooks/segredo-no-commit.mjs +76 -0
- package/templates/hooks/sem-jargao.mjs +58 -0
- package/templates/hooks/task-so-fecha-verde.mjs +33 -0
- package/templates/legado/manifesto.json +1267 -0
- package/templates/rules/comentarios.md +166 -0
- package/templates/rules/harness-projeto.md +41 -0
- package/templates/skills/auditar-performance/SKILL.md +53 -0
- package/templates/skills/auditar-performance/references/pontos-quentes.md +35 -0
- package/templates/skills/auditar-performance/templates/relatorios/CLIENTE.md +26 -0
- package/templates/skills/auditar-performance/templates/relatorios/TECNICO.md +27 -0
- package/templates/skills/auditar-seguranca/SKILL.md +67 -0
- package/templates/skills/auditar-seguranca/references/seguranca-tecnica.md +23 -0
- package/templates/skills/auditar-seguranca/templates/relatorios/CLIENTE.md +30 -0
- package/templates/skills/auditar-seguranca/templates/relatorios/TECNICO.md +38 -0
- package/templates/skills/auditar-ui/SKILL.md +64 -0
- package/templates/skills/auditar-ui/references/lentes.md +33 -0
- package/templates/skills/auditar-ui/templates/relatorios/CLIENTE.md +26 -0
- package/templates/skills/auditar-ui/templates/relatorios/TECNICO.md +31 -0
- package/templates/skills/evoluir-harness/SKILL.md +81 -0
- package/templates/skills/evoluir-harness/references/agent.md +83 -0
- package/templates/skills/evoluir-harness/references/auditoria.md +107 -0
- package/templates/skills/evoluir-harness/references/code-reviewer.md +68 -0
- package/templates/skills/evoluir-harness/references/design-system.md +56 -0
- package/templates/skills/evoluir-harness/references/hook.md +95 -0
- package/templates/skills/evoluir-harness/references/rule.md +67 -0
- package/templates/skills/evoluir-harness/references/skill.md +63 -0
- package/templates/skills/executar-plan/SKILL.md +114 -0
- package/templates/skills/executar-plan/references/delegacao.md +43 -0
- package/templates/skills/executar-plan/references/retomada.md +33 -0
- package/templates/skills/executar-plan/templates/BLOQUEIOS.md +30 -0
- package/templates/skills/harness/SKILL.md +75 -0
- package/templates/skills/harness/references/auto-tag.md +118 -0
- package/templates/skills/harness/references/ignores.md +40 -0
- package/templates/skills/harness/references/scaffold.md +29 -0
- package/templates/skills/harness/references/validar-ideia.md +42 -0
- package/templates/skills/memoria/SKILL.md +52 -0
- package/templates/skills/nova-tarefa/SKILL.md +97 -0
- package/templates/skills/nova-tarefa/references/isso-ja-existe.md +24 -0
- package/templates/skills/nova-tarefa/references/regras-do-plano.md +33 -0
- package/templates/skills/nova-tarefa/templates/DECISOES.md +20 -0
- package/templates/skills/nova-tarefa/templates/PLAN.md +58 -0
- package/templates/skills/nova-tarefa/templates/SPEC.md +37 -0
- package/templates/skills/resolver-bug/SKILL.md +104 -0
- package/templates/skills/resolver-bug/references/relatorio-para-cliente.md +26 -0
- package/templates/skills/resolver-bug/templates/PLAN.md +62 -0
- package/templates/skills/resolver-bug/templates/relatorios/CLIENTE.md +29 -0
- package/templates/skills/resolver-bug/templates/relatorios/TECNICO.md +32 -0
- package/bin/cli.js +0 -122
- package/src/init.js +0 -317
- package/src/installer.js +0 -92
- package/src/run.js +0 -99
- package/templates/commands/auditar-performance.md +0 -27
- package/templates/commands/auditar-seguranca.md +0 -64
- package/templates/commands/auditar-ui.md +0 -38
- package/templates/commands/codigo-morto.md +0 -22
- package/templates/commands/configurar-auto-tag.md +0 -185
- package/templates/commands/configurar-comentarios.md +0 -543
- package/templates/commands/configurar-orquestrador.md +0 -13
- package/templates/commands/corrigir-review.md +0 -11
- package/templates/commands/criar-agents.md +0 -42
- package/templates/commands/criar-code-reviewer.md +0 -30
- package/templates/commands/criar-hooks.md +0 -32
- package/templates/commands/criar-rag.md +0 -25
- package/templates/commands/criar-rules.md +0 -31
- package/templates/commands/criar-skill.md +0 -22
- package/templates/commands/design-system.md +0 -27
- package/templates/commands/executar-plan.md +0 -36
- package/templates/commands/gerar-claude-md.md +0 -66
- package/templates/commands/gerar-ignore.md +0 -37
- package/templates/commands/gerar-spec.md +0 -33
- package/templates/commands/harness.md +0 -34
- package/templates/commands/nova-tarefa.md +0 -60
- package/templates/commands/resolver-bug.md +0 -25
- package/templates/commands/review-por-task.md +0 -16
- package/templates/commands/scaffold.md +0 -25
- package/templates/commands/validar-commands.md +0 -24
- package/templates/commands/validar-ideia.md +0 -36
|
@@ -1,15 +1,183 @@
|
|
|
1
1
|
---
|
|
2
|
-
description:
|
|
2
|
+
description: Gera o SPEC.md (entrevista) e o CLAUDE.md que faltam e reconcilia os existentes com o código e os agents do projeto, incluindo a seção de orquestração. Só grava o que for aprovado.
|
|
3
|
+
argument-hint: "[spec | claude-md | reconciliar] [seção ou tema opcional]"
|
|
3
4
|
---
|
|
4
5
|
|
|
5
|
-
#
|
|
6
|
+
# /sincronizar-claude-md — SPEC.md e CLAUDE.md gerados e em dia com o código
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
Argumentos: $ARGUMENTS
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
> **🚦 Há um GATE de aprovação** antes de gravar qualquer arquivo. Este comando **não faz commit nem push**. Rode no
|
|
11
|
+
> setup e sempre que o projeto mudar: o que não existe é gerado, o que existe é reconciliado.
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
## Passo 0 — Detectar o modo (somente leitura)
|
|
14
|
+
|
|
15
|
+
1. Leia o primeiro argumento. `spec`, `claude-md` ou `reconciliar` forçam o modo; o resto é o foco (seção ou tema).
|
|
16
|
+
2. Sem modo forçado, decida pelo que existe na raiz:
|
|
17
|
+
|
|
18
|
+
| Situação | Modo |
|
|
19
|
+
|---|---|
|
|
20
|
+
| Sem `SPEC.md` | **gerar SPEC** |
|
|
21
|
+
| Com `SPEC.md` e sem `CLAUDE.md` (ou só o modelo base, com o título `[NOME DO PROJETO]`) | **gerar CLAUDE.md** |
|
|
22
|
+
| Com os dois | **reconciliar** |
|
|
23
|
+
|
|
24
|
+
3. Levante também, sem alterar nada: `.claude/agents/`, `.claude/rules/harness-projeto.md`, os scripts de build e
|
|
25
|
+
teste (`package.json`, `composer.json`, `Makefile`, `docker-compose.yml`, `.dproj`...) e os trabalhos em
|
|
26
|
+
`docs/trabalhos/<id>/` (`SPEC.md`, `PLAN.md`, `DECISOES.md`) que mudaram o que os documentos descrevem.
|
|
27
|
+
4. Casos forçados: `spec` com `SPEC.md` existente avisa que a entrevista gera um arquivo novo e pergunta se não é o
|
|
28
|
+
caso de `reconciliar`; `claude-md` com `CLAUDE.md` existente regenera a partir do SPEC e lista no GATE tudo que
|
|
29
|
+
seria perdido; `reconciliar` sem `SPEC.md` confronta só o `CLAUDE.md` e registra a falta do SPEC.
|
|
30
|
+
5. Informe o modo escolhido e o motivo antes de seguir.
|
|
31
|
+
|
|
32
|
+
## Passo 1 — Coletar o conteúdo
|
|
33
|
+
|
|
34
|
+
### Modo gerar SPEC — entrevista
|
|
35
|
+
|
|
36
|
+
Conduza uma entrevista de requisitos, **uma pergunta por vez**. Se já houver código, leia-o antes e pergunte só o que
|
|
37
|
+
ele não responde, confirmando o que foi inferido. Cubra obrigatoriamente:
|
|
38
|
+
|
|
39
|
+
- problema real que o sistema resolve e quem são os usuários;
|
|
40
|
+
- funcionalidades essenciais, nice-to-have e o que está fora do escopo;
|
|
41
|
+
- módulos e a responsabilidade de cada um;
|
|
42
|
+
- stack técnica e constraints (prazo, infraestrutura, integrações, legado);
|
|
43
|
+
- como saber que está pronto (critérios de aceitação verificáveis).
|
|
44
|
+
|
|
45
|
+
Com informação suficiente, avise que está pronto para rascunhar.
|
|
46
|
+
|
|
47
|
+
### Modo gerar CLAUDE.md
|
|
48
|
+
|
|
49
|
+
Leia o `SPEC.md` para intenção, escopo e stack pretendida. Leia o código para o que é **fato**: stack instalada,
|
|
50
|
+
estrutura de pastas, comandos de instalação, execução e teste, framework e pasta dos testes, convenções já em uso.
|
|
51
|
+
Se existir o `CLAUDE.md` modelo base, guarde as seções do harness (filosofia de trabalho, modelo de arquivos,
|
|
52
|
+
garantias do harness, código legado) para preservá-las.
|
|
53
|
+
|
|
54
|
+
### Modo reconciliar
|
|
55
|
+
|
|
56
|
+
Confronte `CLAUDE.md` e `SPEC.md` com o estado **real** — stack, scripts, estrutura de pastas, comandos de build e
|
|
57
|
+
teste, convenções e fluxos descritos — e com os agents de `.claude/agents/`. Com foco nos argumentos, limite-se a ele.
|
|
58
|
+
Para cada divergência, registre o que o documento afirma, o que o código mostra e a evidência (`arquivo:linha`),
|
|
59
|
+
classificada em:
|
|
60
|
+
|
|
61
|
+
- **DESATUALIZADO** — o documento ficou para trás;
|
|
62
|
+
- **FALTANDO** — existe no código ou em `.claude/agents/`, não no documento;
|
|
63
|
+
- **OBSOLETO** — está no documento, não existe mais (inclui agent ou comando citado que não está instalado).
|
|
64
|
+
|
|
65
|
+
Marque como **OBSOLETO** o bloco antigo "Comentários em código" e a linha `@.claude/rules/comentarios.md`: a rule
|
|
66
|
+
`.claude/rules/comentarios.md` é carregada automaticamente pelo Claude Code e não precisa estar no `CLAUDE.md`.
|
|
67
|
+
|
|
68
|
+
## Passo 2 — Rascunhar (sem gravar)
|
|
69
|
+
|
|
70
|
+
### SPEC.md
|
|
71
|
+
|
|
72
|
+
Estrutura fixa, sem adicionar nem remover seções:
|
|
73
|
+
|
|
74
|
+
```markdown
|
|
75
|
+
# SPEC.md
|
|
76
|
+
|
|
77
|
+
## Problema
|
|
78
|
+
## Usuários
|
|
79
|
+
## Funcionalidades
|
|
80
|
+
### Essenciais
|
|
81
|
+
### Fora do escopo
|
|
82
|
+
## Módulos
|
|
83
|
+
## Stack
|
|
84
|
+
## Constraints técnicas
|
|
85
|
+
## Critérios de aceitação
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Não invente o que não surgiu na entrevista; ponto ambíguo fica como "a definir" e vira decisão em aberto no
|
|
89
|
+
`CLAUDE.md`. Este é o SPEC **principal**; a spec de cada trabalho fica em `docs/trabalhos/<id>/SPEC.md`, gerada pelo
|
|
90
|
+
`/nova-tarefa`.
|
|
91
|
+
|
|
92
|
+
### CLAUDE.md
|
|
93
|
+
|
|
94
|
+
```markdown
|
|
95
|
+
# [NOME DO PROJETO]
|
|
96
|
+
> [1 frase: o que o sistema faz e qual problema resolve]
|
|
97
|
+
|
|
98
|
+
## Stack
|
|
99
|
+
| Camada | Tecnologia |
|
|
100
|
+
|--------|------------|
|
|
101
|
+
|
|
102
|
+
## Estrutura de pastas
|
|
103
|
+
[árvore com um comentário curto por pasta]
|
|
104
|
+
|
|
105
|
+
## Como rodar localmente
|
|
106
|
+
[comandos exatos de instalação e execução, por parte do sistema]
|
|
107
|
+
|
|
108
|
+
## Padrões de código
|
|
109
|
+
- Nomenclatura de arquivos / variáveis e funções / endpoints / componentes / tipagem: [regra]
|
|
110
|
+
|
|
111
|
+
## Orquestração
|
|
112
|
+
[seção do Passo 3]
|
|
113
|
+
|
|
114
|
+
## TDD
|
|
115
|
+
- Framework e onde ficam os testes: [...]
|
|
116
|
+
- Regra de cobertura: [...]
|
|
117
|
+
- Testes críticos deste projeto: [lista extraída do SPEC]
|
|
118
|
+
|
|
119
|
+
## Nunca fazer
|
|
120
|
+
- Nunca [proibição específica deste projeto]
|
|
121
|
+
|
|
122
|
+
## Decisões em aberto
|
|
123
|
+
- [ ] [decisão não resolvida no SPEC]
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Preencha só com o que está no SPEC ou verificado no código; o resto fica "a definir". Preserve integralmente, na
|
|
127
|
+
posição atual, as seções do harness que já existirem. Não gere bloco de comentários nem `@import` de rule.
|
|
128
|
+
|
|
129
|
+
### Correções (modo reconciliar)
|
|
130
|
+
|
|
131
|
+
Liste as divergências do Passo 1 com o trecho atual e o trecho proposto, lado a lado, agrupadas por arquivo.
|
|
132
|
+
|
|
133
|
+
## Passo 3 — Seção de orquestração
|
|
134
|
+
|
|
135
|
+
Vale para todo modo que grava o `CLAUDE.md`. Liste os agents do projeto em `.claude/agents/` (nome, descrição e as
|
|
136
|
+
pastas que tocam) e os do harness (`revisor-testes`, `qa`, `investigador`, `auditor-plano`; no plugin, com o prefixo
|
|
137
|
+
`fontdata:`). Gere ou atualize, no lugar, uma única seção — nunca duplicada:
|
|
138
|
+
|
|
139
|
+
```markdown
|
|
140
|
+
## Orquestração
|
|
141
|
+
|
|
142
|
+
- A sessão principal é a **orquestradora**: delega cada task ao agent do domínio dela e não implementa diretamente.
|
|
143
|
+
- Planejamento com `/nova-tarefa`; execução com `/executar-plan`; defeitos com `/resolver-bug`.
|
|
144
|
+
- Nenhuma task fecha sem os gates `revisor-testes` e `qa` (agents do harness, somente leitura) e suíte verde.
|
|
145
|
+
- Toda delegação e todo gate são relatados no chat, dizendo **qual agent está ativo**.
|
|
146
|
+
- Dúvida durante a execução vira bloqueio em `docs/trabalhos/<id>/BLOQUEIOS.md`, não pergunta ao usuário.
|
|
147
|
+
- Mapa de agents por domínio, portões extras e comando da suíte: `.claude/rules/harness-projeto.md`.
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
O `CLAUDE.md` aponta para a rule e **não duplica** o que está nela. Rascunhe também a atualização de
|
|
151
|
+
`.claude/rules/harness-projeto.md` (criando-a se não existir), só com as seções que valem:
|
|
152
|
+
|
|
153
|
+
- **Mapa de agents por domínio** — uma linha por agent do projeto, com a pasta ou área que implementa;
|
|
154
|
+
- **Portões extras** — além de `revisor-testes` e `qa`, só os que existem (ex.: um agent de code review instalado);
|
|
155
|
+
- **Suíte de testes** — comando e onde roda, **verificados** nos scripts do projeto.
|
|
156
|
+
|
|
157
|
+
Agent listado na rule que não existe mais sai; agent novo entra. Sem agents do projeto, deixe o mapa vazio e sugira
|
|
158
|
+
`/evoluir-harness` para criá-los.
|
|
159
|
+
|
|
160
|
+
## Passo 4 — 🚦 GATE de aprovação
|
|
161
|
+
|
|
162
|
+
Mostre, por arquivo, o conteúdo completo (arquivo novo) ou as mudanças propostas (arquivo existente), incluindo o
|
|
163
|
+
que seria removido. No modo reconciliar, **pergunte quais correções aplicar**. Nada é gravado sem aprovação explícita.
|
|
164
|
+
|
|
165
|
+
## Passo 5 — Gravar e encadear
|
|
166
|
+
|
|
167
|
+
1. Grave exatamente o que foi aprovado e mostre os trechos que ficaram em cada arquivo.
|
|
168
|
+
2. Se houver um trabalho em execução, registre a mudança também no `DECISOES.md` dele — o hook `proteger-contratos`
|
|
169
|
+
avisa sobre edição de contrato durante a execução.
|
|
170
|
+
3. Acabou de gravar o `SPEC.md` e o `CLAUDE.md` não existe? Volte ao Passo 1 no modo gerar CLAUDE.md.
|
|
171
|
+
4. Encerre sugerindo o próximo passo: `/nova-tarefa` para planejar, `/evoluir-harness` para criar agents, rules,
|
|
172
|
+
skills ou hooks, `/harness` para o setup completo, e `npx @fontdata/ai-harness doctor` para conferir o contrato.
|
|
173
|
+
|
|
174
|
+
## Guardrails
|
|
175
|
+
|
|
176
|
+
- **Nunca** gravar `SPEC.md`, `CLAUDE.md` ou `.claude/rules/harness-projeto.md` sem aprovação no GATE.
|
|
177
|
+
- **Nunca** fazer commit, push ou qualquer outra alteração fora desses três arquivos (e do `DECISOES.md` do trabalho
|
|
178
|
+
em execução).
|
|
179
|
+
- **Nunca** inventar stack, comando ou convenção: comando citado precisa estar verificado no código; o que não se
|
|
180
|
+
sabe fica "a definir".
|
|
181
|
+
- **Nunca** duplicar a seção de orquestração nem copiar para o `CLAUDE.md` o conteúdo de `harness-projeto.md`.
|
|
182
|
+
- **Nunca** gerar bloco de comentários ou `@import` de rule no `CLAUDE.md`.
|
|
183
|
+
- Conteúdo em **pt-BR**.
|
|
File without changes
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync } from 'node:fs';
|
|
3
|
+
import { extname, relative, join, dirname } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const MAX_BLOCO = Number(process.env.COMENTARIOS_MAX_BLOCO ?? 3);
|
|
6
|
+
const MAX_DOCSTRING = Number(process.env.COMENTARIOS_MAX_DOCSTRING ?? 5);
|
|
7
|
+
const DENSIDADE_ALVO = 0.1;
|
|
8
|
+
|
|
9
|
+
const EXTENSOES = new Set([
|
|
10
|
+
'.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
|
|
11
|
+
'.py', '.go', '.rs', '.java', '.cs', '.php',
|
|
12
|
+
'.pas', '.dpr', '.sql', '.sh',
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
const IGNORAR = [
|
|
16
|
+
/node_modules/,
|
|
17
|
+
/[\\/]\.git[\\/]/,
|
|
18
|
+
/[\\/](dist|build|coverage|out|\.next|\.nuxt|vendor|migrations|generated|__generated__)[\\/]/,
|
|
19
|
+
/\.d\.ts$/,
|
|
20
|
+
/\.(test|spec)\.[jt]sx?$/,
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const CABECALHO_PERMITIDO =
|
|
24
|
+
/eslint|ts-nocheck|ts-ignore|prettier|@license|copyright|spdx|jshint|globals|^#!|use (strict|client|server)/i;
|
|
25
|
+
|
|
26
|
+
const BANNER = /^[-=─-╿_*#~+]{3,}|[-=─-╿_*#~+]{3,}$/;
|
|
27
|
+
|
|
28
|
+
const RASTREIO = [
|
|
29
|
+
[/\b(task|tarefa|fase|etapa|plano|spec|sprint|epico|épico)\s*[\dIVX]/i, 'referência a task/fase/plano/spec'],
|
|
30
|
+
[/\bT\d+(\.\d+)+\b/, 'referência a task (T0.0)'],
|
|
31
|
+
[/docs\/(plans|specs|bugs|investigacoes)\//i, 'caminho de docs/ dentro do código'],
|
|
32
|
+
[/\bachado d[oe]\b/i, 'achado de review no código'],
|
|
33
|
+
[/@[\w-]*(reviewer|auditor|orquestrador|agent)\b/i, 'menção a agent no código'],
|
|
34
|
+
[/\b(commit|sha|hash)\s+[0-9a-f]{7,40}\b/i, 'hash de commit'],
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const DATA = /(^|[^"'\w])20\d{2}-\d{2}-\d{2}([^"'\w]|$)/;
|
|
38
|
+
const CODIGO_TOKEN = /(^|\s)(const|let|var|function|return|if|for|while|await|import|export|def|class)\b/;
|
|
39
|
+
const CODIGO_FIM = /[;{}]\s*$/;
|
|
40
|
+
|
|
41
|
+
function comentario(linha, ext) {
|
|
42
|
+
const t = linha.trim();
|
|
43
|
+
if (!t) return null;
|
|
44
|
+
if (ext === '.py' || ext === '.sh') return t.startsWith('#') ? t.replace(/^#+/, '').trim() : null;
|
|
45
|
+
if (ext === '.sql') {
|
|
46
|
+
if (t.startsWith('--')) return t.replace(/^-+/, '').trim();
|
|
47
|
+
if (t.startsWith('/*') || t.startsWith('*')) return t.replace(/^[/*]+/, '').trim();
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
if (ext === '.pas' || ext === '.dpr') {
|
|
51
|
+
if (t.startsWith('//')) return t.replace(/^\/+/, '').trim();
|
|
52
|
+
if (t.startsWith('(*')) return t.replace(/^\(\*/, '').trim();
|
|
53
|
+
if (t.startsWith('{') && !t.startsWith('{$')) return t.replace(/^\{/, '').trim();
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
if (t.startsWith('//')) return t.replace(/^\/+/, '').trim();
|
|
57
|
+
if (t.startsWith('/*')) return t.replace(/^\/\*+/, '').trim();
|
|
58
|
+
if (t.startsWith('*')) return t.replace(/^\*+\/?/, '').trim();
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function analisar(caminho, fonte) {
|
|
63
|
+
const ext = extname(caminho);
|
|
64
|
+
const linhas = fonte.split('\n');
|
|
65
|
+
const achados = [];
|
|
66
|
+
const marca = (linha, msg) => achados.push({ linha, msg });
|
|
67
|
+
const ehComentario = linhas.map((l) => comentario(l, ext));
|
|
68
|
+
|
|
69
|
+
let primeiroCodigo = -1;
|
|
70
|
+
for (let i = 0; i < linhas.length; i++) {
|
|
71
|
+
if (ehComentario[i] === null && linhas[i].trim()) { primeiroCodigo = i; break; }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let i = 0;
|
|
75
|
+
while (i < linhas.length) {
|
|
76
|
+
if (ehComentario[i] === null) { i++; continue; }
|
|
77
|
+
const inicio = i;
|
|
78
|
+
const conteudo = [];
|
|
79
|
+
while (i < linhas.length && ehComentario[i] !== null) { conteudo.push(ehComentario[i]); i++; }
|
|
80
|
+
const fim = i - 1;
|
|
81
|
+
const corpo = conteudo.filter(Boolean);
|
|
82
|
+
const texto = corpo.join(' ');
|
|
83
|
+
|
|
84
|
+
for (let k = 0; k < conteudo.length; k++) {
|
|
85
|
+
if (conteudo[k] && BANNER.test(conteudo[k])) {
|
|
86
|
+
marca(inicio + k + 1, 'separador/banner decorativo — divida o arquivo em vez de decorá-lo');
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
for (const [re, motivo] of RASTREIO) {
|
|
91
|
+
if (re.test(texto)) { marca(inicio + 1, `${motivo} — isso mora no commit ou em docs/, não no código`); break; }
|
|
92
|
+
}
|
|
93
|
+
if (DATA.test(texto)) marca(inicio + 1, 'data de mudança no comentário — o git já registra isso');
|
|
94
|
+
for (let k = 0; k < conteudo.length; k++) {
|
|
95
|
+
if (conteudo[k] && CODIGO_TOKEN.test(conteudo[k]) && CODIGO_FIM.test(conteudo[k])) {
|
|
96
|
+
marca(inicio + k + 1, 'código comentado — apague, o git guarda');
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let proxima = fim + 1;
|
|
102
|
+
while (proxima < linhas.length && !linhas[proxima].trim()) proxima++;
|
|
103
|
+
const ehDocstring =
|
|
104
|
+
/^(\/\*\*|"""|'''|\/\/\/)/.test(linhas[inicio].trim()) &&
|
|
105
|
+
proxima < linhas.length &&
|
|
106
|
+
ehComentario[proxima] === null;
|
|
107
|
+
const teto = ehDocstring ? MAX_DOCSTRING : MAX_BLOCO;
|
|
108
|
+
if (corpo.length > teto) {
|
|
109
|
+
marca(inicio + 1, `bloco de ${corpo.length} linhas (teto ${teto}) — mova o rationale para o commit ou docs/`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// `ehDocstring` exclui o caso do primeiro símbolo do arquivo já vir documentado:
|
|
113
|
+
// isso é docstring do símbolo, não cabeçalho.
|
|
114
|
+
if (primeiroCodigo !== -1 && inicio < primeiroCodigo && !ehDocstring && !CABECALHO_PERMITIDO.test(texto)) {
|
|
115
|
+
marca(inicio + 1, 'cabeçalho de arquivo — proibido; o nome do arquivo e os exports já dizem o que ele é');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const comentadas = ehComentario.filter((c) => c !== null).length;
|
|
120
|
+
const uteis = linhas.filter((l) => l.trim()).length;
|
|
121
|
+
return { achados, comentadas, uteis, densidade: uteis ? comentadas / uteis : 0 };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function elegivel(caminho) {
|
|
125
|
+
return EXTENSOES.has(extname(caminho)) && !IGNORAR.some((re) => re.test(caminho));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const raiz = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
129
|
+
const BASELINE = join(raiz, '.claude', 'hooks', 'comentarios', 'baseline.json');
|
|
130
|
+
|
|
131
|
+
function varrer(dir, saida = []) {
|
|
132
|
+
let itens;
|
|
133
|
+
try { itens = readdirSync(dir, { withFileTypes: true }); } catch { return saida; }
|
|
134
|
+
for (const item of itens) {
|
|
135
|
+
const cheio = join(dir, item.name);
|
|
136
|
+
if (item.isDirectory()) {
|
|
137
|
+
if (item.name === 'node_modules' || item.name === '.git') continue;
|
|
138
|
+
if (IGNORAR.some((re) => re.test(`${cheio}/`))) continue;
|
|
139
|
+
varrer(cheio, saida);
|
|
140
|
+
} else if (elegivel(cheio)) saida.push(cheio);
|
|
141
|
+
}
|
|
142
|
+
return saida;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (process.argv.includes('--gerar-baseline')) {
|
|
146
|
+
const mapa = {};
|
|
147
|
+
let arquivos = 0;
|
|
148
|
+
let violacoes = 0;
|
|
149
|
+
for (const f of varrer(raiz)) {
|
|
150
|
+
let fonte;
|
|
151
|
+
try { fonte = readFileSync(f, 'utf8'); } catch { continue; }
|
|
152
|
+
const { achados } = analisar(f, fonte);
|
|
153
|
+
if (achados.length > 0) { mapa[relative(raiz, f)] = achados.length; arquivos++; violacoes += achados.length; }
|
|
154
|
+
}
|
|
155
|
+
if (!existsSync(dirname(BASELINE))) mkdirSync(dirname(BASELINE), { recursive: true });
|
|
156
|
+
writeFileSync(BASELINE, `${JSON.stringify(mapa, null, 2)}\n`);
|
|
157
|
+
console.log(`[comentarios] baseline gravado: ${arquivos} arquivos legados, ${violacoes} violações toleradas.`);
|
|
158
|
+
console.log('[comentarios] arquivo novo responde ao padrão cheio; legado só não pode piorar.');
|
|
159
|
+
process.exit(0);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (process.env.COMENTARIOS_CHECK === 'off') process.exit(0);
|
|
163
|
+
|
|
164
|
+
let payload = null;
|
|
165
|
+
try { payload = JSON.parse(readFileSync(0, 'utf8')); } catch { process.exit(0); }
|
|
166
|
+
const caminho = payload?.tool_input?.file_path;
|
|
167
|
+
if (!caminho || !elegivel(caminho)) process.exit(0);
|
|
168
|
+
|
|
169
|
+
let fonte;
|
|
170
|
+
try { fonte = readFileSync(caminho, 'utf8'); } catch { process.exit(0); }
|
|
171
|
+
|
|
172
|
+
const { achados, comentadas, uteis, densidade } = analisar(caminho, fonte);
|
|
173
|
+
const chave = relative(raiz, caminho);
|
|
174
|
+
const rel = !chave || chave.startsWith('..') ? caminho : chave;
|
|
175
|
+
|
|
176
|
+
let baseline = null;
|
|
177
|
+
try { baseline = JSON.parse(readFileSync(BASELINE, 'utf8')); } catch {}
|
|
178
|
+
const tolerado = baseline?.[chave] ?? 0;
|
|
179
|
+
|
|
180
|
+
if (achados.length <= tolerado) {
|
|
181
|
+
if (baseline && tolerado > 0 && achados.length < tolerado) {
|
|
182
|
+
if (achados.length === 0) delete baseline[chave];
|
|
183
|
+
else baseline[chave] = achados.length;
|
|
184
|
+
try { writeFileSync(BASELINE, `${JSON.stringify(baseline, null, 2)}\n`); } catch {}
|
|
185
|
+
console.error(`[comentarios] ${rel}: ${tolerado} → ${achados.length} violações. Catraca desceu — não pode mais subir.`);
|
|
186
|
+
} else if (achados.length === 0 && densidade > DENSIDADE_ALVO) {
|
|
187
|
+
console.error(`[comentarios] aviso: ${rel} tem ${(densidade * 100).toFixed(0)}% de linhas de comentário (${comentadas}/${uteis}), alvo <= ${DENSIDADE_ALVO * 100}%.`);
|
|
188
|
+
}
|
|
189
|
+
process.exit(0);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const lista = achados.sort((a, b) => a.linha - b.linha).map((a) => ` ${rel}:${a.linha} ${a.msg}`);
|
|
193
|
+
const contexto =
|
|
194
|
+
tolerado > 0
|
|
195
|
+
? `Este arquivo é legado: a baseline tolera ${tolerado} violação(ões) e agora há ${achados.length}. Não é preciso limpar o arquivo inteiro — basta não piorá-lo.`
|
|
196
|
+
: 'Este arquivo não tem tolerância na baseline: vale o padrão cheio.';
|
|
197
|
+
|
|
198
|
+
console.error(
|
|
199
|
+
[
|
|
200
|
+
`[comentarios] ${rel} viola a rule .claude/rules/comentarios.md:`,
|
|
201
|
+
'',
|
|
202
|
+
...lista,
|
|
203
|
+
'',
|
|
204
|
+
contexto,
|
|
205
|
+
'Tetos: bloco interno <= 3 linhas, docstring pública <= 5, cabeçalho de arquivo proibido.',
|
|
206
|
+
'Task/plano/data/achado de review e rationale longo vão para a mensagem de commit ou docs/ — nunca para o código.',
|
|
207
|
+
'Corrija no próprio arquivo antes de continuar a task.',
|
|
208
|
+
].join('\n'),
|
|
209
|
+
);
|
|
210
|
+
process.exit(2);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"UserPromptSubmit": [
|
|
4
|
+
{
|
|
5
|
+
"hooks": [
|
|
6
|
+
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/templates/hooks/lembrete.mjs\"" }
|
|
7
|
+
]
|
|
8
|
+
}
|
|
9
|
+
],
|
|
10
|
+
"PreToolUse": [
|
|
11
|
+
{
|
|
12
|
+
"matcher": "Bash",
|
|
13
|
+
"hooks": [
|
|
14
|
+
{ "type": "command", "if": "Bash(git commit *)", "command": "node \"${CLAUDE_PLUGIN_ROOT}/templates/hooks/segredo-no-commit.mjs\"" }
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"matcher": "Edit|Write|MultiEdit",
|
|
19
|
+
"hooks": [
|
|
20
|
+
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/templates/hooks/proteger-contratos.mjs\"" }
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
"PostToolUse": [
|
|
25
|
+
{
|
|
26
|
+
"matcher": "Edit|Write|MultiEdit",
|
|
27
|
+
"hooks": [
|
|
28
|
+
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/templates/hooks/task-so-fecha-verde.mjs\"" },
|
|
29
|
+
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/templates/hooks/sem-jargao.mjs\"" }
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
// Hook UserPromptSubmit: quando o pedido parece bug, feature, segurança ou performance,
|
|
3
|
+
// lembra o modelo de seguir o fluxo do harness em vez de agir por conta própria.
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { runHook } from './lib/hook.mjs';
|
|
7
|
+
import { classifyPrompt } from './lib/temas.mjs';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {string} root
|
|
11
|
+
* @param {string} id
|
|
12
|
+
*/
|
|
13
|
+
function installed(root, id) {
|
|
14
|
+
return existsSync(join(root, '.claude', 'commands', `${id}.md`)) || existsSync(join(root, '.claude', 'skills', id, 'SKILL.md'));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
await runHook('lembrete', (input, { root }) => {
|
|
18
|
+
const match = classifyPrompt(String(input.prompt ?? ''));
|
|
19
|
+
if (!match) return null;
|
|
20
|
+
|
|
21
|
+
// No plugin os fluxos existem sempre, com prefixo; instalado pelo init, só sugere o que está no projeto.
|
|
22
|
+
const plugin = Boolean(process.env.CLAUDE_PLUGIN_ROOT);
|
|
23
|
+
if (!plugin && !installed(root, match.comando)) return null;
|
|
24
|
+
const command = plugin ? `/fontdata:${match.comando}` : `/${match.comando}`;
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
motivo: `Este pedido parece ${match.descricao}. Antes de agir por conta própria, siga o fluxo ${command} (${match.fluxo}).`,
|
|
28
|
+
};
|
|
29
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
// Modo de cada hook lido da config do projeto (.ai-harness/config.json).
|
|
3
|
+
// Sem dependências: roda também no plugin instalado a partir do GitHub.
|
|
4
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
/** @typedef {'aviso' | 'bloqueio' | 'desligado'} HookMode */
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Mesmos padrões de src/config/config.ts: segurança nasce em bloqueio, método em aviso.
|
|
11
|
+
* @type {Record<string, HookMode>}
|
|
12
|
+
*/
|
|
13
|
+
export const DEFAULT_HOOK_MODES = {
|
|
14
|
+
lembrete: 'aviso',
|
|
15
|
+
'segredo-no-commit': 'bloqueio',
|
|
16
|
+
'task-so-fecha-verde': 'aviso',
|
|
17
|
+
'proteger-contratos': 'aviso',
|
|
18
|
+
'sem-jargao': 'aviso',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** @type {HookMode[]} */
|
|
22
|
+
const MODES = ['aviso', 'bloqueio', 'desligado'];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Modo configurado para o hook; config ausente, ilegível ou com valor inválido cai no padrão.
|
|
26
|
+
* @param {string} root
|
|
27
|
+
* @param {string} hook
|
|
28
|
+
* @returns {HookMode}
|
|
29
|
+
*/
|
|
30
|
+
export function hookMode(root, hook) {
|
|
31
|
+
const fallback = DEFAULT_HOOK_MODES[hook] ?? 'aviso';
|
|
32
|
+
const file = join(root, '.ai-harness', 'config.json');
|
|
33
|
+
if (!existsSync(file)) return fallback;
|
|
34
|
+
try {
|
|
35
|
+
const mode = JSON.parse(readFileSync(file, 'utf8'))?.hooks?.[hook];
|
|
36
|
+
return MODES.includes(mode) ? mode : fallback;
|
|
37
|
+
} catch {
|
|
38
|
+
return fallback;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
// Esqueleto comum dos hooks do ai-harness: lê o evento do stdin, respeita o modo da config,
|
|
3
|
+
// responde no protocolo do Claude Code e registra no rastro. Sem dependências.
|
|
4
|
+
import { DEFAULT_HOOK_MODES, hookMode } from './config.mjs';
|
|
5
|
+
import { recordEvent } from './rastro.mjs';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {{ motivo: string }} Violation
|
|
9
|
+
* @typedef {{ root: string, mode: 'aviso' | 'bloqueio' }} HookContext
|
|
10
|
+
* @callback Handler
|
|
11
|
+
* @param {Record<string, any>} input
|
|
12
|
+
* @param {HookContext} context
|
|
13
|
+
* @returns {Promise<Violation | null> | Violation | null}
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
async function readStdin() {
|
|
17
|
+
let data = '';
|
|
18
|
+
for await (const chunk of process.stdin) data += chunk;
|
|
19
|
+
return data;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Resposta no formato de cada evento: em `PreToolUse` o bloqueio nega a permissão e o aviso só
|
|
24
|
+
* acrescenta contexto; nos demais eventos o bloqueio usa `decision: "block"`.
|
|
25
|
+
* @param {string} event
|
|
26
|
+
* @param {'aviso' | 'bloqueio'} mode
|
|
27
|
+
* @param {string} message
|
|
28
|
+
*/
|
|
29
|
+
function response(event, mode, message) {
|
|
30
|
+
if (event === 'PreToolUse') {
|
|
31
|
+
return mode === 'bloqueio'
|
|
32
|
+
? { hookSpecificOutput: { hookEventName: event, permissionDecision: 'deny', permissionDecisionReason: message } }
|
|
33
|
+
: { hookSpecificOutput: { hookEventName: event, additionalContext: message } };
|
|
34
|
+
}
|
|
35
|
+
if (mode === 'bloqueio') return { decision: 'block', reason: message };
|
|
36
|
+
return { hookSpecificOutput: { hookEventName: event, additionalContext: message } };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Executa um hook. O handler devolve `{ motivo }` quando há algo a avisar ou bloquear, ou null.
|
|
41
|
+
* Erros falham abertos (não atrapalham o usuário), exceto em hook de segurança no modo bloqueio.
|
|
42
|
+
* @param {string} name
|
|
43
|
+
* @param {Handler} handler
|
|
44
|
+
*/
|
|
45
|
+
export async function runHook(name, handler) {
|
|
46
|
+
/** @type {any} */
|
|
47
|
+
let input;
|
|
48
|
+
try {
|
|
49
|
+
input = JSON.parse(await readStdin());
|
|
50
|
+
} catch {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (typeof input !== 'object' || input === null) return;
|
|
54
|
+
|
|
55
|
+
const root = process.env.CLAUDE_PROJECT_DIR || input.cwd || process.cwd();
|
|
56
|
+
const mode = hookMode(root, name);
|
|
57
|
+
if (mode === 'desligado') return;
|
|
58
|
+
|
|
59
|
+
/** @type {Violation | null} */
|
|
60
|
+
let violation;
|
|
61
|
+
try {
|
|
62
|
+
violation = await handler(input, { root, mode });
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if (mode !== 'bloqueio' || DEFAULT_HOOK_MODES[name] !== 'bloqueio') return;
|
|
65
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
66
|
+
violation = { motivo: `o hook não conseguiu verificar (${reason}); por segurança, a ação foi bloqueada.` };
|
|
67
|
+
}
|
|
68
|
+
if (!violation) return;
|
|
69
|
+
|
|
70
|
+
const message = `[ai-harness · ${name}] ${violation.motivo}`;
|
|
71
|
+
process.stdout.write(JSON.stringify(response(String(input.hook_event_name ?? ''), mode, message)));
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
recordEvent(root, {
|
|
75
|
+
evento: mode === 'bloqueio' ? 'hook_bloqueio' : 'hook_aviso',
|
|
76
|
+
origem: 'hook',
|
|
77
|
+
nome: name,
|
|
78
|
+
modo: mode,
|
|
79
|
+
resultado: mode,
|
|
80
|
+
dados: { motivo: violation.motivo.slice(0, 1000) },
|
|
81
|
+
});
|
|
82
|
+
} catch {
|
|
83
|
+
// o rastro nunca impede a resposta do hook.
|
|
84
|
+
}
|
|
85
|
+
}
|