@payconductor/houndsman 0.10.0 → 0.12.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 (125) hide show
  1. package/README.md +4 -19
  2. package/package.json +1 -1
  3. package/src/cli/help.ts +0 -7
  4. package/src/cli/index.ts +9 -3
  5. package/src/core/ai/ai-client.ts +3 -7
  6. package/src/core/ai/ai-sdk-model-provider.ts +4 -24
  7. package/src/core/ai/features/brief-enricher.ts +0 -1
  8. package/src/core/ai/features/classifier.ts +0 -1
  9. package/src/core/ai/features/context-summarizer.ts +0 -1
  10. package/src/core/ai/features/pre-analyzer.ts +0 -1
  11. package/src/core/ai/features/technical-summarizer.ts +0 -1
  12. package/src/core/ai/features/verifier.ts +0 -1
  13. package/src/core/ai/index.ts +0 -3
  14. package/src/core/ai/prompts/base.ts +7 -0
  15. package/src/core/ai/prompts/context.ts +2 -1
  16. package/src/core/ai/prompts/pre-analysis.ts +3 -2
  17. package/src/core/ai/prompts/technical.ts +2 -1
  18. package/src/core/ai/schemas.ts +1 -23
  19. package/src/core/ai/types.ts +2 -15
  20. package/src/core/ai/usage-budget.ts +1 -1
  21. package/src/core/ast/diff.ts +37 -2
  22. package/src/core/ast/service.ts +1 -1
  23. package/src/core/brief.ts +54 -5
  24. package/src/core/changed-scope.ts +207 -0
  25. package/src/core/commands/scaffold.ts +1 -18
  26. package/src/core/config-loader.ts +18 -4
  27. package/src/core/config.ts +2 -15
  28. package/src/core/defaults.ts +0 -6
  29. package/src/core/engine/review-scope.ts +55 -8
  30. package/src/core/engine/review-workspace.ts +6 -2
  31. package/src/core/jev/jev-detector.ts +7 -2
  32. package/src/core/logging/index.ts +1 -1
  33. package/src/core/logging/logger.ts +3 -3
  34. package/src/core/orchestrator.ts +19 -14
  35. package/src/core/project-config.ts +0 -28
  36. package/src/core/registry-check.ts +2 -12
  37. package/src/core/rule-text.ts +3 -55
  38. package/src/core/services/git-repository.ts +8 -0
  39. package/src/core/services/worktree-manager.ts +61 -2
  40. package/src/core/storage/types.ts +0 -1
  41. package/src/core/types.ts +3 -14
  42. package/src/index.ts +25 -3
  43. package/src/server/client/utils/constants.ts +3 -6
  44. package/src/server/client/utils/types.ts +0 -1
  45. package/src/skill/OUTPUT-FORMAT.md +5 -0
  46. package/src/skill/README.md +2 -1
  47. package/src/skill/SETUP.md +1 -1
  48. package/src/skill/fix.md +1 -1
  49. package/src/skill/hunt.md +2 -6
  50. package/src/tests/__snapshots__/baseline.test.ts.snap +50 -32
  51. package/src/tests/ai-features.test.ts +11 -0
  52. package/src/tests/changed-scope.test.ts +197 -0
  53. package/src/tests/fallback.test.ts +1 -96
  54. package/src/tests/fixtures/fake-model-provider.ts +0 -1
  55. package/src/tests/review-scope.test.ts +33 -0
  56. package/src/tests/worktree.test.ts +72 -4
  57. package/src/cli/commands/hunt-command.ts +0 -51
  58. package/src/harness/README.md +0 -15
  59. package/src/harness/agent/agent-context.ts +0 -33
  60. package/src/harness/agent/agent-errors.ts +0 -30
  61. package/src/harness/agent/agent-factory.ts +0 -46
  62. package/src/harness/agent/agent-reporter.ts +0 -34
  63. package/src/harness/agent/agent-result.ts +0 -16
  64. package/src/harness/agent/agent-runner.ts +0 -32
  65. package/src/harness/agent/agent-scheduler.ts +0 -37
  66. package/src/harness/agent/review-agent.ts +0 -401
  67. package/src/harness/config/harness-config.ts +0 -89
  68. package/src/harness/hunt/hunt-harness.ts +0 -83
  69. package/src/harness/hunt/hunt-options.ts +0 -92
  70. package/src/harness/hunt/hunt-result.ts +0 -17
  71. package/src/harness/hunt/hunt-session.ts +0 -359
  72. package/src/harness/hunt/review-planner.ts +0 -98
  73. package/src/harness/index.ts +0 -36
  74. package/src/harness/investigation/agent-state.ts +0 -134
  75. package/src/harness/investigation/evidence-verifier.ts +0 -129
  76. package/src/harness/investigation/evidence.ts +0 -110
  77. package/src/harness/investigation/finding-critic.ts +0 -96
  78. package/src/harness/investigation/investigation-planner.ts +0 -118
  79. package/src/harness/investigation/investigation-task.ts +0 -10
  80. package/src/harness/investigation/review-journal.ts +0 -87
  81. package/src/harness/investigation/rule-analyzer.ts +0 -178
  82. package/src/harness/investigation/rule-signals.ts +0 -110
  83. package/src/harness/prompts/agent-schemas.ts +0 -57
  84. package/src/harness/prompts/reviewer-prompt-builder.ts +0 -101
  85. package/src/harness/prompts/states/conclude-prompt.ts +0 -16
  86. package/src/harness/prompts/states/investigate-prompt.ts +0 -37
  87. package/src/harness/prompts/states/orient-prompt.ts +0 -15
  88. package/src/harness/prompts/states/verify-prompt.ts +0 -22
  89. package/src/harness/tools/agent-tool.ts +0 -102
  90. package/src/harness/tools/builtin/changed-symbols-tool.ts +0 -48
  91. package/src/harness/tools/builtin/doc-context-tool.ts +0 -74
  92. package/src/harness/tools/builtin/file-outline-tool.ts +0 -66
  93. package/src/harness/tools/builtin/find-references-tool.ts +0 -66
  94. package/src/harness/tools/builtin/find-symbol-tool.ts +0 -91
  95. package/src/harness/tools/builtin/helpers.ts +0 -115
  96. package/src/harness/tools/builtin/impact-tool.ts +0 -48
  97. package/src/harness/tools/builtin/imports-exports-tool.ts +0 -53
  98. package/src/harness/tools/builtin/index.ts +0 -59
  99. package/src/harness/tools/builtin/list-files-tool.ts +0 -85
  100. package/src/harness/tools/builtin/pr-context-tool.ts +0 -41
  101. package/src/harness/tools/builtin/read-file-range-tool.ts +0 -73
  102. package/src/harness/tools/builtin/rule-signals-tool.ts +0 -36
  103. package/src/harness/tools/builtin/search-code-tool.ts +0 -65
  104. package/src/harness/tools/builtin/symbol-context-tool.ts +0 -106
  105. package/src/harness/tools/builtin/tests-for-symbol-tool.ts +0 -61
  106. package/src/harness/tools/path-guard.ts +0 -90
  107. package/src/harness/tools/result-limiter.ts +0 -49
  108. package/src/harness/tools/secret-redactor.ts +0 -54
  109. package/src/harness/tools/tool-cache.ts +0 -50
  110. package/src/harness/tools/tool-call-budget.ts +0 -60
  111. package/src/harness/tools/tool-context.ts +0 -5
  112. package/src/harness/tools/tool-middleware.ts +0 -105
  113. package/src/harness/tools/tool-policy.ts +0 -51
  114. package/src/harness/tools/tool-registry.ts +0 -112
  115. package/src/harness/tools/tool-result.ts +0 -15
  116. package/src/harness/tools/toolbox.ts +0 -36
  117. package/src/harness/validation/finding-validator.ts +0 -119
  118. package/src/tests/fixtures/fake-agent-tools.ts +0 -69
  119. package/src/tests/fixtures/fake-investigation.ts +0 -64
  120. package/src/tests/harness-agent.test.ts +0 -141
  121. package/src/tests/harness-investigation.test.ts +0 -206
  122. package/src/tests/harness-tools.test.ts +0 -148
  123. package/src/tests/hunt-command.test.ts +0 -212
  124. package/src/tests/rule-analyzer.test.ts +0 -61
  125. package/src/tests/tool-contracts.test.ts +0 -82
