@payconductor/houndsman 0.10.0 → 0.12.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 (125) hide show
  1. package/README.md +4 -19
  2. package/package.json +1 -1
  3. package/src/cli/help.ts +0 -7
  4. package/src/cli/index.ts +9 -3
  5. package/src/core/ai/ai-client.ts +3 -7
  6. package/src/core/ai/ai-sdk-model-provider.ts +4 -24
  7. package/src/core/ai/features/brief-enricher.ts +0 -1
  8. package/src/core/ai/features/classifier.ts +0 -1
  9. package/src/core/ai/features/context-summarizer.ts +0 -1
  10. package/src/core/ai/features/pre-analyzer.ts +0 -1
  11. package/src/core/ai/features/technical-summarizer.ts +0 -1
  12. package/src/core/ai/features/verifier.ts +0 -1
  13. package/src/core/ai/index.ts +0 -3
  14. package/src/core/ai/prompts/base.ts +7 -0
  15. package/src/core/ai/prompts/context.ts +2 -1
  16. package/src/core/ai/prompts/pre-analysis.ts +3 -2
  17. package/src/core/ai/prompts/technical.ts +2 -1
  18. package/src/core/ai/schemas.ts +1 -23
  19. package/src/core/ai/types.ts +2 -15
  20. package/src/core/ai/usage-budget.ts +1 -1
  21. package/src/core/ast/diff.ts +37 -2
  22. package/src/core/ast/service.ts +1 -1
  23. package/src/core/brief.ts +54 -5
  24. package/src/core/changed-scope.ts +207 -0
  25. package/src/core/commands/scaffold.ts +1 -18
  26. package/src/core/config-loader.ts +18 -4
  27. package/src/core/config.ts +2 -15
  28. package/src/core/defaults.ts +0 -6
  29. package/src/core/engine/review-scope.ts +55 -8
  30. package/src/core/engine/review-workspace.ts +6 -2
  31. package/src/core/jev/jev-detector.ts +7 -2
  32. package/src/core/logging/index.ts +1 -1
  33. package/src/core/logging/logger.ts +3 -3
  34. package/src/core/orchestrator.ts +19 -14
  35. package/src/core/project-config.ts +0 -28
  36. package/src/core/registry-check.ts +2 -12
  37. package/src/core/rule-text.ts +3 -55
  38. package/src/core/services/git-repository.ts +8 -0
  39. package/src/core/services/worktree-manager.ts +61 -2
  40. package/src/core/storage/types.ts +0 -1
  41. package/src/core/types.ts +3 -14
  42. package/src/index.ts +25 -3
  43. package/src/server/client/utils/constants.ts +3 -6
  44. package/src/server/client/utils/types.ts +0 -1
  45. package/src/skill/OUTPUT-FORMAT.md +5 -0
  46. package/src/skill/README.md +2 -1
  47. package/src/skill/SETUP.md +1 -1
  48. package/src/skill/fix.md +1 -1
  49. package/src/skill/hunt.md +2 -6
  50. package/src/tests/__snapshots__/baseline.test.ts.snap +50 -32
  51. package/src/tests/ai-features.test.ts +11 -0
  52. package/src/tests/changed-scope.test.ts +197 -0
  53. package/src/tests/fallback.test.ts +1 -96
  54. package/src/tests/fixtures/fake-model-provider.ts +0 -1
  55. package/src/tests/review-scope.test.ts +33 -0
  56. package/src/tests/worktree.test.ts +72 -4
  57. package/src/cli/commands/hunt-command.ts +0 -51
  58. package/src/harness/README.md +0 -15
  59. package/src/harness/agent/agent-context.ts +0 -33
  60. package/src/harness/agent/agent-errors.ts +0 -30
  61. package/src/harness/agent/agent-factory.ts +0 -46
  62. package/src/harness/agent/agent-reporter.ts +0 -34
  63. package/src/harness/agent/agent-result.ts +0 -16
  64. package/src/harness/agent/agent-runner.ts +0 -32
  65. package/src/harness/agent/agent-scheduler.ts +0 -37
  66. package/src/harness/agent/review-agent.ts +0 -401
  67. package/src/harness/config/harness-config.ts +0 -89
  68. package/src/harness/hunt/hunt-harness.ts +0 -83
  69. package/src/harness/hunt/hunt-options.ts +0 -92
  70. package/src/harness/hunt/hunt-result.ts +0 -17
  71. package/src/harness/hunt/hunt-session.ts +0 -359
  72. package/src/harness/hunt/review-planner.ts +0 -98
  73. package/src/harness/index.ts +0 -36
  74. package/src/harness/investigation/agent-state.ts +0 -134
  75. package/src/harness/investigation/evidence-verifier.ts +0 -129
  76. package/src/harness/investigation/evidence.ts +0 -110
  77. package/src/harness/investigation/finding-critic.ts +0 -96
  78. package/src/harness/investigation/investigation-planner.ts +0 -118
  79. package/src/harness/investigation/investigation-task.ts +0 -10
  80. package/src/harness/investigation/review-journal.ts +0 -87
  81. package/src/harness/investigation/rule-analyzer.ts +0 -178
  82. package/src/harness/investigation/rule-signals.ts +0 -110
  83. package/src/harness/prompts/agent-schemas.ts +0 -57
  84. package/src/harness/prompts/reviewer-prompt-builder.ts +0 -101
  85. package/src/harness/prompts/states/conclude-prompt.ts +0 -16
  86. package/src/harness/prompts/states/investigate-prompt.ts +0 -37
  87. package/src/harness/prompts/states/orient-prompt.ts +0 -15
  88. package/src/harness/prompts/states/verify-prompt.ts +0 -22
  89. package/src/harness/tools/agent-tool.ts +0 -102
  90. package/src/harness/tools/builtin/changed-symbols-tool.ts +0 -48
  91. package/src/harness/tools/builtin/doc-context-tool.ts +0 -74
  92. package/src/harness/tools/builtin/file-outline-tool.ts +0 -66
  93. package/src/harness/tools/builtin/find-references-tool.ts +0 -66
  94. package/src/harness/tools/builtin/find-symbol-tool.ts +0 -91
  95. package/src/harness/tools/builtin/helpers.ts +0 -115
  96. package/src/harness/tools/builtin/impact-tool.ts +0 -48
  97. package/src/harness/tools/builtin/imports-exports-tool.ts +0 -53
  98. package/src/harness/tools/builtin/index.ts +0 -59
  99. package/src/harness/tools/builtin/list-files-tool.ts +0 -85
  100. package/src/harness/tools/builtin/pr-context-tool.ts +0 -41
  101. package/src/harness/tools/builtin/read-file-range-tool.ts +0 -73
  102. package/src/harness/tools/builtin/rule-signals-tool.ts +0 -36
  103. package/src/harness/tools/builtin/search-code-tool.ts +0 -65
  104. package/src/harness/tools/builtin/symbol-context-tool.ts +0 -106
  105. package/src/harness/tools/builtin/tests-for-symbol-tool.ts +0 -61
  106. package/src/harness/tools/path-guard.ts +0 -90
  107. package/src/harness/tools/result-limiter.ts +0 -49
  108. package/src/harness/tools/secret-redactor.ts +0 -54
  109. package/src/harness/tools/tool-cache.ts +0 -50
  110. package/src/harness/tools/tool-call-budget.ts +0 -60
  111. package/src/harness/tools/tool-context.ts +0 -5
  112. package/src/harness/tools/tool-middleware.ts +0 -105
  113. package/src/harness/tools/tool-policy.ts +0 -51
  114. package/src/harness/tools/tool-registry.ts +0 -112
  115. package/src/harness/tools/tool-result.ts +0 -15
  116. package/src/harness/tools/toolbox.ts +0 -36
  117. package/src/harness/validation/finding-validator.ts +0 -119
  118. package/src/tests/fixtures/fake-agent-tools.ts +0 -69
  119. package/src/tests/fixtures/fake-investigation.ts +0 -64
  120. package/src/tests/harness-agent.test.ts +0 -141
  121. package/src/tests/harness-investigation.test.ts +0 -206
  122. package/src/tests/harness-tools.test.ts +0 -148
  123. package/src/tests/hunt-command.test.ts +0 -212
  124. package/src/tests/rule-analyzer.test.ts +0 -61
  125. package/src/tests/tool-contracts.test.ts +0 -82
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Escopo do diff — âncoras, símbolos alterados e fallback nos hunks.
3
+ *
4
+ * Congela a regra que impede o watchdog de reportar código pré-existente: só as
5
+ * faixas tocadas pela revisão (símbolo folha do AST ou hunk) são revisáveis.
6
+ */
7
+
8
+ import { describe, expect, test } from "bun:test";
9
+ import { changedAnchors } from "../core/ast/diff";
10
+ import type { AstAnalysis, AstFacts, AstSymbol } from "../core/ast/types";
11
+ import {
12
+ buildChangedScope,
13
+ formatRanges,
14
+ formatSymbols,
15
+ isLineInScope,
16
+ mergeRanges,
17
+ summarizeScope,
18
+ } from "../core/changed-scope";
19
+ import type { DiffSource } from "../core/diff-source";
20
+
21
+ /** Cabecalho padrão de um patch de arquivo. */
22
+ function patch(body: string): string {
23
+ return `diff --git a/src/a.ts b/src/a.ts\n--- a/src/a.ts\n+++ b/src/a.ts\n${body}`;
24
+ }
25
+
26
+ function diffOf(patchText: string): DiffSource {
27
+ return {
28
+ byFile: new Map([["src/a.ts", patchText]]),
29
+ churn: new Map(),
30
+ };
31
+ }
32
+
33
+ function symbol(
34
+ overrides: Partial<AstSymbol> & Pick<AstSymbol, "name" | "kind">,
35
+ ): AstSymbol {
36
+ return {
37
+ exported: false,
38
+ startLine: 1,
39
+ endLine: 1,
40
+ ...overrides,
41
+ };
42
+ }
43
+
44
+ function analysis(facts: AstFacts): AstAnalysis {
45
+ return { facts };
46
+ }
47
+
48
+ const FACTS: AstFacts = {
49
+ language: "typescript",
50
+ imports: [],
51
+ exports: [],
52
+ decorators: [],
53
+ hasSyntaxErrors: false,
54
+ symbols: [
55
+ symbol({ name: "Outer", kind: "class", startLine: 1, endLine: 20 }),
56
+ symbol({ name: "Outer.inner", kind: "method", startLine: 5, endLine: 10 }),
57
+ symbol({ name: "top", kind: "function", startLine: 25, endLine: 30 }),
58
+ ],
59
+ };
60
+
61
+ describe("changedAnchors", () => {
62
+ test("adições e remoções apontam para o lado novo", () => {
63
+ const anchors = changedAnchors(
64
+ patch(
65
+ "@@ -10,3 +10,4 @@\n const before = 1;\n-const old = 2;\n+const current = 2;\n+const extra = 3;\n const after = 4;\n",
66
+ ),
67
+ );
68
+
69
+ expect([...anchors.added].sort((a, b) => a - b)).toEqual([11, 12]);
70
+ expect([...anchors.deletedAt]).toEqual([11]);
71
+ });
72
+
73
+ test("remoção pura ancora no ponto do arquivo (sem consumir linha nova)", () => {
74
+ const anchors = changedAnchors(patch("@@ -10,2 +10,0 @@\n-a\n-b\n"));
75
+
76
+ expect([...anchors.added]).toEqual([]);
77
+ expect([...anchors.deletedAt]).toEqual([10]);
78
+ });
79
+ });
80
+
81
+ describe("buildChangedScope", () => {
82
+ const entries = [{ path: "src/a.ts", status: "modified" }];
83
+
84
+ test("âncora dentro de símbolo folha vira o intervalo do símbolo", () => {
85
+ const text = patch(
86
+ [
87
+ "@@ -2,0 +3,1 @@",
88
+ "+export const header = 1;",
89
+ "@@ -7,0 +8,1 @@",
90
+ "+ const inside = 1;",
91
+ "@@ -26,0 +27,1 @@",
92
+ "+ return 1;",
93
+ "",
94
+ ].join("\n"),
95
+ );
96
+ const scope = buildChangedScope({
97
+ entries,
98
+ diff: diffOf(text),
99
+ ast: new Map([["src/a.ts", analysis(FACTS)]]),
100
+ });
101
+
102
+ const file = scope["src/a.ts"];
103
+ expect(file?.source).toBe("ast");
104
+ // `inner` (5-10) e `top` (25-30); o container `Outer` não expande.
105
+ expect(file?.symbols.map((item) => item.name)).toEqual(["Outer.inner", "top"]);
106
+ // Cabeçalho da classe (linha 3) fica como faixa avulsa.
107
+ expect(file?.ranges).toEqual([
108
+ { start: 3, end: 3 },
109
+ { start: 5, end: 10 },
110
+ { start: 25, end: 30 },
111
+ ]);
112
+ });
113
+
114
+ test("sem AST cai para os hunks do diff", () => {
115
+ const text = patch("@@ -10,0 +11,2 @@\n+a\n+b\n");
116
+ const scope = buildChangedScope({ entries, diff: diffOf(text) });
117
+
118
+ const file = scope["src/a.ts"];
119
+ expect(file?.source).toBe("hunks");
120
+ expect(file?.symbols).toEqual([]);
121
+ expect(file?.ranges).toEqual([{ start: 11, end: 12 }]);
122
+ });
123
+
124
+ test("rename/binário sem hunks fica sem faixas", () => {
125
+ const scope = buildChangedScope({
126
+ entries: [{ path: "src/a.ts", status: "renamed" }],
127
+ diff: diffOf(patch("")),
128
+ });
129
+
130
+ expect(scope["src/a.ts"]).toEqual({ ranges: [], symbols: [], source: "hunks" });
131
+ });
132
+
133
+ test("arquivo deletado nem entra no escopo", () => {
134
+ const scope = buildChangedScope({
135
+ entries: [{ path: "src/a.ts", status: "deleted" }],
136
+ diff: diffOf(patch("@@ -1,1 +0,0 @@\n-x\n")),
137
+ });
138
+
139
+ expect(scope["src/a.ts"]).toBeUndefined();
140
+ });
141
+ });
142
+
143
+ describe("helpers de escopo", () => {
144
+ test("isLineInScope respeita as faixas", () => {
145
+ const scope = {
146
+ "src/a.ts": {
147
+ ranges: [{ start: 5, end: 10 }],
148
+ symbols: [],
149
+ source: "hunks" as const,
150
+ },
151
+ };
152
+
153
+ expect(isLineInScope(scope, "src/a.ts", 7)).toBe(true);
154
+ expect(isLineInScope(scope, "src/a.ts", 11)).toBe(false);
155
+ expect(isLineInScope(scope, "src/b.ts", 7)).toBe(false);
156
+ });
157
+
158
+ test("mergeRanges une sobrepostos e adjacentes", () => {
159
+ expect(
160
+ mergeRanges([
161
+ { start: 10, end: 12 },
162
+ { start: 11, end: 15 },
163
+ { start: 16, end: 18 },
164
+ { start: 30, end: 30 },
165
+ ]),
166
+ ).toEqual([
167
+ { start: 10, end: 18 },
168
+ { start: 30, end: 30 },
169
+ ]);
170
+ });
171
+
172
+ test("formatadores compactos", () => {
173
+ expect(formatRanges([])).toBe("sem linhas alteradas");
174
+ expect(
175
+ formatRanges([
176
+ { start: 10, end: 14 },
177
+ { start: 22, end: 22 },
178
+ ]),
179
+ ).toBe("10-14, 22");
180
+ expect(
181
+ formatSymbols([
182
+ { name: "foo", kind: "function", startLine: 10, endLine: 14 },
183
+ ]),
184
+ ).toBe("`foo` (10-14)");
185
+ expect(
186
+ summarizeScope({
187
+ "src/a.ts": {
188
+ ranges: [{ start: 1, end: 2 }],
189
+ symbols: [
190
+ { name: "foo", kind: "function", startLine: 1, endLine: 2 },
191
+ ],
192
+ source: "ast",
193
+ },
194
+ }),
195
+ ).toEqual({ files: 1, ranges: 1, symbols: 1 });
196
+ });
197
+ });
@@ -1,16 +1,10 @@
1
- /**
2
- * Fallback de classificação e findings emergentes (lógica/domínio).
3
- */
1
+ /** Fallback de classificação (arquivos sem dono). */
4
2
 
