@payconductor/houndsman 0.5.0 → 0.7.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 (175) hide show
  1. package/README.md +24 -4
  2. package/assets/grammars/tree-sitter-javascript.wasm +0 -0
  3. package/assets/grammars/tree-sitter-tsx.wasm +0 -0
  4. package/assets/grammars/tree-sitter-typescript.wasm +0 -0
  5. package/assets/grammars/web-tree-sitter.wasm +0 -0
  6. package/package.json +11 -3
  7. package/src/cli/commands/command.ts +5 -0
  8. package/src/cli/commands/doctor-command.ts +116 -24
  9. package/src/cli/commands/hunt-command.ts +25 -0
  10. package/src/cli/commands/server-command.ts +7 -1
  11. package/src/cli/help.ts +18 -2
  12. package/src/cli/index.ts +10 -6
  13. package/src/core/ai/ai-client.ts +221 -0
  14. package/src/core/ai/ai-sdk-model-provider.ts +101 -0
  15. package/src/core/ai/errors.ts +36 -0
  16. package/src/core/ai/features/brief-enricher.ts +62 -0
  17. package/src/core/ai/features/classifier.ts +50 -0
  18. package/src/core/ai/features/context-summarizer.ts +31 -0
  19. package/src/core/ai/features/pre-analyzer.ts +51 -0
  20. package/src/core/ai/features/verifier.ts +45 -0
  21. package/src/core/ai/index.ts +56 -0
  22. package/src/core/ai/model-factory.ts +137 -0
  23. package/src/core/ai/prompts/base.ts +36 -0
  24. package/src/core/ai/prompts/brief.ts +87 -0
  25. package/src/core/ai/prompts/classification.ts +36 -0
  26. package/src/core/ai/prompts/context.ts +29 -0
  27. package/src/core/ai/prompts/pre-analysis.ts +49 -0
  28. package/src/core/ai/prompts/verify.ts +40 -0
  29. package/src/core/ai/schemas.ts +110 -0
  30. package/src/core/ai/types.ts +76 -0
  31. package/src/core/ai/usage-budget.ts +75 -0
  32. package/src/core/ai/usage-recorder.ts +79 -0
  33. package/src/core/assets.ts +16 -0
  34. package/src/core/ast/diff.ts +220 -0
  35. package/src/core/ast/impact.ts +127 -0
  36. package/src/core/ast/parser.ts +167 -0
  37. package/src/core/ast/service.ts +233 -0
  38. package/src/core/ast/symbols.ts +294 -0
  39. package/src/core/ast/types.ts +125 -0
  40. package/src/core/brief.ts +138 -0
  41. package/src/core/commands/findings.ts +65 -2
  42. package/src/core/commands/review.ts +2 -0
  43. package/src/core/commands/scaffold.ts +159 -10
  44. package/src/core/commands/update.ts +5 -3
  45. package/src/core/config.ts +85 -0
  46. package/src/core/defaults.ts +27 -0
  47. package/src/core/domain/watchdog.ts +43 -5
  48. package/src/core/engine/houndsman-engine.ts +16 -1
  49. package/src/core/engine/review-scope.ts +52 -6
  50. package/src/core/engine/review-session.ts +27 -2
  51. package/src/core/env.ts +101 -0
  52. package/src/core/file-classifier.ts +25 -4
  53. package/src/core/git-runner.ts +25 -13
  54. package/src/core/github-review.ts +10 -7
  55. package/src/core/jev/jev-client.ts +8 -7
  56. package/src/core/jev/jev-verify.ts +2 -1
  57. package/src/core/logging/index.ts +17 -0
  58. package/src/core/logging/logger.ts +187 -0
  59. package/src/core/orchestrator.ts +357 -9
  60. package/src/core/project-config.ts +123 -1
  61. package/src/core/registry-check.ts +148 -6
  62. package/src/core/renderers/manifest-writer.ts +33 -0
  63. package/src/core/rule-text.ts +97 -0
  64. package/src/core/services/file-classifier.ts +10 -3
  65. package/src/core/services/gh-client.ts +38 -22
  66. package/src/core/services/verifier.ts +14 -3
  67. package/src/core/storage/paths.ts +6 -5
  68. package/src/core/storage/types.ts +6 -0
  69. package/src/core/storage/work-area.ts +12 -0
  70. package/src/core/types.ts +37 -1
  71. package/src/core/verify.ts +17 -5
  72. package/src/harness/README.md +15 -0
  73. package/src/harness/agent/agent-context.ts +33 -0
  74. package/src/harness/agent/agent-errors.ts +30 -0
  75. package/src/harness/agent/agent-factory.ts +45 -0
  76. package/src/harness/agent/agent-reporter.ts +34 -0
  77. package/src/harness/agent/agent-result.ts +16 -0
  78. package/src/harness/agent/agent-runner.ts +32 -0
  79. package/src/harness/agent/agent-scheduler.ts +37 -0
  80. package/src/harness/agent/review-agent.ts +391 -0
  81. package/src/harness/config/harness-config.ts +84 -0
  82. package/src/harness/hunt/hunt-harness.ts +83 -0
  83. package/src/harness/hunt/hunt-options.ts +75 -0
  84. package/src/harness/hunt/hunt-result.ts +17 -0
  85. package/src/harness/hunt/hunt-session.ts +372 -0
  86. package/src/harness/hunt/review-planner.ts +98 -0
  87. package/src/harness/index.ts +36 -0
  88. package/src/harness/investigation/agent-state.ts +134 -0
  89. package/src/harness/investigation/evidence-verifier.ts +129 -0
  90. package/src/harness/investigation/evidence.ts +110 -0
  91. package/src/harness/investigation/finding-critic.ts +96 -0
  92. package/src/harness/investigation/investigation-planner.ts +118 -0
  93. package/src/harness/investigation/investigation-task.ts +10 -0
  94. package/src/harness/investigation/review-journal.ts +87 -0
  95. package/src/harness/investigation/rule-analyzer.ts +178 -0
  96. package/src/harness/investigation/rule-signals.ts +110 -0
  97. package/src/harness/prompts/agent-schemas.ts +57 -0
  98. package/src/harness/prompts/reviewer-prompt-builder.ts +101 -0
  99. package/src/harness/prompts/states/conclude-prompt.ts +16 -0
  100. package/src/harness/prompts/states/investigate-prompt.ts +37 -0
  101. package/src/harness/prompts/states/orient-prompt.ts +15 -0
  102. package/src/harness/prompts/states/verify-prompt.ts +22 -0
  103. package/src/harness/tools/agent-tool.ts +102 -0
  104. package/src/harness/tools/builtin/changed-symbols-tool.ts +48 -0
  105. package/src/harness/tools/builtin/doc-context-tool.ts +74 -0
  106. package/src/harness/tools/builtin/file-outline-tool.ts +66 -0
  107. package/src/harness/tools/builtin/find-references-tool.ts +66 -0
  108. package/src/harness/tools/builtin/find-symbol-tool.ts +91 -0
  109. package/src/harness/tools/builtin/helpers.ts +115 -0
  110. package/src/harness/tools/builtin/impact-tool.ts +48 -0
  111. package/src/harness/tools/builtin/imports-exports-tool.ts +53 -0
  112. package/src/harness/tools/builtin/index.ts +59 -0
  113. package/src/harness/tools/builtin/list-files-tool.ts +85 -0
  114. package/src/harness/tools/builtin/pr-context-tool.ts +41 -0
  115. package/src/harness/tools/builtin/read-file-range-tool.ts +73 -0
  116. package/src/harness/tools/builtin/rule-signals-tool.ts +36 -0
  117. package/src/harness/tools/builtin/search-code-tool.ts +65 -0
  118. package/src/harness/tools/builtin/symbol-context-tool.ts +106 -0
  119. package/src/harness/tools/builtin/tests-for-symbol-tool.ts +61 -0
  120. package/src/harness/tools/path-guard.ts +90 -0
  121. package/src/harness/tools/result-limiter.ts +49 -0
  122. package/src/harness/tools/secret-redactor.ts +54 -0
  123. package/src/harness/tools/tool-cache.ts +50 -0
  124. package/src/harness/tools/tool-call-budget.ts +60 -0
  125. package/src/harness/tools/tool-context.ts +5 -0
  126. package/src/harness/tools/tool-middleware.ts +105 -0
  127. package/src/harness/tools/tool-policy.ts +51 -0
  128. package/src/harness/tools/tool-registry.ts +112 -0
  129. package/src/harness/tools/tool-result.ts +15 -0
  130. package/src/harness/tools/toolbox.ts +36 -0
  131. package/src/harness/validation/finding-validator.ts +119 -0
  132. package/src/index.ts +46 -0
  133. package/src/server/client/App.tsx +38 -12
  134. package/src/server/client/CommentsPanel.tsx +255 -0
  135. package/src/server/client/FindingCard.tsx +12 -4
  136. package/src/server/client/FindingsList.tsx +4 -1
  137. package/src/server/client/Sidebar.tsx +397 -0
  138. package/src/server/client/SubmitModal.tsx +61 -5
  139. package/src/server/client/icons.tsx +19 -0
  140. package/src/server/client/index.tsx +1 -1
  141. package/src/server/client/utils/constants.ts +29 -5
  142. package/src/server/client/utils/format.ts +47 -2
  143. package/src/server/client/utils/types.ts +88 -0
  144. package/src/server/editor.ts +129 -0
  145. package/src/server/index.ts +115 -5
  146. package/src/server/pr-comments.ts +256 -0
  147. package/src/server/pr-inbox.ts +202 -0
  148. package/src/server/run-repository.ts +33 -1
  149. package/src/skill/OUTPUT-FORMAT.md +12 -0
  150. package/src/skill/SETUP.md +6 -2
  151. package/src/skill/hunt.md +14 -2
  152. package/src/skill/verify.md +1 -1
  153. package/src/tests/__snapshots__/baseline.test.ts.snap +96 -14
  154. package/src/tests/ai-core.test.ts +120 -0
  155. package/src/tests/ai-features.test.ts +183 -0
  156. package/src/tests/ast.test.ts +335 -0
  157. package/src/tests/cli.test.ts +33 -0
  158. package/src/tests/env.test.ts +69 -0
  159. package/src/tests/fallback.test.ts +172 -0
  160. package/src/tests/fixtures/fake-agent-tools.ts +69 -0
  161. package/src/tests/fixtures/fake-investigation.ts +64 -0
  162. package/src/tests/fixtures/fake-model-provider.ts +48 -0
  163. package/src/tests/github-review.test.ts +9 -0
  164. package/src/tests/harness-agent.test.ts +122 -0
  165. package/src/tests/harness-investigation.test.ts +206 -0
  166. package/src/tests/harness-tools.test.ts +148 -0
  167. package/src/tests/hunt-command.test.ts +212 -0
  168. package/src/tests/logger.test.ts +83 -0
  169. package/src/tests/review-scope.test.ts +11 -11
  170. package/src/tests/rule-analyzer.test.ts +61 -0
  171. package/src/tests/server-editor.test.ts +100 -0
  172. package/src/tests/server.test.ts +295 -2
  173. package/src/tests/tool-contracts.test.ts +82 -0
  174. package/src/tests/verify.test.ts +1 -1
  175. package/src/server/client/RunList.tsx +0 -75