package/README.md CHANGED
@@ -28,7 +28,7 @@ diferente das PRs. O motor só coordena a caçada — as regras são problema se
28
28
  - **git** — o diff usa `git diff <base>...<head>`.
29
29
  - **gh** (Opcional) — Obtém contexto da PR e dispara comentários para o GitHub
30
30
  - **`HOUNDSMAN_TYPE_SAFE_API_KEY`** (opcional) — pré-análise Jev.
31
- - **`HOUNDSMAN_DEEP_SEEK_API_KEY`** (opcional) — IA própria do `hunt` nativo e das features de IA.
31
+ - **`HOUNDSMAN_DEEP_SEEK_API_KEY`** (opcional) — features de IA (briefs, contexto, classificação, resumo técnico).
32
32
  - **`HOUNDSMAN_OPENAI_API_KEY`** / **`HOUNDSMAN_ANTHROPIC_API_KEY`** (opcional) — apenas se
33
33
  `ai.provider` for `openai`/`anthropic`.
34
34
 
@@ -74,7 +74,7 @@ bun run houndsman doctor
74
74
 
75
75
  ### 6. Rode a primeira revisão
76
76
 
77
- No seu harness (Claude Code / agents), invoque o skill:
77
+ No seu agente (Claude Code / agents), invoque o skill:
78
78
 
