@payconductor/houndsman 0.12.0 → 0.13.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 +17 -1
- package/package.json +1 -1
- package/src/cli/commands/doctor-command.ts +22 -0
- package/src/cli/commands/hunt-command.ts +462 -0
- package/src/cli/commands/review-command.ts +20 -2
- package/src/cli/help.ts +17 -2
- package/src/cli/index.ts +17 -2
- package/src/cli/progress.ts +45 -0
- package/src/core/ai/ai-sdk-model-provider.ts +39 -0
- package/src/core/ai/index.ts +6 -1
- package/src/core/ai/types.ts +2 -0
- package/src/core/commands/scaffold.ts +11 -0
- package/src/core/config.ts +28 -0
- package/src/core/defaults.ts +15 -0
- package/src/core/engine/review-scope.ts +92 -25
- package/src/core/engine/review-session.ts +7 -3
- package/src/core/engine/review-workspace.ts +14 -0
- package/src/core/env.ts +14 -0
- package/src/core/hunt/hunt-runner.ts +328 -0
- package/src/core/hunt/index.ts +59 -0
- package/src/core/hunt/maki-install.ts +331 -0
- package/src/core/hunt/maki-model.ts +101 -0
- package/src/core/hunt/maki-prompt.ts +71 -0
- package/src/core/hunt/maki-release.ts +104 -0
- package/src/core/hunt/maki-runner.ts +276 -0
- package/src/core/hunt/pool.ts +30 -0
- package/src/core/logging/index.ts +2 -0
- package/src/core/logging/logger.ts +19 -0
- package/src/core/logging/progress.ts +282 -0
- package/src/core/orchestrator.ts +158 -49
- package/src/core/project-config.ts +38 -0
- package/src/core/storage/types.ts +1 -0
- package/src/index.ts +49 -0
- package/src/skill/README.md +12 -0
- package/src/skill/SETUP.md +21 -1
- package/src/skill/SKILL.md +4 -0
- package/src/skill/hunt.md +36 -0
- package/src/tests/__snapshots__/baseline.test.ts.snap +22 -3
- package/src/tests/ai-core.test.ts +38 -1
- package/src/tests/cli.test.ts +1 -0
- package/src/tests/hunt-cli.test.ts +185 -0
- package/src/tests/hunt-install.test.ts +153 -0
- package/src/tests/hunt-maki.test.ts +467 -0
- package/src/tests/progress.test.ts +117 -0
- package/src/tests/review-scope.test.ts +62 -0
package/src/index.ts
CHANGED
|
@@ -80,13 +80,18 @@ export type {
|
|
|
80
80
|
HoundsmanConcurrency,
|
|
81
81
|
HoundsmanConfig,
|
|
82
82
|
HoundsmanConfigInput,
|
|
83
|
+
HoundsmanHunt,
|
|
83
84
|
HoundsmanStorage,
|
|
84
85
|
} from "./core/config";
|
|
85
86
|
export {
|
|
86
87
|
DEFAULT_ALWAYS_INCLUDE_DOCS,
|
|
87
88
|
DEFAULT_AST_MAX_FILE_BYTES,
|
|
88
89
|
DEFAULT_AST_MAX_IMPACT_FILES,
|
|
90
|
+
DEFAULT_HUNT_CONCURRENCY,
|
|
91
|
+
DEFAULT_HUNT_RETRIES,
|
|
92
|
+
DEFAULT_HUNT_TIMEOUT_MS,
|
|
89
93
|
DEFAULT_JEV_CONCURRENCY,
|
|
94
|
+
DEFAULT_MAKI_VERSION,
|
|
90
95
|
DEFAULT_MAX_FILES_PER_BRIEF,
|
|
91
96
|
defineHoundsmanConfig,
|
|
92
97
|
resolveJevConcurrency,
|
|
@@ -181,6 +186,50 @@ export {
|
|
|
181
186
|
syncState,
|
|
182
187
|
} from "./core/findings-state";
|
|
183
188
|
export type { GitResult, GitRunner } from "./core/git-runner";
|
|
189
|
+
export type {
|
|
190
|
+
HuntEvent,
|
|
191
|
+
HuntReport,
|
|
192
|
+
HuntRunnerOptions,
|
|
193
|
+
HuntTotals,
|
|
194
|
+
HuntWatchdogResult,
|
|
195
|
+
InstallMakiOptions,
|
|
196
|
+
InstallMakiResult,
|
|
197
|
+
MakiBinary,
|
|
198
|
+
MakiBinarySource,
|
|
199
|
+
MakiInstallManifest,
|
|
200
|
+
MakiModel,
|
|
201
|
+
MakiProcessRunner,
|
|
202
|
+
MakiRunMetrics,
|
|
203
|
+
MakiRunRequest,
|
|
204
|
+
MakiRunResult,
|
|
205
|
+
MakiUsage,
|
|
206
|
+
} from "./core/hunt";
|
|
207
|
+
export {
|
|
208
|
+
extractJsonObject,
|
|
209
|
+
HuntRunner,
|
|
210
|
+
houndsmanKeyEnvFor,
|
|
211
|
+
installMaki,
|
|
212
|
+
MAKI_API_KEY_ENV,
|
|
213
|
+
MAKI_DISALLOWED_TOOLS,
|
|
214
|
+
makiArgs,
|
|
215
|
+
makiCurrentFile,
|
|
216
|
+
makiInstallDir,
|
|
217
|
+
makiSourceLabel,
|
|
218
|
+
makiToolsRoot,
|
|
219
|
+
mapPool,
|
|
220
|
+
metricsFrom,
|
|
221
|
+
parseMakiOutput,
|
|
222
|
+
probeMakiVersion,
|
|
223
|
+
readMakiInstall,
|
|
224
|
+
renderHuntPrompt,
|
|
225
|
+
renderRetrySuffix,
|
|
226
|
+
requireMakiBinary,
|
|
227
|
+
requireMakiModel,
|
|
228
|
+
resolveMakiBinary,
|
|
229
|
+
resolveMakiModel,
|
|
230
|
+
SpawnMakiRunner,
|
|
231
|
+
uninstallMaki,
|
|
232
|
+
} from "./core/hunt";
|
|
184
233
|
export {
|
|
185
234
|
CORE_DIR,
|
|
186
235
|
CORE_SKILL_DIR,
|
package/src/skill/README.md
CHANGED
|
@@ -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).
|
|
@@ -53,6 +63,8 @@ Um único skill (`/houndsman`), quatro capabilities (detalhadas em `hunt.md`, `t
|
|
|
53
63
|
| `houndsman` | Passo 1 — preparar (gera os briefs) |
|
|
54
64
|
| `houndsman --pr <n>` | Passo 1 em worktree dedicado (revisões paralelas) |
|
|
55
65
|
| `houndsman --finalize` | Passo 3 — gerar o relatório (`--pr <n>` reusa o worktree) |
|
|
66
|
+
| `houndsman hunt [--pr <n>]` | Harness local opcional (maki): prepare → watchdogs → finalize |
|
|
67
|
+
| `houndsman hunt install` | Baixa o maki fixado para `<HOUNDSMAN_HOME>/tools/maki` |
|
|
56
68
|
| `houndsman worktree <ação>` | `list`/`add`/`remove`/`prune`/`path` dos worktrees |
|
|
57
69
|
| `houndsman watchdog <id>` | Contexto de um watchdog (`--json`, `--rules`) |
|
|
58
70
|
| `houndsman findings <sub>` | Triagem/correção dos findings (`list`/`set`/`done`/`sync`/`batch`/`verify`/...) |
|
package/src/skill/SETUP.md
CHANGED
|
@@ -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):
|
|
@@ -91,6 +110,7 @@ Definido em `src/core/config.ts` (pacote) e espelhado em `.houndsman/core/types.
|
|
|
91
110
|
| `budgets` | **Opcional** — teto de arquivos por brief (`maxFilesPerBrief`, default `40`); o excedente vira "fora do orçamento" (registrar em `skipped`). |
|
|
92
111
|
| `concurrency` | **Opcional** — chamadas Jev simultâneas (`jev`, default `5`). |
|
|
93
112
|
| `ast` | **Opcional** — AST determinístico para TS/JS (`enabled`, `maxFileBytes`, `maxImpactFiles`, `languages`, `aliases`). Default: ligado; falhas são fail-open. |
|
|
113
|
+
| `hunt` | **Opcional** — harness local (`model`, `concurrency`, `timeoutMs`, `retries`, `binaryPath`, `enabled`). Só afeta `houndsman hunt`; a skill não depende. |
|
|
94
114
|
|
|
95
115
|
> O motor **não contém** ids, paths ou regras do projeto — tudo chega por `HoundsmanConfig`,
|
|
96
116
|
> carregado em runtime a partir de `houndsman.config.ts` (ou `--config <path>`).
|
|
@@ -103,7 +123,7 @@ houndsman/
|
|
|
103
123
|
└── src/
|
|
104
124
|
├── index.ts # API pública (defineWatchdog, defineHoundsmanConfig, tipos)
|
|
105
125
|
├── cli/index.ts # CLI único — bin `houndsman`
|
|
106
|
-
├── core/ # bibliotecas genéricas (config, orchestrator, brief, jev/, …)
|
|
126
|
+
├── core/ # bibliotecas genéricas (config, orchestrator, brief, jev/, hunt/, …)
|
|
107
127
|
├── skill/*.md # assets markdown (embarcados no pacote)
|
|
108
128
|
└── tests/ # suíte da lib (project-agnóstica, "project fake")
|
|
109
129
|
```
|
package/src/skill/SKILL.md
CHANGED
|
@@ -38,6 +38,10 @@ especialista por domínio · **finding** = um achado da revisão · **Jev** = pr
|
|
|
38
38
|
Um argumento extra é o alvo da capability. Escolha a capability pelo que o usuário quer,
|
|
39
39
|
leia o documento correspondente em `.houndsman/core/skill/<capability>.md` e siga-o.
|
|
40
40
|
|
|
41
|
+
> **Harness local (opcional):** para rodar sem agente externo, o operador pode instalar o
|
|
42
|
+
> maki (`houndsman hunt install`) e usar `houndsman hunt --pr 456`. Isso não muda esta
|
|
43
|
+
> skill: se você é o agente, siga o fluxo normal e não rode `houndsman hunt`.
|
|
44
|
+
|
|
41
45
|
### Alvo da revisão (PR × branch)
|
|
42
46
|
|
|
43
47
|
Interprete o alvo em linguagem natural **antes** de rodar o CLI:
|
package/src/skill/hunt.md
CHANGED
|
@@ -8,6 +8,20 @@ Invocação: `/houndsman` (default), `/houndsman hunt` ou `/houndsman hunt pr 45
|
|
|
8
8
|
O motor é o CLI **`houndsman`**; as regras do projeto ficam em `houndsman.config.ts` +
|
|
9
9
|
`.houndsman/`.
|
|
10
10
|
|
|
11
|
+
## Dois modos de execução
|
|
12
|
+
|
|
13
|
+
| Modo | Quem usa | Requisitos | Quem despacha os briefs |
|
|
14
|
+
|---|---|---|---|
|
|
15
|
+
| **Skill `/houndsman hunt`** (padrão) | agente externo (Copilot/Claude/etc.) | nenhum | o próprio agente, via subagentes |
|
|
16
|
+
| **CLI `houndsman hunt`** | operador/CI, sem agente | `houndsman hunt install` uma vez | o houndsman, via maki headless |
|
|
17
|
+
|
|
18
|
+
Os dois modos produzem os mesmos artefatos, usam o mesmo staging e o mesmo ciclo
|
|
19
|
+
`prepare → findings → finalize`. Nada muda no fluxo abaixo para quem usa a skill.
|
|
20
|
+
|
|
21
|
+
> **Se você é o agente executando esta skill, NÃO rode `houndsman hunt`.** Ele instala/usa o
|
|
22
|
+
> maki e despacha outro harness. Siga os passos 0–3 normalmente; o comando CLI é para quem
|
|
23
|
+
> quer rodar tudo no terminal.
|
|
24
|
+
|
|
11
25
|
## Alvo da revisão
|
|
12
26
|
|
|
13
27
|
Traduza o pedido do usuário para flags **antes** de começar:
|
|
@@ -306,6 +320,28 @@ completo) · `--no-jev` · `--no-ast` · `--fail-on-blocking` (com `--finalize`)
|
|
|
306
320
|
|
|
307
321
|
---
|
|
308
322
|
|
|
323
|
+
## Harness local (opcional, fora da skill)
|
|
324
|
+
|
|
325
|
+
Para rodar a revisão inteira pelo terminal, sem agente externo, o mesmo `hunt` existe como
|
|
326
|
+
comando. A instalação é única e fica fora do projeto:
|
|
327
|
+
|
|
328
|
+
```bash
|
|
329
|
+
houndsman hunt install # baixa o maki fixado em <HOUNDSMAN_HOME>/tools/maki
|
|
330
|
+
houndsman hunt doctor # diagnóstico (não falha se o maki não existir)
|
|
331
|
+
houndsman hunt --pr 456 # prepare → um processo maki por brief → finalize
|
|
332
|
+
houndsman hunt --pr 456 --allow-partial # finaliza mesmo se algum watchdog falhar
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
- Reaproveita a credencial já configurada (`HOUNDSMAN_DEEP_SEEK_API_KEY` → `DEEPSEEK_API_KEY`)
|
|
336
|
+
e o modelo de `ai.model` (default `deepseek/deepseek-v4-pro`).
|
|
337
|
+
- O watchdog **não recebe escrita**: o maki devolve o JSON na resposta final e o houndsman
|
|
338
|
+
valida e grava `findings/<watchdog>.json`. `bash`, rede e edição ficam negados.
|
|
339
|
+
- Sem maki instalado, nada muda para a skill: `setup`, `doctor` e `/houndsman hunt` seguem
|
|
340
|
+
funcionando.
|
|
341
|
+
- O relatório do harness fica em `hunt-report.json` (sessões, custo e tokens por watchdog).
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
309
345
|
## Anti-padrões
|
|
310
346
|
|
|
311
347
|
- ❌ Enviar git diff completo para qualquer watchdog
|
|
@@ -238,6 +238,10 @@ especialista por domínio · **finding** = um achado da revisão · **Jev** = pr
|
|
|
238
238
|
Um argumento extra é o alvo da capability. Escolha a capability pelo que o usuário quer,
|
|
239
239
|
leia o documento correspondente em \`.houndsman/core/skill/<capability>.md\` e siga-o.
|
|
240
240
|
|
|
241
|
+
> **Harness local (opcional):** para rodar sem agente externo, o operador pode instalar o
|
|
242
|
+
> maki (\`houndsman hunt install\`) e usar \`houndsman hunt --pr 456\`. Isso não muda esta
|
|
243
|
+
> skill: se você é o agente, siga o fluxo normal e não rode \`houndsman hunt\`.
|
|
244
|
+
|
|
241
245
|
### Alvo da revisão (PR × branch)
|
|
242
246
|
|
|
243
247
|
Interprete o alvo em linguagem natural **antes** de rodar o CLI:
|
|
@@ -323,6 +327,10 @@ especialista por domínio · **finding** = um achado da revisão · **Jev** = pr
|
|
|
323
327
|
Um argumento extra é o alvo da capability. Escolha a capability pelo que o usuário quer,
|
|
324
328
|
leia o documento correspondente em \`.houndsman/core/skill/<capability>.md\` e siga-o.
|
|
325
329
|
|
|
330
|
+
> **Harness local (opcional):** para rodar sem agente externo, o operador pode instalar o
|
|
331
|
+
> maki (\`houndsman hunt install\`) e usar \`houndsman hunt --pr 456\`. Isso não muda esta
|
|
332
|
+
> skill: se você é o agente, siga o fluxo normal e não rode \`houndsman hunt\`.
|
|
333
|
+
|
|
326
334
|
### Alvo da revisão (PR × branch)
|
|
327
335
|
|
|
328
336
|
Interprete o alvo em linguagem natural **antes** de rodar o CLI:
|
|
@@ -516,6 +524,16 @@ export type HoundsmanAi = {
|
|
|
516
524
|
watchdogs?: Record<string, HoundsmanAiWatchdog>;
|
|
517
525
|
};
|
|
518
526
|
|
|
527
|
+
/** Harness local da capability \`hunt\` (\`houndsman hunt\`) — opcional. */
|
|
528
|
+
export type HoundsmanHunt = {
|
|
529
|
+
enabled?: boolean;
|
|
530
|
+
model?: string;
|
|
531
|
+
concurrency?: number;
|
|
532
|
+
timeoutMs?: number;
|
|
533
|
+
retries?: number;
|
|
534
|
+
binaryPath?: string;
|
|
535
|
+
};
|
|
536
|
+
|
|
519
537
|
/** Contrato de configuração do motor. */
|
|
520
538
|
export type HoundsmanConfig<TId extends string = string> = {
|
|
521
539
|
projectName: string;
|
|
@@ -529,6 +547,7 @@ export type HoundsmanConfig<TId extends string = string> = {
|
|
|
529
547
|
concurrency?: HoundsmanConcurrency;
|
|
530
548
|
ast?: HoundsmanAst;
|
|
531
549
|
ai?: HoundsmanAi;
|
|
550
|
+
hunt?: HoundsmanHunt;
|
|
532
551
|
};
|
|
533
552
|
|
|
534
553
|
/** Helper de autoria (identidade) — apenas tipagem. */
|
|
@@ -654,11 +673,11 @@ exports[`baseline — camada gerada setup gera os mesmos arquivos e conteúdos 3
|
|
|
654
673
|
},
|
|
655
674
|
{
|
|
656
675
|
"file": ".houndsman/core/skill/README.md",
|
|
657
|
-
"sha256": "
|
|
676
|
+
"sha256": "2ecf43f34a624011",
|
|
658
677
|
},
|
|
659
678
|
{
|
|
660
679
|
"file": ".houndsman/core/skill/SETUP.md",
|
|
661
|
-
"sha256": "
|
|
680
|
+
"sha256": "5720c48ede6c84ea",
|
|
662
681
|
},
|
|
663
682
|
{
|
|
664
683
|
"file": ".houndsman/core/skill/example-review.md",
|
|
@@ -670,7 +689,7 @@ exports[`baseline — camada gerada setup gera os mesmos arquivos e conteúdos 3
|
|
|
670
689
|
},
|
|
671
690
|
{
|
|
672
691
|
"file": ".houndsman/core/skill/hunt.md",
|
|
673
|
-
"sha256": "
|
|
692
|
+
"sha256": "d31198580d894427",
|
|
674
693
|
},
|
|
675
694
|
{
|
|
676
695
|
"file": ".houndsman/core/skill/train.md",
|
|
@@ -6,7 +6,11 @@ import { describe, expect, test } from "bun:test";
|
|
|
6
6
|
import type { LanguageModel } from "ai";
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
import { AiClient, AiUsageBudget, AiUsageRecorder } from "../core/ai";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
buildGenerateTextArgs,
|
|
11
|
+
formatSdkWarning,
|
|
12
|
+
formatSdkWarnings,
|
|
13
|
+
} from "../core/ai/ai-sdk-model-provider";
|
|
10
14
|
import { temperatureApplies } from "../core/ai/provider-capabilities";
|
|
11
15
|
import { defineHoundsmanConfig } from "../core/config";
|
|
12
16
|
import { DEFAULT_AI_MODEL } from "../core/defaults";
|
|
@@ -153,6 +157,39 @@ describe("temperature e providerOptions", () => {
|
|
|
153
157
|
});
|
|
154
158
|
});
|
|
155
159
|
|
|
160
|
+
describe("avisos do AI SDK", () => {
|
|
161
|
+
test("normaliza os avisos do provider em texto", () => {
|
|
162
|
+
expect(
|
|
163
|
+
formatSdkWarning({
|
|
164
|
+
type: "compatibility",
|
|
165
|
+
feature: "responseFormat JSON schema",
|
|
166
|
+
details: "JSON response schema is injected into the system message.",
|
|
167
|
+
}),
|
|
168
|
+
).toBe(
|
|
169
|
+
"compatibility: responseFormat JSON schema — JSON response schema is injected into the system message.",
|
|
170
|
+
);
|
|
171
|
+
expect(
|
|
172
|
+
formatSdkWarning({
|
|
173
|
+
type: "unsupported",
|
|
174
|
+
feature: "user message part type: image",
|
|
175
|
+
}),
|
|
176
|
+
).toBe("unsupported: user message part type: image");
|
|
177
|
+
expect(
|
|
178
|
+
formatSdkWarning({
|
|
179
|
+
type: "deprecated",
|
|
180
|
+
setting: "temperature",
|
|
181
|
+
message: "gone",
|
|
182
|
+
}),
|
|
183
|
+
).toBe("deprecated: temperature — gone");
|
|
184
|
+
expect(formatSdkWarning(null)).toBe("aviso");
|
|
185
|
+
expect(formatSdkWarnings(undefined)).toEqual([]);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("logger default do SDK fica silencioso após carregar o adapter", () => {
|
|
189
|
+
expect(typeof globalThis.AI_SDK_LOG_WARNINGS).toBe("function");
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
156
193
|
describe("AiUsageBudget / AiUsageRecorder", () => {
|
|
157
194
|
test("acumula uso e bloqueia no teto", () => {
|
|
158
195
|
const budget = new AiUsageBudget({ maxCalls: 2 });
|
package/src/tests/cli.test.ts
CHANGED
|
@@ -192,6 +192,7 @@ describe("cli smoke — imports dinâmicos", () => {
|
|
|
192
192
|
const result = runCli(project.cwd, ["doctor", "--no-ping"]);
|
|
193
193
|
expect(result.code).toBe(0);
|
|
194
194
|
expect(result.stdout).toContain("Validando");
|
|
195
|
+
expect(result.stdout).toContain("maki (hunt local)");
|
|
195
196
|
} finally {
|
|
196
197
|
project.cleanup();
|
|
197
198
|
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smoke test do comando `hunt` (harness local).
|
|
3
|
+
*
|
|
4
|
+
* Não executa o maki real: cobre help, doctor e os erros acionáveis antes do
|
|
5
|
+
* prepare. A skill `/houndsman hunt` continua coberta pelos testes da camada.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, test } from "bun:test";
|
|
9
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
|
|
13
|
+
const CLI = join(import.meta.dir, "..", "cli", "index.ts");
|
|
14
|
+
|
|
15
|
+
const CONFIG = (hunt = "") => `export default {
|
|
16
|
+
projectName: "Hunt Smoke",
|
|
17
|
+
storage: { provider: "sqlite", path: "./houndsman.db" },
|
|
18
|
+
watchdogs: {
|
|
19
|
+
alpha: {
|
|
20
|
+
id: "alpha",
|
|
21
|
+
name: "Alpha",
|
|
22
|
+
description: "Watchdog de smoke.",
|
|
23
|
+
scope: "src/**",
|
|
24
|
+
classify: [{ value: "^src/" }],
|
|
25
|
+
rules: [{ severity: "attention", text: "Evitar any." }],
|
|
26
|
+
contextFiles: [],
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
exclusions: [],
|
|
30
|
+
docMappings: [],
|
|
31
|
+
${hunt}
|
|
32
|
+
};
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
type Project = { cwd: string; home: string; cleanup: () => void };
|
|
36
|
+
|
|
37
|
+
function makeProject(hunt = ""): Project {
|
|
38
|
+
const cwd = mkdtempSync(join(tmpdir(), "houndsman-hunt-cli-"));
|
|
39
|
+
const home = mkdtempSync(join(tmpdir(), "houndsman-hunt-home-"));
|
|
40
|
+
writeFileSync(join(cwd, "houndsman.config.ts"), CONFIG(hunt));
|
|
41
|
+
return {
|
|
42
|
+
cwd,
|
|
43
|
+
home,
|
|
44
|
+
cleanup: () => {
|
|
45
|
+
rmSync(cwd, { recursive: true, force: true });
|
|
46
|
+
rmSync(home, { recursive: true, force: true });
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type CliResult = { code: number; stdout: string; stderr: string };
|
|
52
|
+
|
|
53
|
+
function runCli(
|
|
54
|
+
cwd: string,
|
|
55
|
+
args: string[],
|
|
56
|
+
env: Record<string, string> = {},
|
|
57
|
+
): CliResult {
|
|
58
|
+
const proc = Bun.spawnSync(["bun", CLI, ...args], {
|
|
59
|
+
cwd,
|
|
60
|
+
env: { ...process.env, NO_COLOR: "1", ...env },
|
|
61
|
+
stdout: "pipe",
|
|
62
|
+
stderr: "pipe",
|
|
63
|
+
});
|
|
64
|
+
return {
|
|
65
|
+
code: proc.exitCode ?? -1,
|
|
66
|
+
stdout: proc.stdout.toString(),
|
|
67
|
+
stderr: proc.stderr.toString(),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
describe("hunt — help e diagnóstico", () => {
|
|
72
|
+
test("--help distingue a skill do comando CLI", () => {
|
|
73
|
+
const result = runCli(process.cwd(), ["hunt", "--help"]);
|
|
74
|
+
expect(result.code).toBe(0);
|
|
75
|
+
expect(result.stdout).toContain("/houndsman hunt");
|
|
76
|
+
expect(result.stdout).toContain("houndsman hunt install");
|
|
77
|
+
expect(result.stdout).toContain("--allow-partial");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("doctor não falha sem maki e trata o harness como opcional", () => {
|
|
81
|
+
const project = makeProject();
|
|
82
|
+
try {
|
|
83
|
+
const result = runCli(project.cwd, ["hunt", "doctor"], {
|
|
84
|
+
HOUNDSMAN_HOME: project.home,
|
|
85
|
+
HOUNDSMAN_MAKI_PATH: join(project.cwd, "maki-inexistente"),
|
|
86
|
+
});
|
|
87
|
+
expect(result.code).toBe(0);
|
|
88
|
+
expect(result.stdout).toContain("Harness local");
|
|
89
|
+
expect(result.stdout).toContain("modelo:");
|
|
90
|
+
expect(result.stdout).toContain("harness:");
|
|
91
|
+
} finally {
|
|
92
|
+
project.cleanup();
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("doctor funciona sem projeto configurado (diagnóstico global)", () => {
|
|
97
|
+
const cwd = mkdtempSync(join(tmpdir(), "houndsman-hunt-nocfg-"));
|
|
98
|
+
const home = mkdtempSync(join(tmpdir(), "houndsman-hunt-home-"));
|
|
99
|
+
try {
|
|
100
|
+
const result = runCli(cwd, ["hunt", "doctor"], {
|
|
101
|
+
HOUNDSMAN_HOME: home,
|
|
102
|
+
HOUNDSMAN_MAKI_PATH: join(cwd, "maki-inexistente"),
|
|
103
|
+
});
|
|
104
|
+
expect(result.code).toBe(0);
|
|
105
|
+
expect(result.stdout).toContain("Harness local");
|
|
106
|
+
} finally {
|
|
107
|
+
rmSync(cwd, { recursive: true, force: true });
|
|
108
|
+
rmSync(home, { recursive: true, force: true });
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("hunt — erros antes do prepare", () => {
|
|
114
|
+
test("sem binário, orienta a instalar (e não toca na review)", () => {
|
|
115
|
+
const project = makeProject();
|
|
116
|
+
try {
|
|
117
|
+
const result = runCli(project.cwd, ["hunt", "--branch", "main"], {
|
|
118
|
+
HOUNDSMAN_HOME: project.home,
|
|
119
|
+
HOUNDSMAN_MAKI_PATH: join(project.cwd, "maki-inexistente"),
|
|
120
|
+
HOUNDSMAN_DEEP_SEEK_API_KEY: "sk-test",
|
|
121
|
+
});
|
|
122
|
+
expect(result.code).toBe(1);
|
|
123
|
+
expect(result.stderr).toContain("hunt install");
|
|
124
|
+
} finally {
|
|
125
|
+
project.cleanup();
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("sem credencial, aponta a env correta", () => {
|
|
130
|
+
const project = makeProject();
|
|
131
|
+
try {
|
|
132
|
+
const env = { ...process.env };
|
|
133
|
+
delete env.HOUNDSMAN_DEEP_SEEK_API_KEY;
|
|
134
|
+
const proc = Bun.spawnSync(["bun", CLI, "hunt", "--branch", "main"], {
|
|
135
|
+
cwd: project.cwd,
|
|
136
|
+
env: {
|
|
137
|
+
...env,
|
|
138
|
+
NO_COLOR: "1",
|
|
139
|
+
HOUNDSMAN_HOME: project.home,
|
|
140
|
+
HOUNDSMAN_MAKI_PATH: join(project.cwd, "maki-inexistente"),
|
|
141
|
+
},
|
|
142
|
+
stdout: "pipe",
|
|
143
|
+
stderr: "pipe",
|
|
144
|
+
});
|
|
145
|
+
expect(proc.exitCode).toBe(1);
|
|
146
|
+
expect(proc.stderr.toString()).toContain("HOUNDSMAN_DEEP_SEEK_API_KEY");
|
|
147
|
+
} finally {
|
|
148
|
+
project.cleanup();
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("respeita hunt.enabled: false", () => {
|
|
153
|
+
const project = makeProject("hunt: { enabled: false },");
|
|
154
|
+
try {
|
|
155
|
+
const result = runCli(project.cwd, ["hunt"], {
|
|
156
|
+
HOUNDSMAN_HOME: project.home,
|
|
157
|
+
HOUNDSMAN_DEEP_SEEK_API_KEY: "sk-test",
|
|
158
|
+
});
|
|
159
|
+
expect(result.code).toBe(1);
|
|
160
|
+
expect(result.stderr).toContain("desligado");
|
|
161
|
+
} finally {
|
|
162
|
+
project.cleanup();
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("rejeita --finalize (o hunt já finaliza)", () => {
|
|
167
|
+
const project = makeProject();
|
|
168
|
+
try {
|
|
169
|
+
const result = runCli(project.cwd, ["hunt", "--finalize"], {
|
|
170
|
+
HOUNDSMAN_HOME: project.home,
|
|
171
|
+
});
|
|
172
|
+
expect(result.code).toBe(1);
|
|
173
|
+
expect(result.stderr).toContain("já finaliza");
|
|
174
|
+
} finally {
|
|
175
|
+
project.cleanup();
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("subcomando desconhecido mostra o help", () => {
|
|
180
|
+
const result = runCli(process.cwd(), ["hunt", "foo"]);
|
|
181
|
+
expect(result.code).toBe(1);
|
|
182
|
+
expect(result.stderr).toContain("desconhecido");
|
|
183
|
+
expect(result.stdout).toContain("houndsman hunt install");
|
|
184
|
+
});
|
|
185
|
+
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Instalação local do maki (sem rede).
|
|
3
|
+
*
|
|
4
|
+
* Valida a resolução de asset/versão/checksum e a instalação a partir de um
|
|
5
|
+
* arquivo local (`--from`), além do ponteiro gerenciado e do uninstall.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
9
|
+
import {
|
|
10
|
+
existsSync,
|
|
11
|
+
mkdirSync,
|
|
12
|
+
mkdtempSync,
|
|
13
|
+
readFileSync,
|
|
14
|
+
rmSync,
|
|
15
|
+
writeFileSync,
|
|
16
|
+
} from "node:fs";
|
|
17
|
+
import { tmpdir } from "node:os";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import {
|
|
20
|
+
installMaki,
|
|
21
|
+
makiCurrentFile,
|
|
22
|
+
makiInstallDir,
|
|
23
|
+
makiSourceLabel,
|
|
24
|
+
readMakiInstall,
|
|
25
|
+
resolveMakiBinary,
|
|
26
|
+
uninstallMaki,
|
|
27
|
+
} from "../core/hunt/maki-install";
|
|
28
|
+
import {
|
|
29
|
+
effectiveMakiVersion,
|
|
30
|
+
makiAssetFor,
|
|
31
|
+
makiBinaryName,
|
|
32
|
+
makiChecksumsUrl,
|
|
33
|
+
makiDownloadUrl,
|
|
34
|
+
parseChecksums,
|
|
35
|
+
} from "../core/hunt/maki-release";
|
|
36
|
+
|
|
37
|
+
const original = {
|
|
38
|
+
home: process.env.HOUNDSMAN_HOME,
|
|
39
|
+
makiPath: process.env.HOUNDSMAN_MAKI_PATH,
|
|
40
|
+
makiVersion: process.env.HOUNDSMAN_MAKI_VERSION,
|
|
41
|
+
};
|
|
42
|
+
const dirs: string[] = [];
|
|
43
|
+
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
restoreEnv("HOUNDSMAN_HOME", original.home);
|
|
46
|
+
restoreEnv("HOUNDSMAN_MAKI_PATH", original.makiPath);
|
|
47
|
+
restoreEnv("HOUNDSMAN_MAKI_VERSION", original.makiVersion);
|
|
48
|
+
for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
function restoreEnv(name: string, value: string | undefined): void {
|
|
52
|
+
if (value === undefined) delete process.env[name];
|
|
53
|
+
else process.env[name] = value;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function tempDir(prefix: string): string {
|
|
57
|
+
const dir = mkdtempSync(join(tmpdir(), prefix));
|
|
58
|
+
dirs.push(dir);
|
|
59
|
+
return dir;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
describe("maki-release", () => {
|
|
63
|
+
test("resolve asset por plataforma/arquitetura", () => {
|
|
64
|
+
expect(makiAssetFor("win32", "x64", "0.6.5")?.name).toBe(
|
|
65
|
+
"maki-v0.6.5-x86_64-pc-windows-msvc.zip",
|
|
66
|
+
);
|
|
67
|
+
expect(makiAssetFor("darwin", "arm64", "v0.6.5")?.name).toBe(
|
|
68
|
+
"maki-v0.6.5-aarch64-apple-darwin.tar.gz",
|
|
69
|
+
);
|
|
70
|
+
expect(makiAssetFor("linux", "x64", "0.6.5")?.name).toBe(
|
|
71
|
+
"maki-v0.6.5-x86_64-unknown-linux-musl.tar.gz",
|
|
72
|
+
);
|
|
73
|
+
expect(makiAssetFor("solaris", "x64", "0.6.5")).toBeNull();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("versão normaliza v e aceita override por env", () => {
|
|
77
|
+
expect(effectiveMakiVersion("v1.2.3")).toBe("1.2.3");
|
|
78
|
+
process.env.HOUNDSMAN_MAKI_VERSION = "v2.0.0";
|
|
79
|
+
expect(effectiveMakiVersion()).toBe("2.0.0");
|
|
80
|
+
expect(() => effectiveMakiVersion("../../x")).toThrow("Versão do maki inválida");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("URLs e parse de sha256sums", () => {
|
|
84
|
+
expect(makiDownloadUrl("0.6.5", "maki.tar.gz")).toContain(
|
|
85
|
+
"/releases/download/v0.6.5/maki.tar.gz",
|
|
86
|
+
);
|
|
87
|
+
expect(makiChecksumsUrl("0.6.5").endsWith("/v0.6.5/sha256sums.txt")).toBe(true);
|
|
88
|
+
const checksums = parseChecksums(
|
|
89
|
+
[
|
|
90
|
+
`${"a".repeat(64)} maki-a.tar.gz`,
|
|
91
|
+
`${"b".repeat(64)} *maki-b.zip`,
|
|
92
|
+
"linha inválida",
|
|
93
|
+
].join("\n"),
|
|
94
|
+
);
|
|
95
|
+
expect(checksums.get("maki-a.tar.gz")).toBe("a".repeat(64));
|
|
96
|
+
expect(checksums.get("maki-b.zip")).toBe("b".repeat(64));
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("resolução do binário", () => {
|
|
101
|
+
test("HOUNDSMAN_MAKI_PATH tem precedência", () => {
|
|
102
|
+
const root = tempDir("houndsman-maki-resolve-");
|
|
103
|
+
const binary = join(root, "maki-custom");
|
|
104
|
+
writeFileSync(binary, "fake");
|
|
105
|
+
process.env.HOUNDSMAN_MAKI_PATH = binary;
|
|
106
|
+
const resolved = resolveMakiBinary();
|
|
107
|
+
expect(resolved?.path).toBe(binary);
|
|
108
|
+
expect(resolved?.source).toBe("env");
|
|
109
|
+
expect(makiSourceLabel("env")).toBe("HOUNDSMAN_MAKI_PATH");
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("installMaki (arquivo local)", () => {
|
|
114
|
+
const hasTar = Boolean(Bun.which("tar"));
|
|
115
|
+
|
|
116
|
+
test.skipIf(!hasTar)("extrai, grava o ponteiro e remove", async () => {
|
|
117
|
+
const home = tempDir("houndsman-maki-home-");
|
|
118
|
+
process.env.HOUNDSMAN_HOME = home;
|
|
119
|
+
|
|
120
|
+
const src = tempDir("houndsman-maki-src-");
|
|
121
|
+
const name = makiBinaryName();
|
|
122
|
+
mkdirSync(join(src, "nested"), { recursive: true });
|
|
123
|
+
writeFileSync(join(src, "nested", name), "#!/bin/sh\necho fake\n");
|
|
124
|
+
const archive = join(tempDir("houndsman-maki-archive-"), "maki.tar.gz");
|
|
125
|
+
const tar = Bun.spawnSync(["tar", "-czf", archive, "-C", src, "."], {
|
|
126
|
+
stdout: "pipe",
|
|
127
|
+
stderr: "pipe",
|
|
128
|
+
});
|
|
129
|
+
expect(tar.exitCode).toBe(0);
|
|
130
|
+
|
|
131
|
+
const first = await installMaki({ version: "9.9.9", from: archive });
|
|
132
|
+
expect(first.skipped).toBe(false);
|
|
133
|
+
expect(existsSync(first.path)).toBe(true);
|
|
134
|
+
expect(first.path).toBe(join(makiInstallDir("9.9.9"), name));
|
|
135
|
+
expect(readFileSync(first.path, "utf8")).toContain("echo fake");
|
|
136
|
+
|
|
137
|
+
const manifest = readMakiInstall();
|
|
138
|
+
expect(manifest?.version).toBe("9.9.9");
|
|
139
|
+
expect(manifest?.path).toBe(first.path);
|
|
140
|
+
expect(existsSync(makiCurrentFile())).toBe(true);
|
|
141
|
+
|
|
142
|
+
const resolved = resolveMakiBinary();
|
|
143
|
+
expect(resolved?.source).toBe("managed");
|
|
144
|
+
expect(resolved?.version).toBe("9.9.9");
|
|
145
|
+
|
|
146
|
+
const second = await installMaki({ version: "9.9.9", from: archive });
|
|
147
|
+
expect(second.skipped).toBe(true);
|
|
148
|
+
|
|
149
|
+
const removed = uninstallMaki();
|
|
150
|
+
expect(removed).toBe(join(home, "tools", "maki"));
|
|
151
|
+
expect(existsSync(join(home, "tools", "maki"))).toBe(false);
|
|
152
|
+
});
|
|
153
|
+
});
|