@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.
Files changed (71) hide show
  1. package/README.md +20 -0
  2. package/package.json +1 -1
  3. package/src/cli/commands/command.ts +10 -1
  4. package/src/cli/commands/context-command.ts +40 -2
  5. package/src/cli/commands/hunt-command.ts +28 -2
  6. package/src/cli/commands/review-command.ts +30 -2
  7. package/src/cli/commands/server-command.ts +7 -1
  8. package/src/cli/commands/worktree-command.ts +118 -0
  9. package/src/cli/help.ts +13 -3
  10. package/src/cli/index.ts +14 -4
  11. package/src/core/ast/service.ts +7 -2
  12. package/src/core/brief.ts +1 -0
  13. package/src/core/commands/context.ts +9 -1
  14. package/src/core/commands/findings.ts +17 -6
  15. package/src/core/commands/review.ts +13 -0
  16. package/src/core/commands/scaffold.ts +4 -2
  17. package/src/core/config.ts +13 -0
  18. package/src/core/defaults.ts +8 -1
  19. package/src/core/engine/houndsman-engine.ts +26 -3
  20. package/src/core/engine/review-scope.ts +17 -22
  21. package/src/core/engine/review-session.ts +25 -2
  22. package/src/core/engine/review-workspace.ts +204 -0
  23. package/src/core/git-runner.ts +25 -13
  24. package/src/core/github-review.ts +10 -7
  25. package/src/core/jev/jev-detector.ts +10 -2
  26. package/src/core/orchestrator.ts +62 -13
  27. package/src/core/project-config.ts +12 -0
  28. package/src/core/services/gh-client.ts +38 -22
  29. package/src/core/services/git-repository.ts +46 -0
  30. package/src/core/services/pr-resolver.ts +147 -0
  31. package/src/core/services/worktree-manager.ts +269 -0
  32. package/src/core/storage/file/file-storage.ts +14 -1
  33. package/src/core/storage/paths.ts +30 -1
  34. package/src/core/storage/sqlite/mappers.ts +4 -0
  35. package/src/core/storage/sqlite/schema.ts +13 -3
  36. package/src/core/storage/sqlite/sqlite-storage.ts +28 -8
  37. package/src/core/storage/types.ts +12 -0
  38. package/src/core/storage/work-area.ts +20 -1
  39. package/src/core/types.ts +2 -0
  40. package/src/harness/hunt/hunt-options.ts +17 -0
  41. package/src/harness/hunt/hunt-session.ts +7 -20
  42. package/src/index.ts +31 -0
  43. package/src/server/client/App.tsx +86 -22
  44. package/src/server/client/CommentsPanel.tsx +255 -0
  45. package/src/server/client/DeleteRunModal.tsx +163 -0
  46. package/src/server/client/FindingCard.tsx +12 -4
  47. package/src/server/client/FindingsList.tsx +4 -1
  48. package/src/server/client/ResumePanel.tsx +8 -8
  49. package/src/server/client/Sidebar.tsx +452 -0
  50. package/src/server/client/SubmitModal.tsx +61 -5
  51. package/src/server/client/icons.tsx +29 -0
  52. package/src/server/client/index.tsx +1 -1
  53. package/src/server/client/utils/constants.ts +2 -0
  54. package/src/server/client/utils/format.ts +47 -2
  55. package/src/server/client/utils/types.ts +108 -0
  56. package/src/server/editor.ts +129 -0
  57. package/src/server/index.ts +205 -3
  58. package/src/server/pr-comments.ts +256 -0
  59. package/src/server/pr-inbox.ts +213 -0
  60. package/src/server/run-repository.ts +136 -3
  61. package/src/skill/README.md +11 -3
  62. package/src/skill/SKILL.md +44 -10
  63. package/src/skill/hunt.md +33 -7
  64. package/src/tests/__snapshots__/baseline.test.ts.snap +94 -24
  65. package/src/tests/ai-core.test.ts +2 -2
  66. package/src/tests/cli.test.ts +35 -1
  67. package/src/tests/github-review.test.ts +9 -0
  68. package/src/tests/server-editor.test.ts +100 -0
  69. package/src/tests/server.test.ts +333 -2
  70. package/src/tests/worktree.test.ts +369 -0
  71. 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.6.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
- { branch: parser.value("--branch") },
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 engine = await context.engine();
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, options, context.logger);
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 result = await engine.session(options).run();
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({ config: engine.config, port, host });
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
- server Sobe a UI local de triagem (--port, --host, --open)
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
- engine: async () => {
94
- const { config } = await loadConfig({
95
- configPath: parser.value("--config"),
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
 
@@ -27,10 +27,15 @@ export class AstService {
27
27
  private readonly git: GitRepository;
28
28
  private readonly parser: AstParser;
29
29
 
30
- constructor(config: HoundsmanConfig, git: GitRepository = new GitRepository()) {
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
- const layerGrammars = join(process.cwd(), CORE_DIR, "grammars");
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: { branch?: string } = {},
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
- return { storage, review, work: new ReviewWorkArea(review) };
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(process.cwd()));
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(process.cwd()));
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(process.cwd());
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`, `--exec`, `--include-sent`). */
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 \`.runs/\` guarda os artefatos de revisão.
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
  // ---------------------------------------------------------------------------
@@ -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).