amicus 1.9.0 → 2.0.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 +149 -0
- package/README.md +40 -170
- package/bin/amicus.js +14 -20
- package/commands/council.md +3 -1
- package/electron/fold.js +10 -1
- package/electron/ipc-setup.js +10 -15
- package/electron/main.js +21 -16
- package/electron/preload-setup.js +0 -1
- package/electron/setup-ui-council.js +64 -10
- package/electron/setup-ui-styles.js +34 -3
- package/electron/setup-ui.js +44 -12
- package/package.json +2 -5
- package/skills/second-opinion/MODEL-NOTES.md +2 -2
- package/skills/second-opinion/SKILL.md +24 -23
- package/skills/sidecar/SKILL.md +3 -3
- package/src/cli-handlers-council.js +101 -1
- package/src/cli-handlers-doctor.js +7 -0
- package/src/cli-handlers-run.js +4 -4
- package/src/cli-handlers-spend.js +198 -0
- package/src/cli.js +35 -0
- package/src/council/presets-cli.js +141 -0
- package/src/headless.js +146 -38
- package/src/index.js +1 -9
- package/src/mcp-server.js +132 -108
- package/src/mcp-tools.js +27 -3
- package/src/mcp-wait.js +8 -5
- package/src/opencode-client.js +33 -10
- package/src/prompt-builder.js +32 -11
- package/src/session-manager.js +7 -14
- package/src/sidecar/continue.js +12 -5
- package/src/sidecar/conversation-mirror.js +22 -1
- package/src/sidecar/crash-handler.js +2 -1
- package/src/sidecar/fanout-leg.js +12 -3
- package/src/sidecar/fanout.js +27 -10
- package/src/sidecar/interactive-process.js +6 -17
- package/src/sidecar/interactive.js +5 -6
- package/src/sidecar/models.js +33 -4
- package/src/sidecar/progress.js +2 -1
- package/src/sidecar/read.js +4 -6
- package/src/sidecar/resume.js +19 -4
- package/src/sidecar/session-finalize.js +2 -1
- package/src/sidecar/session-utils.js +13 -35
- package/src/sidecar/setup-window.js +2 -3
- package/src/sidecar/start.js +22 -7
- package/src/utils/abort-coordinator.js +57 -7
- package/src/utils/api-key-store.js +2 -13
- package/src/utils/config.js +30 -43
- package/src/utils/council-presets.js +87 -0
- package/src/utils/env-loader.js +1 -2
- package/src/utils/fold-marker.js +79 -0
- package/src/utils/idle-watchdog.js +9 -12
- package/src/utils/lifecycle.js +1 -1
- package/src/utils/mcp-discovery.js +29 -5
- package/src/utils/mcp-self-identity.js +12 -5
- package/src/utils/model-catalog.js +54 -6
- package/src/utils/read-slice.js +73 -0
- package/src/utils/remediation-hints.js +9 -0
- package/src/utils/result-schema.js +8 -2
- package/src/utils/session-abort.js +1 -1
- package/src/utils/session-index-tmp-sweep.js +80 -0
- package/src/utils/session-index.js +4 -5
- package/src/utils/session-path.js +6 -10
- package/src/utils/shared-server.js +7 -5
- package/src/utils/spend-ledger.js +80 -0
- package/src/utils/updater.js +2 -3
- package/src/utils/env-compat.js +0 -38
package/electron/fold.js
CHANGED
|
@@ -9,6 +9,7 @@ const { logger } = require('../src/utils/logger');
|
|
|
9
9
|
const { requestSummaryFromModel } = require('./summary');
|
|
10
10
|
const { getSummaryTemplate } = require('../src/prompt-builder');
|
|
11
11
|
const { tokenCss } = require('../src/design/tokens');
|
|
12
|
+
const { buildFoldMarker, generateFoldNonce } = require('../src/utils/fold-marker');
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Create a fold handler bound to the window state
|
|
@@ -19,9 +20,17 @@ const { tokenCss } = require('../src/design/tokens');
|
|
|
19
20
|
* @param {string} state.sessionId - OpenCode session ID
|
|
20
21
|
* @param {string} state.taskId - Sidecar task ID
|
|
21
22
|
* @param {number} state.port - OpenCode server port
|
|
23
|
+
* @param {string} [state.nonce] - Per-run fold nonce (15b.3, #BL-7 residual). Set by
|
|
24
|
+
* main.js from AMICUS_FOLD_NONCE — the SAME value baked into the system prompt's
|
|
25
|
+
* fold instruction, so a completion this handler writes matches what the model was
|
|
26
|
+
* actually asked to emit. Falls back to a freshly generated nonce when absent (e.g.
|
|
27
|
+
* a caller/test that doesn't thread one through) — this is purely defensive: the
|
|
28
|
+
* GUI fold path is exit-code driven, not marker-detected, so an un-advertised
|
|
29
|
+
* fallback nonce here cannot be exploited the way headless.js's detector could.
|
|
22
30
|
* @returns {{ triggerFold: Function, hasFolded: Function, isFolding: Function, hasCompleted: Function }}
|
|
23
31
|
*/
|
|
24
32
|
function createFoldHandler(state) {
|
|
33
|
+
const nonce = state.nonce || generateFoldNonce();
|
|
25
34
|
// `folded` is set synchronously at triggerFold ENTRY and covers both
|
|
26
35
|
// "in flight" and "done" — this is `hasFolded()`'s existing external
|
|
27
36
|
// contract (main.js wires it straight into createCloseGuard's `hasFolded`
|
|
@@ -52,7 +61,7 @@ function createFoldHandler(state) {
|
|
|
52
61
|
}
|
|
53
62
|
|
|
54
63
|
const output = [
|
|
55
|
-
|
|
64
|
+
buildFoldMarker(nonce),
|
|
56
65
|
`Model: ${state.model}`,
|
|
57
66
|
`Session: ${state.sessionId || state.taskId}`,
|
|
58
67
|
`Client: ${state.client}`,
|
package/electron/ipc-setup.js
CHANGED
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
* Extracted from main.js to keep file sizes under 300 lines.
|
|
5
5
|
* Registers all setup-mode IPC handlers: validate-key, save-key,
|
|
6
6
|
* remove-key, setup-done, save-config, get-config, get-api-keys,
|
|
7
|
-
*
|
|
7
|
+
* get-catalog, and refresh-catalog.
|
|
8
|
+
* (sidecar:fetch-models was retired in B33/#12 — Step 3's alias editor now
|
|
9
|
+
* shares the TTL-cached get-catalog data Step 2 loads instead of a second,
|
|
10
|
+
* uncached live fetch.)
|
|
8
11
|
*/
|
|
9
12
|
|
|
10
13
|
const { ipcMain } = require('electron');
|
|
@@ -160,19 +163,6 @@ function registerSetupHandlers(getMainWindow) {
|
|
|
160
163
|
}
|
|
161
164
|
});
|
|
162
165
|
|
|
163
|
-
ipcMain.handle('sidecar:fetch-models', async () => {
|
|
164
|
-
try {
|
|
165
|
-
const { readApiKeyValues } = require('../src/utils/api-key-store');
|
|
166
|
-
const { fetchAllModels, groupModelsByFamily } = require('../src/utils/model-fetcher');
|
|
167
|
-
const keys = readApiKeyValues();
|
|
168
|
-
const models = await fetchAllModels(keys);
|
|
169
|
-
return groupModelsByFamily(models);
|
|
170
|
-
} catch (err) {
|
|
171
|
-
logger.error('fetch-models handler error', { error: err.message });
|
|
172
|
-
return [];
|
|
173
|
-
}
|
|
174
|
-
});
|
|
175
|
-
|
|
176
166
|
// F5: wizard Step 2 reads the catalog CACHE (self-refreshing when stale).
|
|
177
167
|
ipcMain.handle('sidecar:get-catalog', async () => {
|
|
178
168
|
try {
|
|
@@ -206,7 +196,12 @@ function registerSetupHandlers(getMainWindow) {
|
|
|
206
196
|
const catalog = await getCatalog();
|
|
207
197
|
const free = listFreeModels(catalog);
|
|
208
198
|
const suggested = new Set(suggestFreeCouncil(free, 3).map(r => r.id));
|
|
209
|
-
return free.map(r => ({
|
|
199
|
+
return free.map(r => ({
|
|
200
|
+
id: r.id,
|
|
201
|
+
suggested: suggested.has(r.id),
|
|
202
|
+
name: r.name,
|
|
203
|
+
vendor: r.id.split('/')[1] || '',
|
|
204
|
+
}));
|
|
210
205
|
} catch (_err) { return []; }
|
|
211
206
|
});
|
|
212
207
|
}
|
package/electron/main.js
CHANGED
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
const { app, BrowserWindow, BrowserView, globalShortcut, ipcMain, screen } = require('electron');
|
|
16
16
|
const path = require('path');
|
|
17
17
|
const { logger } = require('../src/utils/logger');
|
|
18
|
-
const { getCompatEnv } = require('../src/utils/env-compat');
|
|
19
18
|
const { buildToolbarHTML, TOOLBAR_H, getBrandName } = require('./toolbar');
|
|
20
19
|
const { createFoldHandler } = require('./fold');
|
|
21
20
|
const { createCloseGuard } = require('./close-guard');
|
|
@@ -58,20 +57,25 @@ process.on('unhandledRejection', (reason) => {
|
|
|
58
57
|
// Configuration from Environment (set by src/sidecar/start.js)
|
|
59
58
|
// ============================================================================
|
|
60
59
|
|
|
61
|
-
const MODE =
|
|
62
|
-
const TASK_ID =
|
|
63
|
-
const MODEL =
|
|
64
|
-
const CWD =
|
|
60
|
+
const MODE = process.env.AMICUS_MODE || 'sidecar';
|
|
61
|
+
const TASK_ID = process.env.AMICUS_TASK_ID || 'unknown';
|
|
62
|
+
const MODEL = process.env.AMICUS_MODEL || 'unknown';
|
|
63
|
+
const CWD = process.env.AMICUS_CWD || process.cwd();
|
|
65
64
|
// The directory the OpenCode session was actually scoped to (#45). Set by the
|
|
66
65
|
// interactive launcher as canonicalProjectPath(--cwd) so the Web-UI route is
|
|
67
66
|
// built from the SAME directory createSession used. Falls back to CWD for
|
|
68
67
|
// back-compat with launchers that predate this env var.
|
|
69
|
-
const SESSION_DIRECTORY =
|
|
70
|
-
const CLIENT =
|
|
71
|
-
const OPENCODE_PORT = parseInt(
|
|
72
|
-
const OPENCODE_SESSION_ID =
|
|
73
|
-
const FOLD_SHORTCUT =
|
|
74
|
-
const WINDOW_POSITION =
|
|
68
|
+
const SESSION_DIRECTORY = process.env.AMICUS_SESSION_DIRECTORY || CWD;
|
|
69
|
+
const CLIENT = process.env.AMICUS_CLIENT || 'code-local';
|
|
70
|
+
const OPENCODE_PORT = parseInt(process.env.AMICUS_OPENCODE_PORT || '4096', 10);
|
|
71
|
+
const OPENCODE_SESSION_ID = process.env.AMICUS_SESSION_ID;
|
|
72
|
+
const FOLD_SHORTCUT = process.env.AMICUS_FOLD_SHORTCUT || 'CommandOrControl+Shift+F';
|
|
73
|
+
const WINDOW_POSITION = process.env.AMICUS_WINDOW_POSITION || 'right';
|
|
74
|
+
// 15b.3: per-run fold nonce (#BL-7 residual). Set by the interactive launcher
|
|
75
|
+
// (src/sidecar/interactive-process.js buildElectronEnv) from the SAME value
|
|
76
|
+
// baked into the system prompt's fold instruction. undefined when a launcher
|
|
77
|
+
// predates this env var — fold.js falls back to the legacy bare marker.
|
|
78
|
+
const FOLD_NONCE = process.env.AMICUS_FOLD_NONCE;
|
|
75
79
|
|
|
76
80
|
const OPENCODE_URL = `http://localhost:${OPENCODE_PORT}`;
|
|
77
81
|
|
|
@@ -89,7 +93,8 @@ const foldHandler = createFoldHandler({
|
|
|
89
93
|
cwd: CWD,
|
|
90
94
|
sessionId: OPENCODE_SESSION_ID,
|
|
91
95
|
taskId: TASK_ID,
|
|
92
|
-
port: OPENCODE_PORT
|
|
96
|
+
port: OPENCODE_PORT,
|
|
97
|
+
nonce: FOLD_NONCE
|
|
93
98
|
});
|
|
94
99
|
// Auto-fold on close (backlog B01): a user-initiated window close with no
|
|
95
100
|
// fold yet run must not silently discard the session summary. See
|
|
@@ -128,7 +133,7 @@ function createAmicusWindow() {
|
|
|
128
133
|
|
|
129
134
|
// Check for updates: prefer env var from CLI (cache is one-shot), fallback to direct check
|
|
130
135
|
let updateInfo = null;
|
|
131
|
-
const updateInfoRaw =
|
|
136
|
+
const updateInfoRaw = process.env.AMICUS_UPDATE_INFO;
|
|
132
137
|
if (updateInfoRaw) {
|
|
133
138
|
try { updateInfo = JSON.parse(updateInfoRaw); } catch (_) {}
|
|
134
139
|
}
|
|
@@ -201,7 +206,7 @@ function createAmicusWindow() {
|
|
|
201
206
|
// silently hung process (the historical "Starting up... | 0 messages" bug).
|
|
202
207
|
const failsafe = attachLoadFailsafe({
|
|
203
208
|
webContents: contentView.webContents,
|
|
204
|
-
timeoutMs: parseInt(
|
|
209
|
+
timeoutMs: parseInt(process.env.AMICUS_GUI_LOAD_TIMEOUT_MS || '', 10) || undefined,
|
|
205
210
|
onFail: ({ reason, errorCode, errorDescription, validatedURL }) => {
|
|
206
211
|
logger.error('OpenCode UI failed to load', {
|
|
207
212
|
reason, errorCode, errorDescription, validatedURL, url: contentUrl
|
|
@@ -217,7 +222,7 @@ function createAmicusWindow() {
|
|
|
217
222
|
// On timeout, show whatever is in flight rather than aborting the load.
|
|
218
223
|
mainWindow.addBrowserView(contentView);
|
|
219
224
|
updateContentBounds();
|
|
220
|
-
if (!process.env.
|
|
225
|
+
if (!process.env.AMICUS_HEADLESS_TEST) {
|
|
221
226
|
mainWindow.show();
|
|
222
227
|
}
|
|
223
228
|
}
|
|
@@ -234,7 +239,7 @@ function createAmicusWindow() {
|
|
|
234
239
|
failsafe.cancel();
|
|
235
240
|
mainWindow.addBrowserView(contentView);
|
|
236
241
|
updateContentBounds();
|
|
237
|
-
if (!process.env.
|
|
242
|
+
if (!process.env.AMICUS_HEADLESS_TEST) {
|
|
238
243
|
mainWindow.show();
|
|
239
244
|
}
|
|
240
245
|
});
|
|
@@ -38,23 +38,77 @@ function buildCouncilScript() {
|
|
|
38
38
|
else if (meta && !loaded) { meta.textContent = ''; }
|
|
39
39
|
};
|
|
40
40
|
|
|
41
|
+
// Rows arrive pre-sorted by vendor (listFreeModels); group consecutive
|
|
42
|
+
// same-vendor runs into collapsible sections, mirroring the Step-3
|
|
43
|
+
// alias editor's .alias-group <details>/<summary> precedent.
|
|
44
|
+
function groupByVendor(rows) {
|
|
45
|
+
var groups = [];
|
|
46
|
+
var byVendor = {};
|
|
47
|
+
(rows || []).forEach(function(r) {
|
|
48
|
+
var v = r.vendor || (r.id.split('/')[1] || '');
|
|
49
|
+
if (!byVendor[v]) {
|
|
50
|
+
byVendor[v] = { vendor: v, rows: [] };
|
|
51
|
+
groups.push(byVendor[v]);
|
|
52
|
+
}
|
|
53
|
+
byVendor[v].rows.push(r);
|
|
54
|
+
});
|
|
55
|
+
return groups;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function vendorLabel(vendor) {
|
|
59
|
+
if (!vendor) { return 'Other'; }
|
|
60
|
+
return vendor.charAt(0).toUpperCase() + vendor.slice(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function buildRow(r, idx) {
|
|
64
|
+
var id = 'fc-' + idx;
|
|
65
|
+
var row = document.createElement('label');
|
|
66
|
+
row.className = 'council-row';
|
|
67
|
+
var cb = document.createElement('input');
|
|
68
|
+
cb.type = 'checkbox'; cb.value = r.id; cb.id = id; cb.checked = !!r.suggested;
|
|
69
|
+
var text = document.createElement('span');
|
|
70
|
+
text.className = 'council-row-text';
|
|
71
|
+
var nameEl = document.createElement('span');
|
|
72
|
+
nameEl.className = 'council-row-name'; nameEl.textContent = r.name || r.id;
|
|
73
|
+
var idEl = document.createElement('span');
|
|
74
|
+
idEl.className = 'council-row-id'; idEl.textContent = r.id;
|
|
75
|
+
text.appendChild(nameEl); text.appendChild(idEl);
|
|
76
|
+
row.appendChild(cb); row.appendChild(text);
|
|
77
|
+
return row;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function buildGroup(group, startIdx) {
|
|
81
|
+
var details = document.createElement('details');
|
|
82
|
+
details.className = 'council-group';
|
|
83
|
+
details.open = true;
|
|
84
|
+
var summary = document.createElement('summary');
|
|
85
|
+
var summaryLabel = document.createElement('span');
|
|
86
|
+
summaryLabel.textContent = vendorLabel(group.vendor);
|
|
87
|
+
var count = document.createElement('span');
|
|
88
|
+
count.className = 'council-group-count';
|
|
89
|
+
count.textContent = '(' + group.rows.length + ')';
|
|
90
|
+
summary.appendChild(summaryLabel); summary.appendChild(count);
|
|
91
|
+
details.appendChild(summary);
|
|
92
|
+
group.rows.forEach(function(r, i) { details.appendChild(buildRow(r, startIdx + i)); });
|
|
93
|
+
return details;
|
|
94
|
+
}
|
|
95
|
+
|
|
41
96
|
async function loadFree() {
|
|
42
97
|
if (loaded) { return; }
|
|
43
98
|
try {
|
|
44
99
|
var rows = await window.sidecarSetup.invoke('sidecar:fetch-free-models');
|
|
45
100
|
loaded = true;
|
|
46
101
|
results.innerHTML = '';
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
cb.type = 'checkbox'; cb.value = r.id; cb.id = id; cb.checked = !!r.suggested;
|
|
53
|
-
var span = document.createElement('span'); span.textContent = r.id;
|
|
54
|
-
row.appendChild(cb); row.appendChild(span);
|
|
55
|
-
results.appendChild(row);
|
|
102
|
+
var groups = groupByVendor(rows);
|
|
103
|
+
var idx = 0;
|
|
104
|
+
groups.forEach(function(group) {
|
|
105
|
+
results.appendChild(buildGroup(group, idx));
|
|
106
|
+
idx += group.rows.length;
|
|
56
107
|
});
|
|
57
|
-
if (meta) {
|
|
108
|
+
if (meta) {
|
|
109
|
+
meta.textContent = (rows || []).length + ' free models across ' + groups.length +
|
|
110
|
+
' provider' + (groups.length === 1 ? '' : 's');
|
|
111
|
+
}
|
|
58
112
|
} catch (_e) { if (meta) { meta.textContent = 'Could not load free models.'; } }
|
|
59
113
|
}
|
|
60
114
|
|
|
@@ -357,9 +357,40 @@ function __rawWizardCSS() {
|
|
|
357
357
|
/* Free council picker (Step 2) */
|
|
358
358
|
.council-section { margin-top: 14px; }
|
|
359
359
|
.council-toggle { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--text-muted); cursor: pointer; }
|
|
360
|
-
.council-results {
|
|
361
|
-
|
|
362
|
-
|
|
360
|
+
.council-results {
|
|
361
|
+
max-height: 220px; overflow-y: auto; margin-top: 6px;
|
|
362
|
+
border: 1px solid var(--border); border-radius: var(--r-6);
|
|
363
|
+
}
|
|
364
|
+
.council-results:empty { border: none; }
|
|
365
|
+
.council-group { margin: 0; border-bottom: 1px solid var(--border); }
|
|
366
|
+
.council-group:last-child { border-bottom: none; }
|
|
367
|
+
.council-group summary {
|
|
368
|
+
display: flex; align-items: center; gap: 6px; padding: 6px 10px;
|
|
369
|
+
cursor: pointer; font-size: 12px; font-weight: 500; color: var(--text-muted);
|
|
370
|
+
list-style: none; transition: color var(--dur-fast);
|
|
371
|
+
}
|
|
372
|
+
.council-group summary::-webkit-details-marker { display: none; }
|
|
373
|
+
.council-group summary::before {
|
|
374
|
+
content: '\\25B6'; font-size: 8px; color: var(--text-faint); transition: transform var(--dur-fast);
|
|
375
|
+
}
|
|
376
|
+
.council-group[open] summary::before { transform: rotate(90deg); }
|
|
377
|
+
.council-group summary:hover { color: var(--accent); }
|
|
378
|
+
.council-group-count { color: var(--text-faint); font-weight: 400; }
|
|
379
|
+
.council-row {
|
|
380
|
+
display: flex; align-items: center; gap: 8px;
|
|
381
|
+
padding: 5px 10px 5px 24px; font-size: 12px; color: var(--text); cursor: pointer;
|
|
382
|
+
}
|
|
383
|
+
.council-row:hover { background: var(--surface-hover); }
|
|
384
|
+
.council-row input[type="checkbox"] { accent-color: var(--accent); flex-shrink: 0; }
|
|
385
|
+
.council-row-text { display: flex; flex-direction: column; min-width: 0; }
|
|
386
|
+
.council-row-name {
|
|
387
|
+
color: var(--text); font-size: 12px; overflow: hidden;
|
|
388
|
+
text-overflow: ellipsis; white-space: nowrap;
|
|
389
|
+
}
|
|
390
|
+
.council-row-id {
|
|
391
|
+
color: var(--text-muted); font-size: 11px; font-family: var(--font-mono);
|
|
392
|
+
margin-top: 1px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
393
|
+
}
|
|
363
394
|
.council-note { font-size: 10px; color: var(--text-faint); margin-top: 6px; line-height: 1.4; }
|
|
364
395
|
|
|
365
396
|
/* Alias example-icon strokes — driven by class rules (var() is invalid as an SVG attribute) */
|
package/electron/setup-ui.js
CHANGED
|
@@ -9,6 +9,7 @@ const { buildCouncilSectionHTML, buildCouncilScript } = require('./setup-ui-coun
|
|
|
9
9
|
const { getDefaultAliases } = require('../src/utils/config');
|
|
10
10
|
const { getBrandName } = require('./toolbar');
|
|
11
11
|
const { resolveQuickPicks } = require('../src/utils/quick-picks');
|
|
12
|
+
const { PROVIDER_FAMILY_NAMES } = require('../src/utils/model-fetcher');
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* @param {object} [options={}]
|
|
@@ -30,6 +31,7 @@ function buildSetupHTML(options = {}) {
|
|
|
30
31
|
const modelChoicesJson = JSON.stringify(quickPicks);
|
|
31
32
|
const providerNamesJson = JSON.stringify(PROVIDER_NAMES);
|
|
32
33
|
const defaultAliasesJson = JSON.stringify(getDefaultAliases());
|
|
34
|
+
const familyNamesJson = JSON.stringify(PROVIDER_FAMILY_NAMES);
|
|
33
35
|
return `<!DOCTYPE html>
|
|
34
36
|
<html><head><meta charset="utf-8"><title>Amicus Setup</title>
|
|
35
37
|
<style>${css}</style></head><body>
|
|
@@ -51,11 +53,11 @@ function buildSetupHTML(options = {}) {
|
|
|
51
53
|
</div>
|
|
52
54
|
</div>
|
|
53
55
|
<div class="footer"><div class="footer-brand"><svg width="15" height="15" viewBox="0 0 32 32" fill="none"><path d="M4 8H19"/><path d="M4 11H14L19 8"/><path d="M4 14H13L19 8"/><path d="M4 17H12L19 8"/><path d="M4 20H11L19 8"/><path d="M4 23H10L19 8"/><path class="brand-main" d="M19 8H28"/></svg> ${brandName}</div><div class="footer-nav"><button class="nav-btn" id="back-btn" style="display:none">Back</button><button class="nav-btn primary" id="next-btn" disabled>Next</button><button class="nav-btn primary" id="finish-btn" style="display:none">Finish</button></div></div>
|
|
54
|
-
${buildWizardScript(providersJson, modelChoicesJson, providerNamesJson, defaultAliasesJson)}
|
|
56
|
+
${buildWizardScript(providersJson, modelChoicesJson, providerNamesJson, defaultAliasesJson, familyNamesJson)}
|
|
55
57
|
</body></html>`;
|
|
56
58
|
}
|
|
57
59
|
|
|
58
|
-
function buildWizardScript(providersJson, modelChoicesJson, providerNamesJson, defaultAliasesJson) {
|
|
60
|
+
function buildWizardScript(providersJson, modelChoicesJson, providerNamesJson, defaultAliasesJson, familyNamesJson) {
|
|
59
61
|
const keysJs = buildKeysScript();
|
|
60
62
|
const aliasJs = buildAliasScript();
|
|
61
63
|
const councilJs = buildCouncilScript();
|
|
@@ -69,6 +71,7 @@ function buildWizardScript(providersJson, modelChoicesJson, providerNamesJson, d
|
|
|
69
71
|
var modelChoicesData = ${modelChoicesJson};
|
|
70
72
|
var providerNamesData = ${providerNamesJson};
|
|
71
73
|
var defaultAliases = ${defaultAliasesJson};
|
|
74
|
+
var PROVIDER_FAMILY_NAMES = ${familyNamesJson};
|
|
72
75
|
var routingChoices = {};
|
|
73
76
|
var aliasEdits = {};
|
|
74
77
|
var aliasDisplay = {};
|
|
@@ -178,7 +181,10 @@ function buildWizardScript(providersJson, modelChoicesJson, providerNamesJson, d
|
|
|
178
181
|
if (step === 2) { updateRoutingPills(); ensureCatalogLoaded(); window.refreshCouncilGating && window.refreshCouncilGating(); }
|
|
179
182
|
if (step === 3) {
|
|
180
183
|
updateAliasRoutes();
|
|
181
|
-
|
|
184
|
+
// B33 / #12: Step 3 shares Step 2's TTL-cached catalog load (single
|
|
185
|
+
// in-page cache: ensureCatalogLoaded no-ops if Step 2 already loaded
|
|
186
|
+
// it) instead of a separate live sidecar:fetch-models round-trip.
|
|
187
|
+
ensureCatalogLoaded();
|
|
182
188
|
}
|
|
183
189
|
updateNextState();
|
|
184
190
|
}
|
|
@@ -393,15 +399,11 @@ function buildWizardScript(providersJson, modelChoicesJson, providerNamesJson, d
|
|
|
393
399
|
} catch (_e) { finishBtn.disabled = false; finishBtn.textContent = 'Finish'; }
|
|
394
400
|
});
|
|
395
401
|
|
|
396
|
-
|
|
397
|
-
try {
|
|
398
|
-
var groups = await window.sidecarSetup.invoke('sidecar:fetch-models');
|
|
399
|
-
if (groups && groups.length > 0) { window.availableModels = groups; }
|
|
400
|
-
} catch (_e) {}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// ===== F5: searchable catalog picker (Step 2) =====
|
|
402
|
+
// ===== F5: searchable catalog picker (Step 2) + B33/#12: shared with Step 3 =====
|
|
404
403
|
var catalogRows = null, catalogFetchedAt = null;
|
|
404
|
+
// #13: last-refresh outcome, so a stale cache (refresh keeps failing) is
|
|
405
|
+
// shown honestly instead of looking current.
|
|
406
|
+
var catalogLastRefreshAttempt = null, catalogLastRefreshError = null;
|
|
405
407
|
window.customDefaultModel = null;
|
|
406
408
|
|
|
407
409
|
async function ensureCatalogLoaded() {
|
|
@@ -412,9 +414,32 @@ function buildWizardScript(providersJson, modelChoicesJson, providerNamesJson, d
|
|
|
412
414
|
} catch (_e) {}
|
|
413
415
|
}
|
|
414
416
|
|
|
417
|
+
// Re-derive Step 3's grouped {family, models} shape from the flat catalog
|
|
418
|
+
// rows client-side (mirrors src/utils/model-fetcher.js groupModelsByFamily
|
|
419
|
+
// keying: family name from the id prefix, falling back to the prefix
|
|
420
|
+
// itself for any provider not in PROVIDER_FAMILY_NAMES).
|
|
421
|
+
function groupCatalogByFamily(rows) {
|
|
422
|
+
if (!rows || rows.length === 0) { return []; }
|
|
423
|
+
var order = [], byFamily = {};
|
|
424
|
+
rows.forEach(function(m) {
|
|
425
|
+
var prefix = m.id.split('/')[0];
|
|
426
|
+
var family = PROVIDER_FAMILY_NAMES[prefix] || prefix;
|
|
427
|
+
if (!byFamily[family]) { byFamily[family] = []; order.push(family); }
|
|
428
|
+
byFamily[family].push(m);
|
|
429
|
+
});
|
|
430
|
+
return order.map(function(family) { return { family: family, models: byFamily[family] }; });
|
|
431
|
+
}
|
|
432
|
+
|
|
415
433
|
function applyCatalog(info) {
|
|
416
434
|
catalogRows = (info && info.models) || [];
|
|
417
435
|
catalogFetchedAt = info && info.fetchedAt;
|
|
436
|
+
catalogLastRefreshAttempt = info && info.lastRefreshAttempt;
|
|
437
|
+
catalogLastRefreshError = info && info.lastRefreshError;
|
|
438
|
+
// Single shared in-page cache: Step 3's alias dropdown (buildModelSelect)
|
|
439
|
+
// reads window.availableModels, re-derived from the same catalog load
|
|
440
|
+
// Step 2 uses — no second get-catalog round-trip, and the refresh
|
|
441
|
+
// button (Step 2) re-applying here keeps Step 3's dropdown data current.
|
|
442
|
+
window.availableModels = groupCatalogByFamily(catalogRows);
|
|
418
443
|
renderSearchMeta();
|
|
419
444
|
renderSearchResults();
|
|
420
445
|
if (catalogRows.length === 0) {
|
|
@@ -427,7 +452,14 @@ function buildWizardScript(providersJson, modelChoicesJson, providerNamesJson, d
|
|
|
427
452
|
var meta = $('model-search-meta');
|
|
428
453
|
if (!meta) { return; }
|
|
429
454
|
var when = catalogFetchedAt ? new Date(catalogFetchedAt).toLocaleString() : 'never';
|
|
430
|
-
|
|
455
|
+
var text = catalogRows.length + ' models \\u00b7 catalog fetched ' + when;
|
|
456
|
+
// #13: one-line stale hint when the last refresh attempt failed AFTER
|
|
457
|
+
// the data currently shown was fetched (don't redesign Step 2 for this).
|
|
458
|
+
if (catalogLastRefreshError && catalogLastRefreshAttempt &&
|
|
459
|
+
(!catalogFetchedAt || catalogLastRefreshAttempt > catalogFetchedAt)) {
|
|
460
|
+
text += ' \\u2014 \\u26a0 refresh failed, showing last-known data';
|
|
461
|
+
}
|
|
462
|
+
meta.textContent = text;
|
|
431
463
|
}
|
|
432
464
|
|
|
433
465
|
function fmtCtx(n) { return n == null ? '' : ' \\u00b7 ctx ' + n; }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "amicus",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"mcpName": "io.github.BourbonDog/amicus",
|
|
5
5
|
"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.",
|
|
6
6
|
"keywords": [
|
|
@@ -28,9 +28,7 @@
|
|
|
28
28
|
"homepage": "https://bourbondog.github.io/amicus/",
|
|
29
29
|
"bin": {
|
|
30
30
|
"amicus": "./bin/amicus.js",
|
|
31
|
-
"am": "./bin/amicus.js"
|
|
32
|
-
"sidecar": "./bin/amicus.js",
|
|
33
|
-
"claude-sidecar": "./bin/amicus.js"
|
|
31
|
+
"am": "./bin/amicus.js"
|
|
34
32
|
},
|
|
35
33
|
"main": "src/index.js",
|
|
36
34
|
"exports": {
|
|
@@ -85,7 +83,6 @@
|
|
|
85
83
|
"devDependencies": {
|
|
86
84
|
"chrome-remote-interface": "^0.33.3",
|
|
87
85
|
"eslint": "^8.0.0",
|
|
88
|
-
"husky": "^9.1.7",
|
|
89
86
|
"jest": "^29.0.0",
|
|
90
87
|
"lint-staged": "^16.3.2",
|
|
91
88
|
"puppeteer": "^24.36.0",
|
|
@@ -25,8 +25,8 @@ injection, long-read failures, judge tool-wander; see changelog)._
|
|
|
25
25
|
- **Transient provider errors** (502s, connection drops): re-run the affected leg (solo
|
|
26
26
|
`amicus start --json`, same briefing file) or the wave — see per-model notes for
|
|
27
27
|
model-specific signals. Never present a half-finished run as an answer.
|
|
28
|
-
- **Credentials:** keys live in `~/.config/amicus/.env
|
|
29
|
-
|
|
28
|
+
- **Credentials:** keys live in `~/.config/amicus/.env`. The legacy `~/.config/sidecar/.env`
|
|
29
|
+
fallback was removed in v2.0.0 (see `docs/SHIMS.md`). Configure with `amicus setup`.
|
|
30
30
|
- **PowerShell `--models` quoting (Windows):** always quote comma-separated model lists —
|
|
31
31
|
`--models "gemini,gpt,deepseek"`. Unquoted, PowerShell splits on commas and amicus receives one
|
|
32
32
|
mangled alias → instant arg-parse failure. (Now baked into every SKILL.md example.)
|
|
@@ -165,16 +165,16 @@ equivalent.
|
|
|
165
165
|
|
|
166
166
|
Instruct models to emit the structured JSON verbatim after the prose, without preamble, so it parses cleanly.
|
|
167
167
|
|
|
168
|
-
|
|
168
|
+
Save each leg's full output (prose + findings block) to the run folder as `review-<model>.md`
|
|
169
|
+
(one file per reviewer) before moving on.
|
|
170
|
+
|
|
171
|
+
**After the wave returns, validate each leg's findings block** by running `amicus council validate <leg-file> --json` (a thin CLI wrapper over `validateFindings`, Unit A — `src/council/findings.js`). It reads the leg's saved `review-<model>.md` and prints `{ok, findings, errors}`. Exit codes are a **tri-state** contract: `0` when `ok:true` (well-formed, proceed), `2` when `ok:false` (validation failed — a distinct, scriptable outcome, not a crash), `1` (`BAD_ARGS`) for a missing/unreadable file. If a leg's JSON fails validation (`ok:false` / exit 2):
|
|
169
172
|
1. Issue a **solo `start --json`** re-prompt to that one model: "re-emit only the findings JSON, fixing: \<errors\>." Keep the first-pass prose. (Solo `start` passes through the **same budget gate** as `fanout`. If launching the wave required `--max-cost <$>` or `--no-cost-gate`, pass the **same flag on every repair re-prompt and on the chair call** — otherwise the gate can refuse a repair or the chair mid-council.)
|
|
170
173
|
2. If still malformed, retry **once more** (cap = **2** re-prompts total).
|
|
171
174
|
3. If still malformed after 2 retries, mark the review `unstructured` and hand-parse its prose into the schema. The review proceeds — never dropped for a formatting miss.
|
|
172
175
|
|
|
173
176
|
Record per-model **conformance** (`clean` | `repaired` | `unstructured`) for inclusion in the tally input's `runStats` and the Stage-6 MODEL-NOTES note.
|
|
174
177
|
|
|
175
|
-
Save each leg's full output (prose + findings block) to the run folder as `review-<model>.md`
|
|
176
|
-
(one file per reviewer) before moving on.
|
|
177
|
-
|
|
178
178
|
**"Claude in the council" (when toggled on):** Claude also produces a **fresh** Stage-1 review on the artifact in the identical findings format — a new structured pass on the artifact, not a formalization of anything said upstream. This review is added to the bundle as one more anonymous entry. Claude does not rank or adjudicate in Stage 2 (it holds the label map), and does not chair in Stage 3. Save it as `review-claude.md`.
|
|
179
179
|
|
|
180
180
|
**Wave-degrade rules (Stage 1).** Read failures from the wave document — never silently ignore
|
|
@@ -210,7 +210,7 @@ amicus fanout --models "<m1,m2,m3>" --prompt-file <run-folder>/_tmp-bundle-stage
|
|
|
210
210
|
(Background, same JSON handling as Stage 1.) Each judge's leg `summary` is its ranking +
|
|
211
211
|
adjudication response. **Stage-2 degrade:** a judge leg dies → tally over the surviving judges
|
|
212
212
|
(≥ 1) and disclose the reduced bench in `crossreview-matrix.md`; tier definitions are unchanged
|
|
213
|
-
(they already count "judges engaged").
|
|
213
|
+
(they already count "judges engaged").
|
|
214
214
|
|
|
215
215
|
**Judge-briefing hardening (required).** Open `_tmp-bundle-stage2.md` with this preamble, verbatim, as its first line:
|
|
216
216
|
|
|
@@ -218,6 +218,8 @@ adjudication response. **Stage-2 degrade:** a judge leg dies → tally over the
|
|
|
218
218
|
|
|
219
219
|
Plan-agent judges have wandered to tools mid-adjudication (reading files instead of judging and returning only narration), and a tool-capable judge can read the de-anonymized `review-<model>.md` files in the run folder — an anonymization leak. The preamble closes both. **Scratch-cwd (optional second layer):** launch the Stage-2 wave (and the Stage-3 chair call) with `--cwd <run-folder>/_scratch/` — create the empty directory first — so even a wandering agent finds nothing to read. Caveat: those legs' session records then live under `_scratch/.claude/amicus_sessions/`, so any later `amicus read <taskId>` for them needs the same `--cwd`.
|
|
220
220
|
|
|
221
|
+
Each judge is asked to do two things on the bundle:
|
|
222
|
+
|
|
221
223
|
**Task A — Rank.** Order the reviews from most to least accurate and insightful. End the response with a parseable block in exactly this format (no other text on those lines):
|
|
222
224
|
|
|
223
225
|
```
|
|
@@ -242,12 +244,14 @@ As each judge's ranking + adjudication response returns, collect it (the raw per
|
|
|
242
244
|
|
|
243
245
|
**Five-keys checklist — verify `tally-input.json` has ALL of:** `meta` (with `meta.models`), `findings`, `adjudications`, `rankings`, `runStats` (`runStats` may be `[]`; the other four are required). Do not call `tally` until all five are present.
|
|
244
246
|
|
|
245
|
-
Then call:
|
|
247
|
+
Then call, saving the printed `record` to `<run-folder>/tally.json` (Stage 5's `amicus council verdict` reads it back from disk):
|
|
246
248
|
|
|
247
249
|
```
|
|
248
|
-
amicus council tally <run-folder>/tally-input.json --json
|
|
250
|
+
amicus council tally <run-folder>/tally-input.json --json > <run-folder>/tally.json
|
|
249
251
|
```
|
|
250
252
|
|
|
253
|
+
**Windows PowerShell 5.1 caveat:** that `>` redirect writes UTF-16 under legacy Windows PowerShell 5.1 (fine on pwsh 7+ or bash), which corrupts `tally.json` for Stage 5's `amicus council verdict` and surfaces as a confusing `BAD_ARGS` there instead of here — on 5.1 pipe through `| Out-File -Encoding utf8` (or run under pwsh 7+) instead of a bare `>`.
|
|
254
|
+
|
|
251
255
|
The output `record` carries the deterministic tiers (Disputed / Confirmed / Contested / Singleton), `confidence` (`solid` | `thin`), both street-cred numbers (`withSelf` and `peersOnly`), the validated `runStats`, and `tierCounts`. **Claude may override a `thin`-confidence tier at the margins** before Stage 4 — record the override in `tierOverride: {from, to, reason}`; the matrix and `verdict.json` surface it. De-anonymize and write the tally results to `crossreview-matrix.md` — the adjudication grid plus the street-cred table. This data feeds Stage 3 (chair briefing) and is never re-anonymized or forwarded to any council model.
|
|
252
256
|
|
|
253
257
|
---
|
|
@@ -334,7 +338,7 @@ Do not advance to Stage 5 until every finding in both tiers has a recorded decis
|
|
|
334
338
|
- `review-<model>.md` × N (already saved in Stage 1)
|
|
335
339
|
- `crossreview-matrix.md` — the de-anonymized adjudication grid and street-cred table
|
|
336
340
|
- `verdict.md` (already saved in Stage 3)
|
|
337
|
-
- `verdict.json` — write
|
|
341
|
+
- `verdict.json` — write by running `amicus council verdict <run-folder>/tally.json --decisions <run-folder>/decisions.json -o <run-folder>/verdict.json` (a thin CLI wrapper over `buildVerdict(record, decisions)` + `writeVerdictAtomic`, `src/council/verdict.js`). `<run-folder>/tally.json` is the `record` saved from the Stage-2 `amicus council tally` call. `<run-folder>/decisions.json` is a **JSON array**, one object per finding: `{id, decision, applied?, duplicateOf?, tierOverride?}` — `id` is the run-global label id (e.g. `A1`); `decision` is the Stage-4 outcome (accepted / denied / modified / deferred); `applied` (optional bool) marks whether the accepted change was actually applied to the artifact in Stage 5; `duplicateOf` (optional) links to another finding's id when Claude identified a duplicate; `tierOverride` (optional) carries any `{from, to, reason}` override recorded in Stage 2. Save this array to `<run-folder>/decisions.json` first, then run the command — it parses the tally record and the decisions file, calls `buildVerdict`, and writes the schema-stamped machine-readable record to the run folder via the same atomic tmp+rename convention the function always used.
|
|
338
342
|
- `report.md` — the chair's synthesis + the full Stage-4 decision log + a summary of what was
|
|
339
343
|
applied (+ the "How Claude's review fared" readout when "Claude in the council" is on) + a
|
|
340
344
|
**run-stats table**: one row per model call — **stage** (which stage you launched the call for)
|
|
@@ -343,15 +347,17 @@ Do not advance to Stage 5 until every finding in both tiers has a recorded decis
|
|
|
343
347
|
— exact for `reported`, `~` for `estimated`, `?` for `unknown` — and never
|
|
344
348
|
invent a figure. Add a wave **total cost** row from the wave document's
|
|
345
349
|
`usage.cost` (`source: reported|estimated|mixed|unknown`). Any leg with no run doc → `durationMs: null`, `usage: null`; never invent a value.
|
|
346
|
-
- **Renderer:** once `verdict.json` is written,
|
|
347
|
-
`amicus council report <run-folder>/verdict.json --
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
shareable page. This emits the
|
|
350
|
+
- **Renderer:** once `verdict.json` is written, run
|
|
351
|
+
`amicus council report <run-folder>/verdict.json --html > <run-folder>/report.html` — a
|
|
352
|
+
**separate, deterministic** artifact, not report.md itself. **`report.html` is the default
|
|
353
|
+
final artifact to hand the user** — a self-contained, shareable page. This emits the
|
|
351
354
|
adjudication matrix (finding × judge), the peers-only street-cred table, the
|
|
352
355
|
findings-by-tier groupings (Disputed-first), and the per-model + wave cost —
|
|
353
|
-
deterministic data only.
|
|
354
|
-
|
|
356
|
+
deterministic data only. To assemble report.md, also run
|
|
357
|
+
`amicus council report <run-folder>/verdict.json --md` (no redirect — read its stdout) and
|
|
358
|
+
paste that Markdown into report.md as one section; reserve the rest of report.md's prose for
|
|
359
|
+
the chair's synthesis and the decision log. Prefer the renderer's Markdown over
|
|
360
|
+
hand-assembling the matrix by hand.
|
|
355
361
|
|
|
356
362
|
Tell the user exactly which files were written and where, leading with `report.html`, **and present the verdict inline in chat** — the chair's overall assessment (verbatim or lightly trimmed) plus the tier counts (Confirmed/Disputed/Contested/Singleton) and what was applied. Never hand over only file paths.
|
|
357
363
|
|
|
@@ -465,14 +471,9 @@ Always **rank recommendations by fit**, state the trade-off for each option, and
|
|
|
465
471
|
- `review-<model>.md` ×N — raw Stage 1 reviews (plus `review-claude.md` when "Claude in the council" is on)
|
|
466
472
|
- `crossreview-matrix.md` — adjudication grid + de-anonymized street-cred table
|
|
467
473
|
- `verdict.md` — the chair's synthesis (prose)
|
|
468
|
-
- `verdict.json` — schema-stamped machine-readable record: tally output + Stage-4 decisions, written via `
|
|
469
|
-
- `report.md` —
|
|
470
|
-
|
|
471
|
-
block. Cost is `usage.cost.amount` (USD); mark it with its `usage.cost.source`
|
|
472
|
-
— exact for `reported`, `~` for `estimated`, `?` for `unknown` — and never
|
|
473
|
-
invent a figure. Add a wave **total cost** row from the wave document's
|
|
474
|
-
`usage.cost` (`source: reported|estimated|mixed|unknown`). Any leg with no run doc → `durationMs: null`, `usage: null`.
|
|
475
|
-
- `report.html` — the same report rendered as a self-contained page (`amicus council report <verdict.json> --html`); the default artifact to share.
|
|
474
|
+
- `verdict.json` — schema-stamped machine-readable record: tally output + Stage-4 decisions, written via `amicus council verdict` at Stage 5
|
|
475
|
+
- `report.md` — Claude-authored; full contract defined once in *Stage 5 → Run-folder artifacts* above (chair's synthesis + Stage-4 decision log + run-stats table).
|
|
476
|
+
- `report.html` — a **separate, deterministic** artifact generated by the `amicus council report <verdict.json> --html` renderer directly from `verdict.json` (no chair prose, no decision-log narrative — see Stage 5's *Renderer* note); the default artifact to share.
|
|
476
477
|
- Reviewed copy: `<stem>-reviewed.<ext>`, next to the source.
|
|
477
478
|
- Temp working files (`_tmp-*.md`: extracts, stage briefings, red-team brief, bundle, chair packet, proposed
|
|
478
479
|
MODEL-NOTES diff) live in the run folder and are cleaned up at the end of the run — the proposed-diff file
|
package/skills/sidecar/SKILL.md
CHANGED
|
@@ -747,13 +747,13 @@ The summary includes:
|
|
|
747
747
|
|
|
748
748
|
### Handling Config Update Notices
|
|
749
749
|
|
|
750
|
-
When amicus emits `[AMICUS_CONFIG_UPDATE]` on stderr
|
|
750
|
+
When amicus emits `[AMICUS_CONFIG_UPDATE]` on stderr, the user's model aliases have changed (via `amicus setup`). The message contains a hash comment and a markdown alias table.
|
|
751
751
|
|
|
752
752
|
**Action required:**
|
|
753
753
|
1. Find the "Model Aliases" section in the project's CLAUDE.md
|
|
754
754
|
2. Replace that section's content with the block provided in the message
|
|
755
755
|
3. If no "Model Aliases" section exists, add one after the title line
|
|
756
|
-
4. Do **not** remove the `<!-- amicus-config-hash: ... -->` comment
|
|
756
|
+
4. Do **not** remove the `<!-- amicus-config-hash: ... -->` comment — it tracks config changes and prevents duplicate notifications
|
|
757
757
|
|
|
758
758
|
The update is purely informational (alias mappings) and safe to apply without user confirmation.
|
|
759
759
|
|
|
@@ -846,7 +846,7 @@ The mutex approach looks correct. Add tests."
|
|
|
846
846
|
|
|
847
847
|
### "Missing Authentication header" in Claude Code or CI
|
|
848
848
|
|
|
849
|
-
API keys in `~/.zshrc` are not available in non-interactive shells. Resolution order: `process.env` > `~/.config/amicus/.env` (legacy `~/.config/sidecar/.env`
|
|
849
|
+
API keys in `~/.zshrc` are not available in non-interactive shells. Resolution order: `process.env` > `~/.config/amicus/.env` (the legacy `~/.config/sidecar/.env` fallback was removed in v2.0.0 — see `docs/SHIMS.md`) > `~/.local/share/opencode/auth.json` (first wins). Fix:
|
|
850
850
|
1. Run `amicus setup` (stores keys in `~/.config/amicus/.env`)
|
|
851
851
|
2. Or move exports to `~/.zshenv`
|
|
852
852
|
3. Or add credentials to `~/.local/share/opencode/auth.json`
|