@payconductor/houndsman 0.0.0-stage → 0.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/LICENSE +21 -0
- package/README.md +195 -3
- package/assets/houndsman.png +0 -0
- package/package.json +63 -5
- package/src/cli/index.ts +210 -0
- package/src/core/assets.ts +20 -0
- package/src/core/brief.ts +162 -0
- package/src/core/commands/context.ts +39 -0
- package/src/core/commands/review.ts +50 -0
- package/src/core/commands/scaffold.ts +373 -0
- package/src/core/commands/setup.ts +75 -0
- package/src/core/commands/test.ts +20 -0
- package/src/core/commands/update.ts +30 -0
- package/src/core/commands/watchdog.ts +105 -0
- package/src/core/config-loader.ts +87 -0
- package/src/core/config.ts +59 -0
- package/src/core/doc-discoverer.ts +33 -0
- package/src/core/file-classifier.ts +247 -0
- package/src/core/jev/index.ts +12 -0
- package/src/core/jev/jev-candidates.ts +105 -0
- package/src/core/jev/jev-client.ts +99 -0
- package/src/core/jev/jev-detector.ts +203 -0
- package/src/core/jev/jev-questions.ts +92 -0
- package/src/core/jev/jev-types.ts +91 -0
- package/src/core/orchestrator.ts +444 -0
- package/src/core/paths.ts +30 -0
- package/src/core/pr-context.ts +271 -0
- package/src/core/registry-check.ts +145 -0
- package/src/core/report-builder.ts +357 -0
- package/src/core/types.ts +137 -0
- package/src/core/watchdog-registry.ts +43 -0
- package/src/index.ts +56 -0
- package/src/skill/AUDIT-PROTOCOL.md +96 -0
- package/src/skill/OUTPUT-FORMAT.md +70 -0
- package/src/skill/README.md +116 -0
- package/src/skill/SETUP.md +99 -0
- package/src/skill/example-review.md +58 -0
- package/src/skill/houndsman-update.md +251 -0
- package/src/skill/houndsman.md +239 -0
- package/src/tests/fixtures/fake-project.ts +64 -0
- package/src/tests/review-scripts.test.ts +851 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Houndsman contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,195 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# houndsman
|
|
2
|
+
|
|
3
|
+
Orquestrador de revisão de Pull Requests. Classifica o diff, despacha **watchdogs**
|
|
4
|
+
especializados por domínio (um subagente por watchdog) e consolida os achados em um
|
|
5
|
+
relatório único.
|
|
6
|
+
|
|
7
|
+
> Review de PR no piloto automático: alguém aprova no "LGTM" sem ler, um `catch {}` engole
|
|
8
|
+
> o erro e ninguém nota. Sexta às 18h a produção cobra a conta — e aí todo mundo lembra
|
|
9
|
+
> daquela linha.
|
|
10
|
+
|
|
11
|
+

