@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.
- package/README.md +68 -89
- package/assets/logo-legacy.png +0 -0
- package/assets/logo.png +0 -0
- package/package.json +3 -2
- package/src/cli/index.ts +101 -10
- package/src/core/assets.ts +11 -0
- package/src/core/brief.ts +27 -5
- package/src/core/commands/findings.ts +618 -0
- package/src/core/commands/review.ts +2 -0
- package/src/core/commands/scaffold.ts +33 -44
- package/src/core/config.ts +42 -0
- package/src/core/diff-source.ts +140 -0
- package/src/core/file-classifier.ts +50 -2
- package/src/core/finding-anchor.ts +147 -0
- package/src/core/findings-state.ts +556 -0
- package/src/core/fix-batch.ts +236 -0
- package/src/core/git-revision.ts +105 -0
- package/src/core/jev/index.ts +1 -0
- package/src/core/jev/jev-client.ts +72 -11
- package/src/core/jev/jev-detector.ts +21 -19
- package/src/core/jev/jev-verify.ts +178 -0
- package/src/core/orchestrator.ts +362 -23
- package/src/core/paths.ts +9 -0
- package/src/core/registry-check.ts +15 -0
- package/src/core/report-builder.ts +118 -9
- package/src/core/run-status.ts +123 -0
- package/src/core/types.ts +10 -0
- package/src/core/verify.ts +175 -0
- package/src/core/watchdog-registry.ts +1 -2
- package/src/index.ts +78 -2
- package/src/server/artifacts.ts +67 -0
- package/src/server/client/App.tsx +158 -0
- package/src/server/client/ArtifactsPanel.tsx +48 -0
- package/src/server/client/Filters.tsx +83 -0
- package/src/server/client/FindingCard.tsx +122 -0
- package/src/server/client/FindingsList.tsx +54 -0
- package/src/server/client/RunList.tsx +52 -0
- package/src/server/client/SummaryBar.tsx +21 -0
- package/src/server/client/api.ts +37 -0
- package/src/server/client/constants.ts +94 -0
- package/src/server/client/index.tsx +12 -0
- package/src/server/client/types.ts +52 -0
- package/src/server/client-build.ts +47 -0
- package/src/server/index.ts +376 -0
- package/src/skill/AUDIT-PROTOCOL.md +2 -2
- package/src/skill/README.md +43 -11
- package/src/skill/SETUP.md +16 -4
- package/src/skill/SKILL.md +48 -0
- package/src/skill/fix.md +206 -0
- package/src/skill/{houndsman.md → hunt.md} +83 -33
- package/src/skill/{houndsman-update.md → train.md} +12 -12
- package/src/skill/verify.md +70 -0
- package/src/tests/findings-anchor.test.ts +120 -0
- package/src/tests/findings-commands.test.ts +476 -0
- package/src/tests/findings-state.test.ts +325 -0
- package/src/tests/fix-batch.test.ts +257 -0
- package/src/tests/review-scripts.test.ts +299 -0
- package/src/tests/run-status.test.ts +147 -0
- package/src/tests/scaffold.test.ts +59 -0
- package/src/tests/server.test.ts +242 -0
- 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
|
|
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
|
---
|
package/src/skill/README.md
CHANGED
|
@@ -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
|
|
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
|
-
>
|
|
101
|
-
>
|
|
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
|
|
116
|
-
> **`
|
|
147
|
+
> Para qualquer alteração de regra (pontual ou auditoria completa), use a capability
|
|
148
|
+
> **`train`**: `/houndsman train` → `.houndsman/core/skill/train.md`.
|
package/src/skill/SETUP.md
CHANGED
|
@@ -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).
|
|
11
|
-
|
|
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** |
|
|
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
|
|
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>`).
|