@payconductor/houndsman 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +17 -1
  2. package/package.json +1 -1
  3. package/src/cli/commands/doctor-command.ts +22 -0
  4. package/src/cli/commands/hunt-command.ts +462 -0
  5. package/src/cli/commands/review-command.ts +20 -2
  6. package/src/cli/help.ts +17 -2
  7. package/src/cli/index.ts +17 -2
  8. package/src/cli/progress.ts +45 -0
  9. package/src/core/ai/ai-sdk-model-provider.ts +39 -0
  10. package/src/core/ai/index.ts +6 -1
  11. package/src/core/ai/types.ts +2 -0
  12. package/src/core/commands/scaffold.ts +11 -0
  13. package/src/core/config.ts +28 -0
  14. package/src/core/defaults.ts +15 -0
  15. package/src/core/engine/review-scope.ts +92 -25
  16. package/src/core/engine/review-session.ts +7 -3
  17. package/src/core/engine/review-workspace.ts +14 -0
  18. package/src/core/env.ts +14 -0
  19. package/src/core/hunt/hunt-runner.ts +328 -0
  20. package/src/core/hunt/index.ts +59 -0
  21. package/src/core/hunt/maki-install.ts +331 -0
  22. package/src/core/hunt/maki-model.ts +101 -0
  23. package/src/core/hunt/maki-prompt.ts +71 -0
  24. package/src/core/hunt/maki-release.ts +104 -0
  25. package/src/core/hunt/maki-runner.ts +276 -0
  26. package/src/core/hunt/pool.ts +30 -0
  27. package/src/core/logging/index.ts +2 -0
  28. package/src/core/logging/logger.ts +19 -0
  29. package/src/core/logging/progress.ts +282 -0
  30. package/src/core/orchestrator.ts +158 -49
  31. package/src/core/project-config.ts +38 -0
  32. package/src/core/storage/types.ts +1 -0
  33. package/src/index.ts +49 -0
  34. package/src/skill/README.md +12 -0
  35. package/src/skill/SETUP.md +21 -1
  36. package/src/skill/SKILL.md +4 -0
  37. package/src/skill/hunt.md +36 -0
  38. package/src/tests/__snapshots__/baseline.test.ts.snap +22 -3
  39. package/src/tests/ai-core.test.ts +38 -1
  40. package/src/tests/cli.test.ts +1 -0
  41. package/src/tests/hunt-cli.test.ts +185 -0
  42. package/src/tests/hunt-install.test.ts +153 -0
  43. package/src/tests/hunt-maki.test.ts +467 -0
  44. package/src/tests/progress.test.ts +117 -0
  45. package/src/tests/review-scope.test.ts +62 -0
package/README.md CHANGED
@@ -28,9 +28,11 @@ diferente das PRs. O motor só coordena a caçada — as regras são problema se
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) — features de IA (briefs, contexto, classificação, resumo técnico).
31
+ - **`HOUNDSMAN_DEEP_SEEK_API_KEY`** (opcional) — features de IA (briefs, contexto, classificação, resumo técnico) e harness local `hunt`.
32
32
  - **`HOUNDSMAN_OPENAI_API_KEY`** / **`HOUNDSMAN_ANTHROPIC_API_KEY`** (opcional) — apenas se
33
33
  `ai.provider` for `openai`/`anthropic`.
34
+ - **maki** (opcional) — só para o harness local `houndsman hunt`; a skill `/houndsman hunt`
35
+ não precisa dele. Instale com `houndsman hunt install`.
34
36
 
35
37
  > Coloque as chaves em um `.env` na raiz do projeto; o Bun carrega automaticamente.
36
38
 
@@ -84,6 +86,20 @@ O skill orquestra tudo e persiste o relatório no storage global (SQLite em
84
86
  `<HOUNDSMAN_HOME>/houndsman.db`); a UI lê os artefatos direto do storage. Não precisa rodar
85
87
  nenhum script à mão.
86
88
 
