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/export.mjs CHANGED
@@ -13,7 +13,15 @@ import path from 'node:path';
13
13
  import { POLICY } from './policy.mjs';
14
14
  import { humanSize } from './expose-paths.mjs';
15
15
 
16
+ /** @typedef {import('./store.mjs').Round} Round A round parsed from the history files. */
17
+ /** @typedef {import('./store.mjs').BriefRecord} BriefRecord */
18
+ /** @typedef {import('./store.mjs').JobRecord} JobRecord */
19
+
16
20
  export class ExportError extends Error {
21
+ /**
22
+ * @param {string} message
23
+ * @param {string} code
24
+ */
17
25
  constructor(message, code) {
18
26
  super(message);
19
27
  this.name = 'ExportError';
@@ -23,14 +31,28 @@ export class ExportError extends Error {
23
31
 
24
32
  const historyDir = () => path.join(POLICY.home, 'history');
25
33
 
34
+ /** @param {string} p */
26
35
  function readJson(p) {
27
- try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return null; }
36
+ try {
37
+ return JSON.parse(fs.readFileSync(p, 'utf8'));
38
+ } catch {
39
+ return null;
40
+ }
28
41
  }
29
42
 
43
+ /**
44
+ * @param {string} chainId
45
+ * @returns {Round[]}
46
+ */
30
47
  function roundsOf(chainId) {
31
48
  const dir = path.join(historyDir(), chainId);
49
+ /** @type {string[]} */
32
50
  let names = [];
33
- try { names = fs.readdirSync(dir); } catch { return []; }
51
+ try {
52
+ names = fs.readdirSync(dir);
53
+ } catch {
54
+ return [];
55
+ }
34
56
  return names
35
57
  .filter((n) => /^round-\d+\.json$/.test(n))
36
58
  .sort()
@@ -41,6 +63,10 @@ function roundsOf(chainId) {
41
63
  // A fan-out is one question sent to several consultants, and each consultant
42
64
  // answers on its own chain. Nothing in the group file records which chain that
43
65
  // was, so the index -- one line per consultation -- is what maps them back.
66
+ /**
67
+ * @param {string} groupId
68
+ * @returns {string[]}
69
+ */
44
70
  function chainsOfGroup(groupId) {
45
71
  const group = readJson(path.join(historyDir(), 'groups', `${groupId}.json`));
46
72
  if (!group) throw new ExportError(`no fan-out with group_id "${groupId}" in ~/.severally/history`, 'unknown_group');
@@ -51,7 +77,10 @@ function chainsOfGroup(groupId) {
51
77
  const row = JSON.parse(line);
52
78
  byJob.set(row.job_id, row.chain_id);
53
79
  }
54
- } catch { /* an unreadable index leaves the group unresolvable, handled below */ }
80
+ } catch {
81
+ /* an unreadable index leaves the group unresolvable, handled below */
82
+ }
83
+ /** @type {string[]} */
55
84
  const chains = [];
56
85
  for (const jobId of group.job_ids ?? []) {
57
86
  const chainId = byJob.get(jobId);
@@ -63,16 +92,30 @@ function chainsOfGroup(groupId) {
63
92
  return chains;
64
93
  }
65
94
 
95
+ /**
96
+ * @param {string[] | null | undefined} items
97
+ * @param {(item: string) => string} render
98
+ */
66
99
  const bullets = (items, render) => (items ?? []).map(render);
67
100
 
101
+ /**
102
+ * @param {string} text
103
+ * @param {string | null | undefined} language
104
+ */
68
105
  function fence(text, language) {
69
106
  const marker = String(text).includes('```') ? '~~~' : '```';
70
107
  return [`${marker}${language ?? ''}`, text, marker];
71
108
  }
72
109
 
110
+ /** @param {BriefRecord | null | undefined} brief */
73
111
  function briefSection(brief) {
74
112
  if (!brief) return ['*(the brief was not recorded for this round)*', ''];
113
+ /** @type {string[]} */
75
114
  const out = [];
115
+ /**
116
+ * @param {string} title
117
+ * @param {string | string[] | null | undefined} body
118
+ */
76
119
  const block = (title, body) => {
77
120
  if (!body || (Array.isArray(body) && body.length === 0)) return;
78
121
  out.push(`**${title}**`, '');
@@ -80,11 +123,23 @@ function briefSection(brief) {
80
123
  out.push('');
81
124
  };
82
125
  block('Objective', brief.objective);
83
- block('Success criteria', bullets(brief.success_criteria, (c) => `- ${c}`));
84
- block('Constraints', bullets(brief.constraints, (c) => `- ${c}`));
85
- block('Facts as given', bullets(brief.facts, (f) => `- ${f}`));
126
+ block(
127
+ 'Success criteria',
128
+ bullets(brief.success_criteria, (c) => `- ${c}`),
129
+ );
130
+ block(
131
+ 'Constraints',
132
+ bullets(brief.constraints, (c) => `- ${c}`),
133
+ );
134
+ block(
135
+ 'Facts as given',
136
+ bullets(brief.facts, (f) => `- ${f}`),
137
+ );
86
138
  block('Proposal', brief.proposal);
87
- block('Opposing claims', bullets(brief.counterpoints, (c) => `- ${c}`));
139
+ block(
140
+ 'Opposing claims',
141
+ bullets(brief.counterpoints, (c) => `- ${c}`),
142
+ );
88
143
  for (const a of brief.artifacts ?? []) {
89
144
  out.push(`**Material: ${a.name}** (${[a.kind, a.language, a.source].filter(Boolean).join(', ')})`, '');
90
145
  out.push(...fence(a.excerpt, a.language));
@@ -98,15 +153,22 @@ function briefSection(brief) {
98
153
  if (exposed?.entries?.length || exposed?.skipped?.length) {
99
154
  out.push('**Files exposed to the consultant**', '');
100
155
  for (const e of exposed.entries) {
101
- const since = e.changed_since_parent === undefined ? '' : e.changed_since_parent ? ', changed since the earlier round' : ', unchanged since the earlier round';
156
+ const since =
157
+ e.changed_since_parent === undefined
158
+ ? ''
159
+ : e.changed_since_parent
160
+ ? ', changed since the earlier round'
161
+ : ', unchanged since the earlier round';
102
162
  out.push(`- ${e.exposed_as} (${e.kind}, ${e.files} file(s), ${humanSize(e.bytes)}${since}) -- from ${e.source}`);
103
163
  }
164
+ /** @param {string} reason */
104
165
  const count = (reason) => (exposed.skipped ?? []).filter((x) => x.reason === reason).length;
105
166
  if (count('symlink')) out.push(`- ${count('symlink')} symlink(s) inside those directories were skipped, not followed`);
106
167
  if (count('protected')) out.push(`- ${count('protected')} credential or history path(s) were left out on purpose`);
107
168
  if (count('gone')) out.push(`- ${count('gone')} path(s) from the earlier round no longer existed and were not copied again`);
108
- if (count('masked')) out.push(`- ${count('masked')} path(s) from the earlier round were masked in its record and were not copied again`);
109
- if (count('replaced')) out.push('- the earlier round\'s change was replaced by the diff given in this round');
169
+ if (count('masked'))
170
+ out.push(`- ${count('masked')} path(s) from the earlier round were masked in its record and were not copied again`);
171
+ if (count('replaced')) out.push("- the earlier round's change was replaced by the diff given in this round");
110
172
  out.push('');
111
173
  }
112
174
  return out;
@@ -115,6 +177,10 @@ function briefSection(brief) {
115
177
  // The lead's own line, printed under the point it belongs to. An id with no
116
178
  // entry says so out loud: "nobody looked at this" is the thing a record of a
117
179
  // decision most easily loses.
180
+ /**
181
+ * @param {JobRecord | null | undefined} record
182
+ * @param {string} id
183
+ */
118
184
  function verdictLine(record, id) {
119
185
  const entry = (record?.entries ?? []).find((e) => e.id === id);
120
186
  if (!entry) return ' - checked: no verdict recorded';
@@ -122,6 +188,7 @@ function verdictLine(record, id) {
122
188
  return ` - checked: ${entry.verdict}${tail ? ` -- ${tail}` : ''}`;
123
189
  }
124
190
 
191
+ /** @param {Round} round */
125
192
  function answerSection(round) {
126
193
  const r = round.result;
127
194
  if (!r) {
@@ -132,6 +199,7 @@ function answerSection(round) {
132
199
  '',
133
200
  ];
134
201
  }
202
+ /** @type {string[]} */
135
203
  const out = [];
136
204
  const record = round.record;
137
205
  out.push('**Summary**', '', r.summary, '');
@@ -199,6 +267,10 @@ function answerSection(round) {
199
267
  // A fan-out sends the byte-identical brief to every consultant, so printing it
200
268
  // once per consultant buries the answers in repetition. Compared, not judged:
201
269
  // identical text is named as identical, anything else is printed in full.
270
+ /**
271
+ * @param {Round} round
272
+ * @param {BriefRecord | null} previousBrief
273
+ */
202
274
  function roundSection(round, previousBrief) {
203
275
  const secs = round.duration_ms == null ? null : (round.duration_ms / 1000).toFixed(1);
204
276
  const head = [
@@ -209,7 +281,9 @@ function roundSection(round, previousBrief) {
209
281
  ];
210
282
  if (round.result) {
211
283
  head.push(`- stance as declared: ${round.result.stance ?? 'not declared'}`);
212
- head.push(`- the consultant's own confidence: ${round.result.confidence ?? 'not declared'}, evidence basis: ${round.result.evidence_basis ?? 'not declared'}`);
284
+ head.push(
285
+ `- the consultant's own confidence: ${round.result.confidence ?? 'not declared'}, evidence basis: ${round.result.evidence_basis ?? 'not declared'}`,
286
+ );
213
287
  }
214
288
  head.push('');
215
289
  head.push('### Question', '', round.question, '');
@@ -239,21 +313,20 @@ function roundSection(round, previousBrief) {
239
313
  }
240
314
 
241
315
  /**
242
- * @param {{chain_id?: string, group_id?: string}} target
316
+ * @param {{chain_id?: string | undefined, group_id?: string | undefined}} target
243
317
  * @returns {{markdown: string, rounds: number, chain_ids: string[]}}
244
318
  */
245
319
  export function exportChain({ chain_id: chainId, group_id: groupId } = {}) {
246
320
  if (Boolean(chainId) === Boolean(groupId)) {
247
321
  throw new ExportError('pass exactly one of chain_id or group_id', 'export_target_required');
248
322
  }
249
- const chainIds = groupId ? chainsOfGroup(groupId) : [chainId];
323
+ const chainIds = groupId ? chainsOfGroup(groupId) : chainId ? [chainId] : [];
250
324
  const perChain = chainIds.map((id) => ({ chain_id: id, rounds: roundsOf(id) }));
251
- const total = perChain.reduce((n, c) => n + c.rounds.length, 0);
252
- if (total === 0) {
325
+ const first = perChain.find((c) => c.rounds.length)?.rounds[0];
326
+ if (!first) {
253
327
  throw new ExportError(`no consultation recorded under "${chainId ?? groupId}" in ~/.severally/history`, 'unknown_chain');
254
328
  }
255
-
256
- const first = perChain.find((c) => c.rounds.length)?.rounds[0];
329
+ const total = perChain.reduce((n, c) => n + c.rounds.length, 0);
257
330
  const lines = [
258
331
  `# Consultation record: ${first.question}`,
259
332
  '',
@@ -266,6 +339,7 @@ export function exportChain({ chain_id: chainId, group_id: groupId } = {}) {
266
339
  'ways, both are printed and the difference is left standing.',
267
340
  '',
268
341
  ];
342
+ /** @type {BriefRecord | null} */
269
343
  let previousBrief = null;
270
344
  for (const chain of perChain) {
271
345
  for (const round of chain.rounds) {
@@ -273,5 +347,12 @@ export function exportChain({ chain_id: chainId, group_id: groupId } = {}) {
273
347
  if (round.brief) previousBrief = round.brief;
274
348
  }
275
349
  }
276
- return { markdown: `${lines.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd()}\n`, rounds: total, chain_ids: chainIds };
350
+ return {
351
+ markdown: `${lines
352
+ .join('\n')
353
+ .replace(/\n{3,}/g, '\n\n')
354
+ .trimEnd()}\n`,
355
+ rounds: total,
356
+ chain_ids: chainIds,
357
+ };
277
358
  }