@payconductor/houndsman 0.5.0 → 0.7.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 (175) hide show
  1. package/README.md +24 -4
  2. package/assets/grammars/tree-sitter-javascript.wasm +0 -0
  3. package/assets/grammars/tree-sitter-tsx.wasm +0 -0
  4. package/assets/grammars/tree-sitter-typescript.wasm +0 -0
  5. package/assets/grammars/web-tree-sitter.wasm +0 -0
  6. package/package.json +11 -3
  7. package/src/cli/commands/command.ts +5 -0
  8. package/src/cli/commands/doctor-command.ts +116 -24
  9. package/src/cli/commands/hunt-command.ts +25 -0
  10. package/src/cli/commands/server-command.ts +7 -1
  11. package/src/cli/help.ts +18 -2
  12. package/src/cli/index.ts +10 -6
  13. package/src/core/ai/ai-client.ts +221 -0
  14. package/src/core/ai/ai-sdk-model-provider.ts +101 -0
  15. package/src/core/ai/errors.ts +36 -0
  16. package/src/core/ai/features/brief-enricher.ts +62 -0
  17. package/src/core/ai/features/classifier.ts +50 -0
  18. package/src/core/ai/features/context-summarizer.ts +31 -0
  19. package/src/core/ai/features/pre-analyzer.ts +51 -0
  20. package/src/core/ai/features/verifier.ts +45 -0
  21. package/src/core/ai/index.ts +56 -0
  22. package/src/core/ai/model-factory.ts +137 -0
  23. package/src/core/ai/prompts/base.ts +36 -0
  24. package/src/core/ai/prompts/brief.ts +87 -0
  25. package/src/core/ai/prompts/classification.ts +36 -0
  26. package/src/core/ai/prompts/context.ts +29 -0
  27. package/src/core/ai/prompts/pre-analysis.ts +49 -0
  28. package/src/core/ai/prompts/verify.ts +40 -0
  29. package/src/core/ai/schemas.ts +110 -0
  30. package/src/core/ai/types.ts +76 -0
  31. package/src/core/ai/usage-budget.ts +75 -0
  32. package/src/core/ai/usage-recorder.ts +79 -0
  33. package/src/core/assets.ts +16 -0
  34. package/src/core/ast/diff.ts +220 -0
  35. package/src/core/ast/impact.ts +127 -0
  36. package/src/core/ast/parser.ts +167 -0
  37. package/src/core/ast/service.ts +233 -0
  38. package/src/core/ast/symbols.ts +294 -0
  39. package/src/core/ast/types.ts +125 -0
  40. package/src/core/brief.ts +138 -0
  41. package/src/core/commands/findings.ts +65 -2
  42. package/src/core/commands/review.ts +2 -0
  43. package/src/core/commands/scaffold.ts +159 -10
  44. package/src/core/commands/update.ts +5 -3
  45. package/src/core/config.ts +85 -0
  46. package/src/core/defaults.ts +27 -0
  47. package/src/core/domain/watchdog.ts +43 -5
  48. package/src/core/engine/houndsman-engine.ts +16 -1
  49. package/src/core/engine/review-scope.ts +52 -6
  50. package/src/core/engine/review-session.ts +27 -2
  51. package/src/core/env.ts +101 -0
  52. package/src/core/file-classifier.ts +25 -4
  53. package/src/core/git-runner.ts +25 -13
  54. package/src/core/github-review.ts +10 -7
  55. package/src/core/jev/jev-client.ts +8 -7
  56. package/src/core/jev/jev-verify.ts +2 -1
  57. package/src/core/logging/index.ts +17 -0
  58. package/src/core/logging/logger.ts +187 -0
  59. package/src/core/orchestrator.ts +357 -9
  60. package/src/core/project-config.ts +123 -1
  61. package/src/core/registry-check.ts +148 -6
  62. package/src/core/renderers/manifest-writer.ts +33 -0
  63. package/src/core/rule-text.ts +97 -0
  64. package/src/core/services/file-classifier.ts +10 -3
  65. package/src/core/services/gh-client.ts +38 -22
  66. package/src/core/services/verifier.ts +14 -3
  67. package/src/core/storage/paths.ts +6 -5
  68. package/src/core/storage/types.ts +6 -0
  69. package/src/core/storage/work-area.ts +12 -0
  70. package/src/core/types.ts +37 -1
  71. package/src/core/verify.ts +17 -5
  72. package/src/harness/README.md +15 -0
  73. package/src/harness/agent/agent-context.ts +33 -0
  74. package/src/harness/agent/agent-errors.ts +30 -0
  75. package/src/harness/agent/agent-factory.ts +45 -0
  76. package/src/harness/agent/agent-reporter.ts +34 -0
  77. package/src/harness/agent/agent-result.ts +16 -0
  78. package/src/harness/agent/agent-runner.ts +32 -0
  79. package/src/harness/agent/agent-scheduler.ts +37 -0
  80. package/src/harness/agent/review-agent.ts +391 -0
  81. package/src/harness/config/harness-config.ts +84 -0
  82. package/src/harness/hunt/hunt-harness.ts +83 -0
  83. package/src/harness/hunt/hunt-options.ts +75 -0
  84. package/src/harness/hunt/hunt-result.ts +17 -0
  85. package/src/harness/hunt/hunt-session.ts +372 -0
  86. package/src/harness/hunt/review-planner.ts +98 -0
  87. package/src/harness/index.ts +36 -0
  88. package/src/harness/investigation/agent-state.ts +134 -0
  89. package/src/harness/investigation/evidence-verifier.ts +129 -0
  90. package/src/harness/investigation/evidence.ts +110 -0
  91. package/src/harness/investigation/finding-critic.ts +96 -0
  92. package/src/harness/investigation/investigation-planner.ts +118 -0
  93. package/src/harness/investigation/investigation-task.ts +10 -0
  94. package/src/harness/investigation/review-journal.ts +87 -0
  95. package/src/harness/investigation/rule-analyzer.ts +178 -0
  96. package/src/harness/investigation/rule-signals.ts +110 -0
  97. package/src/harness/prompts/agent-schemas.ts +57 -0
  98. package/src/harness/prompts/reviewer-prompt-builder.ts +101 -0
  99. package/src/harness/prompts/states/conclude-prompt.ts +16 -0
  100. package/src/harness/prompts/states/investigate-prompt.ts +37 -0
  101. package/src/harness/prompts/states/orient-prompt.ts +15 -0
  102. package/src/harness/prompts/states/verify-prompt.ts +22 -0
  103. package/src/harness/tools/agent-tool.ts +102 -0
  104. package/src/harness/tools/builtin/changed-symbols-tool.ts +48 -0
  105. package/src/harness/tools/builtin/doc-context-tool.ts +74 -0
  106. package/src/harness/tools/builtin/file-outline-tool.ts +66 -0
  107. package/src/harness/tools/builtin/find-references-tool.ts +66 -0
  108. package/src/harness/tools/builtin/find-symbol-tool.ts +91 -0
  109. package/src/harness/tools/builtin/helpers.ts +115 -0
  110. package/src/harness/tools/builtin/impact-tool.ts +48 -0
  111. package/src/harness/tools/builtin/imports-exports-tool.ts +53 -0
  112. package/src/harness/tools/builtin/index.ts +59 -0
  113. package/src/harness/tools/builtin/list-files-tool.ts +85 -0
  114. package/src/harness/tools/builtin/pr-context-tool.ts +41 -0
  115. package/src/harness/tools/builtin/read-file-range-tool.ts +73 -0
  116. package/src/harness/tools/builtin/rule-signals-tool.ts +36 -0
  117. package/src/harness/tools/builtin/search-code-tool.ts +65 -0
  118. package/src/harness/tools/builtin/symbol-context-tool.ts +106 -0
  119. package/src/harness/tools/builtin/tests-for-symbol-tool.ts +61 -0
  120. package/src/harness/tools/path-guard.ts +90 -0
  121. package/src/harness/tools/result-limiter.ts +49 -0
  122. package/src/harness/tools/secret-redactor.ts +54 -0
  123. package/src/harness/tools/tool-cache.ts +50 -0
  124. package/src/harness/tools/tool-call-budget.ts +60 -0
  125. package/src/harness/tools/tool-context.ts +5 -0
  126. package/src/harness/tools/tool-middleware.ts +105 -0
  127. package/src/harness/tools/tool-policy.ts +51 -0
  128. package/src/harness/tools/tool-registry.ts +112 -0
  129. package/src/harness/tools/tool-result.ts +15 -0
  130. package/src/harness/tools/toolbox.ts +36 -0
  131. package/src/harness/validation/finding-validator.ts +119 -0
  132. package/src/index.ts +46 -0
  133. package/src/server/client/App.tsx +38 -12
  134. package/src/server/client/CommentsPanel.tsx +255 -0
  135. package/src/server/client/FindingCard.tsx +12 -4
  136. package/src/server/client/FindingsList.tsx +4 -1
  137. package/src/server/client/Sidebar.tsx +397 -0
  138. package/src/server/client/SubmitModal.tsx +61 -5
  139. package/src/server/client/icons.tsx +19 -0
  140. package/src/server/client/index.tsx +1 -1
  141. package/src/server/client/utils/constants.ts +29 -5
  142. package/src/server/client/utils/format.ts +47 -2
  143. package/src/server/client/utils/types.ts +88 -0
  144. package/src/server/editor.ts +129 -0
  145. package/src/server/index.ts +115 -5
  146. package/src/server/pr-comments.ts +256 -0
  147. package/src/server/pr-inbox.ts +202 -0
  148. package/src/server/run-repository.ts +33 -1
  149. package/src/skill/OUTPUT-FORMAT.md +12 -0
  150. package/src/skill/SETUP.md +6 -2
  151. package/src/skill/hunt.md +14 -2
  152. package/src/skill/verify.md +1 -1
  153. package/src/tests/__snapshots__/baseline.test.ts.snap +96 -14
  154. package/src/tests/ai-core.test.ts +120 -0
  155. package/src/tests/ai-features.test.ts +183 -0
  156. package/src/tests/ast.test.ts +335 -0
  157. package/src/tests/cli.test.ts +33 -0
  158. package/src/tests/env.test.ts +69 -0
  159. package/src/tests/fallback.test.ts +172 -0
  160. package/src/tests/fixtures/fake-agent-tools.ts +69 -0
  161. package/src/tests/fixtures/fake-investigation.ts +64 -0
  162. package/src/tests/fixtures/fake-model-provider.ts +48 -0
  163. package/src/tests/github-review.test.ts +9 -0
  164. package/src/tests/harness-agent.test.ts +122 -0
  165. package/src/tests/harness-investigation.test.ts +206 -0
  166. package/src/tests/harness-tools.test.ts +148 -0
  167. package/src/tests/hunt-command.test.ts +212 -0
  168. package/src/tests/logger.test.ts +83 -0
  169. package/src/tests/review-scope.test.ts +11 -11
  170. package/src/tests/rule-analyzer.test.ts +61 -0
  171. package/src/tests/server-editor.test.ts +100 -0
  172. package/src/tests/server.test.ts +295 -2
  173. package/src/tests/tool-contracts.test.ts +82 -0
  174. package/src/tests/verify.test.ts +1 -1
  175. package/src/server/client/RunList.tsx +0 -75
