@payconductor/houndsman 0.1.1 → 0.1.3
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/README.md +32 -14
- package/package.json +1 -1
- package/src/cli/index.ts +22 -11
- package/src/core/assets.ts +3 -0
- package/src/core/brief.ts +2 -3
- package/src/core/commands/findings.ts +232 -4
- package/src/core/commands/review.ts +2 -0
- package/src/core/commands/scaffold.ts +13 -44
- package/src/core/diff-source.ts +4 -4
- package/src/core/findings-state.ts +20 -2
- package/src/core/fix-batch.ts +236 -0
- package/src/core/git-revision.ts +105 -0
- package/src/core/jev/index.ts +1 -0
- package/src/core/jev/jev-detector.ts +2 -2
- package/src/core/jev/jev-verify.ts +178 -0
- package/src/core/orchestrator.ts +202 -15
- package/src/core/paths.ts +6 -0
- package/src/core/report-builder.ts +5 -0
- package/src/core/run-status.ts +123 -0
- package/src/core/verify.ts +175 -0
- package/src/core/watchdog-registry.ts +1 -2
- package/src/index.ts +1 -1
- package/src/server/artifacts.ts +1 -0
- package/src/server/client/RunList.tsx +22 -13
- package/src/server/client/constants.ts +32 -1
- package/src/server/client/types.ts +4 -0
- package/src/server/index.ts +12 -1
- package/src/skill/AUDIT-PROTOCOL.md +2 -2
- package/src/skill/README.md +29 -5
- package/src/skill/SETUP.md +11 -1
- package/src/skill/SKILL.md +48 -0
- package/src/skill/fix.md +206 -0
- package/src/skill/{houndsman.md → hunt.md} +41 -68
- package/src/skill/{houndsman-update.md → train.md} +11 -11
- package/src/skill/verify.md +70 -0
- package/src/tests/findings-commands.test.ts +252 -2
- package/src/tests/findings-state.test.ts +52 -1
- package/src/tests/fix-batch.test.ts +257 -0
- package/src/tests/run-status.test.ts +147 -0
- package/src/tests/scaffold.test.ts +59 -0
- package/src/tests/server.test.ts +30 -0
- package/src/tests/verify.test.ts +170 -0
package/README.md
CHANGED
|
@@ -53,8 +53,8 @@ Cria a camada `.houndsman/`, que é **autocontida** (não instala nada no seu pr
|
|
|
53
53
|
| `.houndsman/doc-mappings.ts` | você | path → documentação de contexto |
|
|
54
54
|
| `.houndsman/core/**` | auto | Tipos + docs do skill (sincronizados por `houndsman update`) |
|
|
55
55
|
|
|
56
|
-
>
|
|
57
|
-
>
|
|
56
|
+
> O `setup` já cria a camada inicial com um watchdog de exemplo — troque o exemplo pelas
|
|
57
|
+
> regras reais do seu projeto (ou use `/houndsman train`).
|
|
58
58
|
|
|
59
59
|
### 4. Escreva suas regras
|
|
60
60
|
|
|
@@ -71,7 +71,7 @@ bun run houndsman doctor
|
|
|
71
71
|
No seu harness (Claude Code / agents), invoque o skill:
|
|
72
72
|
|
|
73
73
|
```
|
|
74
|
-
/houndsman
|
|
74
|
+
/houndsman hunt
|
|
75
75
|
```
|
|
76
76
|
|
|
77
77
|
O skill orquestra tudo e grava o relatório em `.houndsman/.runs/<branch-slug>/review.md`.
|
|
@@ -82,16 +82,26 @@ Não precisa rodar nenhum script à mão.
|
|
|
82
82
|
## O fluxo de trabalho
|
|
83
83
|
|
|
84
84
|
```
|
|
85
|
-
/houndsman
|
|
85
|
+
/houndsman hunt
|
|
86
86
|
├─ Passo 0 contexto da PR (gh, opcional)
|
|
87
|
-
├─ Passo 1 classifica o diff → briefs por watchdog
|
|
87
|
+
├─ Passo 1 classifica o diff → briefs por watchdog (incremental quando possível)
|
|
88
88
|
├─ Passo 2 N subagentes revisam em paralelo
|
|
89
89
|
├─ Passo 3 consolida em review.md + state.json
|
|
90
|
-
|
|
90
|
+
├─ Passo 4 valida na UI (server) e marca os achados a executar
|
|
91
|
+
├─ Passo 5 fix corrige o grupo atual → run.json (fix_done)
|
|
92
|
+
└─ Passo 6 verify confere o que resolveu, sem re-revisar
|
|
91
93
|
```
|
|
92
94
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
+
O skill tem **4 capabilities**, todas em `/houndsman`:
|
|
96
|
+
|
|
97
|
+
- **`hunt`** — revisa a PR (default: `/houndsman` = `/houndsman hunt`).
|
|
98
|
+
- **`train`** — mantém/atualiza as regras dos watchdogs.
|
|
99
|
+
- **`fix`** — corrige os findings marcados (`/houndsman fix`).
|
|
100
|
+
- **`verify`** — confere se as violações foram resolvidas (`/houndsman verify`), sem re-revisar.
|
|
101
|
+
|
|
102
|
+
- Revisão normal: `/houndsman` ou `/houndsman hunt` (a branch é detectada; base default `main`).
|
|
103
|
+
- Revisar outra branch: `/houndsman hunt feature/minha-branch`.
|
|
104
|
+
- Validar e triar na UI: `houndsman server --open` — marque os achados a executar (`execute`).
|
|
95
105
|
- Manual (sem skill): `houndsman context` → `houndsman --base main` → *(subagentes)* →
|
|
96
106
|
`houndsman --finalize`.
|
|
97
107
|
- CI: `houndsman --finalize --fail-on-blocking` (exit 1 se houver bloqueantes).
|
|
@@ -114,11 +124,17 @@ bun run houndsman server --open # UI: triar, comentar, marcar feito, ler art
|
|
|
114
124
|
**Auto-fix pelo agente** — as linhas mudam depois do fix, e o motor reancora por conteúdo:
|
|
115
125
|
|
|
116
126
|
```bash
|
|
117
|
-
bun run houndsman findings
|
|
118
|
-
|
|
127
|
+
bun run houndsman findings batch --json # grupo atual (agrupamento automático) + outros
|
|
128
|
+
# ... o agente corrige apenas esse grupo ...
|
|
129
|
+
bun run houndsman findings sync # reancora por conteúdo
|
|
119
130
|
bun run houndsman findings done <id>
|
|
131
|
+
bun run houndsman findings verify # confere o que resolveu (barato, sem re-revisar)
|
|
120
132
|
```
|
|
121
133
|
|
|
134
|
+
> A partir da 2ª revisão o `hunt` é **incremental**: revisa só os arquivos que mudaram desde
|
|
135
|
+
> o último `--finalize` e **carrega** os findings dos arquivos inalterados. `--full` força
|
|
136
|
+
> o review completo.
|
|
137
|
+
|
|
122
138
|
> A identidade do finding é o **conteúdo** (fingerprint), não a linha: itens deslocados
|
|
123
139
|
> pelo fix não se perdem. O estado é local e mora em `.runs/<slug>/state.json` (gitignored).
|
|
124
140
|
|
|
@@ -126,8 +142,8 @@ bun run houndsman findings done <id>
|
|
|
126
142
|
|
|
127
143
|
## Mantendo a configuração
|
|
128
144
|
|
|
129
|
-
- **Alterar regras** → use
|
|
130
|
-
existe (evidência) antes de editar e roda as validações.
|
|
145
|
+
- **Alterar regras** → use a capability **`train`** (`/houndsman train`). Ela prova no código
|
|
146
|
+
que o padrão existe (evidência) antes de editar e roda as validações.
|
|
131
147
|
- **Atualizou a lib?** Sincronize os arquivos auto-gerados sem tocar nas suas regras:
|
|
132
148
|
|
|
133
149
|
```bash
|
|
@@ -152,5 +168,7 @@ bun run houndsman findings done <id>
|
|
|
152
168
|
lib (para quem vai contribuir).
|
|
153
169
|
|
|
154
170
|
- `src/skill/SETUP.md` — referência de configuração passo a passo.
|
|
155
|
-
- `src/skill/
|
|
156
|
-
- `src/skill/
|
|
171
|
+
- `src/skill/SKILL.md` — entry point do skill `/houndsman` (capabilities).
|
|
172
|
+
- `src/skill/hunt.md` — capability `hunt` (revisão de PR).
|
|
173
|
+
- `src/skill/train.md` — capability `train` (manutenção das regras).
|
|
174
|
+
- `src/skill/fix.md` — capability `fix` (correção dos findings).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@payconductor/houndsman",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Orquestrador de revisão de Pull Requests: classifica o diff, despacha watchdogs especializados por domínio e consolida o relatório.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/cli/index.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* (default) Passo 1 — preparar (classificação + briefs + manifesto)
|
|
12
12
|
* --finalize Passo 3 — consolidar os findings em review.md
|
|
13
13
|
* context Passo 0 — coletar o contexto da PR via `gh`
|
|
14
|
-
* findings <sub> Triagem dos findings (list/set/done/sync/...)
|
|
14
|
+
* findings <sub> Triagem/correção dos findings (list/set/done/sync/batch/verify/...)
|
|
15
15
|
* server Sobe a UI local de triagem (Bun.serve)
|
|
16
16
|
* setup Cria a configuração inicial do projeto (config + skills)
|
|
17
17
|
* update Sincroniza os arquivos auto-gerados (.houndsman/core)
|
|
@@ -20,11 +20,11 @@
|
|
|
20
20
|
* watchdog <id> Contexto de um watchdog (`--rules`, `--json`)
|
|
21
21
|
*
|
|
22
22
|
* Flags: `--base <b>` · `--head <ref>` · `--branch <b>` · `--dir <d>` ·
|
|
23
|
-
* `--config <path>` · `--clean` · `--no-jev` · `--fail-on-blocking`.
|
|
23
|
+
* `--config <path>` · `--clean` · `--full` · `--no-jev` · `--fail-on-blocking`.
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
import { runContextCommand } from "../core/commands/context";
|
|
27
|
-
import { runFindingsCommand } from "../core/commands/findings";
|
|
27
|
+
import { runFindingsCommand, runVerifyCommand } from "../core/commands/findings";
|
|
28
28
|
import { parseReviewArgs } from "../core/commands/review";
|
|
29
29
|
import { runSetupCommand } from "../core/commands/setup";
|
|
30
30
|
import { runLibTests } from "../core/commands/test";
|
|
@@ -44,7 +44,7 @@ Comandos:
|
|
|
44
44
|
(nenhum) Prepara a revisão (classification + briefs + manifesto)
|
|
45
45
|
--finalize Consolida os findings em review.md
|
|
46
46
|
context Coleta o contexto da PR via gh (Passo 0)
|
|
47
|
-
findings <sub> Triagem dos findings (list · show · set · done · reset · sync · summary · path)
|
|
47
|
+
findings <sub> Triagem/correção dos findings (list · show · set · done · reset · sync · summary · batch · verify · path)
|
|
48
48
|
server Sobe a UI local de triagem (--port, --host, --open)
|
|
49
49
|
setup Cria a configuração inicial do projeto (config + skills)
|
|
50
50
|
update Sincroniza os arquivos auto-gerados (.houndsman/core)
|
|
@@ -59,6 +59,7 @@ Flags:
|
|
|
59
59
|
--branch <nome> Branch alvo (default: detecta do git)
|
|
60
60
|
--dir <dir> Diretório dos artefatos
|
|
61
61
|
--clean Descarta findings anteriores
|
|
62
|
+
--full Força review completo (ignora o modo incremental)
|
|
62
63
|
--no-jev Desativa a pré-análise Jev
|
|
63
64
|
--fail-on-blocking Exit 1 se houver bloqueantes (com --finalize)
|
|
64
65
|
|
|
@@ -67,11 +68,16 @@ Flags de findings:
|
|
|
67
68
|
--exec <e> pending | in_progress | done | stale
|
|
68
69
|
--comment <text> Comentário/instrução adicional
|
|
69
70
|
--watchdog <id> Filtra por watchdog (list)
|
|
70
|
-
--severity <s> Filtra por severidade (list)
|
|
71
|
+
--severity <s> Filtra por severidade (list, batch)
|
|
71
72
|
--file <text> Filtra por trecho do caminho (list)
|
|
72
73
|
--all Inclui findings ausentes do último relatório
|
|
73
74
|
--json Saída JSON
|
|
74
|
-
--limit <n> Limita o nº de itens (list)
|
|
75
|
+
--limit <n> Limita o nº de itens (list)
|
|
76
|
+
--group <key> Grupo alvo no batch (default: o primeiro)
|
|
77
|
+
--offset <n> Paginação dentro do grupo (batch)
|
|
78
|
+
--include-done Inclui já concluídos no batch
|
|
79
|
+
--apply (verify) Marca os resolvidos como done
|
|
80
|
+
--fail-on-persisting (verify) Exit 1 se algum blocking persistir`;
|
|
75
81
|
|
|
76
82
|
function readFlag(args: string[], flag: string): string | undefined {
|
|
77
83
|
const index = args.indexOf(flag);
|
|
@@ -174,11 +180,16 @@ function runWatchdog(config: HoundsmanConfig, args: string[]): void {
|
|
|
174
180
|
}
|
|
175
181
|
|
|
176
182
|
/**
|
|
177
|
-
* Subcomando `findings` — triagem dos findings (list/show/set/done/reset/
|
|
178
|
-
* sync/summary/path). Deixa o agente interagir com o estado sem abrir
|
|
183
|
+
* Subcomando `findings` — triagem/correção dos findings (list/show/set/done/reset/
|
|
184
|
+
* sync/summary/batch/verify/path). Deixa o agente interagir com o estado sem abrir
|
|
185
|
+
* o JSON. O `verify` é assíncrono (consulta opcional ao Jev) — roteado à parte.
|
|
179
186
|
*/
|
|
180
|
-
function runFindings(config: HoundsmanConfig, args: string[]): void {
|
|
181
|
-
const
|
|
187
|
+
async function runFindings(config: HoundsmanConfig, args: string[]): Promise<void> {
|
|
188
|
+
const sub = args.find((arg) => !arg.startsWith("-"));
|
|
189
|
+
const result =
|
|
190
|
+
sub === "verify"
|
|
191
|
+
? await runVerifyCommand(config, args)
|
|
192
|
+
: runFindingsCommand(config, args);
|
|
182
193
|
if (result.exitCode !== 0) {
|
|
183
194
|
console.error(result.output);
|
|
184
195
|
process.exit(result.exitCode);
|
|
@@ -267,7 +278,7 @@ export async function main(): Promise<void> {
|
|
|
267
278
|
runWatchdog(config, rest);
|
|
268
279
|
return;
|
|
269
280
|
case "findings":
|
|
270
|
-
runFindings(config, rest);
|
|
281
|
+
await runFindings(config, rest);
|
|
271
282
|
return;
|
|
272
283
|
case "server":
|
|
273
284
|
runServer(config, rest);
|
package/src/core/assets.ts
CHANGED
|
@@ -17,6 +17,9 @@ export const MEDIA_DIR = join(import.meta.dir, "..", "..", "assets");
|
|
|
17
17
|
/** Nome do arquivo de schema de output dos watchdogs. */
|
|
18
18
|
export const OUTPUT_FORMAT_FILE = "OUTPUT-FORMAT.md";
|
|
19
19
|
|
|
20
|
+
/** Nome do arquivo-base do skill (`SKILL.md`) — entry point escrito para cada harness. */
|
|
21
|
+
export const SKILL_FILE = "SKILL.md";
|
|
22
|
+
|
|
20
23
|
/** Caminho absoluto de um asset embarcado em `src/skill/`. */
|
|
21
24
|
export function assetPath(fileName: string): string {
|
|
22
25
|
return join(SKILL_DIR, fileName);
|
package/src/core/brief.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Renderização do brief de um watchdog (biblioteca) — `defaultRenderBrief`.
|
|
3
3
|
*
|
|
4
|
-
* O procedimento (`PROCEDURE`) é compartilhado por todos os watchdogs; o que
|
|
5
|
-
*
|
|
4
|
+
* O procedimento (`PROCEDURE`) é compartilhado por todos os watchdogs; o que é
|
|
5
|
+
* específico do domínio vive nas regras do watchdog (`rules`).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import {
|
|
@@ -17,7 +17,6 @@ import {
|
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
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
20
|
*/
|
|
22
21
|
export const PROCEDURE: readonly string[] = [
|
|
23
22
|
"1. **FILTER** — descarte o que não for do seu domínio (ver *Prioridade de leitura* e *Não carregar*).",
|
|
@@ -3,13 +3,19 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Lê/grava o `state.json` da run (`findings-state.ts`). Devolve o texto pronto
|
|
5
5
|
* (markdown ou JSON) + exit code; o CLI decide o log/exit. O agente do usuário
|
|
6
|
-
* usa `list`/`set`/`done` para interagir sem abrir o JSON à mão.
|
|
6
|
+
* usa `list`/`set`/`done` (e `batch` para lotes de correção) para interagir sem abrir o JSON à mão.
|
|
7
7
|
*
|
|
8
8
|
* Se o `state.json` não existir, é re-construído a partir de
|
|
9
9
|
* `findings/*.json` + `classification.json` (não exige `--finalize`).
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import { writeFileSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import {
|
|
15
|
+
type HoundsmanConfig,
|
|
16
|
+
resolveArtifactDir,
|
|
17
|
+
resolveJevConcurrency,
|
|
18
|
+
} from "../config";
|
|
13
19
|
import {
|
|
14
20
|
applyFindingUpdate,
|
|
15
21
|
buildStateFromRunDir,
|
|
@@ -29,9 +35,22 @@ import {
|
|
|
29
35
|
summarize,
|
|
30
36
|
syncState,
|
|
31
37
|
} from "../findings-state";
|
|
32
|
-
import {
|
|
38
|
+
import { buildFixBatch, type FixBatchResult } from "../fix-batch";
|
|
39
|
+
import { JevCandidatesStore, JevClient, JevVerifier } from "../jev";
|
|
40
|
+
import { relFromRoot, VERIFY_FILE } from "../paths";
|
|
33
41
|
import { currentBranch } from "../pr-context";
|
|
42
|
+
import { pingFindingsStatus } from "../run-status";
|
|
34
43
|
import { SEVERITY_EMOJI, SEVERITY_ORDER, type Severity } from "../types";
|
|
44
|
+
import {
|
|
45
|
+
applyJev,
|
|
46
|
+
applyResolutions,
|
|
47
|
+
selectVerifiable,
|
|
48
|
+
summarizeVerify,
|
|
49
|
+
type VerifyEntry,
|
|
50
|
+
type VerifyStatus,
|
|
51
|
+
type VerifySummary,
|
|
52
|
+
verifyLocal,
|
|
53
|
+
} from "../verify";
|
|
35
54
|
|
|
36
55
|
export type FindingsCommandResult = {
|
|
37
56
|
output: string;
|
|
@@ -49,6 +68,8 @@ const VALUE_FLAGS = new Set([
|
|
|
49
68
|
"--limit",
|
|
50
69
|
"--comment",
|
|
51
70
|
"--by",
|
|
71
|
+
"--offset",
|
|
72
|
+
"--group",
|
|
52
73
|
]);
|
|
53
74
|
|
|
54
75
|
type ParsedArgs = {
|
|
@@ -161,6 +182,42 @@ function renderSummary(state: FindingsStateFile, summary: FindingsSummary): stri
|
|
|
161
182
|
].join("\n");
|
|
162
183
|
}
|
|
163
184
|
|
|
185
|
+
/** Mix de severidade compacto (ex.: `❌1 ⚠️2`). */
|
|
186
|
+
function severityBreakdown(bySeverity: Record<Severity, number>): string {
|
|
187
|
+
return SEVERITY_ORDER.filter((severity) => bySeverity[severity] > 0)
|
|
188
|
+
.map((severity) => `${SEVERITY_EMOJI[severity]}${bySeverity[severity]}`)
|
|
189
|
+
.join(" ");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Render do lote de correção: grupo atual paginado + outros grupos. */
|
|
193
|
+
function renderBatch(result: FixBatchResult): string {
|
|
194
|
+
const { dimension, current, others, totals } = result;
|
|
195
|
+
if (totals.queue === 0) return "Nenhum finding pendente de correção.";
|
|
196
|
+
|
|
197
|
+
const lines = [
|
|
198
|
+
`# Lote de correção — agrupado por ${dimension} (${totals.groups} grupo(s))`,
|
|
199
|
+
"",
|
|
200
|
+
`## Grupo atual — ${current.key} (${current.total} finding(s), mostrando ${current.returned})`,
|
|
201
|
+
];
|
|
202
|
+
for (const finding of current.findings) lines.push(...renderFindingLine(finding));
|
|
203
|
+
if (current.remaining > 0) {
|
|
204
|
+
lines.push("", `_… +${current.remaining} neste grupo (use \`--offset\`)._`);
|
|
205
|
+
}
|
|
206
|
+
if (others.length > 0) {
|
|
207
|
+
lines.push("", "## Outros grupos disponíveis");
|
|
208
|
+
for (const group of others) {
|
|
209
|
+
lines.push(
|
|
210
|
+
`- ${group.key} — ${group.count} (${severityBreakdown(group.bySeverity)})`,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
lines.push(
|
|
215
|
+
"",
|
|
216
|
+
`fila: ${totals.queue} · retornados: ${totals.returned} · grupos: ${totals.groups}`,
|
|
217
|
+
);
|
|
218
|
+
return lines.join("\n");
|
|
219
|
+
}
|
|
220
|
+
|
|
164
221
|
// ---------------------------------------------------------------------------
|
|
165
222
|
// Execução
|
|
166
223
|
// ---------------------------------------------------------------------------
|
|
@@ -197,6 +254,13 @@ function filterFrom(parsed: ParsedArgs): FindingsFilter {
|
|
|
197
254
|
};
|
|
198
255
|
}
|
|
199
256
|
|
|
257
|
+
/** Faz ping no `run.json` (status derivado do estado atual) após uma mutação. */
|
|
258
|
+
function pingStatus(config: HoundsmanConfig, parsed: ParsedArgs): void {
|
|
259
|
+
const { dir, branch } = resolveDir(config, parsed);
|
|
260
|
+
const state = FindingsStateStore.at(dir, branch).read();
|
|
261
|
+
if (state) pingFindingsStatus(runDir(dir, branch), state);
|
|
262
|
+
}
|
|
263
|
+
|
|
200
264
|
/** Aplica uma atualização a um finding e persiste. */
|
|
201
265
|
function updateOne(
|
|
202
266
|
config: HoundsmanConfig,
|
|
@@ -294,6 +358,7 @@ function execute(config: HoundsmanConfig, parsed: ParsedArgs): FindingsCommandRe
|
|
|
294
358
|
);
|
|
295
359
|
}
|
|
296
360
|
const finding = updateOne(config, parsed, id, update);
|
|
361
|
+
pingStatus(config, parsed);
|
|
297
362
|
return {
|
|
298
363
|
output: parsed.bools.has("--json")
|
|
299
364
|
? JSON.stringify(finding, null, 2)
|
|
@@ -304,6 +369,7 @@ function execute(config: HoundsmanConfig, parsed: ParsedArgs): FindingsCommandRe
|
|
|
304
369
|
case "done": {
|
|
305
370
|
const ids = requireIds(parsed, "done");
|
|
306
371
|
const updated = updateMany(config, parsed, ids, { execState: "done" });
|
|
372
|
+
pingStatus(config, parsed);
|
|
307
373
|
return {
|
|
308
374
|
output: `Marcados como done: ${updated.map((f) => f.id).join(", ")}.`,
|
|
309
375
|
exitCode: 0,
|
|
@@ -316,6 +382,7 @@ function execute(config: HoundsmanConfig, parsed: ParsedArgs): FindingsCommandRe
|
|
|
316
382
|
execState: "pending",
|
|
317
383
|
comment: "",
|
|
318
384
|
});
|
|
385
|
+
pingStatus(config, parsed);
|
|
319
386
|
return {
|
|
320
387
|
output: `Triagem reiniciada: ${updated.map((f) => f.id).join(", ")}.`,
|
|
321
388
|
exitCode: 0,
|
|
@@ -327,6 +394,7 @@ function execute(config: HoundsmanConfig, parsed: ParsedArgs): FindingsCommandRe
|
|
|
327
394
|
const state = loadState(config, parsed);
|
|
328
395
|
const result = syncState(state, fileReader(process.cwd()));
|
|
329
396
|
store.write(state);
|
|
397
|
+
pingStatus(config, parsed);
|
|
330
398
|
const output = parsed.bools.has("--json")
|
|
331
399
|
? JSON.stringify(result, null, 2)
|
|
332
400
|
: [
|
|
@@ -357,9 +425,26 @@ function execute(config: HoundsmanConfig, parsed: ParsedArgs): FindingsCommandRe
|
|
|
357
425
|
const path = FindingsStateStore.at(dir, branch).filePath;
|
|
358
426
|
return { output: relFromRoot(path), exitCode: 0 };
|
|
359
427
|
}
|
|
428
|
+
case "batch": {
|
|
429
|
+
const state = loadState(config, parsed);
|
|
430
|
+
const severity = parsed.values["--severity"];
|
|
431
|
+
const offset = Number(parsed.values["--offset"]);
|
|
432
|
+
const result = buildFixBatch(state, {
|
|
433
|
+
severity: severity ? (severity as Severity) : undefined,
|
|
434
|
+
includeDone: parsed.bools.has("--include-done"),
|
|
435
|
+
offset: Number.isFinite(offset) ? offset : undefined,
|
|
436
|
+
group: parsed.values["--group"],
|
|
437
|
+
});
|
|
438
|
+
return {
|
|
439
|
+
output: parsed.bools.has("--json")
|
|
440
|
+
? JSON.stringify(result, null, 2)
|
|
441
|
+
: renderBatch(result),
|
|
442
|
+
exitCode: 0,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
360
445
|
default:
|
|
361
446
|
return {
|
|
362
|
-
output: `Subcomando '${parsed.sub}' desconhecido. Use: list, show, set, done, reset, sync, summary, path.`,
|
|
447
|
+
output: `Subcomando '${parsed.sub}' desconhecido. Use: list, show, set, done, reset, sync, summary, batch, path.`,
|
|
363
448
|
exitCode: 1,
|
|
364
449
|
};
|
|
365
450
|
}
|
|
@@ -388,3 +473,146 @@ function requireIds(parsed: ParsedArgs, sub: string): string[] {
|
|
|
388
473
|
}
|
|
389
474
|
return parsed.positionals;
|
|
390
475
|
}
|
|
476
|
+
|
|
477
|
+
// ---------------------------------------------------------------------------
|
|
478
|
+
// verify — as violações foram resolvidas? (sem re-revisar a PR)
|
|
479
|
+
// ---------------------------------------------------------------------------
|
|
480
|
+
|
|
481
|
+
/** Rótulo curto de cada status de verificação. */
|
|
482
|
+
const VERIFY_LABEL: Record<VerifyStatus, string> = {
|
|
483
|
+
resolved: "✅ resolvido",
|
|
484
|
+
persisting: "❌ persiste",
|
|
485
|
+
uncertain: "⚠️ incerto",
|
|
486
|
+
"file-missing": "🗑️ arquivo ausente",
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
/** Render da verificação: contagem por status + uma linha por finding. */
|
|
490
|
+
function renderVerify(
|
|
491
|
+
state: FindingsStateFile,
|
|
492
|
+
entries: VerifyEntry[],
|
|
493
|
+
summary: VerifySummary,
|
|
494
|
+
meta: { jevChecked: number; jevReason?: string },
|
|
495
|
+
): string {
|
|
496
|
+
if (entries.length === 0) return "Nenhum finding para verificar.";
|
|
497
|
+
const lines = [
|
|
498
|
+
`# Verificação — ${state.branch} (${entries.length})`,
|
|
499
|
+
"",
|
|
500
|
+
`Resolvidos: ${summary.resolved} · Persistem: ${summary.persisting} · Incertos: ${summary.uncertain} · Arquivo ausente: ${summary["file-missing"]}`,
|
|
501
|
+
];
|
|
502
|
+
if (meta.jevChecked > 0) lines.push(`Confirmados por Jev: ${meta.jevChecked}.`);
|
|
503
|
+
else if (meta.jevReason) lines.push(`Jev: pulado (${meta.jevReason}).`);
|
|
504
|
+
lines.push("");
|
|
505
|
+
for (const entry of entries) {
|
|
506
|
+
const location = entry.line ? `${entry.file}:${entry.line}` : entry.file;
|
|
507
|
+
const via =
|
|
508
|
+
entry.basis === "jev" ? ` · Jev ${(entry.confidence ?? 0).toFixed(2)}` : "";
|
|
509
|
+
lines.push(
|
|
510
|
+
`- \`${entry.id}\` ${VERIFY_LABEL[entry.status]} — **${entry.severity}** \`${location}\` — ${entry.rule}${via}`,
|
|
511
|
+
` ${entry.message}`,
|
|
512
|
+
);
|
|
513
|
+
if (entry.detail) lines.push(` _${entry.detail}_`);
|
|
514
|
+
}
|
|
515
|
+
return lines.join("\n");
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Subcomando `findings verify` — checa se as violações foram resolvidas, sem
|
|
520
|
+
* re-revisar a PR. Tier local (grátis) + confirmação Jev dos casos ambíguos
|
|
521
|
+
* (fail-open). `--apply` persiste os `resolved` como `done`.
|
|
522
|
+
*/
|
|
523
|
+
export async function runVerifyCommand(
|
|
524
|
+
config: HoundsmanConfig,
|
|
525
|
+
args: string[],
|
|
526
|
+
): Promise<FindingsCommandResult> {
|
|
527
|
+
const parsed = parseArgs(args);
|
|
528
|
+
try {
|
|
529
|
+
return await executeVerify(config, parsed);
|
|
530
|
+
} catch (error) {
|
|
531
|
+
return {
|
|
532
|
+
output: error instanceof Error ? error.message : String(error),
|
|
533
|
+
exitCode: 1,
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
async function executeVerify(
|
|
539
|
+
config: HoundsmanConfig,
|
|
540
|
+
parsed: ParsedArgs,
|
|
541
|
+
): Promise<FindingsCommandResult> {
|
|
542
|
+
const { dir, branch } = resolveDir(config, parsed);
|
|
543
|
+
const store = FindingsStateStore.at(dir, branch);
|
|
544
|
+
const state = loadState(config, parsed);
|
|
545
|
+
|
|
546
|
+
const findings = selectVerifiable(state, filterFrom(parsed));
|
|
547
|
+
const entries = verifyLocal(findings, fileReader(process.cwd()));
|
|
548
|
+
|
|
549
|
+
// Tier 1 — Jev só nos ambíguos (fail-open: sem chave/erro mantém o provisório).
|
|
550
|
+
let jevChecked = 0;
|
|
551
|
+
let jevReason: string | undefined;
|
|
552
|
+
const ambiguous = entries.filter((entry) => entry.ambiguous);
|
|
553
|
+
if (ambiguous.length > 0) {
|
|
554
|
+
const verifier = new JevVerifier(new JevClient());
|
|
555
|
+
if (parsed.bools.has("--no-jev")) {
|
|
556
|
+
jevReason = "desativado (--no-jev)";
|
|
557
|
+
} else if (!verifier.isConfigured()) {
|
|
558
|
+
jevReason = "TYPESAFE_API_KEY não configurada";
|
|
559
|
+
} else {
|
|
560
|
+
const byId = new Map(findings.map((finding) => [finding.id, finding]));
|
|
561
|
+
const outcome = await verifier.verify(
|
|
562
|
+
ambiguous.map((entry) => ({
|
|
563
|
+
id: entry.id,
|
|
564
|
+
file: entry.file,
|
|
565
|
+
rule: entry.rule,
|
|
566
|
+
foundAtSha: byId.get(entry.id)?.foundAtSha,
|
|
567
|
+
})),
|
|
568
|
+
{
|
|
569
|
+
headSha: state.reviewed?.headSha,
|
|
570
|
+
concurrency: resolveJevConcurrency(config),
|
|
571
|
+
},
|
|
572
|
+
);
|
|
573
|
+
applyJev(entries, outcome.confidence, JevCandidatesStore.THRESHOLD);
|
|
574
|
+
jevChecked = outcome.checked;
|
|
575
|
+
jevReason = outcome.reason;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const summary = summarizeVerify(entries);
|
|
580
|
+
const reportPath = join(runDir(dir, branch), VERIFY_FILE);
|
|
581
|
+
writeFileSync(
|
|
582
|
+
reportPath,
|
|
583
|
+
`${renderVerify(state, entries, summary, { jevChecked, jevReason })}\n`,
|
|
584
|
+
);
|
|
585
|
+
|
|
586
|
+
let applied: string[] = [];
|
|
587
|
+
if (parsed.bools.has("--apply")) {
|
|
588
|
+
store.update((current) => {
|
|
589
|
+
applied = applyResolutions(current, entries);
|
|
590
|
+
});
|
|
591
|
+
pingStatus(config, parsed);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const output = parsed.bools.has("--json")
|
|
595
|
+
? JSON.stringify(
|
|
596
|
+
{ branch, summary, jevChecked, jevReason, applied, entries },
|
|
597
|
+
null,
|
|
598
|
+
2,
|
|
599
|
+
)
|
|
600
|
+
: [
|
|
601
|
+
renderVerify(state, entries, summary, { jevChecked, jevReason }),
|
|
602
|
+
"",
|
|
603
|
+
`Relatório: ${relFromRoot(reportPath)}`,
|
|
604
|
+
parsed.bools.has("--apply")
|
|
605
|
+
? `Aplicados (done): ${applied.length > 0 ? applied.join(", ") : "—"}.`
|
|
606
|
+
: "",
|
|
607
|
+
]
|
|
608
|
+
.filter(Boolean)
|
|
609
|
+
.join("\n");
|
|
610
|
+
|
|
611
|
+
const failOnPersisting =
|
|
612
|
+
parsed.bools.has("--fail-on-persisting") &&
|
|
613
|
+
entries.some(
|
|
614
|
+
(entry) => entry.status === "persisting" && entry.severity === "blocking",
|
|
615
|
+
);
|
|
616
|
+
|
|
617
|
+
return { output, exitCode: failOnPersisting ? 1 : 0 };
|
|
618
|
+
}
|
|
@@ -14,6 +14,7 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
14
14
|
"--fail-on-blocking",
|
|
15
15
|
"--clean",
|
|
16
16
|
"--no-jev",
|
|
17
|
+
"--full",
|
|
17
18
|
]);
|
|
18
19
|
|
|
19
20
|
function applyValue(opts: ReviewOptions, flag: string, value: string | undefined): void {
|
|
@@ -29,6 +30,7 @@ function applyBoolean(opts: ReviewOptions, flag: string): void {
|
|
|
29
30
|
else if (flag === "--fail-on-blocking") opts.failOnBlocking = true;
|
|
30
31
|
else if (flag === "--clean") opts.clean = true;
|
|
31
32
|
else if (flag === "--no-jev") opts.noJev = true;
|
|
33
|
+
else if (flag === "--full") opts.full = true;
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
/** Converte argv (sem o subcomando) nas opções de prepare/finalize. */
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
17
17
|
import { basename, join } from "node:path";
|
|
18
|
-
import { SKILL_DIR } from "../assets";
|
|
18
|
+
import { SKILL_DIR, SKILL_FILE } from "../assets";
|
|
19
19
|
import {
|
|
20
20
|
ARTIFACT_DIR,
|
|
21
21
|
CORE_DIR,
|
|
@@ -279,25 +279,6 @@ core/
|
|
|
279
279
|
.runs/
|
|
280
280
|
`;
|
|
281
281
|
|
|
282
|
-
const SKILL_POINTER = (
|
|
283
|
-
name: string,
|
|
284
|
-
projectName: string,
|
|
285
|
-
canonical: string,
|
|
286
|
-
description: string,
|
|
287
|
-
argumentHint: string,
|
|
288
|
-
): string => `---
|
|
289
|
-
name: ${name}
|
|
290
|
-
description: '${description}'
|
|
291
|
-
argument-hint: '${argumentHint}'
|
|
292
|
-
---
|
|
293
|
-
|
|
294
|
-
# ${name} — ${projectName}
|
|
295
|
-
|
|
296
|
-
> **Ponteiro.** O conteúdo canônico está em \`${canonical}\`.
|
|
297
|
-
|
|
298
|
-
Leia e siga \`${canonical}\`.
|
|
299
|
-
`;
|
|
300
|
-
|
|
301
282
|
// ---------------------------------------------------------------------------
|
|
302
283
|
// Escrita
|
|
303
284
|
// ---------------------------------------------------------------------------
|
|
@@ -345,15 +326,17 @@ export function writeUserLayer(ctx: ScaffoldContext): void {
|
|
|
345
326
|
|
|
346
327
|
/**
|
|
347
328
|
* Arquivos auto-gerados: tipos, docs do skill, `.gitignore` (ignora `core/` e
|
|
348
|
-
* `.runs/`) e
|
|
349
|
-
*
|
|
329
|
+
* `.runs/`) e o `SKILL.md` de cada harness. Sempre sobrescritos (é o alvo do
|
|
330
|
+
* `update`).
|
|
350
331
|
*/
|
|
351
332
|
export function syncGeneratedFiles(ctx: ScaffoldContext): void {
|
|
352
333
|
writeGeneratedFile(ctx, join(CORE_DIR, "types.ts"), TYPES_TEMPLATE);
|
|
353
334
|
writeGeneratedFile(ctx, join(PROJECT_DIR, ".gitignore"), PROJECT_GITIGNORE);
|
|
354
335
|
|
|
336
|
+
// Docs do skill vão para `.houndsman/core/skill/`. O `SKILL.md` é o entry point
|
|
337
|
+
// dos harnesses (escrito em `writeHarnessPointers`), não um doc da camada.
|
|
355
338
|
for (const name of readdirSync(SKILL_DIR)) {
|
|
356
|
-
if (!name.endsWith(".md")) continue;
|
|
339
|
+
if (!name.endsWith(".md") || name === SKILL_FILE) continue;
|
|
357
340
|
const raw = readFileSync(join(SKILL_DIR, name), "utf8");
|
|
358
341
|
writeGeneratedFile(
|
|
359
342
|
ctx,
|
|
@@ -365,29 +348,15 @@ export function syncGeneratedFiles(ctx: ScaffoldContext): void {
|
|
|
365
348
|
for (const harness of ctx.harnesses) writeHarnessPointers(ctx, harness);
|
|
366
349
|
}
|
|
367
350
|
|
|
368
|
-
/**
|
|
351
|
+
/**
|
|
352
|
+
* Escreve o `SKILL.md` do harness a partir do asset base (`src/skill/SKILL.md`),
|
|
353
|
+
* resolvendo `{{projectName}}`. O texto do skill vive no asset — não aqui.
|
|
354
|
+
*/
|
|
369
355
|
function writeHarnessPointers(ctx: ScaffoldContext, harness: Harness): void {
|
|
370
|
-
const
|
|
371
|
-
writeGeneratedFile(
|
|
372
|
-
ctx,
|
|
373
|
-
join(pointerDir, "houndsman", "SKILL.md"),
|
|
374
|
-
SKILL_POINTER(
|
|
375
|
-
"houndsman",
|
|
376
|
-
ctx.projectName,
|
|
377
|
-
`${CORE_SKILL_DIR}/houndsman.md`,
|
|
378
|
-
"Revisa Pull Requests com watchdogs especializados por domínio. Orquestra `bun run houndsman context` → `bun run houndsman` → subagentes → `bun run houndsman --finalize`. Config em `houndsman.config.ts`; regras em `.houndsman/`; relatório em `.houndsman/.runs/<branch-slug>/review.md`.",
|
|
379
|
-
"Nome da branch (opcional, detecta automaticamente se omitido)",
|
|
380
|
-
),
|
|
381
|
-
);
|
|
356
|
+
const raw = readFileSync(join(SKILL_DIR, SKILL_FILE), "utf8");
|
|
382
357
|
writeGeneratedFile(
|
|
383
358
|
ctx,
|
|
384
|
-
join(
|
|
385
|
-
|
|
386
|
-
"houndsman-update",
|
|
387
|
-
ctx.projectName,
|
|
388
|
-
`${CORE_SKILL_DIR}/houndsman-update.md`,
|
|
389
|
-
"Atualiza as regras e protocolos do skill houndsman (watchdogs, exclusões, mapeamentos de docs).",
|
|
390
|
-
"Watchdog alvo (opcional)",
|
|
391
|
-
),
|
|
359
|
+
join(`.${harness}`, "skills", "houndsman", SKILL_FILE),
|
|
360
|
+
raw.replaceAll("{{projectName}}", ctx.projectName),
|
|
392
361
|
);
|
|
393
362
|
}
|
package/src/core/diff-source.ts
CHANGED
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
* Coleta do diff em um único passo (biblioteca).
|
|
3
3
|
*
|
|
4
4
|
* Roda `git diff base...head` **uma vez** (patch) e `--numstat` **uma vez**
|
|
5
|
-
* (churn), fatiando o patch por arquivo em memória
|
|
6
|
-
*
|
|
7
|
-
* deixa de crescer com o número de arquivos da PR.
|
|
5
|
+
* (churn), fatiando o patch por arquivo em memória — o custo não cresce com o
|
|
6
|
+
* número de arquivos da PR.
|
|
8
7
|
*
|
|
9
8
|
* Usado por `orchestrator.prepare` (churn para a classificação + patch para o
|
|
10
9
|
* Jev) e por `classifyRange` (fallback standalone).
|
|
@@ -24,7 +23,8 @@ export type CollectDiffOptions = {
|
|
|
24
23
|
maxPerFileChars?: number;
|
|
25
24
|
};
|
|
26
25
|
|
|
27
|
-
|
|
26
|
+
/** Teto default de caracteres por arquivo no patch (compartilhado com o git-revision). */
|
|
27
|
+
export const DEFAULT_MAX_PER_FILE_CHARS = 8_000;
|
|
28
28
|
|
|
29
29
|
/** Executa `git` com `core.quotepath=false` (paths literais) e devolve o stdout. */
|
|
30
30
|
function runGit(args: string[]): string {
|