@payconductor/houndsman 0.5.0 → 0.7.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 +24 -4
- package/assets/grammars/tree-sitter-javascript.wasm +0 -0
- package/assets/grammars/tree-sitter-tsx.wasm +0 -0
- package/assets/grammars/tree-sitter-typescript.wasm +0 -0
- package/assets/grammars/web-tree-sitter.wasm +0 -0
- package/package.json +11 -3
- package/src/cli/commands/command.ts +5 -0
- package/src/cli/commands/doctor-command.ts +116 -24
- package/src/cli/commands/hunt-command.ts +25 -0
- package/src/cli/commands/server-command.ts +7 -1
- package/src/cli/help.ts +18 -2
- package/src/cli/index.ts +10 -6
- package/src/core/ai/ai-client.ts +221 -0
- package/src/core/ai/ai-sdk-model-provider.ts +101 -0
- package/src/core/ai/errors.ts +36 -0
- package/src/core/ai/features/brief-enricher.ts +62 -0
- package/src/core/ai/features/classifier.ts +50 -0
- package/src/core/ai/features/context-summarizer.ts +31 -0
- package/src/core/ai/features/pre-analyzer.ts +51 -0
- package/src/core/ai/features/verifier.ts +45 -0
- package/src/core/ai/index.ts +56 -0
- package/src/core/ai/model-factory.ts +137 -0
- package/src/core/ai/prompts/base.ts +36 -0
- package/src/core/ai/prompts/brief.ts +87 -0
- package/src/core/ai/prompts/classification.ts +36 -0
- package/src/core/ai/prompts/context.ts +29 -0
- package/src/core/ai/prompts/pre-analysis.ts +49 -0
- package/src/core/ai/prompts/verify.ts +40 -0
- package/src/core/ai/schemas.ts +110 -0
- package/src/core/ai/types.ts +76 -0
- package/src/core/ai/usage-budget.ts +75 -0
- package/src/core/ai/usage-recorder.ts +79 -0
- package/src/core/assets.ts +16 -0
- package/src/core/ast/diff.ts +220 -0
- package/src/core/ast/impact.ts +127 -0
- package/src/core/ast/parser.ts +167 -0
- package/src/core/ast/service.ts +233 -0
- package/src/core/ast/symbols.ts +294 -0
- package/src/core/ast/types.ts +125 -0
- package/src/core/brief.ts +138 -0
- package/src/core/commands/findings.ts +65 -2
- package/src/core/commands/review.ts +2 -0
- package/src/core/commands/scaffold.ts +159 -10
- package/src/core/commands/update.ts +5 -3
- package/src/core/config.ts +85 -0
- package/src/core/defaults.ts +27 -0
- package/src/core/domain/watchdog.ts +43 -5
- package/src/core/engine/houndsman-engine.ts +16 -1
- package/src/core/engine/review-scope.ts +52 -6
- package/src/core/engine/review-session.ts +27 -2
- package/src/core/env.ts +101 -0
- package/src/core/file-classifier.ts +25 -4
- package/src/core/git-runner.ts +25 -13
- package/src/core/github-review.ts +10 -7
- package/src/core/jev/jev-client.ts +8 -7
- package/src/core/jev/jev-verify.ts +2 -1
- package/src/core/logging/index.ts +17 -0
- package/src/core/logging/logger.ts +187 -0
- package/src/core/orchestrator.ts +357 -9
- package/src/core/project-config.ts +123 -1
- package/src/core/registry-check.ts +148 -6
- package/src/core/renderers/manifest-writer.ts +33 -0
- package/src/core/rule-text.ts +97 -0
- package/src/core/services/file-classifier.ts +10 -3
- package/src/core/services/gh-client.ts +38 -22
- package/src/core/services/verifier.ts +14 -3
- package/src/core/storage/paths.ts +6 -5
- package/src/core/storage/types.ts +6 -0
- package/src/core/storage/work-area.ts +12 -0
- package/src/core/types.ts +37 -1
- package/src/core/verify.ts +17 -5
- package/src/harness/README.md +15 -0
- package/src/harness/agent/agent-context.ts +33 -0
- package/src/harness/agent/agent-errors.ts +30 -0
- package/src/harness/agent/agent-factory.ts +45 -0
- package/src/harness/agent/agent-reporter.ts +34 -0
- package/src/harness/agent/agent-result.ts +16 -0
- package/src/harness/agent/agent-runner.ts +32 -0
- package/src/harness/agent/agent-scheduler.ts +37 -0
- package/src/harness/agent/review-agent.ts +391 -0
- package/src/harness/config/harness-config.ts +84 -0
- package/src/harness/hunt/hunt-harness.ts +83 -0
- package/src/harness/hunt/hunt-options.ts +75 -0
- package/src/harness/hunt/hunt-result.ts +17 -0
- package/src/harness/hunt/hunt-session.ts +372 -0
- package/src/harness/hunt/review-planner.ts +98 -0
- package/src/harness/index.ts +36 -0
- package/src/harness/investigation/agent-state.ts +134 -0
- package/src/harness/investigation/evidence-verifier.ts +129 -0
- package/src/harness/investigation/evidence.ts +110 -0
- package/src/harness/investigation/finding-critic.ts +96 -0
- package/src/harness/investigation/investigation-planner.ts +118 -0
- package/src/harness/investigation/investigation-task.ts +10 -0
- package/src/harness/investigation/review-journal.ts +87 -0
- package/src/harness/investigation/rule-analyzer.ts +178 -0
- package/src/harness/investigation/rule-signals.ts +110 -0
- package/src/harness/prompts/agent-schemas.ts +57 -0
- package/src/harness/prompts/reviewer-prompt-builder.ts +101 -0
- package/src/harness/prompts/states/conclude-prompt.ts +16 -0
- package/src/harness/prompts/states/investigate-prompt.ts +37 -0
- package/src/harness/prompts/states/orient-prompt.ts +15 -0
- package/src/harness/prompts/states/verify-prompt.ts +22 -0
- package/src/harness/tools/agent-tool.ts +102 -0
- package/src/harness/tools/builtin/changed-symbols-tool.ts +48 -0
- package/src/harness/tools/builtin/doc-context-tool.ts +74 -0
- package/src/harness/tools/builtin/file-outline-tool.ts +66 -0
- package/src/harness/tools/builtin/find-references-tool.ts +66 -0
- package/src/harness/tools/builtin/find-symbol-tool.ts +91 -0
- package/src/harness/tools/builtin/helpers.ts +115 -0
- package/src/harness/tools/builtin/impact-tool.ts +48 -0
- package/src/harness/tools/builtin/imports-exports-tool.ts +53 -0
- package/src/harness/tools/builtin/index.ts +59 -0
- package/src/harness/tools/builtin/list-files-tool.ts +85 -0
- package/src/harness/tools/builtin/pr-context-tool.ts +41 -0
- package/src/harness/tools/builtin/read-file-range-tool.ts +73 -0
- package/src/harness/tools/builtin/rule-signals-tool.ts +36 -0
- package/src/harness/tools/builtin/search-code-tool.ts +65 -0
- package/src/harness/tools/builtin/symbol-context-tool.ts +106 -0
- package/src/harness/tools/builtin/tests-for-symbol-tool.ts +61 -0
- package/src/harness/tools/path-guard.ts +90 -0
- package/src/harness/tools/result-limiter.ts +49 -0
- package/src/harness/tools/secret-redactor.ts +54 -0
- package/src/harness/tools/tool-cache.ts +50 -0
- package/src/harness/tools/tool-call-budget.ts +60 -0
- package/src/harness/tools/tool-context.ts +5 -0
- package/src/harness/tools/tool-middleware.ts +105 -0
- package/src/harness/tools/tool-policy.ts +51 -0
- package/src/harness/tools/tool-registry.ts +112 -0
- package/src/harness/tools/tool-result.ts +15 -0
- package/src/harness/tools/toolbox.ts +36 -0
- package/src/harness/validation/finding-validator.ts +119 -0
- package/src/index.ts +46 -0
- package/src/server/client/App.tsx +38 -12
- package/src/server/client/CommentsPanel.tsx +255 -0
- package/src/server/client/FindingCard.tsx +12 -4
- package/src/server/client/FindingsList.tsx +4 -1
- package/src/server/client/Sidebar.tsx +397 -0
- package/src/server/client/SubmitModal.tsx +61 -5
- package/src/server/client/icons.tsx +19 -0
- package/src/server/client/index.tsx +1 -1
- package/src/server/client/utils/constants.ts +29 -5
- package/src/server/client/utils/format.ts +47 -2
- package/src/server/client/utils/types.ts +88 -0
- package/src/server/editor.ts +129 -0
- package/src/server/index.ts +115 -5
- package/src/server/pr-comments.ts +256 -0
- package/src/server/pr-inbox.ts +202 -0
- package/src/server/run-repository.ts +33 -1
- package/src/skill/OUTPUT-FORMAT.md +12 -0
- package/src/skill/SETUP.md +6 -2
- package/src/skill/hunt.md +14 -2
- package/src/skill/verify.md +1 -1
- package/src/tests/__snapshots__/baseline.test.ts.snap +96 -14
- package/src/tests/ai-core.test.ts +120 -0
- package/src/tests/ai-features.test.ts +183 -0
- package/src/tests/ast.test.ts +335 -0
- package/src/tests/cli.test.ts +33 -0
- package/src/tests/env.test.ts +69 -0
- package/src/tests/fallback.test.ts +172 -0
- package/src/tests/fixtures/fake-agent-tools.ts +69 -0
- package/src/tests/fixtures/fake-investigation.ts +64 -0
- package/src/tests/fixtures/fake-model-provider.ts +48 -0
- package/src/tests/github-review.test.ts +9 -0
- package/src/tests/harness-agent.test.ts +122 -0
- package/src/tests/harness-investigation.test.ts +206 -0
- package/src/tests/harness-tools.test.ts +148 -0
- package/src/tests/hunt-command.test.ts +212 -0
- package/src/tests/logger.test.ts +83 -0
- package/src/tests/review-scope.test.ts +11 -11
- package/src/tests/rule-analyzer.test.ts +61 -0
- package/src/tests/server-editor.test.ts +100 -0
- package/src/tests/server.test.ts +295 -2
- package/src/tests/tool-contracts.test.ts +82 -0
- package/src/tests/verify.test.ts +1 -1
- package/src/server/client/RunList.tsx +0 -75
package/README.md
CHANGED
|
@@ -27,7 +27,12 @@ diferente das PRs. O motor só coordena a caçada — as regras são problema se
|
|
|
27
27
|
- **Bun** — runtime obrigatório.
|
|
28
28
|
- **git** — o diff usa `git diff <base>...<head>`.
|
|
29
29
|
- **gh** (Opcional) — Obtém contexto da PR e dispara comentários para o GitHub
|
|
30
|
-
- **`
|
|
30
|
+
- **`HOUNDSMAN_TYPE_SAFE_API_KEY`** (opcional) — pré-análise Jev.
|
|
31
|
+
- **`HOUNDSMAN_DEEP_SEEK_API_KEY`** (opcional) — IA própria do `hunt` nativo e das features de IA.
|
|
32
|
+
- **`HOUNDSMAN_OPENAI_API_KEY`** / **`HOUNDSMAN_ANTHROPIC_API_KEY`** (opcional) — apenas se
|
|
33
|
+
`ai.provider` for `openai`/`anthropic`.
|
|
34
|
+
|
|
35
|
+
> Coloque as chaves em um `.env` na raiz do projeto; o Bun carrega automaticamente.
|
|
31
36
|
|
|
32
37
|
### 2. Instale o CLI (global)
|
|
33
38
|
|
|
@@ -48,13 +53,14 @@ Cria a camada `.houndsman/`, que é **autocontida** (não instala nada no seu pr
|
|
|
48
53
|
| Caminho | Dono | O que é |
|
|
49
54
|
|---|---|---|
|
|
50
55
|
| `houndsman.config.ts` | você | Configuração do projeto (um objeto de dados) |
|
|
51
|
-
| `.houndsman/watchdogs/` | você | Suas regras — começa com
|
|
56
|
+
| `.houndsman/watchdogs/` | você | Suas regras — começa com um watchdog `default` (fallback) e um `example` |
|
|
52
57
|
| `.houndsman/exclusions.ts` | você | Arquivos que nunca entram na revisão |
|
|
53
58
|
| `.houndsman/doc-mappings.ts` | você | path → documentação de contexto |
|
|
54
59
|
| `.houndsman/core/**` | auto | Tipos + docs do skill (sincronizados por `houndsman update`) |
|
|
55
60
|
|
|
56
|
-
> O `setup` já cria a camada inicial com um watchdog
|
|
57
|
-
> regras reais do seu projeto (ou use
|
|
61
|
+
> O `setup` já cria a camada inicial com um watchdog `default` (fallback, cobre arquivos sem
|
|
62
|
+
> domínio) e um `example` — troque o exemplo pelas regras reais do seu projeto (ou use
|
|
63
|
+
> `/houndsman train`).
|
|
58
64
|
|
|
59
65
|
### 4. Escreva suas regras
|
|
60
66
|
|
|
@@ -78,6 +84,20 @@ O skill orquestra tudo e persiste o relatório no storage global (SQLite em
|
|
|
78
84
|
`<HOUNDSMAN_HOME>/houndsman.db`); a UI lê os artefatos direto do storage. Não precisa rodar
|
|
79
85
|
nenhum script à mão.
|
|
80
86
|
|
|
87
|
+
### 7. Revisão nativa (opcional) — `houndsman hunt`
|
|
88
|
+
|
|
89
|
+
Além do skill, o houndsman tem um **harness próprio**: o `hunt` roda o agente de revisão
|
|
90
|
+
dentro do CLI, com tools read-only, investigação por evidência e a mesma saída `review.md`.
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
bun run houndsman hunt --dry-run # inspeciona watchdogs/arquivos, sem IA
|
|
94
|
+
HOUNDSMAN_DEEP_SEEK_API_KEY=... bun run houndsman hunt
|
|
95
|
+
bun run houndsman hunt --resume # preserva findings válidos do staging
|
|
96
|
+
bun run houndsman hunt --fail-on-blocking
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Detalhes (catálogo de tools, máquina de estados, custos e segurança): [`docs/HARNESS.md`](docs/HARNESS.md).
|
|
100
|
+
|
|
81
101
|
---
|
|
82
102
|
|
|
83
103
|
## O fluxo de trabalho
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@payconductor/houndsman",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Orquestrador de revisão de Pull Requests: classifica o diff, despacha watchdogs especializados por domínio e consolida o relatório.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -51,14 +51,22 @@
|
|
|
51
51
|
"ci:check:fix": "biome check --fix",
|
|
52
52
|
"ci:fix": "bun run ci:check:fix && bun run ci:format:fix",
|
|
53
53
|
"ci:format": "biome format",
|
|
54
|
-
"ci:format:fix": "biome format --fix"
|
|
54
|
+
"ci:format:fix": "biome format --fix",
|
|
55
|
+
"grammars:sync": "bun run ./scripts/sync-grammars.ts"
|
|
55
56
|
},
|
|
56
57
|
"dependencies": {
|
|
58
|
+
"@ai-sdk/deepseek": "3.0.67",
|
|
59
|
+
"ai": "7.0.143",
|
|
57
60
|
"axios": "^1.12.2",
|
|
58
|
-
"
|
|
61
|
+
"chalk": "^6.0.1",
|
|
62
|
+
"ora": "^9.4.1",
|
|
63
|
+
"preact": "^11.0.1",
|
|
64
|
+
"web-tree-sitter": "0.27.1",
|
|
65
|
+
"zod": "4.6.7"
|
|
59
66
|
},
|
|
60
67
|
"devDependencies": {
|
|
61
68
|
"@biomejs/biome": "2.2.4",
|
|
69
|
+
"@vscode/tree-sitter-wasm": "0.3.1",
|
|
62
70
|
"bun-types": "^1.3.14",
|
|
63
71
|
"typescript": "^7.0.2"
|
|
64
72
|
}
|
|
@@ -6,13 +6,18 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { HoundsmanEngine } from "../../core/engine/houndsman-engine";
|
|
9
|
+
import type { LoggerLike } from "../../core/logging";
|
|
9
10
|
|
|
10
11
|
/** Contexto de execução de um comando. */
|
|
11
12
|
export type CliContext = {
|
|
12
13
|
cwd: string;
|
|
13
14
|
/** Carrega a config do projeto e devolve o engine (lazy). */
|
|
14
15
|
engine: () => Promise<HoundsmanEngine>;
|
|
16
|
+
/** Logger único do CLI (cores/spinner/progresso). */
|
|
17
|
+
logger: LoggerLike;
|
|
18
|
+
/** @deprecated use `logger.info`. */
|
|
15
19
|
out: (message: string) => void;
|
|
20
|
+
/** @deprecated use `logger.error`. */
|
|
16
21
|
err: (message: string) => void;
|
|
17
22
|
};
|
|
18
23
|
|
|
@@ -1,60 +1,152 @@
|
|
|
1
|
-
/** Comando `doctor` (alias `check`) — valida config, ambiente e
|
|
1
|
+
/** Comando `doctor` (alias `check`) — valida config, ambiente e credenciais. */
|
|
2
2
|
|
|
3
|
+
import type { AiClient } from "../../core/ai/ai-client";
|
|
4
|
+
import { ArgParser } from "../../core/arg-parser";
|
|
5
|
+
import type { AiProviderKind } from "../../core/config";
|
|
6
|
+
import { ENV_NAMES, env } from "../../core/env";
|
|
3
7
|
import { checkRegistry } from "../../core/registry-check";
|
|
4
8
|
import { storageLocation } from "../../core/storage/paths";
|
|
5
9
|
import type { CliContext, Command } from "./command";
|
|
6
10
|
|
|
11
|
+
/** Chave de ambiente esperada por provider. */
|
|
12
|
+
function keyFor(provider: AiProviderKind): string | undefined {
|
|
13
|
+
switch (provider) {
|
|
14
|
+
case "deepseek":
|
|
15
|
+
return env.deepSeekApiKey;
|
|
16
|
+
case "openai":
|
|
17
|
+
return env.openAiApiKey;
|
|
18
|
+
case "anthropic":
|
|
19
|
+
return env.anthropicApiKey;
|
|
20
|
+
default:
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Nome da variável de ambiente por provider. */
|
|
26
|
+
function envNameFor(provider: AiProviderKind): string {
|
|
27
|
+
switch (provider) {
|
|
28
|
+
case "openai":
|
|
29
|
+
return ENV_NAMES.openAiApiKey;
|
|
30
|
+
case "anthropic":
|
|
31
|
+
return ENV_NAMES.anthropicApiKey;
|
|
32
|
+
default:
|
|
33
|
+
return ENV_NAMES.deepSeekApiKey;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
7
37
|
export class DoctorCommand implements Command {
|
|
8
38
|
readonly name = "doctor";
|
|
9
39
|
|
|
10
|
-
async run(
|
|
40
|
+
async run(args: string[], context: CliContext): Promise<number> {
|
|
41
|
+
const parser = ArgParser.from(args);
|
|
42
|
+
const noPing = parser.has("--no-ping");
|
|
11
43
|
const engine = await context.engine();
|
|
12
|
-
|
|
44
|
+
const { logger } = context;
|
|
45
|
+
|
|
46
|
+
logger.step("Validando configuração do projeto...");
|
|
13
47
|
const { problems, warnings, jevQuestions } = checkRegistry(engine.config);
|
|
14
48
|
|
|
15
49
|
if (problems.length > 0) {
|
|
16
|
-
|
|
17
|
-
for (const problem of problems)
|
|
50
|
+
logger.error("Problemas na configuração de revisão:");
|
|
51
|
+
for (const problem of problems) logger.error(` - ${problem}`);
|
|
18
52
|
return 1;
|
|
19
53
|
}
|
|
20
|
-
|
|
21
|
-
|
|
54
|
+
logger.success(
|
|
55
|
+
`${engine.config.watchdogIds.length} watchdogs, ${jevQuestions} pergunta(s) Jev.`,
|
|
22
56
|
);
|
|
23
57
|
|
|
24
58
|
if (warnings.length > 0) {
|
|
25
|
-
|
|
26
|
-
for (const warning of warnings)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
59
|
+
logger.warn(`${warnings.length} aviso(s) (não bloqueantes):`);
|
|
60
|
+
for (const warning of warnings) logger.bullet(warning);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
logger.step("Ambiente:");
|
|
64
|
+
logger.bullet(`${Bun.which("git") ? "✅" : "❌"} git`);
|
|
65
|
+
logger.bullet(`${Bun.which("gh") ? "✅" : "⚠️ "} gh (opcional — contexto da PR)`);
|
|
66
|
+
const ast = await engine.ast.check();
|
|
67
|
+
logger.bullet(`${ast.ok ? "✅" : "⚠️ "} AST: ${ast.detail}`);
|
|
68
|
+
|
|
69
|
+
const provider = engine.ai.projectConfig.aiProvider;
|
|
70
|
+
const key = keyFor(provider);
|
|
71
|
+
logger.bullet(
|
|
72
|
+
`${key ? "✅" : "⚠️ "} ${envNameFor(provider)} (provider ${provider})`,
|
|
73
|
+
);
|
|
74
|
+
logger.bullet(
|
|
75
|
+
`IA: provider=${provider}, model=${engine.ai.modelId}, ` +
|
|
76
|
+
`disponível=${engine.ai.available ? "sim" : "não"}`,
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
await this.reportAiCredential(context, engine.ai, provider, key, noPing);
|
|
80
|
+
await this.reportJevCredential(context, noPing);
|
|
81
|
+
|
|
82
|
+
logger.step("Persistência:");
|
|
83
|
+
logger.bullet(`provider: ${engine.storage.kind}`);
|
|
84
|
+
logger.bullet(storageLocation(engine.config));
|
|
85
|
+
logger.bullet(`repo: ${engine.repo.key}`);
|
|
37
86
|
return 0;
|
|
38
87
|
}
|
|
39
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Valida a credencial do provider de IA com um ping mínimo. Fail-open:
|
|
91
|
+
* problemas de credencial são diagnóstico e não alteram o exit code.
|
|
92
|
+
*/
|
|
93
|
+
private async reportAiCredential(
|
|
94
|
+
context: CliContext,
|
|
95
|
+
ai: AiClient,
|
|
96
|
+
provider: AiProviderKind,
|
|
97
|
+
key: string | undefined,
|
|
98
|
+
noPing: boolean,
|
|
99
|
+
): Promise<void> {
|
|
100
|
+
if (!key) {
|
|
101
|
+
context.logger.warn(
|
|
102
|
+
`IA: ${provider} sem credencial — features de IA ficam fail-open.`,
|
|
103
|
+
);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (noPing) {
|
|
107
|
+
context.logger.info("IA: validação pulada (--no-ping).");
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
context.logger.step("Validando credencial de IA...");
|
|
111
|
+
const check = await ai.validateCredential();
|
|
112
|
+
if (check.ok) {
|
|
113
|
+
context.logger.success(
|
|
114
|
+
`IA: credencial válida (modelo ${check.model ?? provider}).`,
|
|
115
|
+
);
|
|
116
|
+
} else if (check.kind === "invalid") {
|
|
117
|
+
context.logger.error(`IA: credencial inválida — ${check.detail}`);
|
|
118
|
+
} else {
|
|
119
|
+
context.logger.warn(`IA: não foi possível validar — ${check.detail}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
40
123
|
/**
|
|
41
124
|
* Reporta o estado da credencial Jev: presença e validade (chamada mínima).
|
|
42
125
|
* Fail-open — nunca altera o exit code (`doctor` é diagnóstico).
|
|
43
126
|
*/
|
|
44
|
-
private async reportJevCredential(
|
|
127
|
+
private async reportJevCredential(
|
|
128
|
+
context: CliContext,
|
|
129
|
+
noPing: boolean,
|
|
130
|
+
): Promise<void> {
|
|
45
131
|
const { JevClient } = await import("../../core/jev");
|
|
46
132
|
const client = new JevClient();
|
|
47
133
|
if (!client.isConfigured()) {
|
|
48
|
-
context.
|
|
134
|
+
context.logger.warn(
|
|
135
|
+
`${ENV_NAMES.typeSafeApiKey} (opcional — pré-análise Jev)`,
|
|
136
|
+
);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (noPing) {
|
|
140
|
+
context.logger.info("Jev: validação pulada (--no-ping).");
|
|
49
141
|
return;
|
|
50
142
|
}
|
|
51
143
|
const check = await client.validateCredential();
|
|
52
144
|
if (check.ok) {
|
|
53
|
-
context.
|
|
145
|
+
context.logger.success(`Jev: credencial válida (modelo ${check.model}).`);
|
|
54
146
|
} else if (check.kind === "invalid") {
|
|
55
|
-
context.
|
|
147
|
+
context.logger.error(`Jev: credencial inválida — ${check.detail}`);
|
|
56
148
|
} else {
|
|
57
|
-
context.
|
|
149
|
+
context.logger.warn(`Jev: não foi possível validar — ${check.detail}`);
|
|
58
150
|
}
|
|
59
151
|
}
|
|
60
152
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Comando `hunt` — revisão nativa com IA (harness próprio). */
|
|
2
|
+
|
|
3
|
+
import { HuntHarness } from "../../harness";
|
|
4
|
+
import { parseHuntArgs } from "../../harness/hunt/hunt-options";
|
|
5
|
+
import type { CliContext, Command } from "./command";
|
|
6
|
+
|
|
7
|
+
/** Executa o harness nativo. */
|
|
8
|
+
export class HuntCommand implements Command {
|
|
9
|
+
readonly name = "hunt";
|
|
10
|
+
|
|
11
|
+
async run(args: string[], context: CliContext): Promise<number> {
|
|
12
|
+
const options = parseHuntArgs(args);
|
|
13
|
+
const engine = await context.engine();
|
|
14
|
+
try {
|
|
15
|
+
const harness = await HuntHarness.from(engine, options, context.logger);
|
|
16
|
+
const result = await harness.run();
|
|
17
|
+
for (const message of result.messages) context.logger.info(message);
|
|
18
|
+
if (result.status === "failed") return 1;
|
|
19
|
+
if (options.failOnBlocking && result.blockers > 0) return 1;
|
|
20
|
+
return 0;
|
|
21
|
+
} finally {
|
|
22
|
+
engine.close();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -26,9 +26,15 @@ export class ServerCommand implements Command {
|
|
|
26
26
|
const parser = ArgParser.from(args);
|
|
27
27
|
const port = Number(parser.value("--port")) || 7331;
|
|
28
28
|
const host = parser.value("--host") ?? "127.0.0.1";
|
|
29
|
+
const editorPreference = parser.value("--editor");
|
|
29
30
|
|
|
30
31
|
const { startServer } = await import("../../server");
|
|
31
|
-
const { url } = startServer({
|
|
32
|
+
const { url } = startServer({
|
|
33
|
+
config: engine.config,
|
|
34
|
+
port,
|
|
35
|
+
host,
|
|
36
|
+
editorPreference,
|
|
37
|
+
});
|
|
32
38
|
context.out(`[houndsman] servidor em ${url}`);
|
|
33
39
|
context.out("[houndsman] Ctrl+C para encerrar.");
|
|
34
40
|
if (parser.has("--open")) openBrowser(url);
|
package/src/cli/help.ts
CHANGED
|
@@ -5,9 +5,10 @@ Comandos:
|
|
|
5
5
|
(nenhum) Prepara a revisão (classification + briefs + manifesto)
|
|
6
6
|
--finalize Consolida os findings em review.md
|
|
7
7
|
context Coleta o contexto da PR via gh (Passo 0)
|
|
8
|
+
hunt Revisão nativa com IA (harness próprio): prepare + agentes + finalize
|
|
8
9
|
findings <sub> Triagem/correção dos findings (list · show · set · done · reset · sync · summary · batch · verify · submit · path)
|
|
9
10
|
status [ping] Mostra o status da run (ping renova o heartbeat da execução)
|
|
10
|
-
server Sobe a UI local de triagem (--port, --host, --open)
|
|
11
|
+
server Sobe a UI local de triagem (--port, --host, --open, --editor)
|
|
11
12
|
setup Cria a configuração inicial do projeto (config + skills)
|
|
12
13
|
update Sincroniza os arquivos auto-gerados (.houndsman/core)
|
|
13
14
|
doctor Valida a configuração, o ambiente e a credencial Jev
|
|
@@ -22,8 +23,16 @@ Flags:
|
|
|
22
23
|
--clean Descarta findings brutos do staging anterior
|
|
23
24
|
--full Força review completo (ignora o modo incremental)
|
|
24
25
|
--no-jev Desativa a pré-análise Jev
|
|
26
|
+
--no-ast Desativa a análise estrutural (tree-sitter)
|
|
27
|
+
--no-ai Desativa as features de IA do fluxo normal
|
|
25
28
|
--fail-on-blocking Exit 1 se houver bloqueantes (com --finalize)
|
|
26
29
|
|
|
30
|
+
Flags do hunt:
|
|
31
|
+
--base <b> · --head <ref> · --branch <nome> · --watchdog <id>
|
|
32
|
+
--full · --clean · --no-ast · --with-jev
|
|
33
|
+
--resume · --allow-partial · --dry-run · --fail-on-blocking
|
|
34
|
+
--max-calls <n> · --concurrency <n> · --provider <p> · --model <m>
|
|
35
|
+
|
|
27
36
|
Flags de findings:
|
|
28
37
|
--decision <d> pending | execute | ignore
|
|
29
38
|
--exec <e> pending | in_progress | done | stale
|
|
@@ -41,8 +50,15 @@ Flags de findings:
|
|
|
41
50
|
--fail-on-persisting (verify) Exit 1 se algum blocking persistir
|
|
42
51
|
--include-sent (submit) Reenvia findings já enviados à PR
|
|
43
52
|
--dry-run (submit) Só a prévia do envio, sem postar
|
|
53
|
+
--no-ping (doctor) Pula a validação de rede das credenciais
|
|
44
54
|
|
|
45
55
|
Persistência:
|
|
46
56
|
Runs, findings e artefatos ficam no storage global (SQLite em
|
|
47
57
|
<HOUNDSMAN_HOME>/houndsman.db). Use storage.provider: "file" na config para
|
|
48
|
-
voltar ao backend de arquivos. Nada é gravado em .houndsman/.runs
|
|
58
|
+
voltar ao backend de arquivos. Nada é gravado em .houndsman/.runs.
|
|
59
|
+
|
|
60
|
+
Ambiente (o Bun carrega .env automaticamente):
|
|
61
|
+
HOUNDSMAN_TYPE_SAFE_API_KEY Pré-análise Jev (opcional)
|
|
62
|
+
HOUNDSMAN_DEEP_SEEK_API_KEY IA (provider deepseek, opcional)
|
|
63
|
+
HOUNDSMAN_OPENAI_API_KEY IA (provider openai, opcional)
|
|
64
|
+
HOUNDSMAN_ANTHROPIC_API_KEY IA (provider anthropic, opcional)`;
|
package/src/cli/index.ts
CHANGED
|
@@ -24,17 +24,19 @@
|
|
|
24
24
|
* watchdog <id> Contexto de um watchdog (`--rules`, `--json`)
|
|
25
25
|
*
|
|
26
26
|
* Flags: `--base <b>` · `--head <ref>` · `--branch <b>` · `--config <path>` ·
|
|
27
|
-
* `--clean` · `--full` · `--no-jev` · `--fail-on-blocking`.
|
|
27
|
+
* `--clean` · `--full` · `--no-jev` · `--no-ast` · `--fail-on-blocking`.
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
30
|
import { loadConfig } from "../core/config-loader";
|
|
31
31
|
import { HoundsmanEngine } from "../core/engine/houndsman-engine";
|
|
32
|
+
import { logger } from "../core/logging";
|
|
32
33
|
import { ArgParser } from "./arg-parser";
|
|
33
34
|
import type { CliContext } from "./commands/command";
|
|
34
35
|
import { ContextCommand } from "./commands/context-command";
|
|
35
36
|
import { DoctorCommand } from "./commands/doctor-command";
|
|
36
37
|
import { FindingsCommand } from "./commands/findings-command";
|
|
37
38
|
import { HelpCommand } from "./commands/help-command";
|
|
39
|
+
import { HuntCommand } from "./commands/hunt-command";
|
|
38
40
|
import { CommandRegistry } from "./commands/registry";
|
|
39
41
|
import { ReviewCommand } from "./commands/review-command";
|
|
40
42
|
import { ServerCommand } from "./commands/server-command";
|
|
@@ -53,6 +55,7 @@ function buildRegistry(): CommandRegistry {
|
|
|
53
55
|
return new CommandRegistry()
|
|
54
56
|
.register(new ReviewCommand(), { fallback: true })
|
|
55
57
|
.register(new HelpCommand())
|
|
58
|
+
.register(new HuntCommand())
|
|
56
59
|
.register(new ContextCommand())
|
|
57
60
|
.register(new FindingsCommand())
|
|
58
61
|
.register(new StatusCommand())
|
|
@@ -77,15 +80,16 @@ export async function main(): Promise<void> {
|
|
|
77
80
|
const command = registry.resolve(commandName);
|
|
78
81
|
|
|
79
82
|
if (!command) {
|
|
80
|
-
|
|
81
|
-
|
|
83
|
+
logger.error(`Subcomando desconhecido: '${commandName}'.\n`);
|
|
84
|
+
logger.info(HELP);
|
|
82
85
|
process.exit(1);
|
|
83
86
|
}
|
|
84
87
|
|
|
85
88
|
const context: CliContext = {
|
|
86
89
|
cwd: process.cwd(),
|
|
87
|
-
|
|
88
|
-
|
|
90
|
+
logger,
|
|
91
|
+
out: (message) => logger.info(message),
|
|
92
|
+
err: (message) => logger.error(message),
|
|
89
93
|
engine: async () => {
|
|
90
94
|
const { config } = await loadConfig({
|
|
91
95
|
configPath: parser.value("--config"),
|
|
@@ -100,7 +104,7 @@ export async function main(): Promise<void> {
|
|
|
100
104
|
|
|
101
105
|
if (import.meta.main) {
|
|
102
106
|
await main().catch((error) => {
|
|
103
|
-
|
|
107
|
+
logger.error(error instanceof Error ? error.message : String(error));
|
|
104
108
|
process.exit(1);
|
|
105
109
|
});
|
|
106
110
|
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fachada de IA (`core/ai`) — fail-open, budget e uso.
|
|
3
|
+
*
|
|
4
|
+
* As features do core usam `generateObject`/`generateText` e tratam `ok:false`
|
|
5
|
+
* como "sem IA": a revisão determinística segue idêntica. O harness nativo usa
|
|
6
|
+
* `requireProvider` e falha com mensagem clara quando a IA não está configurada.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ZodType } from "zod";
|
|
10
|
+
import type { ProjectConfig } from "../project-config";
|
|
11
|
+
import { AiConfigurationError, errorMessage } from "./errors";
|
|
12
|
+
import { ModelFactory } from "./model-factory";
|
|
13
|
+
import type { AiResult, AiTool, AiUsage, ModelProvider, ModelRequest } from "./types";
|
|
14
|
+
import { AiUsageBudget } from "./usage-budget";
|
|
15
|
+
import { AiUsageRecorder } from "./usage-recorder";
|
|
16
|
+
|
|
17
|
+
/** Opções de construção do cliente. */
|
|
18
|
+
export type AiClientOptions = {
|
|
19
|
+
project: ProjectConfig;
|
|
20
|
+
/** Provider injetado (testes) — dispensa chave/rede. */
|
|
21
|
+
provider?: ModelProvider | null;
|
|
22
|
+
factory?: ModelFactory;
|
|
23
|
+
recorder?: AiUsageRecorder;
|
|
24
|
+
budget?: AiUsageBudget;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Pedido de objeto estruturado. */
|
|
28
|
+
export type ObjectRequest<T> = {
|
|
29
|
+
instructions?: string;
|
|
30
|
+
prompt: string;
|
|
31
|
+
outputSchema: ZodType<T>;
|
|
32
|
+
tools?: Record<string, AiTool>;
|
|
33
|
+
maxSteps?: number;
|
|
34
|
+
maxOutputTokens?: number;
|
|
35
|
+
temperature?: number;
|
|
36
|
+
signal?: AbortSignal;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** Pedido de texto livre. */
|
|
40
|
+
export type TextRequest = Omit<ObjectRequest<string>, "outputSchema">;
|
|
41
|
+
|
|
42
|
+
/** Resultado da validação de credencial (doctor). */
|
|
43
|
+
export type AiCredentialCheck = {
|
|
44
|
+
ok: boolean;
|
|
45
|
+
kind: "ok" | "missing" | "invalid" | "unreachable";
|
|
46
|
+
detail: string;
|
|
47
|
+
model?: string;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** Cliente de IA compartilhado pela review. */
|
|
51
|
+
export class AiClient {
|
|
52
|
+
private readonly project: ProjectConfig;
|
|
53
|
+
private readonly factory: ModelFactory;
|
|
54
|
+
private readonly override: ModelProvider | null | undefined;
|
|
55
|
+
readonly recorder: AiUsageRecorder;
|
|
56
|
+
readonly budget: AiUsageBudget;
|
|
57
|
+
|
|
58
|
+
constructor(options: AiClientOptions) {
|
|
59
|
+
this.project = options.project;
|
|
60
|
+
this.factory = options.factory ?? new ModelFactory(options.project);
|
|
61
|
+
this.override = options.provider;
|
|
62
|
+
this.recorder =
|
|
63
|
+
options.recorder ??
|
|
64
|
+
new AiUsageRecorder(this.project.aiProvider, this.project.aiModel);
|
|
65
|
+
this.budget =
|
|
66
|
+
options.budget ??
|
|
67
|
+
new AiUsageBudget({
|
|
68
|
+
maxCalls: this.project.aiMaxCallsPerReview,
|
|
69
|
+
maxTokens: this.project.aiMaxTokensPerReview,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
get modelId(): string {
|
|
74
|
+
return this.project.aiModel;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Configuração resolvida do projeto (features, budgets). */
|
|
78
|
+
get projectConfig(): ProjectConfig {
|
|
79
|
+
return this.project;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Features resolvidas (context/brief ligadas por default). */
|
|
83
|
+
get features() {
|
|
84
|
+
return this.project.aiFeatures;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** `true` quando há provider disponível (injetado ou configurado + chave). */
|
|
88
|
+
get available(): boolean {
|
|
89
|
+
if (this.override !== undefined) return this.override !== null;
|
|
90
|
+
return this.factory.isConfigured;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Provider obrigatório — usado pelo harness (falha clara sem IA). */
|
|
94
|
+
async requireProvider(model?: string): Promise<ModelProvider> {
|
|
95
|
+
if (this.override === null) {
|
|
96
|
+
throw new AiConfigurationError(
|
|
97
|
+
"IA indisponível: provider injetado como null.",
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
if (this.override) return this.override;
|
|
101
|
+
return this.factory.create({ model });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Geração de objeto estruturado, fail-open. */
|
|
105
|
+
async generateObject<T>(
|
|
106
|
+
feature: string,
|
|
107
|
+
request: ObjectRequest<T>,
|
|
108
|
+
model?: string,
|
|
109
|
+
): Promise<AiResult<T>> {
|
|
110
|
+
return this.run(
|
|
111
|
+
feature,
|
|
112
|
+
{ ...request, outputSchema: request.outputSchema },
|
|
113
|
+
model,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Geração de texto, fail-open. */
|
|
118
|
+
async generateText(
|
|
119
|
+
feature: string,
|
|
120
|
+
request: TextRequest,
|
|
121
|
+
model?: string,
|
|
122
|
+
): Promise<AiResult<string>> {
|
|
123
|
+
return this.run<string>(feature, { ...request }, model);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Ping mínimo para o `doctor`: valida a credencial sem gastar tokens
|
|
128
|
+
* relevantes. Não lança; devolve o diagnóstico.
|
|
129
|
+
*/
|
|
130
|
+
async validateCredential(
|
|
131
|
+
options: { timeoutMs?: number } = {},
|
|
132
|
+
): Promise<AiCredentialCheck> {
|
|
133
|
+
if (this.override === null || !this.available) {
|
|
134
|
+
return {
|
|
135
|
+
ok: false,
|
|
136
|
+
kind: "missing",
|
|
137
|
+
detail: "provider/chave de IA não configurados",
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const timeoutMs = Math.max(
|
|
141
|
+
1_000,
|
|
142
|
+
options.timeoutMs ?? Math.min(this.project.aiTimeoutMs, 8_000),
|
|
143
|
+
);
|
|
144
|
+
const result = await this.generateText("doctor-ping", {
|
|
145
|
+
prompt: "Responda apenas OK.",
|
|
146
|
+
maxSteps: 1,
|
|
147
|
+
maxOutputTokens: 4,
|
|
148
|
+
temperature: 0,
|
|
149
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
150
|
+
});
|
|
151
|
+
if (result.ok) {
|
|
152
|
+
return {
|
|
153
|
+
ok: true,
|
|
154
|
+
kind: "ok",
|
|
155
|
+
detail: "credencial válida",
|
|
156
|
+
model: result.model,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const message = result.error ?? "falha desconhecida";
|
|
160
|
+
if (
|
|
161
|
+
/401|403|unauthorized|invalid.*(key|token)|authentication|api.?key/i.test(
|
|
162
|
+
message,
|
|
163
|
+
)
|
|
164
|
+
) {
|
|
165
|
+
return { ok: false, kind: "invalid", detail: message };
|
|
166
|
+
}
|
|
167
|
+
return { ok: false, kind: "unreachable", detail: message };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Executa a chamada respeitando budget, timeout e uso. */
|
|
171
|
+
private async run<T>(
|
|
172
|
+
feature: string,
|
|
173
|
+
request: Omit<ObjectRequest<T>, "outputSchema"> & {
|
|
174
|
+
outputSchema?: ZodType<T>;
|
|
175
|
+
},
|
|
176
|
+
model?: string,
|
|
177
|
+
): Promise<AiResult<T>> {
|
|
178
|
+
if (!this.available) {
|
|
179
|
+
return { ok: false, error: "IA não configurada (fail-open)." };
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
this.budget.assertCanCall();
|
|
183
|
+
} catch (error) {
|
|
184
|
+
return { ok: false, error: errorMessage(error) };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
const provider = await this.requireProvider(model);
|
|
189
|
+
const response = await provider.generate({
|
|
190
|
+
...request,
|
|
191
|
+
signal: combineSignals(request.signal, this.project.aiTimeoutMs),
|
|
192
|
+
});
|
|
193
|
+
this.budget.record(response.usage);
|
|
194
|
+
this.recorder.record(feature, provider.modelId, response.usage);
|
|
195
|
+
return {
|
|
196
|
+
ok: true,
|
|
197
|
+
output: response.output,
|
|
198
|
+
usage: response.usage,
|
|
199
|
+
model: provider.modelId,
|
|
200
|
+
};
|
|
201
|
+
} catch (error) {
|
|
202
|
+
return { ok: false, error: errorMessage(error) };
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Combina o sinal do chamador com o timeout configurado. */
|
|
208
|
+
function combineSignals(
|
|
209
|
+
signal: AbortSignal | undefined,
|
|
210
|
+
timeoutMs: number,
|
|
211
|
+
): AbortSignal | undefined {
|
|
212
|
+
const timeout =
|
|
213
|
+
Number.isFinite(timeoutMs) && timeoutMs > 0
|
|
214
|
+
? AbortSignal.timeout(timeoutMs)
|
|
215
|
+
: undefined;
|
|
216
|
+
if (!signal) return timeout;
|
|
217
|
+
if (!timeout) return signal;
|
|
218
|
+
return AbortSignal.any([signal, timeout]);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export type { AiUsage, ModelRequest };
|