@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/src/cli/index.ts CHANGED
@@ -7,11 +7,14 @@
7
7
  * cwd) e orquestra funções/classes de `core/`. Não importa nenhum caminho do
8
8
  * projeto consumidor estaticamente.
9
9
  *
10
+ * Cada comando carrega seus módulos por import dinâmico (lazy) — o startup não
11
+ * paga por `server`, Jev (axios) ou `orchestrator` que o comando não usa.
12
+ *
10
13
  * Comandos:
11
14
  * (default) Passo 1 — preparar (classificação + briefs + manifesto)
12
15
  * --finalize Passo 3 — consolidar os findings em review.md
13
16
  * context Passo 0 — coletar o contexto da PR via `gh`
14
- * findings <sub> Triagem/correção dos findings (list/set/done/sync/batch/verify/...)
17
+ * findings <sub> Triagem/correção dos findings (list/set/done/sync/batch/verify/submit/...)
15
18
  * server Sobe a UI local de triagem (Bun.serve)
16
19
  * setup Cria a configuração inicial do projeto (config + skills)
17
20
  * update Sincroniza os arquivos auto-gerados (.houndsman/core)
@@ -23,274 +26,73 @@
23
26
  * `--config <path>` · `--clean` · `--full` · `--no-jev` · `--fail-on-blocking`.
24
27
  */
25
28
 
26
- import { runContextCommand } from "../core/commands/context";
27
- import { runFindingsCommand, runVerifyCommand } from "../core/commands/findings";
28
- import { parseReviewArgs } from "../core/commands/review";
29
- import { runSetupCommand } from "../core/commands/setup";
30
- import { runLibTests } from "../core/commands/test";
31
- import { runUpdateCommand } from "../core/commands/update";
32
- import { renderWatchdogContext } from "../core/commands/watchdog";
33
- import { type HoundsmanConfig, resolveArtifactDir } from "../core/config";
34
29
  import { loadConfig } from "../core/config-loader";
