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/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 {
|
|
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 {
|
|
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 {
|
|
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(
|
|
84
|
-
|
|
85
|
-
|
|
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(
|
|
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 =
|
|
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'))
|
|
109
|
-
|
|
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(
|
|
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
|
|
252
|
-
if (
|
|
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 {
|
|
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
|
}
|