severally-mcp 2.4.0 → 2.5.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 (50) hide show
  1. package/CONFIG.md +1 -0
  2. package/README.ja.md +44 -19
  3. package/README.md +44 -18
  4. package/config.example.json +1 -1
  5. package/docs/how-it-works.ja.md +2 -0
  6. package/docs/how-it-works.md +2 -0
  7. package/package.json +3 -1
  8. package/plugins/severally/.claude-plugin/plugin.json +1 -1
  9. package/plugins/severally/.codex-plugin/plugin.json +1 -1
  10. package/plugins/severally/dist/severally-mcp.mjs +630 -271
  11. package/plugins/severally/skills/_template/SKILL.md.tmpl +2 -1
  12. package/plugins/severally/skills/_template/reference.md.tmpl +2 -2
  13. package/plugins/severally/skills/antigravity/severally/SKILL.md +2 -1
  14. package/plugins/severally/skills/antigravity/severally/reference.md +2 -2
  15. package/plugins/severally/skills/claude/severally/SKILL.md +2 -1
  16. package/plugins/severally/skills/claude/severally/reference.md +2 -2
  17. package/plugins/severally/skills/codex/severally/SKILL.md +2 -1
  18. package/plugins/severally/skills/codex/severally/reference.md +2 -2
  19. package/plugins/severally/skills/opencode/severally/SKILL.md +2 -1
  20. package/plugins/severally/skills/opencode/severally/reference.md +2 -2
  21. package/scripts/build.mjs +57 -54
  22. package/scripts/doctor.mjs +15 -4
  23. package/scripts/init-config.mjs +2 -1
  24. package/scripts/install.mjs +73 -9
  25. package/scripts/live-check.mjs +11 -5
  26. package/scripts/live-mcp-check.mjs +10 -4
  27. package/src/adapters/antigravity-sandbox.mjs +24 -6
  28. package/src/adapters/antigravity.mjs +80 -17
  29. package/src/adapters/claude-code.mjs +49 -8
  30. package/src/adapters/codex.mjs +56 -6
  31. package/src/adapters/opencode-sandbox.mjs +20 -2
  32. package/src/adapters/opencode.mjs +115 -19
  33. package/src/brief.mjs +84 -7
  34. package/src/consultants.mjs +193 -27
  35. package/src/cross-spawn-escape.d.ts +7 -0
  36. package/src/export.mjs +50 -6
  37. package/src/expose-paths.mjs +149 -7
  38. package/src/failures.mjs +8 -1
  39. package/src/jobs.mjs +632 -152
  40. package/src/jsonc.mjs +11 -3
  41. package/src/parse-result.mjs +91 -14
  42. package/src/platform.mjs +52 -2
  43. package/src/policy.mjs +151 -28
  44. package/src/redact.mjs +22 -3
  45. package/src/result-schema.mjs +13 -0
  46. package/src/run.mjs +158 -12
  47. package/src/schema.mjs +84 -11
  48. package/src/server.mjs +49 -14
  49. package/src/status.mjs +16 -4
  50. package/src/store.mjs +287 -6
package/src/jsonc.mjs CHANGED
@@ -11,8 +11,12 @@
11
11
  // byte offsets -- and therefore JSON.parse's error positions -- still point at
12
12
  // the right place in the original text.
13
13
 
