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