@payconductor/houndsman 0.12.0 → 0.14.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 (119) hide show
  1. package/README.md +23 -2
  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/doctor-command.ts +22 -0
  6. package/src/cli/commands/findings-command.ts +11 -1
  7. package/src/cli/commands/hunt-command.ts +479 -0
  8. package/src/cli/commands/review-command.ts +58 -2
  9. package/src/cli/commands/status-command.ts +3 -2
  10. package/src/cli/help.ts +20 -3
  11. package/src/cli/index.ts +20 -3
  12. package/src/cli/progress.ts +50 -0
  13. package/src/core/ai/ai-client.ts +38 -2
  14. package/src/core/ai/ai-sdk-model-provider.ts +39 -0
  15. package/src/core/ai/features/brief-enricher.ts +1 -1
  16. package/src/core/ai/features/business-analyzer.ts +61 -0
  17. package/src/core/ai/features/business-summarizer.ts +44 -0
  18. package/src/core/ai/index.ts +14 -1
  19. package/src/core/ai/prompts/brief.ts +33 -7
  20. package/src/core/ai/prompts/business.ts +76 -0
  21. package/src/core/ai/provider-capabilities.ts +12 -0
  22. package/src/core/ai/schemas.ts +70 -2
  23. package/src/core/ai/types.ts +2 -0
  24. package/src/core/ai/usage-recorder.ts +53 -1
  25. package/src/core/brief.ts +246 -12
  26. package/src/core/builtins.ts +108 -0
  27. package/src/core/business-summary.ts +583 -0
  28. package/src/core/commands/compare.ts +233 -0
  29. package/src/core/commands/context.ts +6 -2
  30. package/src/core/commands/findings.ts +38 -30
  31. package/src/core/commands/review.ts +6 -0
  32. package/src/core/commands/scaffold.ts +38 -0
  33. package/src/core/commands/status.ts +9 -4
  34. package/src/core/compare.ts +243 -0
  35. package/src/core/concurrency.ts +30 -0
  36. package/src/core/config.ts +57 -1
  37. package/src/core/defaults.ts +63 -2
  38. package/src/core/domain/budget.ts +62 -4
  39. package/src/core/domain/classification.ts +12 -2
  40. package/src/core/engine/carryover.ts +4 -0
  41. package/src/core/engine/finalize-pipeline.ts +347 -0
  42. package/src/core/engine/prepare-pipeline.ts +1294 -0
  43. package/src/core/engine/review-context.ts +51 -0
  44. package/src/core/engine/review-options.ts +47 -0
  45. package/src/core/engine/review-scope.ts +106 -26
  46. package/src/core/engine/review-session.ts +18 -16
  47. package/src/core/engine/review-workspace.ts +14 -0
  48. package/src/core/engine/run-artifacts.ts +196 -0
  49. package/src/core/engine/run-format.ts +4 -0
  50. package/src/core/env.ts +14 -0
  51. package/src/core/findings-state.ts +13 -1
  52. package/src/core/github-review.ts +5 -2
  53. package/src/core/hunt/hunt-runner.ts +418 -0
  54. package/src/core/hunt/index.ts +61 -0
  55. package/src/core/hunt/maki-install.ts +331 -0
  56. package/src/core/hunt/maki-intent-prompt.ts +50 -0
  57. package/src/core/hunt/maki-model.ts +101 -0
  58. package/src/core/hunt/maki-prompt.ts +83 -0
  59. package/src/core/hunt/maki-release.ts +104 -0
  60. package/src/core/hunt/maki-runner.ts +276 -0
  61. package/src/core/hunt/pool.ts +8 -0
  62. package/src/core/logging/index.ts +7 -0
  63. package/src/core/logging/logger.ts +19 -0
  64. package/src/core/logging/progress.ts +366 -0
  65. package/src/core/orchestrator.ts +33 -1180
  66. package/src/core/paths.ts +6 -3
  67. package/src/core/pr-context.ts +2 -4
  68. package/src/core/prepare-report.ts +61 -0
  69. package/src/core/project-config.ts +46 -1
  70. package/src/core/propagation.ts +127 -0
  71. package/src/core/registry-check.ts +60 -21
  72. package/src/core/renderers/manifest-writer.ts +13 -0
  73. package/src/core/renderers/resume-renderer.ts +151 -11
  74. package/src/core/report-builder.ts +108 -278
  75. package/src/core/storage/file/file-storage.ts +7 -3
  76. package/src/core/storage/sqlite/mappers.ts +10 -0
  77. package/src/core/storage/sqlite/schema.ts +104 -23
  78. package/src/core/storage/sqlite/sqlite-storage.ts +22 -9
  79. package/src/core/storage/types.ts +17 -4
  80. package/src/core/storage/work-area.ts +59 -1
  81. package/src/core/types.ts +31 -0
  82. package/src/core/watchdog-registry.ts +21 -3
  83. package/src/index.ts +78 -4
  84. package/src/server/client/ArtifactsPanel.tsx +4 -2
  85. package/src/server/client/FindingCard.tsx +21 -1
  86. package/src/server/client/RunsSection.tsx +5 -0
  87. package/src/server/client/utils/constants.ts +21 -11
  88. package/src/server/client/utils/markdown.ts +1 -1
  89. package/src/server/client/utils/types.ts +14 -2
  90. package/src/server/run-repository.ts +35 -14
  91. package/src/server/styles.css +2 -0
  92. package/src/skill/OUTPUT-FORMAT.md +36 -10
  93. package/src/skill/README.md +30 -14
  94. package/src/skill/SETUP.md +22 -1
  95. package/src/skill/SKILL.md +17 -4
  96. package/src/skill/example-resume.md +43 -0
  97. package/src/skill/hunt.md +181 -58
  98. package/src/tests/__snapshots__/baseline.test.ts.snap +206 -178
  99. package/src/tests/ai-core.test.ts +84 -1
  100. package/src/tests/ai-features.test.ts +95 -4
  101. package/src/tests/baseline.test.ts +29 -121
  102. package/src/tests/brief-scope.test.ts +250 -0
  103. package/src/tests/business-summary.test.ts +258 -0
  104. package/src/tests/cli.test.ts +1 -0
  105. package/src/tests/compare.test.ts +233 -0
  106. package/src/tests/fixtures/fake-model-provider.ts +2 -1
  107. package/src/tests/hunt-cli.test.ts +185 -0
  108. package/src/tests/hunt-install.test.ts +153 -0
  109. package/src/tests/hunt-maki.test.ts +525 -0
  110. package/src/tests/markdown.test.ts +3 -3
  111. package/src/tests/progress.test.ts +145 -0
  112. package/src/tests/propagation.test.ts +119 -0
  113. package/src/tests/review-resume.test.ts +60 -1
  114. package/src/tests/review-scope.test.ts +90 -19
  115. package/src/tests/review-scripts.test.ts +106 -218
  116. package/src/tests/server.test.ts +7 -4
  117. package/src/tests/storage.test.ts +194 -9
  118. package/src/core/renderers/report-renderer.ts +0 -66
  119. package/src/skill/example-review.md +0 -66
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Comparação A/B de duas revisões da mesma branch (funções puras).
3
+ *
4
+ * O objetivo é responder "o que mudou entre as duas execuções?" sem tentar
5
+ * eleger um vencedor: conta achados por severidade, cobertura, tempo e tokens,
6
+ * e lista o que apareceu/sumiu entre A e B.
7
+ */
8
+
9
+ import { SEVERITY_ORDER, type Severity } from "./types";
10
+
11
+ /** Finding normalizado para a comparação. */
12
+ export type ComparisonFinding = {
13
+ fingerprint: string;
14
+ rule: string;
15
+ severity: Severity;
16
+ file: string;
17
+ line?: number;
18
+ message: string;
19
+ watchdogs: readonly string[];
20
+ };
21
+
22
+ /** Snapshot de uma execução (uma das pontas do A/B). */
23
+ export type ReviewSnapshot = {
24
+ /** Rótulo exibido (`A`, `B` ou a variante). */
25
+ label: string;
26
+ slug: string;
27
+ branch: string;
28
+ variant?: string;
29
+ headSha?: string;
30
+ model?: string;
31
+ /** Achados presentes no último relatório. */
32
+ findings: readonly ComparisonFinding[];
33
+ bySeverity: Record<Severity, number>;
34
+ coverage: { watchdogs: number; files: number; overflow: number };
35
+ /** Duração do ciclo (`startedAt` → `reviewedAt`), quando medida. */
36
+ durationMs?: number;
37
+ /** Duração do prepare (`prepare-report.json`). */
38
+ prepareMs?: number;
39
+ /** Duração do harness maki (`hunt-report.json`). */
40
+ huntMs?: number;
41
+ tokens?: number;
42
+ costUsd?: number;
43
+ };
44
+
45
+ /** Resultado da comparação. */
46
+ export type ReviewComparison = {
47
+ branch: string;
48
+ headSha?: string;
49
+ a: ReviewSnapshot;
50
+ b: ReviewSnapshot;
51
+ onlyA: readonly ComparisonFinding[];
52
+ onlyB: readonly ComparisonFinding[];
53
+ common: readonly { a: ComparisonFinding; b: ComparisonFinding }[];
54
+ severityChanged: readonly { a: ComparisonFinding; b: ComparisonFinding }[];
55
+ };
56
+
57
+ /** Conta os achados por severidade. */
58
+ export function countBySeverity(
59
+ findings: readonly ComparisonFinding[],
60
+ ): Record<Severity, number> {
61
+ const counts: Record<Severity, number> = {
62
+ blocking: 0,
63
+ attention: 0,
64
+ suggestion: 0,
65
+ nit: 0,
66
+ };
67
+ for (const finding of findings) counts[finding.severity]++;
68
+ return counts;
69
+ }
70
+
71
+ /** Compara dois snapshots (ordem estável por severidade e caminho). */
72
+ export function buildComparison(a: ReviewSnapshot, b: ReviewSnapshot): ReviewComparison {
73
+ const aByKey = new Map(a.findings.map((finding) => [finding.fingerprint, finding]));
74
+ const bByKey = new Map(b.findings.map((finding) => [finding.fingerprint, finding]));
75
+ const onlyA = a.findings.filter((finding) => !bByKey.has(finding.fingerprint));
76
+ const onlyB = b.findings.filter((finding) => !aByKey.has(finding.fingerprint));
77
+ const common: { a: ComparisonFinding; b: ComparisonFinding }[] = [];
78
+ const severityChanged: { a: ComparisonFinding; b: ComparisonFinding }[] = [];
79
+
80
+ for (const finding of a.findings) {
81
+ const other = bByKey.get(finding.fingerprint);
82
+ if (!other) continue;
83
+ common.push({ a: finding, b: other });
84
+ if (finding.severity !== other.severity) {
85
+ severityChanged.push({ a: finding, b: other });
86
+ }
87
+ }
88
+
89
+ const sortFindings = (items: ComparisonFinding[]): ComparisonFinding[] =>
90
+ [...items].sort(
91
+ (x, y) =>
92
+ SEVERITY_ORDER.indexOf(x.severity) - SEVERITY_ORDER.indexOf(y.severity) ||
93
+ x.file.localeCompare(y.file) ||
94
+ (x.line ?? 0) - (y.line ?? 0) ||
95
+ x.rule.localeCompare(y.rule),
96
+ );
97
+
98
+ return {
99
+ branch: a.branch,
100
+ headSha: a.headSha ?? b.headSha,
101
+ a,
102
+ b,
103
+ onlyA: sortFindings(onlyA),
104
+ onlyB: sortFindings(onlyB),
105
+ common,
106
+ severityChanged: severityChanged.sort(
107
+ (x, y) =>
108
+ SEVERITY_ORDER.indexOf(x.a.severity) -
109
+ SEVERITY_ORDER.indexOf(y.a.severity) ||
110
+ x.a.file.localeCompare(y.a.file),
111
+ ),
112
+ };
113
+ }
114
+
115
+ /** Duração legível (`820ms`, `12.3s`, `2m05s`). */
116
+ function formatMs(ms: number | undefined): string {
117
+ if (ms === undefined || !Number.isFinite(ms)) return "—";
118
+ const safe = Math.max(0, Math.round(ms));
119
+ if (safe < 1_000) return `${safe}ms`;
120
+ const seconds = safe / 1_000;
121
+ if (seconds < 60) return `${seconds.toFixed(1)}s`;
122
+ const minutes = Math.floor(seconds / 60);
123
+ return `${minutes}m${String(Math.round(seconds % 60)).padStart(2, "0")}s`;
124
+ }
125
+
126
+ function formatDelta(
127
+ a: number | undefined,
128
+ b: number | undefined,
129
+ format: (value: number) => string,
130
+ ): string {
131
+ if (a === undefined || b === undefined) return "—";
132
+ const delta = b - a;
133
+ return `${delta > 0 ? "+" : ""}${format(delta)}`;
134
+ }
135
+
136
+ /** Linha da tabela de métricas. */
137
+ function metricRow(label: string, a: string, b: string, delta = "—"): string {
138
+ return `| ${label} | ${a} | ${b} | ${delta} |`;
139
+ }
140
+
141
+ /** Lista compacta de um conjunto de achados. */
142
+ function findingList(title: string, findings: readonly ComparisonFinding[]): string[] {
143
+ if (findings.length === 0) return [`## ${title}`, "", "_Nenhum._", ""];
144
+ const shown = findings.slice(0, 20);
145
+ const lines = [`## ${title} (${findings.length})`, ""];
146
+ for (const finding of shown) {
147
+ const line = finding.line !== undefined ? `:${finding.line}` : "";
148
+ lines.push(
149
+ `- **${finding.severity}** · \`${finding.file}${line}\` — ${finding.message}`,
150
+ );
151
+ }
152
+ if (findings.length > shown.length) {
153
+ lines.push(`- … +${findings.length - shown.length} achado(s)`);
154
+ }
155
+ lines.push("");
156
+ return lines;
157
+ }
158
+
159
+ /** Renderiza a comparação em Markdown (artefato e stdout). */
160
+ export function renderComparison(comparison: ReviewComparison): string {
161
+ const { a, b } = comparison;
162
+ const severityDelta = SEVERITY_ORDER.map((severity) =>
163
+ metricRow(
164
+ severity,
165
+ String(a.bySeverity[severity]),
166
+ String(b.bySeverity[severity]),
167
+ formatDelta(a.bySeverity[severity], b.bySeverity[severity], (value) =>
168
+ String(value),
169
+ ),
170
+ ),
171
+ );
172
+ const totalA = a.findings.length;
173
+ const totalB = b.findings.length;
174
+ const lines = [
175
+ `# Comparação A/B — \`${comparison.branch}\``,
176
+ "",
177
+ `**A:** ${labelOf(a)} · **B:** ${labelOf(b)}`,
178
+ ...(comparison.headSha
179
+ ? [`**Commit:** \`${comparison.headSha.slice(0, 7)}\``]
180
+ : []),
181
+ "",
182
+ "> Compara o que apareceu/sumiu entre as execuções. Não é veredito: confirme",
183
+ "> cada achado novo no código antes de considerá-lo ganho ou perda.",
184
+ "",
185
+ "## Métricas",
186
+ "",
187
+ `| Métrica | A | B | Δ |`,
188
+ `| --- | --- | --- | --- |`,
189
+ metricRow(
190
+ "Findings (presentes)",
191
+ String(totalA),
192
+ String(totalB),
193
+ formatDelta(totalA, totalB, (value) => String(value)),
194
+ ),
195
+ ...severityDelta,
196
+ metricRow(
197
+ "Cobertura (arquivos)",
198
+ String(a.coverage.files),
199
+ String(b.coverage.files),
200
+ ),
201
+ metricRow(
202
+ "Watchdogs",
203
+ String(a.coverage.watchdogs),
204
+ String(b.coverage.watchdogs),
205
+ ),
206
+ metricRow("Duração total", formatMs(a.durationMs), formatMs(b.durationMs)),
207
+ metricRow("Prepare", formatMs(a.prepareMs), formatMs(b.prepareMs)),
208
+ metricRow("Harness", formatMs(a.huntMs), formatMs(b.huntMs)),
209
+ metricRow(
210
+ "Tokens",
211
+ a.tokens !== undefined ? String(a.tokens) : "—",
212
+ b.tokens !== undefined ? String(b.tokens) : "—",
213
+ ),
214
+ metricRow(
215
+ "Custo",
216
+ a.costUsd !== undefined ? `$${a.costUsd.toFixed(4)}` : "—",
217
+ b.costUsd !== undefined ? `$${b.costUsd.toFixed(4)}` : "—",
218
+ ),
219
+ "",
220
+ ...findingList("Só em B (novos)", comparison.onlyB),
221
+ ...findingList("Só em A (não apareceram em B)", comparison.onlyA),
222
+ ];
223
+ if (comparison.severityChanged.length > 0) {
224
+ lines.push(`## Severidade mudou (${comparison.severityChanged.length})`, "");
225
+ for (const item of comparison.severityChanged.slice(0, 20)) {
226
+ lines.push(
227
+ `- \`${item.a.file}\` — ${item.a.rule}: **${item.a.severity} → ${item.b.severity}**`,
228
+ );
229
+ }
230
+ lines.push("");
231
+ }
232
+ return `${lines.join("\n").trimEnd()}\n`;
233
+ }
234
+
235
+ /** JSON estável da comparação (artefato para automação). */
236
+ export function comparisonToJson(comparison: ReviewComparison): string {
237
+ return `${JSON.stringify(comparison, null, 2)}\n`;
238
+ }
239
+
240
+ function labelOf(snapshot: ReviewSnapshot): string {
241
+ const variant = snapshot.variant ? ` (variante \`${snapshot.variant}\`)` : "";
242
+ return `\`${snapshot.slug}\`${variant} · ${snapshot.findings.length} finding(s)`;
243
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Pool de concorrência mínimo (biblioteca).
3
+ *
4
+ * Mantém no máximo `limit` promessas ativas e preserva a ordem dos resultados.
5
+ * Não trata erros: o chamador decide se cada item falha isoladamente.
6
+ */
7
+
8
+ /** Executa `mapper` para cada item com concorrência limitada. */
9
+ export async function mapPool<T, R>(
10
+ items: readonly T[],
11
+ limit: number,
12
+ mapper: (item: T, index: number) => Promise<R>,
13
+ ): Promise<R[]> {
14
+ const results = new Array<R>(items.length);
15
+ if (items.length === 0) return results;
16
+
17
+ let next = 0;
18
+ const workers = Array.from(
19
+ { length: Math.max(1, Math.min(limit, items.length)) },
20
+ async () => {
21
+ while (true) {
22
+ const index = next++;
23
+ if (index >= items.length) return;
24
+ results[index] = await mapper(items[index], index);
25
+ }
26
+ },
27
+ );
28
+ await Promise.all(workers);
29
+ return results;
30
+ }
@@ -19,7 +19,11 @@ export {
19
19
  DEFAULT_ALWAYS_INCLUDE_DOCS,
20
20
  DEFAULT_AST_MAX_FILE_BYTES,
21
21
  DEFAULT_AST_MAX_IMPACT_FILES,
22
+ DEFAULT_HUNT_CONCURRENCY,
23
+ DEFAULT_HUNT_RETRIES,
24
+ DEFAULT_HUNT_TIMEOUT_MS,
22
25
  DEFAULT_JEV_CONCURRENCY,
26
+ DEFAULT_MAKI_VERSION,
23
27
  DEFAULT_MAX_FILES_PER_BRIEF,
24
28
  } from "./defaults";
25
29
 
@@ -68,6 +72,8 @@ export type AiProviderOptions = Record<string, Record<string, unknown>>;
68
72
  export type HoundsmanAiFeatures = {
69
73
  /** Resume o contexto da PR (`context-summary`) quando o subagente não gerou. */
70
74
  context?: boolean;
75
+ /** Reconstrói a intenção da PR (`business-summary`) antes dos watchdogs. */
76
+ business?: boolean;
71
77
  /** Enriquece os briefs com um plano de revisão sugerido. */
72
78
  brief?: boolean;
73
79
  /** Gera os pontos técnicos estruturais do resumo final. */
@@ -95,7 +101,7 @@ export type HoundsmanAi = {
95
101
  model?: string;
96
102
  /** Base URL customizada do provider. */
97
103
  baseURL?: string;
98
- /** Chamadas simultâneas (default: 2). */
104
+ /** Chamadas simultâneas (default: 4). */
99
105
  concurrency?: number;
100
106
  /** Opções do AI SDK por provider (repassadas sem interpretação). */
101
107
  providerOptions?: AiProviderOptions;
@@ -122,6 +128,28 @@ export type HoundsmanWorktrees = {
122
128
  location?: "home" | "workspace" | string;
123
129
  };
124
130
 
131
+ /**
132
+ * Harness local da capability `hunt` (`houndsman hunt`) — opcional.
133
+ *
134
+ * Sem esta config, o comando usa defaults seguros: modelo derivado de `ai`,
135
+ * 8 processos simultâneos (uma onda) e timeout de 15 min por watchdog. A skill
136
+ * `/houndsman hunt` (harness externo) não depende deste bloco.
137
+ */
138
+ export type HoundsmanHunt = {
139
+ /** Desliga o harness local sem desinstalar o binário (default: ligado). */
140
+ enabled?: boolean;
141
+ /** Modelo no formato do maki (`provider/model-id`). Default: derivado de `ai`. */
142
+ model?: string;
143
+ /** Processos maki simultâneos (default: 8). */
144
+ concurrency?: number;
145
+ /** Timeout por watchdog em ms (default: 900_000). */
146
+ timeoutMs?: number;
147
+ /** Tentativas extras quando a saída do watchdog é inválida (default: 1). */
148
+ retries?: number;
149
+ /** Caminho explícito do binário (`HOUNDSMAN_MAKI_PATH` tem precedência). */
150
+ binaryPath?: string;
151
+ };
152
+
125
153
  /** Backend de persistência (reviews, findings e artefatos). */
126
154
  export type HoundsmanStorage = {
127
155
  /**
@@ -136,6 +164,27 @@ export type HoundsmanStorage = {
136
164
  path?: string;
137
165
  };
138
166
 
167
+ /**
168
+ * Regra de prioridade por caminho (ordenada; a primeira que casar vence).
169
+ *
170
+ * O risco do arquivo é `min(churn, maxRiskFromChurn) + bonus`. O `bonus` promove
171
+ * caminhos críticos (em linhas de churn equivalentes) e o `maxRiskFromChurn`
172
+ * satura o superchurn de caminhos de baixa criticidade: um frontend que
173
+ * reformatou 2.000 linhas não escala além do teto, mas um ajuste pequeno no
174
+ * mesmo caminho mantém o peso natural.
175
+ */
176
+ export type HoundsmanPriority = {
177
+ /** Padrão sobre o caminho relativo (ex.: `/^server\\/src\\/db\\//`). */
178
+ pattern: RegExp;
179
+ /** Peso em linhas de churn equivalentes (≥ 0; default: 0). */
180
+ bonus?: number;
181
+ /**
182
+ * Teto do churn considerado no risco (em linhas equivalentes). Acima dele os
183
+ * arquivos do caminho empatam e só o `bonus` diferencia. Default: sem teto.
184
+ */
185
+ maxRiskFromChurn?: number;
186
+ };
187
+
139
188
  /**
140
189
  * Configuração completa do motor de revisão para um projeto.
141
190
  *
@@ -159,6 +208,11 @@ export type HoundsmanConfig<TId extends string = string> = {
159
208
  exclusions: readonly RegExp[];
160
209
  /** Mapeamentos path → documentação relevante. */
161
210
  docMappings: readonly (readonly [RegExp, readonly string[]])[];
211
+ /**
212
+ * Prioridade por caminho (opcional). Ordena arquivos no orçamento, no rateio
213
+ * do diff e no recorte do `general` — a primeira regra que casar vence.
214
+ */
215
+ priorities?: readonly HoundsmanPriority[];
162
216
  /** Docs sempre incluídos nos briefs (default: `DEFAULT_ALWAYS_INCLUDE_DOCS`). */
163
217
  alwaysIncludeDocs?: readonly string[];
164
218
  /** Limites de triage para PRs grandes (default: `DEFAULT_MAX_FILES_PER_BRIEF`). */
@@ -169,6 +223,8 @@ export type HoundsmanConfig<TId extends string = string> = {
169
223
  ast?: HoundsmanAst;
170
224
  /** Camada de IA (briefs, contexto, classificação, pré-análise e verify). */
171
225
  ai?: HoundsmanAi;
226
+ /** Harness local da capability `hunt` (maki) — opcional. */
227
+ hunt?: HoundsmanHunt;
172
228
  };
173
229
 
174
230
  /** Entrada de `defineHoundsmanConfig` — `watchdogIds` é derivado, não informado. */
@@ -38,11 +38,72 @@ export const DEFAULT_AI_PROVIDER = "deepseek" as const;
38
38
  /** Modelo default da camada de IA. */
39
39
  export const DEFAULT_AI_MODEL = "deepseek-v4-pro";
40
40
 
41
- /** Concorrência default das chamadas de IA. */
42
- export const DEFAULT_AI_CONCURRENCY = 2;
41
+ /**
42
+ * Concorrência default das chamadas de IA do prepare.
43
+ *
44
+ * Vale principalmente para os planos de brief (1 chamada por watchdog): com 4,
45
+ * 8 watchdogs fecham em 2 rodadas em vez de 4.
46
+ */
47
+ export const DEFAULT_AI_CONCURRENCY = 4;
43
48
 
44
49
  /** Timeout default de uma chamada de modelo (ms). */
45
50
  export const DEFAULT_AI_TIMEOUT_MS = 120_000;
46
51
 
47
52
  /** Teto default de chamadas de IA por review. */
48
53
  export const DEFAULT_AI_MAX_CALLS_PER_REVIEW = 40;
54
+
55
+ /**
56
+ * Processos maki simultâneos no harness local (`houndsman hunt`).
57
+ *
58
+ * 8 = uma onda para os watchdogs usuais (domínios + `general`), em vez de 2.
59
+ */
60
+ export const DEFAULT_HUNT_CONCURRENCY = 8;
61
+
62
+ /** Timeout default de um watchdog no harness local (15 min). */
63
+ export const DEFAULT_HUNT_TIMEOUT_MS = 900_000;
64
+
65
+ /**
66
+ * Trechos alterados injetados no brief (watchdog de domínio).
67
+ *
68
+ * Substituem a leitura do arquivo inteiro: o subagente começa pelo diff e só
69
+ * abre o arquivo para contexto adjacente. O orçamento é distribuído entre os
70
+ * arquivos (metade em partes iguais, o resto por churn) — antes, os primeiros
71
+ * arquivos consumiam o teto e o subagente lia os demais à mão.
72
+ */
73
+ export const DEFAULT_BRIEF_SNIPPET_CHARS = 24_000;
74
+ export const DEFAULT_BRIEF_SNIPPET_CHARS_PER_FILE = 4_000;
75
+
76
+ /** Mesmos limites para o revisor de fluxo (`general`), que vê todos os arquivos. */
77
+ export const GENERAL_BRIEF_SNIPPET_CHARS = 32_000;
78
+ export const GENERAL_BRIEF_SNIPPET_CHARS_PER_FILE = 5_000;
79
+
80
+ /** Fração do teto dividida igualmente entre os arquivos (o resto vai por churn). */
81
+ export const BRIEF_SNIPPET_FLOOR_RATIO = 0.5;
82
+
83
+ /** Piso de chars por arquivo no rateio igual (evita trechos inúteis). */
84
+ export const BRIEF_SNIPPET_MIN_PER_FILE = 120;
85
+
86
+ /**
87
+ * A partir daqui o `general` revisa por **fluxo** em vez de amostrar arquivos.
88
+ *
89
+ * Em PRs grandes, 40 arquivos com ~800 chars cada viram amostragem: melhor ver
90
+ * os arquivos que participam dos fluxos do modelo de intenção (com diff de
91
+ * verdade) e declarar o resto em `skipped`.
92
+ */
93
+ export const GENERAL_FLOW_MIN_FILES = 40;
94
+ export const GENERAL_FLOW_MIN_LINES = 2_000;
95
+
96
+ /** Arquivos do recorte de fluxo do `general`. */
97
+ export const GENERAL_FLOW_MAX_FILES = 15;
98
+
99
+ /** Diff por arquivo no recorte de fluxo — profundidade em vez de cobertura. */
100
+ export const GENERAL_FLOW_SNIPPET_CHARS_PER_FILE = 2_000;
101
+
102
+ /** Retentativas default quando a saída do watchdog é inválida. */
103
+ export const DEFAULT_HUNT_RETRIES = 1;
104
+
105
+ /**
106
+ * Versão do maki fixada pelo houndsman. O binário é baixado por
107
+ * `houndsman hunt install`; `HOUNDSMAN_MAKI_VERSION` sobrescreve.
108
+ */
109
+ export const DEFAULT_MAKI_VERSION = "0.6.5";
@@ -5,6 +5,7 @@
5
5
  * orçamento sem depender do serviço de classificação.
6
6
  */
7
7
 
8
+ import type { HoundsmanPriority } from "../config";
8
9
  import type { ChurnMap } from "../types";
9
10
 
10
11
  /** Churn total (adições + remoções) de um arquivo; 0 quando ausente. */
@@ -13,10 +14,66 @@ export function churnOf(filePath: string, churn: ChurnMap | undefined): number {
13
14
  return entry ? entry.additions + entry.deletions : 0;
14
15
  }
15
16
 
16
- /** Ordena os arquivos por risco (churn desc; empate pelo caminho). */
17
- export function sortByRisk(files: readonly string[], churn?: ChurnMap): string[] {
17
+ /** Regra de prioridade do caminho (a primeira que casar vence). */
18
+ export function priorityFor(
19
+ filePath: string,
20
+ priorities: readonly HoundsmanPriority[] | undefined,
21
+ ): HoundsmanPriority | undefined {
22
+ for (const rule of priorities ?? []) {
23
+ // Regex global mantém `lastIndex` entre chamadas — zera para não pular match.
24
+ if (rule.pattern.global) rule.pattern.lastIndex = 0;
25
+ if (rule.pattern.test(filePath)) return rule;
26
+ }
27
+ return undefined;
28
+ }
29
+
30
+ /** Bônus da regra (linhas de churn equivalentes; ausente/inválido = 0). */
31
+ function bonusOf(rule: HoundsmanPriority | undefined): number {
32
+ const bonus = rule?.bonus;
33
+ return bonus !== undefined && Number.isFinite(bonus) ? Math.max(0, bonus) : 0;
34
+ }
35
+
36
+ /** Bônus de prioridade do caminho (primeira regra que casar vence). */
37
+ export function priorityBonus(
38
+ filePath: string,
39
+ priorities: readonly HoundsmanPriority[] | undefined,
40
+ ): number {
41
+ return bonusOf(priorityFor(filePath, priorities));
42
+ }
43
+
44
+ /** Teto de churn da regra (ausente/inválido = sem teto). */
45
+ function churnLimitOf(rule: HoundsmanPriority | undefined): number {
46
+ const limit = rule?.maxRiskFromChurn;
47
+ return limit !== undefined && Number.isFinite(limit) && limit > 0
48
+ ? limit
49
+ : Number.POSITIVE_INFINITY;
50
+ }
51
+
52
+ /**
53
+ * Risco do arquivo: `min(churn, maxRiskFromChurn) + bonus`.
54
+ *
55
+ * O teto existe para superchurn de caminho de baixa criticidade (frontend que
56
+ * reformatou uma tabela inteira) não competir com churn real de caminho crítico;
57
+ * abaixo do teto, o peso do caminho é o churn natural.
58
+ */
59
+ export function riskOf(
60
+ filePath: string,
61
+ churn: ChurnMap | undefined,
62
+ priorities?: readonly HoundsmanPriority[],
63
+ ): number {
64
+ const rule = priorityFor(filePath, priorities);
65
+ const churnRisk = Math.min(churnOf(filePath, churn), churnLimitOf(rule));
66
+ return churnRisk + bonusOf(rule);
67
+ }
68
+
69
+ /** Ordena os arquivos por risco (churn + prioridade; empate pelo caminho). */
70
+ export function sortByRisk(
71
+ files: readonly string[],
72
+ churn?: ChurnMap,
73
+ priorities?: readonly HoundsmanPriority[],
74
+ ): string[] {
18
75
  return [...files].sort((a, b) => {
19
- const delta = churnOf(b, churn) - churnOf(a, churn);
76
+ const delta = riskOf(b, churn, priorities) - riskOf(a, churn, priorities);
20
77
  return delta !== 0 ? delta : a.localeCompare(b);
21
78
  });
22
79
  }
@@ -33,8 +90,9 @@ export function selectByBudget(
33
90
  files: readonly string[],
34
91
  churn: ChurnMap | undefined,
35
92
  maxFiles: number,
93
+ priorities?: readonly HoundsmanPriority[],
36
94
  ): BudgetSelection {
37
- const ordered = sortByRisk(files, churn);
95
+ const ordered = sortByRisk(files, churn, priorities);
38
96
  if (!Number.isFinite(maxFiles) || maxFiles <= 0 || ordered.length <= maxFiles) {
39
97
  return { selected: ordered, overflow: [] };
40
98
  }
@@ -5,6 +5,7 @@
5
5
  * consultas usadas pelo orquestrador (arquivos por watchdog, orçamento, churn).
6
6
  */
7
7
 
8
+ import type { HoundsmanPriority } from "../config";
8
9
  import type { ClassificationResult } from "../file-classifier";
9
10
  import type { ChurnMap } from "../types";
10
11
  import { type BudgetSelection, selectByBudget } from "./budget";
@@ -72,8 +73,17 @@ export class Classification {
72
73
  }
73
74
 
74
75
  /** Aplica o orçamento aos arquivos de um watchdog. */
75
- budgetFor(id: string, maxFiles: number): BudgetSelection {
76
- return selectByBudget(this.watchdogsFor(id), this.data.churn, maxFiles);
76
+ budgetFor(
77
+ id: string,
78
+ maxFiles: number,
79
+ priorities?: readonly HoundsmanPriority[],
80
+ ): BudgetSelection {
81
+ return selectByBudget(
82
+ this.watchdogsFor(id),
83
+ this.data.churn,
84
+ maxFiles,
85
+ priorities,
86
+ );
77
87
  }
78
88
 
79
89
  /** Totais de churn do diff. */
@@ -34,6 +34,10 @@ export function buildCarryover(
34
34
  message: finding.message,
35
35
  action: finding.action,
36
36
  evidence: finding.evidence,
37
+ scenario: finding.scenario,
38
+ impact: finding.impact,
39
+ confidence: finding.confidence,
40
+ basis: finding.basis,
37
41
  foundAtSha: finding.foundAtSha,
38
42
  watchdogs: [...finding.watchdogIds],
39
43
  }));