@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.
Files changed (88) hide show
  1. package/README.md +20 -0
  2. package/package.json +1 -1
  3. package/src/cli/commands/command.ts +10 -1
  4. package/src/cli/commands/context-command.ts +40 -2
  5. package/src/cli/commands/hunt-command.ts +28 -2
  6. package/src/cli/commands/review-command.ts +30 -2
  7. package/src/cli/commands/worktree-command.ts +118 -0
  8. package/src/cli/help.ts +12 -2
  9. package/src/cli/index.ts +14 -4
  10. package/src/core/ai/ai-client.ts +21 -0
  11. package/src/core/ai/ai-sdk-model-provider.ts +43 -18
  12. package/src/core/ai/features/technical-summarizer.ts +43 -0
  13. package/src/core/ai/index.ts +4 -1
  14. package/src/core/ai/prompts/context.ts +3 -3
  15. package/src/core/ai/prompts/technical.ts +30 -0
  16. package/src/core/ai/provider-capabilities.ts +45 -0
  17. package/src/core/ai/schemas.ts +19 -3
  18. package/src/core/ai/types.ts +3 -1
  19. package/src/core/ast/service.ts +7 -2
  20. package/src/core/brief.ts +26 -4
  21. package/src/core/commands/context.ts +9 -1
  22. package/src/core/commands/findings.ts +11 -5
  23. package/src/core/commands/review.ts +13 -0
  24. package/src/core/commands/scaffold.ts +9 -2
  25. package/src/core/config.ts +30 -1
  26. package/src/core/defaults.ts +7 -0
  27. package/src/core/engine/houndsman-engine.ts +26 -3
  28. package/src/core/engine/review-scope.ts +17 -22
  29. package/src/core/engine/review-session.ts +25 -2
  30. package/src/core/engine/review-workspace.ts +204 -0
  31. package/src/core/git-runner.ts +8 -1
  32. package/src/core/jev/jev-detector.ts +10 -2
  33. package/src/core/orchestrator.ts +176 -15
  34. package/src/core/paths.ts +3 -0
  35. package/src/core/pr-context.ts +11 -6
  36. package/src/core/project-config.ts +21 -1
  37. package/src/core/renderers/resume-renderer.ts +17 -34
  38. package/src/core/report-builder.ts +6 -6
  39. package/src/core/services/git-repository.ts +46 -0
  40. package/src/core/services/pr-resolver.ts +147 -0
  41. package/src/core/services/worktree-manager.ts +293 -0
  42. package/src/core/storage/file/file-storage.ts +14 -1
  43. package/src/core/storage/paths.ts +30 -1
  44. package/src/core/storage/sqlite/mappers.ts +4 -0
  45. package/src/core/storage/sqlite/schema.ts +13 -3
  46. package/src/core/storage/sqlite/sqlite-storage.ts +28 -8
  47. package/src/core/storage/types.ts +13 -0
  48. package/src/core/storage/work-area.ts +20 -1
  49. package/src/core/technical-summary.ts +398 -0
  50. package/src/core/types.ts +2 -0
  51. package/src/harness/agent/agent-factory.ts +1 -0
  52. package/src/harness/agent/review-agent.ts +11 -1
  53. package/src/harness/config/harness-config.ts +6 -1
  54. package/src/harness/hunt/hunt-options.ts +17 -0
  55. package/src/harness/hunt/hunt-session.ts +7 -20
  56. package/src/index.ts +33 -0
  57. package/src/server/client/App.tsx +51 -12
  58. package/src/server/client/DeleteRunModal.tsx +163 -0
  59. package/src/server/client/PrsSection.tsx +296 -0
  60. package/src/server/client/ResumePanel.tsx +17 -20
  61. package/src/server/client/RunsSection.tsx +73 -0
  62. package/src/server/client/Sidebar.tsx +171 -358
  63. package/src/server/client/icons.tsx +10 -0
  64. package/src/server/client/sidebar-sections.tsx +73 -0
  65. package/src/server/client/utils/constants.ts +7 -2
  66. package/src/server/client/utils/sidebar-prefs.ts +64 -0
  67. package/src/server/client/utils/types.ts +27 -0
  68. package/src/server/index.ts +113 -406
  69. package/src/server/pr-inbox.ts +15 -4
  70. package/src/server/run-repository.ts +103 -2
  71. package/src/server/styles.css +454 -0
  72. package/src/skill/README.md +11 -3
  73. package/src/skill/SKILL.md +44 -10
  74. package/src/skill/hunt.md +33 -7
  75. package/src/tests/__snapshots__/baseline.test.ts.snap +99 -24
  76. package/src/tests/ai-core.test.ts +71 -0
  77. package/src/tests/ai-features.test.ts +36 -0
  78. package/src/tests/baseline.test.ts +4 -1
  79. package/src/tests/cli.test.ts +2 -1
  80. package/src/tests/harness-agent.test.ts +19 -0
  81. package/src/tests/review-resume.test.ts +36 -8
  82. package/src/tests/review-scope.test.ts +67 -0
  83. package/src/tests/review-scripts.test.ts +2 -2
  84. package/src/tests/server.test.ts +54 -1
  85. package/src/tests/sidebar-sections.test.ts +94 -0
  86. package/src/tests/technical-summary.test.ts +176 -0
  87. package/src/tests/worktree.test.ts +404 -0
  88. 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 { type AiUsage, addUsage, emptyUsage, type ModelProvider } from "../../core/ai";
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(fileRoot()),
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
- runGit(["diff", `${options.base}...${options.head}`, "--name-status"]),
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: process.cwd(),
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";