simt-dev 0.2.1 → 0.3.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.
Files changed (95) hide show
  1. package/CHANGELOG.md +239 -0
  2. package/bin/simt-dev.mjs +21 -2
  3. package/docs/decisions/ADR-064-the-context-meter-counts-what-the-tool-wrote.md +107 -0
  4. package/docs/decisions/ADR-065-a-pack-is-one-shared-rule-set-pinned-by-digest.md +97 -0
  5. package/docs/decisions/ADR-066-a-monorepo-projects-once-per-package.md +115 -0
  6. package/docs/decisions/ADR-067-which-harnesses-read-agents-md-is-a-column.md +110 -0
  7. package/docs/decisions/ADR-068-the-mcp-revision-list-cites-what-was-read.md +111 -0
  8. package/docs/decisions/ADR-069-the-conformance-lint-checks-the-published-specification.md +90 -0
  9. package/docs/decisions/ADR-070-the-json-payload-carries-its-own-version.md +91 -0
  10. package/docs/decisions/ADR-071-a-rule-that-can-be-narrowed-is-written-narrow.md +106 -0
  11. package/docs/decisions/ADR-072-the-budget-is-a-declaration-beside-the-gates.md +104 -0
  12. package/docs/decisions/ADR-073-a-kernel-is-a-smaller-projection-the-repository-asks-for.md +92 -0
  13. package/docs/decisions/ADR-074-a-rule-and-a-decision-are-readable-by-uri.md +99 -0
  14. package/docs/decisions/ADR-075-nothing-leaves-the-machine.md +56 -0
  15. package/docs/decisions/ADR-076-a-change-to-what-agents-read-is-measured.md +70 -0
  16. package/docs/decisions/ADR-077-the-command-card-leads-the-region-and-reads-committed-evidence.md +91 -0
  17. package/docs/decisions/ADR-078-a-line-the-repository-already-proves-does-not-ship.md +117 -0
  18. package/docs/decisions/ADR-079-the-rules-are-enforced-at-the-tool-call-from-one-policy-table.md +162 -0
  19. package/docs/decisions/ADR-080-the-tools-own-names-are-not-repositories-and-neither-reader-decides-by-shape.md +99 -0
  20. package/docs/decisions/ADR-081-a-tracker-is-read-through-the-repositorys-template.md +36 -0
  21. package/docs/decisions/ADR-082-the-tracker-override-is-a-manifest-key.md +35 -0
  22. package/package.json +2 -1
  23. package/rules/core.md +1 -0
  24. package/rules/stacks/android.md +2 -0
  25. package/rules/stacks/cloudflare-workers.md +2 -0
  26. package/rules/stacks/connectiq.md +1 -0
  27. package/rules/stacks/container.md +2 -0
  28. package/rules/stacks/dart.md +2 -0
  29. package/rules/stacks/deno.md +2 -0
  30. package/rules/stacks/dotnet.md +2 -0
  31. package/rules/stacks/elixir.md +2 -0
  32. package/rules/stacks/go.md +2 -0
  33. package/rules/stacks/jvm.md +2 -0
  34. package/rules/stacks/node.md +2 -0
  35. package/rules/stacks/python.md +2 -0
  36. package/rules/stacks/react-native.md +2 -0
  37. package/rules/stacks/react-web.md +2 -0
  38. package/rules/stacks/ruby.md +2 -0
  39. package/rules/stacks/rust.md +2 -0
  40. package/rules/stacks/swift.md +2 -0
  41. package/rules/stacks/typescript.md +2 -0
  42. package/rules/stacks/uv.md +2 -0
  43. package/src/agenthooks/decide.mjs +123 -0
  44. package/src/agenthooks/policy.mjs +78 -0
  45. package/src/agenthooks/render.mjs +240 -0
  46. package/src/argv.mjs +55 -1
  47. package/src/bench/ablate.mjs +630 -0
  48. package/src/bench/adapters.json +149 -0
  49. package/src/bench/grade.mjs +229 -0
  50. package/src/bench/mine.mjs +811 -0
  51. package/src/bench/probes.mjs +1350 -0
  52. package/src/bench/report.mjs +899 -0
  53. package/src/bench/run.mjs +1002 -0
  54. package/src/commands/agenthook.mjs +282 -0
  55. package/src/commands/bench.mjs +359 -0
  56. package/src/commands/compare.mjs +15 -0
  57. package/src/commands/graph.mjs +45 -21
  58. package/src/commands/harness.mjs +301 -0
  59. package/src/commands/help.mjs +73 -2
  60. package/src/commands/index.mjs +67 -25
  61. package/src/commands/init.mjs +12 -8
  62. package/src/commands/mcp.mjs +20 -5
  63. package/src/commands/plan.mjs +99 -0
  64. package/src/commands/rules.mjs +42 -57
  65. package/src/commands/skills.mjs +67 -92
  66. package/src/commands/skillsinstall.mjs +17 -2
  67. package/src/commands/sync.mjs +62 -6
  68. package/src/commands/tokens.mjs +169 -0
  69. package/src/compare/row.mjs +73 -2
  70. package/src/decisions/index.mjs +23 -5
  71. package/src/graph/spec.mjs +227 -0
  72. package/src/graph/speccheck.mjs +68 -0
  73. package/src/hooks/dispatcher.mjs +35 -3
  74. package/src/kernel/card.mjs +254 -0
  75. package/src/kernel/inferability.mjs +188 -0
  76. package/src/mcp/resources.mjs +157 -0
  77. package/src/mcp/schema.mjs +139 -0
  78. package/src/mcp/server.mjs +228 -20
  79. package/src/mcp/tools.mjs +372 -1
  80. package/src/measure/context.mjs +542 -0
  81. package/src/measure/loaderView.mjs +435 -0
  82. package/src/measure/session.mjs +267 -0
  83. package/src/monorepo.mjs +339 -0
  84. package/src/output/index.mjs +78 -2
  85. package/src/packs.mjs +182 -0
  86. package/src/plan/index.mjs +650 -0
  87. package/src/plan/template.mjs +331 -0
  88. package/src/projection/loaders.mjs +689 -0
  89. package/src/projection/manifest.mjs +114 -3
  90. package/src/projection/project.mjs +140 -9
  91. package/src/projection/rules.mjs +365 -22
  92. package/src/projection/surfaces.mjs +155 -23
  93. package/src/projection/targets.mjs +37 -0
  94. package/src/toolwords.mjs +69 -0
  95. package/templates/plan/tracker.json +36 -0