35
- import { JevClient } from "../core/jev";
36
- import { finalize, prepare, resolveBranch } from "../core/orchestrator";
37
- import { relFromRoot } from "../core/paths";
38
- import { checkRegistry } from "../core/registry-check";
39
- import { startServer } from "../server";
40
-
41
- const HELP = `Uso: houndsman [comando] [flags]
42
-
43
- Comandos:
44
- (nenhum) Prepara a revisão (classification + briefs + manifesto)
45
- --finalize Consolida os findings em review.md
46
- context Coleta o contexto da PR via gh (Passo 0)
47
- findings <sub> Triagem/correção dos findings (list · show · set · done · reset · sync · summary · batch · verify · path)
48
- server Sobe a UI local de triagem (--port, --host, --open)
49
- setup Cria a configuração inicial do projeto (config + skills)
50
- update Sincroniza os arquivos auto-gerados (.houndsman/core)
51
- doctor Valida a configuração, o ambiente e a credencial Jev
52
- test Roda a suíte de testes da lib
53
- watchdog <id> Contexto de um watchdog (--rules, --json)
54
-
55
- Flags:
56
- --config <path> Caminho do houndsman.config.* (default: detecta no cwd)
57
- --base <branch> Base do diff (default: main)
58
- --head <ref> Head do diff (default: HEAD)
59
- --branch <nome> Branch alvo (default: detecta do git)
60
- --dir <dir> Diretório dos artefatos
61
- --clean Descarta findings anteriores
62
- --full Força review completo (ignora o modo incremental)
63
- --no-jev Desativa a pré-análise Jev
64
- --fail-on-blocking Exit 1 se houver bloqueantes (com --finalize)
65
-
66
- Flags de findings:
67
- --decision <d> pending | execute | ignore
68
- --exec <e> pending | in_progress | done | stale
69
- --comment <text> Comentário/instrução adicional
70
- --watchdog <id> Filtra por watchdog (list)
71
- --severity <s> Filtra por severidade (list, batch)
72
- --file <text> Filtra por trecho do caminho (list)
73
- --all Inclui findings ausentes do último relatório
74
- --json Saída JSON
75
- --limit <n> Limita o nº de itens (list)
76
- --group <key> Grupo alvo no batch (default: o primeiro)
77
- --offset <n> Paginação dentro do grupo (batch)
78
- --include-done Inclui já concluídos no batch
79
- --apply (verify) Marca os resolvidos como done
80
- --fail-on-persisting (verify) Exit 1 se algum blocking persistir`;
81
-
82
- function readFlag(args: string[], flag: string): string | undefined {
83
- const index = args.indexOf(flag);
84
- return index >= 0 ? args[index + 1] : undefined;
85
- }
86
-
87
- /** Passo 0 — contexto da PR via `gh`. */
88
- function runContext(config: HoundsmanConfig, args: string[]): void {
89
- const { paths, context } = runContextCommand(config, {
90
- branch: readFlag(args, "--branch"),
91
- dir: readFlag(args, "--dir"),
92
- });
93
-
94
- if (context.available) {
95
- const src = context.source;
96
- const label = [src?.number ? `#${src.number}` : "", src?.title ?? ""]
97
- .filter(Boolean)
98
- .join(" ");
99
- console.log(
100
- `[houndsman] PR ${label} (${src?.head} → ${src?.base}), ${context.stats?.files ?? 0} arquivo(s).`,
101
- );
102
- console.log(`[houndsman] contexto: ${relFromRoot(paths.context)}`);
103
- console.log(`[houndsman] brief: ${relFromRoot(paths.brief)}`);
104
- console.log(
105
- `[houndsman] próximo passo: subagente resume em ${relFromRoot(paths.summary)}`,
106
- );
107
- } else {
108
- console.log(
109
- `[houndsman] contexto indisponível (${context.reason}): ${context.detail}`,
110
- );
111
- console.log(`[houndsman] marcador: ${relFromRoot(paths.context)}`);
112
- }
113
- }
114
-
115
- /** Valida a configuração do projeto e o ambiente. */
116
- async function runDoctor(config: HoundsmanConfig): Promise<void> {
117
- console.log("→ Validando configuração do projeto...");
118
- const { problems, warnings, jevQuestions } = checkRegistry(config);
119
-
120
- if (problems.length > 0) {
121
- console.error("\n❌ Problemas na configuração de revisão:\n");
122
- for (const problem of problems) console.error(` - ${problem}`);
123
- process.exit(1);
124
- }
125
- console.log(
126
- ` ✅ ${config.watchdogIds.length} watchdogs, ${jevQuestions} pergunta(s) Jev.`,
127
- );
128
-
129
- if (warnings.length > 0) {
130
- console.log(`\n⚠️ ${warnings.length} aviso(s) (não bloqueantes):`);
131
- for (const warning of warnings) console.log(` - ${warning}`);
132
- }
133
-
134
- console.log("\n→ Ambiente:");
135
- console.log(` ${Bun.which("git") ? "✅" : "❌"} git`);
136
- console.log(` ${Bun.which("gh") ? "✅" : "⚠️ "} gh (opcional — contexto da PR)`);
137
- await reportJevCredential();
138
- }
139
-
140
- /**
141
- * Reporta o estado da credencial Jev: presença e validade (chamada mínima).
142
- * Fail-open — nunca altera o exit code (`doctor` é diagnóstico).
143
- */
144
- async function reportJevCredential(): Promise<void> {
145
- const client = new JevClient();
146
- if (!client.isConfigured()) {
147
- console.log(" ⚠️ TYPESAFE_API_KEY (opcional — pré-análise Jev)");
148
- return;
149
- }
150
- const check = await client.validateCredential();
151
- if (check.ok) {
152
- console.log(` ✅ Jev: credencial válida (modelo ${check.model}).`);
153
- } else if (check.kind === "invalid") {
154
- console.log(` ❌ Jev: credencial inválida — ${check.detail}`);
155
- } else {
156
- console.log(` ⚠️ Jev: não foi possível validar — ${check.detail}`);
157
- }
158
- }
159
-
160
- /** Contexto de um watchdog. */
161
- function runWatchdog(config: HoundsmanConfig, args: string[]): void {
162
- const id = args.find((arg) => !arg.startsWith("-"));
163
-
164
- if (!id || renderWatchdogContext(config, id) === null) {
165
- console.error(
166
- id
167
- ? `Watchdog '${id}' não existe.`
168
- : "Informe o id do watchdog: houndsman watchdog <id>",
169
- );
170
- console.error(`Ids válidos: ${config.watchdogIds.join(", ")}`);
171
- process.exit(1);
172
- }
173
-
174
- console.log(
175
- renderWatchdogContext(config, id, {
176
- withRules: args.includes("--rules"),
177
- json: args.includes("--json"),
178
- }),
179
- );
30
+ import { HoundsmanEngine } from "../core/engine/houndsman-engine";
31
+ import { ArgParser } from "./arg-parser";
32
+ import type { CliContext } from "./commands/command";
33
+ import { ContextCommand } from "./commands/context-command";
34
+ import { DoctorCommand } from "./commands/doctor-command";
35
+ import { FindingsCommand } from "./commands/findings-command";
36
+ import { HelpCommand } from "./commands/help-command";
37
+ import { CommandRegistry } from "./commands/registry";
38
+ import { ReviewCommand } from "./commands/review-command";
39
+ import { ServerCommand } from "./commands/server-command";
40
+ import { SetupCommand, TestCommand, UpdateCommand } from "./commands/setup-command";
41
+ import { WatchdogCommand } from "./commands/watchdog-command";
42
+ import { HELP } from "./help";
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Registry de comandos
46
+ // ---------------------------------------------------------------------------
47
+
48
+ /** Monta o registry com todos os comandos do binário. */
49
+ function buildRegistry(): CommandRegistry {
50
+ const doctor = new DoctorCommand();
51
+ return new CommandRegistry()
52
+ .register(new ReviewCommand(), { fallback: true })
53
+ .register(new HelpCommand())
54
+ .register(new ContextCommand())
55
+ .register(new FindingsCommand())
56
+ .register(new WatchdogCommand())
57
+ .register(new ServerCommand())
58
+ .register(new SetupCommand())
59
+ .register(new UpdateCommand())
60
+ .register(new TestCommand())
61
+ .register(doctor)
62
+ .registerAs("check", doctor);
180
63
  }
181
64
 
182
65
  /**
183
- * Subcomando `findings` — triagem/correção dos findings (list/show/set/done/reset/
184
- * sync/summary/batch/verify/path). Deixa o agente interagir com o estado sem abrir
185
- * o JSON. O `verify` é assíncrono (consulta opcional ao Jev) — roteado à parte.
186
- */
187
- async function runFindings(config: HoundsmanConfig, args: string[]): Promise<void> {
188
- const sub = args.find((arg) => !arg.startsWith("-"));
189
- const result =
190
- sub === "verify"
191
- ? await runVerifyCommand(config, args)
192
- : runFindingsCommand(config, args);
193
- if (result.exitCode !== 0) {
194
- console.error(result.output);
195
- process.exit(result.exitCode);
196
- }
197
- console.log(result.output);
198
- }
199
-
200
- /** Abre a URL no navegador padrão (best-effort, por plataforma). */
201
- function openBrowser(url: string): void {
202
- try {
203
- const command =
204
- process.platform === "win32"
205
- ? ["cmd", "/c", "start", "", url]
206
- : process.platform === "darwin"
207
- ? ["open", url]
208
- : ["xdg-open", url];
209
- Bun.spawn(command, { stdout: "ignore", stderr: "ignore" });
210
- } catch {
211
- // best-effort — se não abrir, o usuário copia a URL.
212
- }
213
- }
214
-
215
- /** Subcomando `server` — sobe a UI local de triagem. */
216
- function runServer(config: HoundsmanConfig, args: string[]): void {
217
- const port = Number(readFlag(args, "--port")) || 7331;
218
- const host = readFlag(args, "--host") ?? "127.0.0.1";
219
- const { url } = startServer({ config, port, host });
220
- console.log(`[houndsman] servidor em ${url}`);
221
- console.log("[houndsman] Ctrl+C para encerrar.");
222
- if (args.includes("--open")) openBrowser(url);
223
- }
224
-
225
- /** Passo 1/3 — preparar ou finalizar. */
226
- async function runReview(config: HoundsmanConfig, args: string[]): Promise<void> {
227
- const opts = parseReviewArgs(args, resolveArtifactDir(config));
228
- const branch = resolveBranch(opts);
229
-
230
- if (opts.finalize) {
231
- const result = finalize(opts, branch, config);
232
- if (opts.failOnBlocking && result.blockers > 0) process.exit(1);
233
- } else {
234
- await prepare(opts, branch, config);
235
- }
236
- }
237
-
238
- /**
239
- * Executa um subcomando. `--config` sobrepõe a descoberta automática.
240
- * Comandos que não precisam de projeto (`test`, `setup`) são tratados antes do load.
66
+ * Bootstrap do CLI: resolve o comando no registry, carrega a config sob demanda
67
+ * e delega a execução (o comando decide log/exit).
241
68
  */
242
69
  export async function main(): Promise<void> {
243
70
  const argv = process.argv.slice(2);
244
- const first = argv[0];
245
- const command = first && !first.startsWith("-") ? first : undefined;
246
- const rest = command ? argv.slice(1) : argv;
247
-
248
- if (command === "test") {
249
- process.exit(runLibTests());
250
- }
251
- if (command === "setup") {
252
- await runSetupCommand({ cwd: process.cwd(), args: rest });
253
- return;
254
- }
255
- if (command === "update") {
256
- await runUpdateCommand({ cwd: process.cwd(), args: rest });
257
- return;
258
- }
259
-
260
- if (command === "help") {
261
- console.info(HELP);
262
- return;
71
+ const parser = ArgParser.from(argv);
72
+ const commandName = parser.command();
73
+ const registry = buildRegistry();
74
+ const command = registry.resolve(commandName);
75
+
76
+ if (!command) {
77
+ console.error(`Subcomando desconhecido: '${commandName}'.\n`);
78
+ console.error(HELP);
79
+ process.exit(1);
263
80
  }
264
81
 
265
- const { config } = await loadConfig({
266
- configPath: readFlag(argv, "--config"),
267
- });
268
-
269
- switch (command) {
270
- case "context":
271
- runContext(config, rest);
272
- return;
273
- case "doctor":
274
- case "check":
275
- await runDoctor(config);
276
- return;
277
- case "watchdog":
278
- runWatchdog(config, rest);
279
- return;
280
- case "findings":
281
- await runFindings(config, rest);
282
- return;
283
- case "server":
284
- runServer(config, rest);
285
- return;
286
- case undefined:
287
- await runReview(config, rest);
288
- return;
289
- default:
290
- console.error(`Subcomando desconhecido: '${command}'.\n`);
291
- console.error(HELP);
292
- process.exit(1);
293
- }
82
+ const context: CliContext = {
83
+ cwd: process.cwd(),
84
+ out: (message) => console.log(message),
85
+ err: (message) => console.error(message),
86
+ engine: async () => {
87
+ const { config } = await loadConfig({
88
+ configPath: parser.value("--config"),
89
+ });
90
+ return new HoundsmanEngine(config);
91
+ },
92
+ };
93
+
94
+ const code = await command.run(parser.rest(), context);
95
+ if (code !== 0) process.exit(code);
294
96
  }
295
97
 
296
98
  if (import.meta.main) {
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Parser simples de argv (biblioteca) — compartilhado por CLI e comandos.
3
+ *
4
+ * Não valida flags: só lê valores/booleanos e separa o subcomando.
5
+ */
6
+
7
+ /** argv com leitura de flags por nome. */
8
+ export class ArgParser {
9
+ readonly argv: string[];
10
+
11
+ constructor(argv: readonly string[]) {
12
+ this.argv = [...argv];
13
+ }
14
+
15
+ static from(argv: readonly string[]): ArgParser {
16
+ return new ArgParser(argv);
17
+ }
18
+
19
+ /** Valor da flag (`--x y`), se presente. */
20
+ value(flag: string): string | undefined {
21
+ const index = this.argv.indexOf(flag);
22
+ return index >= 0 ? this.argv[index + 1] : undefined;
23
+ }
24
+
25
+ /** `true` se a flag está presente. */
26
+ has(flag: string): boolean {
27
+ return this.argv.includes(flag);
28
+ }
29
+
30
+ /** Primeiro token que não é flag (subcomando). */
31
+ command(): string | undefined {
32
+ const first = this.argv[0];
33
+ return first && !first.startsWith("-") ? first : undefined;
34
+ }
35
+
36
+ /** Argumentos sem o subcomando. */
37
+ rest(): string[] {
38
+ const command = this.command();
39
+ if (!command) return this.argv;
40
+ const index = this.argv.indexOf(command);
41
+ return [...this.argv.slice(0, index), ...this.argv.slice(index + 1)];
42
+ }
43
+
44
+ /** Todos os tokens que não começam com `-`. */
45
+ positionals(): string[] {
46
+ return this.argv.filter((arg) => !arg.startsWith("-"));
47
+ }
48
+ }