|
|
12
|
+
|
|
13
|
+
**Terminologia:** **houndsman** = orquestrador/treinador (CLI + motor) · **watchdog** = cada
|
|
14
|
+
agente especialista por domínio que audita o diff · **review/revisão** = o processo ·
|
|
15
|
+
**Jev** = pré-análise opcional.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Como iniciar
|
|
20
|
+
|
|
21
|
+
Você monta o **seu time de cães de caça** (os watchdogs), cada um farejando um domínio
|
|
22
|
+
diferente das PRs. O motor só coordena a caçada — as regras são problema seu.
|
|
23
|
+
|
|
24
|
+
### 1. Pré-requisitos
|
|
25
|
+
|
|
26
|
+
- **Bun** — runtime obrigatório.
|
|
27
|
+
- **git** — o diff usa `git diff <base>...<head>`.
|
|
28
|
+
- Opcionais, ambos **falha-aberto**: **`gh`** (contexto da PR) e **`TYPESAFE_API_KEY`**
|
|
29
|
+
(pré-análise Jev). Sem eles, a revisão roda igual.
|
|
30
|
+
|
|
31
|
+
### 2. Instale o CLI (global)
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
bun add -g @payconductor/houndsman
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 3. Configure o projeto (uma vez)
|
|
38
|
+
|
|
39
|
+
Na raiz do projeto:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
bun run houndsman setup
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Cria a camada `.houndsman/`, que é **autocontida** (não instala nada no seu projeto):
|
|
46
|
+
|
|
47
|
+
| Caminho | Dono | O que é |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| `houndsman.config.ts` | você | Configuração do projeto (um objeto de dados) |
|
|
50
|
+
| `.houndsman/watchdogs/` | você | Suas regras — começa com **1 watchdog de exemplo** |
|
|
51
|
+
| `.houndsman/exclusions.ts` | você | Arquivos que nunca entram na revisão |
|
|
52
|
+
| `.houndsman/doc-mappings.ts` | você | path → documentação de contexto |
|
|
53
|
+
| `.houndsman/core/**` | auto | Tipos + docs do skill (sincronizados por `houndsman update`) |
|
|
54
|
+
| `.agents/` · `.claude/` | auto | Ponteiros de skill dos harnesses |
|
|
55
|
+
| `package.json` | — | Adiciona o script `"houndsman"` |
|
|
56
|
+
|
|
57
|
+
> **Não existe "modo completo".** O `setup` já cria a camada inicial com um watchdog de
|
|
58
|
+
> exemplo — troque o exemplo pelas regras reais do seu projeto (ou use `/houndsman-update`).
|
|
59
|
+
|
|
60
|
+
### 4. Escreva suas regras
|
|
61
|
+
|
|
62
|
+
Edite `.houndsman/watchdogs/` — veja [Escrevendo boas regras](#escrevendo-boas-regras).
|
|
63
|
+
|
|
64
|
+
### 5. Valide
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
bun run houndsman doctor
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 6. Rode a primeira revisão
|
|
71
|
+
|
|
72
|
+
No seu harness (Claude Code / agents), invoque o skill:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
/houndsman
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
O skill orquestra tudo e grava o relatório em `.houndsman/.runs/<branch-slug>/review.md`.
|
|
79
|
+
Não precisa rodar nenhum script à mão.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## O fluxo de trabalho
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
/houndsman
|
|
87
|
+
├─ Passo 0 contexto da PR (gh, opcional)
|
|
88
|
+
├─ Passo 1 classifica o diff → briefs por watchdog
|
|
89
|
+
├─ Passo 2 N subagentes revisam em paralelo
|
|
90
|
+
└─ Passo 3 consolida em review.md
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
- Revisão normal: só `/houndsman` (a branch é detectada; base default `main`).
|
|
94
|
+
- Revisar outra branch: `/houndsman feature/minha-branch`.
|
|
95
|
+
- Manual (sem skill): `houndsman context` → `houndsman --base main` → *(subagentes)* →
|
|
96
|
+
`houndsman --finalize`.
|
|
97
|
+
- CI: `houndsman --finalize --fail-on-blocking` (exit 1 se houver bloqueantes).
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Mantendo a configuração
|
|
102
|
+
|
|
103
|
+
- **Alterar regras** → use o skill **`/houndsman-update`**. Ele prova no código que o padrão
|
|
104
|
+
existe (evidência) antes de editar e roda as validações.
|
|
105
|
+
- **Atualizou a lib?** Sincronize os arquivos auto-gerados sem tocar nas suas regras:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
bun run houndsman update
|
|
109
|
+
bun run houndsman doctor
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
O `update` reescreve **só** `.houndsman/core/**` e `.houndsman/.gitignore`. Seus arquivos
|
|
113
|
+
(`watchdogs/`, `exclusions.ts`, `doc-mappings.ts`, `houndsman.config.ts`) nunca são tocados.
|
|
114
|
+
- **Recomeçar a camada do zero** → `houndsman setup --force` (sobrescreve só os arquivos do
|
|
115
|
+
usuário; sem `--force` o `setup` é idempotente).
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Avançado
|
|
120
|
+
|
|
121
|
+
### Comandos do dia a dia
|
|
122
|
+
|
|
123
|
+
| Comando | Função |
|
|
124
|
+
|---|---|
|
|
125
|
+
| `houndsman setup` | Cria a configuração inicial do projeto |
|
|
126
|
+
| `houndsman update` | Sincroniza os arquivos auto-gerados (`.houndsman/core`) |
|
|
127
|
+
| `houndsman doctor` | Valida a configuração e o ambiente (anti-drift) |
|
|
128
|
+
| `houndsman context` | Passo 0 — contexto da PR via `gh` |
|
|
129
|
+
| `houndsman --base main` | Passo 1 — prepara os briefs |
|
|
130
|
+
| `houndsman --finalize` | Passo 3 — gera o relatório |
|
|
131
|
+
| `houndsman watchdog <id>` | Contexto de um watchdog (`--rules`, `--json`) |
|
|
132
|
+
|
|
133
|
+
Flags: `--base <branch>` (default `main`) · `--head <ref>` (default `HEAD`) ·
|
|
134
|
+
`--branch <nome>` · `--dir <dir>` · `--config <path>` · `--clean` · `--no-jev` ·
|
|
135
|
+
`--fail-on-blocking` (com `--finalize`).
|
|
136
|
+
|
|
137
|
+
### Escrevendo boas regras
|
|
138
|
+
|
|
139
|
+
O texto da regra é renderizado **literalmente** no brief do subagente — ambiguidade vira
|
|
140
|
+
ruído na revisão. Toda regra começa com um **prefixo de polaridade**:
|
|
141
|
+
|
|
142
|
+
- `Promover:` → algo que **DEVE** acontecer (o padrão precisa existir no código).
|
|
143
|
+
- `Evitar:` → algo que **NÃO DEVE** acontecer. `Não` e `Nunca` no início são equivalentes.
|
|
144
|
+
|
|
145
|
+
Boas práticas:
|
|
146
|
+
|
|
147
|
+
1. **Só regra que o projeto aplica hoje** — prove com evidência (`arquivo:linha` para
|
|
148
|
+
afirmativas; `0 ocorrências via grep` do anti-padrão para negativas). Regra afirmativa sem
|
|
149
|
+
aplicação real gera falso positivo.
|
|
150
|
+
2. **Geral e reutilizável** — descreva um padrão do domínio, não uma regra de negócio
|
|
151
|
+
amarrada a uma única classe/use-case (isso é papel de teste automatizado).
|
|
152
|
+
3. **Condição inequívoca e critério concreto** — duas IAs chegam à mesma conclusão; use
|
|
153
|
+
limite/lista explícita ou símbolo real do código.
|
|
154
|
+
4. **Sem jargão vago nem qualificador vazio** — nada de "o antigo", "isso", "apenas por
|
|
155
|
+
compatibilidade".
|
|
156
|
+
5. **Declare o `classify`** do watchdog — regex de path (`filename`) ou de conteúdo
|
|
157
|
+
(`content`) para o motor achar os arquivos do domínio.
|
|
158
|
+
6. **Prefira editar via `/houndsman-update`** — ele aplica a rubrica completa e roda as
|
|
159
|
+
verificações (`doctor`, `test`).
|
|
160
|
+
|
|
161
|
+
> `houndsman doctor` emite um aviso (não bloqueante) para regra sem prefixo de polaridade.
|
|
162
|
+
|
|
163
|
+
Exemplo:
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
{ severity: "blocking", text: "Evitar: handler de webhook processa o payload antes de validar a assinatura do provider." }
|
|
167
|
+
{ severity: "attention", text: "Promover: rotas validam a entrada com o schema do domínio." }
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Estrutura gerada por `setup`
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
houndsman.config.ts
|
|
174
|
+
.houndsman/
|
|
175
|
+
├── .gitignore # AUTO
|
|
176
|
+
├── core/ # AUTO — sincronizado por `update`
|
|
177
|
+
│ ├── types.ts
|
|
178
|
+
│ └── skill/*.md
|
|
179
|
+
├── watchdogs/ # você — 1 módulo .ts por watchdog
|
|
180
|
+
│ └── index.ts
|
|
181
|
+
├── exclusions.ts # você
|
|
182
|
+
└── doc-mappings.ts # você
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Os artefatos de cada review ficam em `.houndsman/.runs/<branch-slug>/` (auto-ignorados).
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Documentação
|
|
190
|
+
|
|
191
|
+
- **`docs/CONTRIBUTING.md`** — arquitetura, fluxos internos, princípios e desenvolvimento da
|
|
192
|
+
lib (para quem vai contribuir).
|
|
193
|
+
- `src/skill/SETUP.md` — referência de configuração passo a passo.
|
|
194
|
+
- `src/skill/houndsman.md` — orquestração do skill `/houndsman`.
|
|
195
|
+
- `src/skill/houndsman-update.md` — manutenção das regras dos watchdogs.
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,64 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "@payconductor/houndsman",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Orquestrador de revisão de Pull Requests: classifica o diff, despacha watchdogs especializados por domínio e consolida o relatório.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"bin": {
|
|
8
|
+
"houndsman": "./src/cli/index.ts"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./src/index.ts"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"src",
|
|
15
|
+
"assets",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/payconductor-ai/houndsman.git"
|
|
22
|
+
},
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/payconductor-ai/houndsman/issues"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/payconductor-ai/houndsman#readme",
|
|
27
|
+
"author": "PayConductor",
|
|
28
|
+
"keywords": [
|
|
29
|
+
"pull-request",
|
|
30
|
+
"code-review",
|
|
31
|
+
"cli",
|
|
32
|
+
"bun",
|
|
33
|
+
"ai-agents",
|
|
34
|
+
"orchestrator"
|
|
35
|
+
],
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"bun": ">=1.1.0"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"houndsman": "bun run ./src/cli/index.ts",
|
|
44
|
+
"build": "bun build ./src/cli/index.ts --target bun --outdir dist",
|
|
45
|
+
"test": "bun test ./src/tests",
|
|
46
|
+
"typecheck": "tsc -b",
|
|
47
|
+
"lint": "biome check .",
|
|
48
|
+
"format": "biome format --write .",
|
|
49
|
+
"ci": "biome ci . && tsc -b",
|
|
50
|
+
"ci:check": "biome check",
|
|
51
|
+
"ci:check:fix": "biome check --fix",
|
|
52
|
+
"ci:fix": "bun run ci:check:fix && bun run ci:format:fix",
|
|
53
|
+
"ci:format": "biome format",
|
|
54
|
+
"ci:format:fix": "biome format --fix"
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"axios": "^1.12.2"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@biomejs/biome": "2.2.4",
|
|
61
|
+
"bun-types": "^1.3.14",
|
|
62
|
+
"typescript": "^7.0.2"
|
|
63
|
+
}
|
|
64
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Entrypoint único do `houndsman` — `houndsman [comando]`.
|
|
5
|
+
*
|
|
6
|
+
* Carrega a configuração do projeto (`HoundsmanConfig`) em runtime (a partir do
|
|
7
|
+
* cwd) e orquestra funções/classes de `core/`. Não importa nenhum caminho do
|
|
8
|
+
* projeto consumidor estaticamente.
|
|
9
|
+
*
|
|
10
|
+
* Comandos:
|
|
11
|
+
* (default) Passo 1 — preparar (classificação + briefs + manifesto)
|
|
12
|
+
* --finalize Passo 3 — consolidar os findings em review.md
|
|
13
|
+
* context Passo 0 — coletar o contexto da PR via `gh`
|
|
14
|
+
* setup Cria a configuração inicial do projeto (config + skills)
|
|
15
|
+
* update Sincroniza os arquivos auto-gerados (.houndsman/core)
|
|
16
|
+
* doctor Valida a configuração e o ambiente (anti-drift)
|
|
17
|
+
* test Roda a suíte de testes da lib
|
|
18
|
+
* watchdog <id> Contexto de um watchdog (`--rules`, `--json`)
|
|
19
|
+
*
|
|
20
|
+
* Flags: `--base <b>` · `--head <ref>` · `--branch <b>` · `--dir <d>` ·
|
|
21
|
+
* `--config <path>` · `--clean` · `--no-jev` · `--fail-on-blocking`.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { runContextCommand } from "../core/commands/context";
|
|
25
|
+
import { parseReviewArgs } from "../core/commands/review";
|
|
26
|
+
import { runSetupCommand } from "../core/commands/setup";
|
|
27
|
+
import { runLibTests } from "../core/commands/test";
|
|
28
|
+
import { runUpdateCommand } from "../core/commands/update";
|
|
29
|
+
import { renderWatchdogContext } from "../core/commands/watchdog";
|
|
30
|
+
import { type HoundsmanConfig, resolveArtifactDir } from "../core/config";
|
|
31
|
+
import { loadConfig } from "../core/config-loader";
|
|
32
|
+
import { finalize, prepare, resolveBranch } from "../core/orchestrator";
|
|
33
|
+
import { relFromRoot } from "../core/paths";
|
|
34
|
+
import { checkRegistry } from "../core/registry-check";
|
|
35
|
+
|
|
36
|
+
const HELP = `Uso: houndsman [comando] [flags]
|
|
37
|
+
|
|
38
|
+
Comandos:
|
|
39
|
+
(nenhum) Prepara a revisão (classification + briefs + manifesto)
|
|
40
|
+
--finalize Consolida os findings em review.md
|
|
41
|
+
context Coleta o contexto da PR via gh (Passo 0)
|
|
42
|
+
setup Cria a configuração inicial do projeto (config + skills)
|
|
43
|
+
update Sincroniza os arquivos auto-gerados (.houndsman/core)
|
|
44
|
+
doctor Valida a configuração e o ambiente (anti-drift)
|
|
45
|
+
test Roda a suíte de testes da lib
|
|
46
|
+
watchdog <id> Contexto de um watchdog (--rules, --json)
|
|
47
|
+
|
|
48
|
+
Flags:
|
|
49
|
+
--config <path> Caminho do houndsman.config.* (default: detecta no cwd)
|
|
50
|
+
--base <branch> Base do diff (default: main)
|
|
51
|
+
--head <ref> Head do diff (default: HEAD)
|
|
52
|
+
--branch <nome> Branch alvo (default: detecta do git)
|
|
53
|
+
--dir <dir> Diretório dos artefatos
|
|
54
|
+
--clean Descarta findings anteriores
|
|
55
|
+
--no-jev Desativa a pré-análise Jev
|
|
56
|
+
--fail-on-blocking Exit 1 se houver bloqueantes (com --finalize)`;
|
|
57
|
+
|
|
58
|
+
function readFlag(args: string[], flag: string): string | undefined {
|
|
59
|
+
const index = args.indexOf(flag);
|
|
60
|
+
return index >= 0 ? args[index + 1] : undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Passo 0 — contexto da PR via `gh`. */
|
|
64
|
+
function runContext(config: HoundsmanConfig, args: string[]): void {
|
|
65
|
+
const { paths, context } = runContextCommand(config, {
|
|
66
|
+
branch: readFlag(args, "--branch"),
|
|
67
|
+
dir: readFlag(args, "--dir"),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
if (context.available) {
|
|
71
|
+
const src = context.source;
|
|
72
|
+
const label = [src?.number ? `#${src.number}` : "", src?.title ?? ""]
|
|
73
|
+
.filter(Boolean)
|
|
74
|
+
.join(" ");
|
|
75
|
+
console.log(
|
|
76
|
+
`[houndsman] PR ${label} (${src?.head} → ${src?.base}), ${context.stats?.files ?? 0} arquivo(s).`,
|
|
77
|
+
);
|
|
78
|
+
console.log(`[houndsman] contexto: ${relFromRoot(paths.context)}`);
|
|
79
|
+
console.log(`[houndsman] brief: ${relFromRoot(paths.brief)}`);
|
|
80
|
+
console.log(
|
|
81
|
+
`[houndsman] próximo passo: subagente resume em ${relFromRoot(paths.summary)}`,
|
|
82
|
+
);
|
|
83
|
+
} else {
|
|
84
|
+
console.log(
|
|
85
|
+
`[houndsman] contexto indisponível (${context.reason}): ${context.detail}`,
|
|
86
|
+
);
|
|
87
|
+
console.log(`[houndsman] marcador: ${relFromRoot(paths.context)}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Valida a configuração do projeto e o ambiente (anti-drift). */
|
|
92
|
+
function runDoctor(config: HoundsmanConfig): void {
|
|
93
|
+
console.log("→ Validando configuração do projeto...");
|
|
94
|
+
const { problems, warnings, jevQuestions } = checkRegistry(config);
|
|
95
|
+
|
|
96
|
+
if (problems.length > 0) {
|
|
97
|
+
console.error("\n❌ Problemas na configuração de revisão:\n");
|
|
98
|
+
for (const problem of problems) console.error(` - ${problem}`);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
console.log(
|
|
102
|
+
` ✅ ${config.watchdogIds.length} watchdogs, ${jevQuestions} pergunta(s) Jev.`,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
if (warnings.length > 0) {
|
|
106
|
+
console.log(`\n⚠️ ${warnings.length} aviso(s) (não bloqueantes):`);
|
|
107
|
+
for (const warning of warnings) console.log(` - ${warning}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
console.log("\n→ Ambiente:");
|
|
111
|
+
console.log(` ${Bun.which("git") ? "✅" : "❌"} git`);
|
|
112
|
+
console.log(` ${Bun.which("gh") ? "✅" : "⚠️ "} gh (opcional — contexto da PR)`);
|
|
113
|
+
console.log(
|
|
114
|
+
` ${process.env.TYPESAFE_API_KEY ? "✅" : "⚠️ "} TYPESAFE_API_KEY (opcional — pré-análise Jev)`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Contexto de um watchdog. */
|
|
119
|
+
function runWatchdog(config: HoundsmanConfig, args: string[]): void {
|
|
120
|
+
const id = args.find((arg) => !arg.startsWith("-"));
|
|
121
|
+
|
|
122
|
+
if (!id || renderWatchdogContext(config, id) === null) {
|
|
123
|
+
console.error(
|
|
124
|
+
id
|
|
125
|
+
? `Watchdog '${id}' não existe.`
|
|
126
|
+
: "Informe o id do watchdog: houndsman watchdog <id>",
|
|
127
|
+
);
|
|
128
|
+
console.error(`Ids válidos: ${config.watchdogIds.join(", ")}`);
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
console.log(
|
|
133
|
+
renderWatchdogContext(config, id, {
|
|
134
|
+
withRules: args.includes("--rules"),
|
|
135
|
+
json: args.includes("--json"),
|
|
136
|
+
}),
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Passo 1/3 — preparar ou finalizar. */
|
|
141
|
+
async function runReview(config: HoundsmanConfig, args: string[]): Promise<void> {
|
|
142
|
+
const opts = parseReviewArgs(args, resolveArtifactDir(config));
|
|
143
|
+
const branch = resolveBranch(opts);
|
|
144
|
+
|
|
145
|
+
if (opts.finalize) {
|
|
146
|
+
const result = finalize(opts, branch, config);
|
|
147
|
+
if (opts.failOnBlocking && result.blockers > 0) process.exit(1);
|
|
148
|
+
} else {
|
|
149
|
+
await prepare(opts, branch, config);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Executa um subcomando. `--config` sobrepõe a descoberta automática.
|
|
155
|
+
* Comandos que não precisam de projeto (`test`, `setup`) são tratados antes do load.
|
|
156
|
+
*/
|
|
157
|
+
export async function main(): Promise<void> {
|
|
158
|
+
const argv = process.argv.slice(2);
|
|
159
|
+
const first = argv[0];
|
|
160
|
+
const command = first && !first.startsWith("-") ? first : undefined;
|
|
161
|
+
const rest = command ? argv.slice(1) : argv;
|
|
162
|
+
|
|
163
|
+
if (command === "test") {
|
|
164
|
+
process.exit(runLibTests());
|
|
165
|
+
}
|
|
166
|
+
if (command === "setup") {
|
|
167
|
+
await runSetupCommand({ cwd: process.cwd(), args: rest });
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (command === "update") {
|
|
171
|
+
await runUpdateCommand({ cwd: process.cwd(), args: rest });
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (command === "help") {
|
|
176
|
+
console.info(HELP);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const { config } = await loadConfig({
|
|
181
|
+
configPath: readFlag(argv, "--config"),
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
switch (command) {
|
|
185
|
+
case "context":
|
|
186
|
+
runContext(config, rest);
|
|
187
|
+
return;
|
|
188
|
+
case "doctor":
|
|
189
|
+
case "check":
|
|
190
|
+
runDoctor(config);
|
|
191
|
+
return;
|
|
192
|
+
case "watchdog":
|
|
193
|
+
runWatchdog(config, rest);
|
|
194
|
+
return;
|
|
195
|
+
case undefined:
|
|
196
|
+
await runReview(config, rest);
|
|
197
|
+
return;
|
|
198
|
+
default:
|
|
199
|
+
console.error(`Subcomando desconhecido: '${command}'.\n`);
|
|
200
|
+
console.error(HELP);
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (import.meta.main) {
|
|
206
|
+
await main().catch((error) => {
|
|
207
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
208
|
+
process.exit(1);
|
|
209
|
+
});
|
|
210
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolução dos assets embarcados no pacote (`src/skill/`) — biblioteca.
|
|
3
|
+
*
|
|
4
|
+
* Usados para **gerar** a camada de projeto (`houndsman setup`/`update`), não
|
|
5
|
+
* para resolver caminhos em runtime. Derivados da localização do módulo — não
|
|
6
|
+
* do cwd.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
|
|
11
|
+
/** Diretório de assets markdown embarcados no pacote. */
|
|
12
|
+
export const SKILL_DIR = join(import.meta.dir, "..", "skill");
|
|
13
|
+
|
|
14
|
+
/** Nome do arquivo de schema de output dos watchdogs. */
|
|
15
|
+
export const OUTPUT_FORMAT_FILE = "OUTPUT-FORMAT.md";
|
|
16
|
+
|
|
17
|
+
/** Caminho absoluto de um asset embarcado em `src/skill/`. */
|
|
18
|
+
export function assetPath(fileName: string): string {
|
|
19
|
+
return join(SKILL_DIR, fileName);
|
|
20
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renderização do brief de um watchdog (biblioteca) — `defaultRenderBrief`.
|
|
3
|
+
*
|
|
4
|
+
* O procedimento (`PROCEDURE`) é compartilhado por todos os watchdogs; o que era
|
|
5
|
+
* protocolo específico vive nas regras do watchdog (`rules`).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
type Rule,
|
|
10
|
+
SEVERITY_EMOJI,
|
|
11
|
+
severityRank,
|
|
12
|
+
type WatchdogDefinition,
|
|
13
|
+
type WatchdogRenderArgs,
|
|
14
|
+
type WatchdogRenderCandidate,
|
|
15
|
+
type WatchdogRenderContext,
|
|
16
|
+
} from "./types";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Procedimento de revisão — compartilhado por todos os watchdogs.
|
|
20
|
+
* Substitui o antigo protocolo genérico de 5 passos + o ponteiro `PROTOCOL_GLOB`.
|
|
21
|
+
*/
|
|
22
|
+
export const PROCEDURE: readonly string[] = [
|
|
23
|
+
"1. **FILTER** — descarte o que não for do seu domínio (ver *Prioridade de leitura* e *Não carregar*).",
|
|
24
|
+
"2. **READ HEAD** — leia os arquivos seletivamente (primeiras ~30 linhas). NUNCA o diff completo.",
|
|
25
|
+
"3. **GREP** — use as regras críticas/anti-padrões como heurística para localizar violações.",
|
|
26
|
+
"4. **DEEP-READ** — aprofunde apenas os arquivos com suspeita de violação.",
|
|
27
|
+
'5. **CONCLUDE** — grave o JSON no caminho de output, com `reviewedFiles` preenchido. Sem achados → `"findings": []` (arquivo obrigatório). Não escreva markdown livre.',
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
/** Bloco de contexto da PR, injetado no brief de cada watchdog. */
|
|
31
|
+
export function renderBriefContext(context: WatchdogRenderContext): string[] {
|
|
32
|
+
const lines = ["## Contexto da PR", ""];
|
|
33
|
+
if (context.objective) lines.push(`**Objetivo:** ${context.objective}`);
|
|
34
|
+
if (context.motivation) lines.push(`**Motivação:** ${context.motivation}`);
|
|
35
|
+
const changeSummary = context.changeSummary ?? [];
|
|
36
|
+
if (changeSummary.length > 0) {
|
|
37
|
+
lines.push("", "**Resumo das alterações:**", "");
|
|
38
|
+
for (const item of changeSummary) lines.push(`- ${item}`);
|
|
39
|
+
}
|
|
40
|
+
lines.push("");
|
|
41
|
+
return lines;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Seção de candidatos pré-analisados (Jev) — o watchdog confirma ou refuta. */
|
|
45
|
+
export function renderBriefCandidates(
|
|
46
|
+
candidates: readonly WatchdogRenderCandidate[],
|
|
47
|
+
): string[] {
|
|
48
|
+
const lines = [
|
|
49
|
+
"## Candidatos pré-analisados (Jev) — confirme ou refute",
|
|
50
|
+
"",
|
|
51
|
+
"> Indícios automáticos com confiança calibrada. **NÃO** são achados:",
|
|
52
|
+
"> confirme (virando finding, com a `rule` abaixo) ou ignore se for falso positivo.",
|
|
53
|
+
"",
|
|
54
|
+
];
|
|
55
|
+
for (const candidate of candidates) {
|
|
56
|
+
lines.push(
|
|
57
|
+
`- [ ] \`${candidate.file}\` · **${candidate.severity}** · confiança ${candidate.confidence.toFixed(2)}`,
|
|
58
|
+
` Regra: ${candidate.rule}`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
lines.push("");
|
|
62
|
+
return lines;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Linha de lista em code-span: `- \`valor\``. */
|
|
66
|
+
function codeItem(value: string): string {
|
|
67
|
+
return `- \`${value}\``;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Linha de regra com emoji de severidade. */
|
|
71
|
+
function ruleItem(rule: Rule): string {
|
|
72
|
+
return `- ${SEVERITY_EMOJI[rule.severity]} **${rule.severity}** — ${rule.text}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Seção opcional (`## <título>` + itens) — vazia se não houver itens. */
|
|
76
|
+
function optionalList(title: string, items: readonly string[] | undefined): string[] {
|
|
77
|
+
if (!items || items.length === 0) return [];
|
|
78
|
+
return [`## ${title}`, "", ...items.map((item) => `- ${item}`), ""];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Lista de arquivos do domínio com status. */
|
|
82
|
+
function fileItems(args: WatchdogRenderArgs): string[] {
|
|
83
|
+
return args.files.map((file) => {
|
|
84
|
+
const status = args.statuses[file]?.status ?? "modified";
|
|
85
|
+
return `- \`${file}\` (${status})`;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Seção de deletados (breaking change) — só p/ watchdogs com `breakingChange`. */
|
|
90
|
+
function deletedSection(args: WatchdogRenderArgs, def: WatchdogDefinition): string[] {
|
|
91
|
+
if (!def.breakingChange || args.deleted.length === 0) return [];
|
|
92
|
+
return [
|
|
93
|
+
"## Deletados — checar breaking change",
|
|
94
|
+
"",
|
|
95
|
+
...args.deleted.map(codeItem),
|
|
96
|
+
"",
|
|
97
|
+
];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Render padrão do brief de um watchdog (ordem estável).
|
|
102
|
+
* `def` é a definição do próprio watchdog (evita ciclo com o registry).
|
|
103
|
+
*/
|
|
104
|
+
export function defaultRenderBrief(
|
|
105
|
+
args: WatchdogRenderArgs,
|
|
106
|
+
def: WatchdogDefinition,
|
|
107
|
+
): string[] {
|
|
108
|
+
const lines: string[] = [
|
|
109
|
+
`# Brief — ${def.name} (\`${def.id}\`)`,
|
|
110
|
+
"",
|
|
111
|
+
`**Branch:** ${args.branch}`,
|
|
112
|
+
`**Output obrigatório:** \`${args.outputPath}\``,
|
|
113
|
+
`**Schema do output:** \`${args.outputFormatPath}\``,
|
|
114
|
+
"",
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
if (def.description) lines.push("## Objetivo", "", def.description, "");
|
|
118
|
+
lines.push("## Escopo", "", def.scope, "");
|
|
119
|
+
|
|
120
|
+
if (args.context) lines.push(...renderBriefContext(args.context));
|
|
121
|
+
|
|
122
|
+
lines.push(...optionalList("Prioridade de leitura", def.reviewFocus));
|
|
123
|
+
lines.push(
|
|
124
|
+
"## Regras (por severidade)",
|
|
125
|
+
"",
|
|
126
|
+
...[...def.rules]
|
|
127
|
+
.sort((a, b) => severityRank(a.severity) - severityRank(b.severity))
|
|
128
|
+
.map(ruleItem),
|
|
129
|
+
"",
|
|
130
|
+
);
|
|
131
|
+
lines.push(
|
|
132
|
+
"## Docs de contexto (carregar sob demanda)",
|
|
133
|
+
"",
|
|
134
|
+
...args.docs.map(codeItem),
|
|
135
|
+
"",
|
|
136
|
+
);
|
|
137
|
+
lines.push(
|
|
138
|
+
`## Arquivos do seu domínio (${args.files.length})`,
|
|
139
|
+
"",
|
|
140
|
+
...fileItems(args),
|
|
141
|
+
"",
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
if (args.candidates && args.candidates.length > 0) {
|
|
145
|
+
lines.push(...renderBriefCandidates(args.candidates));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
lines.push(...deletedSection(args, def));
|
|
149
|
+
lines.push(...optionalList("Não carregar", def.exclude));
|
|
150
|
+
lines.push("## Procedimento", "", ...PROCEDURE);
|
|
151
|
+
|
|
152
|
+
return lines;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Renderiza o brief via hook custom (`def.renderBrief`) ou o padrão.
|
|
157
|
+
* Passa a própria definição para que o hook possa reutilizar o render padrão.
|
|
158
|
+
*/
|
|
159
|
+
export function renderBrief(def: WatchdogDefinition, args: WatchdogRenderArgs): string {
|
|
160
|
+
const render = def.renderBrief ?? defaultRenderBrief;
|
|
161
|
+
return render(args, def).join("\n");
|
|
162
|
+
}
|