@payconductor/houndsman 0.2.0 → 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.
- package/package.json +1 -1
- package/src/cli/arg-parser.ts +8 -0
- package/src/cli/commands/command.ts +23 -0
- package/src/cli/commands/context-command.ts +38 -0
- package/src/cli/commands/doctor-command.ts +55 -0
- package/src/cli/commands/findings-command.ts +28 -0
- package/src/cli/commands/help-command.ts +13 -0
- package/src/cli/commands/registry.ts +34 -0
- package/src/cli/commands/review-command.ts +22 -0
- package/src/cli/commands/server-command.ts +37 -0
- package/src/cli/commands/setup-command.ts +34 -0
- package/src/cli/commands/watchdog-command.ts +34 -0
- package/src/cli/help.ts +43 -0
- package/src/cli/index.ts +58 -266
- package/src/core/arg-parser.ts +48 -0
- package/src/core/commands/findings.ts +34 -144
- package/src/core/commands/review.ts +18 -35
- package/src/core/commands/scaffold.ts +174 -64
- package/src/core/commands/setup.ts +16 -11
- package/src/core/commands/update.ts +7 -7
- package/src/core/commands/watchdog.ts +8 -80
- package/src/core/config.ts +9 -19
- package/src/core/defaults.ts +20 -0
- package/src/core/diff-source.ts +1 -13
- package/src/core/doc-discoverer.ts +25 -15
- package/src/core/domain/budget.ts +42 -0
- package/src/core/domain/classification.ts +94 -0
- package/src/core/domain/diff.ts +53 -0
- package/src/core/domain/watchdog.ts +96 -0
- package/src/core/engine/carryover.ts +67 -0
- package/src/core/engine/houndsman-engine.ts +51 -0
- package/src/core/engine/review-scope.ts +95 -0
- package/src/core/engine/review-session.ts +51 -0
- package/src/core/file-classifier.ts +11 -67
- package/src/core/findings-state.ts +170 -38
- package/src/core/git-revision.ts +14 -40
- package/src/core/git-runner.ts +46 -0
- package/src/core/github-review.ts +4 -33
- package/src/core/orchestrator.ts +116 -337
- package/src/core/paths.ts +9 -0
- package/src/core/pr-context.ts +28 -54
- package/src/core/project-config.ts +57 -0
- package/src/core/renderers/batch-presenter.ts +40 -0
- package/src/core/renderers/brief-renderer.ts +23 -0
- package/src/core/renderers/findings-presenter.ts +74 -0
- package/src/core/renderers/manifest-writer.ts +112 -0
- package/src/core/renderers/report-renderer.ts +66 -0
- package/src/core/renderers/verify-presenter.ts +50 -0
- package/src/core/renderers/watchdog-presenter.ts +100 -0
- package/src/core/report-builder.ts +12 -7
- package/src/core/run-layout.ts +108 -0
- package/src/core/run-status.ts +6 -11
- package/src/core/services/clock.ts +12 -0
- package/src/core/services/file-classifier.ts +34 -0
- package/src/core/services/fix-batch-builder.ts +13 -0
- package/src/core/services/gh-client.ts +54 -0
- package/src/core/services/git-repository.ts +123 -0
- package/src/core/services/project-files.ts +79 -0
- package/src/core/services/verifier.ts +58 -0
- package/src/core/verify.ts +2 -1
- package/src/core/watchdog-registry.ts +58 -16
- package/src/index.ts +31 -1
- package/src/server/index.ts +64 -234
- package/src/server/run-repository.ts +291 -0
- package/src/tests/__snapshots__/baseline.test.ts.snap +814 -0
- package/src/tests/baseline.test.ts +513 -493
- package/src/tests/domain.test.ts +328 -0
- package/src/tests/infrastructure.test.ts +210 -0
package/src/cli/index.ts
CHANGED
|
@@ -26,281 +26,73 @@
|
|
|
26
26
|
* `--config <path>` · `--clean` · `--full` · `--no-jev` · `--fail-on-blocking`.
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
|
-
import { type HoundsmanConfig, resolveArtifactDir } from "../core/config";
|
|
30
29
|
import { loadConfig } from "../core/config-loader";
|
|
31
|
-
import {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
--file <text> Filtra por trecho do caminho (list)
|
|
65
|
-
--all Inclui findings ausentes do último relatório
|
|
66
|
-
--json Saída JSON
|
|
67
|
-
--limit <n> Limita o nº de itens (list)
|
|
68
|
-
--group <key> Grupo alvo no batch (default: o primeiro)
|
|
69
|
-
--offset <n> Paginação dentro do grupo (batch)
|
|
70
|
-
--include-done Inclui já concluídos no batch
|
|
71
|
-
--apply (verify) Marca os resolvidos como done
|
|
72
|
-
--fail-on-persisting (verify) Exit 1 se algum blocking persistir
|
|
73
|
-
--include-sent (submit) Reenvia findings já enviados à PR
|
|
74
|
-
--dry-run (submit) Só a prévia do envio, sem postar`;
|
|
75
|
-
|
|
76
|
-
function readFlag(args: string[], flag: string): string | undefined {
|
|
77
|
-
const index = args.indexOf(flag);
|
|
78
|
-
return index >= 0 ? args[index + 1] : undefined;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/** Passo 0 — contexto da PR via `gh`. */
|
|
82
|
-
async function runContext(config: HoundsmanConfig, args: string[]): Promise<void> {
|
|
83
|
-
const { runContextCommand } = await import("../core/commands/context");
|
|
84
|
-
const { paths, context } = runContextCommand(config, {
|
|
85
|
-
branch: readFlag(args, "--branch"),
|
|
86
|
-
dir: readFlag(args, "--dir"),
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
if (context.available) {
|
|
90
|
-
const src = context.source;
|
|
91
|
-
const label = [src?.number ? `#${src.number}` : "", src?.title ?? ""]
|
|
92
|
-
.filter(Boolean)
|
|
93
|
-
.join(" ");
|
|
94
|
-
console.log(
|
|
95
|
-
`[houndsman] PR ${label} (${src?.head} → ${src?.base}), ${context.stats?.files ?? 0} arquivo(s).`,
|
|
96
|
-
);
|
|
97
|
-
console.log(`[houndsman] contexto: ${relFromRoot(paths.context)}`);
|
|
98
|
-
console.log(`[houndsman] brief: ${relFromRoot(paths.brief)}`);
|
|
99
|
-
console.log(
|
|
100
|
-
`[houndsman] próximo passo: subagente resume em ${relFromRoot(paths.summary)}`,
|
|
101
|
-
);
|
|
102
|
-
} else {
|
|
103
|
-
console.log(
|
|
104
|
-
`[houndsman] contexto indisponível (${context.reason}): ${context.detail}`,
|
|
105
|
-
);
|
|
106
|
-
console.log(`[houndsman] marcador: ${relFromRoot(paths.context)}`);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/** Valida a configuração do projeto e o ambiente. */
|
|
111
|
-
async function runDoctor(config: HoundsmanConfig): Promise<void> {
|
|
112
|
-
const { checkRegistry } = await import("../core/registry-check");
|
|
113
|
-
console.log("→ Validando configuração do projeto...");
|
|
114
|
-
const { problems, warnings, jevQuestions } = checkRegistry(config);
|
|
115
|
-
|
|
116
|
-
if (problems.length > 0) {
|
|
117
|
-
console.error("\n❌ Problemas na configuração de revisão:\n");
|
|
118
|
-
for (const problem of problems) console.error(` - ${problem}`);
|
|
119
|
-
process.exit(1);
|
|
120
|
-
}
|
|
121
|
-
console.log(
|
|
122
|
-
` ✅ ${config.watchdogIds.length} watchdogs, ${jevQuestions} pergunta(s) Jev.`,
|
|
123
|
-
);
|
|
124
|
-
|
|
125
|
-
if (warnings.length > 0) {
|
|
126
|
-
console.log(`\n⚠️ ${warnings.length} aviso(s) (não bloqueantes):`);
|
|
127
|
-
for (const warning of warnings) console.log(` - ${warning}`);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
console.log("\n→ Ambiente:");
|
|
131
|
-
console.log(` ${Bun.which("git") ? "✅" : "❌"} git`);
|
|
132
|
-
console.log(` ${Bun.which("gh") ? "✅" : "⚠️ "} gh (opcional — contexto da PR)`);
|
|
133
|
-
await reportJevCredential();
|
|
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);
|
|
134
63
|
}
|
|
135
64
|
|
|
136
65
|
/**
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*/
|
|
140
|
-
async function reportJevCredential(): Promise<void> {
|
|
141
|
-
const { JevClient } = await import("../core/jev");
|
|
142
|
-
const client = new JevClient();
|
|
143
|
-
if (!client.isConfigured()) {
|
|
144
|
-
console.log(" ⚠️ TYPESAFE_API_KEY (opcional — pré-análise Jev)");
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
const check = await client.validateCredential();
|
|
148
|
-
if (check.ok) {
|
|
149
|
-
console.log(` ✅ Jev: credencial válida (modelo ${check.model}).`);
|
|
150
|
-
} else if (check.kind === "invalid") {
|
|
151
|
-
console.log(` ❌ Jev: credencial inválida — ${check.detail}`);
|
|
152
|
-
} else {
|
|
153
|
-
console.log(` ⚠️ Jev: não foi possível validar — ${check.detail}`);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/** Contexto de um watchdog. */
|
|
158
|
-
async function runWatchdog(config: HoundsmanConfig, args: string[]): Promise<void> {
|
|
159
|
-
const { renderWatchdogContext } = await import("../core/commands/watchdog");
|
|
160
|
-
const id = args.find((arg) => !arg.startsWith("-"));
|
|
161
|
-
|
|
162
|
-
if (!id || renderWatchdogContext(config, id) === null) {
|
|
163
|
-
console.error(
|
|
164
|
-
id
|
|
165
|
-
? `Watchdog '${id}' não existe.`
|
|
166
|
-
: "Informe o id do watchdog: houndsman watchdog <id>",
|
|
167
|
-
);
|
|
168
|
-
console.error(`Ids válidos: ${config.watchdogIds.join(", ")}`);
|
|
169
|
-
process.exit(1);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
console.log(
|
|
173
|
-
renderWatchdogContext(config, id, {
|
|
174
|
-
withRules: args.includes("--rules"),
|
|
175
|
-
json: args.includes("--json"),
|
|
176
|
-
}),
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Subcomando `findings` — triagem/correção dos findings (list/show/set/done/reset/
|
|
182
|
-
* sync/summary/batch/verify/path). Deixa o agente interagir com o estado sem abrir
|
|
183
|
-
* o JSON. O `verify` é assíncrono (consulta opcional ao Jev) — roteado à parte.
|
|
184
|
-
*/
|
|
185
|
-
async function runFindings(config: HoundsmanConfig, args: string[]): Promise<void> {
|
|
186
|
-
const { runFindingsCommand, runSubmitCommand, runVerifyCommand } = await import(
|
|
187
|
-
"../core/commands/findings"
|
|
188
|
-
);
|
|
189
|
-
const sub = args.find((arg) => !arg.startsWith("-"));
|
|
190
|
-
const result =
|
|
191
|
-
sub === "verify"
|
|
192
|
-
? await runVerifyCommand(config, args)
|
|
193
|
-
: sub === "submit"
|
|
194
|
-
? await runSubmitCommand(config, args)
|
|
195
|
-
: runFindingsCommand(config, args);
|
|
196
|
-
if (result.exitCode !== 0) {
|
|
197
|
-
console.error(result.output);
|
|
198
|
-
process.exit(result.exitCode);
|
|
199
|
-
}
|
|
200
|
-
console.log(result.output);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/** Abre a URL no navegador padrão (best-effort, por plataforma). */
|
|
204
|
-
function openBrowser(url: string): void {
|
|
205
|
-
try {
|
|
206
|
-
const command =
|
|
207
|
-
process.platform === "win32"
|
|
208
|
-
? ["cmd", "/c", "start", "", url]
|
|
209
|
-
: process.platform === "darwin"
|
|
210
|
-
? ["open", url]
|
|
211
|
-
: ["xdg-open", url];
|
|
212
|
-
Bun.spawn(command, { stdout: "ignore", stderr: "ignore" });
|
|
213
|
-
} catch {
|
|
214
|
-
// best-effort — se não abrir, o usuário copia a URL.
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/** Subcomando `server` — sobe a UI local de triagem. */
|
|
219
|
-
async function runServer(config: HoundsmanConfig, args: string[]): Promise<void> {
|
|
220
|
-
const { startServer } = await import("../server");
|
|
221
|
-
const port = Number(readFlag(args, "--port")) || 7331;
|
|
222
|
-
const host = readFlag(args, "--host") ?? "127.0.0.1";
|
|
223
|
-
const { url } = startServer({ config, port, host });
|
|
224
|
-
console.log(`[houndsman] servidor em ${url}`);
|
|
225
|
-
console.log("[houndsman] Ctrl+C para encerrar.");
|
|
226
|
-
if (args.includes("--open")) openBrowser(url);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/** Passo 1/3 — preparar ou finalizar. */
|
|
230
|
-
async function runReview(config: HoundsmanConfig, args: string[]): Promise<void> {
|
|
231
|
-
const [{ parseReviewArgs }, { finalize, prepare, resolveBranch }] = await Promise.all(
|
|
232
|
-
[import("../core/commands/review"), import("../core/orchestrator")],
|
|
233
|
-
);
|
|
234
|
-
const opts = parseReviewArgs(args, resolveArtifactDir(config));
|
|
235
|
-
const branch = resolveBranch(opts);
|
|
236
|
-
|
|
237
|
-
if (opts.finalize) {
|
|
238
|
-
const result = finalize(opts, branch, config);
|
|
239
|
-
if (opts.failOnBlocking && result.blockers > 0) process.exit(1);
|
|
240
|
-
} else {
|
|
241
|
-
await prepare(opts, branch, config);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/**
|
|
246
|
-
* Executa um subcomando. `--config` sobrepõe a descoberta automática.
|
|
247
|
-
* 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).
|
|
248
68
|
*/
|
|
249
69
|
export async function main(): Promise<void> {
|
|
250
70
|
const argv = process.argv.slice(2);
|
|
251
|
-
const
|
|
252
|
-
const
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const { runSetupCommand } = await import("../core/commands/setup");
|
|
261
|
-
await runSetupCommand({ cwd: process.cwd(), args: rest });
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
if (command === "update") {
|
|
265
|
-
const { runUpdateCommand } = await import("../core/commands/update");
|
|
266
|
-
await runUpdateCommand({ cwd: process.cwd(), args: rest });
|
|
267
|
-
return;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
if (command === "help") {
|
|
271
|
-
console.info(HELP);
|
|
272
|
-
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);
|
|
273
80
|
}
|
|
274
81
|
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
return;
|
|
290
|
-
case "findings":
|
|
291
|
-
await runFindings(config, rest);
|
|
292
|
-
return;
|
|
293
|
-
case "server":
|
|
294
|
-
await runServer(config, rest);
|
|
295
|
-
return;
|
|
296
|
-
case undefined:
|
|
297
|
-
await runReview(config, rest);
|
|
298
|
-
return;
|
|
299
|
-
default:
|
|
300
|
-
console.error(`Subcomando desconhecido: '${command}'.\n`);
|
|
301
|
-
console.error(HELP);
|
|
302
|
-
process.exit(1);
|
|
303
|
-
}
|
|
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);
|
|
304
96
|
}
|
|
305
97
|
|
|
306
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
|
+
}
|