@payconductor/houndsman 0.1.4 → 0.3.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 (84) hide show
  1. package/package.json +1 -1
  2. package/src/cli/arg-parser.ts +8 -0
  3. package/src/cli/commands/command.ts +23 -0
  4. package/src/cli/commands/context-command.ts +38 -0
  5. package/src/cli/commands/doctor-command.ts +55 -0
  6. package/src/cli/commands/findings-command.ts +28 -0
  7. package/src/cli/commands/help-command.ts +13 -0
  8. package/src/cli/commands/registry.ts +34 -0
  9. package/src/cli/commands/review-command.ts +22 -0
  10. package/src/cli/commands/server-command.ts +37 -0
  11. package/src/cli/commands/setup-command.ts +34 -0
  12. package/src/cli/commands/watchdog-command.ts +34 -0
  13. package/src/cli/help.ts +43 -0
  14. package/src/cli/index.ts +62 -260
  15. package/src/core/arg-parser.ts +48 -0
  16. package/src/core/commands/findings.ts +163 -147
  17. package/src/core/commands/review.ts +18 -35
  18. package/src/core/commands/scaffold.ts +174 -64
  19. package/src/core/commands/setup.ts +16 -11
  20. package/src/core/commands/update.ts +7 -7
  21. package/src/core/commands/watchdog.ts +8 -80
  22. package/src/core/config.ts +9 -19
  23. package/src/core/defaults.ts +20 -0
  24. package/src/core/diff-source.ts +1 -13
  25. package/src/core/doc-discoverer.ts +25 -15
  26. package/src/core/domain/budget.ts +42 -0
  27. package/src/core/domain/classification.ts +94 -0
  28. package/src/core/domain/diff.ts +53 -0
  29. package/src/core/domain/watchdog.ts +96 -0
  30. package/src/core/engine/carryover.ts +67 -0
  31. package/src/core/engine/houndsman-engine.ts +51 -0
  32. package/src/core/engine/review-scope.ts +95 -0
  33. package/src/core/engine/review-session.ts +51 -0
  34. package/src/core/file-classifier.ts +11 -67
  35. package/src/core/findings-state.ts +189 -38
  36. package/src/core/git-revision.ts +14 -40
  37. package/src/core/git-runner.ts +46 -0
  38. package/src/core/github-review.ts +423 -0
  39. package/src/core/orchestrator.ts +116 -337
  40. package/src/core/paths.ts +9 -0
  41. package/src/core/pr-context.ts +28 -54
  42. package/src/core/project-config.ts +57 -0
  43. package/src/core/renderers/batch-presenter.ts +40 -0
  44. package/src/core/renderers/brief-renderer.ts +23 -0
  45. package/src/core/renderers/findings-presenter.ts +74 -0
  46. package/src/core/renderers/manifest-writer.ts +112 -0
  47. package/src/core/renderers/report-renderer.ts +66 -0
  48. package/src/core/renderers/verify-presenter.ts +50 -0
  49. package/src/core/renderers/watchdog-presenter.ts +100 -0
  50. package/src/core/report-builder.ts +12 -7
  51. package/src/core/run-layout.ts +108 -0
  52. package/src/core/run-status.ts +6 -11
  53. package/src/core/services/clock.ts +12 -0
  54. package/src/core/services/file-classifier.ts +34 -0
  55. package/src/core/services/fix-batch-builder.ts +13 -0
  56. package/src/core/services/gh-client.ts +54 -0
  57. package/src/core/services/git-repository.ts +123 -0
  58. package/src/core/services/project-files.ts +79 -0
  59. package/src/core/services/verifier.ts +58 -0
  60. package/src/core/verify.ts +2 -1
  61. package/src/core/watchdog-registry.ts +58 -16
  62. package/src/index.ts +31 -1
  63. package/src/server/client/App.tsx +174 -49
  64. package/src/server/client/ArtifactsPanel.tsx +2 -2
  65. package/src/server/client/AutoRefresh.tsx +38 -0
  66. package/src/server/client/Filters.tsx +72 -53
  67. package/src/server/client/FindingCard.tsx +212 -71
  68. package/src/server/client/FindingsList.tsx +8 -6
  69. package/src/server/client/RunList.tsx +30 -7
  70. package/src/server/client/SubmitModal.tsx +418 -0
  71. package/src/server/client/SummaryBar.tsx +65 -12
  72. package/src/server/client/constants.ts +9 -12
  73. package/src/server/client/format.ts +40 -0
  74. package/src/server/client/icons.tsx +143 -0
  75. package/src/server/client/types.ts +69 -0
  76. package/src/server/index.ts +316 -222
  77. package/src/server/run-repository.ts +291 -0
  78. package/src/tests/__snapshots__/baseline.test.ts.snap +814 -0
  79. package/src/tests/baseline.test.ts +513 -0
  80. package/src/tests/cli.test.ts +167 -0
  81. package/src/tests/domain.test.ts +328 -0
  82. package/src/tests/github-review.test.ts +320 -0
  83. package/src/tests/infrastructure.test.ts +210 -0
  84. package/src/tests/server.test.ts +117 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@payconductor/houndsman",
