@payconductor/houndsman 0.1.0 → 0.1.3

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 (61) hide show
  1. package/README.md +68 -89
  2. package/assets/logo-legacy.png +0 -0
  3. package/assets/logo.png +0 -0
  4. package/package.json +3 -2
  5. package/src/cli/index.ts +101 -10
  6. package/src/core/assets.ts +11 -0
  7. package/src/core/brief.ts +27 -5
  8. package/src/core/commands/findings.ts +618 -0
  9. package/src/core/commands/review.ts +2 -0
  10. package/src/core/commands/scaffold.ts +33 -44
  11. package/src/core/config.ts +42 -0
  12. package/src/core/diff-source.ts +140 -0
  13. package/src/core/file-classifier.ts +50 -2
  14. package/src/core/finding-anchor.ts +147 -0
  15. package/src/core/findings-state.ts +556 -0
  16. package/src/core/fix-batch.ts +236 -0
  17. package/src/core/git-revision.ts +105 -0
  18. package/src/core/jev/index.ts +1 -0
  19. package/src/core/jev/jev-client.ts +72 -11
  20. package/src/core/jev/jev-detector.ts +21 -19
  21. package/src/core/jev/jev-verify.ts +178 -0
  22. package/src/core/orchestrator.ts +362 -23
  23. package/src/core/paths.ts +9 -0
  24. package/src/core/registry-check.ts +15 -0
  25. package/src/core/report-builder.ts +118 -9
  26. package/src/core/run-status.ts +123 -0
  27. package/src/core/types.ts +10 -0
  28. package/src/core/verify.ts +175 -0
  29. package/src/core/watchdog-registry.ts +1 -2
  30. package/src/index.ts +78 -2
  31. package/src/server/artifacts.ts +67 -0
  32. package/src/server/client/App.tsx +158 -0
  33. package/src/server/client/ArtifactsPanel.tsx +48 -0
  34. package/src/server/client/Filters.tsx +83 -0
  35. package/src/server/client/FindingCard.tsx +122 -0
  36. package/src/server/client/FindingsList.tsx +54 -0
  37. package/src/server/client/RunList.tsx +52 -0
  38. package/src/server/client/SummaryBar.tsx +21 -0
  39. package/src/server/client/api.ts +37 -0
  40. package/src/server/client/constants.ts +94 -0
  41. package/src/server/client/index.tsx +12 -0
  42. package/src/server/client/types.ts +52 -0
  43. package/src/server/client-build.ts +47 -0
  44. package/src/server/index.ts +376 -0
  45. package/src/skill/AUDIT-PROTOCOL.md +2 -2
  46. package/src/skill/README.md +43 -11
  47. package/src/skill/SETUP.md +16 -4
  48. package/src/skill/SKILL.md +48 -0
  49. package/src/skill/fix.md +206 -0
  50. package/src/skill/{houndsman.md → hunt.md} +83 -33
  51. package/src/skill/{houndsman-update.md → train.md} +12 -12
  52. package/src/skill/verify.md +70 -0
  53. package/src/tests/findings-anchor.test.ts +120 -0
  54. package/src/tests/findings-commands.test.ts +476 -0
  55. package/src/tests/findings-state.test.ts +325 -0
  56. package/src/tests/fix-batch.test.ts +257 -0
  57. package/src/tests/review-scripts.test.ts +299 -0
  58. package/src/tests/run-status.test.ts +147 -0
  59. package/src/tests/scaffold.test.ts +59 -0
  60. package/src/tests/server.test.ts +242 -0
  61. package/src/tests/verify.test.ts +170 -0
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Tipos compartilhados do cliente (espelham `src/core/findings-state.ts`).
3
+ *
4
+ * São um espelho minimalista do contrato da API — evita importar o núcleo
5
+ * (Node) no bundle do navegador.
6
+ */
7
+
8
+ export type Severity = "blocking" | "attention" | "suggestion" | "nit";
9
+ export type Decision = "pending" | "execute" | "ignore";
10
+ export type ExecState = "pending" | "in_progress" | "done" | "stale";
11
+
12
+ /** Estados de execução de uma run (espelham `core/run-status.ts`). */
13
+ export type RunStatus = "reviewing" | "review_done" | "fixing" | "fix_done";
14
+
15
+ export type Finding = {
16
+ id: string;
17
+ rule: string;
18
+ severity: Severity;
19
+ file: string;
20
+ line: number | null;
21
+ message: string;
22
+ action: string;
23
+ evidence?: string;
24
+ count: number;
25
+ present: boolean;
26
+ decision: Decision;
27
+ comment?: string;
28
+ execState: ExecState;
29
+ watchdogIds: string[];
30
+ };
31
+
32
+ export type Summary = {
33
+ total: number;
34
+ present: number;
35
+ byDecision: Record<Decision, number>;
36
+ byExecState: Record<ExecState, number>;
37
+ bySeverity: Record<Severity, number>;
38
+ };
39
+
40
+ export type RunSummary = {
41
+ slug: string;
42
+ branch: string;
43
+ updatedAt?: string;
44
+ status?: RunStatus | null;
45
+ summary: Summary;
46
+ };
47
+
48
+ export type FindingsResponse = {
49
+ branch: string;
50
+ findings: Finding[];
51
+ summary: Summary;
52
+ };
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Empacota o cliente Preact em memória (Bun.build) — sem build step no pacote.
3
+ *
4
+ * Sem cache por padrão: o bundle é refeito quando algum arquivo do cliente muda
5
+ * (comparação por mtime), então editar a UI não exige reiniciar o servidor. O
6
+ * navegador também não cacheia (`cache-control: no-store` + `?v=` na URL).
7
+ */
8
+
9
+ import { readdirSync, statSync } from "node:fs";
10
+ import { join } from "node:path";
11
+
12
+ let cached: string | null = null;
13
+ let cachedStamp = -1;
14
+
15
+ /** Maior mtime entre os arquivos do cliente (recursivo). */
16
+ function sourceStamp(dir: string): number {
17
+ let stamp = 0;
18
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
19
+ const full = join(dir, entry.name);
20
+ stamp = Math.max(
21
+ stamp,
22
+ entry.isDirectory() ? sourceStamp(full) : statSync(full).mtimeMs,
23
+ );
24
+ }
25
+ return stamp;
26
+ }
27
+
28
+ /** Bundle ESM do cliente (target browser). Lança se o build falhar. */
29
+ export async function buildClientBundle(): Promise<string> {
30
+ const sourceDir = join(import.meta.dir, "client");
31
+ const stamp = sourceStamp(sourceDir);
32
+ if (cached !== null && stamp === cachedStamp) return cached;
33
+
34
+ const result = await Bun.build({
35
+ entrypoints: [join(sourceDir, "index.tsx")],
36
+ target: "browser",
37
+ format: "esm",
38
+ jsx: { runtime: "automatic", importSource: "preact" },
39
+ });
40
+ if (!result.success || result.outputs.length === 0) {
41
+ const details = (result.logs ?? []).map((log) => log.message).join("\n");
42
+ throw new Error(`Falha ao empacotar o cliente do houndsman.\n${details}`);
43
+ }
44
+ cached = await result.outputs[0].text();
45
+ cachedStamp = stamp;
46
+ return cached;
47
+ }
@@ -0,0 +1,376 @@
1
+ /**
2
+ * Servidor local do houndsman (`Bun.serve`) — UI de triagem + artefatos.
3
+ *
4
+ * Serve um SPA Preact (bundle em memória) + uma API JSON sobre o `state.json`
5
+ * das runs em `.houndsman/.runs/`. Sem dependências além do Bun e do Preact.
6
+ * Bind padrão em `127.0.0.1` — ferramenta local, sem autenticação.
7
+ */
8
+
9
+ import { join } from "node:path";
10
+ import { mediaPath } from "../core/assets";
11
+ import { type HoundsmanConfig, resolveArtifactDir } from "../core/config";
12
+ import {
13
+ applyFindingUpdate,
14
+ buildStateFromRunDir,
15
+ emptyState,
16
+ type FindingDecision,
17
+ type FindingExecState,
18
+ type FindingsFilter,
19
+ type FindingsStateFile,
20
+ FindingsStateStore,
21
+ type FindingUpdate,
22
+ fileReader,
23
+ filterFindings,
24
+ reorder,
25
+ type SyncResult,
26
+ summarize,
27
+ syncState,
28
+ } from "../core/findings-state";
29
+ import { STATE_FILE } from "../core/paths";
30
+ import { pingFindingsStatus, readRunStatus } from "../core/run-status";
31
+ import type { Severity } from "../core/types";
32
+ import { listRunSlugs, readRunFile, readRunJson, runDirForSlug } from "./artifacts";
33
+ import { buildClientBundle } from "./client-build";
34
+
35
+ type BunServer = ReturnType<typeof Bun.serve>;
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Helpers de resposta
39
+ // ---------------------------------------------------------------------------
40
+
41
+ function json(data: unknown, status = 200): Response {
42
+ return new Response(JSON.stringify(data), {
43
+ status,
44
+ headers: { "content-type": "application/json; charset=utf-8" },
45
+ });
46
+ }
47
+
48
+ function text(body: string, contentType: string, status = 200): Response {
49
+ return new Response(body, { status, headers: { "content-type": contentType } });
50
+ }
51
+
52
+ /** Resposta sem cache — o servidor é local; o navegador sempre pega a versão nova. */
53
+ function noStore(body: string, contentType: string): Response {
54
+ return new Response(body, {
55
+ headers: {
56
+ "content-type": contentType,
57
+ "cache-control": "no-store, no-cache, must-revalidate",
58
+ },
59
+ });
60
+ }
61
+
62
+ function notFound(detail = "não encontrado"): Response {
63
+ return json({ error: detail }, 404);
64
+ }
65
+
66
+ // ---------------------------------------------------------------------------
67
+ // Estado / dados
68
+ // ---------------------------------------------------------------------------
69
+
70
+ function buildForRun(dir: string, slug: string): FindingsStateFile | null {
71
+ const manifest = readRunJson<{ branch?: string }>(dir, "manifest.json");
72
+ return buildStateFromRunDir(dir, manifest?.branch ?? slug, fileReader(process.cwd()));
73
+ }
74
+
75
+ /**
76
+ * Lê o estado da run. Se ausente, reconstrói dos artefatos e PERSISTE — os ids
77
+ * são aleatórios, então precisam ser estáveis entre requisições.
78
+ */
79
+ function loadState(dir: string, slug: string): FindingsStateFile {
80
+ const store = new FindingsStateStore(join(dir, STATE_FILE), slug);
81
+ const existing = store.read();
82
+ if (existing) return existing;
83
+ const built = buildForRun(dir, slug);
84
+ if (built) {
85
+ store.write(built);
86
+ return built;
87
+ }
88
+ return emptyState(slug);
89
+ }
90
+
91
+ function listRuns(artifactDir: string): unknown[] {
92
+ return listRunSlugs(artifactDir).map((slug) => {
93
+ const dir = join(artifactDir, slug);
94
+ const state = loadState(dir, slug);
95
+ const manifest = readRunJson<{ branch?: string; generatedAt?: string }>(
96
+ dir,
97
+ "manifest.json",
98
+ );
99
+ const status = readRunStatus(dir);
100
+ return {
101
+ slug,
102
+ branch: state.branch || manifest?.branch || slug,
103
+ updatedAt: status?.updatedAt ?? state.updatedAt,
104
+ status: status?.status ?? null,
105
+ summary: summarize(state),
106
+ };
107
+ });
108
+ }
109
+
110
+ function runDetail(dir: string, slug: string): unknown {
111
+ const state = loadState(dir, slug);
112
+ const classification = readRunJson<{
113
+ watchdogs?: Record<string, string[]>;
114
+ unclassified?: string[];
115
+ deleted?: string[];
116
+ }>(dir, "classification.json");
117
+ return {
118
+ slug,
119
+ branch: state.branch || slug,
120
+ status: readRunStatus(dir),
121
+ manifest: readRunJson(dir, "manifest.json"),
122
+ summary: summarize(state),
123
+ watchdogs: Object.keys(classification?.watchdogs ?? {}),
124
+ unclassified: classification?.unclassified ?? [],
125
+ deleted: classification?.deleted ?? [],
126
+ };
127
+ }
128
+
129
+ function findingsFor(dir: string, slug: string, params: URLSearchParams): unknown {
130
+ const state = loadState(dir, slug);
131
+ const filter: FindingsFilter = {
132
+ decision: (params.get("decision") as FindingDecision) || undefined,
133
+ execState: (params.get("exec") as FindingExecState) || undefined,
134
+ severity: (params.get("severity") as Severity) || undefined,
135
+ watchdog: params.get("watchdog") || undefined,
136
+ file: params.get("file") || undefined,
137
+ includeAbsent: params.get("all") === "1",
138
+ };
139
+ return {
140
+ branch: state.branch,
141
+ findings: filterFindings(state, filter),
142
+ summary: summarize(state),
143
+ };
144
+ }
145
+
146
+ async function updateFinding(
147
+ dir: string,
148
+ slug: string,
149
+ id: string,
150
+ request: Request,
151
+ ): Promise<Response> {
152
+ const body = (await request.json().catch(() => ({}))) as FindingUpdate;
153
+ const store = new FindingsStateStore(join(dir, STATE_FILE), slug);
154
+ const state = store.read() ?? buildForRun(dir, slug) ?? emptyState(slug);
155
+ const finding = state.findings[id];
156
+ if (!finding) return notFound(`finding '${id}' não encontrado`);
157
+ applyFindingUpdate(finding, body);
158
+ reorder(state);
159
+ store.write(state);
160
+ pingFindingsStatus(dir, state);
161
+ return json({ finding });
162
+ }
163
+
164
+ function syncRun(dir: string, slug: string): Response {
165
+ const store = new FindingsStateStore(join(dir, STATE_FILE), slug);
166
+ const state = store.read() ?? buildForRun(dir, slug) ?? emptyState(slug);
167
+ const result: SyncResult = syncState(state, fileReader(process.cwd()));
168
+ store.write(state);
169
+ pingFindingsStatus(dir, state);
170
+ return json({ result, summary: summarize(state) });
171
+ }
172
+
173
+ function serveArtifact(dir: string, name: string): Response {
174
+ if (!/\.(json|md)$/i.test(name)) return notFound("tipo de artefato não servido");
175
+ const content = readRunFile(dir, name);
176
+ if (content === null) return notFound(`artefato '${name}' não encontrado`);
177
+ const type = name.toLowerCase().endsWith(".json")
178
+ ? "application/json; charset=utf-8"
179
+ : "text/markdown; charset=utf-8";
180
+ return text(content, type);
181
+ }
182
+
183
+ // ---------------------------------------------------------------------------
184
+ // Rotas
185
+ // ---------------------------------------------------------------------------
186
+
187
+ const API_RUN = /^\/api\/runs\/([^/]+)(\/.*)?$/;
188
+ const LOGO_FILE = "logo.png";
189
+
190
+ /** Serve a logo do pacote (`assets/logo.png`). */
191
+ async function serveLogo(): Promise<Response> {
192
+ const file = Bun.file(mediaPath(LOGO_FILE));
193
+ if (!(await file.exists())) return notFound("logo não encontrada");
194
+ return new Response(file, {
195
+ headers: { "cache-control": "no-store" },
196
+ });
197
+ }
198
+
199
+ async function route(request: Request, artifactDir: string): Promise<Response> {
200
+ const url = new URL(request.url);
201
+ const path = url.pathname;
202
+
203
+ if (path === "/" || path === "/index.html") {
204
+ return noStore(renderShell(), "text/html; charset=utf-8");
205
+ }
206
+ if (path === "/assets/app.js") {
207
+ return noStore(await buildClientBundle(), "text/javascript; charset=utf-8");
208
+ }
209
+ if (path === `/assets/${LOGO_FILE}`) return serveLogo();
210
+ if (path === "/api/health") return json({ ok: true });
211
+
212
+ if (path === "/api/runs" && request.method === "GET") {
213
+ return json({ runs: listRuns(artifactDir) });
214
+ }
215
+
216
+ const match = path.match(API_RUN);
217
+ if (!match) return notFound();
218
+ const slug = decodeURIComponent(match[1]);
219
+ const rest = match[2] ?? "";
220
+ const dir = runDirForSlug(artifactDir, slug);
221
+ if (!dir) return notFound(`run '${slug}' não encontrada`);
222
+
223
+ if (rest === "" || rest === "/") return json(runDetail(dir, slug));
224
+
225
+ if (rest === "/findings" && request.method === "GET") {
226
+ return json(findingsFor(dir, slug, url.searchParams));
227
+ }
228
+ if (rest === "/sync" && request.method === "POST") return syncRun(dir, slug);
229
+
230
+ const findingMatch = rest.match(/^\/findings\/([^/]+)$/);
231
+ if (findingMatch) {
232
+ const id = decodeURIComponent(findingMatch[1]);
233
+ if (request.method === "GET") {
234
+ const state = loadState(dir, slug);
235
+ const finding = state.findings[id];
236
+ return finding
237
+ ? json({ finding })
238
+ : notFound(`finding '${id}' não encontrado`);
239
+ }
240
+ if (request.method === "PATCH" || request.method === "POST") {
241
+ return updateFinding(dir, slug, id, request);
242
+ }
243
+ }
244
+
245
+ const artifactMatch = rest.match(/^\/artifacts\/(.+)$/);
246
+ if (artifactMatch && request.method === "GET") {
247
+ return serveArtifact(dir, decodeURIComponent(artifactMatch[1]));
248
+ }
249
+
250
+ return notFound();
251
+ }
252
+
253
+ /** Cria o handler de requisições (testável sem abrir porta). */
254
+ export function createRequestHandler(
255
+ config: HoundsmanConfig,
256
+ ): (request: Request) => Promise<Response> {
257
+ const artifactDir = resolveArtifactDir(config);
258
+ return async (request) => {
259
+ try {
260
+ return await route(request, artifactDir);
261
+ } catch (error) {
262
+ return json(
263
+ { error: error instanceof Error ? error.message : String(error) },
264
+ 500,
265
+ );
266
+ }
267
+ };
268
+ }
269
+
270
+ export type ServerOptions = {
271
+ config: HoundsmanConfig;
272
+ port?: number;
273
+ host?: string;
274
+ };
275
+
276
+ /** Sobe o servidor e devolve a instância + URL. */
277
+ export function startServer(options: ServerOptions): { server: BunServer; url: string } {
278
+ const host = options.host ?? "127.0.0.1";
279
+ const port = options.port ?? 7331;
280
+ const server = Bun.serve({
281
+ hostname: host,
282
+ port,
283
+ fetch: createRequestHandler(options.config),
284
+ });
285
+ return { server, url: `http://${host}:${server.port}` };
286
+ }
287
+
288
+ // ---------------------------------------------------------------------------
289
+ // Shell (HTML + CSS)
290
+ // ---------------------------------------------------------------------------
291
+
292
+ const STYLES = `
293
+ :root { color-scheme: dark; --bg:#161616; --panel:#1e1e1e; --border:#2f2f2f; --fg:#e6e6e6; --muted:#9aa0a6; --accent:#4c8dff; }
294
+ * { box-sizing: border-box; }
295
+ body { margin:0; font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; background:var(--bg); color:var(--fg); }
296
+ code, pre { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
297
+ .app { display:flex; min-height:100vh; }
298
+ .sidebar { width:240px; border-right:1px solid var(--border); background:var(--panel); padding:16px; }
299
+ .brand { display:flex; align-items:center; gap:10px; margin-bottom:10px; }
300
+ .brand .logo { width:40px; height:40px; object-fit:contain; border-radius:8px; background:#111; border:1px solid var(--border); }
301
+ .sidebar h1 { font-size:16px; margin:0; }
302
+ .sub { color:var(--muted); font-size:12px; margin:2px 0 12px; }
303
+ .brand .sub { margin:0; }
304
+ .runs { display:flex; flex-direction:column; gap:4px; }
305
+ .run { text-align:left; background:transparent; border:1px solid transparent; border-radius:6px; color:var(--fg); padding:8px; cursor:pointer; display:flex; flex-direction:column; gap:2px; }
306
+ .run:hover { background:#262626; }
307
+ .run.active { border-color:var(--accent); background:#22304d; }
308
+ .run-branch { font-weight:600; font-size:13px; }
309
+ .run-meta { color:var(--muted); font-size:11px; }
310
+ .run-status { font-size:10px; text-transform:uppercase; letter-spacing:.04em; color:var(--muted); }
311
+ .status-reviewing, .status-fixing { color:#e3a008; }
312
+ .status-review_done { color:#4c8dff; }
313
+ .status-fix_done { color:#3fb950; }
314
+ .status-timeout { color:#e5484d; }
315
+ .main { flex:1; padding:16px 20px; max-width:980px; }
316
+ .topbar { display:flex; justify-content:space-between; align-items:center; margin-bottom:12px; }
317
+ .tabs { display:flex; gap:6px; flex-wrap:wrap; }
318
+ .btn { background:#262626; color:var(--fg); border:1px solid var(--border); border-radius:6px; padding:5px 10px; font-size:12px; cursor:pointer; }
319
+ .btn:hover { border-color:var(--accent); }
320
+ .btn.active { background:#22304d; border-color:var(--accent); }
321
+ .btn:disabled { opacity:.5; cursor:not-allowed; }
322
+ .summary-bar { display:flex; gap:14px; flex-wrap:wrap; color:var(--muted); font-size:12px; border:1px solid var(--border); border-radius:6px; padding:8px 10px; margin-bottom:10px; }
323
+ .filters { display:flex; gap:8px; flex-wrap:wrap; margin-bottom:10px; }
324
+ .filters input[type=text], .filters input:not([type]), .filters select { background:#1b1b1b; color:var(--fg); border:1px solid var(--border); border-radius:6px; padding:6px 8px; font-size:12px; }
325
+ .filters input:not([type]) { min-width:240px; }
326
+ .toggle { color:var(--muted); font-size:12px; display:flex; align-items:center; gap:6px; }
327
+ .caption { color:var(--muted); font-size:12px; line-height:1.5; margin:0 0 12px; }
328
+ .caption code { background:#242424; padding:1px 5px; border-radius:4px; }
329
+ .sev-group h2 { font-size:13px; text-transform:uppercase; letter-spacing:.04em; color:var(--muted); }
330
+ .sev-group h2 .n { color:#666; }
331
+ .card { background:var(--panel); border:1px solid var(--border); border-left:3px solid var(--border); border-radius:8px; padding:12px 14px; margin-bottom:10px; }
332
+ .card.sev-blocking { border-left-color:#e5484d; }
333
+ .card.sev-attention { border-left-color:#e3a008; }
334
+ .card.sev-suggestion { border-left-color:#4c8dff; }
335
+ .card.sev-nit { border-left-color:#666; }
336
+ .card.absent { opacity:.55; }
337
+ .card-head { display:flex; align-items:center; gap:10px; font-size:12px; color:var(--muted); }
338
+ .card-head .loc { color:var(--fg); }
339
+ .card-head .spacer { flex:1; }
340
+ .card-head .id { color:#7c7c7c; }
341
+ .rule { font-size:14px; margin:8px 0 4px; }
342
+ .msg { margin:2px 0; font-size:13px; }
343
+ .action { margin:2px 0; font-size:13px; color:#cfcfcf; }
344
+ .evidence { background:#111; border:1px solid var(--border); border-radius:6px; padding:8px; font-size:12px; overflow:auto; }
345
+ .card-foot { display:flex; gap:12px; color:var(--muted); font-size:11px; margin:6px 0; }
346
+ .decisions { display:flex; gap:6px; flex-wrap:wrap; margin:6px 0; }
347
+ .dec-execute.active { background:#153b21; border-color:#2ea043; }
348
+ .dec-ignore.active { background:#3a2323; border-color:#e5484d; }
349
+ .comment-row textarea { width:100%; min-height:44px; background:#1b1b1b; color:var(--fg); border:1px solid var(--border); border-radius:6px; padding:6px 8px; font-size:12px; }
350
+ .comment-actions { display:flex; gap:6px; margin-top:6px; }
351
+ .btn.done.active { background:#153b21; border-color:#2ea043; }
352
+ .artifacts .tabs { margin-bottom:10px; }
353
+ .artifact-desc { color:var(--muted); font-size:12px; line-height:1.5; margin:0 0 10px; }
354
+ .artifact-view { background:#111; border:1px solid var(--border); border-radius:6px; padding:12px; font-size:12px; overflow:auto; max-height:70vh; }
355
+ .error { color:#ff7b72; font-size:13px; }
356
+ .empty { color:var(--muted); font-size:13px; }
357
+ `;
358
+
359
+ /** Id de build por processo — muda a URL do bundle a cada reinício do servidor. */
360
+ const BUILD_ID = Date.now().toString(36);
361
+
362
+ function renderShell(): string {
363
+ return `<!doctype html>
364
+ <html lang="pt-BR">
365
+ <head>
366
+ <meta charset="utf-8" />
367
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
368
+ <title>Houndsman — Revisões</title>
369
+ <style>${STYLES}</style>
370
+ </head>
371
+ <body>
372
+ <div id="app"></div>
373
+ <script type="module" src="/assets/app.js?v=${BUILD_ID}"></script>
374
+ </body>
375
+ </html>`;
376
+ }
@@ -1,7 +1,7 @@
1
1
  # Protocolo de Auditoria dos Watchdogs — houndsman
