@payconductor/houndsman 0.11.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 (140) hide show
  1. package/README.md +14 -13
  2. package/package.json +1 -1
  3. package/src/cli/commands/doctor-command.ts +22 -0
  4. package/src/cli/commands/hunt-command.ts +440 -29
  5. package/src/cli/commands/review-command.ts +20 -2
  6. package/src/cli/help.ts +16 -8
  7. package/src/cli/index.ts +15 -2
  8. package/src/cli/progress.ts +45 -0
  9. package/src/core/ai/ai-client.ts +3 -7
  10. package/src/core/ai/ai-sdk-model-provider.ts +43 -24
  11. package/src/core/ai/features/brief-enricher.ts +0 -1
  12. package/src/core/ai/features/classifier.ts +0 -1
  13. package/src/core/ai/features/context-summarizer.ts +0 -1
  14. package/src/core/ai/features/pre-analyzer.ts +0 -1
  15. package/src/core/ai/features/technical-summarizer.ts +0 -1
  16. package/src/core/ai/features/verifier.ts +0 -1
  17. package/src/core/ai/index.ts +6 -4
  18. package/src/core/ai/prompts/pre-analysis.ts +3 -2
  19. package/src/core/ai/schemas.ts +1 -23
  20. package/src/core/ai/types.ts +4 -15
  21. package/src/core/ai/usage-budget.ts +1 -1
  22. package/src/core/ast/diff.ts +37 -2
  23. package/src/core/ast/service.ts +1 -1
  24. package/src/core/brief.ts +54 -5
  25. package/src/core/changed-scope.ts +207 -0
  26. package/src/core/commands/scaffold.ts +12 -18
  27. package/src/core/config.ts +30 -15
  28. package/src/core/defaults.ts +14 -5
  29. package/src/core/engine/review-scope.ts +131 -17
  30. package/src/core/engine/review-session.ts +7 -3
  31. package/src/core/engine/review-workspace.ts +14 -0
  32. package/src/core/env.ts +14 -0
  33. package/src/core/hunt/hunt-runner.ts +328 -0
  34. package/src/core/hunt/index.ts +59 -0
  35. package/src/core/hunt/maki-install.ts +331 -0
  36. package/src/core/hunt/maki-model.ts +101 -0
  37. package/src/core/hunt/maki-prompt.ts +71 -0
  38. package/src/core/hunt/maki-release.ts +104 -0
  39. package/src/core/hunt/maki-runner.ts +276 -0
  40. package/src/core/hunt/pool.ts +30 -0
  41. package/src/core/jev/jev-detector.ts +7 -2
  42. package/src/core/logging/index.ts +3 -1
  43. package/src/core/logging/logger.ts +22 -3
  44. package/src/core/logging/progress.ts +282 -0
  45. package/src/core/orchestrator.ts +173 -59
  46. package/src/core/project-config.ts +34 -24
  47. package/src/core/registry-check.ts +2 -12
  48. package/src/core/rule-text.ts +3 -55
  49. package/src/core/services/git-repository.ts +8 -0
  50. package/src/core/storage/types.ts +1 -1
  51. package/src/core/types.ts +3 -14
  52. package/src/index.ts +74 -3
  53. package/src/server/client/utils/constants.ts +3 -6
  54. package/src/server/client/utils/types.ts +0 -1
  55. package/src/skill/OUTPUT-FORMAT.md +5 -0
  56. package/src/skill/README.md +14 -1
  57. package/src/skill/SETUP.md +22 -2
  58. package/src/skill/SKILL.md +4 -0
  59. package/src/skill/fix.md +1 -1
  60. package/src/skill/hunt.md +38 -6
  61. package/src/tests/__snapshots__/baseline.test.ts.snap +69 -32
  62. package/src/tests/ai-core.test.ts +38 -1
  63. package/src/tests/changed-scope.test.ts +197 -0
  64. package/src/tests/cli.test.ts +1 -0
  65. package/src/tests/fallback.test.ts +1 -96
  66. package/src/tests/fixtures/fake-model-provider.ts +0 -1
  67. package/src/tests/hunt-cli.test.ts +185 -0
  68. package/src/tests/hunt-install.test.ts +153 -0
  69. package/src/tests/hunt-maki.test.ts +467 -0
  70. package/src/tests/progress.test.ts +117 -0
  71. package/src/tests/review-scope.test.ts +95 -0
  72. package/src/tests/worktree.test.ts +1 -3
  73. package/src/harness/README.md +0 -15
  74. package/src/harness/agent/agent-context.ts +0 -33
  75. package/src/harness/agent/agent-errors.ts +0 -30
  76. package/src/harness/agent/agent-factory.ts +0 -46
  77. package/src/harness/agent/agent-reporter.ts +0 -34
  78. package/src/harness/agent/agent-result.ts +0 -16
  79. package/src/harness/agent/agent-runner.ts +0 -32
  80. package/src/harness/agent/agent-scheduler.ts +0 -37
  81. package/src/harness/agent/review-agent.ts +0 -401
  82. package/src/harness/config/harness-config.ts +0 -89
  83. package/src/harness/hunt/hunt-harness.ts +0 -83
  84. package/src/harness/hunt/hunt-options.ts +0 -92
  85. package/src/harness/hunt/hunt-result.ts +0 -17
  86. package/src/harness/hunt/hunt-session.ts +0 -359
  87. package/src/harness/hunt/review-planner.ts +0 -98
  88. package/src/harness/index.ts +0 -36
  89. package/src/harness/investigation/agent-state.ts +0 -134
  90. package/src/harness/investigation/evidence-verifier.ts +0 -129
  91. package/src/harness/investigation/evidence.ts +0 -110
  92. package/src/harness/investigation/finding-critic.ts +0 -96
  93. package/src/harness/investigation/investigation-planner.ts +0 -118
  94. package/src/harness/investigation/investigation-task.ts +0 -10
  95. package/src/harness/investigation/review-journal.ts +0 -87
  96. package/src/harness/investigation/rule-analyzer.ts +0 -178
  97. package/src/harness/investigation/rule-signals.ts +0 -110
  98. package/src/harness/prompts/agent-schemas.ts +0 -57
  99. package/src/harness/prompts/reviewer-prompt-builder.ts +0 -101
  100. package/src/harness/prompts/states/conclude-prompt.ts +0 -16
  101. package/src/harness/prompts/states/investigate-prompt.ts +0 -37
  102. package/src/harness/prompts/states/orient-prompt.ts +0 -15
  103. package/src/harness/prompts/states/verify-prompt.ts +0 -22
  104. package/src/harness/tools/agent-tool.ts +0 -102
  105. package/src/harness/tools/builtin/changed-symbols-tool.ts +0 -48
  106. package/src/harness/tools/builtin/doc-context-tool.ts +0 -74
  107. package/src/harness/tools/builtin/file-outline-tool.ts +0 -66
  108. package/src/harness/tools/builtin/find-references-tool.ts +0 -66
  109. package/src/harness/tools/builtin/find-symbol-tool.ts +0 -91
  110. package/src/harness/tools/builtin/helpers.ts +0 -115
  111. package/src/harness/tools/builtin/impact-tool.ts +0 -48
  112. package/src/harness/tools/builtin/imports-exports-tool.ts +0 -53
  113. package/src/harness/tools/builtin/index.ts +0 -59
  114. package/src/harness/tools/builtin/list-files-tool.ts +0 -85
  115. package/src/harness/tools/builtin/pr-context-tool.ts +0 -41
  116. package/src/harness/tools/builtin/read-file-range-tool.ts +0 -73
  117. package/src/harness/tools/builtin/rule-signals-tool.ts +0 -36
  118. package/src/harness/tools/builtin/search-code-tool.ts +0 -65
  119. package/src/harness/tools/builtin/symbol-context-tool.ts +0 -106
  120. package/src/harness/tools/builtin/tests-for-symbol-tool.ts +0 -61
  121. package/src/harness/tools/path-guard.ts +0 -90
  122. package/src/harness/tools/result-limiter.ts +0 -49
  123. package/src/harness/tools/secret-redactor.ts +0 -54
  124. package/src/harness/tools/tool-cache.ts +0 -50
  125. package/src/harness/tools/tool-call-budget.ts +0 -60
  126. package/src/harness/tools/tool-context.ts +0 -5
  127. package/src/harness/tools/tool-middleware.ts +0 -105
  128. package/src/harness/tools/tool-policy.ts +0 -51
  129. package/src/harness/tools/tool-registry.ts +0 -112
  130. package/src/harness/tools/tool-result.ts +0 -15
  131. package/src/harness/tools/toolbox.ts +0 -36
  132. package/src/harness/validation/finding-validator.ts +0 -119
  133. package/src/tests/fixtures/fake-agent-tools.ts +0 -69
  134. package/src/tests/fixtures/fake-investigation.ts +0 -64
  135. package/src/tests/harness-agent.test.ts +0 -141
  136. package/src/tests/harness-investigation.test.ts +0 -206
  137. package/src/tests/harness-tools.test.ts +0 -148
  138. package/src/tests/hunt-command.test.ts +0 -212
  139. package/src/tests/rule-analyzer.test.ts +0 -61
  140. package/src/tests/tool-contracts.test.ts +0 -82
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) — IA própria do `hunt` nativo e das features de IA.
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
 