79
79
  ```
80
80
  /houndsman hunt
@@ -84,28 +84,13 @@ O skill orquestra tudo e persiste o relatório no storage global (SQLite em
84
84
  `<HOUNDSMAN_HOME>/houndsman.db`); a UI lê os artefatos direto do storage. Não precisa rodar
85
85
  nenhum script à mão.
86
86
 
87
- ### 7. Revisão nativa (opcional) — `houndsman hunt`
88
-
89
- Além do skill, o houndsman tem um **harness próprio**: o `hunt` roda o agente de revisão
90
- dentro do CLI, com tools read-only, investigação por evidência e a mesma saída `review.md`.
91
-
92
- ```bash
93
- bun run houndsman hunt --dry-run # inspeciona watchdogs/arquivos, sem IA
94
- HOUNDSMAN_DEEP_SEEK_API_KEY=... bun run houndsman hunt
95
- bun run houndsman hunt --resume # preserva findings válidos do staging
96
- bun run houndsman hunt --fail-on-blocking
97
- ```
98
-
99
- Detalhes (catálogo de tools, máquina de estados, custos e segurança): [`docs/HARNESS.md`](docs/HARNESS.md).
100
-
101
- ### 8. Várias PRs ao mesmo tempo (worktrees)
87
+ ### 7. Várias PRs ao mesmo tempo (worktrees)
102
88
 
103
89
  Cada revisão pode rodar em um **worktree dedicado** (detached no commit da PR), sem
104
90
  checkout no clone principal e sem bloquear a revisão das outras PRs:
105
91
 
106
92
  ```bash
107
93
  houndsman --pr 123 # cria/reusa o worktree e prepara os briefs
108
- houndsman hunt --pr 123 # revisão nativa no mesmo worktree
109
94
  houndsman --pr 123 --finalize # consolida o relatório
110
95
  houndsman worktree list # worktrees ativos
111
96
  houndsman worktree remove feature/x
@@ -153,7 +138,7 @@ Você decide o que fazer com cada finding pela **UI local**, informando se quer
153
138
  bun run houndsman server --open # UI: triar, comentar, marcar feito, ler artefatos
