@payconductor/houndsman 0.4.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.
- package/README.md +35 -41
- 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/status-command.ts +46 -0
- package/src/cli/help.ts +18 -1
- package/src/cli/index.ts +13 -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 +120 -6
- package/src/core/commands/review.ts +2 -0
- package/src/core/commands/scaffold.ts +159 -10
- package/src/core/commands/status.ts +63 -0
- 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 +77 -13
- 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/findings-state.ts +21 -4
- package/src/core/github-review.ts +620 -57
- 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 +454 -16
- package/src/core/paths.ts +3 -0
- 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/renderers/resume-renderer.ts +151 -0
- package/src/core/report-builder.ts +7 -4
- package/src/core/rule-text.ts +97 -0
- package/src/core/run-status.ts +39 -5
- package/src/core/services/file-classifier.ts +10 -3
- package/src/core/services/verifier.ts +14 -3
- package/src/core/storage/file/file-storage.ts +2 -0
- package/src/core/storage/paths.ts +6 -5
- package/src/core/storage/sqlite/mappers.ts +2 -0
- package/src/core/storage/sqlite/schema.ts +9 -1
- package/src/core/storage/sqlite/sqlite-storage.ts +5 -1
- package/src/core/storage/types.ts +15 -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 +26 -9
- package/src/server/client/ArtifactsPanel.tsx +12 -5
- package/src/server/client/AutoRefresh.tsx +1 -1
- package/src/server/client/Filters.tsx +7 -2
- package/src/server/client/FindingCard.tsx +14 -10
- package/src/server/client/FindingsList.tsx +2 -2
- package/src/server/client/MarkdownText.tsx +92 -12
- package/src/server/client/ResumePanel.tsx +368 -0
- package/src/server/client/RunList.tsx +3 -3
- package/src/server/client/SubmitModal.tsx +49 -6
- package/src/server/client/SummaryBar.tsx +1 -1
- package/src/server/client/icons.tsx +17 -0
- package/src/server/client/{constants.ts → utils/constants.ts} +61 -10
- package/src/server/client/utils/markdown.ts +319 -0
- package/src/server/client/{types.ts → utils/types.ts} +64 -0
- package/src/server/index.ts +66 -7
- package/src/server/run-repository.ts +9 -0
- package/src/skill/OUTPUT-FORMAT.md +12 -0
- package/src/skill/SETUP.md +6 -2
- package/src/skill/example-review.md +22 -14
- package/src/skill/hunt.md +45 -19
- package/src/skill/verify.md +3 -2
- package/src/tests/__snapshots__/baseline.test.ts.snap +126 -42
- 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/baseline.test.ts +3 -0
- package/src/tests/code-highlight.test.ts +1 -1
- package/src/tests/env.test.ts +69 -0
- package/src/tests/fallback.test.ts +172 -0
- package/src/tests/findings-state.test.ts +50 -2
- 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 +409 -60
- 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/markdown.test.ts +61 -1
- package/src/tests/review-resume.test.ts +101 -0
- package/src/tests/review-scope.test.ts +302 -0
- package/src/tests/review-scripts.test.ts +73 -0
- package/src/tests/rule-analyzer.test.ts +61 -0
- package/src/tests/run-status-view.test.ts +94 -0
- package/src/tests/run-status.test.ts +53 -0
- package/src/tests/server.test.ts +76 -21
- package/src/tests/storage.test.ts +63 -0
- package/src/tests/tool-contracts.test.ts +82 -0
- package/src/tests/verify.test.ts +1 -1
- package/src/server/client/markdown.ts +0 -153
- /package/src/server/client/{api.ts → utils/api.ts} +0 -0
- /package/src/server/client/{code-highlight.ts → utils/code-highlight.ts} +0 -0
- /package/src/server/client/{format.ts → utils/format.ts} +0 -0
- /package/src/server/client/{json-highlight.ts → utils/json-highlight.ts} +0 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool `imports_exports` — imports, exports e decorators de um arquivo.
|
|
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({ file: z.string().min(1) });
|
|
10
|
+
|
|
11
|
+
/** Lista estrutural do arquivo. */
|
|
12
|
+
export class ImportsExportsTool implements AgentTool<z.infer<typeof Input>, unknown> {
|
|
13
|
+
readonly metadata: ToolMetadata = {
|
|
14
|
+
name: "imports_exports",
|
|
15
|
+
description: "Imports, exports e decorators de um arquivo (AST).",
|
|
16
|
+
category: "ast",
|
|
17
|
+
capabilities: ["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.file);
|
|
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 { ok: false, error: "AST indisponível para este arquivo." };
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
ok: true,
|
|
45
|
+
data: {
|
|
46
|
+
file: path,
|
|
47
|
+
imports: facts.imports,
|
|
48
|
+
exports: facts.exports,
|
|
49
|
+
decorators: facts.decorators,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registro das tools built-in do harness.
|
|
3
|
+
*
|
|
4
|
+
* Adicionar uma tool = criar a classe + registrar aqui + contract test. O
|
|
5
|
+
* `ReviewAgent` não muda.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ToolModule, ToolRegistryLike } from "../agent-tool";
|
|
9
|
+
import { ChangedSymbolsTool } from "./changed-symbols-tool";
|
|
10
|
+
import { DocContextTool } from "./doc-context-tool";
|
|
11
|
+
import { FileOutlineTool } from "./file-outline-tool";
|
|
12
|
+
import { FindReferencesTool } from "./find-references-tool";
|
|
13
|
+
import { FindSymbolTool } from "./find-symbol-tool";
|
|
14
|
+
import { ImpactTool } from "./impact-tool";
|
|
15
|
+
import { ImportsExportsTool } from "./imports-exports-tool";
|
|
16
|
+
import { ListFilesTool } from "./list-files-tool";
|
|
17
|
+
import { PrContextTool } from "./pr-context-tool";
|
|
18
|
+
import { ReadFileRangeTool } from "./read-file-range-tool";
|
|
19
|
+
import { RuleSignalsTool } from "./rule-signals-tool";
|
|
20
|
+
import { SearchCodeTool } from "./search-code-tool";
|
|
21
|
+
import { SymbolContextTool } from "./symbol-context-tool";
|
|
22
|
+
import { TestsForSymbolTool } from "./tests-for-symbol-tool";
|
|
23
|
+
|
|
24
|
+
/** Módulo com todas as tools read-only. */
|
|
25
|
+
export class BuiltinToolsModule implements ToolModule {
|
|
26
|
+
register(registry: ToolRegistryLike): void {
|
|
27
|
+
registry.registerTool(new PrContextTool());
|
|
28
|
+
registry.registerTool(new ListFilesTool());
|
|
29
|
+
registry.registerTool(new FileOutlineTool());
|
|
30
|
+
registry.registerTool(new ReadFileRangeTool());
|
|
31
|
+
registry.registerTool(new SearchCodeTool());
|
|
32
|
+
registry.registerTool(new FindSymbolTool());
|
|
33
|
+
registry.registerTool(new FindReferencesTool());
|
|
34
|
+
registry.registerTool(new SymbolContextTool());
|
|
35
|
+
registry.registerTool(new ChangedSymbolsTool());
|
|
36
|
+
registry.registerTool(new ImpactTool());
|
|
37
|
+
registry.registerTool(new ImportsExportsTool());
|
|
38
|
+
registry.registerTool(new RuleSignalsTool());
|
|
39
|
+
registry.registerTool(new TestsForSymbolTool());
|
|
40
|
+
registry.registerTool(new DocContextTool());
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export {
|
|
45
|
+
ChangedSymbolsTool,
|
|
46
|
+
DocContextTool,
|
|
47
|
+
FileOutlineTool,
|
|
48
|
+
FindReferencesTool,
|
|
49
|
+
FindSymbolTool,
|
|
50
|
+
ImpactTool,
|
|
51
|
+
ImportsExportsTool,
|
|
52
|
+
ListFilesTool,
|
|
53
|
+
PrContextTool,
|
|
54
|
+
ReadFileRangeTool,
|
|
55
|
+
RuleSignalsTool,
|
|
56
|
+
SearchCodeTool,
|
|
57
|
+
SymbolContextTool,
|
|
58
|
+
TestsForSymbolTool,
|
|
59
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool `list_files` — lista arquivos do repositório (mapa, não conteúdo).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { runGit } from "../../../core/git-runner";
|
|
7
|
+
import type { AgentTool, ToolMetadata, ToolResult } from "../agent-tool";
|
|
8
|
+
import { TOOL_LIMITS } from "../result-limiter";
|
|
9
|
+
|
|
10
|
+
const Input = z.object({
|
|
11
|
+
directory: z.string().optional(),
|
|
12
|
+
glob: z.string().optional(),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
/** Lista até 200 arquivos rastreados pelo git. */
|
|
16
|
+
export class ListFilesTool implements AgentTool<z.infer<typeof Input>, string[]> {
|
|
17
|
+
readonly metadata: ToolMetadata = {
|
|
18
|
+
name: "list_files",
|
|
19
|
+
description: "Lista arquivos rastreados (diretório/glob opcionais).",
|
|
20
|
+
category: "code",
|
|
21
|
+
capabilities: ["outline"],
|
|
22
|
+
readOnly: true,
|
|
23
|
+
cost: 1,
|
|
24
|
+
cacheTtlMs: 120_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<string[]>> {
|
|
33
|
+
// `git grep -l` com padrão vazio não lista tudo; usamos `ls-files`.
|
|
34
|
+
let files: string[];
|
|
35
|
+
try {
|
|
36
|
+
const stdout = runGit(["-C", context.repoRoot, "ls-files"]);
|
|
37
|
+
files = stdout
|
|
38
|
+
.split("\n")
|
|
39
|
+
.map((line) => line.trim())
|
|
40
|
+
.filter(Boolean);
|
|
41
|
+
} catch {
|
|
42
|
+
return { ok: false, error: "não foi possível listar os arquivos (git)." };
|
|
43
|
+
}
|
|
44
|
+
if (input.directory) {
|
|
45
|
+
const prefix = input.directory.replace(/\/+$/, "");
|
|
46
|
+
files = files.filter(
|
|
47
|
+
(file) => file.startsWith(`${prefix}/`) || file === prefix,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
if (input.glob) {
|
|
51
|
+
const matcher = globToRegExp(input.glob);
|
|
52
|
+
files = files.filter((file) => matcher.test(file));
|
|
53
|
+
}
|
|
54
|
+
const truncated = files.length > TOOL_LIMITS.maxFiles;
|
|
55
|
+
return {
|
|
56
|
+
ok: true,
|
|
57
|
+
data: files.slice(0, TOOL_LIMITS.maxFiles),
|
|
58
|
+
truncated,
|
|
59
|
+
...(truncated ? { hint: "Refine com directory/glob." } : {}),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Converte um glob simples em regex. */
|
|
65
|
+
export function globToRegExp(glob: string): RegExp {
|
|
66
|
+
let pattern = "";
|
|
67
|
+
for (let index = 0; index < glob.length; index++) {
|
|
68
|
+
const char = glob[index] ?? "";
|
|
69
|
+
if (char === "*") {
|
|
70
|
+
if (glob[index + 1] === "*") {
|
|
71
|
+
pattern += ".*";
|
|
72
|
+
index++;
|
|
73
|
+
} else {
|
|
74
|
+
pattern += "[^/]*";
|
|
75
|
+
}
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (char === "?") {
|
|
79
|
+
pattern += ".";
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
pattern += char.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
83
|
+
}
|
|
84
|
+
return new RegExp(`^${pattern}$`);
|
|
85
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool `pr_context` — contexto resumido da PR (já compactado).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import type { AgentTool, ToolMetadata, ToolResult } from "../agent-tool";
|
|
7
|
+
|
|
8
|
+
/** Contexto da PR para orientação do agente. */
|
|
9
|
+
export class PrContextTool implements AgentTool<Record<string, never>, unknown> {
|
|
10
|
+
readonly metadata: ToolMetadata = {
|
|
11
|
+
name: "pr_context",
|
|
12
|
+
description: "Contexto resumido da PR (objetivo, motivação, mudanças).",
|
|
13
|
+
category: "context",
|
|
14
|
+
capabilities: ["read"],
|
|
15
|
+
readOnly: true,
|
|
16
|
+
cost: 1,
|
|
17
|
+
cacheTtlMs: 300_000,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
readonly inputSchema = z.object({});
|
|
21
|
+
|
|
22
|
+
async execute(
|
|
23
|
+
_input: Record<string, never>,
|
|
24
|
+
context: import("../tool-context").ToolContext,
|
|
25
|
+
): Promise<ToolResult<unknown>> {
|
|
26
|
+
return {
|
|
27
|
+
ok: true,
|
|
28
|
+
data: {
|
|
29
|
+
project: context.agent.projectName,
|
|
30
|
+
branch: context.agent.branch,
|
|
31
|
+
watchdog: context.agent.watchdog.id,
|
|
32
|
+
scope: context.agent.watchdog.scope,
|
|
33
|
+
context: context.agent.context ?? null,
|
|
34
|
+
files: context.agent.files,
|
|
35
|
+
overflow: context.agent.overflow,
|
|
36
|
+
deleted: context.agent.deleted,
|
|
37
|
+
docs: context.agent.docs,
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|