@payconductor/houndsman 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/README.md +24 -4
  2. package/assets/grammars/tree-sitter-javascript.wasm +0 -0
  3. package/assets/grammars/tree-sitter-tsx.wasm +0 -0
  4. package/assets/grammars/tree-sitter-typescript.wasm +0 -0
  5. package/assets/grammars/web-tree-sitter.wasm +0 -0
  6. package/package.json +11 -3
  7. package/src/cli/commands/command.ts +5 -0
  8. package/src/cli/commands/doctor-command.ts +116 -24
  9. package/src/cli/commands/hunt-command.ts +25 -0
  10. package/src/cli/commands/server-command.ts +7 -1
  11. package/src/cli/help.ts +18 -2
  12. package/src/cli/index.ts +10 -6
  13. package/src/core/ai/ai-client.ts +221 -0
  14. package/src/core/ai/ai-sdk-model-provider.ts +101 -0
  15. package/src/core/ai/errors.ts +36 -0
  16. package/src/core/ai/features/brief-enricher.ts +62 -0
  17. package/src/core/ai/features/classifier.ts +50 -0
  18. package/src/core/ai/features/context-summarizer.ts +31 -0
  19. package/src/core/ai/features/pre-analyzer.ts +51 -0
  20. package/src/core/ai/features/verifier.ts +45 -0
  21. package/src/core/ai/index.ts +56 -0
  22. package/src/core/ai/model-factory.ts +137 -0
  23. package/src/core/ai/prompts/base.ts +36 -0
  24. package/src/core/ai/prompts/brief.ts +87 -0
  25. package/src/core/ai/prompts/classification.ts +36 -0
  26. package/src/core/ai/prompts/context.ts +29 -0
  27. package/src/core/ai/prompts/pre-analysis.ts +49 -0
  28. package/src/core/ai/prompts/verify.ts +40 -0
  29. package/src/core/ai/schemas.ts +110 -0
  30. package/src/core/ai/types.ts +76 -0
  31. package/src/core/ai/usage-budget.ts +75 -0
  32. package/src/core/ai/usage-recorder.ts +79 -0
  33. package/src/core/assets.ts +16 -0
  34. package/src/core/ast/diff.ts +220 -0
  35. package/src/core/ast/impact.ts +127 -0
  36. package/src/core/ast/parser.ts +167 -0
  37. package/src/core/ast/service.ts +233 -0
  38. package/src/core/ast/symbols.ts +294 -0
  39. package/src/core/ast/types.ts +125 -0
  40. package/src/core/brief.ts +138 -0
  41. package/src/core/commands/findings.ts +65 -2
  42. package/src/core/commands/review.ts +2 -0
  43. package/src/core/commands/scaffold.ts +159 -10
  44. package/src/core/commands/update.ts +5 -3
  45. package/src/core/config.ts +85 -0
  46. package/src/core/defaults.ts +27 -0
  47. package/src/core/domain/watchdog.ts +43 -5
  48. package/src/core/engine/houndsman-engine.ts +16 -1
  49. package/src/core/engine/review-scope.ts +52 -6
  50. package/src/core/engine/review-session.ts +27 -2
  51. package/src/core/env.ts +101 -0
  52. package/src/core/file-classifier.ts +25 -4
  53. package/src/core/git-runner.ts +25 -13
  54. package/src/core/github-review.ts +10 -7
  55. package/src/core/jev/jev-client.ts +8 -7
  56. package/src/core/jev/jev-verify.ts +2 -1
  57. package/src/core/logging/index.ts +17 -0
  58. package/src/core/logging/logger.ts +187 -0
  59. package/src/core/orchestrator.ts +357 -9
  60. package/src/core/project-config.ts +123 -1
  61. package/src/core/registry-check.ts +148 -6
  62. package/src/core/renderers/manifest-writer.ts +33 -0
  63. package/src/core/rule-text.ts +97 -0
  64. package/src/core/services/file-classifier.ts +10 -3
  65. package/src/core/services/gh-client.ts +38 -22
  66. package/src/core/services/verifier.ts +14 -3
  67. package/src/core/storage/paths.ts +6 -5
  68. package/src/core/storage/types.ts +6 -0
  69. package/src/core/storage/work-area.ts +12 -0
  70. package/src/core/types.ts +37 -1
  71. package/src/core/verify.ts +17 -5
  72. package/src/harness/README.md +15 -0
  73. package/src/harness/agent/agent-context.ts +33 -0
  74. package/src/harness/agent/agent-errors.ts +30 -0
  75. package/src/harness/agent/agent-factory.ts +45 -0
  76. package/src/harness/agent/agent-reporter.ts +34 -0
  77. package/src/harness/agent/agent-result.ts +16 -0
  78. package/src/harness/agent/agent-runner.ts +32 -0
  79. package/src/harness/agent/agent-scheduler.ts +37 -0
  80. package/src/harness/agent/review-agent.ts +391 -0
  81. package/src/harness/config/harness-config.ts +84 -0
  82. package/src/harness/hunt/hunt-harness.ts +83 -0
  83. package/src/harness/hunt/hunt-options.ts +75 -0
  84. package/src/harness/hunt/hunt-result.ts +17 -0
  85. package/src/harness/hunt/hunt-session.ts +372 -0
  86. package/src/harness/hunt/review-planner.ts +98 -0
  87. package/src/harness/index.ts +36 -0
  88. package/src/harness/investigation/agent-state.ts +134 -0
  89. package/src/harness/investigation/evidence-verifier.ts +129 -0
  90. package/src/harness/investigation/evidence.ts +110 -0
  91. package/src/harness/investigation/finding-critic.ts +96 -0
  92. package/src/harness/investigation/investigation-planner.ts +118 -0
  93. package/src/harness/investigation/investigation-task.ts +10 -0
  94. package/src/harness/investigation/review-journal.ts +87 -0
  95. package/src/harness/investigation/rule-analyzer.ts +178 -0
  96. package/src/harness/investigation/rule-signals.ts +110 -0
  97. package/src/harness/prompts/agent-schemas.ts +57 -0
  98. package/src/harness/prompts/reviewer-prompt-builder.ts +101 -0
  99. package/src/harness/prompts/states/conclude-prompt.ts +16 -0
  100. package/src/harness/prompts/states/investigate-prompt.ts +37 -0
  101. package/src/harness/prompts/states/orient-prompt.ts +15 -0
  102. package/src/harness/prompts/states/verify-prompt.ts +22 -0
  103. package/src/harness/tools/agent-tool.ts +102 -0
  104. package/src/harness/tools/builtin/changed-symbols-tool.ts +48 -0
  105. package/src/harness/tools/builtin/doc-context-tool.ts +74 -0
  106. package/src/harness/tools/builtin/file-outline-tool.ts +66 -0
  107. package/src/harness/tools/builtin/find-references-tool.ts +66 -0
  108. package/src/harness/tools/builtin/find-symbol-tool.ts +91 -0
  109. package/src/harness/tools/builtin/helpers.ts +115 -0
  110. package/src/harness/tools/builtin/impact-tool.ts +48 -0
  111. package/src/harness/tools/builtin/imports-exports-tool.ts +53 -0
  112. package/src/harness/tools/builtin/index.ts +59 -0
  113. package/src/harness/tools/builtin/list-files-tool.ts +85 -0
  114. package/src/harness/tools/builtin/pr-context-tool.ts +41 -0
  115. package/src/harness/tools/builtin/read-file-range-tool.ts +73 -0
  116. package/src/harness/tools/builtin/rule-signals-tool.ts +36 -0
  117. package/src/harness/tools/builtin/search-code-tool.ts +65 -0
  118. package/src/harness/tools/builtin/symbol-context-tool.ts +106 -0
  119. package/src/harness/tools/builtin/tests-for-symbol-tool.ts +61 -0
  120. package/src/harness/tools/path-guard.ts +90 -0
  121. package/src/harness/tools/result-limiter.ts +49 -0
  122. package/src/harness/tools/secret-redactor.ts +54 -0
  123. package/src/harness/tools/tool-cache.ts +50 -0
  124. package/src/harness/tools/tool-call-budget.ts +60 -0
  125. package/src/harness/tools/tool-context.ts +5 -0
  126. package/src/harness/tools/tool-middleware.ts +105 -0
  127. package/src/harness/tools/tool-policy.ts +51 -0
  128. package/src/harness/tools/tool-registry.ts +112 -0
  129. package/src/harness/tools/tool-result.ts +15 -0
  130. package/src/harness/tools/toolbox.ts +36 -0
  131. package/src/harness/validation/finding-validator.ts +119 -0
  132. package/src/index.ts +46 -0
  133. package/src/server/client/App.tsx +38 -12
  134. package/src/server/client/CommentsPanel.tsx +255 -0
  135. package/src/server/client/FindingCard.tsx +12 -4
  136. package/src/server/client/FindingsList.tsx +4 -1
  137. package/src/server/client/Sidebar.tsx +397 -0
  138. package/src/server/client/SubmitModal.tsx +61 -5
  139. package/src/server/client/icons.tsx +19 -0
  140. package/src/server/client/index.tsx +1 -1
  141. package/src/server/client/utils/constants.ts +29 -5
  142. package/src/server/client/utils/format.ts +47 -2
  143. package/src/server/client/utils/types.ts +88 -0
  144. package/src/server/editor.ts +129 -0
  145. package/src/server/index.ts +115 -5
  146. package/src/server/pr-comments.ts +256 -0
  147. package/src/server/pr-inbox.ts +202 -0
  148. package/src/server/run-repository.ts +33 -1
  149. package/src/skill/OUTPUT-FORMAT.md +12 -0
  150. package/src/skill/SETUP.md +6 -2
  151. package/src/skill/hunt.md +14 -2
  152. package/src/skill/verify.md +1 -1
  153. package/src/tests/__snapshots__/baseline.test.ts.snap +96 -14
  154. package/src/tests/ai-core.test.ts +120 -0
  155. package/src/tests/ai-features.test.ts +183 -0
  156. package/src/tests/ast.test.ts +335 -0
  157. package/src/tests/cli.test.ts +33 -0
  158. package/src/tests/env.test.ts +69 -0
  159. package/src/tests/fallback.test.ts +172 -0
  160. package/src/tests/fixtures/fake-agent-tools.ts +69 -0
  161. package/src/tests/fixtures/fake-investigation.ts +64 -0
  162. package/src/tests/fixtures/fake-model-provider.ts +48 -0
  163. package/src/tests/github-review.test.ts +9 -0
  164. package/src/tests/harness-agent.test.ts +122 -0
  165. package/src/tests/harness-investigation.test.ts +206 -0
  166. package/src/tests/harness-tools.test.ts +148 -0
  167. package/src/tests/hunt-command.test.ts +212 -0
  168. package/src/tests/logger.test.ts +83 -0
  169. package/src/tests/review-scope.test.ts +11 -11
  170. package/src/tests/rule-analyzer.test.ts +61 -0
  171. package/src/tests/server-editor.test.ts +100 -0
  172. package/src/tests/server.test.ts +295 -2
  173. package/src/tests/tool-contracts.test.ts +82 -0
  174. package/src/tests/verify.test.ts +1 -1
  175. package/src/server/client/RunList.tsx +0 -75
