severally-mcp 2.1.0 → 2.4.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/CODE_OF_CONDUCT.md +83 -0
- package/CONFIG.md +10 -6
- package/README.ja.md +146 -69
- package/README.md +150 -73
- package/bin/severally-mcp.mjs +17 -2
- package/config.example.json +26 -0
- package/docs/how-it-works.ja.md +4 -2
- package/docs/how-it-works.md +5 -3
- package/docs/install.ja.md +59 -24
- package/docs/install.md +60 -24
- package/package.json +16 -4
- package/plugins/severally/.claude-plugin/plugin.json +1 -1
- package/plugins/severally/.codex-plugin/plugin.json +1 -1
- package/plugins/severally/dist/severally-mcp.mjs +29220 -27764
- package/plugins/severally/skills/_template/SKILL.md.tmpl +16 -22
- package/plugins/severally/skills/_template/reference.md.tmpl +11 -5
- package/plugins/severally/skills/antigravity/severally/SKILL.md +16 -22
- package/plugins/severally/skills/antigravity/severally/reference.md +11 -5
- package/plugins/severally/skills/claude/severally/SKILL.md +16 -22
- package/plugins/severally/skills/claude/severally/reference.md +11 -5
- package/plugins/severally/skills/codex/severally/SKILL.md +16 -22
- package/plugins/severally/skills/codex/severally/reference.md +11 -5
- package/plugins/severally/skills/opencode/severally/SKILL.md +16 -22
- package/plugins/severally/skills/opencode/severally/reference.md +11 -5
- package/scripts/build.mjs +31 -26
- package/scripts/doctor.mjs +52 -17
- package/scripts/init-config.mjs +8 -6
- package/scripts/install.mjs +342 -84
- package/scripts/live-check.mjs +52 -19
- package/scripts/live-mcp-check.mjs +42 -12
- package/scripts/test.mjs +10 -4
- package/src/adapters/antigravity-sandbox.mjs +39 -23
- package/src/adapters/antigravity.mjs +34 -16
- package/src/adapters/claude-code.mjs +73 -21
- package/src/adapters/codex.mjs +69 -27
- package/src/adapters/opencode-sandbox.mjs +40 -21
- package/src/adapters/opencode.mjs +31 -13
- package/src/brief.mjs +65 -41
- package/src/consultants.mjs +99 -30
- package/src/export.mjs +49 -12
- package/src/expose-paths.mjs +162 -44
- package/src/failures.mjs +23 -13
- package/src/jobs.mjs +420 -140
- package/src/parse-result.mjs +85 -30
- package/src/platform.mjs +5 -7
- package/src/policy.mjs +139 -32
- package/src/redact.mjs +38 -14
- package/src/run.mjs +104 -22
- package/src/schema.mjs +76 -22
- package/src/server.mjs +176 -166
- package/src/status.mjs +132 -0
- package/src/store.mjs +164 -19
package/src/server.mjs
CHANGED
|
@@ -4,12 +4,12 @@ import path from 'node:path';
|
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { McpServer } from '@modelcontextprotocol/server';
|
|
6
6
|
import { serveStdio } from '@modelcontextprotocol/server/stdio';
|
|
7
|
-
import { POLICY, VERDICTS, availableConsultants, configProblem, limitsSummary } from './policy.mjs';
|
|
7
|
+
import { POLICY, VERDICTS, availableConsultants, configProblem, configWarnings, limitsSummary } from './policy.mjs';
|
|
8
8
|
import { requestSchema, RequestError } from './schema.mjs';
|
|
9
9
|
import { JobManager, pointsOf } from './jobs.mjs';
|
|
10
10
|
import { exportChain, ExportError } from './export.mjs';
|
|
11
11
|
|
|
12
|
-
export const SEVERALLY_VERSION = '2.
|
|
12
|
+
export const SEVERALLY_VERSION = '2.4.0';
|
|
13
13
|
|
|
14
14
|
// consult_get waits this long when no wait_ms is given: a call that forgot it
|
|
15
15
|
// would otherwise return at once and be repeated. Up to 45 s, under the 60 s
|
|
@@ -22,7 +22,11 @@ const DEFAULT_WAIT_MS = process.env.SEVERALLY_MAX_WAIT_MS ? POLICY.maxWaitMs : M
|
|
|
22
22
|
const PROGRESS_EVERY_MS = 15_000;
|
|
23
23
|
|
|
24
24
|
const semver = (v) => String(v).split('.').map(Number);
|
|
25
|
-
const older = (a, b) => {
|
|
25
|
+
const older = (a, b) => {
|
|
26
|
+
const [x, y] = [semver(a), semver(b)];
|
|
27
|
+
for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] < y[i];
|
|
28
|
+
return false;
|
|
29
|
+
};
|
|
26
30
|
|
|
27
31
|
/**
|
|
28
32
|
* The other installed copies of severally, when one runs a different version
|
|
@@ -31,7 +35,13 @@ const older = (a, b) => { const [x, y] = [semver(a), semver(b)]; for (let i = 0;
|
|
|
31
35
|
* version strings are compared. A copy that is missing is not reported.
|
|
32
36
|
*/
|
|
33
37
|
export function staleInstall(home = os.homedir(), severallyHome = POLICY.home) {
|
|
34
|
-
const read = (file) => {
|
|
38
|
+
const read = (file) => {
|
|
39
|
+
try {
|
|
40
|
+
return fs.readFileSync(file, 'utf8');
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
35
45
|
// The runtime's version is read from its bundle, which every install mode
|
|
36
46
|
// replaces; its package.json is written by plugin installs only.
|
|
37
47
|
const bundle = read(path.join(severallyHome, 'runtime', 'severally-mcp.mjs'));
|
|
@@ -39,7 +49,13 @@ export function staleInstall(home = os.homedir(), severallyHome = POLICY.home) {
|
|
|
39
49
|
const found = {
|
|
40
50
|
// This release names it SEVERALLY_VERSION; older ones only had it in the server info.
|
|
41
51
|
runtime: bundle?.match(/(?:SEVERALLY_VERSION = |name: ["']severally["'], version: )["'](\d+\.\d+\.\d+)["']/)?.[1],
|
|
42
|
-
claude_skills_dir: (() => {
|
|
52
|
+
claude_skills_dir: (() => {
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(plugin).version;
|
|
55
|
+
} catch {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
})(),
|
|
43
59
|
};
|
|
44
60
|
const copies = { this_server: SEVERALLY_VERSION };
|
|
45
61
|
// Only plain x.y.z is compared; anything else is left out rather than guessed at.
|
|
@@ -50,9 +66,10 @@ export function staleInstall(home = os.homedir(), severallyHome = POLICY.home) {
|
|
|
50
66
|
return {
|
|
51
67
|
copies,
|
|
52
68
|
behind: Object.keys(copies).filter((k) => older(copies[k], newest)),
|
|
53
|
-
fix:
|
|
54
|
-
|
|
55
|
-
|
|
69
|
+
fix:
|
|
70
|
+
`bring every copy to ${newest}: run \`npx -y severally-mcp@${newest} install\` (or \`node scripts/install.mjs\` from a checkout of that version) ` +
|
|
71
|
+
'(it updates the runtime and the ~/.claude/skills copy), run `/plugin marketplace update severally` for a ' +
|
|
72
|
+
'marketplace install, then restart the clients',
|
|
56
73
|
};
|
|
57
74
|
}
|
|
58
75
|
|
|
@@ -70,100 +87,79 @@ Budget is 1 initial round plus at most ${POLICY.maxRounds - 1} follow-ups per ch
|
|
|
70
87
|
spent only on the specific points where you and the consultant actually diverge. Agreement is not evidence:
|
|
71
88
|
check the grounds behind a point before you adopt it, and record what you adopted, rejected or held, and why.`;
|
|
72
89
|
|
|
73
|
-
const startDescription = `Start a consultation with another agent (or several, via targets). Returns a job_id (or a group_id for several)
|
|
90
|
+
const startDescription = `Start a consultation with another agent (or several, via targets). Returns a job_id (or a group_id for several) at once; the work runs in the background.
|
|
74
91
|
|
|
75
92
|
target: which consultant to ask. This machine can reach (name: CLI, model):
|
|
76
|
-
${
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
targets:
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
it, and which disagreements remain. Both are required.
|
|
110
|
-
|
|
111
|
-
The consultant starts in an empty working directory and is not told where your repository is, so it sees your
|
|
112
|
-
code only if you hand it over in this call. When the question is about code in a repository on this machine,
|
|
113
|
-
name the files it is about by absolute path in context.expose_paths: those files and directories are copied
|
|
114
|
-
read-only into its working directory (under ./workspace) and are then the only part of your repository it has.
|
|
115
|
-
At most 20 entries, 500 files and 5 MB in total; symlinks and protected files are skipped rather than copied,
|
|
116
|
-
and text files are credential-masked exactly as the brief is. A directory inside a git work tree copies only the
|
|
117
|
-
files git lists (tracked, or untracked and not ignored), so a repository root fits; outside git, .git and
|
|
118
|
-
node_modules are skipped. A follow-up starts in a fresh working directory
|
|
119
|
-
with no files: pass context.expose_from_parent: true to copy again, as they are now, the paths the round it follows
|
|
120
|
-
exposed (a changed or vanished entry is marked; an earlier expose_diff brings back the files it touched, not the
|
|
121
|
-
diff), and add expose_paths for anything new. It is never implied: leave it off when the follow-up does not need them. Put the facts it needs into context.facts, and keep
|
|
122
|
-
context.artifacts for what is not a file here (test output, a log); an excerpt there goes alongside the
|
|
123
|
-
exposed files, not instead of them. For a change you are making, pass
|
|
124
|
-
context.expose_diff: { repo: "<absolute repository path>", base?: "<revision, default HEAD>" } instead or as
|
|
125
|
-
well: the diff goes in the brief and every file it touches is copied under ./workspace/repo/ at its repository
|
|
126
|
-
path. Model, permissions, round count, timeout and size
|
|
127
|
-
caps are fixed by this server and cannot be raised from a request.
|
|
128
|
-
|
|
129
|
-
network: optional -- "on" (default) or "off". "off" takes web search and browsing away from every consultant
|
|
130
|
-
in this round: use it when the brief holds material that must not leave the machine through a URL.
|
|
131
|
-
Whatever the setting, consultants are refused credential and history paths (~/.ssh, cloud keys, other
|
|
132
|
-
tools' sessions, severally's own history, .env files) where the CLI can enforce it; each answer's
|
|
133
|
-
\`boundary\` says what held for that consultant.
|
|
134
|
-
|
|
135
|
-
prediction: optional -- { expected, worry }: the bottom line you expect back and, in one sentence, what you are
|
|
136
|
-
most worried about. Stored with the consultation and NEVER sent to the consultant. It can only be
|
|
137
|
-
written here, before the consultant runs, so that afterwards you cannot rewrite what you thought
|
|
138
|
-
beforehand; consult_record takes the other half (reflection) once you have read the answer.`;
|
|
93
|
+
${
|
|
94
|
+
availableConsultants()
|
|
95
|
+
.map((c) => ` ${c.name}: ${c.cli}, ${c.model}`)
|
|
96
|
+
.join('\n') || ' (none -- no consultant CLI is installed)'
|
|
97
|
+
}
|
|
98
|
+
Pass the word the user used: a name, a CLI ("codex", "agy"), a vendor or a model ("claude opus"). One
|
|
99
|
+
that fits several, unresolved by the operator's ranking, tier and version, is refused with the
|
|
100
|
+
candidates (target_ambiguous); one not listed is refused (target_unavailable). Your own CLI on a fresh
|
|
101
|
+
session is a valid consultant, marked same lineage.
|
|
102
|
+
targets: several consultants, the same question (not with target, no duplicates); targets: ["all"] is every
|
|
103
|
+
consultant listed above. Every member gets the byte-identical brief (a follow-up adds its own earlier rounds) and one group_id, for consult_get
|
|
104
|
+
and consult_cancel. A fan-out over the concurrency cap is refused before any member starts.
|
|
105
|
+
followup_to: the job_id of the answer to continue, or a group_id to ask every member of that fan-out again
|
|
106
|
+
(each on its own chain; a member that cannot take one is skipped and listed). A follow-up needs only
|
|
107
|
+
followup_to and question: the consultant, mode, network, objective, constraints, success_criteria,
|
|
108
|
+
facts, proposal, counterpoints and artifacts are the earlier round's unless you pass them, and one you pass,
|
|
109
|
+
even empty, replaces it. Files are not carried over: context.expose_from_parent: true copies again,
|
|
110
|
+
as they are now, what the earlier round exposed. A job's follow-up cannot change the consultant.
|
|
111
|
+
mode: optional, read from context when left out: no proposal is explore, a proposal is review, a proposal with
|
|
112
|
+
counterpoints is debate. review and debate need context.proposal (your plan and its reasoning); debate
|
|
113
|
+
also needs context.counterpoints (the other side's claims); explore refuses a proposal on round 1.
|
|
114
|
+
context: the consultant starts in an empty working directory and is not told where your repository is, so
|
|
115
|
+
give it at least one of: facts, artifacts (material that is not a file here: test output, a log),
|
|
116
|
+
expose_paths (absolute paths copied read-only under ./workspace; a directory in a git work tree copies
|
|
117
|
+
the files git lists; at most 20 entries, 500 files, 5 MB; symlinks and protected files are skipped,
|
|
118
|
+
text is credential-masked like the brief), expose_diff: { repo, base? } (the diff in the brief, the
|
|
119
|
+
files it touches under ./workspace/repo/).
|
|
120
|
+
network: "on" (default) or "off" (no web search or browsing for any consultant this round). Credential and
|
|
121
|
+
history paths are refused either way where the CLI can enforce it; each answer's \`boundary\` says what held.
|
|
122
|
+
caller, caller_model: the CLI and model you run on (self-declared), for the same-vendor caveat and the record.
|
|
123
|
+
initiator: "user" when the user asked for this; leave it out when you started it yourself.
|
|
124
|
+
prediction: { expected, worry }, kept and never sent; it can only be written here, before the answer.
|
|
125
|
+
Model, permissions, rounds, timeout and size caps are fixed by this server.`;
|
|
139
126
|
|
|
140
127
|
// One consultation's record: the single form of consult_record, and each
|
|
141
128
|
// element of its `records`.
|
|
142
129
|
const recordShape = {
|
|
143
|
-
job_id: z.string().min(1).describe('the
|
|
144
|
-
entries: z
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
130
|
+
job_id: z.string().min(1).describe('the job_id consult_get returned'),
|
|
131
|
+
entries: z
|
|
132
|
+
.array(
|
|
133
|
+
z.object({
|
|
134
|
+
id: z.string().min(1).describe('f1, u1, c1 ... from the result'),
|
|
135
|
+
verdict: z.enum(VERDICTS),
|
|
136
|
+
effect: z.string().max(4000).optional().describe('what it changed, or why it is unverified'),
|
|
137
|
+
note: z.string().max(4000).optional().describe('what you checked and found'),
|
|
138
|
+
}),
|
|
139
|
+
)
|
|
140
|
+
.min(1)
|
|
141
|
+
.max(100)
|
|
142
|
+
.optional(),
|
|
143
|
+
reflection: z
|
|
144
|
+
.object({
|
|
145
|
+
delta: z.string().min(1).max(4000).describe('what the answer added over what you expected ("nothing new" counts)'),
|
|
146
|
+
related_item_ids: z.array(z.string().min(1)).max(50).optional().describe('f1, u1, c1 ...'),
|
|
147
|
+
})
|
|
148
|
+
.optional()
|
|
149
|
+
.describe('written after reading the answer'),
|
|
154
150
|
};
|
|
155
151
|
|
|
156
152
|
export function createServer(manager = new JobManager()) {
|
|
157
153
|
// Read once: the copies on disk change only when someone reinstalls, and then
|
|
158
154
|
// this server is the one to restart.
|
|
159
155
|
const stale = staleInstall();
|
|
160
|
-
const server = new McpServer(
|
|
161
|
-
{ name: 'severally', version: SEVERALLY_VERSION },
|
|
162
|
-
{ instructions: SERVER_INSTRUCTIONS },
|
|
163
|
-
);
|
|
156
|
+
const server = new McpServer({ name: 'severally', version: SEVERALLY_VERSION }, { instructions: SERVER_INSTRUCTIONS });
|
|
164
157
|
|
|
165
|
-
const ok = (payload) => ({ content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] });
|
|
166
|
-
const fail = (payload) => ({
|
|
158
|
+
const ok = (payload) => ({ content: [{ type: /** @type {const} */ ('text'), text: JSON.stringify(payload, null, 2) }] });
|
|
159
|
+
const fail = (payload) => ({
|
|
160
|
+
content: [{ type: /** @type {const} */ ('text'), text: JSON.stringify(payload, null, 2) }],
|
|
161
|
+
isError: true,
|
|
162
|
+
});
|
|
167
163
|
|
|
168
164
|
server.registerTool(
|
|
169
165
|
'consult_start',
|
|
@@ -192,31 +188,30 @@ export function createServer(manager = new JobManager()) {
|
|
|
192
188
|
title: 'Get consultation status or result',
|
|
193
189
|
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
194
190
|
description:
|
|
195
|
-
`Fetch
|
|
196
|
-
'
|
|
197
|
-
'
|
|
198
|
-
'
|
|
199
|
-
'
|
|
200
|
-
'
|
|
201
|
-
|
|
202
|
-
'
|
|
203
|
-
'with
|
|
204
|
-
'
|
|
205
|
-
'
|
|
206
|
-
'`comparison.by_target` holds every consultant\'s stance, summary and findings (with their ids) side by ' +
|
|
207
|
-
'side, cut to fixed lengths where `truncated` says so. consult_get({ job_id, ids: ["f2", "u1"] }) on a ' +
|
|
208
|
-
'member returns just those points, each whole with its grounds and your verdict (`recorded`): fetch every point you ' +
|
|
209
|
-
'mean to adopt, that decides your call, that rests on thin grounds, or that every consultant assumes -- ' +
|
|
210
|
-
'not only where they diverge. consult_get({ job_id }) returns the whole answer. A finished ' +
|
|
211
|
-
'consultation names its brief (size, sha256, round file) rather than repeating it; detail: "full" ' +
|
|
212
|
-
'returns the brief, and for a fan-out every member\'s full answer.',
|
|
191
|
+
`Fetch a consultation's state or answer. It waits up to ${DEFAULT_WAIT_MS} ms (under this client's tool-call ` +
|
|
192
|
+
'timeout) for it to finish; wait_ms sets another wait, 0 answers at once. While waiting it sends a progress ' +
|
|
193
|
+
'notification every 15 s when asked; a cancelled call stops waiting. Most take one to ten minutes: call ' +
|
|
194
|
+
'again while status is running. completed carries the structured answer; failed carries ' +
|
|
195
|
+
'failure.kind (timeout, auth, usage_limit, rate_limited, model_unavailable, invalid_output, cli_error, ' +
|
|
196
|
+
'spawn_error, interrupted), meaning no advice was obtained -- not the same as advice with thin evidence ' +
|
|
197
|
+
"(quality.evidence_basis, quality.caveat). group_id fetches a fan-out: each member's status, failure and " +
|
|
198
|
+
'job_id, and `comparison.by_target` with every stance, summary and finding id side by side, cut where ' +
|
|
199
|
+
'`truncated` says so. ids: ["f2", "u1"] with a job_id returns just those points, whole, with their grounds ' +
|
|
200
|
+
'and your verdict (`recorded`). A finished consultation names its brief rather than repeating it; detail: ' +
|
|
201
|
+
'"full" returns the brief, and for a fan-out every member\'s full answer.',
|
|
213
202
|
inputSchema: z.object({
|
|
214
203
|
job_id: z.string().min(1).optional(),
|
|
215
204
|
group_id: z.string().min(1).optional(),
|
|
216
205
|
wait_ms: z.number().int().min(0).max(POLICY.maxWaitMs).optional(),
|
|
217
206
|
detail: z.enum(['full']).optional(),
|
|
218
|
-
ids: z
|
|
219
|
-
.
|
|
207
|
+
ids: z
|
|
208
|
+
.array(z.string().min(1))
|
|
209
|
+
.min(1)
|
|
210
|
+
.max(100)
|
|
211
|
+
.optional()
|
|
212
|
+
.describe(
|
|
213
|
+
'with job_id: return only these points of the finished answer (f1, u1, c1 ...), each whole with its grounds and, under `recorded`, the verdict you wrote',
|
|
214
|
+
),
|
|
220
215
|
}),
|
|
221
216
|
},
|
|
222
217
|
async ({ job_id, group_id, wait_ms, detail, ids }, ctx) => {
|
|
@@ -224,16 +219,26 @@ export function createServer(manager = new JobManager()) {
|
|
|
224
219
|
return fail({ error: 'invalid_request', message: 'pass exactly one of job_id or group_id' });
|
|
225
220
|
}
|
|
226
221
|
if (ids && (group_id || detail)) {
|
|
227
|
-
return fail({
|
|
222
|
+
return fail({
|
|
223
|
+
error: 'invalid_request',
|
|
224
|
+
message: "ids goes with job_id alone: a point id belongs to one consultant's answer",
|
|
225
|
+
});
|
|
228
226
|
}
|
|
229
227
|
const req = ctx?.mcpReq;
|
|
230
228
|
const token = req?._meta?.progressToken;
|
|
231
229
|
let beats = 0;
|
|
232
|
-
const timer =
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
230
|
+
const timer =
|
|
231
|
+
token === undefined
|
|
232
|
+
? null
|
|
233
|
+
: setInterval(() => {
|
|
234
|
+
beats += 1;
|
|
235
|
+
req
|
|
236
|
+
.notify({
|
|
237
|
+
method: 'notifications/progress',
|
|
238
|
+
params: { progressToken: token, progress: beats, message: 'the consultation is still running' },
|
|
239
|
+
})
|
|
240
|
+
.catch(() => {});
|
|
241
|
+
}, PROGRESS_EVERY_MS);
|
|
237
242
|
let view;
|
|
238
243
|
try {
|
|
239
244
|
// A request the client cancels stops the wait with it.
|
|
@@ -244,7 +249,10 @@ export function createServer(manager = new JobManager()) {
|
|
|
244
249
|
clearInterval(timer);
|
|
245
250
|
}
|
|
246
251
|
if (!view) {
|
|
247
|
-
return fail({
|
|
252
|
+
return fail({
|
|
253
|
+
error: 'unknown_job',
|
|
254
|
+
message: `no consultation with ${group_id ? 'group_id' : 'job_id'} "${group_id ?? job_id}"`,
|
|
255
|
+
});
|
|
248
256
|
}
|
|
249
257
|
// Still running: the poll answer as it is; ids applies once there is an answer.
|
|
250
258
|
if (!ids || ['running', 'queued', 'cancelling'].includes(view.status)) return ok(view);
|
|
@@ -264,7 +272,8 @@ export function createServer(manager = new JobManager()) {
|
|
|
264
272
|
annotations: { readOnlyHint: false, idempotentHint: true, openWorldHint: false },
|
|
265
273
|
description:
|
|
266
274
|
'Stop a running consultation and kill the consultant process and everything it spawned. ' +
|
|
267
|
-
'Pass group_id instead of job_id to stop every consultant in a fan-out.'
|
|
275
|
+
'Pass group_id instead of job_id to stop every consultant in a fan-out. One another severally server runs ' +
|
|
276
|
+
'is asked to stop, and that server stops it within a second.',
|
|
268
277
|
inputSchema: z.object({
|
|
269
278
|
job_id: z.string().min(1).optional(),
|
|
270
279
|
group_id: z.string().min(1).optional(),
|
|
@@ -276,7 +285,10 @@ export function createServer(manager = new JobManager()) {
|
|
|
276
285
|
}
|
|
277
286
|
const view = group_id ? manager.cancelGroup(group_id) : manager.cancel(job_id);
|
|
278
287
|
if (!view) {
|
|
279
|
-
return fail({
|
|
288
|
+
return fail({
|
|
289
|
+
error: 'unknown_job',
|
|
290
|
+
message: `no consultation with ${group_id ? 'group_id' : 'job_id'} "${group_id ?? job_id}"`,
|
|
291
|
+
});
|
|
280
292
|
}
|
|
281
293
|
return ok(view);
|
|
282
294
|
},
|
|
@@ -288,45 +300,44 @@ export function createServer(manager = new JobManager()) {
|
|
|
288
300
|
title: 'Record what checking a point showed',
|
|
289
301
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
290
302
|
description:
|
|
291
|
-
'Write your
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
+ '~/.severally/history, which is what makes the decision readable a month from now; the job is '
|
|
302
|
-
+ 'read back from that history, so a consultation from an earlier session can still be recorded against. '
|
|
303
|
-
+ 'It answers with counts (the verdicts tallied, and how many points now carry one); consult_get reads '
|
|
304
|
-
+ 'the entries back. Pass `reflection` to record what the answer added over what you already expected, when the '
|
|
305
|
-
+ 'consultation was started with a `prediction`. A prediction itself cannot be written here: it goes '
|
|
306
|
-
+ 'in consult_start, before the consultant runs, which is the only thing that makes it a prediction. '
|
|
307
|
-
+ 'For a fan-out, record every member in one call: records: [{ job_id, entries, reflection? }, ...] in '
|
|
308
|
-
+ 'place of job_id/entries/reflection. Every record is checked first; if any job or id is wrong, nothing is '
|
|
309
|
-
+ 'written and each bad one is named. A job whose write fails is listed under results with its error, '
|
|
310
|
-
+ 'not counted as saved.',
|
|
303
|
+
'Write your verdict on points of an answer after checking them: ids f1 ... (findings), u1 ... (unknowns), ' +
|
|
304
|
+
'c1 ... (next_checks). verdict is what checking showed -- confirmed (holds here), not_applicable (true in ' +
|
|
305
|
+
'general, not here), unverifiable, unverified (not checked yet; say why in effect) -- not whether you adopted ' +
|
|
306
|
+
'it. effect is what it changed about your decision, note the evidence. A follow-up shows the consultant each ' +
|
|
307
|
+
'verdict and effect (credential-masked), never the note. The same id again replaces the entry. Saved beside ' +
|
|
308
|
+
'the answer in ~/.severally/history, so a job from an earlier session can be recorded against; the server ' +
|
|
309
|
+
'never infers a verdict. Answers with counts; consult_get reads the entries back. reflection records what the ' +
|
|
310
|
+
'answer added over your consult_start prediction. records: [{ job_id, entries, reflection? }, ...] records ' +
|
|
311
|
+
'every member of a fan-out in one call; all are checked first and nothing is written if any job or id is ' +
|
|
312
|
+
'wrong. A job whose write fails is listed under results with its error.',
|
|
311
313
|
inputSchema: z.object({
|
|
312
314
|
...recordShape,
|
|
313
315
|
job_id: recordShape.job_id.optional(),
|
|
314
|
-
records: z
|
|
315
|
-
.
|
|
316
|
+
records: z
|
|
317
|
+
.array(z.object(recordShape))
|
|
318
|
+
.min(1)
|
|
319
|
+
.max(POLICY.maxConcurrent)
|
|
320
|
+
.optional()
|
|
321
|
+
.describe('one per fan-out member, instead of job_id/entries/reflection'),
|
|
316
322
|
}),
|
|
317
323
|
},
|
|
318
324
|
async ({ job_id, entries, reflection, records }) => {
|
|
319
325
|
try {
|
|
320
326
|
if (records !== undefined) {
|
|
321
|
-
const out = manager.record(
|
|
327
|
+
const out = manager.record(
|
|
328
|
+
Object.fromEntries(Object.entries({ job_id, entries, reflection, records }).filter(([, v]) => v !== undefined)),
|
|
329
|
+
);
|
|
322
330
|
return out.not_saved ? fail(out) : ok(out);
|
|
323
331
|
}
|
|
324
|
-
if (!job_id)
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
332
|
+
if (!job_id)
|
|
333
|
+
return fail({ error: 'invalid_request', message: 'pass job_id (with entries and/or reflection), or records' });
|
|
334
|
+
return ok(
|
|
335
|
+
manager.record({
|
|
336
|
+
job_id,
|
|
337
|
+
...(entries === undefined ? {} : { entries }),
|
|
338
|
+
...(reflection === undefined ? {} : { reflection }),
|
|
339
|
+
}),
|
|
340
|
+
);
|
|
330
341
|
} catch (err) {
|
|
331
342
|
if (err instanceof RequestError) {
|
|
332
343
|
return fail({ error: err.code, message: err.message, details: err.details ?? null });
|
|
@@ -342,12 +353,10 @@ export function createServer(manager = new JobManager()) {
|
|
|
342
353
|
title: 'Export a consultation as a record to commit',
|
|
343
354
|
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
344
355
|
description:
|
|
345
|
-
'Render one consultation (chain_id) or
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
+ '(a decision record next to the code it is about), which is the only place a teammate will find it. '
|
|
350
|
-
+ 'Reads the on-disk history, so a consultation from an earlier session can still be exported.',
|
|
356
|
+
'Render one consultation (chain_id) or fan-out (group_id) as Markdown: the brief as sent, each answer as ' +
|
|
357
|
+
'it came back, and the verdicts on each point, unchecked ones marked. Nothing is summarised or scored. ' +
|
|
358
|
+
'The text is returned, not written: put it beside the code the decision is about. Reads the history, so ' +
|
|
359
|
+
"an earlier session's consultation can be exported.",
|
|
351
360
|
inputSchema: z.object({
|
|
352
361
|
chain_id: z.string().min(1).optional(),
|
|
353
362
|
group_id: z.string().min(1).optional(),
|
|
@@ -369,17 +378,17 @@ export function createServer(manager = new JobManager()) {
|
|
|
369
378
|
title: 'List recent consultations',
|
|
370
379
|
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
371
380
|
description:
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
+ 'report is not counted as zero; it is named under not_reported.',
|
|
381
|
+
"Recent consultations, earlier sessions' too (this_session is false), newest first, with their ids, " +
|
|
382
|
+
'status, summary and verdicts; and usage_totals, the tokens (and cost) each consultant reported this session, ' +
|
|
383
|
+
'retries included. A figure a CLI does not report is named under not_reported, not counted as zero.',
|
|
376
384
|
inputSchema: z.object({ limit: z.number().int().min(1).max(100).optional() }),
|
|
377
385
|
},
|
|
378
|
-
async ({ limit }) =>
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
386
|
+
async ({ limit }) =>
|
|
387
|
+
ok({
|
|
388
|
+
jobs: manager.list({ limit: limit ?? 20 }),
|
|
389
|
+
usage_totals: manager.usageTotals(),
|
|
390
|
+
limits: limitsSummary(),
|
|
391
|
+
}),
|
|
383
392
|
);
|
|
384
393
|
|
|
385
394
|
return { server, manager };
|
|
@@ -389,16 +398,17 @@ export async function main() {
|
|
|
389
398
|
// A config mistake is said here, at start, as well as on every refused
|
|
390
399
|
// consult_start: the operator may be the only one who reads stderr.
|
|
391
400
|
const problem = configProblem();
|
|
392
|
-
if (problem)
|
|
401
|
+
if (problem)
|
|
402
|
+
process.stderr.write(`severally: config needs fixing; every consultation will be refused until it is -- ${problem}\n`);
|
|
403
|
+
for (const w of configWarnings())
|
|
404
|
+
process.stderr.write(`severally: config mistake; the consultant or CLI it is in stays unused until it is fixed -- ${w}\n`);
|
|
393
405
|
if (availableConsultants().length === 0) {
|
|
394
406
|
process.stderr.write(
|
|
395
407
|
'severally: no consultant CLI found on PATH (codex / claude / agy / opencode); every consultation will be refused\n',
|
|
396
408
|
);
|
|
397
409
|
}
|
|
398
410
|
if (process.env.SEVERALLY_ACTIVE === '1') {
|
|
399
|
-
process.stderr.write(
|
|
400
|
-
'severally: refusing to start inside a severally consultant session (recursion barrier)\n',
|
|
401
|
-
);
|
|
411
|
+
process.stderr.write('severally: refusing to start inside a severally consultant session (recursion barrier)\n');
|
|
402
412
|
process.exit(2);
|
|
403
413
|
}
|
|
404
414
|
// One manager for the process; serveStdio builds the server once the client's
|