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.
Files changed (55) hide show
  1. package/.claude-plugin/marketplace.json +14 -0
  2. package/.claude-plugin/plugin.json +19 -0
  3. package/CHANGELOG.md +86 -0
  4. package/LICENSE +22 -1
  5. package/README.md +14 -3
  6. package/bin/amicus.js +17 -162
  7. package/electron/ipc-setup.js +30 -9
  8. package/electron/main.js +13 -5
  9. package/electron/preload.js +30 -10
  10. package/electron/setup-ui-keys.js +9 -0
  11. package/electron/setup-ui-model.js +33 -23
  12. package/electron/setup-ui-styles.js +6 -1
  13. package/electron/setup-ui.js +91 -38
  14. package/electron/toolbar.js +4 -5
  15. package/package.json +7 -5
  16. package/scripts/postinstall.js +16 -7
  17. package/skills/second-opinion/COUNCIL-DESIGN.md +36 -34
  18. package/skills/second-opinion/MODEL-NOTES.md +23 -17
  19. package/skills/second-opinion/SKILL.md +84 -51
  20. package/{skill → skills/sidecar}/SKILL.md +14 -4
  21. package/src/cli-handlers-council.js +59 -0
  22. package/src/cli-handlers-doctor.js +173 -0
  23. package/src/cli-handlers-run.js +196 -0
  24. package/src/cli-handlers.js +66 -1
  25. package/src/cli.js +16 -2
  26. package/src/council/findings.js +48 -0
  27. package/src/council/ledger.js +82 -0
  28. package/src/council/tally.js +108 -0
  29. package/src/council/verdict.js +48 -0
  30. package/src/headless.js +43 -149
  31. package/src/mcp-server.js +6 -0
  32. package/src/sidecar/budget.js +83 -0
  33. package/src/sidecar/conversation-mirror.js +128 -0
  34. package/src/sidecar/fanout-leg.js +4 -1
  35. package/src/sidecar/fanout.js +34 -7
  36. package/src/sidecar/interactive-mirror.js +66 -0
  37. package/src/sidecar/interactive.js +35 -21
  38. package/src/sidecar/models.js +41 -10
  39. package/src/sidecar/session-finalize.js +26 -0
  40. package/src/sidecar/session-utils.js +5 -5
  41. package/src/sidecar/setup.js +55 -42
  42. package/src/sidecar/start.js +19 -6
  43. package/src/utils/activity-poller.js +47 -0
  44. package/src/utils/alias-resolver.js +1 -1
  45. package/src/utils/config.js +4 -4
  46. package/src/utils/curated-models.js +88 -45
  47. package/src/utils/error-doc.js +55 -0
  48. package/src/utils/lifecycle.js +1 -1
  49. package/src/utils/model-catalog.js +1 -1
  50. package/src/utils/model-fetcher.js +16 -2
  51. package/src/utils/pricing.js +93 -0
  52. package/src/utils/quick-picks.js +81 -0
  53. package/src/utils/result-schema.js +21 -2
  54. package/src/utils/session-abort.js +40 -13
  55. 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 and pre-selection support.
6
- * Models are disabled when no configured API key matches their routes.
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
- const { getCuratedModels } = require('../src/utils/curated-models');
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 static route text.
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 (F5). Hidden until the wizard script confirms
53
- * a non-empty catalog; rows are rendered client-side from the get-catalog IPC.
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" style="display:none">
55
+ return `<div id="model-search-section">
56
+ <div class="search-label">&hellip;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">&#x21bb;</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: string, label: string, routes: Object<string,string>}>} choices
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
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 = (showToggle && p === bestProvider) || (!showToggle && p === bestProvider);
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 model to use when no --model flag is given.</p>
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, MODEL_CHOICES, PROVIDER_NAMES };
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 };
@@ -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, MODEL_CHOICES, PROVIDER_NAMES } = require('./setup-ui-model');
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 { client = 'code-local' } = options;
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(MODEL_CHOICES);
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(MODEL_CHOICES);
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]) { aliasEdits[k] = cfg.aliases[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 routeLines = [];
305
- modelChoicesData.forEach(function(mc) {
306
- var prov = routingChoices[mc.alias];
307
- if (!prov) {
308
- var provs = Object.keys(mc.routes);
309
- prov = provs.find(function(p) { return configuredKeys[p]; }) || provs[0];
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
- var provName = providerNamesData[prov] || prov;
312
- routeLines.push(mc.alias + ' \\u2192 ' + provName);
313
- });
314
- document.getElementById('review-routing').textContent = routeLines.join(', ');
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 : 'gemini');
342
- var routingOverrides = {};
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
- routingOverrides[k] = aliasEdits[k];
372
+ aliasWrites[k] = aliasEdits[k];
353
373
  });
354
- await window.sidecarSetup.invoke('sidecar:save-config', dm, routingOverrides);
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) {
@@ -8,12 +8,11 @@
8
8
  const TOOLBAR_H = 40;
9
9
 
10
10
  /**
11
- * Get the brand name based on client type
12
- * @param {string} [client='code-local'] - Client type (code-local, code-web, cowork)
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(client) {
16
- return client === 'cowork' ? 'Openwork Amicus' : 'Amicus';
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.0.0",
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": "BourbonDog",
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.com/BourbonDog/amicus#readme",
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",
@@ -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, '..', 'skill', 'SKILL.md');
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
- installSkill();
178
- installCouncilSkill();
179
- registerClaudeCode();
180
- registerClaudeDesktop();
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 (peers agree) → offer one **bulk accept/deny**
110
- (user may name exceptions).
111
- - **Judgment tier** = **Contested** (peers dispute/split) or **Singleton** (only the raiser)
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
- - **Additionally** update the per-model **reviewer-reliability** table (§7).
123
- - **Show the proposed MODEL-NOTES diff and get approval before writing.** Keep it tight.
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 (Claude tallies by hand no code required)
138
- - **Street cred** = each model's **average rank position** across all judges' `FINAL RANKING:`
139
- blocks (lower = better), exactly as LLM Council's aggregate. Surface as a small table.
140
- - **Per-finding peer-confidence** = qualitative tier from the adjudications:
141
- - **Confirmed** agrees clearly outweigh disputes (and ≥2 judges engaged).
142
- - **Contested** meaningful split or explicit disputes.
143
- - **Singleton** — only the original raiser; others neutral/silent.
144
- These tiers drive Stage 4. Claude exercises judgment at the margins; no rigid formula.
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** running average rank when peer-ranked.
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
- three qualitative tiers, if tiers prove too coarse in practice.
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` — reviewer-reliability table, per-model quirks, cost guardrail, Stage-2
226
- briefing tips. Engine workarounds that F1/F2/F4 made obsolete were pruned at v3.
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.