@payconductor/houndsman 0.7.0 → 0.10.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 (88) hide show
  1. package/README.md +20 -0
  2. package/package.json +1 -1
  3. package/src/cli/commands/command.ts +10 -1
  4. package/src/cli/commands/context-command.ts +40 -2
  5. package/src/cli/commands/hunt-command.ts +28 -2
  6. package/src/cli/commands/review-command.ts +30 -2
  7. package/src/cli/commands/worktree-command.ts +118 -0
  8. package/src/cli/help.ts +12 -2
  9. package/src/cli/index.ts +14 -4
  10. package/src/core/ai/ai-client.ts +21 -0
  11. package/src/core/ai/ai-sdk-model-provider.ts +43 -18
  12. package/src/core/ai/features/technical-summarizer.ts +43 -0
  13. package/src/core/ai/index.ts +4 -1
  14. package/src/core/ai/prompts/context.ts +3 -3
  15. package/src/core/ai/prompts/technical.ts +30 -0
  16. package/src/core/ai/provider-capabilities.ts +45 -0
  17. package/src/core/ai/schemas.ts +19 -3
  18. package/src/core/ai/types.ts +3 -1
  19. package/src/core/ast/service.ts +7 -2
  20. package/src/core/brief.ts +26 -4
  21. package/src/core/commands/context.ts +9 -1
  22. package/src/core/commands/findings.ts +11 -5
  23. package/src/core/commands/review.ts +13 -0
  24. package/src/core/commands/scaffold.ts +9 -2
  25. package/src/core/config.ts +30 -1
  26. package/src/core/defaults.ts +7 -0
  27. package/src/core/engine/houndsman-engine.ts +26 -3
  28. package/src/core/engine/review-scope.ts +17 -22
  29. package/src/core/engine/review-session.ts +25 -2
  30. package/src/core/engine/review-workspace.ts +204 -0
  31. package/src/core/git-runner.ts +8 -1
  32. package/src/core/jev/jev-detector.ts +10 -2
  33. package/src/core/orchestrator.ts +176 -15
  34. package/src/core/paths.ts +3 -0
  35. package/src/core/pr-context.ts +11 -6
  36. package/src/core/project-config.ts +21 -1
  37. package/src/core/renderers/resume-renderer.ts +17 -34
  38. package/src/core/report-builder.ts +6 -6
  39. package/src/core/services/git-repository.ts +46 -0
  40. package/src/core/services/pr-resolver.ts +147 -0
  41. package/src/core/services/worktree-manager.ts +293 -0
  42. package/src/core/storage/file/file-storage.ts +14 -1
  43. package/src/core/storage/paths.ts +30 -1
  44. package/src/core/storage/sqlite/mappers.ts +4 -0
  45. package/src/core/storage/sqlite/schema.ts +13 -3
  46. package/src/core/storage/sqlite/sqlite-storage.ts +28 -8
  47. package/src/core/storage/types.ts +13 -0
  48. package/src/core/storage/work-area.ts +20 -1
  49. package/src/core/technical-summary.ts +398 -0
  50. package/src/core/types.ts +2 -0
  51. package/src/harness/agent/agent-factory.ts +1 -0
  52. package/src/harness/agent/review-agent.ts +11 -1
  53. package/src/harness/config/harness-config.ts +6 -1
  54. package/src/harness/hunt/hunt-options.ts +17 -0
  55. package/src/harness/hunt/hunt-session.ts +7 -20
  56. package/src/index.ts +33 -0
  57. package/src/server/client/App.tsx +51 -12
  58. package/src/server/client/DeleteRunModal.tsx +163 -0
  59. package/src/server/client/PrsSection.tsx +296 -0
  60. package/src/server/client/ResumePanel.tsx +17 -20
  61. package/src/server/client/RunsSection.tsx +73 -0
  62. package/src/server/client/Sidebar.tsx +171 -358
  63. package/src/server/client/icons.tsx +10 -0
  64. package/src/server/client/sidebar-sections.tsx +73 -0
  65. package/src/server/client/utils/constants.ts +7 -2
  66. package/src/server/client/utils/sidebar-prefs.ts +64 -0
  67. package/src/server/client/utils/types.ts +27 -0
  68. package/src/server/index.ts +113 -406
  69. package/src/server/pr-inbox.ts +15 -4
  70. package/src/server/run-repository.ts +103 -2
  71. package/src/server/styles.css +454 -0
  72. package/src/skill/README.md +11 -3
  73. package/src/skill/SKILL.md +44 -10
  74. package/src/skill/hunt.md +33 -7
  75. package/src/tests/__snapshots__/baseline.test.ts.snap +99 -24
  76. package/src/tests/ai-core.test.ts +71 -0
  77. package/src/tests/ai-features.test.ts +36 -0
  78. package/src/tests/baseline.test.ts +4 -1
  79. package/src/tests/cli.test.ts +2 -1
  80. package/src/tests/harness-agent.test.ts +19 -0
  81. package/src/tests/review-resume.test.ts +36 -8
  82. package/src/tests/review-scope.test.ts +67 -0
  83. package/src/tests/review-scripts.test.ts +2 -2
  84. package/src/tests/server.test.ts +54 -1
  85. package/src/tests/sidebar-sections.test.ts +94 -0
  86. package/src/tests/technical-summary.test.ts +176 -0
  87. package/src/tests/worktree.test.ts +404 -0
  88. package/assets/logo-legacy.png +0 -0