2
2
 
3
- Referência do Modo B do skill `houndsman-update`. Objetivo: garantir que as regras dos
4
- watchdogs validam **apenas padrões que o projeto aplica hoje**. Regras "utópicas" (não
3
+ Referência do Modo B da capability `train` (skill `/houndsman`). Objetivo: garantir que as
4
+ regras dos watchdogs validam **apenas padrões que o projeto aplica hoje**. Regras "utópicas" (não
5
5
  aplicadas em nenhuma parte do código) devem ser **removidas**, não apenas rebaixadas.
6
6
 
7
7
  ---
@@ -2,10 +2,28 @@
2
2
 
3
3
  Revisão automatizada de Pull Requests — CLI independente + camada de configuração do projeto.
4
4
 
5
+ ## Capabilities
6
+
7
+ Um único skill (`/houndsman`), quatro capabilities (detalhadas em `hunt.md`, `train.md`,
8
+ `fix.md`, `verify.md`):
9
+
10
+ | Capability | O que faz |
11
+ |---|---|
12
+ | `hunt` | Revisão de PR (Passos 0–3; incremental quando possível). |
13
+ | `train` | Manutenção das regras dos watchdogs. |
14
+ | `fix` | Correção dos findings marcados como `execute`. |
15
+ | `verify` | Confere se as violações foram resolvidas, sem re-revisar a PR. |
16
+
5
17
  ## Terminologia
6
18
 
7
19
  - **houndsman** — o orquestrador/treinador (CLI + motor).
8
20
  - **watchdog** — cada agente especialista por domínio que audita o diff.
21
+ - **brief** — instruções prontas para um watchdog (o prompt do subagente).
22
+ - **findings** — os achados que o subagente grava em JSON (único output consumido).
23
+ - **candidato (Jev)** — indício automático; **não** é achado (o watchdog confirma ou refuta).
24
+ - **churn** — linhas adicionadas/removidas por arquivo; usado para priorizar a leitura.
25
+ - **budget / orçamento** — teto de arquivos por brief; o excedente ("fora do orçamento")
26
+ não é revisado, mas fica registrado em `skipped`.
9
27
  - **review/revisão** — o processo de revisão.
10
28
 
11
29
  ## Quem executa o quê
@@ -29,15 +47,17 @@ Revisão automatizada de Pull Requests — CLI independente + camada de configur
29
47
  | `houndsman` | Passo 1 — preparar (gera os briefs) |
30
48
  | `houndsman --finalize` | Passo 3 — gerar o relatório |
31
49
  | `houndsman watchdog <id>` | Contexto de um watchdog (`--json`, `--rules`) |
50
+ | `houndsman findings <sub>` | Triagem/correção dos findings (`list`/`set`/`done`/`sync`/`batch`/`verify`/...) |
51
+ | `houndsman server` | UI local de triagem (Bun.serve + Preact) |
32
52
  | `houndsman setup` | Cria a configuração inicial do projeto |
33
53
  | `houndsman update` | Sincroniza os arquivos auto-gerados (`.houndsman/core`) |
34
- | `houndsman doctor` | Valida a configuração e o ambiente (anti-drift) |
54
+ | `houndsman doctor` | Valida a configuração, o ambiente e a credencial Jev (anti-drift) |
35
55
  | `houndsman test` | Roda a suíte de testes da lib |
36
56
 
37
57
  ### Flags
38
58
 
39
59
  `--config <path>` · `--base <branch>` (default `main`) · `--head <ref>` (default `HEAD`) ·
40
- `--branch <nome>` · `--dir <dir>` · `--clean` · `--no-jev` · `--fail-on-blocking` (no `--finalize`).
60
+ `--branch <nome>` · `--dir <dir>` · `--clean` · `--full` · `--no-jev` · `--fail-on-blocking` (no `--finalize`).
41
61
 
42
62
  ## Artefatos
43
63
 
@@ -45,14 +65,18 @@ Tudo em `.houndsman/.runs/<branch-slug>/` (default do `artifactDir`, `.houndsman
45
65
 
46
66
  | Arquivo | Conteúdo |
47
67
  |---|---|
48
- | `classification.json` | Watchdogs ativados + status (A/M/D/R) |
68
+ | `classification.json` | Watchdogs ativados + status (A/M/D/R) + churn |
49
69
  | `candidates.json` | Candidatos da pré-análise Jev |
50
70
  | `context.json` | Payload bruto da PR via `gh` (ou marcador de indisponível) |
51
71
  | `context-summary.json` | Resumo do subagente (objetivo/motivação/alterações) |
52
- | `briefs/<watchdog>.md` | Instruções prontas para cada sub-agente |
53
- | `manifest.json` | Resumo (watchdogs, arquivos, deletados, status, contexto) |
72
+ | `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) |
54
74
  | `findings/<watchdog>.json` | Saída dos sub-agentes (schema `OUTPUT-FORMAT.md`) |
55
- | `review.md` | Relatório final |
75
+ | `review.md` | Relatório final (achados agrupados por arquivo) |
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) |
56
80
 
