@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
package/src/server/pr-inbox.ts
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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(
|
|
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,
|
|
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(
|
|
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
|
-
|
|
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
|
}
|