@payconductor/houndsman 0.8.0 → 0.11.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/package.json +1 -1
- package/src/cli/index.ts +9 -1
- package/src/core/ai/ai-client.ts +21 -0
- package/src/core/ai/ai-sdk-model-provider.ts +43 -18
- package/src/core/ai/features/technical-summarizer.ts +43 -0
- package/src/core/ai/index.ts +4 -1
- package/src/core/ai/prompts/base.ts +7 -0
- package/src/core/ai/prompts/context.ts +5 -4
- package/src/core/ai/prompts/technical.ts +31 -0
- package/src/core/ai/provider-capabilities.ts +45 -0
- package/src/core/ai/schemas.ts +19 -3
- package/src/core/ai/types.ts +3 -1
- package/src/core/brief.ts +25 -4
- package/src/core/commands/scaffold.ts +5 -0
- package/src/core/config-loader.ts +18 -4
- package/src/core/config.ts +17 -1
- package/src/core/engine/review-workspace.ts +6 -2
- package/src/core/git-runner.ts +8 -1
- package/src/core/orchestrator.ts +114 -2
- package/src/core/paths.ts +3 -0
- package/src/core/pr-context.ts +11 -6
- package/src/core/project-config.ts +9 -1
- package/src/core/renderers/resume-renderer.ts +17 -34
- package/src/core/report-builder.ts +6 -6
- package/src/core/services/worktree-manager.ts +90 -7
- package/src/core/storage/types.ts +1 -0
- package/src/core/technical-summary.ts +398 -0
- package/src/harness/agent/agent-factory.ts +1 -0
- package/src/harness/agent/review-agent.ts +11 -1
- package/src/harness/config/harness-config.ts +6 -1
- package/src/index.ts +2 -0
- package/src/server/client/App.tsx +2 -1
- package/src/server/client/PrsSection.tsx +296 -0
- package/src/server/client/ResumePanel.tsx +10 -13
- package/src/server/client/RunsSection.tsx +73 -0
- package/src/server/client/Sidebar.tsx +171 -413
- package/src/server/client/sidebar-sections.tsx +73 -0
- package/src/server/client/utils/constants.ts +7 -2
- package/src/server/client/utils/sidebar-prefs.ts +64 -0
- package/src/server/client/utils/types.ts +1 -0
- package/src/server/index.ts +23 -409
- package/src/server/styles.css +454 -0
- package/src/tests/__snapshots__/baseline.test.ts.snap +5 -0
- package/src/tests/ai-core.test.ts +71 -0
- package/src/tests/ai-features.test.ts +47 -0
- package/src/tests/baseline.test.ts +4 -1
- package/src/tests/harness-agent.test.ts +19 -0
- package/src/tests/review-resume.test.ts +36 -8
- package/src/tests/review-scope.test.ts +67 -0
- package/src/tests/review-scripts.test.ts +2 -2
- package/src/tests/server.test.ts +16 -1
- package/src/tests/sidebar-sections.test.ts +94 -0
- package/src/tests/technical-summary.test.ts +176 -0
- package/src/tests/worktree.test.ts +110 -5
- package/assets/logo-legacy.png +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@payconductor/houndsman",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Orquestrador de revisão de Pull Requests: classifica o diff, despacha watchdogs especializados por domínio e consolida o relatório.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/cli/index.ts
CHANGED
|
@@ -31,6 +31,7 @@ import type { HoundsmanConfig } from "../core/config";
|
|
|
31
31
|
import { loadConfig } from "../core/config-loader";
|
|
32
32
|
import { HoundsmanEngine } from "../core/engine/houndsman-engine";
|
|
33
33
|
import { logger } from "../core/logging";
|
|
34
|
+
import { syncProjectLayerForRoot } from "../core/services/worktree-manager";
|
|
34
35
|
import { ArgParser } from "./arg-parser";
|
|
35
36
|
import type { CliContext } from "./commands/command";
|
|
36
37
|
import { ContextCommand } from "./commands/context-command";
|
|
@@ -90,7 +91,14 @@ export async function main(): Promise<void> {
|
|
|
90
91
|
|
|
91
92
|
let loaded: Awaited<ReturnType<typeof loadConfig>> | null = null;
|
|
92
93
|
const config = async (): Promise<HoundsmanConfig> => {
|
|
93
|
-
loaded ??= await
|
|
94
|
+
loaded ??= await (async () => {
|
|
95
|
+
// Rodando dentro de um worktree, a camada gerada (`.houndsman/core`)
|
|
96
|
+
// não existe (é ignorada pelo git). Repõe a partir do clone principal
|
|
97
|
+
// antes de importar a config — sem isso os watchdogs não resolvem
|
|
98
|
+
// `../core/types`. No clone principal é no-op.
|
|
99
|
+
syncProjectLayerForRoot(process.cwd());
|
|
100
|
+
return loadConfig({ configPath: parser.value("--config") });
|
|
101
|
+
})();
|
|
94
102
|
return loaded.config;
|
|
95
103
|
};
|
|
96
104
|
|
package/src/core/ai/ai-client.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { ZodType } from "zod";
|
|
10
|
+
import type { AiProviderOptions } from "../config";
|
|
10
11
|
import type { ProjectConfig } from "../project-config";
|
|
11
12
|
import { AiConfigurationError, errorMessage } from "./errors";
|
|
12
13
|
import { ModelFactory } from "./model-factory";
|
|
@@ -33,6 +34,8 @@ export type ObjectRequest<T> = {
|
|
|
33
34
|
maxSteps?: number;
|
|
34
35
|
maxOutputTokens?: number;
|
|
35
36
|
temperature?: number;
|
|
37
|
+
/** Opções do AI SDK por provider (mescladas com a config do projeto). */
|
|
38
|
+
providerOptions?: AiProviderOptions;
|
|
36
39
|
signal?: AbortSignal;
|
|
37
40
|
};
|
|
38
41
|
|
|
@@ -188,6 +191,10 @@ export class AiClient {
|
|
|
188
191
|
const provider = await this.requireProvider(model);
|
|
189
192
|
const response = await provider.generate({
|
|
190
193
|
...request,
|
|
194
|
+
providerOptions: mergeProviderOptions(
|
|
195
|
+
this.project.aiProviderOptions,
|
|
196
|
+
request.providerOptions,
|
|
197
|
+
),
|
|
191
198
|
signal: combineSignals(request.signal, this.project.aiTimeoutMs),
|
|
192
199
|
});
|
|
193
200
|
this.budget.record(response.usage);
|
|
@@ -204,6 +211,20 @@ export class AiClient {
|
|
|
204
211
|
}
|
|
205
212
|
}
|
|
206
213
|
|
|
214
|
+
/** Mescla opções por provider (o override do chamador vence chave a chave). */
|
|
215
|
+
function mergeProviderOptions(
|
|
216
|
+
base: AiProviderOptions | undefined,
|
|
217
|
+
override: AiProviderOptions | undefined,
|
|
218
|
+
): AiProviderOptions | undefined {
|
|
219
|
+
if (!base) return override;
|
|
220
|
+
if (!override) return base;
|
|
221
|
+
const merged: AiProviderOptions = { ...base };
|
|
222
|
+
for (const [provider, values] of Object.entries(override)) {
|
|
223
|
+
merged[provider] = { ...base[provider], ...values };
|
|
224
|
+
}
|
|
225
|
+
return merged;
|
|
226
|
+
}
|
|
227
|
+
|
|
207
228
|
/** Combina o sinal do chamador com o timeout configurado. */
|
|
208
229
|
function combineSignals(
|
|
209
230
|
signal: AbortSignal | undefined,
|
|
@@ -10,6 +10,7 @@ import type { LanguageModel, ToolSet } from "ai";
|
|
|
10
10
|
import { generateText, Output, stepCountIs, tool } from "ai";
|
|
11
11
|
import type { AiProviderKind } from "../config";
|
|
12
12
|
import { AiProviderError } from "./errors";
|
|
13
|
+
import { temperatureApplies } from "./provider-capabilities";
|
|
13
14
|
import {
|
|
14
15
|
type AiTool,
|
|
15
16
|
type AiUsage,
|
|
@@ -45,6 +46,47 @@ function numberOrZero(value: unknown): number {
|
|
|
45
46
|
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
/** Argumentos aceitos por `generateText` (inclui `providerOptions` do SDK). */
|
|
50
|
+
type GenerateTextArgs = Parameters<typeof generateText>[0];
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Argumentos de `generateText` derivados do pedido (puro — testável sem rede).
|
|
54
|
+
*
|
|
55
|
+
* `temperature` só é enviada quando o provider de fato a aplica: em thinking
|
|
56
|
+
* mode (DeepSeek V4 default) o AI SDK a descarta e emite warning por chamada.
|
|
57
|
+
*/
|
|
58
|
+
export function buildGenerateTextArgs<T>(
|
|
59
|
+
model: LanguageModel,
|
|
60
|
+
request: ModelRequest<T>,
|
|
61
|
+
provider: AiProviderKind,
|
|
62
|
+
modelId: string,
|
|
63
|
+
) {
|
|
64
|
+
const tools = request.tools ? toToolSet(request.tools) : undefined;
|
|
65
|
+
const hasTools = tools !== undefined && Object.keys(tools).length > 0;
|
|
66
|
+
const maxSteps = request.maxSteps ?? 1;
|
|
67
|
+
const providerOptions = request.providerOptions
|
|
68
|
+
? (request.providerOptions as GenerateTextArgs["providerOptions"])
|
|
69
|
+
: undefined;
|
|
70
|
+
const temperature =
|
|
71
|
+
request.temperature !== undefined &&
|
|
72
|
+
temperatureApplies(provider, modelId, request.providerOptions)
|
|
73
|
+
? request.temperature
|
|
74
|
+
: undefined;
|
|
75
|
+
return {
|
|
76
|
+
model,
|
|
77
|
+
system: request.instructions,
|
|
78
|
+
prompt: request.prompt,
|
|
79
|
+
...(hasTools ? { tools, stopWhen: stepCountIs(maxSteps) } : {}),
|
|
80
|
+
...(request.outputSchema
|
|
81
|
+
? { output: Output.object({ schema: request.outputSchema }) }
|
|
82
|
+
: {}),
|
|
83
|
+
...(request.maxOutputTokens ? { maxOutputTokens: request.maxOutputTokens } : {}),
|
|
84
|
+
...(providerOptions ? { providerOptions } : {}),
|
|
85
|
+
...(temperature !== undefined ? { temperature } : {}),
|
|
86
|
+
maxRetries: 0,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
48
90
|
/** Provider de modelo apoiado no AI SDK. */
|
|
49
91
|
export class AiSdkModelProvider implements ModelProvider {
|
|
50
92
|
constructor(
|
|
@@ -54,27 +96,10 @@ export class AiSdkModelProvider implements ModelProvider {
|
|
|
54
96
|
) {}
|
|
55
97
|
|
|
56
98
|
async generate<T>(request: ModelRequest<T>): Promise<ModelResponse<T>> {
|
|
57
|
-
const tools = request.tools ? toToolSet(request.tools) : undefined;
|
|
58
|
-
const hasTools = tools !== undefined && Object.keys(tools).length > 0;
|
|
59
|
-
const maxSteps = request.maxSteps ?? 1;
|
|
60
|
-
|
|
61
99
|
try {
|
|
62
100
|
const result = await generateText({
|
|
63
|
-
model
|
|
64
|
-
system: request.instructions,
|
|
65
|
-
prompt: request.prompt,
|
|
66
|
-
...(hasTools ? { tools, stopWhen: stepCountIs(maxSteps) } : {}),
|
|
67
|
-
...(request.outputSchema
|
|
68
|
-
? { output: Output.object({ schema: request.outputSchema }) }
|
|
69
|
-
: {}),
|
|
70
|
-
...(request.maxOutputTokens
|
|
71
|
-
? { maxOutputTokens: request.maxOutputTokens }
|
|
72
|
-
: {}),
|
|
73
|
-
...(request.temperature !== undefined
|
|
74
|
-
? { temperature: request.temperature }
|
|
75
|
-
: {}),
|
|
101
|
+
...buildGenerateTextArgs(this.model, request, this.name, this.modelId),
|
|
76
102
|
...(request.signal ? { abortSignal: request.signal } : {}),
|
|
77
|
-
maxRetries: 0,
|
|
78
103
|
});
|
|
79
104
|
|
|
80
105
|
const output = request.outputSchema
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pontos técnicos estruturais via IA (fallback ao builder determinístico).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
buildTechnicalPromptPayload,
|
|
7
|
+
parseTechnicalSummary,
|
|
8
|
+
type TechnicalPromptInput,
|
|
9
|
+
type TechnicalSummary,
|
|
10
|
+
} from "../../technical-summary";
|
|
11
|
+
import type { AiClient } from "../ai-client";
|
|
12
|
+
import { technicalInstructions, technicalPrompt } from "../prompts/technical";
|
|
13
|
+
import { type TechnicalSummaryPlan, TechnicalSummarySchema } from "../schemas";
|
|
14
|
+
|
|
15
|
+
/** Resume a estrutura da mudança em destaques curtos. */
|
|
16
|
+
export class AiTechnicalSummarizer {
|
|
17
|
+
constructor(private readonly client: AiClient) {}
|
|
18
|
+
|
|
19
|
+
/** `true` quando a feature está ligada e há IA disponível. */
|
|
20
|
+
get enabled(): boolean {
|
|
21
|
+
return this.client.available && this.client.features.technical;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Falha-aberto: sem IA ou saída inválida → `null`. */
|
|
25
|
+
async summarize(input: TechnicalPromptInput): Promise<TechnicalSummary | null> {
|
|
26
|
+
if (!this.enabled) return null;
|
|
27
|
+
const result = await this.client.generateObject<TechnicalSummaryPlan>(
|
|
28
|
+
"technical",
|
|
29
|
+
{
|
|
30
|
+
instructions: technicalInstructions(),
|
|
31
|
+
prompt: technicalPrompt(buildTechnicalPromptPayload(input)),
|
|
32
|
+
outputSchema: TechnicalSummarySchema,
|
|
33
|
+
maxSteps: 1,
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
if (!result.ok) return null;
|
|
37
|
+
return parseTechnicalSummary({
|
|
38
|
+
...result.output,
|
|
39
|
+
source: "ai",
|
|
40
|
+
headSha: input.headSha,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/core/ai/index.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Barrel da camada de IA (`core/ai`).
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
export type { AiProviderKind } from "../config";
|
|
5
|
+
export type { AiProviderKind, AiProviderOptions } from "../config";
|
|
6
6
|
export type {
|
|
7
7
|
AiClientOptions,
|
|
8
8
|
AiCredentialCheck,
|
|
@@ -21,6 +21,7 @@ export { AiBriefEnricher } from "./features/brief-enricher";
|
|
|
21
21
|
export { AiClassifier } from "./features/classifier";
|
|
22
22
|
export { AiContextSummarizer } from "./features/context-summarizer";
|
|
23
23
|
export { AiPreAnalyzer } from "./features/pre-analyzer";
|
|
24
|
+
export { AiTechnicalSummarizer } from "./features/technical-summarizer";
|
|
24
25
|
export { AiVerifier } from "./features/verifier";
|
|
25
26
|
export { hasProviderCredential, ModelFactory } from "./model-factory";
|
|
26
27
|
export {
|
|
@@ -35,6 +36,8 @@ export {
|
|
|
35
36
|
type ContextSummaryPlan,
|
|
36
37
|
ContextSummarySchema,
|
|
37
38
|
SeveritySchema,
|
|
39
|
+
type TechnicalSummaryPlan,
|
|
40
|
+
TechnicalSummarySchema,
|
|
38
41
|
VerifySchema,
|
|
39
42
|
WatchdogReportSchema,
|
|
40
43
|
} from "./schemas";
|
|
@@ -14,6 +14,13 @@ export const UNTRUSTED_DATA_RULES: readonly string[] = [
|
|
|
14
14
|
"Responda apenas no formato estruturado pedido.",
|
|
15
15
|
];
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Idioma da saída textual. Nomes próprios, paths, símbolos e comandos ficam
|
|
19
|
+
* como estão — só a prosa é traduzida.
|
|
20
|
+
*/
|
|
21
|
+
export const OUTPUT_LANGUAGE_RULE =
|
|
22
|
+
"Escreva todo o texto em português do Brasil (pt-BR), mesmo que o contexto, o diff ou os nomes de arquivos estejam em inglês.";
|
|
23
|
+
|
|
17
24
|
/** Monta um system prompt estável (papel + regras). */
|
|
18
25
|
export function buildSystem(role: string, extra: readonly string[] = []): string {
|
|
19
26
|
return [
|
|
@@ -2,16 +2,17 @@
|
|
|
2
2
|
* Prompt do resumo de contexto da PR (`AiContextSummarizer`).
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { buildSystem, clip } from "./base";
|
|
5
|
+
import { buildSystem, clip, OUTPUT_LANGUAGE_RULE } from "./base";
|
|
6
6
|
|
|
7
7
|
/** Instruções de sistema do resumo. */
|
|
8
8
|
export function contextInstructions(): string {
|
|
9
9
|
return buildSystem(
|
|
10
10
|
"Você resume o contexto de uma Pull Request para revisores técnicos.",
|
|
11
11
|
[
|
|
12
|
-
|
|
13
|
-
"
|
|
14
|
-
"
|
|
12
|
+
OUTPUT_LANGUAGE_RULE,
|
|
13
|
+
"Objetivo em até 2 frases (máx. 400 caracteres).",
|
|
14
|
+
"Motivação em até 3 frases (máx. 480 caracteres), com o problema e o impacto esperado.",
|
|
15
|
+
"Resumo das alterações em até 6 bullets (máx. 240 caracteres cada), focado em comportamento, fluxos e impacto.",
|
|
15
16
|
"Não invente informação ausente do contexto.",
|
|
16
17
|
],
|
|
17
18
|
);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt dos pontos técnicos estruturais (`AiTechnicalSummarizer`).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { buildSystem, clip, OUTPUT_LANGUAGE_RULE } from "./base";
|
|
6
|
+
|
|
7
|
+
/** Instruções de sistema dos pontos técnicos. */
|
|
8
|
+
export function technicalInstructions(): string {
|
|
9
|
+
return buildSystem(
|
|
10
|
+
"Você descreve a estrutura técnica de uma Pull Request para revisores.",
|
|
11
|
+
[
|
|
12
|
+
OUTPUT_LANGUAGE_RULE,
|
|
13
|
+
"Foque em arquitetura e contratos: novos módulos/services, interfaces, providers, repositories, camadas, exports, dependências e impacto.",
|
|
14
|
+
"NÃO avalie qualidade, NÃO liste achados, bugs ou riscos de revisão.",
|
|
15
|
+
"NÃO repita a motivação de negócio: isso pertence ao resumo de negócio.",
|
|
16
|
+
"Use no máximo 6 destaques; `area` é um rótulo curto e `detail` cita paths/símbolos concretos.",
|
|
17
|
+
"Não invente módulos, símbolos ou dependências ausentes dos dados.",
|
|
18
|
+
],
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Prompt do usuário com o payload estrutural já compactado. */
|
|
23
|
+
export function technicalPrompt(payload: unknown): string {
|
|
24
|
+
return [
|
|
25
|
+
"Descreva os pontos técnicos estruturais da mudança abaixo.",
|
|
26
|
+
"",
|
|
27
|
+
"```json",
|
|
28
|
+
clip(JSON.stringify(payload), 14_000),
|
|
29
|
+
"```",
|
|
30
|
+
].join("\n");
|
|
31
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capacidades do provider de IA (puro).
|
|
3
|
+
*
|
|
4
|
+
* O AI SDK descarta `temperature`/`topP` e emite warning a cada chamada quando
|
|
5
|
+
* o DeepSeek opera em thinking mode — que é o default nos modelos V4. Enviar o
|
|
6
|
+
* parâmetro nesse caso não tem efeito: o adapter o omite.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { AiProviderKind, AiProviderOptions } from "../config";
|
|
10
|
+
|
|
11
|
+
/** `true` quando o modelo DeepSeek usa thinking sem `providerOptions`. */
|
|
12
|
+
function isDeepSeekV4Model(modelId: string): boolean {
|
|
13
|
+
return !/(^|[/.])deepseek-(?:chat|reasoner|v[1-3](?:\.\d+)?)(?=[-/:.]|$)/.test(
|
|
14
|
+
modelId,
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* `true` quando o DeepSeek vai operar em thinking mode.
|
|
20
|
+
*
|
|
21
|
+
* Espelha a resolução do `@ai-sdk/deepseek`: `thinking.type` explícito manda;
|
|
22
|
+
* sem ele, V4 (e o alias `deepseek-reasoner`) ligam thinking.
|
|
23
|
+
*/
|
|
24
|
+
export function deepSeekThinkingEnabled(
|
|
25
|
+
modelId: string,
|
|
26
|
+
providerOptions: AiProviderOptions | undefined,
|
|
27
|
+
): boolean {
|
|
28
|
+
const thinking = providerOptions?.deepseek?.thinking as
|
|
29
|
+
| Record<string, unknown>
|
|
30
|
+
| undefined;
|
|
31
|
+
const type = thinking?.type;
|
|
32
|
+
if (type === "disabled") return false;
|
|
33
|
+
if (type === "enabled" || type === "adaptive") return true;
|
|
34
|
+
return modelId === "deepseek-reasoner" || isDeepSeekV4Model(modelId);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** `true` quando `temperature` chega ao provider (não é descartada). */
|
|
38
|
+
export function temperatureApplies(
|
|
39
|
+
provider: AiProviderKind,
|
|
40
|
+
modelId: string,
|
|
41
|
+
providerOptions?: AiProviderOptions,
|
|
42
|
+
): boolean {
|
|
43
|
+
if (provider !== "deepseek") return true;
|
|
44
|
+
return !deepSeekThinkingEnabled(modelId, providerOptions);
|
|
45
|
+
}
|
package/src/core/ai/schemas.ts
CHANGED
|
@@ -22,12 +22,28 @@ export const ContextSummarySchema = z.object({
|
|
|
22
22
|
head: z.string().optional(),
|
|
23
23
|
})
|
|
24
24
|
.optional(),
|
|
25
|
-
objective: z.string().max(
|
|
26
|
-
motivation: z.string().max(
|
|
27
|
-
changeSummary: z.array(z.string().max(
|
|
25
|
+
objective: z.string().max(400).optional(),
|
|
26
|
+
motivation: z.string().max(480).optional(),
|
|
27
|
+
changeSummary: z.array(z.string().max(240)).max(6).optional(),
|
|
28
28
|
});
|
|
29
29
|
export type ContextSummaryPlan = z.infer<typeof ContextSummarySchema>;
|
|
30
30
|
|
|
31
|
+
/** Pontos técnicos estruturais do resumo final (`technical-summary.json`). */
|
|
32
|
+
export const TechnicalSummarySchema = z.object({
|
|
33
|
+
/** Parágrafo curto de abertura (opcional). */
|
|
34
|
+
overview: z.string().max(600).optional(),
|
|
35
|
+
highlights: z
|
|
36
|
+
.array(
|
|
37
|
+
z.object({
|
|
38
|
+
area: z.string().min(1).max(80),
|
|
39
|
+
detail: z.string().min(1).max(400),
|
|
40
|
+
}),
|
|
41
|
+
)
|
|
42
|
+
.max(8)
|
|
43
|
+
.default([]),
|
|
44
|
+
});
|
|
45
|
+
export type TechnicalSummaryPlan = z.infer<typeof TechnicalSummarySchema>;
|
|
46
|
+
|
|
31
47
|
/** Plano de revisão sugerido por IA para um brief. */
|
|
32
48
|
export const BriefPlanSchema = z.object({
|
|
33
49
|
focus: z.array(z.string()).max(5).default([]),
|
package/src/core/ai/types.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { ZodType, ZodTypeAny } from "zod";
|
|
9
|
-
import type { AiProviderKind } from "../config";
|
|
9
|
+
import type { AiProviderKind, AiProviderOptions } from "../config";
|
|
10
10
|
|
|
11
11
|
/** Uso de tokens de uma chamada (ou acumulado de uma review). */
|
|
12
12
|
export type AiUsage = {
|
|
@@ -52,6 +52,8 @@ export type ModelRequest<T> = {
|
|
|
52
52
|
maxSteps?: number;
|
|
53
53
|
maxOutputTokens?: number;
|
|
54
54
|
temperature?: number;
|
|
55
|
+
/** Opções do AI SDK por provider (repassadas ao SDK sem interpretação). */
|
|
56
|
+
providerOptions?: AiProviderOptions;
|
|
55
57
|
signal?: AbortSignal;
|
|
56
58
|
};
|
|
57
59
|
|
package/src/core/brief.ts
CHANGED
|
@@ -27,15 +27,36 @@ export const PROCEDURE: readonly string[] = [
|
|
|
27
27
|
'5. **CONCLUDE** — grave o JSON no caminho de output, com `reviewedFiles` preenchido. Sem achados → `"findings": []` (arquivo obrigatório). Não escreva markdown livre.',
|
|
28
28
|
];
|
|
29
29
|
|
|
30
|
+
/** Limites do contexto injetado em cada brief (o resumo final é maior). */
|
|
31
|
+
const BRIEF_CONTEXT_LIMITS = {
|
|
32
|
+
objective: 200,
|
|
33
|
+
motivation: 240,
|
|
34
|
+
changeSummaryItem: 160,
|
|
35
|
+
changeSummaryItems: 4,
|
|
36
|
+
} as const;
|
|
37
|
+
|
|
30
38
|
/** Bloco de contexto da PR, injetado no brief de cada watchdog. */
|
|
31
39
|
export function renderBriefContext(context: WatchdogRenderContext): string[] {
|
|
32
40
|
const lines = ["## Contexto da PR", ""];
|
|
33
|
-
if (context.objective)
|
|
34
|
-
|
|
35
|
-
|
|
41
|
+
if (context.objective) {
|
|
42
|
+
lines.push(
|
|
43
|
+
`**Objetivo:** ${shorten(context.objective, BRIEF_CONTEXT_LIMITS.objective)}`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
if (context.motivation) {
|
|
47
|
+
lines.push(
|
|
48
|
+
`**Motivação:** ${shorten(context.motivation, BRIEF_CONTEXT_LIMITS.motivation)}`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
const changeSummary = (context.changeSummary ?? []).slice(
|
|
52
|
+
0,
|
|
53
|
+
BRIEF_CONTEXT_LIMITS.changeSummaryItems,
|
|
54
|
+
);
|
|
36
55
|
if (changeSummary.length > 0) {
|
|
37
56
|
lines.push("", "**Resumo das alterações:**", "");
|
|
38
|
-
for (const item of changeSummary)
|
|
57
|
+
for (const item of changeSummary) {
|
|
58
|
+
lines.push(`- ${shorten(item, BRIEF_CONTEXT_LIMITS.changeSummaryItem)}`);
|
|
59
|
+
}
|
|
39
60
|
}
|
|
40
61
|
lines.push("");
|
|
41
62
|
return lines;
|
|
@@ -219,10 +219,14 @@ export type HoundsmanAst = {
|
|
|
219
219
|
/** Providers suportados pela camada de IA. */
|
|
220
220
|
export type AiProviderKind = "deepseek" | "openai" | "anthropic";
|
|
221
221
|
|
|
222
|
+
/** Opções do AI SDK por provider (repassadas sem interpretação). */
|
|
223
|
+
export type AiProviderOptions = Record<string, Record<string, unknown>>;
|
|
224
|
+
|
|
222
225
|
/** Features de IA do fluxo normal (todas fail-open). */
|
|
223
226
|
export type HoundsmanAiFeatures = {
|
|
224
227
|
context?: boolean;
|
|
225
228
|
brief?: boolean;
|
|
229
|
+
technical?: boolean;
|
|
226
230
|
classification?: boolean;
|
|
227
231
|
preAnalysis?: boolean;
|
|
228
232
|
verify?: boolean;
|
|
@@ -245,6 +249,7 @@ export type HoundsmanAi = {
|
|
|
245
249
|
maxSteps?: number;
|
|
246
250
|
maxOutputTokens?: number;
|
|
247
251
|
temperature?: number;
|
|
252
|
+
providerOptions?: AiProviderOptions;
|
|
248
253
|
timeoutMs?: number;
|
|
249
254
|
maxCallsPerReview?: number;
|
|
250
255
|
maxTokensPerReview?: number;
|
|
@@ -31,6 +31,12 @@ export type LoadedConfig<TId extends string = string> = {
|
|
|
31
31
|
path: string;
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
+
/** Forma do módulo de config (default ou export nomeado). */
|
|
35
|
+
type ConfigModule<TId extends string> = {
|
|
36
|
+
default?: HoundsmanConfig<TId>;
|
|
37
|
+
houndsmanConfig?: HoundsmanConfig<TId>;
|
|
38
|
+
};
|
|
39
|
+
|
|
34
40
|
/** Primeiro arquivo de config existente no diretório (ou `null`). */
|
|
35
41
|
export function resolveConfigPath(cwd: string = process.cwd()): string | null {
|
|
36
42
|
for (const name of CONFIG_FILE_CANDIDATES) {
|
|
@@ -61,10 +67,18 @@ export async function loadConfig<TId extends string = string>(
|
|
|
61
67
|
throw new Error(MISSING_CONFIG_HINT);
|
|
62
68
|
}
|
|
63
69
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
70
|
+
let module: ConfigModule<TId>;
|
|
71
|
+
try {
|
|
72
|
+
module = (await import(pathToFileURL(path).href)) as ConfigModule<TId>;
|
|
73
|
+
} catch (error) {
|
|
74
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
75
|
+
// Causa comum: camada gerada ausente (`.houndsman/core`), importada pelos
|
|
76
|
+
// watchdogs como `../core/types` — típico de worktree ainda não sincronizado.
|
|
77
|
+
throw new Error(
|
|
78
|
+
`Falha ao carregar ${path}: ${detail}\n` +
|
|
79
|
+
"A camada gerada (`.houndsman/core`) existe? Rode `houndsman update` na raiz do projeto.",
|
|
80
|
+
);
|
|
81
|
+
}
|
|
68
82
|
const config = module.default ?? module.houndsmanConfig;
|
|
69
83
|
|
|
70
84
|
if (!config) {
|
package/src/core/config.ts
CHANGED
|
@@ -57,12 +57,23 @@ export type HoundsmanConcurrency = {
|
|
|
57
57
|
/** Providers suportados pela camada de IA. */
|
|
58
58
|
export type AiProviderKind = "deepseek" | "openai" | "anthropic";
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Opções do AI SDK indexadas pelo id do provider
|
|
62
|
+
* (`{ deepseek: { thinking: { type: "disabled" } } }`).
|
|
63
|
+
*
|
|
64
|
+
* O motor não interpreta o conteúdo: só repassa. Use para controlar
|
|
65
|
+
* thinking/reasoning sem acoplar a config a um provider específico.
|
|
66
|
+
*/
|
|
67
|
+
export type AiProviderOptions = Record<string, Record<string, unknown>>;
|
|
68
|
+
|
|
60
69
|
/** Features de IA do fluxo determinístico (todas fail-open). */
|
|
61
70
|
export type HoundsmanAiFeatures = {
|
|
62
71
|
/** Resume o contexto da PR (`context-summary`) quando o subagente não gerou. */
|
|
63
72
|
context?: boolean;
|
|
64
73
|
/** Enriquece os briefs com um plano de revisão sugerido. */
|
|
65
74
|
brief?: boolean;
|
|
75
|
+
/** Gera os pontos técnicos estruturais do resumo final. */
|
|
76
|
+
technical?: boolean;
|
|
66
77
|
/** Classifica arquivos que nenhum watchdog casou. */
|
|
67
78
|
classification?: boolean;
|
|
68
79
|
/** Gera candidatos de pré-análise (substitui o Jev quando disponível). */
|
|
@@ -94,8 +105,13 @@ export type HoundsmanAi = {
|
|
|
94
105
|
maxSteps?: number;
|
|
95
106
|
/** Teto de tokens de saída por chamada. */
|
|
96
107
|
maxOutputTokens?: number;
|
|
97
|
-
/**
|
|
108
|
+
/**
|
|
109
|
+
* Temperatura (default: 0). Ignorada quando o provider usa thinking
|
|
110
|
+
* (DeepSeek V4 liga thinking por default) — nesse caso o motor não a envia.
|
|
111
|
+
*/
|
|
98
112
|
temperature?: number;
|
|
113
|
+
/** Opções do AI SDK por provider (repassadas sem interpretação). */
|
|
114
|
+
providerOptions?: AiProviderOptions;
|
|
99
115
|
/** Timeout por chamada em ms (default: 120_000). */
|
|
100
116
|
timeoutMs?: number;
|
|
101
117
|
/** Teto de chamadas por review (default: 40). */
|
|
@@ -11,7 +11,7 @@ import { createGitRunner } from "../git-runner";
|
|
|
11
11
|
import { createGhRunner, type GhRunner } from "../services/gh-client";
|
|
12
12
|
import { GitRepository } from "../services/git-repository";
|
|
13
13
|
import { PrResolver } from "../services/pr-resolver";
|
|
14
|
-
import { WorktreeManager } from "../services/worktree-manager";
|
|
14
|
+
import { syncProjectLayer, WorktreeManager } from "../services/worktree-manager";
|
|
15
15
|
import { resolveWorktreesRoot } from "../storage/paths";
|
|
16
16
|
import { resolveRepoIdentity } from "../storage/repo-identity";
|
|
17
17
|
import type { RepoIdentity } from "../storage/types";
|
|
@@ -105,9 +105,13 @@ export function resolveReviewWorkspace(
|
|
|
105
105
|
`Worktree '${root}' está em detached HEAD. Informe --branch <nome>.`,
|
|
106
106
|
);
|
|
107
107
|
}
|
|
108
|
+
// A camada gerada não vem no checkout; sem ela os comandos no worktree
|
|
109
|
+
// não carregam a config (`../core/types`).
|
|
110
|
+
const explicitMainRoot = mainRootOf(git, repo.root);
|
|
111
|
+
syncProjectLayer(explicitMainRoot, root);
|
|
108
112
|
return {
|
|
109
113
|
root,
|
|
110
|
-
mainRoot:
|
|
114
|
+
mainRoot: explicitMainRoot,
|
|
111
115
|
repo,
|
|
112
116
|
git,
|
|
113
117
|
branch,
|
package/src/core/git-runner.ts
CHANGED
|
@@ -24,8 +24,15 @@ export type GitRunner = (args: string[]) => GitResult;
|
|
|
24
24
|
* certo. Sem `cwd`, herda o diretório do processo (default).
|
|
25
25
|
*/
|
|
26
26
|
export function createGitRunner(cwd?: string): GitRunner {
|
|
27
|
+
// `core.longpaths` evita "Filename too long" no checkout de worktrees
|
|
28
|
+
// dentro de caminhos fundos (MAX_PATH do Windows). O `-c` é propagado aos
|
|
29
|
+
// subprocessos do git via GIT_CONFIG_PARAMETERS.
|
|
30
|
+
const config =
|
|
31
|
+
process.platform === "win32"
|
|
32
|
+
? ["-c", "core.quotepath=false", "-c", "core.longpaths=true"]
|
|
33
|
+
: ["-c", "core.quotepath=false"];
|
|
27
34
|
return (args) => {
|
|
28
|
-
const result = Bun.spawnSync(["git",
|
|
35
|
+
const result = Bun.spawnSync(["git", ...config, ...args], {
|
|
29
36
|
cwd,
|
|
30
37
|
stdout: "pipe",
|
|
31
38
|
stderr: "pipe",
|