@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.
Files changed (175) hide show
  1. package/README.md +24 -4
  2. package/assets/grammars/tree-sitter-javascript.wasm +0 -0
  3. package/assets/grammars/tree-sitter-tsx.wasm +0 -0
  4. package/assets/grammars/tree-sitter-typescript.wasm +0 -0
  5. package/assets/grammars/web-tree-sitter.wasm +0 -0
  6. package/package.json +11 -3
  7. package/src/cli/commands/command.ts +5 -0
  8. package/src/cli/commands/doctor-command.ts +116 -24
  9. package/src/cli/commands/hunt-command.ts +25 -0
  10. package/src/cli/commands/server-command.ts +7 -1
  11. package/src/cli/help.ts +18 -2
  12. package/src/cli/index.ts +10 -6
  13. package/src/core/ai/ai-client.ts +221 -0
  14. package/src/core/ai/ai-sdk-model-provider.ts +101 -0
  15. package/src/core/ai/errors.ts +36 -0
  16. package/src/core/ai/features/brief-enricher.ts +62 -0
  17. package/src/core/ai/features/classifier.ts +50 -0
  18. package/src/core/ai/features/context-summarizer.ts +31 -0
  19. package/src/core/ai/features/pre-analyzer.ts +51 -0
  20. package/src/core/ai/features/verifier.ts +45 -0
  21. package/src/core/ai/index.ts +56 -0
  22. package/src/core/ai/model-factory.ts +137 -0
  23. package/src/core/ai/prompts/base.ts +36 -0
  24. package/src/core/ai/prompts/brief.ts +87 -0
  25. package/src/core/ai/prompts/classification.ts +36 -0
  26. package/src/core/ai/prompts/context.ts +29 -0
  27. package/src/core/ai/prompts/pre-analysis.ts +49 -0
  28. package/src/core/ai/prompts/verify.ts +40 -0
  29. package/src/core/ai/schemas.ts +110 -0
  30. package/src/core/ai/types.ts +76 -0
  31. package/src/core/ai/usage-budget.ts +75 -0
  32. package/src/core/ai/usage-recorder.ts +79 -0
  33. package/src/core/assets.ts +16 -0
  34. package/src/core/ast/diff.ts +220 -0
  35. package/src/core/ast/impact.ts +127 -0
  36. package/src/core/ast/parser.ts +167 -0
  37. package/src/core/ast/service.ts +233 -0
  38. package/src/core/ast/symbols.ts +294 -0
  39. package/src/core/ast/types.ts +125 -0
  40. package/src/core/brief.ts +138 -0
  41. package/src/core/commands/findings.ts +65 -2
  42. package/src/core/commands/review.ts +2 -0
  43. package/src/core/commands/scaffold.ts +159 -10
  44. package/src/core/commands/update.ts +5 -3
  45. package/src/core/config.ts +85 -0
  46. package/src/core/defaults.ts +27 -0
  47. package/src/core/domain/watchdog.ts +43 -5
  48. package/src/core/engine/houndsman-engine.ts +16 -1
  49. package/src/core/engine/review-scope.ts +52 -6
  50. package/src/core/engine/review-session.ts +27 -2
  51. package/src/core/env.ts +101 -0
  52. package/src/core/file-classifier.ts +25 -4
  53. package/src/core/git-runner.ts +25 -13
  54. package/src/core/github-review.ts +10 -7
  55. package/src/core/jev/jev-client.ts +8 -7
  56. package/src/core/jev/jev-verify.ts +2 -1
  57. package/src/core/logging/index.ts +17 -0
  58. package/src/core/logging/logger.ts +187 -0
  59. package/src/core/orchestrator.ts +357 -9
  60. package/src/core/project-config.ts +123 -1
  61. package/src/core/registry-check.ts +148 -6
  62. package/src/core/renderers/manifest-writer.ts +33 -0
  63. package/src/core/rule-text.ts +97 -0
  64. package/src/core/services/file-classifier.ts +10 -3
  65. package/src/core/services/gh-client.ts +38 -22
  66. package/src/core/services/verifier.ts +14 -3
  67. package/src/core/storage/paths.ts +6 -5
  68. package/src/core/storage/types.ts +6 -0
  69. package/src/core/storage/work-area.ts +12 -0
  70. package/src/core/types.ts +37 -1
  71. package/src/core/verify.ts +17 -5
  72. package/src/harness/README.md +15 -0
  73. package/src/harness/agent/agent-context.ts +33 -0
  74. package/src/harness/agent/agent-errors.ts +30 -0
  75. package/src/harness/agent/agent-factory.ts +45 -0
  76. package/src/harness/agent/agent-reporter.ts +34 -0
  77. package/src/harness/agent/agent-result.ts +16 -0
  78. package/src/harness/agent/agent-runner.ts +32 -0
  79. package/src/harness/agent/agent-scheduler.ts +37 -0
  80. package/src/harness/agent/review-agent.ts +391 -0
  81. package/src/harness/config/harness-config.ts +84 -0
  82. package/src/harness/hunt/hunt-harness.ts +83 -0
  83. package/src/harness/hunt/hunt-options.ts +75 -0
  84. package/src/harness/hunt/hunt-result.ts +17 -0
  85. package/src/harness/hunt/hunt-session.ts +372 -0
  86. package/src/harness/hunt/review-planner.ts +98 -0
  87. package/src/harness/index.ts +36 -0
  88. package/src/harness/investigation/agent-state.ts +134 -0
  89. package/src/harness/investigation/evidence-verifier.ts +129 -0
  90. package/src/harness/investigation/evidence.ts +110 -0
  91. package/src/harness/investigation/finding-critic.ts +96 -0
  92. package/src/harness/investigation/investigation-planner.ts +118 -0
  93. package/src/harness/investigation/investigation-task.ts +10 -0
  94. package/src/harness/investigation/review-journal.ts +87 -0
  95. package/src/harness/investigation/rule-analyzer.ts +178 -0
  96. package/src/harness/investigation/rule-signals.ts +110 -0
  97. package/src/harness/prompts/agent-schemas.ts +57 -0
  98. package/src/harness/prompts/reviewer-prompt-builder.ts +101 -0
  99. package/src/harness/prompts/states/conclude-prompt.ts +16 -0
  100. package/src/harness/prompts/states/investigate-prompt.ts +37 -0
  101. package/src/harness/prompts/states/orient-prompt.ts +15 -0
  102. package/src/harness/prompts/states/verify-prompt.ts +22 -0
  103. package/src/harness/tools/agent-tool.ts +102 -0
  104. package/src/harness/tools/builtin/changed-symbols-tool.ts +48 -0
  105. package/src/harness/tools/builtin/doc-context-tool.ts +74 -0
  106. package/src/harness/tools/builtin/file-outline-tool.ts +66 -0
  107. package/src/harness/tools/builtin/find-references-tool.ts +66 -0
  108. package/src/harness/tools/builtin/find-symbol-tool.ts +91 -0
  109. package/src/harness/tools/builtin/helpers.ts +115 -0
  110. package/src/harness/tools/builtin/impact-tool.ts +48 -0
  111. package/src/harness/tools/builtin/imports-exports-tool.ts +53 -0
  112. package/src/harness/tools/builtin/index.ts +59 -0
  113. package/src/harness/tools/builtin/list-files-tool.ts +85 -0
  114. package/src/harness/tools/builtin/pr-context-tool.ts +41 -0
  115. package/src/harness/tools/builtin/read-file-range-tool.ts +73 -0
  116. package/src/harness/tools/builtin/rule-signals-tool.ts +36 -0
  117. package/src/harness/tools/builtin/search-code-tool.ts +65 -0
  118. package/src/harness/tools/builtin/symbol-context-tool.ts +106 -0
  119. package/src/harness/tools/builtin/tests-for-symbol-tool.ts +61 -0
  120. package/src/harness/tools/path-guard.ts +90 -0
  121. package/src/harness/tools/result-limiter.ts +49 -0
  122. package/src/harness/tools/secret-redactor.ts +54 -0
  123. package/src/harness/tools/tool-cache.ts +50 -0
  124. package/src/harness/tools/tool-call-budget.ts +60 -0
  125. package/src/harness/tools/tool-context.ts +5 -0
  126. package/src/harness/tools/tool-middleware.ts +105 -0
  127. package/src/harness/tools/tool-policy.ts +51 -0
  128. package/src/harness/tools/tool-registry.ts +112 -0
  129. package/src/harness/tools/tool-result.ts +15 -0
  130. package/src/harness/tools/toolbox.ts +36 -0
  131. package/src/harness/validation/finding-validator.ts +119 -0
  132. package/src/index.ts +46 -0
  133. package/src/server/client/App.tsx +38 -12
  134. package/src/server/client/CommentsPanel.tsx +255 -0
  135. package/src/server/client/FindingCard.tsx +12 -4
  136. package/src/server/client/FindingsList.tsx +4 -1
  137. package/src/server/client/Sidebar.tsx +397 -0
  138. package/src/server/client/SubmitModal.tsx +61 -5
  139. package/src/server/client/icons.tsx +19 -0
  140. package/src/server/client/index.tsx +1 -1
  141. package/src/server/client/utils/constants.ts +29 -5
  142. package/src/server/client/utils/format.ts +47 -2
  143. package/src/server/client/utils/types.ts +88 -0
  144. package/src/server/editor.ts +129 -0
  145. package/src/server/index.ts +115 -5
  146. package/src/server/pr-comments.ts +256 -0
  147. package/src/server/pr-inbox.ts +202 -0
  148. package/src/server/run-repository.ts +33 -1
  149. package/src/skill/OUTPUT-FORMAT.md +12 -0
  150. package/src/skill/SETUP.md +6 -2
  151. package/src/skill/hunt.md +14 -2
  152. package/src/skill/verify.md +1 -1
  153. package/src/tests/__snapshots__/baseline.test.ts.snap +96 -14
  154. package/src/tests/ai-core.test.ts +120 -0
  155. package/src/tests/ai-features.test.ts +183 -0
  156. package/src/tests/ast.test.ts +335 -0
  157. package/src/tests/cli.test.ts +33 -0
  158. package/src/tests/env.test.ts +69 -0
  159. package/src/tests/fallback.test.ts +172 -0
  160. package/src/tests/fixtures/fake-agent-tools.ts +69 -0
  161. package/src/tests/fixtures/fake-investigation.ts +64 -0
  162. package/src/tests/fixtures/fake-model-provider.ts +48 -0
  163. package/src/tests/github-review.test.ts +9 -0
  164. package/src/tests/harness-agent.test.ts +122 -0
  165. package/src/tests/harness-investigation.test.ts +206 -0
  166. package/src/tests/harness-tools.test.ts +148 -0
  167. package/src/tests/hunt-command.test.ts +212 -0
  168. package/src/tests/logger.test.ts +83 -0
  169. package/src/tests/review-scope.test.ts +11 -11
  170. package/src/tests/rule-analyzer.test.ts +61 -0
  171. package/src/tests/server-editor.test.ts +100 -0
  172. package/src/tests/server.test.ts +295 -2
  173. package/src/tests/tool-contracts.test.ts +82 -0
  174. package/src/tests/verify.test.ts +1 -1
  175. 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
