@payconductor/houndsman 0.4.0 → 0.6.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 (196) hide show
  1. package/README.md +35 -41
  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/status-command.ts +46 -0
  11. package/src/cli/help.ts +18 -1
  12. package/src/cli/index.ts +13 -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 +120 -6
  42. package/src/core/commands/review.ts +2 -0
  43. package/src/core/commands/scaffold.ts +159 -10
  44. package/src/core/commands/status.ts +63 -0
  45. package/src/core/commands/update.ts +5 -3
  46. package/src/core/config.ts +85 -0
  47. package/src/core/defaults.ts +27 -0
  48. package/src/core/domain/watchdog.ts +43 -5
  49. package/src/core/engine/houndsman-engine.ts +16 -1
  50. package/src/core/engine/review-scope.ts +77 -13
  51. package/src/core/engine/review-session.ts +27 -2
  52. package/src/core/env.ts +101 -0
  53. package/src/core/file-classifier.ts +25 -4
  54. package/src/core/findings-state.ts +21 -4
  55. package/src/core/github-review.ts +620 -57
  56. package/src/core/jev/jev-client.ts +8 -7
  57. package/src/core/jev/jev-verify.ts +2 -1
  58. package/src/core/logging/index.ts +17 -0
  59. package/src/core/logging/logger.ts +187 -0
  60. package/src/core/orchestrator.ts +454 -16
  61. package/src/core/paths.ts +3 -0
  62. package/src/core/project-config.ts +123 -1
  63. package/src/core/registry-check.ts +148 -6
  64. package/src/core/renderers/manifest-writer.ts +33 -0
  65. package/src/core/renderers/resume-renderer.ts +151 -0
  66. package/src/core/report-builder.ts +7 -4
  67. package/src/core/rule-text.ts +97 -0
  68. package/src/core/run-status.ts +39 -5
  69. package/src/core/services/file-classifier.ts +10 -3
  70. package/src/core/services/verifier.ts +14 -3
  71. package/src/core/storage/file/file-storage.ts +2 -0
  72. package/src/core/storage/paths.ts +6 -5
  73. package/src/core/storage/sqlite/mappers.ts +2 -0
  74. package/src/core/storage/sqlite/schema.ts +9 -1
  75. package/src/core/storage/sqlite/sqlite-storage.ts +5 -1
  76. package/src/core/storage/types.ts +15 -0
  77. package/src/core/storage/work-area.ts +12 -0
  78. package/src/core/types.ts +37 -1
  79. package/src/core/verify.ts +17 -5
  80. package/src/harness/README.md +15 -0
  81. package/src/harness/agent/agent-context.ts +33 -0
  82. package/src/harness/agent/agent-errors.ts +30 -0
  83. package/src/harness/agent/agent-factory.ts +45 -0
  84. package/src/harness/agent/agent-reporter.ts +34 -0
  85. package/src/harness/agent/agent-result.ts +16 -0
  86. package/src/harness/agent/agent-runner.ts +32 -0
  87. package/src/harness/agent/agent-scheduler.ts +37 -0
  88. package/src/harness/agent/review-agent.ts +391 -0
  89. package/src/harness/config/harness-config.ts +84 -0
  90. package/src/harness/hunt/hunt-harness.ts +83 -0
  91. package/src/harness/hunt/hunt-options.ts +75 -0
  92. package/src/harness/hunt/hunt-result.ts +17 -0
  93. package/src/harness/hunt/hunt-session.ts +372 -0
  94. package/src/harness/hunt/review-planner.ts +98 -0
  95. package/src/harness/index.ts +36 -0
  96. package/src/harness/investigation/agent-state.ts +134 -0
  97. package/src/harness/investigation/evidence-verifier.ts +129 -0
  98. package/src/harness/investigation/evidence.ts +110 -0
  99. package/src/harness/investigation/finding-critic.ts +96 -0
  100. package/src/harness/investigation/investigation-planner.ts +118 -0
  101. package/src/harness/investigation/investigation-task.ts +10 -0
  102. package/src/harness/investigation/review-journal.ts +87 -0
  103. package/src/harness/investigation/rule-analyzer.ts +178 -0
  104. package/src/harness/investigation/rule-signals.ts +110 -0
  105. package/src/harness/prompts/agent-schemas.ts +57 -0
  106. package/src/harness/prompts/reviewer-prompt-builder.ts +101 -0
  107. package/src/harness/prompts/states/conclude-prompt.ts +16 -0
  108. package/src/harness/prompts/states/investigate-prompt.ts +37 -0
  109. package/src/harness/prompts/states/orient-prompt.ts +15 -0
  110. package/src/harness/prompts/states/verify-prompt.ts +22 -0
  111. package/src/harness/tools/agent-tool.ts +102 -0
  112. package/src/harness/tools/builtin/changed-symbols-tool.ts +48 -0
  113. package/src/harness/tools/builtin/doc-context-tool.ts +74 -0
  114. package/src/harness/tools/builtin/file-outline-tool.ts +66 -0
  115. package/src/harness/tools/builtin/find-references-tool.ts +66 -0
  116. package/src/harness/tools/builtin/find-symbol-tool.ts +91 -0
  117. package/src/harness/tools/builtin/helpers.ts +115 -0
  118. package/src/harness/tools/builtin/impact-tool.ts +48 -0
  119. package/src/harness/tools/builtin/imports-exports-tool.ts +53 -0
  120. package/src/harness/tools/builtin/index.ts +59 -0
  121. package/src/harness/tools/builtin/list-files-tool.ts +85 -0
  122. package/src/harness/tools/builtin/pr-context-tool.ts +41 -0
  123. package/src/harness/tools/builtin/read-file-range-tool.ts +73 -0
  124. package/src/harness/tools/builtin/rule-signals-tool.ts +36 -0
  125. package/src/harness/tools/builtin/search-code-tool.ts +65 -0
  126. package/src/harness/tools/builtin/symbol-context-tool.ts +106 -0
  127. package/src/harness/tools/builtin/tests-for-symbol-tool.ts +61 -0
  128. package/src/harness/tools/path-guard.ts +90 -0
  129. package/src/harness/tools/result-limiter.ts +49 -0
  130. package/src/harness/tools/secret-redactor.ts +54 -0
  131. package/src/harness/tools/tool-cache.ts +50 -0
  132. package/src/harness/tools/tool-call-budget.ts +60 -0
  133. package/src/harness/tools/tool-context.ts +5 -0
  134. package/src/harness/tools/tool-middleware.ts +105 -0
  135. package/src/harness/tools/tool-policy.ts +51 -0
  136. package/src/harness/tools/tool-registry.ts +112 -0
  137. package/src/harness/tools/tool-result.ts +15 -0
  138. package/src/harness/tools/toolbox.ts +36 -0
  139. package/src/harness/validation/finding-validator.ts +119 -0
  140. package/src/index.ts +46 -0
  141. package/src/server/client/App.tsx +26 -9
  142. package/src/server/client/ArtifactsPanel.tsx +12 -5
  143. package/src/server/client/AutoRefresh.tsx +1 -1
  144. package/src/server/client/Filters.tsx +7 -2
  145. package/src/server/client/FindingCard.tsx +14 -10
  146. package/src/server/client/FindingsList.tsx +2 -2
  147. package/src/server/client/MarkdownText.tsx +92 -12
  148. package/src/server/client/ResumePanel.tsx +368 -0
  149. package/src/server/client/RunList.tsx +3 -3
  150. package/src/server/client/SubmitModal.tsx +49 -6
  151. package/src/server/client/SummaryBar.tsx +1 -1
  152. package/src/server/client/icons.tsx +17 -0
  153. package/src/server/client/{constants.ts → utils/constants.ts} +61 -10
  154. package/src/server/client/utils/markdown.ts +319 -0
  155. package/src/server/client/{types.ts → utils/types.ts} +64 -0
  156. package/src/server/index.ts +66 -7
  157. package/src/server/run-repository.ts +9 -0
  158. package/src/skill/OUTPUT-FORMAT.md +12 -0
  159. package/src/skill/SETUP.md +6 -2
  160. package/src/skill/example-review.md +22 -14
  161. package/src/skill/hunt.md +45 -19
  162. package/src/skill/verify.md +3 -2
  163. package/src/tests/__snapshots__/baseline.test.ts.snap +126 -42
  164. package/src/tests/ai-core.test.ts +120 -0
  165. package/src/tests/ai-features.test.ts +183 -0
  166. package/src/tests/ast.test.ts +335 -0
  167. package/src/tests/baseline.test.ts +3 -0
  168. package/src/tests/code-highlight.test.ts +1 -1
  169. package/src/tests/env.test.ts +69 -0
  170. package/src/tests/fallback.test.ts +172 -0
  171. package/src/tests/findings-state.test.ts +50 -2
  172. package/src/tests/fixtures/fake-agent-tools.ts +69 -0
  173. package/src/tests/fixtures/fake-investigation.ts +64 -0
  174. package/src/tests/fixtures/fake-model-provider.ts +48 -0
  175. package/src/tests/github-review.test.ts +409 -60
  176. package/src/tests/harness-agent.test.ts +122 -0
  177. package/src/tests/harness-investigation.test.ts +206 -0
  178. package/src/tests/harness-tools.test.ts +148 -0
  179. package/src/tests/hunt-command.test.ts +212 -0
  180. package/src/tests/logger.test.ts +83 -0
  181. package/src/tests/markdown.test.ts +61 -1
  182. package/src/tests/review-resume.test.ts +101 -0
  183. package/src/tests/review-scope.test.ts +302 -0
  184. package/src/tests/review-scripts.test.ts +73 -0
  185. package/src/tests/rule-analyzer.test.ts +61 -0
  186. package/src/tests/run-status-view.test.ts +94 -0
  187. package/src/tests/run-status.test.ts +53 -0
  188. package/src/tests/server.test.ts +76 -21
  189. package/src/tests/storage.test.ts +63 -0
  190. package/src/tests/tool-contracts.test.ts +82 -0
  191. package/src/tests/verify.test.ts +1 -1
  192. package/src/server/client/markdown.ts +0 -153
  193. /package/src/server/client/{api.ts → utils/api.ts} +0 -0
  194. /package/src/server/client/{code-highlight.ts → utils/code-highlight.ts} +0 -0
  195. /package/src/server/client/{format.ts → utils/format.ts} +0 -0
  196. /package/src/server/client/{json-highlight.ts → utils/json-highlight.ts} +0 -0
