@payconductor/houndsman 0.3.0 → 0.5.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 +25 -39
- package/package.json +1 -1
- package/src/cli/commands/context-command.ts +11 -9
- package/src/cli/commands/doctor-command.ts +5 -0
- package/src/cli/commands/findings-command.ts +13 -3
- package/src/cli/commands/review-command.ts +1 -2
- package/src/cli/commands/status-command.ts +46 -0
- package/src/cli/help.ts +8 -3
- package/src/cli/index.ts +5 -2
- package/src/core/commands/context.ts +29 -13
- package/src/core/commands/findings.ts +216 -125
- package/src/core/commands/review.ts +7 -8
- package/src/core/commands/scaffold.ts +15 -16
- package/src/core/commands/status.ts +63 -0
- package/src/core/config.ts +16 -7
- package/src/core/engine/houndsman-engine.ts +15 -1
- package/src/core/engine/review-scope.ts +26 -8
- package/src/core/engine/review-session.ts +37 -9
- package/src/core/findings-state.ts +53 -75
- package/src/core/github-review.ts +620 -57
- package/src/core/jev/jev-candidates.ts +49 -35
- package/src/core/orchestrator.ts +271 -134
- package/src/core/paths.ts +26 -17
- package/src/core/pr-context.ts +19 -5
- package/src/core/project-config.ts +4 -5
- package/src/core/renderers/manifest-writer.ts +29 -18
- package/src/core/renderers/resume-renderer.ts +151 -0
- package/src/core/report-builder.ts +7 -4
- package/src/core/run-layout.ts +4 -104
- package/src/core/run-status.ts +59 -50
- package/src/core/services/git-repository.ts +16 -0
- package/src/core/services/project-files.ts +13 -1
- package/src/core/storage/factory.ts +24 -0
- package/src/core/storage/file/file-storage.ts +271 -0
- package/src/core/storage/paths.ts +64 -0
- package/src/core/storage/repo-identity.ts +70 -0
- package/src/core/storage/sqlite/mappers.ts +162 -0
- package/src/core/storage/sqlite/schema.ts +116 -0
- package/src/core/storage/sqlite/sqlite-storage.ts +431 -0
- package/src/core/storage/types.ts +152 -0
- package/src/core/storage/work-area.ts +191 -0
- package/src/index.ts +27 -6
- package/src/server/artifacts.ts +8 -61
- package/src/server/client/App.tsx +26 -9
- package/src/server/client/ArtifactsPanel.tsx +144 -23
- package/src/server/client/AutoRefresh.tsx +1 -1
- package/src/server/client/Filters.tsx +7 -2
- package/src/server/client/FindingCard.tsx +50 -15
- package/src/server/client/FindingsList.tsx +2 -2
- package/src/server/client/MarkdownText.tsx +181 -0
- package/src/server/client/ResumePanel.tsx +368 -0
- package/src/server/client/RunList.tsx +3 -3
- package/src/server/client/SubmitModal.tsx +58 -10
- package/src/server/client/SummaryBar.tsx +1 -1
- package/src/server/client/icons.tsx +17 -0
- package/src/server/client/utils/code-highlight.ts +422 -0
- package/src/server/client/utils/constants.ts +226 -0
- package/src/server/client/utils/json-highlight.ts +55 -0
- package/src/server/client/utils/markdown.ts +319 -0
- package/src/server/client/{types.ts → utils/types.ts} +81 -0
- package/src/server/index.ts +154 -16
- package/src/server/run-repository.ts +153 -136
- package/src/skill/OUTPUT-FORMAT.md +6 -4
- package/src/skill/README.md +22 -14
- package/src/skill/SETUP.md +4 -4
- package/src/skill/example-review.md +22 -14
- package/src/skill/fix.md +5 -5
- package/src/skill/hunt.md +55 -32
- package/src/skill/train.md +1 -1
- package/src/skill/verify.md +7 -6
- package/src/tests/__snapshots__/baseline.test.ts.snap +74 -61
- package/src/tests/baseline.test.ts +79 -34
- package/src/tests/cli.test.ts +40 -19
- package/src/tests/code-highlight.test.ts +123 -0
- package/src/tests/domain.test.ts +1 -1
- package/src/tests/findings-commands.test.ts +153 -131
- package/src/tests/findings-state.test.ts +56 -5
- package/src/tests/fixtures/fake-project.ts +0 -1
- package/src/tests/github-review.test.ts +409 -60
- package/src/tests/infrastructure.test.ts +33 -38
- package/src/tests/markdown.test.ts +149 -0
- package/src/tests/review-resume.test.ts +101 -0
- package/src/tests/review-scope.test.ts +302 -0
- package/src/tests/review-scripts.test.ts +81 -5
- package/src/tests/run-status-view.test.ts +94 -0
- package/src/tests/run-status.test.ts +86 -23
- package/src/tests/server.test.ts +184 -96
- package/src/tests/storage.test.ts +449 -0
- package/src/server/client/constants.ts +0 -91
- /package/src/server/client/{api.ts → utils/api.ts} +0 -0
- /package/src/server/client/{format.ts → utils/format.ts} +0 -0
|
@@ -1,28 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Repositório das runs (servidor) — leitura/escrita
|
|
2
|
+
* Repositório das runs (servidor) — leitura/escrita via storage.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* continua em `artifacts.ts`.
|
|
4
|
+
* Escopa tudo ao repositório do cwd: a UI mostra só as runs do projeto atual.
|
|
5
|
+
* O slug continua sendo o branch-slug (estável nas URLs).
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
|
-
import {
|
|
10
|
-
import { type HoundsmanConfig, resolveArtifactDir } from "../core/config";
|
|
8
|
+
import { resolve } from "node:path";
|
|
11
9
|
import {
|
|
12
10
|
applyFindingUpdate,
|
|
13
|
-
buildStateFromRunDir,
|
|
14
11
|
emptyState,
|
|
15
12
|
type FindingDecision,
|
|
16
13
|
type FindingExecState,
|
|
17
14
|
type FindingState,
|
|
18
15
|
type FindingsFilter,
|
|
19
|
-
FindingsRepository,
|
|
20
16
|
type FindingsStateFile,
|
|
21
17
|
type FindingsSummary,
|
|
22
18
|
type FindingUpdate,
|
|
23
19
|
fileReader,
|
|
24
20
|
filterFindings,
|
|
25
21
|
reorder,
|
|
22
|
+
resolveFindingId,
|
|
26
23
|
type SyncResult,
|
|
27
24
|
summarize,
|
|
28
25
|
syncState,
|
|
@@ -33,23 +30,22 @@ import {
|
|
|
33
30
|
resolvePrTarget,
|
|
34
31
|
type SubmitSelection,
|
|
35
32
|
submitFindings,
|
|
33
|
+
syncResolvedThreads,
|
|
36
34
|
} from "../core/github-review";
|
|
37
|
-
import {
|
|
38
|
-
import {
|
|
39
|
-
|
|
40
|
-
type RunStatus,
|
|
41
|
-
type RunStatusFile,
|
|
42
|
-
readRunStatus,
|
|
43
|
-
} from "../core/run-status";
|
|
35
|
+
import { pingFindingsStatus } from "../core/run-status";
|
|
36
|
+
import { systemClock } from "../core/services/clock";
|
|
37
|
+
import type { RepoIdentity, ReviewRecord, StorageProvider } from "../core/storage/types";
|
|
44
38
|
import type { Severity } from "../core/types";
|
|
45
|
-
import {
|
|
39
|
+
import { isSlug } from "./artifacts";
|
|
46
40
|
|
|
47
41
|
/** Item da lista de execuções (sidebar) — já ordenado do mais recente ao antigo. */
|
|
48
42
|
export type RunListItem = {
|
|
49
43
|
slug: string;
|
|
50
44
|
branch: string;
|
|
51
45
|
updatedAt?: string;
|
|
52
|
-
|
|
46
|
+
startedAt?: string;
|
|
47
|
+
heartbeatAt?: string;
|
|
48
|
+
status: ReviewRecord["status"];
|
|
53
49
|
summary: FindingsSummary;
|
|
54
50
|
};
|
|
55
51
|
|
|
@@ -57,7 +53,7 @@ export type RunListItem = {
|
|
|
57
53
|
export type RunDetail = {
|
|
58
54
|
slug: string;
|
|
59
55
|
branch: string;
|
|
60
|
-
status:
|
|
56
|
+
status: ReviewRecord | null;
|
|
61
57
|
manifest: unknown;
|
|
62
58
|
summary: FindingsSummary;
|
|
63
59
|
watchdogs: string[];
|
|
@@ -70,63 +66,60 @@ export type SubmitResult =
|
|
|
70
66
|
| { available: false; reason: string }
|
|
71
67
|
| { available: true; target: unknown; outcome: unknown };
|
|
72
68
|
|
|
73
|
-
/**
|
|
74
|
-
export
|
|
75
|
-
|
|
69
|
+
/** Metadados de um artefato (sem conteúdo) para a lista da UI. */
|
|
70
|
+
export type ArtifactListItem = {
|
|
71
|
+
name: string;
|
|
72
|
+
kind: string;
|
|
73
|
+
contentType: string;
|
|
74
|
+
updatedAt: string;
|
|
75
|
+
};
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
/** Acesso às runs de um repositório. */
|
|
78
|
+
export class RunRepository {
|
|
79
|
+
private readonly storage: StorageProvider;
|
|
80
|
+
private readonly repo: RepoIdentity;
|
|
80
81
|
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
constructor(storage: StorageProvider, repo: RepoIdentity) {
|
|
83
|
+
this.storage = storage;
|
|
84
|
+
this.repo = repo;
|
|
83
85
|
}
|
|
84
86
|
|
|
85
|
-
/** `true` se o slug aponta para uma run
|
|
87
|
+
/** `true` se o slug aponta para uma run do repositório atual. */
|
|
86
88
|
exists(slug: string): boolean {
|
|
87
|
-
return
|
|
89
|
+
return this.findBySlug(slug) !== null;
|
|
88
90
|
}
|
|
89
91
|
|
|
90
92
|
/** Lista as runs com resumo (mais recentes primeiro). */
|
|
91
93
|
list(): RunListItem[] {
|
|
92
|
-
return (
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
branch: state.branch || manifest?.branch || slug,
|
|
105
|
-
updatedAt: status?.updatedAt ?? state.updatedAt,
|
|
106
|
-
status: status?.status ?? null,
|
|
107
|
-
summary: summarize(state),
|
|
108
|
-
};
|
|
109
|
-
})
|
|
110
|
-
// Mais recentes primeiro (ISO 8601 compara lexicograficamente).
|
|
111
|
-
.sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""))
|
|
112
|
-
);
|
|
94
|
+
return this.storage.listReviews(this.repo.key).map((review) => {
|
|
95
|
+
const state = this.storage.readFindings(review.id);
|
|
96
|
+
return {
|
|
97
|
+
slug: review.slug,
|
|
98
|
+
branch: review.branch,
|
|
99
|
+
updatedAt: review.updatedAt,
|
|
100
|
+
startedAt: review.startedAt,
|
|
101
|
+
heartbeatAt: review.heartbeatAt,
|
|
102
|
+
status: review.status,
|
|
103
|
+
summary: summarize(state ?? emptyState(review.branch)),
|
|
104
|
+
};
|
|
105
|
+
});
|
|
113
106
|
}
|
|
114
107
|
|
|
115
108
|
/** Detalhe da run (null se o slug for inválido). */
|
|
116
109
|
detail(slug: string): RunDetail | null {
|
|
117
|
-
const
|
|
118
|
-
if (!
|
|
119
|
-
const state = this.loadState(
|
|
120
|
-
const classification =
|
|
110
|
+
const review = this.findBySlug(slug);
|
|
111
|
+
if (!review) return null;
|
|
112
|
+
const state = this.loadState(review);
|
|
113
|
+
const classification = this.readArtifactJson<{
|
|
121
114
|
watchdogs?: Record<string, string[]>;
|
|
122
115
|
unclassified?: string[];
|
|
123
116
|
deleted?: string[];
|
|
124
|
-
}>(
|
|
117
|
+
}>(review, "classification.json");
|
|
125
118
|
return {
|
|
126
|
-
slug,
|
|
127
|
-
branch:
|
|
128
|
-
status:
|
|
129
|
-
manifest:
|
|
119
|
+
slug: review.slug,
|
|
120
|
+
branch: review.branch,
|
|
121
|
+
status: review,
|
|
122
|
+
manifest: this.readArtifactJson(review, "manifest.json"),
|
|
130
123
|
summary: summarize(state),
|
|
131
124
|
watchdogs: Object.keys(classification?.watchdogs ?? {}),
|
|
132
125
|
unclassified: classification?.unclassified ?? [],
|
|
@@ -134,6 +127,43 @@ export class RunRepository {
|
|
|
134
127
|
};
|
|
135
128
|
}
|
|
136
129
|
|
|
130
|
+
/** Aplica `VACUUM`/`optimize` no backend (chamado no start do server). */
|
|
131
|
+
maintain(): void {
|
|
132
|
+
this.storage.maintain();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Fecha o provider (shutdown do server). */
|
|
136
|
+
dispose(): void {
|
|
137
|
+
this.storage.close();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// -----------------------------------------------------------------------
|
|
141
|
+
// Internos
|
|
142
|
+
// -----------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
private findBySlug(slug: string): ReviewRecord | null {
|
|
145
|
+
if (!isSlug(slug)) return null;
|
|
146
|
+
return (
|
|
147
|
+
this.storage
|
|
148
|
+
.listReviews(this.repo.key)
|
|
149
|
+
.find((review) => review.slug === slug) ?? null
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private loadState(review: ReviewRecord): FindingsStateFile {
|
|
154
|
+
return this.storage.readFindings(review.id) ?? emptyState(review.branch);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private readArtifactJson<T>(review: ReviewRecord, name: string): T | null {
|
|
158
|
+
const artifact = this.storage.readArtifact(review.id, name);
|
|
159
|
+
if (!artifact) return null;
|
|
160
|
+
try {
|
|
161
|
+
return JSON.parse(artifact.content) as T;
|
|
162
|
+
} catch {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
137
167
|
/** Findings filtrados + resumo (null se o slug for inválido). */
|
|
138
168
|
findings(
|
|
139
169
|
slug: string,
|
|
@@ -143,9 +173,9 @@ export class RunRepository {
|
|
|
143
173
|
findings: (FindingState & { absFile: string })[];
|
|
144
174
|
summary: FindingsSummary;
|
|
145
175
|
} | null {
|
|
146
|
-
const
|
|
147
|
-
if (!
|
|
148
|
-
const state = this.loadState(
|
|
176
|
+
const review = this.findBySlug(slug);
|
|
177
|
+
if (!review) return null;
|
|
178
|
+
const state = this.loadState(review);
|
|
149
179
|
const filter: FindingsFilter = {
|
|
150
180
|
decision: (params.get("decision") as FindingDecision) || undefined,
|
|
151
181
|
execState: (params.get("exec") as FindingExecState) || undefined,
|
|
@@ -155,18 +185,22 @@ export class RunRepository {
|
|
|
155
185
|
includeAbsent: params.get("all") === "1",
|
|
156
186
|
};
|
|
157
187
|
return {
|
|
158
|
-
branch:
|
|
159
|
-
findings: filterFindings(state, filter).map(
|
|
188
|
+
branch: review.branch,
|
|
189
|
+
findings: filterFindings(state, filter).map((finding) =>
|
|
190
|
+
toClientFinding(finding, review),
|
|
191
|
+
),
|
|
160
192
|
summary: summarize(state),
|
|
161
193
|
};
|
|
162
194
|
}
|
|
163
195
|
|
|
164
|
-
/** Um finding (null se ausente/inválido). */
|
|
196
|
+
/** Um finding (null se ausente/inválido). Aceita prefixo único do id. */
|
|
165
197
|
finding(slug: string, id: string): (FindingState & { absFile: string }) | null {
|
|
166
|
-
const
|
|
167
|
-
if (!
|
|
168
|
-
const
|
|
169
|
-
|
|
198
|
+
const review = this.findBySlug(slug);
|
|
199
|
+
if (!review) return null;
|
|
200
|
+
const state = this.loadState(review);
|
|
201
|
+
const resolution = resolveFindingId(state, id);
|
|
202
|
+
if (resolution.status !== "found") return null;
|
|
203
|
+
return toClientFinding(state.findings[resolution.id], review);
|
|
170
204
|
}
|
|
171
205
|
|
|
172
206
|
/** Aplica uma atualização a um finding (null se ausente/inválido). */
|
|
@@ -175,17 +209,22 @@ export class RunRepository {
|
|
|
175
209
|
id: string,
|
|
176
210
|
body: FindingUpdate,
|
|
177
211
|
): (FindingState & { absFile: string }) | null {
|
|
178
|
-
const
|
|
179
|
-
if (!
|
|
180
|
-
const
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
212
|
+
const review = this.findBySlug(slug);
|
|
213
|
+
if (!review) return null;
|
|
214
|
+
const state = this.loadState(review);
|
|
215
|
+
const resolution = resolveFindingId(state, id);
|
|
216
|
+
if (resolution.status !== "found") return null;
|
|
217
|
+
const finding = state.findings[resolution.id];
|
|
184
218
|
applyFindingUpdate(finding, body);
|
|
219
|
+
// "Done" na UI é resolução local: resolve a conversa no PR (fail-open).
|
|
220
|
+
if (body.execState === "done") {
|
|
221
|
+
syncResolvedThreads({ state, ids: [finding.id] });
|
|
222
|
+
}
|
|
185
223
|
reorder(state);
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
224
|
+
state.updatedAt = systemClock();
|
|
225
|
+
this.storage.writeFindings(review.id, state);
|
|
226
|
+
pingFindingsStatus(this.storage, review.id, state);
|
|
227
|
+
return toClientFinding(finding, review);
|
|
189
228
|
}
|
|
190
229
|
|
|
191
230
|
/** Reancora os findings contra os arquivos atuais. */
|
|
@@ -193,22 +232,21 @@ export class RunRepository {
|
|
|
193
232
|
result: SyncResult;
|
|
194
233
|
summary: FindingsSummary;
|
|
195
234
|
} | null {
|
|
196
|
-
const
|
|
197
|
-
if (!
|
|
198
|
-
const
|
|
199
|
-
const state = store.read() ?? this.buildForRun(dir, slug) ?? emptyState(slug);
|
|
235
|
+
const review = this.findBySlug(slug);
|
|
236
|
+
if (!review) return null;
|
|
237
|
+
const state = this.loadState(review);
|
|
200
238
|
const result = syncState(state, fileReader(process.cwd()));
|
|
201
|
-
|
|
202
|
-
pingFindingsStatus(
|
|
239
|
+
this.storage.writeFindings(review.id, state);
|
|
240
|
+
pingFindingsStatus(this.storage, review.id, state);
|
|
203
241
|
return { result, summary: summarize(state) };
|
|
204
242
|
}
|
|
205
243
|
|
|
206
244
|
/** Prévia do envio da revisão (candidatos + disponibilidade da PR). */
|
|
207
245
|
submitPreview(slug: string, runner: GhRunner): unknown | null {
|
|
208
|
-
const
|
|
209
|
-
if (!
|
|
210
|
-
const state = this.loadState(
|
|
211
|
-
const target = resolvePrTarget(runner,
|
|
246
|
+
const review = this.findBySlug(slug);
|
|
247
|
+
if (!review) return null;
|
|
248
|
+
const state = this.loadState(review);
|
|
249
|
+
const target = resolvePrTarget(runner, review.branch);
|
|
212
250
|
return buildSubmitPreview(state, target);
|
|
213
251
|
}
|
|
214
252
|
|
|
@@ -218,10 +256,10 @@ export class RunRepository {
|
|
|
218
256
|
selection: SubmitSelection,
|
|
219
257
|
runner: GhRunner,
|
|
220
258
|
): SubmitResult | null {
|
|
221
|
-
const
|
|
222
|
-
if (!
|
|
223
|
-
const state = this.loadState(
|
|
224
|
-
const target = resolvePrTarget(runner,
|
|
259
|
+
const review = this.findBySlug(slug);
|
|
260
|
+
if (!review) return null;
|
|
261
|
+
const state = this.loadState(review);
|
|
262
|
+
const target = resolvePrTarget(runner, review.branch);
|
|
225
263
|
if (!target.ok) return { available: false, reason: target.reason };
|
|
226
264
|
|
|
227
265
|
const outcome = submitFindings({
|
|
@@ -230,55 +268,31 @@ export class RunRepository {
|
|
|
230
268
|
runner,
|
|
231
269
|
selection,
|
|
232
270
|
});
|
|
233
|
-
this.
|
|
234
|
-
pingFindingsStatus(
|
|
271
|
+
this.storage.writeFindings(review.id, state);
|
|
272
|
+
pingFindingsStatus(this.storage, review.id, state);
|
|
235
273
|
return { available: true, target: target.target, outcome };
|
|
236
274
|
}
|
|
237
275
|
|
|
276
|
+
/** Lista os artefatos da run (sem conteúdo). */
|
|
277
|
+
artifacts(slug: string): ArtifactListItem[] | null {
|
|
278
|
+
const review = this.findBySlug(slug);
|
|
279
|
+
if (!review) return null;
|
|
280
|
+
return this.storage.listArtifacts(review.id).map((artifact) => ({
|
|
281
|
+
name: artifact.name,
|
|
282
|
+
kind: artifact.kind,
|
|
283
|
+
contentType: artifact.contentType,
|
|
284
|
+
updatedAt: artifact.updatedAt,
|
|
285
|
+
}));
|
|
286
|
+
}
|
|
287
|
+
|
|
238
288
|
/** Conteúdo de um artefato (`json`/`md`) da run (null se não servível). */
|
|
239
289
|
artifact(slug: string, name: string): { content: string; type: string } | null {
|
|
240
290
|
if (!/\.(json|md)$/i.test(name)) return null;
|
|
241
|
-
const
|
|
242
|
-
if (!
|
|
243
|
-
const
|
|
244
|
-
if (
|
|
245
|
-
|
|
246
|
-
? "application/json; charset=utf-8"
|
|
247
|
-
: "text/markdown; charset=utf-8";
|
|
248
|
-
return { content, type };
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// -----------------------------------------------------------------------
|
|
252
|
-
// Internos
|
|
253
|
-
// -----------------------------------------------------------------------
|
|
254
|
-
|
|
255
|
-
private store(dir: string, slug: string): FindingsRepository {
|
|
256
|
-
return new FindingsRepository(join(dir, STATE_FILE), slug);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
private buildForRun(dir: string, slug: string): FindingsStateFile | null {
|
|
260
|
-
const manifest = readRunJson<{ branch?: string }>(dir, "manifest.json");
|
|
261
|
-
return buildStateFromRunDir(
|
|
262
|
-
dir,
|
|
263
|
-
manifest?.branch ?? slug,
|
|
264
|
-
fileReader(process.cwd()),
|
|
265
|
-
);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* Lê o estado da run. Se ausente, reconstrói dos artefatos e PERSISTE — os ids
|
|
270
|
-
* são aleatórios, então precisam ser estáveis entre requisições.
|
|
271
|
-
*/
|
|
272
|
-
private loadState(dir: string, slug: string): FindingsStateFile {
|
|
273
|
-
const store = this.store(dir, slug);
|
|
274
|
-
const existing = store.read();
|
|
275
|
-
if (existing) return existing;
|
|
276
|
-
const built = this.buildForRun(dir, slug);
|
|
277
|
-
if (built) {
|
|
278
|
-
store.write(built);
|
|
279
|
-
return built;
|
|
280
|
-
}
|
|
281
|
-
return emptyState(slug);
|
|
291
|
+
const review = this.findBySlug(slug);
|
|
292
|
+
if (!review) return null;
|
|
293
|
+
const artifact = this.storage.readArtifact(review.id, name);
|
|
294
|
+
if (!artifact) return null;
|
|
295
|
+
return { content: artifact.content, type: artifact.contentType };
|
|
282
296
|
}
|
|
283
297
|
}
|
|
284
298
|
|
|
@@ -286,6 +300,9 @@ export class RunRepository {
|
|
|
286
300
|
* Finding + caminho absoluto. O cliente monta o link `vscode://file/...` e não
|
|
287
301
|
* conhece o cwd do servidor — o caminho relativo do relatório não basta.
|
|
288
302
|
*/
|
|
289
|
-
function toClientFinding(
|
|
290
|
-
|
|
303
|
+
function toClientFinding(
|
|
304
|
+
finding: FindingState,
|
|
305
|
+
review: ReviewRecord,
|
|
306
|
+
): FindingState & { absFile: string } {
|
|
307
|
+
return { ...finding, absFile: resolve(review.repoRoot, finding.file) };
|
|
291
308
|
}
|
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
# Formato de Saída dos Watchdogs
|
|
2
2
|
|
|
3
3
|
Todo watchdog DEVE gravar **um arquivo JSON** com seus achados. O `report-builder.ts`
|
|
4
|
-
consolida os JSONs
|
|
4
|
+
consolida os JSONs no artefato `review.md` do storage global.
|
|
5
5
|
|
|
6
6
|
> **Não** escrever markdown livre no chat. O único output consumido pelo pipeline é o JSON.
|
|
7
7
|
|
|
8
8
|
## Caminho do arquivo
|
|
9
9
|
|
|
10
|
-
O caminho exato é informado no brief do watchdog (gerado por `houndsman`)
|
|
10
|
+
O caminho exato é informado no brief do watchdog (gerado por `houndsman`). Ele aponta para o
|
|
11
|
+
staging global da review:
|
|
11
12
|
|
|
12
13
|
```
|
|
13
|
-
|
|
14
|
+
<HOUNDSMAN_HOME>/work/<reviewId>/findings/<watchdogId>.json
|
|
14
15
|
```
|
|
15
16
|
|
|
16
|
-
- `<
|
|
17
|
+
- `<reviewId>`: UUID da review (estável entre prepare/finalize).
|
|
17
18
|
- `<watchdogId>`: id do watchdog (ex.: `backend-architecture`).
|
|
19
|
+
- Use exatamente o `outputPath` do brief — não invente caminhos.
|
|
18
20
|
|
|
19
21
|
## Schema
|
|
20
22
|
|
package/src/skill/README.md
CHANGED
|
@@ -57,26 +57,33 @@ Um único skill (`/houndsman`), quatro capabilities (detalhadas em `hunt.md`, `t
|
|
|
57
57
|
### Flags
|
|
58
58
|
|
|
59
59
|
`--config <path>` · `--base <branch>` (default `main`) · `--head <ref>` (default `HEAD`) ·
|
|
60
|
-
`--branch <nome>` · `--
|
|
60
|
+
`--branch <nome>` · `--clean` · `--full` · `--no-jev` · `--fail-on-blocking` (no `--finalize`).
|
|
61
61
|
|
|
62
62
|
## Artefatos
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
Runs, findings e artefatos vivem no **storage global** (SQLite em `<HOUNDSMAN_HOME>/houndsman.db`):
|
|
65
65
|
|
|
66
|
-
|
|
|
66
|
+
| Item | Conteúdo |
|
|
67
67
|
|---|---|
|
|
68
|
+
| review | Repo (`origin` normalizado), branch, slug, base/head, commit revisado, status e timestamps |
|
|
69
|
+
| findings | Decisão/execução + fingerprint + âncora + histórico + vínculo com o GitHub |
|
|
68
70
|
| `classification.json` | Watchdogs ativados + status (A/M/D/R) + churn |
|
|
69
71
|
| `candidates.json` | Candidatos da pré-análise Jev |
|
|
70
|
-
| `
|
|
71
|
-
| `
|
|
72
|
+
| `manifest.json` | Resumo (watchdogs, arquivos, deletados, contexto, orçamento, churn) |
|
|
73
|
+
| `briefs/<watchdog>.md` | Brief enviado a cada sub-agente (auditoria) |
|
|
74
|
+
| `review.md` / `review.<watchdog>.md` | Relatório consolidado e por domínio |
|
|
75
|
+
| `verify.md` | Relatório do `findings verify` |
|
|
76
|
+
| `context.json` / `context-brief.md` / `context-summary.json` | Contexto da PR |
|
|
77
|
+
|
|
78
|
+
O **staging dos subagentes** fica em `<HOUNDSMAN_HOME>/work/<reviewId>/` e é limpo após o
|
|
79
|
+
`--finalize`. Briefs e contexto também são persistidos como artefatos (acima); apenas os
|
|
80
|
+
findings brutos existem só no staging:
|
|
81
|
+
|
|
82
|
+
| Arquivo | Conteúdo |
|
|
83
|
+
|---|---|
|
|
72
84
|
| `briefs/<watchdog>.md` | Instruções prontas para cada sub-agente (arquivos por risco) |
|
|
73
|
-
| `manifest.json` | Resumo (watchdogs, arquivos, deletados, status, contexto, orçamento, churn) |
|
|
74
85
|
| `findings/<watchdog>.json` | Saída dos sub-agentes (schema `OUTPUT-FORMAT.md`) |
|
|
75
|
-
| `
|
|
76
|
-
| `review.<watchdog>.md` | Relatório por domínio (só quando há achados) |
|
|
77
|
-
| `state.json` | Estado de triagem dos findings (decisão/execução + fingerprint + commit revisado) |
|
|
78
|
-
| `run.json` | Status do run (`reviewing`/`review_done`/`fixing`/`fix_done`) + timestamps |
|
|
79
|
-
| `verify.md` | Relatório do `findings verify` (resolvidos/persistem/incertos) |
|
|
86
|
+
| `context.json` / `context-brief.md` / `context-summary.json` | Contexto da PR |
|
|
80
87
|
|
|
81
88
|
## Estrutura
|
|
82
89
|
|
|
@@ -101,10 +108,11 @@ src/
|
|
|
101
108
|
│ ├── doc-discoverer.ts # descoberta de docs
|
|
102
109
|
│ ├── report-builder.ts # dedupe/render do relatório
|
|
103
110
|
│ ├── finding-anchor.ts # âncora/fingerprint de conteúdo + reancoragem
|
|
104
|
-
│ ├── findings-state.ts # estado de triagem
|
|
111
|
+
│ ├── findings-state.ts # estado de triagem + funções puras
|
|
105
112
|
│ ├── git-revision.ts # refs/ancestralidade + arquivo por commit + diff por intervalo
|
|
106
113
|
│ ├── verify.ts # verificação de findings (tier local + Jev)
|
|
107
|
-
│ ├── run-status.ts #
|
|
114
|
+
│ ├── run-status.ts # status da review (reviewing/review_done/fixing/fix_done)
|
|
115
|
+
│ ├── storage/ # StorageProvider (SQLite default + arquivos) + staging
|
|
108
116
|
│ ├── fix-batch.ts # agrupamento/paginação do lote (findings batch)
|
|
109
117
|
│ ├── registry-check.ts # validações anti-drift (config-driven)
|
|
110
118
|
│ ├── commands/ # context · review · watchdog · setup · update · test
|
|
@@ -118,7 +126,7 @@ src/
|
|
|
118
126
|
```
|
|
119
127
|
houndsman.config.ts # HoundsmanConfig (dados; descoberto em runtime)
|
|
120
128
|
.houndsman/
|
|
121
|
-
├── .gitignore # AUTO — ignora core/
|
|
129
|
+
├── .gitignore # AUTO — ignora core/ (nada de execução fica no projeto)
|
|
122
130
|
├── core/ # AUTO — sincronizado por `houndsman update`
|
|
123
131
|
│ ├── types.ts # snapshot dos tipos + helpers (autocontido)
|
|
124
132
|
│ └── skill/*.md # documentação do skill
|
package/src/skill/SETUP.md
CHANGED
|
@@ -38,12 +38,12 @@ A camada separa o que é **auto-gerado** do que é **do usuário**:
|
|
|
38
38
|
| `.houndsman/doc-mappings.ts` | usuário | path → documentação de contexto |
|
|
39
39
|
| `.houndsman/core/types.ts` | **auto** | Snapshot dos tipos + helpers (tipagem sem install) |
|
|
40
40
|
| `.houndsman/core/skill/*.md` | **auto** | Documentação do skill |
|
|
41
|
-
| `.houndsman/.gitignore` | **auto** | Ignora `core/`
|
|
41
|
+
| `.houndsman/.gitignore` | **auto** | Ignora `core/` (gerado) |
|
|
42
42
|
| `.agents/skills/houndsman/SKILL.md` etc. | **auto** | `SKILL.md` do skill `/houndsman` (capabilities `hunt`/`train`/`fix`) por harness |
|
|
43
43
|
|
|
44
44
|
Além disso, adiciona o script `"houndsman": "houndsman"` ao `package.json` — assim
|
|
45
|
-
`bun run houndsman [...]` funciona na raiz do projeto.
|
|
46
|
-
|
|
45
|
+
`bun run houndsman [...]` funciona na raiz do projeto. Runs, findings e artefatos ficam no
|
|
46
|
+
storage global (`<HOUNDSMAN_HOME>/houndsman.db`) — fora do projeto.
|
|
47
47
|
|
|
48
48
|
Flags: `--name <projeto>` · `--harness agents,claude|both|none` · `--force` (sobrescreve só os
|
|
49
49
|
arquivos do usuário). É **idempotente** — arquivos do usuário existentes são mantidos.
|
|
@@ -80,7 +80,7 @@ Definido em `src/core/config.ts` (pacote) e espelhado em `.houndsman/core/types.
|
|
|
80
80
|
| Campo | Descrição |
|
|
81
81
|
|---|---|
|
|
82
82
|
| `projectName` | Nome legível (prompts/docs). |
|
|
83
|
-
| `
|
|
83
|
+
| `storage` | **Opcional** — `{ provider: "sqlite" \| "file", path? }`. Default: SQLite em `<HOUNDSMAN_HOME>/houndsman.db`. |
|
|
84
84
|
| `watchdogs` | Definições dos watchdogs (incluem `classify`); os **ids são derivados** das chaves. |
|
|
85
85
|
| `exclusions` | Arquivos que nunca entram na revisão. |
|
|
86
86
|
| `docMappings` | Path → documentação de contexto. |
|
|
@@ -14,6 +14,28 @@
|
|
|
14
14
|
- Novo endpoint interno para reprocessar eventos manualmente.
|
|
15
15
|
- Registro de tentativas no handler de envio.
|
|
16
16
|
|
|
17
|
+
## Relatórios por domínio
|
|
18
|
+
|
|
19
|
+
| Watchdog | Achados | Revisados | Relatório |
|
|
20
|
+
|---|---|---|---|
|
|
21
|
+
| Arquitetura Backend | 2 | 3/3 | [`review.backend-architecture.md`](review.backend-architecture.md) |
|
|
22
|
+
| Pagamentos & Integrações | 1 | 4/4 | [`review.payments-integrations.md`](review.payments-integrations.md) |
|
|
23
|
+
| Acquirer Integrations | 2 | 3/4 | [`review.acquirer-integrations.md`](review.acquirer-integrations.md) |
|
|
24
|
+
|
|
25
|
+
## Cobertura
|
|
26
|
+
|
|
27
|
+
| Watchdog | Arquivos revisados | Findings |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| API & Contratos | 2 | 0 |
|
|
30
|
+
| Arquitetura Backend | 3 | 2 |
|
|
31
|
+
| Pagamentos & Integrações | 4 | 1 |
|
|
32
|
+
| Acquirer Integrations | 3/4 (faltam 1) | 2 |
|
|
33
|
+
|
|
34
|
+
## Conclusão
|
|
35
|
+
|
|
36
|
+
**Status:** ❌ Mudanças Necessárias
|
|
37
|
+
**Resumo:** 2 violações bloqueantes, 1 ponto de atenção, 1 sugestão e 1 nit.
|
|
38
|
+
|
|
17
39
|
## ❌ blocking
|
|
18
40
|
|
|
19
41
|
- **Webhook handler sem verifySignature** — `server/src/services/integrations/acquirer/webhook/handle-charge-event.ts:15` · _Acquirer Integrations, Pagamentos & Integrações_
|
|
@@ -42,17 +64,3 @@
|
|
|
42
64
|
- **Ordem de membros** — `server/src/services/webhook/webhook-retry-service.ts:12` · _Arquitetura Backend_
|
|
43
65
|
Campo privado declarado antes do construtor público.
|
|
44
66
|
**Ação:** Reordenar membros (públicos primeiro).
|
|
45
|
-
|
|
46
|
-
## Cobertura
|
|
47
|
-
|
|
48
|
-
| Watchdog | Arquivos revisados | Findings |
|
|
49
|
-
|---|---|---|
|
|
50
|
-
| API & Contratos | 2 | 0 |
|
|
51
|
-
| Arquitetura Backend | 3 | 1 |
|
|
52
|
-
| Pagamentos & Integrações | 4 | 2 |
|
|
53
|
-
| Acquirer Integrations | 3/4 (faltam 1) | 2 |
|
|
54
|
-
|
|
55
|
-
## Conclusão
|
|
56
|
-
|
|
57
|
-
**Status:** ❌ Mudanças Necessárias
|
|
58
|
-
**Resumo:** 2 violações bloqueantes, 1 ponto de atenção, 1 sugestão.
|
package/src/skill/fix.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
> (`.agents/skills/houndsman/SKILL.md` e `.claude/skills/houndsman/SKILL.md`).
|
|
5
5
|
|
|
6
6
|
Corrige os achados de uma revisão **já finalizada** (`hunt`): lê a triagem, aplica as
|
|
7
|
-
correções marcadas e mantém o estado de triagem em dia — **sem editar o
|
|
7
|
+
correções marcadas e mantém o estado de triagem em dia — **sem editar o banco à mão**.
|
|
8
8
|
|
|
9
9
|
Invocação: `/houndsman fix`. Não revisa nem altera regras; consome o que o `hunt` produziu.
|
|
10
10
|
|
|
@@ -15,8 +15,8 @@ Invocação: `/houndsman fix`. Não revisa nem altera regras; consome o que o `h
|
|
|
15
15
|
`findings set <id> --decision execute`.
|
|
16
16
|
- "Corrija os findings", "aplica as correções", "resolve os bloqueantes".
|
|
17
17
|
|
|
18
|
-
> **Pré-requisito:** existe
|
|
19
|
-
>
|
|
18
|
+
> **Pré-requisito:** existe uma review com findings no storage global (gerada no
|
|
19
|
+
> `--finalize`). Se não existir, a revisão não foi finalizada — rode o `hunt` antes.
|
|
20
20
|
> A triagem (`execute`) é feita na UI (`bun run houndsman server --open`) ou via `findings set`,
|
|
21
21
|
> e é o que alimenta a fila de correção.
|
|
22
22
|
|
|
@@ -153,7 +153,7 @@ Subagente serve para **isolar contexto**, não para paralelizar. Regras:
|
|
|
153
153
|
colidem.
|
|
154
154
|
- O subagente recebe **só** o grupo atual (findings + arquivo) e devolve um resumo compacto
|
|
155
155
|
(ids corrigidos / não corrigidos + motivo). Assim o agente principal não carrega o arquivo.
|
|
156
|
-
- A escrita
|
|
156
|
+
- A escrita da triagem fica **centralizada no agente principal**: o subagente apenas
|
|
157
157
|
lê/edita; `sync` / `done` / `set` são do orquestrador. Nada de `findings` dentro do subagente.
|
|
158
158
|
- Grupos pequenos (1–2 findings) o próprio agente corrige inline.
|
|
159
159
|
|
|
@@ -196,7 +196,7 @@ Arquivo: <file>
|
|
|
196
196
|
|
|
197
197
|
- ❌ Carregar a fila inteira de uma vez (`findings list` sem filtro) — use `findings batch`.
|
|
198
198
|
- ❌ Corrigir vários grupos em paralelo (subagentes simultâneos sobre arquivos que se relacionam).
|
|
199
|
-
- ❌ Editar
|
|
199
|
+
- ❌ Editar o banco/storage à mão — use os comandos `findings`.
|
|
200
200
|
- ❌ Corrigir findings `pending` ou `ignore` sem o usuário marcar `execute`.
|
|
201
201
|
- ❌ Chavear por `(file, line)` — a confiança é o **fingerprint** de conteúdo.
|
|
202
202
|
- ❌ Correção cosmética para "sumir" com o finding sem atender a `rule`.
|