@payconductor/houndsman 0.7.0 → 0.10.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 +20 -0
- package/package.json +1 -1
- package/src/cli/commands/command.ts +10 -1
- package/src/cli/commands/context-command.ts +40 -2
- package/src/cli/commands/hunt-command.ts +28 -2
- package/src/cli/commands/review-command.ts +30 -2
- package/src/cli/commands/worktree-command.ts +118 -0
- package/src/cli/help.ts +12 -2
- package/src/cli/index.ts +14 -4
- 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/context.ts +3 -3
- package/src/core/ai/prompts/technical.ts +30 -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/ast/service.ts +7 -2
- package/src/core/brief.ts +26 -4
- package/src/core/commands/context.ts +9 -1
- package/src/core/commands/findings.ts +11 -5
- package/src/core/commands/review.ts +13 -0
- package/src/core/commands/scaffold.ts +9 -2
- package/src/core/config.ts +30 -1
- package/src/core/defaults.ts +7 -0
- package/src/core/engine/houndsman-engine.ts +26 -3
- package/src/core/engine/review-scope.ts +17 -22
- package/src/core/engine/review-session.ts +25 -2
- package/src/core/engine/review-workspace.ts +204 -0
- package/src/core/git-runner.ts +8 -1
- package/src/core/jev/jev-detector.ts +10 -2
- package/src/core/orchestrator.ts +176 -15
- package/src/core/paths.ts +3 -0
- package/src/core/pr-context.ts +11 -6
- package/src/core/project-config.ts +21 -1
- package/src/core/renderers/resume-renderer.ts +17 -34
- package/src/core/report-builder.ts +6 -6
- package/src/core/services/git-repository.ts +46 -0
- package/src/core/services/pr-resolver.ts +147 -0
- package/src/core/services/worktree-manager.ts +293 -0
- package/src/core/storage/file/file-storage.ts +14 -1
- package/src/core/storage/paths.ts +30 -1
- package/src/core/storage/sqlite/mappers.ts +4 -0
- package/src/core/storage/sqlite/schema.ts +13 -3
- package/src/core/storage/sqlite/sqlite-storage.ts +28 -8
- package/src/core/storage/types.ts +13 -0
- package/src/core/storage/work-area.ts +20 -1
- package/src/core/technical-summary.ts +398 -0
- package/src/core/types.ts +2 -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/harness/hunt/hunt-options.ts +17 -0
- package/src/harness/hunt/hunt-session.ts +7 -20
- package/src/index.ts +33 -0
- package/src/server/client/App.tsx +51 -12
- package/src/server/client/DeleteRunModal.tsx +163 -0
- package/src/server/client/PrsSection.tsx +296 -0
- package/src/server/client/ResumePanel.tsx +17 -20
- package/src/server/client/RunsSection.tsx +73 -0
- package/src/server/client/Sidebar.tsx +171 -358
- package/src/server/client/icons.tsx +10 -0
- 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 +27 -0
- package/src/server/index.ts +113 -406
- package/src/server/pr-inbox.ts +15 -4
- package/src/server/run-repository.ts +103 -2
- package/src/server/styles.css +454 -0
- package/src/skill/README.md +11 -3
- package/src/skill/SKILL.md +44 -10
- package/src/skill/hunt.md +33 -7
- package/src/tests/__snapshots__/baseline.test.ts.snap +99 -24
- package/src/tests/ai-core.test.ts +71 -0
- package/src/tests/ai-features.test.ts +36 -0
- package/src/tests/baseline.test.ts +4 -1
- package/src/tests/cli.test.ts +2 -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 +54 -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 +404 -0
- 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
|
+
}
|
package/src/core/types.ts
CHANGED
|
@@ -135,6 +135,8 @@ export type WatchdogRenderArgs = {
|
|
|
135
135
|
branch: string;
|
|
136
136
|
outputPath: string;
|
|
137
137
|
outputFormatPath: string;
|
|
138
|
+
/** Worktree onde a review roda (ausente = clone principal). */
|
|
139
|
+
worktree?: string;
|
|
138
140
|
docs: readonly string[];
|
|
139
141
|
context?: WatchdogRenderContext | null;
|
|
140
142
|
candidates?: readonly WatchdogRenderCandidate[];
|
|
@@ -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. */
|
|
@@ -24,6 +24,12 @@ export type HuntOptions = {
|
|
|
24
24
|
concurrency?: number;
|
|
25
25
|
provider?: AiProviderKind;
|
|
26
26
|
model?: string;
|
|
27
|
+
/** Número da PR (`--pr`) — o comando resolve/cria o worktree. */
|
|
28
|
+
pr?: number;
|
|
29
|
+
/** Worktree informado via flag (relativo ao cwd ou absoluto). */
|
|
30
|
+
worktree?: string;
|
|
31
|
+
/** Worktree resolvido (absoluto) para persistir na review. */
|
|
32
|
+
worktreePath?: string;
|
|
27
33
|
};
|
|
28
34
|
|
|
29
35
|
/** Defaults do hunt. */
|
|
@@ -54,6 +60,17 @@ export function parseHuntArgs(args: string[]): HuntOptions {
|
|
|
54
60
|
const model = parser.value("--model");
|
|
55
61
|
const maxCalls = parser.value("--max-calls");
|
|
56
62
|
const concurrency = parser.value("--concurrency");
|
|
63
|
+
const pr = parser.value("--pr");
|
|
64
|
+
const worktree = parser.value("--worktree");
|
|
65
|
+
if (pr) {
|
|
66
|
+
const number = Number.parseInt(pr, 10);
|
|
67
|
+
if (Number.isFinite(number) && number > 0) options.pr = number;
|
|
68
|
+
else throw new Error(`--pr inválido: '${pr}'.`);
|
|
69
|
+
}
|
|
70
|
+
if (worktree) options.worktree = worktree;
|
|
71
|
+
if (options.pr !== undefined && options.worktree !== undefined) {
|
|
72
|
+
throw new Error("Use --pr ou --worktree, não os dois.");
|
|
73
|
+
}
|
|
57
74
|
if (base) options.base = base;
|
|
58
75
|
if (head) options.head = head;
|
|
59
76
|
if (branch) options.branch = branch;
|
|
@@ -14,11 +14,9 @@ import {
|
|
|
14
14
|
type ModelProvider,
|
|
15
15
|
} from "../../core/ai";
|
|
16
16
|
import type { AstAnalysis } from "../../core/ast/types";
|
|
17
|
-
import { collectDiff } from "../../core/diff-source";
|
|
18
17
|
import type { HoundsmanEngine } from "../../core/engine/houndsman-engine";
|
|
19
18
|
import type { RunClassification } from "../../core/engine/review-scope";
|
|
20
19
|
import { parseEntries } from "../../core/file-classifier";
|
|
21
|
-
import { runGit, tryGit } from "../../core/git-runner";
|
|
22
20
|
import type { LoggerLike } from "../../core/logging";
|
|
23
21
|
import type { PrepareResult } from "../../core/orchestrator";
|
|
24
22
|
import { defaultOptions, type ReviewOptions } from "../../core/orchestrator";
|
|
@@ -67,6 +65,8 @@ export class HuntSession {
|
|
|
67
65
|
clean: options.clean,
|
|
68
66
|
noAi: true,
|
|
69
67
|
finalize: false,
|
|
68
|
+
pr: options.pr,
|
|
69
|
+
worktreePath: options.worktreePath,
|
|
70
70
|
};
|
|
71
71
|
|
|
72
72
|
const session = engine.session(reviewOptions);
|
|
@@ -137,7 +137,7 @@ export class HuntSession {
|
|
|
137
137
|
reporter,
|
|
138
138
|
config: this.deps.config,
|
|
139
139
|
ast: engine.ast,
|
|
140
|
-
readFile: defaultFileReader(
|
|
140
|
+
readFile: defaultFileReader(engine.root),
|
|
141
141
|
});
|
|
142
142
|
const runner = new AgentRunner(factory);
|
|
143
143
|
const scheduler = new AgentScheduler(
|
|
@@ -265,11 +265,11 @@ export class HuntSession {
|
|
|
265
265
|
work: import("../../core/storage/work-area").ReviewWorkArea,
|
|
266
266
|
) {
|
|
267
267
|
const { engine, options } = this.deps;
|
|
268
|
-
const diff = collectDiff(options.base, options.head);
|
|
268
|
+
const diff = engine.git.collectDiff(options.base, options.head);
|
|
269
269
|
const entries = parseEntries(
|
|
270
|
-
|
|
270
|
+
engine.git.diffNameStatus(options.base, options.head),
|
|
271
271
|
);
|
|
272
|
-
const baseRef = mergeBase(options.base, options.head) ?? options.base;
|
|
272
|
+
const baseRef = engine.git.mergeBase(options.base, options.head) ?? options.base;
|
|
273
273
|
const ast: Map<string, AstAnalysis> = options.noAst
|
|
274
274
|
? new Map()
|
|
275
275
|
: await engine.ast.analyze(entries, diff, baseRef);
|
|
@@ -283,7 +283,7 @@ export class HuntSession {
|
|
|
283
283
|
context,
|
|
284
284
|
ast,
|
|
285
285
|
impact,
|
|
286
|
-
repoRoot:
|
|
286
|
+
repoRoot: engine.root,
|
|
287
287
|
diff,
|
|
288
288
|
branch: review.branch,
|
|
289
289
|
watchdog: options.watchdog,
|
|
@@ -357,16 +357,3 @@ function readContextSummary(
|
|
|
357
357
|
return null;
|
|
358
358
|
}
|
|
359
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
|
-
}
|
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,
|
|
@@ -89,6 +90,14 @@ export { buildCarryover, mergeCarryover } from "./core/engine/carryover";
|
|
|
89
90
|
export { HoundsmanEngine } from "./core/engine/houndsman-engine";
|
|
90
91
|
export { ReviewScopeResolver } from "./core/engine/review-scope";
|
|
91
92
|
export { ReviewSession } from "./core/engine/review-session";
|
|
93
|
+
export type {
|
|
94
|
+
ResolvedWorkspace,
|
|
95
|
+
WorkspaceRequest,
|
|
96
|
+
} from "./core/engine/review-workspace";
|
|
97
|
+
export {
|
|
98
|
+
mainRootOf,
|
|
99
|
+
resolveReviewWorkspace,
|
|
100
|
+
} from "./core/engine/review-workspace";
|
|
92
101
|
export type { HoundsmanEnv } from "./core/env";
|
|
93
102
|
export { ENV_NAMES, env, readEnv } from "./core/env";
|
|
94
103
|
export {
|
|
@@ -171,8 +180,27 @@ export { FixBatchBuilder } from "./core/services/fix-batch-builder";
|
|
|
171
180
|
export type { GhResult, GhRunner } from "./core/services/gh-client";
|
|
172
181
|
export { GhClient } from "./core/services/gh-client";
|
|
173
182
|
export { GitRepository } from "./core/services/git-repository";
|
|
183
|
+
export type {
|
|
184
|
+
PrRefs,
|
|
185
|
+
PrResolveResult,
|
|
186
|
+
PullRequestInfo,
|
|
187
|
+
} from "./core/services/pr-resolver";
|
|
188
|
+
export { PrResolver } from "./core/services/pr-resolver";
|
|
174
189
|
export { ProjectFiles } from "./core/services/project-files";
|
|
175
190
|
export { Verifier } from "./core/services/verifier";
|
|
191
|
+
export type {
|
|
192
|
+
EnsureWorktreeInput,
|
|
193
|
+
EnsureWorktreeResult,
|
|
194
|
+
RemoveWorktreeResult,
|
|
195
|
+
WorktreeEntry,
|
|
196
|
+
WorktreeManagerOptions,
|
|
197
|
+
} from "./core/services/worktree-manager";
|
|
198
|
+
export {
|
|
199
|
+
branchSlug,
|
|
200
|
+
parseWorktreeList,
|
|
201
|
+
repoSlug,
|
|
202
|
+
WorktreeManager,
|
|
203
|
+
} from "./core/services/worktree-manager";
|
|
176
204
|
export {
|
|
177
205
|
createMemoryStorage,
|
|
178
206
|
createStorage,
|
|
@@ -226,4 +254,9 @@ export {
|
|
|
226
254
|
} from "./core/watchdog-registry";
|
|
227
255
|
export type { ServerOptions } from "./server";
|
|
228
256
|
export { createRequestHandler, HoundsmanServer, startServer } from "./server";
|
|
257
|
+
export type {
|
|
258
|
+
DeleteRunResult,
|
|
259
|
+
RunDetail,
|
|
260
|
+
RunListItem,
|
|
261
|
+
} from "./server/run-repository";
|
|
229
262
|
export { RunRepository } from "./server/run-repository";
|