@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,397 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sidebar: logo do houndsman + abas `Execuções` e `PR's`.
|
|
3
|
+
*
|
|
4
|
+
* A aba de execuções mantém a lista de runs (status, data e findings). A aba de
|
|
5
|
+
* PRs lista as PRs abertas via `gh`, permite abrir a run correspondente e faz
|
|
6
|
+
* checkout somente quando a árvore de trabalho está limpa.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
|
|
10
|
+
import {
|
|
11
|
+
BranchIcon,
|
|
12
|
+
Clock,
|
|
13
|
+
GitHub,
|
|
14
|
+
GitPullRequest,
|
|
15
|
+
ListIcon,
|
|
16
|
+
Refresh,
|
|
17
|
+
Sparkles,
|
|
18
|
+
Spinner,
|
|
19
|
+
} from "./icons";
|
|
20
|
+
import { api } from "./utils/api";
|
|
21
|
+
import { describeRunStatus } from "./utils/constants";
|
|
22
|
+
import { formatRunTime } from "./utils/format";
|
|
23
|
+
import type {
|
|
24
|
+
CheckoutResponse,
|
|
25
|
+
PrListItem,
|
|
26
|
+
PrListResponse,
|
|
27
|
+
RunSummary,
|
|
28
|
+
WorktreeState,
|
|
29
|
+
} from "./utils/types";
|
|
30
|
+
|
|
31
|
+
type SidebarView = "runs" | "prs";
|
|
32
|
+
|
|
33
|
+
const REVIEW_LABELS: Record<string, string> = {
|
|
34
|
+
APPROVED: "aprovada",
|
|
35
|
+
CHANGES_REQUESTED: "mudanças",
|
|
36
|
+
REVIEW_REQUIRED: "revisão pendente",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function RunItem({
|
|
40
|
+
run,
|
|
41
|
+
active,
|
|
42
|
+
onSelect,
|
|
43
|
+
}: {
|
|
44
|
+
run: RunSummary;
|
|
45
|
+
active: boolean;
|
|
46
|
+
onSelect: (slug: string) => void;
|
|
47
|
+
}) {
|
|
48
|
+
const status = describeRunStatus(run);
|
|
49
|
+
const fixReady = status?.kind === "fix_done";
|
|
50
|
+
return (
|
|
51
|
+
<button
|
|
52
|
+
type="button"
|
|
53
|
+
title={`Abrir a run da branch ${run.branch}`}
|
|
54
|
+
class={`run${active ? " active" : ""}`}
|
|
55
|
+
onClick={() => onSelect(run.slug)}
|
|
56
|
+
>
|
|
57
|
+
<span class="run-top">
|
|
58
|
+
<BranchIcon size={14} class="run-icon" />
|
|
59
|
+
<span class="run-branch">{run.branch}</span>
|
|
60
|
+
</span>
|
|
61
|
+
<span class="run-meta">
|
|
62
|
+
<span class="run-when">
|
|
63
|
+
<Clock size={12} />
|
|
64
|
+
{formatRunTime(run.updatedAt) || "sem data"}
|
|
65
|
+
</span>
|
|
66
|
+
<span
|
|
67
|
+
class="run-count"
|
|
68
|
+
title={`${run.summary.present} de ${run.summary.total} findings presentes`}
|
|
69
|
+
>
|
|
70
|
+
{run.summary.present}/{run.summary.total}
|
|
71
|
+
</span>
|
|
72
|
+
</span>
|
|
73
|
+
{fixReady ? (
|
|
74
|
+
<span class="run-badge">
|
|
75
|
+
<Sparkles size={12} /> Correção pronta
|
|
76
|
+
</span>
|
|
77
|
+
) : (
|
|
78
|
+
status && (
|
|
79
|
+
<span class={`run-status status-${status.kind}`}>{status.label}</span>
|
|
80
|
+
)
|
|
81
|
+
)}
|
|
82
|
+
</button>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function PrItem({
|
|
87
|
+
pr,
|
|
88
|
+
currentBranch,
|
|
89
|
+
worktree,
|
|
90
|
+
busy,
|
|
91
|
+
confirming,
|
|
92
|
+
onConfirm,
|
|
93
|
+
onCancel,
|
|
94
|
+
onCheckout,
|
|
95
|
+
onSelectRun,
|
|
96
|
+
}: {
|
|
97
|
+
pr: PrListItem;
|
|
98
|
+
currentBranch: string | null;
|
|
99
|
+
worktree: WorktreeState;
|
|
100
|
+
busy: boolean;
|
|
101
|
+
confirming: boolean;
|
|
102
|
+
onConfirm: (number: number) => void;
|
|
103
|
+
onCancel: () => void;
|
|
104
|
+
onCheckout: (pr: PrListItem) => void;
|
|
105
|
+
onSelectRun: (slug: string) => void;
|
|
106
|
+
}) {
|
|
107
|
+
const isCurrent = currentBranch === pr.headRefName;
|
|
108
|
+
const runSlug = pr.runSlug;
|
|
109
|
+
const review = pr.reviewDecision ? REVIEW_LABELS[pr.reviewDecision] : undefined;
|
|
110
|
+
return (
|
|
111
|
+
<article class={`pr${isCurrent ? " current" : ""}`}>
|
|
112
|
+
<div class="pr-head">
|
|
113
|
+
<GitPullRequest size={14} class="pr-icon" />
|
|
114
|
+
<span class="pr-number">#{pr.number}</span>
|
|
115
|
+
{pr.isDraft && <span class="tag">draft</span>}
|
|
116
|
+
{isCurrent && <span class="pr-current">atual</span>}
|
|
117
|
+
<span class="spacer" />
|
|
118
|
+
{pr.updatedAt && (
|
|
119
|
+
<span class="run-when">
|
|
120
|
+
<Clock size={12} />
|
|
121
|
+
{formatRunTime(pr.updatedAt)}
|
|
122
|
+
</span>
|
|
123
|
+
)}
|
|
124
|
+
</div>
|
|
125
|
+
<p class="pr-title">{pr.title}</p>
|
|
126
|
+
<p class="pr-meta">
|
|
127
|
+
<span class="pr-branch">{pr.headRefName}</span>
|
|
128
|
+
<span class="pr-arrow">→</span>
|
|
129
|
+
<span>{pr.baseRefName || "—"}</span>
|
|
130
|
+
<span>· {pr.author}</span>
|
|
131
|
+
</p>
|
|
132
|
+
{review && <p class={`pr-review pr-review-${pr.reviewDecision}`}>{review}</p>}
|
|
133
|
+
<div class="pr-actions">
|
|
134
|
+
{runSlug && (
|
|
135
|
+
<button
|
|
136
|
+
type="button"
|
|
137
|
+
class="pr-btn"
|
|
138
|
+
title="Abrir a run local desta branch"
|
|
139
|
+
onClick={() => onSelectRun(runSlug)}
|
|
140
|
+
>
|
|
141
|
+
Abrir run
|
|
142
|
+
</button>
|
|
143
|
+
)}
|
|
144
|
+
{pr.url && (
|
|
145
|
+
<a
|
|
146
|
+
class="pr-btn"
|
|
147
|
+
href={pr.url}
|
|
148
|
+
target="_blank"
|
|
149
|
+
rel="noreferrer"
|
|
150
|
+
title="Abrir a PR no GitHub"
|
|
151
|
+
>
|
|
152
|
+
<GitHub size={12} /> GitHub
|
|
153
|
+
</a>
|
|
154
|
+
)}
|
|
155
|
+
{confirming ? (
|
|
156
|
+
<>
|
|
157
|
+
<button
|
|
158
|
+
type="button"
|
|
159
|
+
class="pr-btn danger"
|
|
160
|
+
disabled={busy}
|
|
161
|
+
title="Confirmar checkout da PR"
|
|
162
|
+
onClick={() => onCheckout(pr)}
|
|
163
|
+
>
|
|
164
|
+
{busy ? <Spinner size={12} class="icon spin" /> : "Confirmar"}
|
|
165
|
+
</button>
|
|
166
|
+
<button
|
|
167
|
+
type="button"
|
|
168
|
+
class="pr-btn"
|
|
169
|
+
disabled={busy}
|
|
170
|
+
onClick={onCancel}
|
|
171
|
+
>
|
|
172
|
+
Cancelar
|
|
173
|
+
</button>
|
|
174
|
+
</>
|
|
175
|
+
) : (
|
|
176
|
+
<button
|
|
177
|
+
type="button"
|
|
178
|
+
class="pr-btn"
|
|
179
|
+
disabled={!worktree.clean || busy}
|
|
180
|
+
title={
|
|
181
|
+
worktree.clean
|
|
182
|
+
? `Fazer checkout da branch ${pr.headRefName}`
|
|
183
|
+
: "Há alterações não commitadas — faça commit ou stash antes"
|
|
184
|
+
}
|
|
185
|
+
onClick={() => onConfirm(pr.number)}
|
|
186
|
+
>
|
|
187
|
+
Checkout
|
|
188
|
+
</button>
|
|
189
|
+
)}
|
|
190
|
+
</div>
|
|
191
|
+
</article>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function Sidebar({
|
|
196
|
+
runs,
|
|
197
|
+
slug,
|
|
198
|
+
onSelect,
|
|
199
|
+
onRefreshRuns,
|
|
200
|
+
}: {
|
|
201
|
+
runs: RunSummary[];
|
|
202
|
+
slug: string | null;
|
|
203
|
+
onSelect: (slug: string) => void;
|
|
204
|
+
onRefreshRuns: () => void;
|
|
205
|
+
}) {
|
|
206
|
+
const [view, setView] = useState<SidebarView>("runs");
|
|
207
|
+
const [data, setData] = useState<PrListResponse | null>(null);
|
|
208
|
+
const [loading, setLoading] = useState(false);
|
|
209
|
+
const [error, setError] = useState("");
|
|
210
|
+
const [notice, setNotice] = useState("");
|
|
211
|
+
const [busy, setBusy] = useState<number | null>(null);
|
|
212
|
+
const [confirming, setConfirming] = useState<number | null>(null);
|
|
213
|
+
const requested = useRef(false);
|
|
214
|
+
|
|
215
|
+
const loadPrs = useCallback(async () => {
|
|
216
|
+
setLoading(true);
|
|
217
|
+
setError("");
|
|
218
|
+
try {
|
|
219
|
+
setData(await api<PrListResponse>("/api/prs"));
|
|
220
|
+
} catch (err) {
|
|
221
|
+
setError(String(err));
|
|
222
|
+
} finally {
|
|
223
|
+
setLoading(false);
|
|
224
|
+
}
|
|
225
|
+
}, []);
|
|
226
|
+
|
|
227
|
+
useEffect(() => {
|
|
228
|
+
if (view !== "prs" || requested.current) return;
|
|
229
|
+
requested.current = true;
|
|
230
|
+
void loadPrs();
|
|
231
|
+
}, [view, loadPrs]);
|
|
232
|
+
|
|
233
|
+
const checkout = useCallback(
|
|
234
|
+
async (pr: PrListItem) => {
|
|
235
|
+
setBusy(pr.number);
|
|
236
|
+
setError("");
|
|
237
|
+
setNotice("");
|
|
238
|
+
try {
|
|
239
|
+
const result = await api<CheckoutResponse>("/api/prs/checkout", {
|
|
240
|
+
method: "POST",
|
|
241
|
+
headers: { "content-type": "application/json" },
|
|
242
|
+
body: JSON.stringify({ number: pr.number }),
|
|
243
|
+
});
|
|
244
|
+
if (!result.ok) {
|
|
245
|
+
if (result.reason === "dirty") {
|
|
246
|
+
setData((current) =>
|
|
247
|
+
current
|
|
248
|
+
? {
|
|
249
|
+
...current,
|
|
250
|
+
worktree: {
|
|
251
|
+
...current.worktree,
|
|
252
|
+
clean: false,
|
|
253
|
+
changes:
|
|
254
|
+
result.changes?.length ??
|
|
255
|
+
current.worktree.changes,
|
|
256
|
+
sample:
|
|
257
|
+
result.changes ?? current.worktree.sample,
|
|
258
|
+
},
|
|
259
|
+
}
|
|
260
|
+
: current,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
setError(
|
|
264
|
+
result.reason === "dirty"
|
|
265
|
+
? `Checkout bloqueado: ${result.detail}`
|
|
266
|
+
: result.detail,
|
|
267
|
+
);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
setConfirming(null);
|
|
271
|
+
setNotice(
|
|
272
|
+
`Checkout de #${pr.number} concluído${
|
|
273
|
+
result.branch ? ` — ${result.branch}` : ""
|
|
274
|
+
}.`,
|
|
275
|
+
);
|
|
276
|
+
await loadPrs();
|
|
277
|
+
onRefreshRuns();
|
|
278
|
+
if (pr.runSlug) onSelect(pr.runSlug);
|
|
279
|
+
} catch (err) {
|
|
280
|
+
setError(String(err));
|
|
281
|
+
} finally {
|
|
282
|
+
setBusy(null);
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
[loadPrs, onRefreshRuns, onSelect],
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
return (
|
|
289
|
+
<aside class="sidebar">
|
|
290
|
+
<div class="brand">
|
|
291
|
+
<img class="logo" src="/assets/logo.png" alt="" />
|
|
292
|
+
<div>
|
|
293
|
+
<h1>Houndsman</h1>
|
|
294
|
+
<p class="sub">Revisões e triagem</p>
|
|
295
|
+
</div>
|
|
296
|
+
</div>
|
|
297
|
+
|
|
298
|
+
<div class="side-tabs" role="tablist" aria-label="Navegação lateral">
|
|
299
|
+
<button
|
|
300
|
+
type="button"
|
|
301
|
+
role="tab"
|
|
302
|
+
aria-selected={view === "runs"}
|
|
303
|
+
class={`side-tab${view === "runs" ? " active" : ""}`}
|
|
304
|
+
onClick={() => setView("runs")}
|
|
305
|
+
>
|
|
306
|
+
<ListIcon size={13} /> Execuções
|
|
307
|
+
<span class="side-count">{runs.length}</span>
|
|
308
|
+
</button>
|
|
309
|
+
<button
|
|
310
|
+
type="button"
|
|
311
|
+
role="tab"
|
|
312
|
+
aria-selected={view === "prs"}
|
|
313
|
+
class={`side-tab${view === "prs" ? " active" : ""}`}
|
|
314
|
+
onClick={() => setView("prs")}
|
|
315
|
+
>
|
|
316
|
+
<GitPullRequest size={13} /> PR's
|
|
317
|
+
</button>
|
|
318
|
+
</div>
|
|
319
|
+
|
|
320
|
+
{view === "runs" ? (
|
|
321
|
+
<nav class="runs" aria-label="Execuções">
|
|
322
|
+
{runs.length === 0 && <p class="empty">Nenhuma run encontrada.</p>}
|
|
323
|
+
{runs.map((run) => (
|
|
324
|
+
<RunItem
|
|
325
|
+
key={run.slug}
|
|
326
|
+
run={run}
|
|
327
|
+
active={run.slug === slug}
|
|
328
|
+
onSelect={onSelect}
|
|
329
|
+
/>
|
|
330
|
+
))}
|
|
331
|
+
</nav>
|
|
332
|
+
) : (
|
|
333
|
+
<>
|
|
334
|
+
<div class="pr-toolbar">
|
|
335
|
+
<p class="section-label">PRs abertas</p>
|
|
336
|
+
<button
|
|
337
|
+
type="button"
|
|
338
|
+
class="icon-btn"
|
|
339
|
+
title="Atualizar lista de PRs"
|
|
340
|
+
disabled={loading}
|
|
341
|
+
onClick={() => void loadPrs()}
|
|
342
|
+
>
|
|
343
|
+
<Refresh
|
|
344
|
+
size={13}
|
|
345
|
+
class={loading ? "icon spin" : undefined}
|
|
346
|
+
/>
|
|
347
|
+
</button>
|
|
348
|
+
</div>
|
|
349
|
+
|
|
350
|
+
{error && <p class="error">{error}</p>}
|
|
351
|
+
{notice && <p class="pr-notice">{notice}</p>}
|
|
352
|
+
{data && !data.available && (
|
|
353
|
+
<p class="empty">
|
|
354
|
+
GitHub indisponível
|
|
355
|
+
{data.reason ? `: ${data.reason}` : "."}
|
|
356
|
+
</p>
|
|
357
|
+
)}
|
|
358
|
+
{data && !data.worktree.available && (
|
|
359
|
+
<p class="empty">Não foi possível ler o estado do git.</p>
|
|
360
|
+
)}
|
|
361
|
+
{data?.worktree.available && !data.worktree.clean && (
|
|
362
|
+
<p class="pr-warning">
|
|
363
|
+
{data.worktree.changes} alteração(ões) não commitada(s) —
|
|
364
|
+
checkout bloqueado até limpar a árvore.
|
|
365
|
+
</p>
|
|
366
|
+
)}
|
|
367
|
+
{data?.currentBranch && (
|
|
368
|
+
<p class="pr-current-branch">
|
|
369
|
+
Branch atual: <code>{data.currentBranch}</code>
|
|
370
|
+
</p>
|
|
371
|
+
)}
|
|
372
|
+
|
|
373
|
+
<nav class="prs" aria-label="Pull requests abertas">
|
|
374
|
+
{loading && !data && <p class="empty">Carregando PRs…</p>}
|
|
375
|
+
{data?.available && data.prs.length === 0 && (
|
|
376
|
+
<p class="empty">Nenhuma PR aberta.</p>
|
|
377
|
+
)}
|
|
378
|
+
{data?.prs.map((pr) => (
|
|
379
|
+
<PrItem
|
|
380
|
+
key={pr.number}
|
|
381
|
+
pr={pr}
|
|
382
|
+
currentBranch={data.currentBranch}
|
|
383
|
+
worktree={data.worktree}
|
|
384
|
+
busy={busy !== null}
|
|
385
|
+
confirming={confirming === pr.number}
|
|
386
|
+
onConfirm={setConfirming}
|
|
387
|
+
onCancel={() => setConfirming(null)}
|
|
388
|
+
onCheckout={checkout}
|
|
389
|
+
onSelectRun={onSelect}
|
|
390
|
+
/>
|
|
391
|
+
))}
|
|
392
|
+
</nav>
|
|
393
|
+
</>
|
|
394
|
+
)}
|
|
395
|
+
</aside>
|
|
396
|
+
);
|
|
397
|
+
}
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* Modal "Enviar revisão": publica os findings selecionados como comentários na
|
|
3
3
|
* Pull Request (`POST /submit`, via `gh`).
|
|
4
4
|
*
|
|
5
|
-
* Filtra por severidade e estado de execução, mostra uma
|
|
6
|
-
* será disparado e o resultado (com o link do comentário
|
|
5
|
+
* Filtra por severidade, decisão (a executar) e estado de execução, mostra uma
|
|
6
|
+
* prévia curta do que será disparado e o resultado (com o link do comentário
|
|
7
|
+
* geral, quando houver).
|
|
7
8
|
* Fail-open: sem `gh`/PR o servidor devolve `available:false` e o modal só
|
|
8
9
|
* informa — nada é postado.
|
|
9
10
|
*/
|
|
@@ -11,8 +12,14 @@
|
|
|
11
12
|
import { useCallback, useEffect, useMemo, useState } from "preact/hooks";
|
|
12
13
|
import { GitHub, Send, Spinner } from "./icons";
|
|
13
14
|
import { api } from "./utils/api";
|
|
14
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
DECISION_LABELS,
|
|
17
|
+
EXEC_STATES,
|
|
18
|
+
SEVERITY_LABELS,
|
|
19
|
+
SEVERITY_ORDER,
|
|
20
|
+
} from "./utils/constants";
|
|
15
21
|
import type {
|
|
22
|
+
Decision,
|
|
16
23
|
ExecState,
|
|
17
24
|
Severity,
|
|
18
25
|
SubmitOutcome,
|
|
@@ -27,6 +34,9 @@ const EXEC_LABELS: Record<ExecState, string> = {
|
|
|
27
34
|
stale: "Stale",
|
|
28
35
|
};
|
|
29
36
|
|
|
37
|
+
/** Decisões elegíveis ao envio: `ignore` fica de fora por definição. */
|
|
38
|
+
const SUBMIT_DECISIONS: Decision[] = ["execute", "pending"];
|
|
39
|
+
|
|
30
40
|
export function SubmitModal({
|
|
31
41
|
slug,
|
|
32
42
|
branch,
|
|
@@ -42,6 +52,7 @@ export function SubmitModal({
|
|
|
42
52
|
}) {
|
|
43
53
|
const [preview, setPreview] = useState<SubmitPreview | null>(null);
|
|
44
54
|
const [severities, setSeverities] = useState<Severity[]>([]);
|
|
55
|
+
const [decisions, setDecisions] = useState<Decision[]>([]);
|
|
45
56
|
const [execStates, setExecStates] = useState<ExecState[]>([]);
|
|
46
57
|
const [includeSent, setIncludeSent] = useState(false);
|
|
47
58
|
const [loading, setLoading] = useState(false);
|
|
@@ -86,6 +97,7 @@ export function SubmitModal({
|
|
|
86
97
|
if (open) return;
|
|
87
98
|
setPreview(null);
|
|
88
99
|
setSeverities([]);
|
|
100
|
+
setDecisions([]);
|
|
89
101
|
setExecStates([]);
|
|
90
102
|
setIncludeSent(false);
|
|
91
103
|
setLoading(false);
|
|
@@ -106,6 +118,12 @@ export function SubmitModal({
|
|
|
106
118
|
? current.filter((item) => item !== value)
|
|
107
119
|
: [...current, value],
|
|
108
120
|
);
|
|
121
|
+
const toggleDecision = (value: Decision) =>
|
|
122
|
+
setDecisions((current) =>
|
|
123
|
+
current.includes(value)
|
|
124
|
+
? current.filter((item) => item !== value)
|
|
125
|
+
: [...current, value],
|
|
126
|
+
);
|
|
109
127
|
|
|
110
128
|
const selected = useMemo(
|
|
111
129
|
() =>
|
|
@@ -113,9 +131,14 @@ export function SubmitModal({
|
|
|
113
131
|
(candidate) =>
|
|
114
132
|
(severities.length === 0 ||
|
|
115
133
|
severities.includes(candidate.severity)) &&
|
|
134
|
+
// Servidor antigo ainda em memória pode não mandar `decision`:
|
|
135
|
+
// nesse caso não escondemos o candidato do filtro.
|
|
136
|
+
(decisions.length === 0 ||
|
|
137
|
+
!candidate.decision ||
|
|
138
|
+
decisions.includes(candidate.decision)) &&
|
|
116
139
|
(execStates.length === 0 || execStates.includes(candidate.execState)),
|
|
117
140
|
),
|
|
118
|
-
[preview, severities, execStates],
|
|
141
|
+
[preview, severities, decisions, execStates],
|
|
119
142
|
);
|
|
120
143
|
|
|
121
144
|
const toSend = useMemo(
|
|
@@ -140,6 +163,7 @@ export function SubmitModal({
|
|
|
140
163
|
headers: { "content-type": "application/json" },
|
|
141
164
|
body: JSON.stringify({
|
|
142
165
|
severities: severities.length > 0 ? severities : undefined,
|
|
166
|
+
decisions: decisions.length > 0 ? decisions : undefined,
|
|
143
167
|
execStates: execStates.length > 0 ? execStates : undefined,
|
|
144
168
|
includeSent,
|
|
145
169
|
}),
|
|
@@ -155,7 +179,7 @@ export function SubmitModal({
|
|
|
155
179
|
} finally {
|
|
156
180
|
setPosting(false);
|
|
157
181
|
}
|
|
158
|
-
}, [slug, severities, execStates, includeSent, onSubmitted]);
|
|
182
|
+
}, [slug, severities, decisions, execStates, includeSent, onSubmitted]);
|
|
159
183
|
|
|
160
184
|
if (!open) return null;
|
|
161
185
|
|
|
@@ -239,6 +263,38 @@ export function SubmitModal({
|
|
|
239
263
|
</div>
|
|
240
264
|
</div>
|
|
241
265
|
|
|
266
|
+
<div class="submit-field">
|
|
267
|
+
<span class="submit-label">Decisão</span>
|
|
268
|
+
<div class="chips">
|
|
269
|
+
<button
|
|
270
|
+
type="button"
|
|
271
|
+
class={`chip${decisions.length === 0 ? " active" : ""}`}
|
|
272
|
+
onClick={() => setDecisions([])}
|
|
273
|
+
>
|
|
274
|
+
Todas
|
|
275
|
+
</button>
|
|
276
|
+
{SUBMIT_DECISIONS.map((decision) => (
|
|
277
|
+
<button
|
|
278
|
+
type="button"
|
|
279
|
+
key={decision}
|
|
280
|
+
title={
|
|
281
|
+
decision === "execute"
|
|
282
|
+
? "Findings marcados como Executar (a executar)"
|
|
283
|
+
: "Findings ainda não triados"
|
|
284
|
+
}
|
|
285
|
+
class={`chip dec-${decision}${
|
|
286
|
+
decisions.includes(decision)
|
|
287
|
+
? " active"
|
|
288
|
+
: ""
|
|
289
|
+
}`}
|
|
290
|
+
onClick={() => toggleDecision(decision)}
|
|
291
|
+
>
|
|
292
|
+
{DECISION_LABELS[decision]}
|
|
293
|
+
</button>
|
|
294
|
+
))}
|
|
295
|
+
</div>
|
|
296
|
+
</div>
|
|
297
|
+
|
|
242
298
|
<div class="submit-field">
|
|
243
299
|
<span class="submit-label">Execução</span>
|
|
244
300
|
<div class="chips">
|
|
@@ -158,3 +158,22 @@ export const Send = (p: IconProps) => (
|
|
|
158
158
|
<path d="M22 2 15 22l-4-9-9-4z" />
|
|
159
159
|
</Svg>
|
|
160
160
|
);
|
|
161
|
+
|
|
162
|
+
/** Pull request — aba de PRs da sidebar. */
|
|
163
|
+
export const GitPullRequest = (p: IconProps) => (
|
|
164
|
+
<Svg {...p}>
|
|
165
|
+
<circle cx="6" cy="6" r="2.4" />
|
|
166
|
+
<circle cx="6" cy="18" r="2.4" />
|
|
167
|
+
<circle cx="18" cy="18" r="2.4" />
|
|
168
|
+
<path d="M6 8.4v7.2" />
|
|
169
|
+
<path d="M18 15.6V12a4 4 0 0 0-4-4h-2.5" />
|
|
170
|
+
<path d="m13.5 5.5-2.5 2.5 2.5 2.5" />
|
|
171
|
+
</Svg>
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
/** Balão de conversa — aba de comentários. */
|
|
175
|
+
export const MessageSquare = (p: IconProps) => (
|
|
176
|
+
<Svg {...p}>
|
|
177
|
+
<path d="M21 15a3 3 0 0 1-3 3H8l-5 3V6a3 3 0 0 1 3-3h12a3 3 0 0 1 3 3z" />
|
|
178
|
+
</Svg>
|
|
179
|
+
);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Entry do cliente (Preact): monta o `<App />` no `#app`.
|
|
3
3
|
*
|
|
4
4
|
* Bundle gerado em memória por `Bun.build` (`client-build.ts`). Os componentes
|
|
5
|
-
* vivem em arquivos irmãos nesta pasta (`App.tsx`, `
|
|
5
|
+
* vivem em arquivos irmãos nesta pasta (`App.tsx`, `Sidebar.tsx`, ...).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { render } from "preact";
|
|
@@ -91,6 +91,12 @@ const KIND_ORDER: ArtifactKind[] = [
|
|
|
91
91
|
"manifest",
|
|
92
92
|
"classification",
|
|
93
93
|
"candidates",
|
|
94
|
+
"ast",
|
|
95
|
+
"ast-impact",
|
|
96
|
+
"ai",
|
|
97
|
+
"ai-usage",
|
|
98
|
+
"ai-brief",
|
|
99
|
+
"harness-transcript",
|
|
94
100
|
"context",
|
|
95
101
|
"context-brief",
|
|
96
102
|
"context-summary",
|
|
@@ -109,11 +115,17 @@ const GROUP_BY_KIND: Record<ArtifactKind, { key: string; label: string; order: n
|
|
|
109
115
|
manifest: { key: "core", label: "Núcleo da revisão", order: 3 },
|
|
110
116
|
classification: { key: "core", label: "Núcleo da revisão", order: 3 },
|
|
111
117
|
candidates: { key: "core", label: "Núcleo da revisão", order: 3 },
|
|
112
|
-
|
|
113
|
-
"
|
|
114
|
-
|
|
115
|
-
"
|
|
116
|
-
|
|
118
|
+
ast: { key: "core", label: "Núcleo da revisão", order: 3 },
|
|
119
|
+
"ast-impact": { key: "core", label: "Núcleo da revisão", order: 3 },
|
|
120
|
+
ai: { key: "ai", label: "IA e harness", order: 4 },
|
|
121
|
+
"ai-usage": { key: "ai", label: "IA e harness", order: 4 },
|
|
122
|
+
"ai-brief": { key: "ai", label: "IA e harness", order: 4 },
|
|
123
|
+
"harness-transcript": { key: "ai", label: "IA e harness", order: 4 },
|
|
124
|
+
context: { key: "context", label: "Contexto da PR", order: 5 },
|
|
125
|
+
"context-brief": { key: "context", label: "Contexto da PR", order: 5 },
|
|
126
|
+
"context-summary": { key: "context", label: "Contexto da PR", order: 5 },
|
|
127
|
+
"finding-report": { key: "other", label: "Outros", order: 6 },
|
|
128
|
+
other: { key: "other", label: "Outros", order: 6 },
|
|
117
129
|
};
|
|
118
130
|
|
|
119
131
|
/** Rótulo curto do tipo (badge do item). */
|
|
@@ -126,6 +138,12 @@ export const ARTIFACT_KIND_LABELS: Record<ArtifactKind, string> = {
|
|
|
126
138
|
manifest: "manifesto",
|
|
127
139
|
classification: "classificação",
|
|
128
140
|
candidates: "jev",
|
|
141
|
+
ast: "ast",
|
|
142
|
+
"ast-impact": "impacto",
|
|
143
|
+
ai: "ia",
|
|
144
|
+
"ai-usage": "uso ia",
|
|
145
|
+
"ai-brief": "plano ia",
|
|
146
|
+
"harness-transcript": "transcript",
|
|
129
147
|
context: "contexto",
|
|
130
148
|
"context-brief": "brief ctx",
|
|
131
149
|
"context-summary": "resumo ctx",
|
|
@@ -159,6 +177,10 @@ export function describeArtifact(meta: ArtifactMeta): string {
|
|
|
159
177
|
return "Classificação do diff: watchdogs ativados por arquivo, status (A/M/D/R) e churn.";
|
|
160
178
|
case "candidates":
|
|
161
179
|
return "Indícios da pré-análise Jev (regra + arquivo + confiança) — não são achados.";
|
|
180
|
+
case "ast":
|
|
181
|
+
return "Fatos estruturais e delta por arquivo (símbolos, imports, exports e assinaturas).";
|
|
182
|
+
case "ast-impact":
|
|
183
|
+
return "Impacto reverso: arquivos que importam os alterados e exports públicos modificados.";
|
|
162
184
|
case "context":
|
|
163
185
|
return "Payload bruto da PR coletado via `gh` (ou marcador de indisponível).";
|
|
164
186
|
case "context-brief":
|
|
@@ -223,4 +245,6 @@ export const TAB_CAPTIONS = {
|
|
|
223
245
|
"Triagem dos achados da revisão: decida Executar, Ignorar ou deixe Pendente — o comentário orienta o auto-fix. Agente: houndsman findings list --decision pending --json · findings done <id>.",
|
|
224
246
|
artifacts:
|
|
225
247
|
"Artefatos persistidos da run: relatórios, briefs de auditoria, manifesto, classificação, Jev, contexto da PR e verificação. JSON com destaque de sintaxe; markdown renderizado com highlight de código.",
|
|
248
|
+
comments:
|
|
249
|
+
"Comentários da PR vinculada à branch: threads de revisão agrupadas por arquivo e comentários gerais. Carregados sob demanda ao abrir esta aba.",
|
|
226
250
|
} as const;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Helpers de apresentação: data/hora das runs e
|
|
3
|
-
*
|
|
2
|
+
* Helpers de apresentação: data/hora das runs e links para abrir o arquivo no
|
|
3
|
+
* editor detectado pelo servidor (`vscode://` ou `jetbrains://`).
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import type { EditorContext } from "./types";
|
|
7
|
+
|
|
6
8
|
/** Data/hora curta em pt-BR — ex.: `14 de out., 17:48`. */
|
|
7
9
|
export function formatRunTime(iso?: string): string {
|
|
8
10
|
if (!iso) return "";
|
|
@@ -38,3 +40,46 @@ export function vscodeFileLink(file: string, line?: number | null): string | nul
|
|
|
38
40
|
);
|
|
39
41
|
return `vscode://file/${encodeURI(normalized)}${line ? `:${line}` : ""}`;
|
|
40
42
|
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Link `jetbrains://<produto>/navigate/reference?project=<nome>&path=<arquivo>`.
|
|
46
|
+
* O IDE espera o caminho absoluto com a linha no fim (`arquivo:linha`).
|
|
47
|
+
*/
|
|
48
|
+
export function jetbrainsFileLink(
|
|
49
|
+
file: string,
|
|
50
|
+
line: number | null | undefined,
|
|
51
|
+
project: string,
|
|
52
|
+
product: string,
|
|
53
|
+
): string | null {
|
|
54
|
+
const path = file.replace(/\\/g, "/");
|
|
55
|
+
if (!/^([a-zA-Z]:\/|\/)/.test(path)) return null;
|
|
56
|
+
const target = `${path}${line ? `:${line}` : ""}`;
|
|
57
|
+
return `jetbrains://${product}/navigate/reference?project=${encodeURIComponent(
|
|
58
|
+
project,
|
|
59
|
+
)}&path=${encodeURIComponent(target)}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Monta o link de código conforme o editor detectado (null = sem link). */
|
|
63
|
+
export function editorFileLink(
|
|
64
|
+
file: string,
|
|
65
|
+
line: number | null | undefined,
|
|
66
|
+
editor: EditorContext,
|
|
67
|
+
): string | null {
|
|
68
|
+
if (editor.kind === "vscode") return vscodeFileLink(file, line);
|
|
69
|
+
if (editor.kind === "jetbrains") {
|
|
70
|
+
return jetbrainsFileLink(
|
|
71
|
+
file,
|
|
72
|
+
line,
|
|
73
|
+
editor.project ?? "",
|
|
74
|
+
editor.product ?? "idea",
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Rótulo do editor para tooltips. */
|
|
81
|
+
export function editorLabel(editor: EditorContext): string {
|
|
82
|
+
if (editor.kind === "jetbrains") return "JetBrains";
|
|
83
|
+
if (editor.kind === "vscode") return "VS Code";
|
|
84
|
+
return "editor";
|
|
85
|
+
}
|