@@ -25,8 +25,8 @@ import type { Severity } from "./types";
25
25
  /** Resultado da verificação de um finding. */
26
26
  export type VerifyStatus = "resolved" | "persisting" | "uncertain" | "file-missing";
27
27
 
28
- /** Mecanismo que decidiu o status (reancoragem local ou LLM). */
29
- export type VerifyBasis = "anchor" | "jev";
28
+ /** Mecanismo que decidiu o status (reancoragem local, Jev ou IA). */
29
+ export type VerifyBasis = "anchor" | "jev" | "ai";
30
30
 
31
31
  /** Valores canônicos de status (ordem de exibição). */
32
32
  export const VERIFY_STATUSES: readonly VerifyStatus[] = [
@@ -121,21 +121,24 @@ export function verifyLocal(
121
121
  }
122
122
 
123
123
  /**
124
- * Tier 1 — aplica a confirmação do Jev aos casos ambíguos.
124
+ * Tier 1 genérico — aplica confirmações externas (Jev ou IA) aos casos ambíguos.
125
125
  * `confidence[id] >= threshold` ⇒ a violação persiste; abaixo ⇒ resolvida.
126
126
  * Ids sem resposta mantêm o provisório local (fail-open). Devolve o nº aplicado.
127
+ *
128
+ * `basis` identifica a fonte da decisão (`jev` ou `ai`).
127
129
  */
128
- export function applyJev(
130
+ export function applyConfidence(
129
131
  entries: VerifyEntry[],
130
132
  confidence: Map<string, number>,
131
133
  threshold: number,
134
+ basis: VerifyBasis = "jev",
132
135
  ): number {
133
136
  let applied = 0;
134
137
  for (const entry of entries) {
135
138
  if (!entry.ambiguous) continue;
136
139
  const value = confidence.get(entry.id);
137
140
  if (value === undefined) continue;
138
- entry.basis = "jev";
141
+ entry.basis = basis;
139
142
  entry.confidence = value;
140
143
  entry.status = value >= threshold ? "persisting" : "resolved";
141
144
  applied++;
@@ -143,6 +146,15 @@ export function applyJev(
143
146
  return applied;
144
147
  }
145
148
 
149
+ /** Compatibilidade: confirmação via Jev. */
150
+ export function applyJev(
151
+ entries: VerifyEntry[],
152
+ confidence: Map<string, number>,
153
+ threshold: number,
154
+ ): number {
155
+ return applyConfidence(entries, confidence, threshold, "jev");
156
+ }
157
+
146
158
  export type VerifySummary = Record<VerifyStatus, number>;
147
159
 
148
160
  /** Contagem por status. */
@@ -0,0 +1,15 @@
1
+ # `src/harness` — harness nativo do houndsman
2
+
3
+ Este diretório é o **runtime de agentes do próprio houndsman** (`houndsman hunt`):
4
+ tools read-only, investigação, máquina de estados e o agente revisor.
5
+
6
+ > O **harness externo** (skill `/houndsman` para ClaudeCode/Copilot/agents) continua em
7
+ > `src/skill/`. Os dois coexistem; o nativo é opt-in.
8
+
9
+ Regras de dependência:
10
+
11
+ - `harness/` pode importar `core/` (inclusive `core/ai`).
12
+ - `core/` **nunca** importa `harness/`.
13
+ - `cli/` importa ambos.
14
+
15
+ Documentação detalhada: [`docs/HARNESS.md`](../../docs/HARNESS.md).
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Contexto de um agente de revisão (um watchdog).
3
+ */
4
+
5
+ import type { AstAnalysis, AstImpact } from "../../core/ast/types";
6
+ import type { DiffSource } from "../../core/diff-source";
7
+ import type { Watchdog } from "../../core/domain/watchdog";
8
+ import type { WatchdogRenderContext } from "../../core/types";
9
+ import type { RuleAnalysis } from "../investigation/rule-analyzer";
10
+
11
+ /** Dados imutáveis entregues ao agente. */
12
+ export type AgentContext = {
13
+ watchdog: Watchdog;
14
+ projectName: string;
15
+ branch: string;
16
+ /** Arquivos do domínio (dentro do orçamento). */
17
+ files: readonly string[];
18
+ /** Arquivos do domínio fora do orçamento (não ler; registrar em skipped). */
19
+ overflow: readonly string[];
20
+ deleted: readonly string[];
21
+ docs: readonly string[];
22
+ context?: WatchdogRenderContext | null;
23
+ ast: ReadonlyMap<string, AstAnalysis>;
24
+ impact?: AstImpact;
25
+ /** Caminho de saída do findings (staging). */
26
+ outputPath: string;
27
+ maxSteps: number;
28
+ maxCalls: number;
29
+ repoRoot: string;
30
+ diff: DiffSource;
31
+ /** Análises determinísticas das regras do watchdog. */
32
+ rules: readonly RuleAnalysis[];
33
+ };
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Erros do harness nativo.
3
+ */
4
+
5
+ /** Falha de configuração/execução do agente. */
6
+ export class AgentError extends Error {
7
+ constructor(
8
+ message: string,
9
+ readonly watchdogId?: string,
10
+ ) {
11
+ super(message);
12
+ this.name = "AgentError";
13
+ }
14
+ }
15
+
16
+ /** Orçamento de IA/tools esgotado durante a investigação. */
17
+ export class AgentBudgetError extends AgentError {
18
+ constructor(message: string, watchdogId?: string) {
19
+ super(message, watchdogId);
20
+ this.name = "AgentBudgetError";
21
+ }
22
+ }
23
+
24
+ /** Saída do modelo inválida após as tentativas permitidas. */
25
+ export class AgentOutputError extends AgentError {
26
+ constructor(message: string, watchdogId?: string) {
27
+ super(message, watchdogId);
28
+ this.name = "AgentOutputError";
29
+ }
30
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Factory de agentes — injeta provider, tools e limites resolvidos.
3
+ */
4
+
5
+ import type { ModelProvider } from "../../core/ai";
6
+ import type { AstService } from "../../core/ast/service";
7
+ import type { HarnessConfig } from "../config/harness-config";
8
+ import type { ToolPolicy } from "../tools/tool-policy";
9
+ import type { ToolRegistry } from "../tools/tool-registry";
10
+ import type { AgentContext } from "./agent-context";
11
+ import type { AgentReporter } from "./agent-reporter";
12
+ import { ReviewAgent } from "./review-agent";
13
+
14
+ /** Dependências compartilhadas por todos os agentes. */
15
+ export type AgentFactoryDeps = {
16
+ provider: ModelProvider;
17
+ registry: ToolRegistry;
18
+ policy?: ToolPolicy;
19
+ reporter: AgentReporter;
20
+ config: HarnessConfig;
21
+ ast: AstService;
22
+ readFile?: (path: string) => string | null;
23
+ };
24
+
25
+ /** Cria `ReviewAgent`s a partir do contexto de cada watchdog. */
26
+ export class AgentFactory {
27
+ constructor(private readonly deps: AgentFactoryDeps) {}
28
+
29
+ /** Instancia um agente para o contexto. */
30
+ create(context: AgentContext): ReviewAgent {
31
+ const { config } = this.deps;
32
+ return new ReviewAgent({
33
+ context,
34
+ provider: this.deps.provider,
35
+ registry: this.deps.registry,
36
+ policy: this.deps.policy,
37
+ reporter: this.deps.reporter,
38
+ maxOutputTokens: config.maxOutputTokensFor(context.watchdog.id),
39
+ temperature: config.temperature,
40
+ timeoutMs: config.timeoutMs,
41
+ ast: this.deps.ast,
42
+ readFile: this.deps.readFile,
43
+ });
44
+ }
45
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Reporter de progresso de um agente.
3
+ *
4
+ * O journal é sempre coletado (`harness-transcript.json`); o console é opcional.
5
+ */
6
+
7
+ import type { LoggerLike } from "../../core/logging";
8
+ import { ReviewJournal } from "../investigation/review-journal";
9
+
10
+ /** Interface de progresso. */
11
+ export interface AgentReporter {
12
+ readonly journal: ReviewJournal;
13
+ progress(message: string): void;
14
+ }
15
+
16
+ /** Reporter silencioso (testes). */
17
+ export class SilentReporter implements AgentReporter {
18
+ readonly journal = new ReviewJournal();
19
+
20
+ progress(): void {
21
+ // intencionalmente silencioso
22
+ }
23
+ }
24
+
25
+ /** Reporter apoiado no logger único do CLI. */
26
+ export class LoggerReporter implements AgentReporter {
27
+ readonly journal = new ReviewJournal();
28
+
29
+ constructor(private readonly logger: LoggerLike) {}
30
+
31
+ progress(message: string): void {
32
+ this.logger.progress(message);
33
+ }
34
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Resultado de um agente (watchdog).
3
+ */
4
+
5
+ import type { AiUsage } from "../../core/ai";
6
+ import type { WatchdogReport } from "../../core/report-builder";
7
+
8
+ /** Resultado da execução de um watchdog. */
9
+ export type AgentResult = {
10
+ watchdogId: string;
11
+ status: "done" | "skipped" | "failed";
12
+ report?: WatchdogReport;
13
+ usage: AiUsage;
14
+ error?: string;
15
+ reason?: string;
16
+ };
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Executor de um agente — retry curto para falhas transitórias.
3
+ *
4
+ * 429/5xx/timeout são tentados uma vez mais; 4xx não. O resultado é sempre
5
+ * estruturado (`failed` com motivo), nunca uma exceção solta.
6
+ */
7
+
8
+ import type { AgentContext } from "./agent-context";
9
+ import type { AgentFactory } from "./agent-factory";
10
+ import type { AgentResult } from "./agent-result";
11
+ import type { ReviewAgentOutcome } from "./review-agent";
12
+
13
+ /** Executor com retry. */
14
+ export class AgentRunner {
15
+ constructor(private readonly factory: AgentFactory) {}
16
+
17
+ /** Executa um agente (com uma retentativa em falha transitória). */
18
+ async run(context: AgentContext): Promise<ReviewAgentOutcome> {
19
+ const first = await this.factory.create(context).run();
20
+ if (first.status !== "failed" || !isTransient(first.error)) return first;
21
+ const second = await this.factory.create(context).run();
22
+ return second;
23
+ }
24
+ }
25
+
26
+ /** Erro transitório (rate limit, 5xx, timeout). */
27
+ function isTransient(message: string | undefined): boolean {
28
+ if (!message) return false;
29
+ return /429|5\d\d|timeout|temporar|ECONNRESET|ETIMEDOUT|abort/i.test(message);
30
+ }
31
+
32
+ export type { AgentResult };
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Scheduler de agentes — concorrência limitada (default 2).
3
+ */
4
+
5
+ import type { AgentContext } from "./agent-context";
6
+ import type { AgentReporter } from "./agent-reporter";
7
+ import type { AgentRunner } from "./agent-runner";
8
+ import type { ReviewAgentOutcome } from "./review-agent";
9
+
10
+ /** Executa agentes com concorrência limitada. */
11
+ export class AgentScheduler {
12
+ constructor(
13
+ private readonly runner: AgentRunner,
14
+ private readonly concurrency: number,
15
+ private readonly reporter?: AgentReporter,
16
+ ) {}
17
+
18
+ /** Executa todos os contextos e devolve os resultados na ordem original. */
19
+ async run(contexts: readonly AgentContext[]): Promise<ReviewAgentOutcome[]> {
20
+ const results = new Array<ReviewAgentOutcome>(contexts.length);
21
+ let cursor = 0;
22
+ const workers = Math.max(1, Math.min(this.concurrency, contexts.length));
23
+
24
+ const worker = async (): Promise<void> => {
25
+ while (cursor < contexts.length) {
26
+ const index = cursor++;
27
+ const context = contexts[index];
28
+ if (!context) return;
29
+ this.reporter?.progress(`[${context.watchdog.id}] investigando...`);
30
+ results[index] = await this.runner.run(context);
31
+ }
32
+ };
33
+
34
+ await Promise.all(Array.from({ length: workers }, () => worker()));
35
+ return results;
36
+ }
37
+ }
@@ -0,0 +1,391 @@
1
+ /**
2
+ * Máquina de estados do revisor (`ReviewAgent`).
3
+ *
4
+ * ORIENT → PLAN → INVESTIGATE → VERIFY → CONCLUDE. O modelo escolhe *como*
5
+ * investigar dentro de INVESTIGATE; ele **não** pode pular VERIFY nem emitir
6
+ * findings sem evidência.
7
+ */
8
+
9
+ import { readFileSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import { type AiUsage, addUsage, emptyUsage, type ModelProvider } from "../../core/ai";
12
+ import type { AstService } from "../../core/ast/service";
13
+ import type { WatchdogReport } from "../../core/report-builder";
14
+ import type { Severity } from "../../core/types";
15
+ import { AgentState } from "../investigation/agent-state";
16
+ import type { FindingDraft } from "../investigation/evidence";
17
+ import { EvidenceVerifier } from "../investigation/evidence-verifier";
18
+ import { downgrade, FindingCritic } from "../investigation/finding-critic";
19
+ import { InvestigationPlanner } from "../investigation/investigation-planner";
20
+ import {
21
+ InvestigateOutputSchema,
22
+ OrientOutputSchema,
23
+ PlanOutputSchema,
24
+ } from "../prompts/agent-schemas";
25
+ import {
26
+ buildReviewerSystem,
27
+ buildReviewerUser,
28
+ } from "../prompts/reviewer-prompt-builder";
29
+ import { investigatePrompt } from "../prompts/states/investigate-prompt";
30
+ import { orientPrompt } from "../prompts/states/orient-prompt";
31
+ import { ToolCache } from "../tools/tool-cache";
32
+ import { ToolCallBudget } from "../tools/tool-call-budget";
33
+ import type { ToolContext } from "../tools/tool-context";
34
+ import { ToolMiddleware } from "../tools/tool-middleware";
35
+ import { type AgentStateName, ToolPolicy } from "../tools/tool-policy";
36
+ import type { ToolRegistry } from "../tools/tool-registry";
37
+ import { Toolbox } from "../tools/toolbox";
38
+ import { FindingValidator } from "../validation/finding-validator";
39
+ import type { AgentContext } from "./agent-context";
40
+ import type { AgentReporter } from "./agent-reporter";
41
+ import type { AgentResult } from "./agent-result";
42
+
43
+ /** Dependências do agente. */
44
+ export type ReviewAgentDeps = {
45
+ context: AgentContext;
46
+ provider: ModelProvider;
47
+ registry: ToolRegistry;
48
+ policy?: ToolPolicy;
49
+ reporter: AgentReporter;
50
+ maxOutputTokens: number;
51
+ temperature: number;
52
+ timeoutMs: number;
53
+ ast: AstService;
54
+ readFile?: (path: string) => string | null;
55
+ };
56
+
57
+ /** Resultado final de um agente com o estado (para transcript). */
58
+ export type ReviewAgentOutcome = AgentResult & { state: AgentState };
59
+
60
+ /** Agente de um watchdog. */
61
+ export class ReviewAgent {
62
+ private readonly deps: ReviewAgentDeps;
63
+ private readonly state = new AgentState();
64
+ private usage: AiUsage = emptyUsage();
65
+ private readonly toolbox: Toolbox;
66
+ private readonly toolContext: ToolContext;
67
+
68
+ constructor(deps: ReviewAgentDeps) {
69
+ this.deps = deps;
70
+ const toolBudget = new ToolCallBudget(
71
+ deps.context.maxCalls,
72
+ Math.max(4, Math.ceil(deps.context.maxCalls / 2)),
73
+ );
74
+ const middleware = new ToolMiddleware();
75
+ const policy = deps.policy ?? new ToolPolicy();
76
+ this.toolbox = new Toolbox(deps.registry, policy, middleware);
77
+ this.toolContext = {
78
+ repoRoot: deps.context.repoRoot,
79
+ watchdog: deps.context.watchdog,
80
+ diff: deps.context.diff,
81
+ ast: deps.ast,
82
+ state: this.state,
83
+ budget: toolBudget,
84
+ cache: new ToolCache(),
85
+ logger: deps.reporter,
86
+ agent: deps.context,
87
+ rules: new Map(
88
+ deps.context.rules.map((analysis) => [analysis.rule, analysis]),
89
+ ),
90
+ };
91
+ }
92
+
93
+ /** Executa a investigação completa. */
94
+ async run(): Promise<ReviewAgentOutcome> {
95
+ const { context, reporter } = this.deps;
96
+ const plan = new InvestigationPlanner().plan(
97
+ {
98
+ watchdogId: context.watchdog.id,
99
+ files: context.files,
100
+ symbols: symbolsOf(context),
101
+ impacted: context.impact?.affectedFiles,
102
+ },
103
+ context.rules,
104
+ );
105
+ this.state.setTasks(plan.tasks);
106
+
107
+ try {
108
+ await this.orient();
109
+ this.transition("ORIENT", "PLAN");
110
+ await this.plan();
111
+ this.transition("PLAN", "INVESTIGATE");
112
+ await this.investigate();
113
+ this.transition("INVESTIGATE", "VERIFY");
114
+ const kept = this.verify();
115
+ this.transition("VERIFY", "CONCLUDE");
116
+ const report = this.conclude(kept);
117
+ reporter.progress(
118
+ `[${context.watchdog.id}] concluído (${report.findings.length} achado(s)).`,
119
+ );
120
+ return {
121
+ watchdogId: context.watchdog.id,
122
+ status: "done",
123
+ report,
124
+ usage: this.usage,
125
+ state: this.state,
126
+ };
127
+ } catch (error) {
128
+ const message = error instanceof Error ? error.message : String(error);
129
+ reporter.journal.note(`falha: ${message}`);
130
+ return {
131
+ watchdogId: context.watchdog.id,
132
+ status: "failed",
133
+ error: message,
134
+ usage: this.usage,
135
+ state: this.state,
136
+ };
137
+ }
138
+ }
139
+
140
+ /** ORIENT — hipóteses iniciais (falha não derruba a investigação). */
141
+ private async orient(): Promise<void> {
142
+ try {
143
+ const output = await this.callModel(
144
+ "harness-orient",
145
+ OrientOutputSchema,
146
+ orientPrompt(this.deps.context),
147
+ 1,
148
+ );
149
+ for (const hypothesis of output.hypotheses)
150
+ this.state.addHypothesis(hypothesis);
151
+ } catch (error) {
152
+ this.deps.reporter.journal.note(
153
+ `orient falhou (segue): ${error instanceof Error ? error.message : String(error)}`,
154
+ );
155
+ }
156
+ }
157
+
158
+ /** PLAN — seleciona tarefas do plano determinístico. */
159
+ private async plan(): Promise<void> {
160
+ const pending = this.state.tasksByStatus("pending");
161
+ if (pending.length === 0) return;
162
+ try {
163
+ const output = await this.callModel(
164
+ "harness-plan",
165
+ PlanOutputSchema,
166
+ [
167
+ "Escolha as tarefas que serão investigadas (ids).",
168
+ ...pending.map((task) => `- ${task.id}: ${task.rule}`),
169
+ ].join("\n"),
170
+ 1,
171
+ );
172
+ const selected = new Set(output.taskIds);
173
+ if (selected.size === 0) return;
174
+ for (const task of pending) {
175
+ if (!selected.has(task.id)) this.state.skipTask(task.id, "fora do plano");
176
+ }
177
+ } catch (error) {
178
+ this.deps.reporter.journal.note(
179
+ `plan falhou (segue com o plano determinístico): ${
180
+ error instanceof Error ? error.message : String(error)
181
+ }`,
182
+ );
183
+ }
184
+ }
185
+
186
+ /** INVESTIGATE — tools + evidências + drafts. */
187
+ private async investigate(): Promise<void> {
188
+ const { context } = this.deps;
189
+ const tools = this.toolbox.toModelTools("INVESTIGATE", this.toolContext);
190
+ const system = buildReviewerSystem(context, this.toolbox.describe("INVESTIGATE"));
191
+ const prompt = [
192
+ buildReviewerUser(context),
193
+ "",
194
+ this.state.hypotheses.length > 0
195
+ ? `## Hipóteses\n${this.state.hypotheses.map((item) => `- ${item}`).join("\n")}`
196
+ : "",
197
+ "",
198
+ investigatePrompt(context, this.state.tasksByStatus("pending")),
199
+ ].join("\n");
200
+
201
+ const output = await this.callModel(
202
+ "harness-investigate",
203
+ InvestigateOutputSchema,
204
+ prompt,
205
+ context.maxSteps,
206
+ system,
207
+ tools,
208
+ );
209
+
210
+ for (const draft of output.findingDrafts) this.state.addDraft(draft);
211
+ for (const coverage of output.coverage) {
212
+ this.state.setCoverage(coverage.rule, coverage.status, coverage.reason);
213
+ }
214
+ for (const task of this.state.tasksByStatus("pending")) {
215
+ const covered = this.state.isCovered(task.rule);
216
+ this.state.completeTask(
217
+ task.id,
218
+ covered ? "coberto" : "investigado sem cobertura",
219
+ );
220
+ if (!covered)
221
+ this.state.setCoverage(task.rule, "skipped", "sem cobertura do modelo");
222
+ }
223
+ }
224
+
225
+ /** VERIFY — evidência obrigatória + crítica. */
226
+ private verify(): FindingDraft[] {
227
+ const { context } = this.deps;
228
+ const readFile = this.deps.readFile ?? this.defaultReader();
229
+ const allowed = new Set([
230
+ ...context.files,
231
+ ...(context.impact?.affectedFiles ?? []),
232
+ ]);
233
+ const verifier = new EvidenceVerifier({ allowedFiles: allowed, readFile });
234
+ const critic = new FindingCritic({
235
+ knownRules: new Set(context.watchdog.rules.map((rule) => rule.text.trim())),
236
+ verifier,
237
+ allowEmergentFindings:
238
+ context.watchdog.definition.allowEmergentFindings === true,
239
+ });
240
+ const decisions = critic.review(this.state.drafts, this.state);
241
+ const kept: FindingDraft[] = [];
242
+ for (const decision of decisions) {
243
+ this.deps.reporter.journal.note(
244
+ `crítico: ${decision.verdict} ${decision.draft.file}:${decision.draft.line ?? "?"} — ${decision.reason}`,
245
+ );
246
+ if (decision.verdict === "drop") {
247
+ this.state.setCoverage(
248
+ decision.draft.rule,
249
+ "checked",
250
+ "refutado pelo crítico",
251
+ );
252
+ continue;
253
+ }
254
+ kept.push(
255
+ decision.verdict === "downgrade"
256
+ ? {
257
+ ...decision.draft,
258
+ severity: downgrade(decision.draft.severity),
259
+ confidence: decision.draft.confidence,
260
+ }
261
+ : decision.draft,
262
+ );
263
+ this.state.setCoverage(decision.draft.rule, "checked", "confirmado");
264
+ }
265
+ return kept;
266
+ }
267
+
268
+ /** CONCLUDE — relatório determinístico + cobertura honesta. */
269
+ private conclude(kept: readonly FindingDraft[]): WatchdogReport {
270
+ const { context } = this.deps;
271
+ for (const rule of context.watchdog.rules) {
272
+ if (!this.state.isCovered(rule.text)) {
273
+ this.state.setCoverage(rule.text, "skipped", "não investigada");
274
+ }
275
+ }
276
+ const evidenceFiles = new Set(
277
+ [...this.state.evidence.values()].map((item) => item.file),
278
+ );
279
+ const skipped = new Set(context.overflow.map((file) => `${file} (budget)`));
280
+ for (const coverage of this.state.coverage.values()) {
281
+ if (coverage.status === "skipped") {
282
+ skipped.add(`${coverage.rule} — ${coverage.reason ?? "skipped"}`);
283
+ }
284
+ }
285
+
286
+ const report: WatchdogReport = {
287
+ watchdogId: context.watchdog.id,
288
+ reviewedFiles: [...evidenceFiles].filter((file) =>
289
+ context.files.includes(file),
290
+ ),
291
+ skipped: [...skipped],
292
+ findings: kept.map((draft) => ({
293
+ rule: draft.rule,
294
+ severity: draft.severity as Severity,
295
+ file: draft.file,
296
+ ...(draft.line ? { line: draft.line } : {}),
297
+ message: draft.message,
298
+ action: draft.action,
299
+ evidence: this.evidenceLabel(draft),
300
+ })),
301
+ };
302
+
303
+ const validator = new FindingValidator({
304
+ watchdogId: context.watchdog.id,
305
+ knownRules: new Set(context.watchdog.rules.map((rule) => rule.text.trim())),
306
+ allowedFiles: new Set([
307
+ ...context.files,
308
+ ...(context.impact?.affectedFiles ?? []),
309
+ ]),
310
+ overflow: new Set(context.overflow),
311
+ readFile: this.deps.readFile ?? this.defaultReader(),
312
+ allowEmergentFindings:
313
+ context.watchdog.definition.allowEmergentFindings === true,
314
+ });
315
+ const result = validator.validate(report);
316
+ for (const dropped of result.dropped) {
317
+ this.deps.reporter.journal.note(
318
+ `validator: descartado ${dropped.finding} — ${dropped.reason}`,
319
+ );
320
+ }
321
+ return result.report;
322
+ }
323
+
324
+ /** Chamada de modelo com uso e timeout. */
325
+ private async callModel<T>(
326
+ feature: string,
327
+ schema: import("zod").ZodType<T>,
328
+ prompt: string,
329
+ maxSteps: number,
330
+ instructions?: string,
331
+ tools?: Record<string, import("../../core/ai").AiTool>,
332
+ ): Promise<T> {
333
+ const response = await this.deps.provider.generate<T>({
334
+ instructions,
335
+ prompt,
336
+ outputSchema: schema,
337
+ ...(tools ? { tools } : {}),
338
+ maxSteps,
339
+ maxOutputTokens: this.deps.maxOutputTokens,
340
+ temperature: this.deps.temperature,
341
+ signal: AbortSignal.timeout(this.deps.timeoutMs),
342
+ });
343
+ this.usage = addUsage(this.usage, response.usage);
344
+ this.deps.reporter.journal.note(
345
+ `${feature}: ${response.usage.calls} chamada(s), ${response.usage.totalTokens} token(s)`,
346
+ );
347
+ return response.output;
348
+ }
349
+
350
+ /** Transição de estado registrada no journal. */
351
+ private transition(from: AgentStateName, to: AgentStateName): void {
352
+ this.deps.reporter.journal.transition(from, to);
353
+ }
354
+
355
+ /** Rótulo de evidência (`arquivo:linha`). */
356
+ private evidenceLabel(draft: FindingDraft): string {
357
+ const first = draft.evidenceIds
358
+ .map((id) => this.state.getEvidence(id))
359
+ .find((item) => item !== undefined);
360
+ if (!first) return draft.file;
361
+ return `${first.file}:${first.startLine}-${first.endLine}`;
362
+ }
363
+
364
+ /** Leitor padrão: relativo à raiz do repo. */
365
+ private defaultReader(): (path: string) => string | null {
366
+ const root = this.deps.context.repoRoot;
367
+ return (path: string) => {
368
+ try {
369
+ return readFileSync(join(root, path), "utf8");
370
+ } catch {
371
+ return null;
372
+ }
373
+ };
374
+ }
375
+ }
376
+
377
+ /** Símbolos citados nas regras (para o planner). */
378
+ function symbolsOf(context: AgentContext): string[] {
379
+ const symbols = new Set<string>();
380
+ for (const analysis of context.rules) {
381
+ for (const signal of analysis.signals) {
382
+ if (signal.kind === "symbol") symbols.add(signal.value);
383
+ }
384
+ }
385
+ for (const analysis of context.ast.values()) {
386
+ for (const symbol of analysis.delta?.addedSymbols ?? []) symbols.add(symbol.name);
387
+ for (const symbol of analysis.delta?.modifiedSymbols ?? [])
388
+ symbols.add(symbol.name);
389
+ }
390
+ return [...symbols].slice(0, 30);
391
+ }