severally-mcp 2.0.0 → 2.1.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/CONFIG.md +4 -2
- package/README.ja.md +28 -15
- package/README.md +29 -15
- package/docs/how-it-works.ja.md +7 -1
- package/docs/how-it-works.md +7 -1
- package/docs/install.ja.md +13 -1
- package/docs/install.md +16 -2
- package/package.json +2 -2
- package/plugins/severally/.claude-plugin/plugin.json +1 -1
- package/plugins/severally/.codex-plugin/plugin.json +4 -2
- package/plugins/severally/.mcp.json +2 -1
- package/plugins/severally/dist/severally-mcp.mjs +327 -94
- package/plugins/severally/skills/_template/SKILL.md.tmpl +5 -3
- package/plugins/severally/skills/_template/reference.md.tmpl +12 -6
- package/plugins/severally/skills/antigravity/severally/SKILL.md +5 -3
- package/plugins/severally/skills/antigravity/severally/reference.md +12 -6
- package/plugins/severally/skills/claude/severally/SKILL.md +5 -3
- package/plugins/severally/skills/claude/severally/reference.md +12 -6
- package/plugins/severally/skills/codex/severally/SKILL.md +5 -3
- package/plugins/severally/skills/codex/severally/reference.md +12 -6
- package/plugins/severally/skills/opencode/severally/SKILL.md +5 -3
- package/plugins/severally/skills/opencode/severally/reference.md +12 -6
- package/scripts/init-config.mjs +1 -1
- package/scripts/install.mjs +35 -5
- package/src/adapters/antigravity-sandbox.mjs +5 -1
- package/src/adapters/codex.mjs +17 -6
- package/src/brief.mjs +38 -16
- package/src/expose-paths.mjs +68 -5
- package/src/jobs.mjs +171 -45
- package/src/policy.mjs +5 -4
- package/src/redact.mjs +20 -1
- package/src/schema.mjs +5 -4
- package/src/server.mjs +44 -16
|
@@ -24336,7 +24336,7 @@ var LADDER_ERROR_HTTP_STATUS = {
|
|
|
24336
24336
|
[ProtocolErrorCode.MissingRequiredClientCapability]: 400,
|
|
24337
24337
|
[HEADER_MISMATCH_ERROR_CODE]: 400
|
|
24338
24338
|
};
|
|
24339
|
-
function rejection(rung, cell, httpStatus, error62,
|
|
24339
|
+
function rejection(rung, cell, httpStatus, error62, settled2) {
|
|
24340
24340
|
return {
|
|
24341
24341
|
kind: "reject",
|
|
24342
24342
|
rung,
|
|
@@ -24345,7 +24345,7 @@ function rejection(rung, cell, httpStatus, error62, settled) {
|
|
|
24345
24345
|
code: error62.code,
|
|
24346
24346
|
message: error62.message,
|
|
24347
24347
|
...error62.data !== void 0 && { data: error62.data },
|
|
24348
|
-
settled
|
|
24348
|
+
settled: settled2
|
|
24349
24349
|
};
|
|
24350
24350
|
}
|
|
24351
24351
|
function carriesValidModernEnvelopeClaim(params) {
|
|
@@ -34560,28 +34560,28 @@ var StdioServerTransport = class {
|
|
|
34560
34560
|
if (this._closed) return Promise.reject(/* @__PURE__ */ new Error("StdioServerTransport is closed"));
|
|
34561
34561
|
return new Promise((resolve, reject) => {
|
|
34562
34562
|
const json2 = serializeMessage(message);
|
|
34563
|
-
let
|
|
34563
|
+
let settled2 = false;
|
|
34564
34564
|
const onError = (error62) => {
|
|
34565
|
-
if (
|
|
34566
|
-
|
|
34565
|
+
if (settled2) return;
|
|
34566
|
+
settled2 = true;
|
|
34567
34567
|
this._stdout.off("error", onError);
|
|
34568
34568
|
this._stdout.off("drain", onDrain);
|
|
34569
34569
|
reject(error62);
|
|
34570
34570
|
};
|
|
34571
34571
|
const onDrain = () => {
|
|
34572
|
-
if (
|
|
34573
|
-
|
|
34572
|
+
if (settled2) return;
|
|
34573
|
+
settled2 = true;
|
|
34574
34574
|
this._stdout.off("error", onError);
|
|
34575
34575
|
this._stdout.off("drain", onDrain);
|
|
34576
34576
|
resolve();
|
|
34577
34577
|
};
|
|
34578
34578
|
this._stdout.once("error", onError);
|
|
34579
34579
|
if (this._stdout.write(json2)) {
|
|
34580
|
-
if (
|
|
34581
|
-
|
|
34580
|
+
if (settled2) return;
|
|
34581
|
+
settled2 = true;
|
|
34582
34582
|
this._stdout.off("error", onError);
|
|
34583
34583
|
resolve();
|
|
34584
|
-
} else if (!
|
|
34584
|
+
} else if (!settled2) this._stdout.once("drain", onDrain);
|
|
34585
34585
|
});
|
|
34586
34586
|
}
|
|
34587
34587
|
};
|
|
@@ -35484,9 +35484,10 @@ var POLICY = Object.freeze({
|
|
|
35484
35484
|
// going beyond that is an operator decision with quota to match.
|
|
35485
35485
|
maxConcurrent: limit("SEVERALLY_MAX_CONCURRENT", cfgServer.max_concurrent, 9, 1, 9),
|
|
35486
35486
|
maxJobsRetained: limit("SEVERALLY_MAX_JOBS_RETAINED", cfgServer.max_jobs_retained, 200, 20, 2e3),
|
|
35487
|
-
// Upper bound for consult_get(wait_ms).
|
|
35488
|
-
//
|
|
35489
|
-
//
|
|
35487
|
+
// Upper bound for consult_get(wait_ms). 45 s stays under the 60 s some MCP
|
|
35488
|
+
// clients allow a tool call, so a long wait does not blow up as a client-side
|
|
35489
|
+
// timeout while the consultation is still healthy; a client registration
|
|
35490
|
+
// that is known to wait longer raises it (scripts/install.mjs).
|
|
35490
35491
|
maxWaitMs: limit("SEVERALLY_MAX_WAIT_MS", cfgServer.max_wait_ms, 45e3, 0, 6e5),
|
|
35491
35492
|
input: Object.freeze({
|
|
35492
35493
|
questionMax: 4e3,
|
|
@@ -35520,7 +35521,7 @@ var POLICY = Object.freeze({
|
|
|
35520
35521
|
})
|
|
35521
35522
|
});
|
|
35522
35523
|
function timeoutMs(consultant) {
|
|
35523
|
-
return consultant?.timeoutMs ?? limit("SEVERALLY_TIMEOUT_MS", cfgServer.timeout_ms,
|
|
35524
|
+
return consultant?.timeoutMs ?? limit("SEVERALLY_TIMEOUT_MS", cfgServer.timeout_ms, 18e5, 1e3, 72e5);
|
|
35524
35525
|
}
|
|
35525
35526
|
function agyExitGraceMs() {
|
|
35526
35527
|
return {
|
|
@@ -35747,7 +35748,7 @@ var RequestError = class extends Error {
|
|
|
35747
35748
|
this.details = details;
|
|
35748
35749
|
}
|
|
35749
35750
|
};
|
|
35750
|
-
function parseRequest(raw, { isFollowup = false, parentMode = null } = {}) {
|
|
35751
|
+
function parseRequest(raw, { isFollowup = false, parentMode = null, groupFollowup = false } = {}) {
|
|
35751
35752
|
const parsed = requestSchema.safeParse(raw);
|
|
35752
35753
|
if (!parsed.success) {
|
|
35753
35754
|
const issues = parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`);
|
|
@@ -35808,9 +35809,9 @@ function parseRequest(raw, { isFollowup = false, parentMode = null } = {}) {
|
|
|
35808
35809
|
"duplicate_targets"
|
|
35809
35810
|
);
|
|
35810
35811
|
}
|
|
35811
|
-
if (isFollowup && unique.length > 1) {
|
|
35812
|
+
if (isFollowup && !groupFollowup && unique.length > 1) {
|
|
35812
35813
|
throw new RequestError(
|
|
35813
|
-
"a follow-up continues the exchange with one consultant; name exactly one target",
|
|
35814
|
+
"a follow-up to a job continues the exchange with one consultant; name exactly one target, or pass followup_to a group_id to ask every member of that fan-out again",
|
|
35814
35815
|
"followup_fanout_not_allowed"
|
|
35815
35816
|
);
|
|
35816
35817
|
}
|
|
@@ -35955,6 +35956,12 @@ var CONSULT_RESULT_SCHEMA = obj(
|
|
|
35955
35956
|
var RESULT_KEYS = Object.keys(CONSULT_RESULT_SCHEMA.properties);
|
|
35956
35957
|
|
|
35957
35958
|
// src/redact.mjs
|
|
35959
|
+
var CODE_WORDS = /* @__PURE__ */ new Set(["true", "false", "null", "none", "undefined", "string", "str", "number", "boolean", "bool", "int", "bytes"]);
|
|
35960
|
+
function looksLikeCode(name, value) {
|
|
35961
|
+
if (CODE_WORDS.has(value.toLowerCase()) || value.startsWith("${") || /^\$[A-Z_][A-Z0-9_]*$/.test(value) || /^(?:process\.env\.|os\.environ\b)/.test(value)) return true;
|
|
35962
|
+
if (value.length >= 8 && /^PASSW(?:OR)?D$/i.test(name.split("_").at(-1))) return false;
|
|
35963
|
+
return /^[A-Za-z_][\w$]*(?:\.[A-Za-z_$][\w$]*)+$/.test(value) || /^[A-Za-z_][\w$.]*\(/.test(value) || value.startsWith("&");
|
|
35964
|
+
}
|
|
35958
35965
|
var PATTERNS = [
|
|
35959
35966
|
// Provider API keys
|
|
35960
35967
|
[/\bsk-ant-[A-Za-z0-9_\-]{12,}/g, "[REDACTED:anthropic-key]"],
|
|
@@ -35963,6 +35970,7 @@ var PATTERNS = [
|
|
|
35963
35970
|
[/\bgh[pousr]_[A-Za-z0-9]{16,}/g, "[REDACTED:github-token]"],
|
|
35964
35971
|
[/\bgithub_pat_[A-Za-z0-9_]{20,}/g, "[REDACTED:github-token]"],
|
|
35965
35972
|
[/\bxox[baprs]-[A-Za-z0-9-]{10,}/g, "[REDACTED:slack-token]"],
|
|
35973
|
+
[/https:\/\/hooks\.slack\.com\/services\/T[A-Za-z0-9]+\/B[A-Za-z0-9]+\/[A-Za-z0-9]+/g, "[REDACTED:slack-webhook]"],
|
|
35966
35974
|
[/\bAKIA[0-9A-Z]{16}\b/g, "[REDACTED:aws-access-key-id]"],
|
|
35967
35975
|
[/\bASIA[0-9A-Z]{16}\b/g, "[REDACTED:aws-access-key-id]"],
|
|
35968
35976
|
[/\bAIza[0-9A-Za-z_\-]{30,}/g, "[REDACTED:google-api-key]"],
|
|
@@ -35980,9 +35988,16 @@ var PATTERNS = [
|
|
|
35980
35988
|
[/(:\/\/[^\s:/@]*:)[^\s@/]+(@)/g, "$1[REDACTED]$2"],
|
|
35981
35989
|
// KEY=value / "token": "value" style assignments
|
|
35982
35990
|
[
|
|
35983
|
-
/\b((?:[A-Za-z0-9_]*_)?(?:API_KEY|APIKEY|ACCESS_KEY|PRIVATE_KEY|ACCESS_TOKEN|REFRESH_TOKEN|ID_TOKEN|SECRET_KEY|CLIENT_SECRET|AUTH_TOKEN|CREDENTIALS|
|
|
35991
|
+
/\b((?:[A-Za-z0-9_]*_)?(?:API_KEY|APIKEY|ACCESS_KEY|PRIVATE_KEY|ACCESS_TOKEN|REFRESH_TOKEN|ID_TOKEN|SECRET_KEY|CLIENT_SECRET|AUTH_TOKEN|CREDENTIALS|ACCOUNTKEY|SHAREDACCESSKEY|SECRET|TOKEN))(["']?\s*[:=]\s*["']?)([^\s"',;)]{8,})/gi,
|
|
35984
35992
|
(_m, k, sep, _v) => `${k}${sep}[REDACTED]`
|
|
35985
35993
|
],
|
|
35994
|
+
// Password-type names, with values from 4 characters; PASS and PWD too (PWD
|
|
35995
|
+
// only after `_` or a letter: bare PWD is the working directory). What reads
|
|
35996
|
+
// as code rather than a password is left alone (looksLikeCode).
|
|
35997
|
+
[
|
|
35998
|
+
/\b((?:[A-Za-z0-9_]*_)?(?:PASSWORD|PASSWD|PASS|(?<=[A-Za-z_])PWD))(["']?\s*[:=]\s*["']?)([^\s"',;)]{4,})/gi,
|
|
35999
|
+
(m, k, sep, v) => looksLikeCode(k, v) ? m : `${k}${sep}[REDACTED]`
|
|
36000
|
+
],
|
|
35986
36001
|
// Any environment variable named *_KEY. Upper case only: in code, primary_key,
|
|
35987
36002
|
// sort_key and idempotency_key name things, not secrets.
|
|
35988
36003
|
[/\b([A-Z0-9_]+_KEY)(\s*=\s*["']?)([^\s"',;)]{8,})/g, (_m, k, sep, _v) => `${k}${sep}[REDACTED]`]
|
|
@@ -36067,6 +36082,10 @@ function walkDir(root, dir, onFile, totals, skipped) {
|
|
|
36067
36082
|
}
|
|
36068
36083
|
for (const item of items) {
|
|
36069
36084
|
const child = path4.join(dir, item.name);
|
|
36085
|
+
if (item.name === ".git" || item.name === "node_modules") {
|
|
36086
|
+
skipped.push({ source: child, reason: "ignored" });
|
|
36087
|
+
continue;
|
|
36088
|
+
}
|
|
36070
36089
|
if (item.isSymbolicLink()) {
|
|
36071
36090
|
skipped.push({ source: child, reason: "symlink" });
|
|
36072
36091
|
continue;
|
|
@@ -36083,12 +36102,73 @@ function walkDir(root, dir, onFile, totals, skipped) {
|
|
|
36083
36102
|
}
|
|
36084
36103
|
}
|
|
36085
36104
|
}
|
|
36105
|
+
function byPathParts(a, b) {
|
|
36106
|
+
const x = a.split("/");
|
|
36107
|
+
const y = b.split("/");
|
|
36108
|
+
for (let i = 0; i < Math.min(x.length, y.length); i++) if (x[i] !== y[i]) return x[i] < y[i] ? -1 : 1;
|
|
36109
|
+
return x.length - y.length;
|
|
36110
|
+
}
|
|
36111
|
+
function walkGit(dir, exposedAs, onFile, totals, skipped) {
|
|
36112
|
+
let listed;
|
|
36113
|
+
let ignored;
|
|
36114
|
+
try {
|
|
36115
|
+
listed = [...new Set(nulSplit(git(dir, ["ls-files", "-z", "--cached", "--others", "--exclude-standard"])))];
|
|
36116
|
+
ignored = nulSplit(git(dir, ["ls-files", "-z", "--others", "--ignored", "--exclude-standard", "--directory"]));
|
|
36117
|
+
} catch {
|
|
36118
|
+
return false;
|
|
36119
|
+
}
|
|
36120
|
+
if (listed.length === 0) return false;
|
|
36121
|
+
for (const p of ignored) skipped.push({ source: path4.join(dir, p), reason: "ignored" });
|
|
36122
|
+
if (fs3.existsSync(path4.join(dir, ".git"))) skipped.push({ source: path4.join(dir, ".git"), reason: "ignored" });
|
|
36123
|
+
const realDir = fs3.realpathSync(dir);
|
|
36124
|
+
const protectedDirs = /* @__PURE__ */ new Set();
|
|
36125
|
+
for (const rel of listed.sort(byPathParts)) {
|
|
36126
|
+
const abs = path4.join(dir, rel);
|
|
36127
|
+
const parts = rel.split("/");
|
|
36128
|
+
const guarded = parts.slice(0, -1).map((_, i) => path4.join(dir, ...parts.slice(0, i + 1))).find(isProtectedPath);
|
|
36129
|
+
if (guarded) {
|
|
36130
|
+
if (!protectedDirs.has(guarded)) skipped.push({ source: guarded, reason: "protected" });
|
|
36131
|
+
protectedDirs.add(guarded);
|
|
36132
|
+
continue;
|
|
36133
|
+
}
|
|
36134
|
+
if (isProtectedPath(abs)) {
|
|
36135
|
+
skipped.push({ source: abs, reason: "protected" });
|
|
36136
|
+
continue;
|
|
36137
|
+
}
|
|
36138
|
+
let stat;
|
|
36139
|
+
try {
|
|
36140
|
+
stat = fs3.lstatSync(abs);
|
|
36141
|
+
} catch {
|
|
36142
|
+
continue;
|
|
36143
|
+
}
|
|
36144
|
+
if (stat.isDirectory()) {
|
|
36145
|
+
skipped.push({ source: abs, reason: "submodule" });
|
|
36146
|
+
continue;
|
|
36147
|
+
}
|
|
36148
|
+
const inside = (() => {
|
|
36149
|
+
try {
|
|
36150
|
+
return fs3.realpathSync(abs).startsWith(realDir + path4.sep);
|
|
36151
|
+
} catch {
|
|
36152
|
+
return false;
|
|
36153
|
+
}
|
|
36154
|
+
})();
|
|
36155
|
+
if (stat.isSymbolicLink() || !inside) {
|
|
36156
|
+
skipped.push({ source: abs, reason: "symlink" });
|
|
36157
|
+
continue;
|
|
36158
|
+
}
|
|
36159
|
+
if (!stat.isFile()) continue;
|
|
36160
|
+
onFile(abs, path4.normalize(rel));
|
|
36161
|
+
checkBudget(totals);
|
|
36162
|
+
}
|
|
36163
|
+
return true;
|
|
36164
|
+
}
|
|
36086
36165
|
function walkEntries(exposePaths, onFile) {
|
|
36087
36166
|
const totals = { files: 0, bytes: 0 };
|
|
36088
36167
|
const skipped = [];
|
|
36089
36168
|
const entries = exposePaths.map((p, i) => {
|
|
36090
36169
|
const { source, index } = typeof p === "string" ? { source: p, index: i } : p;
|
|
36091
36170
|
const stat = statEntry(source);
|
|
36171
|
+
const exposedAs = `workspace/${index}-${path4.basename(source)}`;
|
|
36092
36172
|
const before = { files: totals.files, bytes: totals.bytes };
|
|
36093
36173
|
const paths = [];
|
|
36094
36174
|
if (isProtectedPath(source)) {
|
|
@@ -36097,18 +36177,17 @@ function walkEntries(exposePaths, onFile) {
|
|
|
36097
36177
|
onFile(source, "", index, source, totals);
|
|
36098
36178
|
checkBudget(totals);
|
|
36099
36179
|
} else {
|
|
36100
|
-
|
|
36180
|
+
const add = (abs, rel) => {
|
|
36101
36181
|
paths.push(rel.split(path4.sep).join("/"));
|
|
36102
36182
|
onFile(abs, rel, index, source, totals);
|
|
36103
|
-
}
|
|
36183
|
+
};
|
|
36184
|
+
walkGit(source, exposedAs, add, totals, skipped) || walkDir(source, source, add, totals, skipped);
|
|
36104
36185
|
}
|
|
36105
36186
|
return {
|
|
36106
36187
|
index,
|
|
36107
36188
|
source,
|
|
36108
36189
|
kind: stat.isDirectory() ? "directory" : "file",
|
|
36109
|
-
|
|
36110
|
-
// util.mjs) stay apart, and the brief can name each one exactly.
|
|
36111
|
-
exposedAs: `workspace/${index}-${path4.basename(source)}`,
|
|
36190
|
+
exposedAs,
|
|
36112
36191
|
files: totals.files - before.files,
|
|
36113
36192
|
bytes: totals.bytes - before.bytes,
|
|
36114
36193
|
paths: paths.sort()
|
|
@@ -36501,6 +36580,8 @@ function renderExposeManifest(manifest) {
|
|
|
36501
36580
|
const skipped = [
|
|
36502
36581
|
count("symlink") ? `${count("symlink")} symlink(s), or path(s) through one, were skipped, not followed.` : null,
|
|
36503
36582
|
count("protected") ? `${count("protected")} credential or history path(s) were left out on purpose.` : null,
|
|
36583
|
+
count("ignored") ? `${count("ignored")} path(s) (.git, node_modules, whatever .gitignore excludes) were left out as not source.` : null,
|
|
36584
|
+
count("submodule") ? `${count("submodule")} submodule(s) or nested repositor(ies) were left out; their files are not copied.` : null,
|
|
36504
36585
|
count("gone") ? `No longer there, so not copied again: ${named("gone")}.` : null,
|
|
36505
36586
|
count("masked") ? `Not copied again, because the record of the earlier round masked the path: ${named("masked")}.` : null,
|
|
36506
36587
|
count("replaced") ? `The earlier round's change is replaced by the diff given in this round: ${named("replaced")}.` : null
|
|
@@ -36517,6 +36598,25 @@ function renderExposeManifest(manifest) {
|
|
|
36517
36598
|
].join("\n");
|
|
36518
36599
|
}
|
|
36519
36600
|
var clip = (s2, max = 400) => s2.length > max ? `${s2.slice(0, max)}...` : s2;
|
|
36601
|
+
var HISTORY_CHARS = 4e4;
|
|
36602
|
+
function renderRound(p) {
|
|
36603
|
+
const tag = (id2) => `r${p.round}.${id2}`;
|
|
36604
|
+
const verdict = (x) => x.verdict ? ` -- lead's verdict: ${x.verdict}${x.effect ? ` (${clip(x.effect)})` : ""}` : "";
|
|
36605
|
+
const list = (title, items, line) => items?.length ? [title, ...items.map(line)].join("\n") : null;
|
|
36606
|
+
return [
|
|
36607
|
+
`### Round ${p.round} (${p.mode})`,
|
|
36608
|
+
`Question asked: ${p.question}`,
|
|
36609
|
+
`Your answer, summarised: ${p.summary}`,
|
|
36610
|
+
list(
|
|
36611
|
+
"Points you raised:",
|
|
36612
|
+
p.findings,
|
|
36613
|
+
(f) => `- ${tag(f.id)}: ${clip(f.point)}${verdict(f)}${f.grounds ? `
|
|
36614
|
+
grounds: ${clip(f.grounds)}` : ""}`
|
|
36615
|
+
),
|
|
36616
|
+
list("Unknowns you named:", p.unknowns, (u) => `- ${tag(u.id)}: ${clip(u.point)}${verdict(u)}`),
|
|
36617
|
+
list("Checks you proposed:", p.next_checks, (c) => `- ${tag(c.id)}: ${clip(c.point)}${verdict(c)}`)
|
|
36618
|
+
].filter(Boolean).join("\n");
|
|
36619
|
+
}
|
|
36520
36620
|
function renderBrief(req, chain = { round: 1, priorRounds: [] }, exposeManifest = null) {
|
|
36521
36621
|
const parts = [];
|
|
36522
36622
|
parts.push(renderGuardrails(Boolean(exposeManifest), req.network));
|
|
@@ -36545,19 +36645,19 @@ function renderBrief(req, chain = { round: 1, priorRounds: [] }, exposeManifest
|
|
|
36545
36645
|
if (chain.priorRounds && chain.priorRounds.length) {
|
|
36546
36646
|
parts.push("");
|
|
36547
36647
|
parts.push("## Earlier rounds of this same consultation");
|
|
36548
|
-
|
|
36549
|
-
|
|
36550
|
-
|
|
36551
|
-
|
|
36552
|
-
|
|
36553
|
-
|
|
36554
|
-
|
|
36555
|
-
|
|
36556
|
-
|
|
36557
|
-
|
|
36558
|
-
|
|
36559
|
-
|
|
36560
|
-
|
|
36648
|
+
parts.push(
|
|
36649
|
+
"Each point carries the lead's own check where there is one -- confirmed: it held; not_applicable: checked, and it does not apply here; unverifiable: could not be checked; unverified, or no verdict: not checked yet."
|
|
36650
|
+
);
|
|
36651
|
+
const blocks = chain.priorRounds.map(renderRound);
|
|
36652
|
+
let kept = 0;
|
|
36653
|
+
let used = 0;
|
|
36654
|
+
for (let i = blocks.length - 1; i >= 0 && (kept === 0 || used + blocks[i].length <= HISTORY_CHARS); i--) {
|
|
36655
|
+
used += blocks[i].length;
|
|
36656
|
+
kept += 1;
|
|
36657
|
+
}
|
|
36658
|
+
const left = chain.priorRounds.slice(0, blocks.length - kept).map((p) => p.round);
|
|
36659
|
+
if (left.length) parts.push(`(Round${left.length > 1 ? "s" : ""} ${left.join(", ")} left out to keep this brief within its size.)`);
|
|
36660
|
+
parts.push(...blocks.slice(blocks.length - kept));
|
|
36561
36661
|
parts.push("");
|
|
36562
36662
|
parts.push(
|
|
36563
36663
|
"This round is a follow-up. The lead has narrowed the exchange to the divergences above. Do not restate your earlier answer; address what is new, and say plainly if the new information changes or does not change your position."
|
|
@@ -36981,10 +37081,10 @@ function runChild({
|
|
|
36981
37081
|
});
|
|
36982
37082
|
}
|
|
36983
37083
|
const done = new Promise((resolve) => {
|
|
36984
|
-
let
|
|
37084
|
+
let settled2 = false;
|
|
36985
37085
|
const finish = (payload) => {
|
|
36986
|
-
if (
|
|
36987
|
-
|
|
37086
|
+
if (settled2) return;
|
|
37087
|
+
settled2 = true;
|
|
36988
37088
|
clearTimeout(timer);
|
|
36989
37089
|
clearTimeout(settleTimer);
|
|
36990
37090
|
resolve({ stdout, stderr, truncated, timedOut, cancelled, stalled, ...payload });
|
|
@@ -37371,9 +37471,10 @@ function interpret({ stdout, stderr, code, lastMessageText }) {
|
|
|
37371
37471
|
if (typeof candidate === "string" && candidate.trim()) text2 = candidate.trim();
|
|
37372
37472
|
}
|
|
37373
37473
|
}
|
|
37374
|
-
|
|
37375
|
-
|
|
37376
|
-
|
|
37474
|
+
const errorMsg = errorEvents.map((e) => e.message ?? e.error?.message ?? JSON.stringify(e)).join(" | ");
|
|
37475
|
+
const recovered = errorEvents.length > 0 && Boolean(text2) && events3.at(-1)?.type === "turn.completed";
|
|
37476
|
+
if (errorEvents.length && !recovered) {
|
|
37477
|
+
return { ok: false, failureKind: classifyMessage(errorMsg), message: errorMsg, usageRaw: usage, events: events3.length };
|
|
37377
37478
|
}
|
|
37378
37479
|
if (!text2) {
|
|
37379
37480
|
const msg = (stderr || "").trim() || `codex exited with code ${code} and produced no final message`;
|
|
@@ -37385,7 +37486,13 @@ function interpret({ stdout, stderr, code, lastMessageText }) {
|
|
|
37385
37486
|
events: events3.length
|
|
37386
37487
|
};
|
|
37387
37488
|
}
|
|
37388
|
-
return {
|
|
37489
|
+
return {
|
|
37490
|
+
ok: true,
|
|
37491
|
+
text: text2,
|
|
37492
|
+
usageRaw: usage,
|
|
37493
|
+
events: events3.length,
|
|
37494
|
+
recoveredError: recovered ? { failureKind: classifyMessage(errorMsg), message: errorMsg } : null
|
|
37495
|
+
};
|
|
37389
37496
|
}
|
|
37390
37497
|
function progress({ stdout }) {
|
|
37391
37498
|
const items = [];
|
|
@@ -38185,11 +38292,11 @@ async function prepare() {
|
|
|
38185
38292
|
if (standaloneSupport !== null) return;
|
|
38186
38293
|
if (!probePromise) {
|
|
38187
38294
|
probePromise = new Promise((resolve) => {
|
|
38188
|
-
let
|
|
38295
|
+
let settled2 = false;
|
|
38189
38296
|
let timer = null;
|
|
38190
38297
|
const finishProbe = (supported, cache) => {
|
|
38191
|
-
if (
|
|
38192
|
-
|
|
38298
|
+
if (settled2) return;
|
|
38299
|
+
settled2 = true;
|
|
38193
38300
|
if (timer) clearTimeout(timer);
|
|
38194
38301
|
if (cache) standaloneSupport = Boolean(supported);
|
|
38195
38302
|
else probePromise = null;
|
|
@@ -38630,7 +38737,17 @@ var JobManager = class {
|
|
|
38630
38737
|
if (configProblem()) throw new RequestError(`the severally config file needs fixing first: ${configProblem()}`, "config_invalid");
|
|
38631
38738
|
const followupTo = typeof rawRequest?.followup_to === "string" && rawRequest.followup_to.trim() ? rawRequest.followup_to.trim() : null;
|
|
38632
38739
|
let parent = null;
|
|
38633
|
-
|
|
38740
|
+
let fromGroup = null;
|
|
38741
|
+
if (followupTo?.startsWith("group_")) {
|
|
38742
|
+
if ((rawRequest.target ?? null) !== null || rawRequest.targets?.length) {
|
|
38743
|
+
throw new RequestError(
|
|
38744
|
+
"a follow-up to a group goes to every member that can take one, so leave target and targets out; to follow up one member, pass its job_id",
|
|
38745
|
+
"followup_group_targets"
|
|
38746
|
+
);
|
|
38747
|
+
}
|
|
38748
|
+
fromGroup = this.#groupParents(followupTo);
|
|
38749
|
+
rawRequest = { ...rawRequest, targets: fromGroup.links.map((l) => l.parent.target) };
|
|
38750
|
+
} else if (followupTo) {
|
|
38634
38751
|
parent = this.jobs.get(followupTo) ?? loadRound(followupTo);
|
|
38635
38752
|
if (!parent) throw new RequestError(`followup_to references unknown job "${followupTo}"`, "unknown_job");
|
|
38636
38753
|
if (parent.status !== "completed") {
|
|
@@ -38649,7 +38766,17 @@ var JobManager = class {
|
|
|
38649
38766
|
}
|
|
38650
38767
|
parent = { ...parent, nextRound: Math.max(...rounds.map((j) => j.round)) + 1 };
|
|
38651
38768
|
}
|
|
38652
|
-
const req = parseRequest(rawRequest, {
|
|
38769
|
+
const req = parseRequest(rawRequest, {
|
|
38770
|
+
isFollowup: Boolean(parent || fromGroup),
|
|
38771
|
+
parentMode: parent?.mode ?? fromGroup?.group.mode ?? null,
|
|
38772
|
+
groupFollowup: Boolean(fromGroup)
|
|
38773
|
+
});
|
|
38774
|
+
if (fromGroup && fromGroup.links.some((l, i) => l.parent.target !== req.targets[i])) {
|
|
38775
|
+
throw new RequestError(
|
|
38776
|
+
`the members of group "${followupTo}" no longer resolve to the consultants that answered them; follow up each member by its job_id`,
|
|
38777
|
+
"followup_target_mismatch"
|
|
38778
|
+
);
|
|
38779
|
+
}
|
|
38653
38780
|
if (parent && parent.target !== req.target) {
|
|
38654
38781
|
throw new RequestError(
|
|
38655
38782
|
`follow-up must go to the same consultant as job "${parent.job_id}" (${parent.target})`,
|
|
@@ -38657,7 +38784,7 @@ var JobManager = class {
|
|
|
38657
38784
|
);
|
|
38658
38785
|
}
|
|
38659
38786
|
const round = parent ? parent.nextRound : 1;
|
|
38660
|
-
if (round > POLICY.maxRounds) {
|
|
38787
|
+
if (!fromGroup && round > POLICY.maxRounds) {
|
|
38661
38788
|
throw new RequestError(
|
|
38662
38789
|
`this consultation has used its ${POLICY.maxRounds} rounds (1 initial + ${POLICY.maxRounds - 1} follow-ups); decide with what you have, or start a new consultation with a materially different question`,
|
|
38663
38790
|
"round_limit"
|
|
@@ -38680,13 +38807,46 @@ var JobManager = class {
|
|
|
38680
38807
|
);
|
|
38681
38808
|
}
|
|
38682
38809
|
try {
|
|
38683
|
-
|
|
38810
|
+
const links = fromGroup?.links ?? req.targets.map(() => ({ parent, round }));
|
|
38811
|
+
return this.#admitted(req, links, seats, fromGroup);
|
|
38684
38812
|
} catch (err) {
|
|
38685
38813
|
for (const seat of seats) if (!this.jobs.has(seat.job_id)) releaseSeat(seat.job_id);
|
|
38686
38814
|
throw err;
|
|
38687
38815
|
}
|
|
38688
38816
|
}
|
|
38689
|
-
|
|
38817
|
+
// A follow-up to a fan-out: each member goes on from the newest answered
|
|
38818
|
+
// round of its own chain. A member that is still running, never answered or
|
|
38819
|
+
// has used its rounds is skipped and named; if that is every member, the
|
|
38820
|
+
// request is refused.
|
|
38821
|
+
#groupParents(groupId) {
|
|
38822
|
+
const group = this.groups.get(groupId) ?? loadGroup(groupId);
|
|
38823
|
+
if (!group) throw new RequestError(`followup_to references unknown group "${groupId}"`, "unknown_group");
|
|
38824
|
+
const links = [];
|
|
38825
|
+
const skipped = [];
|
|
38826
|
+
for (const jid of group.job_ids) {
|
|
38827
|
+
const member = this.jobs.get(jid) ?? loadRound(jid);
|
|
38828
|
+
if (!member) {
|
|
38829
|
+
skipped.push({ job_id: jid, target: null, reason: "not found: aged out of the history, or still running in another severally process" });
|
|
38830
|
+
continue;
|
|
38831
|
+
}
|
|
38832
|
+
const rounds = this.#chainRounds(member.chain_id);
|
|
38833
|
+
const latest = rounds.filter((j) => !["failed", "cancelled"].includes(j.status)).sort((a, b) => b.round - a.round)[0];
|
|
38834
|
+
const nextRound = Math.max(...rounds.map((j) => j.round)) + 1;
|
|
38835
|
+
const consultant = consultantByName(member.target);
|
|
38836
|
+
const reason = !latest ? `${member.status}, so there is no answer to follow up` : latest.status !== "completed" ? `round ${latest.round} is still ${latest.status}` : nextRound > POLICY.maxRounds ? `has used its ${POLICY.maxRounds} rounds` : !consultant || !consultantAvailable(consultant) ? "this consultant is no longer configured or available here" : null;
|
|
38837
|
+
if (reason) skipped.push({ job_id: (latest ?? member).job_id, target: member.target, reason });
|
|
38838
|
+
else links.push({ parent: { ...latest, nextRound }, round: nextRound, groupMember: member });
|
|
38839
|
+
}
|
|
38840
|
+
if (!links.length) {
|
|
38841
|
+
throw new RequestError(
|
|
38842
|
+
`no member of group "${groupId}" can take a follow-up: ${skipped.map((x) => `${x.target ?? x.job_id} (${x.reason})`).join(", ")}`,
|
|
38843
|
+
"followup_nothing_to_continue"
|
|
38844
|
+
);
|
|
38845
|
+
}
|
|
38846
|
+
return { group, links, skipped };
|
|
38847
|
+
}
|
|
38848
|
+
#admitted(req, links, seats, fromGroup = null) {
|
|
38849
|
+
const parent = links[0].groupMember ?? links[0].parent;
|
|
38690
38850
|
const groupId = id("group");
|
|
38691
38851
|
let exposeManifest = null;
|
|
38692
38852
|
const parentEntries = req.context.expose_from_parent ? parent.brief?.expose_paths?.entries ?? [] : [];
|
|
@@ -38737,13 +38897,14 @@ var JobManager = class {
|
|
|
38737
38897
|
req._snapshot = path11.join(snapshot, "workspace");
|
|
38738
38898
|
}
|
|
38739
38899
|
const members2 = req.targets.map((target, i) => {
|
|
38740
|
-
const
|
|
38900
|
+
const { parent: from, round: round2 } = links[i];
|
|
38901
|
+
const chainId = from ? from.chain_id : id("chain");
|
|
38741
38902
|
const jobId = seats[i].job_id;
|
|
38742
38903
|
const job = {
|
|
38743
38904
|
job_id: jobId,
|
|
38744
38905
|
chain_id: chainId,
|
|
38745
38906
|
group_id: groupId,
|
|
38746
|
-
round,
|
|
38907
|
+
round: round2,
|
|
38747
38908
|
target,
|
|
38748
38909
|
mode: req.mode,
|
|
38749
38910
|
// Redacted here, at the one point the request text becomes state:
|
|
@@ -38769,7 +38930,7 @@ var JobManager = class {
|
|
|
38769
38930
|
retries: [],
|
|
38770
38931
|
partial_output: null,
|
|
38771
38932
|
tool_calls: null,
|
|
38772
|
-
followup_to:
|
|
38933
|
+
followup_to: from?.job_id ?? null,
|
|
38773
38934
|
status: "queued",
|
|
38774
38935
|
// What this consultant is: the CLI it runs on, the model and its
|
|
38775
38936
|
// lineage, as the operator defined it.
|
|
@@ -38809,19 +38970,21 @@ var JobManager = class {
|
|
|
38809
38970
|
persistGroup(group);
|
|
38810
38971
|
} catch {
|
|
38811
38972
|
}
|
|
38812
|
-
const chain = { round, priorRounds: this.#chainHistory(members2[0].chain_id) };
|
|
38813
|
-
const brief = renderBrief(req, chain, exposeManifest);
|
|
38814
38973
|
for (const job of members2) {
|
|
38974
|
+
const chain = { round: job.round, priorRounds: this.#chainHistory(job.chain_id) };
|
|
38975
|
+
const brief = renderBrief(req, chain, exposeManifest);
|
|
38815
38976
|
job.promise = this.#execute(job, { ...req, target: job.target }, chain, brief).catch((err) => {
|
|
38816
38977
|
this.#fail(job, "cli_error", err?.message ?? String(err));
|
|
38817
38978
|
});
|
|
38818
38979
|
}
|
|
38819
38980
|
if (req._snapshot) cleanupJobDir(groupId);
|
|
38981
|
+
const { round } = members2[0];
|
|
38982
|
+
const sameRound = members2.every((m) => m.round === round);
|
|
38820
38983
|
const common = {
|
|
38821
38984
|
group_id: groupId,
|
|
38822
|
-
round,
|
|
38985
|
+
...sameRound ? { round, rounds_remaining: POLICY.maxRounds - round } : {},
|
|
38986
|
+
...fromGroup ? { followup_to_group: fromGroup.group.group_id, skipped: fromGroup.skipped } : {},
|
|
38823
38987
|
...parentEntries.length ? { exposed_from_parent: inheritedSummary(exposeManifest) } : {},
|
|
38824
|
-
rounds_remaining: POLICY.maxRounds - round,
|
|
38825
38988
|
mode: req.mode,
|
|
38826
38989
|
// Counted, not refused: a brief of facts alone is a legitimate
|
|
38827
38990
|
// consultation, but one about code that hands over no file is one the
|
|
@@ -38850,16 +39013,22 @@ var JobManager = class {
|
|
|
38850
39013
|
...common,
|
|
38851
39014
|
question: group.question,
|
|
38852
39015
|
// the redacted text every member was actually sent
|
|
38853
|
-
jobs: members2.map((m) => ({
|
|
39016
|
+
jobs: members2.map((m) => ({
|
|
39017
|
+
job_id: m.job_id,
|
|
39018
|
+
chain_id: m.chain_id,
|
|
39019
|
+
target: m.target,
|
|
39020
|
+
cli: m.cli,
|
|
39021
|
+
model: m.model,
|
|
39022
|
+
...sameRound ? {} : { round: m.round, rounds_remaining: POLICY.maxRounds - m.round }
|
|
39023
|
+
})),
|
|
38854
39024
|
poll_with: `consult_get({ group_id: "${groupId}" })`,
|
|
38855
39025
|
note: "Every consultant got the identical brief. When they come back, compare the grounds behind the points that differ; matching summaries are not evidence of agreement."
|
|
38856
39026
|
};
|
|
38857
39027
|
}
|
|
38858
39028
|
// The rounds before a follow-up, from the history on disk and from memory
|
|
38859
39029
|
// (memory wins: it can hold a verdict written since the round was saved).
|
|
38860
|
-
// Each
|
|
38861
|
-
// knowing which of its points were checked and how they came out.
|
|
38862
|
-
// finding that carries a verdict is passed; of the rest, the first five.
|
|
39030
|
+
// Each point goes with the lead's verdict on it, so the consultant answers
|
|
39031
|
+
// knowing which of its points were checked and how they came out.
|
|
38863
39032
|
#chainRounds(chainId) {
|
|
38864
39033
|
const byRound = new Map(loadChain(chainId).map((j) => [j.round, j]));
|
|
38865
39034
|
for (const j of this.jobs.values()) if (j.chain_id === chainId) byRound.set(j.round, j);
|
|
@@ -38868,9 +39037,22 @@ var JobManager = class {
|
|
|
38868
39037
|
#chainHistory(chainId) {
|
|
38869
39038
|
return this.#chainRounds(chainId).filter((j) => j.status === "completed" && j.result).sort((a, b) => a.round - b.round).map((j) => {
|
|
38870
39039
|
const verdicts = new Map((j.record?.entries ?? []).map((e) => [e.id, e]));
|
|
38871
|
-
|
|
38872
|
-
|
|
38873
|
-
|
|
39040
|
+
const ordered = (items, shape) => [
|
|
39041
|
+
...items.filter((x) => verdicts.has(x.id)),
|
|
39042
|
+
...items.filter((x) => !verdicts.has(x.id))
|
|
39043
|
+
].map((x) => ({ id: x.id, ...shape(x), verdict: verdicts.get(x.id)?.verdict ?? null, effect: verdicts.get(x.id)?.effect ?? null }));
|
|
39044
|
+
const checked = (x) => x.verdict !== null;
|
|
39045
|
+
const findings = ordered(j.result.findings, (f) => ({ point: f.point, grounds: f.grounds ?? null }));
|
|
39046
|
+
return {
|
|
39047
|
+
round: j.round,
|
|
39048
|
+
mode: j.mode,
|
|
39049
|
+
question: j.question,
|
|
39050
|
+
summary: j.result.summary,
|
|
39051
|
+
// Every checked finding; of the rest, the first five.
|
|
39052
|
+
findings: [...findings.filter(checked), ...findings.filter((f) => !checked(f)).slice(0, 5)],
|
|
39053
|
+
unknowns: ordered(j.result.unknowns, (u) => ({ point: u.item })).slice(0, 5),
|
|
39054
|
+
next_checks: ordered(j.result.next_checks, (c) => ({ point: c.check })).slice(0, 5)
|
|
39055
|
+
};
|
|
38874
39056
|
});
|
|
38875
39057
|
}
|
|
38876
39058
|
#evict() {
|
|
@@ -38996,6 +39178,10 @@ var JobManager = class {
|
|
|
38996
39178
|
}
|
|
38997
39179
|
const lastMessageText = invocation.lastMessagePath ? readIfExists(invocation.lastMessagePath) : "";
|
|
38998
39180
|
interpreted = adapter.interpret({ ...run, lastMessageText });
|
|
39181
|
+
const { recoveredError } = interpreted;
|
|
39182
|
+
if (interpreted.ok && recoveredError && !parses(interpreted.text)) {
|
|
39183
|
+
interpreted = { ok: false, ...recoveredError, usageRaw: interpreted.usageRaw };
|
|
39184
|
+
}
|
|
38999
39185
|
if (interpreted.ok) break;
|
|
39000
39186
|
if (run.stalled && !interpreted.usageRaw) {
|
|
39001
39187
|
return this.#fail(
|
|
@@ -39059,7 +39245,11 @@ var JobManager = class {
|
|
|
39059
39245
|
}
|
|
39060
39246
|
}
|
|
39061
39247
|
job.result = normalized.result;
|
|
39062
|
-
const notes = [
|
|
39248
|
+
const notes = [
|
|
39249
|
+
adviceCaveat(normalized),
|
|
39250
|
+
sameVendorCaveat(job.caller, job.caller_model, consultant),
|
|
39251
|
+
interpreted.recoveredError && `the consultant's CLI reported an error mid-run and then finished its answer: ${redact(interpreted.recoveredError.message).slice(0, 300)}`
|
|
39252
|
+
].filter(Boolean);
|
|
39063
39253
|
job.quality = {
|
|
39064
39254
|
...normalized.quality,
|
|
39065
39255
|
evidence_basis: normalized.result.evidence_basis,
|
|
@@ -39248,32 +39438,32 @@ var JobManager = class {
|
|
|
39248
39438
|
message: `the verdict for "${job.job_id}" could not be written to the history (${e.message}); record it again`
|
|
39249
39439
|
};
|
|
39250
39440
|
}
|
|
39441
|
+
const summary = job.record && recordSummary(job);
|
|
39251
39442
|
return {
|
|
39252
39443
|
job_id: job.job_id,
|
|
39253
39444
|
chain_id: job.chain_id,
|
|
39254
39445
|
round: job.round,
|
|
39255
|
-
record:
|
|
39256
|
-
|
|
39257
|
-
reflection: job.reflection ?? null
|
|
39446
|
+
record: summary ? { updated_at: summary.updated_at, verdicts: summary.verdicts, coverage: summary.coverage } : null,
|
|
39447
|
+
reflection_saved: job.reflection != null
|
|
39258
39448
|
};
|
|
39259
39449
|
}
|
|
39260
|
-
async wait(jobId, waitMs, opts) {
|
|
39450
|
+
async wait(jobId, waitMs, opts, signal) {
|
|
39261
39451
|
const job = this.jobs.get(jobId);
|
|
39262
|
-
if (!job) return this.#waitOnDisk(() => this.view(jobId, opts), waitMs);
|
|
39452
|
+
if (!job) return this.#waitOnDisk(() => this.view(jobId, opts), waitMs, signal);
|
|
39263
39453
|
const budget = Math.min(Math.max(0, waitMs ?? 0), POLICY.maxWaitMs);
|
|
39264
39454
|
if (budget === 0 || job.status === "completed" || job.status === "failed" || job.status === "cancelled") {
|
|
39265
39455
|
return this.view(jobId, opts);
|
|
39266
39456
|
}
|
|
39267
|
-
await
|
|
39457
|
+
await settled(job.promise, budget, signal);
|
|
39268
39458
|
return this.view(jobId, opts);
|
|
39269
39459
|
}
|
|
39270
39460
|
// A consultation another process runs has no promise here, so its history
|
|
39271
39461
|
// is re-read once a second until it is no longer running or the wait is up.
|
|
39272
|
-
async #waitOnDisk(read, waitMs) {
|
|
39462
|
+
async #waitOnDisk(read, waitMs, signal) {
|
|
39273
39463
|
const until = Date.now() + Math.min(Math.max(0, waitMs ?? 0), POLICY.maxWaitMs);
|
|
39274
39464
|
let view = read();
|
|
39275
|
-
while (view && ["running", "queued"].includes(view.status) && Date.now() < until) {
|
|
39276
|
-
await
|
|
39465
|
+
while (view && ["running", "queued"].includes(view.status) && Date.now() < until && !signal?.aborted) {
|
|
39466
|
+
await settled(null, Math.min(1e3, until - Date.now()), signal);
|
|
39277
39467
|
view = read();
|
|
39278
39468
|
}
|
|
39279
39469
|
return view;
|
|
@@ -39347,7 +39537,8 @@ var JobManager = class {
|
|
|
39347
39537
|
// the number of consultants configured rather than asked.
|
|
39348
39538
|
limits: live || !full ? null : limitsSummary()
|
|
39349
39539
|
};
|
|
39350
|
-
|
|
39540
|
+
const anyFinished = members2.some((m) => ["completed", "failed", "cancelled"].includes(m.status));
|
|
39541
|
+
if (anyFinished) view.comparison = comparison(members2, full ? Infinity : GROUP_CHARS - printed(view));
|
|
39351
39542
|
return view;
|
|
39352
39543
|
}
|
|
39353
39544
|
groupView(groupId, opts) {
|
|
@@ -39355,16 +39546,13 @@ var JobManager = class {
|
|
|
39355
39546
|
if (!group) return null;
|
|
39356
39547
|
return this.#shapeGroup(group, group.job_ids.map((jid) => this.view(jid, opts)).filter(Boolean), opts);
|
|
39357
39548
|
}
|
|
39358
|
-
async waitGroup(groupId, waitMs, opts) {
|
|
39549
|
+
async waitGroup(groupId, waitMs, opts, signal) {
|
|
39359
39550
|
const group = this.groups.get(groupId);
|
|
39360
|
-
if (!group) return this.#waitOnDisk(() => this.groupView(groupId, opts), waitMs);
|
|
39551
|
+
if (!group) return this.#waitOnDisk(() => this.groupView(groupId, opts), waitMs, signal);
|
|
39361
39552
|
const budget = Math.min(Math.max(0, waitMs ?? 0), POLICY.maxWaitMs);
|
|
39362
39553
|
if (budget > 0) {
|
|
39363
39554
|
const promises = group.job_ids.map((jid) => this.jobs.get(jid)?.promise).filter(Boolean);
|
|
39364
|
-
await Promise.
|
|
39365
|
-
Promise.all(promises),
|
|
39366
|
-
new Promise((r) => setTimeout(r, budget).unref?.())
|
|
39367
|
-
]);
|
|
39555
|
+
await settled(Promise.all(promises), budget, signal);
|
|
39368
39556
|
}
|
|
39369
39557
|
return this.groupView(groupId, opts);
|
|
39370
39558
|
}
|
|
@@ -39403,11 +39591,11 @@ var JobManager = class {
|
|
|
39403
39591
|
job._handle.stop();
|
|
39404
39592
|
}
|
|
39405
39593
|
}
|
|
39406
|
-
const
|
|
39407
|
-
await Promise.race([
|
|
39594
|
+
const settled2 = Promise.all(live.map((j) => j.promise).filter(Boolean));
|
|
39595
|
+
await Promise.race([settled2, new Promise((r) => setTimeout(r, graceMs).unref?.())]);
|
|
39408
39596
|
for (const job of live) if (job._handle) handles.push(job._handle);
|
|
39409
39597
|
for (const h of new Set(handles)) h.forceKill();
|
|
39410
|
-
await Promise.race([
|
|
39598
|
+
await Promise.race([settled2, new Promise((r) => setTimeout(r, 1e3).unref?.())]);
|
|
39411
39599
|
}
|
|
39412
39600
|
/** The synchronous last resort for process exit: no waiting is possible there. */
|
|
39413
39601
|
killNow() {
|
|
@@ -39449,6 +39637,31 @@ function partialOutput(run) {
|
|
|
39449
39637
|
note: "raw output stream of a consultant that was stopped at its time budget; unparsed and unverified, not an answer"
|
|
39450
39638
|
};
|
|
39451
39639
|
}
|
|
39640
|
+
async function settled(promise2, ms, signal) {
|
|
39641
|
+
let timer;
|
|
39642
|
+
let stop;
|
|
39643
|
+
const until = new Promise((resolve) => {
|
|
39644
|
+
stop = resolve;
|
|
39645
|
+
timer = setTimeout(resolve, ms);
|
|
39646
|
+
if (signal?.aborted) resolve();
|
|
39647
|
+
else signal?.addEventListener("abort", resolve, { once: true });
|
|
39648
|
+
});
|
|
39649
|
+
try {
|
|
39650
|
+
await (promise2 ? Promise.race([promise2, until]) : until);
|
|
39651
|
+
} finally {
|
|
39652
|
+
clearTimeout(timer);
|
|
39653
|
+
signal?.removeEventListener("abort", stop);
|
|
39654
|
+
}
|
|
39655
|
+
}
|
|
39656
|
+
function parses(text2) {
|
|
39657
|
+
try {
|
|
39658
|
+
normalizeResult(text2);
|
|
39659
|
+
return true;
|
|
39660
|
+
} catch (err) {
|
|
39661
|
+
if (err instanceof OutputError) return false;
|
|
39662
|
+
throw err;
|
|
39663
|
+
}
|
|
39664
|
+
}
|
|
39452
39665
|
function recordableIds(result) {
|
|
39453
39666
|
return [
|
|
39454
39667
|
...result.findings.map((f) => f.id),
|
|
@@ -39990,8 +40203,9 @@ function exportChain({ chain_id: chainId, group_id: groupId } = {}) {
|
|
|
39990
40203
|
}
|
|
39991
40204
|
|
|
39992
40205
|
// src/server.mjs
|
|
39993
|
-
var SEVERALLY_VERSION = "2.
|
|
39994
|
-
var DEFAULT_WAIT_MS = Math.min(POLICY.maxWaitMs, 45e3);
|
|
40206
|
+
var SEVERALLY_VERSION = "2.1.0";
|
|
40207
|
+
var DEFAULT_WAIT_MS = process.env.SEVERALLY_MAX_WAIT_MS ? POLICY.maxWaitMs : Math.min(POLICY.maxWaitMs, 45e3);
|
|
40208
|
+
var PROGRESS_EVERY_MS = 15e3;
|
|
39995
40209
|
var semver = (v) => String(v).split(".").map(Number);
|
|
39996
40210
|
var older = (a, b) => {
|
|
39997
40211
|
const [x, y] = [semver(a), semver(b)];
|
|
@@ -40057,8 +40271,12 @@ ${availableConsultants().map((c) => ` ${c.name}: ${c.cli}, ${c.model}`)
|
|
|
40057
40271
|
drift -- and the result is marked as same lineage.
|
|
40058
40272
|
targets: ask several consultants the same question at once (mutually exclusive with target, no duplicates).
|
|
40059
40273
|
targets: ["all"] asks every consultant listed above; "all" cannot be mixed with names.
|
|
40060
|
-
Every member gets the byte-identical brief
|
|
40061
|
-
|
|
40274
|
+
Every member gets the byte-identical brief (on a follow-up, plus its own earlier rounds) and one
|
|
40275
|
+
group_id; poll it with consult_get({ group_id }). Each member takes one slot of the concurrency cap,
|
|
40276
|
+
and a fan-out that would go over it is refused before any member starts.
|
|
40277
|
+
A follow-up (followup_to) to a job names that one consultant. followup_to a group_id, with no target or
|
|
40278
|
+
targets, asks every member of that fan-out the same new question, each on its own chain from its latest
|
|
40279
|
+
round; a member that is still running, never answered or has used its rounds is skipped and listed.
|
|
40062
40280
|
caller: optional -- the CLI you are running in ("codex" / "claude-code" / "antigravity" / "opencode"), so the
|
|
40063
40281
|
server can annotate a same-vendor consultation.
|
|
40064
40282
|
caller_model: optional -- the model you are running on (e.g. "claude-opus-5"), self-declared and never checked.
|
|
@@ -40082,7 +40300,9 @@ code only if you hand it over in this call. When the question is about code in a
|
|
|
40082
40300
|
name the files it is about by absolute path in context.expose_paths: those files and directories are copied
|
|
40083
40301
|
read-only into its working directory (under ./workspace) and are then the only part of your repository it has.
|
|
40084
40302
|
At most 20 entries, 500 files and 5 MB in total; symlinks and protected files are skipped rather than copied,
|
|
40085
|
-
and text files are credential-masked exactly as the brief is. A
|
|
40303
|
+
and text files are credential-masked exactly as the brief is. A directory inside a git work tree copies only the
|
|
40304
|
+
files git lists (tracked, or untracked and not ignored), so a repository root fits; outside git, .git and
|
|
40305
|
+
node_modules are skipped. A follow-up starts in a fresh working directory
|
|
40086
40306
|
with no files: pass context.expose_from_parent: true to copy again, as they are now, the paths the round it follows
|
|
40087
40307
|
exposed (a changed or vanished entry is marked; an earlier expose_diff brings back the files it touched, not the
|
|
40088
40308
|
diff), and add expose_paths for anything new. It is never implied: leave it off when the follow-up does not need them. Put the facts it needs into context.facts, and keep
|
|
@@ -40149,7 +40369,7 @@ function createServer(manager = new JobManager()) {
|
|
|
40149
40369
|
{
|
|
40150
40370
|
title: "Get consultation status or result",
|
|
40151
40371
|
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
40152
|
-
description: `Fetch the state of a consultation. It waits up to ${DEFAULT_WAIT_MS} ms for it to finish before answering,
|
|
40372
|
+
description: `Fetch the state of a consultation. It waits up to ${DEFAULT_WAIT_MS} ms for it to finish before answering, a limit set to stay under this client's tool-call timeout; wait_ms sets another wait, and wait_ms: 0 answers at once. While it waits it sends a progress notification every 15 s if you asked for them, and a call you cancel stops waiting. A typical consultation takes one to ten minutes; call this again while it runs, and pass a shorter wait_ms when you have independent work to do in between. A completed job carries the structured answer; a failed one carries failure.kind (timeout, auth, usage_limit, rate_limited, model_unavailable, invalid_output, cli_error, spawn_error, interrupted) \u2014 that is "no advice was obtained", which is different from advice that arrived with thin evidence (see quality.evidence_basis). Pass group_id instead of job_id to fetch a fan-out; the wait then covers every consultant in it. A fan-out comes back sized to read in one go: each member is its status, failure and job_id, and \`comparison.by_target\` holds every consultant's stance, summary and findings (with their ids) side by side, cut to fixed lengths where \`truncated\` says so. consult_get({ job_id, ids: ["f2", "u1"] }) on a member returns just those points, each whole with its grounds and your verdict (\`recorded\`): fetch every point you mean to adopt, that decides your call, that rests on thin grounds, or that every consultant assumes -- not only where they diverge. consult_get({ job_id }) returns the whole answer. A finished consultation names its brief (size, sha256, round file) rather than repeating it; detail: "full" returns the brief, and for a fan-out every member's full answer.`,
|
|
40153
40373
|
inputSchema: external_exports.object({
|
|
40154
40374
|
job_id: external_exports.string().min(1).optional(),
|
|
40155
40375
|
group_id: external_exports.string().min(1).optional(),
|
|
@@ -40158,14 +40378,27 @@ function createServer(manager = new JobManager()) {
|
|
|
40158
40378
|
ids: external_exports.array(external_exports.string().min(1)).min(1).max(100).optional().describe("with job_id: return only these points of the finished answer (f1, u1, c1 ...), each whole with its grounds and, under `recorded`, the verdict you wrote")
|
|
40159
40379
|
})
|
|
40160
40380
|
},
|
|
40161
|
-
async ({ job_id, group_id, wait_ms, detail, ids }) => {
|
|
40381
|
+
async ({ job_id, group_id, wait_ms, detail, ids }, ctx) => {
|
|
40162
40382
|
if (Boolean(job_id) === Boolean(group_id)) {
|
|
40163
40383
|
return fail({ error: "invalid_request", message: "pass exactly one of job_id or group_id" });
|
|
40164
40384
|
}
|
|
40165
40385
|
if (ids && (group_id || detail)) {
|
|
40166
40386
|
return fail({ error: "invalid_request", message: "ids goes with job_id alone: a point id belongs to one consultant's answer" });
|
|
40167
40387
|
}
|
|
40168
|
-
const
|
|
40388
|
+
const req = ctx?.mcpReq;
|
|
40389
|
+
const token = req?._meta?.progressToken;
|
|
40390
|
+
let beats = 0;
|
|
40391
|
+
const timer = token === void 0 ? null : setInterval(() => {
|
|
40392
|
+
beats += 1;
|
|
40393
|
+
req.notify({ method: "notifications/progress", params: { progressToken: token, progress: beats, message: "the consultation is still running" } }).catch(() => {
|
|
40394
|
+
});
|
|
40395
|
+
}, PROGRESS_EVERY_MS);
|
|
40396
|
+
let view;
|
|
40397
|
+
try {
|
|
40398
|
+
view = group_id ? await manager.waitGroup(group_id, wait_ms ?? DEFAULT_WAIT_MS, { detail }, req?.signal) : await manager.wait(job_id, wait_ms ?? DEFAULT_WAIT_MS, { detail }, req?.signal);
|
|
40399
|
+
} finally {
|
|
40400
|
+
clearInterval(timer);
|
|
40401
|
+
}
|
|
40169
40402
|
if (!view) {
|
|
40170
40403
|
return fail({ error: "unknown_job", message: `no consultation with ${group_id ? "group_id" : "job_id"} "${group_id ?? job_id}"` });
|
|
40171
40404
|
}
|
|
@@ -40205,7 +40438,7 @@ function createServer(manager = new JobManager()) {
|
|
|
40205
40438
|
{
|
|
40206
40439
|
title: "Record what checking a point showed",
|
|
40207
40440
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
40208
|
-
description: 'Write your own verdict against one or more points of an answer, after you have checked them in the repository. Ids come from the result: findings are f1, f2 ..., unknowns u1 ..., next_checks c1 ... . verdict says what checking showed -- "confirmed" (it holds here), "not_applicable" (true in general, not for this codebase), "unverifiable" (cannot be settled with what you can reach), "unverified" (not checked yet, and say in effect why not). It does not say whether you adopted the point. effect is what it changed about your decision; note is the evidence you used. A follow-up to this job shows the consultant each
|
|
40441
|
+
description: 'Write your own verdict against one or more points of an answer, after you have checked them in the repository. Ids come from the result: findings are f1, f2 ..., unknowns u1 ..., next_checks c1 ... . verdict says what checking showed -- "confirmed" (it holds here), "not_applicable" (true in general, not for this codebase), "unverifiable" (cannot be settled with what you can reach), "unverified" (not checked yet, and say in effect why not). It does not say whether you adopted the point. effect is what it changed about your decision; note is the evidence you used. A follow-up to this job shows the consultant each point\'s verdict and effect (credential-masked, like the brief), never the note. Recording the same id again replaces that entry. This server stores what you write and counts the verdicts; it never infers one, and never decides a consultation was worth it. The entry is saved beside the answer and the brief in ~/.severally/history, which is what makes the decision readable a month from now; the job is read back from that history, so a consultation from an earlier session can still be recorded against. It answers with counts (the verdicts tallied, and how many points now carry one); consult_get reads the entries back. Pass `reflection` to record what the answer added over what you already expected, when the consultation was started with a `prediction`. A prediction itself cannot be written here: it goes in consult_start, before the consultant runs, which is the only thing that makes it a prediction. For a fan-out, record every member in one call: records: [{ job_id, entries, reflection? }, ...] in place of job_id/entries/reflection. Every record is checked first; if any job or id is wrong, nothing is written and each bad one is named. A job whose write fails is listed under results with its error, not counted as saved.',
|
|
40209
40442
|
inputSchema: external_exports.object({
|
|
40210
40443
|
...recordShape,
|
|
40211
40444
|
job_id: recordShape.job_id.optional(),
|