@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.
- package/README.md +23 -2
- package/package.json +1 -1
- package/src/cli/commands/compare-command.ts +112 -0
- package/src/cli/commands/context-command.ts +1 -0
- package/src/cli/commands/doctor-command.ts +22 -0
- package/src/cli/commands/findings-command.ts +11 -1
- package/src/cli/commands/hunt-command.ts +479 -0
- package/src/cli/commands/review-command.ts +58 -2
- package/src/cli/commands/status-command.ts +3 -2
- package/src/cli/help.ts +20 -3
- package/src/cli/index.ts +20 -3
- package/src/cli/progress.ts +50 -0
- package/src/core/ai/ai-client.ts +38 -2
- package/src/core/ai/ai-sdk-model-provider.ts +39 -0
- package/src/core/ai/features/brief-enricher.ts +1 -1
- package/src/core/ai/features/business-analyzer.ts +61 -0
- package/src/core/ai/features/business-summarizer.ts +44 -0
- package/src/core/ai/index.ts +14 -1
- package/src/core/ai/prompts/brief.ts +33 -7
- package/src/core/ai/prompts/business.ts +76 -0
- package/src/core/ai/provider-capabilities.ts +12 -0
- package/src/core/ai/schemas.ts +70 -2
- package/src/core/ai/types.ts +2 -0
- package/src/core/ai/usage-recorder.ts +53 -1
- package/src/core/brief.ts +246 -12
- package/src/core/builtins.ts +108 -0
- package/src/core/business-summary.ts +583 -0
- package/src/core/commands/compare.ts +233 -0
- package/src/core/commands/context.ts +6 -2
- package/src/core/commands/findings.ts +38 -30
- package/src/core/commands/review.ts +6 -0
- package/src/core/commands/scaffold.ts +38 -0
- package/src/core/commands/status.ts +9 -4
- package/src/core/compare.ts +243 -0
- package/src/core/concurrency.ts +30 -0
- package/src/core/config.ts +57 -1
- package/src/core/defaults.ts +63 -2
- package/src/core/domain/budget.ts +62 -4
- package/src/core/domain/classification.ts +12 -2
- package/src/core/engine/carryover.ts +4 -0
- package/src/core/engine/finalize-pipeline.ts +347 -0
- package/src/core/engine/prepare-pipeline.ts +1294 -0
- package/src/core/engine/review-context.ts +51 -0
- package/src/core/engine/review-options.ts +47 -0
- package/src/core/engine/review-scope.ts +106 -26
- package/src/core/engine/review-session.ts +18 -16
- package/src/core/engine/review-workspace.ts +14 -0
- package/src/core/engine/run-artifacts.ts +196 -0
- package/src/core/engine/run-format.ts +4 -0
- package/src/core/env.ts +14 -0
- package/src/core/findings-state.ts +13 -1
- package/src/core/github-review.ts +5 -2
- package/src/core/hunt/hunt-runner.ts +418 -0
- package/src/core/hunt/index.ts +61 -0
- package/src/core/hunt/maki-install.ts +331 -0
- package/src/core/hunt/maki-intent-prompt.ts +50 -0
- package/src/core/hunt/maki-model.ts +101 -0
- package/src/core/hunt/maki-prompt.ts +83 -0
- package/src/core/hunt/maki-release.ts +104 -0
- package/src/core/hunt/maki-runner.ts +276 -0
- package/src/core/hunt/pool.ts +8 -0
- package/src/core/logging/index.ts +7 -0
- package/src/core/logging/logger.ts +19 -0
- package/src/core/logging/progress.ts +366 -0
- package/src/core/orchestrator.ts +33 -1180
- package/src/core/paths.ts +6 -3
- package/src/core/pr-context.ts +2 -4
- package/src/core/prepare-report.ts +61 -0
- package/src/core/project-config.ts +46 -1
- package/src/core/propagation.ts +127 -0
- package/src/core/registry-check.ts +60 -21
- package/src/core/renderers/manifest-writer.ts +13 -0
- package/src/core/renderers/resume-renderer.ts +151 -11
- package/src/core/report-builder.ts +108 -278
- package/src/core/storage/file/file-storage.ts +7 -3
- package/src/core/storage/sqlite/mappers.ts +10 -0
- package/src/core/storage/sqlite/schema.ts +104 -23
- package/src/core/storage/sqlite/sqlite-storage.ts +22 -9
- package/src/core/storage/types.ts +17 -4
- package/src/core/storage/work-area.ts +59 -1
- package/src/core/types.ts +31 -0
- package/src/core/watchdog-registry.ts +21 -3
- package/src/index.ts +78 -4
- package/src/server/client/ArtifactsPanel.tsx +4 -2
- package/src/server/client/FindingCard.tsx +21 -1
- package/src/server/client/RunsSection.tsx +5 -0
- package/src/server/client/utils/constants.ts +21 -11
- package/src/server/client/utils/markdown.ts +1 -1
- package/src/server/client/utils/types.ts +14 -2
- package/src/server/run-repository.ts +35 -14
- package/src/server/styles.css +2 -0
- package/src/skill/OUTPUT-FORMAT.md +36 -10
- package/src/skill/README.md +30 -14
- package/src/skill/SETUP.md +22 -1
- package/src/skill/SKILL.md +17 -4
- package/src/skill/example-resume.md +43 -0
- package/src/skill/hunt.md +181 -58
- package/src/tests/__snapshots__/baseline.test.ts.snap +206 -178
- package/src/tests/ai-core.test.ts +84 -1
- package/src/tests/ai-features.test.ts +95 -4
- package/src/tests/baseline.test.ts +29 -121
- package/src/tests/brief-scope.test.ts +250 -0
- package/src/tests/business-summary.test.ts +258 -0
- package/src/tests/cli.test.ts +1 -0
- package/src/tests/compare.test.ts +233 -0
- package/src/tests/fixtures/fake-model-provider.ts +2 -1
- package/src/tests/hunt-cli.test.ts +185 -0
- package/src/tests/hunt-install.test.ts +153 -0
- package/src/tests/hunt-maki.test.ts +525 -0
- package/src/tests/markdown.test.ts +3 -3
- package/src/tests/progress.test.ts +145 -0
- package/src/tests/propagation.test.ts +119 -0
- package/src/tests/review-resume.test.ts +60 -1
- package/src/tests/review-scope.test.ts +90 -19
- package/src/tests/review-scripts.test.ts +106 -218
- package/src/tests/server.test.ts +7 -4
- package/src/tests/storage.test.ts +194 -9
- package/src/core/renderers/report-renderer.ts +0 -66
- 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
|
-
{
|
|
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
|
-
|
|
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
|
|
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)`;
|