@@ -8,6 +8,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks"
8
8
  import { ArtifactsPanel } from "./ArtifactsPanel";
9
9
  import { AutoRefresh } from "./AutoRefresh";
10
10
  import { CommentsPanel } from "./CommentsPanel";
11
+ import { DeleteRunModal } from "./DeleteRunModal";
11
12
  import { Filters, type FiltersState } from "./Filters";
12
13
  import { FindingsList } from "./FindingsList";
13
14
  import { FileText, Layers, ListIcon, MessageSquare, Refresh, Send } from "./icons";
@@ -17,7 +18,13 @@ import { SubmitModal } from "./SubmitModal";
17
18
  import { SummaryBar } from "./SummaryBar";
18
19
  import { api, computeSummary } from "./utils/api";
19
20
  import { AUTO_REFRESH_MS, TAB_CAPTIONS } from "./utils/constants";
20
- import type { EditorContext, Finding, FindingsResponse, RunSummary } from "./utils/types";
21
+ import type {
22
+ DeleteRunResponse,
23
+ EditorContext,
24
+ Finding,
25
+ FindingsResponse,
26
+ RunSummary,
27
+ } from "./utils/types";
21
28
 
22
29
  type Tab = "resume" | "findings" | "artifacts" | "comments";
23
30
 
