simt-dev 0.7.1 → 0.8.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 (186) hide show
  1. package/CHANGELOG.md +36 -1
  2. package/README.md +169 -80
  3. package/docs/decisions/ADR-014-workspace-is-configuration.md +1 -1
  4. package/docs/decisions/ADR-025-a-baseline-is-a-region-in-a-file-the-repo-owns.md +1 -1
  5. package/docs/decisions/ADR-026-a-config-without-a-region-is-a-file-the-repo-owns.md +1 -1
  6. package/docs/decisions/ADR-027-the-one-gitattributes-line-that-is-neither-dangerous-nor-empty.md +1 -1
  7. package/docs/decisions/ADR-028-a-base-file-is-a-floor-so-the-disagreement-it-cannot-repair-becomes-a-check.md +1 -1
  8. package/docs/decisions/ADR-029-the-pins-a-repository-declares-are-its-own.md +1 -1
  9. package/docs/decisions/ADR-030-what-a-plan-does-not-ship-is-a-decision-with-a-record.md +1 -1
  10. package/docs/decisions/ADR-048-a-baseline-carries-only-keys-a-reader-honours.md +1 -1
  11. package/docs/decisions/ADR-082-the-tracker-override-is-a-manifest-key.md +1 -1
  12. package/docs/decisions/ADR-126-the-mcp-surface-stays-a-reader-so-there-is-no-write-path-to-build.md +68 -0
  13. package/docs/decisions/ADR-127-the-mcp-tool-list-is-written-by-hand-and-roots-is-not-asked-for.md +65 -0
  14. package/docs/decisions/ADR-128-a-research-plan-is-a-plan-and-the-tracker-reads-it.md +42 -0
  15. package/ecosystems/bun.json +6 -0
  16. package/ecosystems/cargo.json +25 -0
  17. package/ecosystems/composer.json +6 -0
  18. package/ecosystems/dart.json +24 -0
  19. package/ecosystems/deno.json +21 -0
  20. package/ecosystems/devcontainers.json +6 -0
  21. package/ecosystems/docker-compose.json +6 -0
  22. package/ecosystems/docker.json +6 -0
  23. package/ecosystems/dotnet.json +25 -0
  24. package/ecosystems/elixir.json +23 -0
  25. package/ecosystems/gitsubmodule.json +6 -0
  26. package/ecosystems/go.json +25 -0
  27. package/ecosystems/gradle.json +13 -0
  28. package/ecosystems/maven.json +23 -0
  29. package/ecosystems/node.json +25 -0
  30. package/ecosystems/python.json +20 -0
  31. package/ecosystems/ruby.json +23 -0
  32. package/ecosystems/swift.json +25 -0
  33. package/ecosystems/terraform.json +6 -0
  34. package/ecosystems/uv.json +6 -0
  35. package/harnesses/antigravity.json +162 -0
  36. package/harnesses/claude.json +206 -0
  37. package/harnesses/codex.json +136 -0
  38. package/harnesses/copilot.json +88 -0
  39. package/harnesses/cursor.json +140 -0
  40. package/harnesses/deepseek.json +99 -0
  41. package/harnesses/gemini.json +122 -0
  42. package/harnesses/opencode.json +173 -0
  43. package/package.json +3 -1
  44. package/rules/mdc/intake-questions.mdc +8 -0
  45. package/src/advice/advice.mjs +3 -2
  46. package/src/agenthooks/render.mjs +86 -180
  47. package/src/agents/render.mjs +15 -18
  48. package/src/api.mjs +7 -5
  49. package/src/argv.mjs +19 -0
  50. package/src/bench/ablate.mjs +3 -4
  51. package/src/bench/adapters.json +155 -113
  52. package/src/bench/adapters.mjs +466 -0
  53. package/src/bench/arms.mjs +333 -0
  54. package/src/bench/grade.mjs +12 -26
  55. package/src/bench/graders.mjs +42 -0
  56. package/src/bench/mine.mjs +17 -535
  57. package/src/bench/probe-ledger.mjs +149 -0
  58. package/src/bench/probes.catalog.mjs +918 -0
  59. package/src/bench/probes.mjs +15 -1037
  60. package/src/bench/render.mjs +111 -0
  61. package/src/bench/report.mjs +116 -971
  62. package/src/bench/run.mjs +164 -881
  63. package/src/bench/variants.mjs +4 -5
  64. package/src/bench/workspace.mjs +160 -0
  65. package/src/brief/assemble.mjs +11 -13
  66. package/src/ci/dependabot.mjs +25 -55
  67. package/src/commands/agent.mjs +1 -9
  68. package/src/commands/agenthook.mjs +66 -35
  69. package/src/commands/bench.mjs +2 -2
  70. package/src/commands/ci.mjs +11 -13
  71. package/src/commands/docs.mjs +88 -0
  72. package/src/commands/gate.mjs +10 -13
  73. package/src/commands/harness.mjs +3 -3
  74. package/src/commands/help.mjs +44 -3
  75. package/src/commands/hooks.mjs +20 -20
  76. package/src/commands/ignore.mjs +11 -7
  77. package/src/commands/impact.mjs +4 -8
  78. package/src/commands/index.mjs +49 -9
  79. package/src/commands/init-scan.mjs +90 -0
  80. package/src/commands/init.mjs +122 -104
  81. package/src/commands/lease.mjs +67 -18
  82. package/src/commands/map.mjs +10 -5
  83. package/src/commands/mcp.mjs +2 -17
  84. package/src/commands/repoindex.mjs +8 -8
  85. package/src/commands/rules.mjs +8 -4
  86. package/src/commands/shared.mjs +12 -16
  87. package/src/commands/skills.mjs +5 -6
  88. package/src/commands/skillscompile.mjs +3 -3
  89. package/src/commands/skillsinstall.mjs +5 -4
  90. package/src/commands/slice.mjs +199 -64
  91. package/src/commands/spec.mjs +78 -62
  92. package/src/commands/status.mjs +24 -12
  93. package/src/commands/sync.mjs +14 -14
  94. package/src/commands/tests.mjs +12 -12
  95. package/src/commands/tokens.mjs +19 -24
  96. package/src/commands/upgrade.mjs +8 -4
  97. package/src/commands/why.mjs +6 -13
  98. package/src/compare/hooksstate.mjs +2 -6
  99. package/src/compare/learn.mjs +3 -1
  100. package/src/decisions/index.mjs +3 -2
  101. package/src/docs/commands.mjs +65 -0
  102. package/src/ecosystem/records.mjs +219 -0
  103. package/src/explain.mjs +31 -0
  104. package/src/gates/index.mjs +385 -0
  105. package/src/graph/compile.mjs +8 -4
  106. package/src/graph/load.mjs +2 -1
  107. package/src/graph/spec.mjs +10 -8
  108. package/src/harness/records.mjs +143 -0
  109. package/src/hooks/capability.mjs +5 -3
  110. package/src/hooks/dispatcher.mjs +26 -32
  111. package/src/hooks/gates.mjs +12 -2
  112. package/src/hooks/resolve.mjs +2 -7
  113. package/src/hooks/where.mjs +2 -7
  114. package/src/identity.mjs +2 -11
  115. package/src/index/coverage.mjs +58 -0
  116. package/src/index/extract.mjs +6 -16
  117. package/src/index/gate.mjs +1 -1
  118. package/src/index/govern.mjs +26 -9
  119. package/src/index/impact.mjs +4 -8
  120. package/src/index/map.mjs +14 -15
  121. package/src/index/open.mjs +173 -0
  122. package/src/index/persist.mjs +224 -57
  123. package/src/index/store.mjs +82 -39
  124. package/src/index/tests.mjs +2 -6
  125. package/src/journal.mjs +16 -11
  126. package/src/kernel/card.mjs +37 -63
  127. package/src/kernel/checks.mjs +4 -10
  128. package/src/lease.mjs +102 -7
  129. package/src/ledger/index.mjs +124 -17
  130. package/src/mcp/paginate.mjs +33 -0
  131. package/src/mcp/schemas.mjs +512 -0
  132. package/src/mcp/server.mjs +49 -9
  133. package/src/mcp/tools-facts.mjs +326 -0
  134. package/src/mcp/tools-read.mjs +221 -0
  135. package/src/mcp/tools.mjs +4 -1005
  136. package/src/{bench → measure}/corpus.mjs +1 -1
  137. package/src/measure/metrics.mjs +236 -0
  138. package/src/measure/records.mjs +211 -0
  139. package/src/measure/stats.mjs +538 -0
  140. package/src/measure/task-schema.mjs +125 -0
  141. package/src/measure/tasks.mjs +69 -0
  142. package/src/memory/decide.mjs +6 -15
  143. package/src/memory/lesson-text.mjs +61 -0
  144. package/src/memory/lessons.mjs +29 -82
  145. package/src/name.mjs +7 -0
  146. package/src/output/index.mjs +122 -31
  147. package/src/packs/manage.mjs +15 -9
  148. package/src/plan/index.mjs +1 -1
  149. package/src/plugins.mjs +22 -16
  150. package/src/prbody.mjs +7 -11
  151. package/src/projection/loaders.mjs +7 -692
  152. package/src/projection/manifest.mjs +10 -435
  153. package/src/projection/mdc.mjs +7 -8
  154. package/src/projection/project.mjs +126 -221
  155. package/src/projection/region.mjs +561 -0
  156. package/src/projection/rules.mjs +16 -547
  157. package/src/projection/surfaces.mjs +43 -210
  158. package/src/projection/targets.mjs +5 -5
  159. package/src/repo-root.mjs +13 -1
  160. package/src/resolve/eval.mjs +2 -1
  161. package/src/resolve/fingerprint.mjs +0 -7
  162. package/src/resolve/resolve.mjs +1 -41
  163. package/src/review/critic.mjs +8 -12
  164. package/src/run.mjs +98 -0
  165. package/src/secrets.mjs +14 -22
  166. package/src/state/events.mjs +129 -0
  167. package/src/status-snapshot.mjs +233 -0
  168. package/src/status.mjs +2 -6
  169. package/src/sys/git.mjs +82 -0
  170. package/src/{bench/process.mjs → sys/proc.mjs} +28 -5
  171. package/src/testing/parsers.mjs +265 -0
  172. package/src/testing/patches.mjs +23 -0
  173. package/src/testing/paths.mjs +49 -0
  174. package/src/testing/runners.mjs +186 -0
  175. package/src/testing/vault.mjs +68 -0
  176. package/src/text/frontmatter.mjs +49 -0
  177. package/src/text/tokens.mjs +52 -0
  178. package/src/toolwords.mjs +1 -1
  179. package/src/util/fs.mjs +20 -0
  180. package/src/util/sort.mjs +12 -0
  181. package/src/workspace.mjs +3 -1
  182. package/src/write/plan.mjs +82 -0
  183. package/templates/plan/tracker.json +1 -0
  184. package/docs/decisions/ADR-033-three-plan-rows-closed-by-decision-not-by-authoring.md +0 -82
  185. package/docs/decisions/ADR-102-the-two-queued-review-rows-are-closed-by-ruling.md +0 -49
  186. package/src/bench/tasks.mjs +0 -36
