@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.
- package/README.md +24 -4
- package/assets/grammars/tree-sitter-javascript.wasm +0 -0
- package/assets/grammars/tree-sitter-tsx.wasm +0 -0
- package/assets/grammars/tree-sitter-typescript.wasm +0 -0
- package/assets/grammars/web-tree-sitter.wasm +0 -0
- package/package.json +11 -3
- package/src/cli/commands/command.ts +5 -0
- package/src/cli/commands/doctor-command.ts +116 -24
- package/src/cli/commands/hunt-command.ts +25 -0
- package/src/cli/commands/server-command.ts +7 -1
- package/src/cli/help.ts +18 -2
- package/src/cli/index.ts +10 -6
- package/src/core/ai/ai-client.ts +221 -0
- package/src/core/ai/ai-sdk-model-provider.ts +101 -0
- package/src/core/ai/errors.ts +36 -0
- package/src/core/ai/features/brief-enricher.ts +62 -0
- package/src/core/ai/features/classifier.ts +50 -0
- package/src/core/ai/features/context-summarizer.ts +31 -0
- package/src/core/ai/features/pre-analyzer.ts +51 -0
- package/src/core/ai/features/verifier.ts +45 -0
- package/src/core/ai/index.ts +56 -0
- package/src/core/ai/model-factory.ts +137 -0
- package/src/core/ai/prompts/base.ts +36 -0
- package/src/core/ai/prompts/brief.ts +87 -0
- package/src/core/ai/prompts/classification.ts +36 -0
- package/src/core/ai/prompts/context.ts +29 -0
- package/src/core/ai/prompts/pre-analysis.ts +49 -0
- package/src/core/ai/prompts/verify.ts +40 -0
- package/src/core/ai/schemas.ts +110 -0
- package/src/core/ai/types.ts +76 -0
- package/src/core/ai/usage-budget.ts +75 -0
- package/src/core/ai/usage-recorder.ts +79 -0
- package/src/core/assets.ts +16 -0
- package/src/core/ast/diff.ts +220 -0
- package/src/core/ast/impact.ts +127 -0
- package/src/core/ast/parser.ts +167 -0
- package/src/core/ast/service.ts +233 -0
- package/src/core/ast/symbols.ts +294 -0
- package/src/core/ast/types.ts +125 -0
- package/src/core/brief.ts +138 -0
- package/src/core/commands/findings.ts +65 -2
- package/src/core/commands/review.ts +2 -0
- package/src/core/commands/scaffold.ts +159 -10
- package/src/core/commands/update.ts +5 -3
- package/src/core/config.ts +85 -0
- package/src/core/defaults.ts +27 -0
- package/src/core/domain/watchdog.ts +43 -5
- package/src/core/engine/houndsman-engine.ts +16 -1
- package/src/core/engine/review-scope.ts +52 -6
- package/src/core/engine/review-session.ts +27 -2
- package/src/core/env.ts +101 -0
- package/src/core/file-classifier.ts +25 -4
- package/src/core/git-runner.ts +25 -13
- package/src/core/github-review.ts +10 -7
- package/src/core/jev/jev-client.ts +8 -7
- package/src/core/jev/jev-verify.ts +2 -1
- package/src/core/logging/index.ts +17 -0
- package/src/core/logging/logger.ts +187 -0
- package/src/core/orchestrator.ts +357 -9
- package/src/core/project-config.ts +123 -1
- package/src/core/registry-check.ts +148 -6
- package/src/core/renderers/manifest-writer.ts +33 -0
- package/src/core/rule-text.ts +97 -0
- package/src/core/services/file-classifier.ts +10 -3
- package/src/core/services/gh-client.ts +38 -22
- package/src/core/services/verifier.ts +14 -3
- package/src/core/storage/paths.ts +6 -5
- package/src/core/storage/types.ts +6 -0
- package/src/core/storage/work-area.ts +12 -0
- package/src/core/types.ts +37 -1
- package/src/core/verify.ts +17 -5
- package/src/harness/README.md +15 -0
- package/src/harness/agent/agent-context.ts +33 -0
- package/src/harness/agent/agent-errors.ts +30 -0
- package/src/harness/agent/agent-factory.ts +45 -0
- package/src/harness/agent/agent-reporter.ts +34 -0
- package/src/harness/agent/agent-result.ts +16 -0
- package/src/harness/agent/agent-runner.ts +32 -0
- package/src/harness/agent/agent-scheduler.ts +37 -0
- package/src/harness/agent/review-agent.ts +391 -0
- package/src/harness/config/harness-config.ts +84 -0
- package/src/harness/hunt/hunt-harness.ts +83 -0
- package/src/harness/hunt/hunt-options.ts +75 -0
- package/src/harness/hunt/hunt-result.ts +17 -0
- package/src/harness/hunt/hunt-session.ts +372 -0
- package/src/harness/hunt/review-planner.ts +98 -0
- package/src/harness/index.ts +36 -0
- package/src/harness/investigation/agent-state.ts +134 -0
- package/src/harness/investigation/evidence-verifier.ts +129 -0
- package/src/harness/investigation/evidence.ts +110 -0
- package/src/harness/investigation/finding-critic.ts +96 -0
- package/src/harness/investigation/investigation-planner.ts +118 -0
- package/src/harness/investigation/investigation-task.ts +10 -0
- package/src/harness/investigation/review-journal.ts +87 -0
- package/src/harness/investigation/rule-analyzer.ts +178 -0
- package/src/harness/investigation/rule-signals.ts +110 -0
- package/src/harness/prompts/agent-schemas.ts +57 -0
- package/src/harness/prompts/reviewer-prompt-builder.ts +101 -0
- package/src/harness/prompts/states/conclude-prompt.ts +16 -0
- package/src/harness/prompts/states/investigate-prompt.ts +37 -0
- package/src/harness/prompts/states/orient-prompt.ts +15 -0
- package/src/harness/prompts/states/verify-prompt.ts +22 -0
- package/src/harness/tools/agent-tool.ts +102 -0
- package/src/harness/tools/builtin/changed-symbols-tool.ts +48 -0
- package/src/harness/tools/builtin/doc-context-tool.ts +74 -0
- package/src/harness/tools/builtin/file-outline-tool.ts +66 -0
- package/src/harness/tools/builtin/find-references-tool.ts +66 -0
- package/src/harness/tools/builtin/find-symbol-tool.ts +91 -0
- package/src/harness/tools/builtin/helpers.ts +115 -0
- package/src/harness/tools/builtin/impact-tool.ts +48 -0
- package/src/harness/tools/builtin/imports-exports-tool.ts +53 -0
- package/src/harness/tools/builtin/index.ts +59 -0
- package/src/harness/tools/builtin/list-files-tool.ts +85 -0
- package/src/harness/tools/builtin/pr-context-tool.ts +41 -0
- package/src/harness/tools/builtin/read-file-range-tool.ts +73 -0
- package/src/harness/tools/builtin/rule-signals-tool.ts +36 -0
- package/src/harness/tools/builtin/search-code-tool.ts +65 -0
- package/src/harness/tools/builtin/symbol-context-tool.ts +106 -0
- package/src/harness/tools/builtin/tests-for-symbol-tool.ts +61 -0
- package/src/harness/tools/path-guard.ts +90 -0
- package/src/harness/tools/result-limiter.ts +49 -0
- package/src/harness/tools/secret-redactor.ts +54 -0
- package/src/harness/tools/tool-cache.ts +50 -0
- package/src/harness/tools/tool-call-budget.ts +60 -0
- package/src/harness/tools/tool-context.ts +5 -0
- package/src/harness/tools/tool-middleware.ts +105 -0
- package/src/harness/tools/tool-policy.ts +51 -0
- package/src/harness/tools/tool-registry.ts +112 -0
- package/src/harness/tools/tool-result.ts +15 -0
- package/src/harness/tools/toolbox.ts +36 -0
- package/src/harness/validation/finding-validator.ts +119 -0
- package/src/index.ts +46 -0
- package/src/server/client/App.tsx +38 -12
- package/src/server/client/CommentsPanel.tsx +255 -0
- package/src/server/client/FindingCard.tsx +12 -4
- package/src/server/client/FindingsList.tsx +4 -1
- package/src/server/client/Sidebar.tsx +397 -0
- package/src/server/client/SubmitModal.tsx +61 -5
- package/src/server/client/icons.tsx +19 -0
- package/src/server/client/index.tsx +1 -1
- package/src/server/client/utils/constants.ts +29 -5
- package/src/server/client/utils/format.ts +47 -2
- package/src/server/client/utils/types.ts +88 -0
- package/src/server/editor.ts +129 -0
- package/src/server/index.ts +115 -5
- package/src/server/pr-comments.ts +256 -0
- package/src/server/pr-inbox.ts +202 -0
- package/src/server/run-repository.ts +33 -1
- package/src/skill/OUTPUT-FORMAT.md +12 -0
- package/src/skill/SETUP.md +6 -2
- package/src/skill/hunt.md +14 -2
- package/src/skill/verify.md +1 -1
- package/src/tests/__snapshots__/baseline.test.ts.snap +96 -14
- package/src/tests/ai-core.test.ts +120 -0
- package/src/tests/ai-features.test.ts +183 -0
- package/src/tests/ast.test.ts +335 -0
- package/src/tests/cli.test.ts +33 -0
- package/src/tests/env.test.ts +69 -0
- package/src/tests/fallback.test.ts +172 -0
- package/src/tests/fixtures/fake-agent-tools.ts +69 -0
- package/src/tests/fixtures/fake-investigation.ts +64 -0
- package/src/tests/fixtures/fake-model-provider.ts +48 -0
- package/src/tests/github-review.test.ts +9 -0
- package/src/tests/harness-agent.test.ts +122 -0
- package/src/tests/harness-investigation.test.ts +206 -0
- package/src/tests/harness-tools.test.ts +148 -0
- package/src/tests/hunt-command.test.ts +212 -0
- package/src/tests/logger.test.ts +83 -0
- package/src/tests/review-scope.test.ts +11 -11
- package/src/tests/rule-analyzer.test.ts +61 -0
- package/src/tests/server-editor.test.ts +100 -0
- package/src/tests/server.test.ts +295 -2
- package/src/tests/tool-contracts.test.ts +82 -0
- package/src/tests/verify.test.ts +1 -1
- 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(
|
|
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.
|
package/src/skill/SETUP.md
CHANGED
|
@@ -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
|
|
11
|
-
|
|
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 `
|
|
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
|
package/src/skill/verify.md
CHANGED
|
@@ -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 `
|
|
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
|
|