@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.
Files changed (42) hide show
  1. package/README.md +32 -14
  2. package/package.json +1 -1
  3. package/src/cli/index.ts +22 -11
  4. package/src/core/assets.ts +3 -0
  5. package/src/core/brief.ts +2 -3
  6. package/src/core/commands/findings.ts +232 -4
  7. package/src/core/commands/review.ts +2 -0
  8. package/src/core/commands/scaffold.ts +13 -44
  9. package/src/core/diff-source.ts +4 -4
  10. package/src/core/findings-state.ts +20 -2
  11. package/src/core/fix-batch.ts +236 -0
  12. package/src/core/git-revision.ts +105 -0
  13. package/src/core/jev/index.ts +1 -0
  14. package/src/core/jev/jev-detector.ts +2 -2
  15. package/src/core/jev/jev-verify.ts +178 -0
  16. package/src/core/orchestrator.ts +202 -15
  17. package/src/core/paths.ts +6 -0
  18. package/src/core/report-builder.ts +5 -0
  19. package/src/core/run-status.ts +123 -0
  20. package/src/core/verify.ts +175 -0
  21. package/src/core/watchdog-registry.ts +1 -2
  22. package/src/index.ts +1 -1
  23. package/src/server/artifacts.ts +1 -0
  24. package/src/server/client/RunList.tsx +22 -13
  25. package/src/server/client/constants.ts +32 -1
  26. package/src/server/client/types.ts +4 -0
  27. package/src/server/index.ts +12 -1
  28. package/src/skill/AUDIT-PROTOCOL.md +2 -2
  29. package/src/skill/README.md +29 -5
  30. package/src/skill/SETUP.md +11 -1
  31. package/src/skill/SKILL.md +48 -0
  32. package/src/skill/fix.md +206 -0
  33. package/src/skill/{houndsman.md → hunt.md} +41 -68
  34. package/src/skill/{houndsman-update.md → train.md} +11 -11
  35. package/src/skill/verify.md +70 -0
  36. package/src/tests/findings-commands.test.ts +252 -2
  37. package/src/tests/findings-state.test.ts +52 -1
  38. package/src/tests/fix-batch.test.ts +257 -0
  39. package/src/tests/run-status.test.ts +147 -0
  40. package/src/tests/scaffold.test.ts +59 -0
  41. package/src/tests/server.test.ts +30 -0
  42. 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
- > **Não existe "modo completo".** O `setup` já cria a camada inicial com um watchdog de
57
- > exemplo — troque o exemplo pelas regras reais do seu projeto (ou use `/houndsman-update`).
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
- └─ Passo 4 triagem (UI local ou agente)
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
- - Revisão normal: só `/houndsman` (a branch é detectada; base default `main`).
94
- - Revisar outra branch: `/houndsman feature/minha-branch`.
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 list --decision execute --json
118
- bun run houndsman findings sync
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 o skill **`/houndsman-update`**. Ele prova no código que o padrão
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/houndsman.md` — orquestração do skill `/houndsman`.
156
- - `src/skill/houndsman-update.md` — manutenção das regras dos watchdogs.
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.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 o JSON.
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 result = runFindingsCommand(config, args);
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);
@@ -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 era
5
- * protocolo específico vive nas regras do watchdog (`rules`).
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 { type HoundsmanConfig, resolveArtifactDir } from "../config";
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 { relFromRoot } from "../paths";
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 os ponteiros de skill dos harnesses. Sempre sobrescritos (é o
349
- * alvo do `update`).
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
- /** Escreve os ponteiros de skill de um harness. */
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 pointerDir = `.${harness}/skills`;
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(pointerDir, "houndsman-update", "SKILL.md"),
385
- SKILL_POINTER(
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
  }
@@ -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. Substitui os N spawns de
6
- * `git diff -- <arquivo>` (antes feitos por arquivo, no `jev-detector`): o custo
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
- const DEFAULT_MAX_PER_FILE_CHARS = 8_000;
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 {