@@ -0,0 +1,512 @@
1
+ // The MCP schemas and the two helpers every tool handler shares.
2
+ //
3
+ // Split out of tools.mjs, which crossed the repository ceiling carrying the tool list
4
+ // and the shapes it answers with. The shapes are data a client validates against; the
5
+ // handlers are the tools. A tool module imports both and defines neither.
6
+
7
+ import { loadRegistry } from '../graph/load.mjs';
8
+ import { provenanceDigest } from '../projection/project.mjs';
9
+ import { discover } from '../compare/discover.mjs';
10
+ import { buildRows, resolveConcurrency, summarise } from '../compare/row.mjs';
11
+ import { GATES } from '../hooks/gates.mjs';
12
+ import { checkRepo } from '../commands/sync.mjs';
13
+ import { diagnose, serialisedFindings } from '../commands/doctor.mjs';
14
+ import { rulesCatalogue, rulesWhyFacts } from '../commands/rules.mjs';
15
+ import { skillsBriefFacts, skillsFindFacts, skillsShowFacts, skillsWhyFacts } from '../commands/skills.mjs';
16
+ import { assembleBrief } from '../brief/assemble.mjs';
17
+ import { DEFAULT_BRIEF_BYTES } from '../brief/packet.mjs';
18
+ import { federationAccess, readFederation } from '../federation/config.mjs';
19
+ import { consult } from '../federation/query.mjs';
20
+ import { planNext, planDone } from '../plan/index.mjs';
21
+ import { buildPrBody } from '../prbody.mjs';
22
+ import { configPaths, readUserConfig, resolveWorkspace } from '../workspace.mjs';
23
+ import { measureSkill } from '../graph/load.mjs';
24
+ import { CLI } from '../name.mjs';
25
+
26
+ export const GATE_KINDS = new Map(Object.entries(GATES).map(([name, g]) => [name, g.kind]));
27
+
28
+ /** A schema with no required fields still needs the object declared. */
29
+ export const noArgs = { type: 'object', properties: {}, required: [] };
30
+
31
+ /**
32
+ * Output schemas, one per tool, describing what a SUCCESSFUL call returns.
33
+ *
34
+ * Every one of these tools is a reader: none writes, and none writes twice. The
35
+ * annotations say so, and the schemas say what comes back — because a client that can
36
+ * validate an answer against a declared shape catches a wrong answer at the boundary,
37
+ * while one handed a bag of JSON has to trust that the tool meant it.
38
+ *
39
+ * Properties are required unless the payload can legitimately omit them: `error` is
40
+ * absent when there is none, `body` is null when there is none, and a schema that
41
+ * required the exception would describe a shape that never occurs. `additionalProperties`
42
+ * is left open, which is what the 2026-07-28 revision permits and what a reader wants:
43
+ * these payloads are data, and a new field from a future release should not make an
44
+ * older client's validation fail.
45
+ */
46
+ /**
47
+ * The shape of a failure, exported because a client validating an answer needs it and
48
+ * because the test that checks every tool's answer needs it for the failures. Two
49
+ * copies of this schema would drift, and the copy that drifted would be the one nobody
50
+ * validates against - which is exactly how a remedy quietly stops being a string.
51
+ */
52
+ export const ERROR_SHAPE = {
53
+ type: 'object',
54
+ properties: { error: { type: 'string' }, remedy: { type: ['string', 'null'] } },
55
+ required: ['error'],
56
+ };
57
+
58
+ export const output = {
59
+ doctor: {
60
+ // The `doctor --json` document minus its envelope: the same findings, the
61
+ // same workspace answer, the same fingerprint. (Q15)
62
+ type: 'object',
63
+ properties: {
64
+ root: { type: 'string' },
65
+ stack: { type: 'array', items: { type: 'string' } },
66
+ facts: { type: 'array', items: { type: 'string' } },
67
+ factEvidence: { type: 'object', additionalProperties: { type: ['string', 'null'] } },
68
+ signals: { type: 'array', items: { type: 'string' } },
69
+ // One entry per signal, naming the file that proved it. The payload is a list of
70
+ // paths, not of records - `fingerprint` returns evidence as the paths themselves.
71
+ evidence: { type: 'array', items: { type: 'string' } },
72
+ manifest: { type: ['object', 'null'] },
73
+ error: { type: ['string', 'null'] },
74
+ workspace: {
75
+ type: 'object',
76
+ properties: {
77
+ // Null is the honest value for a repository that is not inside a workspace:
78
+ // there is no root to name, and naming the repository as one would be a claim
79
+ // about the reader's directory layout that nothing established.
80
+ root: { type: ['string', 'null'] },
81
+ source: { type: ['string', 'null'] },
82
+ detail: { type: ['string', 'null'] },
83
+ error: { type: ['string', 'null'] },
84
+ },
85
+ required: ['root', 'source'],
86
+ },
87
+ shared: { type: ['object', 'null'] },
88
+ findings: {
89
+ type: 'array',
90
+ items: {
91
+ type: 'object',
92
+ properties: {
93
+ id: { type: 'string' },
94
+ severity: { type: 'string' },
95
+ detail: { type: 'string' },
96
+ remedy: { type: ['string', 'null'] },
97
+ why: { type: ['string', 'null'] },
98
+ safe: { type: 'boolean' },
99
+ fixed: { type: 'boolean' },
100
+ },
101
+ required: ['id', 'severity', 'detail', 'safe', 'fixed'],
102
+ },
103
+ },
104
+ fix: {
105
+ type: 'object',
106
+ properties: {
107
+ requested: { type: 'boolean' },
108
+ applied: { type: 'boolean' },
109
+ fixed: { type: 'array', items: { type: 'string' } },
110
+ failed: { type: 'array', items: { type: 'object' } },
111
+ },
112
+ required: ['requested', 'applied', 'fixed', 'failed'],
113
+ },
114
+ remedy: { type: ['string', 'null'] },
115
+ },
116
+ required: ['root', 'workspace', 'stack', 'facts', 'signals', 'evidence', 'findings'],
117
+ },
118
+ skills_index: {
119
+ type: 'object',
120
+ properties: {
121
+ skills: { type: 'number' },
122
+ totalBytes: { type: 'number' },
123
+ deferred: { type: 'array', items: { type: 'string' } },
124
+ workspace: { type: 'object' },
125
+ verified: { type: 'boolean' },
126
+ entries: {
127
+ type: 'array',
128
+ items: {
129
+ type: 'object',
130
+ properties: {
131
+ name: { type: 'string' },
132
+ summary: { type: 'string' },
133
+ bytes: { type: 'number' },
134
+ stack: { type: 'array', items: { type: 'string' } },
135
+ deferred: { type: 'boolean' },
136
+ },
137
+ required: ['name', 'bytes'],
138
+ },
139
+ },
140
+ },
141
+ required: ['skills', 'totalBytes', 'entries'],
142
+ },
143
+ // The `skills brief --json` document minus its envelope, or the `--all` one.
144
+ // The CLI keeps its field names; this adopts them. (Q15)
145
+ skills_brief: {
146
+ anyOf: [
147
+ {
148
+ type: 'object',
149
+ properties: {
150
+ repo: {
151
+ type: 'object',
152
+ properties: {
153
+ root: { type: 'string' },
154
+ stack: { type: 'array', items: { type: 'string' } },
155
+ facts: { type: 'array', items: { type: 'string' } },
156
+ },
157
+ required: ['root', 'stack', 'facts'],
158
+ },
159
+ workspace: { type: 'string' },
160
+ task: { type: ['string', 'null'] },
161
+ budget: { type: 'number' },
162
+ inlineBytes: { type: 'number' },
163
+ resolved: {
164
+ type: 'array',
165
+ items: {
166
+ type: 'object',
167
+ properties: {
168
+ name: { type: 'string' },
169
+ bytes: { type: 'number' },
170
+ summary: { type: 'string' },
171
+ why: { type: 'array', items: { type: 'string' } },
172
+ },
173
+ required: ['name', 'bytes', 'why'],
174
+ },
175
+ },
176
+ deferred: {
177
+ type: 'array',
178
+ items: {
179
+ type: 'object',
180
+ properties: { name: { type: 'string' }, bytes: { type: 'number' }, reason: { type: 'string' } },
181
+ required: ['name', 'bytes', 'reason'],
182
+ },
183
+ },
184
+ excluded: {
185
+ type: 'array',
186
+ items: {
187
+ type: 'object',
188
+ properties: { name: { type: 'string' }, reason: { type: 'string' } },
189
+ required: ['name', 'reason'],
190
+ },
191
+ },
192
+ warning: { type: 'string' },
193
+ },
194
+ required: ['repo', 'budget', 'inlineBytes', 'resolved', 'deferred', 'excluded'],
195
+ },
196
+ {
197
+ type: 'object',
198
+ properties: {
199
+ all: { type: 'boolean', enum: [true] },
200
+ repoStack: { type: 'array', items: { type: 'string' } },
201
+ inline: { type: 'array', items: { type: 'string' } },
202
+ deferred: { type: 'array', items: { type: 'string' } },
203
+ warning: { type: 'string' },
204
+ },
205
+ required: ['all', 'repoStack', 'inline', 'deferred'],
206
+ },
207
+ ],
208
+ },
209
+ // The `skills show --json` document minus its envelope, plus the body text the
210
+ // command prints below the metadata. (Q15)
211
+ skills_show: {
212
+ type: 'object',
213
+ properties: {
214
+ name: { type: 'string' },
215
+ summary: { type: 'string' },
216
+ bytes: { type: 'number' },
217
+ capabilities: { type: 'array', items: { type: 'string' } },
218
+ stack: { type: 'array', items: { type: 'string' } },
219
+ keywords: { type: 'array', items: { type: 'string' } },
220
+ deferred: { type: 'boolean' },
221
+ requires: { type: 'array', items: { type: 'string' } },
222
+ variants: {
223
+ type: 'array',
224
+ items: {
225
+ type: 'object',
226
+ properties: { id: { type: 'string' }, stack: { type: 'array', items: { type: 'string' } }, bytes: { type: 'number' } },
227
+ required: ['id'],
228
+ },
229
+ },
230
+ sources: { type: 'array', items: { type: 'object' } },
231
+ aliasOf: { type: ['string', 'null'] },
232
+ variantOf: { type: ['string', 'null'] },
233
+ workspace: { type: 'string' },
234
+ body: { type: ['string', 'null'] },
235
+ bodyFrom: { type: 'string' },
236
+ bodyText: { type: ['string', 'null'] },
237
+ reason: { type: ['string', 'null'], description: 'why there is no body, when there is none' },
238
+ },
239
+ required: ['name', 'bytes', 'body', 'bodyFrom', 'bodyText', 'reason', 'requires', 'variants'],
240
+ },
241
+ skills_find: {
242
+ type: 'object',
243
+ properties: {
244
+ query: { type: 'string' },
245
+ results: {
246
+ type: 'array',
247
+ items: {
248
+ type: 'object',
249
+ properties: {
250
+ name: { type: 'string' },
251
+ score: { type: 'number' },
252
+ summary: { type: 'string' },
253
+ reasons: { type: 'array', items: { type: 'string' } },
254
+ },
255
+ required: ['name', 'score', 'reasons'],
256
+ },
257
+ },
258
+ },
259
+ required: ['query', 'results'],
260
+ },
261
+ // The `skills why --json` document minus its envelope. (Q15)
262
+ skills_why: {
263
+ type: 'object',
264
+ properties: {
265
+ skill: { type: 'string' },
266
+ score: { type: 'number' },
267
+ compatible: { type: 'boolean' },
268
+ gate: { type: 'string' },
269
+ repo: {
270
+ type: 'object',
271
+ properties: {
272
+ root: { type: 'string' },
273
+ stack: { type: 'array', items: { type: 'string' } },
274
+ facts: { type: 'array', items: { type: 'string' } },
275
+ evidence: { type: 'array', items: { type: 'string' } },
276
+ },
277
+ },
278
+ workspace: { type: 'object' },
279
+ paths: { type: 'array', items: { type: 'string' } },
280
+ taskTokens: { type: 'array', items: { type: 'string' } },
281
+ signals: { type: 'array', items: { type: 'string' } },
282
+ variants: { type: 'array', items: { type: 'object' } },
283
+ aliasOf: { type: ['string', 'null'] },
284
+ variantOf: { type: ['string', 'null'] },
285
+ requires: { type: 'array', items: { type: 'string' } },
286
+ },
287
+ required: ['skill', 'score', 'compatible', 'signals'],
288
+ },
289
+ rules_list: {
290
+ type: 'object',
291
+ properties: {
292
+ sha: { type: 'string' },
293
+ notes: { type: 'array', items: { type: 'string' } },
294
+ rules: {
295
+ type: 'array',
296
+ items: {
297
+ type: 'object',
298
+ properties: {
299
+ id: { type: ['string', 'null'] },
300
+ title: { type: ['string', 'null'] },
301
+ // A profile may name one signal or several, and the frontmatter keeps both
302
+ // spellings. Describing only the list would make a conforming rule look
303
+ // broken.
304
+ applies: { anyOf: [{ type: 'array', items: { type: 'string' } }, { type: 'string' }] },
305
+ origin: { type: 'string' },
306
+ source: { type: 'string' },
307
+ bytes: { type: 'number' },
308
+ adrs: { type: 'array', items: { type: 'string' } },
309
+ error: { type: ['string', 'null'] },
310
+ fallback: { type: 'boolean' },
311
+ selected: { type: ['boolean', 'null'] },
312
+ },
313
+ required: ['source', 'bytes', 'selected'],
314
+ },
315
+ },
316
+ },
317
+ required: ['sha', 'rules'],
318
+ },
319
+ // The `rules why --json` document minus its envelope, either direction. (Q15)
320
+ rules_why: {
321
+ type: 'object',
322
+ properties: {
323
+ direction: { type: 'string', enum: ['rule', 'decision'] },
324
+ id: { type: 'string' },
325
+ rule: { type: 'string' },
326
+ title: { type: ['string', 'null'] },
327
+ source: { type: 'string' },
328
+ origin: { type: 'string' },
329
+ adrs: { type: 'array', items: { type: 'object' } },
330
+ uncited: { type: 'boolean' },
331
+ others: { type: 'array', items: { type: 'string' } },
332
+ adr: { type: 'object' },
333
+ supersededBy: { type: ['object', 'null'] },
334
+ inForce: { type: 'object' },
335
+ rules: { type: 'array', items: { type: 'string' } },
336
+ },
337
+ required: ['direction', 'id'],
338
+ },
339
+ check: {
340
+ type: 'object',
341
+ properties: {
342
+ root: { type: 'string' },
343
+ ok: { type: 'boolean' },
344
+ findings: {
345
+ type: 'array',
346
+ items: {
347
+ type: 'object',
348
+ properties: {
349
+ file: { type: 'string' },
350
+ status: { type: 'string' },
351
+ detail: { type: ['string', 'null'] },
352
+ remedy: { type: ['string', 'null'] },
353
+ },
354
+ required: ['file', 'status'],
355
+ },
356
+ },
357
+ },
358
+ required: ['root', 'ok', 'findings'],
359
+ },
360
+ compare: {
361
+ type: 'object',
362
+ properties: {
363
+ root: { type: 'string' },
364
+ shape: { type: 'string', enum: ['repo', 'workspace'] },
365
+ expectedSha: { type: 'string' },
366
+ summary: { type: 'object' },
367
+ repos: { type: 'array', items: { type: 'object' } },
368
+ worktrees: { type: 'array', items: { type: 'object' } },
369
+ directories: { type: 'array', items: { type: 'string' } },
370
+ partial: { type: 'boolean' },
371
+ cursor: { type: ['string', 'null'] },
372
+ total: { type: 'number' },
373
+ },
374
+ required: ['root', 'shape', 'expectedSha', 'summary', 'repos'],
375
+ },
376
+ plan_next: {
377
+ type: 'object',
378
+ properties: {
379
+ root: { type: 'string' },
380
+ next: { type: ['object', 'null'] },
381
+ reason: { type: ['string', 'null'] },
382
+ },
383
+ required: ['root', 'next', 'reason'],
384
+ },
385
+ plan_done: {
386
+ type: 'object',
387
+ properties: {
388
+ root: { type: 'string' },
389
+ id: { type: 'string' },
390
+ doc: { type: 'string' },
391
+ line: { type: 'number' },
392
+ from: { type: 'string' },
393
+ to: { type: 'string' },
394
+ wouldChange: { type: 'boolean' },
395
+ dryRun: { type: 'boolean' },
396
+ remedy: { type: ['string', 'null'] },
397
+ },
398
+ required: ['root', 'id', 'wouldChange', 'dryRun'],
399
+ },
400
+ brief: {
401
+ type: 'object',
402
+ properties: {
403
+ root: { type: 'string' },
404
+ kind: { type: 'string' },
405
+ basis: { type: 'string' },
406
+ task: { type: 'string' },
407
+ scope: { type: 'array', items: { type: 'string' } },
408
+ budget: { type: 'number' },
409
+ bytes: { type: 'number' },
410
+ before: { type: 'number' },
411
+ floorHeld: { type: 'boolean' },
412
+ cut: { type: 'array', items: { type: 'string' } },
413
+ sections: { type: 'object', additionalProperties: { type: 'array', items: { type: 'string' } } },
414
+ text: { type: 'string' },
415
+ note: { type: ['string', 'null'] },
416
+ },
417
+ required: ['root', 'kind', 'basis', 'task', 'scope', 'budget', 'bytes', 'before', 'floorHeld', 'cut', 'sections', 'text'],
418
+ },
419
+ pr_body: {
420
+ type: 'object',
421
+ properties: {
422
+ root: { type: 'string' },
423
+ base: { type: ['string', 'null'] },
424
+ tree: { type: ['string', 'null'] },
425
+ verdict: { type: 'string' },
426
+ unreviewed: { type: 'array', items: { type: 'string' } },
427
+ paths: { type: 'array', items: { type: 'string' } },
428
+ decisions: {
429
+ type: 'array',
430
+ items: {
431
+ type: 'object',
432
+ properties: {
433
+ path: { type: 'string' },
434
+ id: { type: 'string' },
435
+ title: { type: 'string' },
436
+ status: { type: 'string' },
437
+ },
438
+ required: ['path', 'id', 'title', 'status'],
439
+ },
440
+ },
441
+ gates: {
442
+ type: 'object',
443
+ properties: {
444
+ declared: { type: 'array', items: { type: 'string' } },
445
+ evidenceMode: { type: ['string', 'null'] },
446
+ run: { type: ['object', 'null'] },
447
+ rulesOk: { type: ['boolean', 'null'] },
448
+ hooksOk: { type: ['boolean', 'null'] },
449
+ blocking: { type: 'number' },
450
+ warnings: { type: 'number' },
451
+ cached: { type: 'boolean' },
452
+ },
453
+ required: ['declared', 'blocking', 'warnings', 'cached'],
454
+ },
455
+ evidence: {
456
+ type: 'object',
457
+ properties: {
458
+ entries: { type: 'array', items: { type: 'object' } },
459
+ errors: { type: 'array', items: { type: 'string' } },
460
+ },
461
+ required: ['entries', 'errors'],
462
+ },
463
+ },
464
+ required: ['root', 'base', 'tree', 'verdict', 'unreviewed', 'decisions', 'gates', 'evidence', 'paths'],
465
+ },
466
+ federation_query: {
467
+ type: 'object',
468
+ properties: {
469
+ root: { type: 'string' },
470
+ query: { type: 'string' },
471
+ servers: { type: 'array', items: { type: 'string' } },
472
+ answers: {
473
+ type: 'array',
474
+ items: {
475
+ type: 'object',
476
+ properties: {
477
+ server: { type: 'string' },
478
+ tool: { type: 'string' },
479
+ query: { type: 'string' },
480
+ text: { type: 'string' },
481
+ },
482
+ required: ['server', 'tool', 'query', 'text'],
483
+ },
484
+ },
485
+ findings: { type: 'array', items: { type: 'string' } },
486
+ },
487
+ required: ['root', 'query', 'servers', 'answers', 'findings'],
488
+ },
489
+ /** The shape of every tool failure, which is a result and not a protocol error. */
490
+ error: ERROR_SHAPE,
491
+ };
492
+
493
+ export const rootArg = {
494
+ type: 'object',
495
+ properties: { root: { type: 'string', description: 'repository root; defaults to the editor cwd' } },
496
+ required: [],
497
+ };
498
+
499
+
500
+ export function rootOf(args) {
501
+ return typeof args?.root === 'string' && args.root.length > 0 ? args.root : process.cwd();
502
+ }
503
+
504
+ export async function registry(args) {
505
+ const { registry, errors, ok, verified, workspace } = await loadRegistry(null, args ?? {});
506
+ if (!ok) {
507
+ throw Object.assign(new Error(errors[0]), {
508
+ remedy: `run: ${CLI} graph lint`,
509
+ });
510
+ }
511
+ return { registry, verified, workspace };
512
+ }
@@ -24,6 +24,7 @@ import { createInterface } from 'node:readline';
24
24
  import { CLI } from '../name.mjs';
