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.
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +69 -0
- package/README.md +21 -7
- package/bin/amicus.js +5 -0
- package/package.json +1 -1
- package/scripts/postinstall.js +72 -23
- package/skills/second-opinion/MODEL-NOTES.md +47 -1
- package/skills/second-opinion/SKILL.md +36 -8
- package/skills/sidecar/SKILL.md +36 -38
- 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/headless.js +2 -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
package/skills/sidecar/SKILL.md
CHANGED
|
@@ -1,39 +1,37 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: sidecar
|
|
3
3
|
description: >
|
|
4
|
-
Spawn
|
|
5
|
-
Grok, Mistral,
|
|
6
|
-
talk to, chat with,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
- `--
|
|
208
|
-
- `--prompt`: Detailed task description you generate
|
|
205
|
+
- `--prompt` (or `--prompt-file`): Detailed task description you generate
|
|
209
206
|
|
|
210
207
|
**Recommended:**
|
|
211
|
-
- `--
|
|
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` |
|
|
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
|
|
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
|
|
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 };
|
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':
|
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 {
|