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/brief.mjs CHANGED
@@ -7,6 +7,52 @@ import { CONSULT_RESULT_SCHEMA } from './result-schema.mjs';
7
7
  import { redact } from './redact.mjs';
8
8
  import { humanSize } from './expose-paths.mjs';
9
9
 
10
+ /**
11
+ * @typedef {object} BriefArtifact
12
+ * @property {string} name
13
+ * @property {string} kind
14
+ * @property {string | undefined} [language]
15
+ * @property {string | undefined} [source]
16
+ * @property {string} excerpt
17
+ */
18
+
19
+ /**
20
+ * The validated request, as far as the brief reads it.
21
+ * @typedef {object} BriefRequest
22
+ * @property {string} question
23
+ * @property {string | null | undefined} [objective]
24
+ * @property {string[]} [success_criteria]
25
+ * @property {string[]} [constraints]
26
+ * @property {'explore' | 'review' | 'debate'} mode
27
+ * @property {'on' | 'off'} [network]
28
+ * @property {{ facts?: string[], proposal?: string | null, counterpoints?: string[], artifacts: BriefArtifact[] }} context
29
+ */
30
+
31
+ /** @typedef {import('./expose-paths.mjs').ExposeManifest} ExposeManifest */
32
+
33
+ /**
34
+ * A finding, unknown or check from an earlier round, with the lead's verdict on it.
35
+ * @typedef {object} PriorPoint
36
+ * @property {string} id
37
+ * @property {string} point
38
+ * @property {string | null} [grounds]
39
+ * @property {string | null} [verdict]
40
+ * @property {string | null} [effect]
41
+ */
42
+
43
+ /**
44
+ * @typedef {object} PriorRound
45
+ * @property {number} round
46
+ * @property {string} mode
47
+ * @property {string} question
48
+ * @property {string} summary
49
+ * @property {PriorPoint[]} [findings]
50
+ * @property {PriorPoint[]} [unknowns]
51
+ * @property {PriorPoint[]} [next_checks]
52
+ */
53
+
54
+ /** @typedef {{ round: number, priorRounds: PriorRound[] }} BriefChain */
55
+
10
56
  const MODE_BRIEFS = {
11
57
  explore: [
12
58
  'MODE: explore.',
@@ -38,22 +84,23 @@ const GUARDRAILS_HEAD = [
38
84
 
39
85
  // After the web bullet.
40
86
  const GUARDRAILS_RULES = [
41
- '- When the question concerns security, it is a defensive review of the lead\'s own code or system: analyse the weaknesses and how to fix them.',
42
- '- The Constraints section is fixed. Design notes, drafts and internal documents among the facts and artifacts are the lead\'s current direction, not a specification, and you may question them.',
87
+ "- When the question concerns security, it is a defensive review of the lead's own code or system: analyse the weaknesses and how to fix them.",
88
+ "- The Constraints section is fixed. Design notes, drafts and internal documents among the facts and artifacts are the lead's current direction, not a specification, and you may question them.",
43
89
  ];
44
90
 
45
91
  // The web bullet follows the request: with the network off the consultant has
46
92
  // no web tools, and telling it otherwise would have it look for them.
47
93
  const WEB_ON = '- You may search and browse the web freely; cite what you actually opened in `references`.';
48
- const WEB_OFF = '- Web search and browsing are switched off for this consultation. Answer from the brief, and record what you would have looked up under `unknowns`.';
94
+ const WEB_OFF =
95
+ '- Web search and browsing are switched off for this consultation. Answer from the brief, and record what you would have looked up under `unknowns`.';
49
96
 
50
97
  // The one bullet that depends on the request: a consultation that exposed
51
98
  // paths has files, and telling it otherwise would have it record everything
52
99
  // it can plainly read under `unknowns`.
53
100
  const WORKDIR_EMPTY =
54
- '- You are running in an empty working directory and do not have the lead\'s repository; everything you are meant to have is in the brief below. If something is missing, record it under `unknowns` instead of guessing or substituting an assumption.';
101
+ "- You are running in an empty working directory and do not have the lead's repository; everything you are meant to have is in the brief below. If something is missing, record it under `unknowns` instead of guessing or substituting an assumption.";
55
102
  const WORKDIR_EXPOSED =
56
- '- Your working directory is not empty: the lead exposed specific paths, listed under "Files available to you" below and copied read-only under ./workspace. Those are the only part of the lead\'s repository you have. If something you need is missing, record it under `unknowns` instead of guessing or substituting an assumption.';
103
+ '- Your working directory is not empty: the lead exposed specific paths, listed under "Files available to you" below and copied read-only under ./workspace. Those are the only part of the lead\'s repository the lead gave you. If something you need is missing, record it under `unknowns` instead of guessing or substituting an assumption.';
57
104
 
58
105
  const GUARDRAILS_TAIL = [
59
106
  '- Never include credentials, API keys, tokens, or environment variable values in your answer.',
@@ -69,9 +116,18 @@ const GUARDRAILS_TAIL = [
69
116
  'Output contract: reply with exactly one JSON object matching the provided schema. No prose, no markdown fences, no commentary before or after it. Empty arrays are fine; invented content is not.',
70
117
  ];
71
118
 
119
+ /**
120
+ * @param {boolean} [hasExposedPaths]
121
+ * @param {'on' | 'off'} [network]
122
+ */
72
123
  export function renderGuardrails(hasExposedPaths = false, network = 'on') {
73
- return [...GUARDRAILS_HEAD, network === 'off' ? WEB_OFF : WEB_ON, ...GUARDRAILS_RULES,
74
- hasExposedPaths ? WORKDIR_EXPOSED : WORKDIR_EMPTY, ...GUARDRAILS_TAIL].join('\n');
124
+ return [
125
+ ...GUARDRAILS_HEAD,
126
+ network === 'off' ? WEB_OFF : WEB_ON,
127
+ ...GUARDRAILS_RULES,
128
+ hasExposedPaths ? WORKDIR_EXPOSED : WORKDIR_EMPTY,
129
+ ...GUARDRAILS_TAIL,
130
+ ].join('\n');
75
131
  }
76
132
 
77
133
  // The material the lead handed over, kept in the shape it was sent in. The
@@ -79,6 +135,10 @@ export function renderGuardrails(hasExposedPaths = false, network = 'on') {
79
135
  // history holds an answer whose premises are gone -- and a finding read a month
80
136
  // later is only as good as the facts it was answering. Redacted the same way
81
137
  // renderBrief redacts the text itself, so the stored copy is what was sent.
138
+ /**
139
+ * @param {BriefRequest} req
140
+ * @param {ExposeManifest | null} [exposeManifest]
141
+ */
82
142
  export function briefRecord(req, exposeManifest = null) {
83
143
  return redact({
84
144
  objective: req.objective ?? null,
@@ -100,36 +160,42 @@ export function briefRecord(req, exposeManifest = null) {
100
160
  // frozen duplicate of a repository that has moved on since.
101
161
  expose_paths: exposeManifest
102
162
  ? {
103
- entries: exposeManifest.entries.map((e) => ({
104
- index: e.index,
105
- source: e.source,
106
- exposed_as: e.exposedAs,
107
- kind: e.kind,
108
- files: e.files,
109
- bytes: e.bytes,
110
- // What every member was handed, so a reader can check they all got the same.
111
- sha256: e.sha256 ?? null,
112
- ...(e.base ? { base: e.base, changes: e.changes } : {}),
113
- // A follow-up's copy of a path the parent round exposed: whether its bytes changed since.
114
- ...(e.changed === undefined ? {} : { changed_since_parent: e.changed }),
115
- })),
116
- totals: exposeManifest.totals,
117
- skipped: exposeManifest.skipped,
118
- }
163
+ entries: exposeManifest.entries.map((e) => ({
164
+ index: e.index,
165
+ source: e.source,
166
+ exposed_as: e.exposedAs,
167
+ kind: e.kind,
168
+ files: e.files,
169
+ bytes: e.bytes,
170
+ // What every member was handed, so a reader can check they all got the same.
171
+ sha256: e.sha256 ?? null,
172
+ ...(e.base ? { base: e.base, changes: e.changes } : {}),
173
+ // A follow-up's copy of a path the parent round exposed: whether its bytes changed since.
174
+ ...(e.changed === undefined ? {} : { changed_since_parent: e.changed }),
175
+ })),
176
+ totals: exposeManifest.totals,
177
+ skipped: exposeManifest.skipped,
178
+ }
119
179
  : null,
120
180
  });
121
181
  }
122
182
 
183
+ /**
184
+ * @param {string} title
185
+ * @param {string | null | undefined} body
186
+ */
123
187
  function section(title, body) {
124
188
  if (!body) return null;
125
189
  return `## ${title}\n${body}`;
126
190
  }
127
191
 
192
+ /** @param {string[] | null | undefined} items */
128
193
  function bullets(items) {
129
194
  if (!items || items.length === 0) return null;
130
195
  return items.map((i) => `- ${i}`).join('\n');
131
196
  }
132
197
 
198
+ /** @param {BriefArtifact[]} artifacts */
133
199
  function renderArtifacts(artifacts) {
134
200
  if (!artifacts.length) return null;
135
201
  return artifacts
@@ -147,25 +213,30 @@ function renderArtifacts(artifacts) {
147
213
  const EXPOSE_LISTING_MAX = 200;
148
214
 
149
215
  // Deleted files have no copy to open; the brief says which they were.
216
+ /** @param {string[]} [changes] */
150
217
  function deletedLines(changes = []) {
151
218
  const gone = changes.filter((c) => c.endsWith(' (deleted)')).map((c) => c.slice(0, -' (deleted)'.length));
152
219
  if (!gone.length) return [];
153
220
  const shown = gone.slice(0, 50);
154
- return [` - deleted by the change, so not copied: ${shown.join(', ')}${gone.length > shown.length ? `, and ${gone.length - shown.length} more` : ''}`];
221
+ return [
222
+ ` - deleted by the change, so not copied: ${shown.join(', ')}${gone.length > shown.length ? `, and ${gone.length - shown.length} more` : ''}`,
223
+ ];
155
224
  }
156
225
 
226
+ /** @param {ExposeManifest | null} manifest */
157
227
  function renderExposeManifest(manifest) {
158
228
  if (!manifest || (manifest.entries.length === 0 && manifest.skipped.length === 0)) return null;
159
229
  let room = EXPOSE_LISTING_MAX;
160
230
  const lines = manifest.entries.flatMap((e) => {
161
- const shape = (e.kind === 'diff'
162
- ? `the files the lead's change touches, against ${e.base}, at their paths in the repository; ${e.files} copied, ${humanSize(e.bytes)}; the diff itself is among the artifacts`
163
- : e.kind === 'change_files'
164
- ? `the files the lead's change touched in the earlier round, at their paths in the repository, copied again as they are now; ${e.files} copied, ${humanSize(e.bytes)}; the diff is not repeated`
165
- : e.kind === 'directory'
166
- ? `directory, ${e.files} file(s), ${humanSize(e.bytes)}`
167
- : `file, ${humanSize(e.bytes)}`)
168
- + (e.changed === undefined ? '' : e.changed ? '; changed since the earlier round' : '; unchanged since the earlier round');
231
+ const shape =
232
+ (e.kind === 'diff'
233
+ ? `the files the lead's change touches, against ${e.base}, at their paths in the repository; ${e.files} copied, ${humanSize(e.bytes)}; the diff itself is among the artifacts`
234
+ : e.kind === 'change_files'
235
+ ? `the files the lead's change touched in the earlier round, at their paths in the repository, copied again as they are now; ${e.files} copied, ${humanSize(e.bytes)}; the diff is not repeated`
236
+ : e.kind === 'directory'
237
+ ? `directory, ${e.files} file(s), ${humanSize(e.bytes)}`
238
+ : `file, ${humanSize(e.bytes)}`) +
239
+ (e.changed === undefined ? '' : e.changed ? '; changed since the earlier round' : '; unchanged since the earlier round');
169
240
  // Not where it was copied from: the consultant is never told where the
170
241
  // lead's repository is, and Codex, Antigravity and OpenCode could read
171
242
  // the unmasked original there. The source path stays in the history.
@@ -179,13 +250,24 @@ function renderExposeManifest(manifest) {
179
250
  ...(e.kind === 'diff' || e.kind === 'change_files' ? deletedLines(e.changes) : []),
180
251
  ];
181
252
  });
253
+ /** @param {string} reason */
182
254
  const count = (reason) => manifest.skipped.filter((x) => x.reason === reason).length;
183
- const named = (reason) => manifest.skipped.filter((x) => x.reason === reason).map((x) => x.exposed_as).slice(0, 50).join(', ');
255
+ /** @param {string} reason */
256
+ const named = (reason) =>
257
+ manifest.skipped
258
+ .filter((x) => x.reason === reason)
259
+ .map((x) => x.exposed_as)
260
+ .slice(0, 50)
261
+ .join(', ');
184
262
  const skipped = [
185
263
  count('symlink') ? `${count('symlink')} symlink(s), or path(s) through one, were skipped, not followed.` : null,
186
264
  count('protected') ? `${count('protected')} credential or history path(s) were left out on purpose.` : null,
187
- count('ignored') ? `${count('ignored')} path(s) (.git, node_modules, whatever .gitignore excludes) were left out as not source.` : null,
188
- count('submodule') ? `${count('submodule')} submodule(s) or nested repositor(ies) were left out; their files are not copied.` : null,
265
+ count('ignored')
266
+ ? `${count('ignored')} path(s) (.git, node_modules, whatever .gitignore excludes) were left out as not source.`
267
+ : null,
268
+ count('submodule')
269
+ ? `${count('submodule')} submodule(s) or nested repositor(ies) were left out; their files are not copied.`
270
+ : null,
189
271
  count('gone') ? `No longer there, so not copied again: ${named('gone')}.` : null,
190
272
  count('masked') ? `Not copied again, because the record of the earlier round masked the path: ${named('masked')}.` : null,
191
273
  count('replaced') ? `The earlier round's change is replaced by the diff given in this round: ${named('replaced')}.` : null,
@@ -193,7 +275,7 @@ function renderExposeManifest(manifest) {
193
275
  if (skipped.length) skipped.unshift('');
194
276
  return [
195
277
  'The lead exposed these paths; they are copied under ./workspace in your working directory and you may read',
196
- 'them directly. They are the only part of the lead\'s repository you have -- if you need something that is',
278
+ "them directly. They are the only part of the lead's repository the lead gave you -- if you need something that is",
197
279
  'not here, record it under `unknowns` rather than guessing. Text files were passed through the same',
198
280
  'credential masking as this brief; binary files were copied unchanged.',
199
281
  '',
@@ -204,32 +286,49 @@ function renderExposeManifest(manifest) {
204
286
 
205
287
  // An earlier round's point, cut to a length that keeps a five-round chain
206
288
  // from crowding out the question it is there to inform.
289
+ /**
290
+ * @param {string} s
291
+ * @param {number} [max]
292
+ */
207
293
  const clip = (s, max = 400) => (s.length > max ? `${s.slice(0, max)}...` : s);
208
294
 
209
295
  // The earlier rounds have a budget of their own, beside the request's cap:
210
296
  // when they outgrow it, the oldest rounds are left out, and the brief says so.
211
297
  const HISTORY_CHARS = 40_000;
212
298
 
299
+ /** @param {PriorRound} p */
213
300
  function renderRound(p) {
214
301
  // Prefixed with the round, so a point from round 1 is not taken for this one's.
302
+ /** @param {string} id */
215
303
  const tag = (id) => `r${p.round}.${id}`;
304
+ /** @param {PriorPoint} x */
216
305
  const verdict = (x) => (x.verdict ? ` -- lead's verdict: ${x.verdict}${x.effect ? ` (${clip(x.effect)})` : ''}` : '');
306
+ /**
307
+ * @param {string} title
308
+ * @param {PriorPoint[] | undefined} items
309
+ * @param {(x: PriorPoint) => string} line
310
+ */
217
311
  const list = (title, items, line) => (items?.length ? [title, ...items.map(line)].join('\n') : null);
218
312
  return [
219
313
  `### Round ${p.round} (${p.mode})`,
220
314
  `Question asked: ${p.question}`,
221
315
  `Your answer, summarised: ${p.summary}`,
222
- list('Points you raised:', p.findings,
223
- (f) => `- ${tag(f.id)}: ${clip(f.point)}${verdict(f)}${f.grounds ? `\n grounds: ${clip(f.grounds)}` : ''}`),
316
+ list(
317
+ 'Points you raised:',
318
+ p.findings,
319
+ (f) => `- ${tag(f.id)}: ${clip(f.point)}${verdict(f)}${f.grounds ? `\n grounds: ${clip(f.grounds)}` : ''}`,
320
+ ),
224
321
  list('Unknowns you named:', p.unknowns, (u) => `- ${tag(u.id)}: ${clip(u.point)}${verdict(u)}`),
225
322
  list('Checks you proposed:', p.next_checks, (c) => `- ${tag(c.id)}: ${clip(c.point)}${verdict(c)}`),
226
- ].filter(Boolean).join('\n');
323
+ ]
324
+ .filter(Boolean)
325
+ .join('\n');
227
326
  }
228
327
 
229
328
  /**
230
- * @param {object} req validated request
231
- * @param {object} chain { round, priorRounds: [{question, mode, summary, findings: [{id, point, grounds, verdict, effect}],
232
- * unknowns: [{id, point, verdict, effect}], next_checks: [{id, point, verdict, effect}]}] }
329
+ * @param {BriefRequest} req validated request
330
+ * @param {BriefChain} [chain] the round number and the earlier rounds of the chain
331
+ * @param {ExposeManifest | null} [exposeManifest]
233
332
  */
234
333
  export function renderBrief(req, chain = { round: 1, priorRounds: [] }, exposeManifest = null) {
235
334
  const parts = [];
@@ -262,19 +361,21 @@ export function renderBrief(req, chain = { round: 1, priorRounds: [] }, exposeMa
262
361
  parts.push('');
263
362
  parts.push('## Earlier rounds of this same consultation');
264
363
  parts.push(
265
- 'Each point carries the lead\'s own check where there is one -- confirmed: it held; not_applicable: checked, and'
266
- + ' it does not apply here; unverifiable: could not be checked; unverified, or no verdict: not checked yet.',
364
+ "Each point carries the lead's own check where there is one -- confirmed: it held; not_applicable: checked, and" +
365
+ ' it does not apply here; unverifiable: could not be checked; unverified, or no verdict: not checked yet.',
267
366
  );
268
367
  const blocks = chain.priorRounds.map(renderRound);
269
368
  let kept = 0;
270
369
  let used = 0;
271
370
  // Newest first: the round being followed up always goes in.
272
- for (let i = blocks.length - 1; i >= 0 && (kept === 0 || used + blocks[i].length <= HISTORY_CHARS); i--) {
273
- used += blocks[i].length;
371
+ for (const block of [...blocks].reverse()) {
372
+ if (kept > 0 && used + block.length > HISTORY_CHARS) break;
373
+ used += block.length;
274
374
  kept += 1;
275
375
  }
276
376
  const left = chain.priorRounds.slice(0, blocks.length - kept).map((p) => p.round);
277
- if (left.length) parts.push(`(Round${left.length > 1 ? 's' : ''} ${left.join(', ')} left out to keep this brief within its size.)`);
377
+ if (left.length)
378
+ parts.push(`(Round${left.length > 1 ? 's' : ''} ${left.join(', ')} left out to keep this brief within its size.)`);
278
379
  parts.push(...blocks.slice(blocks.length - kept));
279
380
  parts.push('');
280
381
  parts.push(