package/README.md CHANGED
@@ -26,8 +26,13 @@ diferente das PRs. O motor só coordena a caçada — as regras são problema se
26
26
 
27
27
  - **Bun** — runtime obrigatório.
28
28
  - **git** — o diff usa `git diff <base>...<head>`.
29
- - Opcionais: **`gh`** (contexto da PR) e **`TYPESAFE_API_KEY`** (pré-análise Jev). A revisão
30
- funciona normalmente sem eles; o `houndsman doctor` valida a credencial Jev quando presente.
29
+ - **gh** (Opcional) — Obtém contexto da PR e dispara comentários para o GitHub
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
@@ -95,62 +115,38 @@ nenhum script à mão.
95
115
 
96
116
  O skill tem **4 capabilities**, todas em `/houndsman`:
97
117
 
98
- - **`hunt`** — revisa a PR (default: `/houndsman` = `/houndsman hunt`).
118
+ - **`hunt`** — revisa a PR.
99
119
  - **`train`** — mantém/atualiza as regras dos watchdogs.
100
- - **`fix`** — corrige os findings marcados (`/houndsman fix`).
101
- - **`verify`** — confere se as violações foram resolvidas (`/houndsman verify`), sem re-revisar.
120
+ - **`fix`** — corrige os findings marcados.
121
+ - **`verify`** — confere se as violações foram resolvidas, sem re-revisar.
102
122
 