@@ -74,7 +76,7 @@ bun run houndsman doctor
74
76
 
75
77
  ### 6. Rode a primeira revisão
76
78
 
77
- No seu harness (Claude Code / agents), invoque o skill:
79
+ No seu agente (Claude Code / agents), invoque o skill:
78
80
 
79
81
  ```
80
82
  /houndsman hunt
@@ -84,28 +86,27 @@ 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
 
87
- ### 7. Revisão nativa (opcional) — `houndsman hunt`
89
+ #### Alternativa: rodar 100% pelo CLI (opcional)
88
90
 
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
+ 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:
91
93
 
92
94
  ```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
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
97
98
  ```
98
99
 
99
- Detalhes (catálogo de tools, máquina de estados, custos e segurança): [`docs/HARNESS.md`](docs/HARNESS.md).
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.
100
102
 
101
- ### 8. Várias PRs ao mesmo tempo (worktrees)
103
+ ### 7. Várias PRs ao mesmo tempo (worktrees)
102
104
 
103
105
  Cada revisão pode rodar em um **worktree dedicado** (detached no commit da PR), sem
104
106
  checkout no clone principal e sem bloquear a revisão das outras PRs:
105
107
 
106
108
  ```bash
107
109
  houndsman --pr 123 # cria/reusa o worktree e prepara os briefs
