@payconductor/houndsman 0.11.0 → 0.13.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 (140) hide show
  1. package/README.md +14 -13
  2. package/package.json +1 -1
  3. package/src/cli/commands/doctor-command.ts +22 -0
  4. package/src/cli/commands/hunt-command.ts +440 -29
  5. package/src/cli/commands/review-command.ts +20 -2
  6. package/src/cli/help.ts +16 -8
  7. package/src/cli/index.ts +15 -2
  8. package/src/cli/progress.ts +45 -0
  9. package/src/core/ai/ai-client.ts +3 -7
  10. package/src/core/ai/ai-sdk-model-provider.ts +43 -24
  11. package/src/core/ai/features/brief-enricher.ts +0 -1
  12. package/src/core/ai/features/classifier.ts +0 -1
  13. package/src/core/ai/features/context-summarizer.ts +0 -1
  14. package/src/core/ai/features/pre-analyzer.ts +0 -1
  15. package/src/core/ai/features/technical-summarizer.ts +0 -1
  16. package/src/core/ai/features/verifier.ts +0 -1
  17. package/src/core/ai/index.ts +6 -4
  18. package/src/core/ai/prompts/pre-analysis.ts +3 -2
  19. package/src/core/ai/schemas.ts +1 -23
  20. package/src/core/ai/types.ts +4 -15
  21. package/src/core/ai/usage-budget.ts +1 -1
  22. package/src/core/ast/diff.ts +37 -2
  23. package/src/core/ast/service.ts +1 -1
  24. package/src/core/brief.ts +54 -5
  25. package/src/core/changed-scope.ts +207 -0
  26. package/src/core/commands/scaffold.ts +12 -18
  27. package/src/core/config.ts +30 -15
  28. package/src/core/defaults.ts +14 -5
  29. package/src/core/engine/review-scope.ts +131 -17
  30. package/src/core/engine/review-session.ts +7 -3
  31. package/src/core/engine/review-workspace.ts +14 -0
  32. package/src/core/env.ts +14 -0
  33. package/src/core/hunt/hunt-runner.ts +328 -0
  34. package/src/core/hunt/index.ts +59 -0
  35. package/src/core/hunt/maki-install.ts +331 -0
  36. package/src/core/hunt/maki-model.ts +101 -0
  37. package/src/core/hunt/maki-prompt.ts +71 -0
  38. package/src/core/hunt/maki-release.ts +104 -0
  39. package/src/core/hunt/maki-runner.ts +276 -0
  40. package/src/core/hunt/pool.ts +30 -0
  41. package/src/core/jev/jev-detector.ts +7 -2
  42. package/src/core/logging/index.ts +3 -1
  43. package/src/core/logging/logger.ts +22 -3
  44. package/src/core/logging/progress.ts +282 -0
  45. package/src/core/orchestrator.ts +173 -59
  46. package/src/core/project-config.ts +34 -24
  47. package/src/core/registry-check.ts +2 -12
  48. package/src/core/rule-text.ts +3 -55
  49. package/src/core/services/git-repository.ts +8 -0
  50. package/src/core/storage/types.ts +1 -1
  51. package/src/core/types.ts +3 -14
  52. package/src/index.ts +74 -3
  53. package/src/server/client/utils/constants.ts +3 -6
  54. package/src/server/client/utils/types.ts +0 -1
  55. package/src/skill/OUTPUT-FORMAT.md +5 -0
  56. package/src/skill/README.md +14 -1
  57. package/src/skill/SETUP.md +22 -2
  58. package/src/skill/SKILL.md +4 -0
  59. package/src/skill/fix.md +1 -1
  60. package/src/skill/hunt.md +38 -6
  61. package/src/tests/__snapshots__/baseline.test.ts.snap +69 -32
  62. package/src/tests/ai-core.test.ts +38 -1
  63. package/src/tests/changed-scope.test.ts +197 -0
  64. package/src/tests/cli.test.ts +1 -0
  65. package/src/tests/fallback.test.ts +1 -96
  66. package/src/tests/fixtures/fake-model-provider.ts +0 -1
  67. package/src/tests/hunt-cli.test.ts +185 -0
  68. package/src/tests/hunt-install.test.ts +153 -0
  69. package/src/tests/hunt-maki.test.ts +467 -0
  70. package/src/tests/progress.test.ts +117 -0
  71. package/src/tests/review-scope.test.ts +95 -0
  72. package/src/tests/worktree.test.ts +1 -3
  73. package/src/harness/README.md +0 -15
  74. package/src/harness/agent/agent-context.ts +0 -33
  75. package/src/harness/agent/agent-errors.ts +0 -30
  76. package/src/harness/agent/agent-factory.ts +0 -46
  77. package/src/harness/agent/agent-reporter.ts +0 -34
  78. package/src/harness/agent/agent-result.ts +0 -16
  79. package/src/harness/agent/agent-runner.ts +0 -32
  80. package/src/harness/agent/agent-scheduler.ts +0 -37
  81. package/src/harness/agent/review-agent.ts +0 -401
  82. package/src/harness/config/harness-config.ts +0 -89
  83. package/src/harness/hunt/hunt-harness.ts +0 -83
  84. package/src/harness/hunt/hunt-options.ts +0 -92
  85. package/src/harness/hunt/hunt-result.ts +0 -17
  86. package/src/harness/hunt/hunt-session.ts +0 -359
  87. package/src/harness/hunt/review-planner.ts +0 -98
  88. package/src/harness/index.ts +0 -36
  89. package/src/harness/investigation/agent-state.ts +0 -134
  90. package/src/harness/investigation/evidence-verifier.ts +0 -129
  91. package/src/harness/investigation/evidence.ts +0 -110
  92. package/src/harness/investigation/finding-critic.ts +0 -96
  93. package/src/harness/investigation/investigation-planner.ts +0 -118
  94. package/src/harness/investigation/investigation-task.ts +0 -10
  95. package/src/harness/investigation/review-journal.ts +0 -87
  96. package/src/harness/investigation/rule-analyzer.ts +0 -178
  97. package/src/harness/investigation/rule-signals.ts +0 -110
  98. package/src/harness/prompts/agent-schemas.ts +0 -57
  99. package/src/harness/prompts/reviewer-prompt-builder.ts +0 -101
  100. package/src/harness/prompts/states/conclude-prompt.ts +0 -16
  101. package/src/harness/prompts/states/investigate-prompt.ts +0 -37
  102. package/src/harness/prompts/states/orient-prompt.ts +0 -15
  103. package/src/harness/prompts/states/verify-prompt.ts +0 -22
  104. package/src/harness/tools/agent-tool.ts +0 -102
  105. package/src/harness/tools/builtin/changed-symbols-tool.ts +0 -48
  106. package/src/harness/tools/builtin/doc-context-tool.ts +0 -74
  107. package/src/harness/tools/builtin/file-outline-tool.ts +0 -66
  108. package/src/harness/tools/builtin/find-references-tool.ts +0 -66
  109. package/src/harness/tools/builtin/find-symbol-tool.ts +0 -91
  110. package/src/harness/tools/builtin/helpers.ts +0 -115
  111. package/src/harness/tools/builtin/impact-tool.ts +0 -48
  112. package/src/harness/tools/builtin/imports-exports-tool.ts +0 -53
  113. package/src/harness/tools/builtin/index.ts +0 -59
  114. package/src/harness/tools/builtin/list-files-tool.ts +0 -85
  115. package/src/harness/tools/builtin/pr-context-tool.ts +0 -41
  116. package/src/harness/tools/builtin/read-file-range-tool.ts +0 -73
  117. package/src/harness/tools/builtin/rule-signals-tool.ts +0 -36
  118. package/src/harness/tools/builtin/search-code-tool.ts +0 -65
  119. package/src/harness/tools/builtin/symbol-context-tool.ts +0 -106
  120. package/src/harness/tools/builtin/tests-for-symbol-tool.ts +0 -61
  121. package/src/harness/tools/path-guard.ts +0 -90
  122. package/src/harness/tools/result-limiter.ts +0 -49
  123. package/src/harness/tools/secret-redactor.ts +0 -54
  124. package/src/harness/tools/tool-cache.ts +0 -50
  125. package/src/harness/tools/tool-call-budget.ts +0 -60
  126. package/src/harness/tools/tool-context.ts +0 -5
  127. package/src/harness/tools/tool-middleware.ts +0 -105
  128. package/src/harness/tools/tool-policy.ts +0 -51
  129. package/src/harness/tools/tool-registry.ts +0 -112
  130. package/src/harness/tools/tool-result.ts +0 -15
  131. package/src/harness/tools/toolbox.ts +0 -36
  132. package/src/harness/validation/finding-validator.ts +0 -119
  133. package/src/tests/fixtures/fake-agent-tools.ts +0 -69
  134. package/src/tests/fixtures/fake-investigation.ts +0 -64
  135. package/src/tests/harness-agent.test.ts +0 -141
  136. package/src/tests/harness-investigation.test.ts +0 -206
  137. package/src/tests/harness-tools.test.ts +0 -148
  138. package/src/tests/hunt-command.test.ts +0 -212
  139. package/src/tests/rule-analyzer.test.ts +0 -61
  140. package/src/tests/tool-contracts.test.ts +0 -82
