amicus 1.7.7 → 1.8.1

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.
@@ -1,39 +1,37 @@
1
1
  ---
2
2
  name: sidecar
3
3
  description: >
4
- Spawn conversations with other LLMs (Gemini, GPT, ChatGPT, Codex, o3, DeepSeek, Qwen,
5
- Grok, Mistral, etc.) and fold results back into your context. TRIGGER when: user asks to
6
- talk to, chat with, use, call, or spawn another LLM or model; user mentions Gemini, GPT,
7
- ChatGPT, Codex, o3, DeepSeek, Claude (as a sidecar target), Qwen, Grok, Mistral, or any
8
- non-current model by name; user asks to get a second opinion from another model; user
9
- wants parallel exploration with a different model; user says "sidecar", "fork", or "fold".
10
- CRITICAL RULES: (1) ALWAYS launch amicus CLI commands with Bash tool's
11
- run_in_background: true. Never run amicus start/resume/continue in the foreground.
12
- (2) The fold summary returns on stdout when the user clicks Fold in the GUI or the
13
- headless agent finishes. Use TaskOutput to read it when the background task completes.
14
- (3) For long or multi-line briefings, write them to a temp file and pass
15
- --prompt-file <path> (mutually exclusive with --prompt; avoids shell-quoting hazards
16
- and argument-size caps). (4) NEVER use o3 or o3-pro unless the user explicitly asks for it by
17
- name. These models are extremely expensive ($10-60+ per request). If the user asks for
18
- o3, warn them about the cost before proceeding. Default to gemini for most tasks.
19
- (5) When the user asks to query MULTIPLE LLMs simultaneously (e.g., "ask Gemini AND
20
- ChatGPT", "compare Gemini vs GPT"), ALWAYS use --no-ui (headless) for all of them
21
- unless the user explicitly requests interactive. Opening multiple Electron windows at
22
- once is disruptive. Launch them all in parallel with run_in_background: true.
23
- (6) When the SAME prompt should go to N models, use `amicus fanout --models a,b,c
24
- --prompt-file <path> --json` (one headless wave, one JSON result) instead of N
25
- separate start calls. Different prompts per model → separate parallel
26
- `amicus start --no-ui` calls.
27
- (7) For a SINGLE-model sidecar, DEFAULT to interactive — omit --no-ui so the
28
- Electron UI opens and the user can watch, converse, and click Fold. Use --no-ui
29
- for a single model only when the user asks for headless/autonomous, or for
30
- unattended bulk automation. Interactive launches still use run_in_background: true.
4
+ Spawn a conversation with another LLM (Gemini, GPT, ChatGPT, Codex, o3, DeepSeek,
5
+ Qwen, Grok, Mistral, or Claude as a target) and fold the results back into your
6
+ context. TRIGGER when: the user asks to talk to, chat with, call, use, or spawn
7
+ another LLM or model; names any non-current model; wants parallel exploration or
8
+ a quick take from a different model; or says "sidecar", "fork", or "fold". This
9
+ is NOT the skill for structured multi-model review of provided material requests
10
+ like "second opinion", "council review", or red-team/stress-test against criteria
11
+ belong to the second-opinion skill. Before running any amicus command, read the
12
+ Operating Rules section at the top of this document: background launches,
13
+ --prompt-file briefings, interactive vs headless defaults, fanout for same-prompt
14
+ multi-model runs, the o3/o3-pro cost warning, and the npx fallback when amicus is
15
+ not on PATH.
31
16
  ---
32
17
 
33
18
  # Amicus: Multi-Model Sidecar Tool
34
19
 
35
20
  Spawn parallel conversations with different LLMs (Gemini, GPT, ChatGPT, Codex, o3, etc.) and fold results back into your context.
36
21
 
22
+ ## Operating Rules
23
+
24
+ These rules are mandatory for every amicus invocation in this skill:
25
+
26
+ 1. **ALWAYS launch amicus CLI commands with the Bash tool's `run_in_background: true`.** Never run `amicus start/resume/continue` in the foreground.
27
+ 2. **The fold summary returns on stdout** when the user clicks Fold in the GUI or the headless agent finishes. Use TaskOutput to read it when the background task completes.
28
+ 3. **For long or multi-line briefings, write them to a temp file and pass `--prompt-file <path>`** (mutually exclusive with `--prompt`; avoids shell-quoting hazards and argument-size caps).
29
+ 4. **NEVER use o3 or o3-pro unless the user explicitly asks for it by name.** These models are extremely expensive (\$10-60+ per request). If the user asks for o3, warn them about the cost before proceeding. Default to gemini for most tasks. The CLI enforces this in code: a built-in budget gate refuses any model above a per-$/Mtok threshold (o3-pro class) before launch unless you pass `--no-cost-gate`; `--max-cost <$>` sets a soft estimated-total ceiling. When a run is refused with `BUDGET_EXCEEDED`, relay the gate's message — don't silently retry with the flag.
30
+ 5. **When the user asks to query MULTIPLE LLMs simultaneously** (e.g., "ask Gemini AND ChatGPT", "compare Gemini vs GPT"), ALWAYS use `--no-ui` (headless) for all of them unless the user explicitly requests interactive. Opening multiple Electron windows at once is disruptive. Launch them all in parallel with `run_in_background: true`.
31
+ 6. **When the SAME prompt should go to N models, use `amicus fanout --models "a,b,c" --prompt-file <path> --json`** (one headless wave, one JSON result) instead of N separate start calls. Different prompts per model → separate parallel `amicus start --no-ui` calls.
32
+ 7. **For a SINGLE-model sidecar, DEFAULT to interactive** — omit `--no-ui` so the Electron UI opens and the user can watch, converse, and click Fold. Use `--no-ui` for a single model only when the user asks for headless/autonomous, or for unattended bulk automation. Interactive launches still use `run_in_background: true`.
33
+ 8. **If `amicus` is not on PATH** (typical for plugin-only installs), run every command in this skill as `npx -y amicus@latest <args>` (e.g. `npx -y amicus@latest start --model gemini --prompt "..."`), or use the MCP tools (`amicus_start`, `amicus_status`, `amicus_read`, …) instead. Do not conclude the tool is broken because `amicus` is not found.
34
+
37
35
  ## Installation
38
36
 
39
37
  ```bash
@@ -204,11 +202,11 @@ amicus start \
204
202
  ```
205
203
 
206
204
  **Required:**
207
- - `--model`: The model to use (see Models below)
208
- - `--prompt`: Detailed task description you generate
205
+ - `--prompt` (or `--prompt-file`): Detailed task description you generate
209
206
 
210
207
  **Recommended:**
211
- - `--session`: Your Claude Code session ID for accurate context passing
208
+ - `--model`: The model to use (see Models below). Omit it to use your configured default (`amicus setup`); the CLI errors only when neither an explicit model nor a configured default exists.
209
+ - `--session-id`: Your Claude Code session ID for accurate context passing
212
210
 
213
211
  **Optional:**
214
212
  - `--no-ui`: Run autonomously without GUI (for bulk tasks)
@@ -257,7 +255,7 @@ The CLI validates all inputs **before** launching the sidecar. Invalid inputs fa
257
255
 
258
256
  | Input | Validation | Error Message |
259
257
  |-------|------------|---------------|
260
- | `--model` | Must be present, format: `provider/model` | `Error: --model is required` or `Error: --model must be in format provider/model` |
258
+ | `--model` | Optional falls back to the config default. An explicit value must resolve to a known alias or `provider/model` | `Error: Unknown model alias '<x>' …` or `No model specified and no default configured. Run 'amicus setup' to set a default model.` |
261
259
  | `--prompt` | Must be present and non-empty | `Error: --prompt is required` or `Error: --prompt cannot be empty or whitespace-only` |
262
260
  | `--cwd` | If provided, directory must exist | `Error: --cwd path does not exist: <path>` |
263
261
  | `--session-id` | If explicit ID provided (not 'current'), must exist | `Error: --session-id '<id>' not found. Use 'amicus list' to see available sessions or omit --session-id for most recent.` |
@@ -283,7 +281,7 @@ If you receive a validation error, fix the input and retry:
283
281
 
284
282
  ```bash
285
283
  # Error: --session-id 'abc123' not found
286
- # Fix: Use 'current' or omit --session
284
+ # Fix: Use 'current' or omit --session-id
287
285
  amicus start --model gemini --prompt "Task" --session-id current
288
286
 
289
287
  # Error: --agent cannot be empty
@@ -303,7 +301,7 @@ amicus start --model gemini --prompt "Task"
303
301
  ### Fan Out One Prompt to N Models
304
302
 
305
303
  ```bash
306
- amicus fanout --models gemini,gpt,deepseek --prompt-file ./briefing.md --json
304
+ amicus fanout --models "gemini,gpt,deepseek" --prompt-file ./briefing.md --json
307
305
  ```
308
306
 
309
307
  Runs the same prompt on every listed model in parallel (one shared engine server, headless),
@@ -404,7 +402,7 @@ amicus abort --all # stop every running session in this project
404
402
 
405
403
  ### Model Selection
406
404
 
407
- Use short aliases (run `amicus guide` to see all available aliases and their current model IDs):
405
+ Use short aliases (run `amicus models` to see the live catalog, and `amicus models --check` to audit your aliases):
408
406
  - `--model gemini` -- Google Gemini (fast, large context)
409
407
  - `--model opus` -- Claude Opus (deep analysis)
410
408
  - `--model gpt` -- OpenAI GPT
@@ -454,13 +452,13 @@ ls -lt ~/.claude/projects/-Users-john-myproject/*.jsonl | head -5
454
452
  The most recently modified file is likely your current session. Extract the UUID from the filename.
455
453
 
456
454
  **Session ID behavior:**
457
- - **Omit `--session`** or use `--session-id current`: Uses the most recently modified session file (less reliable if multiple sessions are active)
455
+ - **Omit `--session-id`** or pass `--session-id current`: Uses the most recently modified session file (less reliable if multiple sessions are active)
458
456
  - **Explicit session ID** (`--session-id abc123-def456`): Must exist or the command fails immediately with: `Error: --session-id 'abc123-def456' not found`
459
457
 
460
458
  **If you get a session not found error:**
461
459
  1. List available sessions: `amicus list`
462
460
  2. Use one of the listed session IDs, OR
463
- 3. Omit `--session` to use the most recent session
461
+ 3. Omit `--session-id` to use the most recent session
464
462
 
465
463
  ---
466
464
 
@@ -849,7 +847,7 @@ Find the correct encoded path for your project. Remember that `/`, `\`, and `_`
849
847
 
850
848
  ### "Multiple active sessions detected"
851
849
 
852
- You have multiple Claude Code windows. Pass `--session` explicitly:
850
+ You have multiple Claude Code windows. Pass `--session-id` explicitly:
853
851
  ```bash
854
852
  ls -lt ~/.claude/projects/[your-path]/*.jsonl | head -3
855
853
  # Pick the correct session UUID
@@ -912,7 +910,7 @@ amicus start --model gemini --prompt "Debug the auth issue in TokenManager.ts"
912
910
 
913
911
  The explicit session ID doesn't exist. Either:
914
912
  1. Use `amicus list` to find valid session IDs
915
- 2. Omit `--session` to use the most recent session
913
+ 2. Omit `--session-id` to use the most recent session
916
914
  3. Use `--session-id current` for automatic resolution
917
915
 
918
916
  ```bash
@@ -41,6 +41,8 @@ function realDeps() {
41
41
  fix: false,
42
42
  discoverClaudeCodeMcps: () => require('./utils/mcp-discovery').discoverClaudeCodeMcps(),
43
43
  discoverCoworkMcps: () => require('./utils/mcp-discovery').discoverCoworkMcps(),
44
+ inspectLegacyMcpEntries: () => require('./utils/legacy-mcp-migration').inspectAllLegacySidecarEntries(),
45
+ migrateLegacyMcpEntries: () => require('./utils/legacy-mcp-migration').migrateLegacySidecar(),
44
46
  skillInstalled: () => {
45
47
  const dir = path.join(os.homedir(), '.claude', 'skills');
46
48
  return fs.existsSync(path.join(dir, 'sidecar', 'SKILL.md'))
@@ -181,6 +183,47 @@ async function runDoctorChecks(depsOverride = {}) {
181
183
  return { id: 'mcp', name: 'MCP registration', status: 'ok', message: `registered: Claude Code${extra}`, hint: null };
182
184
  }));
183
185
 
186
+ // Duplicate legacy 'sidecar' MCP registration (same server twice — doubles
187
+ // the client-visible tool list). Detection reads the raw config files via
188
+ // legacy-mcp-migration: mcp-discovery can't see it (it strips 'sidecar' as
189
+ // its own recursion guard). --fix removes only identical-in-effect twins.
190
+ checks.push(guard('mcp-legacy', 'Legacy sidecar MCP entry', () => {
191
+ const id = 'mcp-legacy'; const name = 'Legacy sidecar MCP entry';
192
+ const entries = d.inspectLegacyMcpEntries() || [];
193
+ const dupes = entries.filter(e => e.status === 'removable');
194
+ const custom = entries.filter(e => e.status === 'customized');
195
+ // An unreadable config is neither "no problem" nor a duplicate we can act
196
+ // on — reporting it as ok/'none' would hide a config doctor (and --fix)
197
+ // could not actually inspect. Always surface it, even alongside dupes.
198
+ const unreadable = entries.filter(e => e.status === 'unreadable');
199
+ const unreadableNote = unreadable.length
200
+ ? `${unreadable.map(e => e.target).join(', ')} config unreadable — skipped`
201
+ : null;
202
+ if (dupes.length === 0) {
203
+ if (unreadableNote) {
204
+ const suffix = custom.length ? `; custom 'sidecar' entry in ${custom.map(e => e.target).join(', ')} — left alone` : '';
205
+ return { id, name, status: 'warn', message: `${unreadableNote}${suffix}`, hint: null };
206
+ }
207
+ const message = custom.length
208
+ ? `custom 'sidecar' entry in ${custom.map(e => e.target).join(', ')} — left alone`
209
+ : 'none';
210
+ return { id, name, status: 'ok', message, hint: null };
211
+ }
212
+ if (d.fix) {
213
+ const removed = (d.migrateLegacyMcpEntries() || []).filter(r => r.result === 'removed');
214
+ if (removed.length >= dupes.length) {
215
+ const message = `removed legacy entry from: ${removed.map(r => r.target).join(', ')}`;
216
+ return unreadableNote
217
+ ? { id, name, status: 'warn', message: `${message}; ${unreadableNote}`, hint: HINTS.removeLegacySidecar }
218
+ : { id, name, status: 'ok', message, hint: null };
219
+ }
220
+ const message = `removed ${removed.length}/${dupes.length} duplicate(s) — could not update every config`;
221
+ return { id, name, status: 'warn', message: unreadableNote ? `${message}; ${unreadableNote}` : message, hint: HINTS.removeLegacySidecar };
222
+ }
223
+ const message = `duplicate 'sidecar' entry in ${dupes.map(e => e.target).join(', ')} — doubles the MCP tool list`;
224
+ return { id, name, status: 'warn', message: unreadableNote ? `${message}; ${unreadableNote}` : message, hint: HINTS.removeLegacySidecar };
225
+ }));
226
+
184
227
  // #43: OpenRouter credit/free-tier — warns (never errors); skipped when no key.
185
228
  checks.push(await guardAsync('openrouter-credit', 'OpenRouter credit', async () => {
186
229
  const values = d.readApiKeyValues() || {};
@@ -0,0 +1,76 @@
1
+ /**
2
+ * `amicus status <task_id>` — one-shot human/JSON status for a session or wave.
3
+ * Reads the SAME sources as the MCP amicus_status handler by calling it
4
+ * directly (requiring mcp-server does NOT start the server; the MCP SDK is
5
+ * only loaded inside startMcpServer()). Zero duplicated status logic — this
6
+ * inherits crash detection, wave leg rollup, and P6-3 enrichment for free.
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const { validateTaskId } = require('./utils/validators');
12
+
13
+ /** Render a key-value block for a single-session status payload. */
14
+ function formatRunHuman(d) {
15
+ const lines = [
16
+ `Task: ${d.taskId}`,
17
+ `Status: ${d.status}${d.phase ? ` (${d.phase})` : ''}`,
18
+ `Elapsed: ${d.elapsed}`,
19
+ ];
20
+ if (d.model) { lines.push(`Model: ${d.model}`); }
21
+ if (d.mode) { lines.push(`Mode: ${d.mode}`); }
22
+ if (d.messageCount !== undefined) { lines.push(`Messages: ${d.messageCount}`); }
23
+ if (d.lastActivity) { lines.push(`Activity: ${d.lastActivity}`); }
24
+ if (d.latestPreview) { lines.push(`Latest: ${d.latestPreview}`); }
25
+ else if (d.latest) { lines.push(`Latest: ${d.latest}`); }
26
+ if (d.stalled) { lines.push(`STALLED: no activity for ${d.stalledForSeconds}s (see --json for recovery)`); }
27
+ if (d.reason) { lines.push(`Reason: ${d.reason}`); }
28
+ return lines.join('\n');
29
+ }
30
+
31
+ /** Render a wave payload: header + one line per leg. */
32
+ function formatWaveHumanStatus(d) {
33
+ const head = `Wave ${d.taskId}: ${d.status} — ${d.legsComplete}/${d.legsTotal} legs done (${d.elapsed})`;
34
+ const legLines = (d.legs || []).map((l) => {
35
+ const label = String(l.model || l.taskId || '').padEnd(28);
36
+ const st = String(l.status || 'unknown').padEnd(10);
37
+ const msgs = l.messages !== undefined ? `${l.messages} msg` : '';
38
+ const latest = l.latestPreview || l.latestActivity || '';
39
+ const flag = l.stalled ? ' ⏳stalled' : '';
40
+ return ` ${label} ${st} ${msgs} | ${latest}${flag}`;
41
+ });
42
+ return [head, ...legLines].join('\n');
43
+ }
44
+
45
+ /**
46
+ * Handle 'amicus status'. Exit code 0 = status retrieved (any run state, even
47
+ * a failed/crashed run — the QUERY succeeded); 1 = missing/invalid/unknown id.
48
+ * @param {object} args parsed CLI args
49
+ * @returns {Promise<number>}
50
+ */
51
+ async function handleStatus(args) {
52
+ const taskId = args.wave || args._[1];
53
+ if (!taskId || taskId === true) {
54
+ process.stderr.write('Error: task_id is required for status\n');
55
+ process.stderr.write('Usage: amicus status <task_id> [--json] (or: amicus status --wave <wave_id>)\n');
56
+ return 1;
57
+ }
58
+ const check = validateTaskId(String(taskId));
59
+ if (!check.valid) { process.stderr.write(`${check.error}\n`); return 1; }
60
+
61
+ const project = args.cwd || process.cwd();
62
+ const { handlers } = require('./mcp-server');
63
+ const result = await handlers.amicus_status({ taskId: String(taskId) }, project);
64
+ const text = result.content[0].text;
65
+ if (result.isError) { process.stderr.write(`${text}\n`); return 1; }
66
+
67
+ let data;
68
+ try { data = JSON.parse(text); } catch { process.stdout.write(`${text}\n`); return 0; }
69
+ delete data.next_poll; // MCP-agent polling guidance, not CLI output
70
+
71
+ if (args.json) { process.stdout.write(`${JSON.stringify(data, null, 2)}\n`); return 0; }
72
+ process.stdout.write(`${data.type === 'wave' ? formatWaveHumanStatus(data) : formatRunHuman(data)}\n`);
73
+ return 0;
74
+ }
75
+
76
+ module.exports = { handleStatus, formatRunHuman, formatWaveHumanStatus };
@@ -123,6 +123,16 @@ async function handleAbort(args) {
123
123
  console.error(`Session ${taskId} has malformed metadata`);
124
124
  process.exit(1);
125
125
  }
126
+ // Guard against a completed/terminal session: without this, metadata.pid
127
+ // still holds a value forever and `amicus abort <completed-task>` would
128
+ // wait the grace window then TerminateProcess whatever unrelated process
129
+ // now owns that (possibly recycled) pid. Mirrors MCP's amicus_abort guard
130
+ // (src/mcp-server.js) — same wording, no re-mark, no kill.
131
+ if (meta.status !== 'running') {
132
+ console.log(`Session ${taskId} is not running (status: ${meta.status}).`);
133
+ return;
134
+ }
135
+
126
136
  const { markAborted } = require('./utils/session-abort');
127
137
 
128
138
  // F4: aborting a wave aborts every still-running leg too.
@@ -147,6 +157,28 @@ async function handleAbort(args) {
147
157
 
148
158
  markAborted(sessionDir, 'manual abort');
149
159
  console.log(`Session ${taskId} marked as aborted.`);
160
+
161
+ // Phase 3: fallback direct-kill for a session that does not honor the
162
+ // marker. Headless loops poll the marker every ~2s and the interactive
163
+ // abort watch does too, so the normal outcome is a graceful exit during
164
+ // the grace window; only a wedged/legacy process gets SIGTERM. The wait is
165
+ // awaited on purpose — bin/amicus.js arms its force-exit watchdog only
166
+ // after this handler returns.
167
+ if (meta.pid) {
168
+ const { waitThenKill, abortGraceMs } = require('./utils/abort-coordinator');
169
+ const graceSec = Math.ceil(abortGraceMs() / 1000);
170
+ console.log(`Waiting up to ${graceSec}s for the session process (pid ${meta.pid}) to exit gracefully...`);
171
+ const { killed, exited } = await waitThenKill(meta.pid);
172
+ if (killed.length > 0) {
173
+ console.log(`Process ${meta.pid} did not exit in time — sent SIGTERM (a hard kill on Windows).`);
174
+ } else if (exited.length > 0) {
175
+ console.log('Process exited cleanly.');
176
+ } else {
177
+ // 3.1 contract: an EPERM-unkillable pid lands in NEITHER array —
178
+ // it is still alive and we could not signal it. Say so honestly.
179
+ console.log(`Process ${meta.pid} is still running — could not signal it (insufficient permission). It may require manual termination.`);
180
+ }
181
+ }
150
182
  }
151
183
 
152
184
  /**
package/src/cli.js CHANGED
@@ -330,6 +330,7 @@ Commands:
330
330
  start Launch a new amicus session
331
331
  fanout Run N models on the same prompt in parallel (headless)
332
332
  list Show previous sessions
333
+ status One-shot status for a session or wave (--json)
333
334
  resume Reopen a previous session
334
335
  continue New session building on previous
335
336
  read Output session summary/conversation
@@ -416,6 +417,13 @@ Options for 'list':
416
417
  --status <filter> Filter by status (running, complete)
417
418
  --all Show all projects
418
419
  --json Output as JSON
420
+ `,
421
+ status: `
422
+ Options for 'status':
423
+ <task_id> Required. Session or wave ID (positional)
424
+ --wave <wave_id> Alternative to the positional ID for waves
425
+ --json Machine-readable output
426
+ --cwd <path> Project directory (default: cwd)
419
427
  `,
420
428
  abort: `
421
429
  Options for 'abort':
package/src/headless.js CHANGED
@@ -463,6 +463,7 @@ async function runHeadless(model, systemPrompt, userMessage, taskId, project, ti
463
463
  const s = (statusData && statusData.type) ? statusData : (statusData && statusData[sessionId]);
464
464
  if (s && s.type === 'idle') {
465
465
  logger.debug('Session reported idle by SDK — completing', { sessionId });
466
+ completed = true;
466
467
  break;
467
468
  }
468
469
  } catch (statusErr) {
@@ -493,6 +494,7 @@ async function runHeadless(model, systemPrompt, userMessage, taskId, project, ti
493
494
  const threshold = assistantFinished ? stableFinishedPolls : stableIdlePolls;
494
495
  if (stablePolls >= threshold) {
495
496
  logger.debug('Session appears complete (idle)', { stablePolls, assistantFinished });
497
+ completed = true;
496
498
  break;
497
499
  }
498
500
  } else {