amicus 1.4.0 → 1.5.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.
@@ -5,6 +5,8 @@
5
5
  * Supports two modes: 'sidecar' (default) and 'setup'.
6
6
  */
7
7
 
8
+ const { tokenCss } = require('../src/design/tokens');
9
+
8
10
  const TOOLBAR_H = 40;
9
11
 
10
12
  /**
@@ -36,6 +38,7 @@ function buildToolbarHTML(options = {}) {
36
38
  const brandName = getBrandName(client);
37
39
 
38
40
  const baseStyles = `
41
+ ${tokenCss({ absoluteFontUrls: true })}
39
42
  * { margin: 0; padding: 0; box-sizing: border-box; }
40
43
  body {
41
44
  position: fixed;
@@ -43,40 +46,41 @@ function buildToolbarHTML(options = {}) {
43
46
  left: 0;
44
47
  right: 0;
45
48
  height: ${TOOLBAR_H}px;
46
- background: #2D2B2A;
49
+ background: var(--surface-1);
47
50
  display: flex;
48
51
  align-items: center;
49
52
  justify-content: space-between;
50
53
  padding: 0 14px;
51
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
52
- border-top: 1px solid #3D3A38;
54
+ font-family: var(--font-sans), -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
55
+ border-top: 1px solid var(--border);
53
56
  -webkit-app-region: no-drag;
54
57
  user-select: none;
55
58
  }
56
59
  .info {
57
- color: #A09B96;
60
+ color: var(--text-2);
58
61
  font-size: 12px;
59
62
  display: flex;
60
63
  align-items: center;
61
64
  gap: 12px;
62
65
  }
63
66
  .brand {
64
- color: #D97757;
67
+ color: var(--accent);
65
68
  font-size: 11px;
66
69
  font-weight: 600;
67
70
  letter-spacing: 0.8px;
68
71
  text-transform: uppercase;
69
72
  }
70
- .sep { color: #3D3A38; font-size: 14px; }
73
+ .logo path { stroke: var(--accent); }
74
+ .sep { color: var(--border-strong); font-size: 14px; }
71
75
  .detail, .timer {
72
- color: #7A756F;
76
+ color: var(--text-3);
73
77
  font-size: 11px;
74
- font-family: 'SF Mono', Menlo, Monaco, monospace;
78
+ font-family: var(--font-mono), 'SF Mono', Menlo, Monaco, monospace;
75
79
  }
76
80
  .action-btn {
77
81
  padding: 5px 14px;
78
- background: #D97757;
79
- color: #FFF;
82
+ background: var(--accent);
83
+ color: var(--on-accent);
80
84
  border: none;
81
85
  border-radius: 4px;
82
86
  font-size: 12px;
@@ -84,15 +88,15 @@ function buildToolbarHTML(options = {}) {
84
88
  cursor: pointer;
85
89
  transition: background 0.15s;
86
90
  }
87
- .action-btn:hover { background: #C4623F; }
91
+ .action-btn:hover { background: var(--accent-hover); }
88
92
  .action-btn:disabled { opacity: 0.5; cursor: default; }
89
93
  .icon-btn {
90
- background: none; border: 1px solid #3D3A38;
91
- border-radius: 4px; color: #A09B96; cursor: pointer;
94
+ background: none; border: 1px solid var(--border);
95
+ border-radius: 4px; color: var(--text-2); cursor: pointer;
92
96
  font-size: 14px; padding: 3px 8px; transition: all 0.15s;
93
97
  display: flex; align-items: center;
94
98
  }
95
- .icon-btn:hover { border-color: #D97757; color: #D97757; }
99
+ .icon-btn:hover { border-color: var(--accent); color: var(--accent); }
96
100
  .right-actions { display: flex; align-items: center; gap: 8px; }
97
101
  .update-banner {
98
102
  position: fixed;
@@ -100,41 +104,41 @@ function buildToolbarHTML(options = {}) {
100
104
  left: 0;
101
105
  right: 0;
102
106
  height: 32px;
103
- background: #3D3A38;
104
- border-bottom: 1px solid #4D4A48;
107
+ background: var(--surface-2);
108
+ border-bottom: 1px solid var(--border-strong);
105
109
  display: none;
106
110
  align-items: center;
107
111
  justify-content: center;
108
112
  gap: 10px;
109
113
  font-size: 12px;
110
- color: #D4D0CC;
114
+ color: var(--text-1);
111
115
  z-index: 100;
112
116
  }
113
117
  .update-banner .update-btn {
114
118
  padding: 2px 10px;
115
- background: #D97757;
116
- color: #FFF;
119
+ background: var(--accent);
120
+ color: var(--on-accent);
117
121
  border: none;
118
122
  border-radius: 3px;
119
123
  font-size: 11px;
120
124
  cursor: pointer;
121
125
  transition: background 0.15s;
122
126
  }
123
- .update-banner .update-btn:hover { background: #C4623F; }
127
+ .update-banner .update-btn:hover { background: var(--accent-hover); }
124
128
  .update-banner .update-btn:disabled { opacity: 0.5; cursor: default; }
125
129
  .update-banner .dismiss-btn {
126
130
  background: none;
127
131
  border: none;
128
- color: #7A756F;
132
+ color: var(--text-3);
129
133
  cursor: pointer;
130
134
  font-size: 14px;
131
135
  padding: 0 4px;
132
136
  }
133
- .update-banner .dismiss-btn:hover { color: #D4D0CC; }`;
137
+ .update-banner .dismiss-btn:hover { color: var(--text-1); }`;
134
138
 
135
- const logoSvg = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
136
- <path d="M3 2v12" stroke="#D97757" stroke-width="2" stroke-linecap="round"/>
137
- <path d="M10 2v5c0 2-3 3-7 5" stroke="#D97757" stroke-width="2" stroke-linecap="round" stroke-opacity="0.6"/>
139
+ const logoSvg = `<svg class="logo" width="16" height="16" viewBox="0 0 16 16" fill="none">
140
+ <path d="M3 2v12" stroke-width="2" stroke-linecap="round"/>
141
+ <path d="M10 2v5c0 2-3 3-7 5" stroke-width="2" stroke-linecap="round" stroke-opacity="0.6"/>
138
142
  </svg>`;
139
143
 
140
144
  if (mode === 'setup') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amicus",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Multi-model LLM Council + parallel AI window for Claude Code. Run structured council reviews across Gemini, GPT, DeepSeek and more — or fork a conversation to any model and fold the results back.",
5
5
  "keywords": [
6
6
  "claude",
@@ -330,7 +330,7 @@ This stage updates `MODEL-NOTES.md` to make future runs better. **Nothing is wri
330
330
 
331
331
  Draft new or updated entries for the per-model sections of `MODEL-NOTES.md` that capture what was learned.
332
332
 
333
- **Ledger auto-append (automatic — no approval required).** After `verdict.json` is written, the ledger row is appended automatically when the tally record is finalized — `ledger.appendRun(record)` writes one row per (run × model) to the append-only `council-ledger.jsonl` under `getConfigDir()`. The run summary shows the appended row. This is a deterministic, content-free model-level record (no finding text, no claim strings, no artifact body content). The quantitative reviewer-reliability data in `MODEL-NOTES.md` is now sourced entirely from `amicus council stats` (which aggregates the ledger) — **do not hand-edit reliability numbers in MODEL-NOTES**.
333
+ **Ledger auto-append (automatic — no approval required).** Running `amicus council tally` (the Stage-2 finalize call) appends one row per (run × model) to the append-only `council-ledger.jsonl` under `getConfigDir()` — no separate step is needed. Pass `--no-ledger` to compute a tally record *without* recording it (e.g. a re-tally that shouldn't double-count). The run summary shows the appended row. This is a deterministic, content-free model-level record (no finding text, no claim strings, no artifact body content). The quantitative reviewer-reliability data in `MODEL-NOTES.md` is now sourced entirely from `amicus council stats` (which aggregates the ledger) — **do not hand-edit reliability numbers in MODEL-NOTES**.
334
334
 
335
335
  **Compose the proposed MODEL-NOTES diff.** Combine the run-lessons updates and the reviewer-reliability table updates into a single proposed diff (old → new for every changed section). **Write the full diff to a file in the run folder** — `_tmp-proposed-model-notes-update.md` — so the user can open and review it before deciding. Presenting the diff as chat text alone is **not sufficient**: an approval dialog can hide the chat transcript, so the user may be asked to decide on a diff they never saw.
336
336
 
@@ -2,15 +2,15 @@
2
2
  'use strict';
3
3
  const fs = require('fs');
4
4
  const { tally } = require('./council/tally');
5
- const { deriveReliability } = require('./council/ledger');
5
+ const { deriveReliability, appendRun } = require('./council/ledger');
6
6
  const { sumWaveUsage, formatCost } = require('./utils/pricing');
7
7
  const { failJson, ERROR_CODES } = require('./utils/error-doc');
8
8
  const { buildReport } = require('./council/report');
9
9
 
10
- function runTally(inputPath, useJson) {
10
+ function runTally(inputPath, useJson, opts = {}) {
11
11
  if (!inputPath) {
12
12
  return failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: 'council tally needs an <input.json> path',
13
- hint: 'amicus council tally <input.json> [--json]' });
13
+ hint: 'amicus council tally <input.json> [--json] [--no-ledger]' });
14
14
  }
15
15
  let input;
16
16
  try { input = JSON.parse(fs.readFileSync(inputPath, 'utf-8')); }
@@ -24,6 +24,13 @@ function runTally(inputPath, useJson) {
24
24
  return failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: `malformed tally input: ${e.message}`,
25
25
  hint: 'input needs meta.models, findings[], adjudications[], rankings[]' });
26
26
  }
27
+ // Auto-append the run to the reliability ledger (consumed by `amicus council
28
+ // stats`). Tally is the council's finalize step, so this is where the row is
29
+ // recorded. Best-effort: a ledger write failure must not fail the tally.
30
+ if (opts.append !== false) {
31
+ try { appendRun(record); }
32
+ catch (e) { process.stderr.write(`Notice: council ledger append failed: ${e.message}\n`); }
33
+ }
27
34
  process.stdout.write(useJson ? JSON.stringify(record, null, 2) + '\n' : renderRecord(record));
28
35
  return 0;
29
36
  }
@@ -84,7 +91,7 @@ function runReport(args, useJson) {
84
91
  async function handleCouncil(args) {
85
92
  const sub = args._[1];
86
93
  const useJson = !!args.json;
87
- if (sub === 'tally') { return runTally(args._[2], useJson); }
94
+ if (sub === 'tally') { return runTally(args._[2], useJson, { append: !args['no-ledger'] }); }
88
95
  if (sub === 'stats') { return runStats(useJson); }
89
96
  if (sub === 'report') { return runReport(args, useJson); }
90
97
  return failJson(useJson, { code: ERROR_CODES.BAD_ARGS,
package/src/cli.js CHANGED
@@ -113,6 +113,7 @@ function isBooleanFlag(key) {
113
113
  'no-validate-model',
114
114
  'remove', // used by 'key' command only; other handlers ignore it
115
115
  'no-cost-gate', // disable the budget gate for this run
116
+ 'no-ledger', // council tally: compute the record without appending to the reliability ledger
116
117
  'html', // council report: emit a self-contained HTML page
117
118
  'md', // council report: emit Markdown (default)
118
119
  ];
@@ -9,8 +9,20 @@
9
9
 
10
10
  const { formatCost } = require('../utils/pricing');
11
11
  const { TIER_ORDER, SYMBOL } = require('./report');
12
+ const { tokenCss } = require('../design/tokens');
12
13
 
13
- const TIER_COLOR = { Disputed: '#fde2e1', Contested: '#fef3c7', Confirmed: '#dcfce7', Singleton: '#e5e7eb' };
14
+ const TIER_VAR = {
15
+ Disputed: 'var(--tier-disputed)',
16
+ Contested: 'var(--tier-contested)',
17
+ Confirmed: 'var(--tier-confirmed)',
18
+ Singleton: 'var(--tier-singleton)',
19
+ };
20
+ const TIER_INK = {
21
+ Disputed: 'var(--tier-disputed-ink)',
22
+ Contested: 'var(--tier-contested-ink)',
23
+ Confirmed: 'var(--tier-confirmed-ink)',
24
+ Singleton: 'var(--tier-singleton-ink)',
25
+ };
14
26
 
15
27
  function esc(s) {
16
28
  return String(s === null || s === undefined ? '' : s)
@@ -27,9 +39,10 @@ function renderHtml(m) {
27
39
  const v = f.byJudge[j];
28
40
  return `<td class="c">${v ? SYMBOL[v] : ''}${j === f.raiser ? '<sup>*</sup>' : ''}</td>`;
29
41
  }).join('');
30
- return `<tr style="background:${TIER_COLOR[f.tier] || '#fff'}">` +
42
+ return `<tr style="background:${TIER_VAR[f.tier] || '#fff'}">` +
31
43
  `<td>${esc(f.id)}</td><td>${esc(f.severity)}</td><td>${esc(f.raiser)}</td>${cells}` +
32
- `<td>${esc(f.tier)}</td><td>${esc(f.decision || '')}</td></tr>`;
44
+ `<td style="color:${TIER_INK[f.tier] || 'inherit'};font-weight:600">${esc(f.tier)}</td>` +
45
+ `<td>${esc(f.decision || '')}</td></tr>`;
33
46
  }).join('');
34
47
  const credRows = m.streetCred.map(s =>
35
48
  `<tr><td>${esc(s.model)}</td><td>${num(s.peersOnly)}</td><td>${num(s.withSelf)}</td></tr>`).join('');
@@ -45,12 +58,29 @@ function renderHtml(m) {
45
58
  <html lang="en"><head><meta charset="utf-8">
46
59
  <title>Council Report — ${esc(h.runId)}</title>
47
60
  <style>
48
- body { font: 14px/1.5 system-ui, sans-serif; max-width: 1000px; margin: 2rem auto; padding: 0 1rem; color: #1f2937; }
49
- h1 { font-size: 1.5rem; } h2 { margin-top: 2rem; border-bottom: 1px solid #e5e7eb; padding-bottom: .25rem; }
50
- table { border-collapse: collapse; width: 100%; margin: .5rem 0; }
51
- th, td { border: 1px solid #e5e7eb; padding: .35rem .5rem; text-align: left; }
52
- th { background: #f9fafb; } td.c { text-align: center; } .meta { color: #6b7280; }
53
- .legend { color: #6b7280; font-size: .85rem; }
61
+ ${tokenCss()}
62
+ :root {
63
+ /* Council tiers light-ground palette + ink. Intentionally re-declared
64
+ here with the SAME values tokenCss() emits (self-documenting + robust to
65
+ a loader refactor; the cascade resolves identically). */
66
+ --tier-confirmed: #d7ead0;
67
+ --tier-contested: #efe4c4;
68
+ --tier-disputed: #ecd4ec;
69
+ --tier-singleton: #e2e0ea;
70
+ --tier-confirmed-ink: #15803d;
71
+ --tier-contested-ink: #b45309;
72
+ --tier-disputed-ink: #a21caf;
73
+ --tier-singleton-ink: #4b5563;
74
+ }
75
+ body { font: 14px/1.6 'Outfit', ui-sans-serif, system-ui, -apple-system, sans-serif; max-width: 1000px; margin: 2rem auto; padding: 0 1rem; color: #1f2937; background: #fff; }
76
+ h1 { font-family: 'Outfit', ui-sans-serif, system-ui, -apple-system, sans-serif; font-size: 1.5rem; font-weight: 700; letter-spacing: -.02em; }
77
+ h2 { font-family: 'Outfit', ui-sans-serif, system-ui, -apple-system, sans-serif; margin-top: 2rem; font-weight: 600; border-bottom: 1px solid #e5e7eb; padding-bottom: .25rem; }
78
+ table { border-collapse: collapse; width: 100%; margin: .5rem 0; }
79
+ th, td { border: 1px solid #e5e7eb; padding: .35rem .5rem; text-align: left; }
80
+ th { background: #f9fafb; font-family: 'Outfit', ui-sans-serif, system-ui, -apple-system, sans-serif; font-weight: 600; }
81
+ td.c { text-align: center; }
82
+ .meta, .legend { color: #6b7280; font-family: 'IBM Plex Mono', ui-monospace, monospace; }
83
+ .legend { font-size: .85rem; }
54
84
  </style></head><body>
55
85
  <h1>Council Report — ${esc(h.runType)} (${esc(h.runId)})</h1>
56
86
  <p class="meta">${meta}</p>
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,134 @@
1
+ /* ==========================================================================
2
+ Amicus — shared design tokens (clay/gold, neutral-black)
3
+ Single source of truth for the Electron app, council report, and site.
4
+ Re-tinted from the "Spectrum" design export to the shipped clay→gold brand
5
+ on the site's canonical neutral-black ramp. Injected by src/design/tokens.js.
6
+ ========================================================================== */
7
+
8
+ /* ---- Webfonts (bundled, self-hosted) ---------------------------------- */
9
+ /* Paths are relative to THIS file (src/design/tokens.css). The loader
10
+ rewrites them to absolute file-protocol URLs for the Electron app. */
11
+ @font-face { font-family: 'Outfit'; font-style: normal; font-weight: 300; font-display: swap; src: url('./fonts/Outfit-300.ttf') format('truetype'); }
12
+ @font-face { font-family: 'Outfit'; font-style: normal; font-weight: 400; font-display: swap; src: url('./fonts/Outfit-400.ttf') format('truetype'); }
13
+ @font-face { font-family: 'Outfit'; font-style: normal; font-weight: 500; font-display: swap; src: url('./fonts/Outfit-500.ttf') format('truetype'); }
14
+ @font-face { font-family: 'Outfit'; font-style: normal; font-weight: 600; font-display: swap; src: url('./fonts/Outfit-600.ttf') format('truetype'); }
15
+ @font-face { font-family: 'Outfit'; font-style: normal; font-weight: 700; font-display: swap; src: url('./fonts/Outfit-700.ttf') format('truetype'); }
16
+ @font-face { font-family: 'Outfit'; font-style: normal; font-weight: 800; font-display: swap; src: url('./fonts/Outfit-800.ttf') format('truetype'); }
17
+ @font-face { font-family: 'IBM Plex Mono'; font-style: normal; font-weight: 400; font-display: swap; src: url('./fonts/IBMPlexMono-400.ttf') format('truetype'); }
18
+ @font-face { font-family: 'IBM Plex Mono'; font-style: normal; font-weight: 500; font-display: swap; src: url('./fonts/IBMPlexMono-500.ttf') format('truetype'); }
19
+ @font-face { font-family: 'IBM Plex Mono'; font-style: normal; font-weight: 600; font-display: swap; src: url('./fonts/IBMPlexMono-600.ttf') format('truetype'); }
20
+
21
+ :root {
22
+ /* ---- Brand accent · clay (re-tinted from violet) ------------------- */
23
+ --accent-500: #d97757; /* the one brand accent (re-tinted from Spectrum violet) */
24
+ --accent-400: #e8a07c; /* lighter tint — hover text */
25
+ --accent-600: #c45c3f; /* pressed / hover fill; spec value — intentionally supersedes the app's old clay-600 */
26
+ --accent-soft: rgba(217, 119, 87, 0.10); /* tint wash / selected — site --asoft */
27
+ --accent-glow: rgba(217, 119, 87, 0.05); /* radial glow — site --aglow */
28
+ --accent-line: rgba(217, 119, 87, 0.28); /* dashed connectors, borders */
29
+
30
+ /* ---- Counter-accent · gold (re-tinted from lime) ------------------- */
31
+ --gold-400: #e8b24a; /* counter-accent (re-tinted from Spectrum lime) */
32
+ --gold-500: #d49a2e; /* deeper gold */
33
+ --gold-soft: rgba(232, 178, 74, 0.12);
34
+ --gold-line: rgba(232, 178, 74, 0.28);
35
+
36
+ /* ---- Neutral-black ramp (site :root — canonical) ------------------- */
37
+ --bg: #0a0a0a; /* page / window background */
38
+ --surface-1: #111113; /* card / input / inset (site --s1) */
39
+ --surface-2: #161618; /* raised / hover (site --s2) */
40
+ --surface-3: #1c1c1f; /* inset row (site --s3) */
41
+ --surface-sel: #1e1613; /* selected row (app-additive, provisional)*/
42
+ --border: #222225; /* hairline */
43
+ --border-strong: #2c2c30; /* hover / focus hairline (site --border2) */
44
+ --text-1: #f5f5f3; /* primary text */
45
+ --text-2: #a1a1a0; /* secondary text */
46
+ --text-3: #666666; /* tertiary / muted (site --t3 #666) */
47
+ --text-4: #3a3a3e; /* disabled / faint (app-additive) */
48
+
49
+ /* ---- Semantic status (unchanged from export) ---------------------- */
50
+ --ok: #6BBF6B;
51
+ --ok-bright: #4ade80;
52
+ --warn: #FBBF24;
53
+ --danger: #E05252;
54
+ --running: #4ade80;
55
+
56
+ /* ---- Council verdict tiers (report tables, light ground) ----------- */
57
+ --tier-confirmed: #d7ead0;
58
+ --tier-contested: #efe4c4;
59
+ --tier-disputed: #ecd4ec;
60
+ --tier-singleton: #e2e0ea;
61
+ --tier-confirmed-ink: #15803d;
62
+ --tier-contested-ink: #b45309;
63
+ --tier-disputed-ink: #a21caf;
64
+ --tier-singleton-ink: #4b5563;
65
+
66
+ /* ---- Severity ----------------------------------------------------- */
67
+ --sev-blocker: #E05252;
68
+ --sev-major: #FBBF24;
69
+ --sev-minor: #a1a1a0;
70
+ --sev-nit: #666666;
71
+
72
+ /* ---- Provider brand colors (fixed external brands) ----------------- */
73
+ --pv-anthropic: #D97757;
74
+ --pv-google: #4285F4;
75
+ --pv-openai: #10A37F;
76
+ --pv-deepseek: #4D6BFE;
77
+ --pv-meta: #0081FB;
78
+ --pv-xai: #ECE9F5;
79
+ --pv-openrouter: #6566F1;
80
+
81
+ /* ---- Semantic aliases --------------------------------------------- */
82
+ --surface: var(--surface-1);
83
+ --surface-hover: var(--surface-2);
84
+ --text: var(--text-1);
85
+ --text-muted: var(--text-2);
86
+ --text-faint: var(--text-3);
87
+ --text-disabled: var(--text-4);
88
+ --accent: #d97757;
89
+ --accent-hover: var(--accent-600);
90
+ --accent-2: #e8b24a;
91
+ --accent-text: #ffffff;
92
+ --on-accent: #ffffff;
93
+ --focus-ring: var(--accent-500);
94
+
95
+ /* ---- Typography --------------------------------------------------- */
96
+ --font-sans: 'Outfit', system-ui, -apple-system, 'Segoe UI', sans-serif;
97
+ --font-mono: 'IBM Plex Mono', 'SF Mono', Menlo, Monaco, Consolas, monospace;
98
+ --w-light: 300; --w-regular: 400; --w-medium: 500;
99
+ --w-semibold: 600; --w-bold: 700; --w-extra: 800;
100
+ --fs-9: 9px; --fs-10: 10px; --fs-11: 11px; --fs-12: 12px;
101
+ --fs-13: 13px; --fs-15: 15px; --fs-18: 18px; --fs-22: 22px;
102
+ --lh-tight: 1.08; --lh-snug: 1.3; --lh-body: 1.6; --lh-relaxed: 1.7; --lh-display: 0.92;
103
+ --ls-display: -0.05em; --ls-tight: -0.03em; --ls-normal: -0.01em;
104
+ --ls-eyebrow: 0.14em; --ls-label: 0.5px; --ls-brand: 0.8px;
105
+
106
+ /* ---- Radii / borders / shadows / motion (color-free, from export) -- */
107
+ --r-2: 2px; --r-3: 3px; --r-4: 4px; --r-6: 6px; --r-7: 7px;
108
+ --r-8: 8px; --r-10: 10px; --r-12: 12px; --r-full: 999px;
109
+ --bd: 1px solid var(--border);
110
+ --bd-strong: 1px solid var(--border-strong);
111
+ --bd-accent: 1px solid var(--accent-500);
112
+ --bd-dashed: 1px dashed var(--border);
113
+ --bd-connector: 0.8px dashed var(--accent-line);
114
+ --shadow-none: none;
115
+ --shadow-card: 0 1px 2px rgba(0,0,0,0.25);
116
+ --shadow-pop: 0 8px 24px rgba(0,0,0,0.45);
117
+ --shadow-glow: 0 0 0 3px var(--accent-soft);
118
+ --ease-out: cubic-bezier(.16, 1, .3, 1);
119
+ --ease-std: ease;
120
+ --dur-fast: 0.15s; --dur-mid: 0.25s; --dur-slow: 0.7s;
121
+
122
+ /* ---- Spacing ------------------------------------------------------ */
123
+ --space-1: 2px; --space-2: 4px; --space-3: 6px; --space-4: 8px;
124
+ --space-5: 10px; --space-6: 12px; --space-7: 14px; --space-8: 16px;
125
+ --space-10: 20px; --space-12: 24px; --space-16: 32px;
126
+ --space-20: 40px; --space-24: 48px; --space-32: 64px;
127
+ --pad-pill: 3px 8px;
128
+ --pad-btn-sm: 5px 14px;
129
+ --pad-btn: 7px 12px;
130
+ --pad-input: 7px 10px;
131
+ --pad-card: 12px 16px;
132
+ --toolbar-h: 40px;
133
+ --gap-row: 8px;
134
+ }
@@ -0,0 +1,51 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Shared design-token loader.
5
+ *
6
+ * tokenCss(opts) -> the full :root{} + @font-face CSS string from tokens.css.
7
+ * opts.absoluteFontUrls (default false) rewrites the
8
+ * relative ./fonts/*.ttf URLs to absolute file:// URLs so
9
+ * the Electron app (which injects this inline into data:
10
+ * URLs) resolves the bundled webfonts. The report/site
11
+ * leave them relative.
12
+ * TOKENS -> flat map of the canonical hex/rgba values, used by the
13
+ * site drift-guard test.
14
+ */
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const { pathToFileURL } = require('url');
19
+
20
+ const TOKENS_CSS_PATH = path.join(__dirname, 'tokens.css');
21
+ const FONTS_DIR = path.join(__dirname, 'fonts');
22
+
23
+ const TOKENS = {
24
+ accent: '#d97757',
25
+ gold: '#e8b24a',
26
+ bg: '#0a0a0a',
27
+ surface1: '#111113',
28
+ surface2: '#161618',
29
+ surface3: '#1c1c1f',
30
+ border: '#222225',
31
+ borderStrong: '#2c2c30',
32
+ text1: '#f5f5f3',
33
+ text2: '#a1a1a0',
34
+ text3: '#666666',
35
+ accentSoft: 'rgba(217, 119, 87, 0.10)',
36
+ accentGlow: 'rgba(217, 119, 87, 0.05)',
37
+ running: '#4ade80'
38
+ };
39
+
40
+ function tokenCss(opts = {}) {
41
+ let css = fs.readFileSync(TOKENS_CSS_PATH, 'utf8');
42
+ if (opts.absoluteFontUrls) {
43
+ css = css.replace(/url\('\.\/fonts\/([^']+)'\)/g, (_m, file) => {
44
+ const abs = pathToFileURL(path.join(FONTS_DIR, file)).href;
45
+ return `url('${abs}')`;
46
+ });
47
+ }
48
+ return css;
49
+ }
50
+
51
+ module.exports = { tokenCss, TOKENS };
package/src/mcp-server.js CHANGED
@@ -613,7 +613,11 @@ const handlers = {
613
613
  async amicus_council_tally(input) {
614
614
  try {
615
615
  const { tally } = require('./council/tally');
616
- return textResult(JSON.stringify(tally(input)));
616
+ const record = tally(input);
617
+ // Auto-append to the reliability ledger (parity with `amicus council
618
+ // tally`). Best-effort: a ledger write failure must not fail the tally.
619
+ try { require('./council/ledger').appendRun(record); } catch { /* best-effort */ }
620
+ return textResult(JSON.stringify(record));
617
621
  } catch (err) { return textResult(`council tally failed: ${err.message}`, true); }
618
622
  },
619
623
 
@@ -39,6 +39,34 @@ function getConfigDir() {
39
39
  return amicusDir;
40
40
  }
41
41
 
42
+ /**
43
+ * One-time, non-destructive migration of the legacy ~/.config/sidecar config
44
+ * directory onto the canonical ~/.config/amicus. Copies (does not move), so the
45
+ * legacy dir is left intact as a backup. This collapses the two-dir split that
46
+ * let getConfigDir() flip between them and orphan data: once ~/.config/amicus
47
+ * exists it always wins. No-op when amicus already exists, when there is no
48
+ * legacy dir, or when a CONFIG_DIR override is set. Best-effort — returns a
49
+ * result object and never throws. Call once at startup, before any config read.
50
+ *
51
+ * @param {{home?: string}} [opts]
52
+ * @returns {{migrated: boolean, from?: string, to?: string, reason?: string, error?: string}}
53
+ */
54
+ function migrateLegacyConfigDir(opts = {}) {
55
+ if (getCompatEnv('CONFIG_DIR')) { return { migrated: false, reason: 'override-set' }; }
56
+ const home = opts.home || process.env.HOME || process.env.USERPROFILE;
57
+ if (!home) { return { migrated: false, reason: 'no-home' }; }
58
+ const amicusDir = path.join(home, '.config', 'amicus');
59
+ const legacyDir = path.join(home, '.config', 'sidecar');
60
+ try {
61
+ if (fs.existsSync(amicusDir)) { return { migrated: false, reason: 'amicus-exists' }; }
62
+ if (!fs.existsSync(legacyDir)) { return { migrated: false, reason: 'no-legacy' }; }
63
+ fs.cpSync(legacyDir, amicusDir, { recursive: true });
64
+ return { migrated: true, from: legacyDir, to: amicusDir };
65
+ } catch (err) {
66
+ return { migrated: false, reason: 'error', error: err.message };
67
+ }
68
+ }
69
+
42
70
  /** @returns {string} Full path to config.json */
43
71
  function getConfigPath() {
44
72
  return path.join(getConfigDir(), 'config.json');
@@ -326,6 +354,7 @@ function resolveCouncilMembers(name, catalog = []) {
326
354
 
327
355
  module.exports = {
328
356
  getConfigDir,
357
+ migrateLegacyConfigDir,
329
358
  getConfigPath,
330
359
  loadConfig,
331
360
  saveConfig,