severally-mcp 2.4.0 → 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 +44 -19
- package/README.md +44 -18
- package/package.json +3 -1
- 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 +585 -255
- 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 +57 -54
- package/scripts/doctor.mjs +15 -4
- package/scripts/install.mjs +73 -9
- package/scripts/live-check.mjs +11 -5
- package/scripts/live-mcp-check.mjs +10 -4
- package/src/adapters/antigravity-sandbox.mjs +24 -6
- package/src/adapters/antigravity.mjs +80 -17
- package/src/adapters/claude-code.mjs +49 -8
- package/src/adapters/codex.mjs +56 -6
- package/src/adapters/opencode-sandbox.mjs +20 -2
- package/src/adapters/opencode.mjs +115 -19
- package/src/brief.mjs +84 -7
- package/src/consultants.mjs +184 -25
- package/src/cross-spawn-escape.d.ts +7 -0
- package/src/export.mjs +50 -6
- package/src/expose-paths.mjs +149 -7
- package/src/failures.mjs +8 -1
- package/src/jobs.mjs +578 -132
- package/src/jsonc.mjs +11 -3
- package/src/parse-result.mjs +91 -14
- package/src/platform.mjs +52 -2
- package/src/policy.mjs +151 -28
- package/src/redact.mjs +22 -3
- package/src/result-schema.mjs +13 -0
- package/src/run.mjs +158 -12
- package/src/schema.mjs +84 -11
- package/src/server.mjs +49 -14
- package/src/status.mjs +16 -4
- package/src/store.mjs +287 -6
|
@@ -30,6 +30,16 @@ import { POLICY } from '../policy.mjs';
|
|
|
30
30
|
import { classifyMessage } from '../failures.mjs';
|
|
31
31
|
import { spawnCommand } from '../platform.mjs';
|
|
32
32
|
|
|
33
|
+
/** @typedef {import('../run.mjs').BuildRequest} BuildRequest */
|
|
34
|
+
/** @typedef {import('../run.mjs').ChildResult} ChildResult */
|
|
35
|
+
/** @typedef {import('../run.mjs').Invocation} Invocation */
|
|
36
|
+
/** @typedef {import('../run.mjs').Interpreted} Interpreted */
|
|
37
|
+
/** @typedef {import('../run.mjs').Sandbox} Sandbox */
|
|
38
|
+
/** @typedef {import('../run.mjs').UsageRecord} UsageRecord */
|
|
39
|
+
/** @typedef {import('../run.mjs').Boundary} Boundary */
|
|
40
|
+
/** @typedef {import('../run.mjs').InterpretedOk} OpencodeOk */
|
|
41
|
+
/** @typedef {import('../run.mjs').InterpretedFail} InterpretedFail */
|
|
42
|
+
|
|
33
43
|
// Checked against opencode 1.18.31. The list exists so a future edit that
|
|
34
44
|
// reaches for one of them is refused rather than quietly widening the
|
|
35
45
|
// consultant:
|
|
@@ -82,21 +92,28 @@ export { prepareSandbox } from './opencode-sandbox.mjs';
|
|
|
82
92
|
// server lives and dies with the child's process group. 1.18.31 has no such
|
|
83
93
|
// flag (unknown flags die with a usage dump), so support is probed once per
|
|
84
94
|
// process through `run --help` and the invocation stays 1.x-exact without it.
|
|
95
|
+
/** @type {boolean | null} */
|
|
85
96
|
let standaloneSupport = null;
|
|
97
|
+
/** @type {Promise<void> | null} */
|
|
86
98
|
let probePromise = null;
|
|
87
99
|
|
|
88
100
|
/** Probe the CLI's capabilities once per process; safe to call per job. */
|
|
89
101
|
export async function prepare() {
|
|
90
102
|
if (standaloneSupport !== null) return;
|
|
91
103
|
if (!probePromise) {
|
|
92
|
-
probePromise = new Promise((resolve) => {
|
|
104
|
+
probePromise = new Promise((/** @type {() => void} */ resolve) => {
|
|
93
105
|
let settled = false;
|
|
106
|
+
/** @type {NodeJS.Timeout | null} */
|
|
94
107
|
let timer = null;
|
|
95
108
|
// Only a SUCCESSFUL help invocation may cache its answer: a spawn
|
|
96
109
|
// error, a timeout or a nonzero exit is transient, and caching it as
|
|
97
110
|
// "unsupported" would pin a 2.x install to the shared-service route
|
|
98
111
|
// for the process lifetime. Transient outcomes resolve without
|
|
99
112
|
// caching, so the next job probes again.
|
|
113
|
+
/**
|
|
114
|
+
* @param {boolean} supported
|
|
115
|
+
* @param {boolean} cache
|
|
116
|
+
*/
|
|
100
117
|
const finishProbe = (supported, cache) => {
|
|
101
118
|
if (settled) return;
|
|
102
119
|
settled = true;
|
|
@@ -115,10 +132,11 @@ export async function prepare() {
|
|
|
115
132
|
);
|
|
116
133
|
const child = spawnCommand(POLICY.clis.opencode.cli, ['run', '--help'], { env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
117
134
|
let out = '';
|
|
118
|
-
|
|
135
|
+
// stdio above is 'pipe' for both, so the streams exist.
|
|
136
|
+
/** @type {NonNullable<typeof child.stdout>} */ (child.stdout).on('data', (c) => {
|
|
119
137
|
out += c;
|
|
120
138
|
});
|
|
121
|
-
child.stderr.on('data', (c) => {
|
|
139
|
+
/** @type {NonNullable<typeof child.stderr>} */ (child.stderr).on('data', (c) => {
|
|
122
140
|
out += c;
|
|
123
141
|
});
|
|
124
142
|
child.on('error', () => finishProbe(false, false));
|
|
@@ -162,6 +180,8 @@ export async function resetProbe() {
|
|
|
162
180
|
* and the caller retries once. Strictly once per job; every other failure
|
|
163
181
|
* kind -- an auth rejection, a usage limit, invalid output -- means a
|
|
164
182
|
* respawn is wasted compute.
|
|
183
|
+
* @param {{ interpreted: Interpreted, sandbox: Sandbox | undefined, model: string }} args
|
|
184
|
+
* @returns {boolean}
|
|
165
185
|
*/
|
|
166
186
|
export function recoverAuthFailure({ interpreted, sandbox, model }) {
|
|
167
187
|
if (!interpreted || interpreted.ok !== false || interpreted.failureKind !== 'model_unavailable') return false;
|
|
@@ -176,6 +196,10 @@ export function recoverAuthFailure({ interpreted, sandbox, model }) {
|
|
|
176
196
|
// adapter adds or leaves out by capability probe.
|
|
177
197
|
export const RESERVED_FLAGS = ['--model', '--standalone', '--wizard'];
|
|
178
198
|
|
|
199
|
+
/**
|
|
200
|
+
* @param {BuildRequest} req
|
|
201
|
+
* @returns {Invocation}
|
|
202
|
+
*/
|
|
179
203
|
export function buildInvocation({ consultant }) {
|
|
180
204
|
const t = POLICY.clis.opencode;
|
|
181
205
|
const chosen = consultant.model;
|
|
@@ -204,8 +228,43 @@ export function buildInvocation({ consultant }) {
|
|
|
204
228
|
return { command: t.cli, args, model: chosen };
|
|
205
229
|
}
|
|
206
230
|
|
|
207
|
-
/**
|
|
231
|
+
/**
|
|
232
|
+
* One NDJSON event from `opencode run --format json`; the CLI controls it, so
|
|
233
|
+
* every field is optional. `error` is {type, message} on 2.x and
|
|
234
|
+
* {name, data:{message}} on 1.x.
|
|
235
|
+
* @typedef {object} OpencodeEvent
|
|
236
|
+
* @property {string} [type]
|
|
237
|
+
* @property {{
|
|
238
|
+
* type?: string,
|
|
239
|
+
* text?: unknown,
|
|
240
|
+
* time?: { end?: unknown },
|
|
241
|
+
* tokens?: OpencodeTokens,
|
|
242
|
+
* cost?: unknown,
|
|
243
|
+
* tool?: string,
|
|
244
|
+
* callID?: string,
|
|
245
|
+
* id?: string,
|
|
246
|
+
* state?: { status?: string },
|
|
247
|
+
* }} [part]
|
|
248
|
+
* @property {{ message?: unknown, name?: unknown, data?: { message?: unknown } }} [error]
|
|
249
|
+
*/
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* The token figures of a step-finish part.
|
|
253
|
+
* @typedef {{ input?: unknown, output?: unknown, total?: unknown, reasoning?: unknown, cache_read?: unknown, cache?: { read?: unknown } }} OpencodeTokens
|
|
254
|
+
*/
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* The usage figures interpret collects from the event stream.
|
|
258
|
+
* @typedef {{ tokens?: OpencodeTokens | null, cost?: unknown, steps?: unknown }} OpencodeUsage
|
|
259
|
+
*/
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Every complete NDJSON object opencode wrote to stdout, oldest first.
|
|
263
|
+
* @param {string | undefined} stdout
|
|
264
|
+
* @returns {OpencodeEvent[]}
|
|
265
|
+
*/
|
|
208
266
|
export function events(stdout) {
|
|
267
|
+
/** @type {OpencodeEvent[]} */
|
|
209
268
|
const out = [];
|
|
210
269
|
for (const line of (stdout || '').split('\n')) {
|
|
211
270
|
const t = line.trim();
|
|
@@ -227,6 +286,8 @@ export function events(stdout) {
|
|
|
227
286
|
* filled rawCaptureMax and cut the final answer off the stream. Nothing here
|
|
228
287
|
* reads tool results: interpret wants text/step_finish/error, and progress
|
|
229
288
|
* only the tool name.
|
|
289
|
+
* @param {string} line
|
|
290
|
+
* @returns {string}
|
|
230
291
|
*/
|
|
231
292
|
export function compactLine(line) {
|
|
232
293
|
if (!line.includes('"tool_use"')) return line;
|
|
@@ -242,7 +303,11 @@ export function compactLine(line) {
|
|
|
242
303
|
}
|
|
243
304
|
}
|
|
244
305
|
|
|
245
|
-
/**
|
|
306
|
+
/**
|
|
307
|
+
* One line naming the error opencode reported, in classifyMessage's terms.
|
|
308
|
+
* @param {NonNullable<OpencodeEvent['error']>} error
|
|
309
|
+
* @returns {string}
|
|
310
|
+
*/
|
|
246
311
|
function errorMessage(error) {
|
|
247
312
|
if (!error || typeof error !== 'object') return JSON.stringify(error);
|
|
248
313
|
// 2.x: {type, message} -- the message is already a complete human sentence,
|
|
@@ -260,6 +325,8 @@ function errorMessage(error) {
|
|
|
260
325
|
* log line lands). Raw stdout is never the message: it is the event stream
|
|
261
326
|
* of a run that was working fine, its head says nothing about the failure, and
|
|
262
327
|
* numbers inside it ("reasoning":403) would be classified as HTTP statuses.
|
|
328
|
+
* @param {{ errors: string[], stderr?: string | undefined, stdout?: string | undefined, code?: number | null | undefined, signal?: NodeJS.Signals | null | undefined, what: string }} run
|
|
329
|
+
* @returns {{ msg: string, kind: string }}
|
|
263
330
|
*/
|
|
264
331
|
function failureMessage({ errors, stderr, stdout, code, signal, what }) {
|
|
265
332
|
if (errors.length) {
|
|
@@ -276,20 +343,25 @@ function failureMessage({ errors, stderr, stdout, code, signal, what }) {
|
|
|
276
343
|
* Parse the event stream `opencode run --format json` writes to stdout.
|
|
277
344
|
* The answer is the LAST completed text part: a consultation can emit prose
|
|
278
345
|
* between tool calls, and only the final part carries the reply contract.
|
|
346
|
+
* @param {Partial<ChildResult>} run
|
|
347
|
+
* @returns {OpencodeOk | InterpretedFail}
|
|
279
348
|
*/
|
|
280
349
|
export function interpret({ stdout, stderr, code, signal = null, truncated = false }) {
|
|
281
350
|
const all = events(stdout);
|
|
351
|
+
/** @type {string | null} */
|
|
282
352
|
let text = null;
|
|
283
353
|
let textIndex = -1;
|
|
284
354
|
let lastErrorIndex = -1;
|
|
355
|
+
let lastError = '';
|
|
285
356
|
let finishedAfterText = false;
|
|
286
357
|
let tokens = null;
|
|
287
358
|
let cost = null;
|
|
288
359
|
let steps = 0;
|
|
360
|
+
/** @type {string[]} */
|
|
289
361
|
const errors = [];
|
|
362
|
+
/** @type {string[]} */
|
|
290
363
|
const earlierTextParts = [];
|
|
291
|
-
for (
|
|
292
|
-
const e = all[i];
|
|
364
|
+
for (const [i, e] of all.entries()) {
|
|
293
365
|
const part = e.part;
|
|
294
366
|
if (e.type === 'text' && part?.type === 'text' && part.time?.end && typeof part.text === 'string' && part.text.trim()) {
|
|
295
367
|
// The last completed part wins as the answer, but every part that had
|
|
@@ -309,7 +381,8 @@ export function interpret({ stdout, stderr, code, signal = null, truncated = fal
|
|
|
309
381
|
if (part.tokens && typeof part.tokens === 'object') tokens = part.tokens;
|
|
310
382
|
if (typeof part.cost === 'number') cost = part.cost;
|
|
311
383
|
} else if (e.type === 'error' && e.error) {
|
|
312
|
-
|
|
384
|
+
lastError = errorMessage(e.error);
|
|
385
|
+
errors.push(lastError);
|
|
313
386
|
lastErrorIndex = i;
|
|
314
387
|
}
|
|
315
388
|
}
|
|
@@ -329,7 +402,7 @@ export function interpret({ stdout, stderr, code, signal = null, truncated = fal
|
|
|
329
402
|
// failure. What text exists is interim prose, and 2.x exits 0 even on
|
|
330
403
|
// provider errors, so order -- not the exit code -- is what tells this
|
|
331
404
|
// apart from the recovered-retry case below.
|
|
332
|
-
const msg =
|
|
405
|
+
const msg = lastError;
|
|
333
406
|
return { ok: false, failureKind: classifyMessage(msg), message: msg, usageRaw };
|
|
334
407
|
}
|
|
335
408
|
if (code !== 0) {
|
|
@@ -339,7 +412,7 @@ export function interpret({ stdout, stderr, code, signal = null, truncated = fal
|
|
|
339
412
|
const { msg, kind } = failureMessage({ errors, stderr, stdout, code, signal, what: 'after producing an answer' });
|
|
340
413
|
return { ok: false, failureKind: kind, message: msg, usageRaw };
|
|
341
414
|
}
|
|
342
|
-
if (truncated && !(finishedAfterText && all.
|
|
415
|
+
if (truncated && !(finishedAfterText && all.at(-1)?.type === 'step_finish')) {
|
|
343
416
|
// stdout hit the rawCaptureMax before the consultation finished, so the
|
|
344
417
|
// final answer event may never have been read. Under truncation only a
|
|
345
418
|
// stream that ENDS on a step-finish -- the terminal event of a completed
|
|
@@ -362,34 +435,53 @@ export function interpret({ stdout, stderr, code, signal = null, truncated = fal
|
|
|
362
435
|
return { ok: true, text, earlierTextParts, usageRaw };
|
|
363
436
|
}
|
|
364
437
|
|
|
438
|
+
/** The tools that read a file's contents (glob only lists names); a consultation whose calls to them all failed read none of its exposed files. */
|
|
439
|
+
export const FILE_READ_TOOLS = ['read', 'grep'];
|
|
440
|
+
|
|
365
441
|
/**
|
|
366
442
|
* What this consultation has done so far -- read while it runs. Separates
|
|
367
443
|
* "working steadily through tool calls" from "wedged on one call", which is
|
|
368
444
|
* the difference between raising the budget and narrowing the brief.
|
|
369
445
|
*/
|
|
370
|
-
/**
|
|
446
|
+
/**
|
|
447
|
+
* Tool calls seen in the event stream, one per call id; `:error` when the call failed or was refused.
|
|
448
|
+
* @param {{ stdout?: string }} run
|
|
449
|
+
* @returns {Record<string, number>}
|
|
450
|
+
*/
|
|
371
451
|
export function toolCalls({ stdout }) {
|
|
452
|
+
/** @type {Map<unknown, string>} */
|
|
372
453
|
const seen = new Map();
|
|
373
454
|
for (const e of events(stdout)) {
|
|
374
455
|
if (e.type === 'tool_use' && e.part?.tool) {
|
|
375
456
|
seen.set(e.part.callID ?? e.part.id ?? seen.size, e.part.state?.status === 'error' ? `${e.part.tool}:error` : e.part.tool);
|
|
376
457
|
}
|
|
377
458
|
}
|
|
378
|
-
return [...seen.values()].reduce((o, n) => ({ ...o, [n]: (o[n] ?? 0) + 1 }), {});
|
|
459
|
+
return [...seen.values()].reduce((/** @type {Record<string, number>} */ o, n) => ({ ...o, [n]: (o[n] ?? 0) + 1 }), {});
|
|
379
460
|
}
|
|
380
461
|
|
|
462
|
+
/**
|
|
463
|
+
* @param {{ stdout?: string | undefined }} run
|
|
464
|
+
* @returns {string | null}
|
|
465
|
+
*/
|
|
381
466
|
export function progress({ stdout }) {
|
|
382
|
-
const seen = events(stdout).filter(
|
|
383
|
-
|
|
384
|
-
|
|
467
|
+
const seen = events(stdout).filter(
|
|
468
|
+
(e) => e.type !== undefined && ['step_start', 'step_finish', 'text', 'tool_use'].includes(e.type),
|
|
469
|
+
);
|
|
470
|
+
const last = seen.at(-1);
|
|
471
|
+
if (!last) return null;
|
|
385
472
|
const what = last.type === 'tool_use' && last.part?.tool ? `tool ${last.part.tool}` : last.type;
|
|
386
473
|
return `${seen.length} event(s), last was ${what}`;
|
|
387
474
|
}
|
|
388
475
|
|
|
476
|
+
/**
|
|
477
|
+
* @param {unknown} raw the usage figures interpret collected from the event stream
|
|
478
|
+
* @returns {UsageRecord}
|
|
479
|
+
*/
|
|
389
480
|
export function usageRecord(raw) {
|
|
390
|
-
|
|
481
|
+
// interpret's usageRaw is the OpencodeUsage it built (or null).
|
|
482
|
+
const r = /** @type {OpencodeUsage | null} */ (raw) ?? {};
|
|
391
483
|
const t = r.tokens && typeof r.tokens === 'object' ? r.tokens : {};
|
|
392
|
-
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : null);
|
|
484
|
+
const num = (/** @type {unknown} */ v) => (typeof v === 'number' && Number.isFinite(v) ? v : null);
|
|
393
485
|
const cache = t.cache && typeof t.cache === 'object' ? t.cache : {};
|
|
394
486
|
return {
|
|
395
487
|
input_tokens: num(t.input),
|
|
@@ -407,15 +499,19 @@ export function usageRecord(raw) {
|
|
|
407
499
|
* What this consultant was launched with, read off the invocation: the
|
|
408
500
|
* boundary record kept beside each answer. Facts about the launch, not a
|
|
409
501
|
* claim about what the consultant did.
|
|
502
|
+
* @param {{ sandbox: Sandbox | null }} launch
|
|
503
|
+
* @returns {Boundary}
|
|
410
504
|
*/
|
|
411
505
|
export function boundary({ sandbox }) {
|
|
506
|
+
// buildInvocation always runs with the sandbox prepareSandbox made.
|
|
507
|
+
const { tools } = /** @type {Sandbox} */ (sandbox);
|
|
412
508
|
return {
|
|
413
509
|
reads: 'whole disk',
|
|
414
510
|
shell: 'none',
|
|
415
|
-
web:
|
|
511
|
+
web: tools.includes('webfetch'),
|
|
416
512
|
writes: false,
|
|
417
513
|
mcp: false,
|
|
418
514
|
protected_paths: 'denied (permission rules)',
|
|
419
|
-
tools: [...
|
|
515
|
+
tools: [...tools],
|
|
420
516
|
};
|
|
421
517
|
}
|
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.',
|
|
@@ -54,7 +100,7 @@ const WEB_OFF =
|
|
|
54
100
|
const WORKDIR_EMPTY =
|
|
55
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.";
|
|
56
102
|
const WORKDIR_EXPOSED =
|
|
57
|
-
'- 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.';
|
|
58
104
|
|
|
59
105
|
const GUARDRAILS_TAIL = [
|
|
60
106
|
'- Never include credentials, API keys, tokens, or environment variable values in your answer.',
|
|
@@ -70,6 +116,10 @@ const GUARDRAILS_TAIL = [
|
|
|
70
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.',
|
|
71
117
|
];
|
|
72
118
|
|
|
119
|
+
/**
|
|
120
|
+
* @param {boolean} [hasExposedPaths]
|
|
121
|
+
* @param {'on' | 'off'} [network]
|
|
122
|
+
*/
|
|
73
123
|
export function renderGuardrails(hasExposedPaths = false, network = 'on') {
|
|
74
124
|
return [
|
|
75
125
|
...GUARDRAILS_HEAD,
|
|
@@ -85,6 +135,10 @@ export function renderGuardrails(hasExposedPaths = false, network = 'on') {
|
|
|
85
135
|
// history holds an answer whose premises are gone -- and a finding read a month
|
|
86
136
|
// later is only as good as the facts it was answering. Redacted the same way
|
|
87
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
|
+
*/
|
|
88
142
|
export function briefRecord(req, exposeManifest = null) {
|
|
89
143
|
return redact({
|
|
90
144
|
objective: req.objective ?? null,
|
|
@@ -126,16 +180,22 @@ export function briefRecord(req, exposeManifest = null) {
|
|
|
126
180
|
});
|
|
127
181
|
}
|
|
128
182
|
|
|
183
|
+
/**
|
|
184
|
+
* @param {string} title
|
|
185
|
+
* @param {string | null | undefined} body
|
|
186
|
+
*/
|
|
129
187
|
function section(title, body) {
|
|
130
188
|
if (!body) return null;
|
|
131
189
|
return `## ${title}\n${body}`;
|
|
132
190
|
}
|
|
133
191
|
|
|
192
|
+
/** @param {string[] | null | undefined} items */
|
|
134
193
|
function bullets(items) {
|
|
135
194
|
if (!items || items.length === 0) return null;
|
|
136
195
|
return items.map((i) => `- ${i}`).join('\n');
|
|
137
196
|
}
|
|
138
197
|
|
|
198
|
+
/** @param {BriefArtifact[]} artifacts */
|
|
139
199
|
function renderArtifacts(artifacts) {
|
|
140
200
|
if (!artifacts.length) return null;
|
|
141
201
|
return artifacts
|
|
@@ -153,6 +213,7 @@ function renderArtifacts(artifacts) {
|
|
|
153
213
|
const EXPOSE_LISTING_MAX = 200;
|
|
154
214
|
|
|
155
215
|
// Deleted files have no copy to open; the brief says which they were.
|
|
216
|
+
/** @param {string[]} [changes] */
|
|
156
217
|
function deletedLines(changes = []) {
|
|
157
218
|
const gone = changes.filter((c) => c.endsWith(' (deleted)')).map((c) => c.slice(0, -' (deleted)'.length));
|
|
158
219
|
if (!gone.length) return [];
|
|
@@ -162,6 +223,7 @@ function deletedLines(changes = []) {
|
|
|
162
223
|
];
|
|
163
224
|
}
|
|
164
225
|
|
|
226
|
+
/** @param {ExposeManifest | null} manifest */
|
|
165
227
|
function renderExposeManifest(manifest) {
|
|
166
228
|
if (!manifest || (manifest.entries.length === 0 && manifest.skipped.length === 0)) return null;
|
|
167
229
|
let room = EXPOSE_LISTING_MAX;
|
|
@@ -188,7 +250,9 @@ function renderExposeManifest(manifest) {
|
|
|
188
250
|
...(e.kind === 'diff' || e.kind === 'change_files' ? deletedLines(e.changes) : []),
|
|
189
251
|
];
|
|
190
252
|
});
|
|
253
|
+
/** @param {string} reason */
|
|
191
254
|
const count = (reason) => manifest.skipped.filter((x) => x.reason === reason).length;
|
|
255
|
+
/** @param {string} reason */
|
|
192
256
|
const named = (reason) =>
|
|
193
257
|
manifest.skipped
|
|
194
258
|
.filter((x) => x.reason === reason)
|
|
@@ -211,7 +275,7 @@ function renderExposeManifest(manifest) {
|
|
|
211
275
|
if (skipped.length) skipped.unshift('');
|
|
212
276
|
return [
|
|
213
277
|
'The lead exposed these paths; they are copied under ./workspace in your working directory and you may read',
|
|
214
|
-
"them directly. They are the only part of the lead's repository you
|
|
278
|
+
"them directly. They are the only part of the lead's repository the lead gave you -- if you need something that is",
|
|
215
279
|
'not here, record it under `unknowns` rather than guessing. Text files were passed through the same',
|
|
216
280
|
'credential masking as this brief; binary files were copied unchanged.',
|
|
217
281
|
'',
|
|
@@ -222,16 +286,28 @@ function renderExposeManifest(manifest) {
|
|
|
222
286
|
|
|
223
287
|
// An earlier round's point, cut to a length that keeps a five-round chain
|
|
224
288
|
// from crowding out the question it is there to inform.
|
|
289
|
+
/**
|
|
290
|
+
* @param {string} s
|
|
291
|
+
* @param {number} [max]
|
|
292
|
+
*/
|
|
225
293
|
const clip = (s, max = 400) => (s.length > max ? `${s.slice(0, max)}...` : s);
|
|
226
294
|
|
|
227
295
|
// The earlier rounds have a budget of their own, beside the request's cap:
|
|
228
296
|
// when they outgrow it, the oldest rounds are left out, and the brief says so.
|
|
229
297
|
const HISTORY_CHARS = 40_000;
|
|
230
298
|
|
|
299
|
+
/** @param {PriorRound} p */
|
|
231
300
|
function renderRound(p) {
|
|
232
301
|
// Prefixed with the round, so a point from round 1 is not taken for this one's.
|
|
302
|
+
/** @param {string} id */
|
|
233
303
|
const tag = (id) => `r${p.round}.${id}`;
|
|
304
|
+
/** @param {PriorPoint} x */
|
|
234
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
|
+
*/
|
|
235
311
|
const list = (title, items, line) => (items?.length ? [title, ...items.map(line)].join('\n') : null);
|
|
236
312
|
return [
|
|
237
313
|
`### Round ${p.round} (${p.mode})`,
|
|
@@ -250,9 +326,9 @@ function renderRound(p) {
|
|
|
250
326
|
}
|
|
251
327
|
|
|
252
328
|
/**
|
|
253
|
-
* @param {
|
|
254
|
-
* @param {
|
|
255
|
-
*
|
|
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]
|
|
256
332
|
*/
|
|
257
333
|
export function renderBrief(req, chain = { round: 1, priorRounds: [] }, exposeManifest = null) {
|
|
258
334
|
const parts = [];
|
|
@@ -292,8 +368,9 @@ export function renderBrief(req, chain = { round: 1, priorRounds: [] }, exposeMa
|
|
|
292
368
|
let kept = 0;
|
|
293
369
|
let used = 0;
|
|
294
370
|
// Newest first: the round being followed up always goes in.
|
|
295
|
-
for (
|
|
296
|
-
used
|
|
371
|
+
for (const block of [...blocks].reverse()) {
|
|
372
|
+
if (kept > 0 && used + block.length > HISTORY_CHARS) break;
|
|
373
|
+
used += block.length;
|
|
297
374
|
kept += 1;
|
|
298
375
|
}
|
|
299
376
|
const left = chain.priorRounds.slice(0, blocks.length - kept).map((p) => p.round);
|