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.
Files changed (52) hide show
  1. package/CODE_OF_CONDUCT.md +83 -0
  2. package/CONFIG.md +10 -6
  3. package/README.ja.md +146 -69
  4. package/README.md +150 -73
  5. package/bin/severally-mcp.mjs +17 -2
  6. package/config.example.json +26 -0
  7. package/docs/how-it-works.ja.md +4 -2
  8. package/docs/how-it-works.md +5 -3
  9. package/docs/install.ja.md +59 -24
  10. package/docs/install.md +60 -24
  11. package/package.json +16 -4
  12. package/plugins/severally/.claude-plugin/plugin.json +1 -1
  13. package/plugins/severally/.codex-plugin/plugin.json +1 -1
  14. package/plugins/severally/dist/severally-mcp.mjs +29220 -27764
  15. package/plugins/severally/skills/_template/SKILL.md.tmpl +16 -22
  16. package/plugins/severally/skills/_template/reference.md.tmpl +11 -5
  17. package/plugins/severally/skills/antigravity/severally/SKILL.md +16 -22
  18. package/plugins/severally/skills/antigravity/severally/reference.md +11 -5
  19. package/plugins/severally/skills/claude/severally/SKILL.md +16 -22
  20. package/plugins/severally/skills/claude/severally/reference.md +11 -5
  21. package/plugins/severally/skills/codex/severally/SKILL.md +16 -22
  22. package/plugins/severally/skills/codex/severally/reference.md +11 -5
  23. package/plugins/severally/skills/opencode/severally/SKILL.md +16 -22
  24. package/plugins/severally/skills/opencode/severally/reference.md +11 -5
  25. package/scripts/build.mjs +31 -26
  26. package/scripts/doctor.mjs +52 -17
  27. package/scripts/init-config.mjs +8 -6
  28. package/scripts/install.mjs +342 -84
  29. package/scripts/live-check.mjs +52 -19
  30. package/scripts/live-mcp-check.mjs +42 -12
  31. package/scripts/test.mjs +10 -4
  32. package/src/adapters/antigravity-sandbox.mjs +39 -23
  33. package/src/adapters/antigravity.mjs +34 -16
  34. package/src/adapters/claude-code.mjs +73 -21
  35. package/src/adapters/codex.mjs +69 -27
  36. package/src/adapters/opencode-sandbox.mjs +40 -21
  37. package/src/adapters/opencode.mjs +31 -13
  38. package/src/brief.mjs +65 -41
  39. package/src/consultants.mjs +99 -30
  40. package/src/export.mjs +49 -12
  41. package/src/expose-paths.mjs +162 -44
  42. package/src/failures.mjs +23 -13
  43. package/src/jobs.mjs +420 -140
  44. package/src/parse-result.mjs +85 -30
  45. package/src/platform.mjs +5 -7
  46. package/src/policy.mjs +139 -32
  47. package/src/redact.mjs +38 -14
  48. package/src/run.mjs +104 -22
  49. package/src/schema.mjs +76 -22
  50. package/src/server.mjs +176 -166
  51. package/src/status.mjs +132 -0
  52. 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.1.0';
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) => { const [x, y] = [semver(a), semver(b)]; for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] < y[i]; return false; };
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) => { try { return fs.readFileSync(file, 'utf8'); } catch { return null; } };
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: (() => { try { return JSON.parse(plugin).version; } catch { return undefined; } })(),
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: `bring every copy to ${newest}: run \`npx -y severally-mcp@${newest} install\` (or \`node scripts/install.mjs\` from a checkout of that version) `
54
- + '(it updates the runtime and the ~/.claude/skills copy), run `/plugin marketplace update severally` for a '
55
- + 'marketplace install, then restart the clients',
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) immediately; the work runs in the background.
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
- ${availableConsultants().map((c) => ` ${c.name}: ${c.cli}, ${c.model}`).join('\n') || ' (none -- no consultant CLI is installed)'}
77
- Pass the name the user used: a consultant's name, its CLI ("codex", "agy"), its vendor ("openai") or
78
- its model ("glm", "claude opus", "gpt-6.1"). A name that fits several consultants goes to the
79
- operator's ranking first, then to the higher tier (opus over sonnet) and the newer version; when
80
- nothing separates them it is refused with the candidates -- ask the user which one, do not guess.
81
- A consultant that is not listed above is refused up front, so do not retry it -- say which ones
82
- are available instead.
83
- Consulting your own CLI on a fresh session is encouraged -- it sheds this session's history and
84
- drift -- and the result is marked as same lineage.
85
- targets: ask several consultants the same question at once (mutually exclusive with target, no duplicates).
86
- targets: ["all"] asks every consultant listed above; "all" cannot be mixed with names.
87
- Every member gets the byte-identical brief (on a follow-up, plus its own earlier rounds) and one
88
- group_id; poll it with consult_get({ group_id }). Each member takes one slot of the concurrency cap,
89
- and a fan-out that would go over it is refused before any member starts.
90
- A follow-up (followup_to) to a job names that one consultant. followup_to a group_id, with no target or
91
- targets, asks every member of that fan-out the same new question, each on its own chain from its latest
92
- round; a member that is still running, never answered or has used its rounds is skipped and listed.
93
- caller: optional -- the CLI you are running in ("codex" / "claude-code" / "antigravity" / "opencode"), so the
94
- server can annotate a same-vendor consultation.
95
- caller_model: optional -- the model you are running on (e.g. "claude-opus-5"), self-declared and never checked.
96
- With it, a same-vendor caveat can say "same lineage, different model" and the record keeps who asked
97
- whom; it never changes which model the consultant runs.
98
- initiator: optional -- "user" when the user asked for this consultation; leave it out when you started it
99
- yourself. Self-declared, never checked, gates nothing; it lets the record tell the two apart.
100
- mode: optional. Left out, it is read from context: no proposal is explore, a proposal is review, a proposal
101
- with counterpoints is debate; a follow-up keeps its chain's mode. To gather opinions without showing
102
- yours, leave context.proposal empty.
103
- explore - hand over objective/constraints/facts and withhold your own preferred solution, to get independent
104
- options, alternative problem framings and blind spots. context.proposal MUST be empty on round 1.
105
- review - hand over your current proposal AND the reasoning behind it, to get weaknesses, counterexamples,
106
- concrete improvements and the conditions under which it holds. context.proposal is required.
107
- debate - hand over the disputed point, your position (context.proposal), the other side's claims
108
- (context.counterpoints) and the extra evidence, to get what would change the judgement, how to test
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 consultation to record against -- the job_id consult_get returned for it'),
144
- entries: z.array(z.object({
145
- id: z.string().min(1).describe('the point this verdict is about: f1, u1, c1 ... as returned in the result'),
146
- verdict: z.enum(VERDICTS),
147
- effect: z.string().max(4000).optional().describe('what it changed about your decision, or why it is still unverified'),
148
- note: z.string().max(4000).optional().describe('what you checked and what you found'),
149
- })).min(1).max(100).optional(),
150
- reflection: z.object({
151
- delta: z.string().min(1).max(4000).describe('what the answer added over what you already expected -- including "nothing new", which is a real outcome'),
152
- related_item_ids: z.array(z.string().min(1)).max(50).optional().describe('the points this is about: f1, u1, c1 ...'),
153
- }).optional().describe('written after you have read the answer. There is deliberately no hit/miss label: a point you predicted can still arrive with the evidence that settles it, and a surprise can still be wrong.'),
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) => ({ content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }], isError: true });
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 the state of a consultation. It waits up to ${DEFAULT_WAIT_MS} ms for it to finish before ` +
196
- 'answering, a limit set to stay under this client\'s tool-call timeout; wait_ms sets another wait, and ' +
197
- 'wait_ms: 0 answers at once. While it waits it sends a progress notification every 15 s if you asked ' +
198
- 'for them, and a call you cancel stops waiting. A typical consultation takes one to ten minutes; call ' +
199
- 'this again while it runs, and pass a shorter wait_ms when you have independent work to do in between. ' +
200
- 'A completed job carries the structured ' +
201
- 'answer; a failed one carries failure.kind (timeout, auth, usage_limit, rate_limited, model_unavailable, ' +
202
- 'invalid_output, cli_error, spawn_error, interrupted) — that is "no advice was obtained", which is different from advice that arrived ' +
203
- 'with thin evidence (see quality.evidence_basis). ' +
204
- 'Pass group_id instead of job_id to fetch a fan-out; the wait then covers every consultant in it. ' +
205
- 'A fan-out comes back sized to read in one go: each member is its status, failure and job_id, and ' +
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.array(z.string().min(1)).min(1).max(100).optional()
219
- .describe('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'),
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({ error: 'invalid_request', message: 'ids goes with job_id alone: a point id belongs to one consultant\'s answer' });
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 = token === undefined ? null : setInterval(() => {
233
- beats += 1;
234
- req.notify({ method: 'notifications/progress', params: { progressToken: token, progress: beats, message: 'the consultation is still running' } })
235
- .catch(() => {});
236
- }, PROGRESS_EVERY_MS);
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({ error: 'unknown_job', message: `no consultation with ${group_id ? 'group_id' : 'job_id'} "${group_id ?? job_id}"` });
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({ error: 'unknown_job', message: `no consultation with ${group_id ? 'group_id' : 'job_id'} "${group_id ?? job_id}"` });
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 own verdict against one or more points of an answer, after you have checked them in the '
292
- + 'repository. Ids come from the result: findings are f1, f2 ..., unknowns u1 ..., next_checks c1 ... . '
293
- + 'verdict says what checking showed -- "confirmed" (it holds here), "not_applicable" (true in general, '
294
- + 'not for this codebase), "unverifiable" (cannot be settled with what you can reach), "unverified" (not '
295
- + 'checked yet, and say in effect why not). It does not say whether you adopted the point. effect is what '
296
- + 'it changed about your decision; note is the evidence you used. A follow-up to this job shows the '
297
- + 'consultant each point\'s verdict and effect (credential-masked, like the brief), never the note. '
298
- + 'Recording the same id again replaces '
299
- + 'that entry. This server stores what you write and counts the verdicts; it never infers one, and never '
300
- + 'decides a consultation was worth it. The entry is saved beside the answer and the brief in '
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.array(z.object(recordShape)).min(1).max(POLICY.maxConcurrent).optional()
315
- .describe('several consultations at once -- one per member of a fan-out -- instead of job_id/entries/reflection'),
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(Object.fromEntries(Object.entries({ job_id, entries, reflection, records }).filter(([, v]) => v !== undefined)));
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) return fail({ error: 'invalid_request', message: 'pass job_id (with entries and/or reflection), or records' });
325
- return ok(manager.record({
326
- job_id,
327
- ...(entries === undefined ? {} : { entries }),
328
- ...(reflection === undefined ? {} : { reflection }),
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 one fan-out (group_id) as Markdown: the brief as it was sent, '
346
- + 'each consultant\'s answer as it came back, and the verdicts recorded against each point -- with the '
347
- + 'ones nobody checked marked as unchecked. Nothing is summarised across consultants and nothing is '
348
- + 'scored. The text is returned, not written: put it wherever the decision belongs in the repository '
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
- 'Recent consultations from this session, newest first, with their status, one-line summary and who '
373
- + 'asked for them (initiator), and usage_totals: the token (and, where the CLI reports one, cost) figures each consultant reported this '
374
- + 'session, summed per consultant over every attempt, automatic re-runs included. A figure a CLI does not '
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 }) => ok({
379
- jobs: manager.list({ limit: limit ?? 20 }),
380
- usage_totals: manager.usageTotals(),
381
- limits: limitsSummary(),
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) process.stderr.write(`severally: config needs fixing; every consultation will be refused until it is -- ${problem}\n`);
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