@payconductor/houndsman 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/commands/server-command.ts +7 -1
- package/src/cli/help.ts +18 -2
- 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 +65 -2
- 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/git-runner.ts +25 -13
- package/src/core/github-review.ts +10 -7
- 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/gh-client.ts +38 -22
- 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/App.tsx +38 -12
- package/src/server/client/CommentsPanel.tsx +255 -0
- package/src/server/client/FindingCard.tsx +12 -4
- package/src/server/client/FindingsList.tsx +4 -1
- package/src/server/client/Sidebar.tsx +397 -0
- package/src/server/client/SubmitModal.tsx +61 -5
- package/src/server/client/icons.tsx +19 -0
- package/src/server/client/index.tsx +1 -1
- package/src/server/client/utils/constants.ts +29 -5
- package/src/server/client/utils/format.ts +47 -2
- package/src/server/client/utils/types.ts +88 -0
- package/src/server/editor.ts +129 -0
- package/src/server/index.ts +115 -5
- package/src/server/pr-comments.ts +256 -0
- package/src/server/pr-inbox.ts +202 -0
- package/src/server/run-repository.ts +33 -1
- 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/cli.test.ts +33 -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/github-review.test.ts +9 -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/server-editor.test.ts +100 -0
- package/src/tests/server.test.ts +295 -2
- package/src/tests/tool-contracts.test.ts +82 -0
- package/src/tests/verify.test.ts +1 -1
- package/src/server/client/RunList.tsx +0 -75
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuração resolvida do harness nativo.
|
|
3
|
+
*
|
|
4
|
+
* O harness **não** reimplementa defaults: consome o `ProjectConfig` do motor e
|
|
5
|
+
* aplica apenas overrides de execução (`hunt --model`, `--max-calls`, etc.).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { AiProviderKind } from "../../core/config";
|
|
9
|
+
import { ProjectConfig } from "../../core/project-config";
|
|
10
|
+
|
|
11
|
+
/** Overrides de execução do harness. */
|
|
12
|
+
export type HarnessOverrides = {
|
|
13
|
+
provider?: AiProviderKind;
|
|
14
|
+
model?: string;
|
|
15
|
+
concurrency?: number;
|
|
16
|
+
maxSteps?: number;
|
|
17
|
+
maxCalls?: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** Configuração resolvida (imutável para o agente). */
|
|
21
|
+
export class HarnessConfig {
|
|
22
|
+
readonly project: ProjectConfig;
|
|
23
|
+
private readonly overrides: HarnessOverrides;
|
|
24
|
+
|
|
25
|
+
constructor(project: ProjectConfig, overrides: HarnessOverrides = {}) {
|
|
26
|
+
this.project = project;
|
|
27
|
+
this.overrides = overrides;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** `ProjectConfig` com os overrides aplicados (usado pelo `AiClient`). */
|
|
31
|
+
withOverrides(): ProjectConfig {
|
|
32
|
+
const provider = this.overrides.provider ?? this.project.aiProvider;
|
|
33
|
+
const model = this.overrides.model?.trim() || this.project.aiModel;
|
|
34
|
+
return new ProjectConfig({
|
|
35
|
+
...this.project.raw,
|
|
36
|
+
ai: { ...this.project.raw.ai, provider, model },
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get provider(): AiProviderKind {
|
|
41
|
+
return this.overrides.provider ?? this.project.aiProvider;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get model(): string {
|
|
45
|
+
return this.overrides.model?.trim() || this.project.aiModel;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
modelFor(watchdogId: string): string {
|
|
49
|
+
return this.overrides.model?.trim() || this.project.aiModelFor(watchdogId);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
maxStepsFor(watchdogId: string): number {
|
|
53
|
+
return positive(this.overrides.maxSteps, this.project.aiMaxStepsFor(watchdogId));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
maxOutputTokensFor(watchdogId: string): number {
|
|
57
|
+
return this.project.aiMaxOutputTokensFor(watchdogId);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
get concurrency(): number {
|
|
61
|
+
return positive(this.overrides.concurrency, this.project.aiConcurrency);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
get maxCalls(): number {
|
|
65
|
+
return positive(this.overrides.maxCalls, this.project.aiMaxCallsPerReview);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
get maxTokens(): number | undefined {
|
|
69
|
+
return this.project.aiMaxTokensPerReview;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
get timeoutMs(): number {
|
|
73
|
+
return this.project.aiTimeoutMs;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
get temperature(): number {
|
|
77
|
+
return this.project.aiTemperature;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Inteiro positivo ou o default. */
|
|
82
|
+
function positive(value: number | undefined, fallback: number): number {
|
|
83
|
+
return value !== undefined && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
84
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composition root do harness nativo (`houndsman hunt`).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { AiUsageRecorder, ModelFactory, type ModelProvider } from "../../core/ai";
|
|
6
|
+
import { AiConfigurationError } from "../../core/ai/errors";
|
|
7
|
+
import type { HoundsmanEngine } from "../../core/engine/houndsman-engine";
|
|
8
|
+
import { logger as defaultLogger, type LoggerLike } from "../../core/logging";
|
|
9
|
+
import { type AgentReporter, LoggerReporter } from "../agent/agent-reporter";
|
|
10
|
+
import { HarnessConfig } from "../config/harness-config";
|
|
11
|
+
import type { HuntOptions } from "./hunt-options";
|
|
12
|
+
import type { HuntResult } from "./hunt-result";
|
|
13
|
+
import { HuntSession } from "./hunt-session";
|
|
14
|
+
|
|
15
|
+
/** Orquestrador do hunt nativo. */
|
|
16
|
+
export class HuntHarness {
|
|
17
|
+
private constructor(
|
|
18
|
+
private readonly engine: HoundsmanEngine,
|
|
19
|
+
private readonly options: HuntOptions,
|
|
20
|
+
private readonly reporter: AgentReporter,
|
|
21
|
+
private readonly logger: LoggerLike,
|
|
22
|
+
private readonly config: HarnessConfig,
|
|
23
|
+
private readonly provider?: ModelProvider,
|
|
24
|
+
) {}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Monta o harness: resolve provider/model (overrides das flags) e cria o
|
|
28
|
+
* provider quando não é dry-run. `--provider`/`--model` sobrescrevem a config.
|
|
29
|
+
*/
|
|
30
|
+
static async from(
|
|
31
|
+
engine: HoundsmanEngine,
|
|
32
|
+
options: HuntOptions,
|
|
33
|
+
logger: LoggerLike = defaultLogger,
|
|
34
|
+
providerOverride?: ModelProvider,
|
|
35
|
+
): Promise<HuntHarness> {
|
|
36
|
+
const config = new HarnessConfig(engine.project, {
|
|
37
|
+
provider: options.provider,
|
|
38
|
+
model: options.model,
|
|
39
|
+
concurrency: options.concurrency,
|
|
40
|
+
maxCalls: options.maxCalls,
|
|
41
|
+
});
|
|
42
|
+
const reporter = new LoggerReporter(logger);
|
|
43
|
+
if (options.dryRun) {
|
|
44
|
+
return new HuntHarness(engine, options, reporter, logger, config);
|
|
45
|
+
}
|
|
46
|
+
if (providerOverride) {
|
|
47
|
+
return new HuntHarness(
|
|
48
|
+
engine,
|
|
49
|
+
options,
|
|
50
|
+
reporter,
|
|
51
|
+
logger,
|
|
52
|
+
config,
|
|
53
|
+
providerOverride,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
const factory = new ModelFactory(config.withOverrides());
|
|
57
|
+
try {
|
|
58
|
+
const provider = await factory.create();
|
|
59
|
+
return new HuntHarness(engine, options, reporter, logger, config, provider);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (error instanceof AiConfigurationError) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`hunt exige IA configurada: ${error.message} Use --dry-run para inspecionar o plano.`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Executa o hunt. */
|
|
71
|
+
async run(): Promise<HuntResult> {
|
|
72
|
+
const recorder = new AiUsageRecorder(this.config.provider, this.config.model);
|
|
73
|
+
return new HuntSession({
|
|
74
|
+
engine: this.engine,
|
|
75
|
+
options: this.options,
|
|
76
|
+
config: this.config,
|
|
77
|
+
reporter: this.reporter,
|
|
78
|
+
logger: this.logger,
|
|
79
|
+
provider: this.provider,
|
|
80
|
+
recorder,
|
|
81
|
+
}).run();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opções do `houndsman hunt` (harness nativo).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { ArgParser } from "../../core/arg-parser";
|
|
6
|
+
import type { AiProviderKind } from "../../core/config";
|
|
7
|
+
|
|
8
|
+
/** Opções resolvidas do hunt. */
|
|
9
|
+
export type HuntOptions = {
|
|
10
|
+
base: string;
|
|
11
|
+
head: string;
|
|
12
|
+
branch?: string;
|
|
13
|
+
watchdog?: string;
|
|
14
|
+
full: boolean;
|
|
15
|
+
clean: boolean;
|
|
16
|
+
noAst: boolean;
|
|
17
|
+
/** Hunt usa IA própria; Jev fica desligado por padrão (evita custo duplo). */
|
|
18
|
+
noJev: boolean;
|
|
19
|
+
resume: boolean;
|
|
20
|
+
allowPartial: boolean;
|
|
21
|
+
dryRun: boolean;
|
|
22
|
+
failOnBlocking: boolean;
|
|
23
|
+
maxCalls?: number;
|
|
24
|
+
concurrency?: number;
|
|
25
|
+
provider?: AiProviderKind;
|
|
26
|
+
model?: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** Defaults do hunt. */
|
|
30
|
+
export function defaultHuntOptions(): HuntOptions {
|
|
31
|
+
return {
|
|
32
|
+
base: "main",
|
|
33
|
+
head: "HEAD",
|
|
34
|
+
full: false,
|
|
35
|
+
clean: false,
|
|
36
|
+
noAst: false,
|
|
37
|
+
noJev: true,
|
|
38
|
+
resume: false,
|
|
39
|
+
allowPartial: false,
|
|
40
|
+
dryRun: false,
|
|
41
|
+
failOnBlocking: false,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Converte argv nas opções do hunt. */
|
|
46
|
+
export function parseHuntArgs(args: string[]): HuntOptions {
|
|
47
|
+
const parser = ArgParser.from(args);
|
|
48
|
+
const options = defaultHuntOptions();
|
|
49
|
+
const base = parser.value("--base");
|
|
50
|
+
const head = parser.value("--head");
|
|
51
|
+
const branch = parser.value("--branch");
|
|
52
|
+
const watchdog = parser.value("--watchdog");
|
|
53
|
+
const provider = parser.value("--provider");
|
|
54
|
+
const model = parser.value("--model");
|
|
55
|
+
const maxCalls = parser.value("--max-calls");
|
|
56
|
+
const concurrency = parser.value("--concurrency");
|
|
57
|
+
if (base) options.base = base;
|
|
58
|
+
if (head) options.head = head;
|
|
59
|
+
if (branch) options.branch = branch;
|
|
60
|
+
if (watchdog) options.watchdog = watchdog;
|
|
61
|
+
if (provider) options.provider = provider as AiProviderKind;
|
|
62
|
+
if (model) options.model = model;
|
|
63
|
+
if (maxCalls) options.maxCalls = Number.parseInt(maxCalls, 10);
|
|
64
|
+
if (concurrency) options.concurrency = Number.parseInt(concurrency, 10);
|
|
65
|
+
|
|
66
|
+
if (parser.has("--full")) options.full = true;
|
|
67
|
+
if (parser.has("--clean")) options.clean = true;
|
|
68
|
+
if (parser.has("--no-ast")) options.noAst = true;
|
|
69
|
+
if (parser.has("--with-jev")) options.noJev = false;
|
|
70
|
+
if (parser.has("--resume")) options.resume = true;
|
|
71
|
+
if (parser.has("--allow-partial")) options.allowPartial = true;
|
|
72
|
+
if (parser.has("--dry-run")) options.dryRun = true;
|
|
73
|
+
if (parser.has("--fail-on-blocking")) options.failOnBlocking = true;
|
|
74
|
+
return options;
|
|
75
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resultado do `hunt`.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { AiUsage } from "../../core/ai";
|
|
6
|
+
import type { AgentResult } from "../agent/agent-result";
|
|
7
|
+
|
|
8
|
+
/** Status final da execução. */
|
|
9
|
+
export type HuntResult = {
|
|
10
|
+
status: "completed" | "partial" | "dry-run" | "failed";
|
|
11
|
+
reviewId: string;
|
|
12
|
+
blockers: number;
|
|
13
|
+
attention: number;
|
|
14
|
+
watchdogs: AgentResult[];
|
|
15
|
+
usage: AiUsage;
|
|
16
|
+
messages: string[];
|
|
17
|
+
};
|
|
@@ -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
|
+
}
|