@payconductor/houndsman 0.13.0 → 0.16.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 +6 -1
- 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/findings-command.ts +11 -1
- package/src/cli/commands/golden-command.ts +92 -0
- package/src/cli/commands/hunt-command.ts +105 -4
- package/src/cli/commands/review-command.ts +38 -0
- package/src/cli/commands/status-command.ts +3 -2
- package/src/cli/help.ts +6 -1
- package/src/cli/index.ts +5 -1
- package/src/cli/progress.ts +7 -2
- package/src/core/ai/ai-client.ts +38 -2
- 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 +8 -0
- 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/usage-recorder.ts +53 -1
- package/src/core/brief.ts +278 -16
- package/src/core/builtins.ts +108 -0
- package/src/core/business-summary.ts +583 -0
- package/src/core/commands/compare.ts +266 -0
- package/src/core/commands/context.ts +6 -2
- package/src/core/commands/findings.ts +38 -30
- package/src/core/commands/golden.ts +176 -0
- package/src/core/commands/review.ts +6 -0
- package/src/core/commands/scaffold.ts +29 -0
- package/src/core/commands/status.ts +9 -4
- package/src/core/compare.ts +310 -0
- package/src/core/concurrency.ts +30 -0
- package/src/core/config.ts +41 -3
- package/src/core/defaults.ts +50 -4
- 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 +371 -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 +14 -1
- package/src/core/engine/review-session.ts +11 -13
- package/src/core/engine/run-artifacts.ts +196 -0
- package/src/core/engine/run-format.ts +4 -0
- package/src/core/findings-state.ts +22 -2
- package/src/core/github-review.ts +5 -2
- package/src/core/golden.ts +346 -0
- package/src/core/hunt/hunt-runner.ts +621 -34
- package/src/core/hunt/index.ts +40 -1
- package/src/core/hunt/maki-intent-prompt.ts +50 -0
- package/src/core/hunt/maki-prompt.ts +298 -5
- package/src/core/hunt/maki-runner.ts +93 -16
- package/src/core/hunt/pool.ts +4 -26
- package/src/core/hunt/review-checks.ts +460 -0
- package/src/core/logging/index.ts +6 -1
- package/src/core/logging/progress.ts +85 -1
- package/src/core/orchestrator.ts +33 -1289
- 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 +18 -1
- package/src/core/propagation.ts +127 -0
- package/src/core/registry-check.ts +90 -27
- package/src/core/renderers/manifest-writer.ts +13 -0
- package/src/core/renderers/resume-renderer.ts +151 -11
- package/src/core/report-builder.ts +116 -278
- package/src/core/severity.ts +245 -0
- 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 +51 -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 +55 -10
- package/src/skill/README.md +19 -14
- package/src/skill/SETUP.md +1 -0
- package/src/skill/SKILL.md +13 -4
- package/src/skill/example-resume.md +43 -0
- package/src/skill/hunt.md +205 -58
- package/src/skill/train.md +3 -0
- package/src/tests/__snapshots__/baseline.test.ts.snap +208 -184
- package/src/tests/ai-core.test.ts +46 -0
- 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/compare.test.ts +256 -0
- package/src/tests/findings-state.test.ts +82 -0
- package/src/tests/fixtures/fake-model-provider.ts +2 -1
- package/src/tests/golden.test.ts +195 -0
- package/src/tests/hunt-maki.test.ts +710 -2
- package/src/tests/markdown.test.ts +3 -3
- package/src/tests/progress.test.ts +29 -1
- package/src/tests/propagation.test.ts +119 -0
- package/src/tests/review-resume.test.ts +60 -1
- package/src/tests/review-scope.test.ts +38 -29
- package/src/tests/review-scripts.test.ts +125 -218
- package/src/tests/server.test.ts +7 -4
- package/src/tests/severity.test.ts +235 -0
- 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
package/README.md
CHANGED
|
@@ -15,6 +15,11 @@ agente especialista por domínio que audita o diff · **review/revisão** = o pr
|
|
|
15
15
|
**Jev** = pré-análise opcional. O vocabulário completo (brief, findings, manifest, budget,
|
|
16
16
|
overflow, churn, candidato…) está em [`docs/GLOSSARY.md`](docs/GLOSSARY.md).
|
|
17
17
|
|
|
18
|
+
> Toda revisão tem duas etapas fixas: um **modelo de intenção** da PR
|
|
19
|
+
> (`business-summary.json`) e o revisor de fluxo **`general`**. Nenhum dos dois é
|
|
20
|
+
> configurável — o primeiro enriquece os briefs; o segundo procura gaps de lógica/domínio,
|
|
21
|
+
> não só violações de regra.
|
|
22
|
+
|
|
18
23
|
---
|
|
19
24
|
|
|
20
25
|
## Como iniciar
|
|
@@ -128,7 +133,7 @@ mostra o worktree ativo de cada PR e permite criá-lo pela sidebar.
|
|
|
128
133
|
├─ Passo 0 contexto da PR (gh, opcional)
|
|
129
134
|
├─ Passo 1 classifica o diff → briefs por watchdog (incremental quando possível)
|
|
130
135
|
├─ Passo 2 N subagentes revisam em paralelo
|
|
131
|
-
├─ Passo 3
|
|
136
|
+
├─ Passo 3 grava o resumo final (review-resume.md) + triagem no storage
|
|
132
137
|
├─ Passo 4 valida na UI (server) e marca os achados a executar
|
|
133
138
|
├─ Passo 5 fix corrige o grupo atual → status fix_done
|
|
134
139
|
└─ Passo 6 verify confere o que resolveu, sem re-revisar
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@payconductor/houndsman",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.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",
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comando `compare` — teste A/B entre duas runs (por slug ou branch+variantes).
|
|
3
|
+
*
|
|
4
|
+
* Exemplo:
|
|
5
|
+
* houndsman compare --branch feature/x --a baseline --b otimizado
|
|
6
|
+
* houndsman compare feature-x--baseline-1a2b3c feature-x--otimizado-9f8e7d
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { listRunsForCompare, runCompare } from "../../core/commands/compare";
|
|
10
|
+
import { ArgParser } from "../arg-parser";
|
|
11
|
+
import type { CliContext, Command } from "./command";
|
|
12
|
+
|
|
13
|
+
/** Flags com valor (não entram nos positionais). */
|
|
14
|
+
const VALUE_FLAGS = new Set(["--branch", "--a", "--b", "--config"]);
|
|
15
|
+
|
|
16
|
+
export const COMPARE_HELP = `Uso: houndsman compare [<slugA> <slugB>] [flags]
|
|
17
|
+
|
|
18
|
+
Compara duas runs da mesma branch e grava o relatório na run B.
|
|
19
|
+
|
|
20
|
+
Flags:
|
|
21
|
+
--branch <nome> Branch alvo (default: a atual do git)
|
|
22
|
+
--a <variante> Rótulo A (default: run principal)
|
|
23
|
+
--b <variante> Rótulo B (obrigatório sem slugs)
|
|
24
|
+
--list Lista as runs/variantes com contagem por severidade
|
|
25
|
+
--json Saída JSON (sem markdown)
|
|
26
|
+
|
|
27
|
+
Exemplos:
|
|
28
|
+
houndsman compare --branch feature/x --a baseline --b otimizado
|
|
29
|
+
houndsman compare feature-x--baseline-1a2b3c feature-x--otimizado-9f8e7d
|
|
30
|
+
houndsman compare --list --branch feature/x`;
|
|
31
|
+
|
|
32
|
+
/** Argumentos posicionais (ignora flags com valor). */
|
|
33
|
+
function positionals(args: readonly string[]): string[] {
|
|
34
|
+
const result: string[] = [];
|
|
35
|
+
for (let index = 0; index < args.length; index++) {
|
|
36
|
+
const arg = args[index];
|
|
37
|
+
if (arg.startsWith("--")) {
|
|
38
|
+
if (VALUE_FLAGS.has(arg)) index++;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
result.push(arg);
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class CompareCommand implements Command {
|
|
47
|
+
readonly name = "compare";
|
|
48
|
+
|
|
49
|
+
async run(args: string[], context: CliContext): Promise<number> {
|
|
50
|
+
const parser = ArgParser.from(args);
|
|
51
|
+
if (parser.has("--help") || parser.has("-h")) {
|
|
52
|
+
context.out(COMPARE_HELP);
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const engine = await context.engine();
|
|
57
|
+
const branch = parser.value("--branch");
|
|
58
|
+
|
|
59
|
+
if (parser.has("--list")) {
|
|
60
|
+
const runs = listRunsForCompare(engine.storage, engine.repo, branch);
|
|
61
|
+
if (runs.length === 0) {
|
|
62
|
+
context.out("Nenhuma run encontrada.");
|
|
63
|
+
engine.close();
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
context.logger.table([
|
|
67
|
+
["slug", "variante", "findings", "blocking", "attention", "atualizada"],
|
|
68
|
+
...runs.map((run) => [
|
|
69
|
+
run.slug,
|
|
70
|
+
run.variant ?? "(principal)",
|
|
71
|
+
String(run.findings),
|
|
72
|
+
String(run.bySeverity.blocking),
|
|
73
|
+
String(run.bySeverity.attention),
|
|
74
|
+
run.updatedAt,
|
|
75
|
+
]),
|
|
76
|
+
]);
|
|
77
|
+
engine.close();
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const [slugA, slugB] = positionals(args);
|
|
82
|
+
if (slugA && !slugB) {
|
|
83
|
+
context.err(
|
|
84
|
+
"Passe dois slugs (`houndsman compare <a> <b>`) ou use --b <variante>.",
|
|
85
|
+
);
|
|
86
|
+
return 1;
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
const result = runCompare(engine.storage, engine.repo, {
|
|
90
|
+
slugA,
|
|
91
|
+
slugB,
|
|
92
|
+
branch,
|
|
93
|
+
variantA: parser.value("--a"),
|
|
94
|
+
variantB: parser.value("--b"),
|
|
95
|
+
});
|
|
96
|
+
if (parser.has("--json")) {
|
|
97
|
+
context.out(result.json.trimEnd());
|
|
98
|
+
} else {
|
|
99
|
+
context.out(result.markdown.trimEnd());
|
|
100
|
+
context.logger.success(
|
|
101
|
+
`Comparação salva em ${result.artifacts.markdown} (run ${result.b.slug}).`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
return 0;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
context.err(error instanceof Error ? error.message : String(error));
|
|
107
|
+
return 1;
|
|
108
|
+
} finally {
|
|
109
|
+
engine.close();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -14,21 +14,31 @@ export class FindingsCommand implements Command {
|
|
|
14
14
|
async run(args: string[], context: CliContext): Promise<number> {
|
|
15
15
|
const engine = await context.engine();
|
|
16
16
|
const sub = ArgParser.from(args).command();
|
|
17
|
+
// Raiz/git do engine: o comando nunca lê `process.cwd()` implicitamente.
|
|
18
|
+
const options = { root: engine.root, git: engine.git };
|
|
17
19
|
const result =
|
|
18
20
|
sub === "verify"
|
|
19
|
-
? await runVerifyCommand(
|
|
21
|
+
? await runVerifyCommand(
|
|
22
|
+
engine.config,
|
|
23
|
+
engine.storage,
|
|
24
|
+
engine.repo,
|
|
25
|
+
args,
|
|
26
|
+
options,
|
|
27
|
+
)
|
|
20
28
|
: sub === "submit"
|
|
21
29
|
? await runSubmitCommand(
|
|
22
30
|
engine.config,
|
|
23
31
|
engine.storage,
|
|
24
32
|
engine.repo,
|
|
25
33
|
args,
|
|
34
|
+
options,
|
|
26
35
|
)
|
|
27
36
|
: runFindingsCommand(
|
|
28
37
|
engine.config,
|
|
29
38
|
engine.storage,
|
|
30
39
|
engine.repo,
|
|
31
40
|
args,
|
|
41
|
+
options,
|
|
32
42
|
);
|
|
33
43
|
|
|
34
44
|
if (result.exitCode !== 0) context.err(result.output);
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comando `golden` — avalia as runs da branch contra defeitos congelados.
|
|
3
|
+
*
|
|
4
|
+
* Exemplo:
|
|
5
|
+
* houndsman golden .houndsman/golden/asaas-split.json
|
|
6
|
+
* houndsman golden .houndsman/golden/asaas-split.json --variant otimizado-4
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { runGolden } from "../../core/commands/golden";
|
|
10
|
+
import { ArgParser } from "../arg-parser";
|
|
11
|
+
import type { CliContext, Command } from "./command";
|
|
12
|
+
|
|
13
|
+
/** Flags com valor (não entram nos positionais). */
|
|
14
|
+
const VALUE_FLAGS = new Set(["--branch", "--variant", "--slug", "--config"]);
|
|
15
|
+
|
|
16
|
+
export const GOLDEN_HELP = `Uso: houndsman golden <arquivo.json> [flags]
|
|
17
|
+
|
|
18
|
+
Avalia uma run (ou todas as variantes da branch) contra um golden set —
|
|
19
|
+
defeitos já confirmados no código — e reporta recall/precisão.
|
|
20
|
+
|
|
21
|
+
Flags:
|
|
22
|
+
--branch <nome> Branch alvo (default: a atual do git)
|
|
23
|
+
--variant <rótulo> Avalia só uma variante
|
|
24
|
+
--slug <slug> Avalia uma run específica
|
|
25
|
+
--json Saída JSON (sem markdown)
|
|
26
|
+
|
|
27
|
+
O relatório é gravado como \`golden-<slug>.md|json\` na run avaliada.
|
|
28
|
+
|
|
29
|
+
Formato do arquivo:
|
|
30
|
+
{ "version": 1, "title": "...", "commit": "abc1234",
|
|
31
|
+
"expected": [{ "id": "seller-api-key", "file": "src/a.ts",
|
|
32
|
+
"line": 116, "tolerance": 30, "match": "sellerApiKey",
|
|
33
|
+
"severity": "blocking", "note": "..." }] 1}
|
|
34
|
+
|
|
35
|
+
Exemplos:
|
|
36
|
+
houndsman golden .houndsman/golden/asaas-split.json
|
|
37
|
+
houndsman golden .houndsman/golden/asaas-split.json --variant otimizado-5`;
|
|
38
|
+
|
|
39
|
+
/** Primeiro argumento posicional (ignora flags com valor). */
|
|
40
|
+
function positionalFile(args: readonly string[]): string | undefined {
|
|
41
|
+
for (let index = 0; index < args.length; index++) {
|
|
42
|
+
const arg = args[index];
|
|
43
|
+
if (arg.startsWith("--")) {
|
|
44
|
+
if (VALUE_FLAGS.has(arg)) index++;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
return arg;
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class GoldenCommand implements Command {
|
|
53
|
+
readonly name = "golden";
|
|
54
|
+
|
|
55
|
+
async run(args: string[], context: CliContext): Promise<number> {
|
|
56
|
+
const parser = ArgParser.from(args);
|
|
57
|
+
if (parser.has("--help") || parser.has("-h")) {
|
|
58
|
+
context.logger.info(GOLDEN_HELP);
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const file = positionalFile(args);
|
|
63
|
+
if (!file) {
|
|
64
|
+
context.logger.info(GOLDEN_HELP);
|
|
65
|
+
return 1;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const engine = await context.engine();
|
|
69
|
+
try {
|
|
70
|
+
const result = runGolden(engine.storage, engine.repo, {
|
|
71
|
+
file,
|
|
72
|
+
branch: parser.value("--branch"),
|
|
73
|
+
variant: parser.value("--variant"),
|
|
74
|
+
slug: parser.value("--slug"),
|
|
75
|
+
});
|
|
76
|
+
if (parser.has("--json")) {
|
|
77
|
+
context.logger.info(result.json.trimEnd());
|
|
78
|
+
} else {
|
|
79
|
+
context.logger.info(result.markdown.trimEnd());
|
|
80
|
+
context.logger.success(
|
|
81
|
+
`Golden salvo em ${result.artifacts?.markdown} (run ${result.target.slug}).`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
return 0;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
context.logger.error(error instanceof Error ? error.message : String(error));
|
|
87
|
+
return 1;
|
|
88
|
+
} finally {
|
|
89
|
+
engine.close();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
resolveMakiModel,
|
|
27
27
|
} from "../../core/hunt/maki-model";
|
|
28
28
|
import { SpawnMakiRunner } from "../../core/hunt/maki-runner";
|
|
29
|
-
import { formatDuration, ProgressBoard } from "../../core/logging";
|
|
29
|
+
import { formatDuration, type LoggerLike, ProgressBoard } from "../../core/logging";
|
|
30
30
|
import { displayPath } from "../../core/paths";
|
|
31
31
|
import { ProjectConfig } from "../../core/project-config";
|
|
32
32
|
import { touchRunStatus } from "../../core/run-status";
|
|
@@ -49,6 +49,7 @@ Subcomandos:
|
|
|
49
49
|
Flags:
|
|
50
50
|
--pr <n> Revisa a PR em worktree dedicado
|
|
51
51
|
--branch <b> Branch alvo
|
|
52
|
+
--variant <label> Rótulo da variante A/B (runs separadas para a mesma branch)
|
|
52
53
|
--worktree <path> Worktree já existente
|
|
53
54
|
--allow-partial Finaliza mesmo se algum watchdog falhar (findings vazios)
|
|
54
55
|
--no-context Não coleta o contexto da PR via gh
|
|
@@ -56,6 +57,8 @@ Flags:
|
|
|
56
57
|
--hunt-concurrency Processos maki simultâneos
|
|
57
58
|
--hunt-timeout Timeout por watchdog (ms)
|
|
58
59
|
--no-retry Não repete saída inválida
|
|
60
|
+
--no-checklist Sem a passada extra de fluxos/invariantes não verificados
|
|
61
|
+
--no-second-opinion Sem a passada adversarial do revisor de fluxo
|
|
59
62
|
--fail-on-blocking Exit 1 se houver bloqueantes
|
|
60
63
|
|
|
61
64
|
Instalação:
|
|
@@ -180,6 +183,7 @@ export class HuntCommand implements Command {
|
|
|
180
183
|
engine.repo,
|
|
181
184
|
{
|
|
182
185
|
branch: workspace.branch,
|
|
186
|
+
variant: options.variant,
|
|
183
187
|
worktreePath: workspace.worktreePath,
|
|
184
188
|
prNumber: workspace.prNumber,
|
|
185
189
|
},
|
|
@@ -235,8 +239,12 @@ export class HuntCommand implements Command {
|
|
|
235
239
|
concurrency,
|
|
236
240
|
timeoutMs,
|
|
237
241
|
retries,
|
|
242
|
+
checklist: parser.has("--no-checklist") ? false : project.huntChecklist,
|
|
243
|
+
secondOpinion: parser.has("--no-second-opinion")
|
|
244
|
+
? false
|
|
245
|
+
: project.huntSecondOpinion,
|
|
238
246
|
runner: new SpawnMakiRunner(binary.path),
|
|
239
|
-
onEvent: (event) => this.logEvent(board, event),
|
|
247
|
+
onEvent: (event) => this.logEvent(board, event, context.logger),
|
|
240
248
|
});
|
|
241
249
|
// Runs longas não podem parecer “timeout” na UI: a skill pinga antes de
|
|
242
250
|
// cada onda; o CLI renova o heartbeat enquanto o maki trabalha.
|
|
@@ -265,6 +273,56 @@ export class HuntCommand implements Command {
|
|
|
265
273
|
`${report.totals.findings} finding(s), $${report.totals.costUsd.toFixed(4)}, ` +
|
|
266
274
|
`${Math.round(report.durationMs / 1000)}s.`,
|
|
267
275
|
);
|
|
276
|
+
const checklistCost = report.watchdogs.reduce(
|
|
277
|
+
(sum, watchdog) => sum + (watchdog.checklist?.costUsd ?? 0),
|
|
278
|
+
0,
|
|
279
|
+
);
|
|
280
|
+
const opinionCost = report.watchdogs.reduce(
|
|
281
|
+
(sum, watchdog) => sum + (watchdog.secondOpinion?.costUsd ?? 0),
|
|
282
|
+
0,
|
|
283
|
+
);
|
|
284
|
+
if (checklistCost + opinionCost > 0) {
|
|
285
|
+
context.logger.info(
|
|
286
|
+
`Passadas extras: checklist $${checklistCost.toFixed(4)} · ` +
|
|
287
|
+
`segunda opinião $${opinionCost.toFixed(4)}.`,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
const unchecked = report.watchdogs.filter(
|
|
291
|
+
(watchdog) =>
|
|
292
|
+
(watchdog.checks?.missing ?? 0) + (watchdog.rules?.missing ?? 0) > 0,
|
|
293
|
+
);
|
|
294
|
+
if (unchecked.length > 0) {
|
|
295
|
+
context.logger.warn(
|
|
296
|
+
`Checklist de intenção/regras incompleto em ${unchecked.length} watchdog(s): ` +
|
|
297
|
+
unchecked
|
|
298
|
+
.map(
|
|
299
|
+
(watchdog) =>
|
|
300
|
+
`${watchdog.watchdogId} (itens ${watchdog.checks?.missing ?? 0}, regras ${watchdog.rules?.missing ?? 0})`,
|
|
301
|
+
)
|
|
302
|
+
.join(", "),
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
const businessReview = report.watchdogs.find(
|
|
306
|
+
(watchdog) => watchdog.business !== undefined,
|
|
307
|
+
)?.business;
|
|
308
|
+
if (businessReview && !businessReview.accepted) {
|
|
309
|
+
context.logger.info(
|
|
310
|
+
"Modelo de intenção devolvido pelo revisor foi descartado " +
|
|
311
|
+
`(inferior ao já gravado: ${report.business ?? "n/d"}).`,
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
const incomplete = report.watchdogs
|
|
315
|
+
.filter((watchdog) => (watchdog.coverage?.undeclared ?? 0) > 0)
|
|
316
|
+
.map(
|
|
317
|
+
(watchdog) =>
|
|
318
|
+
`${watchdog.watchdogId} ${watchdog.coverage?.reviewed ?? 0}/${watchdog.coverage?.total ?? 0}`,
|
|
319
|
+
);
|
|
320
|
+
if (incomplete.length > 0) {
|
|
321
|
+
context.logger.info(
|
|
322
|
+
`Cobertura declarada incompleta: ${incomplete.join(", ")} ` +
|
|
323
|
+
"(detalhe em `hunt-report.json`).",
|
|
324
|
+
);
|
|
325
|
+
}
|
|
268
326
|
|
|
269
327
|
const failed = report.watchdogs.filter(
|
|
270
328
|
(watchdog) => watchdog.status === "failed",
|
|
@@ -403,17 +461,60 @@ export class HuntCommand implements Command {
|
|
|
403
461
|
`harness: ${project.huntEnabled ? "ligado" : "desligado"} · ` +
|
|
404
462
|
`concorrência ${project.huntConcurrency} · ` +
|
|
405
463
|
`timeout ${Math.round(project.huntTimeoutMs / 1000)}s · ` +
|
|
406
|
-
`retries ${project.huntRetries}
|
|
464
|
+
`retries ${project.huntRetries} · ` +
|
|
465
|
+
`checklist ${project.huntChecklist ? "on" : "off"} · ` +
|
|
466
|
+
`segunda opinião ${project.huntSecondOpinion ? "on" : "off"}.`,
|
|
407
467
|
);
|
|
408
468
|
return 0;
|
|
409
469
|
}
|
|
410
470
|
|
|
411
471
|
/** Log de progresso por watchdog. */
|
|
412
|
-
private logEvent(board: ProgressBoard, event: HuntEvent): void {
|
|
472
|
+
private logEvent(board: ProgressBoard, event: HuntEvent, logger: LoggerLike): void {
|
|
413
473
|
switch (event.type) {
|
|
474
|
+
case "intent":
|
|
475
|
+
if (event.status === "start") {
|
|
476
|
+
logger.step("Reconstruindo a intenção da PR (maki)…");
|
|
477
|
+
} else if (event.status === "done") {
|
|
478
|
+
logger.success(
|
|
479
|
+
`Intenção da PR reconstruída (${event.source ?? "reviewer"}).`,
|
|
480
|
+
);
|
|
481
|
+
} else if (event.status === "skipped") {
|
|
482
|
+
logger.info(`Intenção da PR já disponível (${event.source}).`);
|
|
483
|
+
} else {
|
|
484
|
+
logger.warn(
|
|
485
|
+
`Intenção da PR manteve o modelo determinístico: ${event.error ?? "falha"}`,
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
return;
|
|
414
489
|
case "start":
|
|
415
490
|
board.add(event.watchdogId, "revisando");
|
|
416
491
|
return;
|
|
492
|
+
case "checklist":
|
|
493
|
+
if (event.status === "start") {
|
|
494
|
+
logger.step(
|
|
495
|
+
`${event.watchdogId}: ${event.missing} item(s) e ${event.missingRules} regra(s) sem verificação — passada de checklist…`,
|
|
496
|
+
);
|
|
497
|
+
} else if (event.status === "failed") {
|
|
498
|
+
logger.warn(
|
|
499
|
+
`${event.watchdogId}: checklist falhou (${event.error ?? "erro"}); relatório da primeira passada mantido.`,
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
return;
|
|
503
|
+
case "second-opinion":
|
|
504
|
+
if (event.status === "start") {
|
|
505
|
+
logger.step(
|
|
506
|
+
`${event.watchdogId}: passada adversarial (credencial/estado, capacidade, identificador, idempotência, contrato)…`,
|
|
507
|
+
);
|
|
508
|
+
} else if (event.status === "done") {
|
|
509
|
+
logger.info(
|
|
510
|
+
`${event.watchdogId}: segunda opinião adicionou ${event.findings ?? 0} finding(s).`,
|
|
511
|
+
);
|
|
512
|
+
} else {
|
|
513
|
+
logger.warn(
|
|
514
|
+
`${event.watchdogId}: segunda opinião falhou (${event.error ?? "erro"}); relatório anterior mantido.`,
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
return;
|
|
417
518
|
case "retry":
|
|
418
519
|
board.retry(
|
|
419
520
|
event.watchdogId,
|
|
@@ -1,5 +1,7 @@
|
|
|
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";
|
|
5
7
|
import { displayPath } from "../../core/paths";
|
|
@@ -52,6 +54,42 @@ export class ReviewCommand implements Command {
|
|
|
52
54
|
pr: workspace.prNumber,
|
|
53
55
|
worktreePath: workspace.worktreePath,
|
|
54
56
|
};
|
|
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
|
+
}
|
|
55
93
|
if (options.finalize) context.logger.step("Consolidando os findings…");
|
|
56
94
|
const result = await engine
|
|
57
95
|
.session(resolved)
|
|
@@ -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,8 +3,10 @@ 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
|
|
9
|
+
golden <arquivo> Avalia as runs contra um golden set (recall/precisão)
|
|
8
10
|
findings <sub> Triagem/correção dos findings (list · show · set · done · reset · sync · summary · batch · verify · submit · path)
|
|
9
11
|
hunt [flags] Harness local (maki): prepare → watchdogs via maki → finalize
|
|
10
12
|
(a skill /houndsman hunt continua existindo p/ agentes externos)
|
|
@@ -25,6 +27,7 @@ Flags:
|
|
|
25
27
|
--base <branch> Base do diff (default: main)
|
|
26
28
|
--head <ref> Head do diff (default: HEAD)
|
|
27
29
|
--branch <nome> Branch alvo (default: detecta do git)
|
|
30
|
+
--variant <label> Rótulo da variante A/B (runs separadas para a mesma branch)
|
|
28
31
|
--pr <n> Revisa a PR em um worktree dedicado (cria/reusa automaticamente)
|
|
29
32
|
--worktree <path> Revisa em um worktree já existente
|
|
30
33
|
--clean Descarta findings brutos do staging anterior
|
|
@@ -41,6 +44,8 @@ Flags de hunt (harness local):
|
|
|
41
44
|
--hunt-concurrency Processos maki simultâneos
|
|
42
45
|
--hunt-timeout Timeout por watchdog (ms)
|
|
43
46
|
--no-retry Não repete saída inválida
|
|
47
|
+
--no-checklist Sem a passada extra de fluxos/invariantes não verificados
|
|
48
|
+
--no-second-opinion Sem a passada adversarial do revisor de fluxo
|
|
44
49
|
|
|
45
50
|
Flags de findings:
|
|
46
51
|
--decision <d> pending | execute | ignore
|
package/src/cli/index.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Comandos:
|
|
14
14
|
* (default) Passo 1 — preparar (classificação + briefs + manifesto)
|
|
15
|
-
* --finalize Passo 3 — consolidar os findings
|
|
15
|
+
* --finalize Passo 3 — consolidar os findings no resumo final
|
|
16
16
|
* context Passo 0 — coletar o contexto da PR via `gh`
|
|
17
17
|
* findings <sub> Triagem/correção dos findings (list/set/done/sync/batch/verify/submit/...)
|
|
18
18
|
* hunt [flags] Harness local (maki): prepare → watchdogs via maki → finalize
|
|
@@ -35,9 +35,11 @@ import { formatDuration, logger } from "../core/logging";
|
|
|
35
35
|
import { syncProjectLayerForRoot } from "../core/services/worktree-manager";
|
|
36
36
|
import { ArgParser } from "./arg-parser";
|
|
37
37
|
import type { CliContext } from "./commands/command";
|
|
38
|
+
import { CompareCommand } from "./commands/compare-command";
|
|
38
39
|
import { ContextCommand } from "./commands/context-command";
|
|
39
40
|
import { DoctorCommand } from "./commands/doctor-command";
|
|
40
41
|
import { FindingsCommand } from "./commands/findings-command";
|
|
42
|
+
import { GoldenCommand } from "./commands/golden-command";
|
|
41
43
|
import { HelpCommand } from "./commands/help-command";
|
|
42
44
|
import { HuntCommand } from "./commands/hunt-command";
|
|
43
45
|
import { CommandRegistry } from "./commands/registry";
|
|
@@ -61,6 +63,8 @@ function buildRegistry(): CommandRegistry {
|
|
|
61
63
|
.register(new HelpCommand())
|
|
62
64
|
.register(new HuntCommand())
|
|
63
65
|
.register(new ContextCommand())
|
|
66
|
+
.register(new CompareCommand())
|
|
67
|
+
.register(new GoldenCommand())
|
|
64
68
|
.register(new FindingsCommand())
|
|
65
69
|
.register(new StatusCommand())
|
|
66
70
|
.register(new WatchdogCommand())
|
package/src/cli/progress.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* primeiro `Discovery:`.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { formatDuration, type LoggerLike,
|
|
9
|
+
import { formatDuration, type LoggerLike, ParallelPhaseReporter } from "../core/logging";
|
|
10
10
|
import type { PrepareProgress, PrepareStage } from "../core/orchestrator";
|
|
11
11
|
|
|
12
12
|
/** Texto de cada etapa (início no gerúndio, fim no particípio). */
|
|
@@ -20,6 +20,10 @@ export const PREPARE_STAGE_TEXT: Record<PrepareStage, { start: string; done: str
|
|
|
20
20
|
impact: { start: "Medindo o impacto nos importadores", done: "Impacto calculado" },
|
|
21
21
|
jev: { start: "Pré-analisando o diff (Jev/IA)", done: "Pré-análise concluída" },
|
|
22
22
|
context: { start: "Resumindo o contexto da PR", done: "Contexto resumido" },
|
|
23
|
+
business: {
|
|
24
|
+
start: "Reconstruindo a intenção da PR",
|
|
25
|
+
done: "Intenção reconstruída",
|
|
26
|
+
},
|
|
23
27
|
technical: {
|
|
24
28
|
start: "Consolidando os pontos técnicos",
|
|
25
29
|
done: "Pontos técnicos prontos",
|
|
@@ -31,7 +35,8 @@ export const PREPARE_STAGE_TEXT: Record<PrepareStage, { start: string; done: str
|
|
|
31
35
|
|
|
32
36
|
/** Cria o callback de progresso do prepare ligado ao logger do CLI. */
|
|
33
37
|
export function prepareProgress(logger: LoggerLike): PrepareProgress {
|
|
34
|
-
|
|
38
|
+
// O prepare roda etapas em paralelo; o reporter aceita fases concorrentes.
|
|
39
|
+
const reporter = new ParallelPhaseReporter(logger);
|
|
35
40
|
return (event) => {
|
|
36
41
|
const text = PREPARE_STAGE_TEXT[event.stage];
|
|
37
42
|
if (event.type === "start") reporter.begin(event.stage, text.start);
|
package/src/core/ai/ai-client.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type { AiProviderOptions } from "../config";
|
|
|
10
10
|
import type { ProjectConfig } from "../project-config";
|
|
11
11
|
import { AiConfigurationError, errorMessage } from "./errors";
|
|
12
12
|
import { ModelFactory } from "./model-factory";
|
|
13
|
+
import { LEAN_PROVIDER_OPTIONS } from "./provider-capabilities";
|
|
13
14
|
import type { AiResult, AiUsage, ModelProvider, ModelRequest } from "./types";
|
|
14
15
|
import { AiUsageBudget } from "./usage-budget";
|
|
15
16
|
import { AiUsageRecorder } from "./usage-recorder";
|
|
@@ -109,7 +110,13 @@ export class AiClient {
|
|
|
109
110
|
): Promise<AiResult<T>> {
|
|
110
111
|
return this.run(
|
|
111
112
|
feature,
|
|
112
|
-
{
|
|
113
|
+
{
|
|
114
|
+
...request,
|
|
115
|
+
outputSchema: request.outputSchema,
|
|
116
|
+
providerOptions:
|
|
117
|
+
request.providerOptions ??
|
|
118
|
+
leanProviderOptions(this.project.aiProviderOptions),
|
|
119
|
+
},
|
|
113
120
|
model,
|
|
114
121
|
);
|
|
115
122
|
}
|
|
@@ -183,6 +190,7 @@ export class AiClient {
|
|
|
183
190
|
return { ok: false, error: errorMessage(error) };
|
|
184
191
|
}
|
|
185
192
|
|
|
193
|
+
const startedAt = Date.now();
|
|
186
194
|
try {
|
|
187
195
|
const provider = await this.requireProvider(model);
|
|
188
196
|
const response = await provider.generate({
|
|
@@ -193,6 +201,17 @@ export class AiClient {
|
|
|
193
201
|
),
|
|
194
202
|
signal: combineSignals(request.signal, this.project.aiTimeoutMs),
|
|
195
203
|
});
|
|
204
|
+
// Objeto truncado não é resultado parcial útil: sem o JSON completo a
|
|
205
|
+
// feature inteira caía silenciosamente no fallback. Registra e falha.
|
|
206
|
+
if (request.outputSchema && response.finishReason === "length") {
|
|
207
|
+
const error = `Resposta truncada (finishReason=length; maxOutputTokens=${request.maxOutputTokens ?? "sem teto"}).`;
|
|
208
|
+
this.recorder.recordFailure(feature, provider.modelId, {
|
|
209
|
+
durationMs: Date.now() - startedAt,
|
|
210
|
+
error,
|
|
211
|
+
finishReason: response.finishReason,
|
|
212
|
+
});
|
|
213
|
+
return { ok: false, error };
|
|
214
|
+
}
|
|
196
215
|
this.budget.record(response.usage);
|
|
197
216
|
this.recorder.record(feature, provider.modelId, response.usage);
|
|
198
217
|
return {
|
|
@@ -202,7 +221,12 @@ export class AiClient {
|
|
|
202
221
|
model: provider.modelId,
|
|
203
222
|
};
|
|
204
223
|
} catch (error) {
|
|
205
|
-
|
|
224
|
+
const message = errorMessage(error);
|
|
225
|
+
this.recorder.recordFailure(feature, model ?? this.modelId, {
|
|
226
|
+
durationMs: Date.now() - startedAt,
|
|
227
|
+
error: message,
|
|
228
|
+
});
|
|
229
|
+
return { ok: false, error: message };
|
|
206
230
|
}
|
|
207
231
|
}
|
|
208
232
|
}
|
|
@@ -221,6 +245,18 @@ function mergeProviderOptions(
|
|
|
221
245
|
return merged;
|
|
222
246
|
}
|
|
223
247
|
|
|
248
|
+
/**
|
|
249
|
+
* Opções enxutas para objetos estruturados: thinking desligado, salvo quando o
|
|
250
|
+
* projeto já configurou `thinking` explicitamente — aí a config manda.
|
|
251
|
+
*/
|
|
252
|
+
function leanProviderOptions(
|
|
253
|
+
base: AiProviderOptions | undefined,
|
|
254
|
+
): AiProviderOptions | undefined {
|
|
255
|
+
const configured = base?.deepseek;
|
|
256
|
+
if (configured && "thinking" in configured) return undefined;
|
|
257
|
+
return LEAN_PROVIDER_OPTIONS;
|
|
258
|
+
}
|
|
259
|
+
|
|
224
260
|
/** Combina o sinal do chamador com o timeout configurado. */
|
|
225
261
|
function combineSignals(
|
|
226
262
|
signal: AbortSignal | undefined,
|