amicus 1.7.7 → 1.8.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 +38 -0
- package/README.md +12 -3
- package/bin/amicus.js +5 -0
- package/package.json +1 -1
- package/scripts/postinstall.js +72 -23
- package/src/cli-handlers-doctor.js +43 -0
- package/src/cli-handlers-status.js +76 -0
- package/src/cli-handlers.js +32 -0
- package/src/cli.js +8 -0
- package/src/mcp-server.js +124 -21
- package/src/mcp-tools.js +28 -1
- package/src/mcp-wait.js +163 -0
- package/src/sidecar/conversation-mirror.js +17 -4
- package/src/sidecar/interactive-abort.js +112 -0
- package/src/sidecar/interactive.js +32 -2
- package/src/sidecar/progress-fields.js +60 -0
- package/src/sidecar/progress.js +52 -48
- package/src/utils/abort-coordinator.js +91 -0
- package/src/utils/legacy-mcp-migration.js +119 -0
- package/src/utils/lifecycle.js +1 -1
- package/src/utils/remediation-hints.js +8 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "amicus",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.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
|
"author": {
|
|
6
6
|
"name": "Christian Wagner"
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,44 @@ All notable changes to Amicus are documented here. Format follows
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [1.8.0] - 2026-07-02
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **`amicus_wait` MCP tool: blocking wait for a session or fan-out wave.** Blocks inside one tool call until the
|
|
12
|
+
target reaches a terminal state or the wait window closes, replacing the sleep+`amicus_status` polling loop with
|
|
13
|
+
a single call. Returns the same JSON shape as `amicus_status` plus `waitedMs` and `{timedOut: true}` (with a
|
|
14
|
+
`hint`) on expiry — re-call it while it keeps returning `timedOut: true`. Works for sessions or waves started by
|
|
15
|
+
other processes, not just the caller. Torn-read tolerant: a transient read of `metadata.json` mid-write is
|
|
16
|
+
treated as a missed poll tick, not a hard failure. Legacy alias `sidecar_wait` is available under
|
|
17
|
+
`AMICUS_LEGACY_ALIASES=1`.
|
|
18
|
+
- **Agent-visible progress.** A new `amicus status <task_id>` (or `--wave <id>`) one-shot CLI command delegates
|
|
19
|
+
directly to the MCP status handler — same crash detection and wave-leg rollup, zero duplicated logic.
|
|
20
|
+
`amicus_status` and `amicus_list` are enriched with agent-facing `mode`, `phase`, `messageCount`,
|
|
21
|
+
`lastActivityAt`, and `latestPreview` (the pinned raw `stage` field is unchanged for back-compat; wave legs
|
|
22
|
+
additionally surface the raw `stage` alongside the coarse `phase`). Interactive (Electron GUI) runs now write
|
|
23
|
+
the same lifecycle progress stages headless runs always have (`initializing`, `server_ready`, `session_created`,
|
|
24
|
+
`prompt_sent`), and long-thinking turns emit periodic thinking-delta progress ticks instead of at most one ever
|
|
25
|
+
— so a live GUI run no longer reads "Starting up... | 0 messages" forever.
|
|
26
|
+
- **`amicus doctor` duplicate-registration check.** A new `mcp-legacy` check flags plugin-channel installs
|
|
27
|
+
(`AMICUS_SKIP_POSTINSTALL=1`) that never ran the postinstall migration and still carry a duplicate legacy
|
|
28
|
+
`sidecar` MCP registration; `doctor --fix` cleans it up.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
- **`amicus abort` now actually stops interactive sessions and wave legs.** Marker-first, honest output — reports
|
|
32
|
+
what really happened including the unkillable-pid case — and no-ops cleanly with a clear message when the
|
|
33
|
+
target isn't running.
|
|
34
|
+
- **Legacy-MCP remediation's `claude mcp add-json` (CLI) path no longer drops a user's custom `env`** on
|
|
35
|
+
re-registration — it now merges the previous registration's `env` the same way the file-fallback path already did.
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
- **Legacy `sidecar_*` MCP tool aliases are now opt-in** via `AMICUS_LEGACY_ALIASES=1` (breaking-adjacent —
|
|
39
|
+
carrying release must be a MINOR, v1.8.0). The default client-visible surface is the `amicus_*` toolset (14
|
|
40
|
+
tools as of this release); saved allowlists that still reference `mcp__amicus__sidecar_*` stop resolving unless
|
|
41
|
+
you opt back in.
|
|
42
|
+
- **Postinstall no longer registers a separate `sidecar` MCP server** and auto-removes a verified-identical
|
|
43
|
+
duplicate left over from pre-1.8 installs. A customized `sidecar` entry or a sole `sidecar` registration (no
|
|
44
|
+
`amicus` twin) is never touched.
|
|
45
|
+
|
|
8
46
|
## [1.7.7] - 2026-07-01
|
|
9
47
|
|
|
10
48
|
Correctness patch from the 2026-07-01 full product review (multi-agent review, every finding adversarially
|
package/README.md
CHANGED
|
@@ -259,6 +259,7 @@ amicus update
|
|
|
259
259
|
| `amicus resume` | Reopen a previous session with full history. |
|
|
260
260
|
| `amicus continue` | Start a new session building on a previous one. |
|
|
261
261
|
| `amicus read` | Output a session's summary / conversation / metadata. |
|
|
262
|
+
| `amicus status <id>` | One-shot status for a session or fan-out wave (human or `--json`; `--wave <id>` alternative spelling). |
|
|
262
263
|
| `amicus models` | List, search, refresh the catalog, or audit aliases. |
|
|
263
264
|
| `amicus abort` | Abort a running session (or `--all`). |
|
|
264
265
|
| `amicus setup` | Configure default model, API keys, and aliases. |
|
|
@@ -329,6 +330,10 @@ amicus read <id> --conversation # full conversation
|
|
|
329
330
|
amicus read <id> --metadata # session metadata
|
|
330
331
|
amicus read <id> --json # stable JSON (run or wave document)
|
|
331
332
|
|
|
333
|
+
amicus status <id> # one-shot status for a session or wave
|
|
334
|
+
amicus status --wave <id> # alternative spelling for a wave ID
|
|
335
|
+
amicus status <id> --json # machine-readable output
|
|
336
|
+
|
|
332
337
|
amicus resume <id> # reopen with full history
|
|
333
338
|
amicus continue <id> --prompt "..." # new session, previous one as read-only context
|
|
334
339
|
|
|
@@ -371,12 +376,13 @@ amicus models --check # audit your aliases against the catalog
|
|
|
371
376
|
|
|
372
377
|
## MCP integration
|
|
373
378
|
|
|
374
|
-
The MCP server is auto-registered on install (Claude Code and Claude Desktop / Cowork). It exposes
|
|
379
|
+
The MCP server is auto-registered on install (Claude Code and Claude Desktop / Cowork). It exposes fourteen tools:
|
|
375
380
|
|
|
376
381
|
| Tool | What it does |
|
|
377
382
|
|------|--------------|
|
|
378
383
|
| `amicus_start` | Spawn a session; returns a task ID immediately. |
|
|
379
384
|
| `amicus_status` | Poll a task (or a fanout wave) for completion. |
|
|
385
|
+
| `amicus_wait` | Block inside one tool call until a session/wave finishes or the wait window closes. |
|
|
380
386
|
| `amicus_read` | Read results: summary, conversation, metadata, or JSON. |
|
|
381
387
|
| `amicus_list` | List past sessions. |
|
|
382
388
|
| `amicus_resume` | Reopen a session. |
|
|
@@ -385,8 +391,11 @@ The MCP server is auto-registered on install (Claude Code and Claude Desktop / C
|
|
|
385
391
|
| `amicus_setup` | Open the setup wizard. |
|
|
386
392
|
| `amicus_guide` | Return usage guidance (model choice, briefings, polling). |
|
|
387
393
|
| `amicus_fanout` | Launch a same-prompt wave; returns `{ waveId, taskIds[] }`. |
|
|
394
|
+
| `amicus_council_tally` | Aggregate a council wave's reviews into a scored tally. |
|
|
395
|
+
| `amicus_council_stats` | Reviewer-reliability stats from past council runs. |
|
|
396
|
+
| `amicus_verdict` | Build the final council verdict from a tally + decisions. |
|
|
388
397
|
|
|
389
|
-
The async pattern is **start → status → read**: `amicus_start` (or `amicus_fanout`) returns immediately, you poll `amicus_status`, then `amicus_read` once it's done — so the calling agent never blocks.
|
|
398
|
+
The async pattern is **start → status → read**: `amicus_start` (or `amicus_fanout`) returns immediately, you poll `amicus_status`, then `amicus_read` once it's done — so the calling agent never blocks. Prefer `amicus_wait` over manual sleep+status polling for headless runs: it collapses the poll loop into a single blocking call that returns as soon as the run finishes (or the wait window closes).
|
|
390
399
|
|
|
391
400
|
To register manually (user scope):
|
|
392
401
|
|
|
@@ -394,7 +403,7 @@ To register manually (user scope):
|
|
|
394
403
|
claude mcp add-json amicus '{"command":"npx","args":["-y","amicus@latest","mcp"]}' --scope user
|
|
395
404
|
```
|
|
396
405
|
|
|
397
|
-
> Legacy `sidecar_*` tool names are
|
|
406
|
+
> Legacy `sidecar_*` tool names are no longer registered by default (v1.8.0). To restore them, add `"env": {"AMICUS_LEGACY_ALIASES": "1"}` to the server entry. They will be removed entirely in the next major.
|
|
398
407
|
|
|
399
408
|
---
|
|
400
409
|
|
package/bin/amicus.js
CHANGED
|
@@ -94,6 +94,11 @@ async function main() {
|
|
|
94
94
|
case 'list':
|
|
95
95
|
await handleList(args);
|
|
96
96
|
break;
|
|
97
|
+
case 'status': {
|
|
98
|
+
const { handleStatus } = require('../src/cli-handlers-status');
|
|
99
|
+
exitCode = await handleStatus(args);
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
97
102
|
case 'resume':
|
|
98
103
|
exitCode = await handleResume(args);
|
|
99
104
|
break;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "amicus",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.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",
|
package/scripts/postinstall.js
CHANGED
|
@@ -15,6 +15,7 @@ const { execFileSync } = require('child_process');
|
|
|
15
15
|
|
|
16
16
|
const { repairElectron } = require('../src/sidecar/electron-install');
|
|
17
17
|
const HINTS = require('../src/utils/remediation-hints');
|
|
18
|
+
const { isAmicusMcpConfig } = require('../src/utils/mcp-self-identity');
|
|
18
19
|
|
|
19
20
|
const SETUP_HOOKS_SCRIPT = path.join(__dirname, 'setup-hooks.js');
|
|
20
21
|
|
|
@@ -41,7 +42,18 @@ const MCP_CONFIG = { command: 'npx', args: ['-y', 'amicus@latest', 'mcp'] };
|
|
|
41
42
|
|
|
42
43
|
/**
|
|
43
44
|
* Add or update an MCP server in a JSON config file.
|
|
44
|
-
*
|
|
45
|
+
*
|
|
46
|
+
* Refreshes command/args on every install/upgrade. If the EXISTING entry at
|
|
47
|
+
* this key is already amicus-shaped (per isAmicusMcpConfig — Phase 1's single
|
|
48
|
+
* source of truth for "this is amicus"), we MERGE instead of overwrite: the
|
|
49
|
+
* user's `env` (and any other extra keys, e.g. a future `cwd`) survive the
|
|
50
|
+
* refresh. Without this, `npm i -g amicus` silently wiped
|
|
51
|
+
* "env": {"AMICUS_LEGACY_ALIASES":"1"} — the exact opt-in escape hatch Phase 4
|
|
52
|
+
* tells users to add — on every upgrade.
|
|
53
|
+
*
|
|
54
|
+
* A NON-amicus-shaped entry at this key is overwritten as before: 'amicus' is
|
|
55
|
+
* a reserved registration name, so a foreign entry there is reclaimed rather
|
|
56
|
+
* than merged with.
|
|
45
57
|
*
|
|
46
58
|
* @param {string} configPath - Path to the JSON config file
|
|
47
59
|
* @param {string} name - MCP server name
|
|
@@ -59,9 +71,10 @@ function addMcpToConfigFile(configPath, name, config) {
|
|
|
59
71
|
if (!existing.mcpServers) { existing.mcpServers = {}; }
|
|
60
72
|
|
|
61
73
|
const prev = existing.mcpServers[name];
|
|
62
|
-
const
|
|
74
|
+
const nextConfig = (prev && isAmicusMcpConfig(prev)) ? { ...prev, ...config } : config;
|
|
75
|
+
const status = !prev ? 'added' : JSON.stringify(prev) !== JSON.stringify(nextConfig) ? 'updated' : 'unchanged';
|
|
63
76
|
|
|
64
|
-
existing.mcpServers[name] =
|
|
77
|
+
existing.mcpServers[name] = nextConfig;
|
|
65
78
|
if (status !== 'unchanged') {
|
|
66
79
|
const dir = path.dirname(configPath);
|
|
67
80
|
if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true, mode: 0o700 }); }
|
|
@@ -111,28 +124,42 @@ function installCouncilSkill(sourceDir = COUNCIL_SOURCE_DIR) {
|
|
|
111
124
|
}
|
|
112
125
|
}
|
|
113
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Read the previous 'amicus' entry from ~/.claude.json, if any — used by the
|
|
129
|
+
* CLI add-json path to merge env the same way the file-fallback path does.
|
|
130
|
+
* Never throws: a missing/unreadable file just means "no previous entry".
|
|
131
|
+
* @returns {object|undefined}
|
|
132
|
+
*/
|
|
133
|
+
function readPrevClaudeCodeAmicusEntry() {
|
|
134
|
+
try {
|
|
135
|
+
const parsed = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.claude.json'), 'utf-8'));
|
|
136
|
+
return parsed && parsed.mcpServers ? parsed.mcpServers.amicus : undefined;
|
|
137
|
+
} catch {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
114
142
|
/** Register MCP server in Claude Code config */
|
|
115
143
|
function registerClaudeCode() {
|
|
116
144
|
// Try the CLI first
|
|
117
145
|
try {
|
|
118
|
-
|
|
146
|
+
// Merge the PREVIOUS registration's env into the add-json payload — same
|
|
147
|
+
// merge semantics as addMcpToConfigFile's file-fallback path (`{ ...prev,
|
|
148
|
+
// ...config }`: prev env keys survive, canonical MCP_CONFIG keys win on
|
|
149
|
+
// collision). Without this, a user's custom env (API key, AMICUS_* tuning
|
|
150
|
+
// knobs) on the old registration was silently dropped whenever the
|
|
151
|
+
// `claude` CLI was present, because the CLI path built its JSON payload
|
|
152
|
+
// from the bare MCP_CONFIG and delegated overwrite semantics to the
|
|
153
|
+
// claude binary — which has no idea about the user's previous entry.
|
|
154
|
+
const prev = readPrevClaudeCodeAmicusEntry();
|
|
155
|
+
const nextConfig = (prev && isAmicusMcpConfig(prev)) ? { ...prev, ...MCP_CONFIG } : MCP_CONFIG;
|
|
156
|
+
const mcpJson = JSON.stringify(nextConfig);
|
|
119
157
|
execFileSync('claude', ['mcp', 'add-json', 'amicus', mcpJson, '--scope', 'user'], {
|
|
120
158
|
stdio: 'pipe',
|
|
121
159
|
timeout: 10000,
|
|
122
160
|
});
|
|
123
161
|
console.log('[amicus] MCP registered in Claude Code (via CLI).');
|
|
124
162
|
|
|
125
|
-
// DEPRECATED(amicus-shim): also register 'sidecar' so existing clients that
|
|
126
|
-
// reference the old server name keep resolving. Remove in next major.
|
|
127
|
-
try {
|
|
128
|
-
execFileSync('claude', ['mcp', 'add-json', 'sidecar', mcpJson, '--scope', 'user'], {
|
|
129
|
-
stdio: 'pipe',
|
|
130
|
-
timeout: 10000,
|
|
131
|
-
});
|
|
132
|
-
} catch {
|
|
133
|
-
// Best-effort; ignore failures for the shim registration
|
|
134
|
-
}
|
|
135
|
-
|
|
136
163
|
return;
|
|
137
164
|
} catch {
|
|
138
165
|
// CLI not available or failed — fall back to file edit
|
|
@@ -148,10 +175,6 @@ function registerClaudeCode() {
|
|
|
148
175
|
} else {
|
|
149
176
|
console.log('[amicus] MCP already registered in Claude Code.');
|
|
150
177
|
}
|
|
151
|
-
|
|
152
|
-
// DEPRECATED(amicus-shim): also register 'sidecar' entry so existing clients
|
|
153
|
-
// that reference the old server name keep resolving. Remove in next major.
|
|
154
|
-
addMcpToConfigFile(claudeConfigPath, 'sidecar', MCP_CONFIG);
|
|
155
178
|
}
|
|
156
179
|
|
|
157
180
|
/** Register MCP server in Claude Desktop / Cowork config */
|
|
@@ -174,10 +197,33 @@ function registerClaudeDesktop() {
|
|
|
174
197
|
} else {
|
|
175
198
|
console.log('[amicus] MCP already registered in Claude Desktop.');
|
|
176
199
|
}
|
|
200
|
+
}
|
|
177
201
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
202
|
+
/**
|
|
203
|
+
* One-shot migration: drop the duplicate legacy 'sidecar' MCP entry that
|
|
204
|
+
* pre-1.8 postinstalls registered alongside 'amicus' (same server twice —
|
|
205
|
+
* doubled the client-visible tool list). Only removes an entry whose command
|
|
206
|
+
* is an amicus MCP invocation; a customized 'sidecar' entry is left alone.
|
|
207
|
+
* Covers both files the three legacy registration paths wrote to:
|
|
208
|
+
* ~/.claude.json (CLI + file fallback) and claude_desktop_config.json.
|
|
209
|
+
* Never throws (postinstall must always exit 0).
|
|
210
|
+
*/
|
|
211
|
+
function migrateLegacyMcp(deps = {}) {
|
|
212
|
+
try {
|
|
213
|
+
const impl = deps.migrateLegacySidecar
|
|
214
|
+
|| require('../src/utils/legacy-mcp-migration').migrateLegacySidecar;
|
|
215
|
+
for (const r of impl()) {
|
|
216
|
+
if (r.result === 'removed') {
|
|
217
|
+
console.log(`[amicus] Removed duplicate legacy 'sidecar' MCP entry from ${r.target} (same server — kept as 'amicus').`);
|
|
218
|
+
} else if (r.result === 'customized') {
|
|
219
|
+
console.log(`[amicus] Kept custom 'sidecar' MCP entry in ${r.target} (does not point at amicus).`);
|
|
220
|
+
} else if (r.result === 'write-failed') {
|
|
221
|
+
console.warn(`[amicus] Warning: could not remove the legacy 'sidecar' MCP entry from ${r.target} — run: amicus doctor --fix`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
} catch (err) {
|
|
225
|
+
console.warn(`[amicus] Warning: legacy MCP cleanup skipped: ${err && err.message}`);
|
|
226
|
+
}
|
|
181
227
|
}
|
|
182
228
|
|
|
183
229
|
/**
|
|
@@ -291,6 +337,7 @@ async function main(deps = {}) {
|
|
|
291
337
|
const _installCouncilSkill = deps.installCouncilSkill || installCouncilSkill;
|
|
292
338
|
const _registerClaudeCode = deps.registerClaudeCode || registerClaudeCode;
|
|
293
339
|
const _registerClaudeDesktop = deps.registerClaudeDesktop || registerClaudeDesktop;
|
|
340
|
+
const _migrateLegacyMcp = deps.migrateLegacyMcp || migrateLegacyMcp;
|
|
294
341
|
const _setupHooks = deps.setupHooks || setupHooks;
|
|
295
342
|
const _provisionElectron = deps.provisionElectron || provisionElectron;
|
|
296
343
|
|
|
@@ -302,6 +349,7 @@ async function main(deps = {}) {
|
|
|
302
349
|
_installCouncilSkill();
|
|
303
350
|
_registerClaudeCode();
|
|
304
351
|
_registerClaudeDesktop();
|
|
352
|
+
_migrateLegacyMcp(deps);
|
|
305
353
|
|
|
306
354
|
// Non-fatal, cache-only: heal the optional Electron binary from local cache
|
|
307
355
|
// or emit a deferred notice (GUI provisions on first use). Never throws.
|
|
@@ -336,4 +384,5 @@ if (require.main === module) {
|
|
|
336
384
|
runCli();
|
|
337
385
|
}
|
|
338
386
|
|
|
339
|
-
module.exports = { main, runCli, addMcpToConfigFile, installSkill, installCouncilSkill,
|
|
387
|
+
module.exports = { main, runCli, addMcpToConfigFile, installSkill, installCouncilSkill,
|
|
388
|
+
setupHooks, provisionElectron, registerClaudeCode, registerClaudeDesktop, migrateLegacyMcp, COUNCIL_FILES };
|
|
@@ -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 };
|
package/src/cli-handlers.js
CHANGED
|
@@ -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':
|