@payconductor/houndsman 0.8.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 (51) hide show
  1. package/package.json +1 -1
  2. package/src/core/ai/ai-client.ts +21 -0
  3. package/src/core/ai/ai-sdk-model-provider.ts +43 -18
  4. package/src/core/ai/features/technical-summarizer.ts +43 -0
  5. package/src/core/ai/index.ts +4 -1
  6. package/src/core/ai/prompts/context.ts +3 -3
  7. package/src/core/ai/prompts/technical.ts +30 -0
  8. package/src/core/ai/provider-capabilities.ts +45 -0
  9. package/src/core/ai/schemas.ts +19 -3
  10. package/src/core/ai/types.ts +3 -1
  11. package/src/core/brief.ts +25 -4
  12. package/src/core/commands/scaffold.ts +5 -0
  13. package/src/core/config.ts +17 -1
  14. package/src/core/git-runner.ts +8 -1
  15. package/src/core/orchestrator.ts +114 -2
  16. package/src/core/paths.ts +3 -0
  17. package/src/core/pr-context.ts +11 -6
  18. package/src/core/project-config.ts +9 -1
  19. package/src/core/renderers/resume-renderer.ts +17 -34
  20. package/src/core/report-builder.ts +6 -6
  21. package/src/core/services/worktree-manager.ts +29 -5
  22. package/src/core/storage/types.ts +1 -0
  23. package/src/core/technical-summary.ts +398 -0
  24. package/src/harness/agent/agent-factory.ts +1 -0
  25. package/src/harness/agent/review-agent.ts +11 -1
  26. package/src/harness/config/harness-config.ts +6 -1
  27. package/src/index.ts +2 -0
  28. package/src/server/client/App.tsx +2 -1
  29. package/src/server/client/PrsSection.tsx +296 -0
  30. package/src/server/client/ResumePanel.tsx +10 -13
  31. package/src/server/client/RunsSection.tsx +73 -0
  32. package/src/server/client/Sidebar.tsx +171 -413
  33. package/src/server/client/sidebar-sections.tsx +73 -0
  34. package/src/server/client/utils/constants.ts +7 -2
  35. package/src/server/client/utils/sidebar-prefs.ts +64 -0
  36. package/src/server/client/utils/types.ts +1 -0
  37. package/src/server/index.ts +23 -409
  38. package/src/server/styles.css +454 -0
  39. package/src/tests/__snapshots__/baseline.test.ts.snap +5 -0
  40. package/src/tests/ai-core.test.ts +71 -0
  41. package/src/tests/ai-features.test.ts +36 -0
  42. package/src/tests/baseline.test.ts +4 -1
  43. package/src/tests/harness-agent.test.ts +19 -0
  44. package/src/tests/review-resume.test.ts +36 -8
  45. package/src/tests/review-scope.test.ts +67 -0
  46. package/src/tests/review-scripts.test.ts +2 -2
  47. package/src/tests/server.test.ts +16 -1
  48. package/src/tests/sidebar-sections.test.ts +94 -0
  49. package/src/tests/technical-summary.test.ts +176 -0
  50. package/src/tests/worktree.test.ts +39 -4
  51. package/assets/logo-legacy.png +0 -0
