@payconductor/houndsman 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/README.md +35 -41
  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/status-command.ts +46 -0
  11. package/src/cli/help.ts +18 -1
  12. package/src/cli/index.ts +13 -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 +120 -6
  42. package/src/core/commands/review.ts +2 -0
  43. package/src/core/commands/scaffold.ts +159 -10
  44. package/src/core/commands/status.ts +63 -0
  45. package/src/core/commands/update.ts +5 -3
  46. package/src/core/config.ts +85 -0
  47. package/src/core/defaults.ts +27 -0
  48. package/src/core/domain/watchdog.ts +43 -5
  49. package/src/core/engine/houndsman-engine.ts +16 -1
  50. package/src/core/engine/review-scope.ts +77 -13
  51. package/src/core/engine/review-session.ts +27 -2
  52. package/src/core/env.ts +101 -0
  53. package/src/core/file-classifier.ts +25 -4
  54. package/src/core/findings-state.ts +21 -4
  55. package/src/core/github-review.ts +620 -57
  56. package/src/core/jev/jev-client.ts +8 -7
  57. package/src/core/jev/jev-verify.ts +2 -1
  58. package/src/core/logging/index.ts +17 -0
  59. package/src/core/logging/logger.ts +187 -0
  60. package/src/core/orchestrator.ts +454 -16
  61. package/src/core/paths.ts +3 -0
  62. package/src/core/project-config.ts +123 -1
  63. package/src/core/registry-check.ts +148 -6
  64. package/src/core/renderers/manifest-writer.ts +33 -0
  65. package/src/core/renderers/resume-renderer.ts +151 -0
  66. package/src/core/report-builder.ts +7 -4
  67. package/src/core/rule-text.ts +97 -0
  68. package/src/core/run-status.ts +39 -5
  69. package/src/core/services/file-classifier.ts +10 -3
  70. package/src/core/services/verifier.ts +14 -3
  71. package/src/core/storage/file/file-storage.ts +2 -0
  72. package/src/core/storage/paths.ts +6 -5
  73. package/src/core/storage/sqlite/mappers.ts +2 -0
  74. package/src/core/storage/sqlite/schema.ts +9 -1
  75. package/src/core/storage/sqlite/sqlite-storage.ts +5 -1
  76. package/src/core/storage/types.ts +15 -0
  77. package/src/core/storage/work-area.ts +12 -0
  78. package/src/core/types.ts +37 -1
  79. package/src/core/verify.ts +17 -5
  80. package/src/harness/README.md +15 -0
  81. package/src/harness/agent/agent-context.ts +33 -0
  82. package/src/harness/agent/agent-errors.ts +30 -0
  83. package/src/harness/agent/agent-factory.ts +45 -0
  84. package/src/harness/agent/agent-reporter.ts +34 -0
  85. package/src/harness/agent/agent-result.ts +16 -0
  86. package/src/harness/agent/agent-runner.ts +32 -0
  87. package/src/harness/agent/agent-scheduler.ts +37 -0
  88. package/src/harness/agent/review-agent.ts +391 -0
  89. package/src/harness/config/harness-config.ts +84 -0
  90. package/src/harness/hunt/hunt-harness.ts +83 -0
  91. package/src/harness/hunt/hunt-options.ts +75 -0
  92. package/src/harness/hunt/hunt-result.ts +17 -0
  93. package/src/harness/hunt/hunt-session.ts +372 -0
  94. package/src/harness/hunt/review-planner.ts +98 -0
  95. package/src/harness/index.ts +36 -0
  96. package/src/harness/investigation/agent-state.ts +134 -0
  97. package/src/harness/investigation/evidence-verifier.ts +129 -0
  98. package/src/harness/investigation/evidence.ts +110 -0
  99. package/src/harness/investigation/finding-critic.ts +96 -0
  100. package/src/harness/investigation/investigation-planner.ts +118 -0
  101. package/src/harness/investigation/investigation-task.ts +10 -0
  102. package/src/harness/investigation/review-journal.ts +87 -0
  103. package/src/harness/investigation/rule-analyzer.ts +178 -0
  104. package/src/harness/investigation/rule-signals.ts +110 -0
  105. package/src/harness/prompts/agent-schemas.ts +57 -0
  106. package/src/harness/prompts/reviewer-prompt-builder.ts +101 -0
  107. package/src/harness/prompts/states/conclude-prompt.ts +16 -0
  108. package/src/harness/prompts/states/investigate-prompt.ts +37 -0
  109. package/src/harness/prompts/states/orient-prompt.ts +15 -0
  110. package/src/harness/prompts/states/verify-prompt.ts +22 -0
  111. package/src/harness/tools/agent-tool.ts +102 -0
  112. package/src/harness/tools/builtin/changed-symbols-tool.ts +48 -0
  113. package/src/harness/tools/builtin/doc-context-tool.ts +74 -0
  114. package/src/harness/tools/builtin/file-outline-tool.ts +66 -0
  115. package/src/harness/tools/builtin/find-references-tool.ts +66 -0
  116. package/src/harness/tools/builtin/find-symbol-tool.ts +91 -0
  117. package/src/harness/tools/builtin/helpers.ts +115 -0
  118. package/src/harness/tools/builtin/impact-tool.ts +48 -0
  119. package/src/harness/tools/builtin/imports-exports-tool.ts +53 -0
  120. package/src/harness/tools/builtin/index.ts +59 -0
  121. package/src/harness/tools/builtin/list-files-tool.ts +85 -0
  122. package/src/harness/tools/builtin/pr-context-tool.ts +41 -0
  123. package/src/harness/tools/builtin/read-file-range-tool.ts +73 -0
  124. package/src/harness/tools/builtin/rule-signals-tool.ts +36 -0
  125. package/src/harness/tools/builtin/search-code-tool.ts +65 -0
  126. package/src/harness/tools/builtin/symbol-context-tool.ts +106 -0
  127. package/src/harness/tools/builtin/tests-for-symbol-tool.ts +61 -0
  128. package/src/harness/tools/path-guard.ts +90 -0
  129. package/src/harness/tools/result-limiter.ts +49 -0
  130. package/src/harness/tools/secret-redactor.ts +54 -0
  131. package/src/harness/tools/tool-cache.ts +50 -0
  132. package/src/harness/tools/tool-call-budget.ts +60 -0
  133. package/src/harness/tools/tool-context.ts +5 -0
  134. package/src/harness/tools/tool-middleware.ts +105 -0
  135. package/src/harness/tools/tool-policy.ts +51 -0
  136. package/src/harness/tools/tool-registry.ts +112 -0
  137. package/src/harness/tools/tool-result.ts +15 -0
  138. package/src/harness/tools/toolbox.ts +36 -0
  139. package/src/harness/validation/finding-validator.ts +119 -0
  140. package/src/index.ts +46 -0
  141. package/src/server/client/App.tsx +26 -9
  142. package/src/server/client/ArtifactsPanel.tsx +12 -5
  143. package/src/server/client/AutoRefresh.tsx +1 -1
  144. package/src/server/client/Filters.tsx +7 -2
  145. package/src/server/client/FindingCard.tsx +14 -10
  146. package/src/server/client/FindingsList.tsx +2 -2
  147. package/src/server/client/MarkdownText.tsx +92 -12
  148. package/src/server/client/ResumePanel.tsx +368 -0
  149. package/src/server/client/RunList.tsx +3 -3
  150. package/src/server/client/SubmitModal.tsx +49 -6
  151. package/src/server/client/SummaryBar.tsx +1 -1
  152. package/src/server/client/icons.tsx +17 -0
  153. package/src/server/client/{constants.ts → utils/constants.ts} +61 -10
  154. package/src/server/client/utils/markdown.ts +319 -0
  155. package/src/server/client/{types.ts → utils/types.ts} +64 -0
  156. package/src/server/index.ts +66 -7
  157. package/src/server/run-repository.ts +9 -0
  158. package/src/skill/OUTPUT-FORMAT.md +12 -0
  159. package/src/skill/SETUP.md +6 -2
  160. package/src/skill/example-review.md +22 -14
  161. package/src/skill/hunt.md +45 -19
  162. package/src/skill/verify.md +3 -2
  163. package/src/tests/__snapshots__/baseline.test.ts.snap +126 -42
  164. package/src/tests/ai-core.test.ts +120 -0
  165. package/src/tests/ai-features.test.ts +183 -0
  166. package/src/tests/ast.test.ts +335 -0
  167. package/src/tests/baseline.test.ts +3 -0
  168. package/src/tests/code-highlight.test.ts +1 -1
  169. package/src/tests/env.test.ts +69 -0
  170. package/src/tests/fallback.test.ts +172 -0
  171. package/src/tests/findings-state.test.ts +50 -2
  172. package/src/tests/fixtures/fake-agent-tools.ts +69 -0
  173. package/src/tests/fixtures/fake-investigation.ts +64 -0
  174. package/src/tests/fixtures/fake-model-provider.ts +48 -0
  175. package/src/tests/github-review.test.ts +409 -60
  176. package/src/tests/harness-agent.test.ts +122 -0
  177. package/src/tests/harness-investigation.test.ts +206 -0
  178. package/src/tests/harness-tools.test.ts +148 -0
  179. package/src/tests/hunt-command.test.ts +212 -0
  180. package/src/tests/logger.test.ts +83 -0
  181. package/src/tests/markdown.test.ts +61 -1
  182. package/src/tests/review-resume.test.ts +101 -0
  183. package/src/tests/review-scope.test.ts +302 -0
  184. package/src/tests/review-scripts.test.ts +73 -0
  185. package/src/tests/rule-analyzer.test.ts +61 -0
  186. package/src/tests/run-status-view.test.ts +94 -0
  187. package/src/tests/run-status.test.ts +53 -0
  188. package/src/tests/server.test.ts +76 -21
  189. package/src/tests/storage.test.ts +63 -0
  190. package/src/tests/tool-contracts.test.ts +82 -0
  191. package/src/tests/verify.test.ts +1 -1
  192. package/src/server/client/markdown.ts +0 -153
  193. /package/src/server/client/{api.ts → utils/api.ts} +0 -0
  194. /package/src/server/client/{code-highlight.ts → utils/code-highlight.ts} +0 -0
  195. /package/src/server/client/{format.ts → utils/format.ts} +0 -0
  196. /package/src/server/client/{json-highlight.ts → utils/json-highlight.ts} +0 -0
