@rigour-labs/cli 6.14.0-rc.2 → 7.0.0-rc.1

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.
@@ -3,7 +3,7 @@ import path from 'path';
3
3
  import chalk from 'chalk';
4
4
  import yaml from 'yaml';
5
5
  import { GateRunner, ConfigSchema, normalizeScopePatterns, deepAnalysisError, recordScore, getScoreTrend, resolveDeepOptions, getProvenanceTrends, getQualityTrend, IncrementalCache, renderFullReport } from '@rigour-labs/core';
6
- import inquirer from 'inquirer';
6
+ import { input, select, Separator } from '@inquirer/prompts';
7
7
  import { randomUUID } from 'crypto';
8
8
  import { EXIT_PASS, EXIT_FAIL, EXIT_CONFIG_ERROR, EXIT_INTERNAL_ERROR, exitCodeFor } from './exit-codes.js';
9
9
  import { deepProvider } from './deep-provider.js';
@@ -490,19 +490,11 @@ async function interactiveMode(report, config) {
490
490
  name: `[${f.id}] ${f.title}`,
491
491
  value: i
492
492
  }));
493
- choices.push(new inquirer.Separator());
493
+ choices.push(new Separator());
494
494
  choices.push({ name: 'Exit', value: -1 });
495
495
  let exit = false;