- - **`TYPESAFE_API_KEY`** (Opcional) — Env a ser informada na raiz, usada em pré-analise com JEV
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 **1 watchdog de exemplo** |
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 de exemplo — troque o exemplo pelas
57
- > regras reais do seu projeto (ou use `/houndsman train`).
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@payconductor/houndsman",
3
- "version": "0.5.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
- "preact": "^11.0.1"
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 credencial Jev. */
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(_args: string[], context: CliContext): Promise<number> {
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
- context.out("→ Validando configuração do projeto...");
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
- context.err("\n❌ Problemas na configuração de revisão:\n");
17
- for (const problem of problems) context.err(` - ${problem}`);
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
- context.out(
21
- ` ✅ ${engine.config.watchdogIds.length} watchdogs, ${jevQuestions} pergunta(s) Jev.`,
54
+ logger.success(
55
+ `${engine.config.watchdogIds.length} watchdogs, ${jevQuestions} pergunta(s) Jev.`,
22
56
  );
23
57
 
24
58
  if (warnings.length > 0) {
25
- context.out(`\n⚠️ ${warnings.length} aviso(s) (não bloqueantes):`);
26
- for (const warning of warnings) context.out(` - ${warning}`);
27
- }
28
-
29
- context.out("\n→ Ambiente:");
30
- context.out(` ${Bun.which("git") ? "✅" : "❌"} git`);
31
- context.out(` ${Bun.which("gh") ? "✅" : "⚠️ "} gh (opcional — contexto da PR)`);
32
- context.out("\n→ Persistência:");
33
- context.out(` provider: ${engine.storage.kind}`);
34
- context.out(` ${storageLocation(engine.config)}`);
35
- context.out(` repo: ${engine.repo.key}`);
36
- await this.reportJevCredential(context);
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(context: CliContext): Promise<void> {
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.out(" ⚠️ TYPESAFE_API_KEY (opcional — pré-análise Jev)");
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.out(` ✅ Jev: credencial válida (modelo ${check.model}).`);
145
+ context.logger.success(`Jev: credencial válida (modelo ${check.model}).`);
54
146
  } else if (check.kind === "invalid") {
55
- context.out(` ❌ Jev: credencial inválida — ${check.detail}`);
147
+ context.logger.error(`Jev: credencial inválida — ${check.detail}`);
56
148
  } else {
57
- context.out(` ⚠️ Jev: não foi possível validar — ${check.detail}`);
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({ config: engine.config, port, host });
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
- console.error(`Subcomando desconhecido: '${commandName}'.\n`);
81
- console.error(HELP);
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
- out: (message) => console.log(message),
88
- err: (message) => console.error(message),
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
- console.error(error instanceof Error ? error.message : String(error));
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 };