@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
@@ -157,7 +157,7 @@ export function FindingCard({
157
157
  </span>
158
158
  )}
159
159
  {!finding.present && (
160
- <span class="tag" title="Não aparece no último relatório consolidado">
160
+ <span class="tag" title="Não aparece na última revisão">
161
161
  ausente
162
162
  </span>
163
163
  )}
@@ -206,6 +206,26 @@ export function FindingCard({
206
206
  <div class="card-body">
207
207
  <div class="meta-row">{finding.rule}</div>
208
208
 
209
+ {finding.scenario && (
210
+ <div class="action-block">
211
+ <Sparkles size={16} class="action-icon" />
212
+ <div class="action-text">
213
+ <span class="action-label">Cenário:</span>
214
+ <MarkdownText text={finding.scenario} />
215
+ </div>
216
+ </div>
217
+ )}
218
+
219
+ {finding.impact && (
220
+ <div class="action-block">
221
+ <Sparkles size={16} class="action-icon" />
222
+ <div class="action-text">
223
+ <span class="action-label">Impacto:</span>
224
+ <MarkdownText text={finding.impact} />
225
+ </div>
226
+ </div>
227
+ )}
228
+
209
229
  <div class="action-block">
210
230
  <Sparkles size={16} class="action-icon" />
211
231
  <div class="action-text">
@@ -30,6 +30,11 @@ function RunItem({
30
30
  <span class="run-top">
31
31
  <BranchIcon size={14} class="run-icon" />
32
32
  <span class="run-branch">{run.branch}</span>
33
+ {run.variant ? (
34
+ <span class="run-variant" title="Variante A/B">
35
+ {run.variant}
36
+ </span>
37
+ ) : null}
33
38
  </span>
34
39
  <span class="run-meta">
35
40
  <span class="run-when">
@@ -83,13 +83,13 @@ export function describeRunStatus(
83
83
 
84
84
  /** Ordem canônica dos tipos de artefato na lista da UI. */
85
85
  const KIND_ORDER: ArtifactKind[] = [
86
- "review",
86
+ "compare",
87
87
  "resume",
88
88
  "technical",
89
- "domain-review",
90
89
  "verify",
91
90
  "brief",
92
91
  "manifest",
92
+ "prepare",
93
93
  "classification",
94
94
  "candidates",
95
95
  "ast",
@@ -100,6 +100,8 @@ const KIND_ORDER: ArtifactKind[] = [
100
100
  "context",
101
101
  "context-brief",
102
102
  "context-summary",
103
+ "business",
104
+ "business-brief",
103
105
  "finding-report",
104
106
  "other",
105
107
  ];
@@ -107,12 +109,12 @@ const KIND_ORDER: ArtifactKind[] = [
107
109
  /** Grupo visual de cada tipo (relatórios, briefs, núcleo, contexto…). */
108
110
  const GROUP_BY_KIND: Record<ArtifactKind, { key: string; label: string; order: number }> =
109
111
  {
110
- review: { key: "report", label: "Relatórios", order: 0 },
112
+ compare: { key: "report", label: "Relatórios", order: 0 },
111
113
  resume: { key: "report", label: "Relatórios", order: 0 },
112
114
  technical: { key: "core", label: "Núcleo da revisão", order: 3 },
113
- "domain-review": { key: "report", label: "Relatórios", order: 0 },
114
115
  verify: { key: "verify", label: "Verificação", order: 1 },
115
116
  brief: { key: "brief", label: "Briefs (auditoria)", order: 2 },
117
+ prepare: { key: "core", label: "Núcleo da revisão", order: 3 },
116
118
  manifest: { key: "core", label: "Núcleo da revisão", order: 3 },
117
119
  classification: { key: "core", label: "Núcleo da revisão", order: 3 },
118
120
  candidates: { key: "core", label: "Núcleo da revisão", order: 3 },
@@ -124,17 +126,19 @@ const GROUP_BY_KIND: Record<ArtifactKind, { key: string; label: string; order: n
124
126
  context: { key: "context", label: "Contexto da PR", order: 5 },
125
127
  "context-brief": { key: "context", label: "Contexto da PR", order: 5 },
126
128
  "context-summary": { key: "context", label: "Contexto da PR", order: 5 },
129
+ business: { key: "business", label: "Intenção da PR", order: 5 },
130
+ "business-brief": { key: "business", label: "Intenção da PR", order: 5 },
127
131
  "finding-report": { key: "other", label: "Outros", order: 6 },
128
132
  other: { key: "other", label: "Outros", order: 6 },
129
133
  };
130
134
 
131
135
  /** Rótulo curto do tipo (badge do item). */
132
136
  export const ARTIFACT_KIND_LABELS: Record<ArtifactKind, string> = {
133
- review: "relatório",
137
+ compare: "comparação",
134
138
  resume: "resumo",
135
139
  technical: "pontos técnicos",
136
- "domain-review": "domínio",
137
140
  verify: "verify",
141
+ prepare: "prepare",
138
142
  brief: "brief",
139
143
  manifest: "manifesto",
140
144
  classification: "classificação",
@@ -147,6 +151,8 @@ export const ARTIFACT_KIND_LABELS: Record<ArtifactKind, string> = {
147
151
  context: "contexto",
148
152
  "context-brief": "brief ctx",
149
153
  "context-summary": "resumo ctx",
154
+ business: "intenção",
155
+ "business-brief": "brief intenção",
150
156
  "finding-report": "findings",
151
157
  other: "outro",
152
158
  };
@@ -161,16 +167,16 @@ export function displayArtifactName(meta: ArtifactMeta): string {
161
167
  /** Descrição curta do artefato (cabeçalho do painel). */
162
168
  export function describeArtifact(meta: ArtifactMeta): string {
163
169
  switch (meta.kind) {
164
- case "review":
165
- return "Relatório consolidado da revisão — o entregável.";
170
+ case "compare":
171
+ return "Comparação A/B entre duas runs: achados novos/sumidos, cobertura, tempo e tokens.";
166
172
  case "resume":
167
- return "Resumo final da revisão: visão de negócio e estrutura técnica (leitura humana).";
173
+ return "Resumo final: negócio, pontos técnicos, cobertura por domínio e conclusão.";
168
174
  case "technical":
169
175
  return "Destaques estruturais da mudança (módulos, contratos, dependências) usados no resumo final.";
170
- case "domain-review":
171
- return "Relatório isolado do domínio: achados daquele watchdog.";
172
176
  case "verify":
173
177
  return "Resultado do `findings verify` (resolvidos, persistentes e incertos).";
178
+ case "prepare":
179
+ return "Duração por etapa do prepare e uso de IA (diagnóstico de performance).";
174
180
  case "brief":
175
181
  return "Instruções enviadas ao subagente (auditoria do prompt).";
176
182
  case "manifest":
@@ -189,6 +195,10 @@ export function describeArtifact(meta: ArtifactMeta): string {
189
195
  return "Brief do subagente que resume o contexto da PR.";
190
196
  case "context-summary":
191
197
  return "Resumo da PR (objetivo, motivação e alterações) gerado no Passo 0.";
198
+ case "business":
199
+ return "Modelo de intenção da PR: o que ela deve entregar (fluxos, invariantes, contratos e estados).";
200
+ case "business-brief":
201
+ return "Brief do passo de intenção (subagente na skill / pré-passe maki).";
192
202
  default:
193
203
  return meta.name;
194
204
  }
@@ -240,7 +240,7 @@ export function sectionBlocks(text: string, title: string): MarkdownBlock[] {
240
240
  const FILE_EXTENSION =
241
241
  /\.(?:[cm]?[jt]sx?|json|md|ya?ml|toml|css|scss|html?|py|go|rs|java|kt|cs|php|rb|sh|sql|txt|lock)$/i;
242
242
 
243
- /** Caminho/arquivo dentro de um texto corrido (`src/foo.ts`, `review.md`). */
243
+ /** Caminho/arquivo dentro de um texto corrido (`src/foo.ts`, `manifest.json`). */
244
244
  const PATH_TOKEN =
245
245
  /(?:[\w.-]+[\\/])*[\w-]+\.(?:[cm]?[jt]sx?|json|md|ya?ml|toml|css|scss|html?|py|go|rs|java|kt|cs|php|rb|sh|sql|txt|lock)\b/g;
246
246
 
@@ -32,6 +32,14 @@ export type Finding = {
32
32
  message: string;
33
33
  action: string;
34
34
  evidence?: string;
35
+ /** Gatilho concreto do gap (fora de regra). */
36
+ scenario?: string;
37
+ /** Consequência observável do gap. */
38
+ impact?: string;
39
+ /** Confiança do revisor (0–1). */
40
+ confidence?: number;
41
+ /** Origem do achado: regra, lógica ou domínio. */
42
+ basis?: "rule" | "logic" | "domain";
35
43
  count: number;
36
44
  present: boolean;
37
45
  decision: Decision;
@@ -59,17 +67,19 @@ export type ArtifactKind =
59
67
  | "context"
60
68
  | "context-brief"
61
69
  | "context-summary"
70
+ | "business"
71
+ | "business-brief"
62
72
  | "finding-report"
63
- | "review"
64
73
  | "resume"
65
74
  | "technical"
66
- | "domain-review"
67
75
  | "verify"
68
76
  | "ast"
69
77
  | "ast-impact"
70
78
  | "ai"
71
79
  | "ai-usage"
72
80
  | "ai-brief"
81
+ | "prepare"
82
+ | "compare"
73
83
  | "other";
74
84
 
75
85
  /** Metadados de artefato retornados pela API (sem conteúdo). */
@@ -83,6 +93,8 @@ export type ArtifactMeta = {
83
93
  export type RunSummary = {
84
94
  slug: string;
85
95
  branch: string;
96
+ /** Variante A/B (ausente = run principal da branch). */
97
+ variant?: string;
86
98
  /** Worktree onde a run foi executada (ausente = clone principal). */
87
99
  worktreePath?: string;
88
100
  prNumber?: number;
@@ -48,6 +48,8 @@ import { fetchPrComments, type PrCommentsResponse } from "./pr-comments";
48
48
  export type RunListItem = {
49
49
  slug: string;
50
50
  branch: string;
51
+ /** Variante A/B (ausente = run principal da branch). */
52
+ variant?: string;
51
53
  /** Worktree onde a run foi executada (ausente = clone principal). */
52
54
  worktreePath?: string;
53
55
  /** PR revisada (quando iniciada via `--pr`). */
@@ -63,6 +65,8 @@ export type RunListItem = {
63
65
  export type RunDetail = {
64
66
  slug: string;
65
67
  branch: string;
68
+ /** Variante A/B (ausente = run principal da branch). */
69
+ variant?: string;
66
70
  /** Worktree onde a run foi executada (ausente = clone principal). */
67
71
  worktreePath?: string;
68
72
  /** PR revisada (quando iniciada via `--pr`). */
@@ -129,6 +133,7 @@ export class RunRepository {
129
133
  return this.storage.listReviews(this.repo.key).map((review) => {
130
134
  const state = this.storage.readFindings(review.id);
131
135
  return {
136
+ variant: review.variant,
132
137
  slug: review.slug,
133
138
  branch: review.branch,
134
139
  worktreePath: review.worktreePath,
@@ -144,11 +149,11 @@ export class RunRepository {
144
149
 
145
150
  /** Mapa branch → slug das runs (usado para casar PRs com execuções). */
146
151
  branchIndex(): Map<string, string> {
147
- return new Map(
148
- this.storage
149
- .listReviews(this.repo.key)
150
- .map((review) => [review.branch, review.slug]),
151
- );
152
+ const index = new Map<string, string>();
153
+ for (const review of this.reviewsForBranchLinks()) {
154
+ if (!index.has(review.branch)) index.set(review.branch, review.slug);
155
+ }
156
+ return index;
152
157
  }
153
158
 
154
159
  /** Mapa branch → run + worktree (sidebar de PRs). */
@@ -156,15 +161,30 @@ export class RunRepository {
156
161
  string,
157
162
  { slug: string; worktreePath?: string; prNumber?: number }
158
163
  > {
159
- return new Map(
160
- this.storage.listReviews(this.repo.key).map((review) => [
161
- review.branch,
162
- {
163
- slug: review.slug,
164
- worktreePath: review.worktreePath,
165
- prNumber: review.prNumber,
166
- },
167
- ]),
164
+ const index = new Map<
165
+ string,
166
+ { slug: string; worktreePath?: string; prNumber?: number }
167
+ >();
168
+ for (const review of this.reviewsForBranchLinks()) {
169
+ if (index.has(review.branch)) continue;
170
+ index.set(review.branch, {
171
+ slug: review.slug,
172
+ worktreePath: review.worktreePath,
173
+ prNumber: review.prNumber,
174
+ });
175
+ }
176
+ return index;
177
+ }
178
+
179
+ /**
180
+ * Runs ordenadas para links de PR: a principal da branch primeiro (uma
181
+ * variante A/B não deve "roubar" o atalho da PR), depois as mais recentes.
182
+ */
183
+ private reviewsForBranchLinks(): ReviewRecord[] {
184
+ return [...this.storage.listReviews(this.repo.key)].sort(
185
+ (a, b) =>
186
+ Number(Boolean(a.variant)) - Number(Boolean(b.variant)) ||
187
+ b.updatedAt.localeCompare(a.updatedAt),
168
188
  );
169
189
  }
170
190
 
@@ -180,6 +200,7 @@ export class RunRepository {
180
200
  }>(review, "classification.json");
181
201
  return {
182
202
  slug: review.slug,
203
+ variant: review.variant,
183
204
  branch: review.branch,
184
205
  worktreePath: review.worktreePath,
185
206
  prNumber: review.prNumber,
@@ -77,6 +77,8 @@ body.resizing-sidebar { cursor:col-resize; user-select:none; }
77
77
  .run-icon { flex:none; margin-top:2px; color:var(--muted-2); }
78
78
  .run.active .run-icon { color:var(--accent); }
79
79
  .run-branch { font-size:12.5px; font-weight:600; line-height:1.35; word-break:break-word; }
80
+ .run-variant { flex:none; align-self:flex-start; padding:1px 6px; border-radius:999px; font-family:var(--font-mono);
81
+ font-size:9.5px; color:var(--accent); background:var(--accent-soft); border:1px solid var(--accent-line); }
80
82
  .run-meta { display:flex; align-items:center; gap:10px; font-family:var(--font-mono); font-size:10.5px; color:var(--muted); }
81
83
  .run-when { display:inline-flex; align-items:center; gap:6px; }
82
84
  .run-count { margin-left:auto; color:var(--muted-2); }
@@ -1,7 +1,8 @@
1
1
  # Formato de Saída dos Watchdogs
2
2
 
3
3
  Todo watchdog DEVE gravar **um arquivo JSON** com seus achados. O `report-builder.ts`
4
- consolida os JSONs no artefato `review.md` do storage global.
4
+ valida e deduplica os JSONs; o `--finalize` grava a triagem no storage e o
5
+ `review-resume.md` com a cobertura por domínio.
5
6
 
6
7
  > **Não** escrever markdown livre no chat. O único output consumido pelo pipeline é o JSON.
7
8
 
@@ -33,7 +34,11 @@ staging global da review:
33
34
  "line": 5,
34
35
  "message": "Arquivo de rota importa `db` e executa query inline.",
35
36
  "action": "Mover a query para o service correspondente.",
36
- "evidence": "import { db } from '../db'"
37
+ "evidence": "import { db } from '../db'",
38
+ "scenario": "request autenticada em /orders executa a query na rota",
39
+ "impact": "acoplamento e bypass das regras do service",
40
+ "confidence": 0.9,
41
+ "basis": "logic"
37
42
  }
38
43
  ]
39
44
  }
@@ -44,7 +49,7 @@ staging global da review:
44
49
  | Campo | Tipo | Obrigatório | Descrição |
45
50
  |---|---|---|---|
46
51
  | `watchdogId` | string | ✅ | Id do watchdog (deve casar com uma chave de `.houndsman/watchdogs/index.ts`) |
47
- | `reviewedFiles` | string[] | ✅ | Arquivos efetivamente inspecionados |
52
+ | `reviewedFiles` | string[] | ✅ | Arquivos **do seu domínio** efetivamente revisados (subconjunto de `## Arquivos do seu domínio`; não inclua chamadores/importadores nem arquivos de outros domínios) |
48
53
  | `skipped` | string[] | ✅ | Arquivos do domínio que NÃO foram lidos + motivo |
49
54
  | `findings` | object[] | ✅ | Achados (vazio se nada encontrado) |
50
55
  | `findings[].rule` | string | ✅ | Regra violada (conforme o brief do watchdog) |
@@ -54,6 +59,10 @@ staging global da review:
54
59
  | `findings[].message` | string | ✅ | O que está errado (curto, específico) |
55
60
  | `findings[].action` | string | ✅ | O que fazer |
56
61
  | `findings[].evidence` | string | — | Trecho que comprova o achado |
62
+ | `findings[].scenario` | string | — | Gatilho concreto que expõe o gap (ex.: "renovação de assinatura Pix sem split") |
63
+ | `findings[].impact` | string | — | Consequência observável (ex.: "cobrança recriada sem o split configurado") |
64
+ | `findings[].confidence` | number | — | Confiança do revisor (0–1) |
65
+ | `findings[].basis` | `"rule" \| "logic" \| "domain"` | — | Origem do achado; inferida pelo prefixo `Lógica:`/`Domínio:` |
57
66
 
58
67
  ## Severidade
59
68
 
@@ -68,7 +77,14 @@ staging global da review:
68
77
 
69
78
  - Sem achados → `"findings": []` (arquivo **obrigatório**).
70
79
  - Um achado por regra violada; não agrupar regras diferentes.
80
+ - **Teto de 20 achados por watchdog.** Se houver mais, mantenha os de maior severidade e
81
+ agrupe ocorrências idênticas (mesmo trecho + mesma regra) em um único item. O JSON precisa
82
+ caber inteiro — relatório cortado perde achados.
83
+ - `reviewedFiles ∪ skipped` deve cobrir **todo** o escopo do brief. O que não foi revisado
84
+ entra em `skipped` com o motivo (`budget`, `truncated`, `fora do fluxo`).
71
85
  - `file:line` sempre que possível; `evidence` quando o achado não é óbvio.
86
+ - **Trecho truncado** (`… [trecho truncado]`): sem abrir o arquivo para ver o restante, não
87
+ afirme violação sobre ele — reduza a `confidence` ou não reporte.
72
88
  - **Escopo do diff:** a `line` deve cair dentro das faixas listadas em
73
89
  *Escopo do diff (símbolos alterados)* no brief. Código fora delas é contexto — não é
74
90
  achado desta revisão. Se a alteração quebra código não alterado, ancore o finding na
@@ -76,14 +92,24 @@ staging global da review:
76
92
  - Achado **sem linha** só quando for inerente à alteração (vira comentário de arquivo).
77
93
  - Não inventar achados — se não houver violação, o array fica vazio.
78
94
 
79
- ## Findings emergentes (opt-in)
95
+ ## Findings emergentes (obrigatórios no `general`)
80
96
 
81
- Quando o watchdog declara `allowEmergentFindings: true`, você pode reportar falhas de
82
- **lógica** ou **domínio** que não correspondem a uma regra explícita usando os prefixos
83
- reservados:
97
+ O watchdog `general` (id reservado, sempre ativo) **deve** reportar gaps de lógica e domínio
98
+ que não correspondem a uma regra explícita, usando os prefixos reservados:
84
99
 
85
100
  - `Lógica: <descrição>` — nome/contrato vs comportamento, fluxos, estados, bordas.
86
- - `Domínio: <descrição>` — transições, idempotência, invariantes, arredondamento, timezone.
101
+ - `Domínio: <descrição>` — transições, idempotência, invariantes, arredondamento, timezone,
102
+ paridade de capacidade, propagação de credencial.
87
103
 
88
- Esses findings continuam exigindo `evidence` e têm severidade máxima `attention`. Sem o
89
- opt-in, todo finding precisa citar uma regra existente.
104
+ Demonstre o gap em três partes:
105
+
106
+ 1. `scenario` — o gatilho concreto (ex.: "`AsaasSplit` recebe `RenewAutomaticPix`").
107
+ 2. `message` — o comportamento observado e o invariante/regra violado.
108
+ 3. `evidence` — o trecho que comprova.
109
+
110
+ **Severidade:** fora de regra, `blocking` só é aceito com `scenario` **e** `evidence`. Sem os
111
+ dois, o motor rebaixa para `attention` (e registra um aviso). Regras explícitas mantêm a
112
+ severidade declarada pelo watchdog.
113
+
114
+ Os demais watchdogs podem usar os mesmos prefixos apenas quando declararem
115
+ `allowEmergentFindings: true`.
@@ -14,6 +14,16 @@ Um único skill (`/houndsman`), quatro capabilities (detalhadas em `hunt.md`, `t
14
14
  | `fix` | Correção dos findings marcados como `execute`. |
15
15
  | `verify` | Confere se as violações foram resolvidas, sem re-revisar a PR. |
16
16
 
17
+ ## Dois caminhos para o `hunt`
18
+
19
+ | Caminho | Quem usa | Requisitos |
20
+ |---|---|---|
21
+ | Skill `/houndsman hunt` | agente externo (Copilot/Claude/etc.) | nenhum |
22
+ | CLI `houndsman hunt` | operador/CI, sem agente | `houndsman hunt install` |
23
+
24
+ Os dois usam o mesmo `prepare`/`finalize`, o mesmo staging e o mesmo schema de findings.
25
+ O CLI só substitui o despacho manual dos subagentes por processos maki headless.
26
+
17
27
  ## Terminologia
18
28
 
19
29
  - **houndsman** — o orquestrador/treinador (CLI + motor).
@@ -35,24 +45,29 @@ Um único skill (`/houndsman`), quatro capabilities (detalhadas em `hunt.md`, `t
35
45
 
36
46
  | Passo | Comando | Quem roda | Resultado |
37
47
  |---|---|---|---|
38
- | 0. Contexto | `bun run houndsman context` | subagente | `context.json` + `context-brief.md` → `context-summary.json` |
39
- | 1. Preparar | `bun run houndsman` | skill | `classification.json` + `candidates.json` + `briefs/` + `manifest.json` |
40
- | 2. Revisar | *(subagentes)* | skill | `findings/<watchdog>.json` (um por watchdog) |
41
- | 3. Finalizar | `bun run houndsman --finalize` | skill | `review.md` |
42
-
43
- > **Alvo = PR** (ex.: “revise a PR 456”): troque os passos por `houndsman context --pr 456`,
44
- > `houndsman --pr 456` e `houndsman --pr 456 --finalize`. O prepare cria um **worktree**
45
- > detached em `<repo>/.houndsman/worktrees/<repo>/<branch>`; os briefs trazem
46
- > `**Worktree:**` e os subagentes devem ler essa árvore (nunca o clone principal).
48
+ | 0. Contexto | `houndsman --pr` (via `gh`) | prepare | `context.json` + `context-brief.md` → `context-summary.json` (IA; sem IA, subagente) |
49
+ | 1. Preparar | `bun run houndsman` | skill | `classification.json` + `candidates.json` + `business-summary.json` + `briefs/` + `manifest.json` |
50
+ | 1.5. Intenção | *(subagente)* | skill, só sem IA | `business-summary.json` enriquecido (só quando não veio da IA) |
51
+ | 2. Revisar | *(subagentes)* | skill | `findings/<watchdog>.json` (inclui o `general`, sempre ativo) |
52
+ | 3. Finalizar | `bun run houndsman --finalize` | skill | `review-resume.md` + triagem no storage |
53
+
54
+ > **Alvo = PR** (ex.: “revise a PR 456”): use `houndsman --pr 456` e
55
+ > `houndsman --pr 456 --finalize`. O prepare coleta o contexto via `gh`, cria um **worktree**
56
+ > detached em `<repo>/.houndsman/worktrees/<repo>/<branch>` e resume o contexto com IA;
57
+ > os briefs trazem `**Worktree:**` e os subagentes devem ler essa árvore (nunca o clone
58
+ > principal). Despache os briefs em paralelo (mesmo bloco de tool calls).
47
59
 
48
60
  ## Comandos
49
61
 
50
62
  | Comando | Função |
51
63
  |---|---|
52
- | `houndsman context` | Passo 0 — contexto da PR via `gh` (`--pr <n>`/`--worktree <path>`) |
64
+ | `houndsman context` | Contexto da PR via `gh` (`--pr <n>`/`--worktree <path>`) — o prepare já coleta com `--pr` |
53
65
  | `houndsman` | Passo 1 — preparar (gera os briefs) |
54
66
  | `houndsman --pr <n>` | Passo 1 em worktree dedicado (revisões paralelas) |
55
- | `houndsman --finalize` | Passo 3 — gerar o relatório (`--pr <n>` reusa o worktree) |
67
+ | `houndsman --finalize` | Passo 3 — gravar a triagem e o resumo final (`--pr <n>` reusa o worktree) |
68
+ | `houndsman hunt [--pr <n>]` | Harness local opcional (maki): prepare → watchdogs → finalize |
69
+ | `houndsman compare` | Compara duas runs/variantes (`--list`, `--a`, `--b` ou dois slugs) |
70
+ | `houndsman hunt install` | Baixa o maki fixado para `<HOUNDSMAN_HOME>/tools/maki` |
56
71
  | `houndsman worktree <ação>` | `list`/`add`/`remove`/`prune`/`path` dos worktrees |
57
72
  | `houndsman watchdog <id>` | Contexto de um watchdog (`--json`, `--rules`) |
58
73
  | `houndsman findings <sub>` | Triagem/correção dos findings (`list`/`set`/`done`/`sync`/`batch`/`verify`/...) |
@@ -78,9 +93,10 @@ Runs, findings e artefatos vivem no **storage global** (SQLite em `<HOUNDSMAN_HO
78
93
  | findings | Decisão/execução + fingerprint + âncora + histórico + vínculo com o GitHub |
79
94
  | `classification.json` | Watchdogs ativados + status (A/M/D/R) + churn |
80
95
  | `candidates.json` | Candidatos da pré-análise Jev |
81
- | `manifest.json` | Resumo (watchdogs, arquivos, deletados, contexto, orçamento, churn) |
96
+ | `business-summary.json` / `business-brief.md` | Modelo de intenção da PR e o brief do passo |
97
+ | `manifest.json` | Resumo (watchdogs, arquivos, deletados, contexto, intenção, orçamento, churn) |
82
98
  | `briefs/<watchdog>.md` | Brief enviado a cada sub-agente (auditoria) |
83
- | `review.md` / `review.<watchdog>.md` | Relatório consolidado e por domínio |
99
+ | `review-resume.md` | Resumo final: negócio, pontos técnicos, cobertura por domínio e conclusão |
84
100
  | `verify.md` | Relatório do `findings verify` |
85
101
  | `context.json` / `context-brief.md` / `context-summary.json` | Contexto da PR |
86
102
 
@@ -109,7 +125,7 @@ src/
109
125
  │ ├── paths.ts # layout fixo da camada (.houndsman/) + relFromRoot
110
126
  │ ├── types.ts # Severity, Rule, WatchdogDefinition, defineWatchdog
111
127
  │ ├── watchdog-registry.ts # getWatchdog/findWatchdog/getWatchdogContext(config)
112
- │ ├── orchestrator.ts # preparar/finalizar
128
+ │ ├── orchestrator.ts # fachada: preparar/finalizar (pipelines em engine/)
113
129
  │ ├── brief.ts # defaultRenderBrief + PROCEDURE
114
130
  │ ├── pr-context.ts # contexto da PR via gh
115
131
  │ ├── file-classifier.ts # classificação (via `classify` dos watchdogs) + churn
@@ -76,6 +76,25 @@ Depois do setup, invoque o skill `/houndsman` no seu harness:
76
76
  | `/houndsman train` | Adicionar/ajustar/remover regras dos watchdogs. |
77
77
  | `/houndsman fix` | Corrigir os findings marcados como `execute`. |
78
78
 
79
+ ## Harness local (opcional) — `houndsman hunt`
80
+
81
+ Quem não quer rodar a skill dentro de um agente externo pode executar a revisão 100% pelo
82
+ CLI. O maki é instalado sob demanda, fora do projeto, e reaproveita a credencial já existente:
83
+
84
+ ```bash
85
+ houndsman hunt install # baixa o maki fixado (sha256) em <HOUNDSMAN_HOME>/tools/maki
86
+ houndsman hunt doctor # binário, modelo e credencial (opcional; não falha sem maki)
87
+ houndsman hunt --pr 456 # prepare → maki (um processo por brief) → finalize
88
+ ```
89
+
90
+ - A skill `/houndsman hunt` continua sendo o caminho canônico e não depende do maki.
91
+ - `HOUNDSMAN_MAKI_PATH` aponta um binário já instalado; `HOUNDSMAN_MAKI_VERSION` troca a
92
+ versão baixada.
93
+ - Config opcional em `houndsman.config.ts`: `hunt: { model?, concurrency?, timeoutMs?,
94
+ retries?, binaryPath?, enabled? }`.
95
+ - Se algum watchdog falhar, o comando **não finaliza** (preserva o staging); use
96
+ `--allow-partial` para finalizar com findings vazios registrados em `skipped`.
97
+
79
98
  ## Contrato do motor (`HoundsmanConfig`)
80
99
 
81
100
  Definido em `src/core/config.ts` (pacote) e espelhado em `.houndsman/core/types.ts` (gerado):
@@ -87,10 +106,12 @@ Definido em `src/core/config.ts` (pacote) e espelhado em `.houndsman/core/types.
87
106
  | `watchdogs` | Definições dos watchdogs (incluem `classify`); os **ids são derivados** das chaves. |
88
107
  | `exclusions` | Arquivos que nunca entram na revisão. |
89
108
  | `docMappings` | Path → documentação de contexto. |
109
+ | `priorities` | **Opcional** — prioridade por caminho `[{ pattern: RegExp, bonus?: number, maxRiskFromChurn?: number }]`. Risco = `min(churn, maxRiskFromChurn) + bonus`: `bonus` promove caminho crítico e o teto impede que superchurn de caminho de baixa criticidade escale. A primeira regra que casar vence. Ordena o orçamento, o rateio do diff e o recorte do `general`. Sem config: churn puro. |
90
110
  | `alwaysIncludeDocs` | Docs sempre presentes nos briefs (default `["AGENTS.md"]`). |
91
111
  | `budgets` | **Opcional** — teto de arquivos por brief (`maxFilesPerBrief`, default `40`); o excedente vira "fora do orçamento" (registrar em `skipped`). |
92
112
  | `concurrency` | **Opcional** — chamadas Jev simultâneas (`jev`, default `5`). |
93
113
  | `ast` | **Opcional** — AST determinístico para TS/JS (`enabled`, `maxFileBytes`, `maxImpactFiles`, `languages`, `aliases`). Default: ligado; falhas são fail-open. |
114
+ | `hunt` | **Opcional** — harness local (`model`, `concurrency`, `timeoutMs`, `retries`, `binaryPath`, `enabled`). Só afeta `houndsman hunt`; a skill não depende. |
94
115
 
95
116
  > O motor **não contém** ids, paths ou regras do projeto — tudo chega por `HoundsmanConfig`,
96
117
  > carregado em runtime a partir de `houndsman.config.ts` (ou `--config <path>`).
@@ -103,7 +124,7 @@ houndsman/
103
124
  └── src/
104
125
  ├── index.ts # API pública (defineWatchdog, defineHoundsmanConfig, tipos)
105
126
  ├── cli/index.ts # CLI único — bin `houndsman`
106
- ├── core/ # bibliotecas genéricas (config, orchestrator, brief, jev/, …)
127
+ ├── core/ # bibliotecas genéricas (config, orchestrator, brief, jev/, hunt/, …)
107
128
  ├── skill/*.md # assets markdown (embarcados no pacote)
108
129
  └── tests/ # suíte da lib (project-agnóstica, "project fake")
109
130
  ```
@@ -14,6 +14,11 @@ consolida tudo num relatório. O motor é o CLI **`houndsman`**; as regras do pr
14
14
  **Terminologia:** **houndsman** = orquestrador/treinador (CLI + motor) · **watchdog** = agente
15
15
  especialista por domínio · **finding** = um achado da revisão · **Jev** = pré-análise opcional.
16
16
 
17
+ > **Duas etapas são obrigatórias em toda revisão:** o **modelo de intenção**
18
+ > (`business-summary.json`) e o revisor de fluxo **`general`** (id reservado, sempre ativo).
19
+ > O modelo vem da IA no prepare quando há chave; sem chave, um subagente (Passo 1.5) ou o
20
+ > pré-passe maki o produz. Nenhum dos dois é configurável pelo usuário.
21
+
17
22
  ## Capabilities
18
23
 
19
24
  | Capability | Objetivo | Documento |
@@ -38,6 +43,10 @@ especialista por domínio · **finding** = um achado da revisão · **Jev** = pr
38
43
  Um argumento extra é o alvo da capability. Escolha a capability pelo que o usuário quer,
39
44
  leia o documento correspondente em `.houndsman/core/skill/<capability>.md` e siga-o.
40
45
 
46
+ > **Harness local (opcional):** para rodar sem agente externo, o operador pode instalar o
47
+ > maki (`houndsman hunt install`) e usar `houndsman hunt --pr 456`. Isso não muda esta
48
+ > skill: se você é o agente, siga o fluxo normal e não rode `houndsman hunt`.
49
+
41
50
  ### Alvo da revisão (PR × branch)
42
51
 
43
52
  Interprete o alvo em linguagem natural **antes** de rodar o CLI:
@@ -56,12 +65,16 @@ Interprete o alvo em linguagem natural **antes** de rodar o CLI:
56
65
  Sequência para uma PR (ex.: 456):
57
66
 
58
67
  ```bash
59
- houndsman context --pr 456
60
- houndsman --pr 456 # cria/reusa o worktree e imprime o caminho
61
- # despache os briefs (cada um traz **Worktree:** <path>)
68
+ houndsman --pr 456 # cria/reusa o worktree, coleta o contexto e prepara
69
+ # despache os briefs em paralelo (cada um traz **Worktree:** <path>)
62
70
  houndsman --pr 456 --finalize
63
71
  ```
64
72
 
73
+ - O contexto da PR é coletado pelo próprio prepare (via `gh`); com IA ligada ele também é
74
+ resumido ali — não gaste um subagente nisso. Desative com `--no-context`.
75
+ - Para comparar duas execuções, use `--variant <label>` em cada uma e depois
76
+ `houndsman compare --branch <head> --a <a> --b <b>` (ou `compare --list`).
77
+
65
78
  - O worktree vive em `<repo>/.houndsman/worktrees/<repo>/<branch>` (detached no commit da PR)
66
79
  e é reutilizado nas próximas execuções da mesma branch. Ele fica **dentro do workspace**
67
80
  (e o staging também) para o agente ler/executar sem prompts de permissão.
@@ -79,4 +92,4 @@ houndsman --pr 456 --finalize
79
92
  - `.houndsman/core/skill/OUTPUT-FORMAT.md` — schema do JSON de saída dos watchdogs.
80
93
  - `.houndsman/core/skill/AUDIT-PROTOCOL.md` — taxonomia da auditoria de regras (Modo B do `train`).
81
94
  - `.houndsman/core/skill/SETUP.md` · `.houndsman/core/skill/README.md` — configuração e visão geral.
82
- - `.houndsman/core/skill/example-review.md` — exemplo de relatório.
95
+ - `.houndsman/core/skill/example-resume.md` — exemplo do resumo final.
@@ -0,0 +1,43 @@
1
+ # Resumo da revisão — feature/add-webhook-retry
2
+
3
+ **Data:** 2026-04-20 · **Status:** ❌ Mudanças Necessárias
4
+
5
+ ## Resumo de negócio
6
+
7
+ Tornar o envio de webhooks resiliente a falhas temporárias do destinatário, sem perda de
8
+ eventos e sem reenvio manual.
9
+
10
+ **Invariantes:**
11
+ - Todo evento entregue tem no máximo uma tentativa `in_flight` por destinatário.
12
+ - O retry nunca duplica o `eventId` já aceito pelo integrador.
13
+
14
+ **Fluxos (2):**
15
+ - **Retry de webhook** — disparo: resposta não-2xx/timeout — esperado: reagendamento com
16
+ backoff exponencial e registro da tentativa.
17
+
18
+ ## Pontos técnicos
19
+
20
+ - **Contratos** — `WebhookRetryPolicy`, `RetryAttempt` (`src/webhooks/types.ts`).
21
+ - **Implementações** — `WebhookDispatcher` (`src/webhooks/dispatcher.ts`).
22
+ - **Novas dependências** — `p-retry`.
23
+
24
+ ## Cobertura por domínio
25
+
26
+ | Domínio | Achados | Revisados | Faltam |
27
+ |---|---|---|---|
28
+ | Arquitetura Backend | 2 | 3/3 | — |
29
+ | Pagamentos & Integrações | 1 | 4/4 | — |
30
+ | Acquirer Integrations | 2 | 3/4 | 1 |
31
+
32
+ ## Candidatos não verificados (1)
33
+
34
+ > Indícios da pré-análise que nenhum watchdog confirmou. Não bloqueiam.
35
+
36
+ - `src/webhooks/queue.ts` · attention · Fila sem limite de tentativas.
37
+
38
+ ## Conclusão
39
+
40
+ **Resumo:** 2 bloqueantes · 1 atenção · 1 sugestões · 1 nits.
41
+
42
+ > Achados individuais (com `id` de triagem) vivem na UI (`Findings`) e em
43
+ > `houndsman findings list --json` — o resumo não os duplica.