496
496
  while (!exit) {
497
- const { index } = await inquirer.prompt([
498
- {
499
- type: 'list',
500
- name: 'index',
501
- message: 'Select a violation to view details:',
502
- choices,
503
- pageSize: 15
504
- }
505
- ]);
497
+ const index = await select({ message: 'Select a violation to view details:', choices, pageSize: 15 });
506
498
  if (index === -1) {
507
499
  exit = true;
508
500
  continue;
@@ -527,7 +519,7 @@ async function interactiveMode(report, config) {
527
519
  console.log(`${chalk.bold('Verified:')} ${failure.verified ? chalk.green('Yes') : chalk.red('No')}`);
528
520
  }
529
521
  console.log(chalk.dim('\n' + '─'.repeat(40)));
530
- await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to return to list...' }]);
522
+ await input({ message: 'Press Enter to return to list...' });
531
523
  console.clear();
532
524
  console.log(chalk.bold.blue('══ Rigour Interactive Review ══\n'));
533
525
  }
@@ -850,6 +850,9 @@ export async function hooksCheckCommand(cwd, options = {}) {
850
850
  else {
851
851
  process.stdout.write(JSON.stringify(result));
852
852
  }
853
+ // What the file already had before this change: said, never blocking.
854
+ for (const note of result.notes ?? [])
855
+ process.stderr.write(`[rigour/${note.gate}] note: ${note.file}${note.line ? `:${note.line}` : ''}: ${note.message}\n`);
853
856
  if (result.status === 'fail') {
854
857
  for (const failure of result.failures) {
855
858
  const loc = failure.line ? `:${failure.line}` : '';
@@ -863,6 +866,7 @@ export async function hooksCheckCommand(cwd, options = {}) {
863
866
  /** One verdict for a hook call that checked files in several repositories. */
864
867
  function mergeHookResults(results) {
865
868
  const failures = results.flatMap(r => r.failures);
869
+ const notes = results.flatMap(r => r.notes ?? []);
866
870
  const status = results.some(r => r.status === 'error') ? 'error' : failures.length > 0 ? 'fail' : 'pass';
867
- return { status, failures, duration_ms: Math.max(0, ...results.map(r => r.duration_ms)) };
871
+ return { status, failures, ...(notes.length ? { notes } : {}), duration_ms: Math.max(0, ...results.map(r => r.duration_ms)) };
868
872
  }
@@ -8,7 +8,7 @@ import chalk from 'chalk';
8
8
  import { execa } from 'execa';
9
9
  import fs from 'fs';
10
10
  import path from 'path';
11
- import { locateTransformers, semanticRuntimeDir, TRANSFORMERS_SPEC } from '@rigour-labs/core';
11
+ import { locateTransformers, pruneSemanticRuntime, RETIRED_TRANSFORMERS_PACKAGE, retiredSemanticRuntimeInstalled, SEMANTIC_INSTALL_ENV, semanticRuntimeDir, TRANSFORMERS_SPEC } from '@rigour-labs/core';
12
12
  /** What the library takes on disk, with room to spare for npm's own cache while it installs. */
13
13
  const NEEDED_BYTES = 1024 ** 3;
14
14
  const INSTALL_TIMEOUT_MS = 10 * 60_000;
@@ -24,11 +24,23 @@ async function ensureSemanticRuntime(cwd) {
24
24
  if (!fs.existsSync(path.join(dir, 'package.json')))
25
25
  fs.writeFileSync(path.join(dir, 'package.json'), '{ "name": "rigour-semantic-runtime", "private": true }\n');
26
26
  const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
27
- const result = await execa(npm, ['install', '--no-audit', '--no-fund', '--omit=dev', TRANSFORMERS_SPEC], { cwd: dir, reject: false, timeout: INSTALL_TIMEOUT_MS });
27
+ const result = await execa(npm, ['install', '--no-audit', '--no-fund', '--omit=dev', TRANSFORMERS_SPEC], { cwd: dir, env: SEMANTIC_INSTALL_ENV, reject: false, timeout: INSTALL_TIMEOUT_MS });
28
28
  if (result.exitCode !== 0)
29
29
  return { state: 'failed', reason: String(result.stderr || result.stdout || `npm exited ${result.exitCode}`).trim().split('\n').slice(-3).join(' ') };
30
30
  const where = locateTransformers(cwd);
31
- return where ? { state: 'installed', where } : { state: 'failed', reason: `npm finished, but ${TRANSFORMERS_SPEC} does not resolve from ${dir}` };
31
+ if (!where)
32
+ return { state: 'failed', reason: `npm finished, but ${TRANSFORMERS_SPEC} does not resolve from ${dir}` };
33
+ // The copy an earlier setup installed is never loaded again, nor are other platforms' runtimes: free their space.
34
+ // A failure here costs only disk.
35
+ if (retiredSemanticRuntimeInstalled())
36
+ await execa(npm, ['uninstall', '--no-audit', '--no-fund', RETIRED_TRANSFORMERS_PACKAGE], { cwd: dir, env: SEMANTIC_INSTALL_ENV, reject: false, timeout: INSTALL_TIMEOUT_MS });
37
+ try {
38
+ pruneSemanticRuntime(dir);
39
+ }
40
+ catch {
41
+ // left in place
42
+ }
43
+ return { state: 'installed', where };
32
44
  }
33
45
  function freeBytes(dir) {
34
46
  try {
@@ -45,7 +57,7 @@ export async function setupSemantic(cwd) {
45
57
  console.log(chalk.green('✔ Semantic search is on (local embeddings)'));
46
58
  return;
47
59
  }
48
- console.log(chalk.dim('Installing semantic search: about 230 MB, once for every Rigour version on this machine (skip with --no-semantic)...'));
60
+ console.log(chalk.dim('Installing semantic search: about 250 MB on Linux, 280 MB on macOS, once for every Rigour version on this machine (skip with --no-semantic)...'));
49
61
  const result = await ensureSemanticRuntime(cwd);
50
62
  if (result.state === 'installed' || result.state === 'present')
51
63
  console.log(chalk.green('✔ Semantic search is on (local embeddings)'));
@@ -57,5 +69,7 @@ export function semanticStatusLine(cwd) {
57
69
  const where = locateTransformers(cwd);
58
70
  return where
59
71
  ? chalk.green(` ✓ Semantic search: on (${where.includes(semanticRuntimeDir()) ? "Rigour's shared copy" : 'installed with Rigour or the project'})`)
60
- : chalk.yellow(' ⚠ Semantic search: off. Recall and pattern matching use keywords, which find far less. Run: rigour setup');
72
+ : chalk.yellow(retiredSemanticRuntimeInstalled()
73
+ ? ` ⚠ Semantic search: off. It moved to ${TRANSFORMERS_SPEC}; existing indexes keep working. Run: rigour setup`
74
+ : ' ⚠ Semantic search: off. Recall and pattern matching use keywords, which find far less. Run: rigour setup');
61
75
  }
@@ -50,6 +50,28 @@ export interface LessonJourney {
50
50
  };
51
51
  /** A candidate only review bots raised, nothing waiting on a person (core quietBotCandidate): hidden until a person asks to see bot points. */
52
52
  fromBots?: true;
53
+ /**
54
+ * Teammates' decisions on a review lesson, received from the team database (core team-decisions.ts): each with the
55
+ * teammate's display name and when the team database got it; and, when a later team decision settled the lesson
56
+ * against this person's own, both, with why this machine does not share theirs (`yoursOnly`) when it does not.
57
+ */
58
+ team?: {
59
+ decisions: Array<{
60
+ kind: string;
61
+ name: string;
62
+ at: string;
63
+ detail?: string;
64
+ }>;
65
+ overruled?: {
66
+ yours: string;
67
+ team: {
68
+ kind: string;
69
+ name: string;
70
+ at: string;
71
+ };
72
+ yoursOnly?: string;
73
+ };
74
+ };
53
75
  }
54
76
  export interface StudioLearning {
55
77
  lessons: LessonJourney[];
@@ -7,7 +7,7 @@
7
7
  * lesson's subject prefix). Counts that Rigour cannot know here are null, never 0: PR catches
8
8
  * recorded on another machine (CI) never reach this one.
9
9
  */
10
- import { acceptSuggestedText, scopeLesson, decideCompiledCheck, decideLesson, fixLessonPrefix, proposeCompiledChecks, readCompiledChecks, suspension, localOutcomeMetrics, pendingDecision, quietBotCandidate, listKnowledgeLessons, readLessons } from '@rigour-labs/core';
10
+ import { acceptSuggestedText, scopeLesson, decideCompiledCheck, decideLesson, fixLessonPrefix, proposeCompiledChecks, readCompiledChecks, suspension, localOutcomeMetrics, pendingDecision, quietBotCandidate, listKnowledgeLessons, readLessons, lastDecision, readTeamDecisionCache } from '@rigour-labs/core';
11
11
  import { decider } from './git-identity.js';
12
12
  import { checkoutRoots, eventsAcross, storiesAcross } from './studio-checkouts.js';
13
13
  const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
@@ -71,6 +71,18 @@ export function buildLearning(input) {
71
71
  });
72
72
  return { lessons, weeks, prRecorded };
73
73
  }
74
+ /** A review lesson's team decisions for Studio, and whether a later one settled it against this person's own. */
75
+ function teamOn(lesson, sharing) {
76
+ const decisions = lesson.evidence.filter(e => e.team).map(e => ({ kind: e.kind ?? 'point', name: e.team.name, at: e.team.receivedAt, ...(e.detail ? { detail: e.detail } : {}) }));
77
+ if (decisions.length === 0)
78
+ return {};
79
+ const settled = lastDecision(lesson);
80
+ const yours = lesson.evidence.filter(e => !e.team && e.author && (e.kind === 'accepted' || e.kind === 'rejected')).at(-1);
81
+ const overruled = settled?.team && yours?.kind && yours.kind !== settled.kind
82
+ ? { yours: yours.kind, team: { kind: settled.kind ?? 'accepted', name: settled.team.name, at: settled.team.receivedAt }, ...(sharing && !sharing.shares && sharing.reason ? { yoursOnly: sharing.reason } : {}) }
83
+ : undefined;
84
+ return { team: { decisions, ...(overruled ? { overruled } : {}) } };
85
+ }
74
86
  function learnedFromPoints(points) {
75
87
  const authors = [...new Set(points.map(e => e.author).filter(Boolean))];
76
88
  return `At PR ${[...new Set(points.map(e => `#${e.pr}`))].join(', ')}${authors.length ? `, from ${authors.join(', ')}` : ''}`;
@@ -78,7 +90,8 @@ function learnedFromPoints(points) {
78
90
  /**
79
91
  * What a person can decide on a review lesson, from the last of its evidence and decisions: taken back by evidence
80
92
  * (promote it again, or drop it), back to a candidate when outcomes stopped promoting (promote it again, or dismiss),
81
- * or a candidate with a later fix on its lines (promote it, or dismiss the evidence).
93
+ * a candidate with a later fix on its lines (promote it, or dismiss the evidence), or any other candidate (promote it,
94
+ * or reject it): a person confirming a review point is how it becomes a lesson.
82
95
  */
83
96
  function decisionFor(lesson) {
84
97
  const last = pendingDecision(lesson);
@@ -88,7 +101,7 @@ function decisionFor(lesson) {
88
101
  return { canDecide: true, takenBack: { detail: last.detail ?? '', prs: lesson.evidence.filter(e => e.kind === 'against').map(e => e.pr), at: last.at ?? '' } };
89
102
  if (last?.kind === 'lines')
90
103
  return { canDecide: true, suggested: { detail: last.detail ?? '', pr: last.pr, at: last.at ?? '' } };
91
- return { canDecide: false };
104
+ return { canDecide: lesson.state === 'candidate' };
92
105
  }
93
106
  /** The reason a person gave in Studio, else that they decided there. */
94
107
  function studioWhy(why) {
@@ -151,10 +164,14 @@ export function proposeChecksFromStudio(cwd) {
151
164
  }
152
165
  export async function loadLearning(cwd, now = new Date(), weeks = WEEKS) {
153
166
  const roots = checkoutRoots(cwd);
154
- const learning = buildLearning({ now, lessons: await listKnowledgeLessons(cwd), reviewLessons: readLessons(cwd), stories: storiesAcross(roots), events: eventsAcross(roots), weeks });
167
+ const reviewLessons = readLessons(cwd);
168
+ const built = buildLearning({ now, lessons: await listKnowledgeLessons(cwd), reviewLessons, stories: storiesAcross(roots), events: eventsAcross(roots), weeks });
169
+ // Teammates' decisions (team database), with why this machine does not share its person's when it does not.
170
+ const sharing = readTeamDecisionCache(cwd)?.sharing;
171
+ const byId = new Map(reviewLessons.map(l => [l.id, l]));
172
+ const learning = { ...built, lessons: built.lessons.map(j => (byId.has(j.id) ? { ...j, ...teamOn(byId.get(j.id), sharing) } : j)) };
155
173
  const outcomes = localOutcomeMetrics(cwd);
156
- const lessons = new Map(readLessons(cwd).map(l => [l.id, l]));
157
- const withChecks = { ...learning, compiled: readCompiledChecks(cwd).map(c => ({ ...c, ...(suspension(c, lessons) ? { suspended: suspension(c, lessons) } : {}) })) };
174
+ const withChecks = { ...learning, compiled: readCompiledChecks(cwd).map(c => ({ ...c, ...(suspension(c, byId) ? { suspended: suspension(c, byId) } : {}) })) };
158
175
  return outcomes ? { ...withChecks, outcomes } : withChecks;
159
176
  }
160
177
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rigour-labs/cli",
3
- "version": "6.14.0-rc.2",
3
+ "version": "7.0.0-rc.1",
4
4
  "description": "Code review that happens while your agent writes: deterministic checks on the lines it changed, quiet unless it can prove a defect, and learning from your team's fixes and dismissals. Works with Claude Code, Cursor, Codex, Cline and Windsurf.",
5
5
  "engines": {
6
6
  "node": ">=22.13"
@@ -50,22 +50,21 @@
50
50
  "provenance": true
51
51
  },
52
52
  "dependencies": {
53
+ "@inquirer/prompts": "^8.7.3",
53
54
  "chalk": "^5.3.0",
54
55
  "commander": "^12.0.0",
55
56
  "cosmiconfig": "^9.0.0",
56
57
  "execa": "^8.0.1",
57
58
  "fs-extra": "^11.4.0",
58
59
  "globby": "^14.0.1",
59
- "inquirer": "9.2.16",
60
60
  "ora": "^8.0.1",
61
61
  "yaml": "^2.8.2",
62
- "@rigour-labs/core": "6.14.0-rc.2"
62
+ "@rigour-labs/core": "7.0.0-rc.1"
63
63
  },
64
64
  "devDependencies": {
65
65
  "@types/fs-extra": "^11.0.4",
66
- "@types/inquirer": "9.0.7",
67
66
  "@types/node": "^25.0.3",
68
- "@rigour-labs/studio": "6.14.0-rc.2"
67
+ "@rigour-labs/studio": "7.0.0-rc.1"
69
68
  },
70
69
  "scripts": {
71
70
  "build": "tsc && pnpm bundle-studio",
@@ -0,0 +1 @@
1
+ .st-page{max-width:1040px;margin:0 auto;padding:8px 4px 64px;color:var(--text-main)}.st-h1{margin:0;font-size:28px;font-weight:600;line-height:1.3;color:var(--text-primary)}.st-lead{margin:6px 0 0;font-size:15px;line-height:1.6;color:var(--text-dim);max-width:720px}.st-sub{font-size:13px;color:var(--text-dim)}.st-mono{font-family:IBM Plex Mono,JetBrains Mono,ui-monospace,monospace}.st-card{border:1px solid var(--border-dim);border-radius:12px;padding:20px;background:var(--bg-card)}.st-grid{display:grid;grid-template-columns:minmax(0,1fr) 300px;gap:28px;margin-top:28px}.st-stack{display:flex;flex-direction:column;gap:10px}.st-side{display:flex;flex-direction:column;gap:16px}.st-big{font-size:40px;font-weight:600;line-height:1;color:var(--text-primary)}.st-btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:40px;padding:0 18px;border:1px solid var(--text-dim);border-radius:8px;background:var(--bg-card);color:var(--text-primary);font:inherit;font-size:14px;font-weight:500;cursor:pointer;transition:background .12s,border-color .12s}.st-btn:hover{border-color:var(--text-primary);background:var(--bg-accent)}.st-btn:focus-visible{outline:2px solid var(--accent-primary);outline-offset:2px}.st-btn:disabled{opacity:.45;cursor:not-allowed}.st-btn.primary{background:var(--accent-primary);border-color:var(--accent-primary);color:#fff}.st-btn.primary:hover{background:var(--accent-tertiary);border-color:var(--accent-tertiary)}.st-chip{display:inline-block;padding:3px 10px;border-radius:999px;font-size:12px;background:var(--bg-accent);color:var(--text-dim);white-space:nowrap}.st-chip.ok{background:color-mix(in srgb,var(--status-success) 14%,transparent);color:var(--status-success)}.st-chip.warn{background:color-mix(in srgb,var(--status-warning) 16%,transparent);color:var(--status-warning)}.st-chip.bad{background:color-mix(in srgb,var(--status-error) 14%,transparent);color:var(--status-error)}.st-need{border:1px solid color-mix(in srgb,var(--status-warning) 45%,transparent);border-radius:12px;padding:20px;background:color-mix(in srgb,var(--status-warning) 6%,var(--bg-card))}.st-story{display:block;width:100%;padding:18px 20px;border:1px solid var(--border-dim);border-radius:12px;background:var(--bg-card);color:inherit;font:inherit;text-align:left;cursor:pointer}.st-story:hover{border-color:var(--border-med)}.st-story-body{border:1px solid var(--border-dim);border-top:0;border-radius:0 0 12px 12px;margin-top:-10px;padding:20px;background:var(--bg-surface)}.st-code{background:var(--code-bg);border:1px solid var(--border-dim);border-radius:8px;padding:10px 0;font-size:13px;line-height:1.7;overflow-x:auto}.st-ln{display:block;padding:0 14px;white-space:pre}.st-ln.add{background:color-mix(in srgb,var(--status-success) 12%,transparent)}.st-ln.del{background:color-mix(in srgb,var(--status-error) 12%,transparent)}.st-steps{display:flex;gap:24px}.st-step{flex:1;display:flex;gap:12px;align-items:flex-start;font-size:14px}.st-num{flex:none;width:28px;height:28px;border-radius:50%;background:var(--bg-accent);color:var(--accent-primary);display:flex;align-items:center;justify-content:center;font-weight:600}.st-journey{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));margin-top:16px;border:1px solid var(--border-dim);border-radius:10px;overflow:hidden}.st-journey>div{padding:12px 14px}.st-journey>div+div{border-left:1px solid var(--border-dim)}.st-bars{display:flex;gap:28px;align-items:flex-end;height:120px;margin-top:18px}.st-bar{flex:1;display:flex;flex-direction:column;align-items:center;gap:6px}.st-bar span{width:100%;max-width:72px;border-radius:6px 6px 0 0;background:var(--accent-primary)}.st-row{display:flex;gap:16px;align-items:center}.st-empty{border:1px dashed var(--border-med);border-radius:12px;padding:24px;color:var(--text-dim);font-size:14px;line-height:1.6}.st-inline-code{font-size:.9em;padding:1px 5px;border-radius:4px;background:var(--bg-accent)}.st-table{width:100%;border-collapse:collapse;font-size:14px}.st-table th{text-align:left;font-weight:500;font-size:12px;color:var(--text-dim);padding:8px 20px;border-bottom:1px solid var(--border-dim);white-space:nowrap}.st-table td{padding:10px 20px;border-bottom:1px solid var(--border-dim);font-variant-numeric:tabular-nums}.st-table tr:last-child td{border-bottom:0}@media(max-width:820px){.st-grid{grid-template-columns:minmax(0,1fr)}.st-steps,.st-row{flex-direction:column;align-items:flex-start}.st-journey{grid-template-columns:repeat(2,minmax(0,1fr))}}.st-seg{display:inline-flex;padding:2px;gap:2px;border:1px solid var(--text-dim);border-radius:8px;background:var(--bg-card)}.st-seg button{min-height:30px;padding:0 12px;border:0;border-radius:6px;background:transparent;color:var(--text-dim);font:inherit;font-size:13px;cursor:pointer;white-space:nowrap;transition:background .12s,color .12s}.st-seg button:hover:not(:disabled):not(.on){background:var(--bg-accent);color:var(--text-primary)}.st-seg button.on{background:var(--accent-primary);color:#fff;font-weight:500}.st-seg button:focus-visible{outline:2px solid var(--accent-primary);outline-offset:1px}.st-seg button:disabled{cursor:not-allowed}.st-seg button:disabled:not(.on){opacity:.5}.st-setting{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:12px 24px;align-items:start}.st-setting-controls{display:grid;grid-template-columns:auto auto;gap:8px 12px;align-items:center;justify-items:start}@media(max-width:760px){.st-setting{grid-template-columns:1fr}}:root{--bg-main: #f7f8fa;--bg-sidebar: #ffffff;--bg-header: #ffffff;--bg-card: #ffffff;--bg-accent: #eef0f4;--bg-surface: #f3f4f7;--border-dim: #e3e5ea;--border-med: #d2d6de;--border-color: #e3e5ea;--text-main: #14161a;--text-dim: #5e6573;--text-vibrant: #000000;--text-primary: #14161a;--accent-primary: #4f46e5;--accent-secondary: #6366f1;--accent-tertiary: #3730a3;--accent-glow: rgba(79, 70, 229, .1);--status-ready: #17663f;--status-success: #17663f;--status-error: #a12a2a;--status-warning: #8a5300;--code-bg: #f1f2f5;--gradient-accent: linear-gradient(135deg, #4f46e5 0%, #6366f1 100%)}[data-theme=dark]{--bg-main: #050506;--bg-sidebar: rgba(10, 10, 15, .72);--bg-header: rgba(10, 10, 15, .65);--bg-card: rgba(255, 255, 255, .04);--bg-accent: rgba(255, 255, 255, .06);--bg-surface: rgba(13, 13, 18, .85);--border-dim: rgba(255, 255, 255, .08);--border-med: rgba(255, 255, 255, .12);--border-color: rgba(255, 255, 255, .08);--text-main: #ededeef2;--text-dim: #8a8f98;--text-vibrant: #ffffff;--text-primary: #f8fafc;--accent-primary: #818cf8;--accent-secondary: #a5b4fc;--accent-tertiary: #c7d2fe;--accent-glow: rgba(129, 140, 248, .18);--status-ready: #34d399;--status-success: #34d399;--status-error: #f87171;--status-warning: #fbbf24;--code-bg: rgba(0, 0, 0, .35);--radius-card: 16px;--easing-cinema: cubic-bezier(.16, 1, .3, 1);--gradient-accent: linear-gradient(135deg, #6366f1 0%, #818cf8 100%);--bg-cinema: linear-gradient(180deg, #0a0a0f 0%, #050506 100%)}*{box-sizing:border-box;margin:0;padding:0}body{font-family:Fira Sans,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;background:var(--bg-cinema, var(--bg-main));background-color:var(--bg-main);color:var(--text-main);-webkit-font-smoothing:antialiased;overflow:hidden;letter-spacing:-.01em}code,pre,.font-mono,.kpi-value,.overview-dl dd{font-family:Fira Code,JetBrains Mono,ui-monospace,monospace}.studio{display:flex;height:100vh;width:100vw;position:relative;isolation:isolate}.sidebar{width:190px;background-color:var(--bg-sidebar);border-right:1px solid var(--border-dim);display:flex;flex-direction:column;padding:16px;flex-shrink:0}.brand{display:flex;align-items:center;gap:12px;padding:8px 12px;margin-bottom:32px}.logo-icon{width:28px;height:28px;background:linear-gradient(135deg,var(--accent-primary),var(--accent-secondary));border-radius:6px;display:flex;align-items:center;justify-content:center;font-weight:800;color:#fff;font-size:16px}.brand span{font-weight:700;font-size:18px;color:var(--text-vibrant);letter-spacing:-.5px}.version-pill{font-size:10px;background-color:var(--bg-accent);color:var(--text-dim);padding:2px 6px;border-radius:4px;font-family:JetBrains Mono,monospace;margin-left:auto}nav{display:flex;flex-direction:column;gap:4px;flex-grow:1}.nav-item{display:flex;align-items:center;gap:12px;padding:12px;min-height:44px;border-radius:8px;color:var(--text-dim);text-decoration:none;font-size:14px;font-weight:500;background:transparent;border:none;cursor:pointer;width:100%;text-align:left;transition:all .2s ease;position:relative}.status-indicator.connecting .pulse-emitter{background:var(--status-warning)}.status-indicator.offline .pulse-emitter{background:var(--status-error);animation:none}.system-health{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:8px;margin:10px 16px 0;padding:10px;border:1px solid var(--border-dim);border-radius:12px;background:var(--bg-surface)}.system-health.loading,.system-health.unavailable{display:flex;align-items:center;min-height:52px;color:var(--text-dim)}.system-health.unavailable{color:var(--status-error)}.system-health.unavailable button{margin-left:auto;min-height:36px}.system-health.stale{border-color:var(--status-warning)}.health-item{display:grid;grid-template-columns:auto 1fr;gap:2px 8px;min-width:0;padding:7px 8px;border-left:2px solid var(--border-med)}.health-item.ready{border-left-color:var(--status-ready)}.health-item.warming{border-left-color:var(--status-warning)}.health-item.degraded{border-left-color:var(--status-error)}.health-item>span{min-width:0}.health-item strong,.health-item small{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.health-item strong{font-size:12px}.health-item small{margin-top:2px;color:var(--text-dim);font-size:10px}.health-state{grid-column:2;color:var(--text-dim);font-size:10px;text-transform:uppercase}@media(max-width:1200px){.system-health{grid-template-columns:repeat(3,minmax(0,1fr))}}@media(max-width:760px){.system-health{grid-template-columns:1fr 1fr}}.nav-item:hover{background-color:var(--bg-accent);color:var(--text-main)}.nav-item.active{color:var(--text-vibrant)}.sidebar-footer{margin-top:auto;padding-top:16px;border-top:1px solid var(--border-dim);display:flex;align-items:center;gap:8px}.trust-indicator{display:flex;align-items:center;gap:6px;font-size:11px;color:var(--text-dim);padding:6px 10px;background:#10b9810d;border:1px solid rgba(16,185,129,.1);border-radius:100px;margin-right:auto}.main-content{flex-grow:1;display:flex;flex-direction:column;overflow:hidden}header{height:56px;background-color:var(--bg-header);border-bottom:1px solid var(--border-dim);display:flex;align-items:center;justify-content:space-between;padding:0 24px;flex-shrink:0}.header-right{display:flex;align-items:center;gap:20px}.project-identity{display:flex;align-items:center;gap:12px;background:var(--bg-accent);padding:4px 12px;border-radius:8px;border:1px solid var(--border-med)}.project-name{font-weight:700;font-size:14px;color:var(--text-vibrant)}.connection-status{display:flex;align-items:center;gap:8px;font-size:11px;color:var(--text-dim);background:#22d3ee0d;padding:4px 10px;border-radius:6px;border:1px solid rgba(34,211,238,.1);font-family:JetBrains Mono,monospace}.status-indicator{display:flex;align-items:center;gap:6px;font-size:11px;font-weight:700;color:var(--status-ready)}.pulse-emitter{width:6px;height:6px;background-color:var(--status-ready);border-radius:50%;position:relative;box-shadow:0 0 10px var(--status-ready)}.pulse-emitter:after{content:"";position:absolute;width:100%;height:100%;background-color:var(--status-ready);border-radius:50%;animation:pulse 2s infinite}@keyframes pulse{0%{transform:scale(1);opacity:.8}to{transform:scale(3);opacity:0}}.view-container{flex-grow:1;padding:32px;overflow-y:auto}.health-popover{position:fixed;z-index:50;top:64px;right:18px;width:min(620px,calc(100vw - 36px));padding:12px;border:1px solid var(--border-med);border-radius:13px;background:color-mix(in srgb,var(--bg-sidebar) 96%,transparent);box-shadow:0 24px 60px #0000006b;-webkit-backdrop-filter:blur(18px);backdrop-filter:blur(18px)}.health-popover-title{display:flex;align-items:center;justify-content:space-between;padding:2px 4px 10px}.health-popover-title strong{font-size:12px}.health-popover-title button{width:30px;height:30px;display:grid;place-items:center;border:1px solid var(--border-dim);border-radius:8px;color:var(--text-dim);background:var(--bg-accent);cursor:pointer}.health-popover .system-health{margin:0;grid-template-columns:repeat(2,minmax(0,1fr))}.connection-status{cursor:pointer}.empty-state{text-align:center;display:flex;flex-direction:column;align-items:center;gap:16px;max-width:320px}.empty-state h3{font-weight:600;color:var(--text-vibrant)}.empty-state p{font-size:14px;color:var(--text-dim);line-height:1.5}.folder-icon{color:var(--accent-primary);opacity:.8}::-webkit-scrollbar{width:8px}::-webkit-scrollbar-track{background:var(--bg-main)}::-webkit-scrollbar-thumb{background:var(--border-med);border-radius:10px}::-webkit-scrollbar-thumb:hover{background:var(--text-dim)}.theme-toggle{background:var(--bg-accent);border:1px solid var(--border-dim);border-radius:8px;padding:8px;cursor:pointer;color:var(--text-dim);display:flex;align-items:center;transition:all .2s}.theme-toggle:hover{color:var(--text-main);border-color:var(--border-med)}.refresh-btn{background:var(--bg-accent);border:1px solid var(--border-dim);border-radius:8px;padding:8px;cursor:pointer;color:var(--text-dim);transition:all .2s}.refresh-btn:hover{color:var(--accent-primary);border-color:var(--accent-primary)}.refresh-btn .spinning{animation:spin 1s linear infinite}.learned-rules{margin-top:16px;display:flex;flex-direction:column;gap:10px}.learned-rules h4{font-size:12px;color:var(--text-dim);text-transform:uppercase}@keyframes rotate-spark{0%{transform:rotate(0) scale(1)}50%{transform:rotate(180deg) scale(1.2)}to{transform:rotate(360deg) scale(1)}}.spinning{animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes pulse-alert{0%,to{opacity:1}50%{opacity:.6}}@keyframes spin-anim{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.overview-banner{display:flex;gap:12px;padding:12px 14px;border-radius:8px;border:1px solid rgba(217,119,6,.35);background:#d9770614;font-size:.85rem}.overview-banner strong{display:block;margin-bottom:2px}.overview-banner p{color:var(--text-dim)}.readonly-note{align-items:center;padding:8px 12px}.readonly-note span{flex:1}.readonly-note-close{background:none;border:0;color:var(--text-dim);cursor:pointer;display:flex;padding:2px}@keyframes blob-drift{0%{transform:translateZ(0) scale(1)}to{transform:translate3d(40px,24px,0) scale(1.08)}}@media(prefers-reduced-motion:reduce){.ambient-blob,.pulse-emitter{animation:none!important}}.studio>.sidebar,.studio>.main-content{position:relative;z-index:1}.glass-shell,.glass-card{backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);border:1px solid var(--border-dim);border-radius:var(--radius-card, 16px)}[data-theme=dark] .sidebar.glass-shell{background:var(--bg-sidebar);border-right:1px solid var(--border-dim);box-shadow:inset -1px 0 #22d3ee0a}[data-theme=dark] header.glass-shell{background:var(--bg-header);border-bottom:1px solid var(--border-dim)}[data-theme=dark] .glass-card{background:var(--bg-card)}.nav-item{cursor:pointer;transition:background .2s var(--easing-cinema, ease),color .2s ease,transform .16s ease}.nav-item:active{transform:scale(.98)}.nav-item:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible{outline:2px solid var(--accent-primary);outline-offset:2px}[data-theme=dark] .nav-item.active{box-shadow:0 0 0 1px #22d3ee40,0 0 24px #22d3ee14}.project-version-pill{font-size:.72rem;color:var(--accent-secondary);border:1px solid var(--border-dim);border-radius:999px;padding:1px 8px;font-family:Fira Code,monospace}@keyframes shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}@media(max-width:920px){.view-container{padding:20px}.main-content>header{gap:8px;padding:0 14px}.project-identity{min-width:0;gap:8px}.project-name{max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.header-right{gap:8px}}