@@ -0,0 +1,454 @@
1
+ /*
2
+ * Estilos da UI de triagem (`houndsman server`) — tema escuro, primária âmbar.
3
+ * Servido em `/assets/styles.css`; o shell (`index.ts`) só aponta o <link>.
4
+ */
5
+
6
+ @import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
7
+
8
+ :root {
9
+ color-scheme: dark;
10
+ --bg:#0a0a0c; --surface:#101014; --surface-2:#17171c; --card:#131318;
11
+ --border:rgba(255,255,255,.07); --border-strong:rgba(255,255,255,.14);
12
+ --fg:#ece9e4; --muted:#9d978e; --muted-2:#6d6862;
13
+ --accent:#f0a52c; --accent-strong:#ffc247; --accent-soft:rgba(240,165,44,.07); --accent-line:rgba(240,165,44,.25);
14
+ --blocking:#f2595e; --attention:#eda13a; --suggestion:#5aa4f0; --nit:#8d8a85; --success:#45c07a;
15
+ --radius:16px; --radius-lg:18px;
16
+ --font-sans:"Poppins","Space Grotesk","Geist Sans",ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;
17
+ --font-mono:"JetBrains Mono",ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
18
+ }
19
+ * { box-sizing:border-box; }
20
+ html, body { height:100%; }
21
+ body { margin:0; font-size:14px; background:var(--bg); color:var(--fg); font-family:var(--font-sans); -webkit-font-smoothing:antialiased; }
22
+ body::before { content:""; position:fixed; inset:0; pointer-events:none; z-index:0;
23
+ background:radial-gradient(820px 420px at 8% -12%, rgba(240,165,44,.08), transparent 62%); }
24
+ code, pre, .mono { font-family:var(--font-mono); tab-size:4; }
25
+ ::-webkit-scrollbar { width:10px; height:10px; }
26
+ ::-webkit-scrollbar-thumb { background:rgba(255,255,255,.1); border:2px solid transparent; border-radius:999px; background-clip:padding-box; }
27
+ ::-webkit-scrollbar-thumb:hover { background:rgba(255,255,255,.18); background-clip:padding-box; }
28
+ .app { position:relative; z-index:1; display:flex; height:100vh; overflow:hidden; }
29
+ /* Sidebar = rail fixa de seções + painel expansível (largura em --side-width). */
30
+ .sidebar { position:relative; flex:0 0 auto; display:flex; height:100%; overflow:hidden;
31
+ --rail-w:52px; --side-width:360px;
32
+ width:calc(var(--rail-w) + var(--side-width));
33
+ background:var(--surface); border-right:1px solid var(--border); }
34
+ .sidebar.collapsed { width:var(--rail-w); }
35
+ .sidebar.collapsed .side-panel, .sidebar.collapsed .side-resizer { display:none; }
36
+ .side-rail { flex:0 0 var(--rail-w); width:var(--rail-w); display:flex; flex-direction:column; align-items:center; gap:4px;
37
+ padding:10px 0; background:var(--surface-2); border-right:1px solid var(--border);
38
+ overflow-x:hidden; overflow-y:auto; }
39
+ .rail-btn { position:relative; flex:none; display:grid; place-items:center; width:40px; height:40px; padding:0;
40
+ color:var(--muted); background:transparent; border:1px solid transparent; border-radius:11px; cursor:pointer;
41
+ transition:color .15s ease, background .15s ease, border-color .15s ease; }
42
+ .rail-btn:hover { color:var(--fg); background:rgba(255,255,255,.05); }
43
+ .rail-btn:focus-visible { outline:none; color:var(--fg); border-color:var(--accent-line); }
44
+ .rail-btn.active { color:var(--accent); background:var(--accent-soft); }
45
+ .rail-btn.active::before { content:""; position:absolute; left:-6px; top:9px; bottom:9px; width:2px; border-radius:2px; background:var(--accent); }
46
+ .rail-badge { position:absolute; top:2px; right:0; min-width:16px; padding:0 4px; font-family:var(--font-mono);
47
+ font-size:9px; font-weight:600; line-height:15px; text-align:center; color:var(--muted); background:var(--surface);
48
+ border:1px solid var(--border-strong); border-radius:999px; }
49
+ .rail-btn.active .rail-badge { color:var(--accent); border-color:var(--accent-line); }
50
+ .rail-spacer { flex:1; }
51
+ .rail-toggle { color:var(--muted-2); }
52
+ .rail-toggle .icon.flip { transform:rotate(180deg); }
53
+ .side-panel { flex:1; min-width:0; display:flex; flex-direction:column; overflow:hidden; }
54
+ .side-head { display:flex; flex-direction:column; gap:12px; padding:16px 14px 12px; border-bottom:1px solid var(--border); }
55
+ .side-title { min-width:0; }
56
+ .side-title h2 { margin:0 0 3px; font-size:13px; font-weight:700; letter-spacing:.01em; }
57
+ .side-hint { margin:0; color:var(--muted-2); font-size:11px; line-height:1.45; }
58
+ .side-body { flex:1; min-height:0; display:flex; flex-direction:column; gap:14px; padding:14px;
59
+ overflow-x:hidden; overflow-y:auto; }
60
+ .side-section { display:flex; flex-direction:column; gap:14px; min-width:0; }
61
+ .side-section[hidden] { display:none; }
62
+ .side-resizer { position:absolute; top:0; right:-3px; bottom:0; z-index:20; width:7px; background:transparent;
63
+ cursor:col-resize; touch-action:none; }
64
+ .side-resizer:hover, .side-resizer:focus-visible { background:var(--accent-line); outline:none; }
65
+ body.resizing-sidebar { cursor:col-resize; user-select:none; }
66
+ .brand { display:flex; align-items:center; gap:12px; }
67
+ .brand .logo { width:42px; height:42px; object-fit:contain; border-radius:12px; background:var(--surface-2); border:1px solid var(--border); }
68
+ .sidebar h1 { font-size:15.5px; font-weight:700; letter-spacing:-.01em; margin:0; }
69
+ .sub { color:var(--muted); font-size:11.5px; margin:0; }
70
+ .section-label { margin:4px 0 0; font-size:10px; font-weight:700; letter-spacing:.16em; text-transform:uppercase; color:var(--muted-2); }
71
+ .runs { display:flex; flex-direction:column; gap:6px; }
72
+ .run { display:flex; flex-direction:column; gap:8px; text-align:left; cursor:pointer; font:inherit; color:inherit; padding:11px 13px;
73
+ background:transparent; border:1px solid transparent; border-radius:var(--radius); transition:background .15s ease, border-color .15s ease; }
74
+ .run:hover { background:var(--surface-2); border-color:var(--border); }
75
+ .run.active { background:var(--accent-soft); border-color:var(--accent-line); }
76
+ .run-top { display:flex; align-items:flex-start; gap:8px; }
77
+ .run-icon { flex:none; margin-top:2px; color:var(--muted-2); }
78
+ .run.active .run-icon { color:var(--accent); }
79
+ .run-branch { font-size:12.5px; font-weight:600; line-height:1.35; word-break:break-word; }
80
+ .run-meta { display:flex; align-items:center; gap:10px; font-family:var(--font-mono); font-size:10.5px; color:var(--muted); }
81
+ .run-when { display:inline-flex; align-items:center; gap:6px; }
82
+ .run-count { margin-left:auto; color:var(--muted-2); }
83
+ .run-badge { align-self:flex-start; display:inline-flex; align-items:center; gap:6px; padding:4px 9px; border-radius:999px;
84
+ font-size:10px; font-weight:700; letter-spacing:.07em; text-transform:uppercase;
85
+ color:var(--success); background:rgba(69,192,122,.12); border:1px solid rgba(69,192,122,.32); }
86
+ .run-status { font-size:10px; font-weight:600; letter-spacing:.07em; text-transform:uppercase; color:var(--muted-2); }
87
+ .status-reviewing, .status-fixing { color:var(--accent); }
88
+ .status-review_done { color:var(--suggestion); }
89
+ .status-fix_done { color:var(--success); }
90
+ .status-timeout { color:var(--blocking); }
91
+ .icon-btn { display:inline-grid; place-items:center; width:26px; height:26px; color:var(--muted); background:transparent; border:1px solid var(--border); border-radius:8px; cursor:pointer; text-decoration:none; }
92
+ .icon-btn:hover { color:var(--fg); border-color:var(--border-strong); }
93
+ .icon-btn:disabled { opacity:.5; cursor:not-allowed; }
94
+ .pr-toolbar { display:flex; align-items:center; justify-content:space-between; gap:8px; }
95
+ .pr-toolbar .section-label { margin:0; }
96
+ .prs { display:flex; flex-direction:column; gap:8px; }
97
+ .pr { display:flex; flex-direction:column; gap:7px; padding:11px 12px; background:var(--card); border:1px solid var(--border); border-radius:var(--radius); }
98
+ .pr.current { background:var(--accent-soft); border-color:var(--accent-line); }
99
+ .pr-head { display:flex; align-items:center; gap:7px; font-family:var(--font-mono); font-size:11px; color:var(--muted); }
100
+ .pr-icon { color:var(--muted-2); }
101
+ .pr.current .pr-icon { color:var(--accent); }
102
+ .pr-number { font-weight:600; color:var(--fg); }
103
+ .pr-current { padding:1px 6px; font-size:9px; font-weight:700; letter-spacing:.08em; text-transform:uppercase; color:var(--accent); background:rgba(240,165,44,.12); border-radius:999px; }
104
+ .pr-title { margin:0; font-size:12.5px; font-weight:600; line-height:1.4; word-break:break-word; }
105
+ .pr-meta { display:flex; flex-wrap:wrap; align-items:center; gap:1px 5px; margin:0; font-family:var(--font-mono); font-size:10.5px; color:var(--muted-2); }
106
+ .pr-branch { color:var(--suggestion); }
107
+ .pr-worktree-badge { display:inline-flex; align-items:center; gap:4px; padding:1px 6px; font-family:var(--font-mono); font-size:9px;
108
+ font-weight:700; letter-spacing:.06em; text-transform:uppercase; color:var(--success);
109
+ background:rgba(69,192,122,.12); border:1px solid rgba(69,192,122,.3); border-radius:999px; }
110
+ .pr-review { margin:0; font-size:10.5px; font-weight:600; }
111
+ .pr-review-APPROVED { color:var(--success); }
112
+ .pr-review-CHANGES_REQUESTED { color:var(--blocking); }
113
+ .pr-review-REVIEW_REQUIRED { color:var(--attention); }
114
+ .pr-actions { display:flex; flex-wrap:wrap; gap:6px; margin-top:2px; }
115
+ .pr-btn { display:inline-flex; align-items:center; gap:5px; font:inherit; font-size:10.5px; font-weight:600; cursor:pointer; padding:4px 9px; color:var(--muted);
116
+ background:transparent; border:1px solid var(--border-strong); border-radius:999px; text-decoration:none; }
117
+ .pr-btn:hover { color:var(--fg); border-color:var(--accent-line); }
118
+ .pr-btn:disabled { opacity:.45; cursor:not-allowed; }
119
+ .pr-btn.danger { color:var(--accent); background:var(--accent-soft); border-color:var(--accent-line); }
120
+ .pr-warning { margin:0; padding:8px 10px; font-size:11px; line-height:1.5; color:var(--attention);
121
+ background:rgba(237,161,58,.08); border:1px solid rgba(237,161,58,.24); border-radius:10px; }
122
+ .pr-notice { margin:0; padding:8px 10px; font-size:11px; line-height:1.5; color:var(--success);
123
+ background:rgba(69,192,122,.08); border:1px solid rgba(69,192,122,.24); border-radius:10px; }
124
+ .pr-current-branch { margin:0; font-family:var(--font-mono); font-size:10.5px; color:var(--muted-2); }
125
+ .pr-current-branch code { color:var(--fg); }
126
+ .comments { display:flex; flex-direction:column; gap:18px; }
127
+ .comments-toolbar { display:flex; align-items:center; justify-content:space-between; gap:12px; }
128
+ .comments-counts { display:flex; flex-wrap:wrap; align-items:center; gap:12px; font-size:11.5px; color:var(--muted); }
129
+ .comments-counts b { font-family:var(--font-mono); font-weight:600; color:var(--fg); }
130
+ .comment-group { display:flex; flex-direction:column; gap:10px; }
131
+ .comment-group-title { margin:0; font-size:11px; font-weight:700; letter-spacing:.16em; text-transform:uppercase; color:var(--muted-2); }
132
+ .comment-file-group { display:flex; flex-direction:column; gap:8px; }
133
+ .comment-group-label { display:flex; align-items:center; gap:8px; margin:0; font-family:var(--font-mono); font-size:11.5px; color:var(--suggestion); }
134
+ .comment-thread { display:flex; flex-direction:column; overflow:hidden; background:var(--card); border:1px solid var(--border); border-radius:var(--radius-lg); }
135
+ .comment-thread-head { display:flex; align-items:center; gap:8px; padding:9px 13px; font-family:var(--font-mono); font-size:11px; color:var(--muted);
136
+ background:var(--surface-2); border-bottom:1px solid var(--border); }
137
+ .comment-file { overflow:hidden; color:var(--fg); text-overflow:ellipsis; white-space:nowrap; }
138
+ .pr-comment { display:flex; flex-direction:column; gap:7px; padding:12px 14px; border-top:1px solid var(--border); }
139
+ .comment-thread .pr-comment:first-of-type { border-top:0; }
140
+ .pr-comment.reply { margin-left:26px; background:rgba(255,255,255,.012); border-left:2px solid var(--accent-line); }
141
+ .pr-comment-head { display:flex; align-items:center; gap:9px; font-size:11px; color:var(--muted-2); }
142
+ .pr-comment-author { font-size:12px; font-weight:600; color:var(--fg); }
143
+ .pr-comment-link { display:inline-flex; align-items:center; gap:5px; font-size:11px; color:var(--muted); text-decoration:none; }
144
+ .pr-comment-link:hover { color:var(--accent); }
145
+ .pr-comment-body { font-size:12.5px; line-height:1.6; color:#f0ece6; }
146
+ .pr-comment-body .md-p, .pr-comment-body .md-list { font-size:12.5px; }
147
+ .main { flex:1; min-width:0; display:flex; flex-direction:column; overflow-y:auto; }
148
+ .topbar { position:sticky; top:0; z-index:10; display:flex; align-items:center; justify-content:space-between; gap:16px;
149
+ padding:13px 24px; border-bottom:1px solid var(--border); background:rgba(16,16,20,.74);
150
+ backdrop-filter:blur(16px) saturate(140%); -webkit-backdrop-filter:blur(16px) saturate(140%); }
151
+ .segmented { display:inline-flex; gap:4px; padding:4px; background:var(--surface-2); border:1px solid var(--border); border-radius:999px; }
152
+ .seg-btn { display:inline-flex; align-items:center; gap:8px; font:inherit; font-size:12.5px; font-weight:600; cursor:pointer;
153
+ color:var(--muted); background:transparent; border:1px solid transparent; border-radius:999px; padding:7px 16px;
154
+ transition:color .15s ease, background .15s ease; }
155
+ .seg-btn:hover { color:var(--fg); }
156
+ .seg-btn.active { color:#1b1305; background:linear-gradient(180deg, var(--accent-strong), var(--accent)); box-shadow:0 4px 14px rgba(240,165,44,.24); }
157
+ .topbar-right { display:flex; align-items:center; gap:14px; }
158
+ .auto-refresh { display:inline-flex; align-items:center; gap:8px; white-space:nowrap; font-family:var(--font-mono); font-size:11.5px; color:var(--muted); }
159
+ .dot { width:8px; height:8px; border-radius:50%; background:var(--success); box-shadow:0 0 0 3px rgba(69,192,122,.14); }
160
+ .dot.busy { background:var(--accent); box-shadow:0 0 0 3px rgba(240,165,44,.18); animation:pulse 1.1s ease-in-out infinite; }
161
+ .btn { display:inline-flex; align-items:center; gap:8px; font:inherit; font-size:12.5px; font-weight:500; cursor:pointer; color:var(--fg);
162
+ padding:8px 15px; background:var(--surface-2); border:1px solid var(--border-strong); border-radius:999px;
163
+ transition:background .15s ease, border-color .15s ease, color .15s ease; }
164
+ .btn:hover { background:#202027; border-color:var(--accent-line); }
165
+ .btn:disabled { opacity:.55; cursor:not-allowed; }
166
+ .btn.danger { color:#ffb3b3; background:rgba(255,86,86,.12); border-color:rgba(255,86,86,.4); }
167
+ .btn.danger:hover { background:rgba(255,86,86,.2); border-color:rgba(255,86,86,.62); }
168
+ .modal-check { display:flex; align-items:flex-start; gap:10px; padding:12px 14px; font-size:12.5px; line-height:1.55;
169
+ color:var(--fg); background:var(--surface-2); border:1px solid var(--border); border-radius:12px; cursor:pointer; }
170
+ .modal-check input { margin:3px 0 0; flex:none; accent-color:var(--accent); }
171
+ .modal-check code { font-family:var(--font-mono); font-size:11.5px; color:var(--muted); word-break:break-all; }
172
+ .modal-warn { margin:0; font-size:12px; line-height:1.55; color:var(--muted); }
173
+ .icon.spin { animation:spin .8s linear infinite; }
174
+ .content { width:100%; max-width:1180px; margin:0 auto; padding:22px 24px 64px; }
175
+ .page-header { display:flex; align-items:flex-start; justify-content:space-between; gap:18px; margin-bottom:16px; }
176
+ .page-header-text { min-width:0; }
177
+ .page-header-actions { display:flex; align-items:center; gap:10px; flex:none; }
178
+ .btn.submit-review { color:#1b1305; background:linear-gradient(180deg, var(--accent-strong), var(--accent)); border-color:transparent; box-shadow:0 4px 14px rgba(240,165,44,.22); }
179
+ .btn.submit-review:hover { background:linear-gradient(180deg, var(--accent-strong), var(--accent)); border-color:transparent; filter:brightness(1.07); }
180
+ .page-title { margin:0 0 8px; font-size:26px; font-weight:700; letter-spacing:-.02em; word-break:break-word; }
181
+ .page-sub { margin:0; max-width:78ch; color:var(--muted); font-size:12.5px; line-height:1.55; }
182
+ .page-sub code { color:var(--fg); background:rgba(255,255,255,.06); border-radius:6px; padding:1px 6px; font-size:11.5px; }
183
+ .error { color:#ff8b8f; font-size:12.5px; }
184
+ .empty { color:var(--muted); font-size:13px; }
185
+ .kpis { display:grid; grid-template-columns:repeat(4, minmax(0,1fr)); gap:12px; margin:20px 0 18px; }
186
+ .kpi { display:flex; flex-direction:column; gap:8px; padding:15px 16px; background:var(--card);
187
+ border:1px solid var(--border); border-radius:var(--radius-lg); }
188
+ .kpi-label { font-size:10px; font-weight:700; letter-spacing:.14em; text-transform:uppercase; color:var(--muted-2); }
189
+ .kpi-value { font-size:30px; font-weight:700; line-height:1; letter-spacing:-.02em; font-variant-numeric:tabular-nums; }
190
+ .kpi-hint { font-family:var(--font-mono); font-size:11px; color:var(--muted-2); }
191
+ .kpi-execute .kpi-value { color:var(--accent); }
192
+ .kpi-ignore .kpi-value { color:var(--muted); }
193
+ .kpi-done .kpi-value { color:var(--success); }
194
+ .progress { margin:0 0 22px; }
195
+ .progress-head { display:flex; align-items:center; justify-content:space-between; margin-bottom:8px; font-size:11.5px; color:var(--muted); }
196
+ .progress-head .pct { color:var(--fg); font-family:var(--font-mono); }
197
+ .progress-track { height:8px; border-radius:999px; background:var(--surface-2); border:1px solid var(--border); overflow:hidden; }
198
+ .progress-fill { display:block; height:100%; border-radius:999px; background:linear-gradient(90deg, #2c8f57, var(--success)); transition:width .35s ease; }
199
+ .filters { display:flex; flex-wrap:wrap; align-items:center; gap:10px; margin-bottom:20px; }
200
+ .search { display:flex; align-items:center; gap:8px; flex:1 1 240px; max-width:330px; padding:8px 14px; color:var(--muted);
201
+ background:var(--surface-2); border:1px solid var(--border-strong); border-radius:999px; }
202
+ .search:focus-within { color:var(--accent); border-color:var(--accent-line); }
203
+ .search input { flex:1; min-width:0; font:inherit; font-size:12.5px; color:var(--fg); background:transparent; border:0; outline:0; }
204
+ .search input::placeholder { color:var(--muted-2); }
205
+ .search-clear { padding:0 2px; font-size:15px; line-height:1; color:var(--muted); background:transparent; border:0; cursor:pointer; }
206
+ .chips { display:inline-flex; gap:3px; padding:4px; background:var(--surface-2); border:1px solid var(--border); border-radius:999px; }
207
+ .chip { display:inline-flex; align-items:center; gap:6px; font:inherit; font-size:12px; font-weight:600; cursor:pointer; color:var(--muted);
208
+ padding:6px 13px; background:transparent; border:1px solid transparent; border-radius:999px; transition:color .15s ease, background .15s ease; }
209
+ .chip:hover { color:var(--fg); }
210
+ .chip.active { color:#14100a; background:#e9e5df!important; }
211
+ .sev-group { margin-bottom:24px; }
212
+ .sev-head { display:flex; align-items:center; gap:8px; margin:0 0 10px; font-size:11px; font-weight:700;
213
+ letter-spacing:.16em; text-transform:uppercase; color:var(--muted-2); }
214
+ .sev-head .n { font-family:var(--font-mono); letter-spacing:0; }
215
+ .sev-group.sev-blocking .sev-head { color:var(--blocking); }
216
+ .sev-group.sev-attention .sev-head { color:var(--attention); }
217
+ .sev-group.sev-suggestion .sev-head { color:var(--suggestion); }
218
+ .sev-group.sev-nit .sev-head { color:var(--nit); }
219
+ .card { position:relative; margin-bottom:20px; overflow:hidden; background:var(--card);
220
+ border:1px solid var(--border); border-left:4px solid var(--border-strong); border-radius:var(--radius-lg);
221
+ transition:background .15s ease, border-color .15s ease, opacity .2s ease; }
222
+ .card:hover { background:#16161b; }
223
+ .card.sev-blocking { border-left-color:var(--blocking); }
224
+ .card.sev-attention { border-left-color:var(--attention); }
225
+ .card.sev-suggestion { border-left-color:var(--suggestion); }
226
+ .card.sev-nit { border-left-color:var(--nit); }
227
+ .card.absent { opacity:.5; }
228
+ .card.done { opacity:.6; }
229
+ .card-head { display:flex; align-items:center; gap:10px; padding:12px 14px 0; }
230
+ .chev { display:grid; place-items:center; flex:none; width:22px; height:22px; cursor:pointer; color:var(--muted-2);
231
+ background:transparent; border:1px solid transparent; border-radius:7px; }
232
+ .chev:hover { color:var(--fg); background:rgba(255,255,255,.06); }
233
+ .chev:focus-visible { color:var(--fg); border-color:var(--accent-line); outline:none; }
234
+ .chev svg { transition:transform .18s ease; }
235
+ .chev.open svg { transform:rotate(90deg); }
236
+ .sev-badge { display:inline-flex; align-items:center; flex:none; padding:3px 9px; border-radius:999px;
237
+ font-size:10px; letter-spacing:.08em; text-transform:uppercase; }
238
+ .card.sev-blocking .sev-badge { color:var(--blocking); background:rgba(242,89,94,.12); }
239
+ .card.sev-attention .sev-badge { color:var(--attention); background:rgba(237,161,58,.12); }
240
+ .card.sev-suggestion .sev-badge { color:var(--suggestion); background:rgba(90,164,240,.12); }
241
+ .card.sev-nit .sev-badge { color:var(--nit); background:rgba(255,255,255,.06); }
242
+ .loc { display:inline-flex; align-items:center; gap:7px; max-width: 406ch; overflow:hidden; white-space:break-spaces; text-overflow:ellipsis;
243
+ padding:4px 9px; font-family:var(--font-mono); font-size:11.5px; color:#aba7a1; text-decoration:none; }
244
+ .loc:hover { color:#fff; }
245
+ .loc .ext { flex:none; opacity:0; transition:opacity .15s ease; }
246
+ .loc:hover .ext { opacity:1; }
247
+ .loc.static { color:var(--muted); }
248
+ .count { padding:2px 7px; font-family:var(--font-mono); font-size:11px; color:var(--muted);
249
+ background:var(--surface-2); border:1px solid var(--border); border-radius:999px; }
250
+ .spacer { flex:1; }
251
+ .done-btn { display:inline-flex; align-items:center; gap:7px; flex:none; font:inherit; font-size:12px; font-weight:600; cursor:pointer;
252
+ padding:7px 13px; color:var(--muted); background:transparent; border:1px solid var(--border-strong); border-radius:999px;
253
+ transition:color .15s ease, background .15s ease, border-color .15s ease; }
254
+ .done-btn:hover { color:var(--fg); border-color:rgba(69,192,122,.55); }
255
+ .done-btn:disabled { opacity:.55; cursor:not-allowed; }
256
+ .done-btn.is-done { color:var(--success); background:rgba(69,192,122,.14); border-color:rgba(69,192,122,.5); }
257
+ .card-title { margin:9px 0 0; padding:0 16px 20px; font-size:14px; font-weight:400; }
258
+ .card.done .card-title { color:#b7b7b7; text-decoration:line-through; text-decoration-color:rgba(255,255,255,.15); }
259
+ .card-body { display:flex; flex-direction:column; gap:12px; padding:20px 16px 15px; background:rgba(255,255,255,.012); border-top:1px solid var(--border); }
260
+ .meta-row { display:flex; flex-wrap:wrap; gap:14px; font-size:11.5px; color:var(--muted); }
261
+ .meta { display:inline-flex; align-items:center; gap:7px; }
262
+ .meta-k { font-size:10px; font-weight:700; letter-spacing:.1em; text-transform:uppercase; color:var(--muted-2); }
263
+ .meta code { padding:2px 6px; font-size:11.5px; color:var(--fg); background:rgba(255,255,255,.04); border:1px solid var(--border); border-radius:6px; }
264
+ .action-block { display:flex; gap:11px; padding:0; background:transparent; border:0; border-radius:0; }
265
+ .action-icon { flex:none; display:inline-block; margin-top:3px; margin-right:2px; color:var(--accent); }
266
+ .action-text { flex:1; min-width:0; }
267
+ .action-label { display:block; margin-bottom:4px; font-size:10px; font-weight:600; letter-spacing:.12em; text-transform:uppercase; color:var(--accent); }
268
+ .action-text p, .md-p { margin:0 0 6px; font-size:13px; line-height:1.65; color:#f0ece6; white-space:pre-wrap; }
269
+ .action-text p:last-child { margin-bottom:0; }
270
+ .md-list { margin:0 0 6px; padding-left:20px; font-size:13px; line-height:1.65; color:#f0ece6; }
271
+ .md-list li { margin:0 0 2px; }
272
+ .md-quote { margin:0 0 6px; padding:2px 0 2px 10px; border-left:2px solid var(--accent-line); color:var(--muted); font-size:13px; line-height:1.65; white-space:pre-wrap; }
273
+ .action-text code, .md-code { font-family:var(--font-mono); font-size:11.5px; padding:1px 5px; color:#f0ece6; background:rgba(255,255,255,.06); border:1px solid var(--border); border-radius:5px; }
274
+ code.md-path { color:#8fc7ff; background:rgba(90,164,240,.1); border-color:rgba(90,164,240,.24); }
275
+ .action-text a, .md-p a, .md-list a, .md-quote a, .md-table a, .md-h a { color:var(--accent-strong); text-decoration:underline; text-underline-offset:2px; }
276
+ .action-text strong, .md-p strong, .md-list strong, .md-quote strong, .md-table strong, .md-h strong { color:#fff; font-weight:600; }
277
+ .action-text em, .md-p em, .md-list em, .md-quote em, .md-table em, .md-h em { color:#e9e4dc; }
278
+ .md-code-block { margin:6px 0; padding:10px 12px; max-height:320px; overflow:auto; white-space:pre; font-size:11.5px; line-height:1.6; color:#cdc7bf; background:#0b0b0e; border-radius:10px; }
279
+ .md-h { margin:16px 0 8px; font-size:14px; font-weight:700; line-height:1.35; letter-spacing:-.01em; color:#eee9e2; }
280
+ .md-h:first-child { margin-top:0; }
281
+ .md-h1 { font-size:19px; }
282
+ .md-h2 { font-size:16px; }
283
+ .md-h3 { font-size:14px; }
284
+ .md-h4, .md-h5, .md-h6 { font-size:13px; color:#e4dfd7; font-weight:600; }
285
+ .md-hr { margin:14px 0; border:0; border-top:1px solid var(--border); }
286
+ .md-table-wrap { margin:8px 0 12px; padding:4px; overflow-x:auto; border:1px solid rgba(255,255,255,.07); border-radius:10px; background:rgba(255,255,255,.012); }
287
+ .md-table { width:100%; border-collapse:separate; border-spacing:3px; font-size:12px; line-height:1.5; }
288
+ .md-table th, .md-table td { padding:6px 9px; text-align:left; vertical-align:top; border:1px solid rgba(255,255,255,.035); border-radius:6px; }
289
+ .md-table th { color:var(--muted); font-weight:600; background:rgba(255,255,255,.03); }
290
+ .md-table td { color:#d8d3cc; }
291
+ .decision-tags-block { display:flex; align-items:center; gap:8px; }
292
+ .evidence { margin:0; padding: 16px; white-space:break-spaces; font-size:12px; line-height:1.6; color:#cdc7bf;
293
+ background:#0b0b0e; border:1px solid var(--border); border-radius:var(--radius); }
294
+ .evidence-block { display:flex; flex-direction:column; gap:6px; }
295
+ .evidence-head { display:flex; justify-content:flex-end; }
296
+ .code-lang { font-family:var(--font-mono); font-size:9.5px; font-weight:700; letter-spacing:.1em; text-transform:uppercase; color:var(--muted-2); }
297
+ .code-evidence { white-space:pre; overflow:auto; }
298
+ .tok-keyword { color:#c792ea; }
299
+ .tok-string { color:#9ddc8a; }
300
+ .tok-comment { color:#6d6862; font-style:italic; }
301
+ .tok-number { color:#8fc7ff; }
302
+ .tok-type { color:var(--accent-strong); }
303
+ .tok-function { color:#82aaff; }
304
+ .tok-property { color:#e6c07b; }
305
+ .tok-punct { color:#8d8a85; }
306
+ .tags { display:flex; flex-wrap:wrap; align-items:center; gap:7px; margin-left: auto; }
307
+ .tags-label { display:inline-flex; align-items:center; gap:6px; font-size:10px; font-weight:700; letter-spacing:.1em; text-transform:uppercase; color:var(--muted-2); }
308
+ .tag { display:inline-flex; align-items:center; gap:6px; padding:3px 9px; font-family:var(--font-mono); font-size:11px; color:var(--muted);
309
+ background:var(--surface-2); border:1px solid var(--border); border-radius:999px; }
310
+ .decisions { display:inline-flex; gap:3px; align-self:flex-start; padding:4px; background:var(--surface-2); border:1px solid var(--border); border-radius:999px; }
311
+ .dec-btn { font:inherit; font-size:12px; font-weight:600; cursor:pointer; color:var(--muted); padding:6px 15px;
312
+ background:transparent; border:1px solid transparent; border-radius:999px; transition:color .15s ease, background .15s ease; }
313
+ .dec-btn:hover { color:var(--fg); }
314
+ .dec-btn:disabled { opacity:.55; cursor:not-allowed; }
315
+ .dec-btn.dec-execute.active { color:#1b1305; background:linear-gradient(180deg, var(--accent-strong), var(--accent)); }
316
+ .dec-btn.dec-ignore.active { color:#ffd2d3; background:rgba(242,89,94,.18); box-shadow:inset 0 0 0 1px rgba(242,89,94,.45); }
317
+ .dec-btn.dec-pending.active { color:var(--fg); background:rgba(255,255,255,.08); box-shadow:inset 0 0 0 1px var(--border-strong); }
318
+ .comment { position:relative; }
319
+ .comment textarea { width:100%; min-height:58px; padding:11px 13px 26px; resize:vertical; font:inherit; font-size:12.5px; line-height:1.5;
320
+ color:var(--fg); background:var(--surface-2); border:1px solid var(--border-strong); border-radius:var(--radius); outline:none;
321
+ transition:border-color .15s ease, box-shadow .15s ease; }
322
+ .comment textarea::placeholder { color:var(--muted-2); }
323
+ .comment textarea:focus { border-color:var(--accent-line); box-shadow:0 0 0 3px rgba(240,165,44,.12); }
324
+ .comment-status { position:absolute; right:12px; bottom:9px; display:inline-flex; align-items:center; gap:6px;
325
+ font-family:var(--font-mono); font-size:11px; color:var(--muted); }
326
+ .comment-status.saved { color:var(--success); }
327
+ .artifacts { display:flex; flex-direction:column; }
328
+ .artifact-layout { display:grid; grid-template-columns:minmax(190px,250px) minmax(0,1fr); gap:16px; align-items:start; }
329
+ .artifact-list { display:flex; flex-direction:column; gap:14px; max-height:72vh; overflow:auto; padding:2px 4px 2px 0; }
330
+ .artifact-group { display:flex; flex-direction:column; gap:4px; }
331
+ .artifact-group-label { margin:0 0 2px; font-size:10px; font-weight:700; letter-spacing:.14em; text-transform:uppercase; color:var(--muted); }
332
+ .artifact-item { display:flex; flex-direction:column; gap:2px; text-align:left; cursor:pointer; font:inherit; color:var(--muted);
333
+ padding:8px 10px; background:transparent; border:1px solid transparent; border-radius:10px;
334
+ transition:background .15s ease, border-color .15s ease, color .15s ease; }
335
+ .artifact-item:hover { color:var(--fg); background:var(--surface-2); border-color:var(--border); }
336
+ .artifact-item.active { color:var(--fg); background:var(--accent-soft); border-color:var(--accent-line); }
337
+ .artifact-item-name { font-family:var(--font-mono); font-size:11.5px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
338
+ .artifact-item-kind { font-size:9.5px; font-weight:700; letter-spacing:.1em; text-transform:uppercase; color:var(--muted-2); }
339
+ .artifact-item.active .artifact-item-kind { color:var(--accent); }
340
+ .artifact-pane { min-width:0; }
341
+ .artifact-head { display:flex; align-items:flex-start; justify-content:space-between; gap:14px; margin-bottom:12px; }
342
+ .artifact-title { margin:0 0 5px; font-family:var(--font-mono); font-size:13px; font-weight:600; word-break:break-word; }
343
+ .artifact-desc { margin:0; max-width:78ch; color:var(--muted); font-size:12.5px; line-height:1.55; }
344
+ .artifact-kind-badge { flex:none; display:inline-flex; align-items:center; padding:3px 9px; font-size:10px; font-weight:700;
345
+ letter-spacing:.08em; text-transform:uppercase; color:var(--accent); background:var(--accent-soft);
346
+ border:1px solid var(--accent-line); border-radius:999px; }
347
+ .artifact-view { margin:0; padding:14px 16px; max-height:70vh; overflow:auto; font-size:12px; line-height:1.6; color:#cdc7bf;
348
+ background:#0b0b0e; border:1px solid var(--border); border-radius:var(--radius); }
349
+ .artifact-markdown { white-space:normal; }
350
+ .artifact-markdown .md-p, .artifact-markdown .md-list, .artifact-markdown .md-quote { font-size:12.5px; }
351
+ .artifact-markdown .md-p, .artifact-markdown .md-list { color:#d8d3cc; }
352
+ .artifact-markdown .md-code-block { border:1px solid var(--border); }
353
+ .json-view { white-space:pre; }
354
+ .json-key { color:var(--accent-strong); }
355
+ .json-string { color:#9ddc8a; }
356
+ .json-number { color:#8fc7ff; }
357
+ .json-literal { color:#d7a6ff; }
358
+ .json-punct { color:#8d8a85; }
359
+ @media (max-width:900px) { .artifact-layout { grid-template-columns:1fr; } .artifact-list { max-height:240px; } }
360
+ .sent-tag { display:inline-flex; align-items:center; gap:5px; padding:2px 8px; font-size:10px; font-weight:700; letter-spacing:.06em; text-transform:uppercase;
361
+ color:var(--success); background:rgba(69,192,122,.12); border:1px solid rgba(69,192,122,.32); border-radius:999px; text-decoration:none; }
362
+ .sent-tag:hover { color:#fff; }
363
+ .sent-tag.gh { color:#b9c7d6; background:rgba(139,164,191,.12); border-color:rgba(139,164,191,.35); }
364
+ .sent-tag.gh:hover { color:#fff; background:rgba(139,164,191,.2); }
365
+ .resume { display:flex; flex-direction:column; }
366
+ .resume-hero { display:flex; align-items:flex-start; justify-content:space-between; gap:16px; padding:18px 20px;
367
+ background:linear-gradient(135deg, rgba(240,165,44,.08), rgba(255,255,255,.015) 55%);
368
+ border:1px solid var(--border); border-radius:var(--radius-lg); }
369
+ .resume-kicker { display:block; font-size:10px; font-weight:700; letter-spacing:.14em; text-transform:uppercase; color:var(--accent); }
370
+ .resume-title { margin:5px 0 7px; font-size:22px; font-weight:700; letter-spacing:-.02em; word-break:break-word; }
371
+ .resume-meta { display:flex; flex-wrap:wrap; align-items:center; gap:12px; margin:0; font-size:12px; color:var(--muted); }
372
+ .resume-pr { display:inline-flex; align-items:center; gap:5px; color:var(--accent-strong); text-decoration:none; }
373
+ .resume-pr:hover { text-decoration:underline; }
374
+ .resume-hero-side { display:flex; flex:none; flex-direction:column; align-items:flex-end; gap:10px; }
375
+ .resume-status { display:inline-flex; align-items:center; padding:5px 12px; font-size:11px; font-weight:700; letter-spacing:.04em; border-radius:999px; }
376
+ .resume-status.blocking { color:var(--blocking); background:rgba(242,89,94,.12); border:1px solid rgba(242,89,94,.35); }
377
+ .resume-status.attention { color:var(--attention); background:rgba(237,161,58,.12); border:1px solid rgba(237,161,58,.35); }
378
+ .resume-status.clean { color:var(--success); background:rgba(69,192,122,.12); border:1px solid rgba(69,192,122,.35); }
379
+ .resume-kpis { margin:14px 0 18px; }
380
+ .resume-kpis .kpi-value { font-size:24px; }
381
+ .churn-add { color:var(--success); }
382
+ .churn-del { color:var(--blocking); }
383
+ .churn-sep { margin:0 3px; color:var(--muted-2); font-weight:400; }
384
+ .kpi-blocking .kpi-value { color:var(--blocking); }
385
+ .kpi-attention .kpi-value { color:var(--attention); }
386
+ .kpi-total .kpi-value { color:var(--fg); }
387
+ .resume-grid { display:grid; grid-template-columns:minmax(0,1.35fr) minmax(0,1fr); gap:16px; align-items:start; }
388
+ .resume-card { min-width:0; padding:16px 18px; background:var(--card); border:1px solid var(--border); border-radius:var(--radius-lg); }
389
+ .resume-card-head { display:flex; align-items:center; justify-content:space-between; gap:10px; }
390
+ .resume-card-title { margin:0 0 10px; font-size:12px; font-weight:700; letter-spacing:.1em; text-transform:uppercase; color:var(--muted); }
391
+ .resume-card-count { display:inline-flex; align-items:center; justify-content:center; min-width:22px; height:22px; margin-bottom:10px; padding:0 7px;
392
+ font-family:var(--font-mono); font-size:11px; color:var(--accent); background:var(--accent-soft); border:1px solid var(--accent-line); border-radius:999px; }
393
+ .resume-narrative .md-p:last-child { margin-bottom:0; }
394
+ .resume-domains { display:flex; flex-direction:column; gap:8px; margin:0; padding:0; list-style:none; }
395
+ .resume-domain { display:flex; flex-direction:column; gap:7px; padding:10px 12px; background:var(--surface-2);
396
+ border:1px solid var(--border); border-radius:10px; }
397
+ .resume-domain-head { display:flex; align-items:baseline; justify-content:space-between; gap:10px; }
398
+ .resume-domain-name { font-size:12.5px; font-weight:600; color:var(--fg); }
399
+ .resume-domain-files { flex:none; font-family:var(--font-mono); font-size:10.5px; color:var(--muted-2); }
400
+ .resume-domain-sev { display:flex; flex-wrap:wrap; gap:6px; }
401
+ .resume-domain-sev .sev-badge { padding:2px 7px; font-size:9.5px; }
402
+ .resume-domain-ok { font-size:11.5px; color:var(--success); }
403
+ @media (max-width:900px) { .resume-grid { grid-template-columns:1fr; } .resume-hero { flex-direction:column; } .resume-hero-side { flex-direction:row; align-items:flex-start; } }
404
+ .modal-layer { position:fixed; inset:0; z-index:50; display:grid; place-items:center; padding:24px; }
405
+ .modal-backdrop { position:absolute; inset:0; width:100%; padding:0; background:rgba(5,5,7,.66); border:0; cursor:default; backdrop-filter:blur(5px); -webkit-backdrop-filter:blur(5px); }
406
+ .modal { position:relative; width:min(640px, 100%); max-height:min(78vh, 780px); display:flex; flex-direction:column; overflow:hidden;
407
+ background:var(--surface); border:1px solid var(--border-strong); border-radius:var(--radius-lg); box-shadow:0 24px 70px rgba(0,0,0,.5); }
408
+ .modal-head { display:flex; align-items:flex-start; justify-content:space-between; gap:14px; padding:18px 20px 14px; border-bottom:1px solid var(--border); }
409
+ .modal-title { margin:0; font-size:17px; font-weight:700; letter-spacing:-.01em; }
410
+ .modal-sub { margin:5px 0 0; font-size:12px; color:var(--muted); line-height:1.5; }
411
+ .modal-close { flex:none; display:grid; place-items:center; width:30px; height:30px; font:inherit; font-size:19px; line-height:1; color:var(--muted); background:transparent; border:1px solid var(--border); border-radius:9px; cursor:pointer; }
412
+ .modal-close:hover { color:var(--fg); border-color:var(--border-strong); }
413
+ .modal-body { display:flex; flex-direction:column; gap:16px; padding:16px 20px; overflow-y:auto; }
414
+ .modal-foot { display:flex; align-items:center; justify-content:space-between; gap:12px; padding:14px 20px; background:var(--surface-2); border-top:1px solid var(--border); }
415
+ .submit-field { display:flex; flex-direction:column; gap:8px; }
416
+ .submit-label { font-size:10px; font-weight:700; letter-spacing:.14em; text-transform:uppercase; color:var(--muted-2); }
417
+ .submit-summary { padding:13px 15px; font-size:12.5px; color:var(--fg); background:var(--accent-soft); border:1px solid var(--accent-line); border-radius:var(--radius); }
418
+ .submit-summary b { font-family:var(--font-mono); }
419
+ .submit-list { display:flex; flex-direction:column; gap:6px; max-height:240px; overflow-y:auto; padding-right:2px; }
420
+ .submit-item { display:flex; align-items:center; gap:10px; padding:8px 11px; font-size:12px; background:var(--surface-2); border:1px solid var(--border); border-radius:10px; }
421
+ .submit-item.sent { opacity:.7; }
422
+ .submit-item .rule { flex:1; min-width:0; color:var(--muted); overflow:hidden; white-space:nowrap; text-overflow:ellipsis; }
423
+ .submit-empty { color:var(--muted); font-size:12.5px; }
424
+ .submit-field .chips { flex-wrap:wrap; }
425
+ .submit-check { display:inline-flex; align-items:center; gap:8px; font-size:12.5px; color:var(--muted); cursor:pointer; }
426
+ .submit-check input { accent-color:var(--accent); }
427
+ .submit-actions { display:flex; align-items:center; gap:8px; }
428
+ .sev-badge.sev-blocking { color:var(--blocking); background:rgba(242,89,94,.12); }
429
+ .sev-badge.sev-attention { color:var(--attention); background:rgba(237,161,58,.12); }
430
+ .sev-badge.sev-suggestion { color:var(--suggestion); background:rgba(90,164,240,.12); }
431
+ .sev-badge.sev-nit { color:var(--nit); background:rgba(255,255,255,.06); }
432
+ @keyframes pulse { 0%,100% { opacity:1; transform:scale(1); } 50% { opacity:.4; transform:scale(.8); } }
433
+ @keyframes spin { to { transform:rotate(360deg); } }
434
+ @media (max-width:980px) { .kpis { grid-template-columns:repeat(2, minmax(0,1fr)); } }
435
+ @media (max-width:1100px) {
436
+ /* Painel largo reduz o espaço do conteúdo: compacta o topo para evitar scroll-x. */
437
+ .topbar { gap:12px; padding:12px 16px; }
438
+ .seg-btn { gap:6px; padding:7px 11px; font-size:12px; }
439
+ .auto-refresh { display:none; }
440
+ }
441
+ @media (max-width:720px) {
442
+ /* Em telas estreitas as abas viram só ícone (o título do botão explica cada uma). */
443
+ .seg-btn { padding:8px 10px; }
444
+ .seg-btn .seg-label { display:none; }
445
+ }
446
+ @media (max-width:820px) {
447
+ /* Mantém a rail fixa e encolhe o painel para preservar o conteúdo principal. */
448
+ .sidebar { width:calc(var(--rail-w) + clamp(220px, 35vw, var(--side-width))); }
449
+ .side-head { padding:14px 12px 10px; }
450
+ .side-body { padding:12px; }
451
+ .topbar { padding:12px 16px; }
452
+ .content { padding:18px 16px 56px; }
453
+ .auto-refresh { display:none; }
454
+ }
@@ -493,10 +493,14 @@ export type HoundsmanAst = {
493
493
  /** Providers suportados pela camada de IA. */
494
494
  export type AiProviderKind = "deepseek" | "openai" | "anthropic";
495
495
 
496
+ /** Opções do AI SDK por provider (repassadas sem interpretação). */
497
+ export type AiProviderOptions = Record<string, Record<string, unknown>>;
498
+
496
499
  /** Features de IA do fluxo normal (todas fail-open). */
497
500
  export type HoundsmanAiFeatures = {
498
501
  context?: boolean;
499
502
  brief?: boolean;
503
+ technical?: boolean;
500
504
  classification?: boolean;
501
505
  preAnalysis?: boolean;
502
506
  verify?: boolean;
@@ -519,6 +523,7 @@ export type HoundsmanAi = {
519
523
  maxSteps?: number;
520
524
  maxOutputTokens?: number;
521
525
  temperature?: number;
526
+ providerOptions?: AiProviderOptions;
522
527
  timeoutMs?: number;
523
528
  maxCallsPerReview?: number;
524
529
  maxTokensPerReview?: number;
@@ -3,8 +3,11 @@
3
3
  */
4
4
 
5
5
  import { describe, expect, test } from "bun:test";
6
+ import type { LanguageModel } from "ai";
6
7
  import { z } from "zod";
7
8
  import { AiClient, AiUsageBudget, AiUsageRecorder } from "../core/ai";
9
+ import { buildGenerateTextArgs } from "../core/ai/ai-sdk-model-provider";
10
+ import { temperatureApplies } from "../core/ai/provider-capabilities";
8
11
  import { defineHoundsmanConfig } from "../core/config";
9
12
  import { DEFAULT_AI_MODEL } from "../core/defaults";
10
13
  import { ProjectConfig } from "../core/project-config";
@@ -82,6 +85,74 @@ describe("AiClient", () => {
82
85
  });
83
86
  });
84
87
 
88
+ describe("temperature e providerOptions", () => {
89
+ test("DeepSeek V4 ignora temperature; thinking disabled reabilita", () => {
90
+ expect(temperatureApplies("deepseek", "deepseek-v4-pro", undefined)).toBe(false);
91
+ expect(
92
+ temperatureApplies("deepseek", "deepseek-v4-pro", {
93
+ deepseek: { thinking: { type: "disabled" } },
94
+ }),
95
+ ).toBe(true);
96
+ expect(
97
+ temperatureApplies("deepseek", "deepseek-v4-pro", {
98
+ deepseek: { thinking: { type: "enabled" } },
99
+ }),
100
+ ).toBe(false);
101
+ expect(temperatureApplies("deepseek", "deepseek-chat", undefined)).toBe(true);
102
+ expect(temperatureApplies("openai", "gpt-4o", undefined)).toBe(true);
103
+ });
104
+
105
+ test("adapter omite temperature ignorada e repassa providerOptions", () => {
106
+ const model = {} as LanguageModel;
107
+ const thinkingOff = buildGenerateTextArgs(
108
+ model,
109
+ {
110
+ prompt: "oi",
111
+ temperature: 0,
112
+ providerOptions: { deepseek: { thinking: { type: "disabled" } } },
113
+ },
114
+ "deepseek",
115
+ "deepseek-v4-pro",
116
+ );
117
+ expect(thinkingOff).toMatchObject({
118
+ temperature: 0,
119
+ providerOptions: { deepseek: { thinking: { type: "disabled" } } },
120
+ });
121
+
122
+ const defaultThinking = buildGenerateTextArgs(
123
+ model,
124
+ { prompt: "oi", temperature: 0 },
125
+ "deepseek",
126
+ "deepseek-v4-pro",
127
+ );
128
+ expect(defaultThinking).not.toHaveProperty("temperature");
129
+
130
+ const openai = buildGenerateTextArgs(
131
+ model,
132
+ { prompt: "oi", temperature: 0 },
133
+ "openai",
134
+ "gpt-4o",
135
+ );
136
+ expect(openai).toMatchObject({ temperature: 0 });
137
+ });
138
+
139
+ test("AiClient injeta providerOptions da config", async () => {
140
+ const configured = new ProjectConfig({
141
+ ...fakeHoundsmanConfig,
142
+ ai: { providerOptions: { deepseek: { thinking: { type: "disabled" } } } },
143
+ });
144
+ const provider = new FakeModelProvider(() => ({ ok: true }));
145
+ const client = new AiClient({ project: configured, provider });
146
+ await client.generateObject("x", {
147
+ prompt: "oi",
148
+ outputSchema: z.object({ ok: z.boolean() }),
149
+ });
150
+ expect(provider.requests[0]?.providerOptions).toEqual({
151
+ deepseek: { thinking: { type: "disabled" } },
152
+ });
153
+ });
154
+ });
155
+
85
156
  describe("AiUsageBudget / AiUsageRecorder", () => {
86
157
  test("acumula uso e bloqueia no teto", () => {
87
158
  const budget = new AiUsageBudget({ maxCalls: 2 });
@@ -8,6 +8,7 @@ import { AiBriefEnricher } from "../core/ai/features/brief-enricher";
8
8
  import { AiClassifier } from "../core/ai/features/classifier";
9
9
  import { AiContextSummarizer } from "../core/ai/features/context-summarizer";
10
10
  import { AiPreAnalyzer } from "../core/ai/features/pre-analyzer";
11
+ import { AiTechnicalSummarizer } from "../core/ai/features/technical-summarizer";
11
12
  import { AiVerifier } from "../core/ai/features/verifier";
12
13
  import { briefInstructions, briefPrompt } from "../core/ai/prompts/brief";
13
14
  import { renderBrief } from "../core/brief";
@@ -153,6 +154,41 @@ describe("AiClassifier", () => {
153
154
  });
154
155
  });
155
156
 
157
+ describe("AiTechnicalSummarizer", () => {
158
+ test("devolve os destaques estruturais do modelo", async () => {
159
+ const provider = new FakeModelProvider(() => ({
160
+ overview: "Novo módulo bin-lookup",
161
+ highlights: [{ area: "Contratos", detail: "`BinLookupProvider`" }],
162
+ }));
163
+ const summarizer = new AiTechnicalSummarizer(new AiClient({ project, provider }));
164
+ const summary = await summarizer.summarize({
165
+ branch: "main",
166
+ classification: {
167
+ files: { "src/bin-lookup/service.ts": { status: "added" } },
168
+ churn: { "src/bin-lookup/service.ts": { additions: 3, deletions: 0 } },
169
+ },
170
+ headSha: "abc",
171
+ domains: ["Alpha"],
172
+ });
173
+ expect(summary?.source).toBe("ai");
174
+ expect(summary?.headSha).toBe("abc");
175
+ expect(summary?.highlights[0].area).toBe("Contratos");
176
+ });
177
+
178
+ test("sem IA o resumo é nulo (fallback no chamador)", async () => {
179
+ const summarizer = new AiTechnicalSummarizer(
180
+ new AiClient({ project, provider: null }),
181
+ );
182
+ expect(summarizer.enabled).toBe(false);
183
+ expect(
184
+ await summarizer.summarize({
185
+ branch: "main",
186
+ classification: { files: {}, churn: {} },
187
+ }),
188
+ ).toBeNull();
189
+ });
190
+ });
191
+
156
192
  describe("AiVerifier", () => {
157
193
  test("devolve confiança apenas para ids conhecidos", async () => {
158
194
  const provider = new FakeModelProvider(() => ({
@@ -28,6 +28,7 @@ import {
28
28
  import { defineHoundsmanConfig } from "../core/config";
29
29
  import { defaultOptions, finalize, prepare, reviewContext } from "../core/orchestrator";
30
30
  import { displayPath } from "../core/paths";
31
+ import { RESUME_MAX_CHARS } from "../core/renderers/resume-renderer";
31
32
  import {
32
33
  type MergedFinding,
33
34
  renderDomainReport,
@@ -434,7 +435,9 @@ describe("baseline — artefatos de prepare/finalize", () => {
434
435
  }
435
436
  const resume = storage.readArtifact(review.id, "review-resume.md");
436
437
  expect(resume?.kind).toBe("resume");
437
- expect((resume?.content ?? "").length).toBeLessThanOrEqual(2500);
438
+ expect(resume?.content).toContain("## Pontos técnicos");
439
+ expect(resume?.content).toContain("Remoções");
440
+ expect((resume?.content ?? "").length).toBeLessThanOrEqual(RESUME_MAX_CHARS);
438
441
  expect(
439
442
  normalize(JSON.stringify(storage.readFindings(review.id), null, 2)),
440
443
  ).toMatchSnapshot("state.json");