@@ -67,6 +74,7 @@ export function App() {
67
74
  const [refreshing, setRefreshing] = useState(false);
68
75
  const [syncing, setSyncing] = useState(false);
69
76
  const [submitOpen, setSubmitOpen] = useState(false);
77
+ const [deleteOpen, setDeleteOpen] = useState(false);
70
78
  /** Editor detectado pelo servidor — base dos links de código. */
71
79
  const [editor, setEditor] = useState<EditorContext>({ kind: "vscode" });
72
80
  /** Refs espelhando estado volátil — o polling não deve recriar o intervalo. */
@@ -212,8 +220,28 @@ export function App() {
212
220
 
213
221
  const summary = useMemo(() => computeSummary(data?.findings ?? []), [data]);
214
222
 
215
- const branch =
216
- data?.branch ?? runs.find((run) => run.slug === slug)?.branch ?? slug ?? "";
223
+ const selectedRun = runs.find((run) => run.slug === slug) ?? null;
224
+ const branch = data?.branch ?? selectedRun?.branch ?? slug ?? "";
225
+
226
+ /** Após excluir: limpa a seleção e escolhe a próxima run da lista. */
227
+ const afterDelete = useCallback((result: DeleteRunResponse) => {
228
+ setDeleteOpen(false);
229
+ setData(null);
230
+ setSlug(null);
231
+ if (result.warning) {
232
+ setError(
233
+ `Execução excluída, mas o worktree não foi removido: ${result.warning}`,
234
+ );
235
+ } else {
236
+ setError("");
237
+ }
238
+ api<{ runs: RunSummary[] }>("/api/runs")
239
+ .then(({ runs: list }) => {
240
+ setRuns(list);
241
+ setSlug(list[0]?.slug ?? null);
242
+ })
243
+ .catch((err) => setError(String(err)));
244
+ }, []);
217
245
 
218
246
  return (
219
247
  <div class="app">
@@ -231,11 +259,12 @@ export function App() {
231
259
  type="button"
232
260
  key={item.id}
233
261
  title={item.hint}
262
+ aria-label={item.label}
234
263
  class={`seg-btn${tab === item.id ? " active" : ""}`}
235
264
  onClick={() => setTab(item.id)}
236
265
  >
237
266
  <TabIcon id={item.id} />
238
- {item.label}
267
+ <span class="seg-label">{item.label}</span>
239
268
  </button>
240
269
  ))}
241
270
  </div>
@@ -290,7 +319,7 @@ export function App() {
290
319
  branch={branch}
291
320
  findings={data.findings}
292
321
  summary={summary}
293
- onOpenArtifacts={() => setTab("artifacts")}
322
+ onRequestDelete={() => setDeleteOpen(true)}
294
323
  />
295
324
  )}