154
139
  ```
155
140
 
156
- > A partir da 2ª revisão o `hunt` é **incremental**: revisa apenas os arquivos que mudaram desde
141
+ > A partir da 2ª revisão o prepare é **incremental**: revisa apenas os arquivos que mudaram desde
157
142
  > a última revisão. `--full` força o review completo.
158
143
 
159
144
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@payconductor/houndsman",
3
- "version": "0.10.0",
3
+ "version": "0.12.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",
package/src/cli/help.ts CHANGED
@@ -5,7 +5,6 @@ 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)
10
9
  status [ping] Mostra o status da run (ping renova o heartbeat da execução)
11
10
  worktree <ação> Lista/cria/remove worktrees de revisão (list · add · remove · prune · path)
@@ -30,12 +29,6 @@ Flags:
30
29
  --no-ai Desativa as features de IA do fluxo normal
31
30
  --fail-on-blocking Exit 1 se houver bloqueantes (com --finalize)
32
31
 
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>
38
-
39
32
  Flags de findings:
40
33
  --decision <d> pending | execute | ignore
41
34
  --exec <e> pending | in_progress | done | stale
package/src/cli/index.ts CHANGED
@@ -31,13 +31,13 @@ import type { HoundsmanConfig } from "../core/config";
31
31
  import { loadConfig } from "../core/config-loader";
32
32
  import { HoundsmanEngine } from "../core/engine/houndsman-engine";
33
33
  import { logger } from "../core/logging";
34
+ import { syncProjectLayerForRoot } from "../core/services/worktree-manager";
34
35
  import { ArgParser } from "./arg-parser";
35
36
  import type { CliContext } from "./commands/command";
36
37
  import { ContextCommand } from "./commands/context-command";
37
38
  import { DoctorCommand } from "./commands/doctor-command";
38
39
  import { FindingsCommand } from "./commands/findings-command";
39
40
  import { HelpCommand } from "./commands/help-command";
40
- import { HuntCommand } from "./commands/hunt-command";
41
41
  import { CommandRegistry } from "./commands/registry";
42
42
  import { ReviewCommand } from "./commands/review-command";
43
43
  import { ServerCommand } from "./commands/server-command";
@@ -57,7 +57,6 @@ function buildRegistry(): CommandRegistry {
57
57
  return new CommandRegistry()
58
58
  .register(new ReviewCommand(), { fallback: true })
59
59
  .register(new HelpCommand())
60
- .register(new HuntCommand())
61
60
  .register(new ContextCommand())
62
61
  .register(new FindingsCommand())
63
62
  .register(new StatusCommand())
@@ -90,7 +89,14 @@ export async function main(): Promise<void> {
90
89
 
91
90
  let loaded: Awaited<ReturnType<typeof loadConfig>> | null = null;
92
91
  const config = async (): Promise<HoundsmanConfig> => {
93
- loaded ??= await loadConfig({ configPath: parser.value("--config") });
92
+ loaded ??= await (async () => {
93
+ // Rodando dentro de um worktree, a camada gerada (`.houndsman/core`)
94
+ // não existe (é ignorada pelo git). Repõe a partir do clone principal
95
+ // antes de importar a config — sem isso os watchdogs não resolvem
96
+ // `../core/types`. No clone principal é no-op.
97
+ syncProjectLayerForRoot(process.cwd());
98
+ return loadConfig({ configPath: parser.value("--config") });
99
+ })();
94
100
  return loaded.config;
95
101
  };
96
102
 
@@ -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>;
@@ -61,9 +46,6 @@ export function buildGenerateTextArgs<T>(
61
46
  provider: AiProviderKind,
62
47
  modelId: string,
63
48
  ) {
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
49
  const providerOptions = request.providerOptions
68
50
  ? (request.providerOptions as GenerateTextArgs["providerOptions"])
69
51
  : undefined;
@@ -76,7 +58,6 @@ export function buildGenerateTextArgs<T>(
76
58
  model,
77
59
  system: request.instructions,
78
60
  prompt: request.prompt,
79
- ...(hasTools ? { tools, stopWhen: stepCountIs(maxSteps) } : {}),
80
61
  ...(request.outputSchema
81
62
  ? { output: Output.object({ schema: request.outputSchema }) }
82
63
  : {}),
@@ -108,7 +89,6 @@ export class AiSdkModelProvider implements ModelProvider {
108
89
  return {
109
90
  output,
110
91
  usage: mapUsage(result.usage),
111
- steps: result.steps?.length ?? 1,
112
92
  finishReason: result.finishReason ?? "unknown",
113
93
  };
114
94
  } catch (error) {
@@ -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;
@@ -28,7 +28,6 @@ export {
28
28
  type AiCandidate,
29
29
  type AiClassification,
30
30
  type AiVerdict,
31
- type AiWatchdogReport,
32
31
  type BriefPlan,
33
32
  BriefPlanSchema,
34
33
  CandidateSchema,
@@ -39,11 +38,9 @@ export {
39
38
  type TechnicalSummaryPlan,
40
39
  TechnicalSummarySchema,
41
40
  VerifySchema,
42
- WatchdogReportSchema,
43
41
  } from "./schemas";
44
42
  export {
45
43
  type AiResult,
46
- type AiTool,
47
44
  type AiUsage,
48
45
  addUsage,
49
46
  emptyUsage,
@@ -14,6 +14,13 @@ export const UNTRUSTED_DATA_RULES: readonly string[] = [
14
14
  "Responda apenas no formato estruturado pedido.",
15
15
  ];
16
16
 
17
+ /**
18
+ * Idioma da saída textual. Nomes próprios, paths, símbolos e comandos ficam
19
+ * como estão — só a prosa é traduzida.
20
+ */
21
+ export const OUTPUT_LANGUAGE_RULE =
22
+ "Escreva todo o texto em português do Brasil (pt-BR), mesmo que o contexto, o diff ou os nomes de arquivos estejam em inglês.";
23
+
17
24
  /** Monta um system prompt estável (papel + regras). */
18
25
  export function buildSystem(role: string, extra: readonly string[] = []): string {
19
26
  return [
@@ -2,13 +2,14 @@
2
2
  * Prompt do resumo de contexto da PR (`AiContextSummarizer`).
3
3
  */
4
4
 
5
- import { buildSystem, clip } from "./base";
5
+ import { buildSystem, clip, OUTPUT_LANGUAGE_RULE } from "./base";
6
6
 
7
7
  /** Instruções de sistema do resumo. */
8
8
  export function contextInstructions(): string {
9
9
  return buildSystem(
10
10
  "Você resume o contexto de uma Pull Request para revisores técnicos.",
11
11
  [
12
+ OUTPUT_LANGUAGE_RULE,
12
13
  "Objetivo em até 2 frases (máx. 400 caracteres).",
13
14
  "Motivação em até 3 frases (máx. 480 caracteres), com o problema e o impacto esperado.",
14
15
  "Resumo das alterações em até 6 bullets (máx. 240 caracteres cada), focado em comportamento, fluxos e impacto.",
@@ -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
  "```",
