@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
@@ -19,6 +19,13 @@ export type WorktreeState = {
19
19
  sample: string[];
20
20
  };
21
21
 
22
+ /** Run associada à branch de uma PR. */
23
+ export type PrRunInfo = {
24
+ slug: string;
25
+ worktreePath?: string;
26
+ prNumber?: number;
27
+ };
28
+
22
29
  /** PR aberta exibida na sidebar. */
23
30
  export type PrListItem = {
24
31
  number: number;
@@ -32,6 +39,8 @@ export type PrListItem = {
32
39
  url?: string;
33
40
  /** Run local correspondente à branch, quando existir. */
34
41
  runSlug?: string;
42
+ /** Worktree ativo da PR (revisão paralela). */
43
+ worktreePath?: string;
35
44
  };
36
45
 
37
46
  /** Resposta de `GET /api/prs`. */
@@ -70,11 +79,12 @@ type GhPrListItem = {
70
79
 
71
80
  function toItem(
72
81
  raw: GhPrListItem,
73
- runSlugs: ReadonlyMap<string, string>,
82
+ runs: ReadonlyMap<string, PrRunInfo>,
74
83
  ): PrListItem | null {
75
84
  const number = Number(raw.number);
76
85
  const headRefName = raw.headRefName?.trim();
77
86
  if (!Number.isFinite(number) || number <= 0 || !headRefName) return null;
87
+ const run = runs.get(headRefName);
78
88
  return {
79
89
  number,
80
90
  title: raw.title?.trim() || `PR #${number}`,
@@ -85,7 +95,8 @@ function toItem(
85
95
  isDraft: Boolean(raw.isDraft),
86
96
  reviewDecision: raw.reviewDecision?.trim() || undefined,
87
97
  url: raw.url,
88
- runSlug: runSlugs.get(headRefName),
98
+ runSlug: run?.slug,
99
+ worktreePath: run?.worktreePath,
89
100
  };
90
101
  }
91
102
 
@@ -115,7 +126,7 @@ export class PrInbox {
115
126
  }
116
127
 
117
128
  /** Lista as PRs abertas (limite 50) e o estado local do repositório. */
118
- list(runSlugs: ReadonlyMap<string, string> = new Map()): PrListResponse {
129
+ list(runs: ReadonlyMap<string, PrRunInfo> = new Map()): PrListResponse {
119
130
  const currentBranch = this.git.currentBranch();
120
131
  const worktree = this.worktree();
121
132
  const result = this.ghRunner([
@@ -162,7 +173,7 @@ export class PrInbox {
162
173
  }
163
174
 
164
175
  const prs = (parsed as GhPrListItem[])
165
- .map((item) => toItem(item, runSlugs))
176
+ .map((item) => toItem(item, runs))
166
177
  .filter((item): item is PrListItem => item !== null)
167
178
  .sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""));
168
179
 
@@ -5,6 +5,7 @@
5
5
  * O slug continua sendo o branch-slug (estável nas URLs).
6
6
  */
7
7
 
8
+ import { existsSync } from "node:fs";
8
9
  import { resolve } from "node:path";
9
10
  import {
10
11
  applyFindingUpdate,
@@ -35,7 +36,10 @@ import {
35
36
  import { pingFindingsStatus } from "../core/run-status";
36
37
  import { systemClock } from "../core/services/clock";
37
38
  import { defaultGhRunner } from "../core/services/gh-client";
39
+ import type { WorktreeManager } from "../core/services/worktree-manager";
40
+ import { workRoot } from "../core/storage/paths";
38
41
  import type { RepoIdentity, ReviewRecord, StorageProvider } from "../core/storage/types";
42
+ import { ReviewWorkArea } from "../core/storage/work-area";
39
43
  import type { Severity } from "../core/types";
40
44
  import { isSlug } from "./artifacts";
41
45
  import { fetchPrComments, type PrCommentsResponse } from "./pr-comments";
@@ -44,6 +48,10 @@ import { fetchPrComments, type PrCommentsResponse } from "./pr-comments";
44
48
  export type RunListItem = {
45
49
  slug: string;
46
50
  branch: string;
51
+ /** Worktree onde a run foi executada (ausente = clone principal). */
52
+ worktreePath?: string;
53
+ /** PR revisada (quando iniciada via `--pr`). */
54
+ prNumber?: number;
47
55
  updatedAt?: string;
48
56
  startedAt?: string;
49
57
  heartbeatAt?: string;
@@ -55,6 +63,10 @@ export type RunListItem = {
55
63
  export type RunDetail = {
56
64
  slug: string;
57
65
  branch: string;
66
+ /** Worktree onde a run foi executada (ausente = clone principal). */
67
+ worktreePath?: string;
68
+ /** PR revisada (quando iniciada via `--pr`). */
69
+ prNumber?: number;
58
70
  status: ReviewRecord | null;
59
71
  manifest: unknown;
60
72
  summary: FindingsSummary;
@@ -63,6 +75,18 @@ export type RunDetail = {
63
75
  deleted: string[];
64
76
  };
65
77
 
78
+ /** Resultado da exclusão de uma run. */
79
+ export type DeleteRunResult = {
80
+ deleted: boolean;
81
+ /** O usuário pediu para remover o worktree. */
82
+ worktreeRequested: boolean;
83
+ /** O worktree foi de fato removido. */
84
+ worktreeRemoved: boolean;
85
+ worktreePath?: string;
86
+ /** Aviso quando o worktree não pôde ser removido. */
87
+ warning?: string;
88
+ };
89
+
66
90
  /** Resultado do envio dos findings à PR. */
67
91
  export type SubmitResult =
68
92
  | { available: false; reason: string }
@@ -81,15 +105,18 @@ export class RunRepository {
81
105
  private readonly storage: StorageProvider;
82
106
  private readonly repo: RepoIdentity;
83
107
  private readonly ghRunner: GhRunner;
108
+ private readonly worktrees?: WorktreeManager;
84
109
 
85
110
  constructor(
86
111
  storage: StorageProvider,
87
112
  repo: RepoIdentity,
88
113
  ghRunner: GhRunner = defaultGhRunner,
114
+ worktrees?: WorktreeManager,
89
115
  ) {
90
116
  this.storage = storage;
91
117
  this.repo = repo;
92
118
  this.ghRunner = ghRunner;
119
+ this.worktrees = worktrees;
93
120
  }
94
121
 
95
122
  /** `true` se o slug aponta para uma run do repositório atual. */
@@ -104,6 +131,8 @@ export class RunRepository {
104
131
  return {
105
132
  slug: review.slug,
106
133
  branch: review.branch,
134
+ worktreePath: review.worktreePath,
135
+ prNumber: review.prNumber,
107
136
  updatedAt: review.updatedAt,
108
137
  startedAt: review.startedAt,
109
138
  heartbeatAt: review.heartbeatAt,
@@ -122,6 +151,23 @@ export class RunRepository {
122
151
  );
123
152
  }
124
153
 
154
+ /** Mapa branch → run + worktree (sidebar de PRs). */
155
+ workspaceIndex(): Map<
156
+ string,
157
+ { slug: string; worktreePath?: string; prNumber?: number }
158
+ > {
159
+ return new Map(
160
+ this.storage.listReviews(this.repo.key).map((review) => [
161
+ review.branch,
162
+ {
163
+ slug: review.slug,
164
+ worktreePath: review.worktreePath,
165
+ prNumber: review.prNumber,
166
+ },
167
+ ]),
168
+ );
169
+ }
170
+
125
171
  /** Detalhe da run (null se o slug for inválido). */
126
172
  detail(slug: string): RunDetail | null {
127
173
  const review = this.findBySlug(slug);
@@ -135,6 +181,8 @@ export class RunRepository {
135
181
  return {
136
182
  slug: review.slug,
137
183
  branch: review.branch,
184
+ worktreePath: review.worktreePath,
185
+ prNumber: review.prNumber,
138
186
  status: review,
139
187
  manifest: this.readArtifactJson(review, "manifest.json"),
140
188
  summary: summarize(state),
@@ -144,6 +192,49 @@ export class RunRepository {
144
192
  };
145
193
  }
146
194
 
195
+ /**
196
+ * Apaga a run por completo: staging local, findings, histórico, artefatos e a
197
+ * review. Opcionalmente remove o worktree (com `--force`, pois a run foi
198
+ * confirmada para exclusão).
199
+ */
200
+ delete(
201
+ slug: string,
202
+ options: { removeWorktree?: boolean } = {},
203
+ ): DeleteRunResult | null {
204
+ const review = this.findBySlug(slug);
205
+ if (!review) return null;
206
+
207
+ const worktreePath = review.worktreePath;
208
+ const removeWorktree = options.removeWorktree === true && Boolean(worktreePath);
209
+ let worktreeRemoved = false;
210
+ let warning: string | undefined;
211
+
212
+ if (removeWorktree && worktreePath) {
213
+ if (this.worktrees) {
214
+ const result = this.worktrees.remove(worktreePath, { force: true });
215
+ worktreeRemoved = result.removed;
216
+ if (!result.removed) {
217
+ warning = result.detail ?? "worktree não removido";
218
+ }
219
+ } else {
220
+ warning = "worktree não gerenciado por este servidor";
221
+ }
222
+ }
223
+
224
+ // Staging atual (dentro do worktree quando existe) + legado no home.
225
+ new ReviewWorkArea(review).clean();
226
+ new ReviewWorkArea(review, workRoot()).clean();
227
+
228
+ this.storage.deleteReview(review.id);
229
+ return {
230
+ deleted: true,
231
+ worktreeRequested: options.removeWorktree === true,
232
+ worktreeRemoved,
233
+ worktreePath,
234
+ warning,
235
+ };
236
+ }
237
+
147
238
  /** Aplica `VACUUM`/`optimize` no backend (chamado no start do server). */
148
239
  maintain(): void {
149
240
  this.storage.maintain();
@@ -171,6 +262,12 @@ export class RunRepository {
171
262
  return this.storage.readFindings(review.id) ?? emptyState(review.branch);
172
263
  }
173
264
 
265
+ /** Raiz dos arquivos da review: worktree vivo ou clone principal. */
266
+ private rootOf(review: ReviewRecord): string {
267
+ const worktree = review.worktreePath;
268
+ return worktree && existsSync(worktree) ? worktree : this.repo.root;
269
+ }
270
+
174
271
  private readArtifactJson<T>(review: ReviewRecord, name: string): T | null {
175
272
  const artifact = this.storage.readArtifact(review.id, name);
176
273
  if (!artifact) return null;
@@ -252,7 +349,7 @@ export class RunRepository {
252
349
  const review = this.findBySlug(slug);
253
350
  if (!review) return null;
254
351
  const state = this.loadState(review);
255
- const result = syncState(state, fileReader(process.cwd()));
352
+ const result = syncState(state, fileReader(this.rootOf(review)));
256
353
  this.storage.writeFindings(review.id, state);
257
354
  pingFindingsStatus(this.storage, review.id, state);
258
355
  return { result, summary: summarize(state) };
@@ -336,5 +433,9 @@ function toClientFinding(
336
433
  finding: FindingState,
337
434
  review: ReviewRecord,
338
435
  ): FindingState & { absFile: string } {
339
- return { ...finding, absFile: resolve(review.repoRoot, finding.file) };
436
+ const root =
437
+ review.worktreePath && existsSync(review.worktreePath)
438
+ ? review.worktreePath
439
+ : review.repoRoot;
440
+ return { ...finding, absFile: resolve(root, finding.file) };
340
441
  }