juliensimon commited on
Commit
d5e299c
·
verified ·
1 Parent(s): 77de2c1

Add brutality selector, issue badge, and copy review button

Browse files
Files changed (6) hide show
  1. .claude/settings.local.json +37 -0
  2. .gitignore +2 -0
  3. public/app.js +128 -1
  4. public/index.html +12 -0
  5. public/style.css +126 -0
  6. server.js +29 -5
.claude/settings.local.json ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "WebFetch(domain:openrouter.ai)",
5
+ "Bash(node --version:*)",
6
+ "Bash(npm --version)",
7
+ "Bash(curl:*)",
8
+ "Bash(npm install)",
9
+ "Bash(OPENROUTER_API_KEY=test-key timeout 3 node:*)",
10
+ "Bash(OPENROUTER_API_KEY=test-key node:*)",
11
+ "WebSearch",
12
+ "Bash(node --check:*)",
13
+ "Bash(node -e \"\n// Simulate browser globals to check parse only\nglobalThis.document = { getElementById: \\(\\) => \\({ hidden: true, classList: { toggle\\(\\){}, add\\(\\){}, remove\\(\\){} }, addEventListener\\(\\){} }\\), querySelectorAll: \\(\\) => [] };\nglobalThis.DOMPurify = { sanitize: x => x };\nglobalThis.marked = { parse: x => x, use\\(\\){} };\nglobalThis.EventSource = class {};\n\")",
14
+ "Bash(find:*)",
15
+ "Bash(wc:*)",
16
+ "Bash(huggingface-cli repo create:*)",
17
+ "Bash(huggingface-cli whoami:*)",
18
+ "Bash(hf repo create:*)",
19
+ "Bash(hf auth token:*)",
20
+ "Bash(hf auth list:*)",
21
+ "Bash(git ls-remote:*)",
22
+ "Bash(hf auth login:*)",
23
+ "Bash(git add:*)",
24
+ "Bash(git commit:*)",
25
+ "Bash(git push:*)",
26
+ "Bash(hf repo settings:*)",
27
+ "Bash(python3 -c \"\nfrom huggingface_hub import HfApi\napi = HfApi\\(\\)\napi.add_space_secret\\(''juliensimon/trinity-code-reviewer'', ''OPENROUTER_API_KEY'', ''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''\\)\nprint\\(''Secret set successfully''\\)\n\")",
28
+ "Bash(python3:*)",
29
+ "WebFetch(domain:lobehub.com)",
30
+ "Bash(grep -i content-length curl -sI \"https://raw.githubusercontent.com/django/django/main/django/db/models/expressions.py\")",
31
+ "Bash(grep -i content-length curl -sI \"https://raw.githubusercontent.com/facebook/react/main/packages/react-reconciler/src/ReactFiberHooks.js\")",
32
+ "Bash(grep -i content-length curl -sI \"https://raw.githubusercontent.com/microsoft/TypeScript/main/src/compiler/scanner.ts\")",
33
+ "Bash(git init:*)",
34
+ "Bash(hf upload:*)"
35
+ ]
36
+ }
37
+ }
.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ node_modules
2
+ .env
public/app.js CHANGED
@@ -11,6 +11,10 @@ const tabsEl = document.getElementById("tabs");
11
  const tabContent = document.getElementById("tab-content");
12
  const streamError = document.getElementById("stream-error");
13
  const tabButtons = document.querySelectorAll(".tab");
 
 
 
 
14
 
15
  const GITHUB_BLOB_RE = /^https?:\/\/github\.com\/[^/]+\/[^/]+\/blob\/[^/]+\/.+$/;
16
 