@@ -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,
@@ -1,25 +1,31 @@
1
1
  /**
2
2
  * App: layout do servidor — sidebar de execuções, header fixo (abas segmentadas,
3
- * auto-refresh e reancoragem), KPIs, filtros e os painéis de findings/artefatos.
4
- * Concentra o estado e as chamadas à API.
3
+ * auto-refresh e reancoragem), KPIs, filtros e os painéis de resumo/findings/
4
+ * artefatos. Concentra o estado e as chamadas à API.
5
5
  */
6
6
 
7
7
  import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
8
8
  import { ArtifactsPanel } from "./ArtifactsPanel";
9
9
  import { AutoRefresh } from "./AutoRefresh";
10
- import { api, computeSummary } from "./api";
11
- import { AUTO_REFRESH_MS, TAB_CAPTIONS } from "./constants";
12
10
  import { Filters, type FiltersState } from "./Filters";
13
11
  import { FindingsList } from "./FindingsList";
14
- import { Layers, ListIcon, Refresh, Send } from "./icons";
12
+ import { FileText, Layers, ListIcon, Refresh, Send } from "./icons";
13
+ import { ResumePanel } from "./ResumePanel";
15
14
  import { RunList } from "./RunList";
16
15
  import { SubmitModal } from "./SubmitModal";
