severally-mcp 2.3.1 → 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.
@@ -69,13 +69,29 @@ export const FORBIDDEN_FLAGS = [
69
69
  // feature switch that could re-enable what --ignore-user-config drops, and
70
70
  // the modes that leave the read-only sandbox.
71
71
  export const RESERVED_FLAGS = [
72
- '-s', '--sandbox', '--config', '--cd', '--model', '--output-last-message',
73
- '--profile', '-p', '--enable', '--full-auto', '--worktree', '--cyber-access-program',
72
+ '-s',
73
+ '--sandbox',
74
+ '--config',
75
+ '--cd',
76
+ '--model',
77
+ '--output-last-message',
78
+ '--profile',
79
+ '-p',
80
+ '--enable',
81
+ '--full-auto',
82
+ '--worktree',
83
+ '--cyber-access-program',
74
84
  ];
75
85
 
76
86
  const PROFILE = 'severally';
77
87
 
78
- const listDir = (dir) => { try { return fs.readdirSync(dir); } catch { return []; } };
88
+ const listDir = (dir) => {
89
+ try {
90
+ return fs.readdirSync(dir);
91
+ } catch {
92
+ return [];
93
+ }
94
+ };
79
95
 
80
96
  /**
81
97
  * The profile's filesystem table, as TOML: everything readable, the protected
@@ -90,11 +106,16 @@ export function filesystemPermissions() {
90
106
  fs.mkdirSync(path.join(POLICY.home, 'homes'), { recursive: true, mode: 0o700 });
91
107
  const codexHome = path.join(os.homedir(), '.codex').replaceAll('\\', '/');
92
108
  const denied = protectedRoots().flatMap((p) => {
93
- if (p === codexHome) return listDir(codexHome).filter((n) => n !== 'packages').map((n) => `${codexHome}/${n}`);
109
+ if (p === codexHome)
110
+ return listDir(codexHome)
111
+ .filter((n) => n !== 'packages')
112
+ .map((n) => `${codexHome}/${n}`);
94
113
  const star = p.indexOf('/*/');
95
114
  if (star === -1) return [p];
96
115
  const [parent, rest] = [p.slice(0, star), p.slice(star + 3)];
97
- return listDir(parent).map((n) => `${parent}/${n}/${rest}`).filter((q) => fs.existsSync(q));
116
+ return listDir(parent)
117
+ .map((n) => `${parent}/${n}/${rest}`)
118
+ .filter((q) => fs.existsSync(q));
98
119
  });
99
120
  return `{ ":root" = "read", ${denied.map((p) => `${JSON.stringify(p)} = "none"`).join(', ')} }`;
100
121
  }
