amicus 2.0.0 → 2.1.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 +62 -0
- package/README.md +1 -1
- package/bin/amicus.js +11 -93
- package/commands/council.md +7 -5
- package/package.json +1 -1
- package/skills/second-opinion/SKILL.md +6 -5
- package/skills/sidecar/SKILL.md +17 -14
- package/src/cli-handlers-abort.js +244 -0
- package/src/cli-handlers-doctor.js +13 -53
- package/src/cli-handlers-resume-continue.js +103 -0
- package/src/cli-handlers-run.js +5 -4
- package/src/cli-handlers.js +5 -120
- package/src/cli.js +20 -0
- package/src/mcp-server.js +8 -5
- package/src/mcp-tools.js +31 -21
- package/src/sidecar/continue.js +23 -8
- package/src/sidecar/resume.js +23 -8
- package/src/utils/abort-result.js +36 -0
- package/src/utils/cli-preflight.js +43 -0
- package/src/utils/doctor-mcp-checks.js +84 -0
- package/src/utils/input-validators.js +52 -1
- package/src/utils/mcp-discovery.js +51 -14
- package/src/utils/result-schema-version.js +14 -0
- package/src/utils/result-schema.js +10 -10
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "amicus",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.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,68 @@ All notable changes to Amicus are documented here. Format follows
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [2.1.0] - 2026-07-04
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `--json` on `resume`, `continue`, and `abort`. `amicus resume <id> --no-ui --json` and
|
|
13
|
+
`amicus continue <id> --prompt "..." --no-ui --json` emit the same versioned run document as
|
|
14
|
+
`start --json` (a `continue` run's document carries the new continuation task id, not the old
|
|
15
|
+
one). `amicus abort <id|--all> --json` emits a new `type: 'abort'` document
|
|
16
|
+
(`{ schemaVersion, type, ok, scope, taskId, aborted, count }`) covering single-session, wave, and
|
|
17
|
+
`--all` aborts, success and failure alike — stdout carries exactly one parseable document either
|
|
18
|
+
way, and non-`--json` human output is unchanged (byte-identical pinned messages still hold).
|
|
19
|
+
- Did-you-mean suggestions for unknown CLI commands: a near-miss typo like `amicus contnue` now
|
|
20
|
+
prints `Unknown command: contnue` followed by `Did you mean: continue` on stderr (still exits 1).
|
|
21
|
+
Suggestions are capped at 3 and only shown within edit-distance 2 of a known command; unrelated
|
|
22
|
+
garbage input gets no suggestion.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- Agent-facing polling guidance now recommends `amicus_wait` first across every headless-flow
|
|
27
|
+
reminder, tool description, and guide section (MCP system-reminders, `amicus_start`/`amicus_resume`/
|
|
28
|
+
`amicus_continue`/`amicus_fanout` descriptions, `amicus_guide`'s headless workflow, and the
|
|
29
|
+
`second-opinion`/`sidecar` skill docs) — one blocking call replaces the sleep+status poll loop.
|
|
30
|
+
`sleep 25` + `amicus_status` polling remains documented as the explicit fallback for clients
|
|
31
|
+
without the `amicus_wait` tool; it is never presented as the only mechanism.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- `amicus doctor`'s MCP registration check no longer false-negatives on a healthy Claude Code
|
|
36
|
+
registration. The check's only signal was `discoverClaudeCodeMcps()`, which always strips every
|
|
37
|
+
`amicus`/`sidecar`-shaped entry as its own recursive-spawn guard — so the check could never see
|
|
38
|
+
its own registration and warned "not registered in Claude Code" even when one existed. The check
|
|
39
|
+
now reads the same config sources directly (unstripped) to answer "is amicus registered?".
|
|
40
|
+
|
|
41
|
+
### CI / Security
|
|
42
|
+
|
|
43
|
+
- `council-review.yml`: both fanout legs (review wave and synthesis) now request
|
|
44
|
+
`--summary-length normal` instead of `verbose`. `--summary-length` only shapes the prompt (there is
|
|
45
|
+
no engine-side output-token cap), so `verbose` was asking every model in the wave — on a paid CI
|
|
46
|
+
key — for maximally long output on every PR.
|
|
47
|
+
- `council-review.yml`: the model-to-model handoff from the review wave into the synthesis leg is
|
|
48
|
+
now neutralized. The synthesis briefing previously concatenated raw model review text
|
|
49
|
+
(`reviews.md`) straight into another model's prompt with no sanitization; it now runs the same
|
|
50
|
+
neutralization (byte-identical sed rules, duplicated into the synthesis step's own shell) used on
|
|
51
|
+
the human-facing PR comment, and wraps the reviews in an explicit untrusted-data block before
|
|
52
|
+
handing them to the synthesis model. The comment path itself is unchanged.
|
|
53
|
+
- `ci.yml`: the `quality` job now runs [actionlint](https://github.com/rhysd/actionlint) (pinned to
|
|
54
|
+
v1.7.7) over `.github/workflows/`, which also shellchecks every `run:` block via ubuntu-latest's
|
|
55
|
+
preinstalled shellcheck. Verified locally with the actionlint + shellcheck Windows binaries before
|
|
56
|
+
landing; both are clean against all 5 workflows (0 findings), so no suppression config was needed.
|
|
57
|
+
|
|
58
|
+
### Documentation
|
|
59
|
+
|
|
60
|
+
- Corrected the `--agent` default docs in `skills/sidecar/SKILL.md`: the flag defaults to `Chat`
|
|
61
|
+
only in interactive mode — headless (`--no-ui`) runs default to `Build`, since `chat` stalls
|
|
62
|
+
without user interaction. The file previously claimed an unqualified "defaults to Chat" in
|
|
63
|
+
several spots while also correctly documenting the headless-`Build` default elsewhere,
|
|
64
|
+
contradicting itself; `docs/usage.md` was already correct and unchanged.
|
|
65
|
+
- Corrected `commands/council.md`'s description of the council pipeline order: `amicus council
|
|
66
|
+
validate` runs per-leg during Stage 1 (independent reviews), and `amicus council tally` runs
|
|
67
|
+
after Stage 2 (cross-review) and before Stage 3 (chair synthesis) — not, as previously worded,
|
|
68
|
+
both after all three review waves.
|
|
69
|
+
|
|
8
70
|
## [2.0.0] - 2026-07-03
|
|
9
71
|
|
|
10
72
|
Amicus's first major release: the **`sidecar*` shim removal** (#19). v1.x carried a full
|
package/README.md
CHANGED
package/bin/amicus.js
CHANGED
|
@@ -11,12 +11,12 @@
|
|
|
11
11
|
const { loadCredentials } = require('../src/utils/env-loader');
|
|
12
12
|
loadCredentials();
|
|
13
13
|
|
|
14
|
-
const { parseArgs, getUsage } = require('../src/cli');
|
|
15
|
-
const { validateTaskId } = require('../src/utils/validators');
|
|
16
|
-
const { resolveModelFromArgs, validateFallbackModel } = require('../src/utils/start-helpers');
|
|
14
|
+
const { parseArgs, getUsage, getCommandNames } = require('../src/cli');
|
|
17
15
|
const { handleSetup, handleAbort, handleUpdate, handleMcp, handleKey } = require('../src/cli-handlers');
|
|
18
16
|
const { handleStart, handleFanout, handleRead } = require('../src/cli-handlers-run');
|
|
17
|
+
const { handleResume, handleContinue } = require('../src/cli-handlers-resume-continue');
|
|
19
18
|
const { isOneShotCommand, armExitWatchdog } = require('../src/utils/lifecycle');
|
|
19
|
+
const { suggestCommand } = require('../src/utils/input-validators');
|
|
20
20
|
const { logger } = require('../src/utils/logger');
|
|
21
21
|
|
|
22
22
|
const VERSION = require('../package.json').version;
|
|
@@ -124,7 +124,7 @@ async function main() {
|
|
|
124
124
|
await handleKey(args);
|
|
125
125
|
break;
|
|
126
126
|
case 'abort':
|
|
127
|
-
await handleAbort(args);
|
|
127
|
+
exitCode = await handleAbort(args);
|
|
128
128
|
break;
|
|
129
129
|
case 'mcp':
|
|
130
130
|
await handleMcp();
|
|
@@ -132,10 +132,16 @@ async function main() {
|
|
|
132
132
|
case 'update':
|
|
133
133
|
await handleUpdate();
|
|
134
134
|
break;
|
|
135
|
-
default:
|
|
135
|
+
default: {
|
|
136
136
|
console.error(`Unknown command: ${command}`);
|
|
137
|
+
// suggestCommand honors a cap-3 contract (up to 3 candidates, closest
|
|
138
|
+
// first) — print all of them, not just the closest, matching the
|
|
139
|
+
// join precedent in src/cli-handlers.js.
|
|
140
|
+
const candidates = suggestCommand(command, getCommandNames());
|
|
141
|
+
if (candidates.length > 0) { console.error(`Did you mean: ${candidates.join(', ')}`); }
|
|
137
142
|
console.log(getUsage());
|
|
138
143
|
process.exit(1);
|
|
144
|
+
}
|
|
139
145
|
}
|
|
140
146
|
} catch (err) {
|
|
141
147
|
console.error(`Error: ${err.message}`);
|
|
@@ -166,94 +172,6 @@ async function handleList(args) {
|
|
|
166
172
|
});
|
|
167
173
|
}
|
|
168
174
|
|
|
169
|
-
/**
|
|
170
|
-
* Handle 'sidecar resume' command
|
|
171
|
-
* Spec Reference: §4.3
|
|
172
|
-
*/
|
|
173
|
-
async function handleResume(args) {
|
|
174
|
-
const taskId = args._[1];
|
|
175
|
-
|
|
176
|
-
if (!taskId) {
|
|
177
|
-
console.error('Error: task_id is required for resume');
|
|
178
|
-
console.error('Usage: amicus resume <task_id>');
|
|
179
|
-
process.exit(1);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const taskIdCheck = validateTaskId(taskId);
|
|
183
|
-
if (!taskIdCheck.valid) {
|
|
184
|
-
console.error(taskIdCheck.error);
|
|
185
|
-
process.exit(1);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const { resumeAmicus } = require('../src/index');
|
|
189
|
-
|
|
190
|
-
return await resumeAmicus({
|
|
191
|
-
taskId,
|
|
192
|
-
project: args.cwd,
|
|
193
|
-
headless: args['no-ui'],
|
|
194
|
-
timeout: args.timeout
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Handle 'sidecar continue' command
|
|
200
|
-
* Spec Reference: §4.4
|
|
201
|
-
*/
|
|
202
|
-
async function handleContinue(args) {
|
|
203
|
-
const taskId = args._[1];
|
|
204
|
-
|
|
205
|
-
if (!taskId) {
|
|
206
|
-
console.error('Error: task_id is required for continue');
|
|
207
|
-
console.error('Usage: amicus continue <task_id> --prompt "..."');
|
|
208
|
-
process.exit(1);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
const taskIdCheck = validateTaskId(taskId);
|
|
212
|
-
if (!taskIdCheck.valid) {
|
|
213
|
-
console.error(taskIdCheck.error);
|
|
214
|
-
process.exit(1);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// BL-1: accept --prompt-file (XOR --prompt) so the MCP handler can pass a long
|
|
218
|
-
// follow-up prompt via file, dodging the ~32KB Windows command-line cap.
|
|
219
|
-
if (args['prompt-file'] !== undefined) {
|
|
220
|
-
const { resolvePromptSource } = require('../src/utils/prompt-source');
|
|
221
|
-
const promptRes = resolvePromptSource(args);
|
|
222
|
-
if (promptRes.error) {
|
|
223
|
-
console.error(promptRes.error);
|
|
224
|
-
process.exit(1);
|
|
225
|
-
}
|
|
226
|
-
args.prompt = promptRes.prompt;
|
|
227
|
-
delete args['prompt-file'];
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
if (!args.prompt && !args.briefing) {
|
|
231
|
-
console.error('Error: --prompt is required for continue');
|
|
232
|
-
process.exit(1);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// F5: an explicitly passed --model gets the same resolution+validation as start.
|
|
236
|
-
if (args.model !== undefined) {
|
|
237
|
-
const { model, alias } = resolveModelFromArgs(args);
|
|
238
|
-
args.model = model;
|
|
239
|
-
args.model = await validateFallbackModel(args, alias);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const { continueAmicus } = require('../src/index');
|
|
243
|
-
|
|
244
|
-
return await continueAmicus({
|
|
245
|
-
taskId,
|
|
246
|
-
newTaskId: args['task-id'],
|
|
247
|
-
briefing: args.prompt || args.briefing,
|
|
248
|
-
model: args.model,
|
|
249
|
-
project: args.cwd,
|
|
250
|
-
contextTurns: args['context-turns'],
|
|
251
|
-
contextMaxTokens: args['context-max-tokens'],
|
|
252
|
-
headless: args['no-ui'],
|
|
253
|
-
timeout: args.timeout
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
|
|
257
175
|
// Run main
|
|
258
176
|
main().catch(err => {
|
|
259
177
|
console.error(`Fatal error: ${err.message}`);
|
package/commands/council.md
CHANGED
|
@@ -17,8 +17,10 @@ the **analysis request**, and the **criteria**. If any of the three is missing o
|
|
|
17
17
|
ambiguous, ask for it before launching any model (the skill's Stage 0 covers this —
|
|
18
18
|
don't re-ask for what is already present).
|
|
19
19
|
|
|
20
|
-
Then follow the second-opinion skill end to end: Stage 0
|
|
21
|
-
setup, council selection with a cost estimate
|
|
22
|
-
|
|
23
|
-
`amicus council
|
|
24
|
-
|
|
20
|
+
Then follow the second-opinion skill end to end, in pipeline order: Stage 0
|
|
21
|
+
intake/prep and run-folder setup, then council selection with a cost estimate
|
|
22
|
+
and explicit user confirmation; Stage 1 independent reviews, running
|
|
23
|
+
`amicus council validate` on each leg's findings block as it lands; Stage 2
|
|
24
|
+
cross-review, followed by `amicus council tally` once cross-review settles;
|
|
25
|
+
Stage 3 council-chair synthesis; Stage 4 the accept/deny decision pass; and
|
|
26
|
+
Stage 5, which runs `amicus council verdict` to write the final `verdict.json`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "amicus",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.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": [
|
|
@@ -31,7 +31,7 @@ Operating lessons from each run fold back into `MODEL-NOTES.md` (with approval),
|
|
|
31
31
|
|
|
32
32
|
**Before launching any model, READ `MODEL-NOTES.md`** (next to this file). It holds the operating rules and per-model quirks that decide whether a run succeeds or silently fails. These were learned the hard way; skipping them wastes runs and produces empty results that look like answers.
|
|
33
33
|
|
|
34
|
-
**Transport rule — CLI not on PATH:** every command below assumes the `amicus` CLI. If `amicus` is not on PATH (typical for **plugin-only installs**), run the identical commands as `npx -y amicus@latest <args>` (e.g. `npx -y amicus@latest fanout --models "m1,m2,m3" --prompt-file <path> --json`), or use the equivalent MCP tools (`amicus_fanout`, `amicus_start`, `amicus_status`, `amicus_read`, `amicus_council_tally`, `amicus_council_stats`, `amicus_verdict`) — council briefings are always self-contained (`--no-context`), so MCP transport is equivalent.
|
|
34
|
+
**Transport rule — CLI not on PATH:** every command below assumes the `amicus` CLI. If `amicus` is not on PATH (typical for **plugin-only installs**), run the identical commands as `npx -y amicus@latest <args>` (e.g. `npx -y amicus@latest fanout --models "m1,m2,m3" --prompt-file <path> --json`), or use the equivalent MCP tools (`amicus_fanout`, `amicus_start`, `amicus_wait`, `amicus_status`, `amicus_read`, `amicus_council_tally`, `amicus_council_stats`, `amicus_verdict`) — council briefings are always self-contained (`--no-context`), so MCP transport is equivalent.
|
|
35
35
|
|
|
36
36
|
## When to use
|
|
37
37
|
|
|
@@ -124,7 +124,7 @@ Always quote the `--models` list — unquoted, PowerShell splits on commas and t
|
|
|
124
124
|
|
|
125
125
|
Run it in the background (`run_in_background: true`); you are notified on completion — do not
|
|
126
126
|
poll. `fanout` is headless by definition. The command exits when every leg is terminal and prints
|
|
127
|
-
ONE JSON wave document on stdout (`schemaVersion:
|
|
127
|
+
ONE JSON wave document on stdout (`schemaVersion: 2`; the wave's id field is `waveId`, each leg's id is `taskId`): check `status` (`complete` | `partial` |
|
|
128
128
|
`error`), `counts`, and each leg in `legs[]` — a leg's `summary` field IS that model's review;
|
|
129
129
|
`model`/`modelInput` identify the reviewer (`model` is the resolved id, `modelInput` the alias you passed — use the alias for `review-<model>.md` filenames); `status`/`error` identify failures. Exit code 0 =
|
|
130
130
|
all legs complete, 2 = partial (apply the wave-degrade rules below), 1 = error/aborted. (To re-fetch a single leg later: `amicus read <taskId> --json`.)
|
|
@@ -141,9 +141,10 @@ amicus start --model <redteam-model> --no-ui --json \
|
|
|
141
141
|
Its stdout is a single run document; the `summary` field is the review.
|
|
142
142
|
|
|
143
143
|
**Cowork / no-Bash environments:** use the MCP tools instead — `amicus_fanout` (briefing via
|
|
144
|
-
file) returns `{waveId, taskIds[]}` immediately
|
|
145
|
-
|
|
146
|
-
|
|
144
|
+
file) returns `{waveId, taskIds[]}` immediately. Preferred: call `amicus_wait` with the waveId —
|
|
145
|
+
one blocking call per wave; re-call it while it returns `timedOut: true`. Fallback: poll
|
|
146
|
+
`amicus_status`. Either way, `amicus_read` each leg when done. The council's briefings are always
|
|
147
|
+
self-contained (`--no-context`), so MCP transport is equivalent.
|
|
147
148
|
|
|
148
149
|
**Required structured output from every model.** Instruct each council model to produce:
|
|
149
150
|
|
package/skills/sidecar/SKILL.md
CHANGED
|
@@ -45,7 +45,7 @@ These rules are mandatory for every amicus invocation in this skill:
|
|
|
45
45
|
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`.
|
|
46
46
|
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.
|
|
47
47
|
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`.
|
|
48
|
-
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.
|
|
48
|
+
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_wait`, `amicus_status`, `amicus_read`, …) instead. Do not conclude the tool is broken because `amicus` is not found.
|
|
49
49
|
|
|
50
50
|
## Installation
|
|
51
51
|
|
|
@@ -65,7 +65,7 @@ amicus --version
|
|
|
65
65
|
|
|
66
66
|
### MCP Server (Auto-Registered)
|
|
67
67
|
|
|
68
|
-
On install, an MCP server is auto-registered for Claude Cowork and Claude Desktop. If you're in an MCP-enabled environment, you can use `amicus_start`, `amicus_status`, `amicus_read`, and other MCP tools directly instead of CLI commands. Call `amicus_guide` for detailed usage instructions.
|
|
68
|
+
On install, an MCP server is auto-registered for Claude Cowork and Claude Desktop. If you're in an MCP-enabled environment, you can use `amicus_start`, `amicus_wait`, `amicus_status`, `amicus_read`, and other MCP tools directly instead of CLI commands. Call `amicus_guide` for detailed usage instructions.
|
|
69
69
|
|
|
70
70
|
---
|
|
71
71
|
|
|
@@ -177,7 +177,7 @@ amicus start --model anthropic/<model-name> --prompt "..."
|
|
|
177
177
|
|
|
178
178
|
### Agent Selection Guidelines
|
|
179
179
|
|
|
180
|
-
**Chat mode (default)** — no `--agent` flag needed. Reads are auto-approved, writes and bash commands require user permission in the Electron UI:
|
|
180
|
+
**Chat mode (interactive default)** — no `--agent` flag needed. Reads are auto-approved, writes and bash commands require user permission in the Electron UI:
|
|
181
181
|
```bash
|
|
182
182
|
# Default — good for questions, analysis, and guided work
|
|
183
183
|
amicus start --model gemini --prompt "Analyze the auth flow and suggest improvements"
|
|
@@ -199,7 +199,7 @@ amicus start --model gemini --prompt "Implement the login feature" --agent Build
|
|
|
199
199
|
|
|
200
200
|
| Mode | Use When |
|
|
201
201
|
|------|----------|
|
|
202
|
-
| **Chat** (default) | Questions, analysis, guided exploration — you control what gets written |
|
|
202
|
+
| **Chat** (interactive default) | Questions, analysis, guided exploration — you control what gets written |
|
|
203
203
|
| **Plan** | Comprehensive read-only analysis where no changes should happen |
|
|
204
204
|
| **Build** | Offloading implementation tasks where full autonomy is desired |
|
|
205
205
|
|
|
@@ -250,14 +250,17 @@ amicus start \
|
|
|
250
250
|
- `--prompt-file <path>`: Read the prompt/briefing from a UTF-8 file (mutually exclusive with
|
|
251
251
|
`--prompt`). Use for long or multi-line briefings.
|
|
252
252
|
- `--json`: With `--no-ui`, emit the run result as one stable JSON document on stdout
|
|
253
|
-
(`schemaVersion:
|
|
253
|
+
(`schemaVersion: 2`; the `summary` field is the model's output).
|
|
254
254
|
- `--no-validate-model`: Skip the model-catalog pre-flight check (validation is on by default).
|
|
255
|
-
- `--agent <agent>`: Agent mode (controls tool permissions). If omitted, defaults to
|
|
255
|
+
- `--agent <agent>`: Agent mode (controls tool permissions). If omitted, defaults to
|
|
256
|
+
**Chat** in interactive mode and **Build** in headless (`--no-ui`) mode — `chat`
|
|
257
|
+
stalls without user interaction, so headless runs need an agent that doesn't wait
|
|
258
|
+
on write/bash approval.
|
|
256
259
|
|
|
257
260
|
**Primary Agents (for `amicus start`):**
|
|
258
|
-
- `Chat` **(default)**: Reads auto-approved, writes/bash require user permission
|
|
261
|
+
- `Chat` **(interactive default)**: Reads auto-approved, writes/bash require user permission
|
|
259
262
|
- `Plan`: Read-only mode - no file modifications possible
|
|
260
|
-
- `Build
|
|
263
|
+
- `Build` **(headless default)**: Full tool access - all operations auto-approved
|
|
261
264
|
|
|
262
265
|
**Custom Agents:**
|
|
263
266
|
Custom agents defined in `~/.config/opencode/agents/` or `.opencode/agents/` are passed through directly.
|
|
@@ -573,16 +576,16 @@ Amicus uses OpenCode's agent framework with three primary modes:
|
|
|
573
576
|
|
|
574
577
|
| Agent | Reads | Writes/Edits | Bash | Default |
|
|
575
578
|
|-------|-------|-------------|------|---------|
|
|
576
|
-
| **Chat** | auto | asks permission | asks permission |
|
|
579
|
+
| **Chat** | auto | asks permission | asks permission | Interactive |
|
|
577
580
|
| **Plan** | auto | denied | denied | No |
|
|
578
|
-
| **Build** | auto | auto | auto |
|
|
581
|
+
| **Build** | auto | auto | auto | Headless |
|
|
579
582
|
|
|
580
|
-
#### Chat Agent (Default)
|
|
583
|
+
#### Chat Agent (Interactive Default)
|
|
581
584
|
|
|
582
|
-
Conversational mode — reads are auto-approved, writes and bash commands prompt for user permission in the UI. This is the default when no `--agent` flag is provided.
|
|
585
|
+
Conversational mode — reads are auto-approved, writes and bash commands prompt for user permission in the UI. This is the default when no `--agent` flag is provided **in interactive mode**; headless (`--no-ui`) runs default to Build instead (see the Headless section below).
|
|
583
586
|
|
|
584
587
|
```bash
|
|
585
|
-
# These are equivalent — Chat is the default
|
|
588
|
+
# These are equivalent — Chat is the interactive default
|
|
586
589
|
amicus start --model gemini --prompt "Analyze the auth flow"
|
|
587
590
|
amicus start --model gemini --prompt "Analyze the auth flow" --agent Chat
|
|
588
591
|
```
|
|
@@ -781,7 +784,7 @@ If a relevant sidecar exists:
|
|
|
781
784
|
### Example 1: Interactive Debugging (Chat Mode - Default)
|
|
782
785
|
|
|
783
786
|
```bash
|
|
784
|
-
#
|
|
787
|
+
# Chat mode (interactive default) — can read freely, asks before writing
|
|
785
788
|
amicus start \
|
|
786
789
|
--model gpt \
|
|
787
790
|
--session-id "$(ls -t ~/.claude/projects/-Users-john-myproject/*.jsonl | head -1 | xargs basename .jsonl)" \
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Abort Handler (B21-rest extraction)
|
|
3
|
+
*
|
|
4
|
+
* Split out of src/cli-handlers.js — that file was already near the 300-line
|
|
5
|
+
* size gate and had no headroom for the --json branch added here. Re-exported
|
|
6
|
+
* from src/cli-handlers.js so existing callers/tests are unaffected.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const { validateTaskId, safeSessionDir } = require('./utils/validators');
|
|
14
|
+
const { failJson, ERROR_CODES } = require('./utils/error-doc');
|
|
15
|
+
const { buildAbortResult } = require('./utils/result-schema');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Handle 'amicus abort --all --json': mark every running session aborted.
|
|
19
|
+
* @returns {number} exit code (always 0 — even a no-op --all is a success)
|
|
20
|
+
*/
|
|
21
|
+
function handleAbortAllJson(project) {
|
|
22
|
+
const { enumerateSessions } = require('./sidecar/read');
|
|
23
|
+
const { markAborted } = require('./utils/session-abort');
|
|
24
|
+
const { resolveExistingSessionDir } = require('./session-manager');
|
|
25
|
+
const running = enumerateSessions(project, { status: 'running' });
|
|
26
|
+
const aborted = [];
|
|
27
|
+
for (const s of running) {
|
|
28
|
+
if (markAborted(resolveExistingSessionDir(project, s.id), 'abort --all')) { aborted.push(s.id); }
|
|
29
|
+
}
|
|
30
|
+
console.log(JSON.stringify(buildAbortResult({ scope: 'all', taskId: null, aborted }), null, 2));
|
|
31
|
+
return 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Handle 'amicus abort <taskId> --json' for a single session or a wave.
|
|
36
|
+
* Mirrors the human-mode logic in handleAbort below but emits ONE doc on
|
|
37
|
+
* stdout instead of the multi-line console.log prose; the same waitThenKill
|
|
38
|
+
* fallback still runs, its narration routed to stderr instead of stdout.
|
|
39
|
+
* @returns {Promise<number>} exit code (always 0 for a resolved abort doc/error
|
|
40
|
+
* doc — both are "the command ran"; ok:false is signaled inside the doc)
|
|
41
|
+
*/
|
|
42
|
+
async function handleAbortTaskJson(args, taskId) {
|
|
43
|
+
const project = args.cwd || process.cwd();
|
|
44
|
+
const sessionDir = safeSessionDir(project, taskId);
|
|
45
|
+
const metaPath = path.join(sessionDir, 'metadata.json');
|
|
46
|
+
|
|
47
|
+
if (!fs.existsSync(metaPath)) {
|
|
48
|
+
process.exit(failJson(true, { code: ERROR_CODES.BAD_SESSION, message: `Session ${taskId} not found` }));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let meta;
|
|
52
|
+
try {
|
|
53
|
+
meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
|
|
54
|
+
} catch (_err) {
|
|
55
|
+
process.exit(failJson(true, { code: ERROR_CODES.BAD_SESSION, message: `Session ${taskId} has malformed metadata` }));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (meta.status !== 'running') {
|
|
59
|
+
// Not a hard error — the task exists — but nothing was aborted by this call.
|
|
60
|
+
// Scope must reflect the task's own type (wave vs session), not assume
|
|
61
|
+
// 'session' — a terminal wave still has meta.type === 'wave'.
|
|
62
|
+
const scope = meta.type === 'wave' ? 'wave' : 'session';
|
|
63
|
+
console.log(JSON.stringify(buildAbortResult({ scope, taskId, aborted: [] }), null, 2));
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const { markAborted } = require('./utils/session-abort');
|
|
68
|
+
|
|
69
|
+
if (meta.type === 'wave') {
|
|
70
|
+
const { resolveExistingSessionDir } = require('./session-manager');
|
|
71
|
+
const aborted = [];
|
|
72
|
+
for (const legId of meta.legs || []) {
|
|
73
|
+
const legDir = resolveExistingSessionDir(project, legId);
|
|
74
|
+
try {
|
|
75
|
+
const legMeta = JSON.parse(fs.readFileSync(path.join(legDir, 'metadata.json'), 'utf-8'));
|
|
76
|
+
if (legMeta.status === 'running') {
|
|
77
|
+
if (markAborted(legDir, 'wave abort')) { aborted.push(legId); }
|
|
78
|
+
}
|
|
79
|
+
} catch { /* skip unreadable leg */ }
|
|
80
|
+
}
|
|
81
|
+
// Only report the wave itself as aborted if its own markAborted write
|
|
82
|
+
// succeeded — mirrors the leg gating above and the --all/single-session
|
|
83
|
+
// gating (aborted[] must list ids ACTUALLY marked aborted per
|
|
84
|
+
// buildAbortResult's doc-comment).
|
|
85
|
+
if (markAborted(sessionDir, 'manual abort')) { aborted.unshift(taskId); }
|
|
86
|
+
console.log(JSON.stringify(buildAbortResult({ scope: 'wave', taskId, aborted }), null, 2));
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const wasMarked = markAborted(sessionDir, 'manual abort');
|
|
91
|
+
|
|
92
|
+
// Same fallback direct-kill as human mode (see the comment on the
|
|
93
|
+
// equivalent block in handleAbort below) — json mode still needs the
|
|
94
|
+
// process actually signalled, it just can't narrate it on stdout (stdout
|
|
95
|
+
// must carry ONLY the doc). Route the same chatter to stderr instead.
|
|
96
|
+
if (meta.pid) {
|
|
97
|
+
const { waitThenKill, abortGraceMs } = require('./utils/abort-coordinator');
|
|
98
|
+
const graceSec = Math.ceil(abortGraceMs() / 1000);
|
|
99
|
+
process.stderr.write(`Waiting up to ${graceSec}s for the session process (pid ${meta.pid}) to exit gracefully...\n`);
|
|
100
|
+
const { killed, exited } = await waitThenKill(meta.pid);
|
|
101
|
+
if (killed.length > 0) {
|
|
102
|
+
process.stderr.write(`Process ${meta.pid} did not exit in time — sent SIGTERM (a hard kill on Windows).\n`);
|
|
103
|
+
} else if (exited.length > 0) {
|
|
104
|
+
process.stderr.write('Process exited cleanly.\n');
|
|
105
|
+
} else {
|
|
106
|
+
process.stderr.write(`Process ${meta.pid} is still running — could not signal it (insufficient permission). It may require manual termination.\n`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// aborted[] must list ids ACTUALLY marked aborted (buildAbortResult's doc
|
|
111
|
+
// comment) — gate on markAborted's own return, matching --all/wave-leg gating.
|
|
112
|
+
console.log(JSON.stringify(buildAbortResult({ scope: 'session', taskId, aborted: wasMarked ? [taskId] : [] }), null, 2));
|
|
113
|
+
return 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Handle 'sidecar abort' command
|
|
118
|
+
* Marks a running session as aborted
|
|
119
|
+
* @returns {Promise<number|undefined>} exit code (json mode only; human mode
|
|
120
|
+
* uses process.exit internally on failure paths and implicitly returns 0)
|
|
121
|
+
*/
|
|
122
|
+
async function handleAbort(args) {
|
|
123
|
+
const useJson = !!args.json;
|
|
124
|
+
|
|
125
|
+
if (args.all) {
|
|
126
|
+
const project = args.cwd || process.cwd();
|
|
127
|
+
if (useJson) { return handleAbortAllJson(project); }
|
|
128
|
+
|
|
129
|
+
const { enumerateSessions } = require('./sidecar/read');
|
|
130
|
+
const { markAborted } = require('./utils/session-abort');
|
|
131
|
+
const { resolveExistingSessionDir } = require('./session-manager');
|
|
132
|
+
// A session may complete between enumeration and the write (TOCTOU); the
|
|
133
|
+
// window is tiny for a local CLI and markAborted is best-effort, so we count
|
|
134
|
+
// only sessions actually marked aborted.
|
|
135
|
+
const running = enumerateSessions(project, { status: 'running' });
|
|
136
|
+
if (running.length === 0) {
|
|
137
|
+
console.log('No running sessions to abort.');
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
let aborted = 0;
|
|
141
|
+
for (const s of running) {
|
|
142
|
+
if (markAborted(resolveExistingSessionDir(project, s.id), 'abort --all')) {
|
|
143
|
+
aborted++;
|
|
144
|
+
console.log(`Aborted ${s.id}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
console.log(`Aborted ${aborted} running session(s).`);
|
|
148
|
+
return 0;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const taskId = args._[1];
|
|
152
|
+
|
|
153
|
+
if (!taskId) {
|
|
154
|
+
if (useJson) { process.exit(failJson(true, { code: ERROR_CODES.BAD_SESSION, message: 'Error: task_id is required for abort' })); }
|
|
155
|
+
console.error('Error: task_id is required for abort');
|
|
156
|
+
console.error('Usage: amicus abort <task_id>');
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const taskIdCheck = validateTaskId(taskId);
|
|
161
|
+
if (!taskIdCheck.valid) {
|
|
162
|
+
if (useJson) { process.exit(failJson(true, { code: ERROR_CODES.BAD_SESSION, message: taskIdCheck.error })); }
|
|
163
|
+
console.error(taskIdCheck.error);
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (useJson) { return await handleAbortTaskJson(args, taskId); }
|
|
168
|
+
|
|
169
|
+
const project = args.cwd || process.cwd();
|
|
170
|
+
const sessionDir = safeSessionDir(project, taskId);
|
|
171
|
+
const metaPath = path.join(sessionDir, 'metadata.json');
|
|
172
|
+
|
|
173
|
+
if (!fs.existsSync(metaPath)) {
|
|
174
|
+
console.error(`Session ${taskId} not found`);
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
let meta;
|
|
179
|
+
try {
|
|
180
|
+
meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
|
|
181
|
+
} catch (_err) {
|
|
182
|
+
console.error(`Session ${taskId} has malformed metadata`);
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
// Guard against a completed/terminal session: without this, metadata.pid
|
|
186
|
+
// still holds a value forever and `amicus abort <completed-task>` would
|
|
187
|
+
// wait the grace window then TerminateProcess whatever unrelated process
|
|
188
|
+
// now owns that (possibly recycled) pid. Mirrors MCP's amicus_abort guard
|
|
189
|
+
// (src/mcp-server.js) — same wording, no re-mark, no kill.
|
|
190
|
+
if (meta.status !== 'running') {
|
|
191
|
+
console.log(`Session ${taskId} is not running (status: ${meta.status}).`);
|
|
192
|
+
return 0;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const { markAborted } = require('./utils/session-abort');
|
|
196
|
+
|
|
197
|
+
// F4: aborting a wave aborts every still-running leg too.
|
|
198
|
+
if (meta.type === 'wave') {
|
|
199
|
+
const { resolveExistingSessionDir } = require('./session-manager');
|
|
200
|
+
let aborted = 0;
|
|
201
|
+
for (const legId of meta.legs || []) {
|
|
202
|
+
const legDir = resolveExistingSessionDir(project, legId);
|
|
203
|
+
try {
|
|
204
|
+
const legMeta = JSON.parse(fs.readFileSync(path.join(legDir, 'metadata.json'), 'utf-8'));
|
|
205
|
+
// TOCTOU: a leg may complete between this read and markAborted —
|
|
206
|
+
// best-effort, same contract as abort --all above.
|
|
207
|
+
if (legMeta.status === 'running') {
|
|
208
|
+
if (markAborted(legDir, 'wave abort')) { aborted++; }
|
|
209
|
+
}
|
|
210
|
+
} catch { /* skip unreadable leg */ }
|
|
211
|
+
}
|
|
212
|
+
markAborted(sessionDir, 'manual abort');
|
|
213
|
+
console.log(`Wave ${taskId} marked as aborted (${aborted} running leg(s) aborted).`);
|
|
214
|
+
return 0;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
markAborted(sessionDir, 'manual abort');
|
|
218
|
+
console.log(`Session ${taskId} marked as aborted.`);
|
|
219
|
+
|
|
220
|
+
// Phase 3: fallback direct-kill for a session that does not honor the
|
|
221
|
+
// marker. Headless loops poll the marker every ~2s and the interactive
|
|
222
|
+
// abort watch does too, so the normal outcome is a graceful exit during
|
|
223
|
+
// the grace window; only a wedged/legacy process gets SIGTERM. The wait is
|
|
224
|
+
// awaited on purpose — bin/amicus.js arms its force-exit watchdog only
|
|
225
|
+
// after this handler returns.
|
|
226
|
+
if (meta.pid) {
|
|
227
|
+
const { waitThenKill, abortGraceMs } = require('./utils/abort-coordinator');
|
|
228
|
+
const graceSec = Math.ceil(abortGraceMs() / 1000);
|
|
229
|
+
console.log(`Waiting up to ${graceSec}s for the session process (pid ${meta.pid}) to exit gracefully...`);
|
|
230
|
+
const { killed, exited } = await waitThenKill(meta.pid);
|
|
231
|
+
if (killed.length > 0) {
|
|
232
|
+
console.log(`Process ${meta.pid} did not exit in time — sent SIGTERM (a hard kill on Windows).`);
|
|
233
|
+
} else if (exited.length > 0) {
|
|
234
|
+
console.log('Process exited cleanly.');
|
|
235
|
+
} else {
|
|
236
|
+
// 3.1 contract: an EPERM-unkillable pid lands in NEITHER array —
|
|
237
|
+
// it is still alive and we could not signal it. Say so honestly.
|
|
238
|
+
console.log(`Process ${meta.pid} is still running — could not signal it (insufficient permission). It may require manual termination.`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return 0;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
module.exports = { handleAbort };
|