amicus 1.3.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.
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +36 -0
- package/README.md +12 -1
- package/bin/amicus.js +11 -0
- package/electron/fold.js +17 -7
- package/electron/ipc-setup.js +18 -1
- package/electron/preload-setup.js +2 -1
- package/electron/setup-ui-aliases.js +3 -3
- package/electron/setup-ui-council.js +77 -0
- package/electron/setup-ui-styles.js +203 -143
- package/electron/setup-ui.js +11 -5
- package/electron/toolbar.js +29 -25
- package/package.json +1 -1
- package/skills/second-opinion/MODEL-NOTES.md +6 -0
- package/skills/second-opinion/SKILL.md +17 -1
- package/src/cli-handlers-council.js +11 -4
- package/src/cli-handlers-run.js +24 -2
- package/src/cli.js +2 -0
- package/src/council/report-html.js +39 -9
- package/src/design/fonts/IBMPlexMono-400.ttf +0 -0
- package/src/design/fonts/IBMPlexMono-500.ttf +0 -0
- package/src/design/fonts/IBMPlexMono-600.ttf +0 -0
- package/src/design/fonts/Outfit-300.ttf +0 -0
- package/src/design/fonts/Outfit-400.ttf +0 -0
- package/src/design/fonts/Outfit-500.ttf +0 -0
- package/src/design/fonts/Outfit-600.ttf +0 -0
- package/src/design/fonts/Outfit-700.ttf +0 -0
- package/src/design/fonts/Outfit-800.ttf +0 -0
- package/src/design/tokens.css +134 -0
- package/src/design/tokens.js +51 -0
- package/src/mcp-server.js +38 -5
- package/src/mcp-tools.js +5 -2
- package/src/sidecar/setup.js +129 -9
- package/src/utils/config.js +83 -0
- package/src/utils/free-models.js +50 -0
package/electron/toolbar.js
CHANGED
|
@@ -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:
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
.
|
|
73
|
+
.logo path { stroke: var(--accent); }
|
|
74
|
+
.sep { color: var(--border-strong); font-size: 14px; }
|
|
71
75
|
.detail, .timer {
|
|
72
|
-
color:
|
|
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:
|
|
79
|
-
color:
|
|
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:
|
|
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
|
|
91
|
-
border-radius: 4px; color:
|
|
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:
|
|
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:
|
|
104
|
-
border-bottom: 1px solid
|
|
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:
|
|
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:
|
|
116
|
-
color:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
137
|
-
<path d="M10 2v5c0 2-3 3-7 5" stroke
|
|
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.
|
|
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",
|
|
@@ -79,6 +79,12 @@ This section keeps only per-model **qualitative quirks** and **structural-confor
|
|
|
79
79
|
- **gpt** — thorough but verbose; peers have dinged it for volume-over-judgment. Self-ranked its own review #1 in the 2026-06-04 run → the peers-only street-cred rule (now enforced by `tally`) mitigates this. Conforms cleanly. Accessible via OpenRouter.
|
|
80
80
|
- **gemini** — fast, very large context; tends toward absolute severity labels ("blocker" inflation vs peers). Conforms cleanly; watch for preamble narration — instruct it to emit the JSON block verbatim after the prose.
|
|
81
81
|
|
|
82
|
+
## Free-tier models (OpenRouter `:free`)
|
|
83
|
+
- Heavily rate-limited (shared daily pool); a 3-leg parallel wave + cross-review can 429 mid-run.
|
|
84
|
+
- Quality-variable; weaker at strict structured (findings JSON) output.
|
|
85
|
+
- Some `:free` models 404 unless the account enables data-sharing at openrouter.ai/settings/privacy.
|
|
86
|
+
- No reliability history — chair selection can't use `council stats`; pick the strongest free model and disclose lower confidence.
|
|
87
|
+
|
|
82
88
|
## Cost guardrail
|
|
83
89
|
- The budget gate enforces this in code: a per-$/Mtok threshold (ON by default)
|
|
84
90
|
refuses o3/o3-pro-class models before a wave launches. This replaces the old
|
|
@@ -56,6 +56,22 @@ in this run is written here. Use its absolute path in all `--prompt-file` argume
|
|
|
56
56
|
|
|
57
57
|
**Pick the council.** Default: **3 models from different families (non-Claude)**. Recommend them ranked by fit, consulting both the reviewer-reliability data from `amicus council stats` (the authoritative quantitative source — runs, avg peers-only street-cred, confirm-rate, fact-error rate) and the qualitative quirks in `MODEL-NOTES.md`. State the estimated cost. The estimate is the budget gate's pre-flight figure (per-$/Mtok pricing from the cached catalog; direct-provider legs without catalog pricing are disclosed as "cost unknown"). State it as an estimate, not a guarantee. **Disclose the run shape up front** before asking for confirmation — e.g.:
|
|
58
58
|
|
|
59
|
+
**Free council (zero-cost).** If the user asks for a "free council" / "zero-cost council",
|
|
60
|
+
read `councils.free` from `~/.config/amicus/config.json` and run
|
|
61
|
+
`amicus fanout --council free --prompt-file <briefing>`. Free-tier handling:
|
|
62
|
+
- Cost ≈ $0 — skip the paid-run cost framing (the budget gate is a no-op at zero price).
|
|
63
|
+
- No reliability history: free models have no `amicus council stats` / `MODEL-NOTES` record,
|
|
64
|
+
so don't rank on street-cred. Pick the most capable free model as chair and state lower confidence.
|
|
65
|
+
- Weak structured output: small free models are less reliable at the strict findings JSON; expect
|
|
66
|
+
more `validateFindings` repair-loop hits.
|
|
67
|
+
- Throttled/truncated legs: a mid-stream 429 can yield a leg marked `complete` with a truncated,
|
|
68
|
+
unparseable review. When a free-council leg is `complete` but `validateFindings` returns
|
|
69
|
+
`NO_FENCED_BLOCK`/`NOT_PARSEABLE`, treat it as suspect/throttled — don't burn the repair loop on the
|
|
70
|
+
same throttled model; disclose it and apply the ≥2-reviews-survive wave-degrade rule.
|
|
71
|
+
- Prerequisite: free models require enabling data-sharing in OpenRouter privacy settings
|
|
72
|
+
(openrouter.ai/settings/privacy) or legs 404 at run time — catalog validation cannot catch this.
|
|
73
|
+
State this up front.
|
|
74
|
+
|
|
59
75
|
> This run uses 3 council models across 2 fanout waves + 1 chair call (~7 model runs), ~10 min.
|
|
60
76
|
|
|
61
77
|
Then **wait for confirmation**. Never launch without it. The budget gate enforces the cost guardrail in code: by default it refuses any leg whose price exceeds the per-$/Mtok threshold (the o3/o3-pro guard). To run an intentionally expensive model the user explicitly asked for by name, pass `--no-cost-gate`; to raise only the total ceiling, pass `--max-cost <$>`.
|
|
@@ -314,7 +330,7 @@ This stage updates `MODEL-NOTES.md` to make future runs better. **Nothing is wri
|
|
|
314
330
|
|
|
315
331
|
Draft new or updated entries for the per-model sections of `MODEL-NOTES.md` that capture what was learned.
|
|
316
332
|
|
|
317
|
-
**Ledger auto-append (automatic — no approval required).**
|
|
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**.
|
|
318
334
|
|
|
319
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.
|
|
320
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-handlers-run.js
CHANGED
|
@@ -108,8 +108,30 @@ async function handleFanout(args) {
|
|
|
108
108
|
if (promptRes.error) {
|
|
109
109
|
process.exit(failJson(useJson, { code: ERROR_CODES.MISSING_PROMPT, message: promptRes.error }));
|
|
110
110
|
}
|
|
111
|
-
|
|
112
|
-
|
|
111
|
+
// Council preset: expand a saved council into args.models (mutually exclusive with --models).
|
|
112
|
+
const hasModels = typeof args.models === 'string' && args.models.trim();
|
|
113
|
+
const hasCouncil = args.council !== undefined && args.council !== false;
|
|
114
|
+
if (hasModels && hasCouncil) {
|
|
115
|
+
process.exit(failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: 'Error: pass exactly one of --models / --council, not both' }));
|
|
116
|
+
}
|
|
117
|
+
if (!hasModels && !hasCouncil) {
|
|
118
|
+
process.exit(failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: 'Error: --models is required (comma-separated aliases or provider/model IDs), or use --council <name>' }));
|
|
119
|
+
}
|
|
120
|
+
if (hasCouncil) {
|
|
121
|
+
if (typeof args.council !== 'string' || !args.council.trim()) {
|
|
122
|
+
process.exit(failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: 'Error: --council requires a council name (e.g. --council free)' }));
|
|
123
|
+
}
|
|
124
|
+
const { resolveCouncilMembers } = require('./utils/config');
|
|
125
|
+
const { readCache } = require('./utils/model-catalog');
|
|
126
|
+
const catalog = (readCache() || {}).models || [];
|
|
127
|
+
const expanded = resolveCouncilMembers(args.council.trim(), catalog);
|
|
128
|
+
if (expanded.error) {
|
|
129
|
+
process.exit(failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: `Error: ${expanded.error}` }));
|
|
130
|
+
}
|
|
131
|
+
if (expanded.dropped && expanded.dropped.length && !useJson) {
|
|
132
|
+
process.stderr.write(`Notice: dropped unavailable council member(s): ${expanded.dropped.join(', ')}\n`);
|
|
133
|
+
}
|
|
134
|
+
args.models = expanded.models.join(',');
|
|
113
135
|
}
|
|
114
136
|
if (args['wave-id']) {
|
|
115
137
|
const check = validateTaskId(String(args['wave-id']));
|
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
|
];
|
|
@@ -356,6 +357,7 @@ Options for 'start':
|
|
|
356
357
|
|
|
357
358
|
Options for 'fanout':
|
|
358
359
|
--models <a,b,c> Required. Comma-separated aliases or provider/model IDs
|
|
360
|
+
--council <name> Run a saved council instead of --models (e.g. free). Mutually exclusive with --models
|
|
359
361
|
--prompt <text> Task briefing (or use --prompt-file)
|
|
360
362
|
--prompt-file <path> Read the briefing from a UTF-8 file (avoids the
|
|
361
363
|
~32KB Windows argument cap). Mutually exclusive
|
|
@@ -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
|
|
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:${
|
|
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
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
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
|
@@ -531,9 +531,38 @@ const handlers = {
|
|
|
531
531
|
async amicus_fanout(input, project) {
|
|
532
532
|
const cwd = project || getProjectDir(input.project);
|
|
533
533
|
const { generateTaskId } = require('./sidecar/start');
|
|
534
|
-
const { deriveLegIds } = require('./sidecar/fanout');
|
|
534
|
+
const { deriveLegIds, DEFAULT_MAX_LEGS } = require('./sidecar/fanout');
|
|
535
|
+
|
|
536
|
+
// Resolve a single effective models list (council OR models), validated
|
|
537
|
+
// BEFORE any wave dir / metadata is written so a bad request never strands
|
|
538
|
+
// a pid-less 'running' orphan wave.
|
|
539
|
+
const inputModels = Array.isArray(input.models) ? input.models : [];
|
|
540
|
+
const hasModels = inputModels.length > 0;
|
|
541
|
+
const hasCouncil = typeof input.council === 'string' && input.council.trim();
|
|
542
|
+
if (hasModels && hasCouncil) {
|
|
543
|
+
return textResult("Pass exactly one of 'models' / 'council', not both.", true);
|
|
544
|
+
}
|
|
545
|
+
let effectiveModels;
|
|
546
|
+
if (hasCouncil) {
|
|
547
|
+
const { resolveCouncilMembers } = require('./utils/config');
|
|
548
|
+
const { readCache } = require('./utils/model-catalog');
|
|
549
|
+
const catalog = (readCache() || {}).models || [];
|
|
550
|
+
const expanded = resolveCouncilMembers(input.council.trim(), catalog);
|
|
551
|
+
if (expanded.error) { return textResult(expanded.error, true); }
|
|
552
|
+
effectiveModels = expanded.models;
|
|
553
|
+
} else if (hasModels) {
|
|
554
|
+
effectiveModels = inputModels;
|
|
555
|
+
} else {
|
|
556
|
+
return textResult("Provide 'models' or 'council'.", true);
|
|
557
|
+
}
|
|
558
|
+
const envCap = Number(process.env.AMICUS_FANOUT_MAX_LEGS);
|
|
559
|
+
const maxLegs = (Number.isInteger(envCap) && envCap > 0) ? envCap : DEFAULT_MAX_LEGS;
|
|
560
|
+
if (effectiveModels.length > maxLegs) {
|
|
561
|
+
return textResult(`Council/model list exceeds the fan-out cap of ${maxLegs} legs.`, true);
|
|
562
|
+
}
|
|
563
|
+
|
|
535
564
|
const waveId = generateTaskId();
|
|
536
|
-
const legIds = deriveLegIds(waveId,
|
|
565
|
+
const legIds = deriveLegIds(waveId, effectiveModels.length);
|
|
537
566
|
const waveDir = getSessionDir(cwd, waveId);
|
|
538
567
|
|
|
539
568
|
let briefingPath;
|
|
@@ -545,14 +574,14 @@ const handlers = {
|
|
|
545
574
|
fs.writeFileSync(briefingPath, input.prompt, { mode: 0o600 });
|
|
546
575
|
fs.writeFileSync(path.join(waveDir, 'metadata.json'), JSON.stringify({
|
|
547
576
|
taskId: waveId, type: 'wave', status: 'running', legs: legIds,
|
|
548
|
-
models:
|
|
577
|
+
models: effectiveModels, headless: true, createdAt: new Date().toISOString(),
|
|
549
578
|
}, null, 2), { mode: 0o600 });
|
|
550
579
|
} catch (err) {
|
|
551
580
|
return textResult(`Failed to prepare fan-out wave: ${err.message}`, true);
|
|
552
581
|
}
|
|
553
582
|
|
|
554
583
|
const args = [
|
|
555
|
-
'fanout', '--models',
|
|
584
|
+
'fanout', '--models', effectiveModels.join(','),
|
|
556
585
|
'--prompt-file', briefingPath, '--wave-id', waveId,
|
|
557
586
|
'--json', '--client', 'cowork', '--cwd', cwd,
|
|
558
587
|
];
|
|
@@ -584,7 +613,11 @@ const handlers = {
|
|
|
584
613
|
async amicus_council_tally(input) {
|
|
585
614
|
try {
|
|
586
615
|
const { tally } = require('./council/tally');
|
|
587
|
-
|
|
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));
|
|
588
621
|
} catch (err) { return textResult(`council tally failed: ${err.message}`, true); }
|
|
589
622
|
},
|
|
590
623
|
|