@payconductor/houndsman 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +18 -6
  2. package/package.json +1 -1
  3. package/src/cli/commands/context-command.ts +11 -9
  4. package/src/cli/commands/doctor-command.ts +5 -0
  5. package/src/cli/commands/findings-command.ts +13 -3
  6. package/src/cli/commands/review-command.ts +1 -2
  7. package/src/cli/help.ts +7 -3
  8. package/src/cli/index.ts +2 -2
  9. package/src/core/commands/context.ts +29 -13
  10. package/src/core/commands/findings.ts +155 -120
  11. package/src/core/commands/review.ts +7 -8
  12. package/src/core/commands/scaffold.ts +15 -16
  13. package/src/core/config.ts +16 -7
  14. package/src/core/engine/houndsman-engine.ts +15 -1
  15. package/src/core/engine/review-session.ts +37 -9
  16. package/src/core/findings-state.ts +32 -71
  17. package/src/core/jev/jev-candidates.ts +49 -35
  18. package/src/core/orchestrator.ts +175 -128
  19. package/src/core/paths.ts +23 -17
  20. package/src/core/pr-context.ts +19 -5
  21. package/src/core/project-config.ts +4 -5
  22. package/src/core/renderers/manifest-writer.ts +29 -18
  23. package/src/core/run-layout.ts +4 -104
  24. package/src/core/run-status.ts +28 -53
  25. package/src/core/services/git-repository.ts +16 -0
  26. package/src/core/services/project-files.ts +13 -1
  27. package/src/core/storage/factory.ts +24 -0
  28. package/src/core/storage/file/file-storage.ts +269 -0
  29. package/src/core/storage/paths.ts +64 -0
  30. package/src/core/storage/repo-identity.ts +70 -0
  31. package/src/core/storage/sqlite/mappers.ts +160 -0
  32. package/src/core/storage/sqlite/schema.ts +108 -0
  33. package/src/core/storage/sqlite/sqlite-storage.ts +427 -0
  34. package/src/core/storage/types.ts +143 -0
  35. package/src/core/storage/work-area.ts +191 -0
  36. package/src/index.ts +27 -6
  37. package/src/server/artifacts.ts +8 -61
  38. package/src/server/client/ArtifactsPanel.tsx +137 -23
  39. package/src/server/client/FindingCard.tsx +37 -6
  40. package/src/server/client/MarkdownText.tsx +101 -0
  41. package/src/server/client/SubmitModal.tsx +9 -4
  42. package/src/server/client/code-highlight.ts +422 -0
  43. package/src/server/client/constants.ts +130 -24
  44. package/src/server/client/json-highlight.ts +55 -0
  45. package/src/server/client/markdown.ts +153 -0
  46. package/src/server/client/types.ts +23 -0
  47. package/src/server/index.ts +95 -15
  48. package/src/server/run-repository.ts +144 -136
  49. package/src/skill/OUTPUT-FORMAT.md +6 -4
  50. package/src/skill/README.md +22 -14
  51. package/src/skill/SETUP.md +4 -4
  52. package/src/skill/fix.md +5 -5
  53. package/src/skill/hunt.md +27 -18
  54. package/src/skill/train.md +1 -1
  55. package/src/skill/verify.md +5 -5
  56. package/src/tests/__snapshots__/baseline.test.ts.snap +44 -33
  57. package/src/tests/baseline.test.ts +76 -34
  58. package/src/tests/cli.test.ts +40 -19
  59. package/src/tests/code-highlight.test.ts +123 -0
  60. package/src/tests/domain.test.ts +1 -1
  61. package/src/tests/findings-commands.test.ts +153 -131
  62. package/src/tests/findings-state.test.ts +6 -3
  63. package/src/tests/fixtures/fake-project.ts +0 -1
  64. package/src/tests/infrastructure.test.ts +33 -38
  65. package/src/tests/markdown.test.ts +89 -0
  66. package/src/tests/review-scripts.test.ts +8 -5
  67. package/src/tests/run-status.test.ts +33 -23
  68. package/src/tests/server.test.ts +108 -75
  69. package/src/tests/storage.test.ts +386 -0
