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
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
const HINTS = require('./utils/remediation-hints');
|
|
5
|
+
// B14/4.3: 'mcp' + 'mcp-legacy' check bodies (mirrors the B15 tmpSweep split — see file header).
|
|
6
|
+
const mcpChecks = require('./utils/doctor-mcp-checks');
|
|
5
7
|
|
|
6
8
|
const MAX_CATALOG_AGE_MS = 24 * 60 * 60 * 1000; // 24h (mirrors model-catalog DEFAULT_MAX_AGE_MS)
|
|
7
9
|
|
|
@@ -39,8 +41,13 @@ function realDeps() {
|
|
|
39
41
|
// stays separate; repair only runs when fix is requested.
|
|
40
42
|
repairElectron: (opts) => require('./sidecar/electron-install').repairElectron(opts),
|
|
41
43
|
fix: false,
|
|
42
|
-
discoverClaudeCodeMcps: () => require('./utils/mcp-discovery').discoverClaudeCodeMcps(),
|
|
43
44
|
discoverCoworkMcps: () => require('./utils/mcp-discovery').discoverCoworkMcps(),
|
|
45
|
+
// B14: raw (unstripped) read — the PRIMARY 'mcp' check signal.
|
|
46
|
+
// discoverClaudeCodeMcps() always strips 'amicus'/'sidecar'-shaped
|
|
47
|
+
// entries (recursive-spawn guard, src/utils/mcp-self-identity.js) and so
|
|
48
|
+
// can never be used to detect a healthy registration — see
|
|
49
|
+
// utils/doctor-mcp-checks.js for the full rationale.
|
|
50
|
+
hasAmicusRegistration: () => require('./utils/mcp-discovery').hasAmicusRegistration(),
|
|
44
51
|
inspectLegacyMcpEntries: () => require('./utils/legacy-mcp-migration').inspectAllLegacySidecarEntries(),
|
|
45
52
|
migrateLegacyMcpEntries: () => require('./utils/legacy-mcp-migration').migrateLegacySidecar(),
|
|
46
53
|
skillInstalled: () => {
|
|
@@ -175,59 +182,12 @@ async function runDoctorChecks(depsOverride = {}) {
|
|
|
175
182
|
: { id: 'skills', name: 'Skills installed', status: 'warn', message: 'one or both skills missing', hint: `${HINTS.reinstall} (re-runs the skill install)` }
|
|
176
183
|
)));
|
|
177
184
|
|
|
178
|
-
checks.push(guard('mcp', 'MCP registration', () =>
|
|
179
|
-
const code = d.discoverClaudeCodeMcps();
|
|
180
|
-
const cowork = d.discoverCoworkMcps();
|
|
181
|
-
const inCode = !!(code && code.amicus);
|
|
182
|
-
const inCowork = !!(cowork && cowork.amicus);
|
|
183
|
-
// Primary signal: Claude Code MCP registration. Cowork/Desktop is reported as bonus only.
|
|
184
|
-
if (!inCode) {
|
|
185
|
-
return { id: 'mcp', name: 'MCP registration', status: 'warn', message: 'not registered in Claude Code', hint: `${HINTS.reinstall} (or install the amicus plugin)` };
|
|
186
|
-
}
|
|
187
|
-
const extra = inCowork ? ', Cowork/Desktop' : '';
|
|
188
|
-
return { id: 'mcp', name: 'MCP registration', status: 'ok', message: `registered: Claude Code${extra}`, hint: null };
|
|
189
|
-
}));
|
|
185
|
+
checks.push(guard('mcp', 'MCP registration', () => mcpChecks.evaluateMcpRegistration(d)));
|
|
190
186
|
|
|
191
|
-
// Duplicate legacy 'sidecar' MCP registration
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
|
|
195
|
-
checks.push(guard('mcp-legacy', 'Legacy sidecar MCP entry', () => {
|
|
196
|
-
const id = 'mcp-legacy'; const name = 'Legacy sidecar MCP entry';
|
|
197
|
-
const entries = d.inspectLegacyMcpEntries() || [];
|
|
198
|
-
const dupes = entries.filter(e => e.status === 'removable');
|
|
199
|
-
const custom = entries.filter(e => e.status === 'customized');
|
|
200
|
-
// An unreadable config is neither "no problem" nor a duplicate we can act
|
|
201
|
-
// on — reporting it as ok/'none' would hide a config doctor (and --fix)
|
|
202
|
-
// could not actually inspect. Always surface it, even alongside dupes.
|
|
203
|
-
const unreadable = entries.filter(e => e.status === 'unreadable');
|
|
204
|
-
const unreadableNote = unreadable.length
|
|
205
|
-
? `${unreadable.map(e => e.target).join(', ')} config unreadable — skipped`
|
|
206
|
-
: null;
|
|
207
|
-
if (dupes.length === 0) {
|
|
208
|
-
if (unreadableNote) {
|
|
209
|
-
const suffix = custom.length ? `; custom 'sidecar' entry in ${custom.map(e => e.target).join(', ')} — left alone` : '';
|
|
210
|
-
return { id, name, status: 'warn', message: `${unreadableNote}${suffix}`, hint: null };
|
|
211
|
-
}
|
|
212
|
-
const message = custom.length
|
|
213
|
-
? `custom 'sidecar' entry in ${custom.map(e => e.target).join(', ')} — left alone`
|
|
214
|
-
: 'none';
|
|
215
|
-
return { id, name, status: 'ok', message, hint: null };
|
|
216
|
-
}
|
|
217
|
-
if (d.fix) {
|
|
218
|
-
const removed = (d.migrateLegacyMcpEntries() || []).filter(r => r.result === 'removed');
|
|
219
|
-
if (removed.length >= dupes.length) {
|
|
220
|
-
const message = `removed legacy entry from: ${removed.map(r => r.target).join(', ')}`;
|
|
221
|
-
return unreadableNote
|
|
222
|
-
? { id, name, status: 'warn', message: `${message}; ${unreadableNote}`, hint: HINTS.removeLegacySidecar }
|
|
223
|
-
: { id, name, status: 'ok', message, hint: null };
|
|
224
|
-
}
|
|
225
|
-
const message = `removed ${removed.length}/${dupes.length} duplicate(s) — could not update every config`;
|
|
226
|
-
return { id, name, status: 'warn', message: unreadableNote ? `${message}; ${unreadableNote}` : message, hint: HINTS.removeLegacySidecar };
|
|
227
|
-
}
|
|
228
|
-
const message = `duplicate 'sidecar' entry in ${dupes.map(e => e.target).join(', ')} — doubles the MCP tool list`;
|
|
229
|
-
return { id, name, status: 'warn', message: unreadableNote ? `${message}; ${unreadableNote}` : message, hint: HINTS.removeLegacySidecar };
|
|
230
|
-
}));
|
|
187
|
+
// Duplicate legacy 'sidecar' MCP registration check — logic lives in
|
|
188
|
+
// utils/doctor-mcp-checks.js (mirrors the B15 tmpSweep split) to keep this
|
|
189
|
+
// file under the 300-line size gate.
|
|
190
|
+
checks.push(guard('mcp-legacy', 'Legacy sidecar MCP entry', () => mcpChecks.evaluateLegacyMcpEntry(d)));
|
|
231
191
|
|
|
232
192
|
checks.push(guard('sessions-index-tmp', 'Session index tmp files', () => tmpSweep.evaluateSessionIndexTmpSweep(d)));
|
|
233
193
|
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Resume/Continue Handlers (B21-rest extraction)
|
|
3
|
+
*
|
|
4
|
+
* Split out of src/cli-handlers-run.js (which stayed over the 300-line size
|
|
5
|
+
* gate once --json plumbing landed here) — same extraction rationale as the
|
|
6
|
+
* original WS-2 split of bin/amicus.js.
|
|
7
|
+
*
|
|
8
|
+
* Contains: handleResume, handleContinue
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const { resolveModelFromArgs, validateFallbackModel } = require('./utils/start-helpers');
|
|
14
|
+
const { failJson, ERROR_CODES } = require('./utils/error-doc');
|
|
15
|
+
const { requireNoUiForJson, requireValidTaskId } = require('./utils/cli-preflight');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Handle 'amicus resume' command
|
|
19
|
+
* Spec Reference: §4.3
|
|
20
|
+
*/
|
|
21
|
+
async function handleResume(args) {
|
|
22
|
+
const useJson = !!args.json;
|
|
23
|
+
const taskId = requireValidTaskId(args, useJson, 'resume', 'Usage: amicus resume <task_id>');
|
|
24
|
+
requireNoUiForJson(args, useJson);
|
|
25
|
+
|
|
26
|
+
const { resumeAmicus } = require('./index');
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
return await resumeAmicus({
|
|
30
|
+
taskId,
|
|
31
|
+
project: args.cwd,
|
|
32
|
+
headless: args['no-ui'],
|
|
33
|
+
timeout: args.timeout,
|
|
34
|
+
json: useJson,
|
|
35
|
+
});
|
|
36
|
+
} catch (err) {
|
|
37
|
+
// resumeSidecar throws a plain Error before it has a chance to consult
|
|
38
|
+
// `json` (e.g. the session directory doesn't exist) — under --json that
|
|
39
|
+
// must still land as ONE parseable envelope on stdout, not an uncaught
|
|
40
|
+
// throw. Non-json mode is unaffected: re-throw so bin/amicus.js's
|
|
41
|
+
// existing top-level catch prints `Error: <message>` exactly as before.
|
|
42
|
+
if (!useJson) { throw err; }
|
|
43
|
+
process.exit(failJson(true, { code: ERROR_CODES.BAD_SESSION, message: err.message }));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Handle 'amicus continue' command
|
|
49
|
+
* Spec Reference: §4.4
|
|
50
|
+
*/
|
|
51
|
+
async function handleContinue(args) {
|
|
52
|
+
const useJson = !!args.json;
|
|
53
|
+
const taskId = requireValidTaskId(args, useJson, 'continue', 'Usage: amicus continue <task_id> --prompt "..."');
|
|
54
|
+
|
|
55
|
+
// BL-1: accept --prompt-file (XOR --prompt) so the MCP handler can pass a long
|
|
56
|
+
// follow-up prompt via file, dodging the ~32KB Windows command-line cap.
|
|
57
|
+
if (args['prompt-file'] !== undefined) {
|
|
58
|
+
const { resolvePromptSource } = require('./utils/prompt-source');
|
|
59
|
+
const promptRes = resolvePromptSource(args);
|
|
60
|
+
if (promptRes.error) {
|
|
61
|
+
process.exit(failJson(useJson, { code: ERROR_CODES.MISSING_PROMPT, message: promptRes.error }));
|
|
62
|
+
}
|
|
63
|
+
args.prompt = promptRes.prompt;
|
|
64
|
+
delete args['prompt-file'];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!args.prompt && !args.briefing) {
|
|
68
|
+
process.exit(failJson(useJson, { code: ERROR_CODES.MISSING_PROMPT, message: 'Error: --prompt is required for continue' }));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
requireNoUiForJson(args, useJson);
|
|
72
|
+
|
|
73
|
+
// F5: an explicitly passed --model gets the same resolution+validation as start.
|
|
74
|
+
if (args.model !== undefined) {
|
|
75
|
+
const { model, alias } = resolveModelFromArgs(args);
|
|
76
|
+
args.model = model;
|
|
77
|
+
args.model = await validateFallbackModel(args, alias);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const { continueAmicus } = require('./index');
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
return await continueAmicus({
|
|
84
|
+
taskId,
|
|
85
|
+
newTaskId: args['task-id'],
|
|
86
|
+
briefing: args.prompt || args.briefing,
|
|
87
|
+
model: args.model,
|
|
88
|
+
project: args.cwd,
|
|
89
|
+
contextTurns: args['context-turns'],
|
|
90
|
+
contextMaxTokens: args['context-max-tokens'],
|
|
91
|
+
headless: args['no-ui'],
|
|
92
|
+
timeout: args.timeout,
|
|
93
|
+
json: useJson,
|
|
94
|
+
});
|
|
95
|
+
} catch (err) {
|
|
96
|
+
// Same rationale as handleResume above: continueSidecar/loadPreviousSession
|
|
97
|
+
// throws before consulting `json` when the PREVIOUS session doesn't exist.
|
|
98
|
+
if (!useJson) { throw err; }
|
|
99
|
+
process.exit(failJson(true, { code: ERROR_CODES.BAD_SESSION, message: err.message }));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = { handleResume, handleContinue };
|
package/src/cli-handlers-run.js
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
* size gate and to make handlers unit-testable without running main().
|
|
6
6
|
*
|
|
7
7
|
* Contains: handleStart, handleFanout, handleRead
|
|
8
|
-
*
|
|
8
|
+
* See also: src/cli-handlers-resume-continue.js (handleResume, handleContinue —
|
|
9
|
+
* split out to stay under the size gate) and src/cli-handlers.js (handleList
|
|
10
|
+
* remains inline in bin/amicus.js).
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
13
|
'use strict';
|
|
@@ -14,6 +16,7 @@ const { validateStartArgs } = require('./cli');
|
|
|
14
16
|
const { validateTaskId } = require('./utils/validators');
|
|
15
17
|
const { resolveModelFromArgs, validateFallbackModel } = require('./utils/start-helpers');
|
|
16
18
|
const { failJson, ERROR_CODES } = require('./utils/error-doc');
|
|
19
|
+
const { requireNoUiForJson } = require('./utils/cli-preflight');
|
|
17
20
|
|
|
18
21
|
/**
|
|
19
22
|
* Handle 'sidecar start' command
|
|
@@ -33,9 +36,7 @@ async function handleStart(args) {
|
|
|
33
36
|
// prompt-file set and trip its mutually-exclusive branch.
|
|
34
37
|
delete args['prompt-file'];
|
|
35
38
|
}
|
|
36
|
-
|
|
37
|
-
process.exit(failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: 'Error: --json requires --no-ui' }));
|
|
38
|
-
}
|
|
39
|
+
requireNoUiForJson(args, useJson);
|
|
39
40
|
|
|
40
41
|
const mc = args['max-cost'];
|
|
41
42
|
if (mc !== undefined && (typeof mc !== 'number' || !Number.isFinite(mc) || mc <= 0)) {
|
package/src/cli-handlers.js
CHANGED
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
* under the 300-line limit.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
// handleAbort moved to src/cli-handlers-abort.js (B21-rest: --json branch
|
|
11
|
+
// needed headroom this file didn't have). Re-exported below for compatibility.
|
|
12
|
+
const { handleAbort } = require('./cli-handlers-abort');
|
|
11
13
|
|
|
12
14
|
/**
|
|
13
15
|
* Handle 'amicus setup' command
|
|
@@ -64,123 +66,6 @@ async function handleSetup(args) {
|
|
|
64
66
|
await runInteractiveSetup();
|
|
65
67
|
}
|
|
66
68
|
|
|
67
|
-
/**
|
|
68
|
-
* Handle 'sidecar abort' command
|
|
69
|
-
* Marks a running session as aborted
|
|
70
|
-
*/
|
|
71
|
-
async function handleAbort(args) {
|
|
72
|
-
if (args.all) {
|
|
73
|
-
const project = args.cwd || process.cwd();
|
|
74
|
-
const { enumerateSessions } = require('./sidecar/read');
|
|
75
|
-
const { markAborted } = require('./utils/session-abort');
|
|
76
|
-
const { resolveExistingSessionDir } = require('./session-manager');
|
|
77
|
-
// A session may complete between enumeration and the write (TOCTOU); the
|
|
78
|
-
// window is tiny for a local CLI and markAborted is best-effort, so we count
|
|
79
|
-
// only sessions actually marked aborted.
|
|
80
|
-
const running = enumerateSessions(project, { status: 'running' });
|
|
81
|
-
if (running.length === 0) {
|
|
82
|
-
console.log('No running sessions to abort.');
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
let aborted = 0;
|
|
86
|
-
for (const s of running) {
|
|
87
|
-
if (markAborted(resolveExistingSessionDir(project, s.id), 'abort --all')) {
|
|
88
|
-
aborted++;
|
|
89
|
-
console.log(`Aborted ${s.id}`);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
console.log(`Aborted ${aborted} running session(s).`);
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const taskId = args._[1];
|
|
97
|
-
|
|
98
|
-
if (!taskId) {
|
|
99
|
-
console.error('Error: task_id is required for abort');
|
|
100
|
-
console.error('Usage: amicus abort <task_id>');
|
|
101
|
-
process.exit(1);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const taskIdCheck = validateTaskId(taskId);
|
|
105
|
-
if (!taskIdCheck.valid) {
|
|
106
|
-
console.error(taskIdCheck.error);
|
|
107
|
-
process.exit(1);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const project = args.cwd || process.cwd();
|
|
111
|
-
const sessionDir = safeSessionDir(project, taskId);
|
|
112
|
-
const metaPath = path.join(sessionDir, 'metadata.json');
|
|
113
|
-
|
|
114
|
-
if (!fs.existsSync(metaPath)) {
|
|
115
|
-
console.error(`Session ${taskId} not found`);
|
|
116
|
-
process.exit(1);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
let meta;
|
|
120
|
-
try {
|
|
121
|
-
meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
|
|
122
|
-
} catch (_err) {
|
|
123
|
-
console.error(`Session ${taskId} has malformed metadata`);
|
|
124
|
-
process.exit(1);
|
|
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
|
-
|
|
136
|
-
const { markAborted } = require('./utils/session-abort');
|
|
137
|
-
|
|
138
|
-
// F4: aborting a wave aborts every still-running leg too.
|
|
139
|
-
if (meta.type === 'wave') {
|
|
140
|
-
const { resolveExistingSessionDir } = require('./session-manager');
|
|
141
|
-
let aborted = 0;
|
|
142
|
-
for (const legId of meta.legs || []) {
|
|
143
|
-
const legDir = resolveExistingSessionDir(project, legId);
|
|
144
|
-
try {
|
|
145
|
-
const legMeta = JSON.parse(fs.readFileSync(path.join(legDir, 'metadata.json'), 'utf-8'));
|
|
146
|
-
// TOCTOU: a leg may complete between this read and markAborted —
|
|
147
|
-
// best-effort, same contract as abort --all above.
|
|
148
|
-
if (legMeta.status === 'running') {
|
|
149
|
-
if (markAborted(legDir, 'wave abort')) { aborted++; }
|
|
150
|
-
}
|
|
151
|
-
} catch { /* skip unreadable leg */ }
|
|
152
|
-
}
|
|
153
|
-
markAborted(sessionDir, 'manual abort');
|
|
154
|
-
console.log(`Wave ${taskId} marked as aborted (${aborted} running leg(s) aborted).`);
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
markAborted(sessionDir, 'manual abort');
|
|
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
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
69
|
/**
|
|
185
70
|
* Handle 'amicus update' command
|
|
186
71
|
* Updates amicus to the latest version
|
package/src/cli.js
CHANGED
|
@@ -442,6 +442,7 @@ Options for 'status':
|
|
|
442
442
|
abort: `
|
|
443
443
|
Options for 'abort':
|
|
444
444
|
--all Abort all running sessions in this project
|
|
445
|
+
--json Emit the abort result as stable JSON
|
|
445
446
|
`,
|
|
446
447
|
read: `
|
|
447
448
|
Options for 'read':
|
|
@@ -457,6 +458,7 @@ Options for 'continue':
|
|
|
457
458
|
--model <model> Optional. Override the model (alias or provider/model)
|
|
458
459
|
--cwd <path> Project directory (default: cwd)
|
|
459
460
|
--no-ui Run without GUI (autonomous mode)
|
|
461
|
+
--json With --no-ui: emit the run result as stable JSON
|
|
460
462
|
--timeout <minutes> Headless timeout (default: 15)
|
|
461
463
|
--context-turns <N> Max conversation turns (default: 50)
|
|
462
464
|
--context-max-tokens <N> Max context tokens (default: 80000)
|
|
@@ -466,6 +468,7 @@ Options for 'resume':
|
|
|
466
468
|
<task_id> Required. Session to reopen (positional)
|
|
467
469
|
--cwd <path> Project directory (default: cwd)
|
|
468
470
|
--no-ui Run without GUI (autonomous mode)
|
|
471
|
+
--json With --no-ui: emit the run result as stable JSON
|
|
469
472
|
--timeout <minutes> Headless timeout (default: 15)
|
|
470
473
|
`,
|
|
471
474
|
council: `
|
|
@@ -546,6 +549,22 @@ Examples:
|
|
|
546
549
|
amicus read abc123 --conversation
|
|
547
550
|
`;
|
|
548
551
|
|
|
552
|
+
// Commands handled directly in bin/amicus.js's switch that have no dedicated
|
|
553
|
+
// USAGE_COMMAND_BLOCKS entry (their usage is covered by USAGE_HEADER's command
|
|
554
|
+
// list only). Kept minimal and explicit rather than parsing the switch itself.
|
|
555
|
+
const SWITCH_ONLY_COMMANDS = ['update'];
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Canonical list of top-level command names, for did-you-mean suggestions and
|
|
559
|
+
* any other consumer that needs "every command amicus recognizes" without a
|
|
560
|
+
* second hand-maintained list. Derived from USAGE_COMMAND_BLOCKS (the existing
|
|
561
|
+
* per-command help source of truth) plus SWITCH_ONLY_COMMANDS.
|
|
562
|
+
* @returns {string[]}
|
|
563
|
+
*/
|
|
564
|
+
function getCommandNames() {
|
|
565
|
+
return [...Object.keys(USAGE_COMMAND_BLOCKS), ...SWITCH_ONLY_COMMANDS];
|
|
566
|
+
}
|
|
567
|
+
|
|
549
568
|
/**
|
|
550
569
|
* Get usage text.
|
|
551
570
|
*
|
|
@@ -570,5 +589,6 @@ module.exports = {
|
|
|
570
589
|
parseArgs,
|
|
571
590
|
validateStartArgs,
|
|
572
591
|
getUsage,
|
|
592
|
+
getCommandNames,
|
|
573
593
|
DEFAULTS
|
|
574
594
|
};
|
package/src/mcp-server.js
CHANGED
|
@@ -205,13 +205,13 @@ function appendVersionWarning(content) {
|
|
|
205
205
|
*/
|
|
206
206
|
function computeNextPoll() {
|
|
207
207
|
return {
|
|
208
|
-
hint: '
|
|
208
|
+
hint: 'Preferred: call amicus_wait with this task ID — one blocking call replaces the sleep+status loop; re-call it while it returns timedOut: true. Fallback (no amicus_wait tool available): run `sleep 25` in your shell before calling amicus_status again. This enforces the wait and prevents token-wasting rapid polls.',
|
|
209
209
|
wait_command: 'sleep 25',
|
|
210
210
|
};
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
-
const HEADLESS_START_REMINDER = '<system-reminder>
|
|
214
|
-
const HEADLESS_STATUS_REMINDER = '<system-reminder>
|
|
213
|
+
const HEADLESS_START_REMINDER = '<system-reminder>Preferred: call amicus_wait with this task ID instead — one blocking call replaces the sleep+status loop; re-call it while it returns timedOut: true. Fallback (no amicus_wait tool available): before calling amicus_status, you MUST run `sleep 25` in your shell first. This enforces the polling interval and prevents token waste. Do other useful work while waiting, or run `sleep 25` to block until the next poll window.</system-reminder>';
|
|
214
|
+
const HEADLESS_STATUS_REMINDER = '<system-reminder>Preferred: call amicus_wait with this task ID instead — one blocking call replaces the sleep+status loop; re-call it while it returns timedOut: true. Fallback (no amicus_wait tool available): this session is still running. Before calling amicus_status again, you MUST run `sleep 25` in your shell first. Each premature poll wastes context tokens for zero benefit. Run `sleep 25` now, then check again.</system-reminder>';
|
|
215
215
|
|
|
216
216
|
/** Spawn an Amicus CLI process (fire-and-forget) */
|
|
217
217
|
function spawnSidecarProcess(args, sessionDir) {
|
|
@@ -672,7 +672,8 @@ const handlers = {
|
|
|
672
672
|
const legsTotal = (readMeta.legs || []).length;
|
|
673
673
|
const stillRunning = !readMeta.status || readMeta.status === 'running';
|
|
674
674
|
const msg = stillRunning
|
|
675
|
-
? `Wave ${input.taskId} is still running (${legsTotal} legs).
|
|
675
|
+
? `Wave ${input.taskId} is still running (${legsTotal} legs). Preferred: call amicus_wait ` +
|
|
676
|
+
'with this waveId — one blocking call replaces polling. Fallback: poll amicus_status.'
|
|
676
677
|
: `Wave ${input.taskId} ended with status '${readMeta.status}' before writing wave.json ` +
|
|
677
678
|
'(fan-out may have been killed). Read individual legs by taskId, or use mode \'metadata\'.';
|
|
678
679
|
return textResult(msg);
|
|
@@ -976,7 +977,9 @@ const handlers = {
|
|
|
976
977
|
|
|
977
978
|
const body = JSON.stringify({
|
|
978
979
|
waveId, taskIds: legIds, status: 'running', mode: 'headless',
|
|
979
|
-
message: 'Fan-out started.
|
|
980
|
+
message: 'Fan-out started. Preferred: call amicus_wait with the waveId — one blocking call ' +
|
|
981
|
+
'replaces polling; re-call it while it returns timedOut: true. Fallback: poll amicus_status ' +
|
|
982
|
+
'with the waveId. Either way, amicus_read the waveId when complete.',
|
|
980
983
|
});
|
|
981
984
|
return { content: [{ type: 'text', text: body }, { type: 'text', text: HEADLESS_START_REMINDER }] };
|
|
982
985
|
},
|
package/src/mcp-tools.js
CHANGED
|
@@ -44,8 +44,11 @@ function getTools() {
|
|
|
44
44
|
'ALWAYS use HEADLESS (noUi: true) for all of them unless the user ' +
|
|
45
45
|
'explicitly requests interactive. Opening multiple Electron windows ' +
|
|
46
46
|
'at once is disruptive. ' +
|
|
47
|
-
'For headless mode,
|
|
48
|
-
'
|
|
47
|
+
'For headless mode, prefer calling amicus_wait with the task ID — one ' +
|
|
48
|
+
'blocking call replaces the sleep+status loop; re-call it while it returns ' +
|
|
49
|
+
'timedOut: true. Fallback (no amicus_wait tool available): ALWAYS run ' +
|
|
50
|
+
'`sleep 25` in your shell before each amicus_status call to enforce the ' +
|
|
51
|
+
'polling interval. ' +
|
|
49
52
|
'For interactive mode, do not poll. Wait for the user to tell you ' +
|
|
50
53
|
'they\'ve clicked Fold, then use amicus_read. ' +
|
|
51
54
|
'Call amicus_guide first if you need help choosing a model or writing a good briefing.' +
|
|
@@ -61,9 +64,9 @@ function getTools() {
|
|
|
61
64
|
),
|
|
62
65
|
agent: z.enum(['Chat', 'Plan', 'Build']).optional()
|
|
63
66
|
.default('Chat').describe(
|
|
64
|
-
'Agent mode. Chat (default
|
|
65
|
-
'permission. Plan: read-only
|
|
66
|
-
'(all operations approved).'
|
|
67
|
+
'Agent mode. Chat (interactive default; headless runs auto-convert ' +
|
|
68
|
+
'to Build): reads auto, writes ask permission. Plan: read-only ' +
|
|
69
|
+
'analysis. Build: full auto (all operations approved).'
|
|
67
70
|
),
|
|
68
71
|
noUi: z.boolean().optional().default(false).describe(
|
|
69
72
|
'Run headless without GUI. Default false (opens Electron window).'
|
|
@@ -215,7 +218,8 @@ function getTools() {
|
|
|
215
218
|
description:
|
|
216
219
|
'Reopen a previous Amicus session with full conversation history ' +
|
|
217
220
|
'preserved. The session continues in the same OpenCode session. ' +
|
|
218
|
-
'Returns a task ID immediately — use
|
|
221
|
+
'Returns a task ID immediately — use amicus_wait to block until done ' +
|
|
222
|
+
'(or poll amicus_status).',
|
|
219
223
|
inputSchema: {
|
|
220
224
|
taskId: safeTaskId.describe(
|
|
221
225
|
'The task ID of the session to resume.'
|
|
@@ -238,7 +242,7 @@ function getTools() {
|
|
|
238
242
|
'Start a new Amicus session that inherits a previous session\'s ' +
|
|
239
243
|
'conversation as context. The previous session\'s messages become ' +
|
|
240
244
|
'read-only background for the new task. Returns a task ID ' +
|
|
241
|
-
'immediately — use
|
|
245
|
+
'immediately — use amicus_wait to block until done (or poll amicus_status).',
|
|
242
246
|
inputSchema: {
|
|
243
247
|
taskId: safeTaskId.describe(
|
|
244
248
|
'The task ID of the previous session to continue from.'
|
|
@@ -296,10 +300,12 @@ function getTools() {
|
|
|
296
300
|
description:
|
|
297
301
|
'Run N models on the SAME prompt in parallel (one shared engine) and ' +
|
|
298
302
|
'aggregate the results. Headless only. Returns {waveId, taskIds[]} ' +
|
|
299
|
-
'immediately.
|
|
300
|
-
'
|
|
301
|
-
'
|
|
302
|
-
'
|
|
303
|
+
'immediately. Preferred: call amicus_wait with the waveId — one blocking ' +
|
|
304
|
+
'call replaces polling; re-call it while it returns timedOut: true. ' +
|
|
305
|
+
'Fallback (no amicus_wait tool available): poll amicus_status with the ' +
|
|
306
|
+
'waveId (run `sleep 25` between polls). Either way, amicus_read the waveId ' +
|
|
307
|
+
'when done for the aggregated JSON wave document (per-leg summaries ' +
|
|
308
|
+
'inside). Each leg is also an ordinary session readable by taskId.',
|
|
303
309
|
inputSchema: {
|
|
304
310
|
models: z.array(safeModel).min(1).max(10).optional().describe(
|
|
305
311
|
`1-10 models (2+ for genuine fan-out). Short aliases (${aliasNames}) or full provider/model IDs. Duplicates allowed. Omit when using 'council'.`
|
|
@@ -442,16 +448,18 @@ Amicus spawns parallel conversations with different LLMs and folds results back
|
|
|
442
448
|
|
|
443
449
|
### Headless Mode (noUi: true)
|
|
444
450
|
1. amicus_start with model + prompt + noUi: true -> get task ID
|
|
445
|
-
2.
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
6. Act on findings
|
|
451
|
+
2. **Preferred:** call amicus_wait with the task ID — one blocking call (up to
|
|
452
|
+
~50s) replaces the sleep+status loop; re-call it while it returns timedOut: true
|
|
453
|
+
3. amicus_read to get the summary once complete
|
|
454
|
+
4. Act on findings
|
|
450
455
|
|
|
451
|
-
(
|
|
452
|
-
|
|
456
|
+
**Fallback (only if amicus_wait is unavailable):**
|
|
457
|
+
1. Run \`sleep 25\` in your shell (this enforces the polling interval)
|
|
458
|
+
2. amicus_status to check progress
|
|
459
|
+
3. If still running, run \`sleep 25\` again before each subsequent amicus_status call
|
|
460
|
+
4. amicus_read to get the summary once complete
|
|
453
461
|
|
|
454
|
-
**IMPORTANT:** Always run \`sleep 25\` before every amicus_status call. This is not optional. Each premature poll wastes context tokens for zero benefit. The sleep command enforces the wait mechanically.
|
|
462
|
+
**IMPORTANT (fallback path only):** Always run \`sleep 25\` before every amicus_status call. This is not optional. Each premature poll wastes context tokens for zero benefit. The sleep command enforces the wait mechanically.
|
|
455
463
|
|
|
456
464
|
### Interactive Mode (noUi: false, default)
|
|
457
465
|
1. amicus_start with model + prompt -> get task ID
|
|
@@ -463,17 +471,19 @@ up to ~50s and returns status; call it again while it returns timedOut: true.)
|
|
|
463
471
|
### Fan-Out (amicus_fanout)
|
|
464
472
|
Run the SAME prompt across 1-10 models in parallel (one shared engine):
|
|
465
473
|
1. amicus_fanout with models + prompt -> {waveId, taskIds[]}
|
|
466
|
-
2. sleep 25, then amicus_status with the waveId (repeat until done)
|
|
474
|
+
2. **Preferred:** call amicus_wait with the waveId (re-call while timedOut: true). **Fallback:** sleep 25, then amicus_status with the waveId (repeat until done)
|
|
467
475
|
3. amicus_read the waveId -> aggregated JSON wave document (per-leg summaries inside)
|
|
468
476
|
Each leg is an ordinary session: read/resume/continue it by taskId.
|
|
469
477
|
|
|
470
478
|
## Agent Selection
|
|
471
479
|
| Agent | Reads | Writes | Bash | Use When |
|
|
472
480
|
|-------|-------|--------|------|----------|
|
|
473
|
-
| Chat (default) | auto | asks | asks | Questions, analysis |
|
|
481
|
+
| Chat (interactive default*) | auto | asks | asks | Questions, analysis |
|
|
474
482
|
| Plan | auto | denied | denied | Read-only analysis |
|
|
475
483
|
| Build | auto | auto | auto | Implementation tasks |
|
|
476
484
|
|
|
485
|
+
* Headless (\`noUi\`) runs auto-convert Chat to Build — Chat would otherwise stall waiting on write/bash approval with no UI to approve it.
|
|
486
|
+
|
|
477
487
|
## Writing Good Briefings
|
|
478
488
|
Include: Objective, Background, Files of interest, Success criteria, Constraints.
|
|
479
489
|
|
package/src/sidecar/continue.js
CHANGED
|
@@ -123,7 +123,7 @@ async function continueSidecar(options) {
|
|
|
123
123
|
headless = false,
|
|
124
124
|
timeout = 15,
|
|
125
125
|
agent,
|
|
126
|
-
mcp, mcpConfig, client, noMcp, excludeMcp
|
|
126
|
+
mcp, mcpConfig, client, noMcp, excludeMcp, json = false
|
|
127
127
|
} = options;
|
|
128
128
|
|
|
129
129
|
// Load previous session data
|
|
@@ -185,10 +185,17 @@ async function continueSidecar(options) {
|
|
|
185
185
|
|
|
186
186
|
try {
|
|
187
187
|
if (headless) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
188
|
+
try {
|
|
189
|
+
result = await runHeadless(
|
|
190
|
+
model, systemPrompt, userMessage, newTaskId, project,
|
|
191
|
+
timeout * 60 * 1000, effectiveAgent, { mcp: mcpServers, nonce: foldNonce }
|
|
192
|
+
);
|
|
193
|
+
} catch (err) {
|
|
194
|
+
if (!json) { throw err; }
|
|
195
|
+
// --json contract: stdout must always carry a parseable run doc,
|
|
196
|
+
// even when the engine throws rather than returning {error}.
|
|
197
|
+
result = { summary: '', completed: false, timedOut: false, aborted: false, error: err.message, taskId: newTaskId };
|
|
198
|
+
}
|
|
192
199
|
summary = result.summary ||
|
|
193
200
|
'## Sidecar Results: No Output\n\nContinued session completed without summary.';
|
|
194
201
|
|
|
@@ -209,8 +216,8 @@ async function continueSidecar(options) {
|
|
|
209
216
|
releaseLock(prevSessionDir);
|
|
210
217
|
}
|
|
211
218
|
|
|
212
|
-
// Output summary
|
|
213
|
-
outputSummary(summary);
|
|
219
|
+
// Output summary (human mode only — json mode keeps stdout to the doc below)
|
|
220
|
+
if (!json) { outputSummary(summary); }
|
|
214
221
|
|
|
215
222
|
// Load current metadata for finalization
|
|
216
223
|
const metaPath = SessionPaths.metadataFile(sessionDir);
|
|
@@ -230,8 +237,16 @@ async function continueSidecar(options) {
|
|
|
230
237
|
writeFileAtomic(metaPath, JSON.stringify(meta, null, 2), { mode: 0o600 });
|
|
231
238
|
logger.error('Continuation completed with error', { taskId: newTaskId, error: meta.reason });
|
|
232
239
|
} else {
|
|
233
|
-
finalizeSession(sessionDir, summary, project, meta, { status: terminal.status });
|
|
240
|
+
finalizeSession(sessionDir, summary, project, meta, { quietStdout: json, status: terminal.status });
|
|
234
241
|
}
|
|
242
|
+
|
|
243
|
+
if (json) {
|
|
244
|
+
const { buildRunResult } = require('../utils/result-schema');
|
|
245
|
+
const finalMeta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
|
|
246
|
+
const doc = buildRunResult({ taskId: newTaskId, metadata: finalMeta, result, summary, sessionDir });
|
|
247
|
+
console.log(JSON.stringify(doc, null, 2));
|
|
248
|
+
}
|
|
249
|
+
|
|
235
250
|
return terminal.exitCode;
|
|
236
251
|
}
|
|
237
252
|
|