@payconductor/houndsman 0.13.0 → 0.16.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.
Files changed (116) hide show
  1. package/README.md +6 -1
  2. package/package.json +1 -1
  3. package/src/cli/commands/compare-command.ts +112 -0
  4. package/src/cli/commands/context-command.ts +1 -0
  5. package/src/cli/commands/findings-command.ts +11 -1
  6. package/src/cli/commands/golden-command.ts +92 -0
  7. package/src/cli/commands/hunt-command.ts +105 -4
  8. package/src/cli/commands/review-command.ts +38 -0
  9. package/src/cli/commands/status-command.ts +3 -2
  10. package/src/cli/help.ts +6 -1
  11. package/src/cli/index.ts +5 -1
  12. package/src/cli/progress.ts +7 -2
  13. package/src/core/ai/ai-client.ts +38 -2
  14. package/src/core/ai/features/brief-enricher.ts +1 -1
  15. package/src/core/ai/features/business-analyzer.ts +61 -0
  16. package/src/core/ai/features/business-summarizer.ts +44 -0
  17. package/src/core/ai/index.ts +8 -0
  18. package/src/core/ai/prompts/brief.ts +33 -7
  19. package/src/core/ai/prompts/business.ts +76 -0
  20. package/src/core/ai/provider-capabilities.ts +12 -0
  21. package/src/core/ai/schemas.ts +70 -2
  22. package/src/core/ai/usage-recorder.ts +53 -1
  23. package/src/core/brief.ts +278 -16
  24. package/src/core/builtins.ts +108 -0
  25. package/src/core/business-summary.ts +583 -0
  26. package/src/core/commands/compare.ts +266 -0
  27. package/src/core/commands/context.ts +6 -2
  28. package/src/core/commands/findings.ts +38 -30
  29. package/src/core/commands/golden.ts +176 -0
  30. package/src/core/commands/review.ts +6 -0
  31. package/src/core/commands/scaffold.ts +29 -0
  32. package/src/core/commands/status.ts +9 -4
  33. package/src/core/compare.ts +310 -0
  34. package/src/core/concurrency.ts +30 -0
  35. package/src/core/config.ts +41 -3
  36. package/src/core/defaults.ts +50 -4
  37. package/src/core/domain/budget.ts +62 -4
  38. package/src/core/domain/classification.ts +12 -2
  39. package/src/core/engine/carryover.ts +4 -0
  40. package/src/core/engine/finalize-pipeline.ts +371 -0
  41. package/src/core/engine/prepare-pipeline.ts +1294 -0
  42. package/src/core/engine/review-context.ts +51 -0
  43. package/src/core/engine/review-options.ts +47 -0
  44. package/src/core/engine/review-scope.ts +14 -1
  45. package/src/core/engine/review-session.ts +11 -13
  46. package/src/core/engine/run-artifacts.ts +196 -0
  47. package/src/core/engine/run-format.ts +4 -0
  48. package/src/core/findings-state.ts +22 -2
  49. package/src/core/github-review.ts +5 -2
  50. package/src/core/golden.ts +346 -0
  51. package/src/core/hunt/hunt-runner.ts +621 -34
  52. package/src/core/hunt/index.ts +40 -1
  53. package/src/core/hunt/maki-intent-prompt.ts +50 -0
  54. package/src/core/hunt/maki-prompt.ts +298 -5
  55. package/src/core/hunt/maki-runner.ts +93 -16
  56. package/src/core/hunt/pool.ts +4 -26
  57. package/src/core/hunt/review-checks.ts +460 -0
  58. package/src/core/logging/index.ts +6 -1
  59. package/src/core/logging/progress.ts +85 -1
  60. package/src/core/orchestrator.ts +33 -1289
  61. package/src/core/paths.ts +6 -3
  62. package/src/core/pr-context.ts +2 -4
  63. package/src/core/prepare-report.ts +61 -0
  64. package/src/core/project-config.ts +18 -1
  65. package/src/core/propagation.ts +127 -0
  66. package/src/core/registry-check.ts +90 -27
  67. package/src/core/renderers/manifest-writer.ts +13 -0
  68. package/src/core/renderers/resume-renderer.ts +151 -11
  69. package/src/core/report-builder.ts +116 -278
  70. package/src/core/severity.ts +245 -0
  71. package/src/core/storage/file/file-storage.ts +7 -3
  72. package/src/core/storage/sqlite/mappers.ts +10 -0
  73. package/src/core/storage/sqlite/schema.ts +104 -23
  74. package/src/core/storage/sqlite/sqlite-storage.ts +22 -9
  75. package/src/core/storage/types.ts +17 -4
  76. package/src/core/storage/work-area.ts +59 -1
  77. package/src/core/types.ts +31 -0
  78. package/src/core/watchdog-registry.ts +21 -3
  79. package/src/index.ts +51 -4
  80. package/src/server/client/ArtifactsPanel.tsx +4 -2
  81. package/src/server/client/FindingCard.tsx +21 -1
  82. package/src/server/client/RunsSection.tsx +5 -0
  83. package/src/server/client/utils/constants.ts +21 -11
  84. package/src/server/client/utils/markdown.ts +1 -1
  85. package/src/server/client/utils/types.ts +14 -2
  86. package/src/server/run-repository.ts +35 -14
  87. package/src/server/styles.css +2 -0
  88. package/src/skill/OUTPUT-FORMAT.md +55 -10
  89. package/src/skill/README.md +19 -14
  90. package/src/skill/SETUP.md +1 -0
  91. package/src/skill/SKILL.md +13 -4
  92. package/src/skill/example-resume.md +43 -0
  93. package/src/skill/hunt.md +205 -58
  94. package/src/skill/train.md +3 -0
  95. package/src/tests/__snapshots__/baseline.test.ts.snap +208 -184
  96. package/src/tests/ai-core.test.ts +46 -0
  97. package/src/tests/ai-features.test.ts +95 -4
  98. package/src/tests/baseline.test.ts +29 -121
  99. package/src/tests/brief-scope.test.ts +250 -0
  100. package/src/tests/business-summary.test.ts +258 -0
  101. package/src/tests/compare.test.ts +256 -0
  102. package/src/tests/findings-state.test.ts +82 -0
  103. package/src/tests/fixtures/fake-model-provider.ts +2 -1
  104. package/src/tests/golden.test.ts +195 -0
  105. package/src/tests/hunt-maki.test.ts +710 -2
  106. package/src/tests/markdown.test.ts +3 -3
  107. package/src/tests/progress.test.ts +29 -1
  108. package/src/tests/propagation.test.ts +119 -0
  109. package/src/tests/review-resume.test.ts +60 -1
  110. package/src/tests/review-scope.test.ts +38 -29
  111. package/src/tests/review-scripts.test.ts +125 -218
  112. package/src/tests/server.test.ts +7 -4
  113. package/src/tests/severity.test.ts +235 -0
  114. package/src/tests/storage.test.ts +194 -9
  115. package/src/core/renderers/report-renderer.ts +0 -66
  116. package/src/skill/example-review.md +0 -66
