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.
Files changed (90) hide show
  1. package/CHANGELOG.md +375 -7
  2. package/README.md +4 -4
  3. package/docs/decisions/ADR-019-a-declared-command-is-the-runner.md +27 -0
  4. package/docs/decisions/ADR-059-the-version-a-repository-runs-is-the-one-it-pinned.md +75 -0
  5. package/docs/decisions/ADR-060-a-repository-with-nothing-to-run-is-adopted-and-says-so.md +131 -0
  6. package/docs/decisions/ADR-061-the-skill-registry-is-workspace-data.md +113 -0
  7. package/docs/decisions/ADR-062-a-fresh-adoption-seeds-the-gates-it-can-run.md +94 -0
  8. package/docs/decisions/ADR-063-the-adoption-escape-hatch-is-opt-in.md +69 -0
  9. package/docs/decisions/ADR-064-the-context-meter-counts-what-the-tool-wrote.md +107 -0
  10. package/docs/decisions/ADR-065-a-pack-is-one-shared-rule-set-pinned-by-digest.md +97 -0
  11. package/docs/decisions/ADR-066-a-monorepo-projects-once-per-package.md +115 -0
  12. package/docs/decisions/ADR-067-which-harnesses-read-agents-md-is-a-column.md +110 -0
  13. package/docs/decisions/ADR-068-the-mcp-revision-list-cites-what-was-read.md +111 -0
  14. package/docs/decisions/ADR-069-the-conformance-lint-checks-the-published-specification.md +90 -0
  15. package/docs/decisions/ADR-070-the-json-payload-carries-its-own-version.md +91 -0
  16. package/docs/decisions/ADR-071-a-rule-that-can-be-narrowed-is-written-narrow.md +106 -0
  17. package/docs/decisions/ADR-072-the-budget-is-a-declaration-beside-the-gates.md +104 -0
  18. package/docs/decisions/ADR-073-a-kernel-is-a-smaller-projection-the-repository-asks-for.md +92 -0
  19. package/docs/decisions/README.md +17 -0
  20. package/package.json +1 -2
  21. package/rules/core.md +1 -0
  22. package/rules/stacks/android.md +2 -0
  23. package/rules/stacks/cloudflare-workers.md +2 -0
  24. package/rules/stacks/connectiq.md +1 -0
  25. package/rules/stacks/container.md +2 -0
  26. package/rules/stacks/dart.md +40 -0
  27. package/rules/stacks/deno.md +38 -0
  28. package/rules/stacks/dotnet.md +37 -0
  29. package/rules/stacks/elixir.md +41 -0
  30. package/rules/stacks/go.md +36 -0
  31. package/rules/stacks/jvm.md +2 -0
  32. package/rules/stacks/node.md +2 -0
  33. package/rules/stacks/python.md +2 -0
  34. package/rules/stacks/react-native.md +2 -0
  35. package/rules/stacks/react-web.md +2 -0
  36. package/rules/stacks/ruby.md +38 -0
  37. package/rules/stacks/rust.md +36 -0
  38. package/rules/stacks/swift.md +39 -0
  39. package/rules/stacks/typescript.md +2 -0
  40. package/rules/stacks/uv.md +2 -0
  41. package/src/argv.mjs +54 -6
  42. package/src/ci/dependabot.mjs +1 -1
  43. package/src/ci/workflow.mjs +33 -11
  44. package/src/commands/compare.mjs +15 -0
  45. package/src/commands/graph.mjs +58 -25
  46. package/src/commands/harness.mjs +150 -0
  47. package/src/commands/help.mjs +37 -5
  48. package/src/commands/hooks.mjs +173 -57
  49. package/src/commands/index.mjs +95 -69
  50. package/src/commands/init.mjs +100 -20
  51. package/src/commands/mcp.mjs +20 -5
  52. package/src/commands/rules.mjs +96 -60
  53. package/src/commands/skills.mjs +189 -86
  54. package/src/commands/skillsinstall.mjs +20 -5
  55. package/src/commands/sync.mjs +213 -15
  56. package/src/commands/tokens.mjs +138 -0
  57. package/src/compare/hooksstate.mjs +96 -9
  58. package/src/compare/row.mjs +107 -7
  59. package/src/decisions/index.mjs +21 -6
  60. package/src/graph/load.mjs +125 -14
  61. package/src/graph/spec.mjs +227 -0
  62. package/src/graph/speccheck.mjs +68 -0
  63. package/src/hooks/capability.mjs +48 -4
  64. package/src/hooks/dispatcher.mjs +84 -7
  65. package/src/hooks/gates.mjs +0 -2
  66. package/src/hooks/resolve.mjs +24 -25
  67. package/src/hooks/shim.mjs +29 -5
  68. package/src/identity.mjs +0 -11
  69. package/src/mcp/schema.mjs +139 -0
  70. package/src/mcp/server.mjs +197 -20
  71. package/src/mcp/tools.mjs +290 -3
  72. package/src/measure/context.mjs +371 -0
  73. package/src/monorepo.mjs +339 -0
  74. package/src/output/index.mjs +86 -9
  75. package/src/package-version.mjs +74 -0
  76. package/src/packs.mjs +182 -0
  77. package/src/projection/conflicts.mjs +56 -5
  78. package/src/projection/manifest.mjs +335 -25
  79. package/src/projection/mdc.mjs +4 -13
  80. package/src/projection/project.mjs +139 -8
  81. package/src/projection/rules.mjs +308 -27
  82. package/src/projection/surfaces.mjs +133 -15
  83. package/src/projection/targets.mjs +90 -7
  84. package/src/resolve/eval.mjs +64 -0
  85. package/src/resolve/fingerprint.mjs +1 -0
  86. package/src/resolve/near.mjs +26 -7
  87. package/src/resolve/resolve.mjs +1 -1
  88. package/src/resolve/source.mjs +1 -1
  89. package/src/workspace.mjs +0 -9
  90. package/graph/skills.graph.json +0 -1622
@@ -22,7 +22,24 @@
22
22
  import { createInterface } from 'node:readline';
23
23
  import { CLI } from '../name.mjs';
24
24
 
25
- export const PROTOCOL_VERSION = '2025-06-18';
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, { version } = {}) {
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 result(id, {
60
- protocolVersion: negotiate(params?.protocolVersion),
61
- capabilities: { tools: { listChanged: false } },
62
- serverInfo: { ...SERVER_INFO, version: version ?? '0.0.0' },
63
- instructions:
64
- `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.`,
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
- return result(id, {});
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 result(id, { tools: tools.map(describe) });
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
- return result(id, { content: [contentOf(payload)], isError: false });
89
- } catch (failure) {
90
- return result(id, {
91
- content: [contentOf({ error: failure.message, remedy: failure.remedy ?? null })],
92
- isError: true,
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
- const SUPPORTED = new Set([PROTOCOL_VERSION]);
111
- return typeof requested === 'string' && SUPPORTED.has(requested) ? requested : PROTOCOL_VERSION;
239
+ return typeof requested === 'string' && SUPPORTED_VERSIONS.includes(requested)
240
+ ? requested
241
+ : PROTOCOL_VERSION;
112
242
  }
113
243
 
114
- function describe(tool) {
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, version, onError } = {}) {
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, { version });
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, REGISTRY_PATH } from '../graph/load.mjs';
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(REGISTRY_PATH, args ?? {});
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 restsates. A rule citing no decision reports that - it is a finding, not an empty answer.',
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.',