5
3
  import { describe, expect, test } from "bun:test";
6
4
  import { defineHoundsmanConfig } from "../core/config";
7
5
  import { classifyEntries } from "../core/file-classifier";
8
6
  import { checkRegistry } from "../core/registry-check";
9
7
  import { defineWatchdog } from "../core/types";
10
- import { AgentState } from "../harness/investigation/agent-state";
11
- import { EvidenceVerifier } from "../harness/investigation/evidence-verifier";
12
- import { FindingCritic } from "../harness/investigation/finding-critic";
13
- import { FindingValidator } from "../harness/validation/finding-validator";
14
8
 
15
9
  const alpha = defineWatchdog({
16
10
  id: "alpha",
@@ -28,7 +22,6 @@ const fallback = defineWatchdog({
28
22
  description: "Fallback de lógica/domínio.",
29
23
  scope: "arquivos sem dono",
30
24
  fallback: true,
31
- allowEmergentFindings: true,
32
25
  rules: [
33
26
  {
34
27
  severity: "attention",
@@ -82,91 +75,3 @@ describe("classificação com fallback", () => {
82
75
  ).toBe(true);
83
76
  });
84
77
  });
85
-
86
- describe("findings emergentes", () => {
87
- const file = "a.ts";
88
- const content = "const x = 1;\n";
89
- const state = new AgentState();
90
- const evidence = state.addEvidence({
91
- file,
92
- startLine: 1,
93
- endLine: 1,
94
- snippet: content.trim(),
95
- kind: "definition",
96
- sourceTool: "test",
97
- confidence: 0.9,
98
- });
99
- const draft = {
100
- rule: "Lógica: a função fazA na verdade fazB",
101
- severity: "blocking" as const,
102
- file,
103
- line: 1,
104
- message: "comportamento diverge do nome",
105
- action: "renomear ou corrigir",
106
- evidenceIds: [evidence.id],
107
- confidence: 0.9,
108
- };
109
- const verifier = new EvidenceVerifier({
110
- allowedFiles: [file],
111
- readFile: (path) => (path === file ? content : null),
112
- });
113
-
114
- test("crítico aceita e limita a severidade quando habilitado", () => {
115
- const critic = new FindingCritic({
116
- knownRules: new Set([fallback.rules[0]?.text ?? ""]),
117
- verifier,
118
- allowEmergentFindings: true,
119
- });
120
- const decision = critic.reviewOne(draft, state);
121
- expect(decision.verdict).toBe("keep");
122
- expect(decision.draft.severity).toBe("attention");
123
- });
124
-
125
- test("crítico descarta quando o opt-in está desligado", () => {
126
- const critic = new FindingCritic({
127
- knownRules: new Set([fallback.rules[0]?.text ?? ""]),
128
- verifier,
129
- });
130
- const decision = critic.reviewOne(draft, state);
131
- expect(decision.verdict).toBe("drop");
132
- expect(decision.reason).toContain("desabilitado");
133
- });
134
-
135
- test("validador espelha a mesma regra", () => {
136
- const report = {
137
- watchdogId: "default",
138
- reviewedFiles: [file],
139
- skipped: [],
140
- findings: [draft],
141
- };
142
- const allowed = new FindingValidator({
143
- watchdogId: "default",
144
- knownRules: new Set([fallback.rules[0]?.text ?? ""]),
145
- allowedFiles: new Set([file]),
146
- overflow: new Set(),
147
- readFile: (path) => (path === file ? content : null),
148
- allowEmergentFindings: true,
149
- }).validate(report);
150
- expect(allowed.report.findings[0]?.severity).toBe("attention");
151
- expect(allowed.dropped).toHaveLength(0);
152
-
153
- const denied = new FindingValidator({
154
- watchdogId: "default",
155
- knownRules: new Set([fallback.rules[0]?.text ?? ""]),
156
- allowedFiles: new Set([file]),
157
- overflow: new Set(),
158
- readFile: (path) => (path === file ? content : null),
159
- }).validate(report);
160
- expect(denied.report.findings).toHaveLength(0);
161
- });
162
-
163
- test("finding emergente sem descrição é descartado", () => {
164
- const critic = new FindingCritic({
165
- knownRules: new Set([fallback.rules[0]?.text ?? ""]),
166
- verifier,
167
- allowEmergentFindings: true,
168
- });
169
- const decision = critic.reviewOne({ ...draft, rule: "Lógica:" }, state);
170
- expect(decision.verdict).toBe("drop");
171
- });
172
- });
@@ -41,7 +41,6 @@ export class FakeModelProvider implements ModelProvider {
41
41
  return {
42
42
  output: output as T,
43
43
  usage: fakeUsage(),
44
- steps: 1,
45
44
  finishReason: "stop",
46
45
  };
47
46
  }
@@ -9,6 +9,7 @@ import { describe, expect, test } from "bun:test";
9
9
  import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
10
10
  import { tmpdir } from "node:os";
11
11
  import { join } from "node:path";
12
+ import { AstService } from "../core/ast/service";
12
13
  import { ReviewScopeResolver, type ScopeOptions } from "../core/engine/review-scope";
13
14
  import type { FindingState, FindingsStateFile } from "../core/findings-state";
14
15
  import { defaultOptions, finalize, prepare, reviewContext } from "../core/orchestrator";
@@ -138,6 +139,35 @@ describe("ReviewScopeResolver", () => {
138
139
  if (scope.kind !== "review") throw new Error("esperava review");
139
140
  expect(scope.classification.mode).toBe("full");
140
141
  expect(scope.classification.headSha).toBe(repo.head);
142
+ expect(scope.classification.changedScope?.["src/alpha/a.ts"]).toEqual({
143
+ ranges: [{ start: 1, end: 1 }],
144
+ symbols: [],
145
+ source: "hunks",
146
+ });
147
+ } finally {
148
+ process.chdir(originalCwd);
149
+ repo.cleanup();
150
+ }
151
+ });
152
+
153
+ test("escopo usa o símbolo AST quando disponível", async () => {
154
+ const repo = makeRepo();
155
+ const originalCwd = process.cwd();
156
+ try {
157
+ process.chdir(repo.cwd);
158
+ const ast = new AstService(fakeHoundsmanConfig);
159
+ const scope = await new ReviewScopeResolver(
160
+ options({ base: repo.base, full: true }),
161
+ fakeHoundsmanConfig,
162
+ ast,
163
+ ).resolve(null, repo.head);
164
+
165
+ expect(scope.kind).toBe("review");
166
+ if (scope.kind !== "review") throw new Error("esperava review");
167
+ const file = scope.classification.changedScope?.["src/alpha/a.ts"];
168
+ expect(file?.source).toBe("ast");
169
+ expect(file?.symbols.map((symbol) => symbol.name)).toEqual(["a"]);
170
+ expect(file?.ranges).toEqual([{ start: 1, end: 1 }]);
141
171
  } finally {
142
172
  process.chdir(originalCwd);
143
173
  repo.cleanup();
@@ -163,6 +193,9 @@ describe("ReviewScopeResolver", () => {
163
193
  expect(scope.classification.mode).toBe("incremental");
164
194
  expect(scope.classification.reviewedFrom).toBe(repo.base);
165
195
  expect(Object.keys(scope.classification.files)).toEqual(["src/alpha/a.ts"]);
196
+ expect(scope.classification.changedScope?.["src/alpha/a.ts"]?.ranges).toEqual(
197
+ [{ start: 1, end: 1 }],
198
+ );
166
199
  expect(scope.classification.carryover?.map((f) => f.file)).toEqual([
167
200
  "src/alpha/keep.ts",
168
201
  ]);
@@ -24,9 +24,13 @@ import { createGitRunner } from "../core/git-runner";
24
24
  import { defaultOptions } from "../core/orchestrator";
25
25
  import type { GhRunner } from "../core/services/gh-client";
26
26
  import { GitRepository } from "../core/services/git-repository";
27
- import { parseWorktreeList, WorktreeManager } from "../core/services/worktree-manager";
27
+ import {
28
+ parseWorktreeList,
29
+ syncProjectLayer,
30
+ syncProjectLayerForRoot,
31
+ WorktreeManager,
32
+ } from "../core/services/worktree-manager";
28
33
  import { SQLiteStorageProvider } from "../core/storage/sqlite/sqlite-storage";
29
- import { parseHuntArgs } from "../harness/hunt/hunt-options";
30
34
  import { fakeHoundsmanConfig } from "./fixtures/fake-project";
31
35
 
32
36
  function git(cwd: string, args: string[]): string {
@@ -122,11 +126,10 @@ function fakeGh(
122
126
  }
123
127
 
124
128
  describe("flags de worktree", () => {
125
- test("parse de review/hunt e exclusão mútua", () => {
129
+ test("parse de review e exclusão mútua", () => {
126
130
  expect(parseReviewArgs(["--pr", "7"]).pr).toBe(7);
127
131
  expect(parseReviewArgs(["--worktree", "/tmp/wt"]).worktree).toBe("/tmp/wt");
128
132
  expect(() => parseReviewArgs(["--pr", "7", "--worktree", "/tmp/wt"])).toThrow();
129
- expect(parseHuntArgs(["--pr", "8"]).pr).toBe(8);
130
133
  });
131
134
  });
132
135
 
@@ -186,6 +189,71 @@ describe("nomes de diretório do worktree", () => {
186
189
  });
187
190
  });
188
191
 
192
+ describe("camada gerada no worktree", () => {
193
+ test("espelha `.houndsman/core` do clone principal e ignora origem ausente", () => {
194
+ const sandbox = makeSandbox();
195
+ try {
196
+ const worktree = join(sandbox.root, "wt-layer");
197
+ mkdirSync(worktree, { recursive: true });
198
+ mkdirSync(join(sandbox.repo, ".houndsman", "core"), { recursive: true });
199
+ writeFileSync(
200
+ join(sandbox.repo, ".houndsman", "core", "types.ts"),
201
+ "export {};\n",
202
+ );
203
+
204
+ expect(syncProjectLayer(sandbox.repo, worktree)).toBe(true);
205
+ expect(existsSync(join(worktree, ".houndsman", "core", "types.ts"))).toBe(
206
+ true,
207
+ );
208
+
209
+ // Sem camada na origem: no-op (fail-open).
210
+ const empty = join(sandbox.root, "empty");
211
+ mkdirSync(empty, { recursive: true });
212
+ expect(syncProjectLayer(empty, worktree)).toBe(false);
213
+ } finally {
214
+ sandbox.cleanup();
215
+ }
216
+ });
217
+
218
+ test("ensure copia a camada e o auto-reparo repõe num worktree existente", () => {
219
+ const sandbox = makeSandbox();
220
+ try {
221
+ mkdirSync(join(sandbox.repo, ".houndsman", "core"), { recursive: true });
222
+ writeFileSync(
223
+ join(sandbox.repo, ".houndsman", "core", "types.ts"),
224
+ "export {};\n",
225
+ );
226
+ const manager = new WorktreeManager(
227
+ new GitRepository(createGitRunner(sandbox.repo)),
228
+ {
229
+ repoKey: "path:fake",
230
+ repoRoot: sandbox.repo,
231
+ baseDir: sandbox.home,
232
+ },
233
+ );
234
+
235
+ const ensured = manager.ensure({
236
+ branch: "feature",
237
+ ref: sandbox.featureSha,
238
+ });
239
+ const layer = join(ensured.path, ".houndsman", "core", "types.ts");
240
+ expect(existsSync(layer)).toBe(true);
241
+
242
+ // Comando rodando no worktree (sem `--pr`) também se auto-repara.
243
+ rmSync(join(ensured.path, ".houndsman", "core"), {
244
+ recursive: true,
245
+ force: true,
246
+ });
247
+ expect(syncProjectLayerForRoot(ensured.path)).toBe(true);
248
+ expect(existsSync(layer)).toBe(true);
249
+
250
+ manager.remove("feature");
251
+ } finally {
252
+ sandbox.cleanup();
253
+ }
254
+ }, 20_000);
255
+ });
256
+
189
257
  describe("WorktreeManager", () => {
190
258
  test("cria detached, reusa o mesmo commit e remove", () => {
191
259
  const sandbox = makeSandbox();
@@ -1,51 +0,0 @@
1
- /** Comando `hunt` — revisão nativa com IA (harness próprio). */
2
-
3
- import { resolveReviewWorkspace } from "../../core/engine/review-workspace";
4
- import { HuntHarness } from "../../harness";
5
- import { parseHuntArgs } from "../../harness/hunt/hunt-options";
6
- import type { CliContext, Command } from "./command";
7
-
8
- /** Executa o harness nativo. */
9
- export class HuntCommand implements Command {
10
- readonly name = "hunt";
11
-
12
- async run(args: string[], context: CliContext): Promise<number> {
13
- const options = parseHuntArgs(args);
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
- };
40
- try {
41
- const harness = await HuntHarness.from(engine, resolved, context.logger);
42
- const result = await harness.run();
43
- for (const message of result.messages) context.logger.info(message);
44
- if (result.status === "failed") return 1;
45
- if (options.failOnBlocking && result.blockers > 0) return 1;
46
- return 0;
47
- } finally {
48
- engine.close();
49
- }
50
- }
51
- }
@@ -1,15 +0,0 @@
1
- # `src/harness` — harness nativo do houndsman
2
-
3
- Este diretório é o **runtime de agentes do próprio houndsman** (`houndsman hunt`):
4
- tools read-only, investigação, máquina de estados e o agente revisor.
5
-
6
- > O **harness externo** (skill `/houndsman` para ClaudeCode/Copilot/agents) continua em
7
- > `src/skill/`. Os dois coexistem; o nativo é opt-in.
8
-
9
- Regras de dependência:
10
-
11
- - `harness/` pode importar `core/` (inclusive `core/ai`).
12
- - `core/` **nunca** importa `harness/`.
13
- - `cli/` importa ambos.
14
-
15
- Documentação detalhada: [`docs/HARNESS.md`](../../docs/HARNESS.md).
@@ -1,33 +0,0 @@
1
- /**
2
- * Contexto de um agente de revisão (um watchdog).
3
- */
4
-
5
- import type { AstAnalysis, AstImpact } from "../../core/ast/types";
6
- import type { DiffSource } from "../../core/diff-source";
7
- import type { Watchdog } from "../../core/domain/watchdog";
8
- import type { WatchdogRenderContext } from "../../core/types";
9
- import type { RuleAnalysis } from "../investigation/rule-analyzer";
10
-
11
- /** Dados imutáveis entregues ao agente. */
12
- export type AgentContext = {
13
- watchdog: Watchdog;
14
- projectName: string;
15
- branch: string;
16
- /** Arquivos do domínio (dentro do orçamento). */
17
- files: readonly string[];
18
- /** Arquivos do domínio fora do orçamento (não ler; registrar em skipped). */
19
- overflow: readonly string[];
20
- deleted: readonly string[];
21
- docs: readonly string[];
22
- context?: WatchdogRenderContext | null;
23
- ast: ReadonlyMap<string, AstAnalysis>;
24
- impact?: AstImpact;
25
- /** Caminho de saída do findings (staging). */
26
- outputPath: string;
27
- maxSteps: number;
28
- maxCalls: number;
29
- repoRoot: string;
30
- diff: DiffSource;
31
- /** Análises determinísticas das regras do watchdog. */
32
- rules: readonly RuleAnalysis[];
33
- };
@@ -1,30 +0,0 @@
1
- /**
2
- * Erros do harness nativo.
3
- */
4
-
5
- /** Falha de configuração/execução do agente. */
6
- export class AgentError extends Error {
7
- constructor(
8
- message: string,
9
- readonly watchdogId?: string,
10
- ) {
11
- super(message);
12
- this.name = "AgentError";
13
- }
14
- }
15
-
16
- /** Orçamento de IA/tools esgotado durante a investigação. */
17
- export class AgentBudgetError extends AgentError {
18
- constructor(message: string, watchdogId?: string) {
19
- super(message, watchdogId);
20
- this.name = "AgentBudgetError";
21
- }
22
- }
23
-
24
- /** Saída do modelo inválida após as tentativas permitidas. */
25
- export class AgentOutputError extends AgentError {
26
- constructor(message: string, watchdogId?: string) {
27
- super(message, watchdogId);
28
- this.name = "AgentOutputError";
29
- }
30
- }
@@ -1,46 +0,0 @@
1
- /**
2
- * Factory de agentes — injeta provider, tools e limites resolvidos.
3
- */
4
-
5
- import type { ModelProvider } from "../../core/ai";
6
- import type { AstService } from "../../core/ast/service";
7
- import type { HarnessConfig } from "../config/harness-config";
8
- import type { ToolPolicy } from "../tools/tool-policy";
9
- import type { ToolRegistry } from "../tools/tool-registry";
10
- import type { AgentContext } from "./agent-context";
11
- import type { AgentReporter } from "./agent-reporter";
12
- import { ReviewAgent } from "./review-agent";
13
-
14
- /** Dependências compartilhadas por todos os agentes. */
15
- export type AgentFactoryDeps = {
16
- provider: ModelProvider;
17
- registry: ToolRegistry;
18
- policy?: ToolPolicy;
19
- reporter: AgentReporter;
20
- config: HarnessConfig;
21
- ast: AstService;
22
- readFile?: (path: string) => string | null;
23
- };
24
-
25
- /** Cria `ReviewAgent`s a partir do contexto de cada watchdog. */
26
- export class AgentFactory {
27
- constructor(private readonly deps: AgentFactoryDeps) {}
28
-
29
- /** Instancia um agente para o contexto. */
30
- create(context: AgentContext): ReviewAgent {
31
- const { config } = this.deps;
32
- return new ReviewAgent({
33
- context,
34
- provider: this.deps.provider,
35
- registry: this.deps.registry,
36
- policy: this.deps.policy,
37
- reporter: this.deps.reporter,
38
- maxOutputTokens: config.maxOutputTokensFor(context.watchdog.id),
39
- temperature: config.temperature,
40
- providerOptions: config.providerOptions,
41
- timeoutMs: config.timeoutMs,
42
- ast: this.deps.ast,
43
- readFile: this.deps.readFile,
44
- });
45
- }
46
- }
@@ -1,34 +0,0 @@
1
- /**
2
- * Reporter de progresso de um agente.
3
- *
4
- * O journal é sempre coletado (`harness-transcript.json`); o console é opcional.
5
- */
6
-
7
- import type { LoggerLike } from "../../core/logging";
8
- import { ReviewJournal } from "../investigation/review-journal";
9
-
10
- /** Interface de progresso. */
11
- export interface AgentReporter {
12
- readonly journal: ReviewJournal;
13
- progress(message: string): void;
14
- }
15
-
16
- /** Reporter silencioso (testes). */
17
- export class SilentReporter implements AgentReporter {
18
- readonly journal = new ReviewJournal();
19
-
20
- progress(): void {
21
- // intencionalmente silencioso
22
- }
23
- }
24
-
25
- /** Reporter apoiado no logger único do CLI. */
26
- export class LoggerReporter implements AgentReporter {
27
- readonly journal = new ReviewJournal();
28
-
29
- constructor(private readonly logger: LoggerLike) {}
30
-
31
- progress(message: string): void {
32
- this.logger.progress(message);
33
- }
34
- }