@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.
- package/README.md +24 -4
- package/assets/grammars/tree-sitter-javascript.wasm +0 -0
- package/assets/grammars/tree-sitter-tsx.wasm +0 -0
- package/assets/grammars/tree-sitter-typescript.wasm +0 -0
- package/assets/grammars/web-tree-sitter.wasm +0 -0
- package/package.json +11 -3
- package/src/cli/commands/command.ts +5 -0
- package/src/cli/commands/doctor-command.ts +116 -24
- package/src/cli/commands/hunt-command.ts +25 -0
- package/src/cli/commands/server-command.ts +7 -1
- package/src/cli/help.ts +18 -2
- package/src/cli/index.ts +10 -6
- package/src/core/ai/ai-client.ts +221 -0
- package/src/core/ai/ai-sdk-model-provider.ts +101 -0
- package/src/core/ai/errors.ts +36 -0
- package/src/core/ai/features/brief-enricher.ts +62 -0
- package/src/core/ai/features/classifier.ts +50 -0
- package/src/core/ai/features/context-summarizer.ts +31 -0
- package/src/core/ai/features/pre-analyzer.ts +51 -0
- package/src/core/ai/features/verifier.ts +45 -0
- package/src/core/ai/index.ts +56 -0
- package/src/core/ai/model-factory.ts +137 -0
- package/src/core/ai/prompts/base.ts +36 -0
- package/src/core/ai/prompts/brief.ts +87 -0
- package/src/core/ai/prompts/classification.ts +36 -0
- package/src/core/ai/prompts/context.ts +29 -0
- package/src/core/ai/prompts/pre-analysis.ts +49 -0
- package/src/core/ai/prompts/verify.ts +40 -0
- package/src/core/ai/schemas.ts +110 -0
- package/src/core/ai/types.ts +76 -0
- package/src/core/ai/usage-budget.ts +75 -0
- package/src/core/ai/usage-recorder.ts +79 -0
- package/src/core/assets.ts +16 -0
- package/src/core/ast/diff.ts +220 -0
- package/src/core/ast/impact.ts +127 -0
- package/src/core/ast/parser.ts +167 -0
- package/src/core/ast/service.ts +233 -0
- package/src/core/ast/symbols.ts +294 -0
- package/src/core/ast/types.ts +125 -0
- package/src/core/brief.ts +138 -0
- package/src/core/commands/findings.ts +65 -2
- package/src/core/commands/review.ts +2 -0
- package/src/core/commands/scaffold.ts +159 -10
- package/src/core/commands/update.ts +5 -3
- package/src/core/config.ts +85 -0
- package/src/core/defaults.ts +27 -0
- package/src/core/domain/watchdog.ts +43 -5
- package/src/core/engine/houndsman-engine.ts +16 -1
- package/src/core/engine/review-scope.ts +52 -6
- package/src/core/engine/review-session.ts +27 -2
- package/src/core/env.ts +101 -0
- package/src/core/file-classifier.ts +25 -4
- package/src/core/git-runner.ts +25 -13
- package/src/core/github-review.ts +10 -7
- package/src/core/jev/jev-client.ts +8 -7
- package/src/core/jev/jev-verify.ts +2 -1
- package/src/core/logging/index.ts +17 -0
- package/src/core/logging/logger.ts +187 -0
- package/src/core/orchestrator.ts +357 -9
- package/src/core/project-config.ts +123 -1
- package/src/core/registry-check.ts +148 -6
- package/src/core/renderers/manifest-writer.ts +33 -0
- package/src/core/rule-text.ts +97 -0
- package/src/core/services/file-classifier.ts +10 -3
- package/src/core/services/gh-client.ts +38 -22
- package/src/core/services/verifier.ts +14 -3
- package/src/core/storage/paths.ts +6 -5
- package/src/core/storage/types.ts +6 -0
- package/src/core/storage/work-area.ts +12 -0
- package/src/core/types.ts +37 -1
- package/src/core/verify.ts +17 -5
- package/src/harness/README.md +15 -0
- package/src/harness/agent/agent-context.ts +33 -0
- package/src/harness/agent/agent-errors.ts +30 -0
- package/src/harness/agent/agent-factory.ts +45 -0
- package/src/harness/agent/agent-reporter.ts +34 -0
- package/src/harness/agent/agent-result.ts +16 -0
- package/src/harness/agent/agent-runner.ts +32 -0
- package/src/harness/agent/agent-scheduler.ts +37 -0
- package/src/harness/agent/review-agent.ts +391 -0
- package/src/harness/config/harness-config.ts +84 -0
- package/src/harness/hunt/hunt-harness.ts +83 -0
- package/src/harness/hunt/hunt-options.ts +75 -0
- package/src/harness/hunt/hunt-result.ts +17 -0
- package/src/harness/hunt/hunt-session.ts +372 -0
- package/src/harness/hunt/review-planner.ts +98 -0
- package/src/harness/index.ts +36 -0
- package/src/harness/investigation/agent-state.ts +134 -0
- package/src/harness/investigation/evidence-verifier.ts +129 -0
- package/src/harness/investigation/evidence.ts +110 -0
- package/src/harness/investigation/finding-critic.ts +96 -0
- package/src/harness/investigation/investigation-planner.ts +118 -0
- package/src/harness/investigation/investigation-task.ts +10 -0
- package/src/harness/investigation/review-journal.ts +87 -0
- package/src/harness/investigation/rule-analyzer.ts +178 -0
- package/src/harness/investigation/rule-signals.ts +110 -0
- package/src/harness/prompts/agent-schemas.ts +57 -0
- package/src/harness/prompts/reviewer-prompt-builder.ts +101 -0
- package/src/harness/prompts/states/conclude-prompt.ts +16 -0
- package/src/harness/prompts/states/investigate-prompt.ts +37 -0
- package/src/harness/prompts/states/orient-prompt.ts +15 -0
- package/src/harness/prompts/states/verify-prompt.ts +22 -0
- package/src/harness/tools/agent-tool.ts +102 -0
- package/src/harness/tools/builtin/changed-symbols-tool.ts +48 -0
- package/src/harness/tools/builtin/doc-context-tool.ts +74 -0
- package/src/harness/tools/builtin/file-outline-tool.ts +66 -0
- package/src/harness/tools/builtin/find-references-tool.ts +66 -0
- package/src/harness/tools/builtin/find-symbol-tool.ts +91 -0
- package/src/harness/tools/builtin/helpers.ts +115 -0
- package/src/harness/tools/builtin/impact-tool.ts +48 -0
- package/src/harness/tools/builtin/imports-exports-tool.ts +53 -0
- package/src/harness/tools/builtin/index.ts +59 -0
- package/src/harness/tools/builtin/list-files-tool.ts +85 -0
- package/src/harness/tools/builtin/pr-context-tool.ts +41 -0
- package/src/harness/tools/builtin/read-file-range-tool.ts +73 -0
- package/src/harness/tools/builtin/rule-signals-tool.ts +36 -0
- package/src/harness/tools/builtin/search-code-tool.ts +65 -0
- package/src/harness/tools/builtin/symbol-context-tool.ts +106 -0
- package/src/harness/tools/builtin/tests-for-symbol-tool.ts +61 -0
- package/src/harness/tools/path-guard.ts +90 -0
- package/src/harness/tools/result-limiter.ts +49 -0
- package/src/harness/tools/secret-redactor.ts +54 -0
- package/src/harness/tools/tool-cache.ts +50 -0
- package/src/harness/tools/tool-call-budget.ts +60 -0
- package/src/harness/tools/tool-context.ts +5 -0
- package/src/harness/tools/tool-middleware.ts +105 -0
- package/src/harness/tools/tool-policy.ts +51 -0
- package/src/harness/tools/tool-registry.ts +112 -0
- package/src/harness/tools/tool-result.ts +15 -0
- package/src/harness/tools/toolbox.ts +36 -0
- package/src/harness/validation/finding-validator.ts +119 -0
- package/src/index.ts +46 -0
- package/src/server/client/App.tsx +38 -12
- package/src/server/client/CommentsPanel.tsx +255 -0
- package/src/server/client/FindingCard.tsx +12 -4
- package/src/server/client/FindingsList.tsx +4 -1
- package/src/server/client/Sidebar.tsx +397 -0
- package/src/server/client/SubmitModal.tsx +61 -5
- package/src/server/client/icons.tsx +19 -0
- package/src/server/client/index.tsx +1 -1
- package/src/server/client/utils/constants.ts +29 -5
- package/src/server/client/utils/format.ts +47 -2
- package/src/server/client/utils/types.ts +88 -0
- package/src/server/editor.ts +129 -0
- package/src/server/index.ts +115 -5
- package/src/server/pr-comments.ts +256 -0
- package/src/server/pr-inbox.ts +202 -0
- package/src/server/run-repository.ts +33 -1
- package/src/skill/OUTPUT-FORMAT.md +12 -0
- package/src/skill/SETUP.md +6 -2
- package/src/skill/hunt.md +14 -2
- package/src/skill/verify.md +1 -1
- package/src/tests/__snapshots__/baseline.test.ts.snap +96 -14
- package/src/tests/ai-core.test.ts +120 -0
- package/src/tests/ai-features.test.ts +183 -0
- package/src/tests/ast.test.ts +335 -0
- package/src/tests/cli.test.ts +33 -0
- package/src/tests/env.test.ts +69 -0
- package/src/tests/fallback.test.ts +172 -0
- package/src/tests/fixtures/fake-agent-tools.ts +69 -0
- package/src/tests/fixtures/fake-investigation.ts +64 -0
- package/src/tests/fixtures/fake-model-provider.ts +48 -0
- package/src/tests/github-review.test.ts +9 -0
- package/src/tests/harness-agent.test.ts +122 -0
- package/src/tests/harness-investigation.test.ts +206 -0
- package/src/tests/harness-tools.test.ts +148 -0
- package/src/tests/hunt-command.test.ts +212 -0
- package/src/tests/logger.test.ts +83 -0
- package/src/tests/review-scope.test.ts +11 -11
- package/src/tests/rule-analyzer.test.ts +61 -0
- package/src/tests/server-editor.test.ts +100 -0
- package/src/tests/server.test.ts +295 -2
- package/src/tests/tool-contracts.test.ts +82 -0
- package/src/tests/verify.test.ts +1 -1
- package/src/server/client/RunList.tsx +0 -75
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool `doc_context` — trechos de docs mapeados (nunca o documento inteiro).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import type { AgentTool, ToolMetadata, ToolResult } from "../agent-tool";
|
|
7
|
+
import { PathGuard } from "../path-guard";
|
|
8
|
+
import { TOOL_LIMITS } from "../result-limiter";
|
|
9
|
+
import { readLines, snippet } from "./helpers";
|
|
10
|
+
|
|
11
|
+
const Input = z.object({
|
|
12
|
+
query: z.string().min(2),
|
|
13
|
+
docs: z.array(z.string()).optional(),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
/** Procura a query nos docs do brief e devolve 2 KB de contexto. */
|
|
17
|
+
export class DocContextTool implements AgentTool<z.infer<typeof Input>, unknown> {
|
|
18
|
+
readonly metadata: ToolMetadata = {
|
|
19
|
+
name: "doc_context",
|
|
20
|
+
description: "Trechos de documentação relevantes para a revisão.",
|
|
21
|
+
category: "context",
|
|
22
|
+
capabilities: ["docs", "search"],
|
|
23
|
+
readOnly: true,
|
|
24
|
+
cost: 2,
|
|
25
|
+
cacheTtlMs: 120_000,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
readonly inputSchema = Input;
|
|
29
|
+
|
|
30
|
+
async execute(
|
|
31
|
+
input: z.infer<typeof Input>,
|
|
32
|
+
context: import("../tool-context").ToolContext,
|
|
33
|
+
): Promise<ToolResult<unknown>> {
|
|
34
|
+
const guard = new PathGuard(context.repoRoot);
|
|
35
|
+
const docs = input.docs ?? [...context.agent.docs];
|
|
36
|
+
const needle = input.query.toLowerCase();
|
|
37
|
+
const matches: unknown[] = [];
|
|
38
|
+
const evidence: import("../../investigation/evidence").Evidence[] = [];
|
|
39
|
+
|
|
40
|
+
for (const doc of docs.slice(0, 20)) {
|
|
41
|
+
let relative: string;
|
|
42
|
+
try {
|
|
43
|
+
relative = guard.relativePath(doc);
|
|
44
|
+
} catch {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const lines = readLines(guard.assertReadable(relative));
|
|
48
|
+
if (!lines) continue;
|
|
49
|
+
const index = lines.findIndex((line) => line.toLowerCase().includes(needle));
|
|
50
|
+
if (index < 0) continue;
|
|
51
|
+
const start = Math.max(1, index - 2);
|
|
52
|
+
const end = Math.min(lines.length, index + 12);
|
|
53
|
+
matches.push({ file: relative, startLine: start, endLine: end });
|
|
54
|
+
evidence.push({
|
|
55
|
+
id: "",
|
|
56
|
+
file: relative,
|
|
57
|
+
startLine: start,
|
|
58
|
+
endLine: end,
|
|
59
|
+
snippet: snippet(lines, start, end).slice(0, TOOL_LIMITS.maxBytes),
|
|
60
|
+
kind: "doc",
|
|
61
|
+
sourceTool: "doc_context",
|
|
62
|
+
confidence: 0.5,
|
|
63
|
+
});
|
|
64
|
+
if (matches.length >= 5) break;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
ok: true,
|
|
69
|
+
data: { matches },
|
|
70
|
+
evidence,
|
|
71
|
+
...(matches.length === 0 ? { hint: "Nenhum doc menciona a query." } : {}),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool `file_outline` — mapa de símbolos do arquivo (sem corpo).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import type { AgentTool, ToolMetadata, ToolResult } from "../agent-tool";
|
|
7
|
+
import { PathGuard } from "../path-guard";
|
|
8
|
+
|
|
9
|
+
const Input = z.object({ path: z.string().min(1) });
|
|
10
|
+
|
|
11
|
+
/** Metadados + imports/exports + símbolos do arquivo. */
|
|
12
|
+
export class FileOutlineTool implements AgentTool<z.infer<typeof Input>, unknown> {
|
|
13
|
+
readonly metadata: ToolMetadata = {
|
|
14
|
+
name: "file_outline",
|
|
15
|
+
description: "Mapa de símbolos, imports e exports de um arquivo (sem corpo).",
|
|
16
|
+
category: "symbol",
|
|
17
|
+
capabilities: ["outline", "ast"],
|
|
18
|
+
readOnly: true,
|
|
19
|
+
cost: 2,
|
|
20
|
+
cacheTtlMs: 120_000,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
readonly inputSchema = Input;
|
|
24
|
+
|
|
25
|
+
async execute(
|
|
26
|
+
input: z.infer<typeof Input>,
|
|
27
|
+
context: import("../tool-context").ToolContext,
|
|
28
|
+
): Promise<ToolResult<unknown>> {
|
|
29
|
+
const guard = new PathGuard(context.repoRoot);
|
|
30
|
+
let path: string;
|
|
31
|
+
try {
|
|
32
|
+
path = guard.relativePath(input.path);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
return {
|
|
35
|
+
ok: false,
|
|
36
|
+
error: error instanceof Error ? error.message : String(error),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const facts = await context.ast.factsOf(path);
|
|
40
|
+
if (!facts) {
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
error: "AST indisponível para este arquivo (grammar/língua).",
|
|
44
|
+
hint: "Use read_file_range para inspecionar o trecho.",
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
ok: true,
|
|
49
|
+
data: {
|
|
50
|
+
path,
|
|
51
|
+
language: facts.language,
|
|
52
|
+
symbols: facts.symbols.map((symbol) => ({
|
|
53
|
+
name: symbol.name,
|
|
54
|
+
kind: symbol.kind,
|
|
55
|
+
exported: symbol.exported,
|
|
56
|
+
startLine: symbol.startLine,
|
|
57
|
+
endLine: symbol.endLine,
|
|
58
|
+
signature: symbol.signature,
|
|
59
|
+
})),
|
|
60
|
+
imports: facts.imports,
|
|
61
|
+
exports: facts.exports,
|
|
62
|
+
decorators: facts.decorators,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool `find_references` — usos/chamadas de um símbolo via `git grep`.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import type { AgentTool, ToolMetadata, ToolResult } from "../agent-tool";
|
|
7
|
+
import { TOOL_LIMITS } from "../result-limiter";
|
|
8
|
+
import { gitGrep, parseGrepLine } from "./helpers";
|
|
9
|
+
|
|
10
|
+
const Input = z.object({
|
|
11
|
+
symbol: z.string().min(2),
|
|
12
|
+
file: z.string().optional(),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
/** Até 50 referências com arquivo/linha/trecho. */
|
|
16
|
+
export class FindReferencesTool implements AgentTool<z.infer<typeof Input>, unknown> {
|
|
17
|
+
readonly metadata: ToolMetadata = {
|
|
18
|
+
name: "find_references",
|
|
19
|
+
description: "Procura usos/chamadas de um símbolo (até 50 referências).",
|
|
20
|
+
category: "symbol",
|
|
21
|
+
capabilities: ["references", "search"],
|
|
22
|
+
readOnly: true,
|
|
23
|
+
cost: 2,
|
|
24
|
+
cacheTtlMs: 60_000,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
readonly inputSchema = Input;
|
|
28
|
+
|
|
29
|
+
async execute(
|
|
30
|
+
input: z.infer<typeof Input>,
|
|
31
|
+
context: import("../tool-context").ToolContext,
|
|
32
|
+
): Promise<ToolResult<unknown>> {
|
|
33
|
+
const args = ["-n", "-I", "-F", "-w", "-e", input.symbol];
|
|
34
|
+
if (input.file) args.push("--", input.file);
|
|
35
|
+
const result = gitGrep(context.repoRoot, args);
|
|
36
|
+
if (!result.ok) {
|
|
37
|
+
return { ok: true, data: { references: [], truncated: false } };
|
|
38
|
+
}
|
|
39
|
+
const lines = result.stdout.split("\n").map(parseGrepLine);
|
|
40
|
+
const references = lines
|
|
41
|
+
.filter((line): line is NonNullable<typeof line> => line !== null)
|
|
42
|
+
.slice(0, TOOL_LIMITS.maxResults)
|
|
43
|
+
.map((line) => ({
|
|
44
|
+
file: line.file,
|
|
45
|
+
line: line.line,
|
|
46
|
+
text: line.text.slice(0, 200),
|
|
47
|
+
}));
|
|
48
|
+
const evidence = references.slice(0, 10).map((reference) => ({
|
|
49
|
+
id: "",
|
|
50
|
+
file: reference.file,
|
|
51
|
+
startLine: reference.line,
|
|
52
|
+
endLine: reference.line,
|
|
53
|
+
snippet: reference.text,
|
|
54
|
+
kind: "reference" as const,
|
|
55
|
+
sourceTool: "find_references",
|
|
56
|
+
confidence: 0.7,
|
|
57
|
+
}));
|
|
58
|
+
const truncated = lines.length > references.length;
|
|
59
|
+
return {
|
|
60
|
+
ok: true,
|
|
61
|
+
data: { references, truncated },
|
|
62
|
+
evidence,
|
|
63
|
+
...(truncated ? { truncated: true, hint: "Refine com file." } : {}),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool `find_symbol` — localiza a definição de um símbolo via AST.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import type { AgentTool, ToolMetadata, ToolResult } from "../agent-tool";
|
|
7
|
+
import { PathGuard } from "../path-guard";
|
|
8
|
+
import { TOOL_LIMITS } from "../result-limiter";
|
|
9
|
+
import { readLines, snippet } from "./helpers";
|
|
10
|
+
|
|
11
|
+
const Input = z.object({
|
|
12
|
+
name: z.string().min(2),
|
|
13
|
+
kind: z.string().optional(),
|
|
14
|
+
file: z.string().optional(),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
/** Definições encontradas (até 20), com evidência. */
|
|
18
|
+
export class FindSymbolTool implements AgentTool<z.infer<typeof Input>, unknown> {
|
|
19
|
+
readonly metadata: ToolMetadata = {
|
|
20
|
+
name: "find_symbol",
|
|
21
|
+
description: "Encontra a definição de um símbolo (AST) e o trecho curto.",
|
|
22
|
+
category: "symbol",
|
|
23
|
+
capabilities: ["symbol"],
|
|
24
|
+
readOnly: true,
|
|
25
|
+
cost: 3,
|
|
26
|
+
cacheTtlMs: 60_000,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
readonly inputSchema = Input;
|
|
30
|
+
|
|
31
|
+
async execute(
|
|
32
|
+
input: z.infer<typeof Input>,
|
|
33
|
+
context: import("../tool-context").ToolContext,
|
|
34
|
+
): Promise<ToolResult<unknown>> {
|
|
35
|
+
const guard = new PathGuard(context.repoRoot);
|
|
36
|
+
const files = input.file
|
|
37
|
+
? [input.file]
|
|
38
|
+
: [
|
|
39
|
+
...new Set([
|
|
40
|
+
...context.agent.files,
|
|
41
|
+
...(context.agent.impact?.affectedFiles ?? []),
|
|
42
|
+
]),
|
|
43
|
+
];
|
|
44
|
+
const definitions: unknown[] = [];
|
|
45
|
+
const evidence: import("../../investigation/evidence").Evidence[] = [];
|
|
46
|
+
|
|
47
|
+
for (const file of files.slice(0, 50)) {
|
|
48
|
+
let relative: string;
|
|
49
|
+
try {
|
|
50
|
+
relative = guard.relativePath(file);
|
|
51
|
+
} catch {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const facts = await context.ast.factsOf(relative);
|
|
55
|
+
if (!facts) continue;
|
|
56
|
+
for (const symbol of facts.symbols) {
|
|
57
|
+
if (!symbol.name.includes(input.name)) continue;
|
|
58
|
+
if (input.kind && symbol.kind !== input.kind) continue;
|
|
59
|
+
const lines = readLines(guard.assertReadable(relative)) ?? [];
|
|
60
|
+
definitions.push({
|
|
61
|
+
file: relative,
|
|
62
|
+
name: symbol.name,
|
|
63
|
+
kind: symbol.kind,
|
|
64
|
+
exported: symbol.exported,
|
|
65
|
+
startLine: symbol.startLine,
|
|
66
|
+
endLine: symbol.endLine,
|
|
67
|
+
signature: symbol.signature,
|
|
68
|
+
});
|
|
69
|
+
evidence.push({
|
|
70
|
+
id: "",
|
|
71
|
+
file: relative,
|
|
72
|
+
startLine: symbol.startLine,
|
|
73
|
+
endLine: Math.min(symbol.startLine + 10, symbol.endLine),
|
|
74
|
+
snippet: snippet(lines, symbol.startLine, symbol.startLine + 10),
|
|
75
|
+
kind: "definition",
|
|
76
|
+
sourceTool: "find_symbol",
|
|
77
|
+
confidence: 0.9,
|
|
78
|
+
});
|
|
79
|
+
if (definitions.length >= 20) break;
|
|
80
|
+
}
|
|
81
|
+
if (definitions.length >= 20) break;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
ok: definitions.length > 0,
|
|
86
|
+
data: { definitions: definitions.slice(0, 20) },
|
|
87
|
+
evidence: evidence.slice(0, TOOL_LIMITS.maxResults),
|
|
88
|
+
...(definitions.length === 0 ? { error: "símbolo não encontrado." } : {}),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers compartilhados pelas tools built-in (leitura, grep e snippets).
|
|
3
|
+
*
|
|
4
|
+
* Tudo é read-only e passa pelo `PathGuard`; nada executa shell arbitrário.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import { relative } from "node:path";
|
|
9
|
+
import { runGit } from "../../../core/git-runner";
|
|
10
|
+
import type { PathGuard } from "../path-guard";
|
|
11
|
+
import { limitLines, limitText, numberLines, TOOL_LIMITS } from "../result-limiter";
|
|
12
|
+
|
|
13
|
+
/** Linhas de um arquivo; `null` se ilegível. */
|
|
14
|
+
export function readLines(path: string): string[] | null {
|
|
15
|
+
try {
|
|
16
|
+
return readFileSync(path, "utf8").replace(/\r\n/g, "\n").split("\n");
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Trecho numerado de um arquivo (1-based, inclusivo). */
|
|
23
|
+
export function numberedRange(
|
|
24
|
+
lines: readonly string[],
|
|
25
|
+
startLine: number,
|
|
26
|
+
endLine: number,
|
|
27
|
+
): string {
|
|
28
|
+
const start = Math.max(1, startLine);
|
|
29
|
+
const end = Math.min(lines.length, Math.max(start, endLine));
|
|
30
|
+
const slice = lines.slice(start - 1, end);
|
|
31
|
+
return numberLines(start, slice);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Snippet bruto (sem numeração), limitado em linhas. */
|
|
35
|
+
export function snippet(
|
|
36
|
+
lines: readonly string[],
|
|
37
|
+
startLine: number,
|
|
38
|
+
endLine: number,
|
|
39
|
+
): string {
|
|
40
|
+
const start = Math.max(1, startLine);
|
|
41
|
+
const end = Math.min(lines.length, Math.max(start, endLine));
|
|
42
|
+
const limited = limitLines(lines.slice(start - 1, end), TOOL_LIMITS.maxSnippetLines);
|
|
43
|
+
return limitText(limited.lines.join("\n")).text;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Executa `git grep` na raiz do repositório. */
|
|
47
|
+
export function gitGrep(
|
|
48
|
+
root: string,
|
|
49
|
+
args: string[],
|
|
50
|
+
): { ok: boolean; stdout: string; stderr: string } {
|
|
51
|
+
try {
|
|
52
|
+
const stdout = runGit(["-C", root, "grep", ...args]);
|
|
53
|
+
return { ok: true, stdout, stderr: "" };
|
|
54
|
+
} catch (error) {
|
|
55
|
+
return {
|
|
56
|
+
ok: false,
|
|
57
|
+
stdout: "",
|
|
58
|
+
stderr: error instanceof Error ? error.message : String(error),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Caminho relativo normalizado. */
|
|
64
|
+
export function relativeTo(root: string, path: string): string {
|
|
65
|
+
return relative(root, path).replace(/\\/g, "/");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Lê uma faixa validada (PathGuard + limites). */
|
|
69
|
+
export function readRange(
|
|
70
|
+
guard: PathGuard,
|
|
71
|
+
path: string,
|
|
72
|
+
startLine: number,
|
|
73
|
+
endLine: number,
|
|
74
|
+
): {
|
|
75
|
+
path: string;
|
|
76
|
+
startLine: number;
|
|
77
|
+
endLine: number;
|
|
78
|
+
totalLines: number;
|
|
79
|
+
content: string;
|
|
80
|
+
truncated: boolean;
|
|
81
|
+
} | null {
|
|
82
|
+
const absolute = guard.assertReadable(path);
|
|
83
|
+
const lines = readLines(absolute);
|
|
84
|
+
if (!lines) return null;
|
|
85
|
+
const start = Math.max(1, startLine);
|
|
86
|
+
const requestedEnd = Math.max(start, endLine);
|
|
87
|
+
const cappedEnd = Math.min(
|
|
88
|
+
requestedEnd,
|
|
89
|
+
start + TOOL_LIMITS.maxLines - 1,
|
|
90
|
+
lines.length,
|
|
91
|
+
);
|
|
92
|
+
const limited = limitText(
|
|
93
|
+
numberLines(start, lines.slice(start - 1, cappedEnd)),
|
|
94
|
+
TOOL_LIMITS.maxBytes,
|
|
95
|
+
);
|
|
96
|
+
return {
|
|
97
|
+
path: relativeTo(guard.root, absolute),
|
|
98
|
+
startLine: start,
|
|
99
|
+
endLine: cappedEnd,
|
|
100
|
+
totalLines: lines.length,
|
|
101
|
+
content: limited.text,
|
|
102
|
+
truncated: limited.truncated || cappedEnd < requestedEnd,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Parseia uma linha `arquivo:linha:conteúdo` do `git grep -n`. */
|
|
107
|
+
export function parseGrepLine(
|
|
108
|
+
line: string,
|
|
109
|
+
): { file: string; line: number; text: string } | null {
|
|
110
|
+
const match = /^(.+?):(\d+):(.*)$/.exec(line);
|
|
111
|
+
if (!match) return null;
|
|
112
|
+
const [, file, number, text] = match;
|
|
113
|
+
if (!file || !number) return null;
|
|
114
|
+
return { file, line: Number.parseInt(number, 10), text: text ?? "" };
|
|
115
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool `impact` — importadores e exports alterados (impacto reverso).
|
|
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
|
+
/** Dependências reversas do diff (top N). */
|
|
11
|
+
export class ImpactTool implements AgentTool<z.infer<typeof Input>, unknown> {
|
|
12
|
+
readonly metadata: ToolMetadata = {
|
|
13
|
+
name: "impact",
|
|
14
|
+
description:
|
|
15
|
+
"Arquivos que importam o diff e exports alterados (impacto reverso).",
|
|
16
|
+
category: "ast",
|
|
17
|
+
capabilities: ["impact"],
|
|
18
|
+
readOnly: true,
|
|
19
|
+
cost: 2,
|
|
20
|
+
cacheTtlMs: 300_000,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
readonly inputSchema = Input;
|
|
24
|
+
|
|
25
|
+
async execute(
|
|
26
|
+
input: z.infer<typeof Input>,
|
|
27
|
+
context: import("../tool-context").ToolContext,
|
|
28
|
+
): Promise<ToolResult<unknown>> {
|
|
29
|
+
const impact = context.agent.impact;
|
|
30
|
+
if (!impact) {
|
|
31
|
+
return {
|
|
32
|
+
ok: true,
|
|
33
|
+
data: { importers: {}, affectedFiles: [], exportedChanges: [] },
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const importers = input.file
|
|
37
|
+
? { [input.file]: impact.importers[input.file] ?? [] }
|
|
38
|
+
: impact.importers;
|
|
39
|
+
return {
|
|
40
|
+
ok: true,
|
|
41
|
+
data: {
|
|
42
|
+
importers,
|
|
43
|
+
affectedFiles: impact.affectedFiles.slice(0, 40),
|
|
44
|
+
exportedChanges: impact.exportedChanges.slice(0, 40),
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|