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.
- package/README.ja.md +17 -7
- package/README.md +17 -7
- package/bin/severally-mcp.mjs +14 -2
- package/docs/install.ja.md +48 -20
- package/docs/install.md +48 -21
- package/package.json +13 -3
- package/plugins/severally/.claude-plugin/plugin.json +1 -1
- package/plugins/severally/.codex-plugin/plugin.json +1 -1
- package/plugins/severally/dist/severally-mcp.mjs +10669 -10404
- package/scripts/build.mjs +31 -26
- package/scripts/doctor.mjs +52 -17
- package/scripts/init-config.mjs +8 -6
- package/scripts/install.mjs +259 -86
- package/scripts/live-check.mjs +52 -19
- package/scripts/live-mcp-check.mjs +42 -12
- package/scripts/test.mjs +10 -4
- package/src/adapters/antigravity-sandbox.mjs +39 -23
- package/src/adapters/antigravity.mjs +34 -16
- package/src/adapters/claude-code.mjs +73 -21
- package/src/adapters/codex.mjs +69 -27
- package/src/adapters/opencode-sandbox.mjs +40 -21
- package/src/adapters/opencode.mjs +31 -13
- package/src/brief.mjs +65 -41
- package/src/consultants.mjs +72 -24
- package/src/export.mjs +49 -12
- package/src/expose-paths.mjs +162 -44
- package/src/failures.mjs +23 -13
- package/src/jobs.mjs +320 -152
- package/src/parse-result.mjs +85 -30
- package/src/platform.mjs +5 -7
- package/src/policy.mjs +97 -27
- package/src/redact.mjs +38 -14
- package/src/run.mjs +67 -18
- package/src/schema.mjs +50 -18
- package/src/server.mjs +134 -72
- package/src/status.mjs +81 -12
- package/src/store.mjs +131 -29
package/src/adapters/codex.mjs
CHANGED
|
@@ -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',
|
|
73
|
-
'--
|
|
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) => {
|
|
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)
|
|
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)
|
|
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',
|
|
110
|
-
'
|
|
111
|
-
'-
|
|
112
|
-
|
|
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',
|
|
115
|
-
'
|
|
116
|
-
'-c',
|
|
117
|
-
'
|
|
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',
|
|
123
|
-
|
|
124
|
-
'-
|
|
125
|
-
|
|
126
|
-
'-
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
274
|
-
|
|
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 {
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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 {
|
|
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
|
|
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 {
|
|
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 (
|
|
250
|
-
|
|
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 {
|
|
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(
|
|
260
|
-
|
|
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(
|
|
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) => {
|
|
118
|
-
|
|
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(() => {
|
|
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',
|
|
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',
|
|
187
|
-
'
|
|
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 {
|
|
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({
|
|
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
|
-
|
|
42
|
-
|
|
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 =
|
|
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
|
-
|
|
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 [
|
|
74
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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 [
|
|
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 =
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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) =>
|
|
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')
|
|
188
|
-
|
|
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
|
-
|
|
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(
|
|
223
|
-
|
|
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
|
-
]
|
|
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
|
-
|
|
266
|
-
|
|
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)
|
|
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(
|