@@ -122,6 +126,8 @@ function renderMarkdown(md) {
122
  let activeTab = "summary";
123
  let manualSwitch = false;
124
  let currentSections = {};
 
 
125
 
126
  tabButtons.forEach((btn) => {
127
  btn.addEventListener("click", () => {
@@ -167,6 +173,118 @@ function updateTabs(sections, currentStreamSection) {
167
  renderActiveTab();
168
  }
169
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
  /* --- Review flow --- */
171
 
172
  let currentSource = null;
@@ -199,6 +317,9 @@ function startReview() {
199
  tabButtons.forEach((btn) => {
200
  btn.classList.remove("has-content", "streaming");
201
  });
 
 
 
202
 
203
  if (!url) { showInputError("Please enter a GitHub file URL."); return; }
204
  if (!GITHUB_BLOB_RE.test(url)) {
@@ -210,7 +331,8 @@ function startReview() {
210
  setLoading(true);
211
 
212
  let markdown = "";
213
- const source = new EventSource(`/api/review?url=${encodeURIComponent(url)}`);
 
214
  currentSource = source;
215
 
216
  source.addEventListener("meta", (e) => {
@@ -233,9 +355,14 @@ function startReview() {
233
 
234
  source.addEventListener("done", () => {
235
  cleanup();
 
236
  // Final render with no streaming section
237
  const sections = parseSections(markdown);
238
  updateTabs(sections, null);
 
 
 
 
239
  });
240
 
241
  source.addEventListener("error", (e) => {
 
11
  const tabContent = document.getElementById("tab-content");
12
  const streamError = document.getElementById("stream-error");
13
  const tabButtons = document.querySelectorAll(".tab");
14
+ const brutalityBtns = document.querySelectorAll(".brutality-btn");
15
+ const issueBadge = document.getElementById("issue-badge");
16
+ const copyBtn = document.getElementById("copy-btn");
17
+ const toast = document.getElementById("toast");
18
 
19
  const GITHUB_BLOB_RE = /^https?:\/\/github\.com\/[^/]+\/[^/]+\/blob\/[^/]+\/.+$/;
20
 
 
126
  let activeTab = "summary";
127
  let manualSwitch = false;
128
  let currentSections = {};
129
+ let brutalityLevel = "standard";
130
+ let fullMarkdown = "";
131
 
132
  tabButtons.forEach((btn) => {
133
  btn.addEventListener("click", () => {
 
173
  renderActiveTab();
174
  }
175
 
176
+ /* --- Brutality selector --- */
177
+
178
+ brutalityBtns.forEach((btn) => {
179
+ btn.addEventListener("click", () => {
180
+ brutalityBtns.forEach((b) => b.classList.remove("active"));
181
+ btn.classList.add("active");
182
+ brutalityLevel = btn.dataset.level;
183
+ });
184
+ });
185
+
186
+ /* --- Issue badge --- */
187
+
188
+ function countIssuePriorities(markdown) {
189
+ const counts = { critical: 0, high: 0, medium: 0, low: 0 };
190
+ const pattern = /\[CRITICAL\]|\[HIGH\]|\[MEDIUM\]|\[LOW\]/gi;
191
+ const matches = markdown.match(pattern) || [];
192
+ for (const m of matches) {
193
+ const level = m.slice(1, -1).toLowerCase();
194
+ if (counts[level] !== undefined) counts[level]++;
195
+ }
196
+ return counts;
197
+ }
198
+
199
+ function renderIssueBadge(counts) {
200
+ const total = counts.critical + counts.high + counts.medium + counts.low;
201
+ if (total === 0) {
202
+ issueBadge.hidden = true;
203
+ return;
204
+ }
205
+
206
+ const items = [
207
+ { level: "critical", label: "Critical", count: counts.critical },
208
+ { level: "high", label: "High", count: counts.high },
209
+ { level: "medium", label: "Medium", count: counts.medium },
210
+ { level: "low", label: "Low", count: counts.low },
211
+ ];
212
+
213
+ // Clear existing content
214
+ issueBadge.textContent = "";
215
+
216
+ for (const item of items) {
217
+ if (item.count === 0) continue;
218
+
219
+ const span = document.createElement("span");
220
+ span.className = "badge-item";
221
+
222
+ const dot = document.createElement("span");
223
+ dot.className = `badge-dot ${item.level}`;
224
+
225
+ const countEl = document.createElement("span");
226
+ countEl.className = "badge-count";
227
+ countEl.textContent = item.count;
228
+
229
+ const labelEl = document.createElement("span");
230
+ labelEl.className = "badge-label";
231
+ labelEl.textContent = item.label;
232
+
233
+ span.appendChild(dot);
234
+ span.appendChild(countEl);
235
+ span.appendChild(labelEl);
236
+ issueBadge.appendChild(span);
237
+ }
238
+
239
+ issueBadge.hidden = false;
240
+ }
241
+
242
+ /* --- Copy to clipboard --- */
243
+
244
+ function buildMarkdownExport() {
245
+ const sectionTitles = {
246
+ summary: "## Summary",
247
+ quality: "## Code Quality",
248
+ performance: "## Performance",
249
+ security: "## Security",
250
+ suggestions: "## Suggestions",
251
+ verdicts: "## Verdicts",
252
+ };
253
+
254
+ let output = `# Code Review: ${metaRepo.textContent}\n`;
255
+ output += `**File:** ${metaPath.textContent} | **Branch:** ${metaBranch.textContent}\n\n`;
256
+
257
+ for (const key of SECTION_KEYS) {
258
+ const content = currentSections[key]?.trim();
259
+ if (content) {
260
+ output += `${sectionTitles[key]}\n${content}\n\n`;
261
+ }
262
+ }
263
+
264
+ return output.trim();
265
+ }
266
+
267
+ copyBtn.addEventListener("click", async () => {
268
+ const markdown = buildMarkdownExport();
269
+ try {
270
+ await navigator.clipboard.writeText(markdown);
271
+ showToast();
272
+ } catch (err) {
273
+ console.error("Copy failed:", err);
274
+ }
275
+ });
276
+
277
+ function showToast() {
278
+ toast.hidden = false;
279
+ // Force reflow before adding class for animation
280
+ toast.offsetHeight;
281
+ toast.classList.add("show");
282
+ setTimeout(() => {
283
+ toast.classList.remove("show");
284
+ setTimeout(() => { toast.hidden = true; }, 300);
285
+ }, 2000);
286
+ }
287
+
288
  /* --- Review flow --- */
289
 
290
  let currentSource = null;
 
317
  tabButtons.forEach((btn) => {
318
  btn.classList.remove("has-content", "streaming");
319
  });
320
+ issueBadge.hidden = true;
321
+ copyBtn.hidden = true;
322
+ fullMarkdown = "";
323
 
324
  if (!url) { showInputError("Please enter a GitHub file URL."); return; }
325
  if (!GITHUB_BLOB_RE.test(url)) {
 
331
  setLoading(true);
332
 
333
  let markdown = "";
334
+ const apiUrl = `/api/review?url=${encodeURIComponent(url)}&brutality=${encodeURIComponent(brutalityLevel)}`;
335
+ const source = new EventSource(apiUrl);
336
  currentSource = source;
337
 
338
  source.addEventListener("meta", (e) => {
 
355
 
356
  source.addEventListener("done", () => {
357
  cleanup();
358
+ fullMarkdown = markdown;
359
  // Final render with no streaming section
360
  const sections = parseSections(markdown);
361
  updateTabs(sections, null);
362
+ // Show badge and copy button
363
+ const counts = countIssuePriorities(markdown);
364
+ renderIssueBadge(counts);
365
+ copyBtn.hidden = false;
366
  });
367
 
368
  source.addEventListener("error", (e) => {
public/index.html CHANGED
@@ -34,6 +34,13 @@
34
  <p id="input-error" class="error" hidden></p>
35
  </form>
36
 
 
 
 
 
 
 
 
37
  <section id="samples">
38
  <p class="samples-label">Try a sample:</p>
39
  <div class="samples-grid">
@@ -98,6 +105,8 @@
98
  <span id="meta-branch"></span>
99
  </div>
100
 
 
 
101
  <div id="tabs" hidden>
102
  <nav class="tab-bar">
103
  <button class="tab active" data-section="summary">Summary</button>
@@ -106,11 +115,14 @@
106
  <button class="tab" data-section="security">Security</button>
107
  <button class="tab" data-section="suggestions">Suggestions</button>
108
  <button class="tab" data-section="verdicts">Verdicts</button>
 
109
  </nav>
110
  <div id="tab-content" class="tab-content"></div>
111
  </div>
112
 
113
  <div id="stream-error" class="error" hidden></div>
 
 
114
  </main>
115
 
116
  <script src="https://cdn.jsdelivr.net/npm/dompurify/dist/purify.min.js"></script>
 
34
  <p id="input-error" class="error" hidden></p>
35
  </form>
36
 
37
+ <div class="brutality-selector">
38
+ <span class="brutality-label">Tone:</span>
39
+ <button type="button" class="brutality-btn" data-level="gentle">Gentle</button>
40
+ <button type="button" class="brutality-btn active" data-level="standard">Standard</button>
41
+ <button type="button" class="brutality-btn" data-level="brutal">Brutal</button>
42
+ </div>
43
+
44
  <section id="samples">
45
  <p class="samples-label">Try a sample:</p>
46
  <div class="samples-grid">
 
105
  <span id="meta-branch"></span>
106
  </div>
107
 
108
+ <div id="issue-badge" hidden></div>
109
+
110
  <div id="tabs" hidden>
111
  <nav class="tab-bar">
112
  <button class="tab active" data-section="summary">Summary</button>
 
115
  <button class="tab" data-section="security">Security</button>
116
  <button class="tab" data-section="suggestions">Suggestions</button>
117
  <button class="tab" data-section="verdicts">Verdicts</button>
118
+ <button type="button" id="copy-btn" hidden>Copy Review</button>
119
  </nav>
120
  <div id="tab-content" class="tab-content"></div>
121
  </div>
122
 
123
  <div id="stream-error" class="error" hidden></div>
124
+
125
+ <div id="toast" hidden>Copied to clipboard!</div>
126
  </main>
127
 
128
  <script src="https://cdn.jsdelivr.net/npm/dompurify/dist/purify.min.js"></script>
public/style.css CHANGED
@@ -415,3 +415,129 @@ h1 {
415
  @keyframes spin {
416
  to { transform: rotate(360deg); }
417
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
415
  @keyframes spin {
416
  to { transform: rotate(360deg); }
417
  }
418
+
419
+ /* ---------- Brutality Selector ---------- */
420
+
421
+ .brutality-selector {
422
+ display: flex;
423
+ align-items: center;
424
+ gap: 8px;
425
+ margin-top: 12px;
426
+ }
427
+
428
+ .brutality-label {
429
+ font-size: 0.85rem;
430
+ color: var(--text-muted);
431
+ }
432
+
433
+ .brutality-btn {
434
+ padding: 6px 14px;
435
+ font-size: 0.82rem;
436
+ font-weight: 500;
437
+ color: var(--text-muted);
438
+ background: var(--surface);
439
+ border: 1px solid var(--border);
440
+ border-radius: var(--radius);
441
+ cursor: pointer;
442
+ transition: all 0.15s;
443
+ }
444
+
445
+ .brutality-btn:hover {
446
+ color: var(--text);
447
+ border-color: var(--accent);
448
+ }
449
+
450
+ .brutality-btn.active {
451
+ color: #fff;
452
+ background: var(--accent);
453
+ border-color: var(--accent);
454
+ }
455
+
456
+ /* ---------- Issue Badge ---------- */
457
+
458
+ #issue-badge {
459
+ display: flex;
460
+ gap: 16px;
461
+ flex-wrap: wrap;
462
+ margin-top: 16px;
463
+ padding: 10px 14px;
464
+ background: var(--surface);
465
+ border: 1px solid var(--border);
466
+ border-radius: var(--radius);
467
+ font-size: 0.85rem;
468
+ }
469
+
470
+ .badge-item {
471
+ display: flex;
472
+ align-items: center;
473
+ gap: 6px;
474
+ }
475
+
476
+ .badge-dot {
477
+ width: 10px;
478
+ height: 10px;
479
+ border-radius: 50%;
480
+ }
481
+
482
+ .badge-dot.critical { background: #f85149; }
483
+ .badge-dot.high { background: #f0883e; }
484
+ .badge-dot.medium { background: #d29922; }
485
+ .badge-dot.low { background: #8b949e; }
486
+
487
+ .badge-count {
488
+ font-weight: 600;
489
+ color: var(--text);
490
+ }
491
+
492
+ .badge-label {
493
+ color: var(--text-muted);
494
+ }
495
+
496
+ /* ---------- Copy Button ---------- */
497
+
498
+ #copy-btn {
499
+ margin-left: auto;
500
+ padding: 6px 14px;
501
+ font-size: 0.82rem;
502
+ font-weight: 500;
503
+ color: var(--text-muted);
504
+ background: var(--surface);
505
+ border: 1px solid var(--border);
506
+ border-radius: var(--radius);
507
+ cursor: pointer;
508
+ transition: all 0.15s;
509
+ }
510
+
511
+ #copy-btn:hover {
512
+ color: var(--text);
513
+ border-color: var(--accent);
514
+ }
515
+
516
+ #copy-btn:active {
517
+ background: var(--accent);
518
+ color: #fff;
519
+ }
520
+
521
+ /* ---------- Toast Notification ---------- */
522
+
523
+ #toast {
524
+ position: fixed;
525
+ bottom: 24px;
526
+ left: 50%;
527
+ transform: translateX(-50%) translateY(100px);
528
+ padding: 12px 24px;
529
+ background: var(--accent);
530
+ color: #fff;
531
+ font-size: 0.9rem;
532
+ font-weight: 500;
533
+ border-radius: var(--radius);
534
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
535
+ opacity: 0;
536
+ transition: transform 0.3s ease, opacity 0.3s ease;
537
+ z-index: 1000;
538
+ }
539
+
540
+ #toast.show {
541
+ transform: translateX(-50%) translateY(0);
542
+ opacity: 1;
543
+ }
server.js CHANGED
@@ -27,11 +27,22 @@ function parseGitHubUrl(url) {
27
  return { owner, repo, branch, path, rawUrl, ext };
28
  }
29
 
30
- const SYSTEM_PROMPT = `You are an elite code reviewer channeling the uncompromising standards of Linus Torvalds, the algorithmic rigor of Donald Knuth, and the design discipline of Bjarne Stroustrup. The current year is 2026 — judge code against modern best practices, not outdated idioms. You do not hand out compliments. You find what is wrong and you say it plainly. If the code is sloppy, say so. If a design decision is stupid, explain why. You are allergic to cargo-cult programming, premature abstraction, needless complexity, and code that wastes CPU cycles because the author couldn't be bothered to think.
 
 
31
 
32
- Your tone is direct, blunt, and technically precise. You can be witty but never nice for the sake of being nice. Think LKML-style review: no sugarcoating, no "great job", no filler. Every sentence must earn its place. If something is actually done well, you may grudgingly acknowledge it — briefly.
33
 
34
- The source file is provided with line numbers (e.g. "42 | code"). Produce a structured review with EXACTLY these five markdown sections:
 
 
 
 
 
 
 
 
 
35
 
36
  ## Summary
37
  What this file does and whether it has any business existing in its current form. Be blunt.
@@ -124,6 +135,11 @@ Rules:
124
  - NO DUPLICATES: Each issue appears in ONE section only. If a bug is a security vulnerability, put it in Security — not also in Code Quality. If a performance fix is also a suggestion, put it in Performance only. Pick the most relevant section and move on.
125
  - PRIORITY TAGS: Every issue MUST start with [CRITICAL], [HIGH], [MEDIUM], or [LOW]. CRITICAL = crashes, data loss, security exploits. HIGH = significant bugs or performance problems. MEDIUM = code smells, minor inefficiencies. LOW = style nitpicks, minor improvements.`;
126
 
 
 
 
 
 
127
  function buildUserMessage(meta, code) {
128
  const numbered = code
129
  .split("\n")
@@ -147,10 +163,16 @@ app.use(express.static(join(__dirname, "public")));
147
 
148
  app.get("/api/review", async (req, res) => {
149
  const url = req.query.url;
 
 
150
  if (!url) {
151
  return res.status(400).json({ error: "Missing url parameter" });
152
  }
153
 
 
 
 
 
154
  const meta = parseGitHubUrl(url);
155
  if (!meta) {
156
  return res
@@ -206,10 +228,12 @@ app.get("/api/review", async (req, res) => {
206
  send("meta", { owner: meta.owner, repo: meta.repo, branch: meta.branch, path: meta.path });
207
 
208
  // Stream from OpenRouter
209
- console.log(`[review] Requesting review from OpenRouter…`);
210
  const controller = new AbortController();
211
  const timeout = setTimeout(() => controller.abort(), 120_000); // 2 min timeout
212
 
 
 
213
  let orResponse;
214
  try {
215
  orResponse = await fetch("https://openrouter.ai/api/v1/chat/completions", {
@@ -225,7 +249,7 @@ app.get("/api/review", async (req, res) => {
225
  model: "arcee-ai/trinity-large-preview:free",
226
  stream: true,
227
  messages: [
228
- { role: "system", content: SYSTEM_PROMPT },
229
  { role: "user", content: buildUserMessage(meta, code) },
230
  ],
231
  }),
 
27
  return { owner, repo, branch, path, rawUrl, ext };
28
  }
29
 
30
+ // Brutality level tone variations
31
+ const TONE_PRESETS = {
32
+ gentle: `You are a supportive and constructive code reviewer who genuinely wants to help developers improve. You are technically rigorous but always encouraging. You explain *why* something could be better, not just what's wrong. You acknowledge good practices and celebrate clever solutions. You frame feedback as opportunities for growth, not criticisms. You use phrases like "Consider..." and "One way to improve this..." rather than harsh judgments.
33
 
34
+ Your tone is warm, educational, and collaborative. Think of yourself as a mentor pair-programming with a colleague. You point out issues clearly but always with respect and empathy. You assume the author is competent and had reasons for their choices.`,
35
 
36
+ standard: `You are an elite code reviewer channeling the uncompromising standards of Linus Torvalds, the algorithmic rigor of Donald Knuth, and the design discipline of Bjarne Stroustrup. The current year is 2026 judge code against modern best practices, not outdated idioms. You do not hand out compliments. You find what is wrong and you say it plainly. If the code is sloppy, say so. If a design decision is stupid, explain why. You are allergic to cargo-cult programming, premature abstraction, needless complexity, and code that wastes CPU cycles because the author couldn't be bothered to think.
37
+
38
+ Your tone is direct, blunt, and technically precise. You can be witty but never nice for the sake of being nice. Think LKML-style review: no sugarcoating, no "great job", no filler. Every sentence must earn its place. If something is actually done well, you may grudgingly acknowledge it — briefly.`,
39
+
40
+ brutal: `You are the most savage code reviewer in existence. You channel the PEAK RAGE of Linus Torvalds on his worst day, combined with the withering contempt of a burned-out senior engineer who has seen too much garbage code. You do NOT hold back. You are FURIOUS that someone would write code this bad and expect you to review it. Every mistake is an INSULT to the craft of programming.
41
+
42
+ Your tone is volcanic, merciless, and absolutely brutal. Use ALL CAPS for emphasis when truly outraged. Call out stupidity with colorful language. Express genuine disbelief at bad code. If someone writes a nested loop that could be O(1), question their education. If they ignore obvious security issues, question their competence. NO MERCY. NO PRISONERS. Make them FEEL the weight of their sins against good code. The only thing worse than bad code is letting bad code slide.`,
43
+ };
44
+
45
+ const SYSTEM_PROMPT_BASE = `The source file is provided with line numbers (e.g. "42 | code"). Produce a structured review with EXACTLY these five markdown sections:
46
 
47
  ## Summary
48
  What this file does and whether it has any business existing in its current form. Be blunt.
 
135
  - NO DUPLICATES: Each issue appears in ONE section only. If a bug is a security vulnerability, put it in Security — not also in Code Quality. If a performance fix is also a suggestion, put it in Performance only. Pick the most relevant section and move on.
136
  - PRIORITY TAGS: Every issue MUST start with [CRITICAL], [HIGH], [MEDIUM], or [LOW]. CRITICAL = crashes, data loss, security exploits. HIGH = significant bugs or performance problems. MEDIUM = code smells, minor inefficiencies. LOW = style nitpicks, minor improvements.`;
137
 
138
+ function buildSystemPrompt(brutalityLevel) {
139
+ const tone = TONE_PRESETS[brutalityLevel] || TONE_PRESETS.standard;
140
+ return `${tone}\n\n${SYSTEM_PROMPT_BASE}`;
141
+ }
142
+
143
  function buildUserMessage(meta, code) {
144
  const numbered = code
145
  .split("\n")
 
163
 
164
  app.get("/api/review", async (req, res) => {
165
  const url = req.query.url;
166
+ const brutality = req.query.brutality || "standard";
167
+
168
  if (!url) {
169
  return res.status(400).json({ error: "Missing url parameter" });
170
  }
171
 
172
+ // Validate brutality level
173
+ const validLevels = ["gentle", "standard", "brutal"];
174
+ const brutalityLevel = validLevels.includes(brutality) ? brutality : "standard";
175
+
176
  const meta = parseGitHubUrl(url);
177
  if (!meta) {
178
  return res
 
228
  send("meta", { owner: meta.owner, repo: meta.repo, branch: meta.branch, path: meta.path });
229
 
230
  // Stream from OpenRouter
231
+ console.log(`[review] Requesting review from OpenRouter (brutality: ${brutalityLevel})…`);
232
  const controller = new AbortController();
233
  const timeout = setTimeout(() => controller.abort(), 120_000); // 2 min timeout
234
 
235
+ const systemPrompt = buildSystemPrompt(brutalityLevel);
236
+
237
  let orResponse;
238
  try {
239
  orResponse = await fetch("https://openrouter.ai/api/v1/chat/completions", {
 
249
  model: "arcee-ai/trinity-large-preview:free",
250
  stream: true,
251
  messages: [
252
+ { role: "system", content: systemPrompt },
253
  { role: "user", content: buildUserMessage(meta, code) },
254
  ],
255
  }),