@@ -1,28 +1,25 @@
1
1
  /**
2
- * Repositório das runs (servidor) — leitura/escrita dos artefatos e estado.
2
+ * Repositório das runs (servidor) — leitura/escrita via storage.
3
3
  *
4
- * Concentra o acesso a disco usado pelas rotas: lista de runs, detalhe,
5
- * findings, sync, envio à PR e artefatos. A validação de slug (path traversal)
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 { join, resolve } from "node:path";
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,
@@ -34,22 +31,18 @@ import {
34
31
  type SubmitSelection,
35
32
  submitFindings,
36
33
  } from "../core/github-review";
37
- import { STATE_FILE } from "../core/paths";
38
- import {
39
- pingFindingsStatus,
40
- type RunStatus,
41
- type RunStatusFile,
42
- readRunStatus,
43
- } from "../core/run-status";
34
+ import { pingFindingsStatus } from "../core/run-status";
35
+ import { systemClock } from "../core/services/clock";
36
+ import type { RepoIdentity, ReviewRecord, StorageProvider } from "../core/storage/types";
44
37
  import type { Severity } from "../core/types";
45
- import { listRunSlugs, readRunFile, readRunJson, runDirForSlug } from "./artifacts";
38
+ import { isSlug } from "./artifacts";
46
39
 
47
40
  /** Item da lista de execuções (sidebar) — já ordenado do mais recente ao antigo. */
48
41
  export type RunListItem = {
49
42
  slug: string;
50
43
  branch: string;
51
44
  updatedAt?: string;
52
- status: RunStatus | null;
45
+ status: ReviewRecord["status"];
53
46
  summary: FindingsSummary;
54
47
  };
55
48
 