296
325
  {slug && tab === "findings" && data && (
@@ -312,13 +341,23 @@ export function App() {
312
341
  {!slug && !error && <p class="empty">Nenhuma execução para triar.</p>}
313
342
  </div>
314
343
  {slug && (
315
- <SubmitModal
316
- slug={slug}
317
- branch={branch}
318
- open={submitOpen}
319
- onClose={() => setSubmitOpen(false)}
320
- onSubmitted={afterSubmit}
321
- />
344
+ <>
345
+ <SubmitModal
346
+ slug={slug}
347
+ branch={branch}
348
+ open={submitOpen}
349
+ onClose={() => setSubmitOpen(false)}
350
+ onSubmitted={afterSubmit}
351
+ />
352
+ <DeleteRunModal
353
+ slug={slug}
354
+ branch={branch}
355
+ worktreePath={selectedRun?.worktreePath}
356
+ open={deleteOpen}
357
+ onClose={() => setDeleteOpen(false)}
358
+ onDeleted={afterDelete}
359
+ />
360
+ </>
322
361
  )}
323
362
  </main>
324
363
  </div>
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Modal "Excluir execução": confirma a remoção definitiva da run.
3
+ *
4
+ * Apaga a review, findings, histórico de triagem, artefatos e o staging local.
5
+ * Quando a run tem worktree, oferece removê-lo junto (alterações não commitadas
6
+ * nele são descartadas).
7
+ */
8
+
9
+ import { useEffect, useState } from "preact/hooks";
10
+ import { Spinner, Trash } from "./icons";
11
+ import { api } from "./utils/api";
12
+ import type { DeleteRunResponse } from "./utils/types";
13
+
14
+ export function DeleteRunModal({
15
+ slug,
16
+ branch,
17
+ worktreePath,
18
+ open,
19
+ onClose,
20
+ onDeleted,
21
+ }: {
22
+ slug: string;
23
+ branch: string;
24
+ worktreePath?: string;
25
+ open: boolean;
26
+ onClose: () => void;
27
+ onDeleted: (result: DeleteRunResponse) => void;
28
+ }) {
29
+ const [removeWorktree, setRemoveWorktree] = useState(Boolean(worktreePath));
30
+ const [deleting, setDeleting] = useState(false);
31
+ const [error, setError] = useState("");
32
+
33
+ useEffect(() => {
34
+ if (!open) return;
35
+ setRemoveWorktree(Boolean(worktreePath));
36
+ setDeleting(false);
37
+ setError("");
38
+ }, [open, worktreePath]);
39
+
40
+ useEffect(() => {
41
+ if (!open) return;
42
+ const onKey = (event: KeyboardEvent) => {
43
+ if (event.key === "Escape" && !deleting) onClose();
44
+ };
45
+ document.addEventListener("keydown", onKey);
46
+ return () => document.removeEventListener("keydown", onKey);
47
+ }, [open, deleting, onClose]);
48
+
49
+ if (!open) return null;
50
+
51
+ const confirm = async () => {
52
+ setDeleting(true);
53
+ setError("");
54
+ try {
55
+ const suffix = removeWorktree && worktreePath ? "?worktree=1" : "";
56
+ const result = await api<DeleteRunResponse>(`/api/runs/${slug}${suffix}`, {
57
+ method: "DELETE",
58
+ });
59
+ if (!result.deleted) throw new Error("A execução não foi encontrada.");
60
+ onDeleted(result);
61
+ } catch (err) {
62
+ setError(err instanceof Error ? err.message : String(err));
63
+ } finally {
64
+ setDeleting(false);
65
+ }
66
+ };
67
+
68
+ return (
69
+ <div class="modal-layer">
70
+ <button
71
+ type="button"
72
+ class="modal-backdrop"
73
+ aria-label="Fechar"
74
+ title="Fechar"
75
+ onClick={() => {
76
+ if (!deleting) onClose();
77
+ }}
78
+ />
79
+ <div
80
+ class="modal"
81
+ role="dialog"
82
+ aria-modal="true"
83
+ aria-label="Excluir execução"
84
+ >
85
+ <header class="modal-head">
86
+ <div>
87
+ <h2 class="modal-title">Excluir execução</h2>
88
+ <p class="modal-sub">
89
+ Esta ação é permanente e apaga a revisão de{" "}
90
+ <code>{branch}</code>.
91
+ </p>
92
+ </div>
93
+ <button
94
+ type="button"
95
+ class="modal-close"
96
+ title="Fechar"
97
+ disabled={deleting}
98
+ onClick={onClose}
99
+ >
100
+ ×
101
+ </button>
102
+ </header>
103
+
104
+ <div class="modal-body">
105
+ <p class="modal-warn">
106
+ Serão removidos os findings, o histórico de triagem, todos os
107
+ artefatos (briefs, classificação, manifesto, relatório, verify) e
108
+ o staging local desta run. O envio já feito ao GitHub não é
109
+ desfeito.
110
+ </p>
111
+
112
+ {worktreePath && (
113
+ <label class="modal-check">
114
+ <input
115
+ type="checkbox"
116
+ checked={removeWorktree}
117
+ disabled={deleting}
118
+ onChange={(event) =>
119
+ setRemoveWorktree(event.currentTarget.checked)
120
+ }
121
+ />
122
+ <span>
123
+ Excluir também o worktree desta execução. Alterações não
124
+ commitadas nele serão perdidas.
125
+ <br />
126
+ <code>{worktreePath}</code>
127
+ </span>
128
+ </label>
129
+ )}
130
+
131
+ {error && <p class="error">{error}</p>}
132
+ </div>
133
+
134
+ <footer class="modal-foot">
135
+ <span class="submit-label">Run: {branch}</span>
136
+ <div class="submit-actions">
137
+ <button
138
+ type="button"
139
+ class="btn"
140
+ disabled={deleting}
141
+ onClick={onClose}
142
+ >
143
+ Cancelar
144
+ </button>
145
+ <button
146
+ type="button"
147
+ class="btn danger"
148
+ disabled={deleting}
149
+ onClick={() => void confirm()}
150
+ >
151
+ {deleting ? (
152
+ <Spinner size={15} class="icon spin" />
153
+ ) : (
154
+ <Trash size={15} />
155
+ )}
156
+ {deleting ? "Excluindo…" : "Excluir execução"}
157
+ </button>
158
+ </div>
159
+ </footer>
160
+ </div>
161
+ </div>
162
+ );
163
+ }
@@ -0,0 +1,296 @@
1
+ /**
2
+ * Seção `PRs` da sidebar: lista as PRs abertas via `gh`, abre a run local da
3
+ * branch, cria worktree dedicado e faz checkout quando a árvore está limpa.
4
+ *
5
+ * A seção fica montada mesmo quando outra está ativa — o fetch só ocorre na
6
+ * primeira ativação (`active`), preservando filtros e estados ao alternar.
7
+ */
8
+
9
+ import { useCallback, useEffect, useRef, useState } from "preact/hooks";
10
+ import { BranchIcon, Clock, GitHub, GitPullRequest, Refresh, Spinner } from "./icons";
11
+ import type { SidebarSectionContext } from "./sidebar-sections";
12
+ import { api } from "./utils/api";
13
+ import { formatRunTime } from "./utils/format";
14
+ import type {
15
+ CheckoutResponse,
16
+ PrListItem,
17
+ PrListResponse,
18
+ WorktreeState,
19
+ } from "./utils/types";
20
+
21
+ const REVIEW_LABELS: Record<string, string> = {
22
+ APPROVED: "aprovada",
23
+ CHANGES_REQUESTED: "mudanças",
24
+ REVIEW_REQUIRED: "revisão pendente",
25
+ };
26
+
27
+ function PrItem({
28
+ pr,
29
+ currentBranch,
30
+ worktree,
31
+ busy,
32
+ confirming,
33
+ onConfirm,
34
+ onCancel,
35
+ onCheckout,
36
+ onSelectRun,
37
+ }: {
38
+ pr: PrListItem;
39
+ currentBranch: string | null;
40
+ worktree: WorktreeState;
41
+ busy: boolean;
42
+ confirming: boolean;
43
+ onConfirm: (number: number) => void;
44
+ onCancel: () => void;
45
+ onCheckout: (pr: PrListItem) => void;
46
+ onSelectRun: (slug: string) => void;
47
+ }) {
48
+ const isCurrent = currentBranch === pr.headRefName;
49
+ const runSlug = pr.runSlug;
50
+ const review = pr.reviewDecision ? REVIEW_LABELS[pr.reviewDecision] : undefined;
51
+ // O título da PR às vezes repete o nome da branch; nesse caso só o
52
+ // indicador `head → base` fica visível.
53
+ const showTitle = pr.title.trim() !== pr.headRefName.trim();
54
+ return (
55
+ <article class={`pr${isCurrent ? " current" : ""}`}>
56
+ <div class="pr-head">
57
+ <GitPullRequest size={14} class="pr-icon" />
58
+ <span class="pr-number">#{pr.number}</span>
59
+ {pr.isDraft && <span class="tag">draft</span>}
60
+ {isCurrent && <span class="pr-current">atual</span>}
61
+ {pr.worktreePath && (
62
+ <span
63
+ class="pr-worktree-badge"
64
+ title={`Worktree: ${pr.worktreePath}`}
65
+ >
66
+ <BranchIcon size={10} />
67
+ worktree
68
+ </span>
69
+ )}
70
+ <span class="spacer" />
71
+ {pr.updatedAt && (
72
+ <span class="run-when">
73
+ <Clock size={12} />
74
+ {formatRunTime(pr.updatedAt)}
75
+ </span>
76
+ )}
77
+ </div>
78
+ {showTitle && <p class="pr-title">{pr.title}</p>}
79
+ <p class="pr-meta">
80
+ <span class="pr-branch">{pr.headRefName}</span>
81
+ <span class="pr-arrow">→</span>
82
+ <span>{pr.baseRefName || "—"}</span>
83
+ <span>· {pr.author}</span>
84
+ </p>
85
+ {review && <p class={`pr-review pr-review-${pr.reviewDecision}`}>{review}</p>}
86
+ <div class="pr-actions">
87
+ {runSlug && (
88
+ <button
89
+ type="button"
90
+ class="pr-btn"
91
+ title="Abrir a run local desta branch"
92
+ onClick={() => onSelectRun(runSlug)}
93
+ >
94
+ Abrir run
95
+ </button>
96
+ )}
97
+ {pr.url && (
98
+ <a
99
+ class="pr-btn"
100
+ href={pr.url}
101
+ target="_blank"
102
+ rel="noreferrer"
103
+ title="Abrir a PR no GitHub"
104
+ >
105
+ <GitHub size={12} /> GitHub
106
+ </a>
107
+ )}
108
+ {confirming ? (
109
+ <>
110
+ <button
111
+ type="button"
112
+ class="pr-btn danger"
113
+ disabled={busy}
114
+ title="Confirmar checkout da PR"
115
+ onClick={() => onCheckout(pr)}
116
+ >
117
+ {busy ? <Spinner size={12} class="icon spin" /> : "Confirmar"}
118
+ </button>
119
+ <button
120
+ type="button"
121
+ class="pr-btn"
122
+ disabled={busy}
123
+ onClick={onCancel}
124
+ >
125
+ Cancelar
126
+ </button>
127
+ </>
128
+ ) : (
129
+ <button
130
+ type="button"
131
+ class="pr-btn"
132
+ disabled={!worktree.clean || busy}
133
+ title={
134
+ worktree.clean
135
+ ? `Fazer checkout da branch ${pr.headRefName}`
136
+ : "Há alterações não commitadas — faça commit ou stash antes"
137
+ }
138
+ onClick={() => onConfirm(pr.number)}
139
+ >
140
+ Checkout
141
+ </button>
142
+ )}
143
+ </div>
144
+ </article>
145
+ );
146
+ }
147
+
148
+ export function PrsSection({ active, onSelect, onRefreshRuns }: SidebarSectionContext) {
149
+ const [data, setData] = useState<PrListResponse | null>(null);
150
+ const [loading, setLoading] = useState(false);
151
+ const [error, setError] = useState("");
152
+ const [notice, setNotice] = useState("");
153
+ const [busy, setBusy] = useState<number | null>(null);
154
+ const [confirming, setConfirming] = useState<number | null>(null);
155
+ const requested = useRef(false);
156
+
157
+ const loadPrs = useCallback(async () => {
158
+ setLoading(true);
159
+ setError("");
160
+ try {
161
+ setData(await api<PrListResponse>("/api/prs"));
162
+ } catch (err) {
163
+ setError(String(err));
164
+ } finally {
165
+ setLoading(false);
166
+ }
167
+ }, []);
168
+
169
+ useEffect(() => {
170
+ if (!active || requested.current) return;
171
+ requested.current = true;
172
+ void loadPrs();
173
+ }, [active, loadPrs]);
174
+
175
+ const checkout = useCallback(
176
+ async (pr: PrListItem) => {
177
+ setBusy(pr.number);
178
+ setError("");
179
+ setNotice("");
180
+ try {
181
+ const result = await api<CheckoutResponse>("/api/prs/checkout", {
182
+ method: "POST",
183
+ headers: { "content-type": "application/json" },
184
+ body: JSON.stringify({ number: pr.number }),
185
+ });
186
+ if (!result.ok) {
187
+ if (result.reason === "dirty") {
188
+ setData((current) =>
189
+ current
190
+ ? {
191
+ ...current,
192
+ worktree: {
193
+ ...current.worktree,
194
+ clean: false,
195
+ changes:
196
+ result.changes?.length ??
197
+ current.worktree.changes,
198
+ sample:
199
+ result.changes ?? current.worktree.sample,
200
+ },
201
+ }
202
+ : current,
203
+ );
204
+ }
205
+ setError(
206
+ result.reason === "dirty"
207
+ ? `Checkout bloqueado: ${result.detail}`
208
+ : result.detail,
209
+ );
210
+ return;
211
+ }
212
+ setConfirming(null);
213
+ setNotice(
214
+ `Checkout de #${pr.number} concluído${
215
+ result.branch ? ` — ${result.branch}` : ""
216
+ }.`,
217
+ );
218
+ await loadPrs();
219
+ onRefreshRuns();
220
+ if (pr.runSlug) onSelect(pr.runSlug);
221
+ } catch (err) {
222
+ setError(String(err));
223
+ } finally {
224
+ setBusy(null);
225
+ }
226
+ },
227
+ [loadPrs, onRefreshRuns, onSelect],
228
+ );
229
+
230
+ const openCount = data?.available ? data.prs.length : null;
231
+
232
+ return (
233
+ <>
234
+ <div class="pr-toolbar">
235
+ <p class="section-label">
236
+ {openCount === null
237
+ ? "PRs abertas"
238
+ : `${openCount} ${openCount === 1 ? "aberta" : "abertas"}`}
239
+ </p>
240
+ <button
241
+ type="button"
242
+ class="icon-btn"
243
+ title="Atualizar lista de PRs"
244
+ disabled={loading}
245
+ onClick={() => void loadPrs()}
246
+ >
247
+ <Refresh size={13} class={loading ? "icon spin" : undefined} />
248
+ </button>
249
+ </div>
250
+
251
+ {error && <p class="error">{error}</p>}
252
+ {notice && <p class="pr-notice">{notice}</p>}
253
+ {data && !data.available && (
254
+ <p class="empty">
255
+ GitHub indisponível
256
+ {data.reason ? `: ${data.reason}` : "."}
257
+ </p>
258
+ )}
259
+ {data && !data.worktree.available && (
260
+ <p class="empty">Não foi possível ler o estado do git.</p>
261
+ )}
262
+ {data?.worktree.available && !data.worktree.clean && (
263
+ <p class="pr-warning">
264
+ {data.worktree.changes} alteração(ões) não commitada(s) — checkout
265
+ bloqueado até limpar a árvore.
266
+ </p>
267
+ )}
268
+ {data?.currentBranch && (
269
+ <p class="pr-current-branch">
270
+ Branch atual: <code>{data.currentBranch}</code>
271
+ </p>
272
+ )}
273
+
274
+ <nav class="prs" aria-label="Pull requests abertas">
275
+ {loading && !data && <p class="empty">Carregando PRs…</p>}
276
+ {data?.available && data.prs.length === 0 && (
277
+ <p class="empty">Nenhuma PR aberta.</p>
278
+ )}
279
+ {data?.prs.map((pr) => (
280
+ <PrItem
281
+ key={pr.number}
282
+ pr={pr}
283
+ currentBranch={data.currentBranch}
284
+ worktree={data.worktree}
285
+ busy={busy !== null}
286
+ confirming={confirming === pr.number}
287
+ onConfirm={setConfirming}
288
+ onCancel={() => setConfirming(null)}
289
+ onCheckout={checkout}
290
+ onSelectRun={onSelect}
291
+ />
292
+ ))}
293
+ </nav>
294
+ </>
295
+ );
296
+ }
@@ -1,21 +1,19 @@
1
1
  /**
2
2
  * Aba "Resumo" (inicial): visão principal da revisão.
3
3
  *
4
- * O texto narrativo vem do artefato `review-resume.md` (Resumo de negócio); os
5
- * números viram cards a partir do `classification.json`/`manifest.json` e o
6
- * card de domínios resume os achados por severidade em cada watchdog — assim a
7
- * UI mostra os mesmos dados do resumo de forma mais organizada, sem duplicar o
8
- * texto.
4
+ * O texto narrativo vem do artefato `review-resume.md` e é exibido por inteiro
5
+ * (resumo de negócio + pontos técnicos); os números viram cards a partir do
6
+ * `classification.json`/`manifest.json` e o card de domínios resume os achados
7
+ * por severidade em cada watchdog.
9
8
  */
10
9
 
11
10
  import type { ComponentChildren } from "preact";
12
11
  import { useEffect, useMemo, useState } from "preact/hooks";
13
- import { ExternalLink, Layers } from "./icons";
14
- import { MarkdownBlocks } from "./MarkdownText";
12
+ import { ExternalLink, Trash } from "./icons";
13
+ import { MarkdownText } from "./MarkdownText";
15
14
  import { api } from "./utils/api";
16
15
  import { SEVERITY_LABELS, SEVERITY_ORDER } from "./utils/constants";
17
16
  import { formatRunTime } from "./utils/format";
18
- import { sectionBlocks } from "./utils/markdown";
19
17
  import type {
20
18
  ClassificationArtifact,
21
19
  Finding,
@@ -79,13 +77,13 @@ export function ResumePanel({
79
77
  branch,
80
78
  findings,
81
79
  summary,
82
- onOpenArtifacts,
80
+ onRequestDelete,
83
81
  }: {
84
82
  slug: string;
85
83
  branch: string;
86
84
  findings: Finding[];
87
85
  summary: Summary;
88
- onOpenArtifacts: () => void;
86
+ onRequestDelete: () => void;
89
87
  }) {
90
88
  const [resume, setResume] = useState("");
91
89
  const [detail, setDetail] = useState<RunDetail | null>(null);
@@ -126,7 +124,6 @@ export function ResumePanel({
126
124
  }, [slug]);
127
125
 
128
126
  const manifest: RunManifest | null = detail?.manifest ?? null;
129
- const business = useMemo(() => sectionBlocks(resume, "Resumo de negócio"), [resume]);
130
127
  const status =
131
128
  summary.bySeverity.blocking > 0
132
129
  ? STATUS_VIEW.blocking
@@ -241,12 +238,12 @@ export function ResumePanel({
241
238
  <span class={`resume-status ${status.tone}`}>{status.label}</span>
242
239
  <button
243
240
  type="button"
244
- class="btn"
245
- title="Ver os artefatos brutos da revisão"
246
- onClick={onOpenArtifacts}
241
+ class="btn danger"
242
+ title="Excluir esta execução, seus findings e artefatos locais"
243
+ onClick={onRequestDelete}
247
244
  >
248
- <Layers size={14} />
249
- Ver artefatos
245
+ <Trash size={14} />
246
+ Excluir
250
247
  </button>
251
248
  </div>
252
249
  </section>
@@ -295,16 +292,16 @@ export function ResumePanel({
295
292
 
296
293
  <div class="resume-grid">
297
294
  <section class="resume-card">
298
- <h3 class="resume-card-title">Resumo de negócio</h3>
295
+ <h3 class="resume-card-title">Resumo da revisão</h3>
299
296
  {loading ? (
300
297
  <p class="empty">Carregando resumo…</p>
301
- ) : business.length > 0 ? (
298
+ ) : resume.trim().length > 0 ? (
302
299
  <div class="resume-narrative">
303
- <MarkdownBlocks blocks={business} />
300
+ <MarkdownText text={resume} />
304
301
  </div>
305
302
  ) : (
306
303
  <p class="empty">
307
- O resumo narrativo ainda não foi gerado para esta run. Rode o{" "}
304
+ O resumo ainda não foi gerado para esta run. Rode o{" "}
308
305
  <code>--finalize</code> para criar{" "}
309
306
  <code>review-resume.md</code>.
310
307
  </p>