103
- - Revisão normal: `/houndsman` ou `/houndsman hunt` (a branch é detectada; base default `main`).
104
123
  - Revisar outra branch: `/houndsman hunt feature/minha-branch`.
105
- - Validar e triar na UI: `houndsman server --open` — marque os achados a executar (`execute`).
106
- - Manual (sem skill): `houndsman context` → `houndsman --base main` → *(subagentes)* →
107
- `houndsman --finalize`.
108
- - CI: `houndsman --finalize --fail-on-blocking` (exit 1 se houver bloqueantes).
124
+ - Validar na UI: `houndsman server --open` — marque os findings a executar (`execute`).
109
125
 
110
126
  ---
111
127
 
112
- ## Triando os findings (opcional)
128
+ ## Validando os findings (opcional)
113
129
 
114
- Depois do `--finalize`, cada finding tem um `id` curto e um `fingerprint` de **conteúdo**
115
- no storage global. Você decide o que fazer com cada um — pela
116
- **UI local** ou pelo **agente** (auto-fix) — sem abrir o JSON à mão:
130
+ Você decide o que fazer com cada finding pela **UI local**, informando se quer `Executar` ou `Ignorar`
117
131
 
118
132
  ```bash
119
133
  bun run houndsman server --open # UI: triar, comentar, marcar feito, ler artefatos
120
134
  ```
