@payconductor/houndsman 0.8.0 → 0.11.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 (55) hide show
  1. package/package.json +1 -1
  2. package/src/cli/index.ts +9 -1
  3. package/src/core/ai/ai-client.ts +21 -0
  4. package/src/core/ai/ai-sdk-model-provider.ts +43 -18
  5. package/src/core/ai/features/technical-summarizer.ts +43 -0
  6. package/src/core/ai/index.ts +4 -1
  7. package/src/core/ai/prompts/base.ts +7 -0
  8. package/src/core/ai/prompts/context.ts +5 -4
  9. package/src/core/ai/prompts/technical.ts +31 -0
  10. package/src/core/ai/provider-capabilities.ts +45 -0
  11. package/src/core/ai/schemas.ts +19 -3
  12. package/src/core/ai/types.ts +3 -1
  13. package/src/core/brief.ts +25 -4
  14. package/src/core/commands/scaffold.ts +5 -0
  15. package/src/core/config-loader.ts +18 -4
  16. package/src/core/config.ts +17 -1
  17. package/src/core/engine/review-workspace.ts +6 -2
  18. package/src/core/git-runner.ts +8 -1
  19. package/src/core/orchestrator.ts +114 -2
  20. package/src/core/paths.ts +3 -0
  21. package/src/core/pr-context.ts +11 -6
  22. package/src/core/project-config.ts +9 -1
  23. package/src/core/renderers/resume-renderer.ts +17 -34
  24. package/src/core/report-builder.ts +6 -6
  25. package/src/core/services/worktree-manager.ts +90 -7
  26. package/src/core/storage/types.ts +1 -0
  27. package/src/core/technical-summary.ts +398 -0
  28. package/src/harness/agent/agent-factory.ts +1 -0
  29. package/src/harness/agent/review-agent.ts +11 -1
  30. package/src/harness/config/harness-config.ts +6 -1
  31. package/src/index.ts +2 -0
  32. package/src/server/client/App.tsx +2 -1
  33. package/src/server/client/PrsSection.tsx +296 -0
  34. package/src/server/client/ResumePanel.tsx +10 -13
  35. package/src/server/client/RunsSection.tsx +73 -0
  36. package/src/server/client/Sidebar.tsx +171 -413
  37. package/src/server/client/sidebar-sections.tsx +73 -0
  38. package/src/server/client/utils/constants.ts +7 -2
  39. package/src/server/client/utils/sidebar-prefs.ts +64 -0
  40. package/src/server/client/utils/types.ts +1 -0
  41. package/src/server/index.ts +23 -409
  42. package/src/server/styles.css +454 -0
  43. package/src/tests/__snapshots__/baseline.test.ts.snap +5 -0
  44. package/src/tests/ai-core.test.ts +71 -0
  45. package/src/tests/ai-features.test.ts +47 -0
  46. package/src/tests/baseline.test.ts +4 -1
  47. package/src/tests/harness-agent.test.ts +19 -0
  48. package/src/tests/review-resume.test.ts +36 -8
  49. package/src/tests/review-scope.test.ts +67 -0
  50. package/src/tests/review-scripts.test.ts +2 -2
  51. package/src/tests/server.test.ts +16 -1
  52. package/src/tests/sidebar-sections.test.ts +94 -0
  53. package/src/tests/technical-summary.test.ts +176 -0
  54. package/src/tests/worktree.test.ts +110 -5
  55. package/assets/logo-legacy.png +0 -0
@@ -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, Trash } 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,
@@ -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
@@ -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>
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Seção `Execuções` da sidebar: lista das runs revisadas, com status, data e
3
+ * contagem de findings. Selecionar uma run troca apenas o conteúdo principal.
4
+ */
5
+
6
+ import { BranchIcon, Clock, Sparkles } from "./icons";
7
+ import type { SidebarSectionContext } from "./sidebar-sections";
8
+ import { describeRunStatus } from "./utils/constants";
9
+ import { formatRunTime } from "./utils/format";
10
+ import type { RunSummary } from "./utils/types";
11
+
12
+ function RunItem({
13
+ run,
14
+ active,
15
+ onSelect,
16
+ }: {
17
+ run: RunSummary;
18
+ active: boolean;
19
+ onSelect: (slug: string) => void;
20
+ }) {
21
+ const status = describeRunStatus(run);
22
+ const fixReady = status?.kind === "fix_done";
23
+ return (
24
+ <button
25
+ type="button"
26
+ title={`Abrir a run da branch ${run.branch}`}
27
+ class={`run${active ? " active" : ""}`}
28
+ onClick={() => onSelect(run.slug)}
29
+ >
30
+ <span class="run-top">
31
+ <BranchIcon size={14} class="run-icon" />
32
+ <span class="run-branch">{run.branch}</span>
33
+ </span>
34
+ <span class="run-meta">
35
+ <span class="run-when">
36
+ <Clock size={12} />
37
+ {formatRunTime(run.updatedAt) || "sem data"}
38
+ </span>
39
+ <span
40
+ class="run-count"
41
+ title={`${run.summary.present} de ${run.summary.total} findings presentes`}
42
+ >
43
+ {run.summary.present}/{run.summary.total}
44
+ </span>
45
+ </span>
46
+ {fixReady ? (
47
+ <span class="run-badge">
48
+ <Sparkles size={12} /> Correção pronta
49
+ </span>
50
+ ) : (
51
+ status && (
52
+ <span class={`run-status status-${status.kind}`}>{status.label}</span>
53
+ )
54
+ )}
55
+ </button>
56
+ );
57
+ }
58
+
59
+ export function RunsSection({ runs, slug, onSelect }: SidebarSectionContext) {
60
+ return (
61
+ <nav class="runs" aria-label="Execuções">
62
+ {runs.length === 0 && <p class="empty">Nenhuma run encontrada.</p>}
63
+ {runs.map((run) => (
64
+ <RunItem
65
+ key={run.slug}
66
+ run={run}
67
+ active={run.slug === slug}
68
+ onSelect={onSelect}
69
+ />
70
+ ))}
71
+ </nav>
72
+ );
73
+ }