@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
package/README.md CHANGED
@@ -74,8 +74,9 @@ No seu harness (Claude Code / agents), invoque o skill:
74
74
  /houndsman hunt
75
75
  ```
76
76
 
77
- O skill orquestra tudo e grava o relatório em `.houndsman/.runs/<branch-slug>/review.md`.
78
- Não precisa rodar nenhum script à mão.
77
+ O skill orquestra tudo e persiste o relatório no storage global (SQLite em
78
+ `<HOUNDSMAN_HOME>/houndsman.db`); a UI lê os artefatos direto do storage. Não precisa rodar
79
+ nenhum script à mão.
79
80
 
80
81
  ---
81
82
 
@@ -86,9 +87,9 @@ Não precisa rodar nenhum script à mão.
86
87
  ├─ Passo 0 contexto da PR (gh, opcional)
87
88
  ├─ Passo 1 classifica o diff → briefs por watchdog (incremental quando possível)
88
89
  ├─ Passo 2 N subagentes revisam em paralelo
89
- ├─ Passo 3 consolida em review.md + state.json
90
+ ├─ Passo 3 consolida em review.md + triagem no storage
90
91
  ├─ Passo 4 valida na UI (server) e marca os achados a executar
91
- ├─ Passo 5 fix corrige o grupo atual → run.json (fix_done)
92
+ ├─ Passo 5 fix corrige o grupo atual → status fix_done
92
93
  └─ Passo 6 verify confere o que resolveu, sem re-revisar
93
94
  ```
94
95
 
@@ -111,7 +112,7 @@ O skill tem **4 capabilities**, todas em `/houndsman`:
111
112
  ## Triando os findings (opcional)
112
113
 
113
114
  Depois do `--finalize`, cada finding tem um `id` curto e um `fingerprint` de **conteúdo**
114
- no `.houndsman/.runs/<branch-slug>/state.json`. Você decide o que fazer com cada um — pela
115
+ no storage global. Você decide o que fazer com cada um — pela
115
116
  **UI local** ou pelo **agente** (auto-fix) — sem abrir o JSON à mão:
116
117
 