@@ -21,8 +21,26 @@
21
21
 
22
22
  import { createInterface } from 'node:readline';
23
23
  import { CLI } from '../name.mjs';
24
+ import { RESOURCE_TEMPLATES, listResources, readResource } from './resources.mjs';
24
25
 
25
- export const PROTOCOL_VERSION = '2025-06-18';
26
+ /**
27
+ * Every protocol revision this server speaks, newest first.
28
+ *
29
+ * The list is a claim about revisions that were read, not about ones that exist: each
30
+ * entry names a published revision and the behaviour this file implements for it, and
31
+ * ADR-068 records what changed. A revision not in this list is answered with
32
+ * `UnsupportedProtocolVersionError` rather than an optimistic guess, because the failure
33
+ * mode of guessing is a client that believes a feature is on and watches it misbehave.
34
+ */
35
+ export const SUPPORTED_VERSIONS = ['2026-07-28', '2025-11-25', '2025-06-18'];
36
+
37
+ /**
38
+ * The revision this server answers with when the client has not asked for one.
39
+ *
40
+ * Named the way every caller has always named it, and it is the newest supported
41
+ * revision rather than the only one: a client that asks for an older one gets it.
42
+ */
43
+ export const PROTOCOL_VERSION = SUPPORTED_VERSIONS[0];
26
44
 
27
45
  /** JSON-RPC codes. -32700/-32600/-32601 are the spec's parse/request/method set. */
