@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.
Files changed (55) hide show
  1. package/package.json +1 -1
  2. package/src/cli/index.ts +9 -1
  3. package/src/core/ai/ai-client.ts +21 -0
  4. package/src/core/ai/ai-sdk-model-provider.ts +43 -18
  5. package/src/core/ai/features/technical-summarizer.ts +43 -0
  6. package/src/core/ai/index.ts +4 -1
  7. package/src/core/ai/prompts/base.ts +7 -0
  8. package/src/core/ai/prompts/context.ts +5 -4
  9. package/src/core/ai/prompts/technical.ts +31 -0
  10. package/src/core/ai/provider-capabilities.ts +45 -0
  11. package/src/core/ai/schemas.ts +19 -3
  12. package/src/core/ai/types.ts +3 -1
  13. package/src/core/brief.ts +25 -4
  14. package/src/core/commands/scaffold.ts +5 -0
  15. package/src/core/config-loader.ts +18 -4
  16. package/src/core/config.ts +17 -1
  17. package/src/core/engine/review-workspace.ts +6 -2
  18. package/src/core/git-runner.ts +8 -1
  19. package/src/core/orchestrator.ts +114 -2
  20. package/src/core/paths.ts +3 -0
  21. package/src/core/pr-context.ts +11 -6
  22. package/src/core/project-config.ts +9 -1
  23. package/src/core/renderers/resume-renderer.ts +17 -34
  24. package/src/core/report-builder.ts +6 -6
  25. package/src/core/services/worktree-manager.ts +90 -7
  26. package/src/core/storage/types.ts +1 -0
  27. package/src/core/technical-summary.ts +398 -0
  28. package/src/harness/agent/agent-factory.ts +1 -0
  29. package/src/harness/agent/review-agent.ts +11 -1
  30. package/src/harness/config/harness-config.ts +6 -1
  31. package/src/index.ts +2 -0
  32. package/src/server/client/App.tsx +2 -1
  33. package/src/server/client/PrsSection.tsx +296 -0
  34. package/src/server/client/ResumePanel.tsx +10 -13
  35. package/src/server/client/RunsSection.tsx +73 -0
  36. package/src/server/client/Sidebar.tsx +171 -413
  37. package/src/server/client/sidebar-sections.tsx +73 -0
  38. package/src/server/client/utils/constants.ts +7 -2
  39. package/src/server/client/utils/sidebar-prefs.ts +64 -0
  40. package/src/server/client/utils/types.ts +1 -0
  41. package/src/server/index.ts +23 -409
  42. package/src/server/styles.css +454 -0
  43. package/src/tests/__snapshots__/baseline.test.ts.snap +5 -0
  44. package/src/tests/ai-core.test.ts +71 -0
  45. package/src/tests/ai-features.test.ts +47 -0
  46. package/src/tests/baseline.test.ts +4 -1
  47. package/src/tests/harness-agent.test.ts +19 -0
  48. package/src/tests/review-resume.test.ts +36 -8
  49. package/src/tests/review-scope.test.ts +67 -0
  50. package/src/tests/review-scripts.test.ts +2 -2
  51. package/src/tests/server.test.ts +16 -1
  52. package/src/tests/sidebar-sections.test.ts +94 -0
  53. package/src/tests/technical-summary.test.ts +176 -0
  54. package/src/tests/worktree.test.ts +110 -5
  55. 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.8.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 loadConfig({ configPath: parser.value("--config") });
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
 
@@ -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: this.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
+ }
@@ -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
- "Objetivo em uma frase (máx. 200 caracteres).",
13
- "Motivação em uma frase (máx. 240 caracteres).",
14
- "Resumo das alterações em até 4 bullets (máx. 160 caracteres cada).",
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
+ }
@@ -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(200).optional(),
26
- motivation: z.string().max(240).optional(),
27
- changeSummary: z.array(z.string().max(160)).max(4).optional(),
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([]),
@@ -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) lines.push(`**Objetivo:** ${context.objective}`);
34
- if (context.motivation) lines.push(`**Motivação:** ${context.motivation}`);
35
- const changeSummary = context.changeSummary ?? [];
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) lines.push(`- ${item}`);
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
- const module = (await import(pathToFileURL(path).href)) as {
65
- default?: HoundsmanConfig<TId>;
66
- houndsmanConfig?: HoundsmanConfig<TId>;
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) {
@@ -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
- /** Temperatura (default: 0). */
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: mainRootOf(git, repo.root),
114
+ mainRoot: explicitMainRoot,
111
115
  repo,
112
116
  git,
113
117
  branch,
@@ -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", "-c", "core.quotepath=false", ...args], {
35
+ const result = Bun.spawnSync(["git", ...config, ...args], {
29
36
  cwd,
30
37
  stdout: "pipe",
31
38
  stderr: "pipe",