@payconductor/houndsman 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -13
- package/package.json +1 -1
- package/src/cli/commands/doctor-command.ts +22 -0
- package/src/cli/commands/hunt-command.ts +440 -29
- package/src/cli/commands/review-command.ts +20 -2
- package/src/cli/help.ts +16 -8
- package/src/cli/index.ts +15 -2
- package/src/cli/progress.ts +45 -0
- package/src/core/ai/ai-client.ts +3 -7
- package/src/core/ai/ai-sdk-model-provider.ts +43 -24
- package/src/core/ai/features/brief-enricher.ts +0 -1
- package/src/core/ai/features/classifier.ts +0 -1
- package/src/core/ai/features/context-summarizer.ts +0 -1
- package/src/core/ai/features/pre-analyzer.ts +0 -1
- package/src/core/ai/features/technical-summarizer.ts +0 -1
- package/src/core/ai/features/verifier.ts +0 -1
- package/src/core/ai/index.ts +6 -4
- package/src/core/ai/prompts/pre-analysis.ts +3 -2
- package/src/core/ai/schemas.ts +1 -23
- package/src/core/ai/types.ts +4 -15
- package/src/core/ai/usage-budget.ts +1 -1
- package/src/core/ast/diff.ts +37 -2
- package/src/core/ast/service.ts +1 -1
- package/src/core/brief.ts +54 -5
- package/src/core/changed-scope.ts +207 -0
- package/src/core/commands/scaffold.ts +12 -18
- package/src/core/config.ts +30 -15
- package/src/core/defaults.ts +14 -5
- package/src/core/engine/review-scope.ts +131 -17
- package/src/core/engine/review-session.ts +7 -3
- package/src/core/engine/review-workspace.ts +14 -0
- package/src/core/env.ts +14 -0
- package/src/core/hunt/hunt-runner.ts +328 -0
- package/src/core/hunt/index.ts +59 -0
- package/src/core/hunt/maki-install.ts +331 -0
- package/src/core/hunt/maki-model.ts +101 -0
- package/src/core/hunt/maki-prompt.ts +71 -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 +30 -0
- package/src/core/jev/jev-detector.ts +7 -2
- package/src/core/logging/index.ts +3 -1
- package/src/core/logging/logger.ts +22 -3
- package/src/core/logging/progress.ts +282 -0
- package/src/core/orchestrator.ts +173 -59
- package/src/core/project-config.ts +34 -24
- package/src/core/registry-check.ts +2 -12
- package/src/core/rule-text.ts +3 -55
- package/src/core/services/git-repository.ts +8 -0
- package/src/core/storage/types.ts +1 -1
- package/src/core/types.ts +3 -14
- package/src/index.ts +74 -3
- package/src/server/client/utils/constants.ts +3 -6
- package/src/server/client/utils/types.ts +0 -1
- package/src/skill/OUTPUT-FORMAT.md +5 -0
- package/src/skill/README.md +14 -1
- package/src/skill/SETUP.md +22 -2
- package/src/skill/SKILL.md +4 -0
- package/src/skill/fix.md +1 -1
- package/src/skill/hunt.md +38 -6
- package/src/tests/__snapshots__/baseline.test.ts.snap +69 -32
- package/src/tests/ai-core.test.ts +38 -1
- package/src/tests/changed-scope.test.ts +197 -0
- package/src/tests/cli.test.ts +1 -0
- package/src/tests/fallback.test.ts +1 -96
- package/src/tests/fixtures/fake-model-provider.ts +0 -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 +467 -0
- package/src/tests/progress.test.ts +117 -0
- package/src/tests/review-scope.test.ts +95 -0
- package/src/tests/worktree.test.ts +1 -3
- package/src/harness/README.md +0 -15
- package/src/harness/agent/agent-context.ts +0 -33
- package/src/harness/agent/agent-errors.ts +0 -30
- package/src/harness/agent/agent-factory.ts +0 -46
- package/src/harness/agent/agent-reporter.ts +0 -34
- package/src/harness/agent/agent-result.ts +0 -16
- package/src/harness/agent/agent-runner.ts +0 -32
- package/src/harness/agent/agent-scheduler.ts +0 -37
- package/src/harness/agent/review-agent.ts +0 -401
- package/src/harness/config/harness-config.ts +0 -89
- package/src/harness/hunt/hunt-harness.ts +0 -83
- package/src/harness/hunt/hunt-options.ts +0 -92
- package/src/harness/hunt/hunt-result.ts +0 -17
- package/src/harness/hunt/hunt-session.ts +0 -359
- package/src/harness/hunt/review-planner.ts +0 -98
- package/src/harness/index.ts +0 -36
- package/src/harness/investigation/agent-state.ts +0 -134
- package/src/harness/investigation/evidence-verifier.ts +0 -129
- package/src/harness/investigation/evidence.ts +0 -110
- package/src/harness/investigation/finding-critic.ts +0 -96
- package/src/harness/investigation/investigation-planner.ts +0 -118
- package/src/harness/investigation/investigation-task.ts +0 -10
- package/src/harness/investigation/review-journal.ts +0 -87
- package/src/harness/investigation/rule-analyzer.ts +0 -178
- package/src/harness/investigation/rule-signals.ts +0 -110
- package/src/harness/prompts/agent-schemas.ts +0 -57
- package/src/harness/prompts/reviewer-prompt-builder.ts +0 -101
- package/src/harness/prompts/states/conclude-prompt.ts +0 -16
- package/src/harness/prompts/states/investigate-prompt.ts +0 -37
- package/src/harness/prompts/states/orient-prompt.ts +0 -15
- package/src/harness/prompts/states/verify-prompt.ts +0 -22
- package/src/harness/tools/agent-tool.ts +0 -102
- package/src/harness/tools/builtin/changed-symbols-tool.ts +0 -48
- package/src/harness/tools/builtin/doc-context-tool.ts +0 -74
- package/src/harness/tools/builtin/file-outline-tool.ts +0 -66
- package/src/harness/tools/builtin/find-references-tool.ts +0 -66
- package/src/harness/tools/builtin/find-symbol-tool.ts +0 -91
- package/src/harness/tools/builtin/helpers.ts +0 -115
- package/src/harness/tools/builtin/impact-tool.ts +0 -48
- package/src/harness/tools/builtin/imports-exports-tool.ts +0 -53
- package/src/harness/tools/builtin/index.ts +0 -59
- package/src/harness/tools/builtin/list-files-tool.ts +0 -85
- package/src/harness/tools/builtin/pr-context-tool.ts +0 -41
- package/src/harness/tools/builtin/read-file-range-tool.ts +0 -73
- package/src/harness/tools/builtin/rule-signals-tool.ts +0 -36
- package/src/harness/tools/builtin/search-code-tool.ts +0 -65
- package/src/harness/tools/builtin/symbol-context-tool.ts +0 -106
- package/src/harness/tools/builtin/tests-for-symbol-tool.ts +0 -61
- package/src/harness/tools/path-guard.ts +0 -90
- package/src/harness/tools/result-limiter.ts +0 -49
- package/src/harness/tools/secret-redactor.ts +0 -54
- package/src/harness/tools/tool-cache.ts +0 -50
- package/src/harness/tools/tool-call-budget.ts +0 -60
- package/src/harness/tools/tool-context.ts +0 -5
- package/src/harness/tools/tool-middleware.ts +0 -105
- package/src/harness/tools/tool-policy.ts +0 -51
- package/src/harness/tools/tool-registry.ts +0 -112
- package/src/harness/tools/tool-result.ts +0 -15
- package/src/harness/tools/toolbox.ts +0 -36
- package/src/harness/validation/finding-validator.ts +0 -119
- package/src/tests/fixtures/fake-agent-tools.ts +0 -69
- package/src/tests/fixtures/fake-investigation.ts +0 -64
- package/src/tests/harness-agent.test.ts +0 -141
- package/src/tests/harness-investigation.test.ts +0 -206
- package/src/tests/harness-tools.test.ts +0 -148
- package/src/tests/hunt-command.test.ts +0 -212
- package/src/tests/rule-analyzer.test.ts +0 -61
- package/src/tests/tool-contracts.test.ts +0 -82
package/src/cli/help.ts
CHANGED
|
@@ -5,8 +5,12 @@ Comandos:
|
|
|
5
5
|
(nenhum) Prepara a revisão (classification + briefs + manifesto)
|
|
6
6
|
--finalize Consolida os findings em review.md
|
|
7
7
|
context Coleta o contexto da PR via gh (Passo 0; --pr/--worktree)
|
|
8
|
-
hunt Revisão nativa com IA (harness próprio): prepare + agentes + finalize
|
|
9
8
|
findings <sub> Triagem/correção dos findings (list · show · set · done · reset · sync · summary · batch · verify · submit · path)
|
|
9
|
+
hunt [flags] Harness local (maki): prepare → watchdogs via maki → finalize
|
|
10
|
+
(a skill /houndsman hunt continua existindo p/ agentes externos)
|
|
11
|
+
hunt install Baixa o maki fixado para <HOUNDSMAN_HOME>/tools/maki
|
|
12
|
+
hunt doctor Diagnóstico do harness local (opcional, não falha sem maki)
|
|
13
|
+
hunt uninstall Remove o binário gerenciado
|
|
10
14
|
status [ping] Mostra o status da run (ping renova o heartbeat da execução)
|
|
11
15
|
worktree <ação> Lista/cria/remove worktrees de revisão (list · add · remove · prune · path)
|
|
12
16
|
server Sobe a UI local de triagem (--port, --host, --open, --editor)
|
|
@@ -30,11 +34,13 @@ Flags:
|
|
|
30
34
|
--no-ai Desativa as features de IA do fluxo normal
|
|
31
35
|
--fail-on-blocking Exit 1 se houver bloqueantes (com --finalize)
|
|
32
36
|
|
|
33
|
-
Flags
|
|
34
|
-
--
|
|
35
|
-
--
|
|
36
|
-
--
|
|
37
|
-
--
|
|
37
|
+
Flags de hunt (harness local):
|
|
38
|
+
--allow-partial Finaliza mesmo se algum watchdog maki falhar
|
|
39
|
+
--no-context Não coleta o contexto da PR via gh
|
|
40
|
+
--hunt-model <m> Modelo do maki (provider/model-id)
|
|
41
|
+
--hunt-concurrency Processos maki simultâneos
|
|
42
|
+
--hunt-timeout Timeout por watchdog (ms)
|
|
43
|
+
--no-retry Não repete saída inválida
|
|
38
44
|
|
|
39
45
|
Flags de findings:
|
|
40
46
|
--decision <d> pending | execute | ignore
|
|
@@ -69,6 +75,8 @@ Persistência:
|
|
|
69
75
|
|
|
70
76
|
Ambiente (o Bun carrega .env automaticamente):
|
|
71
77
|
HOUNDSMAN_TYPE_SAFE_API_KEY Pré-análise Jev (opcional)
|
|
72
|
-
HOUNDSMAN_DEEP_SEEK_API_KEY IA (provider deepseek, opcional)
|
|
78
|
+
HOUNDSMAN_DEEP_SEEK_API_KEY IA (provider deepseek, opcional; reusada pelo hunt)
|
|
73
79
|
HOUNDSMAN_OPENAI_API_KEY IA (provider openai, opcional)
|
|
74
|
-
HOUNDSMAN_ANTHROPIC_API_KEY IA (provider anthropic, opcional)
|
|
80
|
+
HOUNDSMAN_ANTHROPIC_API_KEY IA (provider anthropic, opcional)
|
|
81
|
+
HOUNDSMAN_MAKI_PATH Binário do maki (harness local \`hunt\`)
|
|
82
|
+
HOUNDSMAN_MAKI_VERSION Versão do maki a instalar (default: fixada no pacote)`;
|
package/src/cli/index.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* --finalize Passo 3 — consolidar os findings em review.md
|
|
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
|
+
* hunt [flags] Harness local (maki): prepare → watchdogs via maki → finalize
|
|
18
19
|
* status [ping] Mostra o status da run; `ping` renova o heartbeat da execução
|
|
19
20
|
* server Sobe a UI local de triagem (Bun.serve)
|
|
20
21
|
* setup Cria a configuração inicial do projeto (config + skills)
|
|
@@ -30,7 +31,7 @@
|
|
|
30
31
|
import type { HoundsmanConfig } from "../core/config";
|
|
31
32
|
import { loadConfig } from "../core/config-loader";
|
|
32
33
|
import { HoundsmanEngine } from "../core/engine/houndsman-engine";
|
|
33
|
-
import { logger } from "../core/logging";
|
|
34
|
+
import { formatDuration, logger } from "../core/logging";
|
|
34
35
|
import { syncProjectLayerForRoot } from "../core/services/worktree-manager";
|
|
35
36
|
import { ArgParser } from "./arg-parser";
|
|
36
37
|
import type { CliContext } from "./commands/command";
|
|
@@ -90,14 +91,26 @@ export async function main(): Promise<void> {
|
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
let loaded: Awaited<ReturnType<typeof loadConfig>> | null = null;
|
|
94
|
+
// `--json` é contrato de máquina: nada de progresso no stdout.
|
|
95
|
+
const silentProgress = parser.has("--json");
|
|
93
96
|
const config = async (): Promise<HoundsmanConfig> => {
|
|
94
97
|
loaded ??= await (async () => {
|
|
98
|
+
const started = Date.now();
|
|
99
|
+
if (!silentProgress) logger.step("Carregando a configuração do projeto…");
|
|
95
100
|
// Rodando dentro de um worktree, a camada gerada (`.houndsman/core`)
|
|
96
101
|
// não existe (é ignorada pelo git). Repõe a partir do clone principal
|
|
97
102
|
// antes de importar a config — sem isso os watchdogs não resolvem
|
|
98
103
|
// `../core/types`. No clone principal é no-op.
|
|
99
104
|
syncProjectLayerForRoot(process.cwd());
|
|
100
|
-
|
|
105
|
+
const result = await loadConfig({ configPath: parser.value("--config") });
|
|
106
|
+
if (!silentProgress) {
|
|
107
|
+
logger.success(
|
|
108
|
+
`Configuração: ${result.config.projectName} · ` +
|
|
109
|
+
`${result.config.watchdogIds.length} watchdog(s) ` +
|
|
110
|
+
`(${formatDuration(Date.now() - started)})`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
return result;
|
|
101
114
|
})();
|
|
102
115
|
return loaded.config;
|
|
103
116
|
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Progresso do CLI por etapa do prepare (apresentação).
|
|
3
|
+
*
|
|
4
|
+
* O core emite eventos (`PrepareProgressEvent`); aqui eles viram spinner/linha
|
|
5
|
+
* com a duração de cada fase — o `hunt` começa a dar sinal de vida antes do
|
|
6
|
+
* primeiro `Discovery:`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { formatDuration, type LoggerLike, PhaseReporter } from "../core/logging";
|
|
10
|
+
import type { PrepareProgress, PrepareStage } from "../core/orchestrator";
|
|
11
|
+
|
|
12
|
+
/** Texto de cada etapa (início no gerúndio, fim no particípio). */
|
|
13
|
+
export const PREPARE_STAGE_TEXT: Record<PrepareStage, { start: string; done: string }> = {
|
|
14
|
+
diff: { start: "Lendo o diff da revisão", done: "Diff lido" },
|
|
15
|
+
ast: { start: "Analisando a AST dos arquivos alterados", done: "AST analisada" },
|
|
16
|
+
classify: {
|
|
17
|
+
start: "Classificando os arquivos por watchdog",
|
|
18
|
+
done: "Classificação concluída",
|
|
19
|
+
},
|
|
20
|
+
impact: { start: "Medindo o impacto nos importadores", done: "Impacto calculado" },
|
|
21
|
+
jev: { start: "Pré-analisando o diff (Jev/IA)", done: "Pré-análise concluída" },
|
|
22
|
+
context: { start: "Resumindo o contexto da PR", done: "Contexto resumido" },
|
|
23
|
+
technical: {
|
|
24
|
+
start: "Consolidando os pontos técnicos",
|
|
25
|
+
done: "Pontos técnicos prontos",
|
|
26
|
+
},
|
|
27
|
+
"ai-plans": { start: "Planejando os briefs com IA", done: "Planos de brief prontos" },
|
|
28
|
+
briefs: { start: "Gerando os briefs", done: "Briefs gerados" },
|
|
29
|
+
manifest: { start: "Gravando o manifesto da revisão", done: "Manifesto gravado" },
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** Cria o callback de progresso do prepare ligado ao logger do CLI. */
|
|
33
|
+
export function prepareProgress(logger: LoggerLike): PrepareProgress {
|
|
34
|
+
const reporter = new PhaseReporter(logger);
|
|
35
|
+
return (event) => {
|
|
36
|
+
const text = PREPARE_STAGE_TEXT[event.stage];
|
|
37
|
+
if (event.type === "start") reporter.begin(event.stage, text.start);
|
|
38
|
+
else reporter.end(event.stage, text.done, event.durationMs);
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Log de um passo concluído com a duração (usado fora do prepare). */
|
|
43
|
+
export function stepDone(logger: LoggerLike, message: string, startedMs: number): void {
|
|
44
|
+
logger.success(`${message} (${formatDuration(Date.now() - startedMs)})`);
|
|
45
|
+
}
|
package/src/core/ai/ai-client.ts
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
* Fachada de IA (`core/ai`) — fail-open, budget e uso.
|
|
3
3
|
*
|
|
4
4
|
* As features do core usam `generateObject`/`generateText` e tratam `ok:false`
|
|
5
|
-
* como "sem IA": a revisão determinística segue idêntica.
|
|
6
|
-
* `requireProvider` e falha com mensagem clara quando a IA não está configurada.
|
|
5
|
+
* como "sem IA": a revisão determinística segue idêntica.
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
8
|
import type { ZodType } from "zod";
|
|
@@ -11,7 +10,7 @@ import type { AiProviderOptions } from "../config";
|
|
|
11
10
|
import type { ProjectConfig } from "../project-config";
|
|
12
11
|
import { AiConfigurationError, errorMessage } from "./errors";
|
|
13
12
|
import { ModelFactory } from "./model-factory";
|
|
14
|
-
import type { AiResult,
|
|
13
|
+
import type { AiResult, AiUsage, ModelProvider, ModelRequest } from "./types";
|
|
15
14
|
import { AiUsageBudget } from "./usage-budget";
|
|
16
15
|
import { AiUsageRecorder } from "./usage-recorder";
|
|
17
16
|
|
|
@@ -30,8 +29,6 @@ export type ObjectRequest<T> = {
|
|
|
30
29
|
instructions?: string;
|
|
31
30
|
prompt: string;
|
|
32
31
|
outputSchema: ZodType<T>;
|
|
33
|
-
tools?: Record<string, AiTool>;
|
|
34
|
-
maxSteps?: number;
|
|
35
32
|
maxOutputTokens?: number;
|
|
36
33
|
temperature?: number;
|
|
37
34
|
/** Opções do AI SDK por provider (mescladas com a config do projeto). */
|
|
@@ -93,7 +90,7 @@ export class AiClient {
|
|
|
93
90
|
return this.factory.isConfigured;
|
|
94
91
|
}
|
|
95
92
|
|
|
96
|
-
/** Provider obrigatório
|
|
93
|
+
/** Provider obrigatório (lança quando a IA não está configurada). */
|
|
97
94
|
async requireProvider(model?: string): Promise<ModelProvider> {
|
|
98
95
|
if (this.override === null) {
|
|
99
96
|
throw new AiConfigurationError(
|
|
@@ -146,7 +143,6 @@ export class AiClient {
|
|
|
146
143
|
);
|
|
147
144
|
const result = await this.generateText("doctor-ping", {
|
|
148
145
|
prompt: "Responda apenas OK.",
|
|
149
|
-
maxSteps: 1,
|
|
150
146
|
maxOutputTokens: 4,
|
|
151
147
|
temperature: 0,
|
|
152
148
|
signal: AbortSignal.timeout(timeoutMs),
|
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Adapter do AI SDK (`generateText` + `Output.object`
|
|
2
|
+
* Adapter do AI SDK (`generateText` + `Output.object`).
|
|
3
3
|
*
|
|
4
4
|
* O resto do motor depende só de `ModelProvider`; trocar de SDK é trocar este
|
|
5
|
-
* arquivo.
|
|
6
|
-
* via `AbortSignal`.
|
|
5
|
+
* arquivo. Timeout/cancelamento vêm do `AbortSignal`.
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
|
-
import type { LanguageModel
|
|
10
|
-
import { generateText, Output
|
|
8
|
+
import type { LanguageModel } from "ai";
|
|
9
|
+
import { generateText, Output } from "ai";
|
|
11
10
|
import type { AiProviderKind } from "../config";
|
|
12
11
|
import { AiProviderError } from "./errors";
|
|
13
12
|
import { temperatureApplies } from "./provider-capabilities";
|
|
14
13
|
import {
|
|
15
|
-
type AiTool,
|
|
16
14
|
type AiUsage,
|
|
17
15
|
emptyUsage,
|
|
18
16
|
type ModelProvider,
|
|
@@ -20,19 +18,6 @@ import {
|
|
|
20
18
|
type ModelResponse,
|
|
21
19
|
} from "./types";
|
|
22
20
|
|
|
23
|
-
/** Converte as tools do motor para o formato do AI SDK. */
|
|
24
|
-
function toToolSet(tools: Record<string, AiTool>): ToolSet {
|
|
25
|
-
const entries = Object.entries(tools).map(([name, item]) => [
|
|
26
|
-
name,
|
|
27
|
-
tool({
|
|
28
|
-
description: item.description,
|
|
29
|
-
inputSchema: item.inputSchema,
|
|
30
|
-
execute: async (input: unknown) => item.execute(input),
|
|
31
|
-
}),
|
|
32
|
-
]);
|
|
33
|
-
return Object.fromEntries(entries) as ToolSet;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
21
|
/** Mapeia o uso do SDK de forma defensiva (campos opcionais por provider). */
|
|
37
22
|
export function mapUsage(raw: unknown): AiUsage {
|
|
38
23
|
const usage = (raw ?? {}) as Record<string, unknown>;
|
|
@@ -46,6 +31,43 @@ function numberOrZero(value: unknown): number {
|
|
|
46
31
|
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
47
32
|
}
|
|
48
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Silencia o emissor default de avisos do AI SDK.
|
|
36
|
+
*
|
|
37
|
+
* O default usa `process.emitWarning` — com stack trace — a cada chamada, o que
|
|
38
|
+
* polui o stderr do CLI. Os avisos continuam disponíveis no resultado da
|
|
39
|
+
* chamada (`ModelResponse.warnings`). Um chamador que queira o comportamento
|
|
40
|
+
* original basta definir `AI_SDK_LOG_WARNINGS` antes de usar o SDK.
|
|
41
|
+
*/
|
|
42
|
+
function installWarningSink(): void {
|
|
43
|
+
if (globalThis.AI_SDK_LOG_WARNINGS === undefined) {
|
|
44
|
+
globalThis.AI_SDK_LOG_WARNINGS = () => {};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
installWarningSink();
|
|
49
|
+
|
|
50
|
+
/** Normaliza um aviso do provider em texto legível (puro). */
|
|
51
|
+
export function formatSdkWarning(raw: unknown): string {
|
|
52
|
+
const warning = (raw ?? {}) as Record<string, unknown>;
|
|
53
|
+
const label = [warning.type, warning.feature, warning.setting]
|
|
54
|
+
.filter((value): value is string => typeof value === "string" && value !== "")
|
|
55
|
+
.join(": ");
|
|
56
|
+
const detail =
|
|
57
|
+
typeof warning.details === "string"
|
|
58
|
+
? warning.details
|
|
59
|
+
: typeof warning.message === "string"
|
|
60
|
+
? warning.message
|
|
61
|
+
: "";
|
|
62
|
+
const head = label || "aviso";
|
|
63
|
+
return detail ? `${head} — ${detail}` : head;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Lista de avisos do provider em texto (puro). */
|
|
67
|
+
export function formatSdkWarnings(raw: unknown): string[] {
|
|
68
|
+
return Array.isArray(raw) ? raw.map(formatSdkWarning) : [];
|
|
69
|
+
}
|
|
70
|
+
|
|
49
71
|
/** Argumentos aceitos por `generateText` (inclui `providerOptions` do SDK). */
|
|
50
72
|
type GenerateTextArgs = Parameters<typeof generateText>[0];
|
|
51
73
|
|
|
@@ -61,9 +83,6 @@ export function buildGenerateTextArgs<T>(
|
|
|
61
83
|
provider: AiProviderKind,
|
|
62
84
|
modelId: string,
|
|
63
85
|
) {
|
|
64
|
-
const tools = request.tools ? toToolSet(request.tools) : undefined;
|
|
65
|
-
const hasTools = tools !== undefined && Object.keys(tools).length > 0;
|
|
66
|
-
const maxSteps = request.maxSteps ?? 1;
|
|
67
86
|
const providerOptions = request.providerOptions
|
|
68
87
|
? (request.providerOptions as GenerateTextArgs["providerOptions"])
|
|
69
88
|
: undefined;
|
|
@@ -76,7 +95,6 @@ export function buildGenerateTextArgs<T>(
|
|
|
76
95
|
model,
|
|
77
96
|
system: request.instructions,
|
|
78
97
|
prompt: request.prompt,
|
|
79
|
-
...(hasTools ? { tools, stopWhen: stepCountIs(maxSteps) } : {}),
|
|
80
98
|
...(request.outputSchema
|
|
81
99
|
? { output: Output.object({ schema: request.outputSchema }) }
|
|
82
100
|
: {}),
|
|
@@ -105,11 +123,12 @@ export class AiSdkModelProvider implements ModelProvider {
|
|
|
105
123
|
const output = request.outputSchema
|
|
106
124
|
? (result.output as T)
|
|
107
125
|
: ((result.text ?? "") as unknown as T);
|
|
126
|
+
const warnings = formatSdkWarnings(result.warnings);
|
|
108
127
|
return {
|
|
109
128
|
output,
|
|
110
129
|
usage: mapUsage(result.usage),
|
|
111
|
-
steps: result.steps?.length ?? 1,
|
|
112
130
|
finishReason: result.finishReason ?? "unknown",
|
|
131
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
113
132
|
};
|
|
114
133
|
} catch (error) {
|
|
115
134
|
if (error instanceof Error && error.name === "AbortError") {
|
|
@@ -23,7 +23,6 @@ export class AiContextSummarizer {
|
|
|
23
23
|
instructions: contextInstructions(),
|
|
24
24
|
prompt: contextPrompt(raw),
|
|
25
25
|
outputSchema: ContextSummarySchema,
|
|
26
|
-
maxSteps: 1,
|
|
27
26
|
});
|
|
28
27
|
if (!result.ok) return null;
|
|
29
28
|
return parseContextSummary({ ...result.output, available: true });
|
package/src/core/ai/index.ts
CHANGED
|
@@ -10,7 +10,12 @@ export type {
|
|
|
10
10
|
TextRequest,
|
|
11
11
|
} from "./ai-client";
|
|
12
12
|
export { AiClient } from "./ai-client";
|
|
13
|
-
export {
|
|
13
|
+
export {
|
|
14
|
+
AiSdkModelProvider,
|
|
15
|
+
formatSdkWarning,
|
|
16
|
+
formatSdkWarnings,
|
|
17
|
+
mapUsage,
|
|
18
|
+
} from "./ai-sdk-model-provider";
|
|
14
19
|
export {
|
|
15
20
|
AiBudgetError,
|
|
16
21
|
AiConfigurationError,
|
|
@@ -28,7 +33,6 @@ export {
|
|
|
28
33
|
type AiCandidate,
|
|
29
34
|
type AiClassification,
|
|
30
35
|
type AiVerdict,
|
|
31
|
-
type AiWatchdogReport,
|
|
32
36
|
type BriefPlan,
|
|
33
37
|
BriefPlanSchema,
|
|
34
38
|
CandidateSchema,
|
|
@@ -39,11 +43,9 @@ export {
|
|
|
39
43
|
type TechnicalSummaryPlan,
|
|
40
44
|
TechnicalSummarySchema,
|
|
41
45
|
VerifySchema,
|
|
42
|
-
WatchdogReportSchema,
|
|
43
46
|
} from "./schemas";
|
|
44
47
|
export {
|
|
45
48
|
type AiResult,
|
|
46
|
-
type AiTool,
|
|
47
49
|
type AiUsage,
|
|
48
50
|
addUsage,
|
|
49
51
|
emptyUsage,
|
|
@@ -20,7 +20,8 @@ export function preAnalysisInstructions(): string {
|
|
|
20
20
|
return buildSystem(
|
|
21
21
|
"Você faz uma pré-análise de possíveis violações de regras em um diff.",
|
|
22
22
|
[
|
|
23
|
-
"Produza candidatos apenas quando houver indício concreto.",
|
|
23
|
+
"Produza candidatos apenas quando houver indício concreto no trecho alterado.",
|
|
24
|
+
"O trecho fornecido é o diff (linhas alteradas); código não alterado é apenas contexto.",
|
|
24
25
|
"Use o texto de uma das regras fornecidas.",
|
|
25
26
|
"Confiança calibrada entre 0 e 1; prefira valores baixos na dúvida.",
|
|
26
27
|
"Não invente arquivos.",
|
|
@@ -36,7 +37,7 @@ export function preAnalysisPrompt(input: PreAnalysisPromptInput): string {
|
|
|
36
37
|
"## Regras",
|
|
37
38
|
...input.rules.map((rule) => `- [${rule.severity}] ${rule.text}`),
|
|
38
39
|
"",
|
|
39
|
-
"## Arquivos e trechos",
|
|
40
|
+
"## Arquivos e trechos alterados (diff)",
|
|
40
41
|
...input.files.flatMap((item) => [
|
|
41
42
|
`### ${item.file}`,
|
|
42
43
|
"```",
|
package/src/core/ai/schemas.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Schemas Zod da camada de IA.
|
|
3
3
|
*
|
|
4
|
-
* Espelham os contratos existentes (`ContextSummary
|
|
5
|
-
* planos das features. Todo schema é fechado por validação: saída inválida do
|
|
4
|
+
* Espelham os contratos existentes (`ContextSummary`) e os planos das features. Todo schema é fechado por validação: saída inválida do
|
|
6
5
|
* modelo vira falha fail-open, nunca dado corrompido.
|
|
7
6
|
*/
|
|
8
7
|
|
|
@@ -89,27 +88,6 @@ export const CandidateSchema = z.object({
|
|
|
89
88
|
});
|
|
90
89
|
export type AiCandidate = z.infer<typeof CandidateSchema>["candidates"][number];
|
|
91
90
|
|
|
92
|
-
/** Relatório de um watchdog (espelha `OUTPUT-FORMAT.md`). */
|
|
93
|
-
export const WatchdogReportSchema = z.object({
|
|
94
|
-
watchdogId: z.string(),
|
|
95
|
-
reviewedFiles: z.array(z.string()).default([]),
|
|
96
|
-
skipped: z.array(z.string()).default([]),
|
|
97
|
-
findings: z
|
|
98
|
-
.array(
|
|
99
|
-
z.object({
|
|
100
|
-
rule: z.string(),
|
|
101
|
-
severity: SeveritySchema,
|
|
102
|
-
file: z.string(),
|
|
103
|
-
line: z.number().int().positive().optional(),
|
|
104
|
-
message: z.string(),
|
|
105
|
-
action: z.string(),
|
|
106
|
-
evidence: z.string().optional(),
|
|
107
|
-
}),
|
|
108
|
-
)
|
|
109
|
-
.default([]),
|
|
110
|
-
});
|
|
111
|
-
export type AiWatchdogReport = z.infer<typeof WatchdogReportSchema>;
|
|
112
|
-
|
|
113
91
|
/** Veredito de verificação (confirma/refuta um finding ambíguo). */
|
|
114
92
|
export const VerifySchema = z.object({
|
|
115
93
|
verdicts: z
|
package/src/core/ai/types.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Contratos genéricos da camada de IA (biblioteca).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* harness nativo (`src/harness/`) consome estes contratos.
|
|
4
|
+
* Expõe modelo, uso e resultado; as features do core são as únicas consumidoras.
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
|
-
import type { ZodType
|
|
7
|
+
import type { ZodType } from "zod";
|
|
9
8
|
import type { AiProviderKind, AiProviderOptions } from "../config";
|
|
10
9
|
|
|
11
10
|
/** Uso de tokens de uma chamada (ou acumulado de uma review). */
|
|
@@ -31,25 +30,14 @@ export function addUsage(a: AiUsage, b: AiUsage): AiUsage {
|
|
|
31
30
|
};
|
|
32
31
|
}
|
|
33
32
|
|
|
34
|
-
/** Tool exposta ao modelo (execução já vinculada a um contexto). */
|
|
35
|
-
export type AiTool = {
|
|
36
|
-
name: string;
|
|
37
|
-
description: string;
|
|
38
|
-
inputSchema: ZodTypeAny;
|
|
39
|
-
execute(input: unknown): Promise<string>;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
33
|
/** Pedido de geração de modelo (texto ou objeto estruturado). */
|
|
43
34
|
export type ModelRequest<T> = {
|
|
44
35
|
/** Instruções de sistema. */
|
|
45
36
|
instructions?: string;
|
|
46
37
|
/** Prompt do usuário. */
|
|
47
38
|
prompt: string;
|
|
48
|
-
/** Tools disponíveis para o loop do modelo. */
|
|
49
|
-
tools?: Record<string, AiTool>;
|
|
50
39
|
/** Schema de saída (quando ausente, a saída é texto). */
|
|
51
40
|
outputSchema?: ZodType<T>;
|
|
52
|
-
maxSteps?: number;
|
|
53
41
|
maxOutputTokens?: number;
|
|
54
42
|
temperature?: number;
|
|
55
43
|
/** Opções do AI SDK por provider (repassadas ao SDK sem interpretação). */
|
|
@@ -61,8 +49,9 @@ export type ModelRequest<T> = {
|
|
|
61
49
|
export type ModelResponse<T> = {
|
|
62
50
|
output: T;
|
|
63
51
|
usage: AiUsage;
|
|
64
|
-
steps: number;
|
|
65
52
|
finishReason: string;
|
|
53
|
+
/** Avisos do provider já normalizados em texto (ex.: modo de compatibilidade). */
|
|
54
|
+
warnings?: string[];
|
|
66
55
|
};
|
|
67
56
|
|
|
68
57
|
/** Provider de modelo (adaptador do AI SDK ou fake nos testes). */
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Orçamento de IA por review (chamadas e tokens).
|
|
3
3
|
*
|
|
4
4
|
* O budget é consultado antes de cada chamada e atualizado com o uso real. Ao
|
|
5
|
-
* estourar, o chamador decide parar (features do core)
|
|
5
|
+
* estourar, o chamador decide parar a operação (features do core).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { AiBudgetError } from "./errors";
|
package/src/core/ast/diff.ts
CHANGED
|
@@ -14,7 +14,7 @@ import type {
|
|
|
14
14
|
AstSymbol,
|
|
15
15
|
} from "./types";
|
|
16
16
|
|
|
17
|
-
/** Um hunk do patch, com as linhas novas adicionadas. */
|
|
17
|
+
/** Um hunk do patch, com as linhas novas adicionadas e as âncoras de remoção. */
|
|
18
18
|
export type AstHunk = {
|
|
19
19
|
oldStart: number;
|
|
20
20
|
oldLines: number;
|
|
@@ -22,6 +22,11 @@ export type AstHunk = {
|
|
|
22
22
|
newLines: number;
|
|
23
23
|
/** Linhas novas (1-based) introduzidas pelo hunk. */
|
|
24
24
|
addedLines: number[];
|
|
25
|
+
/**
|
|
26
|
+
* Posições (1-based, no lado novo) onde linhas foram removidas. Uma remoção
|
|
27
|
+
* pura não tem linha nova, mas marca o ponto do arquivo que ela alterou.
|
|
28
|
+
*/
|
|
29
|
+
deletedAt: number[];
|
|
25
30
|
};
|
|
26
31
|
|
|
27
32
|
/** Regex do cabeçalho de hunk (`@@ -a,b +c,d @@`). */
|
|
@@ -52,6 +57,7 @@ export function parseHunks(patch: string): AstHunk[] {
|
|
|
52
57
|
newStart: Number.parseInt(match[3], 10),
|
|
53
58
|
newLines: countOf(match[4]),
|
|
54
59
|
addedLines: [],
|
|
60
|
+
deletedAt: [],
|
|
55
61
|
};
|
|
56
62
|
newLine = current.newStart;
|
|
57
63
|
hunks.push(current);
|
|
@@ -68,7 +74,11 @@ export function parseHunks(patch: string): AstHunk[] {
|
|
|
68
74
|
newLine++;
|
|
69
75
|
continue;
|
|
70
76
|
}
|
|
71
|
-
if (line.startsWith("-"))
|
|
77
|
+
if (line.startsWith("-")) {
|
|
78
|
+
// Remoção pura não consome linha nova: ancora no ponto atual do arquivo.
|
|
79
|
+
current.deletedAt.push(Math.max(1, newLine));
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
72
82
|
if (line.startsWith("\\")) continue;
|
|
73
83
|
if (line.startsWith(" ")) newLine++;
|
|
74
84
|
}
|
|
@@ -85,6 +95,31 @@ export function changedNewLines(patch: string): Set<number> {
|
|
|
85
95
|
return lines;
|
|
86
96
|
}
|
|
87
97
|
|
|
98
|
+
/** Âncoras de alteração do patch: adições e posições de remoção no lado novo. */
|
|
99
|
+
export type ChangedAnchors = {
|
|
100
|
+
/** Linhas novas (1-based) adicionadas. */
|
|
101
|
+
added: Set<number>;
|
|
102
|
+
/** Posições (1-based, lado novo) onde houve remoção. */
|
|
103
|
+
deletedAt: Set<number>;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/** Extrai as âncoras de alteração de um patch (falha aberta: patch vazio → vazio). */
|
|
107
|
+
export function changedAnchors(patch: string): ChangedAnchors {
|
|
108
|
+
const added = new Set<number>();
|
|
109
|
+
const deletedAt = new Set<number>();
|
|
110
|
+
for (const hunk of parseHunks(patch)) {
|
|
111
|
+
for (const line of hunk.addedLines) added.add(line);
|
|
112
|
+
for (const line of hunk.deletedAt) deletedAt.add(line);
|
|
113
|
+
}
|
|
114
|
+
return { added, deletedAt };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** União das âncoras (adições + remoções) — o que a mudança tocou no arquivo. */
|
|
118
|
+
export function changedAnchorLines(patch: string): Set<number> {
|
|
119
|
+
const { added, deletedAt } = changedAnchors(patch);
|
|
120
|
+
return new Set([...added, ...deletedAt]);
|
|
121
|
+
}
|
|
122
|
+
|
|
88
123
|
/** Chave estável de um símbolo (espécie + nome). */
|
|
89
124
|
function symbolKey(symbol: AstSymbol): string {
|
|
90
125
|
return `${symbol.kind}:${symbol.name}`;
|
package/src/core/ast/service.ts
CHANGED
|
@@ -162,7 +162,7 @@ export class AstService {
|
|
|
162
162
|
return buildImpact(analyses, importersByFile);
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
/** Fatos estruturais do arquivo atual (usado
|
|
165
|
+
/** Fatos estruturais do arquivo atual (usado pelo motor e pelo server). */
|
|
166
166
|
async factsOf(filePath: string): Promise<AstFacts | null> {
|
|
167
167
|
if (!this.enabled) return null;
|
|
168
168
|
const content = this.readWorkingFile(filePath);
|