28
46
  export const RPC = {
@@ -30,16 +48,60 @@ export const RPC = {
30
48
  REQUEST: -32600,
31
49
  METHOD: -32601,
32
50
  INTERNAL: -32603,
51
+ /**
52
+ * `UnsupportedProtocolVersionError`, renumbered to -32022 in 2026-07-28. The spec
53
+ * reserves -32000..-32019 for implementations and -32020..-32099 for itself, so the
54
+ * older -32004 would now be an implementation-defined code in the wrong range.
55
+ */
56
+ UNSUPPORTED_VERSION: -32022,
57
+ /** Resource not found: -32002 in every revision up to 2025-11-25, -32602 after. */
58
+ RESOURCE_NOT_FOUND_LEGACY: -32002,
59
+ RESOURCE_NOT_FOUND: -32602,
60
+ };
61
+
62
+ /** The `_meta` keys 2026-07-28 puts on a request and in a result. */
63
+ export const META = {
64
+ PROTOCOL_VERSION: 'io.modelcontextprotocol/protocolVersion',
65
+ CLIENT_INFO: 'io.modelcontextprotocol/clientInfo',
66
+ CLIENT_CAPABILITIES: 'io.modelcontextprotocol/clientCapabilities',
67
+ SERVER_INFO: 'io.modelcontextprotocol/serverInfo',
33
68
  };
34
69
 
70
+ /** Revisions that removed `ping`, per the 2026-07-28 changelog (SEP-2575). */
71
+ const PING_REMOVED_IN = '2026-07-28';
72
+
35
73
  const SERVER_INFO = { name: 'simt-dev', title: 'SIMT developer and agent tooling' };
36
74
 
75
+ /**
76
+ * One hour, and the reason it is a number rather than an omission.
77
+ *
78
+ * `ttlMs` and `cacheScope` are required on list results from 2026-07-28, and the value
79
+ * is a fact about this server rather than a courtesy: the tool list changes when the
80
+ * release changes and not otherwise, so an hour is long enough to spare a client the
81
+ * round trip and short enough that a client started from a stale one re-reads it within
82
+ * a working day. `private`, because the list names the reader's own repository and no
83
+ * intermediary may hold it.
84
+ */
85
+ const LIST_CACHE = { ttlMs: 3600000, cacheScope: 'private' };
86
+
37
87
  /**
38
88
  * Handle one parsed request. Pure with respect to the process: returns the response
39
89
  * object or null (for a notification), and never writes anywhere. Testable without a
40
90
  * pipe, which is the only way to test a line protocol honestly.
91
+ *
92
+ * `state` is the negotiated revision, and it is the only thing this function mutates.
93
+ * 2026-07-28 removed the handshake and made every request carry its own version, so a
94
+ * session is not part of the protocol any more - but a client on an older revision
95
+ * negotiates once and then sends bare requests, and answering those at the newest
96
+ * revision would hand it fields it did not ask for. So the version is read from the
97
+ * request when there is one and remembered only for the clients that need it.
98
+ *
99
+ * @param {object|null} request a parsed JSON-RPC message
100
+ * @param {object[]} tools
101
+ * @param {{packageVersion?: string, state?: {version: string|null}}} options
102
+ * @returns {Promise<object|null>}
41
103
  */
42
- export async function handle(request, tools, { version } = {}) {
104
+ export async function handle(request, tools, { packageVersion, state } = {}) {
43
105
  if (request === null || typeof request !== 'object' || Array.isArray(request)) {
44
106
  return error(null, RPC.REQUEST, 'request must be a JSON object');
45
107
  }
@@ -54,25 +116,68 @@ export async function handle(request, tools, { version } = {}) {
54
116
  }
55
117
  if (isNotification) return null;
56
118
 
119
+ const asked = params?._meta?.[META.PROTOCOL_VERSION];
120
+ if (typeof asked === 'string' && !SUPPORTED_VERSIONS.includes(asked)) {
121
+ // The one place a client checks whether it got what it asked for, so this is where
122
+ // an unsupported version has to fail. The message names the revisions that do work,
123
+ // because "unsupported" alone leaves the client to guess between the two answers a
124
+ // server can give.
125
+ return error(
126
+ id,
127
+ RPC.UNSUPPORTED_VERSION,
128
+ `protocol version ${asked} is not supported; this server speaks ${SUPPORTED_VERSIONS.join(', ')}`,
129
+ );
130
+ }
131
+ const negotiated =
132
+ typeof asked === 'string' ? asked : method === 'initialize' ? negotiate(params?.protocolVersion) : state?.version;
133
+ // A caller that passes no `state` gets one request with no memory: `serve` threads one
134
+ // through the stream, and a test that calls this directly usually wants exactly that.
135
+ const session = state ?? { version: null };
136
+ if (method === 'initialize') session.version = negotiated;
137
+ const v = negotiated ?? PROTOCOL_VERSION;
138
+
139
+ const reply = (value) => result(id, decorate(value, v, { packageVersion, tools }));
140
+
57
141
  switch (method) {
58
142
  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.`,
143
+ return reply({
144
+ protocolVersion: v,
145
+ capabilities: capabilities(),
146
+ // `serverInfo` stays in the body here even on 2026-07-28: a client that has just
147
+ // called a method the newest revision removed is by definition an older client,
148
+ // and the field is where it looks.
149
+ serverInfo: serverInfo(packageVersion),
150
+ instructions: INSTRUCTIONS,
65
151
  });
66
152
 
67
153
  case 'notifications/initialized':
68
154
  case 'initialized':
69
155
  return null;
70
156
 
157
+ // MUST, from 2026-07-28 on. Cheap, stateless, and the stdio way for a client that
158
+ // supports both shapes to find out which one it is talking to before it commits.
159
+ case 'server/discover':
160
+ return reply({
161
+ supportedVersions: [...SUPPORTED_VERSIONS],
162
+ capabilities: capabilities(),
163
+ instructions: INSTRUCTIONS,
164
+ ...(cacheable(v) ? LIST_CACHE : {}),
165
+ });
166
+
71
167
  case 'ping':
72
- return result(id, {});
168
+ // Removed in 2026-07-28, and kept before it: a client that speaks an older
169
+ // revision and probes liveness gets an answer, and one that speaks the newest
170
+ // gets told the method is gone rather than a success it would read as support.
171
+ if (atLeast(v, PING_REMOVED_IN)) {
172
+ return error(id, RPC.METHOD, `ping was removed in ${PING_REMOVED_IN}; this session negotiated ${v}`);
173
+ }
174
+ return reply({});
73
175
 
74
176
  case 'tools/list':
75
- return result(id, { tools: tools.map(describe) });
177
+ return reply({
178
+ tools: tools.map((t) => describe(t, v)),
179
+ ...(cacheable(v) ? LIST_CACHE : {}),
180
+ });
76
181
 
77
182
  case 'tools/call': {
78
183
  const name = params?.name;
@@ -85,20 +190,75 @@ export async function handle(request, tools, { version } = {}) {
85
190
  const args = params?.arguments ?? {};
86
191
  try {
87
192
  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,
193
+ // Both blocks, always. `structuredContent` is what a client validates against
194
+ // `outputSchema` and what it can index; the text block is what a client that
195
+ // predates structured output reads. Emitting one without the other leaves half
196
+ // the clients guessing, which is the state this release was filed for.
197
+ return reply({
198
+ content: [contentOf(payload)],
199
+ structuredContent: payload,
200
+ isError: false,
93
201
  });
202
+ } catch (failure) {
203
+ // An error is a result too, and it carries the same two shapes: the remedy is
204
+ // the part a model needs to act on, and it has to survive in whichever block the
205
+ // client reads.
206
+ const payload = { error: failure.message, remedy: failure.remedy ?? null };
207
+ return reply({ content: [contentOf(payload)], structuredContent: payload, isError: true });
94
208
  }
95
209
  }
96
210
 
211
+ case 'resources/list':
212
+ return reply(
213
+ cacheable(v) ? { resources: await listResources(), ...LIST_CACHE } : { resources: await listResources() },
214
+ );
215
+ case 'resources/templates/list':
216
+ return reply(
217
+ cacheable(v)
218
+ ? { resourceTemplates: RESOURCE_TEMPLATES, ...LIST_CACHE }
219
+ : { resourceTemplates: RESOURCE_TEMPLATES },
220
+ );
221
+ case 'resources/read': {
222
+ // The code for a resource that is not there CHANGED, and a server that answers
223
+ // with one code to both revisions is a server that is wrong to whichever half of
224
+ // the client's world it is not. The newest revision forbids an empty `contents`
225
+ // array as the answer, so "absent" can only be an error.
226
+ const code = atLeast(v, '2026-07-28') ? RPC.RESOURCE_NOT_FOUND : RPC.RESOURCE_NOT_FOUND_LEGACY;
227
+ const uri = request.params?.uri;
228
+ const found = await readResource(uri);
229
+ if (found === null) return error(id, code, `no such resource: ${String(uri)}`);
230
+ return reply(
231
+ cacheable(v) ? { contents: [found], ...LIST_CACHE } : { contents: [found] },
232
+ );
233
+ }
97
234
  default:
98
235
  return isNotification ? null : error(id, RPC.METHOD, `unknown method "${method}"`);
99
236
  }
100
237
  }
101
238
 
239
+ const INSTRUCTIONS =
240
+ `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.`;
241
+
242
+ function serverInfo(packageVersion) {
243
+ return { ...SERVER_INFO, version: packageVersion ?? '0.0.0' };
244
+ }
245
+
246
+ function capabilities() {
247
+ return {
248
+ tools: { listChanged: false },
249
+ // Unconditional, and that is the point: every rule and every decision record ships
250
+ // with the package, so there is always something to read. The empty object is the
251
+ // documented way to say "resources, but no subscriptions and no change
252
+ // notifications" — both of which this server has no way to honour, and a client
253
+ // told otherwise would subscribe to a stream that never arrives.
254
+ //
255
+ // It used to be conditional on a property hung on the tools array, which nothing
256
+ // ever set: a capability gated on a flag nobody flips is the same promise a client
257
+ // waits on forever, only quieter.
258
+ resources: {},
259
+ };
260
+ }
261
+
102
262
  /**
103
263
  * Echo the client's protocol version when it is one this server implements.
104
264
  *
@@ -107,16 +267,58 @@ export async function handle(request, tools, { version } = {}) {
107
267
  * fails loudly at the one place the client checks.
108
268
  */
109
269
  function negotiate(requested) {
110
- const SUPPORTED = new Set([PROTOCOL_VERSION]);
111
- return typeof requested === 'string' && SUPPORTED.has(requested) ? requested : PROTOCOL_VERSION;
270
+ return typeof requested === 'string' && SUPPORTED_VERSIONS.includes(requested)
271
+ ? requested
272
+ : PROTOCOL_VERSION;
112
273
  }
113
274
 
114
- function describe(tool) {
275
+ /** Revision ordering. These are dates, so lexicographic is chronological - stated here
276
+ * because "sort a version string" is the kind of helper that gets rewritten by someone
277
+ * who thinks it is a number, and this one is right. */
278
+ function atLeast(version, floor) {
279
+ return version >= floor;
280
+ }
281
+
282
+ /** 2026-07-28 requires `ttlMs` and `cacheScope` on list results; earlier ones ignore them. */
283
+ function cacheable(version) {
284
+ return atLeast(version, '2026-07-28');
285
+ }
286
+
287
+ /**
288
+ * What the newest revision adds to every result.
289
+ *
290
+ * `resultType` is required from 2026-07-28 and is absent before it — clients on older
291
+ * revisions treat it as an unknown field, and clients MUST read a missing one as
292
+ * `complete`, so emitting it unconditionally is the compatible half and omitting it on
293
+ * the new revision is not. `_meta.serverInfo` is a SHOULD, and it is how a stateless
294
+ * client learns who it is talking to now that `initialize` is gone.
295
+ */
296
+ function decorate(value, version, { packageVersion }) {
297
+ if (!atLeast(version, '2026-07-28')) return value;
115
298
  return {
299
+ resultType: 'complete',
300
+ ...value,
301
+ _meta: { [META.SERVER_INFO]: serverInfo(packageVersion) },
302
+ };
303
+ }
304
+
305
+ function describe(tool, version) {
306
+ const entry = {
116
307
  name: tool.name,
117
308
  description: tool.description,
118
309
  inputSchema: tool.inputSchema,
310
+ // Every tool here is a reader: none writes, and none writes twice. Saying so lets a
311
+ // client cache the answer and, where it offers, let the model call without a
312
+ // confirmation step. The declaration is per tool rather than global because the next
313
+ // tool this file gains may not be one.
314
+ annotations: { readOnlyHint: true, idempotentHint: true, ...(tool.annotations ?? {}) },
119
315
  };
316
+ // `outputSchema` exists in all three revisions, and a tool that declares its input
317
+ // shape but not its output shape leaves the client guessing what a successful answer
318
+ // looks like - which is the same defect as a returned value nothing can be checked
319
+ // against.
320
+ if (tool.outputSchema) entry.outputSchema = tool.outputSchema;
321
+ return entry;
120
322
  }
121
323
 
122
324
  /** Content is one text block. Bounded, and truncation is announced in the text itself -
@@ -146,15 +348,21 @@ function error(id, code, message) {
146
348
  * Lines are processed strictly in order. A server that interleaves responses would let a
147
349
  * client match the wrong answer to the wrong question, and the tools here are already
148
350
  * sequential and cheap - parallelising them would buy nothing and cost correctness.
351
+ *
352
+ * One `state` for the whole stream, holding the revision a client negotiated through
353
+ * `initialize`. That is the residue of a handshake the 2026-07-28 revision removed, kept
354
+ * only because a client on an older revision sends bare requests afterwards and expects
355
+ * the answers it agreed to.
149
356
  */
150
- export async function serve(tools, { input = process.stdin, output = process.stdout, version, onError } = {}) {
357
+ export async function serve(tools, { input = process.stdin, output = process.stdout, packageVersion, onError } = {}) {
151
358
  const rl = createInterface({ input, crlfDelay: Infinity });
359
+ const state = { version: null };
152
360
  for await (const line of rl) {
153
361
  if (line.trim() === '') continue;
154
362
 
155
363
  let response;
156
364
  try {
157
- response = await handle(JSON.parse(line), tools, { version });
365
+ response = await handle(JSON.parse(line), tools, { packageVersion, state });
158
366
  } catch (parseFailure) {
159
367
  response = error(null, RPC.PARSE, `invalid JSON: ${parseFailure.message}`);
160
368
  }