@@ -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
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Tool `read_file_range` — lê um trecho explícito (nunca o arquivo 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, readRange } from "./helpers";
10
+
11
+ const Input = z.object({
12
+ path: z.string().min(1),
13
+ startLine: z.number().int().min(1),
14
+ endLine: z.number().int().min(1),
15
+ });
16
+
17
+ /** Leitura por faixa, com no máximo 200 linhas/32 KB. */
18
+ export class ReadFileRangeTool implements AgentTool<z.infer<typeof Input>, unknown> {
19
+ readonly metadata: ToolMetadata = {
20
+ name: "read_file_range",
21
+ description: "Lê um trecho de arquivo (faixa obrigatória, até 200 linhas).",
22
+ category: "code",
23
+ capabilities: ["read"],
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
+ try {
37
+ const range = readRange(guard, input.path, input.startLine, input.endLine);
38
+ if (!range) return { ok: false, error: "arquivo ilegível." };
39
+ const lines = readLines(guard.assertReadable(input.path)) ?? [];
40
+ const evidence = {
41
+ id: "",
42
+ file: range.path,
43
+ startLine: range.startLine,
44
+ endLine: range.endLine,
45
+ snippet: lines.slice(range.startLine - 1, range.endLine).join("\n"),
46
+ kind: "diff" as const,
47
+ sourceTool: "read_file_range",
48
+ confidence: 0.6,
49
+ };
50
+ return {
51
+ ok: true,
52
+ data: range,
53
+ ...(range.truncated
54
+ ? {
55
+ truncated: true,
56
+ hint: `Faixa limitada a ${TOOL_LIMITS.maxLines} linhas; refine o intervalo.`,
57
+ }
58
+ : {}),
59
+ evidence: [
60
+ {
61
+ ...evidence,
62
+ snippet: evidence.snippet.slice(0, TOOL_LIMITS.maxBytes),
63
+ },
64
+ ],
65
+ };
66
+ } catch (error) {
67
+ return {
68
+ ok: false,
69
+ error: error instanceof Error ? error.message : String(error),
70
+ };
71
+ }
72
+ }
73
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Tool `rule_signals` — traduz uma regra em sinais e checks.
3
+ */
4
+
5
+ import { z } from "zod";
6
+ import type { AgentTool, ToolMetadata, ToolResult } from "../agent-tool";
7
+
8
+ const Input = z.object({ rule: z.string().min(3) });
9
+
10
+ /** Devolve a `RuleAnalysis` determinística da regra. */
11
+ export class RuleSignalsTool implements AgentTool<z.infer<typeof Input>, unknown> {
12
+ readonly metadata: ToolMetadata = {
13
+ name: "rule_signals",
14
+ description: "Sinais, keywords e checks sugeridos para uma regra do watchdog.",
15
+ category: "rules",
16
+ capabilities: ["rules"],
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 analysis =
29
+ context.rules.get(input.rule) ??
30
+ [...context.rules.values()].find((item) => item.rule.includes(input.rule));
31
+ if (!analysis) {
32
+ return { ok: false, error: "regra não encontrada no watchdog." };
33
+ }
34
+ return { ok: true, data: analysis };
35
+ }
36
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Tool `search_code` — localiza padrões via `git grep` e devolve snippets.
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
+ query: z.string().min(2),
12
+ mode: z.enum(["fixed", "regex"]).optional(),
13
+ path: z.string().optional(),
14
+ contextLines: z.number().int().min(0).max(3).optional(),
15
+ maxResults: z.number().int().min(1).max(50).optional(),
16
+ });
17
+
18
+ /** Busca com limite de resultados e snippets curtos. */
19
+ export class SearchCodeTool implements AgentTool<z.infer<typeof Input>, unknown> {
20
+ readonly metadata: ToolMetadata = {
21
+ name: "search_code",
22
+ description: "Procura um padrão no código (até 50 resultados com snippet).",
23
+ category: "code",
24
+ capabilities: ["search"],
25
+ readOnly: true,
26
+ cost: 2,
27
+ cacheTtlMs: 60_000,
28
+ };
29
+
30
+ readonly inputSchema = Input;
31
+
32
+ async execute(
33
+ input: z.infer<typeof Input>,
34
+ context: import("../tool-context").ToolContext,
35
+ ): Promise<ToolResult<unknown>> {
36
+ const mode = input.mode === "regex" ? "-E" : "-F";
37
+ const args = ["-n", "-I", mode, "-e", input.query];
38
+ if (input.path) args.push("--", input.path);
39
+ const result = gitGrep(context.repoRoot, args);
40
+ if (!result.ok) {
41
+ // `git grep` retorna 1 quando não há match (não é erro).
42
+ if (result.stderr.includes("exit") || result.stdout === "") {
43
+ return { ok: true, data: { results: [], truncated: false } };
44
+ }
45
+ return { ok: false, error: result.stderr || "falha no git grep." };
46
+ }
47
+ const max = input.maxResults ?? TOOL_LIMITS.maxResults;
48
+ const results = result.stdout
49
+ .split("\n")
50
+ .map(parseGrepLine)
51
+ .filter((line): line is NonNullable<typeof line> => line !== null)
52
+ .slice(0, max)
53
+ .map((line) => ({
54
+ file: line.file,
55
+ line: line.line,
56
+ text: line.text.slice(0, 240),
57
+ }));
58
+ const truncated = result.stdout.trim().split("\n").length > results.length;
59
+ return {
60
+ ok: true,
61
+ data: { results, truncated },
62
+ ...(truncated ? { truncated: true, hint: "Refine a query/path." } : {}),
63
+ };
64
+ }
65
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Tool `symbol_context` — definição + janela curta do corpo + referências.
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 { gitGrep, numberedRange, parseGrepLine, readLines, snippet } from "./helpers";
10
+
11
+ const Input = z.object({
12
+ symbol: z.string().min(2),
13
+ file: z.string().optional(),
14
+ });
15
+
16
+ /** Devolve no máximo ~120 linhas do símbolo (nunca o arquivo inteiro). */
17
+ export class SymbolContextTool implements AgentTool<z.infer<typeof Input>, unknown> {
18
+ readonly metadata: ToolMetadata = {
19
+ name: "symbol_context",
20
+ description: "Definição + assinatura + corpo curto + referências de um símbolo.",
21
+ category: "symbol",
22
+ capabilities: ["symbol", "read"],
23
+ readOnly: true,
24
+ cost: 3,
25
+ cacheTtlMs: 60_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 files = input.file
36
+ ? [input.file]
37
+ : [
38
+ ...new Set([
39
+ ...context.agent.files,
40
+ ...(context.agent.impact?.affectedFiles ?? []),
41
+ ]),
42
+ ];
43
+ const maxLines = Math.min(TOOL_LIMITS.maxLines, 120);
44
+
45
+ for (const file of files.slice(0, 50)) {
46
+ let relative: string;
47
+ try {
48
+ relative = guard.relativePath(file);
49
+ } catch {
50
+ continue;
51
+ }
52
+ const facts = await context.ast.factsOf(relative);
53
+ if (!facts) continue;
54
+ const symbol = facts.symbols.find((item) => item.name === input.symbol);
55
+ if (!symbol) continue;
56
+
57
+ const lines = readLines(guard.assertReadable(relative)) ?? [];
58
+ const endLine = Math.min(symbol.endLine, symbol.startLine + maxLines - 1);
59
+ const references = referencesOf(context.repoRoot, input.symbol);
60
+ return {
61
+ ok: true,
62
+ data: {
63
+ file: relative,
64
+ name: symbol.name,
65
+ kind: symbol.kind,
66
+ exported: symbol.exported,
67
+ signature: symbol.signature,
68
+ startLine: symbol.startLine,
69
+ endLine,
70
+ truncatedBody: symbol.endLine > endLine,
71
+ body: numberedRange(lines, symbol.startLine, endLine),
72
+ references,
73
+ },
74
+ evidence: [
75
+ {
76
+ id: "",
77
+ file: relative,
78
+ startLine: symbol.startLine,
79
+ endLine,
80
+ snippet: snippet(lines, symbol.startLine, endLine),
81
+ kind: "definition",
82
+ sourceTool: "symbol_context",
83
+ confidence: 0.9,
84
+ },
85
+ ],
86
+ };
87
+ }
88
+ return {
89
+ ok: false,
90
+ error: `símbolo '${input.symbol}' não encontrado (AST).`,
91
+ hint: "Use search_code/find_references.",
92
+ };
93
+ }
94
+ }
95
+
96
+ /** Referências resumidas (arquivo:linha). */
97
+ function referencesOf(root: string, symbol: string): { file: string; line: number }[] {
98
+ const result = gitGrep(root, ["-n", "-I", "-F", "-w", "-e", symbol]);
99
+ if (!result.ok) return [];
100
+ return result.stdout
101
+ .split("\n")
102
+ .map(parseGrepLine)
103
+ .filter((line): line is NonNullable<typeof line> => line !== null)
104
+ .slice(0, 20)
105
+ .map((line) => ({ file: line.file, line: line.line }));
106
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Tool `tests_for_symbol` — arquivos de teste relacionados a um símbolo.
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({ symbol: z.string().min(2), file: z.string().optional() });
11
+
12
+ const TEST_PATHSPECS = [
13
+ "*.test.*",
14
+ "*.spec.*",
15
+ "**/__tests__/**",
16
+ "**/tests/**",
17
+ "**/test/**",
18
+ ];
19
+
20
+ /** Referências em arquivos de teste (até 20 arquivos). */
21
+ export class TestsForSymbolTool implements AgentTool<z.infer<typeof Input>, unknown> {
22
+ readonly metadata: ToolMetadata = {
23
+ name: "tests_for_symbol",
24
+ description: "Arquivos de teste que mencionam um símbolo.",
25
+ category: "evidence",
26
+ capabilities: ["tests", "search"],
27
+ readOnly: true,
28
+ cost: 2,
29
+ cacheTtlMs: 120_000,
30
+ };
31
+
32
+ readonly inputSchema = Input;
33
+
34
+ async execute(
35
+ input: z.infer<typeof Input>,
36
+ context: import("../tool-context").ToolContext,
37
+ ): Promise<ToolResult<unknown>> {
38
+ const args = ["-n", "-I", "-F", "-w", "-e", input.symbol];
39
+ if (input.file) args.push("--", input.file);
40
+ else args.push("--", ...TEST_PATHSPECS);
41
+ const result = gitGrep(context.repoRoot, args);
42
+ if (!result.ok) return { ok: true, data: { files: [], references: [] } };
43
+ const references = result.stdout
44
+ .split("\n")
45
+ .map(parseGrepLine)
46
+ .filter((line): line is NonNullable<typeof line> => line !== null)
47
+ .slice(0, TOOL_LIMITS.maxResults);
48
+ const files = [...new Set(references.map((line) => line.file))].slice(0, 20);
49
+ const evidence = references.slice(0, 10).map((line) => ({
50
+ id: "",
51
+ file: line.file,
52
+ startLine: line.line,
53
+ endLine: line.line,
54
+ snippet: line.text,
55
+ kind: "test" as const,
56
+ sourceTool: "tests_for_symbol",
57
+ confidence: 0.6,
58
+ }));
59
+ return { ok: true, data: { files, references }, evidence };
60
+ }
61
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Guarda de caminhos das tools — impede leitura fora do repositório e de
3
+ * arquivos sensíveis (`..`, `.git`, `.env*`, binários, arquivos enormes).
4
+ */
5
+
6
+ import { statSync } from "node:fs";
7
+ import { extname, isAbsolute, relative, resolve, sep } from "node:path";
8
+
9
+ /** Erro de caminho bloqueado (mensagem segura). */
10
+ export class PathGuardError extends Error {
11
+ constructor(message: string) {
12
+ super(message);
13
+ this.name = "PathGuardError";
14
+ }
15
+ }
16
+
17
+ /** Extensões consideradas binárias. */
18
+ const BINARY_EXTENSIONS = new Set([
19
+ ".png",
20
+ ".jpg",
21
+ ".jpeg",
22
+ ".gif",
23
+ ".webp",
24
+ ".ico",
25
+ ".pdf",
26
+ ".zip",
27
+ ".gz",
28
+ ".tgz",
29
+ ".wasm",
30
+ ".node",
31
+ ".so",
32
+ ".dll",
33
+ ".exe",
34
+ ".lockb",
35
+ ]);
36
+
37
+ /** Nomes/diretórios bloqueados. */
38
+ function isBlockedPath(path: string): boolean {
39
+ const normalized = path.replace(/\\/g, "/");
40
+ if (/(^|\/)\.git(\/|$)/.test(normalized)) return true;
41
+ if (/(^|\/)node_modules(\/|$)/.test(normalized)) return true;
42
+ if (/(^|\/)\.env(\.|$)/.test(normalized)) return true;
43
+ if (/(^|\/)\.houndsman\/core\/grammars(\/|$)/.test(normalized)) return true;
44
+ return false;
45
+ }
46
+
47
+ /** Guarda de caminhos com raiz fixa. */
48
+ export class PathGuard {
49
+ readonly root: string;
50
+ private readonly maxFileBytes: number;
51
+
52
+ constructor(root: string, maxFileBytes = 1_000_000) {
53
+ this.root = resolve(root);
54
+ this.maxFileBytes = maxFileBytes;
55
+ }
56
+
57
+ /** Resolve um caminho relativo dentro da raiz; lança se escapar. */
58
+ resolvePath(input: string): string {
59
+ if (!input.trim()) throw new PathGuardError("caminho vazio");
60
+ const absolute = isAbsolute(input) ? resolve(input) : resolve(this.root, input);
61
+ const rel = relative(this.root, absolute);
62
+ if (rel.startsWith("..") || rel.split(sep).includes("..")) {
63
+ throw new PathGuardError("caminho fora do repositório");
64
+ }
65
+ if (rel === "") throw new PathGuardError("caminho inválido");
66
+ if (isBlockedPath(rel)) throw new PathGuardError("caminho bloqueado");
67
+ if (BINARY_EXTENSIONS.has(extname(absolute).toLowerCase())) {
68
+ throw new PathGuardError("arquivo binário");
69
+ }
70
+ return absolute;
71
+ }
72
+
73
+ /** Resolve e valida tamanho/tipo; devolve o caminho absoluto. */
74
+ assertReadable(input: string): string {
75
+ const absolute = this.resolvePath(input);
76
+ let size = 0;
77
+ try {
78
+ size = statSync(absolute).size;
79
+ } catch {
80
+ throw new PathGuardError("arquivo inexistente");
81
+ }
82
+ if (size > this.maxFileBytes) throw new PathGuardError("arquivo muito grande");
83
+ return absolute;
84
+ }
85
+
86
+ /** Caminho relativo normalizado (para respostas). */
87
+ relativePath(input: string): string {
88
+ return relative(this.root, this.resolvePath(input)).replace(/\\/g, "/");
89
+ }
90
+ }