@payconductor/houndsman 0.4.0 → 0.5.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 (58) hide show
  1. package/README.md +12 -38
  2. package/package.json +1 -1
  3. package/src/cli/commands/status-command.ts +46 -0
  4. package/src/cli/help.ts +1 -0
  5. package/src/cli/index.ts +3 -0
  6. package/src/core/commands/findings.ts +61 -5
  7. package/src/core/commands/status.ts +63 -0
  8. package/src/core/engine/review-scope.ts +26 -8
  9. package/src/core/findings-state.ts +21 -4
  10. package/src/core/github-review.ts +620 -57
  11. package/src/core/orchestrator.ts +98 -8
  12. package/src/core/paths.ts +3 -0
  13. package/src/core/renderers/resume-renderer.ts +151 -0
  14. package/src/core/report-builder.ts +7 -4
  15. package/src/core/run-status.ts +39 -5
  16. package/src/core/storage/file/file-storage.ts +2 -0
  17. package/src/core/storage/sqlite/mappers.ts +2 -0
  18. package/src/core/storage/sqlite/schema.ts +9 -1
  19. package/src/core/storage/sqlite/sqlite-storage.ts +5 -1
  20. package/src/core/storage/types.ts +9 -0
  21. package/src/server/client/App.tsx +26 -9
  22. package/src/server/client/ArtifactsPanel.tsx +12 -5
  23. package/src/server/client/AutoRefresh.tsx +1 -1
  24. package/src/server/client/Filters.tsx +7 -2
  25. package/src/server/client/FindingCard.tsx +14 -10
  26. package/src/server/client/FindingsList.tsx +2 -2
  27. package/src/server/client/MarkdownText.tsx +92 -12
  28. package/src/server/client/ResumePanel.tsx +368 -0
  29. package/src/server/client/RunList.tsx +3 -3
  30. package/src/server/client/SubmitModal.tsx +49 -6
  31. package/src/server/client/SummaryBar.tsx +1 -1
  32. package/src/server/client/icons.tsx +17 -0
  33. package/src/server/client/{constants.ts → utils/constants.ts} +34 -5
  34. package/src/server/client/utils/markdown.ts +319 -0
  35. package/src/server/client/{types.ts → utils/types.ts} +58 -0
  36. package/src/server/index.ts +63 -5
  37. package/src/server/run-repository.ts +9 -0
  38. package/src/skill/example-review.md +22 -14
  39. package/src/skill/hunt.md +31 -17
  40. package/src/skill/verify.md +2 -1
  41. package/src/tests/__snapshots__/baseline.test.ts.snap +32 -30
  42. package/src/tests/baseline.test.ts +3 -0
  43. package/src/tests/code-highlight.test.ts +1 -1
  44. package/src/tests/findings-state.test.ts +50 -2
  45. package/src/tests/github-review.test.ts +409 -60
  46. package/src/tests/markdown.test.ts +61 -1
  47. package/src/tests/review-resume.test.ts +101 -0
  48. package/src/tests/review-scope.test.ts +302 -0
  49. package/src/tests/review-scripts.test.ts +73 -0
  50. package/src/tests/run-status-view.test.ts +94 -0
  51. package/src/tests/run-status.test.ts +53 -0
  52. package/src/tests/server.test.ts +76 -21
  53. package/src/tests/storage.test.ts +63 -0
  54. package/src/server/client/markdown.ts +0 -153
  55. /package/src/server/client/{api.ts → utils/api.ts} +0 -0
  56. /package/src/server/client/{code-highlight.ts → utils/code-highlight.ts} +0 -0
  57. /package/src/server/client/{format.ts → utils/format.ts} +0 -0
  58. /package/src/server/client/{json-highlight.ts → utils/json-highlight.ts} +0 -0
@@ -315,7 +315,7 @@ code, pre, .mono { font-family:var(--font-mono); tab-size:4; }
315
315
  .btn:disabled { opacity:.55; cursor:not-allowed; }
316
316
  .icon.spin { animation:spin .8s linear infinite; }
317
317
  .content { width:100%; max-width:1180px; margin:0 auto; padding:22px 24px 64px; }
318
- .page-header { display:flex; align-items:flex-start; justify-content:space-between; gap:18px; }
318
+ .page-header { display:flex; align-items:flex-start; justify-content:space-between; gap:18px; margin-bottom:16px; }
319
319
  .page-header-text { min-width:0; }
320
320
  .page-header-actions { display:flex; align-items:center; gap:10px; flex:none; }