108
- houndsman hunt --pr 123 # revisão nativa no mesmo worktree
109
110
  houndsman --pr 123 --finalize # consolida o relatório
110
111
  houndsman worktree list # worktrees ativos
111
112
  houndsman worktree remove feature/x
@@ -153,7 +154,7 @@ Você decide o que fazer com cada finding pela **UI local**, informando se quer
153
154
  bun run houndsman server --open # UI: triar, comentar, marcar feito, ler artefatos
154
155
  ```
155
156
 
156
- > A partir da 2ª revisão o `hunt` é **incremental**: revisa apenas os arquivos que mudaram desde
157
+ > A partir da 2ª revisão o prepare é **incremental**: revisa apenas os arquivos que mudaram desde
157
158
  > a última revisão. `--full` força o review completo.
158
159
 
159
160
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@payconductor/houndsman",
3
- "version": "0.11.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(
@@ -1,51 +1,462 @@
1
- /** Comando `hunt` — revisão nativa com IA (harness próprio). */
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
+ */
2
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";
3
13
  import { resolveReviewWorkspace } from "../../core/engine/review-workspace";
4
- import { HuntHarness } from "../../harness";
5
- import { parseHuntArgs } from "../../harness/hunt/hunt-options";
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";
6
35
  import type { CliContext, Command } from "./command";
7
36
 
8
- /** Executa o harness nativo. */
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. */
9
70
  export class HuntCommand implements Command {
10
71
  readonly name = "hunt";
11
72
 
12
73
  async run(args: string[], context: CliContext): Promise<number> {
13
- const options = parseHuntArgs(args);
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
+
14
115
  const config = await context.config();
15
- const workspace = resolveReviewWorkspace(
16
- {
17
- cwd: context.cwd,
18
- base: options.base,
19
- head: options.head,
20
- branch: options.branch,
21
- pr: options.pr,
22
- worktree: options.worktree,
23
- },
24
- { worktreesLocation: config.worktrees?.location },
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,
25
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
+ }
26
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
+ );
27
165
 
166
+ const engineStarted = Date.now();
167
+ context.logger.step("Abrindo o storage da revisão…");
28
168
  const engine = await context.engine({
29
169
  root: workspace.root,
30
170
  mainRoot: workspace.mainRoot,
31
171
  });
32
- const resolved = {
33
- ...options,
34
- branch: workspace.branch,
35
- base: workspace.base,
36
- head: workspace.head,
37
- pr: workspace.prNumber,
38
- worktreePath: workspace.worktreePath,
39
- };
172
+ stepDone(context.logger, "Storage pronto", engineStarted);
40
173
  try {
41
- const harness = await HuntHarness.from(engine, resolved, context.logger);
42
- const result = await harness.run();
43
- for (const message of result.messages) context.logger.info(message);
44
- if (result.status === "failed") return 1;
45
- if (options.failOnBlocking && result.blockers > 0) return 1;
46
- return 0;
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;
47
313
  } finally {
48
314
  engine.close();
49
315
  }
50
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;
51
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