@@ -1,69 +0,0 @@
1
- /**
2
- * Tools fake para a suíte do harness.
3
- */
4
-
5
- import { z } from "zod";
6
- import type {
7
- AgentTool,
8
- ToolContext,
9
- ToolMetadata,
10
- ToolResult,
11
- } from "../../harness/tools/agent-tool";
12
-
13
- /** Tool de eco (testa registry/middleware). */
14
- export class EchoTool implements AgentTool<{ value: string }, { value: string }> {
15
- readonly metadata: ToolMetadata = {
16
- name: "echo",
17
- description: "Devolve o valor recebido.",
18
- category: "context",
19
- capabilities: ["read"],
20
- readOnly: true,
21
- cost: 1,
22
- cacheTtlMs: 60_000,
23
- };
24
-
25
- readonly inputSchema = z.object({ value: z.string() });
26
-
27
- async execute(
28
- input: { value: string },
29
- _context: ToolContext,
30
- ): Promise<ToolResult<{ value: string }>> {
31
- return { ok: true, data: { value: input.value } };
32
- }
33
- }
34
-
35
- /** Tool que produz evidência para o estado. */
36
- export class EvidenceTool implements AgentTool<{ file: string }, unknown> {
37
- readonly metadata: ToolMetadata = {
38
- name: "evidence",
39
- description: "Produz uma evidência fake.",
40
- category: "evidence",
41
- capabilities: ["read"],
42
- readOnly: true,
43
- cost: 1,
44
- };
45
-
46
- readonly inputSchema = z.object({ file: z.string() });
47
-
48
- async execute(
49
- input: { file: string },
50
- _context: ToolContext,
51
- ): Promise<ToolResult<unknown>> {
52
- return {
53
- ok: true,
54
- data: { file: input.file },
55
- evidence: [
56
- {
57
- id: "",
58
- file: input.file,
59
- startLine: 1,
60
- endLine: 1,
61
- snippet: "const x: any = 1;",
62
- kind: "definition",
63
- sourceTool: "evidence",
64
- confidence: 0.9,
65
- },
66
- ],
67
- };
68
- }
69
- }
@@ -1,64 +0,0 @@
1
- /**
2
- * Fixtures de investigação/agente (sem rede, sem git).
3
- */
4
-
5
- import { AstService } from "../../core/ast/service";
6
- import { Watchdog } from "../../core/domain/watchdog";
7
- import type { AgentContext } from "../../harness/agent/agent-context";
8
- import { SilentReporter } from "../../harness/agent/agent-reporter";
9
- import type { ReviewAgentDeps } from "../../harness/agent/review-agent";
10
- import { RuleAnalyzer } from "../../harness/investigation/rule-analyzer";
11
- import { BuiltinToolsModule } from "../../harness/tools/builtin";
12
- import { ToolRegistry } from "../../harness/tools/tool-registry";
13
- import type { FakeModelProvider } from "./fake-model-provider";
14
- import { fakeHoundsmanConfig } from "./fake-project";
15
-
16
- /** Contexto de agente para o watchdog `alpha`. */
17
- export function fakeAgentContext(overrides: Partial<AgentContext> = {}): AgentContext {
18
- const watchdog = new Watchdog(fakeHoundsmanConfig.watchdogs.alpha);
19
- const rules = new RuleAnalyzer().analyze(watchdog.rules);
20
- return {
21
- watchdog,
22
- projectName: "Fake",
23
- branch: "main",
24
- files: ["src/alpha/a.ts"],
25
- overflow: [],
26
- deleted: [],
27
- docs: [],
28
- context: null,
29
- ast: new Map(),
30
- outputPath: "findings/alpha.json",
31
- maxSteps: 4,
32
- maxCalls: 8,
33
- repoRoot: process.cwd(),
34
- diff: { byFile: new Map(), churn: new Map() },
35
- rules,
36
- ...overrides,
37
- };
38
- }
39
-
40
- /** Registry com tools built-in. */
41
- export function builtinRegistry(): ToolRegistry {
42
- const registry = new ToolRegistry();
43
- registry.registerModule(new BuiltinToolsModule());
44
- return registry;
45
- }
46
-
47
- /** Deps completas de um `ReviewAgent` com provider fake. */
48
- export function fakeAgentDeps(
49
- provider: FakeModelProvider,
50
- overrides: Partial<ReviewAgentDeps> = {},
51
- ): ReviewAgentDeps {
52
- const context = overrides.context ?? fakeAgentContext();
53
- return {
54
- context,
55
- provider,
56
- registry: builtinRegistry(),
57
- reporter: new SilentReporter(),
58
- maxOutputTokens: 512,
59
- temperature: 0,
60
- timeoutMs: 5_000,
61
- ast: new AstService(fakeHoundsmanConfig),
62
- ...overrides,
63
- };
64
- }
@@ -1,141 +0,0 @@
1
- /**
2
- * ReviewAgent — máquina de estados com provider fake e tools reais.
3
- */
4
-
5
- import { afterAll, describe, expect, test } from "bun:test";
6
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
7
- import { tmpdir } from "node:os";
8
- import { join } from "node:path";
9
- import { SilentReporter } from "../harness/agent/agent-reporter";
10
- import { ReviewAgent } from "../harness/agent/review-agent";
11
- import { fakeAgentContext, fakeAgentDeps } from "./fixtures/fake-investigation";
12
- import { FakeModelProvider } from "./fixtures/fake-model-provider";
13
-
14
- const RULE = "Nunca usar `any`.";
15
-
16
- const dir = mkdtempSync(join(tmpdir(), "houndsman-agent-"));
17
- mkdirSync(join(dir, "src/alpha"), { recursive: true });
18
- writeFileSync(join(dir, "src/alpha/a.ts"), "const x: any = 1;\nconsole.log(x);\n");
19
- afterAll(() => rmSync(dir, { recursive: true, force: true }));
20
-
21
- /** Provider que investiga com `read_file_range` e devolve um draft. */
22
- function investigatingProvider(options: { withEvidence?: boolean } = {}) {
23
- return new FakeModelProvider(async (request, index) => {
24
- if (index === 0) return { hypotheses: ["uso de any"] };
25
- if (index === 1) return { taskIds: ["rule-0"] };
26
- const tool = request.tools?.read_file_range;
27
- if (!tool) throw new Error("tool read_file_range ausente");
28
- const raw = await tool.execute({
29
- path: "src/alpha/a.ts",
30
- startLine: 1,
31
- endLine: 2,
32
- });
33
- const parsed = JSON.parse(raw) as {
34
- evidence?: { id: string }[];
35
- };
36
- const evidenceId = parsed.evidence?.[0]?.id;
37
- return {
38
- findingDrafts: [
39
- {
40
- rule: RULE,
41
- severity: "blocking",
42
- file: "src/alpha/a.ts",
43
- line: 1,
44
- message: "Uso de any.",
45
- action: "Tipar o valor.",
46
- evidenceIds:
47
- options.withEvidence === false || !evidenceId ? [] : [evidenceId],
48
- confidence: 0.9,
49
- },
50
- ],
51
- coverage: [
52
- { rule: RULE, status: "checked" },
53
- { rule: "Validar entrada do usuário.", status: "not_applicable" },
54
- { rule: "Evitar console.log.", status: "checked" },
55
- ],
56
- };
57
- });
58
- }
59
-
60
- function buildAgent(provider: FakeModelProvider): {
61
- agent: ReviewAgent;
62
- reporter: SilentReporter;
63
- } {
64
- const reporter = new SilentReporter();
65
- const deps = fakeAgentDeps(provider, {
66
- context: fakeAgentContext({ repoRoot: dir }),
67
- reporter,
68
- });
69
- return { agent: new ReviewAgent(deps), reporter };
70
- }
71
-
72
- describe("ReviewAgent", () => {
73
- test("ORIENT→PLAN→INVESTIGATE→VERIFY→CONCLUDE com evidência", async () => {
74
- const { agent, reporter } = buildAgent(investigatingProvider());
75
- const result = await agent.run();
76
- expect(result.status).toBe("done");
77
- expect(result.report?.findings).toHaveLength(1);
78
- expect(result.report?.findings[0]?.evidence).toContain("src/alpha/a.ts");
79
- expect(result.report?.reviewedFiles).toContain("src/alpha/a.ts");
80
- const transitions = reporter.journal.transitions.map((item) => item.to);
81
- expect(transitions).toEqual(["PLAN", "INVESTIGATE", "VERIFY", "CONCLUDE"]);
82
- expect(result.state.coverage.size).toBe(3);
83
- });
84
-
85
- test("VERIFY é obrigatório: draft sem evidência é descartado", async () => {
86
- const { agent } = buildAgent(investigatingProvider({ withEvidence: false }));
87
- const result = await agent.run();
88
- expect(result.status).toBe("done");
89
- expect(result.report?.findings).toHaveLength(0);
90
- const coverage = result.state.coverageOf(RULE);
91
- expect(coverage?.status).toBe("checked");
92
- expect(coverage?.reason).toContain("refutado");
93
- });
94
-
95
- test("toda regra termina com cobertura (mesmo sem o modelo cobrir)", async () => {
96
- const provider = new FakeModelProvider((_request, index) => {
97
- if (index === 0) return { hypotheses: [] };
98
- if (index === 1) return { taskIds: [] };
99
- return { findingDrafts: [], coverage: [] };
100
- });
101
- const { agent } = buildAgent(provider);
102
- const result = await agent.run();
103
- expect(result.state.coverage.size).toBe(3);
104
- expect(
105
- [...result.state.coverage.values()].every(
106
- (item) => item.status === "skipped",
107
- ),
108
- ).toBe(true);
109
- });
110
-
111
- test("falha do provider vira status failed", async () => {
112
- const provider = new FakeModelProvider((_request, index) => {
113
- if (index === 0) throw new Error("boom");
114
- if (index === 1) throw new Error("boom");
115
- throw new Error("429 rate limit");
116
- });
117
- const { agent } = buildAgent(provider);
118
- const result = await agent.run();
119
- expect(result.status).toBe("failed");
120
- expect(result.error).toContain("429");
121
- });
122
-
123
- test("repassa providerOptions da config em toda chamada", async () => {
124
- const provider = investigatingProvider();
125
- const reporter = new SilentReporter();
126
- const agent = new ReviewAgent(
127
- fakeAgentDeps(provider, {
128
- context: fakeAgentContext({ repoRoot: dir }),
129
- reporter,
130
- providerOptions: { deepseek: { thinking: { type: "disabled" } } },
131
- }),
132
- );
133
- await agent.run();
134
- expect(provider.requests.length).toBeGreaterThan(0);
135
- for (const request of provider.requests) {
136
- expect(request.providerOptions).toEqual({
137
- deepseek: { thinking: { type: "disabled" } },
138
- });
139
- }
140
- });
141
- });
@@ -1,206 +0,0 @@
1
- /**
2
- * Investigação: planner, evidência, crítico e cobertura.
3
- */
4
-
5
- import { afterAll, describe, expect, test } from "bun:test";
6
- import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
7
- import { tmpdir } from "node:os";
8
- import { join } from "node:path";
9
- import { AgentState } from "../harness/investigation/agent-state";
10
- import { EvidenceVerifier } from "../harness/investigation/evidence-verifier";
11
- import { FindingCritic } from "../harness/investigation/finding-critic";
12
- import { InvestigationPlanner } from "../harness/investigation/investigation-planner";
13
- import { RuleAnalyzer } from "../harness/investigation/rule-analyzer";
14
-
15
- const RULE = "Nunca usar `any`.";
16
- const analyses = new RuleAnalyzer().analyze([
17
- { severity: "suggestion", text: "Evitar console.log." },
18
- { severity: "blocking", text: RULE },
19
- { severity: "attention", text: "Validar entrada do usuário." },
20
- ]);
21
-
22
- const dir = mkdtempSync(join(tmpdir(), "houndsman-evidence-"));
23
- const file = join(dir, "a.ts");
24
- writeFileSync(file, "const x: any = 1;\nconsole.log(x);\n");
25
- afterAll(() => rmSync(dir, { recursive: true, force: true }));
26
-
27
- function verifier(): EvidenceVerifier {
28
- return new EvidenceVerifier({
29
- allowedFiles: ["a.ts"],
30
- readFile: (path) =>
31
- path === "a.ts" ? "const x: any = 1;\nconsole.log(x);\n" : null,
32
- });
33
- }
34
-
35
- describe("InvestigationPlanner", () => {
36
- test("prioriza por severidade e calcula estimativa", () => {
37
- const plan = new InvestigationPlanner().plan(
38
- { watchdogId: "alpha", files: ["a.ts"], symbols: [] },
39
- analyses,
40
- );
41
- expect(plan.tasks[0]?.rule).toBe(RULE);
42
- expect(plan.tasks[0]?.severity).toBe("blocking");
43
- expect(plan.totalFiles).toBe(1);
44
- expect(plan.estimatedToolCalls).toBeGreaterThan(0);
45
- });
46
-
47
- test("filtra arquivos por hint de path", () => {
48
- const [analysis] = new RuleAnalyzer().analyze([
49
- {
50
- severity: "attention",
51
- text: "Evitar: query no banco em rota.",
52
- hints: { paths: ["^src/alpha/"] },
53
- },
54
- ]);
55
- const plan = new InvestigationPlanner().plan(
56
- {
57
- watchdogId: "alpha",
58
- files: ["src/alpha/a.ts", "src/beta/b.ts"],
59
- symbols: [],
60
- },
61
- analysis ? [analysis] : [],
62
- );
63
- expect(plan.tasks[0]?.files).toEqual(["src/alpha/a.ts"]);
64
- });
65
- });
66
-
67
- describe("EvidenceVerifier", () => {
68
- test("aceita snippet na linha e reancora quando deslocado", () => {
69
- const state = new AgentState();
70
- const exact = state.addEvidence({
71
- file: "a.ts",
72
- startLine: 1,
73
- endLine: 1,
74
- snippet: "const x: any = 1;",
75
- kind: "definition",
76
- sourceTool: "t",
77
- confidence: 0.9,
78
- });
79
- const moved = state.addEvidence({
80
- file: "a.ts",
81
- startLine: 9,
82
- endLine: 9,
83
- snippet: "console.log(x);",
84
- kind: "reference",
85
- sourceTool: "t",
86
- confidence: 0.7,
87
- });
88
- expect(verifier().verify(exact).valid).toBe(true);
89
- const verdict = verifier().verify(moved);
90
- expect(verdict.valid).toBe(true);
91
- expect(verdict.startLine).toBe(2);
92
- });
93
-
94
- test("rejeita arquivo fora do domínio e snippet inexistente", () => {
95
- const state = new AgentState();
96
- const outside = state.addEvidence({
97
- file: "b.ts",
98
- startLine: 1,
99
- endLine: 1,
100
- snippet: "x",
101
- kind: "definition",
102
- sourceTool: "t",
103
- confidence: 1,
104
- });
105
- const missing = state.addEvidence({
106
- file: "a.ts",
107
- startLine: 1,
108
- endLine: 1,
109
- snippet: "não existe",
110
- kind: "definition",
111
- sourceTool: "t",
112
- confidence: 1,
113
- });
114
- expect(verifier().verify(outside).valid).toBe(false);
115
- expect(verifier().verify(missing).valid).toBe(false);
116
- });
117
-
118
- test("draft sem evidência válida é inválido", () => {
119
- const state = new AgentState();
120
- const evidence = state.addEvidence({
121
- file: "a.ts",
122
- startLine: 1,
123
- endLine: 1,
124
- snippet: "const x: any = 1;",
125
- kind: "definition",
126
- sourceTool: "t",
127
- confidence: 0.9,
128
- });
129
- const draft = {
130
- rule: RULE,
131
- severity: "blocking" as const,
132
- file: "a.ts",
133
- line: 1,
134
- message: "m",
135
- action: "a",
136
- evidenceIds: [evidence.id],
137
- confidence: 0.9,
138
- };
139
- expect(verifier().verifyDraft(draft, state).valid).toBe(true);
140
- expect(
141
- verifier().verifyDraft({ ...draft, evidenceIds: ["nope"] }, state).valid,
142
- ).toBe(false);
143
- });
144
- });
145
-
146
- describe("FindingCritic", () => {
147
- const state = new AgentState();
148
- const evidence = state.addEvidence({
149
- file: "a.ts",
150
- startLine: 1,
151
- endLine: 1,
152
- snippet: "const x: any = 1;",
153
- kind: "definition",
154
- sourceTool: "t",
155
- confidence: 0.9,
156
- });
157
- const critic = new FindingCritic({
158
- knownRules: new Set([RULE]),
159
- verifier: verifier(),
160
- });
161
- const draft = {
162
- rule: RULE,
163
- severity: "blocking" as const,
164
- file: "a.ts",
165
- line: 1,
166
- message: "m",
167
- action: "a",
168
- evidenceIds: [evidence.id],
169
- confidence: 0.9,
170
- };
171
-
172
- test("keep com evidência e confiança", () => {
173
- expect(critic.reviewOne(draft, state).verdict).toBe("keep");
174
- });
175
-
176
- test("drop sem evidência, sem regra ou confiança baixa", () => {
177
- expect(critic.reviewOne({ ...draft, evidenceIds: [] }, state).verdict).toBe(
178
- "drop",
179
- );
180
- expect(critic.reviewOne({ ...draft, rule: "inexistente" }, state).verdict).toBe(
181
- "drop",
182
- );
183
- expect(critic.reviewOne({ ...draft, confidence: 0.2 }, state).verdict).toBe(
184
- "drop",
185
- );
186
- });
187
-
188
- test("downgrade quando a confiança não sustenta a severidade", () => {
189
- expect(critic.reviewOne({ ...draft, confidence: 0.5 }, state).verdict).toBe(
190
- "downgrade",
191
- );
192
- });
193
- });
194
-
195
- describe("AgentState", () => {
196
- test("cobertura e hipóteses sem duplicar", () => {
197
- const state = new AgentState();
198
- state.addHypothesis("a");
199
- state.addHypothesis("a");
200
- expect(state.hypotheses).toEqual(["a"]);
201
- state.setCoverage(RULE, "skipped", "motivo");
202
- expect(state.isCovered(RULE)).toBe(true);
203
- state.setCoverage(RULE, "checked");
204
- expect(state.coverageOf(RULE)?.status).toBe("checked");
205
- });
206
- });
@@ -1,148 +0,0 @@
1
- /**
2
- * Tools do harness: path guard, limites, cache, budget e redaction.
3
- */
4
-
5
- import { afterAll, describe, expect, test } from "bun:test";
6
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
7
- import { tmpdir } from "node:os";
8
- import { join } from "node:path";
9
- import { AstService } from "../core/ast/service";
10
- import { Watchdog } from "../core/domain/watchdog";
11
- import { SilentReporter } from "../harness/agent/agent-reporter";
12
- import { AgentState } from "../harness/investigation/agent-state";
13
- import type { ToolContext } from "../harness/tools/agent-tool";
14
- import { BuiltinToolsModule } from "../harness/tools/builtin";
15
- import { PathGuard } from "../harness/tools/path-guard";
16
- import { redactSecrets } from "../harness/tools/secret-redactor";
17
- import { ToolCache } from "../harness/tools/tool-cache";
18
- import { ToolCallBudget } from "../harness/tools/tool-call-budget";
19
- import { ToolMiddleware } from "../harness/tools/tool-middleware";
20
- import { ToolRegistry } from "../harness/tools/tool-registry";
21
- import { EchoTool } from "./fixtures/fake-agent-tools";
22
- import { fakeAgentContext } from "./fixtures/fake-investigation";
23
- import { fakeHoundsmanConfig } from "./fixtures/fake-project";
24
-
25
- const dir = mkdtempSync(join(tmpdir(), "houndsman-tools-"));
26
- mkdirSync(join(dir, "src/alpha"), { recursive: true });
27
- writeFileSync(
28
- join(dir, "src/alpha/a.ts"),
29
- "const x: any = 1;\nconsole.log(x);\nconst y = 2;\n",
30
- );
31
- afterAll(() => rmSync(dir, { recursive: true, force: true }));
32
-
33
- function context(): ToolContext {
34
- const state = new AgentState();
35
- return {
36
- repoRoot: dir,
37
- watchdog: new Watchdog(fakeHoundsmanConfig.watchdogs.alpha),
38
- diff: { byFile: new Map(), churn: new Map() },
39
- ast: new AstService(fakeHoundsmanConfig),
40
- state,
41
- budget: new ToolCallBudget(20, 10),
42
- cache: new ToolCache(),
43
- logger: new SilentReporter(),
44
- agent: fakeAgentContext({ repoRoot: dir }),
45
- rules: new Map(),
46
- };
47
- }
48
-
49
- describe("PathGuard", () => {
50
- test("bloqueia traversal, .git e .env", () => {
51
- const guard = new PathGuard(dir);
52
- expect(() => guard.resolvePath("../segredo")).toThrow();
53
- expect(() => guard.resolvePath(".git/config")).toThrow();
54
- expect(() => guard.resolvePath(".env")).toThrow();
55
- });
56
-
57
- test("aceita caminho interno", () => {
58
- const guard = new PathGuard(dir);
59
- expect(guard.resolvePath("src/alpha/a.ts")).toContain("a.ts");
60
- });
61
- });
62
-
63
- describe("read_file_range", () => {
64
- test("devolve faixa numerada + evidência com id", async () => {
65
- const registry = new ToolRegistry();
66
- registry.registerModule(new BuiltinToolsModule());
67
- const middleware = new ToolMiddleware();
68
- const ctx = context();
69
- const result = await registry.execute(
70
- "read_file_range",
71
- { path: "src/alpha/a.ts", startLine: 1, endLine: 2 },
72
- ctx,
73
- middleware,
74
- );
75
- expect(result.ok).toBe(true);
76
- const data = result.data as { content: string };
77
- expect(data.content).toContain("1\tconst x: any = 1;");
78
- expect(data.content).toContain("2\tconsole.log(x);");
79
- expect(result.evidence?.[0]?.id).toBeTruthy();
80
- expect(ctx.state.evidence.size).toBe(1);
81
- });
82
-
83
- test("caminho fora do repo vira erro estruturado", async () => {
84
- const registry = new ToolRegistry();
85
- registry.registerModule(new BuiltinToolsModule());
86
- const result = await registry.execute(
87
- "read_file_range",
88
- { path: "../x", startLine: 1, endLine: 2 },
89
- context(),
90
- new ToolMiddleware(),
91
- );
92
- expect(result.ok).toBe(false);
93
- expect(result.error).toContain("fora do repositório");
94
- });
95
- });
96
-
97
- describe("ToolRegistry / middleware", () => {
98
- test("nome duplicado falha no registro", () => {
99
- const registry = new ToolRegistry();
100
- registry.registerTool(new EchoTool());
101
- expect(() => registry.registerTool(new EchoTool())).toThrow();
102
- });
103
-
104
- test("resultado em cache não consome budget", async () => {
105
- const registry = new ToolRegistry();
106
- registry.registerTool(new EchoTool());
107
- const middleware = new ToolMiddleware();
108
- const ctx = context();
109
- await registry.execute("echo", { value: "a" }, ctx, middleware);
110
- expect(ctx.budget.total).toBe(1);
111
- await registry.execute("echo", { value: "a" }, ctx, middleware);
112
- expect(ctx.budget.total).toBe(1);
113
- await registry.execute("echo", { value: "b" }, ctx, middleware);
114
- expect(ctx.budget.total).toBe(2);
115
- });
116
-
117
- test("input inválido vira erro (Zod)", async () => {
118
- const registry = new ToolRegistry();
119
- registry.registerTool(new EchoTool());
120
- const result = await registry.execute(
121
- "echo",
122
- { value: 1 },
123
- context(),
124
- new ToolMiddleware(),
125
- );
126
- expect(result.ok).toBe(false);
127
- expect(result.error).toContain("input inválido");
128
- });
129
-
130
- test("budget esgotado interrompe", async () => {
131
- const registry = new ToolRegistry();
132
- registry.registerTool(new EchoTool());
133
- const ctx = context();
134
- ctx.budget = new ToolCallBudget(1, 1);
135
- const middleware = new ToolMiddleware();
136
- await registry.execute("echo", { value: "a" }, ctx, middleware);
137
- const second = await registry.execute("echo", { value: "b" }, ctx, middleware);
138
- expect(second.ok).toBe(false);
139
- expect(second.error).toContain("Orçamento");
140
- });
141
- });
142
-
143
- describe("redaction", () => {
144
- test("remove tokens e chaves", () => {
145
- expect(redactSecrets("token sk-abcdefgh123")).toContain("[REDACTED]");
146
- expect(redactSecrets("api_key = segredo")).toContain("[REDACTED]");
147
- });
148
- });