121
135
 
122
- - **decision:** `pending` · `execute` · `ignore` (+ comentário).
123
- - **execState:** `pending` · `in_progress` · `done` · `stale`.
124
-
125
- **Auto-fix pelo agente** — as linhas mudam depois do fix, e o motor reancora por conteúdo:
126
-
127
- ```bash
128
- bun run houndsman findings batch --json # grupo atual (agrupamento automático) + outros
129
- # ... o agente corrige apenas esse grupo ...
130
- bun run houndsman findings sync # reancora por conteúdo
131
- bun run houndsman findings done <id>
132
- bun run houndsman findings verify # confere o que resolveu (barato, sem re-revisar)
133
- ```
134
-
135
- > A partir da 2ª revisão o `hunt` é **incremental**: revisa só os arquivos que mudaram desde
136
- > o último `--finalize` e **carrega** os findings dos arquivos inalterados. `--full` força
137
- > o review completo.
138
-
139
- > A identidade do finding é o **conteúdo** (fingerprint), não a linha: itens deslocados
140
- > pelo fix não se perdem. O estado mora no storage global (`<HOUNDSMAN_HOME>/houndsman.db`).
136
+ > A partir da 2ª revisão o `hunt` é **incremental**: revisa apenas os arquivos que mudaram desde
137
+ > a última revisão. `--full` força o review completo.
141
138
 
142
139
  ---
143
140
 
144
141
  ## Mantendo a configuração
145
142
 
146
143
  - **Persistência** → por default, SQLite global em `<HOUNDSMAN_HOME>/houndsman.db`
147
- (`HOUNDSMAN_HOME` sobrescreve o diretório de dados). Para voltar ao backend de arquivos,
148
- troque só o provider — a interface `StorageProvider` é a mesma:
144
+ (`HOUNDSMAN_HOME` sobrescreve o diretório de dados). Caso queira storage base em arquivos, na config, troque provider para `file`:
149
145
 
150
146
  ```ts
151
147
  export const houndsmanConfig = {
152
148
  // ...
153
- storage: { provider: "file", path: "./houndsman-runs" },
149
+ storage: { provider: "file", path: "./houndsman/runs" },
154
150
  };
155
151
  ```
156
152
 
@@ -165,8 +161,6 @@ bun run houndsman findings verify # confere o que resolveu (barato, se
165
161
 
166
162
  O `update` reescreve **só** `.houndsman/core/**` e `.houndsman/.gitignore`. Seus arquivos