57
81
  ## Estrutura
58
82
 
@@ -72,9 +96,16 @@ src/
72
96
  │ ├── orchestrator.ts # preparar/finalizar
73
97
  │ ├── brief.ts # defaultRenderBrief + PROCEDURE
74
98
  │ ├── pr-context.ts # contexto da PR via gh
75
- │ ├── file-classifier.ts # classificação (via `classify` dos watchdogs)
99
+ │ ├── file-classifier.ts # classificação (via `classify` dos watchdogs) + churn
100
+ │ ├── diff-source.ts # diff único (patch + --numstat) p/ churn/orçamento
76
101
  │ ├── doc-discoverer.ts # descoberta de docs
77
102
  │ ├── report-builder.ts # dedupe/render do relatório
103
+ │ ├── finding-anchor.ts # âncora/fingerprint de conteúdo + reancoragem
104
+ │ ├── findings-state.ts # estado de triagem (state.json) + store atômico
105
+ │ ├── git-revision.ts # refs/ancestralidade + arquivo por commit + diff por intervalo
106
+ │ ├── verify.ts # verificação de findings (tier local + Jev)
107
+ │ ├── run-status.ts # run.json (reviewing/review_done/fixing/fix_done)
108
+ │ ├── fix-batch.ts # agrupamento/paginação do lote (findings batch)
78
109
  │ ├── registry-check.ts # validações anti-drift (config-driven)
