@payconductor/houndsman 0.5.0 → 0.6.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 (153) 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/help.ts +17 -1
  11. package/src/cli/index.ts +10 -6
  12. package/src/core/ai/ai-client.ts +221 -0
  13. package/src/core/ai/ai-sdk-model-provider.ts +101 -0
  14. package/src/core/ai/errors.ts +36 -0
  15. package/src/core/ai/features/brief-enricher.ts +62 -0
  16. package/src/core/ai/features/classifier.ts +50 -0
  17. package/src/core/ai/features/context-summarizer.ts +31 -0
  18. package/src/core/ai/features/pre-analyzer.ts +51 -0
  19. package/src/core/ai/features/verifier.ts +45 -0
  20. package/src/core/ai/index.ts +56 -0
  21. package/src/core/ai/model-factory.ts +137 -0
  22. package/src/core/ai/prompts/base.ts +36 -0
  23. package/src/core/ai/prompts/brief.ts +87 -0
  24. package/src/core/ai/prompts/classification.ts +36 -0
  25. package/src/core/ai/prompts/context.ts +29 -0
  26. package/src/core/ai/prompts/pre-analysis.ts +49 -0
  27. package/src/core/ai/prompts/verify.ts +40 -0
  28. package/src/core/ai/schemas.ts +110 -0
  29. package/src/core/ai/types.ts +76 -0
  30. package/src/core/ai/usage-budget.ts +75 -0
  31. package/src/core/ai/usage-recorder.ts +79 -0
  32. package/src/core/assets.ts +16 -0
  33. package/src/core/ast/diff.ts +220 -0
  34. package/src/core/ast/impact.ts +127 -0
  35. package/src/core/ast/parser.ts +167 -0
  36. package/src/core/ast/service.ts +233 -0
  37. package/src/core/ast/symbols.ts +294 -0
  38. package/src/core/ast/types.ts +125 -0
  39. package/src/core/brief.ts +138 -0
  40. package/src/core/commands/findings.ts +59 -1
  41. package/src/core/commands/review.ts +2 -0
  42. package/src/core/commands/scaffold.ts +159 -10
  43. package/src/core/commands/update.ts +5 -3
  44. package/src/core/config.ts +85 -0
  45. package/src/core/defaults.ts +27 -0
  46. package/src/core/domain/watchdog.ts +43 -5
  47. package/src/core/engine/houndsman-engine.ts +16 -1
  48. package/src/core/engine/review-scope.ts +52 -6
  49. package/src/core/engine/review-session.ts +27 -2
  50. package/src/core/env.ts +101 -0
  51. package/src/core/file-classifier.ts +25 -4
  52. package/src/core/jev/jev-client.ts +8 -7
  53. package/src/core/jev/jev-verify.ts +2 -1
  54. package/src/core/logging/index.ts +17 -0
  55. package/src/core/logging/logger.ts +187 -0
  56. package/src/core/orchestrator.ts +357 -9
  57. package/src/core/project-config.ts +123 -1
  58. package/src/core/registry-check.ts +148 -6
  59. package/src/core/renderers/manifest-writer.ts +33 -0
  60. package/src/core/rule-text.ts +97 -0
  61. package/src/core/services/file-classifier.ts +10 -3
  62. package/src/core/services/verifier.ts +14 -3
  63. package/src/core/storage/paths.ts +6 -5
  64. package/src/core/storage/types.ts +6 -0
  65. package/src/core/storage/work-area.ts +12 -0
  66. package/src/core/types.ts +37 -1
  67. package/src/core/verify.ts +17 -5
  68. package/src/harness/README.md +15 -0
  69. package/src/harness/agent/agent-context.ts +33 -0
  70. package/src/harness/agent/agent-errors.ts +30 -0
  71. package/src/harness/agent/agent-factory.ts +45 -0
  72. package/src/harness/agent/agent-reporter.ts +34 -0
  73. package/src/harness/agent/agent-result.ts +16 -0
  74. package/src/harness/agent/agent-runner.ts +32 -0
  75. package/src/harness/agent/agent-scheduler.ts +37 -0
  76. package/src/harness/agent/review-agent.ts +391 -0
  77. package/src/harness/config/harness-config.ts +84 -0
  78. package/src/harness/hunt/hunt-harness.ts +83 -0
  79. package/src/harness/hunt/hunt-options.ts +75 -0
  80. package/src/harness/hunt/hunt-result.ts +17 -0
  81. package/src/harness/hunt/hunt-session.ts +372 -0
  82. package/src/harness/hunt/review-planner.ts +98 -0
  83. package/src/harness/index.ts +36 -0
  84. package/src/harness/investigation/agent-state.ts +134 -0
  85. package/src/harness/investigation/evidence-verifier.ts +129 -0
  86. package/src/harness/investigation/evidence.ts +110 -0
  87. package/src/harness/investigation/finding-critic.ts +96 -0
  88. package/src/harness/investigation/investigation-planner.ts +118 -0
  89. package/src/harness/investigation/investigation-task.ts +10 -0
  90. package/src/harness/investigation/review-journal.ts +87 -0
  91. package/src/harness/investigation/rule-analyzer.ts +178 -0
  92. package/src/harness/investigation/rule-signals.ts +110 -0
  93. package/src/harness/prompts/agent-schemas.ts +57 -0
  94. package/src/harness/prompts/reviewer-prompt-builder.ts +101 -0
  95. package/src/harness/prompts/states/conclude-prompt.ts +16 -0
  96. package/src/harness/prompts/states/investigate-prompt.ts +37 -0
  97. package/src/harness/prompts/states/orient-prompt.ts +15 -0
  98. package/src/harness/prompts/states/verify-prompt.ts +22 -0
  99. package/src/harness/tools/agent-tool.ts +102 -0
  100. package/src/harness/tools/builtin/changed-symbols-tool.ts +48 -0
  101. package/src/harness/tools/builtin/doc-context-tool.ts +74 -0
  102. package/src/harness/tools/builtin/file-outline-tool.ts +66 -0
  103. package/src/harness/tools/builtin/find-references-tool.ts +66 -0
  104. package/src/harness/tools/builtin/find-symbol-tool.ts +91 -0
  105. package/src/harness/tools/builtin/helpers.ts +115 -0
  106. package/src/harness/tools/builtin/impact-tool.ts +48 -0
  107. package/src/harness/tools/builtin/imports-exports-tool.ts +53 -0
  108. package/src/harness/tools/builtin/index.ts +59 -0
  109. package/src/harness/tools/builtin/list-files-tool.ts +85 -0
  110. package/src/harness/tools/builtin/pr-context-tool.ts +41 -0
  111. package/src/harness/tools/builtin/read-file-range-tool.ts +73 -0
  112. package/src/harness/tools/builtin/rule-signals-tool.ts +36 -0
  113. package/src/harness/tools/builtin/search-code-tool.ts +65 -0
  114. package/src/harness/tools/builtin/symbol-context-tool.ts +106 -0
  115. package/src/harness/tools/builtin/tests-for-symbol-tool.ts +61 -0
  116. package/src/harness/tools/path-guard.ts +90 -0
  117. package/src/harness/tools/result-limiter.ts +49 -0
  118. package/src/harness/tools/secret-redactor.ts +54 -0
  119. package/src/harness/tools/tool-cache.ts +50 -0
  120. package/src/harness/tools/tool-call-budget.ts +60 -0
  121. package/src/harness/tools/tool-context.ts +5 -0
  122. package/src/harness/tools/tool-middleware.ts +105 -0
  123. package/src/harness/tools/tool-policy.ts +51 -0
  124. package/src/harness/tools/tool-registry.ts +112 -0
  125. package/src/harness/tools/tool-result.ts +15 -0
  126. package/src/harness/tools/toolbox.ts +36 -0
  127. package/src/harness/validation/finding-validator.ts +119 -0
  128. package/src/index.ts +46 -0
  129. package/src/server/client/utils/constants.ts +27 -5
  130. package/src/server/client/utils/types.ts +6 -0
  131. package/src/server/index.ts +3 -2
  132. package/src/skill/OUTPUT-FORMAT.md +12 -0
  133. package/src/skill/SETUP.md +6 -2
  134. package/src/skill/hunt.md +14 -2
  135. package/src/skill/verify.md +1 -1
  136. package/src/tests/__snapshots__/baseline.test.ts.snap +96 -14
  137. package/src/tests/ai-core.test.ts +120 -0
  138. package/src/tests/ai-features.test.ts +183 -0
  139. package/src/tests/ast.test.ts +335 -0
  140. package/src/tests/env.test.ts +69 -0
  141. package/src/tests/fallback.test.ts +172 -0
  142. package/src/tests/fixtures/fake-agent-tools.ts +69 -0
  143. package/src/tests/fixtures/fake-investigation.ts +64 -0
  144. package/src/tests/fixtures/fake-model-provider.ts +48 -0
  145. package/src/tests/harness-agent.test.ts +122 -0
  146. package/src/tests/harness-investigation.test.ts +206 -0
  147. package/src/tests/harness-tools.test.ts +148 -0
  148. package/src/tests/hunt-command.test.ts +212 -0
  149. package/src/tests/logger.test.ts +83 -0
  150. package/src/tests/review-scope.test.ts +11 -11
  151. package/src/tests/rule-analyzer.test.ts +61 -0
  152. package/src/tests/tool-contracts.test.ts +82 -0
  153. package/src/tests/verify.test.ts +1 -1
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Análise determinística de regras (`RuleAnalyzer`).
3
+ *
4
+ * Transforma uma regra textual em polaridade, categoria, keywords, sinais e
5
+ * checks. É a base do planner e do `rule_signals` tool. Um analyzer por IA pode
6
+ * complementar regras ambíguas, mas o resultado continua validado.
7
+ */
8
+
9
+ import { POLARITY_WORDS, polarityOf, type RuleIntent } from "../../core/rule-text";
10
+ import type { Rule, Severity } from "../../core/types";
11
+ import {
12
+ DefaultRuleCheckFactory,
13
+ HeuristicRuleSignalExtractor,
14
+ type RuleCheck,
15
+ type RuleCheckFactory,
16
+ type RuleSignal,
17
+ type RuleSignalExtractor,
18
+ } from "./rule-signals";
19
+
20
+ export { polarityOf, type RuleIntent } from "../../core/rule-text";
21
+
22
+ /** Categoria da regra. */
23
+ export type RuleCategory =
24
+ | "architecture"
25
+ | "security"
26
+ | "data"
27
+ | "tests"
28
+ | "performance"
29
+ | "style"
30
+ | "other";
31
+
32
+ /** Regra analisada (entrada do planner). */
33
+ export type RuleAnalysis = {
34
+ rule: string;
35
+ severity: Severity;
36
+ intent: RuleIntent;
37
+ category: RuleCategory;
38
+ keywords: string[];
39
+ signals: RuleSignal[];
40
+ checks: RuleCheck[];
41
+ confidence: "high" | "medium" | "low";
42
+ };
43
+
44
+ const GENERIC_STOPWORDS = [
45
+ "para",
46
+ "pelo",
47
+ "pela",
48
+ "como",
49
+ "ser",
50
+ "que",
51
+ "com",
52
+ "sem",
53
+ "uma",
54
+ "dos",
55
+ "das",
56
+ "nos",
57
+ "nas",
58
+ "por",
59
+ "usar",
60
+ "todo",
61
+ "toda",
62
+ "quando",
63
+ "onde",
64
+ ] as const;
65
+
66
+ /** Stopwords + palavras de polaridade (fonte única em `core/rule-text`). */
67
+ const STOPWORDS = new Set<string>([...GENERIC_STOPWORDS, ...POLARITY_WORDS]);
68
+
69
+ const CATEGORY_HINTS: readonly [RuleCategory, readonly string[]][] = [
70
+ [
71
+ "security",
72
+ [
73
+ "segredo",
74
+ "senha",
75
+ "token",
76
+ "auth",
77
+ "sanit",
78
+ "injection",
79
+ "sql",
80
+ "cripto",
81
+ "permiss",
82
+ ],
83
+ ],
84
+ ["data", ["banco", "database", "db", "query", "migration", "schema", "transa"]],
85
+ ["tests", ["teste", "test", "spec", "cobertura", "mock", "fixture"]],
86
+ [
87
+ "performance",
88
+ ["cache", "performance", "latência", "latencia", "n+1", "índice", "indice"],
89
+ ],
90
+ [
91
+ "architecture",
92
+ [
93
+ "camada",
94
+ "arquitetura",
95
+ "rota",
96
+ "controller",
97
+ "service",
98
+ "domínio",
99
+ "dominio",
100
+ "depend",
101
+ ],
102
+ ],
103
+ ["style", ["nome", "formata", "lint", "coment", "estilo", "padrão", "padrao"]],
104
+ ];
105
+
106
+ /** Analisador determinístico. */
107
+ export class RuleAnalyzer {
108
+ private readonly extractor: RuleSignalExtractor;
109
+ private readonly checks: RuleCheckFactory;
110
+
111
+ constructor(
112
+ extractor: RuleSignalExtractor = new HeuristicRuleSignalExtractor(),
113
+ checks: RuleCheckFactory = new DefaultRuleCheckFactory(),
114
+ ) {
115
+ this.extractor = extractor;
116
+ this.checks = checks;
117
+ }
118
+
119
+ /** Analisa todas as regras (ordem estável). */
120
+ analyze(rules: readonly Rule[]): RuleAnalysis[] {
121
+ return rules.map((rule) => this.analyzeRule(rule));
122
+ }
123
+
124
+ /** Analisa uma regra. */
125
+ analyzeRule(rule: Rule): RuleAnalysis {
126
+ const intent = polarityOf(rule.text);
127
+ const signals = this.extractor.extract(rule);
128
+ const category = categoryOf(rule.text, signals);
129
+ const confidence = confidenceOf(rule, signals);
130
+ return {
131
+ rule: rule.text,
132
+ severity: rule.severity,
133
+ intent,
134
+ category,
135
+ keywords: keywordsOf(rule.text),
136
+ signals,
137
+ checks: this.checks.create(rule, signals),
138
+ confidence,
139
+ };
140
+ }
141
+ }
142
+
143
+ /** Categoria inferida por keywords e sinais. */
144
+ export function categoryOf(text: string, signals: readonly RuleSignal[]): RuleCategory {
145
+ const haystack =
146
+ `${text} ${signals.map((signal) => signal.value).join(" ")}`.toLowerCase();
147
+ for (const [category, hints] of CATEGORY_HINTS) {
148
+ if (hints.some((hint) => haystack.includes(hint))) return category;
149
+ }
150
+ return "other";
151
+ }
152
+
153
+ /** Keywords normalizadas (sem stopwords, limitadas). */
154
+ export function keywordsOf(text: string): string[] {
155
+ const words = text
156
+ .toLowerCase()
157
+ .replace(/[^\p{L}\p{N}+\-/]+/gu, " ")
158
+ .split(/\s+/)
159
+ .filter((word) => word.length >= 4 && !STOPWORDS.has(word));
160
+ return [...new Set(words)].slice(0, 12);
161
+ }
162
+
163
+ /** Confiança da análise (hints explícitos valem mais que heurística). */
164
+ function confidenceOf(
165
+ rule: Rule,
166
+ signals: readonly RuleSignal[],
167
+ ): RuleAnalysis["confidence"] {
168
+ const hints = rule.hints;
169
+ const hintCount =
170
+ (hints?.symbols?.length ?? 0) +
171
+ (hints?.imports?.length ?? 0) +
172
+ (hints?.decorators?.length ?? 0) +
173
+ (hints?.paths?.length ?? 0) +
174
+ (hints?.checks?.length ?? 0);
175
+ if (hintCount > 0) return "high";
176
+ if (signals.length > 0) return "medium";
177
+ return "low";
178
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Extração determinística de sinais de uma regra em linguagem natural.
3
+ *
4
+ * Cada extrator é substituível (`RuleSignalExtractor`); o default usa `hints`
5
+ * + termos em backticks + identificadores. Nunca chama IA.
6
+ */
7
+
8
+ import type { Rule } from "../../core/types";
9
+
10
+ /** Sinal concreto sugerido por uma regra. */
11
+ export type RuleSignal = {
12
+ kind: "symbol" | "import" | "decorator" | "path" | "grep" | "ast";
13
+ value: string;
14
+ rationale?: string;
15
+ };
16
+
17
+ /** Contrato de um extrator de sinais. */
18
+ export interface RuleSignalExtractor {
19
+ extract(rule: Rule): RuleSignal[];
20
+ }
21
+
22
+ /** Extrai sinais dos `hints`, backticks e identificadores em CamelCase. */
23
+ export class HeuristicRuleSignalExtractor implements RuleSignalExtractor {
24
+ extract(rule: Rule): RuleSignal[] {
25
+ const signals: RuleSignal[] = [];
26
+ const seen = new Set<string>();
27
+ const push = (kind: RuleSignal["kind"], value: string, rationale?: string) => {
28
+ const key = `${kind}:${value}`;
29
+ if (!value.trim() || seen.has(key)) return;
30
+ seen.add(key);
31
+ signals.push({ kind, value, ...(rationale ? { rationale } : {}) });
32
+ };
33
+
34
+ const hints = rule.hints ?? {};
35
+ for (const item of hints.symbols ?? []) push("symbol", item, "hint");
36
+ for (const item of hints.imports ?? []) push("import", item, "hint");
37
+ for (const item of hints.decorators ?? []) push("decorator", item, "hint");
38
+ for (const item of hints.paths ?? []) push("path", item, "hint");
39
+
40
+ for (const match of rule.text.matchAll(/`([^`]{2,80})`/g)) {
41
+ const value = match[1]?.trim();
42
+ if (value) push(classifyLiteral(value), value, "texto da regra");
43
+ }
44
+ for (const match of rule.text.matchAll(/\b([A-Z][a-zA-Z0-9]{2,})\b/g)) {
45
+ push("symbol", match[1] ?? "", "identificador");
46
+ }
47
+ return signals;
48
+ }
49
+ }
50
+
51
+ /** Classifica um literal em backticks (import, path, símbolo). */
52
+ function classifyLiteral(value: string): RuleSignal["kind"] {
53
+ if (value.startsWith("@") || value.includes("/")) {
54
+ return value.includes("/") && !value.includes("@") ? "path" : "import";
55
+ }
56
+ if (/^[A-Z][a-zA-Z0-9]*$/.test(value)) return "symbol";
57
+ return "grep";
58
+ }
59
+
60
+ /** Gera checks concretos para uma regra. */
61
+ export interface RuleCheckFactory {
62
+ create(rule: Rule, signals: readonly RuleSignal[]): RuleCheck[];
63
+ }
64
+
65
+ /** Check que prova/refuta uma regra usando uma tool. */
66
+ export type RuleCheck = {
67
+ id: string;
68
+ description: string;
69
+ tool: string;
70
+ input: unknown;
71
+ required: boolean;
72
+ };
73
+
74
+ /** Checks default por presença de sinais. */
75
+ export class DefaultRuleCheckFactory implements RuleCheckFactory {
76
+ create(rule: Rule, signals: readonly RuleSignal[]): RuleCheck[] {
77
+ const checks: RuleCheck[] = [];
78
+ const explicit = rule.hints?.checks ?? [];
79
+ explicit.forEach((tool, index) => {
80
+ checks.push({
81
+ id: `${index}:${tool}`,
82
+ description: `Executar ${tool} para a regra`,
83
+ tool,
84
+ input: {},
85
+ required: true,
86
+ });
87
+ });
88
+ const symbols = signals.filter((signal) => signal.kind === "symbol");
89
+ if (symbols.length > 0 && explicit.length === 0) {
90
+ checks.push({
91
+ id: "symbol-context",
92
+ description: `Inspecionar o contexto de ${symbols[0]?.value}`,
93
+ tool: "symbol_context",
94
+ input: { symbol: symbols[0]?.value },
95
+ required: false,
96
+ });
97
+ }
98
+ const imports = signals.filter((signal) => signal.kind === "import");
99
+ if (imports.length > 0) {
100
+ checks.push({
101
+ id: "find-references",
102
+ description: `Procurar usos de ${imports[0]?.value}`,
103
+ tool: "find_references",
104
+ input: { symbol: imports[0]?.value },
105
+ required: false,
106
+ });
107
+ }
108
+ return checks;
109
+ }
110
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Schemas das saídas intermediárias da máquina de estados.
3
+ */
4
+
5
+ import { z } from "zod";
6
+ import { SeveritySchema } from "../../core/ai/schemas";
7
+
8
+ /** ORIENT: hipóteses iniciais. */
9
+ export const OrientOutputSchema = z.object({
10
+ hypotheses: z.array(z.string()).max(8).default([]),
11
+ });
12
+ export type OrientOutput = z.infer<typeof OrientOutputSchema>;
13
+
14
+ /** PLAN: tarefas escolhidas do plano determinístico. */
15
+ export const PlanOutputSchema = z.object({
16
+ taskIds: z.array(z.string()).default([]),
17
+ });
18
+ export type PlanOutput = z.infer<typeof PlanOutputSchema>;
19
+
20
+ /** INVESTIGATE: drafts + cobertura. */
21
+ export const FindingDraftSchema = z.object({
22
+ rule: z.string(),
23
+ severity: SeveritySchema,
24
+ file: z.string(),
25
+ line: z.number().int().positive().optional(),
26
+ message: z.string(),
27
+ action: z.string(),
28
+ evidenceIds: z.array(z.string()).default([]),
29
+ confidence: z.number().min(0).max(1).default(0.6),
30
+ });
31
+
32
+ export const CoverageOutputSchema = z.object({
33
+ rule: z.string(),
34
+ status: z.enum(["checked", "not_applicable", "skipped"]),
35
+ reason: z.string().optional(),
36
+ });
37
+
38
+ export const InvestigateOutputSchema = z.object({
39
+ findingDrafts: z.array(FindingDraftSchema).default([]),
40
+ coverage: z.array(CoverageOutputSchema).default([]),
41
+ });
42
+ export type InvestigateOutput = z.infer<typeof InvestigateOutputSchema>;
43
+
44
+ /** VERIFY: decisão do crítico (opcional; determinístico por default). */
45
+ export const CriticOutputSchema = z.object({
46
+ decisions: z
47
+ .array(
48
+ z.object({
49
+ rule: z.string(),
50
+ file: z.string(),
51
+ verdict: z.enum(["keep", "drop", "downgrade"]),
52
+ reason: z.string().optional(),
53
+ }),
54
+ )
55
+ .default([]),
56
+ });
57
+ export type CriticOutput = z.infer<typeof CriticOutputSchema>;
@@ -0,0 +1,101 @@
1
+ /**
2
+ * System prompt do revisor (harness nativo).
3
+ *
4
+ * Ordem estável: papel, regras, escopo, procedimento, contrato e segurança.
5
+ * Descrições de tools vêm do `ToolRegistry` (nunca hardcoded).
6
+ */
7
+
8
+ import { buildSystem } from "../../core/ai/prompts/base";
9
+ import { OUTPUT_FORMAT_PATH } from "../../core/paths";
10
+ import type { AgentContext } from "../agent/agent-context";
11
+
12
+ /** System prompt do agente. */
13
+ export function buildReviewerSystem(
14
+ context: AgentContext,
15
+ toolDescriptions: string,
16
+ ): string {
17
+ const rules = context.watchdog.rules.map(
18
+ (rule) => `- [${rule.severity}] ${rule.text}`,
19
+ );
20
+ return buildSystem(
21
+ `Você é um revisor sênior de ${context.projectName}, especialista no domínio '${context.watchdog.name}'.`,
22
+ [
23
+ "Você investiga o diff com tools read-only e só conclui com evidência.",
24
+ "Nunca invente achado, arquivo, linha ou regra.",
25
+ "Toda regra deve terminar com cobertura (checked, not_applicable ou skipped).",
26
+ "blocking/attention exigem evidência rastreável.",
27
+ context.watchdog.definition.allowEmergentFindings
28
+ ? "Findings emergentes de lógica/domínio usam `Lógica:`/`Domínio:`, exigem evidência e severidade máxima attention."
29
+ : "Todo finding deve citar uma regra existente do watchdog.",
30
+ "",
31
+ "## Regras do watchdog",
32
+ ...rules,
33
+ "",
34
+ "## Escopo",
35
+ context.watchdog.scope,
36
+ "",
37
+ "## Procedimento",
38
+ "1. ORIENT — entenda regras, diff, AST e impacto.",
39
+ "2. PLAN — escolha as tarefas priorizadas.",
40
+ "3. INVESTIGATE — use tools para coletar evidências; refute hipóteses.",
41
+ "4. VERIFY — cada draft precisa de evidência válida.",
42
+ "5. CONCLUDE — produza o relatório.",
43
+ "",
44
+ "## Tools disponíveis",
45
+ toolDescriptions,
46
+ "",
47
+ "## Contrato de saída",
48
+ `O relatório final segue ${OUTPUT_FORMAT_PATH}.`,
49
+ "Em INVESTIGATE produza JSON com findingDrafts e coverage.",
50
+ ],
51
+ );
52
+ }
53
+
54
+ /** Prompt do usuário (contexto compactado; nunca arquivo inteiro). */
55
+ export function buildReviewerUser(context: AgentContext): string {
56
+ const lines = [
57
+ `Branch: ${context.branch}`,
58
+ `Watchdog: ${context.watchdog.name} (${context.watchdog.id})`,
59
+ `Arquivos do domínio (${context.files.length}):`,
60
+ ...context.files.slice(0, 40).map((file) => `- ${file}`),
61
+ ];
62
+ if (context.overflow.length > 0) {
63
+ lines.push(
64
+ "",
65
+ `Fora do orçamento (${context.overflow.length}) — NÃO ler; registrar em skipped:`,
66
+ ...context.overflow.slice(0, 40).map((file) => `- ${file}`),
67
+ );
68
+ }
69
+ if (context.deleted.length > 0) {
70
+ lines.push("", "Deletados:", ...context.deleted.map((file) => `- ${file}`));
71
+ }
72
+ if (context.context) {
73
+ lines.push("", "## Contexto da PR");
74
+ if (context.context.objective)
75
+ lines.push(`Objetivo: ${context.context.objective}`);
76
+ if (context.context.motivation)
77
+ lines.push(`Motivação: ${context.context.motivation}`);
78
+ for (const item of context.context.changeSummary ?? []) lines.push(`- ${item}`);
79
+ }
80
+ if (context.ast.size > 0) {
81
+ lines.push("", "## Estrutura (AST)");
82
+ for (const [file, analysis] of context.ast) {
83
+ if (!analysis.delta) continue;
84
+ const delta = analysis.delta;
85
+ lines.push(
86
+ `- ${file}: +${delta.addedSymbols.length} −${delta.removedSymbols.length} ~${delta.modifiedSymbols.length}`,
87
+ );
88
+ }
89
+ }
90
+ if (context.impact && context.impact.affectedFiles.length > 0) {
91
+ lines.push(
92
+ "",
93
+ `Impacto: ${context.impact.affectedFiles.length} arquivo(s) importam o diff.`,
94
+ ...context.impact.affectedFiles.slice(0, 20).map((file) => `- ${file}`),
95
+ );
96
+ }
97
+ if (context.docs.length > 0) {
98
+ lines.push("", "Docs de contexto:", ...context.docs.map((doc) => `- ${doc}`));
99
+ }
100
+ return lines.join("\n");
101
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Prompt do estado CONCLUDE (documentação do contrato).
3
+ *
4
+ * A conclusão do harness é determinística (bits de evidência/coverage); este
5
+ * texto existe para manter o contrato explícito e servir a um futuro modo
6
+ * "conclude por modelo".
7
+ */
8
+
9
+ /** Instruções de conclusão. */
10
+ export function concludePrompt(): string {
11
+ return [
12
+ "Conclua com base apenas nos drafts sobreviventes ao VERIFY.",
13
+ "Não adicione achados novos.",
14
+ "Toda regra sem achado deve constar como coverage checked/not_applicable/skipped.",
15
+ ].join("\n");
16
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Prompt do estado INVESTIGATE — o coração da coleta de evidências.
3
+ */
4
+
5
+ import type { AgentContext } from "../../agent/agent-context";
6
+ import type { InvestigationTask } from "../../investigation/investigation-task";
7
+
8
+ /** Instruções do estado de investigação. */
9
+ export function investigatePrompt(
10
+ context: AgentContext,
11
+ tasks: readonly InvestigationTask[],
12
+ ): string {
13
+ const lines = [
14
+ "## Tarefas priorizadas",
15
+ ...tasks.map(
16
+ (task, index) =>
17
+ `${index + 1}. [${task.severity}] ${task.rule} — arquivos: ${task.files.slice(0, 8).join(", ") || "(domínio)"}`,
18
+ ),
19
+ "",
20
+ "## Como investigar",
21
+ "- Use file_outline/symbol_context/find_references em vez de ler arquivos inteiros.",
22
+ "- Registre evidência para cada draft (evidenceIds).",
23
+ "- Prefira não concluir quando faltar evidência: use coverage 'skipped' com motivo.",
24
+ "- Não cite arquivos fora do domínio/impacto.",
25
+ "",
26
+ "## Formato",
27
+ "Produza JSON: { findingDrafts: [...], coverage: [{ rule, status, reason? }] }.",
28
+ ];
29
+ if (context.files.length > 0) {
30
+ lines.push(
31
+ "",
32
+ "## Arquivos do domínio",
33
+ ...context.files.slice(0, 40).map((file) => `- ${file}`),
34
+ );
35
+ }
36
+ return lines.join("\n");
37
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Prompt do estado ORIENT.
3
+ */
4
+
5
+ import type { AgentContext } from "../../agent/agent-context";
6
+
7
+ /** Pede hipóteses iniciais (sem achados). */
8
+ export function orientPrompt(context: AgentContext): string {
9
+ return [
10
+ `Oriente a revisão do watchdog '${context.watchdog.name}'.`,
11
+ "Liste hipóteses de violação que precisam ser investigadas.",
12
+ "Use as regras e o delta AST fornecidos. Não emita findings neste estado.",
13
+ "Produza JSON com `hypotheses`.",
14
+ ].join("\n");
15
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Prompt do estado VERIFY (crítica refutadora).
3
+ *
4
+ * O crítico determinístico é o default; este prompt fica disponível para um
5
+ * crítico por IA, que deve tentar **derrubar** cada draft.
6
+ */
7
+
8
+ import type { FindingDraft } from "../../investigation/evidence";
9
+
10
+ /** Prompt de refutação. */
11
+ export function verifyPrompt(drafts: readonly FindingDraft[]): string {
12
+ return [
13
+ "Tente refutar cada draft abaixo.",
14
+ "Responda `keep` apenas se a evidência sustenta a violação.",
15
+ "Use `downgrade` quando a evidência sustenta algo mais leve.",
16
+ ...drafts.map(
17
+ (draft) =>
18
+ `- ${draft.rule} · ${draft.file}:${draft.line ?? "?"} · confiança ${draft.confidence}`,
19
+ ),
20
+ "Produza JSON com `decisions`.",
21
+ ].join("\n");
22
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Contrato das tools do harness nativo.
3
+ *
4
+ * Cada tool declara metadados (categoria, capabilities, custo, cache, limite) e
5
+ * um schema Zod. É read-only por construção: não executa shell arbitrário, não
6
+ * escreve arquivos e não acessa a rede.
7
+ */
8
+
9
+ import type { ZodType, ZodTypeAny } from "zod";
10
+ import type { AstService } from "../../core/ast/service";
11
+ import type { DiffSource } from "../../core/diff-source";
12
+ import type { Watchdog } from "../../core/domain/watchdog";
13
+ import type { AgentContext } from "../agent/agent-context";
14
+ import type { AgentReporter } from "../agent/agent-reporter";
15
+ import type { AgentState } from "../investigation/agent-state";
16
+ import type { Evidence } from "../investigation/evidence";
17
+ import type { RuleAnalysis } from "../investigation/rule-analyzer";
18
+ import type { ToolCache } from "./tool-cache";
19
+ import type { ToolCallBudget } from "./tool-call-budget";
20
+
21
+ /** Categoria de agrupamento (usada no prompt e na policy). */
22
+ export type ToolCategory = "context" | "code" | "symbol" | "ast" | "rules" | "evidence";
23
+
24
+ /** Capability declarada por uma tool (a policy libera por capability). */
25
+ export type ToolCapability =
26
+ | "read"
27
+ | "search"
28
+ | "outline"
29
+ | "symbol"
30
+ | "references"
31
+ | "ast"
32
+ | "impact"
33
+ | "rules"
34
+ | "tests"
35
+ | "docs";
36
+
37
+ /** Metadados estáveis (o prompt é gerado a partir daqui). */
38
+ export type ToolMetadata = {
39
+ name: string;
40
+ description: string;
41
+ category: ToolCategory;
42
+ capabilities: readonly ToolCapability[];
43
+ readOnly: true;
44
+ /** Custo relativo para o planner (1 = barato, 5 = caro). */
45
+ cost: 1 | 2 | 3 | 4 | 5;
46
+ cacheTtlMs?: number;
47
+ maxCallsPerAgent?: number;
48
+ };
49
+
50
+ /** Resultado de uma tool (sempre estruturado). */
51
+ export type ToolResult<T = unknown> = {
52
+ ok: boolean;
53
+ data?: T;
54
+ evidence?: Evidence[];
55
+ truncated?: boolean;
56
+ hint?: string;
57
+ error?: string;
58
+ };
59
+
60
+ /** Contexto de execução de uma tool. */
61
+ export type ToolContext = {
62
+ repoRoot: string;
63
+ watchdog: Watchdog;
64
+ diff: DiffSource;
65
+ ast: AstService;
66
+ state: AgentState;
67
+ budget: ToolCallBudget;
68
+ cache: ToolCache;
69
+ logger: AgentReporter;
70
+ agent: AgentContext;
71
+ /** Análises determinísticas das regras do watchdog (por texto). */
72
+ rules: ReadonlyMap<string, RuleAnalysis>;
73
+ };
74
+
75
+ /** Tool executável por um agente. */
76
+ export interface AgentTool<I = unknown, O = unknown> {
77
+ readonly metadata: ToolMetadata;
78
+ readonly inputSchema: ZodType<I>;
79
+ execute(input: I, context: ToolContext): Promise<ToolResult<O>>;
80
+ }
81
+
82
+ /** Módulo de registro de tools (permite agrupar por domínio). */
83
+ export interface ToolModule {
84
+ register(registry: ToolRegistryLike): void;
85
+ }
86
+
87
+ /**
88
+ * Forma aceita pelo registry. `execute(input: never)` aceita qualquer tool
89
+ * concreta por contravariância, sem apelar para `any`.
90
+ */
91
+ export type RegisterableTool = {
92
+ readonly metadata: ToolMetadata;
93
+ readonly inputSchema: ZodTypeAny;
94
+ execute(input: never, context: ToolContext): Promise<ToolResult<unknown>>;
95
+ };
96
+
97
+ /** Superfície mínima de registro (evita dependência circular com a classe). */
98
+ export interface ToolRegistryLike {
99
+ registerTool(tool: RegisterableTool): void;
100
+ }
101
+
102
+ export type { ZodTypeAny };
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Tool `changed_symbols` — delta AST da revisão (o que realmente mudou).
3
+ */
4
+
5
+ import { z } from "zod";
6
+ import type { AgentTool, ToolMetadata, ToolResult } from "../agent-tool";
7
+
8
+ const Input = z.object({ file: z.string().optional() });
9
+
10
+ /** Símbolos adicionados/removidos/modificados e assinaturas alteradas. */
11
+ export class ChangedSymbolsTool implements AgentTool<z.infer<typeof Input>, unknown> {
12
+ readonly metadata: ToolMetadata = {
13
+ name: "changed_symbols",
14
+ description: "Delta AST da revisão: símbolos adicionados/removidos/modificados.",
15
+ category: "ast",
16
+ capabilities: ["ast"],
17
+ readOnly: true,
18
+ cost: 1,
19
+ cacheTtlMs: 300_000,
20
+ };
21
+
22
+ readonly inputSchema = Input;
23
+
24
+ async execute(
25
+ input: z.infer<typeof Input>,
26
+ context: import("../tool-context").ToolContext,
27
+ ): Promise<ToolResult<unknown>> {
28
+ const entries = [];
29
+ for (const [file, analysis] of context.agent.ast) {
30
+ if (input.file && file !== input.file) continue;
31
+ if (!analysis.delta) continue;
32
+ entries.push({
33
+ file,
34
+ added: analysis.delta.addedSymbols.map((symbol) => symbol.name),
35
+ removed: analysis.delta.removedSymbols.map((symbol) => symbol.name),
36
+ modified: analysis.delta.modifiedSymbols.map((symbol) => symbol.name),
37
+ changedSignatures: analysis.delta.changedSignatures,
38
+ addedImports: analysis.delta.addedImports,
39
+ removedImports: analysis.delta.removedImports,
40
+ });
41
+ }
42
+ return {
43
+ ok: true,
44
+ data: { files: entries },
45
+ ...(entries.length === 0 ? { hint: "Sem delta AST para os filtros." } : {}),
46
+ };
47
+ }
48
+ }