@payconductor/houndsman 0.6.0 → 0.8.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/server-command.ts +7 -1
- package/src/cli/commands/worktree-command.ts +118 -0
- package/src/cli/help.ts +13 -3
- package/src/cli/index.ts +14 -4
- package/src/core/ast/service.ts +7 -2
- package/src/core/brief.ts +1 -0
- package/src/core/commands/context.ts +9 -1
- package/src/core/commands/findings.ts +17 -6
- package/src/core/commands/review.ts +13 -0
- package/src/core/commands/scaffold.ts +4 -2
- package/src/core/config.ts +13 -0
- package/src/core/defaults.ts +8 -1
- 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 +25 -13
- package/src/core/github-review.ts +10 -7
- package/src/core/jev/jev-detector.ts +10 -2
- package/src/core/orchestrator.ts +62 -13
- package/src/core/project-config.ts +12 -0
- package/src/core/services/gh-client.ts +38 -22
- 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 +269 -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 +12 -0
- package/src/core/storage/work-area.ts +20 -1
- package/src/core/types.ts +2 -0
- package/src/harness/hunt/hunt-options.ts +17 -0
- package/src/harness/hunt/hunt-session.ts +7 -20
- package/src/index.ts +31 -0
- package/src/server/client/App.tsx +86 -22
- package/src/server/client/CommentsPanel.tsx +255 -0
- package/src/server/client/DeleteRunModal.tsx +163 -0
- package/src/server/client/FindingCard.tsx +12 -4
- package/src/server/client/FindingsList.tsx +4 -1
- package/src/server/client/ResumePanel.tsx +8 -8
- package/src/server/client/Sidebar.tsx +452 -0
- package/src/server/client/SubmitModal.tsx +61 -5
- package/src/server/client/icons.tsx +29 -0
- package/src/server/client/index.tsx +1 -1
- package/src/server/client/utils/constants.ts +2 -0
- package/src/server/client/utils/format.ts +47 -2
- package/src/server/client/utils/types.ts +108 -0
- package/src/server/editor.ts +129 -0
- package/src/server/index.ts +205 -3
- package/src/server/pr-comments.ts +256 -0
- package/src/server/pr-inbox.ts +213 -0
- package/src/server/run-repository.ts +136 -3
- 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 +94 -24
- package/src/tests/ai-core.test.ts +2 -2
- package/src/tests/cli.test.ts +35 -1
- package/src/tests/github-review.test.ts +9 -0
- package/src/tests/server-editor.test.ts +100 -0
- package/src/tests/server.test.ts +333 -2
- package/src/tests/worktree.test.ts +369 -0
- package/src/server/client/RunList.tsx +0 -75
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.8.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
|
|
|
@@ -26,9 +26,15 @@ export class ServerCommand implements Command {
|
|
|
26
26
|
const parser = ArgParser.from(args);
|
|
27
27
|
const port = Number(parser.value("--port")) || 7331;
|
|
28
28
|
const host = parser.value("--host") ?? "127.0.0.1";
|
|
29
|
+
const editorPreference = parser.value("--editor");
|
|
29
30
|
|
|
30
31
|
const { startServer } = await import("../../server");
|
|
31
|
-
const { url } = startServer({
|
|
32
|
+
const { url } = startServer({
|
|
33
|
+
config: engine.config,
|
|
34
|
+
port,
|
|
35
|
+
host,
|
|
36
|
+
editorPreference,
|
|
37
|
+
});
|
|
32
38
|
context.out(`[houndsman] servidor em ${url}`);
|
|
33
39
|
context.out("[houndsman] Ctrl+C para encerrar.");
|
|
34
40
|
if (parser.has("--open")) openBrowser(url);
|
|
@@ -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,11 +4,12 @@ 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
|
-
|
|
11
|
+
worktree <ação> Lista/cria/remove worktrees de revisão (list · add · remove · prune · path)
|
|
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)
|
|
14
15
|
doctor Valida a configuração, o ambiente e a credencial Jev
|
|
@@ -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/ast/service.ts
CHANGED
|
@@ -27,10 +27,15 @@ export class AstService {
|
|
|
27
27
|
private readonly git: GitRepository;
|
|
28
28
|
private readonly parser: AstParser;
|
|
29
29
|
|
|
30
|
-
constructor(
|
|
30
|
+
constructor(
|
|
31
|
+
config: HoundsmanConfig,
|
|
32
|
+
git: GitRepository = new GitRepository(),
|
|
33
|
+
root: string = process.cwd(),
|
|
34
|
+
) {
|
|
31
35
|
this.project = new ProjectConfig(config);
|
|
32
36
|
this.git = git;
|
|
33
|
-
|
|
37
|
+
// Camada do projeto (quando presente); sem ela o parser usa o wasm embarcado.
|
|
38
|
+
const layerGrammars = join(root, CORE_DIR, "grammars");
|
|
34
39
|
this.parser = new AstParser({
|
|
35
40
|
maxFileBytes: this.project.astMaxFileBytes,
|
|
36
41
|
languages: this.project.ast.languages,
|
package/src/core/brief.ts
CHANGED
|
@@ -265,6 +265,7 @@ export function defaultRenderBrief(
|
|
|
265
265
|
`# Brief — ${def.name} (\`${def.id}\`)`,
|
|
266
266
|
"",
|
|
267
267
|
`**Branch:** ${args.branch}`,
|
|
268
|
+
...(args.worktree ? [`**Worktree:** \`${args.worktree}\``] : []),
|
|
268
269
|
`**Output obrigatório:** \`${args.outputPath}\``,
|
|
269
270
|
`**Schema do output:** \`${args.outputFormatPath}\``,
|
|
270
271
|
"",
|
|
@@ -26,13 +26,21 @@ export function runContextCommand(
|
|
|
26
26
|
_config: HoundsmanConfig,
|
|
27
27
|
storage: StorageProvider,
|
|
28
28
|
repo: RepoIdentity,
|
|
29
|
-
opts: {
|
|
29
|
+
opts: {
|
|
30
|
+
branch?: string;
|
|
31
|
+
/** Worktree onde a review roda (persistido na review). */
|
|
32
|
+
worktreePath?: string;
|
|
33
|
+
/** PR de origem (persistida na review). */
|
|
34
|
+
prNumber?: number;
|
|
35
|
+
} = {},
|
|
30
36
|
): ContextRunResult {
|
|
31
37
|
const branch = opts.branch ?? currentBranch();
|
|
32
38
|
const review = storage.getOrCreateReview({
|
|
33
39
|
repo,
|
|
34
40
|
branch,
|
|
35
41
|
slug: sanitizeBranch(branch),
|
|
42
|
+
worktreePath: opts.worktreePath,
|
|
43
|
+
prNumber: opts.prNumber,
|
|
36
44
|
});
|
|
37
45
|
const work = new ReviewWorkArea(review);
|
|
38
46
|
const paths = work.contextPaths;
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* com o UUID interno no banco.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import { existsSync } from "node:fs";
|
|
13
14
|
import { AiClient } from "../ai/ai-client";
|
|
14
15
|
import { AiVerifier } from "../ai/features/verifier";
|
|
15
16
|
import { type HoundsmanConfig, resolveJevConcurrency } from "../config";
|
|
@@ -107,11 +108,13 @@ function parseArgs(args: string[]): ParsedArgs {
|
|
|
107
108
|
return parsed;
|
|
108
109
|
}
|
|
109
110
|
|
|
110
|
-
/** Contexto de uma review (storage + registro + staging). */
|
|
111
|
+
/** Contexto de uma review (storage + registro + staging + raiz). */
|
|
111
112
|
type CommandContext = {
|
|
112
113
|
storage: StorageProvider;
|
|
113
114
|
review: ReviewRecord;
|
|
114
115
|
work: ReviewWorkArea;
|
|
116
|
+
/** Worktree da review quando existir; senão, a raiz do repositório. */
|
|
117
|
+
root: string;
|
|
115
118
|
};
|
|
116
119
|
|
|
117
120
|
/** Resolve a review da branch atual (explícita ou via git). */
|
|
@@ -127,7 +130,10 @@ function openContext(
|
|
|
127
130
|
`Nenhuma revisão para '${branch}'. Rode \`houndsman --finalize\` primeiro.`,
|
|
128
131
|
);
|
|
129
132
|
}
|
|
130
|
-
|
|
133
|
+
const worktree = review.worktreePath;
|
|
134
|
+
// Sem worktree, mantém a raiz onde o comando foi invocado (comportamento histórico).
|
|
135
|
+
const root = worktree && existsSync(worktree) ? worktree : process.cwd();
|
|
136
|
+
return { storage, review, work: new ReviewWorkArea(review), root };
|
|
131
137
|
}
|
|
132
138
|
|
|
133
139
|
/** Lê o estado de triagem; lança se a review ainda não tem findings. */
|
|
@@ -380,7 +386,7 @@ function execute(
|
|
|
380
386
|
}
|
|
381
387
|
case "sync": {
|
|
382
388
|
const state = loadState(ctx);
|
|
383
|
-
const result = syncState(state, fileReader(
|
|
389
|
+
const result = syncState(state, fileReader(ctx.root));
|
|
384
390
|
ctx.storage.writeFindings(ctx.review.id, state);
|
|
385
391
|
pingStatus(ctx, state);
|
|
386
392
|
const output = parsed.bools.has("--json")
|
|
@@ -517,7 +523,7 @@ async function executeVerify(
|
|
|
517
523
|
const { JevCandidatesStore, JevClient, JevVerifier } = await import("../jev");
|
|
518
524
|
|
|
519
525
|
const findings = verifier.select(state, filterFrom(parsed));
|
|
520
|
-
const entries = verifier.verifyLocal(findings, fileReader(
|
|
526
|
+
const entries = verifier.verifyLocal(findings, fileReader(ctx.root));
|
|
521
527
|
|
|
522
528
|
// Tier 1 — Jev só nos ambíguos (fail-open: sem chave/erro mantém o provisório).
|
|
523
529
|
let jevChecked = 0;
|
|
@@ -565,7 +571,7 @@ async function executeVerify(
|
|
|
565
571
|
} else if (!aiVerifier.enabled) {
|
|
566
572
|
aiReason = "feature ai.features.verify desligada";
|
|
567
573
|
} else {
|
|
568
|
-
const readFile = fileReader(
|
|
574
|
+
const readFile = fileReader(ctx.root);
|
|
569
575
|
const byId = new Map(findings.map((finding) => [finding.id, finding]));
|
|
570
576
|
const confidence = await aiVerifier.verify({
|
|
571
577
|
findings: stillAmbiguous.map((entry) => ({
|
|
@@ -657,18 +663,23 @@ function splitList(raw: string | undefined): string[] {
|
|
|
657
663
|
.filter(Boolean);
|
|
658
664
|
}
|
|
659
665
|
|
|
660
|
-
/** Seleção do envio a partir das flags (`--severity`, `--
|
|
666
|
+
/** Seleção do envio a partir das flags (`--severity`, `--decision`, `--exec`). */
|
|
661
667
|
function submitSelection(parsed: ParsedArgs): SubmitSelection {
|
|
662
668
|
const severities = splitList(parsed.values["--severity"]).filter(
|
|
663
669
|
(value): value is Severity =>
|
|
664
670
|
(SEVERITY_ORDER as readonly string[]).includes(value),
|
|
665
671
|
);
|
|
672
|
+
const decisions = splitList(parsed.values["--decision"]).filter(
|
|
673
|
+
(value): value is FindingDecision =>
|
|
674
|
+
(FINDING_DECISIONS as readonly string[]).includes(value),
|
|
675
|
+
);
|
|
666
676
|
const execStates = splitList(parsed.values["--exec"]).filter(
|
|
667
677
|
(value): value is FindingExecState =>
|
|
668
678
|
(FINDING_EXEC_STATES as readonly string[]).includes(value),
|
|
669
679
|
);
|
|
670
680
|
return {
|
|
671
681
|
severities: severities.length > 0 ? severities : undefined,
|
|
682
|
+
decisions: decisions.length > 0 ? decisions : undefined,
|
|
672
683
|
execStates: execStates.length > 0 ? execStates : undefined,
|
|
673
684
|
includeSent: parsed.bools.has("--include-sent"),
|
|
674
685
|
};
|
|
@@ -20,9 +20,22 @@ export function parseReviewArgs(args: string[]): ReviewOptions {
|
|
|
20
20
|
const base = parser.value("--base");
|
|
21
21
|
const head = parser.value("--head");
|
|
22
22
|
const branch = parser.value("--branch");
|
|
23
|
+
const pr = parser.value("--pr");
|
|
24
|
+
const worktree = parser.value("--worktree");
|
|
23
25
|
if (base) opts.base = base;
|
|
24
26
|
if (head) opts.head = head;
|
|
25
27
|
if (branch) opts.branch = branch;
|
|
28
|
+
if (worktree) opts.worktree = worktree;
|
|
29
|
+
if (pr) {
|
|
30
|
+
const number = Number.parseInt(pr, 10);
|
|
31
|
+
if (!Number.isFinite(number) || number <= 0) {
|
|
32
|
+
throw new Error(`--pr inválido: '${pr}'.`);
|
|
33
|
+
}
|
|
34
|
+
opts.pr = number;
|
|
35
|
+
}
|
|
36
|
+
if (opts.pr !== undefined && opts.worktree !== undefined) {
|
|
37
|
+
throw new Error("Use --pr ou --worktree, não os dois.");
|
|
38
|
+
}
|
|
26
39
|
|
|
27
40
|
if (parser.has("--finalize") || parser.has("--report-only")) opts.finalize = true;
|
|
28
41
|
if (parser.has("--fail-on-blocking")) opts.failOnBlocking = true;
|
|
@@ -398,10 +398,12 @@ const DOC_MAPPINGS_TEMPLATE = `/**
|
|
|
398
398
|
export const DOC_MAPPINGS: readonly (readonly [RegExp, readonly string[]])[] = [];
|
|
399
399
|
`;
|
|
400
400
|
|
|
401
|
-
const PROJECT_GITIGNORE = `# Diretórios gerados pelo houndsman — \`core/\` é regenerado por \`houndsman update
|
|
402
|
-
# e
|
|
401
|
+
const PROJECT_GITIGNORE = `# Diretórios gerados pelo houndsman — \`core/\` é regenerado por \`houndsman update\`.
|
|
402
|
+
# \`work/\` e \`worktrees/\` são runtime de revisão (staging e worktrees).
|
|
403
403
|
core/
|
|
404
404
|
.runs/
|
|
405
|
+
work/
|
|
406
|
+
worktrees/
|
|
405
407
|
`;
|
|
406
408
|
|
|
407
409
|
// ---------------------------------------------------------------------------
|
package/src/core/config.ts
CHANGED
|
@@ -108,6 +108,17 @@ export type HoundsmanAi = {
|
|
|
108
108
|
watchdogs?: Record<string, HoundsmanAiWatchdog>;
|
|
109
109
|
};
|
|
110
110
|
|
|
111
|
+
/** Onde os worktrees de revisão são criados. */
|
|
112
|
+
export type HoundsmanWorktrees = {
|
|
113
|
+
/**
|
|
114
|
+
* `workspace` (default): `<repo>/.houndsman/worktrees` — dentro da pasta
|
|
115
|
+
* aberta no editor, então agentes com permissão escopada ao workspace
|
|
116
|
+
* conseguem ler/executar sem prompts. `home`: `<HOUNDSMAN_HOME>/worktrees`.
|
|
117
|
+
* Qualquer outro valor é um caminho (absoluto ou relativo à raiz do repo).
|
|
118
|
+
*/
|
|
119
|
+
location?: "home" | "workspace" | string;
|
|
120
|
+
};
|
|
121
|
+
|
|
111
122
|
/** Backend de persistência (reviews, findings e artefatos). */
|
|
112
123
|
export type HoundsmanStorage = {
|
|
113
124
|
/**
|
|
@@ -132,6 +143,8 @@ export type HoundsmanConfig<TId extends string = string> = {
|
|
|
132
143
|
projectName: string;
|
|
133
144
|
/** Persistência das runs e artefatos (default: SQLite global). */
|
|
134
145
|
storage?: HoundsmanStorage;
|
|
146
|
+
/** Localização dos worktrees de revisão (default: dentro do workspace). */
|
|
147
|
+
worktrees?: HoundsmanWorktrees;
|
|
135
148
|
/**
|
|
136
149
|
* Ids dos watchdogs — **derivados** das chaves de `watchdogs` por
|
|
137
150
|
* `defineHoundsmanConfig` (não informar manualmente).
|