79
110
  │ ├── commands/ # context · review · watchdog · setup · update · test
80
111
  │ └── jev/ # subsistema Jev (Passo 1.5)
@@ -97,8 +128,9 @@ houndsman.config.ts # HoundsmanConfig (dados; descoberto em runtime)
97
128
  └── doc-mappings.ts # USUÁRIO
98
129
  ```
99
130
 
100
- > Os `SKILL.md` em `.agents/skills/houndsman/` e `.claude/skills/houndsman/` são
101
- > ponteiros para `.houndsman/core/skill/houndsman.md`.
131
+ > O `SKILL.md` do skill (`.agents/skills/houndsman/SKILL.md` e
132
+ > `.claude/skills/houndsman/SKILL.md`) é gerado a partir de `src/skill/SKILL.md`
133
+ > pelo `houndsman setup`/`update` (com `{{projectName}}` resolvido).
102
134
 
103
135
  ## Manutenção
104
136
 
@@ -112,5 +144,5 @@ houndsman.config.ts # HoundsmanConfig (dados; descoberto em runtime)
112
144
  `.houndsman/watchdogs/<id>.ts` + entrada em `.houndsman/watchdogs/index.ts`.
113
145
  2. Rodar `bun run houndsman doctor` — valida a config (definições, classificação, specs Jev).
114
146
 
115
- > Para qualquer alteração de regra (pontual ou auditoria completa), use o skill
116
- > **`houndsman-update`**: `.houndsman/core/skill/houndsman-update.md`.
147
+ > Para qualquer alteração de regra (pontual ou auditoria completa), use a capability
148
+ > **`train`**: `/houndsman train` → `.houndsman/core/skill/train.md`.
@@ -7,8 +7,8 @@ no seu projeto é **autocontida** — não exige nenhuma dependência local do p
7
7
 
8
8
  - **Bun** — o CLI e o motor usam a runtime Bun (execução de TS, `Bun.spawnSync`).
9
9
  - **git** — o diff usa `git diff <base>...<head> --name-status`.
10
- - Opcional: **`gh`** (contexto da PR) e **`TYPESAFE_API_KEY`** (pré-análise Jev). Ambos são
11
- **falha-aberto** — sem eles o pipeline segue normalmente.
10
+ - Opcional: **`gh`** (contexto da PR) e **`TYPESAFE_API_KEY`** (pré-análise Jev). O pipeline
11
+ segue normalmente sem eles.
12
12
 
13
13
  ## Instalação (global)
14
14
 
@@ -39,7 +39,7 @@ A camada separa o que é **auto-gerado** do que é **do usuário**:
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
41
  | `.houndsman/.gitignore` | **auto** | Ignora `core/` e `.runs/` (gerados/temporários) |
42
- | `.agents/skills/houndsman/SKILL.md` etc. | **auto** | Ponteiros de skill dos harnesses |
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
45
  `bun run houndsman [...]` funciona na raiz do projeto. Os artefatos ficam em
@@ -59,11 +59,21 @@ bun run houndsman update # reescreve .houndsman/core/** e .houndsman/.gitig
59
59
  ## Verificar
60
60
 
61
61
  ```bash
