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.
- package/README.ja.md +52 -17
- package/README.md +52 -16
- package/bin/severally-mcp.mjs +14 -2
- package/docs/install.ja.md +48 -20
- package/docs/install.md +48 -21
- package/package.json +15 -3
- package/plugins/severally/.claude-plugin/plugin.json +1 -1
- package/plugins/severally/.codex-plugin/plugin.json +1 -1
- package/plugins/severally/dist/severally-mcp.mjs +6573 -5978
- package/plugins/severally/skills/_template/reference.md.tmpl +2 -2
- package/plugins/severally/skills/antigravity/severally/reference.md +2 -2
- package/plugins/severally/skills/claude/severally/reference.md +2 -2
- package/plugins/severally/skills/codex/severally/reference.md +2 -2
- package/plugins/severally/skills/opencode/severally/reference.md +2 -2
- package/scripts/build.mjs +76 -68
- package/scripts/doctor.mjs +66 -20
- package/scripts/init-config.mjs +8 -6
- package/scripts/install.mjs +327 -90
- package/scripts/live-check.mjs +62 -23
- package/scripts/live-mcp-check.mjs +52 -16
- package/scripts/test.mjs +10 -4
- package/src/adapters/antigravity-sandbox.mjs +62 -28
- package/src/adapters/antigravity.mjs +111 -30
- package/src/adapters/claude-code.mjs +121 -28
- package/src/adapters/codex.mjs +125 -33
- package/src/adapters/opencode-sandbox.mjs +59 -22
- package/src/adapters/opencode.mjs +144 -30
- package/src/brief.mjs +148 -47
- package/src/consultants.mjs +255 -48
- package/src/cross-spawn-escape.d.ts +7 -0
- package/src/export.mjs +99 -18
- package/src/expose-paths.mjs +310 -50
- package/src/failures.mjs +31 -14
- package/src/jobs.mjs +863 -249
- package/src/jsonc.mjs +11 -3
- package/src/parse-result.mjs +172 -40
- package/src/platform.mjs +56 -8
- package/src/policy.mjs +245 -52
- package/src/redact.mjs +59 -16
- package/src/result-schema.mjs +13 -0
- package/src/run.mjs +225 -30
- package/src/schema.mjs +132 -27
- package/src/server.mjs +178 -81
- package/src/status.mjs +94 -13
- 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
|
-
|
|
42
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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 [
|
|
74
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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 [
|
|
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 =
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
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')
|
|
188
|
-
|
|
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
|
-
|
|
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(
|
|
223
|
-
|
|
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
|
-
]
|
|
323
|
+
]
|
|
324
|
+
.filter(Boolean)
|
|
325
|
+
.join('\n');
|
|
227
326
|
}
|
|
228
327
|
|
|
229
328
|
/**
|
|
230
|
-
* @param {
|
|
231
|
-
* @param {
|
|
232
|
-
*
|
|
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
|
-
|
|
266
|
-
|
|
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 (
|
|
273
|
-
used
|
|
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)
|
|
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(
|