17
16
  import { SummaryBar } from "./SummaryBar";
18
- import type { Finding, FindingsResponse, RunSummary } from "./types";
17
+ import { api, computeSummary } from "./utils/api";
18
+ import { AUTO_REFRESH_MS, TAB_CAPTIONS } from "./utils/constants";
19
+ import type { Finding, FindingsResponse, RunSummary } from "./utils/types";
19
20
 
20
- type Tab = "findings" | "artifacts";
21
+ type Tab = "resume" | "findings" | "artifacts";
21
22
 
22
23
  const TABS: { id: Tab; label: string; hint: string }[] = [
24
+ {
25
+ id: "resume",
26
+ label: "Resumo",
27
+ hint: "Visão principal da revisão: negócio, números e pontos técnicos",
28
+ },
23
29
  {
24
30
  id: "findings",
25
31
  label: "Findings",
@@ -36,7 +42,7 @@ export function App() {
36
42
  const [runs, setRuns] = useState<RunSummary[]>([]);
37
43
  const [slug, setSlug] = useState<string | null>(null);
38
44
  const [data, setData] = useState<FindingsResponse | null>(null);
39
- const [tab, setTab] = useState<Tab>("findings");
45
+ const [tab, setTab] = useState<Tab>("resume");
40
46
  const [filters, setFilters] = useState<FiltersState>({
41
47
  query: "",
42
48
  decision: "all",
@@ -200,7 +206,9 @@ export function App() {
200
206
  class={`seg-btn${tab === item.id ? " active" : ""}`}
201
207
  onClick={() => setTab(item.id)}
202
208
  >
203
- {item.id === "findings" ? (
209
+ {item.id === "resume" ? (
210
+ <FileText size={15} />
211
+ ) : item.id === "findings" ? (
204
212
  <ListIcon size={15} />
205
213
  ) : (
206
214
  <Layers size={15} />
@@ -254,6 +262,15 @@ export function App() {
254
262
  </div>
255
263
  {error && <p class="error">{error}</p>}
256
264
 
265
+ {slug && tab === "resume" && data && (
266
+ <ResumePanel
267
+ slug={slug}
268
+ branch={branch}
269
+ findings={data.findings}
270
+ summary={summary}
271
+ onOpenArtifacts={() => setTab("artifacts")}
272
+ />
273
+ )}
257
274
  {slug && tab === "findings" && data && (
258
275
  <>
259
276
  <SummaryBar summary={summary} />
@@ -1,20 +1,23 @@
1
1
  /**
2
2
  * Painel de artefatos: lista todos os artefatos persistidos da run e renderiza
3
- * o conteúdo bruto. JSON ganha destaque de sintaxe; briefs e relatórios são
3
+ * o conteúdo bruto. JSON ganha destaque de sintaxe; markdown é renderizado com
4
+ * o mesmo parser/highlight do restante da UI. Briefs e relatórios ficam
4
5
  * auditáveis pela mesma navegação.
5
6
  */
6
7
 
7
8
  import { useEffect, useMemo, useState } from "preact/hooks";
8
- import { api } from "./api";
9
+ import { MarkdownText } from "./MarkdownText";
10
+ import { api } from "./utils/api";
9
11
  import {
10
12
  ARTIFACT_KIND_LABELS,
11
13
  describeArtifact,
12
14
  displayArtifactName,
13
15
  groupArtifacts,
14
16
  isJsonArtifact,
15
- } from "./constants";
16
- import { formatJson, highlightJson } from "./json-highlight";
17
- import type { ArtifactMeta } from "./types";
17
+ isMarkdownArtifact,
18
+ } from "./utils/constants";
19
+ import { formatJson, highlightJson } from "./utils/json-highlight";
20
+ import type { ArtifactMeta } from "./utils/types";
18
21
 
19
22
  /** Prefere o relatório consolidado; senão, o primeiro artefato disponível. */
20
23
  function pickDefault(items: ArtifactMeta[]): string {
@@ -149,6 +152,10 @@ export function ArtifactsPanel({ slug }: { slug: string }) {
149
152
  ) : selected ? (
150
153
  isJsonArtifact(selected) ? (
151
154
  <JsonView content={content} />
155
+ ) : isMarkdownArtifact(selected) ? (
156
+ <div class="artifact-view artifact-markdown">
157
+ <MarkdownText text={content} />
158
+ </div>
152
159
  ) : (
153
160
  <pre class="artifact-view">{content || "—"}</pre>
154
161
  )
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import { useEffect, useState } from "preact/hooks";
9
- import { formatElapsed } from "./format";
9
+ import { formatElapsed } from "./utils/format";
10
10
 
11
11
  export function AutoRefresh({
12
12
  lastSync,
@@ -3,9 +3,14 @@
3
3
  * o toggle "ausentes".
4
4
  */
5
5
 
6
- import { DECISION_LABELS, DECISIONS, SEVERITY_LABELS, SEVERITY_ORDER } from "./constants";
7
6
  import { Search, Watch } from "./icons";
8
- import type { Decision, Severity } from "./types";
7
+ import {
8
+ DECISION_LABELS,
9
+ DECISIONS,
10
+ SEVERITY_LABELS,
11
+ SEVERITY_ORDER,
12
+ } from "./utils/constants";
13
+ import type { Decision, Severity } from "./utils/types";
9
14
 
10
15
  export type FiltersState = {
11
16
  query: string;