@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.
Files changed (153) hide show
  1. package/README.md +24 -4
  2. package/assets/grammars/tree-sitter-javascript.wasm +0 -0
  3. package/assets/grammars/tree-sitter-tsx.wasm +0 -0
  4. package/assets/grammars/tree-sitter-typescript.wasm +0 -0
  5. package/assets/grammars/web-tree-sitter.wasm +0 -0
  6. package/package.json +11 -3
  7. package/src/cli/commands/command.ts +5 -0
  8. package/src/cli/commands/doctor-command.ts +116 -24
  9. package/src/cli/commands/hunt-command.ts +25 -0
  10. package/src/cli/help.ts +17 -1
  11. package/src/cli/index.ts +10 -6
  12. package/src/core/ai/ai-client.ts +221 -0
  13. package/src/core/ai/ai-sdk-model-provider.ts +101 -0
  14. package/src/core/ai/errors.ts +36 -0
  15. package/src/core/ai/features/brief-enricher.ts +62 -0
  16. package/src/core/ai/features/classifier.ts +50 -0
  17. package/src/core/ai/features/context-summarizer.ts +31 -0
  18. package/src/core/ai/features/pre-analyzer.ts +51 -0
  19. package/src/core/ai/features/verifier.ts +45 -0
  20. package/src/core/ai/index.ts +56 -0
  21. package/src/core/ai/model-factory.ts +137 -0
  22. package/src/core/ai/prompts/base.ts +36 -0
  23. package/src/core/ai/prompts/brief.ts +87 -0
  24. package/src/core/ai/prompts/classification.ts +36 -0
  25. package/src/core/ai/prompts/context.ts +29 -0
  26. package/src/core/ai/prompts/pre-analysis.ts +49 -0
  27. package/src/core/ai/prompts/verify.ts +40 -0
  28. package/src/core/ai/schemas.ts +110 -0
  29. package/src/core/ai/types.ts +76 -0
  30. package/src/core/ai/usage-budget.ts +75 -0
  31. package/src/core/ai/usage-recorder.ts +79 -0
  32. package/src/core/assets.ts +16 -0
  33. package/src/core/ast/diff.ts +220 -0
  34. package/src/core/ast/impact.ts +127 -0
  35. package/src/core/ast/parser.ts +167 -0
  36. package/src/core/ast/service.ts +233 -0
  37. package/src/core/ast/symbols.ts +294 -0
  38. package/src/core/ast/types.ts +125 -0
  39. package/src/core/brief.ts +138 -0
  40. package/src/core/commands/findings.ts +59 -1
  41. package/src/core/commands/review.ts +2 -0
  42. package/src/core/commands/scaffold.ts +159 -10
  43. package/src/core/commands/update.ts +5 -3
  44. package/src/core/config.ts +85 -0
  45. package/src/core/defaults.ts +27 -0
  46. package/src/core/domain/watchdog.ts +43 -5
  47. package/src/core/engine/houndsman-engine.ts +16 -1
  48. package/src/core/engine/review-scope.ts +52 -6
  49. package/src/core/engine/review-session.ts +27 -2
  50. package/src/core/env.ts +101 -0
  51. package/src/core/file-classifier.ts +25 -4
  52. package/src/core/jev/jev-client.ts +8 -7
  53. package/src/core/jev/jev-verify.ts +2 -1
  54. package/src/core/logging/index.ts +17 -0
  55. package/src/core/logging/logger.ts +187 -0
  56. package/src/core/orchestrator.ts +357 -9
  57. package/src/core/project-config.ts +123 -1
  58. package/src/core/registry-check.ts +148 -6
  59. package/src/core/renderers/manifest-writer.ts +33 -0
  60. package/src/core/rule-text.ts +97 -0
  61. package/src/core/services/file-classifier.ts +10 -3
  62. package/src/core/services/verifier.ts +14 -3
  63. package/src/core/storage/paths.ts +6 -5
  64. package/src/core/storage/types.ts +6 -0
  65. package/src/core/storage/work-area.ts +12 -0
  66. package/src/core/types.ts +37 -1
  67. package/src/core/verify.ts +17 -5
  68. package/src/harness/README.md +15 -0
  69. package/src/harness/agent/agent-context.ts +33 -0
  70. package/src/harness/agent/agent-errors.ts +30 -0
  71. package/src/harness/agent/agent-factory.ts +45 -0
  72. package/src/harness/agent/agent-reporter.ts +34 -0
  73. package/src/harness/agent/agent-result.ts +16 -0
  74. package/src/harness/agent/agent-runner.ts +32 -0
  75. package/src/harness/agent/agent-scheduler.ts +37 -0
  76. package/src/harness/agent/review-agent.ts +391 -0
  77. package/src/harness/config/harness-config.ts +84 -0
  78. package/src/harness/hunt/hunt-harness.ts +83 -0
  79. package/src/harness/hunt/hunt-options.ts +75 -0
  80. package/src/harness/hunt/hunt-result.ts +17 -0
  81. package/src/harness/hunt/hunt-session.ts +372 -0
  82. package/src/harness/hunt/review-planner.ts +98 -0
  83. package/src/harness/index.ts +36 -0
  84. package/src/harness/investigation/agent-state.ts +134 -0
  85. package/src/harness/investigation/evidence-verifier.ts +129 -0
  86. package/src/harness/investigation/evidence.ts +110 -0
  87. package/src/harness/investigation/finding-critic.ts +96 -0
  88. package/src/harness/investigation/investigation-planner.ts +118 -0
  89. package/src/harness/investigation/investigation-task.ts +10 -0
  90. package/src/harness/investigation/review-journal.ts +87 -0
  91. package/src/harness/investigation/rule-analyzer.ts +178 -0
  92. package/src/harness/investigation/rule-signals.ts +110 -0
  93. package/src/harness/prompts/agent-schemas.ts +57 -0
  94. package/src/harness/prompts/reviewer-prompt-builder.ts +101 -0
  95. package/src/harness/prompts/states/conclude-prompt.ts +16 -0
  96. package/src/harness/prompts/states/investigate-prompt.ts +37 -0
  97. package/src/harness/prompts/states/orient-prompt.ts +15 -0
  98. package/src/harness/prompts/states/verify-prompt.ts +22 -0
  99. package/src/harness/tools/agent-tool.ts +102 -0
  100. package/src/harness/tools/builtin/changed-symbols-tool.ts +48 -0
  101. package/src/harness/tools/builtin/doc-context-tool.ts +74 -0
  102. package/src/harness/tools/builtin/file-outline-tool.ts +66 -0
  103. package/src/harness/tools/builtin/find-references-tool.ts +66 -0
  104. package/src/harness/tools/builtin/find-symbol-tool.ts +91 -0
  105. package/src/harness/tools/builtin/helpers.ts +115 -0
  106. package/src/harness/tools/builtin/impact-tool.ts +48 -0
  107. package/src/harness/tools/builtin/imports-exports-tool.ts +53 -0
  108. package/src/harness/tools/builtin/index.ts +59 -0
  109. package/src/harness/tools/builtin/list-files-tool.ts +85 -0
  110. package/src/harness/tools/builtin/pr-context-tool.ts +41 -0
  111. package/src/harness/tools/builtin/read-file-range-tool.ts +73 -0
  112. package/src/harness/tools/builtin/rule-signals-tool.ts +36 -0
  113. package/src/harness/tools/builtin/search-code-tool.ts +65 -0
  114. package/src/harness/tools/builtin/symbol-context-tool.ts +106 -0
  115. package/src/harness/tools/builtin/tests-for-symbol-tool.ts +61 -0
  116. package/src/harness/tools/path-guard.ts +90 -0
  117. package/src/harness/tools/result-limiter.ts +49 -0
  118. package/src/harness/tools/secret-redactor.ts +54 -0
  119. package/src/harness/tools/tool-cache.ts +50 -0
  120. package/src/harness/tools/tool-call-budget.ts +60 -0
  121. package/src/harness/tools/tool-context.ts +5 -0
  122. package/src/harness/tools/tool-middleware.ts +105 -0
  123. package/src/harness/tools/tool-policy.ts +51 -0
  124. package/src/harness/tools/tool-registry.ts +112 -0
  125. package/src/harness/tools/tool-result.ts +15 -0
  126. package/src/harness/tools/toolbox.ts +36 -0
  127. package/src/harness/validation/finding-validator.ts +119 -0
  128. package/src/index.ts +46 -0
  129. package/src/server/client/utils/constants.ts +27 -5
  130. package/src/server/client/utils/types.ts +6 -0
  131. package/src/server/index.ts +3 -2
  132. package/src/skill/OUTPUT-FORMAT.md +12 -0
  133. package/src/skill/SETUP.md +6 -2
  134. package/src/skill/hunt.md +14 -2
  135. package/src/skill/verify.md +1 -1
  136. package/src/tests/__snapshots__/baseline.test.ts.snap +96 -14
  137. package/src/tests/ai-core.test.ts +120 -0
  138. package/src/tests/ai-features.test.ts +183 -0
  139. package/src/tests/ast.test.ts +335 -0
  140. package/src/tests/env.test.ts +69 -0
  141. package/src/tests/fallback.test.ts +172 -0
  142. package/src/tests/fixtures/fake-agent-tools.ts +69 -0
  143. package/src/tests/fixtures/fake-investigation.ts +64 -0
  144. package/src/tests/fixtures/fake-model-provider.ts +48 -0
  145. package/src/tests/harness-agent.test.ts +122 -0
  146. package/src/tests/harness-investigation.test.ts +206 -0
  147. package/src/tests/harness-tools.test.ts +148 -0
  148. package/src/tests/hunt-command.test.ts +212 -0
  149. package/src/tests/logger.test.ts +83 -0
  150. package/src/tests/review-scope.test.ts +11 -11
  151. package/src/tests/rule-analyzer.test.ts +61 -0
  152. package/src/tests/tool-contracts.test.ts +82 -0
  153. package/src/tests/verify.test.ts +1 -1