package/README.md CHANGED
@@ -15,6 +15,11 @@ agente especialista por domínio que audita o diff · **review/revisão** = o pr
15
15
  **Jev** = pré-análise opcional. O vocabulário completo (brief, findings, manifest, budget,
16
16
  overflow, churn, candidato…) está em [`docs/GLOSSARY.md`](docs/GLOSSARY.md).
17
17
 
18
+ > Toda revisão tem duas etapas fixas: um **modelo de intenção** da PR
19
+ > (`business-summary.json`) e o revisor de fluxo **`general`**. Nenhum dos dois é
20
+ > configurável — o primeiro enriquece os briefs; o segundo procura gaps de lógica/domínio,
21
+ > não só violações de regra.
22
+
18
23
  ---
19
24
 
20
25
  ## Como iniciar
@@ -128,7 +133,7 @@ mostra o worktree ativo de cada PR e permite criá-lo pela sidebar.
128
133
  ├─ Passo 0 contexto da PR (gh, opcional)
129
134
  ├─ Passo 1 classifica o diff → briefs por watchdog (incremental quando possível)
130
135
  ├─ Passo 2 N subagentes revisam em paralelo
131
- ├─ Passo 3 consolida em review.md + triagem no storage
136
+ ├─ Passo 3 grava o resumo final (review-resume.md) + triagem no storage
132
137
  ├─ Passo 4 valida na UI (server) e marca os achados a executar
133
138
  ├─ Passo 5 fix corrige o grupo atual → status fix_done