89
+ #### Alternativa: rodar 100% pelo CLI (opcional)
90
+
91
+ Quem não quer usar um agente externo pode executar a mesma revisão pelo terminal, com o
92
+ maki como harness local. A instalação é única e fica fora do projeto:
93
+
94
+ ```bash
95
+ houndsman hunt install # baixa o maki fixado em <HOUNDSMAN_HOME>/tools/maki
96
+ houndsman hunt doctor # diagnóstico (não falha se o maki não existir)
97
+ houndsman hunt --pr 456 # prepare → maki (1 processo por brief) → finalize
98
+ ```
99
+
100
+ Os dois caminhos produzem os mesmos artefatos e usam o mesmo staging. A skill
101
+ `/houndsman hunt` continua sendo o caminho canônico para agentes externos.
102
+
87
103
  ### 7. Várias PRs ao mesmo tempo (worktrees)
88
104
 
89
105
  Cada revisão pode rodar em um **worktree dedicado** (detached no commit da PR), sem
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@payconductor/houndsman",
3
- "version": "0.12.0",
3
+ "version": "0.13.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",
@@ -1,9 +1,11 @@
1
1
  /** Comando `doctor` (alias `check`) — valida config, ambiente e credenciais. */
2
2
 
3
+ import { existsSync } from "node:fs";
3
4
  import type { AiClient } from "../../core/ai/ai-client";
4
5
  import { ArgParser } from "../../core/arg-parser";
5
6
  import type { AiProviderKind } from "../../core/config";
6
7
  import { ENV_NAMES, env } from "../../core/env";
8
+ import { displayPath } from "../../core/paths";
7
9
  import { checkRegistry } from "../../core/registry-check";
8
10
  import { storageLocation } from "../../core/storage/paths";
9
11
  import type { CliContext, Command } from "./command";
