severally-mcp 2.3.1 → 2.4.2

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 (45) hide show
  1. package/README.ja.md +52 -17
  2. package/README.md +52 -16
  3. package/bin/severally-mcp.mjs +14 -2
  4. package/docs/install.ja.md +48 -20
  5. package/docs/install.md +48 -21
  6. package/package.json +15 -3
  7. package/plugins/severally/.claude-plugin/plugin.json +1 -1
  8. package/plugins/severally/.codex-plugin/plugin.json +1 -1
  9. package/plugins/severally/dist/severally-mcp.mjs +6573 -5978
  10. package/plugins/severally/skills/_template/reference.md.tmpl +2 -2
  11. package/plugins/severally/skills/antigravity/severally/reference.md +2 -2
  12. package/plugins/severally/skills/claude/severally/reference.md +2 -2
  13. package/plugins/severally/skills/codex/severally/reference.md +2 -2
  14. package/plugins/severally/skills/opencode/severally/reference.md +2 -2
  15. package/scripts/build.mjs +76 -68
  16. package/scripts/doctor.mjs +66 -20
  17. package/scripts/init-config.mjs +8 -6
  18. package/scripts/install.mjs +327 -90
  19. package/scripts/live-check.mjs +62 -23
  20. package/scripts/live-mcp-check.mjs +52 -16
  21. package/scripts/test.mjs +10 -4
  22. package/src/adapters/antigravity-sandbox.mjs +62 -28
  23. package/src/adapters/antigravity.mjs +111 -30
  24. package/src/adapters/claude-code.mjs +121 -28
  25. package/src/adapters/codex.mjs +125 -33
  26. package/src/adapters/opencode-sandbox.mjs +59 -22
  27. package/src/adapters/opencode.mjs +144 -30
  28. package/src/brief.mjs +148 -47
  29. package/src/consultants.mjs +255 -48
  30. package/src/cross-spawn-escape.d.ts +7 -0
  31. package/src/export.mjs +99 -18
  32. package/src/expose-paths.mjs +310 -50
  33. package/src/failures.mjs +31 -14
  34. package/src/jobs.mjs +863 -249
  35. package/src/jsonc.mjs +11 -3
  36. package/src/parse-result.mjs +172 -40
  37. package/src/platform.mjs +56 -8
  38. package/src/policy.mjs +245 -52
  39. package/src/redact.mjs +59 -16
  40. package/src/result-schema.mjs +13 -0
  41. package/src/run.mjs +225 -30
  42. package/src/schema.mjs +132 -27
  43. package/src/server.mjs +178 -81
  44. package/src/status.mjs +94 -13
  45. package/src/store.mjs +418 -35
package/src/schema.mjs CHANGED
@@ -5,12 +5,27 @@
5
5
  import { z } from 'zod';
6
6
  import path from 'node:path';
7
7
  import {
8
- POLICY, CALLER_INPUTS, MODES, STANCES, artifactKinds,
9
- resolveCaller, availableConsultants, consultantAvailable, unavailableReason, brokenConsultant,
8
+ POLICY,
9
+ CALLER_INPUTS,
10
+ MODES,
11
+ STANCES,
12
+ artifactKinds,
13
+ resolveCaller,
14
+ availableConsultants,
15
+ consultantAvailable,
16
+ unavailableReason,
17
+ brokenConsultant,
10
18
  } from './policy.mjs';
11
19
  import { normalizeName, resolveConsultant } from './consultants.mjs';
12
20
 
21
+ /** @typedef {import('./consultants.mjs').Consultant} Consultant */
22
+ /** @typedef {import('./consultants.mjs').CliId} CliId */
23
+
13
24
  const L = POLICY.input;
25
+ /**
26
+ * @param {number} max
27
+ * @param {string} label
28
+ */
14
29
  const trimmed = (max, label) =>
15
30
  z.string().trim().min(1, `${label} must not be empty`).max(max, `${label} exceeds ${max} characters`);
16
31
 
@@ -36,7 +51,11 @@ const exposedPathSchema = z
36
51
  .refine((p) => path.isAbsolute(p), { message: 'context.expose_paths[] must be an absolute path' });
37
52
 
38
53
  // A revision for `git diff`: a ref, a commit, or HEAD~n -- never an option.
39
- const gitRevision = z.string().trim().min(1).max(200)
54
+ const gitRevision = z
55
+ .string()
56
+ .trim()
57
+ .min(1)
58
+ .max(200)
40
59
  .regex(/^[A-Za-z0-9._/~^@{}-]+$/, 'context.expose_diff.base must be a git revision (a branch, tag, commit or HEAD~n)')
41
60
  .refine((r) => !r.startsWith('-'), { message: 'context.expose_diff.base must not start with "-"' });