134
139
  └─ Passo 6 verify confere o que resolveu, sem re-revisar
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@payconductor/houndsman",
3
- "version": "0.13.0",
3
+ "version": "0.16.0",
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",
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Comando `compare` — teste A/B entre duas runs (por slug ou branch+variantes).
3
+ *
4
+ * Exemplo:
5
+ * houndsman compare --branch feature/x --a baseline --b otimizado
6
+ * houndsman compare feature-x--baseline-1a2b3c feature-x--otimizado-9f8e7d
7
+ */
8
+
9
+ import { listRunsForCompare, runCompare } from "../../core/commands/compare";
10
+ import { ArgParser } from "../arg-parser";
11
+ import type { CliContext, Command } from "./command";
12
+
13
+ /** Flags com valor (não entram nos positionais). */
14
+ const VALUE_FLAGS = new Set(["--branch", "--a", "--b", "--config"]);
15
+
16
+ export const COMPARE_HELP = `Uso: houndsman compare [<slugA> <slugB>] [flags]
17
+
18
+ Compara duas runs da mesma branch e grava o relatório na run B.
19
+
20
+ Flags:
21
+ --branch <nome> Branch alvo (default: a atual do git)
22
+ --a <variante> Rótulo A (default: run principal)
23
+ --b <variante> Rótulo B (obrigatório sem slugs)
24
+ --list Lista as runs/variantes com contagem por severidade
25
+ --json Saída JSON (sem markdown)
26
+
27
+ Exemplos:
28
+ houndsman compare --branch feature/x --a baseline --b otimizado
29
+ houndsman compare feature-x--baseline-1a2b3c feature-x--otimizado-9f8e7d
30
+ houndsman compare --list --branch feature/x`;
31
+
32
+ /** Argumentos posicionais (ignora flags com valor). */
33
+ function positionals(args: readonly string[]): string[] {
34
+ const result: string[] = [];
35
+ for (let index = 0; index < args.length; index++) {
36
+ const arg = args[index];
37
+ if (arg.startsWith("--")) {
38
+ if (VALUE_FLAGS.has(arg)) index++;
39
+ continue;
40
+ }
41
+ result.push(arg);
42
+ }
43
+ return result;
44
+ }
45
+
46
+ export class CompareCommand implements Command {
47
+ readonly name = "compare";
48
+
49
+ async run(args: string[], context: CliContext): Promise<number> {
50
+ const parser = ArgParser.from(args);
51
+ if (parser.has("--help") || parser.has("-h")) {
52
+ context.out(COMPARE_HELP);
53
+ return 0;
54
+ }
55
+
56
+ const engine = await context.engine();
57
+ const branch = parser.value("--branch");
58
+
59
+ if (parser.has("--list")) {
60
+ const runs = listRunsForCompare(engine.storage, engine.repo, branch);
61
+ if (runs.length === 0) {
62
+ context.out("Nenhuma run encontrada.");
63
+ engine.close();
64
+ return 0;
65
+ }
66
+ context.logger.table([
67
+ ["slug", "variante", "findings", "blocking", "attention", "atualizada"],
68
+ ...runs.map((run) => [
69
+ run.slug,
70
+ run.variant ?? "(principal)",
71
+ String(run.findings),
72
+ String(run.bySeverity.blocking),
73
+ String(run.bySeverity.attention),
74
+ run.updatedAt,
75
+ ]),
76
+ ]);
77
+ engine.close();
78
+ return 0;
79
+ }
80
+
81
+ const [slugA, slugB] = positionals(args);
82
+ if (slugA && !slugB) {
83
+ context.err(
84
+ "Passe dois slugs (`houndsman compare <a> <b>`) ou use --b <variante>.",
85
+ );
86
+ return 1;
87
+ }
88
+ try {
89
+ const result = runCompare(engine.storage, engine.repo, {
90
+ slugA,
91
+ slugB,
92
+ branch,
93
+ variantA: parser.value("--a"),
94
+ variantB: parser.value("--b"),
95
+ });
96
+ if (parser.has("--json")) {
97
+ context.out(result.json.trimEnd());
98
+ } else {
99
+ context.out(result.markdown.trimEnd());
100
+ context.logger.success(
101
+ `Comparação salva em ${result.artifacts.markdown} (run ${result.b.slug}).`,
102
+ );
103
+ }
104
+ return 0;
105
+ } catch (error) {
106
+ context.err(error instanceof Error ? error.message : String(error));
107
+ return 1;
108
+ } finally {
109
+ engine.close();
110
+ }
111
+ }
112
+ }
@@ -51,6 +51,7 @@ export class ContextCommand implements Command {
51
51
  engine.repo,
52
52
  {
53
53
  branch: workspace?.branch ?? parser.value("--branch"),
54
+ variant: parser.value("--variant"),
54
55
  worktreePath: workspace?.worktreePath,
55
56
  prNumber: workspace?.prNumber,
56
57
  },
@@ -14,21 +14,31 @@ export class FindingsCommand implements Command {
14
14
  async run(args: string[], context: CliContext): Promise<number> {
15
15
  const engine = await context.engine();
16
16
  const sub = ArgParser.from(args).command();
17
+ // Raiz/git do engine: o comando nunca lê `process.cwd()` implicitamente.
18
+ const options = { root: engine.root, git: engine.git };
17
19
  const result =
18
20
  sub === "verify"
19
- ? await runVerifyCommand(engine.config, engine.storage, engine.repo, args)
21
+ ? await runVerifyCommand(
22
+ engine.config,
23
+ engine.storage,
24
+ engine.repo,
25
+ args,
26
+ options,
27
+ )
20
28
  : sub === "submit"
21
29
  ? await runSubmitCommand(
22
30
  engine.config,
23
31
  engine.storage,
24
32
  engine.repo,
25
33
  args,
34
+ options,
26
35
  )
27
36
  : runFindingsCommand(
28
37
  engine.config,
29
38
  engine.storage,
30
39
  engine.repo,
31
40
  args,
41
+ options,
32
42
  );
33
43
 
34
44
  if (result.exitCode !== 0) context.err(result.output);
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Comando `golden` — avalia as runs da branch contra defeitos congelados.
3
+ *
4
+ * Exemplo:
5
+ * houndsman golden .houndsman/golden/asaas-split.json
6
+ * houndsman golden .houndsman/golden/asaas-split.json --variant otimizado-4
7
+ */
8
+
9
+ import { runGolden } from "../../core/commands/golden";
10
+ import { ArgParser } from "../arg-parser";
11
+ import type { CliContext, Command } from "./command";
12
+
13
+ /** Flags com valor (não entram nos positionais). */
14
+ const VALUE_FLAGS = new Set(["--branch", "--variant", "--slug", "--config"]);
15
+
16
+ export const GOLDEN_HELP = `Uso: houndsman golden <arquivo.json> [flags]
17
+
18
+ Avalia uma run (ou todas as variantes da branch) contra um golden set —
19
+ defeitos já confirmados no código — e reporta recall/precisão.
20
+
21
+ Flags:
22
+ --branch <nome> Branch alvo (default: a atual do git)
23
+ --variant <rótulo> Avalia só uma variante
24
+ --slug <slug> Avalia uma run específica
25
+ --json Saída JSON (sem markdown)
26
+
27
+ O relatório é gravado como \`golden-<slug>.md|json\` na run avaliada.
28
+
29
+ Formato do arquivo:
30
+ { "version": 1, "title": "...", "commit": "abc1234",
31
+ "expected": [{ "id": "seller-api-key", "file": "src/a.ts",
32
+ "line": 116, "tolerance": 30, "match": "sellerApiKey",
33
+ "severity": "blocking", "note": "..." }] 1}
34
+
35
+ Exemplos:
36
+ houndsman golden .houndsman/golden/asaas-split.json
37
+ houndsman golden .houndsman/golden/asaas-split.json --variant otimizado-5`;
38
+
39
+ /** Primeiro argumento posicional (ignora flags com valor). */
40
+ function positionalFile(args: readonly string[]): string | undefined {
41
+ for (let index = 0; index < args.length; index++) {
42
+ const arg = args[index];
43
+ if (arg.startsWith("--")) {
44
+ if (VALUE_FLAGS.has(arg)) index++;
45
+ continue;
46
+ }
47
+ return arg;
48
+ }
49
+ return undefined;
50
+ }
51
+
52
+ export class GoldenCommand implements Command {
53
+ readonly name = "golden";
54
+
55
+ async run(args: string[], context: CliContext): Promise<number> {
56
+ const parser = ArgParser.from(args);
57
+ if (parser.has("--help") || parser.has("-h")) {
58
+ context.logger.info(GOLDEN_HELP);
59
+ return 0;
60
+ }
61
+
62
+ const file = positionalFile(args);
63
+ if (!file) {
64
+ context.logger.info(GOLDEN_HELP);
65
+ return 1;
66
+ }
67
+
68
+ const engine = await context.engine();
69
+ try {
70
+ const result = runGolden(engine.storage, engine.repo, {
71
+ file,
72
+ branch: parser.value("--branch"),
73
+ variant: parser.value("--variant"),
74
+ slug: parser.value("--slug"),
75
+ });
76
+ if (parser.has("--json")) {
77
+ context.logger.info(result.json.trimEnd());
78
+ } else {
79
+ context.logger.info(result.markdown.trimEnd());
80
+ context.logger.success(
81
+ `Golden salvo em ${result.artifacts?.markdown} (run ${result.target.slug}).`,
82
+ );
83
+ }
84
+ return 0;
85
+ } catch (error) {
86
+ context.logger.error(error instanceof Error ? error.message : String(error));
87
+ return 1;
88
+ } finally {
89
+ engine.close();
90
+ }
91
+ }
92
+ }
@@ -26,7 +26,7 @@ import {
26
26
  resolveMakiModel,
27
27
  } from "../../core/hunt/maki-model";
28
28
  import { SpawnMakiRunner } from "../../core/hunt/maki-runner";
29
- import { formatDuration, ProgressBoard } from "../../core/logging";
29
+ import { formatDuration, type LoggerLike, ProgressBoard } from "../../core/logging";
30
30
  import { displayPath } from "../../core/paths";
31
31
  import { ProjectConfig } from "../../core/project-config";
32
32
  import { touchRunStatus } from "../../core/run-status";
@@ -49,6 +49,7 @@ Subcomandos:
49
49
  Flags:
50
50
  --pr <n> Revisa a PR em worktree dedicado
51
51
  --branch <b> Branch alvo
52
+ --variant <label> Rótulo da variante A/B (runs separadas para a mesma branch)
52
53
  --worktree <path> Worktree já existente
53
54
  --allow-partial Finaliza mesmo se algum watchdog falhar (findings vazios)
54
55
  --no-context Não coleta o contexto da PR via gh
@@ -56,6 +57,8 @@ Flags:
56
57
  --hunt-concurrency Processos maki simultâneos
57
58
  --hunt-timeout Timeout por watchdog (ms)
58
59
  --no-retry Não repete saída inválida
60
+ --no-checklist Sem a passada extra de fluxos/invariantes não verificados
61
+ --no-second-opinion Sem a passada adversarial do revisor de fluxo
59
62
  --fail-on-blocking Exit 1 se houver bloqueantes
60
63
 
61
64
  Instalação:
@@ -180,6 +183,7 @@ export class HuntCommand implements Command {
180
183
  engine.repo,
181
184
  {
182
185
  branch: workspace.branch,
186
+ variant: options.variant,
183
187
  worktreePath: workspace.worktreePath,
184
188
  prNumber: workspace.prNumber,
185
189
  },
@@ -235,8 +239,12 @@ export class HuntCommand implements Command {
235
239
  concurrency,
236
240
  timeoutMs,
237
241
  retries,
242
+ checklist: parser.has("--no-checklist") ? false : project.huntChecklist,
243
+ secondOpinion: parser.has("--no-second-opinion")
244
+ ? false
245
+ : project.huntSecondOpinion,
238
246
  runner: new SpawnMakiRunner(binary.path),
239
- onEvent: (event) => this.logEvent(board, event),
247
+ onEvent: (event) => this.logEvent(board, event, context.logger),
240
248
  });
241
249
  // Runs longas não podem parecer “timeout” na UI: a skill pinga antes de
242
250
  // cada onda; o CLI renova o heartbeat enquanto o maki trabalha.
@@ -265,6 +273,56 @@ export class HuntCommand implements Command {
265
273
  `${report.totals.findings} finding(s), $${report.totals.costUsd.toFixed(4)}, ` +
266
274
  `${Math.round(report.durationMs / 1000)}s.`,
267
275
  );
276
+ const checklistCost = report.watchdogs.reduce(
277
+ (sum, watchdog) => sum + (watchdog.checklist?.costUsd ?? 0),
278
+ 0,
279
+ );
280
+ const opinionCost = report.watchdogs.reduce(
281
+ (sum, watchdog) => sum + (watchdog.secondOpinion?.costUsd ?? 0),
282
+ 0,
283
+ );
284
+ if (checklistCost + opinionCost > 0) {
285
+ context.logger.info(
286
+ `Passadas extras: checklist $${checklistCost.toFixed(4)} · ` +
287
+ `segunda opinião $${opinionCost.toFixed(4)}.`,
288
+ );
289
+ }
290
+ const unchecked = report.watchdogs.filter(
291
+ (watchdog) =>
292
+ (watchdog.checks?.missing ?? 0) + (watchdog.rules?.missing ?? 0) > 0,
293
+ );
294
+ if (unchecked.length > 0) {
295
+ context.logger.warn(
296
+ `Checklist de intenção/regras incompleto em ${unchecked.length} watchdog(s): ` +
297
+ unchecked
298
+ .map(
299
+ (watchdog) =>
300
+ `${watchdog.watchdogId} (itens ${watchdog.checks?.missing ?? 0}, regras ${watchdog.rules?.missing ?? 0})`,
301
+ )
302
+ .join(", "),
303
+ );
304
+ }
305
+ const businessReview = report.watchdogs.find(
306
+ (watchdog) => watchdog.business !== undefined,
307
+ )?.business;
308
+ if (businessReview && !businessReview.accepted) {
309
+ context.logger.info(
310
+ "Modelo de intenção devolvido pelo revisor foi descartado " +
311
+ `(inferior ao já gravado: ${report.business ?? "n/d"}).`,
312
+ );
313
+ }
314
+ const incomplete = report.watchdogs
315
+ .filter((watchdog) => (watchdog.coverage?.undeclared ?? 0) > 0)
316
+ .map(
317
+ (watchdog) =>
318
+ `${watchdog.watchdogId} ${watchdog.coverage?.reviewed ?? 0}/${watchdog.coverage?.total ?? 0}`,
319
+ );
320
+ if (incomplete.length > 0) {
321
+ context.logger.info(
322
+ `Cobertura declarada incompleta: ${incomplete.join(", ")} ` +
323
+ "(detalhe em `hunt-report.json`).",
324
+ );
325
+ }
268
326
 
269
327
  const failed = report.watchdogs.filter(
270
328
  (watchdog) => watchdog.status === "failed",
@@ -403,17 +461,60 @@ export class HuntCommand implements Command {
403
461
  `harness: ${project.huntEnabled ? "ligado" : "desligado"} · ` +
404
462
  `concorrência ${project.huntConcurrency} · ` +
405
463
  `timeout ${Math.round(project.huntTimeoutMs / 1000)}s · ` +
406
- `retries ${project.huntRetries}.`,
464
+ `retries ${project.huntRetries} · ` +
465
+ `checklist ${project.huntChecklist ? "on" : "off"} · ` +
466
+ `segunda opinião ${project.huntSecondOpinion ? "on" : "off"}.`,
407
467
  );
408
468
  return 0;
409
469
  }
410
470
 
411
471
  /** Log de progresso por watchdog. */
412
- private logEvent(board: ProgressBoard, event: HuntEvent): void {
472
+ private logEvent(board: ProgressBoard, event: HuntEvent, logger: LoggerLike): void {
413
473
  switch (event.type) {
474
+ case "intent":
475
+ if (event.status === "start") {
476
+ logger.step("Reconstruindo a intenção da PR (maki)…");
477
+ } else if (event.status === "done") {
478
+ logger.success(
479
+ `Intenção da PR reconstruída (${event.source ?? "reviewer"}).`,
480
+ );
481
+ } else if (event.status === "skipped") {
482
+ logger.info(`Intenção da PR já disponível (${event.source}).`);
483
+ } else {
484
+ logger.warn(
485
+ `Intenção da PR manteve o modelo determinístico: ${event.error ?? "falha"}`,
486
+ );
487
+ }
488
+ return;
414
489
  case "start":
415
490
  board.add(event.watchdogId, "revisando");
416
491
  return;
492
+ case "checklist":
493
+ if (event.status === "start") {
494
+ logger.step(
495
+ `${event.watchdogId}: ${event.missing} item(s) e ${event.missingRules} regra(s) sem verificação — passada de checklist…`,
496
+ );
497
+ } else if (event.status === "failed") {
498
+ logger.warn(
499
+ `${event.watchdogId}: checklist falhou (${event.error ?? "erro"}); relatório da primeira passada mantido.`,
500
+ );
501
+ }
502
+ return;
503
+ case "second-opinion":
504
+ if (event.status === "start") {
505
+ logger.step(
506
+ `${event.watchdogId}: passada adversarial (credencial/estado, capacidade, identificador, idempotência, contrato)…`,
507
+ );
508
+ } else if (event.status === "done") {
509
+ logger.info(
510
+ `${event.watchdogId}: segunda opinião adicionou ${event.findings ?? 0} finding(s).`,
511
+ );
512
+ } else {
513
+ logger.warn(
514
+ `${event.watchdogId}: segunda opinião falhou (${event.error ?? "erro"}); relatório anterior mantido.`,
515
+ );
516
+ }
517
+ return;
417
518
  case "retry":
418
519
  board.retry(
419
520
  event.watchdogId,
@@ -1,5 +1,7 @@
1
1
  /** Comando default — prepara ou finaliza a revisão. */
2
2
 
3
+ import { ArgParser } from "../../core/arg-parser";
4
+ import { runContextCommand } from "../../core/commands/context";
3
5
  import { parseReviewArgs } from "../../core/commands/review";
4
6
  import { resolveReviewWorkspace } from "../../core/engine/review-workspace";
5
7
  import { displayPath } from "../../core/paths";
@@ -52,6 +54,42 @@ export class ReviewCommand implements Command {
52
54
  pr: workspace.prNumber,
53
55
  worktreePath: workspace.worktreePath,
54
56
  };
57
+ // PR: coleta o contexto via `gh` aqui, como o `hunt` faz. Sem isso, a skill
58
+ // gastava um subagente só para rodar `houndsman context` antes do prepare.
59
+ if (
60
+ options.pr !== undefined &&
61
+ !options.finalize &&
62
+ !ArgParser.from(args).has("--no-context")
63
+ ) {
64
+ const contextStarted = Date.now();
65
+ context.logger.step("Coletando o contexto da PR via gh…");
66
+ try {
67
+ const collected = runContextCommand(
68
+ engine.config,
69
+ engine.storage,
70
+ engine.repo,
71
+ {
72
+ branch: workspace.branch,
73
+ variant: options.variant,
74
+ worktreePath: workspace.worktreePath,
75
+ prNumber: workspace.prNumber,
76
+ },
77
+ );
78
+ stepDone(
79
+ context.logger,
80
+ collected.context.available
81
+ ? "Contexto da PR coletado via gh"
82
+ : `Contexto da PR indisponível (${collected.context.reason})`,
83
+ contextStarted,
84
+ );
85
+ } catch (error) {
86
+ context.logger.warn(
87
+ `Contexto da PR indisponível: ${
88
+ error instanceof Error ? error.message : String(error)
89
+ }`,
90
+ );
91
+ }
92
+ }
55
93
  if (options.finalize) context.logger.step("Consolidando os findings…");
56
94
  const result = await engine
57
95
  .session(resolved)
@@ -11,6 +11,7 @@ export class StatusCommand implements Command {
11
11
  const parser = ArgParser.from(args);
12
12
  const action = parser.command();
13
13
  const branch = parser.value("--branch");
14
+ const variant = parser.value("--variant");
14
15
 
15
16
  if (action && action !== "ping" && action !== "show") {
16
17
  context.err(
@@ -22,8 +23,8 @@ export class StatusCommand implements Command {
22
23
  const engine = await context.engine();
23
24
  const result =
24
25
  action === "ping"
25
- ? pingStatus(engine.storage, engine.repo, branch)
26
- : readStatus(engine.storage, engine.repo, branch);
26
+ ? pingStatus(engine.storage, engine.repo, branch, variant)
27
+ : readStatus(engine.storage, engine.repo, branch, variant);
27
28
 
28
29
  if (action === "ping") {
29
30
  context.out(
package/src/cli/help.ts CHANGED
@@ -3,8 +3,10 @@ export const HELP = `Uso: houndsman [comando] [flags]
3
3
 
4
4
  Comandos:
5
5
  (nenhum) Prepara a revisão (classification + briefs + manifesto)
6
- --finalize Consolida os findings em review.md
6
+ --finalize Consolida os findings e grava o resumo final
7
7
  context Coleta o contexto da PR via gh (Passo 0; --pr/--worktree)
8
+ compare [a] [b] Compara duas runs (A/B) por slug ou --branch/--a/--b
9
+ golden <arquivo> Avalia as runs contra um golden set (recall/precisão)
8
10
  findings <sub> Triagem/correção dos findings (list · show · set · done · reset · sync · summary · batch · verify · submit · path)
9
11
  hunt [flags] Harness local (maki): prepare → watchdogs via maki → finalize
10
12
  (a skill /houndsman hunt continua existindo p/ agentes externos)
@@ -25,6 +27,7 @@ Flags:
25
27
  --base <branch> Base do diff (default: main)
26
28
  --head <ref> Head do diff (default: HEAD)
27
29
  --branch <nome> Branch alvo (default: detecta do git)
30
+ --variant <label> Rótulo da variante A/B (runs separadas para a mesma branch)
28
31
  --pr <n> Revisa a PR em um worktree dedicado (cria/reusa automaticamente)
29
32
  --worktree <path> Revisa em um worktree já existente
30
33
  --clean Descarta findings brutos do staging anterior
@@ -41,6 +44,8 @@ Flags de hunt (harness local):
41
44
  --hunt-concurrency Processos maki simultâneos
42
45
  --hunt-timeout Timeout por watchdog (ms)
43
46
  --no-retry Não repete saída inválida
47
+ --no-checklist Sem a passada extra de fluxos/invariantes não verificados
48
+ --no-second-opinion Sem a passada adversarial do revisor de fluxo
44
49
 
45
50
  Flags de findings:
46
51
  --decision <d> pending | execute | ignore
package/src/cli/index.ts CHANGED
@@ -12,7 +12,7 @@
12
12
  *
13
13
  * Comandos:
14
14
  * (default) Passo 1 — preparar (classificação + briefs + manifesto)
15
- * --finalize Passo 3 — consolidar os findings em review.md
15
+ * --finalize Passo 3 — consolidar os findings no resumo final
16
16
  * context Passo 0 — coletar o contexto da PR via `gh`
17
17
  * findings <sub> Triagem/correção dos findings (list/set/done/sync/batch/verify/submit/...)
18
18
  * hunt [flags] Harness local (maki): prepare → watchdogs via maki → finalize
@@ -35,9 +35,11 @@ import { formatDuration, logger } from "../core/logging";
35
35
  import { syncProjectLayerForRoot } from "../core/services/worktree-manager";
36
36
  import { ArgParser } from "./arg-parser";
37
37
  import type { CliContext } from "./commands/command";
38
+ import { CompareCommand } from "./commands/compare-command";
38
39
  import { ContextCommand } from "./commands/context-command";
39
40
  import { DoctorCommand } from "./commands/doctor-command";
40
41
  import { FindingsCommand } from "./commands/findings-command";
42
+ import { GoldenCommand } from "./commands/golden-command";
41
43
  import { HelpCommand } from "./commands/help-command";
42
44
  import { HuntCommand } from "./commands/hunt-command";
43
45
  import { CommandRegistry } from "./commands/registry";
@@ -61,6 +63,8 @@ function buildRegistry(): CommandRegistry {
61
63
  .register(new HelpCommand())
62
64
  .register(new HuntCommand())
63
65
  .register(new ContextCommand())
66
+ .register(new CompareCommand())
67
+ .register(new GoldenCommand())
64
68
  .register(new FindingsCommand())
65
69
  .register(new StatusCommand())
66
70
  .register(new WatchdogCommand())
@@ -6,7 +6,7 @@
6
6
  * primeiro `Discovery:`.
7
7
  */
8
8
 
9
- import { formatDuration, type LoggerLike, PhaseReporter } from "../core/logging";
9
+ import { formatDuration, type LoggerLike, ParallelPhaseReporter } from "../core/logging";
10
10
  import type { PrepareProgress, PrepareStage } from "../core/orchestrator";
11
11
 
12
12
  /** Texto de cada etapa (início no gerúndio, fim no particípio). */
@@ -20,6 +20,10 @@ export const PREPARE_STAGE_TEXT: Record<PrepareStage, { start: string; done: str
20
20
  impact: { start: "Medindo o impacto nos importadores", done: "Impacto calculado" },
21
21
  jev: { start: "Pré-analisando o diff (Jev/IA)", done: "Pré-análise concluída" },
22
22
  context: { start: "Resumindo o contexto da PR", done: "Contexto resumido" },
23
+ business: {
24
+ start: "Reconstruindo a intenção da PR",
25
+ done: "Intenção reconstruída",
26
+ },
23
27
  technical: {
24
28
  start: "Consolidando os pontos técnicos",
25
29
  done: "Pontos técnicos prontos",
@@ -31,7 +35,8 @@ export const PREPARE_STAGE_TEXT: Record<PrepareStage, { start: string; done: str
31
35
 
32
36
  /** Cria o callback de progresso do prepare ligado ao logger do CLI. */
33
37
  export function prepareProgress(logger: LoggerLike): PrepareProgress {
34
- const reporter = new PhaseReporter(logger);
38
+ // O prepare roda etapas em paralelo; o reporter aceita fases concorrentes.
39
+ const reporter = new ParallelPhaseReporter(logger);
35
40
  return (event) => {
36
41
  const text = PREPARE_STAGE_TEXT[event.stage];
37
42
  if (event.type === "start") reporter.begin(event.stage, text.start);
@@ -10,6 +10,7 @@ import type { AiProviderOptions } from "../config";
10
10
  import type { ProjectConfig } from "../project-config";
11
11
  import { AiConfigurationError, errorMessage } from "./errors";
12
12
  import { ModelFactory } from "./model-factory";
13
+ import { LEAN_PROVIDER_OPTIONS } from "./provider-capabilities";
13
14
  import type { AiResult, AiUsage, ModelProvider, ModelRequest } from "./types";
14
15
  import { AiUsageBudget } from "./usage-budget";
15
16
  import { AiUsageRecorder } from "./usage-recorder";
@@ -109,7 +110,13 @@ export class AiClient {
109
110
  ): Promise<AiResult<T>> {
110
111
  return this.run(
111
112
  feature,
112
- { ...request, outputSchema: request.outputSchema },
113
+ {
114
+ ...request,
115
+ outputSchema: request.outputSchema,
116
+ providerOptions:
117
+ request.providerOptions ??
118
+ leanProviderOptions(this.project.aiProviderOptions),
119
+ },
113
120
  model,
114
121
  );
115
122
  }
@@ -183,6 +190,7 @@ export class AiClient {
183
190
  return { ok: false, error: errorMessage(error) };
184
191
  }
185
192
 
193
+ const startedAt = Date.now();
186
194
  try {
187
195
  const provider = await this.requireProvider(model);
188
196
  const response = await provider.generate({
@@ -193,6 +201,17 @@ export class AiClient {
193
201
  ),
194
202
  signal: combineSignals(request.signal, this.project.aiTimeoutMs),
195
203
  });
204
+ // Objeto truncado não é resultado parcial útil: sem o JSON completo a
205
+ // feature inteira caía silenciosamente no fallback. Registra e falha.
206
+ if (request.outputSchema && response.finishReason === "length") {
207
+ const error = `Resposta truncada (finishReason=length; maxOutputTokens=${request.maxOutputTokens ?? "sem teto"}).`;
208
+ this.recorder.recordFailure(feature, provider.modelId, {
209
+ durationMs: Date.now() - startedAt,
210
+ error,
211
+ finishReason: response.finishReason,
212
+ });
213
+ return { ok: false, error };
214
+ }
196
215
  this.budget.record(response.usage);
197
216
  this.recorder.record(feature, provider.modelId, response.usage);
198
217
  return {
@@ -202,7 +221,12 @@ export class AiClient {
202
221
  model: provider.modelId,
203
222
  };
204
223
  } catch (error) {
205
- return { ok: false, error: errorMessage(error) };
224
+ const message = errorMessage(error);
225
+ this.recorder.recordFailure(feature, model ?? this.modelId, {
226
+ durationMs: Date.now() - startedAt,
227
+ error: message,
228
+ });
229
+ return { ok: false, error: message };
206
230
  }
207
231
  }
208
232
  }
@@ -221,6 +245,18 @@ function mergeProviderOptions(
221
245
  return merged;
222
246
  }
223
247
 
248
+ /**
249
+ * Opções enxutas para objetos estruturados: thinking desligado, salvo quando o
250
+ * projeto já configurou `thinking` explicitamente — aí a config manda.
251
+ */
252
+ function leanProviderOptions(
253
+ base: AiProviderOptions | undefined,
254
+ ): AiProviderOptions | undefined {
255
+ const configured = base?.deepseek;
256
+ if (configured && "thinking" in configured) return undefined;
257
+ return LEAN_PROVIDER_OPTIONS;
258
+ }
259
+
224
260
  /** Combina o sinal do chamador com o timeout configurado. */
225
261
  function combineSignals(
226
262
  signal: AbortSignal | undefined,