@payconductor/houndsman 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/README.md +14 -13
  2. package/package.json +1 -1
  3. package/src/cli/commands/doctor-command.ts +22 -0
  4. package/src/cli/commands/hunt-command.ts +440 -29
  5. package/src/cli/commands/review-command.ts +20 -2
  6. package/src/cli/help.ts +16 -8
  7. package/src/cli/index.ts +15 -2
  8. package/src/cli/progress.ts +45 -0
  9. package/src/core/ai/ai-client.ts +3 -7
  10. package/src/core/ai/ai-sdk-model-provider.ts +43 -24
  11. package/src/core/ai/features/brief-enricher.ts +0 -1
  12. package/src/core/ai/features/classifier.ts +0 -1
  13. package/src/core/ai/features/context-summarizer.ts +0 -1
  14. package/src/core/ai/features/pre-analyzer.ts +0 -1
  15. package/src/core/ai/features/technical-summarizer.ts +0 -1
  16. package/src/core/ai/features/verifier.ts +0 -1
  17. package/src/core/ai/index.ts +6 -4
  18. package/src/core/ai/prompts/pre-analysis.ts +3 -2
  19. package/src/core/ai/schemas.ts +1 -23
  20. package/src/core/ai/types.ts +4 -15
  21. package/src/core/ai/usage-budget.ts +1 -1
  22. package/src/core/ast/diff.ts +37 -2
  23. package/src/core/ast/service.ts +1 -1
  24. package/src/core/brief.ts +54 -5
  25. package/src/core/changed-scope.ts +207 -0
  26. package/src/core/commands/scaffold.ts +12 -18
  27. package/src/core/config.ts +30 -15
  28. package/src/core/defaults.ts +14 -5
  29. package/src/core/engine/review-scope.ts +131 -17
  30. package/src/core/engine/review-session.ts +7 -3
  31. package/src/core/engine/review-workspace.ts +14 -0
  32. package/src/core/env.ts +14 -0
  33. package/src/core/hunt/hunt-runner.ts +328 -0
  34. package/src/core/hunt/index.ts +59 -0
  35. package/src/core/hunt/maki-install.ts +331 -0
  36. package/src/core/hunt/maki-model.ts +101 -0
  37. package/src/core/hunt/maki-prompt.ts +71 -0
  38. package/src/core/hunt/maki-release.ts +104 -0
  39. package/src/core/hunt/maki-runner.ts +276 -0
  40. package/src/core/hunt/pool.ts +30 -0
  41. package/src/core/jev/jev-detector.ts +7 -2
  42. package/src/core/logging/index.ts +3 -1
  43. package/src/core/logging/logger.ts +22 -3
  44. package/src/core/logging/progress.ts +282 -0
  45. package/src/core/orchestrator.ts +173 -59
  46. package/src/core/project-config.ts +34 -24
  47. package/src/core/registry-check.ts +2 -12
  48. package/src/core/rule-text.ts +3 -55
  49. package/src/core/services/git-repository.ts +8 -0
  50. package/src/core/storage/types.ts +1 -1
  51. package/src/core/types.ts +3 -14
  52. package/src/index.ts +74 -3
  53. package/src/server/client/utils/constants.ts +3 -6
  54. package/src/server/client/utils/types.ts +0 -1
  55. package/src/skill/OUTPUT-FORMAT.md +5 -0
  56. package/src/skill/README.md +14 -1
  57. package/src/skill/SETUP.md +22 -2
  58. package/src/skill/SKILL.md +4 -0
  59. package/src/skill/fix.md +1 -1
  60. package/src/skill/hunt.md +38 -6
  61. package/src/tests/__snapshots__/baseline.test.ts.snap +69 -32
  62. package/src/tests/ai-core.test.ts +38 -1
  63. package/src/tests/changed-scope.test.ts +197 -0
  64. package/src/tests/cli.test.ts +1 -0
  65. package/src/tests/fallback.test.ts +1 -96
  66. package/src/tests/fixtures/fake-model-provider.ts +0 -1
  67. package/src/tests/hunt-cli.test.ts +185 -0
  68. package/src/tests/hunt-install.test.ts +153 -0
  69. package/src/tests/hunt-maki.test.ts +467 -0
  70. package/src/tests/progress.test.ts +117 -0
  71. package/src/tests/review-scope.test.ts +95 -0
  72. package/src/tests/worktree.test.ts +1 -3
  73. package/src/harness/README.md +0 -15
  74. package/src/harness/agent/agent-context.ts +0 -33
  75. package/src/harness/agent/agent-errors.ts +0 -30
  76. package/src/harness/agent/agent-factory.ts +0 -46
  77. package/src/harness/agent/agent-reporter.ts +0 -34
  78. package/src/harness/agent/agent-result.ts +0 -16
  79. package/src/harness/agent/agent-runner.ts +0 -32
  80. package/src/harness/agent/agent-scheduler.ts +0 -37
  81. package/src/harness/agent/review-agent.ts +0 -401
  82. package/src/harness/config/harness-config.ts +0 -89
  83. package/src/harness/hunt/hunt-harness.ts +0 -83
  84. package/src/harness/hunt/hunt-options.ts +0 -92
  85. package/src/harness/hunt/hunt-result.ts +0 -17
  86. package/src/harness/hunt/hunt-session.ts +0 -359
  87. package/src/harness/hunt/review-planner.ts +0 -98
  88. package/src/harness/index.ts +0 -36
  89. package/src/harness/investigation/agent-state.ts +0 -134
  90. package/src/harness/investigation/evidence-verifier.ts +0 -129
  91. package/src/harness/investigation/evidence.ts +0 -110
  92. package/src/harness/investigation/finding-critic.ts +0 -96
  93. package/src/harness/investigation/investigation-planner.ts +0 -118
  94. package/src/harness/investigation/investigation-task.ts +0 -10
  95. package/src/harness/investigation/review-journal.ts +0 -87
  96. package/src/harness/investigation/rule-analyzer.ts +0 -178
  97. package/src/harness/investigation/rule-signals.ts +0 -110
  98. package/src/harness/prompts/agent-schemas.ts +0 -57
  99. package/src/harness/prompts/reviewer-prompt-builder.ts +0 -101
  100. package/src/harness/prompts/states/conclude-prompt.ts +0 -16
  101. package/src/harness/prompts/states/investigate-prompt.ts +0 -37
  102. package/src/harness/prompts/states/orient-prompt.ts +0 -15
  103. package/src/harness/prompts/states/verify-prompt.ts +0 -22
  104. package/src/harness/tools/agent-tool.ts +0 -102
  105. package/src/harness/tools/builtin/changed-symbols-tool.ts +0 -48
  106. package/src/harness/tools/builtin/doc-context-tool.ts +0 -74
  107. package/src/harness/tools/builtin/file-outline-tool.ts +0 -66
  108. package/src/harness/tools/builtin/find-references-tool.ts +0 -66
  109. package/src/harness/tools/builtin/find-symbol-tool.ts +0 -91
  110. package/src/harness/tools/builtin/helpers.ts +0 -115
  111. package/src/harness/tools/builtin/impact-tool.ts +0 -48
  112. package/src/harness/tools/builtin/imports-exports-tool.ts +0 -53
  113. package/src/harness/tools/builtin/index.ts +0 -59
  114. package/src/harness/tools/builtin/list-files-tool.ts +0 -85
  115. package/src/harness/tools/builtin/pr-context-tool.ts +0 -41
  116. package/src/harness/tools/builtin/read-file-range-tool.ts +0 -73
  117. package/src/harness/tools/builtin/rule-signals-tool.ts +0 -36
  118. package/src/harness/tools/builtin/search-code-tool.ts +0 -65
  119. package/src/harness/tools/builtin/symbol-context-tool.ts +0 -106
  120. package/src/harness/tools/builtin/tests-for-symbol-tool.ts +0 -61
  121. package/src/harness/tools/path-guard.ts +0 -90
  122. package/src/harness/tools/result-limiter.ts +0 -49
  123. package/src/harness/tools/secret-redactor.ts +0 -54
  124. package/src/harness/tools/tool-cache.ts +0 -50
  125. package/src/harness/tools/tool-call-budget.ts +0 -60
  126. package/src/harness/tools/tool-context.ts +0 -5
  127. package/src/harness/tools/tool-middleware.ts +0 -105
  128. package/src/harness/tools/tool-policy.ts +0 -51
  129. package/src/harness/tools/tool-registry.ts +0 -112
  130. package/src/harness/tools/tool-result.ts +0 -15
  131. package/src/harness/tools/toolbox.ts +0 -36
  132. package/src/harness/validation/finding-validator.ts +0 -119
  133. package/src/tests/fixtures/fake-agent-tools.ts +0 -69
  134. package/src/tests/fixtures/fake-investigation.ts +0 -64
  135. package/src/tests/harness-agent.test.ts +0 -141
  136. package/src/tests/harness-investigation.test.ts +0 -206
  137. package/src/tests/harness-tools.test.ts +0 -148
  138. package/src/tests/hunt-command.test.ts +0 -212
  139. package/src/tests/rule-analyzer.test.ts +0 -61
  140. package/src/tests/tool-contracts.test.ts +0 -82
