@payconductor/houndsman 0.7.0 → 0.10.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/README.md +20 -0
- package/package.json +1 -1
- package/src/cli/commands/command.ts +10 -1
- package/src/cli/commands/context-command.ts +40 -2
- package/src/cli/commands/hunt-command.ts +28 -2
- package/src/cli/commands/review-command.ts +30 -2
- package/src/cli/commands/worktree-command.ts +118 -0
- package/src/cli/help.ts +12 -2
- package/src/cli/index.ts +14 -4
- package/src/core/ai/ai-client.ts +21 -0
- package/src/core/ai/ai-sdk-model-provider.ts +43 -18
- package/src/core/ai/features/technical-summarizer.ts +43 -0
- package/src/core/ai/index.ts +4 -1
- package/src/core/ai/prompts/context.ts +3 -3
- package/src/core/ai/prompts/technical.ts +30 -0
- package/src/core/ai/provider-capabilities.ts +45 -0
- package/src/core/ai/schemas.ts +19 -3
- package/src/core/ai/types.ts +3 -1
- package/src/core/ast/service.ts +7 -2
- package/src/core/brief.ts +26 -4
- package/src/core/commands/context.ts +9 -1
- package/src/core/commands/findings.ts +11 -5
- package/src/core/commands/review.ts +13 -0
- package/src/core/commands/scaffold.ts +9 -2
- package/src/core/config.ts +30 -1
- package/src/core/defaults.ts +7 -0
- package/src/core/engine/houndsman-engine.ts +26 -3
- package/src/core/engine/review-scope.ts +17 -22
- package/src/core/engine/review-session.ts +25 -2
- package/src/core/engine/review-workspace.ts +204 -0
- package/src/core/git-runner.ts +8 -1
- package/src/core/jev/jev-detector.ts +10 -2
- package/src/core/orchestrator.ts +176 -15
- package/src/core/paths.ts +3 -0
- package/src/core/pr-context.ts +11 -6
- package/src/core/project-config.ts +21 -1
- package/src/core/renderers/resume-renderer.ts +17 -34
- package/src/core/report-builder.ts +6 -6
- package/src/core/services/git-repository.ts +46 -0
- package/src/core/services/pr-resolver.ts +147 -0
- package/src/core/services/worktree-manager.ts +293 -0
- package/src/core/storage/file/file-storage.ts +14 -1
- package/src/core/storage/paths.ts +30 -1
- package/src/core/storage/sqlite/mappers.ts +4 -0
- package/src/core/storage/sqlite/schema.ts +13 -3
- package/src/core/storage/sqlite/sqlite-storage.ts +28 -8
- package/src/core/storage/types.ts +13 -0
- package/src/core/storage/work-area.ts +20 -1
- package/src/core/technical-summary.ts +398 -0
- package/src/core/types.ts +2 -0
- package/src/harness/agent/agent-factory.ts +1 -0
- package/src/harness/agent/review-agent.ts +11 -1
- package/src/harness/config/harness-config.ts +6 -1
- package/src/harness/hunt/hunt-options.ts +17 -0
- package/src/harness/hunt/hunt-session.ts +7 -20
- package/src/index.ts +33 -0
- package/src/server/client/App.tsx +51 -12
- package/src/server/client/DeleteRunModal.tsx +163 -0
- package/src/server/client/PrsSection.tsx +296 -0
- package/src/server/client/ResumePanel.tsx +17 -20
- package/src/server/client/RunsSection.tsx +73 -0
- package/src/server/client/Sidebar.tsx +171 -358
- package/src/server/client/icons.tsx +10 -0
- package/src/server/client/sidebar-sections.tsx +73 -0
- package/src/server/client/utils/constants.ts +7 -2
- package/src/server/client/utils/sidebar-prefs.ts +64 -0
- package/src/server/client/utils/types.ts +27 -0
- package/src/server/index.ts +113 -406
- package/src/server/pr-inbox.ts +15 -4
- package/src/server/run-repository.ts +103 -2
- package/src/server/styles.css +454 -0
- package/src/skill/README.md +11 -3
- package/src/skill/SKILL.md +44 -10
- package/src/skill/hunt.md +33 -7
- package/src/tests/__snapshots__/baseline.test.ts.snap +99 -24
- package/src/tests/ai-core.test.ts +71 -0
- package/src/tests/ai-features.test.ts +36 -0
- package/src/tests/baseline.test.ts +4 -1
- package/src/tests/cli.test.ts +2 -1
- package/src/tests/harness-agent.test.ts +19 -0
- package/src/tests/review-resume.test.ts +36 -8
- package/src/tests/review-scope.test.ts +67 -0
- package/src/tests/review-scripts.test.ts +2 -2
- package/src/tests/server.test.ts +54 -1
- package/src/tests/sidebar-sections.test.ts +94 -0
- package/src/tests/technical-summary.test.ts +176 -0
- package/src/tests/worktree.test.ts +404 -0
- package/assets/logo-legacy.png +0 -0
package/README.md
CHANGED
|
@@ -98,6 +98,26 @@ bun run houndsman hunt --fail-on-blocking
|
|
|
98
98
|
|
|
99
99
|
Detalhes (catálogo de tools, máquina de estados, custos e segurança): [`docs/HARNESS.md`](docs/HARNESS.md).
|
|
100
100
|
|
|
101
|
+
### 8. Várias PRs ao mesmo tempo (worktrees)
|
|
102
|
+
|
|
103
|
+
Cada revisão pode rodar em um **worktree dedicado** (detached no commit da PR), sem
|
|
104
|
+
checkout no clone principal e sem bloquear a revisão das outras PRs:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
houndsman --pr 123 # cria/reusa o worktree e prepara os briefs
|
|
108
|
+
houndsman hunt --pr 123 # revisão nativa no mesmo worktree
|
|
109
|
+
houndsman --pr 123 --finalize # consolida o relatório
|
|
110
|
+
houndsman worktree list # worktrees ativos
|
|
111
|
+
houndsman worktree remove feature/x
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Os worktrees ficam em `<repo>/.houndsman/worktrees/<repo>/<branch>` (ignorados pelo git) e
|
|
115
|
+
são removidos manualmente (ou com `houndsman worktree prune`). Ficam **dentro do
|
|
116
|
+
workspace** de propósito: agentes de codificação com permissão escopada à pasta aberta
|
|
117
|
+
conseguem ler/executar sem pedir autorização. Para voltar ao diretório global, use
|
|
118
|
+
`worktrees: { location: "home" }` no `houndsman.config.ts`. A UI (`houndsman server`)
|
|
119
|
+
mostra o worktree ativo de cada PR e permite criá-lo pela sidebar.
|
|
120
|
+
|
|
101
121
|
---
|
|
102
122
|
|
|
103
123
|
## O fluxo de trabalho
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@payconductor/houndsman",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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",
|
|
@@ -5,14 +5,23 @@
|
|
|
5
5
|
* exit code. O engine é carregado sob demanda (comandos sem projeto não pagam).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import type { HoundsmanConfig } from "../../core/config";
|
|
8
9
|
import type { HoundsmanEngine } from "../../core/engine/houndsman-engine";
|
|
9
10
|
import type { LoggerLike } from "../../core/logging";
|
|
10
11
|
|
|
12
|
+
/** Raiz operacional pedida ao engine (worktree/clone principal). */
|
|
13
|
+
export type EngineLoadOptions = {
|
|
14
|
+
root?: string;
|
|
15
|
+
mainRoot?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
11
18
|
/** Contexto de execução de um comando. */
|
|
12
19
|
export type CliContext = {
|
|
13
20
|
cwd: string;
|
|
14
21
|
/** Carrega a config do projeto e devolve o engine (lazy). */
|
|
15
|
-
engine: () => Promise<HoundsmanEngine>;
|
|
22
|
+
engine: (options?: EngineLoadOptions) => Promise<HoundsmanEngine>;
|
|
23
|
+
/** Config do projeto (mesmo carregamento cacheado do engine). */
|
|
24
|
+
config: () => Promise<HoundsmanConfig>;
|
|
16
25
|
/** Logger único do CLI (cores/spinner/progresso). */
|
|
17
26
|
logger: LoggerLike;
|
|
18
27
|
/** @deprecated use `logger.info`. */
|
|
@@ -1,6 +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 { resolveReviewWorkspace } from "../../core/engine/review-workspace";
|
|
4
5
|
import { displayPath } from "../../core/paths";
|
|
5
6
|
import { ArgParser } from "../arg-parser";
|
|
6
7
|
import type { CliContext, Command } from "./command";
|
|
@@ -9,13 +10,50 @@ export class ContextCommand implements Command {
|
|
|
9
10
|
readonly name = "context";
|
|
10
11
|
|
|
11
12
|
async run(args: string[], context: CliContext): Promise<number> {
|
|
12
|
-
const engine = await context.engine();
|
|
13
13
|
const parser = ArgParser.from(args);
|
|
14
|
+
const prFlag = parser.value("--pr");
|
|
15
|
+
const worktreeFlag = parser.value("--worktree");
|
|
16
|
+
const prNumber = prFlag ? Number.parseInt(prFlag, 10) : undefined;
|
|
17
|
+
if (prFlag && (!Number.isFinite(prNumber) || (prNumber ?? 0) <= 0)) {
|
|
18
|
+
context.err(`--pr inválido: '${prFlag}'.`);
|
|
19
|
+
return 1;
|
|
20
|
+
}
|
|
21
|
+
if (prNumber !== undefined && worktreeFlag !== undefined) {
|
|
22
|
+
context.err("Use --pr ou --worktree, não os dois.");
|
|
23
|
+
return 1;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const config = await context.config();
|
|
27
|
+
const workspace =
|
|
28
|
+
prNumber !== undefined || worktreeFlag !== undefined
|
|
29
|
+
? resolveReviewWorkspace(
|
|
30
|
+
{
|
|
31
|
+
cwd: context.cwd,
|
|
32
|
+
base: "main",
|
|
33
|
+
head: "HEAD",
|
|
34
|
+
branch: parser.value("--branch"),
|
|
35
|
+
pr: prNumber,
|
|
36
|
+
worktree: worktreeFlag,
|
|
37
|
+
},
|
|
38
|
+
{ worktreesLocation: config.worktrees?.location },
|
|
39
|
+
)
|
|
40
|
+
: undefined;
|
|
41
|
+
for (const message of workspace?.messages ?? []) context.out(message);
|
|
42
|
+
|
|
43
|
+
const engine = await context.engine(
|
|
44
|
+
workspace
|
|
45
|
+
? { root: workspace.root, mainRoot: workspace.mainRoot }
|
|
46
|
+
: undefined,
|
|
47
|
+
);
|
|
14
48
|
const { paths, context: pr } = runContextCommand(
|
|
15
49
|
engine.config,
|
|
16
50
|
engine.storage,
|
|
17
51
|
engine.repo,
|
|
18
|
-
{
|
|
52
|
+
{
|
|
53
|
+
branch: workspace?.branch ?? parser.value("--branch"),
|
|
54
|
+
worktreePath: workspace?.worktreePath,
|
|
55
|
+
prNumber: workspace?.prNumber,
|
|
56
|
+
},
|
|
19
57
|
);
|
|
20
58
|
|
|
21
59
|
if (pr.available) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Comando `hunt` — revisão nativa com IA (harness próprio). */
|
|
2
2
|
|
|
3
|
+
import { resolveReviewWorkspace } from "../../core/engine/review-workspace";
|
|
3
4
|
import { HuntHarness } from "../../harness";
|
|
4
5
|
import { parseHuntArgs } from "../../harness/hunt/hunt-options";
|
|
5
6
|
import type { CliContext, Command } from "./command";
|
|
@@ -10,9 +11,34 @@ export class HuntCommand implements Command {
|
|
|
10
11
|
|
|
11
12
|
async run(args: string[], context: CliContext): Promise<number> {
|
|
12
13
|
const options = parseHuntArgs(args);
|
|
13
|
-
const
|
|
14
|
+
const config = await context.config();
|
|
15
|
+
const workspace = resolveReviewWorkspace(
|
|
16
|
+
{
|
|
17
|
+
cwd: context.cwd,
|
|
18
|
+
base: options.base,
|
|
19
|
+
head: options.head,
|
|
20
|
+
branch: options.branch,
|
|
21
|
+
pr: options.pr,
|
|
22
|
+
worktree: options.worktree,
|
|
23
|
+
},
|
|
24
|
+
{ worktreesLocation: config.worktrees?.location },
|
|
25
|
+
);
|
|
26
|
+
for (const message of workspace.messages) context.logger.info(message);
|
|
27
|
+
|
|
28
|
+
const engine = await context.engine({
|
|
29
|
+
root: workspace.root,
|
|
30
|
+
mainRoot: workspace.mainRoot,
|
|
31
|
+
});
|
|
32
|
+
const resolved = {
|
|
33
|
+
...options,
|
|
34
|
+
branch: workspace.branch,
|
|
35
|
+
base: workspace.base,
|
|
36
|
+
head: workspace.head,
|
|
37
|
+
pr: workspace.prNumber,
|
|
38
|
+
worktreePath: workspace.worktreePath,
|
|
39
|
+
};
|
|
14
40
|
try {
|
|
15
|
-
const harness = await HuntHarness.from(engine,
|
|
41
|
+
const harness = await HuntHarness.from(engine, resolved, context.logger);
|
|
16
42
|
const result = await harness.run();
|
|
17
43
|
for (const message of result.messages) context.logger.info(message);
|
|
18
44
|
if (result.status === "failed") return 1;
|
|
@@ -1,15 +1,43 @@
|
|
|
1
1
|
/** Comando default — prepara ou finaliza a revisão. */
|
|
2
2
|
|
|
3
3
|
import { parseReviewArgs } from "../../core/commands/review";
|
|
4
|
+
import { resolveReviewWorkspace } from "../../core/engine/review-workspace";
|
|
4
5
|
import type { CliContext, Command } from "./command";
|
|
5
6
|
|
|
6
7
|
export class ReviewCommand implements Command {
|
|
7
8
|
readonly name = "";
|
|
8
9
|
|
|
9
10
|
async run(args: string[], context: CliContext): Promise<number> {
|
|
10
|
-
const engine = await context.engine();
|
|
11
11
|
const options = parseReviewArgs(args);
|
|
12
|
-
const
|
|
12
|
+
const config = await context.config();
|
|
13
|
+
const workspace = resolveReviewWorkspace(
|
|
14
|
+
{
|
|
15
|
+
cwd: context.cwd,
|
|
16
|
+
base: options.base,
|
|
17
|
+
head: options.head,
|
|
18
|
+
branch: options.branch,
|
|
19
|
+
pr: options.pr,
|
|
20
|
+
worktree: options.worktree,
|
|
21
|
+
},
|
|
22
|
+
{ worktreesLocation: config.worktrees?.location },
|
|
23
|
+
);
|
|
24
|
+
for (const message of workspace.messages) context.out(message);
|
|
25
|
+
|
|
26
|
+
// Sem `--pr`/`--worktree`, o engine usa o workspace gravado da review
|
|
27
|
+
// (finalize após um prepare feito em worktree, por exemplo).
|
|
28
|
+
const explicit = options.pr !== undefined || options.worktree !== undefined;
|
|
29
|
+
const engine = await context.engine(
|
|
30
|
+
explicit ? { root: workspace.root, mainRoot: workspace.mainRoot } : undefined,
|
|
31
|
+
);
|
|
32
|
+
const resolved = {
|
|
33
|
+
...options,
|
|
34
|
+
branch: workspace.branch,
|
|
35
|
+
base: workspace.base,
|
|
36
|
+
head: workspace.head,
|
|
37
|
+
pr: workspace.prNumber,
|
|
38
|
+
worktreePath: workspace.worktreePath,
|
|
39
|
+
};
|
|
40
|
+
const result = await engine.session(resolved).run();
|
|
13
41
|
|
|
14
42
|
for (const message of result.messages) context.out(message);
|
|
15
43
|
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comando `worktree` — gerencia os worktrees de revisão.
|
|
3
|
+
*
|
|
4
|
+
* `list` (default) · `add --pr <n>` · `add --branch <b> [--ref <r>]` ·
|
|
5
|
+
* `remove <branch|path> [--force]` · `prune` · `path <branch>`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { resolveReviewWorkspace } from "../../core/engine/review-workspace";
|
|
9
|
+
import { WorktreeManager } from "../../core/services/worktree-manager";
|
|
10
|
+
import { resolveWorktreesRoot } from "../../core/storage/paths";
|
|
11
|
+
import { ArgParser } from "../arg-parser";
|
|
12
|
+
import type { CliContext, Command } from "./command";
|
|
13
|
+
|
|
14
|
+
/** Executa a manutenção dos worktrees. */
|
|
15
|
+
export class WorktreeCommand implements Command {
|
|
16
|
+
readonly name = "worktree";
|
|
17
|
+
|
|
18
|
+
async run(args: string[], context: CliContext): Promise<number> {
|
|
19
|
+
const parser = ArgParser.from(args);
|
|
20
|
+
const action = parser.command() ?? "list";
|
|
21
|
+
const config = await context.config();
|
|
22
|
+
const engine = await context.engine();
|
|
23
|
+
const manager = new WorktreeManager(engine.git, {
|
|
24
|
+
repoKey: engine.repo.key,
|
|
25
|
+
repoRoot: engine.mainRoot,
|
|
26
|
+
baseDir: resolveWorktreesRoot(config.worktrees?.location, engine.mainRoot),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
switch (action) {
|
|
30
|
+
case "list": {
|
|
31
|
+
const entries = manager.managed();
|
|
32
|
+
if (entries.length === 0) {
|
|
33
|
+
context.out(`Nenhum worktree em ${manager.baseDir}.`);
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
context.out(`Worktrees em ${manager.baseDir}:`);
|
|
37
|
+
for (const entry of entries) {
|
|
38
|
+
const branch = entry.branch
|
|
39
|
+
? entry.branch.replace(/^refs\/heads\//, "")
|
|
40
|
+
: "(detached)";
|
|
41
|
+
context.out(
|
|
42
|
+
`- ${branch} · ${entry.path}${entry.prunable ? " · órfão" : ""}`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
case "add": {
|
|
48
|
+
const pr = parser.value("--pr");
|
|
49
|
+
if (pr) {
|
|
50
|
+
const number = Number.parseInt(pr, 10);
|
|
51
|
+
if (!Number.isFinite(number) || number <= 0) {
|
|
52
|
+
context.err(`--pr inválido: '${pr}'.`);
|
|
53
|
+
return 1;
|
|
54
|
+
}
|
|
55
|
+
const workspace = resolveReviewWorkspace(
|
|
56
|
+
{
|
|
57
|
+
cwd: context.cwd,
|
|
58
|
+
base: "main",
|
|
59
|
+
head: "HEAD",
|
|
60
|
+
pr: number,
|
|
61
|
+
},
|
|
62
|
+
{ worktreesLocation: config.worktrees?.location },
|
|
63
|
+
);
|
|
64
|
+
for (const message of workspace.messages) context.out(message);
|
|
65
|
+
context.out(workspace.worktreePath ?? workspace.root);
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const branch = parser.value("--branch");
|
|
70
|
+
if (!branch) {
|
|
71
|
+
context.err("Informe --pr <n> ou --branch <nome>.");
|
|
72
|
+
return 1;
|
|
73
|
+
}
|
|
74
|
+
const ref = parser.value("--ref") ?? branch;
|
|
75
|
+
const result = manager.ensure({ branch, ref });
|
|
76
|
+
context.out(
|
|
77
|
+
`${result.created ? "Criado" : "Reutilizado"}: ${result.path} @ ${result.head.slice(0, 7)}`,
|
|
78
|
+
);
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
case "remove": {
|
|
82
|
+
const target = parser.positionals()[1] ?? parser.value("--branch");
|
|
83
|
+
if (!target) {
|
|
84
|
+
context.err("Informe a branch ou o caminho do worktree.");
|
|
85
|
+
return 1;
|
|
86
|
+
}
|
|
87
|
+
const result = manager.remove(target, { force: parser.has("--force") });
|
|
88
|
+
if (!result.removed) {
|
|
89
|
+
context.err(
|
|
90
|
+
`Worktree não removido: ${result.detail ?? "desconhecido"}.`,
|
|
91
|
+
);
|
|
92
|
+
return 1;
|
|
93
|
+
}
|
|
94
|
+
context.out(`Worktree removido: ${target}.`);
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
case "prune": {
|
|
98
|
+
manager.prune();
|
|
99
|
+
context.out(`Worktrees órfãos removidos em ${manager.baseDir}.`);
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
case "path": {
|
|
103
|
+
const branch = parser.positionals()[1] ?? parser.value("--branch");
|
|
104
|
+
if (!branch) {
|
|
105
|
+
context.err("Informe a branch.");
|
|
106
|
+
return 1;
|
|
107
|
+
}
|
|
108
|
+
context.out(manager.pathFor(branch));
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
111
|
+
default:
|
|
112
|
+
context.err(
|
|
113
|
+
`Ação desconhecida em 'worktree': '${action}'. Use list, add, remove, prune ou path.`,
|
|
114
|
+
);
|
|
115
|
+
return 1;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
package/src/cli/help.ts
CHANGED
|
@@ -4,10 +4,11 @@ export const HELP = `Uso: houndsman [comando] [flags]
|
|
|
4
4
|
Comandos:
|
|
5
5
|
(nenhum) Prepara a revisão (classification + briefs + manifesto)
|
|
6
6
|
--finalize Consolida os findings em review.md
|
|
7
|
-
context Coleta o contexto da PR via gh (Passo 0)
|
|
7
|
+
context Coleta o contexto da PR via gh (Passo 0; --pr/--worktree)
|
|
8
8
|
hunt Revisão nativa com IA (harness próprio): prepare + agentes + finalize
|
|
9
9
|
findings <sub> Triagem/correção dos findings (list · show · set · done · reset · sync · summary · batch · verify · submit · path)
|
|
10
10
|
status [ping] Mostra o status da run (ping renova o heartbeat da execução)
|
|
11
|
+
worktree <ação> Lista/cria/remove worktrees de revisão (list · add · remove · prune · path)
|
|
11
12
|
server Sobe a UI local de triagem (--port, --host, --open, --editor)
|
|
12
13
|
setup Cria a configuração inicial do projeto (config + skills)
|
|
13
14
|
update Sincroniza os arquivos auto-gerados (.houndsman/core)
|
|
@@ -20,6 +21,8 @@ Flags:
|
|
|
20
21
|
--base <branch> Base do diff (default: main)
|
|
21
22
|
--head <ref> Head do diff (default: HEAD)
|
|
22
23
|
--branch <nome> Branch alvo (default: detecta do git)
|
|
24
|
+
--pr <n> Revisa a PR em um worktree dedicado (cria/reusa automaticamente)
|
|
25
|
+
--worktree <path> Revisa em um worktree já existente
|
|
23
26
|
--clean Descarta findings brutos do staging anterior
|
|
24
27
|
--full Força review completo (ignora o modo incremental)
|
|
25
28
|
--no-jev Desativa a pré-análise Jev
|
|
@@ -28,7 +31,7 @@ Flags:
|
|
|
28
31
|
--fail-on-blocking Exit 1 se houver bloqueantes (com --finalize)
|
|
29
32
|
|
|
30
33
|
Flags do hunt:
|
|
31
|
-
--base <b> · --head <ref> · --branch <nome> · --watchdog <id>
|
|
34
|
+
--base <b> · --head <ref> · --branch <nome> · --watchdog <id> · --pr <n> · --worktree <path>
|
|
32
35
|
--full · --clean · --no-ast · --with-jev
|
|
33
36
|
--resume · --allow-partial · --dry-run · --fail-on-blocking
|
|
34
37
|
--max-calls <n> · --concurrency <n> · --provider <p> · --model <m>
|
|
@@ -52,6 +55,13 @@ Flags de findings:
|
|
|
52
55
|
--dry-run (submit) Só a prévia do envio, sem postar
|
|
53
56
|
--no-ping (doctor) Pula a validação de rede das credenciais
|
|
54
57
|
|
|
58
|
+
Worktrees:
|
|
59
|
+
houndsman worktree list
|
|
60
|
+
houndsman worktree add --pr 123
|
|
61
|
+
houndsman worktree remove <branch> [--force]
|
|
62
|
+
Os worktrees ficam em <repo>/.houndsman/worktrees/<repo>/<branch> (detached).
|
|
63
|
+
Use worktrees.location: "home" na config para voltar ao HOUNDSMAN_HOME.
|
|
64
|
+
|
|
55
65
|
Persistência:
|
|
56
66
|
Runs, findings e artefatos ficam no storage global (SQLite em
|
|
57
67
|
<HOUNDSMAN_HOME>/houndsman.db). Use storage.provider: "file" na config para
|
package/src/cli/index.ts
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
* `--clean` · `--full` · `--no-jev` · `--no-ast` · `--fail-on-blocking`.
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
+
import type { HoundsmanConfig } from "../core/config";
|
|
30
31
|
import { loadConfig } from "../core/config-loader";
|
|
31
32
|
import { HoundsmanEngine } from "../core/engine/houndsman-engine";
|
|
32
33
|
import { logger } from "../core/logging";
|
|
@@ -43,6 +44,7 @@ import { ServerCommand } from "./commands/server-command";
|
|
|
43
44
|
import { SetupCommand, TestCommand, UpdateCommand } from "./commands/setup-command";
|
|
44
45
|
import { StatusCommand } from "./commands/status-command";
|
|
45
46
|
import { WatchdogCommand } from "./commands/watchdog-command";
|
|
47
|
+
import { WorktreeCommand } from "./commands/worktree-command";
|
|
46
48
|
import { HELP } from "./help";
|
|
47
49
|
|
|
48
50
|
// ---------------------------------------------------------------------------
|
|
@@ -60,6 +62,7 @@ function buildRegistry(): CommandRegistry {
|
|
|
60
62
|
.register(new FindingsCommand())
|
|
61
63
|
.register(new StatusCommand())
|
|
62
64
|
.register(new WatchdogCommand())
|
|
65
|
+
.register(new WorktreeCommand())
|
|
63
66
|
.register(new ServerCommand())
|
|
64
67
|
.register(new SetupCommand())
|
|
65
68
|
.register(new UpdateCommand())
|
|
@@ -85,16 +88,23 @@ export async function main(): Promise<void> {
|
|
|
85
88
|
process.exit(1);
|
|
86
89
|
}
|
|
87
90
|
|
|
91
|
+
let loaded: Awaited<ReturnType<typeof loadConfig>> | null = null;
|
|
92
|
+
const config = async (): Promise<HoundsmanConfig> => {
|
|
93
|
+
loaded ??= await loadConfig({ configPath: parser.value("--config") });
|
|
94
|
+
return loaded.config;
|
|
95
|
+
};
|
|
96
|
+
|
|
88
97
|
const context: CliContext = {
|
|
89
98
|
cwd: process.cwd(),
|
|
90
99
|
logger,
|
|
91
100
|
out: (message) => logger.info(message),
|
|
92
101
|
err: (message) => logger.error(message),
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
102
|
+
config,
|
|
103
|
+
engine: async (options = {}) => {
|
|
104
|
+
return new HoundsmanEngine(await config(), {
|
|
105
|
+
root: options.root,
|
|
106
|
+
mainRoot: options.mainRoot,
|
|
96
107
|
});
|
|
97
|
-
return new HoundsmanEngine(config);
|
|
98
108
|
},
|
|
99
109
|
};
|
|
100
110
|
|
package/src/core/ai/ai-client.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { ZodType } from "zod";
|
|
10
|
+
import type { AiProviderOptions } from "../config";
|
|
10
11
|
import type { ProjectConfig } from "../project-config";
|
|
11
12
|
import { AiConfigurationError, errorMessage } from "./errors";
|
|
12
13
|
import { ModelFactory } from "./model-factory";
|
|
@@ -33,6 +34,8 @@ export type ObjectRequest<T> = {
|
|
|
33
34
|
maxSteps?: number;
|
|
34
35
|
maxOutputTokens?: number;
|
|
35
36
|
temperature?: number;
|
|
37
|
+
/** Opções do AI SDK por provider (mescladas com a config do projeto). */
|
|
38
|
+
providerOptions?: AiProviderOptions;
|
|
36
39
|
signal?: AbortSignal;
|
|
37
40
|
};
|
|
38
41
|
|
|
@@ -188,6 +191,10 @@ export class AiClient {
|
|
|
188
191
|
const provider = await this.requireProvider(model);
|
|
189
192
|
const response = await provider.generate({
|
|
190
193
|
...request,
|
|
194
|
+
providerOptions: mergeProviderOptions(
|
|
195
|
+
this.project.aiProviderOptions,
|
|
196
|
+
request.providerOptions,
|
|
197
|
+
),
|
|
191
198
|
signal: combineSignals(request.signal, this.project.aiTimeoutMs),
|
|
192
199
|
});
|
|
193
200
|
this.budget.record(response.usage);
|
|
@@ -204,6 +211,20 @@ export class AiClient {
|
|
|
204
211
|
}
|
|
205
212
|
}
|
|
206
213
|
|
|
214
|
+
/** Mescla opções por provider (o override do chamador vence chave a chave). */
|
|
215
|
+
function mergeProviderOptions(
|
|
216
|
+
base: AiProviderOptions | undefined,
|
|
217
|
+
override: AiProviderOptions | undefined,
|
|
218
|
+
): AiProviderOptions | undefined {
|
|
219
|
+
if (!base) return override;
|
|
220
|
+
if (!override) return base;
|
|
221
|
+
const merged: AiProviderOptions = { ...base };
|
|
222
|
+
for (const [provider, values] of Object.entries(override)) {
|
|
223
|
+
merged[provider] = { ...base[provider], ...values };
|
|
224
|
+
}
|
|
225
|
+
return merged;
|
|
226
|
+
}
|
|
227
|
+
|
|
207
228
|
/** Combina o sinal do chamador com o timeout configurado. */
|
|
208
229
|
function combineSignals(
|
|
209
230
|
signal: AbortSignal | undefined,
|
|
@@ -10,6 +10,7 @@ import type { LanguageModel, ToolSet } from "ai";
|
|
|
10
10
|
import { generateText, Output, stepCountIs, tool } from "ai";
|
|
11
11
|
import type { AiProviderKind } from "../config";
|
|
12
12
|
import { AiProviderError } from "./errors";
|
|
13
|
+
import { temperatureApplies } from "./provider-capabilities";
|
|
13
14
|
import {
|
|
14
15
|
type AiTool,
|
|
15
16
|
type AiUsage,
|
|
@@ -45,6 +46,47 @@ function numberOrZero(value: unknown): number {
|
|
|
45
46
|
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
/** Argumentos aceitos por `generateText` (inclui `providerOptions` do SDK). */
|
|
50
|
+
type GenerateTextArgs = Parameters<typeof generateText>[0];
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Argumentos de `generateText` derivados do pedido (puro — testável sem rede).
|
|
54
|
+
*
|
|
55
|
+
* `temperature` só é enviada quando o provider de fato a aplica: em thinking
|
|
56
|
+
* mode (DeepSeek V4 default) o AI SDK a descarta e emite warning por chamada.
|
|
57
|
+
*/
|
|
58
|
+
export function buildGenerateTextArgs<T>(
|
|
59
|
+
model: LanguageModel,
|
|
60
|
+
request: ModelRequest<T>,
|
|
61
|
+
provider: AiProviderKind,
|
|
62
|
+
modelId: string,
|
|
63
|
+
) {
|
|
64
|
+
const tools = request.tools ? toToolSet(request.tools) : undefined;
|
|
65
|
+
const hasTools = tools !== undefined && Object.keys(tools).length > 0;
|
|
66
|
+
const maxSteps = request.maxSteps ?? 1;
|
|
67
|
+
const providerOptions = request.providerOptions
|
|
68
|
+
? (request.providerOptions as GenerateTextArgs["providerOptions"])
|
|
69
|
+
: undefined;
|
|
70
|
+
const temperature =
|
|
71
|
+
request.temperature !== undefined &&
|
|
72
|
+
temperatureApplies(provider, modelId, request.providerOptions)
|
|
73
|
+
? request.temperature
|
|
74
|
+
: undefined;
|
|
75
|
+
return {
|
|
76
|
+
model,
|
|
77
|
+
system: request.instructions,
|
|
78
|
+
prompt: request.prompt,
|
|
79
|
+
...(hasTools ? { tools, stopWhen: stepCountIs(maxSteps) } : {}),
|
|
80
|
+
...(request.outputSchema
|
|
81
|
+
? { output: Output.object({ schema: request.outputSchema }) }
|
|
82
|
+
: {}),
|
|
83
|
+
...(request.maxOutputTokens ? { maxOutputTokens: request.maxOutputTokens } : {}),
|
|
84
|
+
...(providerOptions ? { providerOptions } : {}),
|
|
85
|
+
...(temperature !== undefined ? { temperature } : {}),
|
|
86
|
+
maxRetries: 0,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
48
90
|
/** Provider de modelo apoiado no AI SDK. */
|
|
49
91
|
export class AiSdkModelProvider implements ModelProvider {
|
|
50
92
|
constructor(
|
|
@@ -54,27 +96,10 @@ export class AiSdkModelProvider implements ModelProvider {
|
|
|
54
96
|
) {}
|
|
55
97
|
|
|
56
98
|
async generate<T>(request: ModelRequest<T>): Promise<ModelResponse<T>> {
|
|
57
|
-
const tools = request.tools ? toToolSet(request.tools) : undefined;
|
|
58
|
-
const hasTools = tools !== undefined && Object.keys(tools).length > 0;
|
|
59
|
-
const maxSteps = request.maxSteps ?? 1;
|
|
60
|
-
|
|
61
99
|
try {
|
|
62
100
|
const result = await generateText({
|
|
63
|
-
model
|
|
64
|
-
system: request.instructions,
|
|
65
|
-
prompt: request.prompt,
|
|
66
|
-
...(hasTools ? { tools, stopWhen: stepCountIs(maxSteps) } : {}),
|
|
67
|
-
...(request.outputSchema
|
|
68
|
-
? { output: Output.object({ schema: request.outputSchema }) }
|
|
69
|
-
: {}),
|
|
70
|
-
...(request.maxOutputTokens
|
|
71
|
-
? { maxOutputTokens: request.maxOutputTokens }
|
|
72
|
-
: {}),
|
|
73
|
-
...(request.temperature !== undefined
|
|
74
|
-
? { temperature: request.temperature }
|
|
75
|
-
: {}),
|
|
101
|
+
...buildGenerateTextArgs(this.model, request, this.name, this.modelId),
|
|
76
102
|
...(request.signal ? { abortSignal: request.signal } : {}),
|
|
77
|
-
maxRetries: 0,
|
|
78
103
|
});
|
|
79
104
|
|
|
80
105
|
const output = request.outputSchema
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pontos técnicos estruturais via IA (fallback ao builder determinístico).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
buildTechnicalPromptPayload,
|
|
7
|
+
parseTechnicalSummary,
|
|
8
|
+
type TechnicalPromptInput,
|
|
9
|
+
type TechnicalSummary,
|
|
10
|
+
} from "../../technical-summary";
|
|
11
|
+
import type { AiClient } from "../ai-client";
|
|
12
|
+
import { technicalInstructions, technicalPrompt } from "../prompts/technical";
|
|
13
|
+
import { type TechnicalSummaryPlan, TechnicalSummarySchema } from "../schemas";
|
|
14
|
+
|
|
15
|
+
/** Resume a estrutura da mudança em destaques curtos. */
|
|
16
|
+
export class AiTechnicalSummarizer {
|
|
17
|
+
constructor(private readonly client: AiClient) {}
|
|
18
|
+
|
|
19
|
+
/** `true` quando a feature está ligada e há IA disponível. */
|
|
20
|
+
get enabled(): boolean {
|
|
21
|
+
return this.client.available && this.client.features.technical;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Falha-aberto: sem IA ou saída inválida → `null`. */
|
|
25
|
+
async summarize(input: TechnicalPromptInput): Promise<TechnicalSummary | null> {
|
|
26
|
+
if (!this.enabled) return null;
|
|
27
|
+
const result = await this.client.generateObject<TechnicalSummaryPlan>(
|
|
28
|
+
"technical",
|
|
29
|
+
{
|
|
30
|
+
instructions: technicalInstructions(),
|
|
31
|
+
prompt: technicalPrompt(buildTechnicalPromptPayload(input)),
|
|
32
|
+
outputSchema: TechnicalSummarySchema,
|
|
33
|
+
maxSteps: 1,
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
if (!result.ok) return null;
|
|
37
|
+
return parseTechnicalSummary({
|
|
38
|
+
...result.output,
|
|
39
|
+
source: "ai",
|
|
40
|
+
headSha: input.headSha,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/core/ai/index.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Barrel da camada de IA (`core/ai`).
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
export type { AiProviderKind } from "../config";
|
|
5
|
+
export type { AiProviderKind, AiProviderOptions } from "../config";
|
|
6
6
|
export type {
|
|
7
7
|
AiClientOptions,
|
|
8
8
|
AiCredentialCheck,
|
|
@@ -21,6 +21,7 @@ export { AiBriefEnricher } from "./features/brief-enricher";
|
|
|
21
21
|
export { AiClassifier } from "./features/classifier";
|
|
22
22
|
export { AiContextSummarizer } from "./features/context-summarizer";
|
|
23
23
|
export { AiPreAnalyzer } from "./features/pre-analyzer";
|
|
24
|
+
export { AiTechnicalSummarizer } from "./features/technical-summarizer";
|
|
24
25
|
export { AiVerifier } from "./features/verifier";
|
|
25
26
|
export { hasProviderCredential, ModelFactory } from "./model-factory";
|
|
26
27
|
export {
|
|
@@ -35,6 +36,8 @@ export {
|
|
|
35
36
|
type ContextSummaryPlan,
|
|
36
37
|
ContextSummarySchema,
|
|
37
38
|
SeveritySchema,
|
|
39
|
+
type TechnicalSummaryPlan,
|
|
40
|
+
TechnicalSummarySchema,
|
|
38
41
|
VerifySchema,
|
|
39
42
|
WatchdogReportSchema,
|
|
40
43
|
} from "./schemas";
|
|
@@ -9,9 +9,9 @@ export function contextInstructions(): string {
|
|
|
9
9
|
return buildSystem(
|
|
10
10
|
"Você resume o contexto de uma Pull Request para revisores técnicos.",
|
|
11
11
|
[
|
|
12
|
-
"Objetivo em
|
|
13
|
-
"Motivação em
|
|
14
|
-
"Resumo das alterações em até
|
|
12
|
+
"Objetivo em até 2 frases (máx. 400 caracteres).",
|
|
13
|
+
"Motivação em até 3 frases (máx. 480 caracteres), com o problema e o impacto esperado.",
|
|
14
|
+
"Resumo das alterações em até 6 bullets (máx. 240 caracteres cada), focado em comportamento, fluxos e impacto.",
|
|
15
15
|
"Não invente informação ausente do contexto.",
|
|
16
16
|
],
|
|
17
17
|
);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt dos pontos técnicos estruturais (`AiTechnicalSummarizer`).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { buildSystem, clip } from "./base";
|
|
6
|
+
|
|
7
|
+
/** Instruções de sistema dos pontos técnicos. */
|
|
8
|
+
export function technicalInstructions(): string {
|
|
9
|
+
return buildSystem(
|
|
10
|
+
"Você descreve a estrutura técnica de uma Pull Request para revisores.",
|
|
11
|
+
[
|
|
12
|
+
"Foque em arquitetura e contratos: novos módulos/services, interfaces, providers, repositories, camadas, exports, dependências e impacto.",
|
|
13
|
+
"NÃO avalie qualidade, NÃO liste achados, bugs ou riscos de revisão.",
|
|
14
|
+
"NÃO repita a motivação de negócio: isso pertence ao resumo de negócio.",
|
|
15
|
+
"Use no máximo 6 destaques; `area` é um rótulo curto e `detail` cita paths/símbolos concretos.",
|
|
16
|
+
"Não invente módulos, símbolos ou dependências ausentes dos dados.",
|
|
17
|
+
],
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Prompt do usuário com o payload estrutural já compactado. */
|
|
22
|
+
export function technicalPrompt(payload: unknown): string {
|
|
23
|
+
return [
|
|
24
|
+
"Descreva os pontos técnicos estruturais da mudança abaixo.",
|
|
25
|
+
"",
|
|
26
|
+
"```json",
|
|
27
|
+
clip(JSON.stringify(payload), 14_000),
|
|
28
|
+
"```",
|
|
29
|
+
].join("\n");
|
|
30
|
+
}
|