simt-dev 0.3.1 → 0.5.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/CHANGELOG.md +157 -0
- package/README.md +271 -62
- package/package.json +6 -2
- package/rules/core.md +1 -1
- package/src/advice/advice.mjs +229 -0
- package/src/agenthooks/policy.mjs +10 -2
- package/src/agenthooks/prompt.mjs +81 -0
- package/src/agenthooks/render.mjs +138 -17
- package/src/argv.mjs +28 -1
- package/src/bench/grade.mjs +3 -0
- package/src/bench/run.mjs +88 -54
- package/src/bench/tasks.mjs +38 -0
- package/src/bench/variants.mjs +456 -0
- package/src/brief/assemble.mjs +368 -0
- package/src/brief/kind.mjs +134 -0
- package/src/brief/packet.mjs +200 -0
- package/src/commands/agenthook.mjs +84 -5
- package/src/commands/bench.mjs +72 -15
- package/src/commands/brief.mjs +78 -0
- package/src/commands/compare.mjs +10 -2
- package/src/commands/decide.mjs +116 -0
- package/src/commands/distill.mjs +36 -0
- package/src/commands/help.mjs +62 -3
- package/src/commands/index.mjs +29 -1
- package/src/commands/lessons.mjs +127 -0
- package/src/commands/local.mjs +97 -0
- package/src/commands/map.mjs +42 -7
- package/src/commands/repoindex.mjs +4 -1
- package/src/commands/review.mjs +65 -0
- package/src/commands/rules.mjs +17 -2
- package/src/commands/skills.mjs +5 -50
- package/src/commands/skillscompile.mjs +255 -0
- package/src/commands/sync.mjs +15 -0
- package/src/commands/tests.mjs +2 -49
- package/src/commands/why.mjs +23 -25
- package/src/compare/learn.mjs +680 -0
- package/src/decisions/index.mjs +116 -65
- package/src/distill/distill.mjs +246 -0
- package/src/distill/engine.mjs +187 -0
- package/src/federation/client.mjs +188 -0
- package/src/federation/config.mjs +134 -0
- package/src/federation/query.mjs +48 -0
- package/src/graph/compile.mjs +475 -0
- package/src/index/gate.mjs +1 -13
- package/src/index/govern.mjs +23 -11
- package/src/index/map.mjs +251 -17
- package/src/index/persist.mjs +74 -7
- package/src/index/store.mjs +222 -8
- package/src/kernel/variants.mjs +353 -0
- package/src/ledger/index.mjs +36 -1
- package/src/local/client.mjs +482 -0
- package/src/local/config.mjs +269 -0
- package/src/mcp/tools.mjs +138 -1
- package/src/memory/decide.mjs +462 -0
- package/src/memory/lessons.mjs +858 -0
- package/src/projection/lock.mjs +39 -7
- package/src/projection/rules.mjs +32 -8
- package/src/resolve/body.mjs +46 -0
- package/src/review/critic.mjs +509 -0
- package/src/toolwords.mjs +6 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
// Local model configuration in `simt.json` / `simt-dev.json` (plan 0003 R37, ADR-075, ADR-090).
|
|
2
|
+
//
|
|
3
|
+
// Local models are optional and off by default. When `local` is absent from the manifest,
|
|
4
|
+
// no socket is ever opened. When configured:
|
|
5
|
+
//
|
|
6
|
+
// "local": {
|
|
7
|
+
// "chat": { "url": "http://127.0.0.1:11434/v1", "model": "qwen2.5-coder:7b" },
|
|
8
|
+
// "embed": { "url": "http://127.0.0.1:11434/v1", "model": "nomic-embed-code" }
|
|
9
|
+
// }
|
|
10
|
+
//
|
|
11
|
+
// Non-loopback URLs are refused unless `--allow-remote` is explicitly passed (ADR-075).
|
|
12
|
+
|
|
13
|
+
import { EXIT, fail } from '../output/index.mjs';
|
|
14
|
+
import { MANIFEST_FILE } from '../name.mjs';
|
|
15
|
+
import { readManifest } from '../projection/manifest.mjs';
|
|
16
|
+
|
|
17
|
+
/** Default per-request timeout for local model probes and requests, in milliseconds. */
|
|
18
|
+
export const DEFAULT_LOCAL_TIMEOUT_MS = 5000;
|
|
19
|
+
|
|
20
|
+
const ROLES = ['chat', 'embed'];
|
|
21
|
+
|
|
22
|
+
const IPV4_LOOPBACK = /^127(?:\.(?:25[0-5]|2[0-4]\d|1?\d?\d)){3}$/;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Whether a parsed URL hostname resolves strictly to the local loopback interface.
|
|
26
|
+
*
|
|
27
|
+
* Accepts `localhost` (and `*.localhost`), IPv4 `127.0.0.0/8`, and IPv6 `::1`.
|
|
28
|
+
* Wildcard bind addresses (`0.0.0.0`, `::`) and LAN/remote hosts return false.
|
|
29
|
+
*/
|
|
30
|
+
export function isLoopbackHost(hostname) {
|
|
31
|
+
if (typeof hostname !== 'string') return false;
|
|
32
|
+
const clean = hostname.trim().toLowerCase().replace(/^\[|\]$/g, '');
|
|
33
|
+
if (!clean) return false;
|
|
34
|
+
if (clean === 'localhost' || clean.endsWith('.localhost')) return true;
|
|
35
|
+
if (IPV4_LOOPBACK.test(clean)) return true;
|
|
36
|
+
if (clean === '::1' || clean === '0:0:0:0:0:0:0:1') return true;
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Whether a URL string is a valid HTTP(S) loopback URL with no embedded credentials.
|
|
42
|
+
*/
|
|
43
|
+
export function isLoopbackUrl(urlString) {
|
|
44
|
+
if (typeof urlString !== 'string' || urlString.trim().length === 0) return false;
|
|
45
|
+
let parsed;
|
|
46
|
+
try {
|
|
47
|
+
parsed = new URL(urlString.trim());
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return false;
|
|
52
|
+
if (parsed.username !== '' || parsed.password !== '') return false;
|
|
53
|
+
return isLoopbackHost(parsed.hostname);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Normalise an OpenAI-compatible base URL and append an endpoint path under `/v1`.
|
|
58
|
+
*
|
|
59
|
+
* Accepts base URLs written either with or without a trailing `/v1` (for example
|
|
60
|
+
* `http://127.0.0.1:11434` or `http://127.0.0.1:11434/v1`) so Ollama, llama.cpp
|
|
61
|
+
* `llama-server`, LM Studio and MLX configs work identically.
|
|
62
|
+
*/
|
|
63
|
+
export function resolveEndpointUrl(baseUrl, endpointPath = '') {
|
|
64
|
+
const trimmedBase = String(baseUrl ?? '').trim().replace(/\/+$/, '');
|
|
65
|
+
const baseWithV1 = trimmedBase.endsWith('/v1') ? trimmedBase : `${trimmedBase}/v1`;
|
|
66
|
+
const cleanPath = String(endpointPath ?? '')
|
|
67
|
+
.trim()
|
|
68
|
+
.replace(/^\/+/, '')
|
|
69
|
+
.replace(/^v1(?:\/+|$)/, '');
|
|
70
|
+
return cleanPath ? `${baseWithV1}/${cleanPath}` : baseWithV1;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function parseEndpoint(role, entry, { allowRemote = false } = {}) {
|
|
74
|
+
if (entry === null || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
75
|
+
return { endpoint: null, finding: `\`local.${role}\` must be an object with \`url\` and \`model\`` };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const rawUrl = typeof entry.url === 'string' ? entry.url.trim() : '';
|
|
79
|
+
if (!rawUrl) {
|
|
80
|
+
return { endpoint: null, finding: `\`local.${role}.url\` is required` };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let parsedUrl;
|
|
84
|
+
try {
|
|
85
|
+
parsedUrl = new URL(rawUrl);
|
|
86
|
+
} catch {
|
|
87
|
+
return { endpoint: null, finding: `\`local.${role}.url\` (${rawUrl}) is not a valid URL` };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
|
|
91
|
+
return {
|
|
92
|
+
endpoint: null,
|
|
93
|
+
finding: `\`local.${role}.url\` (${rawUrl}) must use http:// or https://`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (parsedUrl.username !== '' || parsedUrl.password !== '') {
|
|
98
|
+
return {
|
|
99
|
+
endpoint: null,
|
|
100
|
+
finding: `\`local.${role}.url\` must not embed credentials in the URL`,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const model = typeof entry.model === 'string' ? entry.model.trim() : '';
|
|
105
|
+
if (!model) {
|
|
106
|
+
return { endpoint: null, finding: `\`local.${role}.model\` is required` };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const timeoutMs = entry.timeoutMs === undefined ? DEFAULT_LOCAL_TIMEOUT_MS : entry.timeoutMs;
|
|
110
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) {
|
|
111
|
+
return {
|
|
112
|
+
endpoint: null,
|
|
113
|
+
finding: `\`local.${role}.timeoutMs\` must be a positive whole number`,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const loopback = isLoopbackHost(parsedUrl.hostname);
|
|
118
|
+
const baseUrl = resolveEndpointUrl(rawUrl, '');
|
|
119
|
+
const blocked = !loopback && !allowRemote;
|
|
120
|
+
|
|
121
|
+
if (blocked) {
|
|
122
|
+
return {
|
|
123
|
+
endpoint: {
|
|
124
|
+
role,
|
|
125
|
+
url: rawUrl,
|
|
126
|
+
baseUrl,
|
|
127
|
+
model,
|
|
128
|
+
timeoutMs,
|
|
129
|
+
loopback: false,
|
|
130
|
+
blocked: true,
|
|
131
|
+
},
|
|
132
|
+
remoteBlocked: true,
|
|
133
|
+
finding: `\`local.${role}.url\` (${rawUrl}) is not a loopback address; non-loopback URLs are refused without --allow-remote (ADR-075)`,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
endpoint: {
|
|
139
|
+
role,
|
|
140
|
+
url: rawUrl,
|
|
141
|
+
baseUrl,
|
|
142
|
+
model,
|
|
143
|
+
timeoutMs,
|
|
144
|
+
loopback,
|
|
145
|
+
blocked: false,
|
|
146
|
+
},
|
|
147
|
+
remoteBlocked: false,
|
|
148
|
+
finding: null,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Parse and validate the `local` block of a manifest. Pure: never opens a file or socket.
|
|
154
|
+
*/
|
|
155
|
+
export function parseLocalConfig(manifest, { allowRemote = false } = {}) {
|
|
156
|
+
const block = manifest?.local;
|
|
157
|
+
if (block === undefined || block === null) {
|
|
158
|
+
return { configured: false, chat: null, embed: null, findings: [], remoteBlocked: false };
|
|
159
|
+
}
|
|
160
|
+
if (typeof block !== 'object' || Array.isArray(block)) {
|
|
161
|
+
return {
|
|
162
|
+
configured: true,
|
|
163
|
+
chat: null,
|
|
164
|
+
embed: null,
|
|
165
|
+
findings: ['`local` must be an object'],
|
|
166
|
+
remoteBlocked: false,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const findings = [];
|
|
171
|
+
let remoteBlocked = false;
|
|
172
|
+
|
|
173
|
+
for (const key of Object.keys(block)) {
|
|
174
|
+
if (!ROLES.includes(key)) {
|
|
175
|
+
findings.push(`\`local.${key}\` is not a recognised local model role (expected \`chat\` or \`embed\`)`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const parsedRoles = { chat: null, embed: null };
|
|
180
|
+
for (const role of ROLES) {
|
|
181
|
+
const raw = block[role];
|
|
182
|
+
if (raw === undefined || raw === null) continue;
|
|
183
|
+
const parsed = parseEndpoint(role, raw, { allowRemote });
|
|
184
|
+
parsedRoles[role] = parsed.endpoint;
|
|
185
|
+
if (parsed.remoteBlocked) remoteBlocked = true;
|
|
186
|
+
if (parsed.finding) findings.push(parsed.finding);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const configured =
|
|
190
|
+
parsedRoles.chat !== null ||
|
|
191
|
+
parsedRoles.embed !== null ||
|
|
192
|
+
findings.length > 0;
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
configured,
|
|
196
|
+
chat: parsedRoles.chat,
|
|
197
|
+
embed: parsedRoles.embed,
|
|
198
|
+
findings,
|
|
199
|
+
remoteBlocked,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Read and validate the `local` block from a repository's manifest on disk.
|
|
205
|
+
*/
|
|
206
|
+
export async function readLocalConfig(root, { allowRemote = false } = {}) {
|
|
207
|
+
const { manifest, exists, error } = await readManifest(root);
|
|
208
|
+
if (!exists) {
|
|
209
|
+
return {
|
|
210
|
+
configured: false,
|
|
211
|
+
chat: null,
|
|
212
|
+
embed: null,
|
|
213
|
+
findings: [],
|
|
214
|
+
remoteBlocked: false,
|
|
215
|
+
manifestExists: false,
|
|
216
|
+
manifestError: null,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
if (error !== null || manifest === null) {
|
|
220
|
+
return {
|
|
221
|
+
configured: false,
|
|
222
|
+
chat: null,
|
|
223
|
+
embed: null,
|
|
224
|
+
findings: [`${MANIFEST_FILE} is ${error ?? 'unreadable'}`],
|
|
225
|
+
remoteBlocked: false,
|
|
226
|
+
manifestExists: true,
|
|
227
|
+
manifestError: error ?? 'unreadable',
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
...parseLocalConfig(manifest, { allowRemote }),
|
|
232
|
+
manifestExists: true,
|
|
233
|
+
manifestError: null,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* The guard every model consumer shares (R37's distill, R39's critic and bench).
|
|
239
|
+
*
|
|
240
|
+
* A `--local`-shaped call without a usable chat endpoint is a configuration
|
|
241
|
+
* error, never a silent fallback to the deterministic path: a caller who asked
|
|
242
|
+
* for the model and got the deterministic answer would read that answer as the
|
|
243
|
+
* model's — and the reverse, reading model output as deterministic, is worse.
|
|
244
|
+
* Second real user arrived with the critic, so the guard moved here from the
|
|
245
|
+
* distiller's `runEngine` (ADR-002).
|
|
246
|
+
*
|
|
247
|
+
* Returns `{ chat }` on success or `{ exit }` with the failure already shaped
|
|
248
|
+
* (the same pass-through shape the command layer returns).
|
|
249
|
+
*/
|
|
250
|
+
export function chatEndpoint(config) {
|
|
251
|
+
if (config.manifestError !== null) {
|
|
252
|
+
return { exit: fail(`${MANIFEST_FILE}: ${config.manifestError}`, EXIT.CONFIG) };
|
|
253
|
+
}
|
|
254
|
+
if (!config.configured || !config.chat || config.findings.length > 0) {
|
|
255
|
+
const detail = !config.configured
|
|
256
|
+
? 'no local model is configured'
|
|
257
|
+
: config.remoteBlocked
|
|
258
|
+
? 'the configured chat endpoint is non-loopback and was refused'
|
|
259
|
+
: 'the local configuration has errors';
|
|
260
|
+
return {
|
|
261
|
+
exit: fail(
|
|
262
|
+
`--local needs a usable local chat endpoint: ${detail}`,
|
|
263
|
+
EXIT.CONFIG,
|
|
264
|
+
`configure local.chat in ${MANIFEST_FILE} (plan 0003 R37), or run without --local for the deterministic path`,
|
|
265
|
+
),
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
return { chat: config.chat };
|
|
269
|
+
}
|
package/src/mcp/tools.mjs
CHANGED
|
@@ -27,6 +27,10 @@ import { discover } from '../compare/discover.mjs';
|
|
|
27
27
|
import { buildRow, summarise } from '../compare/row.mjs';
|
|
28
28
|
import { GATES } from '../hooks/gates.mjs';
|
|
29
29
|
import { dirtyPaths, parsePathList, readBody } from '../commands/skills.mjs';
|
|
30
|
+
import { assembleBrief } from '../brief/assemble.mjs';
|
|
31
|
+
import { DEFAULT_BRIEF_BYTES } from '../brief/packet.mjs';
|
|
32
|
+
import { readFederation } from '../federation/config.mjs';
|
|
33
|
+
import { consult } from '../federation/query.mjs';
|
|
30
34
|
import { planNext, planDone } from '../plan/index.mjs';
|
|
31
35
|
import { resolveWorkspace } from '../workspace.mjs';
|
|
32
36
|
import { measureSkill } from '../graph/load.mjs';
|
|
@@ -333,6 +337,47 @@ const output = {
|
|
|
333
337
|
},
|
|
334
338
|
required: ['root', 'id', 'wouldChange', 'dryRun'],
|
|
335
339
|
},
|
|
340
|
+
brief: {
|
|
341
|
+
type: 'object',
|
|
342
|
+
properties: {
|
|
343
|
+
root: { type: 'string' },
|
|
344
|
+
kind: { type: 'string' },
|
|
345
|
+
basis: { type: 'string' },
|
|
346
|
+
task: { type: 'string' },
|
|
347
|
+
scope: { type: 'array', items: { type: 'string' } },
|
|
348
|
+
budget: { type: 'number' },
|
|
349
|
+
bytes: { type: 'number' },
|
|
350
|
+
before: { type: 'number' },
|
|
351
|
+
floorHeld: { type: 'boolean' },
|
|
352
|
+
cut: { type: 'array', items: { type: 'string' } },
|
|
353
|
+
sections: { type: 'object', additionalProperties: { type: 'array', items: { type: 'string' } } },
|
|
354
|
+
text: { type: 'string' },
|
|
355
|
+
},
|
|
356
|
+
required: ['root', 'kind', 'basis', 'task', 'scope', 'budget', 'bytes', 'before', 'floorHeld', 'cut', 'sections', 'text'],
|
|
357
|
+
},
|
|
358
|
+
federation_query: {
|
|
359
|
+
type: 'object',
|
|
360
|
+
properties: {
|
|
361
|
+
root: { type: 'string' },
|
|
362
|
+
query: { type: 'string' },
|
|
363
|
+
servers: { type: 'array', items: { type: 'string' } },
|
|
364
|
+
answers: {
|
|
365
|
+
type: 'array',
|
|
366
|
+
items: {
|
|
367
|
+
type: 'object',
|
|
368
|
+
properties: {
|
|
369
|
+
server: { type: 'string' },
|
|
370
|
+
tool: { type: 'string' },
|
|
371
|
+
query: { type: 'string' },
|
|
372
|
+
text: { type: 'string' },
|
|
373
|
+
},
|
|
374
|
+
required: ['server', 'tool', 'query', 'text'],
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
findings: { type: 'array', items: { type: 'string' } },
|
|
378
|
+
},
|
|
379
|
+
required: ['root', 'query', 'servers', 'answers', 'findings'],
|
|
380
|
+
},
|
|
336
381
|
/** The shape of every tool failure, which is a result and not a protocol error. */
|
|
337
382
|
error: ERROR_SHAPE,
|
|
338
383
|
};
|
|
@@ -624,7 +669,7 @@ export const TOOLS = [
|
|
|
624
669
|
const { decisions } = await loadDecisions();
|
|
625
670
|
const index = byId(decisions);
|
|
626
671
|
|
|
627
|
-
if (/^ADR-\d{3}$/i.test(what)) {
|
|
672
|
+
if (/^ADR-\d{3,}$/i.test(what)) {
|
|
628
673
|
const id = what.toUpperCase();
|
|
629
674
|
const d = decisions.find((x) => x.id === id);
|
|
630
675
|
if (!d) throw new Error(`no decision record ${id}`);
|
|
@@ -772,6 +817,98 @@ export const TOOLS = [
|
|
|
772
817
|
};
|
|
773
818
|
},
|
|
774
819
|
},
|
|
820
|
+
{
|
|
821
|
+
outputSchema: output.brief,
|
|
822
|
+
name: 'brief',
|
|
823
|
+
description:
|
|
824
|
+
'The packet for one task, in the bytes a prompt can carry: the kind of task it is, the commands that kind needs, the files the index ranks highest, the tests that cover them, the decisions that govern them, the skills that apply and the lessons that name those paths. Every argument is optional - with no task it describes the repository itself.',
|
|
825
|
+
inputSchema: {
|
|
826
|
+
type: 'object',
|
|
827
|
+
properties: {
|
|
828
|
+
root: { type: 'string', description: 'repository root; defaults to the editor cwd' },
|
|
829
|
+
task: { type: 'string', description: "what the agent is about to do, in the user's own words" },
|
|
830
|
+
paths: { type: 'string', description: 'comma-separated paths to scope the packet to, instead of the files git reports as modified' },
|
|
831
|
+
budget: { type: 'number', description: 'byte ceiling; 0 means the whole packet (default 2048)' },
|
|
832
|
+
},
|
|
833
|
+
required: [],
|
|
834
|
+
},
|
|
835
|
+
async run(args) {
|
|
836
|
+
const root = rootOf(args);
|
|
837
|
+
const budget = Number.isFinite(args?.budget) && args.budget >= 0 ? args.budget : DEFAULT_BRIEF_BYTES;
|
|
838
|
+
// Read, never build: every tool here declares itself read-only, and a call that
|
|
839
|
+
// indexed the tree would make that declaration false. A brief with no index to read
|
|
840
|
+
// says which command makes one - the same rule the delivered packet follows, so a
|
|
841
|
+
// pull and a push pay the same price and neither one writes.
|
|
842
|
+
const result = await assembleBrief({
|
|
843
|
+
root,
|
|
844
|
+
task: typeof args?.task === 'string' ? args.task : '',
|
|
845
|
+
paths: typeof args?.paths === 'string' ? args.paths : null,
|
|
846
|
+
budget,
|
|
847
|
+
build: false,
|
|
848
|
+
});
|
|
849
|
+
if (result.error) throw Object.assign(new Error('the index could not be read'), { remedy: `run: ${CLI} index` });
|
|
850
|
+
if (result.cold) throw Object.assign(new Error('this repository has no index to answer from'), { remedy: `run: ${CLI} index` });
|
|
851
|
+
return {
|
|
852
|
+
root: result.root,
|
|
853
|
+
kind: result.kind,
|
|
854
|
+
basis: result.basis,
|
|
855
|
+
task: result.task,
|
|
856
|
+
scope: result.scope,
|
|
857
|
+
budget: result.budget,
|
|
858
|
+
bytes: result.packed.after,
|
|
859
|
+
before: result.packed.before,
|
|
860
|
+
floorHeld: result.packed.floorHeld,
|
|
861
|
+
cut: result.packed.cut,
|
|
862
|
+
sections: result.sections,
|
|
863
|
+
text: result.packed.text,
|
|
864
|
+
};
|
|
865
|
+
},
|
|
866
|
+
},
|
|
867
|
+
{
|
|
868
|
+
outputSchema: output.federation_query,
|
|
869
|
+
name: 'federation_query',
|
|
870
|
+
description:
|
|
871
|
+
'Ask a code-graph server this repository declared one structural question, and return its answer with the name of the server that gave it. Read-only: it reads the "federation" block from simt.json and starts only the servers the manifest itself names, and a repository that declared none is told so rather than queried.',
|
|
872
|
+
inputSchema: {
|
|
873
|
+
type: 'object',
|
|
874
|
+
properties: {
|
|
875
|
+
root: { type: 'string', description: 'repository root; defaults to the editor cwd' },
|
|
876
|
+
query: { type: 'string', description: "the structural question to ask, in the user's own words" },
|
|
877
|
+
server: { type: 'string', description: 'ask only this declared server, by name; omit to ask every one of them' },
|
|
878
|
+
},
|
|
879
|
+
required: ['query'],
|
|
880
|
+
},
|
|
881
|
+
async run(args) {
|
|
882
|
+
const root = rootOf(args);
|
|
883
|
+
const query = typeof args?.query === 'string' ? args.query.trim() : '';
|
|
884
|
+
if (query.length === 0) {
|
|
885
|
+
throw Object.assign(new Error('the query is empty'), {
|
|
886
|
+
remedy: 'pass a query, for example the symbol or file to look up',
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
const { servers, findings } = await readFederation(root);
|
|
890
|
+
if (servers.length === 0) {
|
|
891
|
+
throw Object.assign(new Error('this repository declares no federation server'), {
|
|
892
|
+
remedy: 'add a "federation" block naming a server to simt.json, or use the brief tool',
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
const only = typeof args?.server === 'string' && args.server.length > 0 ? args.server : null;
|
|
896
|
+
const chosen = only === null ? servers : servers.filter((server) => server.name === only);
|
|
897
|
+
if (only !== null && chosen.length === 0) {
|
|
898
|
+
throw Object.assign(new Error(`no declared federation server is named ${only}`), {
|
|
899
|
+
remedy: `declared servers: ${servers.map((server) => server.name).join(', ')}`,
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
const { answers, findings: failures } = await consult({ root, query, servers: chosen });
|
|
903
|
+
return {
|
|
904
|
+
root,
|
|
905
|
+
query,
|
|
906
|
+
servers: chosen.map((server) => server.name),
|
|
907
|
+
answers,
|
|
908
|
+
findings: [...findings, ...failures],
|
|
909
|
+
};
|
|
910
|
+
},
|
|
911
|
+
},
|
|
775
912
|
];
|
|
776
913
|
|
|
777
914
|
export function toolNames() {
|