package/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 do hunt:
34
- --base <b> · --head <ref> · --branch <nome> · --watchdog <id> · --pr <n> · --worktree <path>
35
- --full · --clean · --no-ast · --with-jev
36
- --resume · --allow-partial · --dry-run · --fail-on-blocking
37
- --max-calls <n> · --concurrency <n> · --provider <p> · --model <m>
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
- return loadConfig({ configPath: parser.value("--config") });
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
+ }
@@ -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. O harness nativo usa
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, AiTool, AiUsage, ModelProvider, ModelRequest } from "./types";
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 — usado pelo harness (falha clara sem IA). */
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` + tools).
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. O loop de tools é do SDK (`stepCountIs`), com timeout/cancelamento
6
- * via `AbortSignal`.
5
+ * arquivo. Timeout/cancelamento vêm do `AbortSignal`.
7
6
  */
8
7
 
9
- import type { LanguageModel, ToolSet } from "ai";
10
- import { generateText, Output, stepCountIs, tool } from "ai";
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") {
@@ -27,7 +27,6 @@ export class AiBriefEnricher {
27
27
  instructions: briefInstructions(),
28
28
  prompt: briefPrompt(input),
29
29
  outputSchema: BriefPlanSchema,
30
- maxSteps: 1,
31
30
  maxOutputTokens: 700,
32
31
  },
33
32
  this.client.projectConfig.aiModelFor(input.watchdogId),
@@ -37,7 +37,6 @@ export class AiClassifier {
37
37
  instructions: classificationInstructions(),
38
38
  prompt: classificationPrompt({ files, watchdogs }),
39
39
  outputSchema: ClassificationSchema,
40
- maxSteps: 1,
41
40
  },
42
41
  );
43
42
  if (!result.ok) return assignments;
@@ -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 });
@@ -32,7 +32,6 @@ export class AiPreAnalyzer {
32
32
  instructions: preAnalysisInstructions(),
33
33
  prompt: preAnalysisPrompt(input),
34
34
  outputSchema: CandidateSchema,
35
- maxSteps: 1,
36
35
  },
37
36
  this.client.projectConfig.aiModelFor(input.watchdogId),
38
37
  );
@@ -30,7 +30,6 @@ export class AiTechnicalSummarizer {
30
30
  instructions: technicalInstructions(),
31
31
  prompt: technicalPrompt(buildTechnicalPromptPayload(input)),
32
32
  outputSchema: TechnicalSummarySchema,
33
- maxSteps: 1,
34
33
  },
35
34
  );
36
35
  if (!result.ok) return null;
@@ -32,7 +32,6 @@ export class AiVerifier {
32
32
  instructions: verifyInstructions(),
33
33
  prompt: verifyPrompt(input),
34
34
  outputSchema: VerifySchema,
35
- maxSteps: 1,
36
35
  },
37
36
  );
38
37
  if (!result.ok) return confidence;
@@ -10,7 +10,12 @@ export type {
10
10
  TextRequest,
11
11
  } from "./ai-client";
12
12
  export { AiClient } from "./ai-client";
13
- export { AiSdkModelProvider, mapUsage } from "./ai-sdk-model-provider";
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
  "```",
@@ -1,8 +1,7 @@
1
1
  /**
2
2
  * Schemas Zod da camada de IA.
3
3
  *
4
- * Espelham os contratos existentes (`ContextSummary`, `WatchdogReport`) e os
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
@@ -1,11 +1,10 @@
1
1
  /**
2
2
  * Contratos genéricos da camada de IA (biblioteca).
3
3
  *
4
- * `core/ai` não conhece o harness: expõe modelo, uso, tools e resultado. O
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, ZodTypeAny } from "zod";
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) ou falhar (harness).
5
+ * estourar, o chamador decide parar a operação (features do core).
6
6
  */
7
7
 
8
8
  import { AiBudgetError } from "./errors";
@@ -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("-")) continue;
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}`;
@@ -162,7 +162,7 @@ export class AstService {
162
162
  return buildImpact(analyses, importersByFile);
163
163
  }
164
164
 
165
- /** Fatos estruturais do arquivo atual (usado pelas tools do harness). */
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);