167
163
  (`watchdogs/`, `exclusions.ts`, `doc-mappings.ts`, `houndsman.config.ts`) nunca são tocados.
168
- - **Recomeçar a camada do zero** → `houndsman setup --force` (sobrescreve só os arquivos do
169
- usuário; sem `--force` o `setup` é idempotente).
170
164
 
171
165
  ---
172
166
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@payconductor/houndsman",
3
- "version": "0.4.0",
3
+ "version": "0.6.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
+ }
@@ -0,0 +1,46 @@
1
+ /** Comando `status` — mostra o status da run ou renova o heartbeat (`status ping`). */
2
+
3
+ import { pingStatus, readStatus } from "../../core/commands/status";
4
+ import { ArgParser } from "../arg-parser";
5
+ import type { CliContext, Command } from "./command";
6
+
7
+ export class StatusCommand implements Command {
8
+ readonly name = "status";
9
+
10
+ async run(args: string[], context: CliContext): Promise<number> {
11
+ const parser = ArgParser.from(args);
12
+ const action = parser.command();
13
+ const branch = parser.value("--branch");
14
+
15
+ if (action && action !== "ping" && action !== "show") {
16
+ context.err(
17
+ `Ação desconhecida em 'status': '${action}'. Use 'status' ou 'status ping'.`,
18
+ );
19
+ return 1;
20
+ }
21
+
22
+ const engine = await context.engine();
23
+ const result =
24
+ action === "ping"
25
+ ? pingStatus(engine.storage, engine.repo, branch)
26
+ : readStatus(engine.storage, engine.repo, branch);
27
+
28
+ if (action === "ping") {
29
+ context.out(
30
+ result.heartbeatAt
31
+ ? `Heartbeat atualizado — ${result.branch} · ${result.status ?? "sem status"} · ${result.heartbeatAt}`
32
+ : `Sem execução ativa — ${result.branch} · ${result.status ?? "sem status"}.`,
33
+ );
34
+ } else {
35
+ context.out(
36
+ [
37
+ `Branch: ${result.branch}`,
38
+ `Status: ${result.status ?? "—"}`,
39
+ `Heartbeat: ${result.heartbeatAt ?? "—"}`,
40
+ `Atualizado: ${result.updatedAt}`,
41
+ ].join("\n"),
42
+ );
43
+ }
44
+ return 0;
45
+ }
46
+ }
package/src/cli/help.ts CHANGED
@@ -5,7 +5,9 @@ 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)
10
+ status [ping] Mostra o status da run (ping renova o heartbeat da execução)
9
11
  server Sobe a UI local de triagem (--port, --host, --open)
10
12
  setup Cria a configuração inicial do projeto (config + skills)
11
13
  update Sincroniza os arquivos auto-gerados (.houndsman/core)
@@ -21,8 +23,16 @@ Flags:
21
23
  --clean Descarta findings brutos do staging anterior
22
24
  --full Força review completo (ignora o modo incremental)
23
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
24
28
  --fail-on-blocking Exit 1 se houver bloqueantes (com --finalize)
25
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
+
26
36
  Flags de findings:
27
37
  --decision <d> pending | execute | ignore
28
38
  --exec <e> pending | in_progress | done | stale
@@ -40,8 +50,15 @@ Flags de findings:
40
50
  --fail-on-persisting (verify) Exit 1 se algum blocking persistir
41
51
  --include-sent (submit) Reenvia findings já enviados à PR
42
52
  --dry-run (submit) Só a prévia do envio, sem postar
53
+ --no-ping (doctor) Pula a validação de rede das credenciais
43
54
 
44
55
  Persistência:
45
56
  Runs, findings e artefatos ficam no storage global (SQLite em
46
57
  <HOUNDSMAN_HOME>/houndsman.db). Use storage.provider: "file" na config para
47
- 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
@@ -15,6 +15,7 @@
15
15
  * --finalize Passo 3 — consolidar os findings em review.md