@@ -2,13 +2,14 @@
2
2
  * Prompt dos pontos técnicos estruturais (`AiTechnicalSummarizer`).
3
3
  */
4
4
 
5
- import { buildSystem, clip } from "./base";
5
+ import { buildSystem, clip, OUTPUT_LANGUAGE_RULE } from "./base";
6
6
 
7
7
  /** Instruções de sistema dos pontos técnicos. */
8
8
  export function technicalInstructions(): string {
9
9
  return buildSystem(
10
10
  "Você descreve a estrutura técnica de uma Pull Request para revisores.",
11
11
  [
12
+ OUTPUT_LANGUAGE_RULE,
12
13
  "Foque em arquitetura e contratos: novos módulos/services, interfaces, providers, repositories, camadas, exports, dependências e impacto.",
13
14
  "NÃO avalie qualidade, NÃO liste achados, bugs ou riscos de revisão.",
14
15
  "NÃO repita a motivação de negócio: isso pertence ao resumo de negócio.",
@@ -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,7 +49,6 @@ 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;
66
53
  };
67
54
 
@@ -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);
package/src/core/brief.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import type { AstDelta } from "./ast/types";
9
+ import { formatRanges, formatSymbols } from "./changed-scope";
9
10
  import {
10
11
  type Rule,
11
12
  SEVERITY_EMOJI,
@@ -21,10 +22,10 @@ import {
21
22
  */
22
23
  export const PROCEDURE: readonly string[] = [
23
24
  "1. **FILTER** — descarte o que não for do seu domínio (ver *Prioridade de leitura* e *Não carregar*).",
24
- "2. **READ HEAD** — leia os arquivos seletivamente (primeiras ~30 linhas). NUNCA o diff completo.",
25
- "3. **GREP** — use as regras críticas/anti-padrões como heurística para localizar violações.",
26
- "4. **DEEP-READ** — aprofunde apenas os arquivos com suspeita de violação.",
27
- '5. **CONCLUDE** — grave o JSON no caminho de output, com `reviewedFiles` preenchido. Sem achados → `"findings": []` (arquivo obrigatório). Não escreva markdown livre.',
25
+ "2. **READ SCOPE** — carregue apenas as faixas alteradas em *Escopo do diff*; o restante do arquivo é só contexto.",
26
+ "3. **GREP** — use as regras críticas/anti-padrões como heurística para localizar violações nas faixas alteradas.",
27
+ "4. **DEEP-READ** — aprofunde apenas as faixas com suspeita de violação dentro do escopo.",
28
+ '5. **CONCLUDE** — grave o JSON no caminho de output, com `reviewedFiles` preenchido e `line` dentro das faixas alteradas. Sem achados → `"findings": []` (arquivo obrigatório). Não escreva markdown livre.',
28
29
  ];
29
30
 
30
31
  /** Limites do contexto injetado em cada brief (o resumo final é maior). */
@@ -83,6 +84,52 @@ export function renderBriefCandidates(
83
84
  return lines;
84
85
  }
85
86
 
87
+ /**
88
+ * Escopo do diff — as únicas faixas que podem virar finding.
89
+ *
90
+ * Sem ele, o watchdog tende a revisar o arquivo inteiro e reportar problemas
91
+ * pré-existentes (fora do que a PR alterou).
92
+ */
93
+ function changedScopeSection(args: WatchdogRenderArgs): string[] {
94
+ const scope = args.changedScope;
95
+ if (!scope) return [];
96
+ const files = args.files.filter((file) => scope[file]);
97
+ if (files.length === 0) return [];
98
+ const lines: string[] = [
99
+ "## Escopo do diff (símbolos alterados)",
100
+ "",
101
+ "> **Regra de escopo:** um finding só é válido quando a `line` está nas faixas",
102
+ "> abaixo. Código fora delas é contexto — use para entender o impacto, nunca",
103
+ "> como achado. Se a mudança quebra código não alterado, ancore o finding na",
104
+ "> linha alterada que causou o problema.",
105
+ "",
106
+ ];
107
+ let rendered = 0;
108
+ for (const file of files) {
109
+ const entry = scope[file];
110
+ if (!entry) continue;
111
+ if (entry.ranges.length === 0) {
112
+ lines.push(
113
+ `- \`${file}\` — sem linhas alteradas (renomeado/binário): não reportar`,
114
+ );
115
+ rendered++;
116
+ if (rendered >= 40) break;
117
+ continue;
118
+ }
119
+ const fallback = entry.source === "hunks" ? " _(faixas do diff)_" : "";
120
+ lines.push(`- \`${file}\` — linhas ${formatRanges(entry.ranges)}${fallback}`);
121
+ const symbols = formatSymbols(entry.symbols);
122
+ if (symbols) lines.push(` símbolos: ${symbols}`);
123
+ rendered++;
124
+ if (rendered >= 40) break;
125
+ }
126
+ if (files.length > rendered) {
127
+ lines.push(`- … +${files.length - rendered} arquivo(s)`);
128
+ }
129
+ lines.push("");
130
+ return lines;
131
+ }
132
+
86
133
  /** Linha de lista em code-span: `- \`valor\``. */
87
134
  function codeItem(value: string): string {
88
135
  return `- \`${value}\``;
@@ -293,7 +340,9 @@ export function defaultRenderBrief(
293
340
  ];
294
341
 
295
342
  if (def.description) lines.push("## Objetivo", "", def.description, "");
296
- lines.push("## Escopo", "", def.scope, "");
343
+ lines.push("## Escopo do domínio", "", def.scope, "");
344
+
345
+ lines.push(...changedScopeSection(args));
297
346
 
298
347
  if (args.context) lines.push(...renderBriefContext(args.context));
299
348