42
61
 
@@ -65,7 +84,11 @@ export const contextSchema = z
65
84
  // for it, its CLI, its vendor, or its model ("glm", "claude opus"). Which
66
85
  // consultant that is depends on the operator's config, so it is resolved in
67
86
  // parseRequest, not by an enum here.
68
- const targetSpec = z.string().trim().min(1, 'target must not be empty').max(120, 'target exceeds 120 characters')
87
+ const targetSpec = z
88
+ .string()
89
+ .trim()
90
+ .min(1, 'target must not be empty')
91
+ .max(120, 'target exceeds 120 characters')
69
92
  .regex(/^[^\p{Cc}\p{Cf}]+$/u, 'target must be a single line of printable characters');
70
93
 
71
94
  // Who asked for a consultation, as the lead reports it.
@@ -128,10 +151,59 @@ export const requestSchema = z
128
151
  })
129
152
  .strict();
130
153
 
154
+ /**
155
+ * What charCount reads of a request, once parseRequest has filled in its defaults.
156
+ * @typedef {{
157
+ * question: string,
158
+ * objective?: string | null | undefined,
159
+ * constraints?: string[] | undefined,
160
+ * success_criteria?: string[] | undefined,
161
+ * context: { facts: string[], counterpoints: string[], proposal?: string | null | undefined, artifacts: { name: string, excerpt: string }[] },
162
+ * }} CountedRequest
163
+ */
164
+
165
+ /** @typedef {'explore' | 'review' | 'debate'} Mode */
166
+ /** @typedef {z.output<typeof requestSchema>} ValidRequest */
167
+ /** @typedef {z.output<typeof contextSchema>} ValidContext */
168
+
169
+ /**
170
+ * A request as parseRequest returns it: validated, defaults filled in, the
171
+ * consultant(s) resolved. `target` is the first of `targets`.
172
+ * @typedef {Omit<
173
+ * ValidRequest,
174
+ * 'target' | 'targets' | 'caller' | 'caller_model' | 'initiator' | 'followup_to' | 'mode' | 'network' | 'success_criteria' | 'constraints' | 'context'
175
+ * > & {
176
+ * target: string,
177
+ * targets: string[],
178
+ * fanout: boolean,
179
+ * caller: CliId | null,
180
+ * caller_model: string | null,
181
+ * initiator: string | null,
182
+ * followup_to: string | null,
183
+ * mode: Mode,
184
+ * network: 'on' | 'off',
185
+ * success_criteria: string[],
186
+ * constraints: string[],
187
+ * context: Omit<ValidContext, 'constraints' | 'success_criteria' | 'facts' | 'counterpoints' | 'artifacts' | 'proposal'> & {
188
+ * facts: string[],
189
+ * counterpoints: string[],
190
+ * artifacts: ValidContext['artifacts'] & {},
191
+ * proposal: string | null,
192
+ * },
193
+ * }} ParsedRequest
194
+ */
195
+
196
+ /**
197
+ * parseRequest's working copy: the request as it is until the defaults are filled in.
198
+ * `constraints` and `success_criteria` may still sit inside the context at first.
199
+ * @typedef {ParsedRequest & { context: { constraints?: string[] | undefined, success_criteria?: string[] | undefined } }} DraftRequest
200
+ */
201
+
202
+ /** @param {CountedRequest} req */
131
203
  export function charCount(req) {
132
204
  let n = req.question.length + (req.objective?.length ?? 0);
133
- for (const c of req.constraints) n += c.length;
134
- for (const c of req.success_criteria) n += c.length;
205
+ for (const c of req.constraints ?? []) n += c.length;
206
+ for (const c of req.success_criteria ?? []) n += c.length;
135
207
  for (const f of req.context.facts) n += f.length;
136
208
  for (const c of req.context.counterpoints) n += c.length;
137
209
  if (req.context.proposal) n += req.context.proposal.length;
@@ -140,6 +212,11 @@ export function charCount(req) {
140
212
  }
141
213
 
142
214
  export class RequestError extends Error {
215
+ /**
216
+ * @param {string} message
217
+ * @param {string} [code]
218
+ * @param {unknown[]} [details]
219
+ */
143
220
  constructor(message, code = 'invalid_request', details = undefined) {
144
221
  super(message);
145
222
  this.name = 'RequestError';
@@ -152,6 +229,9 @@ export class RequestError extends Error {
152
229
  * Validate + normalise a consult request.
153
230
  * `isFollowup` is decided by the caller (the job store resolves followup_to first);
154
231
  * `groupFollowup` when followup_to named a fan-out, whose members are the targets.
232
+ * @param {unknown} raw
233
+ * @param {{ isFollowup?: boolean, parentMode?: Mode | null, groupFollowup?: boolean }} [options]
234
+ * @returns {ParsedRequest}
155
235
  */
156
236
  export function parseRequest(raw, { isFollowup = false, parentMode = null, groupFollowup = false } = {}) {
157
237
  const parsed = requestSchema.safeParse(raw);
@@ -159,46 +239,62 @@ export function parseRequest(raw, { isFollowup = false, parentMode = null, group
159
239
  const issues = parsed.error.issues.map((i) => `${i.path.join('.') || '(root)'}: ${i.message}`);
160
240
  throw new RequestError(`request failed validation: ${issues.join('; ')}`, 'invalid_request', issues);
161
241
  }
162
- const req = parsed.data;
242
+ // Filled in below with what the request resolves to (targets, caller, ...), so
243
+ // until it returns it is only partly a ParsedRequest: the validated data is
244
+ // typed as the finished shape and every field the shape adds is set before the return.
245
+ const req = /** @type {DraftRequest} */ (parsed.data);
163
246
  // Left without a default in the schema, so that a follow-up can tell a list
164
247
  // it left out (inherited from the round before, jobs.mjs) from one it
165
248
  // emptied on purpose: the tool layer applies the schema's defaults first.
166
- for (const k of ['constraints', 'success_criteria']) {
167
- if (req.context[k] === undefined) continue;
249
+ /** @param {'constraints' | 'success_criteria'} k */
250
+ const hoist = (k) => {
251
+ const inner = req.context[k];
252
+ if (inner === undefined) return;
168
253
  if (req[k] !== undefined) {
169
254
  throw new RequestError(`pass ${k} once: at the top of the request or inside context, not both`, 'invalid_request');
170
255
  }
171
- req[k] = req.context[k];
256
+ req[k] = inner;
172
257
  delete req.context[k];
173
- }
258
+ };
259
+ hoist('constraints');
260
+ hoist('success_criteria');
174
261
  req.network ??= 'on';
175
262
  req.success_criteria ??= [];
176
263
  req.constraints ??= [];
177
- for (const k of ['facts', 'counterpoints', 'artifacts']) req.context[k] ??= [];
264
+ req.context.facts ??= [];
265
+ req.context.counterpoints ??= [];
266
+ req.context.artifacts ??= [];
178
267
  const hasSingle = req.target !== undefined && req.target !== null;
179
268
  const hasMany = Array.isArray(req.targets) && req.targets.length > 0;
180
269
  if (hasSingle && hasMany) {
181
270
  throw new RequestError('pass either target (one consultant) or targets (several), not both', 'invalid_request');
182
271
  }
183
- const listed = () => availableConsultants().map((c) => `${c.name} (${c.cli}, ${c.model})`).join(', ')
184
- || 'none -- no consultant CLI is available here';
272
+ const listed = () =>
273
+ availableConsultants()
274
+ .map((c) => `${c.name} (${c.cli}, ${c.model})`)
275
+ .join(', ') || 'none -- no consultant CLI is available here';
185
276
  if (!hasSingle && !hasMany) {
186
277
  throw new RequestError(
187
278
  `name the consultant: target: a consultant, its CLI or its model (available: ${listed()}), or targets: [...] to ask several the same question, targets: ["all"] to ask everyone`,
188
279
  'target_required',
189
280
  );
190
281
  }
282
+ /** @type {string[]} */
191
283
  const named = hasSingle ? [req.target] : req.targets;
192
284
  const isAll = named.map((n) => normalizeName(n) === 'all');
193
285
  if (isAll.some(Boolean) && named.length > 1) {
194
286
  throw new RequestError('"all" already means every available consultant; pass it alone', 'all_mixed');
195
287
  }
196
288
 
289
+ /** @type {Consultant[]} */
197
290
  let chosen;
198
291
  if (isAll[0]) {
199
292
  chosen = availableConsultants();
200
293
  if (!chosen.length) {
201
- throw new RequestError('no consultant is available on this machine -- install one of the CLIs, or check the config file', 'target_unavailable');
294
+ throw new RequestError(
295
+ 'no consultant is available on this machine -- install one of the CLIs, or check the config file',
296
+ 'target_unavailable',
297
+ );
202
298
  }
203
299
  } else {
204
300
  // Resolved among the reachable consultants first, so one that is off does
@@ -211,15 +307,18 @@ export function parseRequest(raw, { isFollowup = false, parentMode = null, group
211
307
  let r = resolveConsultant(name, reachable, POLICY.ranking);
212
308
  if (r.none) r = resolveConsultant(name, POLICY.consultants, POLICY.ranking);
213
309
  if (r.none && brokenConsultant(name)) {
214
- throw new RequestError(`consultant ${JSON.stringify(name)} is not available: ${brokenConsultant(name)}; available: ${listed()}`, 'target_unavailable');
310
+ throw new RequestError(
311
+ `consultant ${JSON.stringify(name)} is not available: ${brokenConsultant(name)}; available: ${listed()}`,
312
+ 'target_unavailable',
313
+ );
215
314
  }
216
315
  if (r.none) {
217
316
  throw new RequestError(`no consultant answers to ${JSON.stringify(name)}; available: ${listed()}`, 'unknown_target');
218
317
  }
219
318
  if (r.ambiguous) {
220
319
  throw new RequestError(
221
- `${JSON.stringify(name)} fits more than one consultant and no ranking rule decides between `
222
- + `${r.ambiguous.map((c) => `${c.name} (${c.cli}, ${c.model})`).join(', ')}; name one of them`,
320
+ `${JSON.stringify(name)} fits more than one consultant and no ranking rule decides between ` +
321
+ `${r.ambiguous.map((c) => `${c.name} (${c.cli}, ${c.model})`).join(', ')}; name one of them`,
223
322
  'target_ambiguous',
224
323
  );
225
324
  }
@@ -228,8 +327,8 @@ export function parseRequest(raw, { isFollowup = false, parentMode = null, group
228
327
  const off = [...new Set(chosen.filter((c) => !consultantAvailable(c)))];
229
328
  if (off.length) {
230
329
  throw new RequestError(
231
- `consultant ${off.map((c) => `${JSON.stringify(c.name)} (${unavailableReason(c)})`).join(', ')} `
232
- + `is not available on this machine; available: ${listed()}`,
330
+ `consultant ${off.map((c) => `${JSON.stringify(c.name)} (${unavailableReason(c)})`).join(', ')} ` +
331
+ `is not available on this machine; available: ${listed()}`,
233
332
  'target_unavailable',
234
333
  );
235
334
  }
@@ -249,7 +348,7 @@ export function parseRequest(raw, { isFollowup = false, parentMode = null, group
249
348
  );
250
349
  }
251
350
  req.targets = unique;
252
- req.target = unique[0]; // single-target consumers (brief, history, adapters) keep working
351
+ req.target = /** @type {string} */ (unique[0]); // non-empty: every name in `named` resolved to a consultant; single-target consumers (brief, history, adapters) keep working
253
352
  req.fanout = unique.length > 1;
254
353
  req.caller = resolveCaller(req.caller ?? '') ?? null;
255
354
  req.caller_model = req.caller_model ?? null;
@@ -261,8 +360,7 @@ export function parseRequest(raw, { isFollowup = false, parentMode = null, group
261
360
  // one its context fits -- no proposal is explore, a proposal is review, a
262
361
  // proposal with the other side's claims is debate. Checked below like a
263
362
  // mode that was given.
264
- req.mode ??= parentMode
265
- ?? (!req.context.proposal ? 'explore' : req.context.counterpoints.length ? 'debate' : 'review');
363
+ req.mode ??= parentMode ?? (!req.context.proposal ? 'explore' : req.context.counterpoints.length ? 'debate' : 'review');
266
364
 
267
365
  const total = charCount(req);
268
366
  if (total > L.totalCharsMax) {
@@ -296,11 +394,18 @@ export function parseRequest(raw, { isFollowup = false, parentMode = null, group
296
394
  'expose_from_parent_needs_followup',
297
395
  );
298
396
  }
299
- if (req.context.facts.length === 0 && req.context.artifacts.length === 0 && req.context.expose_paths.length === 0
300
- && !req.context.expose_diff && !req.context.expose_from_parent) {
397
+ if (
398
+ req.context.facts.length === 0 &&
399
+ req.context.artifacts.length === 0 &&
400
+ req.context.expose_paths.length === 0 &&
401
+ !req.context.expose_diff &&
402
+ !req.context.expose_from_parent
403
+ ) {
301
404
  throw new RequestError(
302
- 'provide at least one entry in context.facts, context.artifacts or context.expose_paths: the consultant starts in an empty working directory and is not told where your repository is, so every fact it needs must be in the request'
303
- + (isFollowup ? '; on a follow-up, context.expose_from_parent: true copies again the files the earlier round was shown' : ''),
405
+ 'provide at least one entry in context.facts, context.artifacts or context.expose_paths: the consultant starts in an empty working directory and is not told where your repository is, so every fact it needs must be in the request' +
406
+ (isFollowup
407
+ ? '; on a follow-up, context.expose_from_parent: true copies again the files the earlier round was shown'
408
+ : ''),
304
409
  'context_required',
305
410
  );
306
411
  }