25
25
  import { getPrompt, PROMPTS } from './prompts.mjs';
26
26
  import { RESOURCE_TEMPLATES, listResources, readResource } from './resources.mjs';
27
+ import { paginate } from './paginate.mjs';
27
28
  import { check } from './schema.mjs';
28
29
 
29
30
  /**
@@ -207,11 +208,15 @@ export async function handle(request, tools, { packageVersion, state, root, sign
207
208
  }
208
209
  return reply({});
209
210
 
210
- case 'tools/list':
211
+ case 'tools/list': {
212
+ const page = paginate(tools, params?.cursor);
213
+ if (page === null) return badCursor(id);
211
214
  return reply({
212
- tools: tools.map((t) => describe(t, v)),
215
+ tools: page.items.map((t) => describe(t, v)),
216
+ ...nextPage(page),
213
217
  ...(cacheable(v) ? LIST_CACHE : {}),
214
218
  });
219
+ }
215
220
 
216
221
  case 'tools/call': {
217
222
  const name = params?.name;
@@ -265,16 +270,22 @@ export async function handle(request, tools, { packageVersion, state, root, sign
265
270
  }
266
271
  }
267
272
 
268
- case 'resources/list':
273
+ case 'resources/list': {
274
+ const page = paginate(await listResources(root), params?.cursor);
275
+ if (page === null) return badCursor(id);
269
276
  return reply(
270
- cacheable(v) ? { resources: await listResources(root), ...LIST_CACHE } : { resources: await listResources(root) },
277
+ cacheable(v) ? { resources: page.items, ...nextPage(page), ...LIST_CACHE } : { resources: page.items, ...nextPage(page) },
271
278
  );
272
- case 'resources/templates/list':
279
+ }
280
+ case 'resources/templates/list': {
281
+ const page = paginate(RESOURCE_TEMPLATES, params?.cursor);
282
+ if (page === null) return badCursor(id);
273
283
  return reply(
274
284
  cacheable(v)
275
- ? { resourceTemplates: RESOURCE_TEMPLATES, ...LIST_CACHE }
276
- : { resourceTemplates: RESOURCE_TEMPLATES },
285
+ ? { resourceTemplates: page.items, ...nextPage(page), ...LIST_CACHE }
286
+ : { resourceTemplates: page.items, ...nextPage(page) },
277
287
  );
288
+ }
278
289
  case 'resources/read': {
279
290
  // The code for a resource that is not there CHANGED, and a server that answers
280
291
  // with one code to both revisions is a server that is wrong to whichever half of
@@ -288,10 +299,13 @@ export async function handle(request, tools, { packageVersion, state, root, sign
288
299
  cacheable(v) ? { contents: [found], ...LIST_CACHE } : { contents: [found] },
289
300
  );
290
301
  }
291
- case 'prompts/list':
302
+ case 'prompts/list': {
303
+ const page = paginate(PROMPTS, params?.cursor);
304
+ if (page === null) return badCursor(id);
292
305
  return reply(
293
- cacheable(v) ? { prompts: PROMPTS, ...LIST_CACHE } : { prompts: PROMPTS },
306
+ cacheable(v) ? { prompts: page.items, ...nextPage(page), ...LIST_CACHE } : { prompts: page.items, ...nextPage(page) },
294
307
  );
308
+ }
295
309
  case 'prompts/get': {
296
310
  const name = params?.name;
297
311
  // `getPrompt` returns null only for a prompt this server does not have: its
@@ -304,6 +318,16 @@ export async function handle(request, tools, { packageVersion, state, root, sign
304
318
  if (found === null) return error(id, RPC.METHOD, `no prompt named "${String(name)}"; call prompts/list first`);
305
319
  return reply(found);
306
320
  }
321
+ case 'resources/subscribe':
322
+ case 'resources/unsubscribe':
323
+ // A named refusal, not `unknown method`. The capability block above says `resources:
324
+ // {}` - no subscriptions - so a well-behaved client never asks; one that asks anyway
325
+ // is asking a protocol question and deserves the reason rather than a message that
326
+ // reads as a typo and sends it hunting for a spelling.
327
+ return isNotification
328
+ ? null
329
+ : error(id, RPC.METHOD, `${method} is not supported: this server has no change source; resources/list is always current`);
330
+
307
331
  default:
308
332
  return isNotification ? null : error(id, RPC.METHOD, `unknown method "${method}"`);
309
333
  }
@@ -383,6 +407,22 @@ function decorate(value, version, { packageVersion }) {
383
407
  };
384
408
  }
385
409
 
410
+ /** The cursor field, present only when there is a next page. */
411
+ function nextPage(page) {
412
+ return page.nextCursor === null ? {} : { nextCursor: page.nextCursor };
413
+ }
414
+
415
+ /**
416
+ * A cursor this server did not issue.
417
+ *
418
+ * A protocol error, not an empty page: an empty page reads as "the list ended", which is
419
+ * a different fact from "your cursor is stale" and leads the client to show a partial
420
+ * estate as a complete one.
421
+ */
422
+ function badCursor(id) {
423
+ return error(id, RPC.INVALID_PARAMS, 'invalid cursor: omit it to start at the first page');
424
+ }
425
+
386
426
  function describe(tool, version) {
387
427
  const entry = {
388
428
  name: tool.name,