severally-mcp 2.1.0 → 2.4.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.
- package/CODE_OF_CONDUCT.md +83 -0
- package/CONFIG.md +10 -6
- package/README.ja.md +146 -69
- package/README.md +150 -73
- package/bin/severally-mcp.mjs +17 -2
- package/config.example.json +26 -0
- package/docs/how-it-works.ja.md +4 -2
- package/docs/how-it-works.md +5 -3
- package/docs/install.ja.md +59 -24
- package/docs/install.md +60 -24
- package/package.json +16 -4
- 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 +29220 -27764
- package/plugins/severally/skills/_template/SKILL.md.tmpl +16 -22
- package/plugins/severally/skills/_template/reference.md.tmpl +11 -5
- package/plugins/severally/skills/antigravity/severally/SKILL.md +16 -22
- package/plugins/severally/skills/antigravity/severally/reference.md +11 -5
- package/plugins/severally/skills/claude/severally/SKILL.md +16 -22
- package/plugins/severally/skills/claude/severally/reference.md +11 -5
- package/plugins/severally/skills/codex/severally/SKILL.md +16 -22
- package/plugins/severally/skills/codex/severally/reference.md +11 -5
- package/plugins/severally/skills/opencode/severally/SKILL.md +16 -22
- package/plugins/severally/skills/opencode/severally/reference.md +11 -5
- package/scripts/build.mjs +31 -26
- package/scripts/doctor.mjs +52 -17
- package/scripts/init-config.mjs +8 -6
- package/scripts/install.mjs +342 -84
- package/scripts/live-check.mjs +52 -19
- package/scripts/live-mcp-check.mjs +42 -12
- package/scripts/test.mjs +10 -4
- package/src/adapters/antigravity-sandbox.mjs +39 -23
- package/src/adapters/antigravity.mjs +34 -16
- package/src/adapters/claude-code.mjs +73 -21
- package/src/adapters/codex.mjs +69 -27
- package/src/adapters/opencode-sandbox.mjs +40 -21
- package/src/adapters/opencode.mjs +31 -13
- package/src/brief.mjs +65 -41
- package/src/consultants.mjs +99 -30
- package/src/export.mjs +49 -12
- package/src/expose-paths.mjs +162 -44
- package/src/failures.mjs +23 -13
- package/src/jobs.mjs +420 -140
- package/src/parse-result.mjs +85 -30
- package/src/platform.mjs +5 -7
- package/src/policy.mjs +139 -32
- package/src/redact.mjs +38 -14
- package/src/run.mjs +104 -22
- package/src/schema.mjs +76 -22
- package/src/server.mjs +176 -166
- package/src/status.mjs +132 -0
- package/src/store.mjs +164 -19
package/src/run.mjs
CHANGED
|
@@ -5,10 +5,7 @@ import { spawnCommand as spawn } from './platform.mjs';
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { POLICY } from './policy.mjs';
|
|
7
7
|
|
|
8
|
-
const DROP_EXACT = new Set([
|
|
9
|
-
'CLAUDECODE', 'CLAUDE_PID', 'CLAUDE_EFFORT', 'AI_AGENT',
|
|
10
|
-
'ANTHROPIC_LOG', 'NODE_OPTIONS',
|
|
11
|
-
]);
|
|
8
|
+
const DROP_EXACT = new Set(['CLAUDECODE', 'CLAUDE_PID', 'CLAUDE_EFFORT', 'AI_AGENT', 'ANTHROPIC_LOG', 'NODE_OPTIONS']);
|
|
12
9
|
const DROP_PREFIX = ['CLAUDE_CODE_', 'SEVERALLY_', 'MCP_'];
|
|
13
10
|
|
|
14
11
|
// Each consultant sees only its own vendor's credentials.
|
|
@@ -164,23 +161,48 @@ export class ProcHandle {
|
|
|
164
161
|
this.stdoutSoFar = readStdout;
|
|
165
162
|
}
|
|
166
163
|
|
|
167
|
-
/**
|
|
164
|
+
/**
|
|
165
|
+
* Kill the child and every descendant it spawned -- also after the child
|
|
166
|
+
* itself has exited: a descendant still holding its pipes keeps the
|
|
167
|
+
* consultation from closing, and on POSIX it is still in the group.
|
|
168
|
+
*/
|
|
168
169
|
stop(signal = 'SIGTERM') {
|
|
169
|
-
if (this.killed || !this.child
|
|
170
|
+
if (this.killed || !this.child) return;
|
|
170
171
|
this.killed = true;
|
|
171
172
|
const pid = this.child.pid;
|
|
172
173
|
if (!pid) return;
|
|
173
174
|
if (process.platform === 'win32') {
|
|
174
175
|
// Negative PIDs/process groups are POSIX-only. Kill the Windows tree
|
|
175
|
-
// before its parent exits and its descendants become unreachable.
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const
|
|
176
|
+
// before its parent exits and its descendants become unreachable. Once
|
|
177
|
+
// it has exited, its pid may already be another process's: leave it.
|
|
178
|
+
if (this.child.exitCode !== null || this.child.signalCode !== null) return;
|
|
179
|
+
const killer = spawn(
|
|
180
|
+
path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'taskkill.exe'),
|
|
181
|
+
['/pid', String(pid), '/T', '/F'],
|
|
182
|
+
{ windowsHide: true, stdio: 'ignore' },
|
|
183
|
+
);
|
|
184
|
+
const fallback = () => {
|
|
185
|
+
try {
|
|
186
|
+
this.child.kill('SIGKILL');
|
|
187
|
+
} catch {
|
|
188
|
+
/* gone */
|
|
189
|
+
}
|
|
190
|
+
};
|
|
179
191
|
killer.on('error', fallback);
|
|
180
|
-
killer.on('exit', (code) => {
|
|
192
|
+
killer.on('exit', (code) => {
|
|
193
|
+
if (code !== 0) fallback();
|
|
194
|
+
});
|
|
181
195
|
return;
|
|
182
196
|
}
|
|
183
|
-
try {
|
|
197
|
+
try {
|
|
198
|
+
process.kill(-pid, signal);
|
|
199
|
+
} catch {
|
|
200
|
+
try {
|
|
201
|
+
this.child.kill(signal);
|
|
202
|
+
} catch {
|
|
203
|
+
/* gone */
|
|
204
|
+
}
|
|
205
|
+
}
|
|
184
206
|
setTimeout(() => this.forceKill(), POLICY.killGraceMs).unref();
|
|
185
207
|
}
|
|
186
208
|
|
|
@@ -194,15 +216,35 @@ export class ProcHandle {
|
|
|
194
216
|
forceKill() {
|
|
195
217
|
const pid = this.child?.pid;
|
|
196
218
|
if (!pid || process.platform === 'win32') return;
|
|
197
|
-
try {
|
|
219
|
+
try {
|
|
220
|
+
process.kill(-pid, 'SIGKILL');
|
|
221
|
+
} catch {
|
|
222
|
+
/* gone */
|
|
223
|
+
}
|
|
198
224
|
}
|
|
199
225
|
}
|
|
200
226
|
|
|
201
227
|
/**
|
|
202
228
|
* @returns {{handle: ProcHandle, done: Promise<{code, signal, stdout, stderr, timedOut, cancelled, truncated}>}}
|
|
203
229
|
*/
|
|
204
|
-
|
|
205
|
-
|
|
230
|
+
/**
|
|
231
|
+
* `pipesForced` in the result: the child exited but something it started kept
|
|
232
|
+
* stdout or stderr open past the kill grace, so the group was stopped and the
|
|
233
|
+
* pipes closed from this side. A descendant outside the group (setsid, or on
|
|
234
|
+
* Windows once the parent is gone) may still be running.
|
|
235
|
+
*/
|
|
236
|
+
export function runChild({
|
|
237
|
+
command,
|
|
238
|
+
args,
|
|
239
|
+
cwd,
|
|
240
|
+
env,
|
|
241
|
+
input,
|
|
242
|
+
timeoutMs,
|
|
243
|
+
onCancelSignal,
|
|
244
|
+
compactLine = null,
|
|
245
|
+
settleAfter = null,
|
|
246
|
+
stopSignal = 'SIGTERM',
|
|
247
|
+
}) {
|
|
206
248
|
const child = spawn(command, args, {
|
|
207
249
|
cwd,
|
|
208
250
|
// PWD is the parent's otherwise, and opencode takes its project root from
|
|
@@ -232,17 +274,26 @@ export function runChild({ command, args, cwd, env, input, timeoutMs, onCancelSi
|
|
|
232
274
|
if (ms == null || Date.now() + ms >= settleAt) return;
|
|
233
275
|
settleAt = Date.now() + ms;
|
|
234
276
|
clearTimeout(settleTimer);
|
|
235
|
-
settleTimer = setTimeout(() => {
|
|
277
|
+
settleTimer = setTimeout(() => {
|
|
278
|
+
stalled = true;
|
|
279
|
+
handle.stop(stopSignal);
|
|
280
|
+
}, ms);
|
|
236
281
|
};
|
|
237
282
|
|
|
238
283
|
const append = (which, chunk) => {
|
|
239
284
|
const text = chunk.toString('utf8');
|
|
240
285
|
if (which === 'out') {
|
|
241
|
-
if (stdout.length >= cap) {
|
|
286
|
+
if (stdout.length >= cap) {
|
|
287
|
+
truncated = true;
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
242
290
|
stdout += text.slice(0, cap - stdout.length);
|
|
243
291
|
if (stdout.length >= cap) truncated = true;
|
|
244
292
|
} else {
|
|
245
|
-
if (stderr.length >= cap) {
|
|
293
|
+
if (stderr.length >= cap) {
|
|
294
|
+
truncated = true;
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
246
297
|
stderr += text.slice(0, cap - stderr.length);
|
|
247
298
|
if (stderr.length >= cap) truncated = true;
|
|
248
299
|
}
|
|
@@ -261,7 +312,10 @@ export function runChild({ command, args, cwd, env, input, timeoutMs, onCancelSi
|
|
|
261
312
|
if (settleAfter) watch(l);
|
|
262
313
|
append('out', `${compact(l)}\n`);
|
|
263
314
|
}
|
|
264
|
-
if (pending.length > cap) {
|
|
315
|
+
if (pending.length > cap) {
|
|
316
|
+
append('out', pending);
|
|
317
|
+
pending = '';
|
|
318
|
+
}
|
|
265
319
|
};
|
|
266
320
|
// Decode as a stream: a multibyte character split across two chunks would
|
|
267
321
|
// otherwise turn into replacement characters inside the answer text.
|
|
@@ -288,22 +342,50 @@ export function runChild({ command, args, cwd, env, input, timeoutMs, onCancelSi
|
|
|
288
342
|
|
|
289
343
|
const done = new Promise((resolve) => {
|
|
290
344
|
let settled = false;
|
|
345
|
+
let pipesForced = false;
|
|
346
|
+
let orphanTimer = null;
|
|
291
347
|
const finish = (payload) => {
|
|
292
348
|
if (settled) return;
|
|
293
349
|
settled = true;
|
|
294
350
|
clearTimeout(timer);
|
|
295
351
|
clearTimeout(settleTimer);
|
|
296
|
-
|
|
352
|
+
clearTimeout(orphanTimer);
|
|
353
|
+
resolve({ stdout, stderr, truncated, timedOut, cancelled, stalled, pipesForced, ...payload });
|
|
354
|
+
};
|
|
355
|
+
const flushPending = () => {
|
|
356
|
+
if (byLine && pending) {
|
|
357
|
+
append('out', compact(pending));
|
|
358
|
+
pending = '';
|
|
359
|
+
}
|
|
297
360
|
};
|
|
298
361
|
child.on('error', (err) => finish({ code: null, signal: null, spawnError: err.message }));
|
|
362
|
+
// 'close' waits for the pipes as well as the child. Once the child has
|
|
363
|
+
// exited, a grace later the rest of its group is stopped, and a grace
|
|
364
|
+
// after that the pipes are closed from here, whoever still holds them.
|
|
365
|
+
child.on('exit', (code, signal) => {
|
|
366
|
+
const grace = POLICY.killGraceMs;
|
|
367
|
+
orphanTimer = setTimeout(() => {
|
|
368
|
+
pipesForced = true;
|
|
369
|
+
handle.stop(stopSignal);
|
|
370
|
+
orphanTimer = setTimeout(() => {
|
|
371
|
+
handle.forceKill();
|
|
372
|
+
flushPending();
|
|
373
|
+
child.stdout.destroy();
|
|
374
|
+
child.stderr.destroy();
|
|
375
|
+
finish({ code, signal });
|
|
376
|
+
}, grace);
|
|
377
|
+
}, grace);
|
|
378
|
+
});
|
|
299
379
|
child.on('close', (code, signal) => {
|
|
300
|
-
|
|
380
|
+
flushPending();
|
|
301
381
|
finish({ code, signal });
|
|
302
382
|
});
|
|
303
383
|
});
|
|
304
384
|
|
|
305
385
|
if (input !== undefined && input !== null) {
|
|
306
|
-
child.stdin.on('error', () => {
|
|
386
|
+
child.stdin.on('error', () => {
|
|
387
|
+
/* child may exit before draining stdin */
|
|
388
|
+
});
|
|
307
389
|
child.stdin.end(input);
|
|
308
390
|
} else {
|
|
309
391
|
child.stdin.end();
|
package/src/schema.mjs
CHANGED
|
@@ -5,8 +5,16 @@
|
|
|
5
5
|
import { z } from 'zod';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import {
|
|
8
|
-
POLICY,
|
|
9
|
-
|
|
8
|
+
POLICY,
|
|
9
|
+
CALLER_INPUTS,
|
|
10
|
+
MODES,
|
|
11
|
+
STANCES,
|
|
12
|
+
artifactKinds,
|
|
13
|
+
resolveCaller,
|
|
14
|
+
availableConsultants,
|
|
15
|
+
consultantAvailable,
|
|
16
|
+
unavailableReason,
|
|
17
|
+
brokenConsultant,
|
|
10
18
|
} from './policy.mjs';
|
|
11
19
|
import { normalizeName, resolveConsultant } from './consultants.mjs';
|
|
12
20
|
|
|
@@ -36,16 +44,20 @@ const exposedPathSchema = z
|
|
|
36
44
|
.refine((p) => path.isAbsolute(p), { message: 'context.expose_paths[] must be an absolute path' });
|
|
37
45
|
|
|
38
46
|
// A revision for `git diff`: a ref, a commit, or HEAD~n -- never an option.
|
|
39
|
-
const gitRevision = z
|
|
47
|
+
const gitRevision = z
|
|
48
|
+
.string()
|
|
49
|
+
.trim()
|
|
50
|
+
.min(1)
|
|
51
|
+
.max(200)
|
|
40
52
|
.regex(/^[A-Za-z0-9._/~^@{}-]+$/, 'context.expose_diff.base must be a git revision (a branch, tag, commit or HEAD~n)')
|
|
41
53
|
.refine((r) => !r.startsWith('-'), { message: 'context.expose_diff.base must not start with "-"' });
|
|
42
54
|
|
|
43
55
|
export const contextSchema = z
|
|
44
56
|
.object({
|
|
45
|
-
facts: z.array(trimmed(L.factMax, 'context.facts[]')).max(L.factsMax).
|
|
57
|
+
facts: z.array(trimmed(L.factMax, 'context.facts[]')).max(L.factsMax).optional(),
|
|
46
58
|
proposal: z.string().trim().max(L.proposalMax).nullish(),
|
|
47
|
-
counterpoints: z.array(trimmed(L.counterpointMax, 'context.counterpoints[]')).max(L.counterpointsMax).
|
|
48
|
-
artifacts: z.array(artifactSchema).max(L.artifactsMax).
|
|
59
|
+
counterpoints: z.array(trimmed(L.counterpointMax, 'context.counterpoints[]')).max(L.counterpointsMax).optional(),
|
|
60
|
+
artifacts: z.array(artifactSchema).max(L.artifactsMax).optional(),
|
|
49
61
|
expose_paths: z.array(exposedPathSchema).max(L.exposePathsMax).default([]),
|
|
50
62
|
// The lead's uncommitted (or since-`base`) change: its diff goes in the
|
|
51
63
|
// brief and the files it touches are copied, under workspace/repo/ at
|
|
@@ -55,6 +67,9 @@ export const contextSchema = z
|
|
|
55
67
|
// round exposed. Never implied -- what reaches another vendor's model is
|
|
56
68
|
// what the lead named in this call.
|
|
57
69
|
expose_from_parent: z.boolean().optional(),
|
|
70
|
+
// Read as the request's own: a lead often nests them beside the facts.
|
|
71
|
+
constraints: z.array(trimmed(L.constraintMax, 'context.constraints[]')).max(L.constraintsMax).optional(),
|
|
72
|
+
success_criteria: z.array(trimmed(L.constraintMax, 'context.success_criteria[]')).max(L.constraintsMax).optional(),
|
|
58
73
|
})
|
|
59
74
|
.strict();
|
|
60
75
|
|
|
@@ -62,7 +77,11 @@ export const contextSchema = z
|
|
|
62
77
|
// for it, its CLI, its vendor, or its model ("glm", "claude opus"). Which
|
|
63
78
|
// consultant that is depends on the operator's config, so it is resolved in
|
|
64
79
|
// parseRequest, not by an enum here.
|
|
65
|
-
const targetSpec = z
|
|
80
|
+
const targetSpec = z
|
|
81
|
+
.string()
|
|
82
|
+
.trim()
|
|
83
|
+
.min(1, 'target must not be empty')
|
|
84
|
+
.max(120, 'target exceeds 120 characters')
|
|
66
85
|
.regex(/^[^\p{Cc}\p{Cf}]+$/u, 'target must be a single line of printable characters');
|
|
67
86
|
|
|
68
87
|
// Who asked for a consultation, as the lead reports it.
|
|
@@ -100,13 +119,13 @@ export const requestSchema = z
|
|
|
100
119
|
// "off" takes web search and browsing away from every consultant in this
|
|
101
120
|
// round, for a brief that must not leave the machine through a URL. The
|
|
102
121
|
// consultant still reads the brief and the disk; it reaches nothing else.
|
|
103
|
-
network: z.enum(NETWORK_MODES).
|
|
122
|
+
network: z.enum(NETWORK_MODES).optional(),
|
|
104
123
|
question: trimmed(L.questionMax, 'question'),
|
|
105
124
|
// Optional: most consultations state what they are after in the question
|
|
106
125
|
// itself, and a second field that repeats it is friction, not information.
|
|
107
126
|
objective: trimmed(L.objectiveMax, 'objective').nullish(),
|
|
108
|
-
success_criteria: z.array(trimmed(L.constraintMax, 'success_criteria[]')).max(L.constraintsMax).
|
|
109
|
-
constraints: z.array(trimmed(L.constraintMax, 'constraints[]')).max(L.constraintsMax).
|
|
127
|
+
success_criteria: z.array(trimmed(L.constraintMax, 'success_criteria[]')).max(L.constraintsMax).optional(),
|
|
128
|
+
constraints: z.array(trimmed(L.constraintMax, 'constraints[]')).max(L.constraintsMax).optional(),
|
|
110
129
|
// prefault, not default: zod 4 returns a default as is, skipping the inner defaults (expose_paths among them).
|
|
111
130
|
context: contextSchema.prefault({}),
|
|
112
131
|
// Written by the lead before the consultant is launched, kept on this
|
|
@@ -156,14 +175,33 @@ export function parseRequest(raw, { isFollowup = false, parentMode = null, group
|
|
|
156
175
|
const issues = parsed.error.issues.map((i) => `${i.path.join('.') || '(root)'}: ${i.message}`);
|
|
157
176
|
throw new RequestError(`request failed validation: ${issues.join('; ')}`, 'invalid_request', issues);
|
|
158
177
|
}
|
|
178
|
+
// Filled in below with what the request resolves to (targets, caller, ...).
|
|
179
|
+
/** @type {Record<string, any>} */
|
|
159
180
|
const req = parsed.data;
|
|
181
|
+
// Left without a default in the schema, so that a follow-up can tell a list
|
|
182
|
+
// it left out (inherited from the round before, jobs.mjs) from one it
|
|
183
|
+
// emptied on purpose: the tool layer applies the schema's defaults first.
|
|
184
|
+
for (const k of ['constraints', 'success_criteria']) {
|
|
185
|
+
if (req.context[k] === undefined) continue;
|
|
186
|
+
if (req[k] !== undefined) {
|
|
187
|
+
throw new RequestError(`pass ${k} once: at the top of the request or inside context, not both`, 'invalid_request');
|
|
188
|
+
}
|
|
189
|
+
req[k] = req.context[k];
|
|
190
|
+
delete req.context[k];
|
|
191
|
+
}
|
|
192
|
+
req.network ??= 'on';
|
|
193
|
+
req.success_criteria ??= [];
|
|
194
|
+
req.constraints ??= [];
|
|
195
|
+
for (const k of ['facts', 'counterpoints', 'artifacts']) req.context[k] ??= [];
|
|
160
196
|
const hasSingle = req.target !== undefined && req.target !== null;
|
|
161
197
|
const hasMany = Array.isArray(req.targets) && req.targets.length > 0;
|
|
162
198
|
if (hasSingle && hasMany) {
|
|
163
199
|
throw new RequestError('pass either target (one consultant) or targets (several), not both', 'invalid_request');
|
|
164
200
|
}
|
|
165
|
-
const listed = () =>
|
|
166
|
-
|
|
201
|
+
const listed = () =>
|
|
202
|
+
availableConsultants()
|
|
203
|
+
.map((c) => `${c.name} (${c.cli}, ${c.model})`)
|
|
204
|
+
.join(', ') || 'none -- no consultant CLI is available here';
|
|
167
205
|
if (!hasSingle && !hasMany) {
|
|
168
206
|
throw new RequestError(
|
|
169
207
|
`name the consultant: target: a consultant, its CLI or its model (available: ${listed()}), or targets: [...] to ask several the same question, targets: ["all"] to ask everyone`,
|
|
@@ -180,7 +218,10 @@ export function parseRequest(raw, { isFollowup = false, parentMode = null, group
|
|
|
180
218
|
if (isAll[0]) {
|
|
181
219
|
chosen = availableConsultants();
|
|
182
220
|
if (!chosen.length) {
|
|
183
|
-
throw new RequestError(
|
|
221
|
+
throw new RequestError(
|
|
222
|
+
'no consultant is available on this machine -- install one of the CLIs, or check the config file',
|
|
223
|
+
'target_unavailable',
|
|
224
|
+
);
|
|
184
225
|
}
|
|
185
226
|
} else {
|
|
186
227
|
// Resolved among the reachable consultants first, so one that is off does
|
|
@@ -192,13 +233,19 @@ export function parseRequest(raw, { isFollowup = false, parentMode = null, group
|
|
|
192
233
|
chosen = named.map((name) => {
|
|
193
234
|
let r = resolveConsultant(name, reachable, POLICY.ranking);
|
|
194
235
|
if (r.none) r = resolveConsultant(name, POLICY.consultants, POLICY.ranking);
|
|
236
|
+
if (r.none && brokenConsultant(name)) {
|
|
237
|
+
throw new RequestError(
|
|
238
|
+
`consultant ${JSON.stringify(name)} is not available: ${brokenConsultant(name)}; available: ${listed()}`,
|
|
239
|
+
'target_unavailable',
|
|
240
|
+
);
|
|
241
|
+
}
|
|
195
242
|
if (r.none) {
|
|
196
243
|
throw new RequestError(`no consultant answers to ${JSON.stringify(name)}; available: ${listed()}`, 'unknown_target');
|
|
197
244
|
}
|
|
198
245
|
if (r.ambiguous) {
|
|
199
246
|
throw new RequestError(
|
|
200
|
-
`${JSON.stringify(name)} fits more than one consultant and no ranking rule decides between `
|
|
201
|
-
|
|
247
|
+
`${JSON.stringify(name)} fits more than one consultant and no ranking rule decides between ` +
|
|
248
|
+
`${r.ambiguous.map((c) => `${c.name} (${c.cli}, ${c.model})`).join(', ')}; name one of them`,
|
|
202
249
|
'target_ambiguous',
|
|
203
250
|
);
|
|
204
251
|
}
|
|
@@ -207,8 +254,8 @@ export function parseRequest(raw, { isFollowup = false, parentMode = null, group
|
|
|
207
254
|
const off = [...new Set(chosen.filter((c) => !consultantAvailable(c)))];
|
|
208
255
|
if (off.length) {
|
|
209
256
|
throw new RequestError(
|
|
210
|
-
`consultant ${off.map((c) => `${JSON.stringify(c.name)} (${unavailableReason(c)})`).join(', ')} `
|
|
211
|
-
|
|
257
|
+
`consultant ${off.map((c) => `${JSON.stringify(c.name)} (${unavailableReason(c)})`).join(', ')} ` +
|
|
258
|
+
`is not available on this machine; available: ${listed()}`,
|
|
212
259
|
'target_unavailable',
|
|
213
260
|
);
|
|
214
261
|
}
|
|
@@ -240,8 +287,7 @@ export function parseRequest(raw, { isFollowup = false, parentMode = null, group
|
|
|
240
287
|
// one its context fits -- no proposal is explore, a proposal is review, a
|
|
241
288
|
// proposal with the other side's claims is debate. Checked below like a
|
|
242
289
|
// mode that was given.
|
|
243
|
-
req.mode ??= parentMode
|
|
244
|
-
?? (!req.context.proposal ? 'explore' : req.context.counterpoints.length ? 'debate' : 'review');
|
|
290
|
+
req.mode ??= parentMode ?? (!req.context.proposal ? 'explore' : req.context.counterpoints.length ? 'debate' : 'review');
|
|
245
291
|
|
|
246
292
|
const total = charCount(req);
|
|
247
293
|
if (total > L.totalCharsMax) {
|
|
@@ -275,10 +321,18 @@ export function parseRequest(raw, { isFollowup = false, parentMode = null, group
|
|
|
275
321
|
'expose_from_parent_needs_followup',
|
|
276
322
|
);
|
|
277
323
|
}
|
|
278
|
-
if (
|
|
279
|
-
|
|
324
|
+
if (
|
|
325
|
+
req.context.facts.length === 0 &&
|
|
326
|
+
req.context.artifacts.length === 0 &&
|
|
327
|
+
req.context.expose_paths.length === 0 &&
|
|
328
|
+
!req.context.expose_diff &&
|
|
329
|
+
!req.context.expose_from_parent
|
|
330
|
+
) {
|
|
280
331
|
throw new RequestError(
|
|
281
|
-
'provide at least one entry in context.facts, context.artifacts or context.expose_paths: the consultant starts in an empty working directory and is not told where your repository is, so every fact it needs must be in the request'
|
|
332
|
+
'provide at least one entry in context.facts, context.artifacts or context.expose_paths: the consultant starts in an empty working directory and is not told where your repository is, so every fact it needs must be in the request' +
|
|
333
|
+
(isFollowup
|
|
334
|
+
? '; on a follow-up, context.expose_from_parent: true copies again the files the earlier round was shown'
|
|
335
|
+
: ''),
|
|
282
336
|
'context_required',
|
|
283
337
|
);
|
|
284
338
|
}
|