simt-dev 0.2.0 → 0.2.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/CHANGELOG.md +375 -7
- package/README.md +4 -4
- package/docs/decisions/ADR-019-a-declared-command-is-the-runner.md +27 -0
- package/docs/decisions/ADR-059-the-version-a-repository-runs-is-the-one-it-pinned.md +75 -0
- package/docs/decisions/ADR-060-a-repository-with-nothing-to-run-is-adopted-and-says-so.md +131 -0
- package/docs/decisions/ADR-061-the-skill-registry-is-workspace-data.md +113 -0
- package/docs/decisions/ADR-062-a-fresh-adoption-seeds-the-gates-it-can-run.md +94 -0
- package/docs/decisions/ADR-063-the-adoption-escape-hatch-is-opt-in.md +69 -0
- package/docs/decisions/ADR-064-the-context-meter-counts-what-the-tool-wrote.md +107 -0
- package/docs/decisions/ADR-065-a-pack-is-one-shared-rule-set-pinned-by-digest.md +97 -0
- package/docs/decisions/ADR-066-a-monorepo-projects-once-per-package.md +115 -0
- package/docs/decisions/ADR-067-which-harnesses-read-agents-md-is-a-column.md +110 -0
- package/docs/decisions/ADR-068-the-mcp-revision-list-cites-what-was-read.md +111 -0
- package/docs/decisions/ADR-069-the-conformance-lint-checks-the-published-specification.md +90 -0
- package/docs/decisions/ADR-070-the-json-payload-carries-its-own-version.md +91 -0
- package/docs/decisions/ADR-071-a-rule-that-can-be-narrowed-is-written-narrow.md +106 -0
- package/docs/decisions/ADR-072-the-budget-is-a-declaration-beside-the-gates.md +104 -0
- package/docs/decisions/ADR-073-a-kernel-is-a-smaller-projection-the-repository-asks-for.md +92 -0
- package/docs/decisions/README.md +17 -0
- package/package.json +1 -2
- package/rules/core.md +1 -0
- package/rules/stacks/android.md +2 -0
- package/rules/stacks/cloudflare-workers.md +2 -0
- package/rules/stacks/connectiq.md +1 -0
- package/rules/stacks/container.md +2 -0
- package/rules/stacks/dart.md +40 -0
- package/rules/stacks/deno.md +38 -0
- package/rules/stacks/dotnet.md +37 -0
- package/rules/stacks/elixir.md +41 -0
- package/rules/stacks/go.md +36 -0
- package/rules/stacks/jvm.md +2 -0
- package/rules/stacks/node.md +2 -0
- package/rules/stacks/python.md +2 -0
- package/rules/stacks/react-native.md +2 -0
- package/rules/stacks/react-web.md +2 -0
- package/rules/stacks/ruby.md +38 -0
- package/rules/stacks/rust.md +36 -0
- package/rules/stacks/swift.md +39 -0
- package/rules/stacks/typescript.md +2 -0
- package/rules/stacks/uv.md +2 -0
- package/src/argv.mjs +54 -6
- package/src/ci/dependabot.mjs +1 -1
- package/src/ci/workflow.mjs +33 -11
- package/src/commands/compare.mjs +15 -0
- package/src/commands/graph.mjs +58 -25
- package/src/commands/harness.mjs +150 -0
- package/src/commands/help.mjs +37 -5
- package/src/commands/hooks.mjs +173 -57
- package/src/commands/index.mjs +95 -69
- package/src/commands/init.mjs +100 -20
- package/src/commands/mcp.mjs +20 -5
- package/src/commands/rules.mjs +96 -60
- package/src/commands/skills.mjs +189 -86
- package/src/commands/skillsinstall.mjs +20 -5
- package/src/commands/sync.mjs +213 -15
- package/src/commands/tokens.mjs +138 -0
- package/src/compare/hooksstate.mjs +96 -9
- package/src/compare/row.mjs +107 -7
- package/src/decisions/index.mjs +21 -6
- package/src/graph/load.mjs +125 -14
- package/src/graph/spec.mjs +227 -0
- package/src/graph/speccheck.mjs +68 -0
- package/src/hooks/capability.mjs +48 -4
- package/src/hooks/dispatcher.mjs +84 -7
- package/src/hooks/gates.mjs +0 -2
- package/src/hooks/resolve.mjs +24 -25
- package/src/hooks/shim.mjs +29 -5
- package/src/identity.mjs +0 -11
- package/src/mcp/schema.mjs +139 -0
- package/src/mcp/server.mjs +197 -20
- package/src/mcp/tools.mjs +290 -3
- package/src/measure/context.mjs +371 -0
- package/src/monorepo.mjs +339 -0
- package/src/output/index.mjs +86 -9
- package/src/package-version.mjs +74 -0
- package/src/packs.mjs +182 -0
- package/src/projection/conflicts.mjs +56 -5
- package/src/projection/manifest.mjs +335 -25
- package/src/projection/mdc.mjs +4 -13
- package/src/projection/project.mjs +139 -8
- package/src/projection/rules.mjs +308 -27
- package/src/projection/surfaces.mjs +133 -15
- package/src/projection/targets.mjs +90 -7
- package/src/resolve/eval.mjs +64 -0
- package/src/resolve/fingerprint.mjs +1 -0
- package/src/resolve/near.mjs +26 -7
- package/src/resolve/resolve.mjs +1 -1
- package/src/resolve/source.mjs +1 -1
- package/src/workspace.mjs +0 -9
- package/graph/skills.graph.json +0 -1622
package/src/mcp/server.mjs
CHANGED
|
@@ -22,7 +22,24 @@
|
|
|
22
22
|
import { createInterface } from 'node:readline';
|
|
23
23
|
import { CLI } from '../name.mjs';
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Every protocol revision this server speaks, newest first.
|
|
27
|
+
*
|
|
28
|
+
* The list is a claim about revisions that were read, not about ones that exist: each
|
|
29
|
+
* entry names a published revision and the behaviour this file implements for it, and
|
|
30
|
+
* ADR-068 records what changed. A revision not in this list is answered with
|
|
31
|
+
* `UnsupportedProtocolVersionError` rather than an optimistic guess, because the failure
|
|
32
|
+
* mode of guessing is a client that believes a feature is on and watches it misbehave.
|
|
33
|
+
*/
|
|
34
|
+
export const SUPPORTED_VERSIONS = ['2026-07-28', '2025-11-25', '2025-06-18'];
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The revision this server answers with when the client has not asked for one.
|
|
38
|
+
*
|
|
39
|
+
* Named the way every caller has always named it, and it is the newest supported
|
|
40
|
+
* revision rather than the only one: a client that asks for an older one gets it.
|
|
41
|
+
*/
|
|
42
|
+
export const PROTOCOL_VERSION = SUPPORTED_VERSIONS[0];
|
|
26
43
|
|
|
27
44
|
/** JSON-RPC codes. -32700/-32600/-32601 are the spec's parse/request/method set. */
|
|
28
45
|
export const RPC = {
|
|
@@ -30,16 +47,60 @@ export const RPC = {
|
|
|
30
47
|
REQUEST: -32600,
|
|
31
48
|
METHOD: -32601,
|
|
32
49
|
INTERNAL: -32603,
|
|
50
|
+
/**
|
|
51
|
+
* `UnsupportedProtocolVersionError`, renumbered to -32022 in 2026-07-28. The spec
|
|
52
|
+
* reserves -32000..-32019 for implementations and -32020..-32099 for itself, so the
|
|
53
|
+
* older -32004 would now be an implementation-defined code in the wrong range.
|
|
54
|
+
*/
|
|
55
|
+
UNSUPPORTED_VERSION: -32022,
|
|
56
|
+
/** Resource not found: -32002 in every revision up to 2025-11-25, -32602 after. */
|
|
57
|
+
RESOURCE_NOT_FOUND_LEGACY: -32002,
|
|
58
|
+
RESOURCE_NOT_FOUND: -32602,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** The `_meta` keys 2026-07-28 puts on a request and in a result. */
|
|
62
|
+
export const META = {
|
|
63
|
+
PROTOCOL_VERSION: 'io.modelcontextprotocol/protocolVersion',
|
|
64
|
+
CLIENT_INFO: 'io.modelcontextprotocol/clientInfo',
|
|
65
|
+
CLIENT_CAPABILITIES: 'io.modelcontextprotocol/clientCapabilities',
|
|
66
|
+
SERVER_INFO: 'io.modelcontextprotocol/serverInfo',
|
|
33
67
|
};
|
|
34
68
|
|
|
69
|
+
/** Revisions that removed `ping`, per the 2026-07-28 changelog (SEP-2575). */
|
|
70
|
+
const PING_REMOVED_IN = '2026-07-28';
|
|
71
|
+
|
|
35
72
|
const SERVER_INFO = { name: 'simt-dev', title: 'SIMT developer and agent tooling' };
|
|
36
73
|
|
|
74
|
+
/**
|
|
75
|
+
* One hour, and the reason it is a number rather than an omission.
|
|
76
|
+
*
|
|
77
|
+
* `ttlMs` and `cacheScope` are required on list results from 2026-07-28, and the value
|
|
78
|
+
* is a fact about this server rather than a courtesy: the tool list changes when the
|
|
79
|
+
* release changes and not otherwise, so an hour is long enough to spare a client the
|
|
80
|
+
* round trip and short enough that a client started from a stale one re-reads it within
|
|
81
|
+
* a working day. `private`, because the list names the reader's own repository and no
|
|
82
|
+
* intermediary may hold it.
|
|
83
|
+
*/
|
|
84
|
+
const LIST_CACHE = { ttlMs: 3600000, cacheScope: 'private' };
|
|
85
|
+
|
|
37
86
|
/**
|
|
38
87
|
* Handle one parsed request. Pure with respect to the process: returns the response
|
|
39
88
|
* object or null (for a notification), and never writes anywhere. Testable without a
|
|
40
89
|
* pipe, which is the only way to test a line protocol honestly.
|
|
90
|
+
*
|
|
91
|
+
* `state` is the negotiated revision, and it is the only thing this function mutates.
|
|
92
|
+
* 2026-07-28 removed the handshake and made every request carry its own version, so a
|
|
93
|
+
* session is not part of the protocol any more - but a client on an older revision
|
|
94
|
+
* negotiates once and then sends bare requests, and answering those at the newest
|
|
95
|
+
* revision would hand it fields it did not ask for. So the version is read from the
|
|
96
|
+
* request when there is one and remembered only for the clients that need it.
|
|
97
|
+
*
|
|
98
|
+
* @param {object|null} request a parsed JSON-RPC message
|
|
99
|
+
* @param {object[]} tools
|
|
100
|
+
* @param {{packageVersion?: string, state?: {version: string|null}}} options
|
|
101
|
+
* @returns {Promise<object|null>}
|
|
41
102
|
*/
|
|
42
|
-
export async function handle(request, tools, {
|
|
103
|
+
export async function handle(request, tools, { packageVersion, state } = {}) {
|
|
43
104
|
if (request === null || typeof request !== 'object' || Array.isArray(request)) {
|
|
44
105
|
return error(null, RPC.REQUEST, 'request must be a JSON object');
|
|
45
106
|
}
|
|
@@ -54,25 +115,68 @@ export async function handle(request, tools, { version } = {}) {
|
|
|
54
115
|
}
|
|
55
116
|
if (isNotification) return null;
|
|
56
117
|
|
|
118
|
+
const asked = params?._meta?.[META.PROTOCOL_VERSION];
|
|
119
|
+
if (typeof asked === 'string' && !SUPPORTED_VERSIONS.includes(asked)) {
|
|
120
|
+
// The one place a client checks whether it got what it asked for, so this is where
|
|
121
|
+
// an unsupported version has to fail. The message names the revisions that do work,
|
|
122
|
+
// because "unsupported" alone leaves the client to guess between the two answers a
|
|
123
|
+
// server can give.
|
|
124
|
+
return error(
|
|
125
|
+
id,
|
|
126
|
+
RPC.UNSUPPORTED_VERSION,
|
|
127
|
+
`protocol version ${asked} is not supported; this server speaks ${SUPPORTED_VERSIONS.join(', ')}`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
const negotiated =
|
|
131
|
+
typeof asked === 'string' ? asked : method === 'initialize' ? negotiate(params?.protocolVersion) : state?.version;
|
|
132
|
+
// A caller that passes no `state` gets one request with no memory: `serve` threads one
|
|
133
|
+
// through the stream, and a test that calls this directly usually wants exactly that.
|
|
134
|
+
const session = state ?? { version: null };
|
|
135
|
+
if (method === 'initialize') session.version = negotiated;
|
|
136
|
+
const v = negotiated ?? PROTOCOL_VERSION;
|
|
137
|
+
|
|
138
|
+
const reply = (value) => result(id, decorate(value, v, { packageVersion, tools }));
|
|
139
|
+
|
|
57
140
|
switch (method) {
|
|
58
141
|
case 'initialize':
|
|
59
|
-
return
|
|
60
|
-
protocolVersion:
|
|
61
|
-
capabilities:
|
|
62
|
-
serverInfo
|
|
63
|
-
|
|
64
|
-
|
|
142
|
+
return reply({
|
|
143
|
+
protocolVersion: v,
|
|
144
|
+
capabilities: capabilities(tools),
|
|
145
|
+
// `serverInfo` stays in the body here even on 2026-07-28: a client that has just
|
|
146
|
+
// called a method the newest revision removed is by definition an older client,
|
|
147
|
+
// and the field is where it looks.
|
|
148
|
+
serverInfo: serverInfo(packageVersion),
|
|
149
|
+
instructions: INSTRUCTIONS,
|
|
65
150
|
});
|
|
66
151
|
|
|
67
152
|
case 'notifications/initialized':
|
|
68
153
|
case 'initialized':
|
|
69
154
|
return null;
|
|
70
155
|
|
|
156
|
+
// MUST, from 2026-07-28 on. Cheap, stateless, and the stdio way for a client that
|
|
157
|
+
// supports both shapes to find out which one it is talking to before it commits.
|
|
158
|
+
case 'server/discover':
|
|
159
|
+
return reply({
|
|
160
|
+
supportedVersions: [...SUPPORTED_VERSIONS],
|
|
161
|
+
capabilities: capabilities(tools),
|
|
162
|
+
instructions: INSTRUCTIONS,
|
|
163
|
+
...(cacheable(v) ? LIST_CACHE : {}),
|
|
164
|
+
});
|
|
165
|
+
|
|
71
166
|
case 'ping':
|
|
72
|
-
|
|
167
|
+
// Removed in 2026-07-28, and kept before it: a client that speaks an older
|
|
168
|
+
// revision and probes liveness gets an answer, and one that speaks the newest
|
|
169
|
+
// gets told the method is gone rather than a success it would read as support.
|
|
170
|
+
if (atLeast(v, PING_REMOVED_IN)) {
|
|
171
|
+
return error(id, RPC.METHOD, `ping was removed in ${PING_REMOVED_IN}; this session negotiated ${v}`);
|
|
172
|
+
}
|
|
173
|
+
return reply({});
|
|
73
174
|
|
|
74
175
|
case 'tools/list':
|
|
75
|
-
return
|
|
176
|
+
return reply({
|
|
177
|
+
tools: tools.map((t) => describe(t, v)),
|
|
178
|
+
...(cacheable(v) ? LIST_CACHE : {}),
|
|
179
|
+
});
|
|
76
180
|
|
|
77
181
|
case 'tools/call': {
|
|
78
182
|
const name = params?.name;
|
|
@@ -85,12 +189,21 @@ export async function handle(request, tools, { version } = {}) {
|
|
|
85
189
|
const args = params?.arguments ?? {};
|
|
86
190
|
try {
|
|
87
191
|
const payload = await tool.run(args);
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
192
|
+
// Both blocks, always. `structuredContent` is what a client validates against
|
|
193
|
+
// `outputSchema` and what it can index; the text block is what a client that
|
|
194
|
+
// predates structured output reads. Emitting one without the other leaves half
|
|
195
|
+
// the clients guessing, which is the state this release was filed for.
|
|
196
|
+
return reply({
|
|
197
|
+
content: [contentOf(payload)],
|
|
198
|
+
structuredContent: payload,
|
|
199
|
+
isError: false,
|
|
93
200
|
});
|
|
201
|
+
} catch (failure) {
|
|
202
|
+
// An error is a result too, and it carries the same two shapes: the remedy is
|
|
203
|
+
// the part a model needs to act on, and it has to survive in whichever block the
|
|
204
|
+
// client reads.
|
|
205
|
+
const payload = { error: failure.message, remedy: failure.remedy ?? null };
|
|
206
|
+
return reply({ content: [contentOf(payload)], structuredContent: payload, isError: true });
|
|
94
207
|
}
|
|
95
208
|
}
|
|
96
209
|
|
|
@@ -99,6 +212,22 @@ export async function handle(request, tools, { version } = {}) {
|
|
|
99
212
|
}
|
|
100
213
|
}
|
|
101
214
|
|
|
215
|
+
const INSTRUCTIONS =
|
|
216
|
+
`Read-only. Every answer is JSON. \`${CLI} skills brief\` is the one to call first: it returns the pre-assembled set for a task, plus what it deliberately left out and why.`;
|
|
217
|
+
|
|
218
|
+
function serverInfo(packageVersion) {
|
|
219
|
+
return { ...SERVER_INFO, version: packageVersion ?? '0.0.0' };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function capabilities(tools) {
|
|
223
|
+
return {
|
|
224
|
+
tools: { listChanged: false },
|
|
225
|
+
// Declared only when there is something to read. A capability with no method behind
|
|
226
|
+
// it is a promise a client will keep waiting on.
|
|
227
|
+
...(tools.resources ? { resources: { listChanged: false, subscribe: false } } : {}),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
102
231
|
/**
|
|
103
232
|
* Echo the client's protocol version when it is one this server implements.
|
|
104
233
|
*
|
|
@@ -107,16 +236,58 @@ export async function handle(request, tools, { version } = {}) {
|
|
|
107
236
|
* fails loudly at the one place the client checks.
|
|
108
237
|
*/
|
|
109
238
|
function negotiate(requested) {
|
|
110
|
-
|
|
111
|
-
|
|
239
|
+
return typeof requested === 'string' && SUPPORTED_VERSIONS.includes(requested)
|
|
240
|
+
? requested
|
|
241
|
+
: PROTOCOL_VERSION;
|
|
112
242
|
}
|
|
113
243
|
|
|
114
|
-
|
|
244
|
+
/** Revision ordering. These are dates, so lexicographic is chronological - stated here
|
|
245
|
+
* because "sort a version string" is the kind of helper that gets rewritten by someone
|
|
246
|
+
* who thinks it is a number, and this one is right. */
|
|
247
|
+
function atLeast(version, floor) {
|
|
248
|
+
return version >= floor;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** 2026-07-28 requires `ttlMs` and `cacheScope` on list results; earlier ones ignore them. */
|
|
252
|
+
function cacheable(version) {
|
|
253
|
+
return atLeast(version, '2026-07-28');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* What the newest revision adds to every result.
|
|
258
|
+
*
|
|
259
|
+
* `resultType` is required from 2026-07-28 and is absent before it — clients on older
|
|
260
|
+
* revisions treat it as an unknown field, and clients MUST read a missing one as
|
|
261
|
+
* `complete`, so emitting it unconditionally is the compatible half and omitting it on
|
|
262
|
+
* the new revision is not. `_meta.serverInfo` is a SHOULD, and it is how a stateless
|
|
263
|
+
* client learns who it is talking to now that `initialize` is gone.
|
|
264
|
+
*/
|
|
265
|
+
function decorate(value, version, { packageVersion }) {
|
|
266
|
+
if (!atLeast(version, '2026-07-28')) return value;
|
|
115
267
|
return {
|
|
268
|
+
resultType: 'complete',
|
|
269
|
+
...value,
|
|
270
|
+
_meta: { [META.SERVER_INFO]: serverInfo(packageVersion) },
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function describe(tool, version) {
|
|
275
|
+
const entry = {
|
|
116
276
|
name: tool.name,
|
|
117
277
|
description: tool.description,
|
|
118
278
|
inputSchema: tool.inputSchema,
|
|
279
|
+
// Every tool here is a reader: none writes, and none writes twice. Saying so lets a
|
|
280
|
+
// client cache the answer and, where it offers, let the model call without a
|
|
281
|
+
// confirmation step. The declaration is per tool rather than global because the next
|
|
282
|
+
// tool this file gains may not be one.
|
|
283
|
+
annotations: { readOnlyHint: true, idempotentHint: true, ...(tool.annotations ?? {}) },
|
|
119
284
|
};
|
|
285
|
+
// `outputSchema` exists in all three revisions, and a tool that declares its input
|
|
286
|
+
// shape but not its output shape leaves the client guessing what a successful answer
|
|
287
|
+
// looks like - which is the same defect as a returned value nothing can be checked
|
|
288
|
+
// against.
|
|
289
|
+
if (tool.outputSchema) entry.outputSchema = tool.outputSchema;
|
|
290
|
+
return entry;
|
|
120
291
|
}
|
|
121
292
|
|
|
122
293
|
/** Content is one text block. Bounded, and truncation is announced in the text itself -
|
|
@@ -146,15 +317,21 @@ function error(id, code, message) {
|
|
|
146
317
|
* Lines are processed strictly in order. A server that interleaves responses would let a
|
|
147
318
|
* client match the wrong answer to the wrong question, and the tools here are already
|
|
148
319
|
* sequential and cheap - parallelising them would buy nothing and cost correctness.
|
|
320
|
+
*
|
|
321
|
+
* One `state` for the whole stream, holding the revision a client negotiated through
|
|
322
|
+
* `initialize`. That is the residue of a handshake the 2026-07-28 revision removed, kept
|
|
323
|
+
* only because a client on an older revision sends bare requests afterwards and expects
|
|
324
|
+
* the answers it agreed to.
|
|
149
325
|
*/
|
|
150
|
-
export async function serve(tools, { input = process.stdin, output = process.stdout,
|
|
326
|
+
export async function serve(tools, { input = process.stdin, output = process.stdout, packageVersion, onError } = {}) {
|
|
151
327
|
const rl = createInterface({ input, crlfDelay: Infinity });
|
|
328
|
+
const state = { version: null };
|
|
152
329
|
for await (const line of rl) {
|
|
153
330
|
if (line.trim() === '') continue;
|
|
154
331
|
|
|
155
332
|
let response;
|
|
156
333
|
try {
|
|
157
|
-
response = await handle(JSON.parse(line), tools, {
|
|
334
|
+
response = await handle(JSON.parse(line), tools, { packageVersion, state });
|
|
158
335
|
} catch (parseFailure) {
|
|
159
336
|
response = error(null, RPC.PARSE, `invalid JSON: ${parseFailure.message}`);
|
|
160
337
|
}
|
package/src/mcp/tools.mjs
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// cause by accident because the caller cannot see the size until it has paid for it.
|
|
20
20
|
|
|
21
21
|
import { fingerprint } from '../resolve/fingerprint.mjs';
|
|
22
|
-
import { loadRegistry
|
|
22
|
+
import { loadRegistry } from '../graph/load.mjs';
|
|
23
23
|
import { tokenize, rankSkills, closure, selectWithinBudget, scoreSkill, selectionVerdict, stackCompatible } from '../resolve/resolve.mjs';
|
|
24
24
|
import { loadRules, select, checkTargets, provenanceDigest } from '../projection/project.mjs';
|
|
25
25
|
import { loadDecisions, byId, citations } from '../decisions/index.mjs';
|
|
@@ -35,6 +35,283 @@ const GATE_KINDS = new Map(Object.entries(GATES).map(([name, g]) => [name, g.kin
|
|
|
35
35
|
|
|
36
36
|
/** A schema with no required fields still needs the object declared. */
|
|
37
37
|
const noArgs = { type: 'object', properties: {}, required: [] };
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Output schemas, one per tool, describing what a SUCCESSFUL call returns.
|
|
41
|
+
*
|
|
42
|
+
* Every one of these tools is a reader: none writes, and none writes twice. The
|
|
43
|
+
* annotations say so, and the schemas say what comes back — because a client that can
|
|
44
|
+
* validate an answer against a declared shape catches a wrong answer at the boundary,
|
|
45
|
+
* while one handed a bag of JSON has to trust that the tool meant it.
|
|
46
|
+
*
|
|
47
|
+
* Properties are required unless the payload can legitimately omit them: `error` is
|
|
48
|
+
* absent when there is none, `body` is null when there is none, and a schema that
|
|
49
|
+
* required the exception would describe a shape that never occurs. `additionalProperties`
|
|
50
|
+
* is left open, which is what the 2026-07-28 revision permits and what a reader wants:
|
|
51
|
+
* these payloads are data, and a new field from a future release should not make an
|
|
52
|
+
* older client's validation fail.
|
|
53
|
+
*/
|
|
54
|
+
/**
|
|
55
|
+
* The shape of a failure, exported because a client validating an answer needs it and
|
|
56
|
+
* because the test that checks every tool's answer needs it for the failures. Two
|
|
57
|
+
* copies of this schema would drift, and the copy that drifted would be the one nobody
|
|
58
|
+
* validates against - which is exactly how a remedy quietly stops being a string.
|
|
59
|
+
*/
|
|
60
|
+
export const ERROR_SHAPE = {
|
|
61
|
+
type: 'object',
|
|
62
|
+
properties: { error: { type: 'string' }, remedy: { type: ['string', 'null'] } },
|
|
63
|
+
required: ['error'],
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const output = {
|
|
67
|
+
doctor: {
|
|
68
|
+
type: 'object',
|
|
69
|
+
properties: {
|
|
70
|
+
root: { type: 'string' },
|
|
71
|
+
workspace: {
|
|
72
|
+
type: 'object',
|
|
73
|
+
properties: {
|
|
74
|
+
// Null is the honest value for a repository that is not inside a workspace:
|
|
75
|
+
// there is no root to name, and naming the repository as one would be a claim
|
|
76
|
+
// about the reader's directory layout that nothing established.
|
|
77
|
+
root: { type: ['string', 'null'] },
|
|
78
|
+
source: { type: ['string', 'null'] },
|
|
79
|
+
detail: { type: ['string', 'null'] },
|
|
80
|
+
},
|
|
81
|
+
required: ['root', 'source'],
|
|
82
|
+
},
|
|
83
|
+
stack: { type: 'array', items: { type: 'string' } },
|
|
84
|
+
facts: { type: 'object', additionalProperties: { type: ['string', 'null'] } },
|
|
85
|
+
signals: { type: 'array', items: { type: 'string' } },
|
|
86
|
+
// One entry per signal, naming the file that proved it. The payload is a list of
|
|
87
|
+
// paths, not of records - `fingerprint` returns evidence as the paths themselves.
|
|
88
|
+
evidence: { type: 'array', items: { type: 'string' } },
|
|
89
|
+
manifest: { type: ['object', 'null'] },
|
|
90
|
+
error: { type: ['string', 'null'] },
|
|
91
|
+
},
|
|
92
|
+
required: ['root', 'workspace', 'stack', 'facts', 'signals', 'evidence'],
|
|
93
|
+
},
|
|
94
|
+
skills_index: {
|
|
95
|
+
type: 'object',
|
|
96
|
+
properties: {
|
|
97
|
+
skills: { type: 'number' },
|
|
98
|
+
totalBytes: { type: 'number' },
|
|
99
|
+
deferred: { type: 'array', items: { type: 'string' } },
|
|
100
|
+
workspace: { type: 'object' },
|
|
101
|
+
verified: { type: 'boolean' },
|
|
102
|
+
entries: {
|
|
103
|
+
type: 'array',
|
|
104
|
+
items: {
|
|
105
|
+
type: 'object',
|
|
106
|
+
properties: {
|
|
107
|
+
name: { type: 'string' },
|
|
108
|
+
summary: { type: 'string' },
|
|
109
|
+
bytes: { type: 'number' },
|
|
110
|
+
stack: { type: 'array', items: { type: 'string' } },
|
|
111
|
+
deferred: { type: 'boolean' },
|
|
112
|
+
},
|
|
113
|
+
required: ['name', 'bytes'],
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
required: ['skills', 'totalBytes', 'entries'],
|
|
118
|
+
},
|
|
119
|
+
skills_brief: {
|
|
120
|
+
type: 'object',
|
|
121
|
+
properties: {
|
|
122
|
+
repo: {
|
|
123
|
+
type: 'object',
|
|
124
|
+
properties: {
|
|
125
|
+
root: { type: 'string' },
|
|
126
|
+
stack: { type: 'array', items: { type: 'string' } },
|
|
127
|
+
facts: { type: 'array', items: { type: 'string' } },
|
|
128
|
+
},
|
|
129
|
+
required: ['root', 'stack', 'facts'],
|
|
130
|
+
},
|
|
131
|
+
task: { type: ['string', 'null'] },
|
|
132
|
+
budget: { type: 'number' },
|
|
133
|
+
inlineBytes: { type: 'number' },
|
|
134
|
+
selected: {
|
|
135
|
+
type: 'array',
|
|
136
|
+
items: {
|
|
137
|
+
type: 'object',
|
|
138
|
+
properties: {
|
|
139
|
+
name: { type: 'string' },
|
|
140
|
+
bytes: { type: 'number' },
|
|
141
|
+
summary: { type: 'string' },
|
|
142
|
+
why: { type: 'array', items: { type: 'string' } },
|
|
143
|
+
},
|
|
144
|
+
required: ['name', 'bytes', 'why'],
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
deferred: {
|
|
148
|
+
type: 'array',
|
|
149
|
+
items: {
|
|
150
|
+
type: 'object',
|
|
151
|
+
properties: { name: { type: 'string' }, bytes: { type: 'number' }, reason: { type: 'string' } },
|
|
152
|
+
required: ['name', 'reason'],
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
notSelected: {
|
|
156
|
+
type: 'array',
|
|
157
|
+
items: {
|
|
158
|
+
type: 'object',
|
|
159
|
+
properties: { name: { type: 'string' }, reason: { type: 'string' } },
|
|
160
|
+
required: ['name', 'reason'],
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
required: ['repo', 'budget', 'inlineBytes', 'selected', 'deferred', 'notSelected'],
|
|
165
|
+
},
|
|
166
|
+
skills_show: {
|
|
167
|
+
type: 'object',
|
|
168
|
+
properties: {
|
|
169
|
+
name: { type: 'string' },
|
|
170
|
+
summary: { type: 'string' },
|
|
171
|
+
bytes: { type: 'number' },
|
|
172
|
+
capabilities: { type: 'array', items: { type: 'string' } },
|
|
173
|
+
stack: { type: 'array', items: { type: 'string' } },
|
|
174
|
+
deferred: { type: 'boolean' },
|
|
175
|
+
source: { type: ['string', 'null'] },
|
|
176
|
+
readFrom: { type: 'string', enum: ['ok', 'empty', 'unavailable'] },
|
|
177
|
+
requires: { type: 'array', items: { type: 'string' } },
|
|
178
|
+
variants: {
|
|
179
|
+
type: 'array',
|
|
180
|
+
items: {
|
|
181
|
+
type: 'object',
|
|
182
|
+
properties: { id: { type: 'string' }, stack: { type: 'array', items: { type: 'string' } }, bytes: { type: 'number' } },
|
|
183
|
+
required: ['id'],
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
maxBytes: { type: 'number' },
|
|
187
|
+
body: { type: ['string', 'null'] },
|
|
188
|
+
},
|
|
189
|
+
required: ['name', 'bytes', 'readFrom', 'requires', 'variants'],
|
|
190
|
+
},
|
|
191
|
+
skills_find: {
|
|
192
|
+
type: 'object',
|
|
193
|
+
properties: {
|
|
194
|
+
query: { type: 'string' },
|
|
195
|
+
results: {
|
|
196
|
+
type: 'array',
|
|
197
|
+
items: {
|
|
198
|
+
type: 'object',
|
|
199
|
+
properties: {
|
|
200
|
+
name: { type: 'string' },
|
|
201
|
+
score: { type: 'number' },
|
|
202
|
+
summary: { type: 'string' },
|
|
203
|
+
why: { type: 'array', items: { type: 'string' } },
|
|
204
|
+
},
|
|
205
|
+
required: ['name', 'score', 'why'],
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
required: ['query', 'results'],
|
|
210
|
+
},
|
|
211
|
+
skills_why: {
|
|
212
|
+
type: 'object',
|
|
213
|
+
properties: {
|
|
214
|
+
name: { type: 'string' },
|
|
215
|
+
score: { type: 'number' },
|
|
216
|
+
compatible: { type: 'boolean' },
|
|
217
|
+
reasons: { type: 'array', items: { type: 'string' } },
|
|
218
|
+
repo: {
|
|
219
|
+
type: 'object',
|
|
220
|
+
properties: { root: { type: 'string' }, signals: { type: 'array', items: { type: 'string' } } },
|
|
221
|
+
},
|
|
222
|
+
task: { type: ['string', 'null'] },
|
|
223
|
+
pathsSeen: { type: 'array', items: { type: 'string' } },
|
|
224
|
+
},
|
|
225
|
+
required: ['name', 'score', 'compatible', 'reasons'],
|
|
226
|
+
},
|
|
227
|
+
rules_list: {
|
|
228
|
+
type: 'object',
|
|
229
|
+
properties: {
|
|
230
|
+
sha: { type: 'string' },
|
|
231
|
+
notes: { type: 'array', items: { type: 'string' } },
|
|
232
|
+
rules: {
|
|
233
|
+
type: 'array',
|
|
234
|
+
items: {
|
|
235
|
+
type: 'object',
|
|
236
|
+
properties: {
|
|
237
|
+
id: { type: ['string', 'null'] },
|
|
238
|
+
title: { type: ['string', 'null'] },
|
|
239
|
+
// A profile may name one signal or several, and the frontmatter keeps both
|
|
240
|
+
// spellings. Describing only the list would make a conforming rule look
|
|
241
|
+
// broken.
|
|
242
|
+
applies: { anyOf: [{ type: 'array', items: { type: 'string' } }, { type: 'string' }] },
|
|
243
|
+
origin: { type: 'string' },
|
|
244
|
+
source: { type: 'string' },
|
|
245
|
+
bytes: { type: 'number' },
|
|
246
|
+
adrs: { type: 'array', items: { type: 'string' } },
|
|
247
|
+
error: { type: ['string', 'null'] },
|
|
248
|
+
selected: { type: 'boolean' },
|
|
249
|
+
},
|
|
250
|
+
required: ['source', 'bytes', 'selected'],
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
required: ['sha', 'rules'],
|
|
255
|
+
},
|
|
256
|
+
rules_why: {
|
|
257
|
+
type: 'object',
|
|
258
|
+
properties: {
|
|
259
|
+
direction: { type: 'string', enum: ['rule', 'decision'] },
|
|
260
|
+
id: { type: 'string' },
|
|
261
|
+
title: { type: ['string', 'null'] },
|
|
262
|
+
status: { type: 'string' },
|
|
263
|
+
source: { type: 'string' },
|
|
264
|
+
origin: { type: 'string' },
|
|
265
|
+
uncited: { type: 'boolean' },
|
|
266
|
+
readWith: { type: 'string' },
|
|
267
|
+
supersededBy: { type: ['string', 'null'] },
|
|
268
|
+
supersedes: { type: 'array', items: { type: 'string' } },
|
|
269
|
+
// A decision record may be partially superseded; the record carries the list of
|
|
270
|
+
// what it replaced, so this is a list and not a flag.
|
|
271
|
+
partial: { type: ['boolean', 'array'] },
|
|
272
|
+
decisions: { type: 'array', items: { type: 'object' } },
|
|
273
|
+
rules: { type: 'array', items: { type: 'string' } },
|
|
274
|
+
},
|
|
275
|
+
required: ['direction', 'id'],
|
|
276
|
+
},
|
|
277
|
+
check: {
|
|
278
|
+
type: 'object',
|
|
279
|
+
properties: {
|
|
280
|
+
root: { type: 'string' },
|
|
281
|
+
ok: { type: 'boolean' },
|
|
282
|
+
findings: {
|
|
283
|
+
type: 'array',
|
|
284
|
+
items: {
|
|
285
|
+
type: 'object',
|
|
286
|
+
properties: {
|
|
287
|
+
file: { type: 'string' },
|
|
288
|
+
status: { type: 'string' },
|
|
289
|
+
detail: { type: ['string', 'null'] },
|
|
290
|
+
remedy: { type: ['string', 'null'] },
|
|
291
|
+
},
|
|
292
|
+
required: ['file', 'status'],
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
required: ['root', 'ok', 'findings'],
|
|
297
|
+
},
|
|
298
|
+
compare: {
|
|
299
|
+
type: 'object',
|
|
300
|
+
properties: {
|
|
301
|
+
root: { type: 'string' },
|
|
302
|
+
shape: { type: 'string', enum: ['repo', 'workspace'] },
|
|
303
|
+
expectedSha: { type: 'string' },
|
|
304
|
+
summary: { type: 'object' },
|
|
305
|
+
repos: { type: 'array', items: { type: 'object' } },
|
|
306
|
+
worktrees: { type: 'array', items: { type: 'object' } },
|
|
307
|
+
directories: { type: 'array', items: { type: 'string' } },
|
|
308
|
+
},
|
|
309
|
+
required: ['root', 'shape', 'expectedSha', 'summary', 'repos'],
|
|
310
|
+
},
|
|
311
|
+
/** The shape of every tool failure, which is a result and not a protocol error. */
|
|
312
|
+
error: ERROR_SHAPE,
|
|
313
|
+
};
|
|
314
|
+
|
|
38
315
|
const rootArg = {
|
|
39
316
|
type: 'object',
|
|
40
317
|
properties: { root: { type: 'string', description: 'repository root; defaults to the editor cwd' } },
|
|
@@ -46,7 +323,7 @@ function rootOf(args) {
|
|
|
46
323
|
}
|
|
47
324
|
|
|
48
325
|
async function registry(args) {
|
|
49
|
-
const { registry, errors, ok, verified, workspace } = await loadRegistry(
|
|
326
|
+
const { registry, errors, ok, verified, workspace } = await loadRegistry(null, args ?? {});
|
|
50
327
|
if (!ok) {
|
|
51
328
|
throw Object.assign(new Error(errors[0]), {
|
|
52
329
|
remedy: `run: ${CLI} graph lint`,
|
|
@@ -57,6 +334,7 @@ async function registry(args) {
|
|
|
57
334
|
|
|
58
335
|
export const TOOLS = [
|
|
59
336
|
{
|
|
337
|
+
outputSchema: output.doctor,
|
|
60
338
|
name: 'doctor',
|
|
61
339
|
description:
|
|
62
340
|
'Classify a repository: its stack tags, the emitted facts with the file that proved each, and whether a simt-dev manifest is present. Call this first when you do not yet know what a repo is.',
|
|
@@ -79,6 +357,7 @@ export const TOOLS = [
|
|
|
79
357
|
},
|
|
80
358
|
|
|
81
359
|
{
|
|
360
|
+
outputSchema: output.skills_index,
|
|
82
361
|
name: 'skills_index',
|
|
83
362
|
description:
|
|
84
363
|
'The full skill catalogue: every skill with its summary, size, stack tags and whether it is deferred. Cheap enough to always afford, and the fallback when brief returns nothing useful.',
|
|
@@ -99,6 +378,7 @@ export const TOOLS = [
|
|
|
99
378
|
},
|
|
100
379
|
|
|
101
380
|
{
|
|
381
|
+
outputSchema: output.skills_brief,
|
|
102
382
|
name: 'skills_brief',
|
|
103
383
|
description:
|
|
104
384
|
'THE entry point. Resolve the skill set for a task in a repository. Returns what was selected, why each was selected, what was deferred, and what was NOT selected with the reason - the last part matters, because it is what lets you override the resolver instead of guessing at it.',
|
|
@@ -165,6 +445,7 @@ export const TOOLS = [
|
|
|
165
445
|
},
|
|
166
446
|
|
|
167
447
|
{
|
|
448
|
+
outputSchema: output.skills_show,
|
|
168
449
|
name: 'skills_show',
|
|
169
450
|
description: 'One skill body, with the metadata needed to decide whether to trust it: source path, which of the two locations it was read from, and its cited decisions.',
|
|
170
451
|
inputSchema: {
|
|
@@ -206,6 +487,7 @@ export const TOOLS = [
|
|
|
206
487
|
},
|
|
207
488
|
|
|
208
489
|
{
|
|
490
|
+
outputSchema: output.skills_find,
|
|
209
491
|
name: 'skills_find',
|
|
210
492
|
description: 'Raw search across capability, keyword and summary. Use when skills_brief has nothing and you suspect a skill exists.',
|
|
211
493
|
inputSchema: {
|
|
@@ -230,6 +512,7 @@ export const TOOLS = [
|
|
|
230
512
|
},
|
|
231
513
|
|
|
232
514
|
{
|
|
515
|
+
outputSchema: output.skills_why,
|
|
233
516
|
name: 'skills_why',
|
|
234
517
|
description:
|
|
235
518
|
'The signal chain for one skill in one repository: its score, the reasons behind it, whether the stack gate admits it at all, and what it would take to make it selected.',
|
|
@@ -263,6 +546,7 @@ export const TOOLS = [
|
|
|
263
546
|
},
|
|
264
547
|
|
|
265
548
|
{
|
|
549
|
+
outputSchema: output.rules_list,
|
|
266
550
|
name: 'rules_list',
|
|
267
551
|
description:
|
|
268
552
|
'The rule catalogue, and which rules a given repository actually receives. Pass `root` to get the selection rather than just the catalogue.',
|
|
@@ -300,9 +584,10 @@ export const TOOLS = [
|
|
|
300
584
|
},
|
|
301
585
|
|
|
302
586
|
{
|
|
587
|
+
outputSchema: output.rules_why,
|
|
303
588
|
name: 'rules_why',
|
|
304
589
|
description:
|
|
305
|
-
'Why a rule exists. Give a rule id for the decisions that settled it and whether each is still in force, or an ADR id for the rules that decision
|
|
590
|
+
'Why a rule exists. Give a rule id for the decisions that settled it and whether each is still in force, or an ADR id for the rules that decision repeats. A rule citing no decision reports that - it is a finding, not an empty answer.',
|
|
306
591
|
inputSchema: {
|
|
307
592
|
type: 'object',
|
|
308
593
|
properties: { what: { type: 'string', description: 'a rule id or ADR-NNN' } },
|
|
@@ -355,6 +640,7 @@ export const TOOLS = [
|
|
|
355
640
|
},
|
|
356
641
|
|
|
357
642
|
{
|
|
643
|
+
outputSchema: output.check,
|
|
358
644
|
name: 'check',
|
|
359
645
|
description:
|
|
360
646
|
'The gate for one repository: is its generated region current, unmanaged, drifted or malformed, and does any rule file lack an id. Read-only - it never repairs.',
|
|
@@ -371,6 +657,7 @@ export const TOOLS = [
|
|
|
371
657
|
},
|
|
372
658
|
|
|
373
659
|
{
|
|
660
|
+
outputSchema: output.compare,
|
|
374
661
|
name: 'compare',
|
|
375
662
|
description:
|
|
376
663
|
'The organisation-wide matrix: every repository under the root, its adoption, region state, pinned simt-dev SHA, hook routing and declared gates, plus the problems found. This is the command that replaces reconstructing the estate by hand.',
|