@payconductor/houndsman 0.7.0 → 0.10.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.
- package/README.md +20 -0
- package/package.json +1 -1
- package/src/cli/commands/command.ts +10 -1
- package/src/cli/commands/context-command.ts +40 -2
- package/src/cli/commands/hunt-command.ts +28 -2
- package/src/cli/commands/review-command.ts +30 -2
- package/src/cli/commands/worktree-command.ts +118 -0
- package/src/cli/help.ts +12 -2
- package/src/cli/index.ts +14 -4
- package/src/core/ai/ai-client.ts +21 -0
- package/src/core/ai/ai-sdk-model-provider.ts +43 -18
- package/src/core/ai/features/technical-summarizer.ts +43 -0
- package/src/core/ai/index.ts +4 -1
- package/src/core/ai/prompts/context.ts +3 -3
- package/src/core/ai/prompts/technical.ts +30 -0
- package/src/core/ai/provider-capabilities.ts +45 -0
- package/src/core/ai/schemas.ts +19 -3
- package/src/core/ai/types.ts +3 -1
- package/src/core/ast/service.ts +7 -2
- package/src/core/brief.ts +26 -4
- package/src/core/commands/context.ts +9 -1
- package/src/core/commands/findings.ts +11 -5
- package/src/core/commands/review.ts +13 -0
- package/src/core/commands/scaffold.ts +9 -2
- package/src/core/config.ts +30 -1
- package/src/core/defaults.ts +7 -0
- package/src/core/engine/houndsman-engine.ts +26 -3
- package/src/core/engine/review-scope.ts +17 -22
- package/src/core/engine/review-session.ts +25 -2
- package/src/core/engine/review-workspace.ts +204 -0
- package/src/core/git-runner.ts +8 -1
- package/src/core/jev/jev-detector.ts +10 -2
- package/src/core/orchestrator.ts +176 -15
- package/src/core/paths.ts +3 -0
- package/src/core/pr-context.ts +11 -6
- package/src/core/project-config.ts +21 -1
- package/src/core/renderers/resume-renderer.ts +17 -34
- package/src/core/report-builder.ts +6 -6
- package/src/core/services/git-repository.ts +46 -0
- package/src/core/services/pr-resolver.ts +147 -0
- package/src/core/services/worktree-manager.ts +293 -0
- package/src/core/storage/file/file-storage.ts +14 -1
- package/src/core/storage/paths.ts +30 -1
- package/src/core/storage/sqlite/mappers.ts +4 -0
- package/src/core/storage/sqlite/schema.ts +13 -3
- package/src/core/storage/sqlite/sqlite-storage.ts +28 -8
- package/src/core/storage/types.ts +13 -0
- package/src/core/storage/work-area.ts +20 -1
- package/src/core/technical-summary.ts +398 -0
- package/src/core/types.ts +2 -0
- package/src/harness/agent/agent-factory.ts +1 -0
- package/src/harness/agent/review-agent.ts +11 -1
- package/src/harness/config/harness-config.ts +6 -1
- package/src/harness/hunt/hunt-options.ts +17 -0
- package/src/harness/hunt/hunt-session.ts +7 -20
- package/src/index.ts +33 -0
- package/src/server/client/App.tsx +51 -12
- package/src/server/client/DeleteRunModal.tsx +163 -0
- package/src/server/client/PrsSection.tsx +296 -0
- package/src/server/client/ResumePanel.tsx +17 -20
- package/src/server/client/RunsSection.tsx +73 -0
- package/src/server/client/Sidebar.tsx +171 -358
- package/src/server/client/icons.tsx +10 -0
- package/src/server/client/sidebar-sections.tsx +73 -0
- package/src/server/client/utils/constants.ts +7 -2
- package/src/server/client/utils/sidebar-prefs.ts +64 -0
- package/src/server/client/utils/types.ts +27 -0
- package/src/server/index.ts +113 -406
- package/src/server/pr-inbox.ts +15 -4
- package/src/server/run-repository.ts +103 -2
- package/src/server/styles.css +454 -0
- package/src/skill/README.md +11 -3
- package/src/skill/SKILL.md +44 -10
- package/src/skill/hunt.md +33 -7
- package/src/tests/__snapshots__/baseline.test.ts.snap +99 -24
- package/src/tests/ai-core.test.ts +71 -0
- package/src/tests/ai-features.test.ts +36 -0
- package/src/tests/baseline.test.ts +4 -1
- package/src/tests/cli.test.ts +2 -1
- package/src/tests/harness-agent.test.ts +19 -0
- package/src/tests/review-resume.test.ts +36 -8
- package/src/tests/review-scope.test.ts +67 -0
- package/src/tests/review-scripts.test.ts +2 -2
- package/src/tests/server.test.ts +54 -1
- package/src/tests/sidebar-sections.test.ts +94 -0
- package/src/tests/technical-summary.test.ts +176 -0
- package/src/tests/worktree.test.ts +404 -0
- package/assets/logo-legacy.png +0 -0
package/src/skill/hunt.md
CHANGED
|
@@ -4,16 +4,32 @@
|
|
|
4
4
|
> (`.agents/skills/houndsman/SKILL.md` e `.claude/skills/houndsman/SKILL.md`).
|
|
5
5
|
|
|
6
6
|
Revisão de PR: classificação de arquivos → revisão paralela por domínio → consolidação.
|
|
7
|
-
Invocação: `/houndsman` (default)
|
|
8
|
-
as regras do projeto ficam em `houndsman.config.ts` +
|
|
7
|
+
Invocação: `/houndsman` (default), `/houndsman hunt` ou `/houndsman hunt pr 456`.
|
|
8
|
+
O motor é o CLI **`houndsman`**; as regras do projeto ficam em `houndsman.config.ts` +
|
|
9
|
+
`.houndsman/`.
|
|
10
|
+
|
|
11
|
+
## Alvo da revisão
|
|
12
|
+
|
|
13
|
+
Traduza o pedido do usuário para flags **antes** de começar:
|
|
14
|
+
|
|
15
|
+
| Pedido | Alvo | Flag |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| “revise a PR 456”, “#456”, URL `.../pull/456` | PR | `--pr 456` |
|
|
18
|
+
| “revise a branch feature/x” | branch | `--branch feature/x` |
|
|
19
|
+
| “revise esta PR / o que mudou” | branch atual | *(sem flag)* |
|
|
20
|
+
|
|
21
|
+
> **Se o alvo é uma PR, use worktree.** Rode `houndsman --pr 456` (não `houndsman`), nunca faça
|
|
22
|
+
> checkout no clone principal e use o caminho impresso na saída como raiz dos subagentes.
|
|
23
|
+
> Isso permite revisar outras PRs em paralelo, cada uma na sua árvore.
|
|
9
24
|
|
|
10
25
|
**Quando usar:** revisar PR antes de merge · code review de feature branch · validar
|
|
11
26
|
mudanças antes de push.
|
|
12
27
|
|
|
13
28
|
> **Harness nativo (opcional):** além deste skill, o CLI tem `houndsman hunt` — o mesmo
|
|
14
29
|
> fluxo executado pelo próprio houndsman, com tools read-only, investigação por evidência
|
|
15
|
-
> e `VERIFY` obrigatório. Use `houndsman hunt --dry-run` para inspecionar sem IA.
|
|
16
|
-
>
|
|
30
|
+
> e `VERIFY` obrigatório. Use `houndsman hunt --dry-run` para inspecionar sem IA. Para uma
|
|
31
|
+
> PR específica, `houndsman hunt --pr 456` roda no worktree automaticamente. Detalhes em
|
|
32
|
+
> `docs/HARNESS.md`.
|
|
17
33
|
|
|
18
34
|
## Quem executa o quê
|
|
19
35
|
|
|
@@ -56,7 +72,8 @@ O contexto vem da **descrição da PR** via GitHub CLI (`gh`), quando disponíve
|
|
|
56
72
|
Um subagente executa o CLI e resume:
|
|
57
73
|
|
|
58
74
|
```
|
|
59
|
-
Você é o subagente de contexto do houndsman. Execute `houndsman context
|
|
75
|
+
Você é o subagente de contexto do houndsman. Execute `houndsman context --pr 456`
|
|
76
|
+
(ou `houndsman context --worktree <path>` se o prepare já criou o worktree);
|
|
60
77
|
leia o `context-brief.md` indicado na saída; siga as instruções e grave
|
|
61
78
|
`context-summary.json`. Retorne `objective`/`motivation`/`changeSummary` ou "indisponível".
|
|
62
79
|
```
|
|
@@ -72,9 +89,16 @@ leia o `context-brief.md` indicado na saída; siga as instruções e grave
|
|
|
72
89
|
## Passo 1 — Preparar (skill executa)
|
|
73
90
|
|
|
74
91
|
```bash
|
|
75
|
-
houndsman --
|
|
92
|
+
houndsman --pr 456 # alvo = PR (cria/reusa o worktree)
|
|
93
|
+
houndsman --base main # alvo = branch atual (clone principal)
|
|
76
94
|
```
|
|
77
95
|
|
|
96
|
+
Quando o alvo é uma PR, o prepare consulta a PR via `gh`, busca `refs/pull/<n>/head`, cria o
|
|
97
|
+
worktree **detached** em `<repo>/.houndsman/worktrees/<repo>/<branch>` e imprime o caminho.
|
|
98
|
+
Cada brief passa a trazer `**Worktree:** <path>` — essa é a raiz que os subagentes devem ler.
|
|
99
|
+
Guarde também a linha `PR #456 — <head> → <base>`: `<head>` é a branch a usar em
|
|
100
|
+
`status ping`, `findings` e `verify`.
|
|
101
|
+
|
|
78
102
|
Grava os artefatos no **storage global** (SQLite em `<HOUNDSMAN_HOME>/houndsman.db`) e o
|
|
79
103
|
staging dos subagentes em `<HOUNDSMAN_HOME>/work/<reviewId>/`:
|
|
80
104
|
|
|
@@ -157,6 +181,7 @@ calls) — evita estourar limites de taxa quando há muitos watchdogs. O prompt
|
|
|
157
181
|
```
|
|
158
182
|
Leia e execute o brief: <caminho absoluto impresso pelo prepare>/briefs/<watchdog-id>.md
|
|
159
183
|
Você é o watchdog <watchdog-id> do {{projectName}}.
|
|
184
|
+
Use como raiz de leitura o **Worktree:** indicado no brief (nunca o clone principal).
|
|
160
185
|
```
|
|
161
186
|
|
|
162
187
|
Cada subagente grava o `outputPath` indicado no brief (schema `.houndsman/core/skill/OUTPUT-FORMAT.md`).
|
|
@@ -171,7 +196,8 @@ não são lidos: registre-os em `skipped`.
|
|
|
171
196
|
## Passo 3 — Finalizar (skill executa)
|
|
172
197
|
|
|
173
198
|
```bash
|
|
174
|
-
houndsman --finalize
|
|
199
|
+
houndsman --pr 456 --finalize # alvo = PR (reusa o worktree gravado)
|
|
200
|
+
houndsman --finalize # alvo = branch atual
|
|
175
201
|
```
|
|
176
202
|
|
|
177
203
|
Falha com mensagem clara se faltar algum `findings/<watchdog>.json` no staging. Quando todos existem:
|
|
@@ -200,8 +200,8 @@ exports[`baseline — camada gerada setup gera os mesmos arquivos e conteúdos 2
|
|
|
200
200
|
".agents/skills/houndsman/SKILL.md":
|
|
201
201
|
"---
|
|
202
202
|
name: houndsman
|
|
203
|
-
description: 'Revisa Pull Requests com watchdogs especializados por domínio de negócio e mantém essas regras alinhadas ao projeto. Use para revisar/code review de PR, atualizar as regras de revisão dos watchdogs e corrigir/verificar os findings da revisão. Capabilities: \`hunt\` (revisar a PR), \`train\` (atualizar regras), \`fix\` (corrigir achados), \`verify\` (violações resolvidas?).'
|
|
204
|
-
argument-hint: 'Capability: hunt | train | fix | verify (default: hunt)'
|
|
203
|
+
description: 'Revisa Pull Requests com watchdogs especializados por domínio de negócio e mantém essas regras alinhadas ao projeto. Use para revisar/code review de PR, atualizar as regras de revisão dos watchdogs e corrigir/verificar os findings da revisão. Quando o alvo é uma PR (#456, “PR 456” ou URL do GitHub), revisa em worktree isolado automaticamente. Capabilities: \`hunt\` (revisar a PR), \`train\` (atualizar regras), \`fix\` (corrigir achados), \`verify\` (violações resolvidas?).'
|
|
204
|
+
argument-hint: 'Capability e alvo: hunt --pr 456 | hunt feature/x | train | fix | verify (default: hunt)'
|
|
205
205
|
---
|
|
206
206
|
|
|
207
207
|
# houndsman — acme
|
|
@@ -226,16 +226,50 @@ especialista por domínio · **finding** = um achado da revisão · **Jev** = pr
|
|
|
226
226
|
## Invocação
|
|
227
227
|
|
|
228
228
|
\`\`\`
|
|
229
|
-
/houndsman
|
|
230
|
-
/houndsman hunt
|
|
231
|
-
/houndsman
|
|
232
|
-
/houndsman
|
|
233
|
-
/houndsman
|
|
229
|
+
/houndsman → hunt (default)
|
|
230
|
+
/houndsman hunt → revisar a Pull Request
|
|
231
|
+
/houndsman hunt, revise a pr 456 → hunt da PR 456 (worktree automático)
|
|
232
|
+
/houndsman hunt feature/minha-branch → hunt da branch (clone atual)
|
|
233
|
+
/houndsman train → atualizar as regras dos watchdogs
|
|
234
|
+
/houndsman fix → corrigir os findings
|
|
235
|
+
/houndsman verify → conferir se as violações foram resolvidas
|
|
234
236
|
\`\`\`
|
|
235
237
|
|
|
236
|
-
Um argumento extra é o alvo da capability
|
|
237
|
-
|
|
238
|
-
|
|
238
|
+
Um argumento extra é o alvo da capability. Escolha a capability pelo que o usuário quer,
|
|
239
|
+
leia o documento correspondente em \`.houndsman/core/skill/<capability>.md\` e siga-o.
|
|
240
|
+
|
|
241
|
+
### Alvo da revisão (PR × branch)
|
|
242
|
+
|
|
243
|
+
Interprete o alvo em linguagem natural **antes** de rodar o CLI:
|
|
244
|
+
|
|
245
|
+
| O usuário disse | Alvo | Flags |
|
|
246
|
+
|---|---|---|
|
|
247
|
+
| \`PR 456\`, \`#456\`, \`pull 456\`, \`456\` (número sozinho), \`https://github.com/org/repo/pull/456\` | PR | \`--pr 456\` |
|
|
248
|
+
| \`feature/minha-branch\`, \`branch feature/x\` | branch | \`--branch feature/minha-branch\` |
|
|
249
|
+
| nada | branch atual | *(sem flag)* |
|
|
250
|
+
|
|
251
|
+
> **PR citada ⇒ worktree obrigatório.** Nunca rode \`houndsman\` “puro” para revisar uma PR que o
|
|
252
|
+
> usuário nomeou: o prepare precisa criar o worktree dedicado. Também **nunca** faça
|
|
253
|
+
> \`git checkout\` da PR no clone principal — é isso que permite revisar várias PRs ao mesmo
|
|
254
|
+
> tempo.
|
|
255
|
+
|
|
256
|
+
Sequência para uma PR (ex.: 456):
|
|
257
|
+
|
|
258
|
+
\`\`\`bash
|
|
259
|
+
houndsman context --pr 456
|
|
260
|
+
houndsman --pr 456 # cria/reusa o worktree e imprime o caminho
|
|
261
|
+
# despache os briefs (cada um traz **Worktree:** <path>)
|
|
262
|
+
houndsman --pr 456 --finalize
|
|
263
|
+
\`\`\`
|
|
264
|
+
|
|
265
|
+
- O worktree vive em \`<repo>/.houndsman/worktrees/<repo>/<branch>\` (detached no commit da PR)
|
|
266
|
+
e é reutilizado nas próximas execuções da mesma branch. Ele fica **dentro do workspace**
|
|
267
|
+
(e o staging também) para o agente ler/executar sem prompts de permissão.
|
|
268
|
+
- Use \`--worktree <path>\` para aproveitar um worktree já existente.
|
|
269
|
+
- O prepare imprime \`PR #456 — <head> → <base>\`; use \`<head>\` em \`status ping\`,
|
|
270
|
+
\`findings\` e \`verify\` (\`--branch <head>\`). A review guarda o worktree, então esses
|
|
271
|
+
comandos leem a árvore correta.
|
|
272
|
+
- PR de fork também funciona: o CLI busca \`refs/pull/<n>/head\`.
|
|
239
273
|
|
|
240
274
|
> **Depois de revisar (\`hunt\`), valide na UI:** \`bun run houndsman server --open\` — revise os
|
|
241
275
|
> achados e marque os que devem ser executados (\`execute\`) antes de rodar \`fix\`.
|
|
@@ -251,8 +285,8 @@ Escolha a capability pelo que o usuário quer, leia o documento correspondente e
|
|
|
251
285
|
".claude/skills/houndsman/SKILL.md":
|
|
252
286
|
"---
|
|
253
287
|
name: houndsman
|
|
254
|
-
description: 'Revisa Pull Requests com watchdogs especializados por domínio de negócio e mantém essas regras alinhadas ao projeto. Use para revisar/code review de PR, atualizar as regras de revisão dos watchdogs e corrigir/verificar os findings da revisão. Capabilities: \`hunt\` (revisar a PR), \`train\` (atualizar regras), \`fix\` (corrigir achados), \`verify\` (violações resolvidas?).'
|
|
255
|
-
argument-hint: 'Capability: hunt | train | fix | verify (default: hunt)'
|
|
288
|
+
description: 'Revisa Pull Requests com watchdogs especializados por domínio de negócio e mantém essas regras alinhadas ao projeto. Use para revisar/code review de PR, atualizar as regras de revisão dos watchdogs e corrigir/verificar os findings da revisão. Quando o alvo é uma PR (#456, “PR 456” ou URL do GitHub), revisa em worktree isolado automaticamente. Capabilities: \`hunt\` (revisar a PR), \`train\` (atualizar regras), \`fix\` (corrigir achados), \`verify\` (violações resolvidas?).'
|
|
289
|
+
argument-hint: 'Capability e alvo: hunt --pr 456 | hunt feature/x | train | fix | verify (default: hunt)'
|
|
256
290
|
---
|
|
257
291
|
|
|
258
292
|
# houndsman — acme
|
|
@@ -277,16 +311,50 @@ especialista por domínio · **finding** = um achado da revisão · **Jev** = pr
|
|
|
277
311
|
## Invocação
|
|
278
312
|
|
|
279
313
|
\`\`\`
|
|
280
|
-
/houndsman
|
|
281
|
-
/houndsman hunt
|
|
282
|
-
/houndsman
|
|
283
|
-
/houndsman
|
|
284
|
-
/houndsman
|
|
314
|
+
/houndsman → hunt (default)
|
|
315
|
+
/houndsman hunt → revisar a Pull Request
|
|
316
|
+
/houndsman hunt, revise a pr 456 → hunt da PR 456 (worktree automático)
|
|
317
|
+
/houndsman hunt feature/minha-branch → hunt da branch (clone atual)
|
|
318
|
+
/houndsman train → atualizar as regras dos watchdogs
|
|
319
|
+
/houndsman fix → corrigir os findings
|
|
320
|
+
/houndsman verify → conferir se as violações foram resolvidas
|
|
285
321
|
\`\`\`
|
|
286
322
|
|
|
287
|
-
Um argumento extra é o alvo da capability
|
|
288
|
-
|
|
289
|
-
|
|
323
|
+
Um argumento extra é o alvo da capability. Escolha a capability pelo que o usuário quer,
|
|
324
|
+
leia o documento correspondente em \`.houndsman/core/skill/<capability>.md\` e siga-o.
|
|
325
|
+
|
|
326
|
+
### Alvo da revisão (PR × branch)
|
|
327
|
+
|
|
328
|
+
Interprete o alvo em linguagem natural **antes** de rodar o CLI:
|
|
329
|
+
|
|
330
|
+
| O usuário disse | Alvo | Flags |
|
|
331
|
+
|---|---|---|
|
|
332
|
+
| \`PR 456\`, \`#456\`, \`pull 456\`, \`456\` (número sozinho), \`https://github.com/org/repo/pull/456\` | PR | \`--pr 456\` |
|
|
333
|
+
| \`feature/minha-branch\`, \`branch feature/x\` | branch | \`--branch feature/minha-branch\` |
|
|
334
|
+
| nada | branch atual | *(sem flag)* |
|
|
335
|
+
|
|
336
|
+
> **PR citada ⇒ worktree obrigatório.** Nunca rode \`houndsman\` “puro” para revisar uma PR que o
|
|
337
|
+
> usuário nomeou: o prepare precisa criar o worktree dedicado. Também **nunca** faça
|
|
338
|
+
> \`git checkout\` da PR no clone principal — é isso que permite revisar várias PRs ao mesmo
|
|
339
|
+
> tempo.
|
|
340
|
+
|
|
341
|
+
Sequência para uma PR (ex.: 456):
|
|
342
|
+
|
|
343
|
+
\`\`\`bash
|
|
344
|
+
houndsman context --pr 456
|
|
345
|
+
houndsman --pr 456 # cria/reusa o worktree e imprime o caminho
|
|
346
|
+
# despache os briefs (cada um traz **Worktree:** <path>)
|
|
347
|
+
houndsman --pr 456 --finalize
|
|
348
|
+
\`\`\`
|
|
349
|
+
|
|
350
|
+
- O worktree vive em \`<repo>/.houndsman/worktrees/<repo>/<branch>\` (detached no commit da PR)
|
|
351
|
+
e é reutilizado nas próximas execuções da mesma branch. Ele fica **dentro do workspace**
|
|
352
|
+
(e o staging também) para o agente ler/executar sem prompts de permissão.
|
|
353
|
+
- Use \`--worktree <path>\` para aproveitar um worktree já existente.
|
|
354
|
+
- O prepare imprime \`PR #456 — <head> → <base>\`; use \`<head>\` em \`status ping\`,
|
|
355
|
+
\`findings\` e \`verify\` (\`--branch <head>\`). A review guarda o worktree, então esses
|
|
356
|
+
comandos leem a árvore correta.
|
|
357
|
+
- PR de fork também funciona: o CLI busca \`refs/pull/<n>/head\`.
|
|
290
358
|
|
|
291
359
|
> **Depois de revisar (\`hunt\`), valide na UI:** \`bun run houndsman server --open\` — revise os
|
|
292
360
|
> achados e marque os que devem ser executados (\`execute\`) antes de rodar \`fix\`.
|
|
@@ -300,10 +368,12 @@ Escolha a capability pelo que o usuário quer, leia o documento correspondente e
|
|
|
300
368
|
"
|
|
301
369
|
,
|
|
302
370
|
".houndsman/.gitignore":
|
|
303
|
-
"# Diretórios gerados pelo houndsman — \`core/\` é regenerado por \`houndsman update
|
|
304
|
-
# e
|
|
371
|
+
"# Diretórios gerados pelo houndsman — \`core/\` é regenerado por \`houndsman update\`.
|
|
372
|
+
# \`work/\` e \`worktrees/\` são runtime de revisão (staging e worktrees).
|
|
305
373
|
core/
|
|
306
374
|
.runs/
|
|
375
|
+
work/
|
|
376
|
+
worktrees/
|
|
307
377
|
"
|
|
308
378
|
,
|
|
309
379
|
".houndsman/core/types.ts":
|
|
@@ -423,10 +493,14 @@ export type HoundsmanAst = {
|
|
|
423
493
|
/** Providers suportados pela camada de IA. */
|
|
424
494
|
export type AiProviderKind = "deepseek" | "openai" | "anthropic";
|
|
425
495
|
|
|
496
|
+
/** Opções do AI SDK por provider (repassadas sem interpretação). */
|
|
497
|
+
export type AiProviderOptions = Record<string, Record<string, unknown>>;
|
|
498
|
+
|
|
426
499
|
/** Features de IA do fluxo normal (todas fail-open). */
|
|
427
500
|
export type HoundsmanAiFeatures = {
|
|
428
501
|
context?: boolean;
|
|
429
502
|
brief?: boolean;
|
|
503
|
+
technical?: boolean;
|
|
430
504
|
classification?: boolean;
|
|
431
505
|
preAnalysis?: boolean;
|
|
432
506
|
verify?: boolean;
|
|
@@ -449,6 +523,7 @@ export type HoundsmanAi = {
|
|
|
449
523
|
maxSteps?: number;
|
|
450
524
|
maxOutputTokens?: number;
|
|
451
525
|
temperature?: number;
|
|
526
|
+
providerOptions?: AiProviderOptions;
|
|
452
527
|
timeoutMs?: number;
|
|
453
528
|
maxCallsPerReview?: number;
|
|
454
529
|
maxTokensPerReview?: number;
|
|
@@ -594,7 +669,7 @@ exports[`baseline — camada gerada setup gera os mesmos arquivos e conteúdos 3
|
|
|
594
669
|
},
|
|
595
670
|
{
|
|
596
671
|
"file": ".houndsman/core/skill/README.md",
|
|
597
|
-
"sha256": "
|
|
672
|
+
"sha256": "1738ef9c6d9ec919",
|
|
598
673
|
},
|
|
599
674
|
{
|
|
600
675
|
"file": ".houndsman/core/skill/SETUP.md",
|
|
@@ -610,7 +685,7 @@ exports[`baseline — camada gerada setup gera os mesmos arquivos e conteúdos 3
|
|
|
610
685
|
},
|
|
611
686
|
{
|
|
612
687
|
"file": ".houndsman/core/skill/hunt.md",
|
|
613
|
-
"sha256": "
|
|
688
|
+
"sha256": "1cf68076325da4ab",
|
|
614
689
|
},
|
|
615
690
|
{
|
|
616
691
|
"file": ".houndsman/core/skill/train.md",
|
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import type { LanguageModel } from "ai";
|
|
6
7
|
import { z } from "zod";
|
|
7
8
|
import { AiClient, AiUsageBudget, AiUsageRecorder } from "../core/ai";
|
|
9
|
+
import { buildGenerateTextArgs } from "../core/ai/ai-sdk-model-provider";
|
|
10
|
+
import { temperatureApplies } from "../core/ai/provider-capabilities";
|
|
8
11
|
import { defineHoundsmanConfig } from "../core/config";
|
|
9
12
|
import { DEFAULT_AI_MODEL } from "../core/defaults";
|
|
10
13
|
import { ProjectConfig } from "../core/project-config";
|
|
@@ -82,6 +85,74 @@ describe("AiClient", () => {
|
|
|
82
85
|
});
|
|
83
86
|
});
|
|
84
87
|
|
|
88
|
+
describe("temperature e providerOptions", () => {
|
|
89
|
+
test("DeepSeek V4 ignora temperature; thinking disabled reabilita", () => {
|
|
90
|
+
expect(temperatureApplies("deepseek", "deepseek-v4-pro", undefined)).toBe(false);
|
|
91
|
+
expect(
|
|
92
|
+
temperatureApplies("deepseek", "deepseek-v4-pro", {
|
|
93
|
+
deepseek: { thinking: { type: "disabled" } },
|
|
94
|
+
}),
|
|
95
|
+
).toBe(true);
|
|
96
|
+
expect(
|
|
97
|
+
temperatureApplies("deepseek", "deepseek-v4-pro", {
|
|
98
|
+
deepseek: { thinking: { type: "enabled" } },
|
|
99
|
+
}),
|
|
100
|
+
).toBe(false);
|
|
101
|
+
expect(temperatureApplies("deepseek", "deepseek-chat", undefined)).toBe(true);
|
|
102
|
+
expect(temperatureApplies("openai", "gpt-4o", undefined)).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("adapter omite temperature ignorada e repassa providerOptions", () => {
|
|
106
|
+
const model = {} as LanguageModel;
|
|
107
|
+
const thinkingOff = buildGenerateTextArgs(
|
|
108
|
+
model,
|
|
109
|
+
{
|
|
110
|
+
prompt: "oi",
|
|
111
|
+
temperature: 0,
|
|
112
|
+
providerOptions: { deepseek: { thinking: { type: "disabled" } } },
|
|
113
|
+
},
|
|
114
|
+
"deepseek",
|
|
115
|
+
"deepseek-v4-pro",
|
|
116
|
+
);
|
|
117
|
+
expect(thinkingOff).toMatchObject({
|
|
118
|
+
temperature: 0,
|
|
119
|
+
providerOptions: { deepseek: { thinking: { type: "disabled" } } },
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const defaultThinking = buildGenerateTextArgs(
|
|
123
|
+
model,
|
|
124
|
+
{ prompt: "oi", temperature: 0 },
|
|
125
|
+
"deepseek",
|
|
126
|
+
"deepseek-v4-pro",
|
|
127
|
+
);
|
|
128
|
+
expect(defaultThinking).not.toHaveProperty("temperature");
|
|
129
|
+
|
|
130
|
+
const openai = buildGenerateTextArgs(
|
|
131
|
+
model,
|
|
132
|
+
{ prompt: "oi", temperature: 0 },
|
|
133
|
+
"openai",
|
|
134
|
+
"gpt-4o",
|
|
135
|
+
);
|
|
136
|
+
expect(openai).toMatchObject({ temperature: 0 });
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("AiClient injeta providerOptions da config", async () => {
|
|
140
|
+
const configured = new ProjectConfig({
|
|
141
|
+
...fakeHoundsmanConfig,
|
|
142
|
+
ai: { providerOptions: { deepseek: { thinking: { type: "disabled" } } } },
|
|
143
|
+
});
|
|
144
|
+
const provider = new FakeModelProvider(() => ({ ok: true }));
|
|
145
|
+
const client = new AiClient({ project: configured, provider });
|
|
146
|
+
await client.generateObject("x", {
|
|
147
|
+
prompt: "oi",
|
|
148
|
+
outputSchema: z.object({ ok: z.boolean() }),
|
|
149
|
+
});
|
|
150
|
+
expect(provider.requests[0]?.providerOptions).toEqual({
|
|
151
|
+
deepseek: { thinking: { type: "disabled" } },
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
85
156
|
describe("AiUsageBudget / AiUsageRecorder", () => {
|
|
86
157
|
test("acumula uso e bloqueia no teto", () => {
|
|
87
158
|
const budget = new AiUsageBudget({ maxCalls: 2 });
|
|
@@ -8,6 +8,7 @@ import { AiBriefEnricher } from "../core/ai/features/brief-enricher";
|
|
|
8
8
|
import { AiClassifier } from "../core/ai/features/classifier";
|
|
9
9
|
import { AiContextSummarizer } from "../core/ai/features/context-summarizer";
|
|
10
10
|
import { AiPreAnalyzer } from "../core/ai/features/pre-analyzer";
|
|
11
|
+
import { AiTechnicalSummarizer } from "../core/ai/features/technical-summarizer";
|
|
11
12
|
import { AiVerifier } from "../core/ai/features/verifier";
|
|
12
13
|
import { briefInstructions, briefPrompt } from "../core/ai/prompts/brief";
|
|
13
14
|
import { renderBrief } from "../core/brief";
|
|
@@ -153,6 +154,41 @@ describe("AiClassifier", () => {
|
|
|
153
154
|
});
|
|
154
155
|
});
|
|
155
156
|
|
|
157
|
+
describe("AiTechnicalSummarizer", () => {
|
|
158
|
+
test("devolve os destaques estruturais do modelo", async () => {
|
|
159
|
+
const provider = new FakeModelProvider(() => ({
|
|
160
|
+
overview: "Novo módulo bin-lookup",
|
|
161
|
+
highlights: [{ area: "Contratos", detail: "`BinLookupProvider`" }],
|
|
162
|
+
}));
|
|
163
|
+
const summarizer = new AiTechnicalSummarizer(new AiClient({ project, provider }));
|
|
164
|
+
const summary = await summarizer.summarize({
|
|
165
|
+
branch: "main",
|
|
166
|
+
classification: {
|
|
167
|
+
files: { "src/bin-lookup/service.ts": { status: "added" } },
|
|
168
|
+
churn: { "src/bin-lookup/service.ts": { additions: 3, deletions: 0 } },
|
|
169
|
+
},
|
|
170
|
+
headSha: "abc",
|
|
171
|
+
domains: ["Alpha"],
|
|
172
|
+
});
|
|
173
|
+
expect(summary?.source).toBe("ai");
|
|
174
|
+
expect(summary?.headSha).toBe("abc");
|
|
175
|
+
expect(summary?.highlights[0].area).toBe("Contratos");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("sem IA o resumo é nulo (fallback no chamador)", async () => {
|
|
179
|
+
const summarizer = new AiTechnicalSummarizer(
|
|
180
|
+
new AiClient({ project, provider: null }),
|
|
181
|
+
);
|
|
182
|
+
expect(summarizer.enabled).toBe(false);
|
|
183
|
+
expect(
|
|
184
|
+
await summarizer.summarize({
|
|
185
|
+
branch: "main",
|
|
186
|
+
classification: { files: {}, churn: {} },
|
|
187
|
+
}),
|
|
188
|
+
).toBeNull();
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
156
192
|
describe("AiVerifier", () => {
|
|
157
193
|
test("devolve confiança apenas para ids conhecidos", async () => {
|
|
158
194
|
const provider = new FakeModelProvider(() => ({
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
import { defineHoundsmanConfig } from "../core/config";
|
|
29
29
|
import { defaultOptions, finalize, prepare, reviewContext } from "../core/orchestrator";
|
|
30
30
|
import { displayPath } from "../core/paths";
|
|
31
|
+
import { RESUME_MAX_CHARS } from "../core/renderers/resume-renderer";
|
|
31
32
|
import {
|
|
32
33
|
type MergedFinding,
|
|
33
34
|
renderDomainReport,
|
|
@@ -434,7 +435,9 @@ describe("baseline — artefatos de prepare/finalize", () => {
|
|
|
434
435
|
}
|
|
435
436
|
const resume = storage.readArtifact(review.id, "review-resume.md");
|
|
436
437
|
expect(resume?.kind).toBe("resume");
|
|
437
|
-
expect(
|
|
438
|
+
expect(resume?.content).toContain("## Pontos técnicos");
|
|
439
|
+
expect(resume?.content).toContain("Remoções");
|
|
440
|
+
expect((resume?.content ?? "").length).toBeLessThanOrEqual(RESUME_MAX_CHARS);
|
|
438
441
|
expect(
|
|
439
442
|
normalize(JSON.stringify(storage.readFindings(review.id), null, 2)),
|
|
440
443
|
).toMatchSnapshot("state.json");
|
package/src/tests/cli.test.ts
CHANGED
|
@@ -188,7 +188,8 @@ describe("cli smoke — imports dinâmicos", () => {
|
|
|
188
188
|
test("doctor valida a config (import lazy de registry-check)", () => {
|
|
189
189
|
const project = makeProject();
|
|
190
190
|
try {
|
|
191
|
-
|
|
191
|
+
// `--no-ping`: o teste cobre import lazy, não latência de rede.
|
|
192
|
+
const result = runCli(project.cwd, ["doctor", "--no-ping"]);
|
|
192
193
|
expect(result.code).toBe(0);
|
|
193
194
|
expect(result.stdout).toContain("Validando");
|
|
194
195
|
} finally {
|
|
@@ -119,4 +119,23 @@ describe("ReviewAgent", () => {
|
|
|
119
119
|
expect(result.status).toBe("failed");
|
|
120
120
|
expect(result.error).toContain("429");
|
|
121
121
|
});
|
|
122
|
+
|
|
123
|
+
test("repassa providerOptions da config em toda chamada", async () => {
|
|
124
|
+
const provider = investigatingProvider();
|
|
125
|
+
const reporter = new SilentReporter();
|
|
126
|
+
const agent = new ReviewAgent(
|
|
127
|
+
fakeAgentDeps(provider, {
|
|
128
|
+
context: fakeAgentContext({ repoRoot: dir }),
|
|
129
|
+
reporter,
|
|
130
|
+
providerOptions: { deepseek: { thinking: { type: "disabled" } } },
|
|
131
|
+
}),
|
|
132
|
+
);
|
|
133
|
+
await agent.run();
|
|
134
|
+
expect(provider.requests.length).toBeGreaterThan(0);
|
|
135
|
+
for (const request of provider.requests) {
|
|
136
|
+
expect(request.providerOptions).toEqual({
|
|
137
|
+
deepseek: { thinking: { type: "disabled" } },
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
});
|
|
122
141
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Resumo final (`review-resume.md`) — conteúdo de negócio + pontos técnicos
|
|
3
|
-
* determinístico e limitado a `RESUME_MAX_CHARS`.
|
|
2
|
+
* Resumo final (`review-resume.md`) — conteúdo de negócio + pontos técnicos
|
|
3
|
+
* estruturais, determinístico e limitado a `RESUME_MAX_CHARS`.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { describe, expect, test } from "bun:test";
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
renderReviewResume,
|
|
11
11
|
} from "../core/renderers/resume-renderer";
|
|
12
12
|
import type { MergedFinding } from "../core/report-builder";
|
|
13
|
+
import type { TechnicalSummary } from "../core/technical-summary";
|
|
13
14
|
|
|
14
15
|
function finding(overrides: Partial<MergedFinding> = {}): MergedFinding {
|
|
15
16
|
return {
|
|
@@ -32,8 +33,24 @@ const classification: ResumeClassification = {
|
|
|
32
33
|
watchdogs: { alpha: ["src/a.ts"], beta: ["src/b.ts"] },
|
|
33
34
|
};
|
|
34
35
|
|
|
36
|
+
const technical: TechnicalSummary = {
|
|
37
|
+
source: "ai",
|
|
38
|
+
headSha: "abc123",
|
|
39
|
+
overview: "Novo service `bin-lookup` com provider e repository.",
|
|
40
|
+
highlights: [
|
|
41
|
+
{
|
|
42
|
+
area: "Contratos",
|
|
43
|
+
detail: "`BinLookupProvider` (src/bin-lookup/provider.ts)",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
area: "Implementações",
|
|
47
|
+
detail: "`BinLookupService` (src/bin-lookup/service.ts)",
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
};
|
|
51
|
+
|
|
35
52
|
describe("renderReviewResume", () => {
|
|
36
|
-
test("une contexto de negócio e pontos técnicos", () => {
|
|
53
|
+
test("une contexto de negócio e pontos técnicos estruturais", () => {
|
|
37
54
|
const md = renderReviewResume({
|
|
38
55
|
branch: "feat/x",
|
|
39
56
|
date: "2026-10-17",
|
|
@@ -48,7 +65,7 @@ describe("renderReviewResume", () => {
|
|
|
48
65
|
finding({ severity: "blocking", message: "quebra o cadastro" }),
|
|
49
66
|
finding({ severity: "attention", file: "src/b.ts", line: undefined }),
|
|
50
67
|
],
|
|
51
|
-
|
|
68
|
+
technical,
|
|
52
69
|
});
|
|
53
70
|
|
|
54
71
|
expect(md).toContain("# Resumo da revisão — feat/x");
|
|
@@ -58,8 +75,11 @@ describe("renderReviewResume", () => {
|
|
|
58
75
|
expect(md).toContain("**Por quê:** Reduzir o esforço manual.");
|
|
59
76
|
expect(md).toContain("- Novo fluxo de cadastro.");
|
|
60
77
|
expect(md).not.toContain("## Números");
|
|
61
|
-
expect(md).toContain("
|
|
62
|
-
expect(md).toContain("`
|
|
78
|
+
expect(md).toContain("## Pontos técnicos");
|
|
79
|
+
expect(md).toContain("Novo service `bin-lookup`");
|
|
80
|
+
expect(md).toContain("**Contratos** — `BinLookupProvider`");
|
|
81
|
+
expect(md).toContain("**Implementações** — `BinLookupService`");
|
|
82
|
+
expect(md).not.toContain("quebra o cadastro");
|
|
63
83
|
expect(md.length).toBeLessThanOrEqual(RESUME_MAX_CHARS);
|
|
64
84
|
});
|
|
65
85
|
|
|
@@ -72,7 +92,7 @@ describe("renderReviewResume", () => {
|
|
|
72
92
|
|
|
73
93
|
expect(md).toContain("Sem resumo de contexto da PR disponível.");
|
|
74
94
|
expect(md).toContain("✅ Aprovado");
|
|
75
|
-
expect(md).toContain("
|
|
95
|
+
expect(md).toContain("Sem mudanças estruturais detectadas nesta revisão.");
|
|
76
96
|
});
|
|
77
97
|
|
|
78
98
|
test("respeita o teto de caracteres", () => {
|
|
@@ -89,10 +109,18 @@ describe("renderReviewResume", () => {
|
|
|
89
109
|
available: true,
|
|
90
110
|
objective: "o".repeat(1000),
|
|
91
111
|
motivation: "m".repeat(1000),
|
|
92
|
-
changeSummary: ["a".repeat(
|
|
112
|
+
changeSummary: ["a".repeat(300), "b".repeat(300)],
|
|
93
113
|
},
|
|
94
114
|
classification,
|
|
95
115
|
findings,
|
|
116
|
+
technical: {
|
|
117
|
+
source: "deterministic",
|
|
118
|
+
overview: "v".repeat(600),
|
|
119
|
+
highlights: Array.from({ length: 10 }, (_, index) => ({
|
|
120
|
+
area: `Área ${index}`,
|
|
121
|
+
detail: "x".repeat(300),
|
|
122
|
+
})),
|
|
123
|
+
},
|
|
96
124
|
});
|
|
97
125
|
|
|
98
126
|
expect(md.length).toBeLessThanOrEqual(RESUME_MAX_CHARS);
|
|
@@ -300,3 +300,70 @@ describe("prepare/finalize — mesmo commit", () => {
|
|
|
300
300
|
}
|
|
301
301
|
}, 30_000);
|
|
302
302
|
});
|
|
303
|
+
|
|
304
|
+
// ---------------------------------------------------------------------------
|
|
305
|
+
// Run vazia
|
|
306
|
+
// ---------------------------------------------------------------------------
|
|
307
|
+
|
|
308
|
+
describe("prepare sem alterações", () => {
|
|
309
|
+
test("run recém-criada é descartada (sem run vazia na lista)", async () => {
|
|
310
|
+
const repo = makeRepo();
|
|
311
|
+
const originalCwd = process.cwd();
|
|
312
|
+
const originalHome = process.env.HOUNDSMAN_HOME;
|
|
313
|
+
let storage: SQLiteStorageProvider | null = null;
|
|
314
|
+
|
|
315
|
+
try {
|
|
316
|
+
process.env.HOUNDSMAN_HOME = join(repo.cwd, ".home");
|
|
317
|
+
process.chdir(repo.cwd);
|
|
318
|
+
|
|
319
|
+
storage = new SQLiteStorageProvider(join(repo.cwd, "houndsman.db"));
|
|
320
|
+
const record = storage.getOrCreateReview({
|
|
321
|
+
repo: resolveRepoIdentity(),
|
|
322
|
+
branch: "main",
|
|
323
|
+
slug: "main",
|
|
324
|
+
});
|
|
325
|
+
const opts = {
|
|
326
|
+
...defaultOptions(),
|
|
327
|
+
base: repo.head,
|
|
328
|
+
head: "HEAD",
|
|
329
|
+
noJev: true,
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const result = await prepare(
|
|
333
|
+
opts,
|
|
334
|
+
"main",
|
|
335
|
+
fakeHoundsmanConfig,
|
|
336
|
+
reviewContext(storage, record),
|
|
337
|
+
);
|
|
338
|
+
expect(result.messages.join("\n")).toContain("nenhuma run criada");
|
|
339
|
+
expect(storage.getReview(record.id)).toBeNull();
|
|
340
|
+
expect(storage.listArtifacts(record.id)).toHaveLength(0);
|
|
341
|
+
|
|
342
|
+
// Run com artefatos anteriores (contexto) continua existindo.
|
|
343
|
+
const kept = storage.getOrCreateReview({
|
|
344
|
+
repo: resolveRepoIdentity(),
|
|
345
|
+
branch: "main",
|
|
346
|
+
slug: "main",
|
|
347
|
+
});
|
|
348
|
+
storage.writeArtifact(kept.id, {
|
|
349
|
+
name: "context.json",
|
|
350
|
+
kind: "context",
|
|
351
|
+
content: "{}\n",
|
|
352
|
+
});
|
|
353
|
+
await prepare(
|
|
354
|
+
opts,
|
|
355
|
+
"main",
|
|
356
|
+
fakeHoundsmanConfig,
|
|
357
|
+
reviewContext(storage, kept),
|
|
358
|
+
);
|
|
359
|
+
expect(storage.getReview(kept.id)?.status).toBe("review_done");
|
|
360
|
+
expect(storage.readArtifact(kept.id, "context.json")).not.toBeNull();
|
|
361
|
+
} finally {
|
|
362
|
+
storage?.close();
|
|
363
|
+
process.chdir(originalCwd);
|
|
364
|
+
if (originalHome === undefined) delete process.env.HOUNDSMAN_HOME;
|
|
365
|
+
else process.env.HOUNDSMAN_HOME = originalHome;
|
|
366
|
+
repo.cleanup();
|
|
367
|
+
}
|
|
368
|
+
}, 30_000);
|
|
369
|
+
});
|
|
@@ -799,9 +799,9 @@ describe("context-summary", () => {
|
|
|
799
799
|
(_, i) => `item ${i} ${"z".repeat(300)}`,
|
|
800
800
|
),
|
|
801
801
|
});
|
|
802
|
-
expect(summary?.objective?.length).toBeLessThanOrEqual(
|
|
802
|
+
expect(summary?.objective?.length).toBeLessThanOrEqual(400);
|
|
803
803
|
expect(summary?.objective?.endsWith("…")).toBe(true);
|
|
804
|
-
expect(summary?.changeSummary).toHaveLength(
|
|
804
|
+
expect(summary?.changeSummary).toHaveLength(6);
|
|
805
805
|
});
|
|
806
806
|
|
|
807
807
|
test("renderReport coloca a seção de contexto quando presente", () => {
|