@@ -106,24 +127,37 @@ export function buildInvocation({ workdir, schemaPath, consultant, network = 'on
106
127
  const args = [
107
128
  'exec',
108
129
  '--json',
109
- '--color', 'never',
110
- '-m', chosen,
111
- '-c', `model_reasoning_effort="${consultant.effort ?? 'high'}"`,
112
- '-c', `tools.web_search=${network !== 'off'}`,
130
+ '--color',
131
+ 'never',
132
+ '-m',
133
+ chosen,
134
+ '-c',
135
+ `model_reasoning_effort="${consultant.effort ?? 'high'}"`,
136
+ '-c',
137
+ `tools.web_search=${network !== 'off'}`,
113
138
  ...(network === 'off' ? ['-c', 'web_search="disabled"'] : []),
114
- '-c', 'hooks.enabled=false',
115
- '-c', 'agents.enabled=false',
116
- '-c', 'shell_environment_policy.inherit="core"',
117
- '-c', 'shell_environment_policy.set={SEVERALLY_ACTIVE="1"}',
139
+ '-c',
140
+ 'hooks.enabled=false',
141
+ '-c',
142
+ 'agents.enabled=false',
143
+ '-c',
144
+ 'shell_environment_policy.inherit="core"',
145
+ '-c',
146
+ 'shell_environment_policy.set={SEVERALLY_ACTIVE="1"}',
118
147
  '--ignore-user-config',
119
148
  '--ignore-rules',
120
149
  '--skip-git-repo-check',
121
150
  '--ephemeral',
122
- '-c', `default_permissions="${PROFILE}"`,
123
- '-c', `permissions.${PROFILE}.filesystem=${filesystemPermissions()}`,
124
- '-C', workdir,
125
- '--output-schema', schemaPath,
126
- '-o', lastMessagePath,
151
+ '-c',
152
+ `default_permissions="${PROFILE}"`,
153
+ '-c',
154
+ `permissions.${PROFILE}.filesystem=${filesystemPermissions()}`,
155
+ '-C',
156
+ workdir,
157
+ '--output-schema',
158
+ schemaPath,
159
+ '-o',
160
+ lastMessagePath,
127
161
  '-',
128
162
  ];
129
163
  return { command: t.cli, args, lastMessagePath, model: chosen };
@@ -146,7 +180,11 @@ export function interpret({ stdout, stderr, code, lastMessageText }) {
146
180
  for (const line of (stdout || '').split('\n')) {
147
181
  const t = line.trim();
148
182
  if (!t.startsWith('{')) continue;
149
- try { events.push(JSON.parse(t)); } catch { /* partial line */ }
183
+ try {
184
+ events.push(JSON.parse(t));
185
+ } catch {
186
+ /* partial line */
187
+ }
150
188
  }
151
189
 
152
190
  const errorEvents = events.filter((e) => e.type === 'error' || e.type === 'turn.failed');
@@ -167,9 +205,7 @@ export function interpret({ stdout, stderr, code, lastMessageText }) {
167
205
  }
168
206
  }
169
207
 
170
- const errorMsg = errorEvents
171
- .map((e) => e.message ?? e.error?.message ?? JSON.stringify(e))
172
- .join(' | ');
208
+ const errorMsg = errorEvents.map((e) => e.message ?? e.error?.message ?? JSON.stringify(e)).join(' | ');
173
209
  // An error the turn recovered from (the stream still ends in
174
210
  // turn.completed, with an answer) does not throw the answer away; the lead
175
211
  // is told about it instead. A turn that ends in turn.failed has failed,
@@ -210,7 +246,9 @@ export function progress({ stdout }) {
210
246
  try {
211
247
  const e = JSON.parse(t);
212
248
  if (typeof e?.type === 'string' && e.type.startsWith('item.')) items.push(e);
213
- } catch { /* a partial line, e.g. when the child was killed mid-write */ }
249
+ } catch {
250
+ /* a partial line, e.g. when the child was killed mid-write */
251
+ }
214
252
  }
215
253
  if (!items.length) return null;
216
254
  const last = items[items.length - 1];
@@ -229,7 +267,11 @@ export function toolCalls({ stdout }) {
229
267
  const t = line.trim();
230
268
  if (!t.startsWith('{')) continue;
231
269
  let e;
232
- try { e = JSON.parse(t); } catch { continue; }
270
+ try {
271
+ e = JSON.parse(t);
272
+ } catch {
273
+ continue;
274
+ }
233
275
  const item = e?.item;
234
276
  if (!item?.type || !['item.started', 'item.completed'].includes(e.type)) continue;
235
277
  if (['agent_message', 'reasoning', 'error'].includes(item.type)) continue;
@@ -269,9 +311,9 @@ export function boundary({ args }) {
269
311
  writes: false,
270
312
  mcp: false,
271
313
  protected_paths: args.some((a) => a.startsWith(`permissions.${PROFILE}.filesystem=`))
272
- ? 'denied by path (permission profile), as the paths stood at launch: ~/.codex entries other than packages/, '
273
- + 'and legacy job homes, created later are not covered; file names such as .env anywhere are not; on Linux a '
274
- + 'protected file replaced by rename during the run is readable'
314
+ ? 'denied by path (permission profile), as the paths stood at launch: ~/.codex entries other than packages/, ' +
315
+ 'and legacy job homes, created later are not covered; file names such as .env anywhere are not; on Linux a ' +
316
+ 'protected file replaced by rename during the run is readable'
275
317
  : 'not enforced',
276
318
  tools: null,
277
319
  };
@@ -49,7 +49,11 @@ import { sandboxHome } from '../store.mjs';
49
49
  // allows older runtimes, where credential seeding degrades to a no-op and the
50
50
  // job fails with the CLI's own auth error instead.
51
51
  let DatabaseSync = null;
52
- try { ({ DatabaseSync } = sqliteRequire(import.meta.url)('node:sqlite')); } catch { /* unavailable */ }
52
+ try {
53
+ ({ DatabaseSync } = sqliteRequire(import.meta.url)('node:sqlite'));
54
+ } catch {
55
+ /* unavailable */
56
+ }
53
57
 
54
58
  // Reading is allowed, not denied: every consultant is an equal reader (Codex
55
59
  // runs a read-only shell; Claude Code and Antigravity can read the whole
@@ -93,10 +97,7 @@ const SANDBOX_TOOLS_OFF = Object.freeze({ edit: false, write: false, patch: fals
93
97
  // workdir, and a run that cannot ask refuses -- so this consultant read less
94
98
  // than the others. The last matching rule wins, so "*" comes first.
95
99
  function readRules() {
96
- const deny = [
97
- ...protectedRoots().flatMap((r) => [r, `${r}/**`]),
98
- ...PROTECTED_FILE_NAMES.flatMap((n) => [n, `**/${n}`]),
99
- ];
100
+ const deny = [...protectedRoots().flatMap((r) => [r, `${r}/**`]), ...PROTECTED_FILE_NAMES.flatMap((n) => [n, `**/${n}`])];
100
101
  return { '*': 'allow', ...Object.fromEntries(deny.map((p) => [p, 'deny'])) };
101
102
  }
102
103
 
@@ -104,7 +105,7 @@ const AUTH_REL = path.join('.local', 'share', 'opencode', 'auth.json');
104
105
 
105
106
  /**
106
107
  * Build the synthesised HOME for one consultation.
107
- * @param {{workdir: string}} opts workdir is <jobdir>/work; the home is store.sandboxHome(<job id>).
108
+ * @param {{workdir: string, network?: 'on' | 'off'}} opts workdir is <jobdir>/work; the home is store.sandboxHome(<job id>).
108
109
  */
109
110
  export function prepareSandbox({ workdir, network = 'on' }) {
110
111
  const root = sandboxHome(path.basename(path.dirname(workdir)));
@@ -180,16 +181,22 @@ export function prepareSandbox({ workdir, network = 'on' }) {
180
181
  // parent directories of the (empty) working directory.
181
182
  OPENCODE_DISABLE_AUTOUPDATE: '1',
182
183
  OPENCODE_DISABLE_PROJECT_CONFIG: '1',
183
- ...(process.platform === 'win32' ? {
184
- USERPROFILE: root,
185
- HOMEDRIVE: path.parse(root).root.replace(/[\\/]$/, ''),
186
- HOMEPATH: root.slice(path.parse(root).root.length - 1),
187
- APPDATA: path.join(root, 'AppData', 'Roaming'),
188
- LOCALAPPDATA: path.join(root, 'AppData', 'Local'),
189
- } : {}),
184
+ ...(process.platform === 'win32'
185
+ ? {
186
+ USERPROFILE: root,
187
+ HOMEDRIVE: path.parse(root).root.replace(/[\\/]$/, ''),
188
+ HOMEPATH: root.slice(path.parse(root).root.length - 1),
189
+ APPDATA: path.join(root, 'AppData', 'Roaming'),
190
+ LOCALAPPDATA: path.join(root, 'AppData', 'Local'),
191
+ }
192
+ : {}),
190
193
  },
191
194
  cleanup() {
192
- try { fs.rmSync(root, { recursive: true, force: true }); } catch { /* best effort */ }
195
+ try {
196
+ fs.rmSync(root, { recursive: true, force: true });
197
+ } catch {
198
+ /* best effort */
199
+ }
193
200
  },
194
201
  };
195
202
  sandbox.seed = (provider) => seedCredentialFromAuth(sandbox, provider);
@@ -231,7 +238,9 @@ export function seedCredentialFromAuth(sandbox, provider) {
231
238
  try {
232
239
  const db = new DatabaseSync(operatorDb, { readOnly: true });
233
240
  try {
234
- const found = db.prepare('SELECT label, value FROM credential WHERE integration_id = ? ORDER BY time_updated DESC LIMIT 1').get(provider);
241
+ const found = db
242
+ .prepare('SELECT label, value FROM credential WHERE integration_id = ? ORDER BY time_updated DESC LIMIT 1')
243
+ .get(provider);
235
244
  if (found && typeof found.value === 'string') {
236
245
  const parsed = JSON.parse(found.value);
237
246
  if (parsed && typeof parsed === 'object' && typeof parsed.key === 'string' && parsed.key) {
@@ -241,23 +250,33 @@ export function seedCredentialFromAuth(sandbox, provider) {
241
250
  } finally {
242
251
  db.close();
243
252
  }
244
- } catch { /* unreadable or reshaped: try the auth.json source */ }
253
+ } catch {
254
+ /* unreadable or reshaped: try the auth.json source */
255
+ }
245
256
  }
246
257
  if (!row && fs.existsSync(authPath)) {
247
258
  try {
248
259
  const entry = JSON.parse(fs.readFileSync(authPath, 'utf8'))[provider];
249
- if (entry && typeof entry === 'object' && typeof entry.key === 'string' && entry.key
250
- && (!entry.type || entry.type === 'api')) {
260
+ if (
261
+ entry &&
262
+ typeof entry === 'object' &&
263
+ typeof entry.key === 'string' &&
264
+ entry.key &&
265
+ (!entry.type || entry.type === 'api')
266
+ ) {
251
267
  row = { label: 'API key', value: JSON.stringify({ type: 'key', key: entry.key }) };
252
268
  }
253
- } catch { /* unreadable auth.json: nothing seedable */ }
269
+ } catch {
270
+ /* unreadable auth.json: nothing seedable */
271
+ }
254
272
  }
255
273
  if (!row) return false;
256
274
  try {
257
275
  const db = new DatabaseSync(dbPath);
258
276
  try {
259
- db.prepare('INSERT INTO credential (id, integration_id, label, value, time_created, time_updated) VALUES (?, ?, ?, ?, ?, ?)')
260
- .run(`cred_${crypto.randomBytes(18).toString('base64url')}`, provider, row.label, row.value, Date.now(), Date.now());
277
+ db.prepare(
278
+ 'INSERT INTO credential (id, integration_id, label, value, time_created, time_updated) VALUES (?, ?, ?, ?, ?, ?)',
279
+ ).run(`cred_${crypto.randomBytes(18).toString('base64url')}`, provider, row.label, row.value, Date.now(), Date.now());
261
280
  } finally {
262
281
  db.close();
263
282
  }
@@ -110,12 +110,17 @@ export async function prepare() {
110
110
  // STUB_* observables (a stubbed CLI cannot tell it ran), keeping the
111
111
  // STUB_HELP knobs so a test can still shape the advertised flag list
112
112
  // and the probe's latency.
113
- const env = Object.fromEntries(Object.entries(process.env)
114
- .filter(([k]) => !k.startsWith('STUB_') || k === 'STUB_HELP' || k === 'STUB_HELP_DELAY_MS'));
113
+ const env = Object.fromEntries(
114
+ Object.entries(process.env).filter(([k]) => !k.startsWith('STUB_') || k === 'STUB_HELP' || k === 'STUB_HELP_DELAY_MS'),
115
+ );
115
116
  const child = spawnCommand(POLICY.clis.opencode.cli, ['run', '--help'], { env, stdio: ['ignore', 'pipe', 'pipe'] });
116
117
  let out = '';
117
- child.stdout.on('data', (c) => { out += c; });
118
- child.stderr.on('data', (c) => { out += c; });
118
+ child.stdout.on('data', (c) => {
119
+ out += c;
120
+ });
121
+ child.stderr.on('data', (c) => {
122
+ out += c;
123
+ });
119
124
  child.on('error', () => finishProbe(false, false));
120
125
  child.on('close', (code) => {
121
126
  // A nonzero exit proves nothing either way -- a diagnostic that
@@ -123,7 +128,14 @@ export async function prepare() {
123
128
  if (code !== 0) return finishProbe(false, false);
124
129
  finishProbe(out.includes('--standalone'), true);
125
130
  });
126
- timer = setTimeout(() => { try { child.kill('SIGKILL'); } catch { /* gone */ } finishProbe(false, false); }, 10_000);
131
+ timer = setTimeout(() => {
132
+ try {
133
+ child.kill('SIGKILL');
134
+ } catch {
135
+ /* gone */
136
+ }
137
+ finishProbe(false, false);
138
+ }, 10_000);
127
139
  timer.unref();
128
140
  } catch {
129
141
  finishProbe(false, false);
@@ -170,7 +182,8 @@ export function buildInvocation({ consultant }) {
170
182
  const effort = consultant.effort ?? 'high';
171
183
  const args = [
172
184
  'run',
173
- '--format', 'json',
185
+ '--format',
186
+ 'json',
174
187
  ...(standaloneSupport ? ['--standalone'] : []),
175
188
  // A failing 2.x run can exit nonzero with no error event and an empty
176
189
  // stderr (observed on GLM after ~5 min), leaving nothing to say why. Its
@@ -183,8 +196,10 @@ export function buildInvocation({ consultant }) {
183
196
  // name the operator wrote with its own `#variant` is passed as is, and a
184
197
  // bare trailing `#` means "no variant", for models that have none
185
198
  // ("Variant unavailable for opencode/nemotron-3-ultra-free: high").
186
- '-m', chosen.endsWith('#') ? chosen.slice(0, -1) : chosen.includes('#') ? chosen : `${chosen}#${effort}`,
187
- '--title', 'severally',
199
+ '-m',
200
+ chosen.endsWith('#') ? chosen.slice(0, -1) : chosen.includes('#') ? chosen : `${chosen}#${effort}`,
201
+ '--title',
202
+ 'severally',
188
203
  ];
189
204
  return { command: t.cli, args, model: chosen };
190
205
  }
@@ -198,7 +213,9 @@ export function events(stdout) {
198
213
  try {
199
214
  const parsed = JSON.parse(t);
200
215
  if (parsed && typeof parsed === 'object') out.push(parsed);
201
- } catch { /* a partial line, e.g. when the child was killed mid-write */ }
216
+ } catch {
217
+ /* a partial line, e.g. when the child was killed mid-write */
218
+ }
202
219
  }
203
220
  return out;
204
221
  }
@@ -216,7 +233,10 @@ export function compactLine(line) {
216
233
  try {
217
234
  const e = JSON.parse(line);
218
235
  if (e?.type !== 'tool_use') return line;
219
- return JSON.stringify({ type: e.type, part: { type: e.part?.type, tool: e.part?.tool, state: { status: e.part?.state?.status } } });
236
+ return JSON.stringify({
237
+ type: e.type,
238
+ part: { type: e.part?.type, tool: e.part?.tool, state: { status: e.part?.state?.status } },
239
+ });
220
240
  } catch {
221
241
  return line;
222
242
  }
@@ -230,9 +250,7 @@ function errorMessage(error) {
230
250
  // message are joined, as before. A CLI upgrade must degrade the wording and
231
251
  // never the classification.
232
252
  if (typeof error.message === 'string' && error.message) return error.message;
233
- const detail = error.data && typeof error.data === 'object' && error.data.message
234
- ? String(error.data.message)
235
- : '';
253
+ const detail = error.data && typeof error.data === 'object' && error.data.message ? String(error.data.message) : '';
236
254
  return [error.name, detail].filter(Boolean).join(': ') || JSON.stringify(error);
237
255
  }
238
256
 
package/src/brief.mjs CHANGED
@@ -38,20 +38,21 @@ const GUARDRAILS_HEAD = [
38
38
 
39
39
  // After the web bullet.
40
40
  const GUARDRAILS_RULES = [
41
- '- When the question concerns security, it is a defensive review of the lead\'s own code or system: analyse the weaknesses and how to fix them.',
42
- '- The Constraints section is fixed. Design notes, drafts and internal documents among the facts and artifacts are the lead\'s current direction, not a specification, and you may question them.',
41
+ "- When the question concerns security, it is a defensive review of the lead's own code or system: analyse the weaknesses and how to fix them.",
42
+ "- The Constraints section is fixed. Design notes, drafts and internal documents among the facts and artifacts are the lead's current direction, not a specification, and you may question them.",
43
43
  ];
44
44
 
45
45
  // The web bullet follows the request: with the network off the consultant has
46
46
  // no web tools, and telling it otherwise would have it look for them.
47
47
  const WEB_ON = '- You may search and browse the web freely; cite what you actually opened in `references`.';
48
- const WEB_OFF = '- Web search and browsing are switched off for this consultation. Answer from the brief, and record what you would have looked up under `unknowns`.';
48
+ const WEB_OFF =
49
+ '- Web search and browsing are switched off for this consultation. Answer from the brief, and record what you would have looked up under `unknowns`.';
49
50
 
50
51
  // The one bullet that depends on the request: a consultation that exposed
51
52
  // paths has files, and telling it otherwise would have it record everything
52
53
  // it can plainly read under `unknowns`.
53
54
  const WORKDIR_EMPTY =
54
- '- You are running in an empty working directory and do not have the lead\'s repository; everything you are meant to have is in the brief below. If something is missing, record it under `unknowns` instead of guessing or substituting an assumption.';
55
+ "- You are running in an empty working directory and do not have the lead's repository; everything you are meant to have is in the brief below. If something is missing, record it under `unknowns` instead of guessing or substituting an assumption.";
55
56
  const WORKDIR_EXPOSED =
56
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 have. If something you need is missing, record it under `unknowns` instead of guessing or substituting an assumption.';
57
58
 
@@ -70,8 +71,13 @@ const GUARDRAILS_TAIL = [
70
71
  ];
71
72
 
72
73
  export function renderGuardrails(hasExposedPaths = false, network = 'on') {
73
- return [...GUARDRAILS_HEAD, network === 'off' ? WEB_OFF : WEB_ON, ...GUARDRAILS_RULES,
74
- hasExposedPaths ? WORKDIR_EXPOSED : WORKDIR_EMPTY, ...GUARDRAILS_TAIL].join('\n');
74
+ return [
75
+ ...GUARDRAILS_HEAD,
76
+ network === 'off' ? WEB_OFF : WEB_ON,
77
+ ...GUARDRAILS_RULES,
78
+ hasExposedPaths ? WORKDIR_EXPOSED : WORKDIR_EMPTY,
79
+ ...GUARDRAILS_TAIL,
80
+ ].join('\n');
75
81
  }
76
82
 
77
83
  // The material the lead handed over, kept in the shape it was sent in. The
@@ -100,22 +106,22 @@ export function briefRecord(req, exposeManifest = null) {
100
106
  // frozen duplicate of a repository that has moved on since.
101
107
  expose_paths: exposeManifest
102
108
  ? {
103
- entries: exposeManifest.entries.map((e) => ({
104
- index: e.index,
105
- source: e.source,
106
- exposed_as: e.exposedAs,
107
- kind: e.kind,
108
- files: e.files,
109
- bytes: e.bytes,
110
- // What every member was handed, so a reader can check they all got the same.
111
- sha256: e.sha256 ?? null,
112
- ...(e.base ? { base: e.base, changes: e.changes } : {}),
113
- // A follow-up's copy of a path the parent round exposed: whether its bytes changed since.
114
- ...(e.changed === undefined ? {} : { changed_since_parent: e.changed }),
115
- })),
116
- totals: exposeManifest.totals,
117
- skipped: exposeManifest.skipped,
118
- }
109
+ entries: exposeManifest.entries.map((e) => ({
110
+ index: e.index,
111
+ source: e.source,
112
+ exposed_as: e.exposedAs,
113
+ kind: e.kind,
114
+ files: e.files,
115
+ bytes: e.bytes,
116
+ // What every member was handed, so a reader can check they all got the same.
117
+ sha256: e.sha256 ?? null,
118
+ ...(e.base ? { base: e.base, changes: e.changes } : {}),
119
+ // A follow-up's copy of a path the parent round exposed: whether its bytes changed since.
120
+ ...(e.changed === undefined ? {} : { changed_since_parent: e.changed }),
121
+ })),
122
+ totals: exposeManifest.totals,
123
+ skipped: exposeManifest.skipped,
124
+ }
119
125
  : null,
120
126
  });
121
127
  }
@@ -151,21 +157,24 @@ function deletedLines(changes = []) {
151
157
  const gone = changes.filter((c) => c.endsWith(' (deleted)')).map((c) => c.slice(0, -' (deleted)'.length));
152
158
  if (!gone.length) return [];
153
159
  const shown = gone.slice(0, 50);
154
- return [` - deleted by the change, so not copied: ${shown.join(', ')}${gone.length > shown.length ? `, and ${gone.length - shown.length} more` : ''}`];
160
+ return [
161
+ ` - deleted by the change, so not copied: ${shown.join(', ')}${gone.length > shown.length ? `, and ${gone.length - shown.length} more` : ''}`,
162
+ ];
155
163
  }
156
164
 
157
165
  function renderExposeManifest(manifest) {
158
166
  if (!manifest || (manifest.entries.length === 0 && manifest.skipped.length === 0)) return null;
159
167
  let room = EXPOSE_LISTING_MAX;
160
168
  const lines = manifest.entries.flatMap((e) => {
161
- const shape = (e.kind === 'diff'
162
- ? `the files the lead's change touches, against ${e.base}, at their paths in the repository; ${e.files} copied, ${humanSize(e.bytes)}; the diff itself is among the artifacts`
163
- : e.kind === 'change_files'
164
- ? `the files the lead's change touched in the earlier round, at their paths in the repository, copied again as they are now; ${e.files} copied, ${humanSize(e.bytes)}; the diff is not repeated`
165
- : e.kind === 'directory'
166
- ? `directory, ${e.files} file(s), ${humanSize(e.bytes)}`
167
- : `file, ${humanSize(e.bytes)}`)
168
- + (e.changed === undefined ? '' : e.changed ? '; changed since the earlier round' : '; unchanged since the earlier round');
169
+ const shape =
170
+ (e.kind === 'diff'
171
+ ? `the files the lead's change touches, against ${e.base}, at their paths in the repository; ${e.files} copied, ${humanSize(e.bytes)}; the diff itself is among the artifacts`
172
+ : e.kind === 'change_files'
173
+ ? `the files the lead's change touched in the earlier round, at their paths in the repository, copied again as they are now; ${e.files} copied, ${humanSize(e.bytes)}; the diff is not repeated`
174
+ : e.kind === 'directory'
175
+ ? `directory, ${e.files} file(s), ${humanSize(e.bytes)}`
176
+ : `file, ${humanSize(e.bytes)}`) +
177
+ (e.changed === undefined ? '' : e.changed ? '; changed since the earlier round' : '; unchanged since the earlier round');
169
178
  // Not where it was copied from: the consultant is never told where the
170
179
  // lead's repository is, and Codex, Antigravity and OpenCode could read
171
180
  // the unmasked original there. The source path stays in the history.
@@ -180,12 +189,21 @@ function renderExposeManifest(manifest) {
180
189
  ];
181
190
  });
182
191
  const count = (reason) => manifest.skipped.filter((x) => x.reason === reason).length;
183
- const named = (reason) => manifest.skipped.filter((x) => x.reason === reason).map((x) => x.exposed_as).slice(0, 50).join(', ');
192
+ const named = (reason) =>
193
+ manifest.skipped
194
+ .filter((x) => x.reason === reason)
195
+ .map((x) => x.exposed_as)
196
+ .slice(0, 50)
197
+ .join(', ');
184
198
  const skipped = [
185
199
  count('symlink') ? `${count('symlink')} symlink(s), or path(s) through one, were skipped, not followed.` : null,
186
200
  count('protected') ? `${count('protected')} credential or history path(s) were left out on purpose.` : null,
187
- count('ignored') ? `${count('ignored')} path(s) (.git, node_modules, whatever .gitignore excludes) were left out as not source.` : null,
188
- count('submodule') ? `${count('submodule')} submodule(s) or nested repositor(ies) were left out; their files are not copied.` : null,
201
+ count('ignored')
202
+ ? `${count('ignored')} path(s) (.git, node_modules, whatever .gitignore excludes) were left out as not source.`
203
+ : null,
204
+ count('submodule')
205
+ ? `${count('submodule')} submodule(s) or nested repositor(ies) were left out; their files are not copied.`
206
+ : null,
189
207
  count('gone') ? `No longer there, so not copied again: ${named('gone')}.` : null,
190
208
  count('masked') ? `Not copied again, because the record of the earlier round masked the path: ${named('masked')}.` : null,
191
209
  count('replaced') ? `The earlier round's change is replaced by the diff given in this round: ${named('replaced')}.` : null,
@@ -193,7 +211,7 @@ function renderExposeManifest(manifest) {
193
211
  if (skipped.length) skipped.unshift('');
194
212
  return [
195
213
  'The lead exposed these paths; they are copied under ./workspace in your working directory and you may read',
196
- 'them directly. They are the only part of the lead\'s repository you have -- if you need something that is',
214
+ "them directly. They are the only part of the lead's repository you have -- if you need something that is",
197
215
  'not here, record it under `unknowns` rather than guessing. Text files were passed through the same',
198
216
  'credential masking as this brief; binary files were copied unchanged.',
199
217
  '',
@@ -219,11 +237,16 @@ function renderRound(p) {
219
237
  `### Round ${p.round} (${p.mode})`,
220
238
  `Question asked: ${p.question}`,
221
239
  `Your answer, summarised: ${p.summary}`,
222
- list('Points you raised:', p.findings,
223
- (f) => `- ${tag(f.id)}: ${clip(f.point)}${verdict(f)}${f.grounds ? `\n grounds: ${clip(f.grounds)}` : ''}`),
240
+ list(
241
+ 'Points you raised:',
242
+ p.findings,
243
+ (f) => `- ${tag(f.id)}: ${clip(f.point)}${verdict(f)}${f.grounds ? `\n grounds: ${clip(f.grounds)}` : ''}`,
244
+ ),
224
245
  list('Unknowns you named:', p.unknowns, (u) => `- ${tag(u.id)}: ${clip(u.point)}${verdict(u)}`),
225
246
  list('Checks you proposed:', p.next_checks, (c) => `- ${tag(c.id)}: ${clip(c.point)}${verdict(c)}`),
226
- ].filter(Boolean).join('\n');
247
+ ]
248
+ .filter(Boolean)
249
+ .join('\n');
227
250
  }
228
251
 
229
252
  /**
@@ -262,8 +285,8 @@ export function renderBrief(req, chain = { round: 1, priorRounds: [] }, exposeMa
262
285
  parts.push('');
263
286
  parts.push('## Earlier rounds of this same consultation');
264
287
  parts.push(
265
- 'Each point carries the lead\'s own check where there is one -- confirmed: it held; not_applicable: checked, and'
266
- + ' it does not apply here; unverifiable: could not be checked; unverified, or no verdict: not checked yet.',
288
+ "Each point carries the lead's own check where there is one -- confirmed: it held; not_applicable: checked, and" +
289
+ ' it does not apply here; unverifiable: could not be checked; unverified, or no verdict: not checked yet.',
267
290
  );
268
291
  const blocks = chain.priorRounds.map(renderRound);
269
292
  let kept = 0;
@@ -274,7 +297,8 @@ export function renderBrief(req, chain = { round: 1, priorRounds: [] }, exposeMa
274
297
  kept += 1;
275
298
  }
276
299
  const left = chain.priorRounds.slice(0, blocks.length - kept).map((p) => p.round);
277
- if (left.length) parts.push(`(Round${left.length > 1 ? 's' : ''} ${left.join(', ')} left out to keep this brief within its size.)`);
300
+ if (left.length)
301
+ parts.push(`(Round${left.length > 1 ? 's' : ''} ${left.join(', ')} left out to keep this brief within its size.)`);
278
302
  parts.push(...blocks.slice(blocks.length - kept));
279
303
  parts.push('');
280
304
  parts.push(