@payconductor/houndsman 0.13.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 +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/hunt-command.ts +20 -3
- package/src/cli/commands/review-command.ts +38 -0
- package/src/cli/commands/status-command.ts +3 -2
- package/src/cli/help.ts +3 -1
- package/src/cli/index.ts +3 -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 +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 +27 -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 +31 -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 +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 +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 +13 -1
- package/src/core/github-review.ts +5 -2
- package/src/core/hunt/hunt-runner.ts +90 -0
- package/src/core/hunt/index.ts +2 -0
- package/src/core/hunt/maki-intent-prompt.ts +50 -0
- package/src/core/hunt/maki-prompt.ts +13 -1
- package/src/core/hunt/pool.ts +4 -26
- 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 +8 -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 +16 -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 +29 -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 +18 -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 +145 -58
- package/src/tests/__snapshots__/baseline.test.ts.snap +187 -178
- 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 +233 -0
- package/src/tests/fixtures/fake-model-provider.ts +2 -1
- package/src/tests/hunt-maki.test.ts +58 -0
- 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 +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
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.14.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);
|
|
@@ -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
|
|
@@ -180,6 +181,7 @@ export class HuntCommand implements Command {
|
|
|
180
181
|
engine.repo,
|
|
181
182
|
{
|
|
182
183
|
branch: workspace.branch,
|
|
184
|
+
variant: options.variant,
|
|
183
185
|
worktreePath: workspace.worktreePath,
|
|
184
186
|
prNumber: workspace.prNumber,
|
|
185
187
|
},
|
|
@@ -236,7 +238,7 @@ export class HuntCommand implements Command {
|
|
|
236
238
|
timeoutMs,
|
|
237
239
|
retries,
|
|
238
240
|
runner: new SpawnMakiRunner(binary.path),
|
|
239
|
-
onEvent: (event) => this.logEvent(board, event),
|
|
241
|
+
onEvent: (event) => this.logEvent(board, event, context.logger),
|
|
240
242
|
});
|
|
241
243
|
// Runs longas não podem parecer “timeout” na UI: a skill pinga antes de
|
|
242
244
|
// cada onda; o CLI renova o heartbeat enquanto o maki trabalha.
|
|
@@ -409,8 +411,23 @@ export class HuntCommand implements Command {
|
|
|
409
411
|
}
|
|
410
412
|
|
|
411
413
|
/** Log de progresso por watchdog. */
|
|
412
|
-
private logEvent(board: ProgressBoard, event: HuntEvent): void {
|
|
414
|
+
private logEvent(board: ProgressBoard, event: HuntEvent, logger: LoggerLike): void {
|
|
413
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;
|
|
414
431
|
case "start":
|
|
415
432
|
board.add(event.watchdogId, "revisando");
|
|
416
433
|
return;
|
|
@@ -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,9 @@ 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)
|
|
9
10
|
hunt [flags] Harness local (maki): prepare → watchdogs via maki → finalize
|
|
10
11
|
(a skill /houndsman hunt continua existindo p/ agentes externos)
|
|
@@ -25,6 +26,7 @@ Flags:
|
|
|
25
26
|
--base <branch> Base do diff (default: main)
|
|
26
27
|
--head <ref> Head do diff (default: HEAD)
|
|
27
28
|
--branch <nome> Branch alvo (default: detecta do git)
|
|
29
|
+
--variant <label> Rótulo da variante A/B (runs separadas para a mesma branch)
|
|
28
30
|
--pr <n> Revisa a PR em um worktree dedicado (cria/reusa automaticamente)
|
|
29
31
|
--worktree <path> Revisa em um worktree já existente
|
|
30
32
|
--clean Descarta findings brutos do staging anterior
|
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,6 +35,7 @@ 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";
|
|
@@ -61,6 +62,7 @@ function buildRegistry(): CommandRegistry {
|
|
|
61
62
|
.register(new HelpCommand())
|
|
62
63
|
.register(new HuntCommand())
|
|
63
64
|
.register(new ContextCommand())
|
|
65
|
+
.register(new CompareCommand())
|
|
64
66
|
.register(new FindingsCommand())
|
|
65
67
|
.register(new StatusCommand())
|
|
66
68
|
.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,
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hipóteses de lógica/domínio via IA (`AiBusinessAnalyzer`).
|
|
3
|
+
*
|
|
4
|
+
* Produz **candidatos** para o revisor `general` confirmar ou refutar — nunca
|
|
5
|
+
* achados. Serve para dar cobertura a PRs maiores que o orçamento do brief.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { GENERAL_WATCHDOG_ID } from "../../builtins";
|
|
9
|
+
import type { BusinessInput, BusinessSummary } from "../../business-summary";
|
|
10
|
+
import { buildBusinessPromptPayload } from "../../business-summary";
|
|
11
|
+
import type { JevCandidate } from "../../jev";
|
|
12
|
+
import type { AiClient } from "../ai-client";
|
|
13
|
+
import {
|
|
14
|
+
businessAnalysisInstructions,
|
|
15
|
+
businessAnalysisPrompt,
|
|
16
|
+
} from "../prompts/business";
|
|
17
|
+
import { BusinessHypothesesSchema, type BusinessHypothesis } from "../schemas";
|
|
18
|
+
|
|
19
|
+
/** Confiança mínima para a hipótese virar candidato. */
|
|
20
|
+
const MIN_CONFIDENCE = 0.5;
|
|
21
|
+
|
|
22
|
+
/** Gera hipóteses de fluxo para o watchdog `general`. */
|
|
23
|
+
export class AiBusinessAnalyzer {
|
|
24
|
+
constructor(private readonly client: AiClient) {}
|
|
25
|
+
|
|
26
|
+
/** `true` quando a feature está ligada e há IA disponível. */
|
|
27
|
+
get enabled(): boolean {
|
|
28
|
+
return this.client.available && this.client.features.business;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Candidatos validados contra os arquivos do escopo. */
|
|
32
|
+
async analyze(input: {
|
|
33
|
+
business: BusinessSummary;
|
|
34
|
+
payload: BusinessInput;
|
|
35
|
+
}): Promise<JevCandidate[]> {
|
|
36
|
+
if (!this.enabled) return [];
|
|
37
|
+
const files = new Set(Object.keys(input.payload.classification.files));
|
|
38
|
+
const result = await this.client.generateObject<{
|
|
39
|
+
hypotheses: BusinessHypothesis[];
|
|
40
|
+
}>("business-analysis", {
|
|
41
|
+
instructions: businessAnalysisInstructions(),
|
|
42
|
+
prompt: businessAnalysisPrompt(
|
|
43
|
+
input.business,
|
|
44
|
+
buildBusinessPromptPayload(input.payload),
|
|
45
|
+
),
|
|
46
|
+
outputSchema: BusinessHypothesesSchema,
|
|
47
|
+
maxOutputTokens: 1_600,
|
|
48
|
+
});
|
|
49
|
+
if (!result.ok) return [];
|
|
50
|
+
return result.output.hypotheses
|
|
51
|
+
.filter((item) => files.has(item.file) && item.confidence >= MIN_CONFIDENCE)
|
|
52
|
+
.map((item, index) => ({
|
|
53
|
+
watchdogId: GENERAL_WATCHDOG_ID,
|
|
54
|
+
rule: `${item.kind === "domain" ? "Domínio" : "Lógica"}: ${item.statement}`,
|
|
55
|
+
severity: "attention" as const,
|
|
56
|
+
file: item.file,
|
|
57
|
+
confidence: item.confidence,
|
|
58
|
+
questionKey: `business-${index}`,
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modelo de intenção via IA (`AiBusinessSummarizer`).
|
|
3
|
+
*
|
|
4
|
+
* Fail-open: sem IA/chave/feature a feature fica desligada e o prepare mantém o
|
|
5
|
+
* modelo determinístico. A saída é validada por schema — nada de texto solto.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
type BusinessInput,
|
|
10
|
+
type BusinessSummary,
|
|
11
|
+
buildBusinessPromptPayload,
|
|
12
|
+
parseBusinessSummary,
|
|
13
|
+
} from "../../business-summary";
|
|
14
|
+
import type { AiClient } from "../ai-client";
|
|
15
|
+
import { businessInstructions, businessPrompt } from "../prompts/business";
|
|
16
|
+
import { type BusinessSummaryPlan, BusinessSummarySchema } from "../schemas";
|
|
17
|
+
|
|
18
|
+
/** Reconstrói a intenção da PR a partir do diff. */
|
|
19
|
+
export class AiBusinessSummarizer {
|
|
20
|
+
constructor(private readonly client: AiClient) {}
|
|
21
|
+
|
|
22
|
+
/** `true` quando a feature está ligada e há IA disponível. */
|
|
23
|
+
get enabled(): boolean {
|
|
24
|
+
return this.client.available && this.client.features.business;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Falha-aberto: sem IA ou saída inválida → `null`. */
|
|
28
|
+
async summarize(input: BusinessInput): Promise<BusinessSummary | null> {
|
|
29
|
+
if (!this.enabled) return null;
|
|
30
|
+
const result = await this.client.generateObject<BusinessSummaryPlan>("business", {
|
|
31
|
+
instructions: businessInstructions(),
|
|
32
|
+
prompt: businessPrompt(buildBusinessPromptPayload(input)),
|
|
33
|
+
outputSchema: BusinessSummarySchema,
|
|
34
|
+
// Schema permite 12 fluxos/12 contratos: 1.400 truncava com thinking ligado.
|
|
35
|
+
maxOutputTokens: 2_000,
|
|
36
|
+
});
|
|
37
|
+
if (!result.ok) return null;
|
|
38
|
+
return parseBusinessSummary(
|
|
39
|
+
{ ...result.output, source: "ai", headSha: input.headSha },
|
|
40
|
+
"ai",
|
|
41
|
+
input.headSha,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/core/ai/index.ts
CHANGED
|
@@ -23,6 +23,8 @@ export {
|
|
|
23
23
|
errorMessage,
|
|
24
24
|
} from "./errors";
|
|
25
25
|
export { AiBriefEnricher } from "./features/brief-enricher";
|
|
26
|
+
export { AiBusinessAnalyzer } from "./features/business-analyzer";
|
|
27
|
+
export { AiBusinessSummarizer } from "./features/business-summarizer";
|
|
26
28
|
export { AiClassifier } from "./features/classifier";
|
|
27
29
|
export { AiContextSummarizer } from "./features/context-summarizer";
|
|
28
30
|
export { AiPreAnalyzer } from "./features/pre-analyzer";
|
|
@@ -33,8 +35,14 @@ export {
|
|
|
33
35
|
type AiCandidate,
|
|
34
36
|
type AiClassification,
|
|
35
37
|
type AiVerdict,
|
|
38
|
+
type BriefCheck,
|
|
39
|
+
BriefCheckSchema,
|
|
36
40
|
type BriefPlan,
|
|
37
41
|
BriefPlanSchema,
|
|
42
|
+
BusinessHypothesesSchema,
|
|
43
|
+
type BusinessHypothesis,
|
|
44
|
+
type BusinessSummaryPlan,
|
|
45
|
+
BusinessSummarySchema,
|
|
38
46
|
CandidateSchema,
|
|
39
47
|
ClassificationSchema,
|
|
40
48
|
type ContextSummaryPlan,
|