@@ -57,7 +50,7 @@ export type RunListItem = {
57
50
  export type RunDetail = {
58
51
  slug: string;
59
52
  branch: string;
60
- status: RunStatusFile | null;
53
+ status: ReviewRecord | null;
61
54
  manifest: unknown;
62
55
  summary: FindingsSummary;
63
56
  watchdogs: string[];
@@ -70,63 +63,58 @@ export type SubmitResult =
70
63
  | { available: false; reason: string }
71
64
  | { available: true; target: unknown; outcome: unknown };
72
65
 
73
- /** Acesso às runs de um diretório de artefatos. */
74
- export class RunRepository {
75
- private readonly artifactDir: string;
66
+ /** Metadados de um artefato (sem conteúdo) para a lista da UI. */
67
+ export type ArtifactListItem = {
68
+ name: string;
69
+ kind: string;
70
+ contentType: string;
71
+ updatedAt: string;
72
+ };
76
73
 
77
- constructor(artifactDir: string) {
78
- this.artifactDir = artifactDir;
79
- }
74
+ /** Acesso às runs de um repositório. */
75
+ export class RunRepository {
76
+ private readonly storage: StorageProvider;
77
+ private readonly repo: RepoIdentity;
80
78
 
81
- static fromConfig(config: HoundsmanConfig): RunRepository {
82
- return new RunRepository(resolveArtifactDir(config));
79
+ constructor(storage: StorageProvider, repo: RepoIdentity) {
80
+ this.storage = storage;
81
+ this.repo = repo;
83
82
  }
84
83
 
85
- /** `true` se o slug aponta para uma run válida. */
84
+ /** `true` se o slug aponta para uma run do repositório atual. */
86
85
  exists(slug: string): boolean {
87
- return runDirForSlug(this.artifactDir, slug) !== null;
86
+ return this.findBySlug(slug) !== null;
88
87
  }
89
88
 
90
89
  /** Lista as runs com resumo (mais recentes primeiro). */
91
90
  list(): RunListItem[] {
92
- return (
93
- listRunSlugs(this.artifactDir)
94
- .map((slug) => {
95
- const dir = join(this.artifactDir, slug);
96
- const state = this.loadState(dir, slug);
97
- const manifest = readRunJson<{
98
- branch?: string;
99
- generatedAt?: string;
100
- }>(dir, "manifest.json");
101
- const status = readRunStatus(dir);
102
- return {
103
- slug,
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
- );
91
+ return this.storage.listReviews(this.repo.key).map((review) => {
92
+ const state = this.storage.readFindings(review.id);
93
+ return {
94
+ slug: review.slug,
95
+ branch: review.branch,
96
+ updatedAt: review.updatedAt,
97
+ status: review.status,
98
+ summary: summarize(state ?? emptyState(review.branch)),
99
+ };
100
+ });
113
101
  }
114
102
 
115
103
  /** Detalhe da run (null se o slug for inválido). */
116
104
  detail(slug: string): RunDetail | null {
117
- const dir = runDirForSlug(this.artifactDir, slug);
118
- if (!dir) return null;
119
- const state = this.loadState(dir, slug);
120
- const classification = readRunJson<{
105
+ const review = this.findBySlug(slug);
106
+ if (!review) return null;
107
+ const state = this.loadState(review);
108
+ const classification = this.readArtifactJson<{
121
109
  watchdogs?: Record<string, string[]>;
122
110
  unclassified?: string[];
123
111
  deleted?: string[];
124
- }>(dir, "classification.json");
112
+ }>(review, "classification.json");
125
113
  return {
126
- slug,
127
- branch: state.branch || slug,
128
- status: readRunStatus(dir),
129
- manifest: readRunJson(dir, "manifest.json"),
114
+ slug: review.slug,
115
+ branch: review.branch,
116
+ status: review,
117
+ manifest: this.readArtifactJson(review, "manifest.json"),
130
118
  summary: summarize(state),
131
119
  watchdogs: Object.keys(classification?.watchdogs ?? {}),
132
120
  unclassified: classification?.unclassified ?? [],
@@ -134,6 +122,43 @@ export class RunRepository {
134
122
  };
135
123
  }
136
124
 
125
+ /** Aplica `VACUUM`/`optimize` no backend (chamado no start do server). */
126
+ maintain(): void {
127
+ this.storage.maintain();
128
+ }
129
+
130
+ /** Fecha o provider (shutdown do server). */
131
+ dispose(): void {
132
+ this.storage.close();
133
+ }
134
+
135
+ // -----------------------------------------------------------------------
136
+ // Internos
137
+ // -----------------------------------------------------------------------
138
+
139
+ private findBySlug(slug: string): ReviewRecord | null {
140
+ if (!isSlug(slug)) return null;
141
+ return (
142
+ this.storage
143
+ .listReviews(this.repo.key)
144
+ .find((review) => review.slug === slug) ?? null
145
+ );
146
+ }
147
+
148
+ private loadState(review: ReviewRecord): FindingsStateFile {
149
+ return this.storage.readFindings(review.id) ?? emptyState(review.branch);
150
+ }
151
+
152
+ private readArtifactJson<T>(review: ReviewRecord, name: string): T | null {
153
+ const artifact = this.storage.readArtifact(review.id, name);
154
+ if (!artifact) return null;
155
+ try {
156
+ return JSON.parse(artifact.content) as T;
157
+ } catch {
158
+ return null;
159
+ }
160
+ }
161
+
137
162
  /** Findings filtrados + resumo (null se o slug for inválido). */
138
163
  findings(
139
164
  slug: string,
@@ -143,9 +168,9 @@ export class RunRepository {
143
168
  findings: (FindingState & { absFile: string })[];
144
169
  summary: FindingsSummary;
145
170
  } | null {
146
- const dir = runDirForSlug(this.artifactDir, slug);
147
- if (!dir) return null;
148
- const state = this.loadState(dir, slug);
171
+ const review = this.findBySlug(slug);
172
+ if (!review) return null;
173
+ const state = this.loadState(review);
149
174
  const filter: FindingsFilter = {
150
175
  decision: (params.get("decision") as FindingDecision) || undefined,
151
176
  execState: (params.get("exec") as FindingExecState) || undefined,
@@ -155,18 +180,22 @@ export class RunRepository {
155
180
  includeAbsent: params.get("all") === "1",
156
181
  };
157
182
  return {
158
- branch: state.branch,
159
- findings: filterFindings(state, filter).map(toClientFinding),
183
+ branch: review.branch,
184
+ findings: filterFindings(state, filter).map((finding) =>
185
+ toClientFinding(finding, review),
186
+ ),
160
187
  summary: summarize(state),
161
188
  };
162
189
  }
163
190
 
164
- /** Um finding (null se ausente/inválido). */
191
+ /** Um finding (null se ausente/inválido). Aceita prefixo único do id. */
165
192
  finding(slug: string, id: string): (FindingState & { absFile: string }) | null {
166
- const dir = runDirForSlug(this.artifactDir, slug);
167
- if (!dir) return null;
168
- const finding = this.loadState(dir, slug).findings[id];
169
- return finding ? toClientFinding(finding) : null;
193
+ const review = this.findBySlug(slug);
194
+ if (!review) return null;
195
+ const state = this.loadState(review);
196
+ const resolution = resolveFindingId(state, id);
197
+ if (resolution.status !== "found") return null;
198
+ return toClientFinding(state.findings[resolution.id], review);
170
199
  }
171
200
 
172
201
  /** Aplica uma atualização a um finding (null se ausente/inválido). */
@@ -175,17 +204,18 @@ export class RunRepository {
175
204
  id: string,
176
205
  body: FindingUpdate,
177
206
  ): (FindingState & { absFile: string }) | null {
178
- const dir = runDirForSlug(this.artifactDir, slug);
179
- if (!dir) return null;
180
- const store = this.store(dir, slug);
181
- const state = store.read() ?? this.buildForRun(dir, slug) ?? emptyState(slug);
182
- const finding = state.findings[id];
183
- if (!finding) return null;
207
+ const review = this.findBySlug(slug);
208
+ if (!review) return null;
209
+ const state = this.loadState(review);
210
+ const resolution = resolveFindingId(state, id);
211
+ if (resolution.status !== "found") return null;
212
+ const finding = state.findings[resolution.id];
184
213
  applyFindingUpdate(finding, body);
185
214
  reorder(state);
186
- store.write(state);
187
- pingFindingsStatus(dir, state);
188
- return toClientFinding(finding);
215
+ state.updatedAt = systemClock();
216
+ this.storage.writeFindings(review.id, state);
217
+ pingFindingsStatus(this.storage, review.id, state);
218
+ return toClientFinding(finding, review);
189
219
  }
190
220
 
191
221
  /** Reancora os findings contra os arquivos atuais. */
@@ -193,22 +223,21 @@ export class RunRepository {
193
223
  result: SyncResult;
194
224
  summary: FindingsSummary;
195
225
  } | null {
196
- const dir = runDirForSlug(this.artifactDir, slug);
197
- if (!dir) return null;
198
- const store = this.store(dir, slug);
199
- const state = store.read() ?? this.buildForRun(dir, slug) ?? emptyState(slug);
226
+ const review = this.findBySlug(slug);
227
+ if (!review) return null;
228
+ const state = this.loadState(review);
200
229
  const result = syncState(state, fileReader(process.cwd()));
201
- store.write(state);
202
- pingFindingsStatus(dir, state);
230
+ this.storage.writeFindings(review.id, state);
231
+ pingFindingsStatus(this.storage, review.id, state);
203
232
  return { result, summary: summarize(state) };
204
233
  }
205
234
 
206
235
  /** Prévia do envio da revisão (candidatos + disponibilidade da PR). */
207
236
  submitPreview(slug: string, runner: GhRunner): unknown | null {
208
- const dir = runDirForSlug(this.artifactDir, slug);
209
- if (!dir) return null;
210
- const state = this.loadState(dir, slug);
211
- const target = resolvePrTarget(runner, state.branch || slug);
237
+ const review = this.findBySlug(slug);
238
+ if (!review) return null;
239
+ const state = this.loadState(review);
240
+ const target = resolvePrTarget(runner, review.branch);
212
241
  return buildSubmitPreview(state, target);
213
242
  }
214
243
 
@@ -218,10 +247,10 @@ export class RunRepository {
218
247
  selection: SubmitSelection,
219
248
  runner: GhRunner,
220
249
  ): SubmitResult | null {
221
- const dir = runDirForSlug(this.artifactDir, slug);
222
- if (!dir) return null;
223
- const state = this.loadState(dir, slug);
224
- const target = resolvePrTarget(runner, state.branch || slug);
250
+ const review = this.findBySlug(slug);
251
+ if (!review) return null;
252
+ const state = this.loadState(review);
253
+ const target = resolvePrTarget(runner, review.branch);
225
254
  if (!target.ok) return { available: false, reason: target.reason };
226
255
 
227
256
  const outcome = submitFindings({
@@ -230,55 +259,31 @@ export class RunRepository {
230
259
  runner,
231
260
  selection,
232
261
  });
233
- this.store(dir, slug).write(state);
234
- pingFindingsStatus(dir, state);
262
+ this.storage.writeFindings(review.id, state);
263
+ pingFindingsStatus(this.storage, review.id, state);
235
264
  return { available: true, target: target.target, outcome };
236
265
  }
237
266
 
267
+ /** Lista os artefatos da run (sem conteúdo). */
268
+ artifacts(slug: string): ArtifactListItem[] | null {
269
+ const review = this.findBySlug(slug);
270
+ if (!review) return null;
271
+ return this.storage.listArtifacts(review.id).map((artifact) => ({
272
+ name: artifact.name,
273
+ kind: artifact.kind,
274
+ contentType: artifact.contentType,
275
+ updatedAt: artifact.updatedAt,
276
+ }));
277
+ }
278
+
238
279
  /** Conteúdo de um artefato (`json`/`md`) da run (null se não servível). */
239
280
  artifact(slug: string, name: string): { content: string; type: string } | null {
240
281
  if (!/\.(json|md)$/i.test(name)) return null;
241
- const dir = runDirForSlug(this.artifactDir, slug);
242
- if (!dir) return null;
243
- const content = readRunFile(dir, name);
244
- if (content === null) return null;
245
- const type = name.toLowerCase().endsWith(".json")
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);
282
+ const review = this.findBySlug(slug);
283
+ if (!review) return null;
284
+ const artifact = this.storage.readArtifact(review.id, name);
285
+ if (!artifact) return null;
286
+ return { content: artifact.content, type: artifact.contentType };
282
287
  }
283
288
  }
284
289
 
@@ -286,6 +291,9 @@ export class RunRepository {
286
291
  * Finding + caminho absoluto. O cliente monta o link `vscode://file/...` e não
287
292
  * conhece o cwd do servidor — o caminho relativo do relatório não basta.
288
293
  */
289
- function toClientFinding(finding: FindingState): FindingState & { absFile: string } {
290
- return { ...finding, absFile: resolve(process.cwd(), finding.file) };
294
+ function toClientFinding(
295
+ finding: FindingState,
296
+ review: ReviewRecord,
297
+ ): FindingState & { absFile: string } {
298
+ return { ...finding, absFile: resolve(review.repoRoot, finding.file) };
291
299
  }
@@ -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 em `.houndsman/.runs/<branch-slug>/review.md`.
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
- .houndsman/.runs/<branch-slug>/findings/<watchdogId>.json
14
+ <HOUNDSMAN_HOME>/work/<reviewId>/findings/<watchdogId>.json
14
15
  ```
15
16
 
16
- - `<branch-slug>`: branch com `/` substituído por `-` (ex.: `feature/x` → `feature-x`).
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
 
@@ -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>` · `--dir <dir>` · `--clean` · `--full` · `--no-jev` · `--fail-on-blocking` (no `--finalize`).
60
+ `--branch <nome>` · `--clean` · `--full` · `--no-jev` · `--fail-on-blocking` (no `--finalize`).
61
61
 
62
62
  ## Artefatos
63
63
 
64
- Tudo em `.houndsman/.runs/<branch-slug>/` (default do `artifactDir`, `.houndsman/.runs`):
64
+ Runs, findings e artefatos vivem no **storage global** (SQLite em `<HOUNDSMAN_HOME>/houndsman.db`):
65
65
 
66
- | Arquivo | Conteúdo |
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
- | `context.json` | Payload bruto da PR via `gh` (ou marcador de indisponível) |
71
- | `context-summary.json` | Resumo do subagente (objetivo/motivação/alterações) |
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
- | `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) |
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 (state.json) + store atômico
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 # run.json (reviewing/review_done/fixing/fix_done)
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/ e .runs/
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
@@ -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/` e `.runs/` (gerados/temporários) |
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. Os artefatos ficam em
46
- `.houndsman/.runs/<branch-slug>/` (auto-ignorados).
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
- | `artifactDir` | **Opcional** — onde gravar os artefatos (default `.houndsman/.runs`). |
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. |
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 `state.json` à mã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 `.houndsman/.runs/<branch-slug>/state.json` da run atual
19
- > (gerado no `--finalize`). Se não existir, a revisão não foi finalizada — rode o `hunt` antes.
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 do `state.json` fica **centralizada no agente principal**: o subagente apenas
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 `.houndsman/.runs/<slug>/state.json` à mão — use os comandos `findings`.
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`.