@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
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pontos técnicos estruturais do resumo final (`technical-summary.json`).
|
|
3
|
+
*
|
|
4
|
+
* Descrevem a arquitetura da mudança (módulos, contratos, implementações,
|
|
5
|
+
* dependências e impacto) — nunca os achados da revisão. A IA apenas refina a
|
|
6
|
+
* redação; sem ela, a heurística abaixo é o resultado final.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { AstAnalysis, AstImpact, AstSymbol, AstSymbolKind } from "./ast/types";
|
|
10
|
+
import type { ClassificationResult } from "./file-classifier";
|
|
11
|
+
|
|
12
|
+
/** Destaque técnico exibido como um bullet do resumo. */
|
|
13
|
+
export type TechnicalHighlight = {
|
|
14
|
+
/** Rótulo curto (ex.: "Contratos", "Implementações"). */
|
|
15
|
+
area: string;
|
|
16
|
+
/** Detalhe em uma linha, com paths/símbolos. */
|
|
17
|
+
detail: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** Pontos técnicos de uma revisão. */
|
|
21
|
+
export type TechnicalSummary = {
|
|
22
|
+
/** `ai` quando o texto veio do modelo; `deterministic` no fallback. */
|
|
23
|
+
source: "ai" | "deterministic";
|
|
24
|
+
/** Commit coberto — permite saber a que revisão o resumo se refere. */
|
|
25
|
+
headSha?: string;
|
|
26
|
+
/** Parágrafo curto de abertura (opcional). */
|
|
27
|
+
overview?: string;
|
|
28
|
+
highlights: TechnicalHighlight[];
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** Entrada mínima para montar os destaques. */
|
|
32
|
+
export type TechnicalInput = {
|
|
33
|
+
classification: Pick<ClassificationResult, "files" | "churn">;
|
|
34
|
+
ast?: ReadonlyMap<string, AstAnalysis>;
|
|
35
|
+
impact?: AstImpact;
|
|
36
|
+
headSha?: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** Entrada do payload enviado à IA (inclui contexto de negócio e domínios). */
|
|
40
|
+
export type TechnicalPromptInput = TechnicalInput & {
|
|
41
|
+
branch: string;
|
|
42
|
+
context?: {
|
|
43
|
+
objective?: string;
|
|
44
|
+
motivation?: string;
|
|
45
|
+
changeSummary?: string[];
|
|
46
|
+
} | null;
|
|
47
|
+
domains?: readonly string[];
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const MAX_HIGHLIGHTS = 6;
|
|
51
|
+
const MAX_DETAIL = 240;
|
|
52
|
+
|
|
53
|
+
/** Corta o texto em `max` caracteres, sinalizando com reticências. */
|
|
54
|
+
function truncate(value: string, max: number): string {
|
|
55
|
+
const text = value.trim().replace(/\s+/g, " ");
|
|
56
|
+
return text.length <= max ? text : `${text.slice(0, max - 1).trimEnd()}…`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Diretório do arquivo (`(raiz)` para paths sem pasta). */
|
|
60
|
+
function moduleOf(path: string): string {
|
|
61
|
+
const slash = path.lastIndexOf("/");
|
|
62
|
+
return slash > 0 ? path.slice(0, slash) : "(raiz)";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** `true` para pacotes externos (fora do projeto e de aliases locais). */
|
|
66
|
+
function isExternal(source: string): boolean {
|
|
67
|
+
return (
|
|
68
|
+
!source.startsWith(".") &&
|
|
69
|
+
!source.startsWith("/") &&
|
|
70
|
+
!source.startsWith("#") &&
|
|
71
|
+
!source.startsWith("@/")
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** `true` para símbolos que representam contratos públicos. */
|
|
76
|
+
function isContract(kind: AstSymbolKind): boolean {
|
|
77
|
+
return kind === "interface" || kind === "type" || kind === "enum";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
type SymbolRef = { symbol: AstSymbol; file: string };
|
|
81
|
+
|
|
82
|
+
/** Lista curta de símbolos com o arquivo de origem. */
|
|
83
|
+
function symbolDetail(refs: SymbolRef[], max = 4): string {
|
|
84
|
+
const seen = new Set<string>();
|
|
85
|
+
const parts: string[] = [];
|
|
86
|
+
for (const ref of refs) {
|
|
87
|
+
const key = `${ref.file}|${ref.symbol.name}`;
|
|
88
|
+
if (seen.has(key)) continue;
|
|
89
|
+
seen.add(key);
|
|
90
|
+
parts.push(`\`${ref.symbol.name}\` (${ref.file})`);
|
|
91
|
+
if (parts.length === max) break;
|
|
92
|
+
}
|
|
93
|
+
const total = new Set(refs.map((ref) => `${ref.file}|${ref.symbol.name}`)).size;
|
|
94
|
+
return `${parts.join(", ")}${total > parts.length ? ` +${total - parts.length}` : ""}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
type SignatureRef = {
|
|
98
|
+
name: string;
|
|
99
|
+
before: string;
|
|
100
|
+
after: string;
|
|
101
|
+
file: string;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/** Assinaturas alteradas, com o `antes → depois`. */
|
|
105
|
+
function signatureDetail(changes: SignatureRef[], max = 3): string {
|
|
106
|
+
return changes
|
|
107
|
+
.slice(0, max)
|
|
108
|
+
.map(
|
|
109
|
+
(change) =>
|
|
110
|
+
`\`${change.name}\` (${truncate(change.before, 60)} → ${truncate(change.after, 60)})`,
|
|
111
|
+
)
|
|
112
|
+
.join(", ");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
type ExportRef = {
|
|
116
|
+
name: string;
|
|
117
|
+
change: "added" | "removed";
|
|
118
|
+
file: string;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
type PromptSymbolRef = { name: string; kind: string; file: string };
|
|
122
|
+
|
|
123
|
+
/** Exports adicionados/removidos, com o arquivo. */
|
|
124
|
+
function exportDetail(changes: ExportRef[], max = 4): string {
|
|
125
|
+
return changes
|
|
126
|
+
.slice(0, max)
|
|
127
|
+
.map((change) => `\`${change.name}\` (${change.change}, ${change.file})`)
|
|
128
|
+
.join(", ");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Agrupa paths por diretório, do maior para o menor grupo. */
|
|
132
|
+
function groupByModule(paths: string[]): [string, number][] {
|
|
133
|
+
const counts = new Map<string, number>();
|
|
134
|
+
for (const path of paths) {
|
|
135
|
+
const dir = moduleOf(path);
|
|
136
|
+
counts.set(dir, (counts.get(dir) ?? 0) + 1);
|
|
137
|
+
}
|
|
138
|
+
return [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Monta os destaques determinísticos. Prioriza símbolos do AST; sem fatos
|
|
143
|
+
* estruturais, agrupa os arquivos novos por diretório. Nunca lança.
|
|
144
|
+
*/
|
|
145
|
+
export function buildTechnicalSummary(input: TechnicalInput): TechnicalSummary {
|
|
146
|
+
const highlights: TechnicalHighlight[] = [];
|
|
147
|
+
const files = input.classification.files;
|
|
148
|
+
const added = Object.entries(files)
|
|
149
|
+
.filter(([, file]) => file.status === "added")
|
|
150
|
+
.map(([path]) => path);
|
|
151
|
+
const deleted = Object.entries(files)
|
|
152
|
+
.filter(([, file]) => file.status === "deleted")
|
|
153
|
+
.map(([path]) => path);
|
|
154
|
+
|
|
155
|
+
const contracts: SymbolRef[] = [];
|
|
156
|
+
const implementations: SymbolRef[] = [];
|
|
157
|
+
const functions: SymbolRef[] = [];
|
|
158
|
+
const signatures: SignatureRef[] = [];
|
|
159
|
+
const exportsChanged: ExportRef[] = [];
|
|
160
|
+
const imports = new Set<string>();
|
|
161
|
+
|
|
162
|
+
if (input.ast) {
|
|
163
|
+
for (const [file, analysis] of input.ast) {
|
|
164
|
+
const delta = analysis.delta;
|
|
165
|
+
if (!delta) continue;
|
|
166
|
+
for (const symbol of delta.addedSymbols) {
|
|
167
|
+
if (isContract(symbol.kind)) contracts.push({ symbol, file });
|
|
168
|
+
else if (symbol.kind === "class") implementations.push({ symbol, file });
|
|
169
|
+
else if (symbol.kind === "function") functions.push({ symbol, file });
|
|
170
|
+
}
|
|
171
|
+
for (const change of delta.changedSignatures) {
|
|
172
|
+
if (!change.exported) continue;
|
|
173
|
+
signatures.push({
|
|
174
|
+
name: change.name,
|
|
175
|
+
before: change.before,
|
|
176
|
+
after: change.after,
|
|
177
|
+
file,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
for (const change of delta.changedExports) {
|
|
181
|
+
exportsChanged.push({
|
|
182
|
+
name: change.name,
|
|
183
|
+
change: change.change,
|
|
184
|
+
file,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
for (const source of delta.addedImports) {
|
|
188
|
+
if (isExternal(source)) imports.add(source);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (contracts.length > 0) {
|
|
194
|
+
highlights.push({ area: "Contratos", detail: symbolDetail(contracts) });
|
|
195
|
+
}
|
|
196
|
+
if (implementations.length > 0) {
|
|
197
|
+
highlights.push({
|
|
198
|
+
area: "Implementações",
|
|
199
|
+
detail: symbolDetail(implementations),
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
if (functions.length > 0 && contracts.length === 0 && implementations.length === 0) {
|
|
203
|
+
highlights.push({ area: "Funções", detail: symbolDetail(functions) });
|
|
204
|
+
}
|
|
205
|
+
if (signatures.length > 0) {
|
|
206
|
+
highlights.push({
|
|
207
|
+
area: "Assinaturas alteradas",
|
|
208
|
+
detail: signatureDetail(signatures),
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
if (exportsChanged.length > 0) {
|
|
212
|
+
highlights.push({
|
|
213
|
+
area: "Exports alterados",
|
|
214
|
+
detail: exportDetail(exportsChanged),
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
if (imports.size > 0) {
|
|
218
|
+
highlights.push({
|
|
219
|
+
area: "Novas dependências",
|
|
220
|
+
detail: [...imports]
|
|
221
|
+
.slice(0, 6)
|
|
222
|
+
.map((source) => `\`${source}\``)
|
|
223
|
+
.join(", "),
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Sem fatos do AST, a estrutura visível é o conjunto de diretórios novos.
|
|
228
|
+
if (highlights.length === 0) {
|
|
229
|
+
for (const [dir, count] of groupByModule(added).slice(0, 3)) {
|
|
230
|
+
highlights.push({
|
|
231
|
+
area: "Estrutura",
|
|
232
|
+
detail: `Novo diretório \`${dir}\` com ${count} arquivo(s).`,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (deleted.length > 0 && highlights.length < MAX_HIGHLIGHTS) {
|
|
238
|
+
highlights.push({
|
|
239
|
+
area: "Remoções",
|
|
240
|
+
detail: `${deleted
|
|
241
|
+
.slice(0, 4)
|
|
242
|
+
.map((path) => `\`${path}\``)
|
|
243
|
+
.join(", ")}${deleted.length > 4 ? ` +${deleted.length - 4}` : ""}`,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (
|
|
248
|
+
input.impact &&
|
|
249
|
+
input.impact.affectedFiles.length > 0 &&
|
|
250
|
+
highlights.length < MAX_HIGHLIGHTS
|
|
251
|
+
) {
|
|
252
|
+
const exported = input.impact.exportedChanges.length;
|
|
253
|
+
highlights.push({
|
|
254
|
+
area: "Impacto",
|
|
255
|
+
detail:
|
|
256
|
+
`${input.impact.affectedFiles.length} arquivo(s) importam os alterados` +
|
|
257
|
+
(exported > 0 ? `; ${exported} export(s) público(s) alterado(s)` : "") +
|
|
258
|
+
".",
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
source: "deterministic",
|
|
264
|
+
headSha: input.headSha,
|
|
265
|
+
highlights: highlights.slice(0, MAX_HIGHLIGHTS).map((highlight) => ({
|
|
266
|
+
area: truncate(highlight.area, 80),
|
|
267
|
+
detail: truncate(highlight.detail, MAX_DETAIL),
|
|
268
|
+
})),
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Payload compacto enviado ao modelo (nunca arquivos inteiros). */
|
|
273
|
+
export function buildTechnicalPromptPayload(
|
|
274
|
+
input: TechnicalPromptInput,
|
|
275
|
+
): Record<string, unknown> {
|
|
276
|
+
const files = Object.entries(input.classification.files)
|
|
277
|
+
.map(([path, file]) => ({
|
|
278
|
+
path,
|
|
279
|
+
status: file.status,
|
|
280
|
+
additions: input.classification.churn[path]?.additions ?? 0,
|
|
281
|
+
deletions: input.classification.churn[path]?.deletions ?? 0,
|
|
282
|
+
}))
|
|
283
|
+
.sort(
|
|
284
|
+
(a, b) =>
|
|
285
|
+
b.additions + b.deletions - (a.additions + a.deletions) ||
|
|
286
|
+
a.path.localeCompare(b.path),
|
|
287
|
+
)
|
|
288
|
+
.slice(0, 80);
|
|
289
|
+
|
|
290
|
+
const contracts: PromptSymbolRef[] = [];
|
|
291
|
+
const implementations: PromptSymbolRef[] = [];
|
|
292
|
+
const signatures: SignatureRef[] = [];
|
|
293
|
+
const exportsChanged: ExportRef[] = [];
|
|
294
|
+
const imports = new Set<string>();
|
|
295
|
+
const deletedFiles: string[] = [];
|
|
296
|
+
|
|
297
|
+
for (const [path, file] of Object.entries(input.classification.files)) {
|
|
298
|
+
if (file.status === "deleted") deletedFiles.push(path);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (input.ast) {
|
|
302
|
+
for (const [file, analysis] of input.ast) {
|
|
303
|
+
const delta = analysis.delta;
|
|
304
|
+
if (!delta) continue;
|
|
305
|
+
for (const symbol of delta.addedSymbols) {
|
|
306
|
+
if (isContract(symbol.kind)) {
|
|
307
|
+
contracts.push({ name: symbol.name, kind: symbol.kind, file });
|
|
308
|
+
} else if (symbol.kind === "class") {
|
|
309
|
+
implementations.push({ name: symbol.name, kind: symbol.kind, file });
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
for (const change of delta.changedSignatures) {
|
|
313
|
+
if (change.exported) {
|
|
314
|
+
signatures.push({
|
|
315
|
+
name: change.name,
|
|
316
|
+
before: change.before,
|
|
317
|
+
after: change.after,
|
|
318
|
+
file,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
for (const change of delta.changedExports) {
|
|
323
|
+
exportsChanged.push({
|
|
324
|
+
name: change.name,
|
|
325
|
+
change: change.change,
|
|
326
|
+
file,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
for (const source of delta.addedImports) {
|
|
330
|
+
if (isExternal(source)) imports.add(source);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
branch: input.branch,
|
|
337
|
+
headSha: input.headSha,
|
|
338
|
+
business: input.context
|
|
339
|
+
? {
|
|
340
|
+
objective: input.context.objective,
|
|
341
|
+
motivation: input.context.motivation,
|
|
342
|
+
changeSummary: input.context.changeSummary,
|
|
343
|
+
}
|
|
344
|
+
: undefined,
|
|
345
|
+
domains: input.domains ?? [],
|
|
346
|
+
files,
|
|
347
|
+
structure: {
|
|
348
|
+
contracts: contracts.slice(0, 40),
|
|
349
|
+
implementations: implementations.slice(0, 40),
|
|
350
|
+
changedSignatures: signatures.slice(0, 20),
|
|
351
|
+
changedExports: exportsChanged.slice(0, 20),
|
|
352
|
+
addedImports: [...imports].slice(0, 20),
|
|
353
|
+
deletedFiles: deletedFiles.slice(0, 20),
|
|
354
|
+
},
|
|
355
|
+
impact: input.impact
|
|
356
|
+
? {
|
|
357
|
+
affectedFiles: input.impact.affectedFiles.length,
|
|
358
|
+
importers: Object.keys(input.impact.importers).length,
|
|
359
|
+
exportedChanges: input.impact.exportedChanges.length,
|
|
360
|
+
}
|
|
361
|
+
: undefined,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** Valida/normaliza o `technical-summary.json`; null quando vazio/inválido. */
|
|
366
|
+
export function parseTechnicalSummary(raw: unknown): TechnicalSummary | null {
|
|
367
|
+
if (typeof raw !== "object" || raw === null) return null;
|
|
368
|
+
const obj = raw as Record<string, unknown>;
|
|
369
|
+
|
|
370
|
+
const highlights: TechnicalHighlight[] = [];
|
|
371
|
+
if (Array.isArray(obj.highlights)) {
|
|
372
|
+
for (const item of obj.highlights) {
|
|
373
|
+
if (typeof item !== "object" || item === null) continue;
|
|
374
|
+
const entry = item as Record<string, unknown>;
|
|
375
|
+
const area = typeof entry.area === "string" ? entry.area.trim() : "";
|
|
376
|
+
const detail = typeof entry.detail === "string" ? entry.detail.trim() : "";
|
|
377
|
+
if (!area || !detail) continue;
|
|
378
|
+
highlights.push({
|
|
379
|
+
area: truncate(area, 80),
|
|
380
|
+
detail: truncate(detail, MAX_DETAIL),
|
|
381
|
+
});
|
|
382
|
+
if (highlights.length === MAX_HIGHLIGHTS) break;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const overview =
|
|
387
|
+
typeof obj.overview === "string" && obj.overview.trim().length > 0
|
|
388
|
+
? truncate(obj.overview, 600)
|
|
389
|
+
: undefined;
|
|
390
|
+
if (highlights.length === 0 && !overview) return null;
|
|
391
|
+
|
|
392
|
+
return {
|
|
393
|
+
source: obj.source === "deterministic" ? "deterministic" : "ai",
|
|
394
|
+
headSha: typeof obj.headSha === "string" ? obj.headSha : undefined,
|
|
395
|
+
overview,
|
|
396
|
+
highlights,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
@@ -37,6 +37,7 @@ export class AgentFactory {
|
|
|
37
37
|
reporter: this.deps.reporter,
|
|
38
38
|
maxOutputTokens: config.maxOutputTokensFor(context.watchdog.id),
|
|
39
39
|
temperature: config.temperature,
|
|
40
|
+
providerOptions: config.providerOptions,
|
|
40
41
|
timeoutMs: config.timeoutMs,
|
|
41
42
|
ast: this.deps.ast,
|
|
42
43
|
readFile: this.deps.readFile,
|
|
@@ -8,7 +8,13 @@
|
|
|
8
8
|
|
|
9
9
|
import { readFileSync } from "node:fs";
|
|
10
10
|
import { join } from "node:path";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
type AiProviderOptions,
|
|
13
|
+
type AiUsage,
|
|
14
|
+
addUsage,
|
|
15
|
+
emptyUsage,
|
|
16
|
+
type ModelProvider,
|
|
17
|
+
} from "../../core/ai";
|
|
12
18
|
import type { AstService } from "../../core/ast/service";
|
|
13
19
|
import type { WatchdogReport } from "../../core/report-builder";
|
|
14
20
|
import type { Severity } from "../../core/types";
|
|
@@ -49,6 +55,7 @@ export type ReviewAgentDeps = {
|
|
|
49
55
|
reporter: AgentReporter;
|
|
50
56
|
maxOutputTokens: number;
|
|
51
57
|
temperature: number;
|
|
58
|
+
providerOptions?: AiProviderOptions;
|
|
52
59
|
timeoutMs: number;
|
|
53
60
|
ast: AstService;
|
|
54
61
|
readFile?: (path: string) => string | null;
|
|
@@ -338,6 +345,9 @@ export class ReviewAgent {
|
|
|
338
345
|
maxSteps,
|
|
339
346
|
maxOutputTokens: this.deps.maxOutputTokens,
|
|
340
347
|
temperature: this.deps.temperature,
|
|
348
|
+
...(this.deps.providerOptions
|
|
349
|
+
? { providerOptions: this.deps.providerOptions }
|
|
350
|
+
: {}),
|
|
341
351
|
signal: AbortSignal.timeout(this.deps.timeoutMs),
|
|
342
352
|
});
|
|
343
353
|
this.usage = addUsage(this.usage, response.usage);
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* aplica apenas overrides de execução (`hunt --model`, `--max-calls`, etc.).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { AiProviderKind } from "../../core/config";
|
|
8
|
+
import type { AiProviderKind, AiProviderOptions } from "../../core/config";
|
|
9
9
|
import { ProjectConfig } from "../../core/project-config";
|
|
10
10
|
|
|
11
11
|
/** Overrides de execução do harness. */
|
|
@@ -76,6 +76,11 @@ export class HarnessConfig {
|
|
|
76
76
|
get temperature(): number {
|
|
77
77
|
return this.project.aiTemperature;
|
|
78
78
|
}
|
|
79
|
+
|
|
80
|
+
/** Opções do AI SDK por provider (config do projeto, sem override de flag). */
|
|
81
|
+
get providerOptions(): AiProviderOptions | undefined {
|
|
82
|
+
return this.project.aiProviderOptions;
|
|
83
|
+
}
|
|
79
84
|
}
|
|
80
85
|
|
|
81
86
|
/** Inteiro positivo ou o default. */
|
package/src/index.ts
CHANGED
|
@@ -49,6 +49,7 @@ export { runFindingsCommand, runVerifyCommand } from "./core/commands/findings";
|
|
|
49
49
|
export { ProjectScaffolder } from "./core/commands/scaffold";
|
|
50
50
|
export type {
|
|
51
51
|
AiProviderKind,
|
|
52
|
+
AiProviderOptions,
|
|
52
53
|
HoundsmanAi,
|
|
53
54
|
HoundsmanAiFeatures,
|
|
54
55
|
HoundsmanAst,
|
|
@@ -195,6 +196,7 @@ export type {
|
|
|
195
196
|
WorktreeManagerOptions,
|
|
196
197
|
} from "./core/services/worktree-manager";
|
|
197
198
|
export {
|
|
199
|
+
branchSlug,
|
|
198
200
|
parseWorktreeList,
|
|
199
201
|
repoSlug,
|
|
200
202
|
WorktreeManager,
|
|
@@ -259,11 +259,12 @@ export function App() {
|
|
|
259
259
|
type="button"
|
|
260
260
|
key={item.id}
|
|
261
261
|
title={item.hint}
|
|
262
|
+
aria-label={item.label}
|
|
262
263
|
class={`seg-btn${tab === item.id ? " active" : ""}`}
|
|
263
264
|
onClick={() => setTab(item.id)}
|
|
264
265
|
>
|
|
265
266
|
<TabIcon id={item.id} />
|
|
266
|
-
{item.label}
|
|
267
|
+
<span class="seg-label">{item.label}</span>
|
|
267
268
|
</button>
|
|
268
269
|
))}
|
|
269
270
|
</div>
|