simt-dev 0.2.1 → 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 (61) hide show
  1. package/CHANGELOG.md +202 -0
  2. package/docs/decisions/ADR-064-the-context-meter-counts-what-the-tool-wrote.md +107 -0
  3. package/docs/decisions/ADR-065-a-pack-is-one-shared-rule-set-pinned-by-digest.md +97 -0
  4. package/docs/decisions/ADR-066-a-monorepo-projects-once-per-package.md +115 -0
  5. package/docs/decisions/ADR-067-which-harnesses-read-agents-md-is-a-column.md +110 -0
  6. package/docs/decisions/ADR-068-the-mcp-revision-list-cites-what-was-read.md +111 -0
  7. package/docs/decisions/ADR-069-the-conformance-lint-checks-the-published-specification.md +90 -0
  8. package/docs/decisions/ADR-070-the-json-payload-carries-its-own-version.md +91 -0
  9. package/docs/decisions/ADR-071-a-rule-that-can-be-narrowed-is-written-narrow.md +106 -0
  10. package/docs/decisions/ADR-072-the-budget-is-a-declaration-beside-the-gates.md +104 -0
  11. package/docs/decisions/ADR-073-a-kernel-is-a-smaller-projection-the-repository-asks-for.md +92 -0
  12. package/package.json +1 -1
  13. package/rules/core.md +1 -0
  14. package/rules/stacks/android.md +2 -0
  15. package/rules/stacks/cloudflare-workers.md +2 -0
  16. package/rules/stacks/connectiq.md +1 -0
  17. package/rules/stacks/container.md +2 -0
  18. package/rules/stacks/dart.md +2 -0
  19. package/rules/stacks/deno.md +2 -0
  20. package/rules/stacks/dotnet.md +2 -0
  21. package/rules/stacks/elixir.md +2 -0
  22. package/rules/stacks/go.md +2 -0
  23. package/rules/stacks/jvm.md +2 -0
  24. package/rules/stacks/node.md +2 -0
  25. package/rules/stacks/python.md +2 -0
  26. package/rules/stacks/react-native.md +2 -0
  27. package/rules/stacks/react-web.md +2 -0
  28. package/rules/stacks/ruby.md +2 -0
  29. package/rules/stacks/rust.md +2 -0
  30. package/rules/stacks/swift.md +2 -0
  31. package/rules/stacks/typescript.md +2 -0
  32. package/rules/stacks/uv.md +2 -0
  33. package/src/argv.mjs +19 -0
  34. package/src/commands/compare.mjs +15 -0
  35. package/src/commands/graph.mjs +45 -21
  36. package/src/commands/harness.mjs +150 -0
  37. package/src/commands/help.mjs +15 -1
  38. package/src/commands/index.mjs +43 -25
  39. package/src/commands/init.mjs +12 -8
  40. package/src/commands/mcp.mjs +20 -5
  41. package/src/commands/rules.mjs +42 -57
  42. package/src/commands/skills.mjs +67 -92
  43. package/src/commands/skillsinstall.mjs +17 -2
  44. package/src/commands/sync.mjs +62 -6
  45. package/src/commands/tokens.mjs +138 -0
  46. package/src/compare/row.mjs +73 -2
  47. package/src/graph/spec.mjs +227 -0
  48. package/src/graph/speccheck.mjs +68 -0
  49. package/src/hooks/dispatcher.mjs +35 -3
  50. package/src/mcp/schema.mjs +139 -0
  51. package/src/mcp/server.mjs +197 -20
  52. package/src/mcp/tools.mjs +288 -1
  53. package/src/measure/context.mjs +371 -0
  54. package/src/monorepo.mjs +339 -0
  55. package/src/output/index.mjs +78 -2
  56. package/src/packs.mjs +182 -0
  57. package/src/projection/manifest.mjs +88 -0
  58. package/src/projection/project.mjs +140 -9
  59. package/src/projection/rules.mjs +288 -20
  60. package/src/projection/surfaces.mjs +133 -15
  61. package/src/projection/targets.mjs +37 -0
package/src/mcp/tools.mjs CHANGED
@@ -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' } },
@@ -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.',