@payconductor/houndsman 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -4
- package/assets/grammars/tree-sitter-javascript.wasm +0 -0
- package/assets/grammars/tree-sitter-tsx.wasm +0 -0
- package/assets/grammars/tree-sitter-typescript.wasm +0 -0
- package/assets/grammars/web-tree-sitter.wasm +0 -0
- package/package.json +11 -3
- package/src/cli/commands/command.ts +5 -0
- package/src/cli/commands/doctor-command.ts +116 -24
- package/src/cli/commands/hunt-command.ts +25 -0
- package/src/cli/help.ts +17 -1
- package/src/cli/index.ts +10 -6
- package/src/core/ai/ai-client.ts +221 -0
- package/src/core/ai/ai-sdk-model-provider.ts +101 -0
- package/src/core/ai/errors.ts +36 -0
- package/src/core/ai/features/brief-enricher.ts +62 -0
- package/src/core/ai/features/classifier.ts +50 -0
- package/src/core/ai/features/context-summarizer.ts +31 -0
- package/src/core/ai/features/pre-analyzer.ts +51 -0
- package/src/core/ai/features/verifier.ts +45 -0
- package/src/core/ai/index.ts +56 -0
- package/src/core/ai/model-factory.ts +137 -0
- package/src/core/ai/prompts/base.ts +36 -0
- package/src/core/ai/prompts/brief.ts +87 -0
- package/src/core/ai/prompts/classification.ts +36 -0
- package/src/core/ai/prompts/context.ts +29 -0
- package/src/core/ai/prompts/pre-analysis.ts +49 -0
- package/src/core/ai/prompts/verify.ts +40 -0
- package/src/core/ai/schemas.ts +110 -0
- package/src/core/ai/types.ts +76 -0
- package/src/core/ai/usage-budget.ts +75 -0
- package/src/core/ai/usage-recorder.ts +79 -0
- package/src/core/assets.ts +16 -0
- package/src/core/ast/diff.ts +220 -0
- package/src/core/ast/impact.ts +127 -0
- package/src/core/ast/parser.ts +167 -0
- package/src/core/ast/service.ts +233 -0
- package/src/core/ast/symbols.ts +294 -0
- package/src/core/ast/types.ts +125 -0
- package/src/core/brief.ts +138 -0
- package/src/core/commands/findings.ts +59 -1
- package/src/core/commands/review.ts +2 -0
- package/src/core/commands/scaffold.ts +159 -10
- package/src/core/commands/update.ts +5 -3
- package/src/core/config.ts +85 -0
- package/src/core/defaults.ts +27 -0
- package/src/core/domain/watchdog.ts +43 -5
- package/src/core/engine/houndsman-engine.ts +16 -1
- package/src/core/engine/review-scope.ts +52 -6
- package/src/core/engine/review-session.ts +27 -2
- package/src/core/env.ts +101 -0
- package/src/core/file-classifier.ts +25 -4
- package/src/core/jev/jev-client.ts +8 -7
- package/src/core/jev/jev-verify.ts +2 -1
- package/src/core/logging/index.ts +17 -0
- package/src/core/logging/logger.ts +187 -0
- package/src/core/orchestrator.ts +357 -9
- package/src/core/project-config.ts +123 -1
- package/src/core/registry-check.ts +148 -6
- package/src/core/renderers/manifest-writer.ts +33 -0
- package/src/core/rule-text.ts +97 -0
- package/src/core/services/file-classifier.ts +10 -3
- package/src/core/services/verifier.ts +14 -3
- package/src/core/storage/paths.ts +6 -5
- package/src/core/storage/types.ts +6 -0
- package/src/core/storage/work-area.ts +12 -0
- package/src/core/types.ts +37 -1
- package/src/core/verify.ts +17 -5
- package/src/harness/README.md +15 -0
- package/src/harness/agent/agent-context.ts +33 -0
- package/src/harness/agent/agent-errors.ts +30 -0
- package/src/harness/agent/agent-factory.ts +45 -0
- package/src/harness/agent/agent-reporter.ts +34 -0
- package/src/harness/agent/agent-result.ts +16 -0
- package/src/harness/agent/agent-runner.ts +32 -0
- package/src/harness/agent/agent-scheduler.ts +37 -0
- package/src/harness/agent/review-agent.ts +391 -0
- package/src/harness/config/harness-config.ts +84 -0
- package/src/harness/hunt/hunt-harness.ts +83 -0
- package/src/harness/hunt/hunt-options.ts +75 -0
- package/src/harness/hunt/hunt-result.ts +17 -0
- package/src/harness/hunt/hunt-session.ts +372 -0
- package/src/harness/hunt/review-planner.ts +98 -0
- package/src/harness/index.ts +36 -0
- package/src/harness/investigation/agent-state.ts +134 -0
- package/src/harness/investigation/evidence-verifier.ts +129 -0
- package/src/harness/investigation/evidence.ts +110 -0
- package/src/harness/investigation/finding-critic.ts +96 -0
- package/src/harness/investigation/investigation-planner.ts +118 -0
- package/src/harness/investigation/investigation-task.ts +10 -0
- package/src/harness/investigation/review-journal.ts +87 -0
- package/src/harness/investigation/rule-analyzer.ts +178 -0
- package/src/harness/investigation/rule-signals.ts +110 -0
- package/src/harness/prompts/agent-schemas.ts +57 -0
- package/src/harness/prompts/reviewer-prompt-builder.ts +101 -0
- package/src/harness/prompts/states/conclude-prompt.ts +16 -0
- package/src/harness/prompts/states/investigate-prompt.ts +37 -0
- package/src/harness/prompts/states/orient-prompt.ts +15 -0
- package/src/harness/prompts/states/verify-prompt.ts +22 -0
- package/src/harness/tools/agent-tool.ts +102 -0
- package/src/harness/tools/builtin/changed-symbols-tool.ts +48 -0
- package/src/harness/tools/builtin/doc-context-tool.ts +74 -0
- package/src/harness/tools/builtin/file-outline-tool.ts +66 -0
- package/src/harness/tools/builtin/find-references-tool.ts +66 -0
- package/src/harness/tools/builtin/find-symbol-tool.ts +91 -0
- package/src/harness/tools/builtin/helpers.ts +115 -0
- package/src/harness/tools/builtin/impact-tool.ts +48 -0
- package/src/harness/tools/builtin/imports-exports-tool.ts +53 -0
- package/src/harness/tools/builtin/index.ts +59 -0
- package/src/harness/tools/builtin/list-files-tool.ts +85 -0
- package/src/harness/tools/builtin/pr-context-tool.ts +41 -0
- package/src/harness/tools/builtin/read-file-range-tool.ts +73 -0
- package/src/harness/tools/builtin/rule-signals-tool.ts +36 -0
- package/src/harness/tools/builtin/search-code-tool.ts +65 -0
- package/src/harness/tools/builtin/symbol-context-tool.ts +106 -0
- package/src/harness/tools/builtin/tests-for-symbol-tool.ts +61 -0
- package/src/harness/tools/path-guard.ts +90 -0
- package/src/harness/tools/result-limiter.ts +49 -0
- package/src/harness/tools/secret-redactor.ts +54 -0
- package/src/harness/tools/tool-cache.ts +50 -0
- package/src/harness/tools/tool-call-budget.ts +60 -0
- package/src/harness/tools/tool-context.ts +5 -0
- package/src/harness/tools/tool-middleware.ts +105 -0
- package/src/harness/tools/tool-policy.ts +51 -0
- package/src/harness/tools/tool-registry.ts +112 -0
- package/src/harness/tools/tool-result.ts +15 -0
- package/src/harness/tools/toolbox.ts +36 -0
- package/src/harness/validation/finding-validator.ts +119 -0
- package/src/index.ts +46 -0
- package/src/server/client/utils/constants.ts +27 -5
- package/src/server/client/utils/types.ts +6 -0
- package/src/server/index.ts +3 -2
- package/src/skill/OUTPUT-FORMAT.md +12 -0
- package/src/skill/SETUP.md +6 -2
- package/src/skill/hunt.md +14 -2
- package/src/skill/verify.md +1 -1
- package/src/tests/__snapshots__/baseline.test.ts.snap +96 -14
- package/src/tests/ai-core.test.ts +120 -0
- package/src/tests/ai-features.test.ts +183 -0
- package/src/tests/ast.test.ts +335 -0
- package/src/tests/env.test.ts +69 -0
- package/src/tests/fallback.test.ts +172 -0
- package/src/tests/fixtures/fake-agent-tools.ts +69 -0
- package/src/tests/fixtures/fake-investigation.ts +64 -0
- package/src/tests/fixtures/fake-model-provider.ts +48 -0
- package/src/tests/harness-agent.test.ts +122 -0
- package/src/tests/harness-investigation.test.ts +206 -0
- package/src/tests/harness-tools.test.ts +148 -0
- package/src/tests/hunt-command.test.ts +212 -0
- package/src/tests/logger.test.ts +83 -0
- package/src/tests/review-scope.test.ts +11 -11
- package/src/tests/rule-analyzer.test.ts +61 -0
- package/src/tests/tool-contracts.test.ts +82 -0
- package/src/tests/verify.test.ts +1 -1
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Middleware de execução das tools.
|
|
3
|
+
*
|
|
4
|
+
* Cadeia: validação Zod → cache → budget → execução → limites → redaction →
|
|
5
|
+
* auditoria. Toda tool passa por aqui; nenhuma executa direto.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ToolResult } from "./agent-tool";
|
|
9
|
+
import { limitText, TOOL_LIMITS } from "./result-limiter";
|
|
10
|
+
import { redactSecrets, redactValue } from "./secret-redactor";
|
|
11
|
+
import { ToolCache } from "./tool-cache";
|
|
12
|
+
import { ToolBudgetError } from "./tool-call-budget";
|
|
13
|
+
import type { ToolContext } from "./tool-context";
|
|
14
|
+
import type { AgentStateName } from "./tool-policy";
|
|
15
|
+
import type { RegisteredTool } from "./tool-registry";
|
|
16
|
+
|
|
17
|
+
/** Executor com validação, cache, budget, limite e auditoria. */
|
|
18
|
+
export class ToolMiddleware {
|
|
19
|
+
async run(
|
|
20
|
+
tool: RegisteredTool,
|
|
21
|
+
input: unknown,
|
|
22
|
+
context: ToolContext,
|
|
23
|
+
state: AgentStateName,
|
|
24
|
+
): Promise<ToolResult<unknown>> {
|
|
25
|
+
const parsed = tool.inputSchema.safeParse(input);
|
|
26
|
+
if (!parsed.success) {
|
|
27
|
+
return {
|
|
28
|
+
ok: false,
|
|
29
|
+
error: `input inválido para '${tool.metadata.name}': ${parsed.error.message}`,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const maxPerAgent = tool.metadata.maxCallsPerAgent;
|
|
34
|
+
if (
|
|
35
|
+
maxPerAgent !== undefined &&
|
|
36
|
+
context.state.callsOf(tool.metadata.name) >= maxPerAgent
|
|
37
|
+
) {
|
|
38
|
+
return {
|
|
39
|
+
ok: false,
|
|
40
|
+
error: `Limite de ${maxPerAgent} chamada(s) da tool '${tool.metadata.name}'.`,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const key = ToolCache.key(tool.metadata.name, parsed.data);
|
|
45
|
+
const cached = context.cache.get<ToolResult<unknown>>(key);
|
|
46
|
+
if (cached) return cached;
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
context.budget.consume(tool.metadata.name);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
return {
|
|
52
|
+
ok: false,
|
|
53
|
+
error: error instanceof ToolBudgetError ? error.message : String(error),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const started = performance.now();
|
|
58
|
+
try {
|
|
59
|
+
// Contravariância: a tool concreta aceita seu próprio input.
|
|
60
|
+
const result = (await tool.execute(
|
|
61
|
+
parsed.data as never,
|
|
62
|
+
context,
|
|
63
|
+
)) as ToolResult<unknown>;
|
|
64
|
+
const limited = limitResult(result);
|
|
65
|
+
const redacted = redactValue(limited);
|
|
66
|
+
context.cache.set(key, redacted, tool.metadata.cacheTtlMs ?? 60_000);
|
|
67
|
+
context.state.recordToolCall(tool.metadata.name);
|
|
68
|
+
const stored = (redacted.evidence ?? []).map(
|
|
69
|
+
(evidence) =>
|
|
70
|
+
context.state.getEvidence(evidence.id) ??
|
|
71
|
+
context.state.addEvidence(evidence),
|
|
72
|
+
);
|
|
73
|
+
if (stored.length > 0) redacted.evidence = stored;
|
|
74
|
+
context.logger.journal.recordToolCall(
|
|
75
|
+
tool.metadata.name,
|
|
76
|
+
state,
|
|
77
|
+
performance.now() - started,
|
|
78
|
+
redacted,
|
|
79
|
+
);
|
|
80
|
+
return redacted;
|
|
81
|
+
} catch (error) {
|
|
82
|
+
const message = redactSecrets(
|
|
83
|
+
error instanceof Error ? error.message : String(error),
|
|
84
|
+
);
|
|
85
|
+
context.logger.journal.recordToolCall(
|
|
86
|
+
tool.metadata.name,
|
|
87
|
+
state,
|
|
88
|
+
performance.now() - started,
|
|
89
|
+
{ ok: false, error: message },
|
|
90
|
+
);
|
|
91
|
+
return { ok: false, error: message };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Aplica tetos de texto ao resultado da tool. */
|
|
97
|
+
function limitResult(result: ToolResult<unknown>): ToolResult<unknown> {
|
|
98
|
+
const copy: ToolResult<unknown> = { ...result };
|
|
99
|
+
if (typeof copy.data === "string") {
|
|
100
|
+
const limited = limitText(copy.data, TOOL_LIMITS.maxBytes);
|
|
101
|
+
copy.data = limited.text;
|
|
102
|
+
if (limited.truncated) copy.truncated = true;
|
|
103
|
+
}
|
|
104
|
+
return copy;
|
|
105
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Política de tools por estado do agente.
|
|
3
|
+
*
|
|
4
|
+
* O modelo só vê as tools permitidas no estado atual. Adicionar uma tool não
|
|
5
|
+
* exige alterar o agente — basta declarar capabilities compatíveis.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ToolCapability, ToolMetadata } from "./agent-tool";
|
|
9
|
+
|
|
10
|
+
/** Estados da máquina de investigação. */
|
|
11
|
+
export type AgentStateName = "ORIENT" | "PLAN" | "INVESTIGATE" | "VERIFY" | "CONCLUDE";
|
|
12
|
+
|
|
13
|
+
/** Capabilities liberadas por estado. */
|
|
14
|
+
const DEFAULT_POLICY: Record<AgentStateName, readonly ToolCapability[]> = {
|
|
15
|
+
ORIENT: ["read", "outline", "ast", "impact", "rules", "symbol"],
|
|
16
|
+
PLAN: ["rules", "symbol", "impact", "ast"],
|
|
17
|
+
INVESTIGATE: [
|
|
18
|
+
"read",
|
|
19
|
+
"search",
|
|
20
|
+
"outline",
|
|
21
|
+
"symbol",
|
|
22
|
+
"references",
|
|
23
|
+
"ast",
|
|
24
|
+
"impact",
|
|
25
|
+
"rules",
|
|
26
|
+
"tests",
|
|
27
|
+
"docs",
|
|
28
|
+
],
|
|
29
|
+
VERIFY: ["symbol", "references", "search", "read"],
|
|
30
|
+
CONCLUDE: [],
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** Política substituível. */
|
|
34
|
+
export class ToolPolicy {
|
|
35
|
+
private readonly policy: Record<AgentStateName, readonly ToolCapability[]>;
|
|
36
|
+
|
|
37
|
+
constructor(policy: Partial<Record<AgentStateName, readonly ToolCapability[]>> = {}) {
|
|
38
|
+
this.policy = { ...DEFAULT_POLICY, ...policy };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Capabilities liberadas no estado. */
|
|
42
|
+
capabilities(state: AgentStateName): readonly ToolCapability[] {
|
|
43
|
+
return this.policy[state];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** `true` se a tool pode ser usada no estado. */
|
|
47
|
+
allows(state: AgentStateName, metadata: ToolMetadata): boolean {
|
|
48
|
+
const allowed = this.policy[state];
|
|
49
|
+
return metadata.capabilities.some((capability) => allowed.includes(capability));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registro das tools do harness.
|
|
3
|
+
*
|
|
4
|
+
* Módulos registram tools; nomes duplicados falham no startup. O registry gera
|
|
5
|
+
* as descrições do prompt a partir do metadata (nunca hardcoded).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { AiTool } from "../../core/ai";
|
|
9
|
+
import type {
|
|
10
|
+
RegisterableTool,
|
|
11
|
+
ToolContext,
|
|
12
|
+
ToolMetadata,
|
|
13
|
+
ToolModule,
|
|
14
|
+
ToolRegistryLike,
|
|
15
|
+
ToolResult,
|
|
16
|
+
} from "./agent-tool";
|
|
17
|
+
import type { ToolMiddleware } from "./tool-middleware";
|
|
18
|
+
import type { AgentStateName, ToolPolicy } from "./tool-policy";
|
|
19
|
+
|
|
20
|
+
/** Tool registrada, com a forma type-erased aceita pelo middleware. */
|
|
21
|
+
export type RegisteredTool = RegisterableTool;
|
|
22
|
+
|
|
23
|
+
/** Registry de tools. */
|
|
24
|
+
export class ToolRegistry implements ToolRegistryLike {
|
|
25
|
+
private readonly tools = new Map<string, RegisteredTool>();
|
|
26
|
+
|
|
27
|
+
/** Registra uma tool; nome duplicado é erro de configuração. */
|
|
28
|
+
registerTool(tool: RegisterableTool): void {
|
|
29
|
+
if (this.tools.has(tool.metadata.name)) {
|
|
30
|
+
throw new Error(`Tool duplicada: '${tool.metadata.name}'.`);
|
|
31
|
+
}
|
|
32
|
+
this.tools.set(tool.metadata.name, tool);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Registra um módulo (agrupa tools). */
|
|
36
|
+
registerModule(module: ToolModule): void {
|
|
37
|
+
module.register(this);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Tool por nome. */
|
|
41
|
+
get(name: string): RegisteredTool | undefined {
|
|
42
|
+
return this.tools.get(name);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Todas as tools (ordem de registro). */
|
|
46
|
+
list(): RegisteredTool[] {
|
|
47
|
+
return [...this.tools.values()];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Tools com uma capability. */
|
|
51
|
+
listByCapability(capability: string): RegisteredTool[] {
|
|
52
|
+
return this.list().filter((tool) =>
|
|
53
|
+
(tool.metadata.capabilities as readonly string[]).includes(capability),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Descrição compacta das tools permitidas (para o prompt). */
|
|
58
|
+
describeForPrompt(policy: ToolPolicy, state: AgentStateName): string {
|
|
59
|
+
return this.list()
|
|
60
|
+
.filter((tool) => policy.allows(state, tool.metadata))
|
|
61
|
+
.map(
|
|
62
|
+
(tool) =>
|
|
63
|
+
`- ${tool.metadata.name} (${tool.metadata.capabilities.join(", ")}): ${tool.metadata.description}`,
|
|
64
|
+
)
|
|
65
|
+
.join("\n");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Converte as tools permitidas no formato do modelo. */
|
|
69
|
+
toModelTools(
|
|
70
|
+
policy: ToolPolicy,
|
|
71
|
+
state: AgentStateName,
|
|
72
|
+
context: ToolContext,
|
|
73
|
+
middleware: ToolMiddleware,
|
|
74
|
+
): Record<string, AiTool> {
|
|
75
|
+
const entries = this.list()
|
|
76
|
+
.filter((tool) => policy.allows(state, tool.metadata))
|
|
77
|
+
.map((tool) => [
|
|
78
|
+
tool.metadata.name,
|
|
79
|
+
{
|
|
80
|
+
name: tool.metadata.name,
|
|
81
|
+
description: tool.metadata.description,
|
|
82
|
+
inputSchema: tool.inputSchema,
|
|
83
|
+
execute: async (input: unknown): Promise<string> => {
|
|
84
|
+
const result = await middleware.run(tool, input, context, state);
|
|
85
|
+
return JSON.stringify(result);
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
]);
|
|
89
|
+
return Object.fromEntries(entries);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Metadados registrados. */
|
|
93
|
+
metadata(): ToolMetadata[] {
|
|
94
|
+
return this.list().map((tool) => tool.metadata);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Executa uma tool com middleware (usado nos testes). */
|
|
98
|
+
execute(
|
|
99
|
+
name: string,
|
|
100
|
+
input: unknown,
|
|
101
|
+
context: ToolContext,
|
|
102
|
+
middleware: ToolMiddleware,
|
|
103
|
+
state: AgentStateName = "INVESTIGATE",
|
|
104
|
+
): Promise<ToolResult<unknown>> {
|
|
105
|
+
const tool = this.tools.get(name);
|
|
106
|
+
if (!tool)
|
|
107
|
+
return Promise.resolve({ ok: false, error: `tool desconhecida: ${name}` });
|
|
108
|
+
return middleware.run(tool, input, context, state) as Promise<
|
|
109
|
+
ToolResult<unknown>
|
|
110
|
+
>;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resultado padrão de uma tool (reexport de contrato).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type { ToolResult } from "./agent-tool";
|
|
6
|
+
|
|
7
|
+
/** Resultado de sucesso. */
|
|
8
|
+
export function ok<T>(data: T, extra: Record<string, unknown> = {}) {
|
|
9
|
+
return { ok: true as const, data, ...extra };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Resultado de erro estruturado. */
|
|
13
|
+
export function fail(error: string) {
|
|
14
|
+
return { ok: false as const, error };
|
|
15
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visão de tools por agente (registry + policy + middleware).
|
|
3
|
+
*
|
|
4
|
+
* O agente pede `toModelTools(state)` e recebe apenas o que pode usar naquele
|
|
5
|
+
* estado, já com cache/budget/redaction aplicados.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { AiTool } from "../../core/ai";
|
|
9
|
+
import type { ToolContext } from "./tool-context";
|
|
10
|
+
import type { ToolMiddleware } from "./tool-middleware";
|
|
11
|
+
import type { AgentStateName, ToolPolicy } from "./tool-policy";
|
|
12
|
+
import type { ToolRegistry } from "./tool-registry";
|
|
13
|
+
|
|
14
|
+
/** Fachada por agente. */
|
|
15
|
+
export class Toolbox {
|
|
16
|
+
constructor(
|
|
17
|
+
readonly registry: ToolRegistry,
|
|
18
|
+
readonly policy: ToolPolicy,
|
|
19
|
+
readonly middleware: ToolMiddleware,
|
|
20
|
+
) {}
|
|
21
|
+
|
|
22
|
+
/** Tools permitidas no estado, no formato do modelo. */
|
|
23
|
+
toModelTools(state: AgentStateName, context: ToolContext): Record<string, AiTool> {
|
|
24
|
+
return this.registry.toModelTools(this.policy, state, context, this.middleware);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Descrição das tools permitidas (prompt). */
|
|
28
|
+
describe(state: AgentStateName): string {
|
|
29
|
+
return this.registry.describeForPrompt(this.policy, state);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Executa uma tool fora do modelo (testes/uso determinístico). */
|
|
33
|
+
execute(name: string, input: unknown, context: ToolContext, state: AgentStateName) {
|
|
34
|
+
return this.registry.execute(name, input, context, this.middleware, state);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validação final do relatório de um watchdog.
|
|
3
|
+
*
|
|
4
|
+
* Descarta findings que citam regra inexistente, arquivo fora do domínio/impacto,
|
|
5
|
+
* overflow (não lido) ou linha inválida. `reviewedFiles` só aceita arquivos
|
|
6
|
+
* existentes; overflow entra em `skipped`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFileSync } from "node:fs";
|
|
10
|
+
import type { WatchdogReport } from "../../core/report-builder";
|
|
11
|
+
import { emergentText, isEmergentRule } from "../../core/rule-text";
|
|
12
|
+
import { SEVERITY_ORDER, type Severity } from "../../core/types";
|
|
13
|
+
import { capSeverity } from "../investigation/finding-critic";
|
|
14
|
+
|
|
15
|
+
/** Contexto de validação. */
|
|
16
|
+
export type ValidationContext = {
|
|
17
|
+
watchdogId: string;
|
|
18
|
+
knownRules: ReadonlySet<string>;
|
|
19
|
+
allowedFiles: ReadonlySet<string>;
|
|
20
|
+
overflow: ReadonlySet<string>;
|
|
21
|
+
readFile: (path: string) => string | null;
|
|
22
|
+
/** Aceita findings `Lógica:`/`Domínio:` fora das regras. */
|
|
23
|
+
allowEmergentFindings?: boolean;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** Resultado da validação. */
|
|
27
|
+
export type ValidationResult = {
|
|
28
|
+
report: WatchdogReport;
|
|
29
|
+
dropped: { finding: string; reason: string }[];
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const SEVERITIES = new Set<string>(SEVERITY_ORDER);
|
|
33
|
+
|
|
34
|
+
/** Validador de relatórios. */
|
|
35
|
+
export class FindingValidator {
|
|
36
|
+
private readonly context: ValidationContext;
|
|
37
|
+
|
|
38
|
+
constructor(context: ValidationContext) {
|
|
39
|
+
this.context = context;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Valida/normaliza o relatório. */
|
|
43
|
+
validate(report: WatchdogReport): ValidationResult {
|
|
44
|
+
const dropped: { finding: string; reason: string }[] = [];
|
|
45
|
+
const findings = report.findings
|
|
46
|
+
.filter((finding) => {
|
|
47
|
+
const reason = this.reasonToDrop(finding);
|
|
48
|
+
if (!reason) return true;
|
|
49
|
+
dropped.push({
|
|
50
|
+
finding: `${finding.file}:${finding.line ?? "?"}`,
|
|
51
|
+
reason,
|
|
52
|
+
});
|
|
53
|
+
return false;
|
|
54
|
+
})
|
|
55
|
+
.map((finding) =>
|
|
56
|
+
isEmergentRule(finding.rule)
|
|
57
|
+
? {
|
|
58
|
+
...finding,
|
|
59
|
+
severity: capSeverity(finding.severity, "attention"),
|
|
60
|
+
}
|
|
61
|
+
: finding,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const reviewedFiles = report.reviewedFiles
|
|
65
|
+
.filter((file) => this.context.allowedFiles.has(file))
|
|
66
|
+
.filter((file) => this.context.readFile(file) !== null);
|
|
67
|
+
|
|
68
|
+
const skipped = new Set(report.skipped);
|
|
69
|
+
for (const file of this.context.overflow) skipped.add(`${file} (budget)`);
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
report: {
|
|
73
|
+
watchdogId: this.context.watchdogId,
|
|
74
|
+
reviewedFiles: [...new Set(reviewedFiles)],
|
|
75
|
+
skipped: [...skipped],
|
|
76
|
+
findings,
|
|
77
|
+
},
|
|
78
|
+
dropped,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Motivo para descartar (ou `null` se válido). */
|
|
83
|
+
private reasonToDrop(finding: {
|
|
84
|
+
rule: string;
|
|
85
|
+
severity: Severity;
|
|
86
|
+
file: string;
|
|
87
|
+
line?: number;
|
|
88
|
+
}): string | null {
|
|
89
|
+
if (isEmergentRule(finding.rule)) {
|
|
90
|
+
if (!this.context.allowEmergentFindings)
|
|
91
|
+
return "finding emergente desabilitado";
|
|
92
|
+
if (!emergentText(finding.rule)) return "finding emergente sem descrição";
|
|
93
|
+
} else if (!this.context.knownRules.has(finding.rule.trim())) {
|
|
94
|
+
return "regra inexistente";
|
|
95
|
+
}
|
|
96
|
+
if (!SEVERITIES.has(finding.severity)) return "severidade inválida";
|
|
97
|
+
if (this.context.overflow.has(finding.file)) return "arquivo fora do orçamento";
|
|
98
|
+
if (!this.context.allowedFiles.has(finding.file))
|
|
99
|
+
return "arquivo fora do domínio";
|
|
100
|
+
const content = this.context.readFile(finding.file);
|
|
101
|
+
if (content === null) return "arquivo ausente";
|
|
102
|
+
if (finding.line !== undefined) {
|
|
103
|
+
const total = content.split("\n").length;
|
|
104
|
+
if (finding.line < 1 || finding.line > total) return "linha fora do arquivo";
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Leitor de arquivos padrão (UTF-8; `null` se ausente). */
|
|
111
|
+
export function defaultFileReader(root: string): (path: string) => string | null {
|
|
112
|
+
return (path: string) => {
|
|
113
|
+
try {
|
|
114
|
+
return readFileSync(`${root}/${path}`, "utf8");
|
|
115
|
+
} catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -5,13 +5,53 @@
|
|
|
5
5
|
* (`houndsman.config.ts`): watchdogs, tipos e helpers de autoria.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
export type {
|
|
9
|
+
AiResult,
|
|
10
|
+
AiTool,
|
|
11
|
+
AiUsage,
|
|
12
|
+
ModelProvider,
|
|
13
|
+
ModelRequest,
|
|
14
|
+
ModelResponse,
|
|
15
|
+
} from "./core/ai";
|
|
16
|
+
export {
|
|
17
|
+
AiBriefEnricher,
|
|
18
|
+
AiClassifier,
|
|
19
|
+
AiClient,
|
|
20
|
+
AiContextSummarizer,
|
|
21
|
+
AiPreAnalyzer,
|
|
22
|
+
AiUsageBudget,
|
|
23
|
+
AiUsageRecorder,
|
|
24
|
+
AiVerifier,
|
|
25
|
+
ModelFactory,
|
|
26
|
+
} from "./core/ai";
|
|
8
27
|
export { ArgParser } from "./core/arg-parser";
|
|
9
28
|
export { assetPath, OUTPUT_FORMAT_FILE } from "./core/assets";
|
|
29
|
+
export { computeDelta, emptyDelta, parseHunks } from "./core/ast/diff";
|
|
30
|
+
export { buildImpact, resolveImport } from "./core/ast/impact";
|
|
31
|
+
export { AstParser, languageForPath } from "./core/ast/parser";
|
|
32
|
+
export { AstService } from "./core/ast/service";
|
|
33
|
+
export type {
|
|
34
|
+
AstAnalysis,
|
|
35
|
+
AstDelta,
|
|
36
|
+
AstEntry,
|
|
37
|
+
AstExport,
|
|
38
|
+
AstFacts,
|
|
39
|
+
AstFileBrief,
|
|
40
|
+
AstImpact,
|
|
41
|
+
AstImport,
|
|
42
|
+
AstLanguage,
|
|
43
|
+
AstSymbol,
|
|
44
|
+
AstSymbolKind,
|
|
45
|
+
} from "./core/ast/types";
|
|
10
46
|
export { defaultRenderBrief, PROCEDURE, renderBrief } from "./core/brief";
|
|
11
47
|
export type { FindingsCommandResult } from "./core/commands/findings";
|
|
12
48
|
export { runFindingsCommand, runVerifyCommand } from "./core/commands/findings";
|
|
13
49
|
export { ProjectScaffolder } from "./core/commands/scaffold";
|
|
14
50
|
export type {
|
|
51
|
+
AiProviderKind,
|
|
52
|
+
HoundsmanAi,
|
|
53
|
+
HoundsmanAiFeatures,
|
|
54
|
+
HoundsmanAst,
|
|
15
55
|
HoundsmanBudgets,
|
|
16
56
|
HoundsmanConcurrency,
|
|
17
57
|
HoundsmanConfig,
|
|
@@ -20,6 +60,8 @@ export type {
|
|
|
20
60
|
} from "./core/config";
|
|
21
61
|
export {
|
|
22
62
|
DEFAULT_ALWAYS_INCLUDE_DOCS,
|
|
63
|
+
DEFAULT_AST_MAX_FILE_BYTES,
|
|
64
|
+
DEFAULT_AST_MAX_IMPACT_FILES,
|
|
23
65
|
DEFAULT_JEV_CONCURRENCY,
|
|
24
66
|
DEFAULT_MAX_FILES_PER_BRIEF,
|
|
25
67
|
defineHoundsmanConfig,
|
|
@@ -47,6 +89,8 @@ export { buildCarryover, mergeCarryover } from "./core/engine/carryover";
|
|
|
47
89
|
export { HoundsmanEngine } from "./core/engine/houndsman-engine";
|
|
48
90
|
export { ReviewScopeResolver } from "./core/engine/review-scope";
|
|
49
91
|
export { ReviewSession } from "./core/engine/review-session";
|
|
92
|
+
export type { HoundsmanEnv } from "./core/env";
|
|
93
|
+
export { ENV_NAMES, env, readEnv } from "./core/env";
|
|
50
94
|
export {
|
|
51
95
|
churnOf,
|
|
52
96
|
classifyEntries,
|
|
@@ -159,6 +203,7 @@ export type {
|
|
|
159
203
|
FileStatus,
|
|
160
204
|
FileStatusMap,
|
|
161
205
|
Rule,
|
|
206
|
+
RuleHints,
|
|
162
207
|
Severity,
|
|
163
208
|
WatchdogDefinition,
|
|
164
209
|
WatchdogMeta,
|
|
@@ -172,6 +217,7 @@ export {
|
|
|
172
217
|
SEVERITY_ORDER,
|
|
173
218
|
severityRank,
|
|
174
219
|
} from "./core/types";
|
|
220
|
+
export { applyConfidence, applyJev, summarizeVerify, verifyLocal } from "./core/verify";
|
|
175
221
|
export {
|
|
176
222
|
findWatchdog,
|
|
177
223
|
getWatchdog,
|
|
@@ -91,6 +91,12 @@ const KIND_ORDER: ArtifactKind[] = [
|
|
|
91
91
|
"manifest",
|
|
92
92
|
"classification",
|
|
93
93
|
"candidates",
|
|
94
|
+
"ast",
|
|
95
|
+
"ast-impact",
|
|
96
|
+
"ai",
|
|
97
|
+
"ai-usage",
|
|
98
|
+
"ai-brief",
|
|
99
|
+
"harness-transcript",
|
|
94
100
|
"context",
|
|
95
101
|
"context-brief",
|
|
96
102
|
"context-summary",
|
|
@@ -109,11 +115,17 @@ const GROUP_BY_KIND: Record<ArtifactKind, { key: string; label: string; order: n
|
|
|
109
115
|
manifest: { key: "core", label: "Núcleo da revisão", order: 3 },
|
|
110
116
|
classification: { key: "core", label: "Núcleo da revisão", order: 3 },
|
|
111
117
|
candidates: { key: "core", label: "Núcleo da revisão", order: 3 },
|
|
112
|
-
|
|
113
|
-
"
|
|
114
|
-
|
|
115
|
-
"
|
|
116
|
-
|
|
118
|
+
ast: { key: "core", label: "Núcleo da revisão", order: 3 },
|
|
119
|
+
"ast-impact": { key: "core", label: "Núcleo da revisão", order: 3 },
|
|
120
|
+
ai: { key: "ai", label: "IA e harness", order: 4 },
|
|
121
|
+
"ai-usage": { key: "ai", label: "IA e harness", order: 4 },
|
|
122
|
+
"ai-brief": { key: "ai", label: "IA e harness", order: 4 },
|
|
123
|
+
"harness-transcript": { key: "ai", label: "IA e harness", order: 4 },
|
|
124
|
+
context: { key: "context", label: "Contexto da PR", order: 5 },
|
|
125
|
+
"context-brief": { key: "context", label: "Contexto da PR", order: 5 },
|
|
126
|
+
"context-summary": { key: "context", label: "Contexto da PR", order: 5 },
|
|
127
|
+
"finding-report": { key: "other", label: "Outros", order: 6 },
|
|
128
|
+
other: { key: "other", label: "Outros", order: 6 },
|
|
117
129
|
};
|
|
118
130
|
|
|
119
131
|
/** Rótulo curto do tipo (badge do item). */
|
|
@@ -126,6 +138,12 @@ export const ARTIFACT_KIND_LABELS: Record<ArtifactKind, string> = {
|
|
|
126
138
|
manifest: "manifesto",
|
|
127
139
|
classification: "classificação",
|
|
128
140
|
candidates: "jev",
|
|
141
|
+
ast: "ast",
|
|
142
|
+
"ast-impact": "impacto",
|
|
143
|
+
ai: "ia",
|
|
144
|
+
"ai-usage": "uso ia",
|
|
145
|
+
"ai-brief": "plano ia",
|
|
146
|
+
"harness-transcript": "transcript",
|
|
129
147
|
context: "contexto",
|
|
130
148
|
"context-brief": "brief ctx",
|
|
131
149
|
"context-summary": "resumo ctx",
|
|
@@ -159,6 +177,10 @@ export function describeArtifact(meta: ArtifactMeta): string {
|
|
|
159
177
|
return "Classificação do diff: watchdogs ativados por arquivo, status (A/M/D/R) e churn.";
|
|
160
178
|
case "candidates":
|
|
161
179
|
return "Indícios da pré-análise Jev (regra + arquivo + confiança) — não são achados.";
|
|
180
|
+
case "ast":
|
|
181
|
+
return "Fatos estruturais e delta por arquivo (símbolos, imports, exports e assinaturas).";
|
|
182
|
+
case "ast-impact":
|
|
183
|
+
return "Impacto reverso: arquivos que importam os alterados e exports públicos modificados.";
|
|
162
184
|
case "context":
|
|
163
185
|
return "Payload bruto da PR coletado via `gh` (ou marcador de indisponível).";
|
|
164
186
|
case "context-brief":
|
|
@@ -64,6 +64,12 @@ export type ArtifactKind =
|
|
|
64
64
|
| "resume"
|
|
65
65
|
| "domain-review"
|
|
66
66
|
| "verify"
|
|
67
|
+
| "ast"
|
|
68
|
+
| "ast-impact"
|
|
69
|
+
| "ai"
|
|
70
|
+
| "ai-usage"
|
|
71
|
+
| "ai-brief"
|
|
72
|
+
| "harness-transcript"
|
|
67
73
|
| "other";
|
|
68
74
|
|
|
69
75
|
/** Metadados de artefato retornados pela API (sem conteúdo). */
|
package/src/server/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
type GhRunner,
|
|
15
15
|
type SubmitSelection,
|
|
16
16
|
} from "../core/github-review";
|
|
17
|
+
import { logger } from "../core/logging";
|
|
17
18
|
import { createStorage } from "../core/storage/factory";
|
|
18
19
|
import { resolveRepoIdentity } from "../core/storage/repo-identity";
|
|
19
20
|
import type { RepoIdentity, StorageProvider } from "../core/storage/types";
|
|
@@ -190,8 +191,8 @@ export class HoundsmanServer {
|
|
|
190
191
|
try {
|
|
191
192
|
this.runs.maintain();
|
|
192
193
|
} catch (error) {
|
|
193
|
-
|
|
194
|
-
`
|
|
194
|
+
logger.warn(
|
|
195
|
+
`VACUUM falhou (seguindo sem compactar): ${
|
|
195
196
|
error instanceof Error ? error.message : String(error)
|
|
196
197
|
}`,
|
|
197
198
|
);
|
|
@@ -70,3 +70,15 @@ staging global da review:
|
|
|
70
70
|
- Um achado por regra violada; não agrupar regras diferentes.
|
|
71
71
|
- `file:line` sempre que possível; `evidence` quando o achado não é óbvio.
|
|
72
72
|
- Não inventar achados — se não houver violação, o array fica vazio.
|
|
73
|
+
|
|
74
|
+
## Findings emergentes (opt-in)
|
|
75
|
+
|
|
76
|
+
Quando o watchdog declara `allowEmergentFindings: true`, você pode reportar falhas de
|
|
77
|
+
**lógica** ou **domínio** que não correspondem a uma regra explícita usando os prefixos
|
|
78
|
+
reservados:
|
|
79
|
+
|
|
80
|
+
- `Lógica: <descrição>` — nome/contrato vs comportamento, fluxos, estados, bordas.
|
|
81
|
+
- `Domínio: <descrição>` — transições, idempotência, invariantes, arredondamento, timezone.
|
|
82
|
+
|
|
83
|
+
Esses findings continuam exigindo `evidence` e têm severidade máxima `attention`. Sem o
|
|
84
|
+
opt-in, todo finding precisa citar uma regra existente.
|
package/src/skill/SETUP.md
CHANGED
|
@@ -7,8 +7,11 @@ no seu projeto é **autocontida** — não exige nenhuma dependência local do p
|
|
|
7
7
|
|
|
8
8
|
- **Bun** — o CLI e o motor usam a runtime Bun (execução de TS, `Bun.spawnSync`).
|
|
9
9
|
- **git** — o diff usa `git diff <base>...<head> --name-status`.
|
|
10
|
-
- Opcional
|
|
11
|
-
|
|
10
|
+
- **gh** (Opcional) — Obtém contexto da PR e dispara comentários para o GitHub
|
|
11
|
+
- **`HOUNDSMAN_TYPE_SAFE_API_KEY`** (opcional) — pré-análise Jev.
|
|
12
|
+
- **`HOUNDSMAN_DEEP_SEEK_API_KEY`** (opcional) — IA própria do `hunt` nativo e das features de IA.
|
|
13
|
+
- **`HOUNDSMAN_OPENAI_API_KEY`** / **`HOUNDSMAN_ANTHROPIC_API_KEY`** (opcional) — apenas se
|
|
14
|
+
`ai.provider` for `openai`/`anthropic`.
|
|
12
15
|
|
|
13
16
|
## Instalação (global)
|
|
14
17
|
|
|
@@ -87,6 +90,7 @@ Definido em `src/core/config.ts` (pacote) e espelhado em `.houndsman/core/types.
|
|
|
87
90
|
| `alwaysIncludeDocs` | Docs sempre presentes nos briefs (default `["AGENTS.md"]`). |
|
|
88
91
|
| `budgets` | **Opcional** — teto de arquivos por brief (`maxFilesPerBrief`, default `40`); o excedente vira "fora do orçamento" (registrar em `skipped`). |
|
|
89
92
|
| `concurrency` | **Opcional** — chamadas Jev simultâneas (`jev`, default `5`). |
|
|
93
|
+
| `ast` | **Opcional** — AST determinístico para TS/JS (`enabled`, `maxFileBytes`, `maxImpactFiles`, `languages`, `aliases`). Default: ligado; falhas são fail-open. |
|
|
90
94
|
|
|
91
95
|
> O motor **não contém** ids, paths ou regras do projeto — tudo chega por `HoundsmanConfig`,
|
|
92
96
|
> carregado em runtime a partir de `houndsman.config.ts` (ou `--config <path>`).
|