@@ -0,0 +1,372 @@
1
+ /**
2
+ * Sessão do hunt nativo: prepare → agentes → finalize.
3
+ *
4
+ * Reutiliza o pipeline determinístico (briefs, classificação, storage) e apenas
5
+ * substitui o "despacho de subagentes" pelo harness.
6
+ */
7
+
8
+ import { existsSync, readFileSync } from "node:fs";
9
+ import {
10
+ type AiUsage,
11
+ type AiUsageRecorder,
12
+ addUsage,
13
+ emptyUsage,
14
+ type ModelProvider,
15
+ } from "../../core/ai";
16
+ import type { AstAnalysis } from "../../core/ast/types";
17
+ import { collectDiff } from "../../core/diff-source";
18
+ import type { HoundsmanEngine } from "../../core/engine/houndsman-engine";
19
+ import type { RunClassification } from "../../core/engine/review-scope";
20
+ import { parseEntries } from "../../core/file-classifier";
21
+ import { runGit, tryGit } from "../../core/git-runner";
22
+ import type { LoggerLike } from "../../core/logging";
23
+ import type { PrepareResult } from "../../core/orchestrator";
24
+ import { defaultOptions, type ReviewOptions } from "../../core/orchestrator";
25
+ import type { ContextSummary } from "../../core/report-builder";
26
+ import type { AgentContext } from "../agent/agent-context";
27
+ import { AgentFactory } from "../agent/agent-factory";
28
+ import type { AgentReporter } from "../agent/agent-reporter";
29
+ import type { AgentResult } from "../agent/agent-result";
30
+ import { AgentRunner } from "../agent/agent-runner";
31
+ import { AgentScheduler } from "../agent/agent-scheduler";
32
+ import type { HarnessConfig } from "../config/harness-config";
33
+ import { BuiltinToolsModule } from "../tools/builtin";
34
+ import { ToolRegistry } from "../tools/tool-registry";
35
+ import { defaultFileReader } from "../validation/finding-validator";
36
+ import type { HuntOptions } from "./hunt-options";
37
+ import type { HuntResult } from "./hunt-result";
38
+ import { ReviewPlanner } from "./review-planner";
39
+
40
+ /** Dependências da sessão. */
41
+ export type HuntSessionDeps = {
42
+ engine: HoundsmanEngine;
43
+ options: HuntOptions;
44
+ config: HarnessConfig;
45
+ reporter: AgentReporter;
46
+ logger: LoggerLike;
47
+ provider?: ModelProvider;
48
+ recorder: AiUsageRecorder;
49
+ };
50
+
51
+ /** Executa o hunt. */
52
+ export class HuntSession {
53
+ constructor(private readonly deps: HuntSessionDeps) {}
54
+
55
+ /** Roda prepare (seco ou não), agentes e finalize. */
56
+ async run(): Promise<HuntResult> {
57
+ const { engine, options, reporter } = this.deps;
58
+ const messages: string[] = [];
59
+ const reviewOptions: ReviewOptions = {
60
+ ...defaultOptions(),
61
+ base: options.base,
62
+ head: options.head,
63
+ branch: options.branch,
64
+ noJev: options.noJev,
65
+ noAst: options.noAst,
66
+ full: options.full,
67
+ clean: options.clean,
68
+ noAi: true,
69
+ finalize: false,
70
+ };
71
+
72
+ const session = engine.session(reviewOptions);
73
+ const prepared: PrepareResult = await this.deps.logger.withSpinner(
74
+ "Preparando revisão...",
75
+ () => session.prepare(),
76
+ );
77
+ messages.push(...prepared.messages);
78
+ const review = session.context.review;
79
+ const work = session.context.work;
80
+
81
+ // Sem alterações desde a última revisão: não gasta IA nem reescreve nada.
82
+ const artifact = engine.storage.readArtifact(review.id, "classification.json");
83
+ if (
84
+ !artifact ||
85
+ prepared.messages.some((item) => item.startsWith("Nenhuma alteração"))
86
+ ) {
87
+ return {
88
+ status: "completed",
89
+ reviewId: review.id,
90
+ blockers: 0,
91
+ attention: 0,
92
+ watchdogs: [],
93
+ usage: emptyUsage(),
94
+ messages,
95
+ };
96
+ }
97
+
98
+ const classification = this.readClassification(engine, review.id);
99
+ const planned = await this.plan(classification, session.context.review, work);
100
+ for (const item of planned) {
101
+ item.context.outputPath = work.findingFile(item.context.watchdog.id);
102
+ }
103
+
104
+ if (options.dryRun) {
105
+ messages.push(
106
+ `Dry-run: ${planned.length} watchdog(s) sem chamadas de IA.`,
107
+ ...planned.map(
108
+ (item) =>
109
+ `- ${item.context.watchdog.id}: ${item.files.length} arquivo(s)` +
110
+ (item.overflow.length > 0
111
+ ? `, ${item.overflow.length} fora do orçamento`
112
+ : ""),
113
+ ),
114
+ );
115
+ return {
116
+ status: "dry-run",
117
+ reviewId: review.id,
118
+ blockers: 0,
119
+ attention: 0,
120
+ watchdogs: [],
121
+ usage: emptyUsage(),
122
+ messages,
123
+ };
124
+ }
125
+
126
+ if (!this.deps.provider) {
127
+ throw new Error(
128
+ "hunt exige IA configurada (HOUNDSMAN_DEEP_SEEK_API_KEY) ou provider injetado.",
129
+ );
130
+ }
131
+
132
+ const registry = new ToolRegistry();
133
+ registry.registerModule(new BuiltinToolsModule());
134
+ const factory = new AgentFactory({
135
+ provider: this.deps.provider,
136
+ registry,
137
+ reporter,
138
+ config: this.deps.config,
139
+ ast: engine.ast,
140
+ readFile: defaultFileReader(fileRoot()),
141
+ });
142
+ const runner = new AgentRunner(factory);
143
+ const scheduler = new AgentScheduler(
144
+ runner,
145
+ this.deps.config.concurrency,
146
+ reporter,
147
+ );
148
+
149
+ // `--resume`: watchdogs com findings válidos no staging são preservados.
150
+ const pending = options.resume
151
+ ? planned.filter(
152
+ (item) =>
153
+ !hasValidFindings(work.findingFile(item.context.watchdog.id)),
154
+ )
155
+ : planned;
156
+ if (pending.length < planned.length) {
157
+ messages.push(
158
+ `Resume: ${planned.length - pending.length} watchdog(s) preservado(s) do staging.`,
159
+ );
160
+ }
161
+
162
+ const contexts: AgentContext[] = pending.map((item) => item.context);
163
+ const outcomes = await scheduler.run(contexts);
164
+ const results: AgentResult[] = [];
165
+ let partial = false;
166
+ let failed = false;
167
+
168
+ for (const [index, outcome] of outcomes.entries()) {
169
+ const context = contexts[index];
170
+ if (!context) continue;
171
+ results.push(outcome);
172
+ this.deps.recorder.record(
173
+ "harness",
174
+ this.deps.provider.modelId,
175
+ outcome.usage,
176
+ );
177
+ if (outcome.status === "done" && outcome.report) {
178
+ work.writeFinding(
179
+ context.watchdog.id,
180
+ `${JSON.stringify(outcome.report, null, 2)}\n`,
181
+ );
182
+ messages.push(
183
+ `[${context.watchdog.id}] ${outcome.report.findings.length} achado(s) em ${context.files.length} arquivo(s).`,
184
+ );
185
+ continue;
186
+ }
187
+ const reason = outcome.error ?? outcome.reason ?? "falha desconhecida";
188
+ if (options.allowPartial) {
189
+ partial = true;
190
+ const synthetic = {
191
+ watchdogId: context.watchdog.id,
192
+ reviewedFiles: [],
193
+ skipped: [`falha: ${reason}`],
194
+ findings: [],
195
+ };
196
+ work.writeFinding(
197
+ context.watchdog.id,
198
+ `${JSON.stringify(synthetic, null, 2)}\n`,
199
+ );
200
+ messages.push(`[${context.watchdog.id}] parcial: ${reason}`);
201
+ } else {
202
+ failed = true;
203
+ messages.push(`[${context.watchdog.id}] falhou: ${reason}`);
204
+ }
205
+ }
206
+
207
+ for (const item of planned) {
208
+ if (pending.some((entry) => entry.context === item.context)) continue;
209
+ results.push({
210
+ watchdogId: item.context.watchdog.id,
211
+ status: "skipped",
212
+ reason: "resume",
213
+ usage: emptyUsage(),
214
+ });
215
+ }
216
+
217
+ this.writeTranscript(engine, review.id, classification, results);
218
+ this.deps.recorder.write(engine.storage, review.id);
219
+
220
+ const usage = totalUsage(results);
221
+ if (failed) {
222
+ return {
223
+ status: "failed",
224
+ reviewId: review.id,
225
+ blockers: 0,
226
+ attention: 0,
227
+ watchdogs: results,
228
+ usage,
229
+ messages,
230
+ };
231
+ }
232
+
233
+ const finalized = await this.deps.logger.withSpinner(
234
+ "Consolidando relatório...",
235
+ async () => engine.session({ ...reviewOptions, finalize: true }).finalize(),
236
+ );
237
+ messages.push(...finalized.messages);
238
+ return {
239
+ status: partial ? "partial" : "completed",
240
+ reviewId: review.id,
241
+ blockers: finalized.blockers,
242
+ attention: finalized.attention,
243
+ watchdogs: results,
244
+ usage,
245
+ messages,
246
+ };
247
+ }
248
+
249
+ /** Lê a classificação persistida pelo prepare. */
250
+ private readClassification(
251
+ engine: HoundsmanEngine,
252
+ reviewId: string,
253
+ ): RunClassification {
254
+ const artifact = engine.storage.readArtifact(reviewId, "classification.json");
255
+ if (!artifact) {
256
+ throw new Error("hunt: classification.json ausente após o prepare.");
257
+ }
258
+ return JSON.parse(artifact.content) as RunClassification;
259
+ }
260
+
261
+ /** Monta os contextos + AST + impacto. */
262
+ private async plan(
263
+ classification: RunClassification,
264
+ review: import("../../core/storage/types").ReviewRecord,
265
+ work: import("../../core/storage/work-area").ReviewWorkArea,
266
+ ) {
267
+ const { engine, options } = this.deps;
268
+ const diff = collectDiff(options.base, options.head);
269
+ const entries = parseEntries(
270
+ runGit(["diff", `${options.base}...${options.head}`, "--name-status"]),
271
+ );
272
+ const baseRef = mergeBase(options.base, options.head) ?? options.base;
273
+ const ast: Map<string, AstAnalysis> = options.noAst
274
+ ? new Map()
275
+ : await engine.ast.analyze(entries, diff, baseRef);
276
+ const impact = ast.size > 0 ? await engine.ast.impact(ast) : undefined;
277
+ const context =
278
+ work.readContextSummary() ?? readContextSummary(engine, review.id);
279
+ const planner = new ReviewPlanner({
280
+ config: engine.config,
281
+ project: engine.project,
282
+ classification,
283
+ context,
284
+ ast,
285
+ impact,
286
+ repoRoot: process.cwd(),
287
+ diff,
288
+ branch: review.branch,
289
+ watchdog: options.watchdog,
290
+ maxCalls: this.deps.config.maxCalls,
291
+ });
292
+ return planner.plan();
293
+ }
294
+
295
+ /** Grava o transcript completo da investigação. */
296
+ private writeTranscript(
297
+ engine: HoundsmanEngine,
298
+ reviewId: string,
299
+ classification: RunClassification,
300
+ results: AgentResult[],
301
+ ): void {
302
+ const content = `${JSON.stringify(
303
+ {
304
+ provider: this.deps.config.provider,
305
+ model: this.deps.config.model,
306
+ classification: {
307
+ mode: classification.mode,
308
+ files: Object.keys(classification.files).length,
309
+ watchdogs: Object.keys(classification.watchdogs),
310
+ },
311
+ watchdogs: results.map((result) => ({
312
+ watchdogId: result.watchdogId,
313
+ status: result.status,
314
+ usage: result.usage,
315
+ ...(result.error ? { error: result.error } : {}),
316
+ ...(result.reason ? { reason: result.reason } : {}),
317
+ })),
318
+ journal: this.deps.reporter.journal.toJSON(),
319
+ },
320
+ null,
321
+ 2,
322
+ )}\n`;
323
+ engine.storage.writeArtifact(reviewId, {
324
+ name: "harness-transcript.json",
325
+ kind: "harness-transcript",
326
+ content,
327
+ });
328
+ }
329
+ }
330
+
331
+ /** Findings válidos já gravados (resume). */
332
+ function hasValidFindings(path: string): boolean {
333
+ if (!existsSync(path)) return false;
334
+ try {
335
+ const parsed = JSON.parse(readFileSync(path, "utf8")) as { findings?: unknown };
336
+ return Array.isArray(parsed.findings);
337
+ } catch {
338
+ return false;
339
+ }
340
+ }
341
+
342
+ /** Soma o uso de todos os agentes. */
343
+ function totalUsage(results: readonly AgentResult[]): AiUsage {
344
+ return results.reduce((acc, result) => addUsage(acc, result.usage), emptyUsage());
345
+ }
346
+
347
+ /** Resumo do contexto persistido (quando o staging já foi limpo). */
348
+ function readContextSummary(
349
+ engine: HoundsmanEngine,
350
+ reviewId: string,
351
+ ): ContextSummary | null {
352
+ const artifact = engine.storage.readArtifact(reviewId, "context-summary.json");
353
+ if (!artifact) return null;
354
+ try {
355
+ return JSON.parse(artifact.content) as ContextSummary;
356
+ } catch {
357
+ return null;
358
+ }
359
+ }
360
+
361
+ /** Merge-base best effort. */
362
+ function mergeBase(base: string, head: string): string | null {
363
+ const result = tryGit(["merge-base", base, head]);
364
+ if (!result.ok) return null;
365
+ const sha = result.stdout.trim();
366
+ return sha.length > 0 ? sha : null;
367
+ }
368
+
369
+ /** Raiz do repositório para leitura das tools. */
370
+ function fileRoot(): string {
371
+ return process.cwd();
372
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Planejador da review nativa — monta um `AgentContext` por watchdog ativo.
3
+ *
4
+ * Lê a classificação já preparada (prepare) e aplica o orçamento por brief.
5
+ * Nada aqui chama IA.
6
+ */
7
+
8
+ import type { AstAnalysis, AstImpact } from "../../core/ast/types";
9
+ import type { HoundsmanConfig } from "../../core/config";
10
+ import type { DiffSource } from "../../core/diff-source";
11
+ import { discoverDocs } from "../../core/doc-discoverer";
12
+ import { selectByBudget } from "../../core/domain/budget";
13
+ import type { RunClassification } from "../../core/engine/review-scope";
14
+ import type { ProjectConfig } from "../../core/project-config";
15
+ import type { ContextSummary } from "../../core/report-builder";
16
+ import type { ChurnMap } from "../../core/types";
17
+ import type { AgentContext } from "../agent/agent-context";
18
+ import { RuleAnalyzer } from "../investigation/rule-analyzer";
19
+
20
+ /** Entrada do planejador. */
21
+ export type ReviewPlannerInput = {
22
+ config: HoundsmanConfig;
23
+ project: ProjectConfig;
24
+ classification: RunClassification;
25
+ context: ContextSummary | null;
26
+ ast: ReadonlyMap<string, AstAnalysis>;
27
+ impact?: AstImpact;
28
+ repoRoot: string;
29
+ diff: DiffSource;
30
+ /** Branch alvo da review (prompt do agente). */
31
+ branch: string;
32
+ /** Filtra um único watchdog (`--watchdog`). */
33
+ watchdog?: string;
34
+ maxCalls: number;
35
+ };
36
+
37
+ /** Um watchdog planejado. */
38
+ export type PlannedWatchdog = {
39
+ context: AgentContext;
40
+ files: string[];
41
+ overflow: string[];
42
+ };
43
+
44
+ /** Planeja os agentes da review. */
45
+ export class ReviewPlanner {
46
+ private readonly input: ReviewPlannerInput;
47
+
48
+ constructor(input: ReviewPlannerInput) {
49
+ this.input = input;
50
+ }
51
+
52
+ /** Watchdogs ativos (filtrados) com contexto pronto. */
53
+ plan(): PlannedWatchdog[] {
54
+ const { config, project, classification, watchdog } = this.input;
55
+ const ids = Object.keys(classification.watchdogs).filter(
56
+ (id) => !watchdog || id === watchdog,
57
+ );
58
+ const docs = discoverDocs(Object.keys(classification.files), config);
59
+ const maxFiles = project.maxFilesPerBrief;
60
+ const analyzer = new RuleAnalyzer();
61
+ const planned: PlannedWatchdog[] = [];
62
+
63
+ for (const id of ids) {
64
+ const domain = project.watchdogs.find(id);
65
+ if (!domain) continue;
66
+ const churn: ChurnMap = classification.churn;
67
+ const { selected, overflow } = selectByBudget(
68
+ classification.watchdogs[id] ?? [],
69
+ churn,
70
+ maxFiles,
71
+ );
72
+ const rules = analyzer.analyze(domain.rules);
73
+ planned.push({
74
+ files: selected,
75
+ overflow,
76
+ context: {
77
+ watchdog: domain,
78
+ projectName: project.projectName,
79
+ branch: this.input.branch,
80
+ files: selected,
81
+ overflow,
82
+ deleted: classification.deleted,
83
+ docs,
84
+ context: this.input.context,
85
+ ast: this.input.ast,
86
+ impact: this.input.impact,
87
+ outputPath: "",
88
+ maxSteps: project.aiMaxStepsFor(id),
89
+ maxCalls: this.input.maxCalls,
90
+ repoRoot: this.input.repoRoot,
91
+ diff: this.input.diff,
92
+ rules,
93
+ },
94
+ });
95
+ }
96
+ return planned;
97
+ }
98
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Barrel do harness nativo.
3
+ *
4
+ * Atenção: `src/skill/` é o harness **externo** (markdown). Este é o runtime de
5
+ * agentes do próprio houndsman.
6
+ */
7
+
8
+ export type { AgentContext } from "./agent/agent-context";
9
+ export { AgentFactory } from "./agent/agent-factory";
10
+ export type { AgentReporter } from "./agent/agent-reporter";
11
+ export { LoggerReporter, SilentReporter } from "./agent/agent-reporter";
12
+ export type { AgentResult } from "./agent/agent-result";
13
+ export { AgentRunner } from "./agent/agent-runner";
14
+ export { AgentScheduler } from "./agent/agent-scheduler";
15
+ export { ReviewAgent } from "./agent/review-agent";
16
+ export { HarnessConfig, type HarnessOverrides } from "./config/harness-config";
17
+ export { HuntHarness } from "./hunt/hunt-harness";
18
+ export { defaultHuntOptions, type HuntOptions, parseHuntArgs } from "./hunt/hunt-options";
19
+ export type { HuntResult } from "./hunt/hunt-result";
20
+ export { AgentState } from "./investigation/agent-state";
21
+ export type {
22
+ CriticDecision,
23
+ Evidence,
24
+ FindingDraft,
25
+ RuleCoverage,
26
+ } from "./investigation/evidence";
27
+ export { EvidenceVerifier } from "./investigation/evidence-verifier";
28
+ export { FindingCritic } from "./investigation/finding-critic";
29
+ export { InvestigationPlanner } from "./investigation/investigation-planner";
30
+ export { ReviewJournal } from "./investigation/review-journal";
31
+ export { type RuleAnalysis, RuleAnalyzer } from "./investigation/rule-analyzer";
32
+ export type { AgentTool, ToolContext, ToolResult } from "./tools/agent-tool";
33
+ export { type AgentStateName, ToolPolicy } from "./tools/tool-policy";
34
+ export { ToolRegistry } from "./tools/tool-registry";
35
+ export { Toolbox } from "./tools/toolbox";
36
+ export { FindingValidator } from "./validation/finding-validator";
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Estado de um agente de investigação.
3
+ *
4
+ * Acumula tarefas, hipóteses, evidências, cobertura de regras, drafts e custo.
5
+ * É a única fonte de verdade que o verificador/crítico consultam.
6
+ */
7
+
8
+ import {
9
+ type Evidence,
10
+ type EvidenceKind,
11
+ evidenceId,
12
+ type FindingDraft,
13
+ type RuleCoverage,
14
+ type RuleCoverageStatus,
15
+ } from "./evidence";
16
+ import type { InvestigationTask } from "./investigation-task";
17
+
18
+ /** Entrada para registrar uma evidência. */
19
+ export type EvidenceInput = Omit<Evidence, "id"> & { id?: string };
20
+
21
+ /** Estado mutável de um agente. */
22
+ export class AgentState {
23
+ readonly tasks: InvestigationTask[] = [];
24
+ readonly hypotheses: string[] = [];
25
+ readonly evidence = new Map<string, Evidence>();
26
+ readonly coverage = new Map<string, RuleCoverage>();
27
+ readonly drafts: FindingDraft[] = [];
28
+ readonly toolCalls = new Map<string, number>();
29
+ private sequence = 0;
30
+
31
+ /** Registra as tarefas do plano. */
32
+ setTasks(tasks: InvestigationTask[]): void {
33
+ this.tasks.push(...tasks);
34
+ }
35
+
36
+ /** Tarefas por status. */
37
+ tasksByStatus(status: InvestigationTask["status"]): InvestigationTask[] {
38
+ return this.tasks.filter((task) => task.status === status);
39
+ }
40
+
41
+ /** Marca uma tarefa como concluída. */
42
+ completeTask(id: string, reason?: string): void {
43
+ const task = this.tasks.find((item) => item.id === id);
44
+ if (!task) return;
45
+ task.status = "done";
46
+ if (reason) task.reason = reason;
47
+ }
48
+
49
+ /** Marca uma tarefa como pulada (com motivo). */
50
+ skipTask(id: string, reason: string): void {
51
+ const task = this.tasks.find((item) => item.id === id);
52
+ if (!task) return;
53
+ task.status = "skipped";
54
+ task.reason = reason;
55
+ }
56
+
57
+ /** Adiciona uma hipótese (sem duplicar). */
58
+ addHypothesis(hypothesis: string): void {
59
+ const value = hypothesis.trim();
60
+ if (value && !this.hypotheses.includes(value)) this.hypotheses.push(value);
61
+ }
62
+
63
+ /** Registra evidência e devolve o id gerado. */
64
+ addEvidence(input: EvidenceInput): Evidence {
65
+ const id = input.id?.trim() ? input.id.trim() : evidenceId(++this.sequence);
66
+ const evidence: Evidence = { ...input, id };
67
+ this.evidence.set(id, evidence);
68
+ return evidence;
69
+ }
70
+
71
+ /** Evidência por id (undefined se não existe). */
72
+ getEvidence(id: string): Evidence | undefined {
73
+ return this.evidence.get(id);
74
+ }
75
+
76
+ /** Evidências de uma tool/esécie. */
77
+ evidenceOfKind(kind: EvidenceKind): Evidence[] {
78
+ return [...this.evidence.values()].filter((item) => item.kind === kind);
79
+ }
80
+
81
+ /** Adiciona um draft de finding. */
82
+ addDraft(draft: FindingDraft): void {
83
+ this.drafts.push(draft);
84
+ }
85
+
86
+ /** Define a cobertura de uma regra (idempotente por texto). */
87
+ setCoverage(rule: string, status: RuleCoverageStatus, reason?: string): void {
88
+ const existing = this.coverage.get(rule);
89
+ if (existing) {
90
+ existing.status = status;
91
+ if (reason) existing.reason = reason;
92
+ return;
93
+ }
94
+ this.coverage.set(rule, {
95
+ rule,
96
+ status,
97
+ files: [],
98
+ symbols: [],
99
+ ...(reason ? { reason } : {}),
100
+ });
101
+ }
102
+
103
+ /** Cobertura atual de uma regra. */
104
+ coverageOf(rule: string): RuleCoverage | undefined {
105
+ return this.coverage.get(rule);
106
+ }
107
+
108
+ /** `true` quando a regra já foi coberta de alguma forma. */
109
+ isCovered(rule: string): boolean {
110
+ return this.coverage.has(rule);
111
+ }
112
+
113
+ /** Registra o uso de uma tool (contagem por nome). */
114
+ recordToolCall(name: string): void {
115
+ this.toolCalls.set(name, (this.toolCalls.get(name) ?? 0) + 1);
116
+ }
117
+
118
+ /** Chamadas registradas de uma tool. */
119
+ callsOf(name: string): number {
120
+ return this.toolCalls.get(name) ?? 0;
121
+ }
122
+
123
+ /** Snapshot serializável (sem prompts). */
124
+ toJSON(): Record<string, unknown> {
125
+ return {
126
+ hypotheses: this.hypotheses,
127
+ tasks: this.tasks,
128
+ evidence: [...this.evidence.values()],
129
+ coverage: [...this.coverage.values()],
130
+ drafts: this.drafts,
131
+ toolCalls: Object.fromEntries(this.toolCalls),
132
+ };
133
+ }
134
+ }