@@ -0,0 +1,256 @@
1
+ /**
2
+ * Comentários de uma Pull Request (servidor) — threads de revisão + comentários
3
+ * gerais, lidos via `gh api`.
4
+ *
5
+ * A aba é lazy: só chama o GitHub quando o cliente pede. O resultado fica em
6
+ * cache por branch (TTL de 60s); `refresh` ignora o cache. Tudo fail-open: sem
7
+ * PR/gh/login, devolve `available:false` e a UI mostra o motivo.
8
+ */
9
+
10
+ import { resolve } from "node:path";
11
+ import { resolvePrTarget } from "../core/github-review";
12
+ import { systemClock } from "../core/services/clock";
13
+ import { describeGhError, type GhRunner } from "../core/services/gh-client";
14
+
15
+ /** Cache do payload já processado (por raiz do repositório + branch). */
16
+ const CACHE_TTL_MS = 60_000;
17
+ const cache = new Map<string, { expiresAt: number; value: PrCommentsResponse }>();
18
+
19
+ /** Limpa o cache (testes e shutdown). */
20
+ export function clearPrCommentsCache(): void {
21
+ cache.clear();
22
+ }
23
+
24
+ /** Comentário já normalizado para a UI. */
25
+ export type PrCommentView = {
26
+ id: number;
27
+ author: string;
28
+ authorType: string;
29
+ body: string;
30
+ createdAt?: string;
31
+ url?: string;
32
+ path?: string;
33
+ line: number | null;
34
+ absFile?: string;
35
+ replyTo?: number | null;
36
+ };
37
+
38
+ /** Thread de revisão (comentário raiz + respostas). */
39
+ export type PrCommentThread = {
40
+ id: number;
41
+ path: string;
42
+ line: number | null;
43
+ absFile: string;
44
+ comments: PrCommentView[];
45
+ };
46
+
47
+ /** Resposta de `GET /api/runs/:slug/comments`. */
48
+ export type PrCommentsResponse = {
49
+ available: boolean;
50
+ reason?: string;
51
+ target?: { number: number; repo: string; url?: string };
52
+ fetchedAt: string;
53
+ threads: PrCommentThread[];
54
+ issueComments: PrCommentView[];
55
+ counts: { threads: number; inline: number; issue: number };
56
+ };
57
+
58
+ type GhInlineComment = {
59
+ id?: number;
60
+ body?: string;
61
+ path?: string;
62
+ line?: number | null;
63
+ original_line?: number | null;
64
+ created_at?: string;
65
+ html_url?: string;
66
+ in_reply_to_id?: number | null;
67
+ user?: { login?: string; type?: string };
68
+ };
69
+
70
+ type GhIssueComment = {
71
+ id?: number;
72
+ body?: string;
73
+ created_at?: string;
74
+ html_url?: string;
75
+ user?: { login?: string; type?: string };
76
+ };
77
+
78
+ type ApiList = { ok: true; items: unknown[] } | { ok: false; reason: string };
79
+
80
+ function unavailable(
81
+ reason: string,
82
+ target?: PrCommentsResponse["target"],
83
+ ): PrCommentsResponse {
84
+ return {
85
+ available: false,
86
+ reason,
87
+ target,
88
+ fetchedAt: systemClock(),
89
+ threads: [],
90
+ issueComments: [],
91
+ counts: { threads: 0, inline: 0, issue: 0 },
92
+ };
93
+ }
94
+
95
+ /** Lista paginada via `gh api`; `--slurp` junta as páginas num único array. */
96
+ function ghApiList(runner: GhRunner, path: string): ApiList {
97
+ let result = runner(["api", "--paginate", "--slurp", path]);
98
+ if (!result.ok && /unknown flag|unknown shorthand|slurp/i.test(result.stderr)) {
99
+ result = runner(["api", path]);
100
+ }
101
+ if (!result.ok) return { ok: false, reason: describeGhError(result) };
102
+ try {
103
+ const parsed = JSON.parse(result.stdout);
104
+ if (Array.isArray(parsed)) {
105
+ // Dependendo da versão do gh, `--slurp` pode devolver `[[...], [...]]`
106
+ // (um array por página) ou um array já achatado.
107
+ const items: unknown[] = [];
108
+ for (const item of parsed) {
109
+ if (Array.isArray(item)) items.push(...item);
110
+ else items.push(item);
111
+ }
112
+ return { ok: true, items };
113
+ }
114
+ return { ok: false, reason: "Resposta inesperada do gh (esperava uma lista)." };
115
+ } catch {
116
+ return { ok: false, reason: "Resposta inválida do gh (JSON malformado)." };
117
+ }
118
+ }
119
+
120
+ function toView(
121
+ raw: GhInlineComment | GhIssueComment,
122
+ repoRoot: string,
123
+ ): PrCommentView | null {
124
+ const id = Number(raw.id);
125
+ const body = raw.body?.trim() ?? "";
126
+ if (!Number.isFinite(id) || body.length === 0) return null;
127
+ const path = "path" in raw ? raw.path : undefined;
128
+ const line = "line" in raw ? (raw.line ?? raw.original_line ?? null) : null;
129
+ return {
130
+ id,
131
+ author: raw.user?.login ?? "desconhecido",
132
+ authorType: raw.user?.type ?? "User",
133
+ body,
134
+ createdAt: raw.created_at,
135
+ url: raw.html_url,
136
+ path,
137
+ line,
138
+ absFile: path ? resolve(repoRoot, path) : undefined,
139
+ replyTo: "in_reply_to_id" in raw ? (raw.in_reply_to_id ?? null) : null,
140
+ };
141
+ }
142
+
143
+ /** Agrupa comentários inline em threads (raiz + respostas). */
144
+ function buildThreads(
145
+ items: unknown[],
146
+ repoRoot: string,
147
+ ): {
148
+ threads: PrCommentThread[];
149
+ inline: number;
150
+ } {
151
+ const raws = (items as GhInlineComment[])
152
+ .filter((raw) => raw && typeof raw === "object")
153
+ .sort(
154
+ (a, b) =>
155
+ (a.created_at ?? "").localeCompare(b.created_at ?? "") ||
156
+ Number(a.id ?? 0) - Number(b.id ?? 0),
157
+ );
158
+
159
+ const views: PrCommentView[] = [];
160
+ const parent = new Map<number, number | null>();
161
+ for (const raw of raws) {
162
+ const view = toView(raw, repoRoot);
163
+ if (!view) continue;
164
+ views.push(view);
165
+ parent.set(view.id, raw.in_reply_to_id ?? null);
166
+ }
167
+
168
+ const rootOf = (id: number): number => {
169
+ let current = id;
170
+ const seen = new Set<number>();
171
+ for (;;) {
172
+ const next = parent.get(current);
173
+ if (!next || seen.has(current)) return current;
174
+ seen.add(current);
175
+ current = next;
176
+ }
177
+ };
178
+
179
+ const byRoot = new Map<number, PrCommentThread>();
180
+ for (const view of views) {
181
+ const rootId = rootOf(view.id);
182
+ const existing = byRoot.get(rootId);
183
+ if (existing) {
184
+ existing.comments.push(view);
185
+ continue;
186
+ }
187
+ const path = view.path ?? "";
188
+ byRoot.set(rootId, {
189
+ id: rootId,
190
+ path,
191
+ line: view.line,
192
+ absFile: view.absFile ?? (path ? resolve(repoRoot, path) : ""),
193
+ comments: [view],
194
+ });
195
+ }
196
+
197
+ const threads = [...byRoot.values()].sort(
198
+ (a, b) =>
199
+ a.path.localeCompare(b.path) ||
200
+ (a.line ?? Number.MAX_SAFE_INTEGER) - (b.line ?? Number.MAX_SAFE_INTEGER) ||
201
+ a.id - b.id,
202
+ );
203
+ return { threads, inline: views.length };
204
+ }
205
+
206
+ /** Lê e agrupa os comentários da PR da branch (com cache de 60s). */
207
+ export function fetchPrComments(options: {
208
+ runner: GhRunner;
209
+ branch: string;
210
+ repoRoot: string;
211
+ refresh?: boolean;
212
+ now?: number;
213
+ }): PrCommentsResponse {
214
+ const { runner, branch, repoRoot, refresh = false } = options;
215
+ const now = options.now ?? Date.now();
216
+ const key = `${resolve(repoRoot)}\0${branch}`;
217
+ const cached = cache.get(key);
218
+ if (!refresh && cached && cached.expiresAt > now) return cached.value;
219
+
220
+ const target = resolvePrTarget(runner, branch);
221
+ if (!target.ok) return unavailable(target.reason);
222
+
223
+ const pr = {
224
+ number: target.target.number,
225
+ repo: target.target.repo,
226
+ url: target.target.url,
227
+ };
228
+ const inline = ghApiList(runner, `repos/${pr.repo}/pulls/${pr.number}/comments`);
229
+ if (!inline.ok) return unavailable(inline.reason, pr);
230
+
231
+ const issue = ghApiList(runner, `repos/${pr.repo}/issues/${pr.number}/comments`);
232
+ if (!issue.ok) return unavailable(issue.reason, pr);
233
+
234
+ const { threads, inline: inlineCount } = buildThreads(inline.items, repoRoot);
235
+ const issueComments = (issue.items as GhIssueComment[])
236
+ .map((raw) => toView(raw, repoRoot))
237
+ .filter((view): view is PrCommentView => view !== null)
238
+ .sort(
239
+ (a, b) => (a.createdAt ?? "").localeCompare(b.createdAt ?? "") || a.id - b.id,
240
+ );
241
+
242
+ const value: PrCommentsResponse = {
243
+ available: true,
244
+ target: pr,
245
+ fetchedAt: systemClock(),
246
+ threads,
247
+ issueComments,
248
+ counts: {
249
+ threads: threads.length,
250
+ inline: inlineCount,
251
+ issue: issueComments.length,
252
+ },
253
+ };
254
+ cache.set(key, { expiresAt: now + CACHE_TTL_MS, value });
255
+ return value;
256
+ }
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Caixa de entrada de Pull Requests do repositório atual (servidor).
3
+ *
4
+ * Lista as PRs abertas via `gh` e executa o checkout somente quando a árvore
5
+ * de trabalho está limpa. Fail-open: sem `gh` (ou sem git), devolve o motivo e
6
+ * a UI degrada sem quebrar as demais abas.
7
+ */
8
+
9
+ import { describeGhError, type GhRunner } from "../core/services/gh-client";
10
+ import type { GitRepository } from "../core/services/git-repository";
11
+
12
+ /** Estado da árvore de trabalho (base do bloqueio do checkout). */
13
+ export type WorktreeState = {
14
+ /** `false` quando o `git status` falhou. */
15
+ available: boolean;
16
+ clean: boolean;
17
+ changes: number;
18
+ /** Amostra dos caminhos alterados (limitada para a UI). */
19
+ sample: string[];
20
+ };
21
+
22
+ /** PR aberta exibida na sidebar. */
23
+ export type PrListItem = {
24
+ number: number;
25
+ title: string;
26
+ headRefName: string;
27
+ baseRefName: string;
28
+ author: string;
29
+ updatedAt?: string;
30
+ isDraft: boolean;
31
+ reviewDecision?: string;
32
+ url?: string;
33
+ /** Run local correspondente à branch, quando existir. */
34
+ runSlug?: string;
35
+ };
36
+
37
+ /** Resposta de `GET /api/prs`. */
38
+ export type PrListResponse = {
39
+ available: boolean;
40
+ reason?: string;
41
+ currentBranch: string | null;
42
+ worktree: WorktreeState;
43
+ prs: PrListItem[];
44
+ };
45
+
46
+ /** Resultado do checkout de uma PR. */
47
+ export type CheckoutResponse =
48
+ | { ok: true; number: number; branch: string | null }
49
+ | {
50
+ ok: false;
51
+ reason: "dirty" | "git" | "gh";
52
+ detail: string;
53
+ changes?: string[];
54
+ };
55
+
56
+ const PR_LIST_FIELDS =
57
+ "number,title,headRefName,baseRefName,author,updatedAt,isDraft,reviewDecision,url";
58
+
59
+ type GhPrListItem = {
60
+ number?: number;
61
+ title?: string;
62
+ headRefName?: string;
63
+ baseRefName?: string;
64
+ author?: { login?: string; name?: string };
65
+ updatedAt?: string;
66
+ isDraft?: boolean;
67
+ reviewDecision?: string;
68
+ url?: string;
69
+ };
70
+
71
+ function toItem(
72
+ raw: GhPrListItem,
73
+ runSlugs: ReadonlyMap<string, string>,
74
+ ): PrListItem | null {
75
+ const number = Number(raw.number);
76
+ const headRefName = raw.headRefName?.trim();
77
+ if (!Number.isFinite(number) || number <= 0 || !headRefName) return null;
78
+ return {
79
+ number,
80
+ title: raw.title?.trim() || `PR #${number}`,
81
+ headRefName,
82
+ baseRefName: raw.baseRefName?.trim() || "",
83
+ author: raw.author?.login ?? raw.author?.name ?? "desconhecido",
84
+ updatedAt: raw.updatedAt,
85
+ isDraft: Boolean(raw.isDraft),
86
+ reviewDecision: raw.reviewDecision?.trim() || undefined,
87
+ url: raw.url,
88
+ runSlug: runSlugs.get(headRefName),
89
+ };
90
+ }
91
+
92
+ /** Acesso às PRs do repositório atual. */
93
+ export class PrInbox {
94
+ constructor(
95
+ private readonly ghRunner: GhRunner,
96
+ private readonly git: GitRepository,
97
+ ) {}
98
+
99
+ /** Estado da árvore de trabalho (`git status --porcelain`). */
100
+ worktree(): WorktreeState {
101
+ const result = this.git.tryRun(["status", "--porcelain"]);
102
+ if (!result.ok) {
103
+ return { available: false, clean: false, changes: 0, sample: [] };
104
+ }
105
+ const lines = result.stdout
106
+ .split("\n")
107
+ .map((line) => line.trimEnd())
108
+ .filter((line) => line.length > 0);
109
+ return {
110
+ available: true,
111
+ clean: lines.length === 0,
112
+ changes: lines.length,
113
+ sample: lines.slice(0, 20),
114
+ };
115
+ }
116
+
117
+ /** Lista as PRs abertas (limite 50) e o estado local do repositório. */
118
+ list(runSlugs: ReadonlyMap<string, string> = new Map()): PrListResponse {
119
+ const currentBranch = this.git.currentBranch();
120
+ const worktree = this.worktree();
121
+ const result = this.ghRunner([
122
+ "pr",
123
+ "list",
124
+ "--state",
125
+ "open",
126
+ "--limit",
127
+ "50",
128
+ "--json",
129
+ PR_LIST_FIELDS,
130
+ ]);
131
+
132
+ if (!result.ok) {
133
+ return {
134
+ available: false,
135
+ reason: describeGhError(result),
136
+ currentBranch,
137
+ worktree,
138
+ prs: [],
139
+ };
140
+ }
141
+
142
+ let parsed: unknown;
143
+ try {
144
+ parsed = JSON.parse(result.stdout);
145
+ } catch {
146
+ return {
147
+ available: false,
148
+ reason: "Resposta inválida do gh (JSON malformado).",
149
+ currentBranch,
150
+ worktree,
151
+ prs: [],
152
+ };
153
+ }
154
+ if (!Array.isArray(parsed)) {
155
+ return {
156
+ available: false,
157
+ reason: "Resposta inesperada do gh (esperava uma lista de PRs).",
158
+ currentBranch,
159
+ worktree,
160
+ prs: [],
161
+ };
162
+ }
163
+
164
+ const prs = (parsed as GhPrListItem[])
165
+ .map((item) => toItem(item, runSlugs))
166
+ .filter((item): item is PrListItem => item !== null)
167
+ .sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""));
168
+
169
+ return { available: true, currentBranch, worktree, prs };
170
+ }
171
+
172
+ /**
173
+ * Faz checkout da PR via `gh pr checkout`.
174
+ *
175
+ * A árvore precisa estar limpa: alterações locais não commitadas bloqueiam a
176
+ * operação para não descartar trabalho do usuário.
177
+ */
178
+ checkout(number: number): CheckoutResponse {
179
+ const worktree = this.worktree();
180
+ if (!worktree.available) {
181
+ return {
182
+ ok: false,
183
+ reason: "git",
184
+ detail: "Não foi possível ler o estado do git.",
185
+ };
186
+ }
187
+ if (!worktree.clean) {
188
+ return {
189
+ ok: false,
190
+ reason: "dirty",
191
+ detail: `${worktree.changes} alteração(ões) não commitada(s).`,
192
+ changes: worktree.sample,
193
+ };
194
+ }
195
+
196
+ const result = this.ghRunner(["pr", "checkout", String(number)]);
197
+ if (!result.ok) {
198
+ return { ok: false, reason: "gh", detail: describeGhError(result) };
199
+ }
200
+ return { ok: true, number, branch: this.git.currentBranch() };
201
+ }
202
+ }
@@ -34,9 +34,11 @@ import {
34
34
  } from "../core/github-review";