14
- /** Remove // and /* *\/ comments and trailing commas, preserving offsets. */
14
+ /**
15
+ * Remove // and /* *\/ comments and trailing commas, preserving offsets.
16
+ * @param {string} text
17
+ */
15
18
  export function stripJsonc(text) {
19
+ /** @type {string[]} */
16
20
  const out = [];
17
21
  let inString = false;
18
22
  let i = 0;
@@ -28,7 +32,7 @@ export function stripJsonc(text) {
28
32
  };
29
33
 
30
34
  while (i < text.length) {
31
- const c = text[i];
35
+ const c = text.charAt(i);
32
36
  const next = text[i + 1];
33
37
 
34
38
  if (inString) {
@@ -78,7 +82,11 @@ export function stripJsonc(text) {
78
82
  return out.join('');
79
83
  }
80
84
 
81
- /** JSON.parse, tolerating comments and trailing commas. */
85
+ /**
86
+ * JSON.parse, tolerating comments and trailing commas.
87
+ * @param {string} text
88
+ * @returns {unknown}
89
+ */
82
90
  export function parseJsonc(text) {
83
91
  return JSON.parse(stripJsonc(text));
84
92
  }
@@ -8,6 +8,10 @@ import { RESULT_KEYS } from './result-schema.mjs';
8
8
  import { redact } from './redact.mjs';
9
9
 
10
10
  export class OutputError extends Error {
11
+ /**
12
+ * @param {string} message
13
+ * @param {string} [detail]
14
+ */
11
15
  constructor(message, detail) {
12
16
  super(message);
13
17
  this.name = 'OutputError';
@@ -17,6 +21,38 @@ export class OutputError extends Error {
17
21
 
18
22
  const O = POLICY.output;
19
23
 
24
+ /** @typedef {'high' | 'medium' | 'low'} Level */
25
+ /** @typedef {'sufficient' | 'thin' | 'insufficient'} EvidenceBasis */
26
+ /** @typedef {(typeof STANCES)[number]} Stance */
27
+ /** @typedef {{ id: string, point: string, grounds: string | null, impact: string | null, severity: Level | null, confidence: Level | null }} Finding */
28
+ /** @typedef {{ option: string, tradeoffs: string | null, when_preferred: string | null }} Alternative */
29
+ /** @typedef {{ id: string, item: string, why_it_matters: string | null, how_to_obtain: string | null }} Unknown */
30
+ /** @typedef {{ condition: string, changes_to: string | null }} DecisionChanger */
31
+ /** @typedef {{ id: string, check: string, method: string | null, expected_signal: string | null }} NextCheck */
32
+ /** @typedef {{ topic: string, your_position: string | null, why_unresolved: string | null }} RemainingDisagreement */
33
+ /** @typedef {{ title: string | null, url: string | null, relevance: string | null }} Reference */
34
+
35
+ /**
36
+ * The validated consult result, in the shape of CONSULT_RESULT_SCHEMA.
37
+ * @typedef {object} ConsultResult
38
+ * @property {string} summary
39
+ * @property {Level | null} confidence
40
+ * @property {EvidenceBasis | null} evidence_basis
41
+ * @property {Stance | null} stance
42
+ * @property {Finding[]} findings
43
+ * @property {Alternative[]} alternatives
44
+ * @property {Unknown[]} unknowns
45
+ * @property {DecisionChanger[]} decision_changers
46
+ * @property {NextCheck[]} next_checks
47
+ * @property {RemainingDisagreement[]} remaining_disagreements
48
+ * @property {Reference[]} references
49
+ */
50
+
51
+ /**
52
+ * @param {unknown} v
53
+ * @param {number} max
54
+ * @returns {string | null}
55
+ */
20
56
  function clampText(v, max) {
21
57
  if (typeof v !== 'string') return null;
22
58
  const t = v.trim();
@@ -24,34 +60,61 @@ function clampText(v, max) {
24
60
  return t.length > max ? `${t.slice(0, max)}\n…[truncated by severally]` : t;
25
61
  }
26
62
 
63
+ /**
64
+ * Narrows a string to one of the listed values.
65
+ * @template {string} T
66
+ * @param {readonly T[]} list
67
+ * @param {string} t
68
+ * @returns {t is T}
69
+ */
70
+ const oneOf = (list, t) => list.some((x) => x === t);
71
+
72
+ /** @type {Level[]} */
27
73
  const LEVELS = ['high', 'medium', 'low'];
74
+ /** @type {EvidenceBasis[]} */
28
75
  const BASIS = ['sufficient', 'thin', 'insufficient'];
29
76
 
77
+ /**
78
+ * @param {unknown} v
79
+ * @param {Level | null} [fallback]
80
+ * @returns {Level | null}
81
+ */
30
82
  function level(v, fallback = null) {
31
83
  const t = typeof v === 'string' ? v.trim().toLowerCase() : '';
32
- return LEVELS.includes(t) ? t : fallback;
84
+ return oneOf(LEVELS, t) ? t : fallback;
33
85
  }
34
86
 
87
+ /**
88
+ * @param {unknown} v
89
+ * @returns {EvidenceBasis | null}
90
+ */
35
91
  function basis(v) {
36
92
  const t = typeof v === 'string' ? v.trim().toLowerCase() : '';
37
- return BASIS.includes(t) ? t : null;
93
+ return oneOf(BASIS, t) ? t : null;
38
94
  }
39
95
 
96
+ /**
97
+ * @param {unknown} v
98
+ * @returns {Stance | null}
99
+ */
40
100
  function stance(v) {
41
101
  const t = typeof v === 'string' ? v.trim().toLowerCase() : '';
42
- return STANCES.includes(t) ? t : null;
102
+ return oneOf(STANCES, t) ? t : null;
43
103
  }
44
104
 
45
- /** Extract a JSON object from raw model text (fenced block, or first balanced object). */
105
+ /**
106
+ * Extract a JSON object from raw model text (fenced block, or first balanced object).
107
+ * @param {unknown} text
108
+ * @returns {Record<string, unknown> | null}
109
+ */
46
110
  export function extractJson(text) {
47
111
  if (typeof text !== 'string') return null;
48
112
  const trimmed = text.trim();
49
113
  if (!trimmed) return null;
114
+ /** @type {string[]} */
50
115
  const candidates = [];
51
116
  if (trimmed.startsWith('{')) candidates.push(trimmed);
52
- const fence = /```(?:json)?\s*([\s\S]*?)```/gi;
53
- let m;
54
- while ((m = fence.exec(trimmed)) !== null) candidates.push(m[1].trim());
117
+ for (const [, body = ''] of trimmed.matchAll(/```(?:json)?\s*([\s\S]*?)```/gi)) candidates.push(body.trim());
55
118
  // Longest balanced {...} run as a last resort.
56
119
  const start = trimmed.indexOf('{');
57
120
  if (start !== -1) {
@@ -98,24 +161,34 @@ export function extractJson(text) {
98
161
  // verdict against one point later. The id is ours, assigned at parse time and
99
162
  // never read from the answer: a consultant that numbered its own findings
100
163
  // cannot make one of them impersonate another.
164
+ /**
165
+ * @template T
166
+ * @param {unknown} raw
167
+ * @param {Record<string, 'text' | 'level'>} fields
168
+ * @param {{ requiredField: string, idPrefix?: string | null }} options
169
+ * @returns {T[]}
170
+ */
101
171
  function mapList(raw, fields, { requiredField, idPrefix = null }) {
102
172
  if (!Array.isArray(raw)) return [];
173
+ /** @type {T[]} */
103
174
  const out = [];
104
175
  for (const entry of raw.slice(0, O.listMax)) {
105
176
  if (!entry || typeof entry !== 'object') continue;
177
+ /** @type {Record<string, unknown>} */
106
178
  const item = idPrefix ? { id: `${idPrefix}${out.length + 1}` } : {};
107
179
  for (const [key, kind] of Object.entries(fields)) {
108
180
  item[key] = kind === 'level' ? level(entry[key]) : clampText(entry[key], O.itemTextMax);
109
181
  }
110
182
  if (!item[requiredField]) continue;
111
- out.push(item);
183
+ // Every key of `fields` is a key of T, and the required one is set.
184
+ out.push(/** @type {T} */ (item));
112
185
  }
113
186
  return out;
114
187
  }
115
188
 
116
189
  /**
117
- * @param {object|string} raw parsed JSON object, or raw text to mine one out of
118
- * @returns {{result: object, quality: {missing_sections: string[], findings_without_grounds: number, unsourced: boolean, list_truncated?: Record<string, number>}}}
190
+ * @param {Record<string, unknown> | string} raw parsed JSON object, or raw text to mine one out of
191
+ * @returns {{result: ConsultResult, quality: {missing_sections: string[], findings_without_grounds: number, unsourced: boolean, list_truncated?: Record<string, number>}}}
119
192
  */
120
193
  export function normalizeResult(raw) {
121
194
  const objIn = typeof raw === 'string' ? extractJson(raw) : raw;
@@ -129,6 +202,7 @@ export function normalizeResult(raw) {
129
202
  throw new OutputError('consultant response has no usable "summary" field', JSON.stringify(src).slice(0, 2000));
130
203
  }
131
204
 
205
+ /** @type {ConsultResult} */
132
206
  const result = {
133
207
  summary,
134
208
  confidence: level(src.confidence),
@@ -202,13 +276,16 @@ export function normalizeResult(raw) {
202
276
  // The lists are cut to O.listMax before anything else looks at them; say how
203
277
  // long the ones that were cut had been, so the cut is not silent.
204
278
  const listTruncated = Object.fromEntries(
205
- ['findings', 'alternatives', 'unknowns', 'decision_changers', 'next_checks', 'remaining_disagreements']
206
- .filter((k) => Array.isArray(src[k]) && src[k].length > O.listMax)
207
- .map((k) => [k, src[k].length]),
279
+ ['findings', 'alternatives', 'unknowns', 'decision_changers', 'next_checks', 'remaining_disagreements'].flatMap((k) => {
280
+ const v = src[k];
281
+ return Array.isArray(v) && v.length > O.listMax ? [[k, v.length]] : [];
282
+ }),
208
283
  );
209
284
 
285
+ /** @type {Record<string, unknown>} */
286
+ const fields = result;
210
287
  const missing = RESULT_KEYS.filter((k) => {
211
- const v = result[k];
288
+ const v = fields[k];
212
289
  return v === null || v === undefined || (Array.isArray(v) && v.length === 0);
213
290
  });
214
291
 
package/src/platform.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ /// <reference path="./cross-spawn-escape.d.ts" />
1
2
  import fs from 'node:fs';
2
3
  import path from 'node:path';
3
4
  import which from 'which';
@@ -5,12 +6,24 @@ import spawn from 'cross-spawn';
5
6
  import escape from 'cross-spawn/lib/util/escape.js';
6
7
  import { spawn as nativeSpawn, spawnSync as nativeSpawnSync } from 'node:child_process';
7
8
 
9
+ /**
10
+ * @param {string} source
11
+ * @param {string} destination
12
+ */
8
13
  export function copyTree(source, destination) {
9
14
  // Node 22.23.2's native cpSync fast path corrupts non-ASCII Windows paths.
10
15
  // A filter selects its JS traversal, preserving Japanese directory names.
11
16
  fs.cpSync(source, destination, { recursive: true, filter: () => true });
12
17
  }
13
18
 
19
+ /** @typedef {import('node:child_process').SpawnOptions & { cwd?: string | undefined, env?: NodeJS.ProcessEnv | undefined }} SpawnOpts */
20
+
21
+ /**
22
+ * @template {SpawnOpts} O
23
+ * @param {string} command
24
+ * @param {readonly string[]} args
25
+ * @param {O} options
26
+ */
14
27
  function batchInvocation(command, args, options) {
15
28
  if (process.platform !== 'win32') return null;
16
29
  const resolved = findCommand(command, options);
@@ -26,17 +39,44 @@ function batchInvocation(command, args, options) {
26
39
  };
27
40
  }
28
41
 
42
+ /**
43
+ * @overload
44
+ * @param {string} command
45
+ * @param {readonly string[]} args
46
+ * @param {SpawnOpts & { stdio: ['pipe', 'pipe', 'pipe'] }} options
47
+ * @returns {import('node:child_process').ChildProcessWithoutNullStreams}
48
+ */
49
+ /**
50
+ * @overload
51
+ * @param {string} command
52
+ * @param {readonly string[]} args
53
+ * @param {SpawnOpts} [options]
54
+ * @returns {import('node:child_process').ChildProcess}
55
+ */
56
+ /**
57
+ * @param {string} command
58
+ * @param {readonly string[]} args
59
+ * @param {SpawnOpts} [options]
60
+ */
29
61
  export function spawnCommand(command, args, options = {}) {
30
62
  const batch = batchInvocation(command, args, options);
31
63
  return batch ? nativeSpawn(batch.command, batch.args, batch.options) : spawn(command, args, options);
32
64
  }
33
65
 
34
66
  // Explicit script paths are supported by cross-spawn's shebang handling too.
67
+ /**
68
+ * @param {string} command
69
+ * @param {{ cwd?: string | undefined, env?: NodeJS.ProcessEnv | undefined }} [options]
70
+ * @returns {string | null}
71
+ */
35
72
  export function findCommand(command, { cwd = process.cwd(), env = process.env } = {}) {
36
73
  if (typeof command !== 'string' || !command) return null;
37
74
  cwd = path.resolve(cwd);
38
75
  const windows = process.platform === 'win32';
39
- const value = (name) => env[windows ? Object.keys(env).find((key) => key.toUpperCase() === name) : name];
76
+ const value = (/** @type {string} */ name) => {
77
+ const key = windows ? Object.keys(env).find((k) => k.toUpperCase() === name) : name;
78
+ return key === undefined ? undefined : env[key];
79
+ };
40
80
  const explicit = command.includes('/') || (windows && command.includes('\\'));
41
81
  const dirs = explicit ? [cwd] : [...(windows ? [cwd] : []), ...(value('PATH') ?? '').split(path.delimiter).filter(Boolean)];
42
82
  for (const dir of dirs) {
@@ -59,8 +99,18 @@ export function findCommand(command, { cwd = process.cwd(), env = process.env }
59
99
  return null;
60
100
  }
61
101
 
102
+ /** @typedef {Omit<import('node:child_process').SpawnSyncOptionsWithStringEncoding, 'cwd' | 'encoding'> & { cwd?: string, encoding?: BufferEncoding }} ExecOptions */
103
+
104
+ /**
105
+ * @param {string} command
106
+ * @param {readonly string[]} args
107
+ * @param {ExecOptions} [options]
108
+ * @returns {string}
109
+ */
62
110
  export function execCommandSync(command, args, options = {}) {
63
- const opts = { windowsHide: true, encoding: 'utf8', ...options };
111
+ /** @type {BufferEncoding} */
112
+ const encoding = 'utf8';
113
+ const opts = { windowsHide: true, encoding, ...options };
64
114
  const batch = batchInvocation(command, args, opts);
65
115
  const result = batch ? nativeSpawnSync(batch.command, batch.args, batch.options) : spawn.sync(command, args, opts);
66
116
  if (result.error) throw result.error;