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,326 @@
1
+ // Half of the MCP tool list. The tools are read-only and answer through the commands.
2
+
3
+ import { loadRegistry } from '../graph/load.mjs';
4
+ import { provenanceDigest } from '../projection/project.mjs';
5
+ import { discover } from '../compare/discover.mjs';
6
+ import { buildRows, resolveConcurrency, summarise } from '../compare/row.mjs';
7
+ import { GATES } from '../hooks/gates.mjs';
8
+ import { checkRepo } from '../commands/sync.mjs';
9
+ import { diagnose, serialisedFindings } from '../commands/doctor.mjs';
10
+ import { rulesCatalogue, rulesWhyFacts } from '../commands/rules.mjs';
11
+ import { skillsBriefFacts, skillsFindFacts, skillsShowFacts, skillsWhyFacts } from '../commands/skills.mjs';
12
+ import { assembleBrief } from '../brief/assemble.mjs';
13
+ import { DEFAULT_BRIEF_BYTES } from '../brief/packet.mjs';
14
+ import { federationAccess, readFederation } from '../federation/config.mjs';
15
+ import { consult } from '../federation/query.mjs';
16
+ import { planNext, planDone } from '../plan/index.mjs';
17
+ import { buildPrBody } from '../prbody.mjs';
18
+ import { configPaths, readUserConfig, resolveWorkspace } from '../workspace.mjs';
19
+ import { measureSkill } from '../graph/load.mjs';
20
+ import { CLI } from '../name.mjs';
21
+ import { ERROR_SHAPE, GATE_KINDS, noArgs, output, rootArg } from './schemas.mjs';
22
+ import { registry, rootOf } from './schemas.mjs';
23
+
24
+ export const TOOLS_FACTS = [
25
+ {
26
+ outputSchema: output.check,
27
+ name: 'check',
28
+ description:
29
+ 'The gate for one repository: region current, unmanaged, drifted or malformed, and rule files lacking an id. Read-only - it never repairs.',
30
+ inputSchema: rootArg,
31
+ async run(args) {
32
+ const root = rootOf(args);
33
+ const { ok, findings } = await checkRepo(root, {});
34
+ return { root, ok, findings };
35
+ },
36
+ },
37
+
38
+ {
39
+ outputSchema: output.compare,
40
+ name: 'compare',
41
+ description:
42
+ 'The workspace matrix: adoption, region, SHA, hooks and gates per repo, plus problems. Pages with limit/cursor; honors progressToken and cancellation.',
43
+ inputSchema: {
44
+ type: 'object',
45
+ properties: {
46
+ root: { type: 'string', description: 'a workspace directory, or one repository' },
47
+ workspace: { type: 'string', description: 'workspace root when root is not given' },
48
+ limit: { type: 'number', description: 'page size; without it every row is built' },
49
+ cursor: { type: 'string', description: 'opaque page start from a previous partial answer' },
50
+ concurrency: { type: 'number', description: 'rows built at once (default 4)' },
51
+ },
52
+ required: [],
53
+ },
54
+ async run(args, ctx = {}) {
55
+ const root = args?.root ?? args?.workspace ?? (await resolveWorkspace(args ?? {})).root ?? process.cwd();
56
+ const found = await discover(root);
57
+ if (found.error) throw Object.assign(new Error(`${root}: ${found.error}`), { remedy: 'pass root' });
58
+ const expectedSha = await provenanceDigest();
59
+ const context = { expectedSha, gates: new Set(Object.keys(GATES)), gateKinds: GATE_KINDS };
60
+ // A page names its start as an index into discovery order, which is
61
+ // stable between calls on an unchanged estate; garbage parses to 0
62
+ // rather than erroring, because a dashboard that refuses to run over a
63
+ // stale cursor is worse than one that restarts the listing.
64
+ const start = Math.max(0, parseInt(typeof args?.cursor === 'string' ? args.cursor : '0', 10) || 0);
65
+ const limit = typeof args?.limit === 'number' && args.limit >= 1 ? Math.floor(args.limit) : null;
66
+ const page = limit === null ? found.repos : found.repos.slice(start, start + limit);
67
+ const rows = await buildRows(page, context, {
68
+ concurrency: resolveConcurrency(args ?? {}),
69
+ signal: ctx?.signal ?? null,
70
+ onBatch: ctx?.onProgress ? (done, total) => ctx.onProgress(start + done, found.repos.length) : null,
71
+ });
72
+ const next = limit !== null && start + page.length < found.repos.length ? String(start + page.length) : null;
73
+ return {
74
+ root,
75
+ shape: found.shape,
76
+ expectedSha,
77
+ summary: summarise(rows, { root, shape: found.shape, expectedSha }),
78
+ repos: rows,
79
+ worktrees: found.worktrees.map((w) => ({ name: w.name, owner: w.ownerName })),
80
+ directories: found.directories,
81
+ ...(limit === null ? {} : { partial: next !== null, cursor: next, total: found.repos.length }),
82
+ };
83
+ },
84
+ },
85
+ {
86
+ outputSchema: output.plan_next,
87
+ name: 'plan_next',
88
+ description: "The next queued slice whose dependencies are complete; its design section only. Pass id for one slice whether or not it is ready.",
89
+ inputSchema: {
90
+ type: 'object',
91
+ properties: {
92
+ root: { type: 'string', description: 'repository root; defaults to the editor cwd' },
93
+ id: { type: 'string', description: 'slice id; when given, return that slice' },
94
+ },
95
+ required: [],
96
+ },
97
+ async run(args) {
98
+ const root = rootOf(args);
99
+ const id = typeof args?.id === 'string' && args.id.length > 0 ? args.id : null;
100
+ const result = await planNext(root, id);
101
+ if (result.error) throw Object.assign(new Error(result.error), { remedy: result.remedy ?? null });
102
+ return { root: result.root, next: result.next, reason: result.reason };
103
+ },
104
+ },
105
+ {
106
+ outputSchema: output.plan_done,
107
+ name: 'plan_done',
108
+ // Same function as the command, and deliberately a preview. This surface does
109
+ // not write: the row change is a diff, and the command applies it behind --yes.
110
+ description: 'Preview the one-row rewrite for a slice. The write stays on the plan done command, behind --yes.',
111
+ inputSchema: {
112
+ type: 'object',
113
+ properties: {
114
+ root: { type: 'string', description: 'repository root; defaults to the editor cwd' },
115
+ id: { type: 'string', description: 'slice id' },
116
+ sha: { type: 'string', description: 'commit sha that closed the slice' },
117
+ test: { type: 'string', description: 'test command that closed it' },
118
+ state: { type: 'string', description: 'target state; omitted means the completed state of the repository template (Complete for tracker)' },
119
+ },
120
+ required: ['id'],
121
+ },
122
+ async run(args) {
123
+ const root = rootOf(args);
124
+ const result = await planDone(root, args?.id, {
125
+ sha: args?.sha ?? null,
126
+ test: args?.test ?? null,
127
+ state: args?.state ?? null,
128
+ dryRun: true,
129
+ });
130
+ if (result.error) throw Object.assign(new Error(result.error), { remedy: result.remedy ?? null });
131
+ return {
132
+ root: result.root,
133
+ id: result.id,
134
+ doc: result.doc,
135
+ line: result.line,
136
+ from: result.from,
137
+ to: result.to,
138
+ wouldChange: result.wouldChange,
139
+ dryRun: true,
140
+ remedy: result.remedy,
141
+ };
142
+ },
143
+ },
144
+ // The PR-flow entry point over this surface: the same engine the `pr-body`
145
+ // command renders, returning JSON for a model. Read-only like everything
146
+ // here — it names the tree, reads the ledger and the check cache, and writes
147
+ // nothing, so the exit-code-never-carries-the-verdict contract stays intact.
148
+ {
149
+ outputSchema: output.pr_body,
150
+ name: 'pr_body',
151
+ // R41's 2 KB description budget is shared, not per tool: this sentence was
152
+ // paid for by shortening `brief` and `federation_query`, and the test on the
153
+ // total holds every future tool to the same trade.
154
+ description:
155
+ 'The pull request body: ledger evidence for this tree, the decisions the diff touches, gate state, and the ADR-008 verdict with what was not reviewed.',
156
+ inputSchema: {
157
+ type: 'object',
158
+ properties: {
159
+ root: { type: 'string', description: 'repository root; defaults to the editor cwd' },
160
+ base: { type: 'string', description: 'the ref the body diffs against; defaults to origin/main, then main' },
161
+ },
162
+ required: [],
163
+ },
164
+ async run(args) {
165
+ const root = rootOf(args);
166
+ const base = typeof args?.base === 'string' && args.base.length > 0 ? args.base : null;
167
+ const report = await buildPrBody(root, { base });
168
+ if (report.error !== null) {
169
+ throw Object.assign(new Error(report.error), {
170
+ remedy: `run: ${CLI} pr-body --base <ref> with a ref git knows`,
171
+ });
172
+ }
173
+ return {
174
+ root,
175
+ base: report.base,
176
+ tree: report.tree,
177
+ verdict: report.verdict,
178
+ unreviewed: report.unreviewed,
179
+ paths: report.paths,
180
+ decisions: report.decisions,
181
+ gates: report.gates,
182
+ evidence: report.evidence,
183
+ };
184
+ },
185
+ },
186
+ {
187
+ outputSchema: output.brief,
188
+ name: 'brief',
189
+ description:
190
+ 'The packet for one task in prompt-sized bytes: kind, commands, files, tests, decisions, skills. With no task it describes the repository.',
191
+ inputSchema: {
192
+ type: 'object',
193
+ properties: {
194
+ root: { type: 'string', description: 'repository root; defaults to the editor cwd' },
195
+ task: { type: 'string', description: "what the agent is about to do, in the user's own words" },
196
+ paths: { type: 'string', description: 'comma-separated paths to scope the packet to, instead of the files git reports as modified' },
197
+ budget: { type: 'number', description: 'byte ceiling; 0 means the whole packet (default 2048)' },
198
+ },
199
+ required: [],
200
+ },
201
+ async run(args) {
202
+ const root = rootOf(args);
203
+ const budget = Number.isFinite(args?.budget) && args.budget >= 0 ? args.budget : DEFAULT_BRIEF_BYTES;
204
+ // Read, never build: every tool here declares itself read-only, and a call that
205
+ // indexed the tree would make that declaration false. A brief with no index to read
206
+ // says which command makes one - the same rule the delivered packet follows, so a
207
+ // pull and a push pay the same price and neither one writes.
208
+ const result = await assembleBrief({
209
+ root,
210
+ task: typeof args?.task === 'string' ? args.task : '',
211
+ paths: typeof args?.paths === 'string' ? args.paths : null,
212
+ budget,
213
+ build: false,
214
+ });
215
+ if (result.error) throw Object.assign(new Error('the index could not be read'), { remedy: `run: ${CLI} index` });
216
+ // A repository with no index is not a failed call. Every surface points here first,
217
+ // so the one tool told to be the entry point must answer in a repository that has
218
+ // never been indexed - it falls back to the skill packet, which is read from the
219
+ // package rather than the tree, and names the command that unlocks the rest (Q56).
220
+ if (result.cold) {
221
+ const task = typeof args?.task === 'string' ? args.task : '';
222
+ const facts = await skillsBriefFacts(root, { ...(args ?? {}), task });
223
+ // Through JSON once, so the packet carries exactly what the text block shows: a
224
+ // property whose value is `undefined` survives `stringify` as nothing at all, and
225
+ // the two blocks would then describe different objects.
226
+ const text = JSON.stringify(facts, null, 2);
227
+ const packet = JSON.parse(text);
228
+ const size = Buffer.byteLength(text);
229
+ return {
230
+ root,
231
+ kind: 'skills',
232
+ basis: 'registry',
233
+ task,
234
+ scope: [],
235
+ budget,
236
+ bytes: size,
237
+ before: size,
238
+ // The skill packet is one document and is not cut to fit: saying the floor held
239
+ // when the answer is over it is the class of claim this review exists to remove.
240
+ floorHeld: size <= budget,
241
+ cut: size <= budget ? [] : ['text'],
242
+ sections: { skills: (packet.resolved ?? []).map((s) => s.name) },
243
+ text,
244
+ note: `no index here, so this is the skill packet; run: ${CLI} index for the repository packet`,
245
+ };
246
+ }
247
+ return {
248
+ root: result.root,
249
+ kind: result.kind,
250
+ basis: result.basis,
251
+ task: result.task,
252
+ scope: result.scope,
253
+ budget: result.budget,
254
+ bytes: result.packed.after,
255
+ before: result.packed.before,
256
+ floorHeld: result.packed.floorHeld,
257
+ cut: result.packed.cut,
258
+ sections: result.sections,
259
+ text: result.packed.text,
260
+ };
261
+ },
262
+ },
263
+ {
264
+ outputSchema: output.federation_query,
265
+ name: 'federation_query',
266
+ description:
267
+ 'Ask a declared code-graph server one structural question; the answer names the server. Starts only servers simt.json names; none declared is an error.',
268
+ // This tool spawns the repo manifest's `command`, so it is not read-only: a client
269
+ // that cached its answer as one would replay a process spawn as a pure read (Q13).
270
+ annotations: { readOnlyHint: false, openWorldHint: true },
271
+ inputSchema: {
272
+ type: 'object',
273
+ properties: {
274
+ root: { type: 'string', description: 'repository root; defaults to the editor cwd' },
275
+ query: { type: 'string', description: "the structural question to ask, in the user's own words" },
276
+ server: { type: 'string', description: 'ask only this declared server, by name; omit to ask every one of them' },
277
+ },
278
+ required: ['query'],
279
+ },
280
+ async run(args) {
281
+ const root = rootOf(args);
282
+ const query = typeof args?.query === 'string' ? args.query.trim() : '';
283
+ if (query.length === 0) {
284
+ throw Object.assign(new Error('the query is empty'), {
285
+ remedy: 'pass a query, for example the symbol or file to look up',
286
+ });
287
+ }
288
+ const { servers, findings } = await readFederation(root);
289
+ if (servers.length === 0) {
290
+ throw Object.assign(new Error('this repository declares no federation server'), {
291
+ remedy: 'add a "federation" block naming a server to simt.json, or use the brief tool',
292
+ });
293
+ }
294
+ const only = typeof args?.server === 'string' && args.server.length > 0 ? args.server : null;
295
+ const chosen = only === null ? servers : servers.filter((server) => server.name === only);
296
+ if (only !== null && chosen.length === 0) {
297
+ throw Object.assign(new Error(`no declared federation server is named ${only}`), {
298
+ remedy: `declared servers: ${servers.map((server) => server.name).join(', ')}`,
299
+ });
300
+ }
301
+ // A model-supplied root can point at a directory with a crafted manifest, and a
302
+ // repo manifest alone must not authorise a spawn on the operator's machine (Q13).
303
+ const ws = await resolveWorkspace(args ?? {});
304
+ const user = await readUserConfig();
305
+ const denied = federationAccess({
306
+ root,
307
+ cwd: process.cwd(),
308
+ workspaceRoot: ws.root,
309
+ serverNames: chosen.map((server) => server.name),
310
+ userConfig: user.config,
311
+ configPath: user.path ?? configPaths()[0] ?? '~/.config/simt-dev/config.json',
312
+ });
313
+ if (denied !== null) {
314
+ throw Object.assign(new Error(denied.message), { remedy: denied.remedy });
315
+ }
316
+ const { answers, findings: failures } = await consult({ root, query, servers: chosen });
317
+ return {
318
+ root,
319
+ query,
320
+ servers: chosen.map((server) => server.name),
321
+ answers,
322
+ findings: [...findings, ...failures],
323
+ };
324
+ },
325
+ },
326
+ ];
@@ -0,0 +1,221 @@
1
+ // Half of the MCP tool list. The tools are read-only and answer through the commands.
2
+
3
+ import { loadRegistry } from '../graph/load.mjs';
4
+ import { provenanceDigest } from '../projection/project.mjs';
5
+ import { discover } from '../compare/discover.mjs';
6
+ import { buildRows, resolveConcurrency, summarise } from '../compare/row.mjs';
7
+ import { GATES } from '../hooks/gates.mjs';
8
+ import { checkRepo } from '../commands/sync.mjs';
9
+ import { diagnose, serialisedFindings } from '../commands/doctor.mjs';
10
+ import { rulesCatalogue, rulesWhyFacts } from '../commands/rules.mjs';
11
+ import { skillsBriefFacts, skillsFindFacts, skillsShowFacts, skillsWhyFacts } from '../commands/skills.mjs';
12
+ import { assembleBrief } from '../brief/assemble.mjs';
13
+ import { DEFAULT_BRIEF_BYTES } from '../brief/packet.mjs';
14
+ import { federationAccess, readFederation } from '../federation/config.mjs';
15
+ import { consult } from '../federation/query.mjs';
16
+ import { planNext, planDone } from '../plan/index.mjs';
17
+ import { buildPrBody } from '../prbody.mjs';
18
+ import { configPaths, readUserConfig, resolveWorkspace } from '../workspace.mjs';
19
+ import { measureSkill } from '../graph/load.mjs';
20
+ import { CLI } from '../name.mjs';
21
+ import { ERROR_SHAPE, GATE_KINDS, noArgs, output, rootArg } from './schemas.mjs';
22
+ import { registry, rootOf } from './schemas.mjs';
23
+
24
+ export const TOOLS_READ = [
25
+ {
26
+ outputSchema: output.doctor,
27
+ name: 'doctor',
28
+ description:
29
+ 'Classify a repository: stack tags, the facts with the file that proved each, and manifest presence. Call this first when the repo is unknown. Read-only: `--fix` and `--diff` stay on the command.',
30
+ inputSchema: rootArg,
31
+ async run(args) {
32
+ const root = rootOf(args);
33
+ const { result, ws, shared, findings, failure } = await diagnose(root, args ?? {});
34
+ return {
35
+ root,
36
+ ...result,
37
+ workspace: { root: ws.root ?? null, source: ws.source, detail: ws.detail, error: ws.error ?? null },
38
+ shared,
39
+ findings: serialisedFindings(findings),
40
+ // Read-only here: the command applies fixes behind `--fix`, the tool only reports.
41
+ fix: { requested: false, applied: false, fixed: [], failed: [] },
42
+ ...(failure ?? {}),
43
+ };
44
+ },
45
+ },
46
+
47
+ {
48
+ outputSchema: output.skills_index,
49
+ name: 'skills_index',
50
+ description:
51
+ 'Full skill catalogue: every skill with summary, size, stack tags and deferred flag. The fallback when brief returns nothing useful.',
52
+ inputSchema: {
53
+ type: 'object',
54
+ properties: {
55
+ registry: { type: 'string', description: 'path to skills.graph.json; else --registry / SIMT_REGISTRY / default tiers' },
56
+ },
57
+ required: [],
58
+ },
59
+ async run(args) {
60
+ const { registry: reg, verified, workspace } = await registry(args);
61
+ return {
62
+ skills: reg.skills.length,
63
+ totalBytes: reg.skills.reduce((n, s) => n + s.bytes, 0),
64
+ deferred: reg.skills.filter((s) => s.deferred).map((s) => s.name),
65
+ workspace,
66
+ verified,
67
+ entries: [...reg.skills]
68
+ .sort((a, b) => a.name.localeCompare(b.name))
69
+ .map((s) => ({ name: s.name, summary: s.summary, bytes: s.bytes, stack: s.stack, deferred: Boolean(s.deferred) })),
70
+ };
71
+ },
72
+ },
73
+
74
+ {
75
+ outputSchema: output.skills_brief,
76
+ name: 'skills_brief',
77
+ description:
78
+ 'THE entry point. Resolve the skill set for a task: selected with why, deferred, and not selected with the reason to override.',
79
+ inputSchema: {
80
+ type: 'object',
81
+ properties: {
82
+ root: { type: 'string' },
83
+ task: { type: 'string', description: 'what you are about to do, in your own words' },
84
+ paths: { type: 'string', description: 'comma-separated globs; defaults to the git diff' },
85
+ budget: { type: 'number', description: 'context budget in bytes (default 20480)' },
86
+ skill: { type: 'string', description: 'force a skill in, comma-separated' },
87
+ all: { type: 'boolean', description: 'ignore the budget' },
88
+ registry: { type: 'string', description: 'path to skills.graph.json; else --registry / SIMT_REGISTRY / default tiers' },
89
+ },
90
+ required: [],
91
+ },
92
+ async run(args) {
93
+ const root = rootOf(args);
94
+ const facts = await skillsBriefFacts(root, args ?? {});
95
+ // No body is ever returned by this tool: a caller that wants one calls
96
+ // skills_show, which makes the cost visible before it is paid.
97
+ if (facts.kind === 'all') {
98
+ return {
99
+ all: true,
100
+ repoStack: facts.repoStack,
101
+ inline: facts.inline.map((s) => s.name),
102
+ deferred: facts.deferred.map((s) => s.name),
103
+ ...(facts.warning ? { warning: facts.warning } : {}),
104
+ };
105
+ }
106
+ const { shown, shownDeferred, shownUsed, pointerOnly, excludedEntries, workspaceRoot, kind, ...doc } = facts;
107
+ return doc;
108
+ },
109
+ },
110
+
111
+ {
112
+ outputSchema: output.skills_show,
113
+ name: 'skills_show',
114
+ description: 'One skill body and the metadata needed to judge it: source path, the location it was read from, and its cited decisions.',
115
+ inputSchema: {
116
+ type: 'object',
117
+ properties: {
118
+ name: { type: 'string' },
119
+ root: { type: 'string' },
120
+ maxBytes: { type: 'number', description: 'hard ceiling on the returned text' },
121
+ registry: { type: 'string', description: 'path to skills.graph.json; else --registry / SIMT_REGISTRY / default tiers' },
122
+ },
123
+ required: ['name'],
124
+ },
125
+ async run(args) {
126
+ const root = rootOf(args);
127
+ const facts = await skillsShowFacts(root, args?.name, args ?? {});
128
+ // The schema promises `body: null` when there is no body, and this used to hand the
129
+ // prose that says so to the field a client tests for null: a caller checking `body`
130
+ // for null read a paragraph as a body (Q56). The prose moves to `reason`, which is
131
+ // where a model looks for what to do about it.
132
+ const missing = typeof facts.bodyText === 'string' && facts.bodyText.startsWith('(body unavailable');
133
+ return {
134
+ ...facts.skill,
135
+ workspace: facts.workspace,
136
+ body: missing ? null : facts.body,
137
+ bodyFrom: facts.bodyFrom,
138
+ bodyText: missing ? null : facts.bodyText,
139
+ reason: missing ? facts.bodyText : null,
140
+ };
141
+ },
142
+ },
143
+
144
+ {
145
+ outputSchema: output.skills_find,
146
+ name: 'skills_find',
147
+ description: 'Raw search across capability, keyword and summary. Use when skills_brief has nothing and you suspect a skill exists.',
148
+ inputSchema: {
149
+ type: 'object',
150
+ properties: {
151
+ query: { type: 'string' },
152
+ root: { type: 'string' },
153
+ registry: { type: 'string', description: 'path to skills.graph.json; else --registry / SIMT_REGISTRY / default tiers' },
154
+ },
155
+ required: ['query'],
156
+ },
157
+ async run(args) {
158
+ const root = rootOf(args);
159
+ return skillsFindFacts(root, typeof args?.query === 'string' ? args.query : '', args ?? {});
160
+ },
161
+ },
162
+
163
+ {
164
+ outputSchema: output.skills_why,
165
+ name: 'skills_why',
166
+ description:
167
+ 'The signal chain for one skill in one repository: its score, the reasons, whether the stack gate admits it, and what would select it.',
168
+ inputSchema: {
169
+ type: 'object',
170
+ properties: {
171
+ name: { type: 'string' },
172
+ root: { type: 'string' },
173
+ task: { type: 'string' },
174
+ registry: { type: 'string', description: 'path to skills.graph.json; else --registry / SIMT_REGISTRY / default tiers' },
175
+ },
176
+ required: ['name'],
177
+ },
178
+ async run(args) {
179
+ const root = rootOf(args);
180
+ return skillsWhyFacts(root, args?.name, args ?? {});
181
+ },
182
+ },
183
+
184
+ {
185
+ outputSchema: output.rules_list,
186
+ name: 'rules_list',
187
+ description:
188
+ 'The rule catalogue, and which rules a repository actually receives. Pass root for the selection, not just the catalogue.',
189
+ inputSchema: {
190
+ type: 'object',
191
+ properties: { root: { type: 'string' } },
192
+ required: [],
193
+ },
194
+ async run(args) {
195
+ const root = typeof args?.root === 'string' && args.root ? args.root : null;
196
+ const { rules, notes } = await rulesCatalogue(root, { selectFor: root });
197
+ return {
198
+ sha: await provenanceDigest(),
199
+ notes,
200
+ rules,
201
+ };
202
+ },
203
+ },
204
+
205
+ {
206
+ outputSchema: output.rules_why,
207
+ name: 'rules_why',
208
+ description:
209
+ 'Why a rule exists: a rule id for the decisions that settled it, or an ADR id for the rules that repeat it. An uncited rule is a finding, reported so.',
210
+ inputSchema: {
211
+ type: 'object',
212
+ properties: { what: { type: 'string', description: 'a rule id or ADR-NNN' } },
213
+ required: ['what'],
214
+ },
215
+ async run(args) {
216
+ const what = typeof args?.what === 'string' ? args.what : '';
217
+ return rulesWhyFacts(null, what, {});
218
+ },
219
+ },
220
+
221
+ ];