amicus 1.0.0 → 1.2.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/marketplace.json +14 -0
- package/.claude-plugin/plugin.json +19 -0
- package/CHANGELOG.md +86 -0
- package/LICENSE +22 -1
- package/README.md +14 -3
- package/bin/amicus.js +17 -162
- package/electron/ipc-setup.js +30 -9
- package/electron/main.js +13 -5
- package/electron/preload.js +30 -10
- package/electron/setup-ui-keys.js +9 -0
- package/electron/setup-ui-model.js +33 -23
- package/electron/setup-ui-styles.js +6 -1
- package/electron/setup-ui.js +91 -38
- package/electron/toolbar.js +4 -5
- package/package.json +7 -5
- package/scripts/postinstall.js +16 -7
- package/skills/second-opinion/COUNCIL-DESIGN.md +36 -34
- package/skills/second-opinion/MODEL-NOTES.md +23 -17
- package/skills/second-opinion/SKILL.md +84 -51
- package/{skill → skills/sidecar}/SKILL.md +14 -4
- package/src/cli-handlers-council.js +59 -0
- package/src/cli-handlers-doctor.js +173 -0
- package/src/cli-handlers-run.js +196 -0
- package/src/cli-handlers.js +66 -1
- package/src/cli.js +16 -2
- package/src/council/findings.js +48 -0
- package/src/council/ledger.js +82 -0
- package/src/council/tally.js +108 -0
- package/src/council/verdict.js +48 -0
- package/src/headless.js +43 -149
- package/src/mcp-server.js +6 -0
- package/src/sidecar/budget.js +83 -0
- package/src/sidecar/conversation-mirror.js +128 -0
- package/src/sidecar/fanout-leg.js +4 -1
- package/src/sidecar/fanout.js +34 -7
- package/src/sidecar/interactive-mirror.js +66 -0
- package/src/sidecar/interactive.js +35 -21
- package/src/sidecar/models.js +41 -10
- package/src/sidecar/session-finalize.js +26 -0
- package/src/sidecar/session-utils.js +5 -5
- package/src/sidecar/setup.js +55 -42
- package/src/sidecar/start.js +19 -6
- package/src/utils/activity-poller.js +47 -0
- package/src/utils/alias-resolver.js +1 -1
- package/src/utils/config.js +4 -4
- package/src/utils/curated-models.js +88 -45
- package/src/utils/error-doc.js +55 -0
- package/src/utils/lifecycle.js +1 -1
- package/src/utils/model-catalog.js +1 -1
- package/src/utils/model-fetcher.js +16 -2
- package/src/utils/pricing.js +93 -0
- package/src/utils/quick-picks.js +81 -0
- package/src/utils/result-schema.js +21 -2
- package/src/utils/session-abort.js +40 -13
- package/src/utils/validators.js +17 -17
|
@@ -2,24 +2,22 @@
|
|
|
2
2
|
* Setup UI - Step 2: Default Model Selection
|
|
3
3
|
*
|
|
4
4
|
* Builds the HTML for the model selection step of the wizard.
|
|
5
|
-
* Renders radio card choices with provider routing
|
|
6
|
-
*
|
|
5
|
+
* Renders radio card choices with provider routing, write-preview,
|
|
6
|
+
* and offline-badge support.
|
|
7
|
+
*
|
|
8
|
+
* Choices are RESOLVED rows passed in at call time (see src/utils/quick-picks.js):
|
|
9
|
+
* { alias, label, blurb, source: 'live'|'fallback', routes: Object<string,string> }
|
|
10
|
+
* No curated-models import — this module is pure builder/renderer.
|
|
7
11
|
*/
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Wizard quick-pick cards \u2014 derived from curated-models (F5).
|
|
12
|
-
* @type {Array<{alias: string, label: string, routes: Object<string,string>}>}
|
|
13
|
-
*/
|
|
14
|
-
const MODEL_CHOICES = getCuratedModels().map(c => ({
|
|
15
|
-
alias: c.alias, label: `${c.label} \u2014 ${c.blurb}`, routes: c.routes
|
|
16
|
-
}));
|
|
13
|
+
'use strict';
|
|
17
14
|
|
|
18
15
|
const PROVIDER_NAMES = {
|
|
19
16
|
openrouter: 'OpenRouter',
|
|
20
17
|
google: 'Google AI',
|
|
21
18
|
openai: 'OpenAI',
|
|
22
|
-
anthropic: 'Anthropic'
|
|
19
|
+
anthropic: 'Anthropic',
|
|
20
|
+
deepseek: 'DeepSeek'
|
|
23
21
|
};
|
|
24
22
|
|
|
25
23
|
/**
|
|
@@ -37,7 +35,7 @@ function isModelAvailable(providers, configuredKeys) {
|
|
|
37
35
|
}
|
|
38
36
|
|
|
39
37
|
/**
|
|
40
|
-
* Find the best available provider for a model's
|
|
38
|
+
* Find the best available provider for a model's route display.
|
|
41
39
|
* Prefers the first provider with a configured key; falls back to first provider.
|
|
42
40
|
* @param {string[]} providers - Route provider IDs
|
|
43
41
|
* @param {Object<string,boolean>} configuredKeys - Which providers have keys
|
|
@@ -49,12 +47,13 @@ function bestAvailableProvider(providers, configuredKeys) {
|
|
|
49
47
|
}
|
|
50
48
|
|
|
51
49
|
/**
|
|
52
|
-
* Search-over-catalog section
|
|
53
|
-
*
|
|
50
|
+
* Search-over-catalog section. Always visible — no display:none gating.
|
|
51
|
+
* Rows are rendered client-side from the get-catalog IPC response.
|
|
54
52
|
* @returns {string} HTML fragment
|
|
55
53
|
*/
|
|
56
54
|
function buildModelSearchHTML() {
|
|
57
|
-
return `<div id="model-search-section"
|
|
55
|
+
return `<div id="model-search-section">
|
|
56
|
+
<div class="search-label">…or pick any model from the catalog</div>
|
|
58
57
|
<div class="search-head">
|
|
59
58
|
<input type="text" id="model-search-input" placeholder="Search all models (id or name)..." autocomplete="off">
|
|
60
59
|
<button class="icon-btn" id="model-search-refresh" title="Refresh catalog">↻</button>
|
|
@@ -65,10 +64,11 @@ function buildModelSearchHTML() {
|
|
|
65
64
|
}
|
|
66
65
|
|
|
67
66
|
/**
|
|
68
|
-
* Build the HTML fragment for Step 2 (Model Selection)
|
|
69
|
-
* @param {Array<{alias:
|
|
70
|
-
*
|
|
71
|
-
* @param {
|
|
67
|
+
* Build the HTML fragment for Step 2 (Model Selection).
|
|
68
|
+
* @param {Array<{alias:string, label:string, blurb:string, source:string, routes:Object<string,string>}>} choices
|
|
69
|
+
* Resolved rows from resolveQuickPicks(). Each row has separate label + blurb fields.
|
|
70
|
+
* @param {string} [selectedAlias] - Pre-selected alias; defaults to first available choice.
|
|
71
|
+
* @param {Object<string,boolean>} [configuredKeys] - Provider IDs the user has keys for.
|
|
72
72
|
* @returns {string} HTML fragment
|
|
73
73
|
*/
|
|
74
74
|
function buildModelStepHTML(choices, selectedAlias, configuredKeys = {}) {
|
|
@@ -100,12 +100,19 @@ function buildModelStepHTML(choices, selectedAlias, configuredKeys = {}) {
|
|
|
100
100
|
const showToggle = available.length >= 2;
|
|
101
101
|
const bestProvider = bestAvailableProvider(providers, configuredKeys);
|
|
102
102
|
|
|
103
|
+
// Resolved id for the write-preview (prefer bestProvider route)
|
|
104
|
+
const previewId = c.routes[bestProvider] || Object.values(c.routes)[0] || '';
|
|
105
|
+
|
|
106
|
+
// Offline badge for fallback rows
|
|
107
|
+
const badge = c.source === 'fallback'
|
|
108
|
+
? '<span class="pick-badge">offline list</span>' : '';
|
|
109
|
+
|
|
103
110
|
let routeHtml = '';
|
|
104
111
|
if (!modelAvailable) {
|
|
105
112
|
routeHtml = '<span class="no-key-hint">No API key configured</span>';
|
|
106
113
|
} else if (hasMultipleRoutes) {
|
|
107
114
|
const pills = providers.map(p => {
|
|
108
|
-
const isActive =
|
|
115
|
+
const isActive = p === bestProvider;
|
|
109
116
|
const cls = isActive ? 'route-pill active' : 'route-pill';
|
|
110
117
|
return `<button class="${cls}" data-alias="${c.alias}" data-provider="${p}">${PROVIDER_NAMES[p]}</button>`;
|
|
111
118
|
}).join('');
|
|
@@ -116,17 +123,20 @@ function buildModelStepHTML(choices, selectedAlias, configuredKeys = {}) {
|
|
|
116
123
|
} else {
|
|
117
124
|
routeHtml = `<span class="route-static">via ${PROVIDER_NAMES[bestProvider]}</span>`;
|
|
118
125
|
}
|
|
126
|
+
|
|
119
127
|
return `<label class="${cardClass}">
|
|
120
128
|
<input type="radio" name="default-model" value="${c.alias}" ${checked}${disabled}>
|
|
121
129
|
<span class="model-alias">${c.alias}</span>
|
|
122
|
-
<span class="model-label">${c.label}</span
|
|
130
|
+
<span class="model-label">${c.label} — ${c.blurb}</span>${badge}
|
|
131
|
+
<span class="model-resolved">${previewId}</span>
|
|
123
132
|
${routeHtml}
|
|
133
|
+
<span class="write-preview" data-alias="${c.alias}">will set <code>${c.alias}</code> → <code class="write-preview-id">${previewId}</code></span>
|
|
124
134
|
</label>`;
|
|
125
135
|
}).join('\n ');
|
|
126
136
|
|
|
127
137
|
return `<div class="step-content">
|
|
128
138
|
<h1>Choose Default Model</h1>
|
|
129
|
-
<p class="subtitle">Pick the
|
|
139
|
+
<p class="subtitle">Current models resolved from the live catalog. Pick the default used when no --model flag is given.</p>
|
|
130
140
|
|
|
131
141
|
<div class="model-list" id="model-list">
|
|
132
142
|
${cards}
|
|
@@ -135,4 +145,4 @@ function buildModelStepHTML(choices, selectedAlias, configuredKeys = {}) {
|
|
|
135
145
|
</div>`;
|
|
136
146
|
}
|
|
137
147
|
|
|
138
|
-
module.exports = { buildModelSearchHTML, buildModelStepHTML,
|
|
148
|
+
module.exports = { buildModelSearchHTML, buildModelStepHTML, PROVIDER_NAMES };
|
|
@@ -321,7 +321,12 @@ function buildWizardCSS() {
|
|
|
321
321
|
.search-row-sub { color: #A09B96; font-size: 11px; margin-top: 2px; }
|
|
322
322
|
.icon-btn { background: none; border: 1px solid #3D3A38; border-radius: 6px; color: #A09B96; cursor: pointer; font-size: 14px; padding: 6px 10px; }
|
|
323
323
|
.icon-btn:hover { border-color: #D97757; color: #D97757; }
|
|
324
|
-
.icon-btn:disabled { opacity: 0.5; cursor: default; }
|
|
324
|
+
.icon-btn:disabled { opacity: 0.5; cursor: default; }
|
|
325
|
+
.search-label { margin: 14px 0 6px; font-size: 12px; opacity: 0.75; }
|
|
326
|
+
.pick-badge { font-size: 10px; padding: 1px 5px; border-radius: 3px; background: #5a4a35; margin-left: 6px; }
|
|
327
|
+
.model-resolved { display: block; font-size: 11px; opacity: 0.6; font-family: monospace; }
|
|
328
|
+
.write-preview { display: none; font-size: 11px; margin-top: 4px; }
|
|
329
|
+
.write-preview-active { display: block; }`;
|
|
325
330
|
}
|
|
326
331
|
|
|
327
332
|
module.exports = { buildWizardCSS };
|
package/electron/setup-ui.js
CHANGED
|
@@ -1,26 +1,32 @@
|
|
|
1
1
|
/** Setup UI - Wizard Orchestrator: API Keys → Models → Aliases → Review */
|
|
2
2
|
const { buildKeysStepHTML, PROVIDERS } = require('./setup-ui-keys');
|
|
3
|
-
const { buildModelStepHTML,
|
|
3
|
+
const { buildModelStepHTML, PROVIDER_NAMES } = require('./setup-ui-model');
|
|
4
4
|
const { buildAliasEditorHTML } = require('./setup-ui-aliases');
|
|
5
5
|
const { buildWizardCSS } = require('./setup-ui-styles');
|
|
6
6
|
const { buildKeysScript } = require('./setup-ui-keys-script');
|
|
7
7
|
const { buildAliasScript } = require('./setup-ui-alias-script');
|
|
8
8
|
const { getDefaultAliases } = require('../src/utils/config');
|
|
9
9
|
const { getBrandName } = require('./toolbar');
|
|
10
|
+
const { resolveQuickPicks } = require('../src/utils/quick-picks');
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* @param {object} [options={}]
|
|
13
14
|
* @param {string} [options.client='code-local'] - Client type for branding
|
|
15
|
+
* @param {Array} [options.quickPicks] - Resolved quick-pick rows from resolveQuickPicks(catalog).
|
|
16
|
+
* Defaults to pinned fallbacks when not provided.
|
|
14
17
|
*/
|
|
15
18
|
function buildSetupHTML(options = {}) {
|
|
16
|
-
const {
|
|
19
|
+
const {
|
|
20
|
+
client = 'code-local',
|
|
21
|
+
quickPicks = resolveQuickPicks([]), // pinned fallbacks when not provided
|
|
22
|
+
} = options;
|
|
17
23
|
const brandName = getBrandName(client);
|
|
18
24
|
const keysHtml = buildKeysStepHTML(PROVIDERS);
|
|
19
|
-
const modelHtml = buildModelStepHTML(
|
|
25
|
+
const modelHtml = buildModelStepHTML(quickPicks);
|
|
20
26
|
const aliasHtml = buildAliasEditorHTML(getDefaultAliases());
|
|
21
27
|
const css = buildWizardCSS();
|
|
22
28
|
const providersJson = JSON.stringify(PROVIDERS);
|
|
23
|
-
const modelChoicesJson = JSON.stringify(
|
|
29
|
+
const modelChoicesJson = JSON.stringify(quickPicks);
|
|
24
30
|
const providerNamesJson = JSON.stringify(PROVIDER_NAMES);
|
|
25
31
|
const defaultAliasesJson = JSON.stringify(getDefaultAliases());
|
|
26
32
|
return `<!DOCTYPE html>
|
|
@@ -63,6 +69,7 @@ function buildWizardScript(providersJson, modelChoicesJson, providerNamesJson, d
|
|
|
63
69
|
var defaultAliases = ${defaultAliasesJson};
|
|
64
70
|
var routingChoices = {};
|
|
65
71
|
var aliasEdits = {};
|
|
72
|
+
var aliasDisplay = {};
|
|
66
73
|
window.availableModels = null;
|
|
67
74
|
var keyValid = false, validatedKey = '';
|
|
68
75
|
var $ = function(id) { return document.getElementById(id); };
|
|
@@ -128,7 +135,7 @@ function buildWizardScript(providersJson, modelChoicesJson, providerNamesJson, d
|
|
|
128
135
|
}
|
|
129
136
|
});
|
|
130
137
|
Object.keys(cfg.aliases).forEach(function(k) {
|
|
131
|
-
if (cfg.aliases[k] !== defaultAliases[k]) {
|
|
138
|
+
if (cfg.aliases[k] !== defaultAliases[k]) { aliasDisplay[k] = cfg.aliases[k]; }
|
|
132
139
|
});
|
|
133
140
|
applyAliasEditsToUI();
|
|
134
141
|
}
|
|
@@ -136,6 +143,12 @@ function buildWizardScript(providersJson, modelChoicesJson, providerNamesJson, d
|
|
|
136
143
|
})();
|
|
137
144
|
|
|
138
145
|
function applyAliasEditsToUI() {
|
|
146
|
+
Object.keys(aliasDisplay).forEach(function(k) {
|
|
147
|
+
var row = document.querySelector('.alias-row[data-alias="' + k + '"]');
|
|
148
|
+
if (!row) { return; }
|
|
149
|
+
var modelSpan = row.querySelector('.alias-model');
|
|
150
|
+
if (modelSpan) { modelSpan.textContent = aliasDisplay[k]; }
|
|
151
|
+
});
|
|
139
152
|
Object.keys(aliasEdits).forEach(function(k) {
|
|
140
153
|
var row = document.querySelector('.alias-row[data-alias="' + k + '"]');
|
|
141
154
|
if (!row) { return; }
|
|
@@ -172,6 +185,23 @@ function buildWizardScript(providersJson, modelChoicesJson, providerNamesJson, d
|
|
|
172
185
|
} else { nextBtn.disabled = false; }
|
|
173
186
|
}
|
|
174
187
|
|
|
188
|
+
// Single source of the route choice for a quick-pick row: explicit pill
|
|
189
|
+
// choice if its key still exists, else first provider with a key, else
|
|
190
|
+
// the row's first route. Returns the full model id or null.
|
|
191
|
+
function pickRouteFor(mc) {
|
|
192
|
+
if (!mc) { return null; }
|
|
193
|
+
var provs = Object.keys(mc.routes);
|
|
194
|
+
var prov = routingChoices[mc.alias];
|
|
195
|
+
if (!prov || !mc.routes[prov]) {
|
|
196
|
+
prov = null;
|
|
197
|
+
for (var i = 0; i < provs.length; i++) {
|
|
198
|
+
if (configuredKeys[provs[i]]) { prov = provs[i]; break; }
|
|
199
|
+
}
|
|
200
|
+
if (!prov) { prov = provs[0]; }
|
|
201
|
+
}
|
|
202
|
+
return mc.routes[prov] || null;
|
|
203
|
+
}
|
|
204
|
+
|
|
175
205
|
function updateRoutingPills() {
|
|
176
206
|
var hasAnyKey = Object.values(configuredKeys).some(function(v) { return v; });
|
|
177
207
|
var firstAvailableAlias = null;
|
|
@@ -240,6 +270,7 @@ function buildWizardScript(providersJson, modelChoicesJson, providerNamesJson, d
|
|
|
240
270
|
var fallback = document.querySelector('input[name="default-model"][value="' + firstAvailableAlias + '"]');
|
|
241
271
|
if (fallback) { fallback.checked = true; }
|
|
242
272
|
}
|
|
273
|
+
updateWritePreviews();
|
|
243
274
|
}
|
|
244
275
|
|
|
245
276
|
function updateAliasRoutes() {
|
|
@@ -275,15 +306,8 @@ function buildWizardScript(providersJson, modelChoicesJson, providerNamesJson, d
|
|
|
275
306
|
if (row.querySelector('.alias-model-select')) { return; }
|
|
276
307
|
var modelSpan = row.querySelector('.alias-model');
|
|
277
308
|
if (!modelSpan) { return; }
|
|
278
|
-
// For MODEL_CHOICES aliases: update text to match available routing
|
|
279
|
-
if (routedModels[alias]) {
|
|
280
|
-
modelSpan.textContent = routedModels[alias];
|
|
281
|
-
aliasEdits[alias] = routedModels[alias];
|
|
282
|
-
row.classList.remove('alias-no-key');
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
309
|
// Check if the model's provider has a configured key
|
|
286
|
-
var model = aliasEdits[alias] || modelSpan.textContent;
|
|
310
|
+
var model = aliasEdits[alias] || aliasDisplay[alias] || modelSpan.textContent;
|
|
287
311
|
var prefix = model.split('/')[0];
|
|
288
312
|
var noKey = false;
|
|
289
313
|
if (prefix === 'openrouter') {
|
|
@@ -301,17 +325,20 @@ function buildWizardScript(providersJson, modelChoicesJson, providerNamesJson, d
|
|
|
301
325
|
kn.length > 0 ? kn.map(function(k) { return k + ' \\u2713'; }).join(', ') : 'None';
|
|
302
326
|
var r = document.querySelector('input[name="default-model"]:checked');
|
|
303
327
|
document.getElementById('review-model').textContent = window.customDefaultModel || (r ? r.value : 'Not selected');
|
|
304
|
-
var
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
328
|
+
var writes = [];
|
|
329
|
+
var r2 = document.querySelector('input[name="default-model"]:checked');
|
|
330
|
+
if (!window.customDefaultModel && r2) {
|
|
331
|
+
var mc2 = null;
|
|
332
|
+
for (var i2 = 0; i2 < modelChoicesData.length; i2++) {
|
|
333
|
+
if (modelChoicesData[i2].alias === r2.value) { mc2 = modelChoicesData[i2]; break; }
|
|
310
334
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
335
|
+
if (mc2) {
|
|
336
|
+
var routeId2 = pickRouteFor(mc2);
|
|
337
|
+
if (routeId2) { writes.push(mc2.alias + ' \\u2192 ' + routeId2); }
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
document.getElementById('review-routing').textContent =
|
|
341
|
+
writes.length > 0 ? writes.join(', ') : 'No alias changes';
|
|
315
342
|
var editCount = Object.keys(aliasEdits).length;
|
|
316
343
|
var reviewAliases = document.getElementById('review-aliases');
|
|
317
344
|
if (reviewAliases) {
|
|
@@ -332,26 +359,31 @@ function buildWizardScript(providersJson, modelChoicesJson, providerNamesJson, d
|
|
|
332
359
|
routingChoices[alias] = provider;
|
|
333
360
|
var toggle = pill.parentElement;
|
|
334
361
|
toggle.querySelectorAll('.route-pill').forEach(function(p) { p.classList.toggle('active', p === pill); });
|
|
362
|
+
updateWritePreviews();
|
|
335
363
|
});
|
|
336
364
|
|
|
337
365
|
finishBtn.addEventListener('click', async function() {
|
|
338
366
|
finishBtn.disabled = true; finishBtn.textContent = 'Saving...';
|
|
339
367
|
try {
|
|
340
368
|
var r = document.querySelector('input[name="default-model"]:checked');
|
|
341
|
-
var dm = window.customDefaultModel || (r ? r.value :
|
|
342
|
-
var
|
|
343
|
-
modelChoicesData.forEach(function(mc) {
|
|
344
|
-
var prov = routingChoices[mc.alias];
|
|
345
|
-
if (!prov) {
|
|
346
|
-
var provs = Object.keys(mc.routes);
|
|
347
|
-
prov = provs.find(function(p) { return configuredKeys[p]; }) || provs[0];
|
|
348
|
-
}
|
|
349
|
-
routingOverrides[mc.alias] = mc.routes[prov];
|
|
350
|
-
});
|
|
369
|
+
var dm = window.customDefaultModel || (r ? r.value : null);
|
|
370
|
+
var aliasWrites = {};
|
|
351
371
|
Object.keys(aliasEdits).forEach(function(k) {
|
|
352
|
-
|
|
372
|
+
aliasWrites[k] = aliasEdits[k];
|
|
353
373
|
});
|
|
354
|
-
|
|
374
|
+
if (!window.customDefaultModel && r) {
|
|
375
|
+
// Selecting a quick pick = explicit touch: upgrade that ONE alias
|
|
376
|
+
// to the resolved id via the chosen route (user-locked decision #2).
|
|
377
|
+
var mc = null;
|
|
378
|
+
for (var i = 0; i < modelChoicesData.length; i++) {
|
|
379
|
+
if (modelChoicesData[i].alias === r.value) { mc = modelChoicesData[i]; break; }
|
|
380
|
+
}
|
|
381
|
+
if (mc) {
|
|
382
|
+
var routeId = pickRouteFor(mc);
|
|
383
|
+
if (routeId) { aliasWrites[mc.alias] = routeId; }
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
await window.sidecarSetup.invoke('sidecar:save-config', dm, aliasWrites);
|
|
355
387
|
var kc = Object.values(configuredKeys).filter(function(v) { return v; }).length;
|
|
356
388
|
await window.sidecarSetup.invoke('sidecar:setup-done', dm, kc);
|
|
357
389
|
} catch (_e) { finishBtn.disabled = false; finishBtn.textContent = 'Finish'; }
|
|
@@ -379,11 +411,12 @@ function buildWizardScript(providersJson, modelChoicesJson, providerNamesJson, d
|
|
|
379
411
|
function applyCatalog(info) {
|
|
380
412
|
catalogRows = (info && info.models) || [];
|
|
381
413
|
catalogFetchedAt = info && info.fetchedAt;
|
|
382
|
-
var section = $('model-search-section');
|
|
383
|
-
if (!section) { return; }
|
|
384
|
-
section.style.display = catalogRows.length > 0 ? '' : 'none';
|
|
385
414
|
renderSearchMeta();
|
|
386
415
|
renderSearchResults();
|
|
416
|
+
if (catalogRows.length === 0) {
|
|
417
|
+
var meta = $('model-search-meta');
|
|
418
|
+
if (meta) { meta.textContent = 'Catalog unavailable (offline?) \\u2014 use \\u21bb to retry.'; }
|
|
419
|
+
}
|
|
387
420
|
}
|
|
388
421
|
|
|
389
422
|
function renderSearchMeta() {
|
|
@@ -434,6 +467,25 @@ function buildWizardScript(providersJson, modelChoicesJson, providerNamesJson, d
|
|
|
434
467
|
var keep = box ? box.scrollTop : 0;
|
|
435
468
|
renderSearchResults();
|
|
436
469
|
if (box) { box.scrollTop = keep; }
|
|
470
|
+
updateWritePreviews();
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function updateWritePreviews() {
|
|
474
|
+
var r = document.querySelector('input[name="default-model"]:checked');
|
|
475
|
+
var sel = (!window.customDefaultModel && r) ? r.value : null;
|
|
476
|
+
document.querySelectorAll('.write-preview').forEach(function(el) {
|
|
477
|
+
var alias = el.getAttribute('data-alias');
|
|
478
|
+
el.classList.toggle('write-preview-active', alias === sel);
|
|
479
|
+
if (alias !== sel) { return; }
|
|
480
|
+
var mc = null;
|
|
481
|
+
for (var i = 0; i < modelChoicesData.length; i++) {
|
|
482
|
+
if (modelChoicesData[i].alias === alias) { mc = modelChoicesData[i]; break; }
|
|
483
|
+
}
|
|
484
|
+
if (!mc) { return; }
|
|
485
|
+
var routeId = pickRouteFor(mc);
|
|
486
|
+
var idEl = el.querySelector('.write-preview-id');
|
|
487
|
+
if (idEl && routeId) { idEl.textContent = routeId; }
|
|
488
|
+
});
|
|
437
489
|
}
|
|
438
490
|
|
|
439
491
|
document.addEventListener('input', function(e) {
|
|
@@ -443,6 +495,7 @@ function buildWizardScript(providersJson, modelChoicesJson, providerNamesJson, d
|
|
|
443
495
|
if (e.target && e.target.name === 'default-model' && e.target.checked) {
|
|
444
496
|
window.customDefaultModel = null;
|
|
445
497
|
renderSearchResults();
|
|
498
|
+
updateWritePreviews();
|
|
446
499
|
}
|
|
447
500
|
});
|
|
448
501
|
document.addEventListener('click', async function(e) {
|
package/electron/toolbar.js
CHANGED
|
@@ -8,12 +8,11 @@
|
|
|
8
8
|
const TOOLBAR_H = 40;
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* Get the brand name
|
|
12
|
-
* @
|
|
13
|
-
* @returns {string} Brand name to display
|
|
11
|
+
* Get the brand name to display
|
|
12
|
+
* @returns {string} Always 'Amicus' — all clients share one brand
|
|
14
13
|
*/
|
|
15
|
-
function getBrandName(
|
|
16
|
-
return
|
|
14
|
+
function getBrandName() {
|
|
15
|
+
return 'Amicus';
|
|
17
16
|
}
|
|
18
17
|
|
|
19
18
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "amicus",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.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",
|
|
@@ -17,14 +17,14 @@
|
|
|
17
17
|
"second-opinion",
|
|
18
18
|
"fanout"
|
|
19
19
|
],
|
|
20
|
-
"author": "
|
|
20
|
+
"author": "Christian Wagner",
|
|
21
21
|
"license": "MIT",
|
|
22
22
|
"repository": {
|
|
23
23
|
"type": "git",
|
|
24
24
|
"url": "git+https://github.com/BourbonDog/amicus.git"
|
|
25
25
|
},
|
|
26
26
|
"bugs": "https://github.com/BourbonDog/amicus/issues",
|
|
27
|
-
"homepage": "https://github.
|
|
27
|
+
"homepage": "https://bourbondog.github.io/amicus/",
|
|
28
28
|
"bin": {
|
|
29
29
|
"amicus": "./bin/amicus.js",
|
|
30
30
|
"am": "./bin/amicus.js",
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
"bin/",
|
|
40
40
|
"src/",
|
|
41
41
|
"electron/",
|
|
42
|
-
"skill/",
|
|
43
42
|
"skills/",
|
|
43
|
+
".claude-plugin/",
|
|
44
44
|
"CHANGELOG.md",
|
|
45
45
|
"scripts/postinstall.js",
|
|
46
46
|
"scripts/setup-hooks.js"
|
|
@@ -63,7 +63,9 @@
|
|
|
63
63
|
"generate-docs": "node scripts/generate-docs.js",
|
|
64
64
|
"generate-docs:check": "node scripts/generate-docs.js --check",
|
|
65
65
|
"validate-docs": "node scripts/validate-docs.js --full",
|
|
66
|
-
"prepare": "node scripts/setup-hooks.js"
|
|
66
|
+
"prepare": "node scripts/setup-hooks.js",
|
|
67
|
+
"check:secrets": "node scripts/check-secrets.js --all",
|
|
68
|
+
"check:sizes": "node scripts/check-file-sizes.js --all"
|
|
67
69
|
},
|
|
68
70
|
"dependencies": {
|
|
69
71
|
"@modelcontextprotocol/sdk": "^1.27.0",
|
package/scripts/postinstall.js
CHANGED
|
@@ -13,7 +13,7 @@ const path = require('path');
|
|
|
13
13
|
const os = require('os');
|
|
14
14
|
const { execFileSync } = require('child_process');
|
|
15
15
|
|
|
16
|
-
const SKILL_SOURCE = path.join(__dirname, '..', '
|
|
16
|
+
const SKILL_SOURCE = path.join(__dirname, '..', 'skills', 'sidecar', 'SKILL.md');
|
|
17
17
|
const COUNCIL_SOURCE_DIR = path.join(__dirname, '..', 'skills', 'second-opinion');
|
|
18
18
|
|
|
19
19
|
/** Council files + per-file install semantics: SKILL/COUNCIL-DESIGN are product code
|
|
@@ -172,12 +172,21 @@ function registerClaudeDesktop() {
|
|
|
172
172
|
addMcpToConfigFile(configPath, 'sidecar', MCP_CONFIG);
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
function main() {
|
|
175
|
+
function main(deps = {}) {
|
|
176
|
+
if (process.env.AMICUS_SKIP_POSTINSTALL === '1') {
|
|
177
|
+
console.log('[amicus] AMICUS_SKIP_POSTINSTALL set — skipping global setup (plugin channel handles registration).');
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const _installSkill = deps.installSkill || installSkill;
|
|
181
|
+
const _installCouncilSkill = deps.installCouncilSkill || installCouncilSkill;
|
|
182
|
+
const _registerClaudeCode = deps.registerClaudeCode || registerClaudeCode;
|
|
183
|
+
const _registerClaudeDesktop = deps.registerClaudeDesktop || registerClaudeDesktop;
|
|
184
|
+
|
|
176
185
|
console.log('[amicus] Installing...');
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
186
|
+
_installSkill();
|
|
187
|
+
_installCouncilSkill();
|
|
188
|
+
_registerClaudeCode();
|
|
189
|
+
_registerClaudeDesktop();
|
|
181
190
|
|
|
182
191
|
console.log('');
|
|
183
192
|
console.log('[amicus] Setup:');
|
|
@@ -190,4 +199,4 @@ if (require.main === module) {
|
|
|
190
199
|
main();
|
|
191
200
|
}
|
|
192
201
|
|
|
193
|
-
module.exports = { addMcpToConfigFile, installSkill, installCouncilSkill, COUNCIL_FILES };
|
|
202
|
+
module.exports = { main, addMcpToConfigFile, installSkill, installCouncilSkill, COUNCIL_FILES };
|
|
@@ -35,7 +35,7 @@ non-Claude chairman + per-model inspectable artifacts.
|
|
|
35
35
|
chair) is prose workflow Claude performs while driving the `amicus` CLI. v3 note: the *transport*
|
|
36
36
|
is now engine-native — each review wave is ONE `amicus fanout --json` call returning structured
|
|
37
37
|
run documents — but scoring, tallying, anonymization, and synthesis remain Claude's manual work.
|
|
38
|
-
No backend, no parsing code beyond reading JSON fields.
|
|
38
|
+
No backend, no parsing code beyond reading JSON fields. Deterministic arithmetic/formatting/schema helpers under `amicus council` (findings validation, tier tally, street-cred, ledger) are sanctioned; judgment, synthesis, anonymization, and de-anonymization remain Claude's inline work.
|
|
39
39
|
|
|
40
40
|
## 3. What changes vs. v1
|
|
41
41
|
|
|
@@ -44,7 +44,7 @@ non-Claude chairman + per-model inspectable artifacts.
|
|
|
44
44
|
| Independent reviews | ✅ Phase 2 parallel sidecars | ✅ Stage 1 — now emits a **structured findings list** |
|
|
45
45
|
| Cross-review | ❌ none | ⭐ **Stage 2** — anonymized peer ranking **+** per-finding adjudication |
|
|
46
46
|
| Synthesis | Claude synthesizes | ⭐ **Council-model chair** synthesizes; Claude only presents |
|
|
47
|
-
| Decision tiers | Claude's consensus/divergence read | ⭐ **Peer-validated** tiers (Confirmed / Contested / Singleton) |
|
|
47
|
+
| Decision tiers | Claude's consensus/divergence read | ⭐ **Peer-validated** tiers (Disputed / Confirmed / Contested / Singleton) |
|
|
48
48
|
| Scoring | none | ⭐ Reviewer **street-cred** + per-finding **peer-confidence** |
|
|
49
49
|
| Artifacts | reviewed copy + report | + per-model raw reviews, cross-review matrix, chair verdict (run folder) |
|
|
50
50
|
| MODEL-NOTES | per-model quirks | + **reviewer-reliability** rolling table feeding recommendations |
|
|
@@ -106,11 +106,9 @@ Run as ordered phases; track as todos. **Three sequential waves of model calls**
|
|
|
106
106
|
- Chair selection & fallback: §5.3.
|
|
107
107
|
|
|
108
108
|
### Stage 4 — Tiered decisions (peer-validated)
|
|
109
|
-
- **Consensus tier** = **Confirmed** findings (
|
|
110
|
-
|
|
111
|
-
-
|
|
112
|
-
findings → present **each individually**, showing the dissent and which model raised/disputed it.
|
|
113
|
-
- Record every decision (accepted / denied / modified).
|
|
109
|
+
- **Consensus tier** = **Confirmed** findings (≥ 2 peer agreements, agrees dominate) → offer one **bulk accept/deny** (user may name exceptions).
|
|
110
|
+
- **Judgment tier** = **Disputed** (strong peer pushback), **Contested** (live dispute), or **Singleton** (only the raiser) → present **each individually**, showing the adjudication data and which model raised/disputed it.
|
|
111
|
+
- Record every decision (accepted / denied / modified / deferred).
|
|
114
112
|
|
|
115
113
|
### Stage 5 — Outputs
|
|
116
114
|
- **Editable source** → write `<stem>-reviewed.<ext>` next to the original (accepted changes
|
|
@@ -119,8 +117,10 @@ Run as ordered phases; track as todos. **Three sequential waves of model calls**
|
|
|
119
117
|
|
|
120
118
|
### Stage 6 — Capture lessons (compounding)
|
|
121
119
|
- Reflect on failures/mitigations and briefing wording, as today.
|
|
122
|
-
- **
|
|
123
|
-
- **
|
|
120
|
+
- **Ledger auto-appends** — `ledger.appendRun(record)` writes one row per (run × model) to `council-ledger.jsonl` automatically at finalize (shown in the run summary). No manual reliability-table update needed.
|
|
121
|
+
- **Qualitative MODEL-NOTES update (approval-gated):** draft per-model quirk/conformance notes; write the proposed diff to `_tmp-proposed-model-notes-update.md`; present its path in the approval prompt; do not write until approved.
|
|
122
|
+
The approval prompt carries the diff file's path; chat text alone is not sufficient (an approval
|
|
123
|
+
dialog can hide the chat transcript). Keep it tight.
|
|
124
124
|
|
|
125
125
|
## 5. Key mechanics
|
|
126
126
|
|
|
@@ -134,14 +134,23 @@ Run as ordered phases; track as todos. **Three sequential waves of model calls**
|
|
|
134
134
|
and judged blind by the council models. Claude never ranks/adjudicates (it holds the map) —
|
|
135
135
|
the asymmetry detailed in §5.4.
|
|
136
136
|
|
|
137
|
-
### 5.2 Scoring (
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
- **
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
137
|
+
### 5.2 Scoring (`amicus council tally` computes; Claude may override at the margins)
|
|
138
|
+
|
|
139
|
+
**Street cred** — computed two ways by `amicus council tally`:
|
|
140
|
+
- **withSelf** = each model's mean rank position across **all** judges' `FINAL RANKING:` blocks (lower = better).
|
|
141
|
+
- **peersOnly** = mean rank across judges **other than** that model (self-vote excluded).
|
|
142
|
+
The cross-review matrix shows both; the ledger and Stage-0 bench recommendations consume **peersOnly** only.
|
|
143
|
+
|
|
144
|
+
**Per-finding peer-confidence tier** — determined by a **peers-only** cascade: for a finding raised by model R, peers are all judges except R (the raiser's own adjudication is excluded — consistent with the peers-only street-cred rule). Let `a` = peer agrees, `d` = peer disputes. The cascade is exhaustive and mutually exclusive:
|
|
145
|
+
|
|
146
|
+
| Priority | Tier | Rule | Meaning |
|
|
147
|
+
|---|---|---|---|
|
|
148
|
+
| 1 | **Disputed** | `d ≥ 2` and `d > a` | Strong peer pushback — the finding itself is likely wrong |
|
|
149
|
+
| 2 | **Confirmed** | `a ≥ 2` and `a > d` | ≥ 2 independent corroborations, agrees dominate |
|
|
150
|
+
| 3 | **Contested** | `d ≥ 1` (whatever remains) | At least one live dispute — in question |
|
|
151
|
+
| 4 | **Singleton** | else (`d = 0` and `a < 2`) | At most one endorsement, no pushback — thin |
|
|
152
|
+
|
|
153
|
+
`confidence` is `thin` when total engaged peers `a + d ≤ 1` — cells `(0,0)`, `(1,0)`, and `(0,1)`. **Claude may override the tier at `thin` margins** (recorded as `tierOverride: {from, to, reason}` and surfaced in the matrix and `verdict.json`). These four tiers drive Stage 4. `amicus council tally` assigns them deterministically; judgment at the margins remains Claude's.
|
|
145
154
|
|
|
146
155
|
### 5.3 Chair selection & fallback
|
|
147
156
|
- Default: Claude **recommends a non-Claude chair** from the council each run (often the
|
|
@@ -159,7 +168,7 @@ Lets you see how the bench judges Claude's *own* take.
|
|
|
159
168
|
- **Which review: always fresh** — Claude does a new structured Stage-1 review on the artifact
|
|
160
169
|
every time it's enabled (not a formalization of upstream feedback).
|
|
161
170
|
- **Readout — "How Claude's review fared":** Claude's street-cred rank among peers and the
|
|
162
|
-
Confirmed/Contested/Singleton split of its findings, reported in the matrix and report.
|
|
171
|
+
Disputed/Confirmed/Contested/Singleton split of its findings, reported in the matrix and report.
|
|
163
172
|
- **Integrity:** when Claude presents results, it reports the bench's verdict on its own review
|
|
164
173
|
at face value — no defending or re-litigating.
|
|
165
174
|
|
|
@@ -169,22 +178,16 @@ One tidy run folder: `output/<stem>-council/` (or `./second-opinion/<stem>-counc
|
|
|
169
178
|
- `review-<model>.md` ×N — raw Stage 1 reviews (plus `review-claude.md` when "Claude in the
|
|
170
179
|
council" is on)
|
|
171
180
|
- `crossreview-matrix.md` — adjudication grid + street-cred table (de-anonymized)
|
|
172
|
-
- `verdict.md` — the chair's synthesis
|
|
181
|
+
- `verdict.md` — the chair's synthesis (prose)
|
|
182
|
+
- `verdict.json` — schema-stamped machine-readable record: tally output + Stage-4 decisions, written via `buildVerdict(record, decisions)` at Stage 5
|
|
173
183
|
- `report.md` — synthesis + decision log + what was applied (+ the "How Claude's review fared"
|
|
174
184
|
readout when "Claude in the council" is on)
|
|
175
185
|
- `<stem>-reviewed.<ext>` — written **next to the original**, as today (editable sources only)
|
|
176
186
|
- Temp extracts get a clearly-temporary name and are cleaned up at the end.
|
|
177
187
|
|
|
178
188
|
## 7. MODEL-NOTES reviewer-reliability
|
|
179
|
-
Add a compact rolling table consulted in Stage 0 and updated (with approval) in Stage 6:
|
|
180
|
-
|
|
181
|
-
| model | runs | avg street-cred | confirm-rate | notes |
|
|
182
|
-
|---|---|---|---|---|
|
|
183
189
|
|
|
184
|
-
- **avg street-cred
|
|
185
|
-
- **confirm-rate** — share of this model's findings that ended up **Confirmed** by peers.
|
|
186
|
-
- Used to justify recommendations ("DeepSeek findings peer-confirm ~80% → strong default
|
|
187
|
-
reviewer"). Kept tight per the existing no-bloat rule; merge/prune rather than append.
|
|
190
|
+
The append-only `council-ledger.jsonl` (consumed via `amicus council stats`) is the **authoritative source of quantitative reviewer-reliability data** — runs, avg peers-only street-cred, confirm-rate, fact-error rate, conformance distribution. `MODEL-NOTES.md` keeps only *qualitative* per-model quirks and structural-conformance notes (`clean` / `repaired` / `unstructured`); it may embed a snapshot generated from `amicus council stats --json` but is no longer hand-edited for numbers. Stage-6 reliability updates are written by the ledger auto-append; the MODEL-NOTES prose update remains approval-gated.
|
|
188
191
|
|
|
189
192
|
## 8. Gating, cost, degradation & failure handling
|
|
190
193
|
|
|
@@ -205,23 +208,22 @@ Add a compact rolling table consulted in Stage 0 and updated (with approval) in
|
|
|
205
208
|
- **Stage 3:** chair failure uses the same fallback chain (re-run → promote next-best non-Claude
|
|
206
209
|
→ Claude chairs with explicit disclosure).
|
|
207
210
|
- **Run stats (v3):** `report.md` includes a per-leg table (model, status, durationMs) read from
|
|
208
|
-
the wave/run documents. The schema carries no cost data — never invent cost figures.
|
|
211
|
+
the wave/run documents. `durationMs` and `usage` are copied verbatim from the per-leg run docs; any leg with no run doc gets `durationMs: null` (and `usage: null`) — never invent a value. The schema carries no cost data — never invent cost figures.
|
|
209
212
|
- **Transient failures:** provider 502s etc. → re-run the affected leg (solo `start --json`) or
|
|
210
213
|
the wave; never present a half-finished run as an answer.
|
|
211
214
|
|
|
212
215
|
## 9. Non-goals (YAGNI)
|
|
213
216
|
- No web UI, API server, or persistent conversation store (LLM Council's app shell).
|
|
214
|
-
- No code/backend for scoring or parsing — Claude does it inline.
|
|
217
|
+
- No code/backend for scoring or parsing — Claude does it inline. Deterministic arithmetic/formatting/schema helpers under `amicus council` (findings validation, tier tally, street-cred, ledger) are sanctioned; judgment, synthesis, anonymization, and de-anonymization remain Claude's inline work.
|
|
215
218
|
- No automatic MODEL-NOTES writes — always approval-gated.
|
|
216
219
|
- Claude is **not** a council member by default; it joins only via the opt-in toggle (§5.4),
|
|
217
220
|
and even then it is judged-but-non-voting/non-chairing.
|
|
218
221
|
|
|
219
222
|
## 10. Open questions
|
|
220
223
|
- None blocking. Possible later refinement: a numeric peer-confidence score instead of the
|
|
221
|
-
|
|
224
|
+
four qualitative tiers, if tiers prove too coarse in practice.
|
|
222
225
|
|
|
223
226
|
## 11. Implementation surface
|
|
224
|
-
- `SKILL.md` — the Stage 0–6 council flow on the v3 transport.
|
|
225
|
-
- `MODEL-NOTES.md` —
|
|
226
|
-
|
|
227
|
-
- No other files.
|
|
227
|
+
- `SKILL.md` — the Stage 0–6 council flow on the v3 transport (WS-3: findings contract, tally assembly recipe, `amicus council tally/stats`, `verdict.json`, ledger auto-append).
|
|
228
|
+
- `MODEL-NOTES.md` — qualitative per-model quirks, structural-conformance notes, cost guardrail, Stage-2 briefing tips. Quantitative reliability data now generated by `amicus council stats` (ledger). Engine workarounds that F1/F2/F4 made obsolete were pruned at v3.
|
|
229
|
+
- `src/council/` — the deterministic helpers (`findings.js`, `tally.js`, `verdict.js`, `ledger.js`). No other files.
|