62
- bun run houndsman doctor # valida config + ambiente (git/gh/Jev)
62
+ bun run houndsman doctor # valida config + ambiente + credencial Jev
63
63
  bun run houndsman test # suíte da lib (project-agnóstica)
64
64
  bun run houndsman --base main # prepara briefs do diff atual
65
65
  ```
66
66
 
67
+ ## Capabilities do skill
68
+
69
+ Depois do setup, invoque o skill `/houndsman` no seu harness:
70
+
71
+ | Capability | Para quê |
72
+ |---|---|
73
+ | `/houndsman hunt` | Revisar a PR (default: `/houndsman`). |
74
+ | `/houndsman train` | Adicionar/ajustar/remover regras dos watchdogs. |
75
+ | `/houndsman fix` | Corrigir os findings marcados como `execute`. |
76
+
67
77
  ## Contrato do motor (`HoundsmanConfig`)
68
78
 
69
79
  Definido em `src/core/config.ts` (pacote) e espelhado em `.houndsman/core/types.ts` (gerado):
@@ -76,6 +86,8 @@ Definido em `src/core/config.ts` (pacote) e espelhado em `.houndsman/core/types.
76
86
  | `exclusions` | Arquivos que nunca entram na revisão. |
77
87
  | `docMappings` | Path → documentação de contexto. |
78
88
  | `alwaysIncludeDocs` | Docs sempre presentes nos briefs (default `["AGENTS.md"]`). |
89
+ | `budgets` | **Opcional** — teto de arquivos por brief (`maxFilesPerBrief`, default `40`); o excedente vira "fora do orçamento" (registrar em `skipped`). |
90
+ | `concurrency` | **Opcional** — chamadas Jev simultâneas (`jev`, default `5`). |
79
91
 
80
92
  > O motor **não contém** ids, paths ou regras do projeto — tudo chega por `HoundsmanConfig`,
81
93
  > carregado em runtime a partir de `houndsman.config.ts` (ou `--config <path>`).