321
321
  .btn.submit-review { color:#1b1305; background:linear-gradient(180deg, var(--accent-strong), var(--accent)); border-color:transparent; box-shadow:0 4px 14px rgba(240,165,44,.22); }
@@ -412,12 +412,25 @@ code, pre, .mono { font-family:var(--font-mono); tab-size:4; }
412
412
  .action-text p:last-child { margin-bottom:0; }
413
413
  .md-list { margin:0 0 6px; padding-left:20px; font-size:13px; line-height:1.65; color:#f0ece6; }
414
414
  .md-list li { margin:0 0 2px; }
415
- .md-quote { margin:0 0 6px; padding:2px 0 2px 10px; border-left:2px solid var(--accent-line); color:var(--muted); font-size:13px; line-height:1.65; }
415
+ .md-quote { margin:0 0 6px; padding:2px 0 2px 10px; border-left:2px solid var(--accent-line); color:var(--muted); font-size:13px; line-height:1.65; white-space:pre-wrap; }
416
416
  .action-text code, .md-code { font-family:var(--font-mono); font-size:11.5px; padding:1px 5px; color:#f0ece6; background:rgba(255,255,255,.06); border:1px solid var(--border); border-radius:5px; }
417
- .action-text a { color:var(--accent-strong); text-decoration:underline; text-underline-offset:2px; }
418
- .action-text strong { color:#fff; font-weight:600; }
419
- .action-text em { color:#e9e4dc; }
417
+ code.md-path { color:#8fc7ff; background:rgba(90,164,240,.1); border-color:rgba(90,164,240,.24); }
418
+ .action-text a, .md-p a, .md-list a, .md-quote a, .md-table a, .md-h a { color:var(--accent-strong); text-decoration:underline; text-underline-offset:2px; }
419
+ .action-text strong, .md-p strong, .md-list strong, .md-quote strong, .md-table strong, .md-h strong { color:#fff; font-weight:600; }
420
+ .action-text em, .md-p em, .md-list em, .md-quote em, .md-table em, .md-h em { color:#e9e4dc; }
420
421
  .md-code-block { margin:6px 0; padding:10px 12px; max-height:320px; overflow:auto; white-space:pre; font-size:11.5px; line-height:1.6; color:#cdc7bf; background:#0b0b0e; border-radius:10px; }
422
+ .md-h { margin:16px 0 8px; font-size:14px; font-weight:700; line-height:1.35; letter-spacing:-.01em; color:#eee9e2; }
423
+ .md-h:first-child { margin-top:0; }
424
+ .md-h1 { font-size:19px; }
425
+ .md-h2 { font-size:16px; }
426
+ .md-h3 { font-size:14px; }
427
+ .md-h4, .md-h5, .md-h6 { font-size:13px; color:#e4dfd7; font-weight:600; }
428
+ .md-hr { margin:14px 0; border:0; border-top:1px solid var(--border); }
429
+ .md-table-wrap { margin:8px 0 12px; padding:4px; overflow-x:auto; border:1px solid rgba(255,255,255,.07); border-radius:10px; background:rgba(255,255,255,.012); }
430
+ .md-table { width:100%; border-collapse:separate; border-spacing:3px; font-size:12px; line-height:1.5; }
431
+ .md-table th, .md-table td { padding:6px 9px; text-align:left; vertical-align:top; border:1px solid rgba(255,255,255,.035); border-radius:6px; }
432
+ .md-table th { color:var(--muted); font-weight:600; background:rgba(255,255,255,.03); }
433
+ .md-table td { color:#d8d3cc; }
421
434
  .decision-tags-block { display:flex; align-items:center; gap:8px; }
422
435
  .evidence { margin:0; padding: 16px; white-space:break-spaces; font-size:12px; line-height:1.6; color:#cdc7bf;
423
436
  background:#0b0b0e; border:1px solid var(--border); border-radius:var(--radius); }
@@ -476,6 +489,10 @@ code, pre, .mono { font-family:var(--font-mono); tab-size:4; }
476
489
  border:1px solid var(--accent-line); border-radius:999px; }
477
490
  .artifact-view { margin:0; padding:14px 16px; max-height:70vh; overflow:auto; font-size:12px; line-height:1.6; color:#cdc7bf;
478
491
  background:#0b0b0e; border:1px solid var(--border); border-radius:var(--radius); }
492
+ .artifact-markdown { white-space:normal; }
493
+ .artifact-markdown .md-p, .artifact-markdown .md-list, .artifact-markdown .md-quote { font-size:12.5px; }
494
+ .artifact-markdown .md-p, .artifact-markdown .md-list { color:#d8d3cc; }
495
+ .artifact-markdown .md-code-block { border:1px solid var(--border); }
479
496
  .json-view { white-space:pre; }
480
497
  .json-key { color:var(--accent-strong); }
481
498
  .json-string { color:#9ddc8a; }
@@ -486,6 +503,47 @@ code, pre, .mono { font-family:var(--font-mono); tab-size:4; }
486
503
  .sent-tag { display:inline-flex; align-items:center; gap:5px; padding:2px 8px; font-size:10px; font-weight:700; letter-spacing:.06em; text-transform:uppercase;
487
504
  color:var(--success); background:rgba(69,192,122,.12); border:1px solid rgba(69,192,122,.32); border-radius:999px; text-decoration:none; }
488
505
  .sent-tag:hover { color:#fff; }
506
+ .sent-tag.gh { color:#b9c7d6; background:rgba(139,164,191,.12); border-color:rgba(139,164,191,.35); }
507
+ .sent-tag.gh:hover { color:#fff; background:rgba(139,164,191,.2); }
508
+ .resume { display:flex; flex-direction:column; }
509
+ .resume-hero { display:flex; align-items:flex-start; justify-content:space-between; gap:16px; padding:18px 20px;
510
+ background:linear-gradient(135deg, rgba(240,165,44,.08), rgba(255,255,255,.015) 55%);
511
+ border:1px solid var(--border); border-radius:var(--radius-lg); }
512
+ .resume-kicker { display:block; font-size:10px; font-weight:700; letter-spacing:.14em; text-transform:uppercase; color:var(--accent); }
513
+ .resume-title { margin:5px 0 7px; font-size:22px; font-weight:700; letter-spacing:-.02em; word-break:break-word; }
514
+ .resume-meta { display:flex; flex-wrap:wrap; align-items:center; gap:12px; margin:0; font-size:12px; color:var(--muted); }
515
+ .resume-pr { display:inline-flex; align-items:center; gap:5px; color:var(--accent-strong); text-decoration:none; }
516
+ .resume-pr:hover { text-decoration:underline; }
517
+ .resume-hero-side { display:flex; flex:none; flex-direction:column; align-items:flex-end; gap:10px; }
518
+ .resume-status { display:inline-flex; align-items:center; padding:5px 12px; font-size:11px; font-weight:700; letter-spacing:.04em; border-radius:999px; }
519
+ .resume-status.blocking { color:var(--blocking); background:rgba(242,89,94,.12); border:1px solid rgba(242,89,94,.35); }
520
+ .resume-status.attention { color:var(--attention); background:rgba(237,161,58,.12); border:1px solid rgba(237,161,58,.35); }
521
+ .resume-status.clean { color:var(--success); background:rgba(69,192,122,.12); border:1px solid rgba(69,192,122,.35); }
522
+ .resume-kpis { margin:14px 0 18px; }
523
+ .resume-kpis .kpi-value { font-size:24px; }
524
+ .churn-add { color:var(--success); }
525
+ .churn-del { color:var(--blocking); }
526
+ .churn-sep { margin:0 3px; color:var(--muted-2); font-weight:400; }
527
+ .kpi-blocking .kpi-value { color:var(--blocking); }
528
+ .kpi-attention .kpi-value { color:var(--attention); }
529
+ .kpi-total .kpi-value { color:var(--fg); }
530
+ .resume-grid { display:grid; grid-template-columns:minmax(0,1.35fr) minmax(0,1fr); gap:16px; align-items:start; }
531
+ .resume-card { min-width:0; padding:16px 18px; background:var(--card); border:1px solid var(--border); border-radius:var(--radius-lg); }
532
+ .resume-card-head { display:flex; align-items:center; justify-content:space-between; gap:10px; }
533
+ .resume-card-title { margin:0 0 10px; font-size:12px; font-weight:700; letter-spacing:.1em; text-transform:uppercase; color:var(--muted); }
534
+ .resume-card-count { display:inline-flex; align-items:center; justify-content:center; min-width:22px; height:22px; margin-bottom:10px; padding:0 7px;
535
+ font-family:var(--font-mono); font-size:11px; color:var(--accent); background:var(--accent-soft); border:1px solid var(--accent-line); border-radius:999px; }
536
+ .resume-narrative .md-p:last-child { margin-bottom:0; }
537
+ .resume-domains { display:flex; flex-direction:column; gap:8px; margin:0; padding:0; list-style:none; }
538
+ .resume-domain { display:flex; flex-direction:column; gap:7px; padding:10px 12px; background:var(--surface-2);
539
+ border:1px solid var(--border); border-radius:10px; }
540
+ .resume-domain-head { display:flex; align-items:baseline; justify-content:space-between; gap:10px; }
541
+ .resume-domain-name { font-size:12.5px; font-weight:600; color:var(--fg); }
542
+ .resume-domain-files { flex:none; font-family:var(--font-mono); font-size:10.5px; color:var(--muted-2); }
543
+ .resume-domain-sev { display:flex; flex-wrap:wrap; gap:6px; }
544
+ .resume-domain-sev .sev-badge { padding:2px 7px; font-size:9.5px; }
545
+ .resume-domain-ok { font-size:11.5px; color:var(--success); }
546
+ @media (max-width:900px) { .resume-grid { grid-template-columns:1fr; } .resume-hero { flex-direction:column; } .resume-hero-side { flex-direction:row; align-items:flex-start; } }
489
547
  .modal-layer { position:fixed; inset:0; z-index:50; display:grid; place-items:center; padding:24px; }
490
548
  .modal-backdrop { position:absolute; inset:0; width:100%; padding:0; background:rgba(5,5,7,.66); border:0; cursor:default; backdrop-filter:blur(5px); -webkit-backdrop-filter:blur(5px); }
491
549
  .modal { position:relative; width:min(640px, 100%); max-height:min(78vh, 780px); display:flex; flex-direction:column; overflow:hidden;
@@ -30,6 +30,7 @@ import {
30
30
  resolvePrTarget,
31
31
  type SubmitSelection,
32
32
  submitFindings,
33
+ syncResolvedThreads,
33
34
  } from "../core/github-review";
34
35
  import { pingFindingsStatus } from "../core/run-status";
35
36
  import { systemClock } from "../core/services/clock";
@@ -42,6 +43,8 @@ export type RunListItem = {
42
43
  slug: string;
43
44
  branch: string;
44
45
  updatedAt?: string;
46
+ startedAt?: string;
47
+ heartbeatAt?: string;
45
48
  status: ReviewRecord["status"];
46
49
  summary: FindingsSummary;
47
50
  };
@@ -94,6 +97,8 @@ export class RunRepository {
94
97
  slug: review.slug,
95
98
  branch: review.branch,
96
99
  updatedAt: review.updatedAt,
100
+ startedAt: review.startedAt,
101
+ heartbeatAt: review.heartbeatAt,
97
102
  status: review.status,
98
103
  summary: summarize(state ?? emptyState(review.branch)),
99
104
  };
@@ -211,6 +216,10 @@ export class RunRepository {
211
216
  if (resolution.status !== "found") return null;
212
217
  const finding = state.findings[resolution.id];
213
218
  applyFindingUpdate(finding, body);
219
+ // "Done" na UI é resolução local: resolve a conversa no PR (fail-open).
220
+ if (body.execState === "done") {
221
+ syncResolvedThreads({ state, ids: [finding.id] });
222
+ }
214
223
  reorder(state);
215
224
  state.updatedAt = systemClock();
216
225
  this.storage.writeFindings(review.id, state);
@@ -14,6 +14,28 @@
14
14
  - Novo endpoint interno para reprocessar eventos manualmente.
15
15
  - Registro de tentativas no handler de envio.
16
16
 
17
+ ## Relatórios por domínio
18
+
19
+ | Watchdog | Achados | Revisados | Relatório |
20
+ |---|---|---|---|
21
+ | Arquitetura Backend | 2 | 3/3 | [`review.backend-architecture.md`](review.backend-architecture.md) |
22
+ | Pagamentos & Integrações | 1 | 4/4 | [`review.payments-integrations.md`](review.payments-integrations.md) |
23
+ | Acquirer Integrations | 2 | 3/4 | [`review.acquirer-integrations.md`](review.acquirer-integrations.md) |
24
+
25
+ ## Cobertura
26
+
27
+ | Watchdog | Arquivos revisados | Findings |
28
+ |---|---|---|
29
+ | API & Contratos | 2 | 0 |
30
+ | Arquitetura Backend | 3 | 2 |
31
+ | Pagamentos & Integrações | 4 | 1 |
32
+ | Acquirer Integrations | 3/4 (faltam 1) | 2 |
33
+
34
+ ## Conclusão
35
+
36
+ **Status:** ❌ Mudanças Necessárias
37
+ **Resumo:** 2 violações bloqueantes, 1 ponto de atenção, 1 sugestão e 1 nit.
38
+
17
39
  ## ❌ blocking
18
40
 
19
41
  - **Webhook handler sem verifySignature** — `server/src/services/integrations/acquirer/webhook/handle-charge-event.ts:15` · _Acquirer Integrations, Pagamentos & Integrações_
@@ -42,17 +64,3 @@
42
64
  - **Ordem de membros** — `server/src/services/webhook/webhook-retry-service.ts:12` · _Arquitetura Backend_
43
65
  Campo privado declarado antes do construtor público.
44
66
  **Ação:** Reordenar membros (públicos primeiro).
45
-
46
- ## Cobertura
47
-
48
- | Watchdog | Arquivos revisados | Findings |
49
- |---|---|---|
50
- | API & Contratos | 2 | 0 |
51
- | Arquitetura Backend | 3 | 1 |
52
- | Pagamentos & Integrações | 4 | 2 |
53
- | Acquirer Integrations | 3/4 (faltam 1) | 2 |
54
-
55
- ## Conclusão
56
-
57
- **Status:** ❌ Mudanças Necessárias
58
- **Resumo:** 2 violações bloqueantes, 1 ponto de atenção, 1 sugestão.
package/src/skill/hunt.md CHANGED
@@ -122,11 +122,14 @@ arquivos alterados desde a última revisão — e **carrega** os findings dos ar
122
122
  (carry-over), sem re-revisá-los. O `manifest.json` traz o bloco `incremental` (`from`,
123
123
  `changedFiles`, `carried`) e o `review.md` abre com um aviso.
124
124
 
125
- - Sem mudanças desde a última revisão → **no-op** (nada a revisar; o estado é preservado).
125
+ - Sem mudanças desde a última revisão → **no-op**: o prepare não gera briefs nem reescreve
126
+ artefatos (classification, manifest, review.md), e o `--finalize` também vira no-op — pare o
127
+ fluxo aí (nenhum subagente é despachado).
126
128
  - Rebase/force-push (histórico reescrito) → cai automaticamente no **review completo**.
127
129
  - Forçar completo: `houndsman --full` (ou `--clean`, que também descarta a triagem anterior).
128
130
 
129
131
  > Custo: o LLM só é gasto nos arquivos que mudaram desde a última revisão — não na PR inteira.
132
+ > Re-rodar `/houndsman` no mesmo commit **não** re-revisa a PR; use `--full` para isso.
130
133
 
131
134
  ---
132
135
 
@@ -135,6 +138,10 @@ arquivos alterados desde a última revisão — e **carrega** os findings dos ar
135
138
  Um subagente por brief. Despache em **ondas** (ex.: até 8 subagentes por bloco de tool
136
139
  calls) — evita estourar limites de taxa quando há muitos watchdogs. O prompt é o próprio brief:
137
140
 
141
+ > **Heartbeat:** antes de despachar cada onda, rode `houndsman status ping`. A UI usa esse
142
+ > sinal (e não `updatedAt`) para saber que a execução está viva; sem ping por 10 min ela
143
+ > mostra "Timeout na revisão".
144
+
138
145
  ```
139
146
  Leia e execute o brief: <caminho absoluto impresso pelo prepare>/briefs/<watchdog-id>.md
140
147
  Você é o watchdog <watchdog-id> do {{projectName}}.
@@ -158,15 +165,18 @@ houndsman --finalize
158
165
  Falha com mensagem clara se faltar algum `findings/<watchdog>.json` no staging. Quando todos existem:
159
166
  1. Valida o schema de cada JSON
160
167
  2. Deduplica achados por `(file, line, rule)` mesclando watchdogs sobrepostos
161
- 3. Agrupa por severidade: ❌ blocking > ⚠️ attention > 💡 suggestion > 🔎 nit
162
- 4. Grava o artefato `review.md` no storage + tabela de cobertura
163
- 5. Anexa os candidatos Jev não confirmados em `🔎 Candidatos não verificados` (não bloqueiam)
164
- 6. Agrupa os achados **por arquivo** dentro de cada severidade (navegabilidade em PRs grandes)
165
- 7. Grava um `review.<watchdog>.md` por watchdog com achados e indexa no `review.md`
168
+ 3. Grava um `review.<watchdog>.md` por watchdog com achados e indexa no `review.md`
166
169
  (**Relatórios por domínio**) — mantém o relatório consolidado enxuto
167
- 8. Persiste a **triagem** no storage — um `id` curto e um `fingerprint` de conteúdo por
168
- finding, preservados entre execuções
169
-
170
+ 4. Grava o `review.md` abrindo pelo **sumário**: contexto, relatórios por domínio, cobertura e
171
+ conclusão — antes dos achados
172
+ 5. Agrupa os achados por severidade (❌ blocking > ⚠️ attention > 💡 suggestion > 🔎 nit) e
173
+ **por arquivo** dentro de cada severidade (navegabilidade em PRs grandes)
174
+ 6. Anexa os candidatos Jev não confirmados em `🔎 Candidatos não verificados` ao final
175
+ (não bloqueiam)
176
+ 7. Persiste a **triagem** no storage — um `id` curto e um `fingerprint` de conteúdo por
177
+ finding, preservados entre execuções9. Marca como **`done`** os findings que sumiram do relatório (corrigidos manualmente entre
178
+ revisões) e **resolve as conversas correspondentes no GitHub** — só as ainda abertas, e
179
+ falha-aberto (sem `gh`/PR o relatório segue igual)
170
180
  Após finalizar, corrija os achados com a capability **`fix`** (`fix.md`): a UI ou os
171
181
  comandos `findings` levam do achado à correção.
172
182
 
@@ -188,14 +198,6 @@ comandos `findings` levam do achado à correção.
188
198
  **Resumo das alterações:**
189
199
  - ...
190
200
 
191
- ## ❌ blocking
192
- ### `src/routes/order-route.ts`
193
- - **<regra>** — `src/routes/order-route.ts:5` · _<watchdogs>_
194
- <mensagem>
195
- **Ação:** <ação>
196
-
197
- ## 🔎 Candidatos não verificados (Jev) ← se houver
198
-
199
201
  ## Relatórios por domínio
200
202
  | Watchdog | Achados | Revisados | Relatório |
201
203
  |---|---|---|---|
@@ -206,8 +208,19 @@ comandos `findings` levam do achado à correção.
206
208
 
207
209
  ## Conclusão
208
210
  **Status:** ✅ Aprovado / ⚠️ Aprovado com Ressalvas / ❌ Mudanças Necessárias
211
+
212
+ ## ❌ blocking
213
+ ### `src/routes/order-route.ts`
214
+ - **<regra>** — `src/routes/order-route.ts:5` · _<watchdogs>_
215
+ <mensagem>
216
+ **Ação:** <ação>
217
+
218
+ ## 🔎 Candidatos não verificados (Jev) ← se houver, sempre ao final
209
219
  ```
210
220
 
221
+ > O **sumário** (relatórios por domínio, cobertura e conclusão) vem **antes** dos achados;
222
+ > os pontos específicos ficam no fim.
223
+
211
224
  ---
212
225
 
213
226
  ## Fluxo Completo
@@ -246,6 +259,7 @@ Se a branch não for informada, detecta com `git branch --show-current`. Base do
246
259
  |---|---|
247
260
  | `houndsman context` | Passo 0 — contexto da PR via `gh` |
248
261
  | `houndsman` | Passo 1 — preparar (briefs + manifesto; incremental quando possível) |
262
+ | `houndsman status ping` | Passo 2 — heartbeat da execução (antes de cada onda) |
249
263
  | `houndsman --finalize` | Passo 3 — gerar o relatório |
250
264
  | `houndsman watchdog <id>` | Contexto de um watchdog (`--json`, `--rules`) |
251
265
 
@@ -43,7 +43,8 @@ bun run houndsman findings verify --apply # marca os resolvidos como done (pi
43
43
 
44
44
  - Sem `--apply`: **só relatório** (grava o artefato `verify.md` no storage). Nada muda na triagem.
45
45
  - Com `--apply`: os findings com status **`resolved`** viram `execState: done` (histórico
46
- `by: "verify"`). `persisting` e `uncertain` **não** são tocados.
46
+ `by: "verify"`) e a **thread correspondente é resolvida no GitHub** (quando há comentário e a
47
+ conversa ainda está aberta; fail-open). `persisting` e `uncertain` **não** são tocados.
47
48
 
48
49
  Filtros (como no `findings list`): `--severity`, `--watchdog`, `--file`, `--decision`. `--all`
49
50
  inclui ausentes. `--no-jev` desliga o tier LLM. `--fail-on-persisting` sai com código 1 se algum
@@ -88,23 +88,6 @@ exports[`baseline — relatório renderReport consolidado 1`] = `
88
88
  - Faz A
89
89
  - Remove B
90
90
 
91
- ## ❌ blocking
92
-
93
- ### \`src/alpha/a.ts\`
94
-
95
- - **Nunca usar \`any\`.** — \`src/alpha/a.ts:1\` · _Alpha_ · \`id-alpha\`
96
- any detectado
97
- **Ação:** tipar
98
- **Evidência:** \`export const a: any = 1;\`
99
-
100
- ## ⚠️ attention
101
-
102
- ### \`src/alpha/b.ts\`
103
-
104
- - **Promover: validar a entrada.** — \`src/alpha/b.ts:2\` · _Alpha, Beta_ · \`id-beta\`
105
- sem validação
106
- **Ação:** adicionar schema
107
-
108
91
  ## Relatórios por domínio
109
92
 
110
93
  | Watchdog | Achados | Revisados | Relatório |
@@ -121,7 +104,25 @@ exports[`baseline — relatório renderReport consolidado 1`] = `
121
104
  ## Conclusão
122
105
 
123
106
  **Status:** ❌ Mudanças Necessárias
124
- **Resumo:** 1 violações bloqueantes, 1 pontos de atenção, 0 sugestões e 0 nits."
107
+ **Resumo:** 1 violações bloqueantes, 1 pontos de atenção, 0 sugestões e 0 nits.
108
+
109
+ ## ❌ blocking
110
+
111
+ ### \`src/alpha/a.ts\`
112
+
113
+ - **Nunca usar \`any\`.** — \`src/alpha/a.ts:1\` · _Alpha_ · \`id-alpha\`
114
+ any detectado
115
+ **Ação:** tipar
116
+ **Evidência:** \`export const a: any = 1;\`
117
+
118
+ ## ⚠️ attention
119
+
120
+ ### \`src/alpha/b.ts\`
121
+
122
+ - **Promover: validar a entrada.** — \`src/alpha/b.ts:2\` · _Alpha, Beta_ · \`id-beta\`
123
+ sem validação
124
+ **Ação:** adicionar schema
125
+ "
125
126
  `;
126
127
 
127
128
  exports[`baseline — relatório renderDomainReport isolado 1`] = `
@@ -528,7 +529,7 @@ exports[`baseline — camada gerada setup gera os mesmos arquivos e conteúdos 3
528
529
  },
529
530
  {
530
531
  "file": ".houndsman/core/skill/example-review.md",
531
- "sha256": "a2627e99ac5caead",
532
+ "sha256": "d9e75054cc9811dc",
532
533
  },
533
534
  {
534
535
  "file": ".houndsman/core/skill/fix.md",
@@ -536,7 +537,7 @@ exports[`baseline — camada gerada setup gera os mesmos arquivos e conteúdos 3
536
537
  },
537
538
  {
538
539
  "file": ".houndsman/core/skill/hunt.md",
539
- "sha256": "c47a24c6c36c1c03",
540
+ "sha256": "c978287603f4e088",
540
541
  },
541
542
  {
542
543
  "file": ".houndsman/core/skill/train.md",
@@ -544,7 +545,7 @@ exports[`baseline — camada gerada setup gera os mesmos arquivos e conteúdos 3
544
545
  },
545
546
  {
546
547
  "file": ".houndsman/core/skill/verify.md",
547
- "sha256": "7ae6e1ca8bf0a868",
548
+ "sha256": "29bfdeb7def2298e",
548
549
  },
549
550
  ]
550
551
  `;
@@ -713,15 +714,6 @@ exports[`baseline — artefatos de prepare/finalize gera classification, brief,
713
714
  **Watchdogs ativos:** alpha
714
715
  **Achados:** 1 bloqueantes · 0 atenção · 0 sugestões · 0 nits
715
716
 
716
- ## ❌ blocking
717
-
718
- ### \`src/alpha/a.ts\`
719
-
720
- - **Nunca usar \`any\`.** — \`src/alpha/a.ts:1\` · _Alpha_ · \`<id>\`
721
- any detectado
722
- **Ação:** tipar
723
- **Evidência:** \`export const a: any = 1;\`
724
-
725
717
  ## Relatórios por domínio
726
718
 
727
719
  | Watchdog | Achados | Revisados | Relatório |
@@ -738,6 +730,16 @@ exports[`baseline — artefatos de prepare/finalize gera classification, brief,
738
730
 
739
731
  **Status:** ❌ Mudanças Necessárias
740
732
  **Resumo:** 1 violações bloqueantes, 0 pontos de atenção, 0 sugestões e 0 nits.
733
+
734
+ ## ❌ blocking
735
+
736
+ ### \`src/alpha/a.ts\`
737
+
738
+ - **Nunca usar \`any\`.** — \`src/alpha/a.ts:1\` · _Alpha_ · \`<id>\`
739
+ any detectado
740
+ **Ação:** tipar
741
+ **Evidência:** \`export const a: any = 1;\`
742
+
741
743
  "
742
744
  `;
743
745
 
@@ -432,6 +432,9 @@ describe("baseline — artefatos de prepare/finalize", () => {
432
432
  const artifact = storage.readArtifact(review.id, name);
433
433
  expect(normalize(artifact?.content ?? "")).toMatchSnapshot(name);
434
434
  }
435
+ const resume = storage.readArtifact(review.id, "review-resume.md");
436
+ expect(resume?.kind).toBe("resume");
437
+ expect((resume?.content ?? "").length).toBeLessThanOrEqual(2500);
435
438
  expect(
436
439
  normalize(JSON.stringify(storage.readFindings(review.id), null, 2)),
437
440
  ).toMatchSnapshot("state.json");
@@ -10,7 +10,7 @@ import {
10
10
  highlightCode,
11
11
  languageFromFile,
12
12
  languageFromName,
13
- } from "../server/client/code-highlight";
13
+ } from "../server/client/utils/code-highlight";
14
14
 
15
15
  /** O texto concatenado dos tokens deve ser idêntico ao input. */
16
16
  function joined(tokens: { text: string }[]): string {
@@ -112,7 +112,7 @@ describe("buildStateFromFindings", () => {
112
112
  }
113
113
  });
114
114
 
115
- test("finding que sumiu fica ausente e vira done se era 'execute'", () => {
115
+ test("finding que sumiu vira done (corrigido entre revisões)", () => {
116
116
  const project = makeProject({ "src/f.ts": DUPLICATED });
117
117
  try {
118
118
  const reader = fileReader(project.cwd);
@@ -121,7 +121,7 @@ describe("buildStateFromFindings", () => {
121
121
  { branch: "main", readFile: reader },
122
122
  );
123
123
  const id = Object.keys(first.findings)[0];
124
- applyFindingUpdate(first.findings[id], { decision: "execute" });
124
+ expect(first.findings[id].decision).toBe("pending");
125
125
 
126
126
  const second = buildStateFromFindings([], {
127
127
  branch: "main",
@@ -135,6 +135,54 @@ describe("buildStateFromFindings", () => {
135
135
  }
136
136
  });
137
137
 
138
+ test("finding ausente e marcado como 'ignore' preserva o estado", () => {
139
+ const project = makeProject({ "src/f.ts": DUPLICATED });
140
+ try {
141
+ const reader = fileReader(project.cwd);
142
+ const first = buildStateFromFindings(
143
+ [merged({ file: "src/f.ts", rule: "r", line: 2 })],
144
+ { branch: "main", readFile: reader },
145
+ );
146
+ const id = Object.keys(first.findings)[0];
147
+ applyFindingUpdate(first.findings[id], { decision: "ignore" });
148
+
149
+ const second = buildStateFromFindings([], {
150
+ branch: "main",
151
+ readFile: reader,
152
+ previous: first,
153
+ });
154
+ expect(second.findings[id].present).toBe(false);
155
+ expect(second.findings[id].execState).toBe("pending");
156
+ expect(second.findings[id].decision).toBe("ignore");
157
+ } finally {
158
+ project.cleanup();
159
+ }
160
+ });
161
+
162
+ test("arquivo fora da revisão (orçamento/skipped) não vira done", () => {
163
+ const project = makeProject({ "src/f.ts": DUPLICATED });
164
+ try {
165
+ const reader = fileReader(project.cwd);
166
+ const first = buildStateFromFindings(
167
+ [merged({ file: "src/f.ts", rule: "r", line: 2 })],
168
+ { branch: "main", readFile: reader },
169
+ );
170
+ const id = Object.keys(first.findings)[0];
171
+
172
+ const second = buildStateFromFindings([], {
173
+ branch: "main",
174
+ readFile: reader,
175
+ previous: first,
176
+ // Nenhum arquivo foi revisado neste ciclo: ausência não prova resolução.
177
+ reviewedPaths: new Set(["src/outro.ts"]),
178
+ });
179
+ expect(second.findings[id].present).toBe(false);
180
+ expect(second.findings[id].execState).toBe("pending");
181
+ } finally {
182
+ project.cleanup();
183
+ }
184
+ });
185
+
138
186
  test("ocorrências idênticas (mesmo contexto) viram um grupo com count", () => {
139
187
  const content = [
140
188
  "const a = 1;",