16
16
  * context Passo 0 — coletar o contexto da PR via `gh`
17
17
  * findings <sub> Triagem/correção dos findings (list/set/done/sync/batch/verify/submit/...)
18
+ * status [ping] Mostra o status da run; `ping` renova o heartbeat da execução
18
19
  * server Sobe a UI local de triagem (Bun.serve)
19
20
  * setup Cria a configuração inicial do projeto (config + skills)
20
21
  * update Sincroniza os arquivos auto-gerados (.houndsman/core)
@@ -23,21 +24,24 @@
23
24
  * watchdog <id> Contexto de um watchdog (`--rules`, `--json`)
24
25
  *
25
26
  * Flags: `--base <b>` · `--head <ref>` · `--branch <b>` · `--config <path>` ·
26
- * `--clean` · `--full` · `--no-jev` · `--fail-on-blocking`.
27
+ * `--clean` · `--full` · `--no-jev` · `--no-ast` · `--fail-on-blocking`.
27
28
  */
28
29
 
29
30
  import { loadConfig } from "../core/config-loader";
30
31
  import { HoundsmanEngine } from "../core/engine/houndsman-engine";
32
+ import { logger } from "../core/logging";
31
33
  import { ArgParser } from "./arg-parser";
32
34
  import type { CliContext } from "./commands/command";
33
35
  import { ContextCommand } from "./commands/context-command";
34
36
  import { DoctorCommand } from "./commands/doctor-command";
35
37
  import { FindingsCommand } from "./commands/findings-command";
36
38
  import { HelpCommand } from "./commands/help-command";
39
+ import { HuntCommand } from "./commands/hunt-command";
37
40
  import { CommandRegistry } from "./commands/registry";
38
41
  import { ReviewCommand } from "./commands/review-command";
39
42
  import { ServerCommand } from "./commands/server-command";
40
43
  import { SetupCommand, TestCommand, UpdateCommand } from "./commands/setup-command";
44
+ import { StatusCommand } from "./commands/status-command";
41
45
  import { WatchdogCommand } from "./commands/watchdog-command";
42
46
  import { HELP } from "./help";
43
47
 
@@ -51,8 +55,10 @@ function buildRegistry(): CommandRegistry {
51
55
  return new CommandRegistry()
52
56
  .register(new ReviewCommand(), { fallback: true })
53
57
  .register(new HelpCommand())
58
+ .register(new HuntCommand())
54
59
  .register(new ContextCommand())
55
60
  .register(new FindingsCommand())
61
+ .register(new StatusCommand())
56
62
  .register(new WatchdogCommand())
57
63
  .register(new ServerCommand())
58
64
  .register(new SetupCommand())
@@ -74,15 +80,16 @@ export async function main(): Promise<void> {
74
80
  const command = registry.resolve(commandName);
75
81
 
76
82
  if (!command) {
77
- console.error(`Subcomando desconhecido: '${commandName}'.\n`);
78
- console.error(HELP);
83
+ logger.error(`Subcomando desconhecido: '${commandName}'.\n`);
84
+ logger.info(HELP);
79
85
  process.exit(1);
80
86
  }
81
87
 
82
88
  const context: CliContext = {
83
89
  cwd: process.cwd(),
84
- out: (message) => console.log(message),
85
- err: (message) => console.error(message),
90
+ logger,
91
+ out: (message) => logger.info(message),
92
+ err: (message) => logger.error(message),
86
93
  engine: async () => {
87
94
  const { config } = await loadConfig({
88
95
  configPath: parser.value("--config"),
@@ -97,7 +104,7 @@ export async function main(): Promise<void> {
97
104
 
98
105
  if (import.meta.main) {
99
106
  await main().catch((error) => {
100
- console.error(error instanceof Error ? error.message : String(error));
107
+ logger.error(error instanceof Error ? error.message : String(error));
101
108
  process.exit(1);
102
109
  });
103
110
  }