@@ -66,6 +68,26 @@ export class DoctorCommand implements Command {
66
68
  const ast = await engine.ast.check();
67
69
  logger.bullet(`${ast.ok ? "✅" : "⚠️ "} AST: ${ast.detail}`);
68
70
 
71
+ // Harness local é opcional: a ausência do maki nunca falha o doctor.
72
+ const { makiSourceLabel, probeMakiVersion, resolveMakiBinary } = await import(
73
+ "../../core/hunt/maki-install"
74
+ );
75
+ const maki = resolveMakiBinary(engine.config.hunt?.binaryPath);
76
+ if (!maki) {
77
+ logger.bullet(
78
+ "⚠️ maki (hunt local): não instalado (opcional — `houndsman hunt install`).",
79
+ );
80
+ } else {
81
+ const exists = existsSync(maki.path);
82
+ const version = exists
83
+ ? (maki.version ?? probeMakiVersion(maki.path))
84
+ : undefined;
85
+ logger.bullet(
86
+ `${exists ? "✅" : "❌"} maki (hunt local): ${displayPath(maki.path)} ` +
87
+ `(${makiSourceLabel(maki.source)}${version ? `, v${version}` : ""}).`,
88
+ );
89
+ }
90
+
69
91
  const provider = engine.ai.projectConfig.aiProvider;
70
92
  const key = keyFor(provider);
71
93
  logger.bullet(
@@ -0,0 +1,462 @@
1
+ /**
2
+ * Comando `hunt` — harness local (maki) da capability de revisão.
3
+ *
4
+ * A skill `/houndsman hunt` continua sendo o caminho canônico para agentes
5
+ * externos. Este comando é o caminho 100% CLI: usa o mesmo prepare/finalize e
6
+ * despacha cada brief num processo maki headless.
7
+ */
8
+
9
+ import { existsSync } from "node:fs";
10
+ import { runContextCommand } from "../../core/commands/context";
11
+ import { parseReviewArgs } from "../../core/commands/review";
12
+ import type { HoundsmanConfig } from "../../core/config";
13
+ import { resolveReviewWorkspace } from "../../core/engine/review-workspace";
14
+ import { type HuntEvent, HuntRunner } from "../../core/hunt/hunt-runner";
15
+ import {
16
+ installMaki,
17
+ makiSourceLabel,
18
+ probeMakiVersion,
19
+ requireMakiBinary,
20
+ resolveMakiBinary,
21
+ uninstallMaki,
22
+ } from "../../core/hunt/maki-install";
23
+ import {
24
+ houndsmanKeyEnvFor,
25
+ requireMakiModel,
26
+ resolveMakiModel,
27
+ } from "../../core/hunt/maki-model";
28
+ import { SpawnMakiRunner } from "../../core/hunt/maki-runner";
29
+ import { formatDuration, ProgressBoard } from "../../core/logging";
30
+ import { displayPath } from "../../core/paths";
31
+ import { ProjectConfig } from "../../core/project-config";
32
+ import { touchRunStatus } from "../../core/run-status";
33
+ import { ArgParser } from "../arg-parser";
34
+ import { prepareProgress, stepDone } from "../progress";
35
+ import type { CliContext, Command } from "./command";
36
+
37
+ /** Texto de ajuda do subcomando. */
38
+ const HUNT_HELP = `Uso: houndsman hunt [--pr <n> | --branch <b> | --worktree <path>] [flags]
39
+
40
+ Harness local (maki) da capability de revisão. A skill \`/houndsman hunt\` continua
41
+ existindo: use-a quando a revisão for orquestrada por um agente externo
42
+ (Copilot/Claude). O comando abaixo faz prepare → watchdogs via maki → finalize.
43
+
44
+ Subcomandos:
45
+ install Baixa o maki fixado para <HOUNDSMAN_HOME>/tools/maki
46
+ doctor Diagnóstico do harness (binário, modelo, credencial)
47
+ uninstall Remove o binário gerenciado
48
+
49
+ Flags:
50
+ --pr <n> Revisa a PR em worktree dedicado
51
+ --branch <b> Branch alvo
52
+ --worktree <path> Worktree já existente
53
+ --allow-partial Finaliza mesmo se algum watchdog falhar (findings vazios)
54
+ --no-context Não coleta o contexto da PR via gh
55
+ --hunt-model <m> Modelo do maki (provider/model-id)
56
+ --hunt-concurrency Processos maki simultâneos
57
+ --hunt-timeout Timeout por watchdog (ms)
58
+ --no-retry Não repete saída inválida
59
+ --fail-on-blocking Exit 1 se houver bloqueantes
60
+
61
+ Instalação:
62
+ houndsman hunt install [--version vX.Y.Z] [--force] [--from <arquivo>]
63
+
64
+ Ambiente:
65
+ HOUNDSMAN_MAKI_PATH Binário do maki (vence a instalação gerenciada)
66
+ HOUNDSMAN_MAKI_VERSION Versão a instalar (default: a fixada no pacote)
67
+ HOUNDSMAN_DEEP_SEEK_API_KEY Credencial reaproveitada pelo maki (provider deepseek)`;
68
+
69
+ /** Um subcomando do CLI. */
70
+ export class HuntCommand implements Command {
71
+ readonly name = "hunt";
72
+
73
+ async run(args: string[], context: CliContext): Promise<number> {
74
+ if (args.includes("--help") || args.includes("-h")) {
75
+ context.out(HUNT_HELP);
76
+ return 0;
77
+ }
78
+ const first = args[0];
79
+ const sub = first && !first.startsWith("-") ? first : undefined;
80
+ const rest = sub ? args.slice(1) : args;
81
+ switch (sub) {
82
+ case undefined:
83
+ return this.runHunt(rest, context);
84
+ case "install":
85
+ return this.install(rest, context);
86
+ case "uninstall":
87
+ return this.uninstall(context);
88
+ case "doctor":
89
+ return this.doctor(context);
90
+ default:
91
+ context.err(`Subcomando desconhecido de 'hunt': '${sub}'.`);
92
+ context.out(HUNT_HELP);
93
+ return 1;
94
+ }
95
+ }
96
+
97
+ /** Fluxo completo: prepare → maki → finalize. */
98
+ private async runHunt(args: string[], context: CliContext): Promise<number> {
99
+ const parser = ArgParser.from(args);
100
+ if (parser.has("--finalize")) {
101
+ context.err(
102
+ "`hunt` já finaliza a revisão; use `houndsman --finalize` isolado.",
103
+ );
104
+ return 1;
105
+ }
106
+
107
+ let options: ReturnType<typeof parseReviewArgs>;
108
+ try {
109
+ options = parseReviewArgs(args);
110
+ } catch (error) {
111
+ context.err(messageOf(error));
112
+ return 1;
113
+ }
114
+
115
+ const config = await context.config();
116
+ if (config.hunt?.enabled === false) {
117
+ context.err("Harness local desligado (`hunt.enabled: false`).");
118
+ return 1;
119
+ }
120
+
121
+ const project = new ProjectConfig(config);
122
+ const harnessStarted = Date.now();
123
+ context.logger.step("Verificando o binário do maki e o modelo do harness…");
124
+ let model: ReturnType<typeof requireMakiModel>;
125
+ let binary: ReturnType<typeof requireMakiBinary>;
126
+ try {
127
+ model = requireMakiModel(project, parser.value("--hunt-model"));
128
+ binary = requireMakiBinary(config.hunt?.binaryPath);
129
+ } catch (error) {
130
+ context.err(messageOf(error));
131
+ return 1;
132
+ }
133
+ stepDone(
134
+ context.logger,
135
+ `Harness pronto: ${model.spec} · ${displayPath(binary.path)}`,
136
+ harnessStarted,
137
+ );
138
+ let workspace: ReturnType<typeof resolveReviewWorkspace>;
139
+ const workspaceStarted = Date.now();
140
+ try {
141
+ workspace = resolveReviewWorkspace(
142
+ {
143
+ cwd: context.cwd,
144
+ base: options.base,
145
+ head: options.head,
146
+ branch: options.branch,
147
+ pr: options.pr,
148
+ worktree: options.worktree,
149
+ },
150
+ {
151
+ worktreesLocation: config.worktrees?.location,
152
+ onProgress: (message) => context.logger.step(message),
153
+ },
154
+ );
155
+ } catch (error) {
156
+ context.err(messageOf(error));
157
+ return 1;
158
+ }
159
+ for (const message of workspace.messages) context.logger.info(message);
160
+ stepDone(
161
+ context.logger,
162
+ `Workspace: ${workspace.branch} em ${displayPath(workspace.root)}`,
163
+ workspaceStarted,
164
+ );
165
+
166
+ const engineStarted = Date.now();
167
+ context.logger.step("Abrindo o storage da revisão…");
168
+ const engine = await context.engine({
169
+ root: workspace.root,
170
+ mainRoot: workspace.mainRoot,
171
+ });
172
+ stepDone(context.logger, "Storage pronto", engineStarted);
173
+ try {
174
+ if (options.pr !== undefined && !parser.has("--no-context")) {
175
+ context.logger.step("Coletando o contexto da PR via gh…");
176
+ try {
177
+ const collected = runContextCommand(
178
+ engine.config,
179
+ engine.storage,
180
+ engine.repo,
181
+ {
182
+ branch: workspace.branch,
183
+ worktreePath: workspace.worktreePath,
184
+ prNumber: workspace.prNumber,
185
+ },
186
+ );
187
+ context.logger.bullet(
188
+ collected.context.available
189
+ ? "Contexto da PR coletado via gh."
190
+ : `Contexto da PR indisponível (${collected.context.reason}).`,
191
+ );
192
+ } catch (error) {
193
+ context.logger.warn(
194
+ `Contexto da PR indisponível: ${messageOf(error)}`,
195
+ );
196
+ }
197
+ }
198
+
199
+ const session = engine.session({
200
+ ...options,
201
+ branch: workspace.branch,
202
+ base: workspace.base,
203
+ head: workspace.head,
204
+ pr: workspace.prNumber,
205
+ worktreePath: workspace.worktreePath,
206
+ });
207
+ const prepared = await session.prepare(prepareProgress(context.logger));
208
+ for (const message of prepared.messages) {
209
+ // O CLI despacha os subagentes; os próximos passos da skill não se aplicam.
210
+ if (SKILL_NEXT_STEPS.test(message)) continue;
211
+ context.logger.info(message);
212
+ }
213
+ if (prepared.activeIds.length === 0) return 0;
214
+
215
+ const concurrency =
216
+ positiveInt(parser.value("--hunt-concurrency")) ??
217
+ project.huntConcurrency;
218
+ const timeoutMs =
219
+ positiveInt(parser.value("--hunt-timeout")) ?? project.huntTimeoutMs;
220
+ const retries = parser.has("--no-retry") ? 0 : project.huntRetries;
221
+
222
+ context.logger.step(
223
+ `Harness local: ${prepared.activeIds.length} watchdog(s) ` +
224
+ `(${model.spec}, concorrência ${concurrency}).`,
225
+ );
226
+ const board = new ProgressBoard({ logger: context.logger });
227
+ board.start(prepared.activeIds.length);
228
+ const runner = new HuntRunner({
229
+ work: session.context.work,
230
+ root: session.context.root,
231
+ projectName: config.projectName,
232
+ model: model.spec,
233
+ apiKeyEnv: model.apiKeyEnv,
234
+ apiKey: model.apiKey,
235
+ concurrency,
236
+ timeoutMs,
237
+ retries,
238
+ runner: new SpawnMakiRunner(binary.path),
239
+ onEvent: (event) => this.logEvent(board, event),
240
+ });
241
+ // Runs longas não podem parecer “timeout” na UI: a skill pinga antes de
242
+ // cada onda; o CLI renova o heartbeat enquanto o maki trabalha.
243
+ const heartbeat = setInterval(() => {
244
+ try {
245
+ touchRunStatus(engine.storage, session.context.review.id);
246
+ } catch {
247
+ // Heartbeat é diagnóstico: nunca derruba a revisão.
248
+ }
249
+ }, 120_000);
250
+ let report: Awaited<ReturnType<typeof runner.run>>;
251
+ try {
252
+ report = await runner.run(prepared.activeIds);
253
+ } finally {
254
+ clearInterval(heartbeat);
255
+ board.stop();
256
+ }
257
+ engine.storage.writeArtifact(session.context.review.id, {
258
+ name: "hunt-report.json",
259
+ kind: "hunt",
260
+ content: `${JSON.stringify(report, null, 2)}\n`,
261
+ });
262
+
263
+ context.logger.step(
264
+ `Harness: ${report.totals.done}/${report.totals.watchdogs} watchdog(s), ` +
265
+ `${report.totals.findings} finding(s), $${report.totals.costUsd.toFixed(4)}, ` +
266
+ `${Math.round(report.durationMs / 1000)}s.`,
267
+ );
268
+
269
+ const failed = report.watchdogs.filter(
270
+ (watchdog) => watchdog.status === "failed",
271
+ );
272
+ if (failed.length > 0 && !parser.has("--allow-partial")) {
273
+ context.logger.error(
274
+ `Harness falhou em ${failed.length}/${report.watchdogs.length} watchdog(s): ` +
275
+ failed.map((watchdog) => watchdog.watchdogId).join(", "),
276
+ );
277
+ context.logger.info(
278
+ `Staging preservado em ${displayPath(session.context.work.root)}.`,
279
+ );
280
+ context.logger.info(
281
+ "Complete os briefs e rode `houndsman --finalize`, ou repita com `--allow-partial`.",
282
+ );
283
+ return 1;
284
+ }
285
+ if (failed.length > 0) {
286
+ for (const watchdog of failed) {
287
+ session.context.work.writeFinding(
288
+ watchdog.watchdogId,
289
+ `${JSON.stringify(
290
+ {
291
+ watchdogId: watchdog.watchdogId,
292
+ reviewedFiles: [],
293
+ skipped: [`hunt: ${watchdog.error ?? "falhou"}`],
294
+ findings: [],
295
+ },
296
+ null,
297
+ 2,
298
+ )}\n`,
299
+ );
300
+ }
301
+ context.logger.warn(
302
+ `${failed.length} watchdog(s) sem findings (registrados em \`skipped\`).`,
303
+ );
304
+ }
305
+
306
+ context.logger.step("Consolidando os findings dos watchdogs…");
307
+ const finalized = session.finalize();
308
+ for (const message of finalized.messages) context.logger.info(message);
309
+ context.logger.success(
310
+ `Revisão finalizada: ${displayPath(finalized.reportPath)}`,
311
+ );
312
+ return options.failOnBlocking && finalized.blockers > 0 ? 1 : 0;
313
+ } finally {
314
+ engine.close();
315
+ }
316
+ }
317
+
318
+ /** Instala/atualiza o binário gerenciado. */
319
+ private async install(args: string[], context: CliContext): Promise<number> {
320
+ const parser = ArgParser.from(args);
321
+ try {
322
+ const result = await installMaki({
323
+ version: parser.value("--version"),
324
+ from: parser.value("--from"),
325
+ force: parser.has("--force"),
326
+ log: (message) => context.logger.bullet(message),
327
+ });
328
+ context.logger.success(
329
+ result.skipped
330
+ ? `maki v${result.version} já instalado em ${displayPath(result.path)}.`
331
+ : `maki v${result.version} instalado em ${displayPath(result.path)}.`,
332
+ );
333
+ return 0;
334
+ } catch (error) {
335
+ context.err(messageOf(error));
336
+ return 1;
337
+ }
338
+ }
339
+
340
+ /** Remove o binário gerenciado. */
341
+ private uninstall(context: CliContext): number {
342
+ const removed = uninstallMaki();
343
+ if (!removed) {
344
+ context.logger.info("Nada para remover: maki gerenciado não está instalado.");
345
+ return 0;
346
+ }
347
+ context.logger.success(`Removido: ${displayPath(removed)}.`);
348
+ return 0;
349
+ }
350
+
351
+ /** Diagnóstico do harness local (nunca falha por ausência do maki). */
352
+ private async doctor(context: CliContext): Promise<number> {
353
+ let config: HoundsmanConfig;
354
+ try {
355
+ config = await context.config();
356
+ } catch (error) {
357
+ const message = messageOf(error);
358
+ // Diagnóstico da capability não exige um projeto configurado, mas um
359
+ // config quebrado precisa aparecer.
360
+ if (!message.includes("Nenhuma configuração")) {
361
+ context.err(message);
362
+ return 1;
363
+ }
364
+ context.logger.warn(
365
+ "Sem houndsman.config.ts no diretório atual; usando defaults do harness.",
366
+ );
367
+ config = {
368
+ projectName: "houndsman",
369
+ watchdogIds: [],
370
+ watchdogs: {},
371
+ exclusions: [],
372
+ docMappings: [],
373
+ };
374
+ }
375
+ const project = new ProjectConfig(config);
376
+ const binary = resolveMakiBinary(config.hunt?.binaryPath);
377
+
378
+ context.logger.step(
379
+ "Harness local (`hunt`) — opcional; a skill /houndsman hunt não depende dele.",
380
+ );
381
+ if (!binary) {
382
+ context.logger.bullet(
383
+ "⚠️ maki: não instalado — rode `houndsman hunt install` (ou defina HOUNDSMAN_MAKI_PATH).",
384
+ );
385
+ } else if (!existsSync(binary.path)) {
386
+ context.logger.bullet(
387
+ `❌ maki: ${displayPath(binary.path)} (${makiSourceLabel(binary.source)}) não existe.`,
388
+ );
389
+ } else {
390
+ const version = binary.version ?? probeMakiVersion(binary.path);
391
+ context.logger.bullet(
392
+ `✅ maki: ${displayPath(binary.path)} (${makiSourceLabel(binary.source)}` +
393
+ `${version ? `, v${version}` : ""}).`,
394
+ );
395
+ }
396
+
397
+ const model = resolveMakiModel(project);
398
+ context.logger.bullet(
399
+ `${model.apiKey ? "✅" : "⚠️ "} modelo: ${model.spec} ` +
400
+ `(${model.apiKey ? "credencial presente" : `defina ${houndsmanKeyEnvFor(model.provider)}`}).`,
401
+ );
402
+ context.logger.bullet(
403
+ `harness: ${project.huntEnabled ? "ligado" : "desligado"} · ` +
404
+ `concorrência ${project.huntConcurrency} · ` +
405
+ `timeout ${Math.round(project.huntTimeoutMs / 1000)}s · ` +
406
+ `retries ${project.huntRetries}.`,
407
+ );
408
+ return 0;
409
+ }
410
+
411
+ /** Log de progresso por watchdog. */
412
+ private logEvent(board: ProgressBoard, event: HuntEvent): void {
413
+ switch (event.type) {
414
+ case "start":
415
+ board.add(event.watchdogId, "revisando");
416
+ return;
417
+ case "retry":
418
+ board.retry(
419
+ event.watchdogId,
420
+ `tentativa ${event.attempt + 1}/${event.maxAttempts}`,
421
+ `${event.watchdogId}: ${event.error} Repetindo ` +
422
+ `(tentativa ${event.attempt + 1}/${event.maxAttempts})…`,
423
+ );
424
+ return;
425
+ case "done": {
426
+ const cost =
427
+ event.metrics.costUsd !== undefined
428
+ ? ` · $${event.metrics.costUsd.toFixed(4)}`
429
+ : "";
430
+ board.succeed(
431
+ event.watchdogId,
432
+ `${event.watchdogId} — ${event.findings} finding(s), ` +
433
+ `${event.reviewedFiles} arquivo(s)${cost} ` +
434
+ `(${formatDuration(event.metrics.durationMs)})`,
435
+ );
436
+ return;
437
+ }
438
+ case "failed":
439
+ board.fail(
440
+ event.watchdogId,
441
+ `${event.watchdogId} falhou (${event.attempts} tentativa(s)): ${event.error}`,
442
+ );
443
+ return;
444
+ }
445
+ }
446
+ }
447
+
448
+ /** Últimas mensagens do prepare são instruções para o harness externo. */
449
+ const SKILL_NEXT_STEPS =
450
+ /^\s*(Próximo passo: despachar os sub-agentes|Quando os findings estiverem gravados)/;
451
+
452
+ /** Mensagem de erro legível. */
453
+ function messageOf(error: unknown): string {
454
+ return error instanceof Error ? error.message : String(error);
455
+ }
456
+
457
+ /** Inteiro positivo da flag, ou `undefined`. */
458
+ function positiveInt(value: string | undefined): number | undefined {
459
+ if (!value) return undefined;
460
+ const parsed = Number.parseInt(value, 10);
461
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
462
+ }
@@ -2,6 +2,8 @@
2
2
 
3
3
  import { parseReviewArgs } from "../../core/commands/review";
4
4
  import { resolveReviewWorkspace } from "../../core/engine/review-workspace";
5
+ import { displayPath } from "../../core/paths";
6
+ import { prepareProgress, stepDone } from "../progress";
5
7
  import type { CliContext, Command } from "./command";
6
8
 
7
9
  export class ReviewCommand implements Command {
@@ -9,7 +11,9 @@ export class ReviewCommand implements Command {
9
11
 
10
12
  async run(args: string[], context: CliContext): Promise<number> {
11
13
  const options = parseReviewArgs(args);
14
+ // O carregamento da config é logado pelo bootstrap (uma vez por processo).
12
15
  const config = await context.config();
16
+ const workspaceStarted = Date.now();
13
17
  const workspace = resolveReviewWorkspace(
14
18
  {
15
19
  cwd: context.cwd,
@@ -19,16 +23,27 @@ export class ReviewCommand implements Command {
19
23
  pr: options.pr,
20
24
  worktree: options.worktree,
21
25
  },
22
- { worktreesLocation: config.worktrees?.location },
26
+ {
27
+ worktreesLocation: config.worktrees?.location,
28
+ onProgress: (message) => context.logger.step(message),
29
+ },
23
30
  );
24
31
  for (const message of workspace.messages) context.out(message);
32
+ stepDone(
33
+ context.logger,
34
+ `Workspace: ${workspace.branch} em ${displayPath(workspace.root)}`,
35
+ workspaceStarted,
36
+ );
25
37
 
26
38
  // Sem `--pr`/`--worktree`, o engine usa o workspace gravado da review
27
39
  // (finalize após um prepare feito em worktree, por exemplo).
28
40
  const explicit = options.pr !== undefined || options.worktree !== undefined;
41
+ const engineStarted = Date.now();
42
+ context.logger.step("Abrindo o storage da revisão…");
29
43
  const engine = await context.engine(
30
44
  explicit ? { root: workspace.root, mainRoot: workspace.mainRoot } : undefined,
31
45
  );
46
+ stepDone(context.logger, "Storage pronto", engineStarted);
32
47
  const resolved = {
33
48
  ...options,
34
49
  branch: workspace.branch,
@@ -37,7 +52,10 @@ export class ReviewCommand implements Command {
37
52
  pr: workspace.prNumber,
38
53
  worktreePath: workspace.worktreePath,
39
54
  };
40
- const result = await engine.session(resolved).run();
55
+ if (options.finalize) context.logger.step("Consolidando os findings…");
56
+ const result = await engine
57
+ .session(resolved)
58
+ .run(prepareProgress(context.logger));
41
59
 
42
60
  for (const message of result.messages) context.out(message);
43
61
 
package/src/cli/help.ts CHANGED
@@ -6,6 +6,11 @@ Comandos:
6
6
  --finalize Consolida os findings em review.md
7
7
  context Coleta o contexto da PR via gh (Passo 0; --pr/--worktree)
8
8
  findings <sub> Triagem/correção dos findings (list · show · set · done · reset · sync · summary · batch · verify · submit · path)
9
+ hunt [flags] Harness local (maki): prepare → watchdogs via maki → finalize
10
+ (a skill /houndsman hunt continua existindo p/ agentes externos)
11
+ hunt install Baixa o maki fixado para <HOUNDSMAN_HOME>/tools/maki
12
+ hunt doctor Diagnóstico do harness local (opcional, não falha sem maki)
13
+ hunt uninstall Remove o binário gerenciado
9
14
  status [ping] Mostra o status da run (ping renova o heartbeat da execução)
10
15
  worktree <ação> Lista/cria/remove worktrees de revisão (list · add · remove · prune · path)
11
16
  server Sobe a UI local de triagem (--port, --host, --open, --editor)
@@ -29,6 +34,14 @@ Flags:
29
34
  --no-ai Desativa as features de IA do fluxo normal
30
35
  --fail-on-blocking Exit 1 se houver bloqueantes (com --finalize)
31
36
 
37
+ Flags de hunt (harness local):
38
+ --allow-partial Finaliza mesmo se algum watchdog maki falhar
39
+ --no-context Não coleta o contexto da PR via gh
40
+ --hunt-model <m> Modelo do maki (provider/model-id)
41
+ --hunt-concurrency Processos maki simultâneos
42
+ --hunt-timeout Timeout por watchdog (ms)
43
+ --no-retry Não repete saída inválida
44
+
32
45
  Flags de findings:
33
46
  --decision <d> pending | execute | ignore
34
47
  --exec <e> pending | in_progress | done | stale
@@ -62,6 +75,8 @@ Persistência:
62
75
 
63
76
  Ambiente (o Bun carrega .env automaticamente):
64
77
  HOUNDSMAN_TYPE_SAFE_API_KEY Pré-análise Jev (opcional)
65
- HOUNDSMAN_DEEP_SEEK_API_KEY IA (provider deepseek, opcional)
78
+ HOUNDSMAN_DEEP_SEEK_API_KEY IA (provider deepseek, opcional; reusada pelo hunt)
66
79
  HOUNDSMAN_OPENAI_API_KEY IA (provider openai, opcional)
67
- HOUNDSMAN_ANTHROPIC_API_KEY IA (provider anthropic, opcional)`;
80
+ HOUNDSMAN_ANTHROPIC_API_KEY IA (provider anthropic, opcional)
81
+ HOUNDSMAN_MAKI_PATH Binário do maki (harness local \`hunt\`)
82
+ HOUNDSMAN_MAKI_VERSION Versão do maki a instalar (default: fixada no pacote)`;