35
35
  import { pingFindingsStatus } from "../core/run-status";
36
36
  import { systemClock } from "../core/services/clock";
37
+ import { defaultGhRunner } from "../core/services/gh-client";
37
38
  import type { RepoIdentity, ReviewRecord, StorageProvider } from "../core/storage/types";
38
39
  import type { Severity } from "../core/types";
39
40
  import { isSlug } from "./artifacts";
41
+ import { fetchPrComments, type PrCommentsResponse } from "./pr-comments";
40
42
 
41
43
  /** Item da lista de execuções (sidebar) — já ordenado do mais recente ao antigo. */
42
44
  export type RunListItem = {
@@ -78,10 +80,16 @@ export type ArtifactListItem = {
78
80
  export class RunRepository {
79
81
  private readonly storage: StorageProvider;
80
82
  private readonly repo: RepoIdentity;
83
+ private readonly ghRunner: GhRunner;
81
84
 
82
- constructor(storage: StorageProvider, repo: RepoIdentity) {
85
+ constructor(
86
+ storage: StorageProvider,
87
+ repo: RepoIdentity,
88
+ ghRunner: GhRunner = defaultGhRunner,
89
+ ) {
83
90
  this.storage = storage;
84
91
  this.repo = repo;
92
+ this.ghRunner = ghRunner;
85
93
  }
86
94
 
87
95
  /** `true` se o slug aponta para uma run do repositório atual. */
@@ -105,6 +113,15 @@ export class RunRepository {
105
113
  });
106
114
  }
107
115
 
116
+ /** Mapa branch → slug das runs (usado para casar PRs com execuções). */
117
+ branchIndex(): Map<string, string> {
118
+ return new Map(
119
+ this.storage
120
+ .listReviews(this.repo.key)
121
+ .map((review) => [review.branch, review.slug]),
122
+ );
123
+ }
124
+
108
125
  /** Detalhe da run (null se o slug for inválido). */
109
126
  detail(slug: string): RunDetail | null {
110
127
  const review = this.findBySlug(slug);
@@ -273,6 +290,21 @@ export class RunRepository {
273
290
  return { available: true, target: target.target, outcome };
274
291
  }
275
292
 
293
+ /** Comentários da PR da run (null se o slug for inválido). */
294
+ comments(
295
+ slug: string,
296
+ options: { refresh?: boolean } = {},
297
+ ): PrCommentsResponse | null {
298
+ const review = this.findBySlug(slug);
299
+ if (!review) return null;
300
+ return fetchPrComments({
301
+ runner: this.ghRunner,
302
+ branch: review.branch,
303
+ repoRoot: review.repoRoot,
304
+ refresh: options.refresh,
305
+ });
306
+ }
307
+
276
308
  /** Lista os artefatos da run (sem conteúdo). */
277
309
  artifacts(slug: string): ArtifactListItem[] | null {
278
310
  const review = this.findBySlug(slug);
@@ -70,3 +70,15 @@ staging global da review:
70
70
  - Um achado por regra violada; não agrupar regras diferentes.
71
71
  - `file:line` sempre que possível; `evidence` quando o achado não é óbvio.
72
72
  - Não inventar achados — se não houver violação, o array fica vazio.
73
+
74
+ ## Findings emergentes (opt-in)
75
+
76
+ Quando o watchdog declara `allowEmergentFindings: true`, você pode reportar falhas de
77
+ **lógica** ou **domínio** que não correspondem a uma regra explícita usando os prefixos
78
+ reservados:
79
+
80
+ - `Lógica: <descrição>` — nome/contrato vs comportamento, fluxos, estados, bordas.
81
+ - `Domínio: <descrição>` — transições, idempotência, invariantes, arredondamento, timezone.
82
+
83
+ Esses findings continuam exigindo `evidence` e têm severidade máxima `attention`. Sem o
84
+ opt-in, todo finding precisa citar uma regra existente.
@@ -7,8 +7,11 @@ no seu projeto é **autocontida** — não exige nenhuma dependência local do p
7
7
 
8
8
  - **Bun** — o CLI e o motor usam a runtime Bun (execução de TS, `Bun.spawnSync`).
9
9
  - **git** — o diff usa `git diff <base>...<head> --name-status`.
10
- - Opcional: **`gh`** (contexto da PR) e **`TYPESAFE_API_KEY`** (pré-análise Jev). O pipeline
11
- segue normalmente sem eles.
10
+ - **gh** (Opcional) — Obtém contexto da PR e dispara comentários para o GitHub
11
+ - **`HOUNDSMAN_TYPE_SAFE_API_KEY`** (opcional) — pré-análise Jev.
12
+ - **`HOUNDSMAN_DEEP_SEEK_API_KEY`** (opcional) — IA própria do `hunt` nativo e das features de IA.
13
+ - **`HOUNDSMAN_OPENAI_API_KEY`** / **`HOUNDSMAN_ANTHROPIC_API_KEY`** (opcional) — apenas se
14
+ `ai.provider` for `openai`/`anthropic`.
12
15
 
13
16
  ## Instalação (global)
14
17
 
@@ -87,6 +90,7 @@ Definido em `src/core/config.ts` (pacote) e espelhado em `.houndsman/core/types.
87
90
  | `alwaysIncludeDocs` | Docs sempre presentes nos briefs (default `["AGENTS.md"]`). |
88
91
  | `budgets` | **Opcional** — teto de arquivos por brief (`maxFilesPerBrief`, default `40`); o excedente vira "fora do orçamento" (registrar em `skipped`). |
89
92
  | `concurrency` | **Opcional** — chamadas Jev simultâneas (`jev`, default `5`). |
93
+ | `ast` | **Opcional** — AST determinístico para TS/JS (`enabled`, `maxFileBytes`, `maxImpactFiles`, `languages`, `aliases`). Default: ligado; falhas são fail-open. |
90
94
 
91
95
  > O motor **não contém** ids, paths ou regras do projeto — tudo chega por `HoundsmanConfig`,
92
96
  > carregado em runtime a partir de `houndsman.config.ts` (ou `--config <path>`).
package/src/skill/hunt.md CHANGED
@@ -10,6 +10,11 @@ as regras do projeto ficam em `houndsman.config.ts` + `.houndsman/`.
10
10
  **Quando usar:** revisar PR antes de merge · code review de feature branch · validar
11
11
  mudanças antes de push.
12
12
 
13
+ > **Harness nativo (opcional):** além deste skill, o CLI tem `houndsman hunt` — o mesmo
14
+ > fluxo executado pelo próprio houndsman, com tools read-only, investigação por evidência
15
+ > e `VERIFY` obrigatório. Use `houndsman hunt --dry-run` para inspecionar sem IA. Detalhes
16
+ > em `docs/HARNESS.md`.
17
+
13
18
  ## Quem executa o quê
14
19
 
15
20
  - **Você** só invoca o skill: `/houndsman [hunt]`. Não precisa rodar nenhum script à mão.
@@ -76,6 +81,8 @@ staging dos subagentes em `<HOUNDSMAN_HOME>/work/<reviewId>/`:
76
81
  | Artefato | Conteúdo |
77
82
  |---|---|
78
83
  | `classification.json` | Watchdogs ativados por arquivo + status (A/M/D/R) + churn (storage) |
84
+ | `ast-summary.json` | Fatos estruturais + delta por arquivo (AST TS/JS) (storage) |
85
+ | `ast-impact.json` | Importadores reversos e exports públicos alterados (storage) |
79
86
  | `candidates.json` | Indícios da pré-análise Jev (regra + arquivo + confiança) (storage) |
80
87
  | `briefs/<watchdog>.md` | Instruções prontas para cada subagente (staging + storage, para auditoria) |
81
88
  | `manifest.json` | Resumo: watchdogs, arquivos, deletados, status, contexto, Jev, orçamento (storage) |
@@ -83,6 +90,11 @@ staging dos subagentes em `<HOUNDSMAN_HOME>/work/<reviewId>/`:
83
90
  > O prepare imprime o caminho absoluto do diretório de briefs e cada brief traz o
84
91
  > `outputPath` do JSON que o subagente deve gravar. Briefs e contexto também ficam
85
92
  > persistidos no storage para auditoria; o staging é limpo após o `--finalize`.
93
+ >
94
+ > **AST (TS/JS, ligado por padrão):** cada brief ganha a seção `Estrutura (AST)` com
95
+ > símbolos adicionados/removidos/alterados, assinaturas e imports; o impacto reverso
96
+ > (quem importa os arquivos alterados) aparece como mapa de leitura. É fail-open:
97
+ > sem grammar/ABI, a revisão segue só com regex. Desative com `--no-ast`.
86
98
 
87
99
  **Excluídos automaticamente:** lock files, `dist/`, `build/`, `.env*`, imagens, source
88
100
  maps (padrões do projeto em `.houndsman/exclusions.ts`).
@@ -106,7 +118,7 @@ arquivos da PR. Cada brief recebe no máximo `budgets.maxFilesPerBrief` arquivos
106
118
 
107
119
  ### Pré-análise Jev (Passo 1.5, opcional)
108
120
 
109
- Se `TYPESAFE_API_KEY` estiver definida, cada arquivo é avaliado pelo modelo **Jev** contra
121
+ Se `HOUNDSMAN_TYPE_SAFE_API_KEY` estiver definida, cada arquivo é avaliado pelo modelo **Jev** contra
110
122
  as regras atômicas dos watchdogs. Respostas acima do limiar viram **candidatos** em
111
123
  `candidates.json` e entram no brief como *warm start* — o subagente **confirma ou refuta**.
112
124
 
@@ -264,7 +276,7 @@ Se a branch não for informada, detecta com `git branch --show-current`. Base do
264
276
  | `houndsman watchdog <id>` | Contexto de um watchdog (`--json`, `--rules`) |
265
277
 
266
278
  Flags do review: `--base` · `--head` · `--branch` · `--clean` · `--full` (força
267
- completo) · `--no-jev` · `--fail-on-blocking` (com `--finalize`).
279
+ completo) · `--no-jev` · `--no-ast` · `--fail-on-blocking` (com `--finalize`).
268
280
 
269
281
  > Persistência: SQLite global em `<HOUNDSMAN_HOME>/houndsman.db`. O staging dos subagentes
270
282
  > (`briefs/`, `findings/`, contexto) vive em `<HOUNDSMAN_HOME>/work/<reviewId>/` e é limpo
@@ -24,7 +24,7 @@ altera regras.
24
24
  - **âncora sumida** (`stale`) → provável **resolvido** — mas o trecho pode ter sido reescrito;
25
25
  - **arquivo ausente** → fora da verificação (deletado/movido).
26
26
 
27
- 2. **Tier Jev (opcional, só nos ambíguos).** Com `TYPESAFE_API_KEY`, os casos `stale` / sem
27
+ 2. **Tier Jev (opcional, só nos ambíguos).** Com `HOUNDSMAN_TYPE_SAFE_API_KEY`, os casos `stale` / sem
28
28
  âncora são confirmados pelo LLM sobre um **diff focado** do arquivo (`foundAtSha..HEAD`), uma
29
29
  pergunta `noul` por finding. Falha-aberto: sem chave/erro → vale o resultado local.
30
30