3
- "version": "0.1.4",
3
+ "version": "0.3.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",
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Parser de argv do CLI — reexporta a implementação de `core/`.
3
+ *
4
+ * O parser vive em `core/` para ser reutilizado pelos comandos da biblioteca
5
+ * (`parseReviewArgs`) sem inverter a camada `core → cli`.
6
+ */
7
+
8
+ export { ArgParser } from "../core/arg-parser";
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Contrato dos comandos do CLI.
3
+ *
4
+ * `core/` continua devolvendo dados; os comandos decidem o que imprimir e o
5
+ * exit code. O engine é carregado sob demanda (comandos sem projeto não pagam).
6
+ */
7
+
8
+ import type { HoundsmanEngine } from "../../core/engine/houndsman-engine";
9
+
10
+ /** Contexto de execução de um comando. */
11
+ export type CliContext = {
12
+ cwd: string;
13
+ /** Carrega a config do projeto e devolve o engine (lazy). */
14
+ engine: () => Promise<HoundsmanEngine>;
15
+ out: (message: string) => void;
16
+ err: (message: string) => void;
17
+ };
18
+
19
+ /** Um subcomando do binário. */
20
+ export interface Command {
21
+ readonly name: string;
22
+ run(args: string[], context: CliContext): Promise<number>;
23
+ }
@@ -0,0 +1,38 @@
1
+ /** Comando `context` — coleta o contexto da PR via `gh` (Passo 0). */
2
+
3
+ import { runContextCommand } from "../../core/commands/context";
4
+ import { relFromRoot } from "../../core/paths";
5
+ import { ArgParser } from "../arg-parser";
6
+ import type { CliContext, Command } from "./command";
7
+
8
+ export class ContextCommand implements Command {
9
+ readonly name = "context";
10
+
11
+ async run(args: string[], context: CliContext): Promise<number> {
12
+ const engine = await context.engine();
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
+ });
18
+
19
+ if (pr.available) {
20
+ const src = pr.source;
21
+ const label = [src?.number ? `#${src.number}` : "", src?.title ?? ""]
22
+ .filter(Boolean)
23
+ .join(" ");
24
+ context.out(
25
+ `[houndsman] PR ${label} (${src?.head} → ${src?.base}), ${pr.stats?.files ?? 0} arquivo(s).`,
26
+ );
27
+ context.out(`[houndsman] contexto: ${relFromRoot(paths.context)}`);
28
+ context.out(`[houndsman] brief: ${relFromRoot(paths.brief)}`);
29
+ context.out(
30
+ `[houndsman] próximo passo: subagente resume em ${relFromRoot(paths.summary)}`,
31
+ );
32
+ } else {
33
+ context.out(`[houndsman] contexto indisponível (${pr.reason}): ${pr.detail}`);
34
+ context.out(`[houndsman] marcador: ${relFromRoot(paths.context)}`);
35
+ }
36
+ return 0;
37
+ }
38
+ }
@@ -0,0 +1,55 @@
1
+ /** Comando `doctor` (alias `check`) — valida config, ambiente e credencial Jev. */
2
+
3
+ import { checkRegistry } from "../../core/registry-check";
4
+ import type { CliContext, Command } from "./command";
5
+
6
+ export class DoctorCommand implements Command {
7
+ readonly name = "doctor";
8
+
9
+ async run(_args: string[], context: CliContext): Promise<number> {
10
+ const engine = await context.engine();
11
+ context.out("→ Validando configuração do projeto...");
12
+ const { problems, warnings, jevQuestions } = checkRegistry(engine.config);
13
+
14
+ if (problems.length > 0) {
15
+ context.err("\n❌ Problemas na configuração de revisão:\n");
16
+ for (const problem of problems) context.err(` - ${problem}`);
17
+ return 1;
18
+ }
19
+ context.out(
20
+ ` ✅ ${engine.config.watchdogIds.length} watchdogs, ${jevQuestions} pergunta(s) Jev.`,
21
+ );
22
+
23
+ if (warnings.length > 0) {
24
+ context.out(`\n⚠️ ${warnings.length} aviso(s) (não bloqueantes):`);
25
+ for (const warning of warnings) context.out(` - ${warning}`);
26
+ }
27
+
28
+ context.out("\n→ Ambiente:");
29
+ context.out(` ${Bun.which("git") ? "✅" : "❌"} git`);
30
+ context.out(` ${Bun.which("gh") ? "✅" : "⚠️ "} gh (opcional — contexto da PR)`);
31
+ await this.reportJevCredential(context);
32
+ return 0;
33
+ }
34
+
35
+ /**
36
+ * Reporta o estado da credencial Jev: presença e validade (chamada mínima).
37
+ * Fail-open — nunca altera o exit code (`doctor` é diagnóstico).
38
+ */
39
+ private async reportJevCredential(context: CliContext): Promise<void> {
40
+ const { JevClient } = await import("../../core/jev");
41
+ const client = new JevClient();
42
+ if (!client.isConfigured()) {
43
+ context.out(" ⚠️ TYPESAFE_API_KEY (opcional — pré-análise Jev)");
44
+ return;
45
+ }
46
+ const check = await client.validateCredential();
47
+ if (check.ok) {
48
+ context.out(` ✅ Jev: credencial válida (modelo ${check.model}).`);
49
+ } else if (check.kind === "invalid") {
50
+ context.out(` ❌ Jev: credencial inválida — ${check.detail}`);
51
+ } else {
52
+ context.out(` ⚠️ Jev: não foi possível validar — ${check.detail}`);
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,28 @@
1
+ /** Comando `findings` — triagem/correção (inclui verify/submit). */
2
+
3
+ import {
4
+ runFindingsCommand,
5
+ runSubmitCommand,
6
+ runVerifyCommand,
7
+ } from "../../core/commands/findings";
8
+ import { ArgParser } from "../arg-parser";
9
+ import type { CliContext, Command } from "./command";
10
+
11
+ export class FindingsCommand implements Command {
12
+ readonly name = "findings";
13
+
14
+ async run(args: string[], context: CliContext): Promise<number> {
15
+ const engine = await context.engine();
16
+ const sub = ArgParser.from(args).command();
17
+ const result =
18
+ sub === "verify"
19
+ ? await runVerifyCommand(engine.config, args)
20
+ : sub === "submit"
21
+ ? await runSubmitCommand(engine.config, args)
22
+ : runFindingsCommand(engine.config, args);
23
+
24
+ if (result.exitCode !== 0) context.err(result.output);
25
+ else context.out(result.output);
26
+ return result.exitCode;
27
+ }
28
+ }
@@ -0,0 +1,13 @@
1
+ /** Comando `help` — imprime o uso e encerra. */
2
+
3
+ import { HELP } from "../help";
4
+ import type { CliContext, Command } from "./command";
5
+
6
+ export class HelpCommand implements Command {
7
+ readonly name = "help";
8
+
9
+ async run(_args: string[], context: CliContext): Promise<number> {
10
+ context.out(HELP);
11
+ return 0;
12
+ }
13
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Registry de comandos (CLI) — resolve nome → comando e guarda o fallback.
3
+ */
4
+
5
+ import type { Command } from "./command";
6
+
7
+ /** Mapa de comandos por nome, com um comando default. */
8
+ export class CommandRegistry {
9
+ private readonly commands = new Map<string, Command>();
10
+ private fallback: Command | undefined;
11
+
12
+ /** Registra um comando (ou o fallback, quando `fallback`). */
13
+ register(command: Command, options: { fallback?: boolean } = {}): this {
14
+ if (options.fallback) this.fallback = command;
15
+ else this.commands.set(command.name, command);
16
+ return this;
17
+ }
18
+
19
+ /** Registra o mesmo comando sob outro nome (alias). */
20
+ registerAs(name: string, command: Command): this {
21
+ this.commands.set(name, command);
22
+ return this;
23
+ }
24
+
25
+ /** Resolve o comando pelo nome; sem nome → fallback. */
26
+ resolve(name: string | undefined): Command | undefined {
27
+ return name ? this.commands.get(name) : this.fallback;
28
+ }
29
+
30
+ /** Nomes registrados (sem o fallback). */
31
+ names(): string[] {
32
+ return [...this.commands.keys()];
33
+ }
34
+ }
@@ -0,0 +1,22 @@
1
+ /** Comando default — prepara ou finaliza a revisão. */
2
+
3
+ import { parseReviewArgs } from "../../core/commands/review";
4
+ import { resolveArtifactDir } from "../../core/config";
5
+ import type { CliContext, Command } from "./command";
6
+
7
+ export class ReviewCommand implements Command {
8
+ readonly name = "";
9
+
10
+ async run(args: string[], context: CliContext): Promise<number> {
11
+ const engine = await context.engine();
12
+ const options = parseReviewArgs(args, resolveArtifactDir(engine.config));
13
+ const result = await engine.session(options).run();
14
+
15
+ for (const message of result.messages) context.out(message);
16
+
17
+ if ("blockers" in result && options.failOnBlocking && result.blockers > 0) {
18
+ return 1;
19
+ }
20
+ return 0;
21
+ }
22
+ }
@@ -0,0 +1,37 @@
1
+ /** Comando `server` — sobe a UI local de triagem. */
2
+
3
+ import { ArgParser } from "../arg-parser";
4
+ import type { CliContext, Command } from "./command";
5
+
6
+ /** Abre a URL no navegador padrão (best-effort, por plataforma). */
7
+ function openBrowser(url: string): void {
8
+ try {
9
+ const command =
10
+ process.platform === "win32"
11
+ ? ["cmd", "/c", "start", "", url]
12
+ : process.platform === "darwin"
13
+ ? ["open", url]
14
+ : ["xdg-open", url];
15
+ Bun.spawn(command, { stdout: "ignore", stderr: "ignore" });
16
+ } catch {
17
+ // best-effort — se não abrir, o usuário copia a URL.
18
+ }
19
+ }
20
+
21
+ export class ServerCommand implements Command {
22
+ readonly name = "server";
23
+
24
+ async run(args: string[], context: CliContext): Promise<number> {
25
+ const engine = await context.engine();
26
+ const parser = ArgParser.from(args);
27
+ const port = Number(parser.value("--port")) || 7331;
28
+ const host = parser.value("--host") ?? "127.0.0.1";
29
+
30
+ const { startServer } = await import("../../server");
31
+ const { url } = startServer({ config: engine.config, port, host });
32
+ context.out(`[houndsman] servidor em ${url}`);
33
+ context.out("[houndsman] Ctrl+C para encerrar.");
34
+ if (parser.has("--open")) openBrowser(url);
35
+ return 0;
36
+ }
37
+ }
@@ -0,0 +1,34 @@
1
+ /** Comandos que não dependem da config do projeto: setup, update e test. */
2
+
3
+ import type { CliContext, Command } from "./command";
4
+
5
+ export class SetupCommand implements Command {
6
+ readonly name = "setup";
7
+
8
+ async run(args: string[], context: CliContext): Promise<number> {
9
+ const { runSetupCommand } = await import("../../core/commands/setup");
10
+ const messages = await runSetupCommand({ cwd: context.cwd, args });
11
+ for (const message of messages) context.out(message);
12
+ return 0;
13
+ }
14
+ }
15
+
16
+ export class UpdateCommand implements Command {
17
+ readonly name = "update";
18
+
19
+ async run(args: string[], context: CliContext): Promise<number> {
20
+ const { runUpdateCommand } = await import("../../core/commands/update");
21
+ const messages = await runUpdateCommand({ cwd: context.cwd, args });
22
+ for (const message of messages) context.out(message);
23
+ return 0;
24
+ }
25
+ }
26
+
27
+ export class TestCommand implements Command {
28
+ readonly name = "test";
29
+
30
+ async run(_args: string[], _context: CliContext): Promise<number> {
31
+ const { runLibTests } = await import("../../core/commands/test");
32
+ return runLibTests();
33
+ }
34
+ }
@@ -0,0 +1,34 @@
1
+ /** Comando `watchdog` — contexto de um watchdog. */
2
+
3
+ import { renderWatchdogContext } from "../../core/commands/watchdog";
4
+ import { ArgParser } from "../arg-parser";
5
+ import type { CliContext, Command } from "./command";
6
+
7
+ export class WatchdogCommand implements Command {
8
+ readonly name = "watchdog";
9
+
10
+ async run(args: string[], context: CliContext): Promise<number> {
11
+ const engine = await context.engine();
12
+ const parser = ArgParser.from(args);
13
+ const id = args.find((arg) => !arg.startsWith("-"));
14
+
15
+ if (!id) {
16
+ context.err("Informe o id do watchdog: houndsman watchdog <id>");
17
+ context.err(`Ids válidos: ${engine.config.watchdogIds.join(", ")}`);
18
+ return 1;
19
+ }
20
+
21
+ const output = renderWatchdogContext(engine.config, id, {
22
+ withRules: parser.has("--rules"),
23
+ json: parser.has("--json"),
24
+ });
25
+ if (output === null) {
26
+ context.err(`Watchdog '${id}' não existe.`);
27
+ context.err(`Ids válidos: ${engine.config.watchdogIds.join(", ")}`);
28
+ return 1;
29
+ }
30
+
31
+ context.out(output);
32
+ return 0;
33
+ }
34
+ }
@@ -0,0 +1,43 @@
1
+ /** Texto de ajuda do CLI (mantido fora do bootstrap para reduzir acoplamento). */
2
+ export const HELP = `Uso: houndsman [comando] [flags]
3
+
4
+ Comandos:
5
+ (nenhum) Prepara a revisão (classification + briefs + manifesto)
6
+ --finalize Consolida os findings em review.md
7
+ context Coleta o contexto da PR via gh (Passo 0)
8
+ findings <sub> Triagem/correção dos findings (list · show · set · done · reset · sync · summary · batch · verify · submit · path)
9
+ server Sobe a UI local de triagem (--port, --host, --open)
10
+ setup Cria a configuração inicial do projeto (config + skills)
11
+ update Sincroniza os arquivos auto-gerados (.houndsman/core)
12
+ doctor Valida a configuração, o ambiente e a credencial Jev
13
+ test Roda a suíte de testes da lib
14
+ watchdog <id> Contexto de um watchdog (--rules, --json)
15
+
16
+ Flags:
17
+ --config <path> Caminho do houndsman.config.* (default: detecta no cwd)
18
+ --base <branch> Base do diff (default: main)
19
+ --head <ref> Head do diff (default: HEAD)
20
+ --branch <nome> Branch alvo (default: detecta do git)
21
+ --dir <dir> Diretório dos artefatos
22
+ --clean Descarta findings anteriores
23
+ --full Força review completo (ignora o modo incremental)
24
+ --no-jev Desativa a pré-análise Jev
25
+ --fail-on-blocking Exit 1 se houver bloqueantes (com --finalize)
26
+
27
+ Flags de findings:
28
+ --decision <d> pending | execute | ignore
29
+ --exec <e> pending | in_progress | done | stale
30
+ --comment <text> Comentário/instrução adicional
31
+ --watchdog <id> Filtra por watchdog (list)
32
+ --severity <s> Filtra por severidade (list, batch)
33
+ --file <text> Filtra por trecho do caminho (list)
34
+ --all Inclui findings ausentes do último relatório
35
+ --json Saída JSON
36
+ --limit <n> Limita o nº de itens (list)
37
+ --group <key> Grupo alvo no batch (default: o primeiro)
38
+ --offset <n> Paginação dentro do grupo (batch)
39
+ --include-done Inclui já concluídos no batch
40
+ --apply (verify) Marca os resolvidos como done
41
+ --fail-on-persisting (verify) Exit 1 se algum blocking persistir
42
+ --include-sent (submit) Reenvia findings já enviados à PR
43
+ --dry-run (submit) Só a prévia do envio, sem postar`;