117
118
  ```bash
@@ -136,12 +137,23 @@ bun run houndsman findings verify # confere o que resolveu (barato, se
136
137
  > o review completo.
137
138
 
138
139
  > A identidade do finding é o **conteúdo** (fingerprint), não a linha: itens deslocados
139
- > pelo fix não se perdem. O estado é local e mora em `.runs/<slug>/state.json` (gitignored).
140
+ > pelo fix não se perdem. O estado mora no storage global (`<HOUNDSMAN_HOME>/houndsman.db`).
140
141
 
141
142
  ---
142
143
 
143
144
  ## Mantendo a configuração
144
145
 
146
+ - **Persistência** → por default, SQLite global em `<HOUNDSMAN_HOME>/houndsman.db`
147
+ (`HOUNDSMAN_HOME` sobrescreve o diretório de dados). Para voltar ao backend de arquivos,
148
+ troque só o provider — a interface `StorageProvider` é a mesma:
149
+
150
+ ```ts
151
+ export const houndsmanConfig = {
152
+ // ...
153
+ storage: { provider: "file", path: "./houndsman-runs" },
154
+ };
155
+ ```
156
+
145
157
  - **Alterar regras** → use a capability **`train`** (`/houndsman train`). Ela prova no código
146
158
  que o padrão existe (evidência) antes de editar e roda as validações.
147
159
  - **Atualizou a lib?** Sincronize os arquivos auto-gerados sem tocar nas suas regras:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@payconductor/houndsman",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Orquestrador de revisão de Pull Requests: classifica o diff, despacha watchdogs especializados por domínio e consolida o relatório.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,7 +1,7 @@
1
1
  /** Comando `context` — coleta o contexto da PR via `gh` (Passo 0). */
2
2
 
3
3
  import { runContextCommand } from "../../core/commands/context";
4
- import { relFromRoot } from "../../core/paths";
4
+ import { displayPath } from "../../core/paths";
5
5
  import { ArgParser } from "../arg-parser";
6
6
  import type { CliContext, Command } from "./command";
7
7
 
@@ -11,10 +11,12 @@ export class ContextCommand implements Command {
11
11
  async run(args: string[], context: CliContext): Promise<number> {
12
12
  const engine = await context.engine();
13
13
  const parser = ArgParser.from(args);
14
- const { paths, context: pr } = runContextCommand(engine.config, {
15
- branch: parser.value("--branch"),
16
- dir: parser.value("--dir"),
17
- });
14
+ const { paths, context: pr } = runContextCommand(
15
+ engine.config,
16
+ engine.storage,
17
+ engine.repo,
18
+ { branch: parser.value("--branch") },
19
+ );
18
20
 
19
21
  if (pr.available) {
20
22
  const src = pr.source;
@@ -24,14 +26,14 @@ export class ContextCommand implements Command {
24
26
  context.out(
25
27
  `[houndsman] PR ${label} (${src?.head} → ${src?.base}), ${pr.stats?.files ?? 0} arquivo(s).`,
26
28
  );
27
- context.out(`[houndsman] contexto: ${relFromRoot(paths.context)}`);
28
- context.out(`[houndsman] brief: ${relFromRoot(paths.brief)}`);
29
+ context.out(`[houndsman] contexto: ${displayPath(paths.context)}`);
30
+ context.out(`[houndsman] brief: ${displayPath(paths.brief)}`);
29
31
  context.out(
30
- `[houndsman] próximo passo: subagente resume em ${relFromRoot(paths.summary)}`,
32
+ `[houndsman] próximo passo: subagente resume em ${displayPath(paths.summary)}`,
31
33
  );
32
34
  } else {
33
35
  context.out(`[houndsman] contexto indisponível (${pr.reason}): ${pr.detail}`);
34
- context.out(`[houndsman] marcador: ${relFromRoot(paths.context)}`);
36
+ context.out(`[houndsman] marcador: ${displayPath(paths.context)}`);
35
37
  }
36
38
  return 0;
37
39
  }
@@ -1,6 +1,7 @@
1
1
  /** Comando `doctor` (alias `check`) — valida config, ambiente e credencial Jev. */
2
2
 
3
3
  import { checkRegistry } from "../../core/registry-check";
4
+ import { storageLocation } from "../../core/storage/paths";
4
5
  import type { CliContext, Command } from "./command";
5
6
 
6
7
  export class DoctorCommand implements Command {
@@ -28,6 +29,10 @@ export class DoctorCommand implements Command {
28
29
  context.out("\n→ Ambiente:");
29
30
  context.out(` ${Bun.which("git") ? "✅" : "❌"} git`);
30
31
  context.out(` ${Bun.which("gh") ? "✅" : "⚠️ "} gh (opcional — contexto da PR)`);
32
+ context.out("\n→ Persistência:");
33
+ context.out(` provider: ${engine.storage.kind}`);
34
+ context.out(` ${storageLocation(engine.config)}`);
35
+ context.out(` repo: ${engine.repo.key}`);
31
36
  await this.reportJevCredential(context);
32
37
  return 0;
33
38
  }
@@ -16,10 +16,20 @@ export class FindingsCommand implements Command {
16
16
  const sub = ArgParser.from(args).command();
17
17
  const result =
18
18
  sub === "verify"
19
- ? await runVerifyCommand(engine.config, args)
19
+ ? await runVerifyCommand(engine.config, engine.storage, engine.repo, args)
20
20
  : sub === "submit"
21
- ? await runSubmitCommand(engine.config, args)
22
- : runFindingsCommand(engine.config, args);
21
+ ? await runSubmitCommand(
22
+ engine.config,
23
+ engine.storage,
24
+ engine.repo,
25
+ args,
26
+ )
27
+ : runFindingsCommand(
28
+ engine.config,
29
+ engine.storage,
30
+ engine.repo,
31
+ args,
32
+ );
23
33
 
24
34
  if (result.exitCode !== 0) context.err(result.output);
25
35
  else context.out(result.output);
@@ -1,7 +1,6 @@
1
1
  /** Comando default — prepara ou finaliza a revisão. */
2
2
 
3
3
  import { parseReviewArgs } from "../../core/commands/review";
4
- import { resolveArtifactDir } from "../../core/config";
5
4
  import type { CliContext, Command } from "./command";
6
5
 
7
6
  export class ReviewCommand implements Command {
@@ -9,7 +8,7 @@ export class ReviewCommand implements Command {
9
8
 
10
9
  async run(args: string[], context: CliContext): Promise<number> {
11
10
  const engine = await context.engine();
12
- const options = parseReviewArgs(args, resolveArtifactDir(engine.config));
11
+ const options = parseReviewArgs(args);
13
12
  const result = await engine.session(options).run();
14
13
 
15
14
  for (const message of result.messages) context.out(message);
package/src/cli/help.ts CHANGED
@@ -18,8 +18,7 @@ Flags:
18
18
  --base <branch> Base do diff (default: main)
19
19
  --head <ref> Head do diff (default: HEAD)
20
20
  --branch <nome> Branch alvo (default: detecta do git)
21
- --dir <dir> Diretório dos artefatos
22
- --clean Descarta findings anteriores
21
+ --clean Descarta findings brutos do staging anterior
23
22
  --full Força review completo (ignora o modo incremental)
24
23
  --no-jev Desativa a pré-análise Jev
25
24
  --fail-on-blocking Exit 1 se houver bloqueantes (com --finalize)
@@ -40,4 +39,9 @@ Flags de findings:
40
39
  --apply (verify) Marca os resolvidos como done
41
40
  --fail-on-persisting (verify) Exit 1 se algum blocking persistir
42
41
  --include-sent (submit) Reenvia findings já enviados à PR
43
- --dry-run (submit) Só a prévia do envio, sem postar`;
42
+ --dry-run (submit) Só a prévia do envio, sem postar
43
+
44
+ Persistência:
45
+ Runs, findings e artefatos ficam no storage global (SQLite em
46
+ <HOUNDSMAN_HOME>/houndsman.db). Use storage.provider: "file" na config para
47
+ voltar ao backend de arquivos. Nada é gravado em .houndsman/.runs.`;
package/src/cli/index.ts CHANGED
@@ -22,8 +22,8 @@
22
22
  * test Roda a suíte de testes da lib
23
23
  * watchdog <id> Contexto de um watchdog (`--rules`, `--json`)
24
24
  *
25
- * Flags: `--base <b>` · `--head <ref>` · `--branch <b>` · `--dir <d>` ·
26
- * `--config <path>` · `--clean` · `--full` · `--no-jev` · `--fail-on-blocking`.
25
+ * Flags: `--base <b>` · `--head <ref>` · `--branch <b>` · `--config <path>` ·
26
+ * `--clean` · `--full` · `--no-jev` · `--fail-on-blocking`.
27
27
  */
28
28
 
29
29
  import { loadConfig } from "../core/config-loader";
@@ -1,19 +1,20 @@
1
1
  /**
2
2
  * Subcomando `context` — coleta o contexto da PR via `gh` (Passo 0).
3
3
  *
4
- * Biblioteca: grava os artefatos e devolve os dados; o CLI faz o log/exit.
4
+ * Biblioteca: grava no staging da review + artefato no storage; o CLI faz o
5
+ * log/exit.
5
6
  */
6
7
 
7
- import { mkdirSync, writeFileSync } from "node:fs";
8
- import { type HoundsmanConfig, resolveArtifactDir } from "../config";
8
+ import type { HoundsmanConfig } from "../config";
9
9
  import {
10
- type ContextPaths,
11
10
  collectPrContext,
12
- contextPaths,
13
11
  currentBranch,
14
12
  type PrContextRaw,
15
13
  renderContextBrief,
16
14
  } from "../pr-context";
15
+ import { sanitizeBranch } from "../report-builder";
16
+ import type { RepoIdentity, StorageProvider } from "../storage/types";
17
+ import { type ContextPaths, ReviewWorkArea } from "../storage/work-area";
17
18
 
18
19
  export type ContextRunResult = {
19
20
  paths: ContextPaths;
@@ -22,18 +23,33 @@ export type ContextRunResult = {
22
23
 
23
24
  /** Coleta a PR atual e grava `context.json` + `context-brief.md`. */
24
25
  export function runContextCommand(
25
- config: HoundsmanConfig,
26
- opts: { branch?: string; dir?: string } = {},
26
+ _config: HoundsmanConfig,
27
+ storage: StorageProvider,
28
+ repo: RepoIdentity,
29
+ opts: { branch?: string } = {},
27
30
  ): ContextRunResult {
28
31
  const branch = opts.branch ?? currentBranch();
29
- const dir = opts.dir ?? resolveArtifactDir(config);
30
- const paths = contextPaths(dir, branch);
31
-
32
- mkdirSync(paths.dir, { recursive: true });
32
+ const review = storage.getOrCreateReview({
33
+ repo,
34
+ branch,
35
+ slug: sanitizeBranch(branch),
36
+ });
37
+ const work = new ReviewWorkArea(review);
38
+ const paths = work.contextPaths;
33
39
 
34
40
  const context = collectPrContext(branch);
35
- writeFileSync(paths.context, `${JSON.stringify(context, null, 2)}\n`);
36
- writeFileSync(paths.brief, `${renderContextBrief(context, branch, paths)}\n`);
41
+ const brief = renderContextBrief(context, branch, paths);
42
+ work.writeContext(context, brief);
43
+ storage.writeArtifact(review.id, {
44
+ name: "context.json",
45
+ kind: "context",
46
+ content: `${JSON.stringify(context, null, 2)}\n`,
47
+ });
48
+ storage.writeArtifact(review.id, {
49
+ name: "context-brief.md",
50
+ kind: "context-brief",
51
+ content: `${brief}\n`,
52
+ });
37
53
 
38
54
  return { paths, context };
39
55
  }