simt-dev 0.5.0 → 0.6.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.
- package/CHANGELOG.md +146 -0
- package/README.md +78 -59
- package/docs/decisions/ADR-058-one-tool-three-channels-and-the-order-is-forced.md +13 -0
- package/docs/decisions/ADR-059-the-version-a-repository-runs-is-the-one-it-pinned.md +1 -1
- package/docs/decisions/ADR-097-the-channels-publish-by-hand-and-the-version-rides-in-as-an-input.md +71 -0
- package/docs/decisions/ADR-098-the-fourth-channel-is-one-file-and-it-needs-no-toolchain.md +149 -0
- package/docs/decisions/ADR-099-the-release-verifies-served-bytes-and-the-formula-mirrors-them.md +79 -0
- package/package.json +4 -4
- package/src/agenthooks/render.mjs +87 -21
- package/src/argv.mjs +13 -0
- package/src/bench/ablate.mjs +1 -3
- package/src/bench/grade.mjs +1 -1
- package/src/bench/mine.mjs +3 -3
- package/src/bench/probes.mjs +1 -3
- package/src/bench/report.mjs +1 -3
- package/src/bench/run.mjs +1 -3
- package/src/bench/tasks.mjs +1 -3
- package/src/bench/variants.mjs +1 -3
- package/src/brief/assemble.mjs +2 -4
- package/src/ci/dependabot.mjs +1 -1
- package/src/ci/workflow.mjs +47 -17
- package/src/commands/agenthook.mjs +6 -2
- package/src/commands/bench.mjs +4 -4
- package/src/commands/ci.mjs +1 -1
- package/src/commands/compare.mjs +1 -1
- package/src/commands/distill.mjs +1 -1
- package/src/commands/gate.mjs +2 -2
- package/src/commands/graph.mjs +2 -2
- package/src/commands/harness.mjs +87 -14
- package/src/commands/help.mjs +29 -7
- package/src/commands/hooks.mjs +1 -1
- package/src/commands/index.mjs +17 -3
- package/src/commands/init.mjs +2 -2
- package/src/commands/local.mjs +11 -0
- package/src/commands/mcp.mjs +1 -1
- package/src/commands/pack.mjs +246 -0
- package/src/commands/plugin.mjs +61 -0
- package/src/commands/rules.mjs +1 -1
- package/src/commands/skills.mjs +1 -1
- package/src/commands/skillscompile.mjs +1 -3
- package/src/commands/skillsinstall.mjs +1 -1
- package/src/commands/sync.mjs +8 -8
- package/src/commands/tokens.mjs +1 -1
- package/src/commands/upgrade.mjs +1 -1
- package/src/commands/why.mjs +1 -1
- package/src/compare/discover.mjs +1 -1
- package/src/compare/hooksstate.mjs +19 -7
- package/src/compare/learn.mjs +2 -2
- package/src/compare/row.mjs +3 -3
- package/src/decisions/index.mjs +1 -1
- package/src/distill/distill.mjs +2 -2
- package/src/distribute/release.mjs +142 -0
- package/src/graph/load.mjs +9 -8
- package/src/hooks/dispatcher.mjs +3 -2
- package/src/hooks/shim.mjs +11 -6
- package/src/index/govern.mjs +1 -1
- package/src/index/map.mjs +1 -1
- package/src/index/persist.mjs +2 -2
- package/src/index/store.mjs +50 -20
- package/src/ledger/index.mjs +2 -2
- package/src/local/client.mjs +22 -8
- package/src/local/config.mjs +17 -0
- package/src/locate.mjs +22 -1
- package/src/mcp/prompts.mjs +118 -0
- package/src/mcp/resources.mjs +96 -22
- package/src/mcp/server.mjs +33 -6
- package/src/mcp/tools.mjs +12 -12
- package/src/measure/session.mjs +1 -1
- package/src/memory/decide.mjs +1 -1
- package/src/memory/lessons.mjs +14 -14
- package/src/name.mjs +11 -1
- package/src/package-version.mjs +12 -10
- package/src/packs/manage.mjs +563 -0
- package/src/packs.mjs +15 -5
- package/src/plugins.mjs +164 -0
- package/src/projection/loaders.mjs +84 -10
- package/src/projection/manifest.mjs +4 -4
- package/src/projection/project.mjs +3 -3
- package/src/projection/rules.mjs +24 -4
- package/src/projection/surfaces.mjs +20 -2
- package/src/review/critic.mjs +4 -4
- package/src/toolwords.mjs +8 -5
- package/src/tsconfig.mjs +3 -3
- package/src/upgrade/plan.mjs +1 -1
- /package/bin/{simt-dev.mjs → simt.mjs} +0 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Two prompt templates for the two jobs this server exists for.
|
|
2
|
+
//
|
|
3
|
+
// A prompt is a message template, not an answer. The server is read-only, so a prompt
|
|
4
|
+
// cannot implement anything either: what it CAN do is hand a calling agent a working set
|
|
5
|
+
// of instructions that name the tools to call and the order to call them in, which is the
|
|
6
|
+
// part a model otherwise has to discover by trial. The text therefore speaks ABOUT the
|
|
7
|
+
// tools and never runs them.
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* `simt-brief`: start a task from the pre-assembled packet.
|
|
11
|
+
*
|
|
12
|
+
* The brief tool is the entry point every other surface points at, so the one prompt a
|
|
13
|
+
* client needs before it knows the model is "start there". Required `task` degrades to a
|
|
14
|
+
* visible placeholder rather than an error: a client that ignored the `required` flag at
|
|
15
|
+
* least gets a message that says so instead of a broken stream, and the agent reading it
|
|
16
|
+
* knows to ask before calling anything.
|
|
17
|
+
*/
|
|
18
|
+
const SIMT_BRIEF = {
|
|
19
|
+
name: 'simt-brief',
|
|
20
|
+
title: 'Work a task from the simt brief',
|
|
21
|
+
description: 'Start a task from the pre-assembled brief packet, and work from what it selected and why.',
|
|
22
|
+
arguments: [
|
|
23
|
+
{ name: 'task', description: "the task, in the user's own words", required: true },
|
|
24
|
+
],
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* `simt-slice`: implement one slice of the working plan.
|
|
29
|
+
*
|
|
30
|
+
* `id` is optional and omission is the DEFAULT reading - the next slice whose
|
|
31
|
+
* dependencies are complete is what "implement the next slice" means, and a plan whose
|
|
32
|
+
* numbering a reader does not know yet has no id to give.
|
|
33
|
+
*/
|
|
34
|
+
const SIMT_SLICE = {
|
|
35
|
+
name: 'simt-slice',
|
|
36
|
+
title: 'Implement a plan slice',
|
|
37
|
+
description: 'Implement the next ready slice of the working plan, or the named one.',
|
|
38
|
+
arguments: [
|
|
39
|
+
{ name: 'id', description: 'slice id; omitted means the next ready one', required: false },
|
|
40
|
+
],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const PROMPTS = [SIMT_BRIEF, SIMT_SLICE];
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The prompt names as words, exported for the tool-vocabulary derivation in
|
|
47
|
+
* `src/toolwords.mjs`: a name this tool serves over MCP is this tool's own name, and the
|
|
48
|
+
* estate guard that reads that derivation would otherwise read `simt-brief` the way it
|
|
49
|
+
* reads a repository (ADR-080).
|
|
50
|
+
*/
|
|
51
|
+
export const PROMPT_WORDS = PROMPTS.map((prompt) => prompt.name);
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The prompt named, or null when there is no such name.
|
|
55
|
+
*
|
|
56
|
+
* `null`, not a throw: the caller decides what a missing prompt is worth (the server
|
|
57
|
+
* answers with the method error a client expects), and `getPrompt` stays a lookup.
|
|
58
|
+
*/
|
|
59
|
+
export function getPrompt(name, args = {}) {
|
|
60
|
+
const brief = name === SIMT_BRIEF.name;
|
|
61
|
+
const slice = name === SIMT_SLICE.name;
|
|
62
|
+
if (!brief && !slice) return null;
|
|
63
|
+
|
|
64
|
+
// Prompts degrade on missing arguments instead of failing: a template with an unfilled
|
|
65
|
+
// variable is still readable, and the agent it reaches is exactly the one that can ask
|
|
66
|
+
// the user what was meant.
|
|
67
|
+
const id = typeof args?.id === 'string' && args.id.length > 0 ? args.id : null;
|
|
68
|
+
const text = brief
|
|
69
|
+
? briefText(typeof args?.task === 'string' && args.task.trim() !== '' ? args.task : null)
|
|
70
|
+
: sliceText(id);
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
description: brief ? SIMT_BRIEF.description : SIMT_SLICE.description,
|
|
74
|
+
messages: [{ role: 'user', content: { type: 'text', text } }],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function briefText(task) {
|
|
79
|
+
const ask = task ?? '(none given - ask the user what to work before calling anything)';
|
|
80
|
+
return `Work the task through this repository's simt tools before touching code.
|
|
81
|
+
|
|
82
|
+
The task: ${ask}
|
|
83
|
+
|
|
84
|
+
1. Call the \`brief\` tool with this task. It returns the pre-assembled packet: the kind
|
|
85
|
+
of task, the commands that kind needs, the files the index ranks highest, the tests
|
|
86
|
+
covering them, the decisions governing them and the skills that apply.
|
|
87
|
+
2. Work from the packet. Where something you need was cut to fit the budget, the packet
|
|
88
|
+
names the tool that fetches it - call that tool instead of guessing.
|
|
89
|
+
3. Make the change, then prove it by running the packet's focused test before anything
|
|
90
|
+
broader.
|
|
91
|
+
|
|
92
|
+
Report what changed and the evidence, not a summary of the effort.
|
|
93
|
+
`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function sliceText(id) {
|
|
97
|
+
const ask = id
|
|
98
|
+
? `id ${id}`
|
|
99
|
+
: 'the next one whose dependencies are complete';
|
|
100
|
+
const lead = id
|
|
101
|
+
? `Call the \`plan_next\` tool with id "${id}" for the design and acceptance checks of exactly that slice.`
|
|
102
|
+
: 'Call the `plan_next` tool with no id: it returns the next queued slice whose dependencies are complete, with that slice\'s design section only.';
|
|
103
|
+
return `Implement one slice of this repository's plan, reading it through the simt tools.
|
|
104
|
+
|
|
105
|
+
The slice: ${ask}
|
|
106
|
+
|
|
107
|
+
1. ${lead}
|
|
108
|
+
2. Read \`simt://kernel\` first: the rules this repository packs for its stack, and the
|
|
109
|
+
ones it defers to \`simt rules show\`. They govern how you work here.
|
|
110
|
+
3. Implement in the design's language, writing the regression test its acceptance check
|
|
111
|
+
asks for as you go. A slice is done when its checks hold, not when the code runs.
|
|
112
|
+
4. Run the tests the design names, then the full suite - the last both in place and
|
|
113
|
+
from a copy at a path with no siblings, because a suite that only passes at home has
|
|
114
|
+
not passed.
|
|
115
|
+
5. Commit locally with a conventional subject and a body that records why, and report
|
|
116
|
+
the sha. Push only on explicit user approval.
|
|
117
|
+
`;
|
|
118
|
+
}
|
package/src/mcp/resources.mjs
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
// Rules and
|
|
1
|
+
// Rules, decisions, the kernel region and the next plan slice as MCP resources.
|
|
2
2
|
//
|
|
3
|
-
// The tools answer questions and the answers are arguments to the next call. These
|
|
4
|
-
// answer a different request: "what is the rule you would apply, in full"
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// The tools answer questions and the answers are arguments to the next call. These
|
|
4
|
+
// answer a different request: "what is the rule you would apply, in full", "what does an
|
|
5
|
+
// agent actually read here", "what does the plan say to do next". A client that reads
|
|
6
|
+
// them can hold the text rather than reconstructing it from a tool's summary, and a
|
|
7
|
+
// reader outside this estate gets the same bytes an adopting repository's agent reads -
|
|
7
8
|
// redacted, because a decision record cites its own rulings and not this repository's
|
|
8
9
|
// layout (ADR-055, ADR-061).
|
|
9
10
|
//
|
|
10
|
-
//
|
|
11
|
+
// All revisions agree on the shape here. They disagree about what a missing resource
|
|
11
12
|
// returns, and that difference is handled in the server, not here: this module answers
|
|
12
13
|
// with a resource or with nothing at all, and never with an empty `contents` array,
|
|
13
14
|
// which the newest revision forbids because it is indistinguishable from an empty rule.
|
|
@@ -16,9 +17,9 @@ import { readFile } from 'node:fs/promises';
|
|
|
16
17
|
import { join } from 'node:path';
|
|
17
18
|
|
|
18
19
|
import { DECISIONS_DIR, loadDecisions, redactEstate, redactInternalPaths } from '../decisions/index.mjs';
|
|
19
|
-
import {
|
|
20
|
+
import { planNext } from '../plan/index.mjs';
|
|
20
21
|
import { loadMdcRules } from '../projection/mdc.mjs';
|
|
21
|
-
import { loadRules } from '../projection/rules.mjs';
|
|
22
|
+
import { desiredRegion, loadRules } from '../projection/rules.mjs';
|
|
22
23
|
|
|
23
24
|
const RULES_SCHEME = 'simt://rules/';
|
|
24
25
|
const DECISIONS_SCHEME = 'simt://decisions/';
|
|
@@ -27,7 +28,9 @@ const DECISIONS_SCHEME = 'simt://decisions/';
|
|
|
27
28
|
* RFC 6570 templates, so a client can discover the shape without a hard-coded string.
|
|
28
29
|
*
|
|
29
30
|
* `id` and `adr` are the handles a reader already has: a gate names a rule id and a
|
|
30
|
-
* finding names an ADR, so the template's variable names match what those print.
|
|
31
|
+
* finding names an ADR, so the template's variable names match what those print. The two
|
|
32
|
+
* fixed resources (`simt://kernel`, `simt://plan/next`) have no handle to substitute and
|
|
33
|
+
* so are listed directly, not templated.
|
|
31
34
|
*/
|
|
32
35
|
export const RESOURCE_TEMPLATES = [
|
|
33
36
|
{
|
|
@@ -57,12 +60,21 @@ export const RESOURCE_TEMPLATES = [
|
|
|
57
60
|
*/
|
|
58
61
|
function parseResourceUri(uri) {
|
|
59
62
|
if (typeof uri !== 'string') return null;
|
|
63
|
+
// `simt://kernel` is one resource and not a collection: there is no handle to split,
|
|
64
|
+
// so it is matched whole, and anything that only looks like it (`simt://kernel/`)
|
|
65
|
+
// falls through to the refusals below.
|
|
66
|
+
if (uri === 'simt://kernel') return { collection: 'kernel' };
|
|
60
67
|
const [collection, ...rest] = uri.split('://');
|
|
61
68
|
if (collection !== 'simt') return null;
|
|
62
69
|
const [name, ...handle] = rest.join('://').split('/');
|
|
63
70
|
const id = handle.join('/');
|
|
64
71
|
if (!id || id === '.' || id === '..' || id.includes('..')) return null;
|
|
65
72
|
if (name === 'rules' && id.includes('/')) return null;
|
|
73
|
+
// `plan` holds one resource per working plan, and the handle says which: `next` is the
|
|
74
|
+
// plan's own vocabulary for the next ready slice. Any other handle names a resource
|
|
75
|
+
// that does not exist, so it is refused here rather than resolved against a reader's
|
|
76
|
+
// guess.
|
|
77
|
+
if (name === 'plan') return id === 'next' ? { collection: 'plan', id: 'next' } : null;
|
|
66
78
|
if (name !== 'rules' && name !== 'decisions') return null;
|
|
67
79
|
return { collection: name, id };
|
|
68
80
|
}
|
|
@@ -79,20 +91,67 @@ function resource(collection, id, title, bytes) {
|
|
|
79
91
|
}
|
|
80
92
|
|
|
81
93
|
/**
|
|
82
|
-
*
|
|
94
|
+
* The kernel region for a root, rendered on demand.
|
|
95
|
+
*
|
|
96
|
+
* `simt://kernel` promises the kernel's bytes by its name, whatever the repository
|
|
97
|
+
* declared: the declared mode governs what `sync` writes to disk, not what a read path
|
|
98
|
+
* may look at, and a read that re-consulted the manifest would answer a different
|
|
99
|
+
* question than the URI asked (ADR-002). `mode` is threaded through `desiredRegion` for
|
|
100
|
+
* exactly this caller.
|
|
101
|
+
*/
|
|
102
|
+
function kernelRegion(root) {
|
|
103
|
+
return desiredRegion(root, { mode: 'kernel' });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Every rule, every decision record, the kernel and - when one is ready - the next
|
|
108
|
+
* plan slice, one entry each.
|
|
83
109
|
*
|
|
84
110
|
* The list is built from what is on disk rather than from a hand-kept index, so it cannot
|
|
85
111
|
* describe a rule that was renamed or a record that was never written. Order is sorted:
|
|
86
112
|
* the newest revision forbids a set that varies per connection or as a side effect of
|
|
87
113
|
* another request, and a list built from a directory read is the only shape that satisfies
|
|
88
114
|
* that without a cache to invalidate.
|
|
115
|
+
*
|
|
116
|
+
* The slice is listed only when one is ready. A listing that promises a read the server
|
|
117
|
+
* then answers with "not found" is a listing that lies, and the plan's "no ready slice"
|
|
118
|
+
* cases are information about the plan, which the `plan_next` tool reports, not about a
|
|
119
|
+
* resource's text.
|
|
89
120
|
*/
|
|
90
|
-
export async function listResources() {
|
|
91
|
-
const [rules, mdc, { decisions }] = await Promise.all([
|
|
121
|
+
export async function listResources(root = process.cwd()) {
|
|
122
|
+
const [rules, mdc, { decisions }, region, plan] = await Promise.all([
|
|
123
|
+
loadRules(null),
|
|
124
|
+
loadMdcRules(),
|
|
125
|
+
loadDecisions({ all: true }),
|
|
126
|
+
kernelRegion(root),
|
|
127
|
+
planNext(root, null),
|
|
128
|
+
]);
|
|
92
129
|
// `size` is the bytes on disk rather than the bytes served: `read` redacts a decision
|
|
93
130
|
// before sending it, so the served length is not knowable without reading the file, and
|
|
94
|
-
// a size that disagrees with the payload is worse than a size that rounds.
|
|
131
|
+
// a size that disagrees with the payload is worse than a size that rounds. The kernel
|
|
132
|
+
// and the slice have no such gap - both serve exactly what they list - so their sizes
|
|
133
|
+
// are the served ones.
|
|
95
134
|
return [
|
|
135
|
+
{
|
|
136
|
+
uri: 'simt://kernel',
|
|
137
|
+
name: 'kernel',
|
|
138
|
+
title: 'The kernel region',
|
|
139
|
+
description: 'The rules this repository packs in kernel mode, with the deferrals named by their fetch command',
|
|
140
|
+
mimeType: 'text/markdown',
|
|
141
|
+
size: Buffer.byteLength(region.body),
|
|
142
|
+
},
|
|
143
|
+
...(plan.error || !plan.next
|
|
144
|
+
? []
|
|
145
|
+
: [
|
|
146
|
+
{
|
|
147
|
+
uri: 'simt://plan/next',
|
|
148
|
+
name: 'plan/next',
|
|
149
|
+
title: 'Next plan slice',
|
|
150
|
+
description: plan.next.title ?? 'the next slice whose dependencies are complete',
|
|
151
|
+
mimeType: 'text/markdown',
|
|
152
|
+
size: plan.next.sectionBytes,
|
|
153
|
+
},
|
|
154
|
+
]),
|
|
96
155
|
...[...rules, ...mdc]
|
|
97
156
|
.filter((rule) => rule.id !== null)
|
|
98
157
|
.map((rule) => resource('rules', rule.id, rule.title ?? rule.id, rule.bytes)),
|
|
@@ -107,10 +166,33 @@ export async function listResources() {
|
|
|
107
166
|
* a protocol error with a code that CHANGED between revisions, so the server decides the
|
|
108
167
|
* code and this module only declines to invent content.
|
|
109
168
|
*/
|
|
110
|
-
export async function readResource(uri) {
|
|
169
|
+
export async function readResource(uri, root = process.cwd()) {
|
|
111
170
|
const parsed = parseResourceUri(uri);
|
|
112
171
|
if (parsed === null) return null;
|
|
113
172
|
|
|
173
|
+
if (parsed.collection === 'kernel') {
|
|
174
|
+
const region = await kernelRegion(root);
|
|
175
|
+
return {
|
|
176
|
+
uri,
|
|
177
|
+
mimeType: 'text/markdown',
|
|
178
|
+
text: region.body,
|
|
179
|
+
// Listed size is read size here on purpose: the body is served exactly as it is
|
|
180
|
+
// rendered, so a client that budgets on the listing gets what it budgeted for.
|
|
181
|
+
bytes: Buffer.byteLength(region.body),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (parsed.collection === 'plan') {
|
|
186
|
+
const plan = await planNext(root, null);
|
|
187
|
+
if (plan.error || !plan.next) return null;
|
|
188
|
+
return {
|
|
189
|
+
uri,
|
|
190
|
+
mimeType: 'text/markdown',
|
|
191
|
+
text: plan.next.section,
|
|
192
|
+
bytes: plan.next.sectionBytes,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
114
196
|
if (parsed.collection === 'rules') {
|
|
115
197
|
const [rules, mdc] = await Promise.all([loadRules(null), loadMdcRules()]);
|
|
116
198
|
const rule = [...rules, ...mdc].find((candidate) => candidate.id === parsed.id);
|
|
@@ -147,11 +229,3 @@ function frontmatterOf(rule) {
|
|
|
147
229
|
function formatValue(value) {
|
|
148
230
|
return Array.isArray(value) ? `[${value.join(', ')}]` : String(value);
|
|
149
231
|
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* The remedy a client gets when it asks for something that is not here.
|
|
153
|
-
*
|
|
154
|
-
* A command, not a path, for the reason every remedy in this package is a command: the
|
|
155
|
-
* reader runs this tool, not this checkout.
|
|
156
|
-
*/
|
|
157
|
-
const MISSING_REMEDY = `run: ${CLI} rules list, or ${CLI} graph lint to see what this copy can read`;
|
package/src/mcp/server.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// The MCP transport: newline-delimited JSON-RPC 2.0 on stdio.
|
|
2
2
|
//
|
|
3
3
|
// This file is the PROTOCOL and nothing else. It knows about `initialize`,
|
|
4
|
-
// `tools/list`, `tools/call` and `ping`; it
|
|
5
|
-
// The tools
|
|
4
|
+
// `tools/list`, `tools/call`, `resources/*`, `prompts/*` and what remains of `ping`; it
|
|
5
|
+
// knows nothing about skills, rules or repos. The tools live in `src/mcp/tools.mjs`, the
|
|
6
|
+
// resources in `src/mcp/resources.mjs`, the prompts in `src/mcp/prompts.mjs`.
|
|
6
7
|
//
|
|
7
8
|
// Three decisions, each of which is a place where the obvious implementation is wrong:
|
|
8
9
|
//
|
|
@@ -21,6 +22,7 @@
|
|
|
21
22
|
|
|
22
23
|
import { createInterface } from 'node:readline';
|
|
23
24
|
import { CLI } from '../name.mjs';
|
|
25
|
+
import { getPrompt, PROMPTS } from './prompts.mjs';
|
|
24
26
|
import { RESOURCE_TEMPLATES, listResources, readResource } from './resources.mjs';
|
|
25
27
|
|
|
26
28
|
/**
|
|
@@ -96,12 +98,17 @@ const LIST_CACHE = { ttlMs: 3600000, cacheScope: 'private' };
|
|
|
96
98
|
* revision would hand it fields it did not ask for. So the version is read from the
|
|
97
99
|
* request when there is one and remembered only for the clients that need it.
|
|
98
100
|
*
|
|
101
|
+
* `root` is the repository the read surface answers for. The resources carry no argument
|
|
102
|
+
* channel in the specification - `resources/read` takes a URI and nothing else - so the
|
|
103
|
+
* caller names the root and the default is the process's cwd, which is what an editor's
|
|
104
|
+
* stdio server sits in.
|
|
105
|
+
*
|
|
99
106
|
* @param {object|null} request a parsed JSON-RPC message
|
|
100
107
|
* @param {object[]} tools
|
|
101
|
-
* @param {{packageVersion?: string, state?: {version: string|null}}} options
|
|
108
|
+
* @param {{packageVersion?: string, state?: {version: string|null}, root?: string}} options
|
|
102
109
|
* @returns {Promise<object|null>}
|
|
103
110
|
*/
|
|
104
|
-
export async function handle(request, tools, { packageVersion, state } = {}) {
|
|
111
|
+
export async function handle(request, tools, { packageVersion, state, root } = {}) {
|
|
105
112
|
if (request === null || typeof request !== 'object' || Array.isArray(request)) {
|
|
106
113
|
return error(null, RPC.REQUEST, 'request must be a JSON object');
|
|
107
114
|
}
|
|
@@ -210,7 +217,7 @@ export async function handle(request, tools, { packageVersion, state } = {}) {
|
|
|
210
217
|
|
|
211
218
|
case 'resources/list':
|
|
212
219
|
return reply(
|
|
213
|
-
cacheable(v) ? { resources: await listResources(), ...LIST_CACHE } : { resources: await listResources() },
|
|
220
|
+
cacheable(v) ? { resources: await listResources(root), ...LIST_CACHE } : { resources: await listResources(root) },
|
|
214
221
|
);
|
|
215
222
|
case 'resources/templates/list':
|
|
216
223
|
return reply(
|
|
@@ -225,12 +232,28 @@ export async function handle(request, tools, { packageVersion, state } = {}) {
|
|
|
225
232
|
// array as the answer, so "absent" can only be an error.
|
|
226
233
|
const code = atLeast(v, '2026-07-28') ? RPC.RESOURCE_NOT_FOUND : RPC.RESOURCE_NOT_FOUND_LEGACY;
|
|
227
234
|
const uri = request.params?.uri;
|
|
228
|
-
const found = await readResource(uri);
|
|
235
|
+
const found = await readResource(uri, root);
|
|
229
236
|
if (found === null) return error(id, code, `no such resource: ${String(uri)}`);
|
|
230
237
|
return reply(
|
|
231
238
|
cacheable(v) ? { contents: [found], ...LIST_CACHE } : { contents: [found] },
|
|
232
239
|
);
|
|
233
240
|
}
|
|
241
|
+
case 'prompts/list':
|
|
242
|
+
return reply(
|
|
243
|
+
cacheable(v) ? { prompts: PROMPTS, ...LIST_CACHE } : { prompts: PROMPTS },
|
|
244
|
+
);
|
|
245
|
+
case 'prompts/get': {
|
|
246
|
+
const name = params?.name;
|
|
247
|
+
// `getPrompt` returns null only for a prompt this server does not have: its
|
|
248
|
+
// argument failures degrade into the template text itself, because an unfilled
|
|
249
|
+
// variable is still readable and the agent it reaches can ask the user. A named
|
|
250
|
+
// prompt that does not exist is the client's mistake, so it is a protocol error -
|
|
251
|
+
// the same rule as tools/call, and for the same reason: there is no result shape
|
|
252
|
+
// that means absence.
|
|
253
|
+
const found = await getPrompt(name, params?.arguments ?? {});
|
|
254
|
+
if (found === null) return error(id, RPC.METHOD, `no prompt named "${String(name)}"; call prompts/list first`);
|
|
255
|
+
return reply(found);
|
|
256
|
+
}
|
|
234
257
|
default:
|
|
235
258
|
return isNotification ? null : error(id, RPC.METHOD, `unknown method "${method}"`);
|
|
236
259
|
}
|
|
@@ -256,6 +279,10 @@ function capabilities() {
|
|
|
256
279
|
// ever set: a capability gated on a flag nobody flips is the same promise a client
|
|
257
280
|
// waits on forever, only quieter.
|
|
258
281
|
resources: {},
|
|
282
|
+
// Two fixed templates, listed statically: there is nothing to subscribe to and
|
|
283
|
+
// nothing that changes without a release, so the same empty object makes the same
|
|
284
|
+
// promise here as above the resources.
|
|
285
|
+
prompts: {},
|
|
259
286
|
};
|
|
260
287
|
}
|
|
261
288
|
|
package/src/mcp/tools.mjs
CHANGED
|
@@ -407,7 +407,7 @@ export const TOOLS = [
|
|
|
407
407
|
outputSchema: output.doctor,
|
|
408
408
|
name: 'doctor',
|
|
409
409
|
description:
|
|
410
|
-
'Classify a repository:
|
|
410
|
+
'Classify a repository: stack tags, emitted facts with the file that proved each, and manifest presence. Call this first when the repo is unknown.',
|
|
411
411
|
inputSchema: rootArg,
|
|
412
412
|
async run(args) {
|
|
413
413
|
const root = rootOf(args);
|
|
@@ -430,7 +430,7 @@ export const TOOLS = [
|
|
|
430
430
|
outputSchema: output.skills_index,
|
|
431
431
|
name: 'skills_index',
|
|
432
432
|
description:
|
|
433
|
-
'
|
|
433
|
+
'Full skill catalogue: every skill with summary, size, stack tags and deferred flag. The fallback when brief returns nothing useful.',
|
|
434
434
|
inputSchema: noArgs,
|
|
435
435
|
async run(args) {
|
|
436
436
|
const { registry: reg, verified, workspace } = await registry(args);
|
|
@@ -451,7 +451,7 @@ export const TOOLS = [
|
|
|
451
451
|
outputSchema: output.skills_brief,
|
|
452
452
|
name: 'skills_brief',
|
|
453
453
|
description:
|
|
454
|
-
'THE entry point. Resolve the skill set for a task
|
|
454
|
+
'THE entry point. Resolve the skill set for a task: selected with why, deferred, and not selected with reason - the reasons let you override the resolver instead of guessing at it.',
|
|
455
455
|
inputSchema: {
|
|
456
456
|
type: 'object',
|
|
457
457
|
properties: {
|
|
@@ -517,7 +517,7 @@ export const TOOLS = [
|
|
|
517
517
|
{
|
|
518
518
|
outputSchema: output.skills_show,
|
|
519
519
|
name: 'skills_show',
|
|
520
|
-
description: 'One skill body
|
|
520
|
+
description: 'One skill body and the metadata needed to judge it: source path, the location it was read from, and its cited decisions.',
|
|
521
521
|
inputSchema: {
|
|
522
522
|
type: 'object',
|
|
523
523
|
properties: {
|
|
@@ -585,7 +585,7 @@ export const TOOLS = [
|
|
|
585
585
|
outputSchema: output.skills_why,
|
|
586
586
|
name: 'skills_why',
|
|
587
587
|
description:
|
|
588
|
-
'The signal chain for one skill in one repository: its score, the reasons
|
|
588
|
+
'The signal chain for one skill in one repository: its score, the reasons, whether the stack gate admits it, and what would select it.',
|
|
589
589
|
inputSchema: {
|
|
590
590
|
type: 'object',
|
|
591
591
|
properties: { name: { type: 'string' }, root: { type: 'string' }, task: { type: 'string' } },
|
|
@@ -619,7 +619,7 @@ export const TOOLS = [
|
|
|
619
619
|
outputSchema: output.rules_list,
|
|
620
620
|
name: 'rules_list',
|
|
621
621
|
description:
|
|
622
|
-
'The rule catalogue, and which rules a
|
|
622
|
+
'The rule catalogue, and which rules a repository actually receives. Pass root for the selection, not just the catalogue.',
|
|
623
623
|
inputSchema: {
|
|
624
624
|
type: 'object',
|
|
625
625
|
properties: { root: { type: 'string' } },
|
|
@@ -657,7 +657,7 @@ export const TOOLS = [
|
|
|
657
657
|
outputSchema: output.rules_why,
|
|
658
658
|
name: 'rules_why',
|
|
659
659
|
description:
|
|
660
|
-
'Why a rule exists
|
|
660
|
+
'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.',
|
|
661
661
|
inputSchema: {
|
|
662
662
|
type: 'object',
|
|
663
663
|
properties: { what: { type: 'string', description: 'a rule id or ADR-NNN' } },
|
|
@@ -713,7 +713,7 @@ export const TOOLS = [
|
|
|
713
713
|
outputSchema: output.check,
|
|
714
714
|
name: 'check',
|
|
715
715
|
description:
|
|
716
|
-
'The gate for one repository:
|
|
716
|
+
'The gate for one repository: region current, unmanaged, drifted or malformed, and rule files lacking an id. Read-only - it never repairs.',
|
|
717
717
|
inputSchema: rootArg,
|
|
718
718
|
async run(args) {
|
|
719
719
|
const root = rootOf(args);
|
|
@@ -730,7 +730,7 @@ export const TOOLS = [
|
|
|
730
730
|
outputSchema: output.compare,
|
|
731
731
|
name: 'compare',
|
|
732
732
|
description:
|
|
733
|
-
'The
|
|
733
|
+
'The workspace matrix: every repository with its adoption, region state, pinned SHA, hook routing and gates, plus the problems found.',
|
|
734
734
|
inputSchema: {
|
|
735
735
|
type: 'object',
|
|
736
736
|
properties: {
|
|
@@ -761,7 +761,7 @@ export const TOOLS = [
|
|
|
761
761
|
{
|
|
762
762
|
outputSchema: output.plan_next,
|
|
763
763
|
name: 'plan_next',
|
|
764
|
-
description:
|
|
764
|
+
description: "The next queued slice whose dependencies are complete; its design section only. Pass id for one slice whether or not it is ready.",
|
|
765
765
|
inputSchema: {
|
|
766
766
|
type: 'object',
|
|
767
767
|
properties: {
|
|
@@ -821,7 +821,7 @@ export const TOOLS = [
|
|
|
821
821
|
outputSchema: output.brief,
|
|
822
822
|
name: 'brief',
|
|
823
823
|
description:
|
|
824
|
-
'The packet for one task
|
|
824
|
+
'The packet for one task in prompt-sized bytes: task kind, needed commands, ranked files, covering tests, governing decisions, applicable skills. With no task it describes the repository.',
|
|
825
825
|
inputSchema: {
|
|
826
826
|
type: 'object',
|
|
827
827
|
properties: {
|
|
@@ -868,7 +868,7 @@ export const TOOLS = [
|
|
|
868
868
|
outputSchema: output.federation_query,
|
|
869
869
|
name: 'federation_query',
|
|
870
870
|
description:
|
|
871
|
-
'Ask a code-graph server
|
|
871
|
+
'Ask a declared code-graph server one structural question; the answer names the server that gave it. Starts only servers simt.json names; a repository declaring none is told so.',
|
|
872
872
|
inputSchema: {
|
|
873
873
|
type: 'object',
|
|
874
874
|
properties: {
|
package/src/measure/session.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import { resolve } from 'node:path';
|
|
|
11
11
|
import { tokens } from './context.mjs';
|
|
12
12
|
|
|
13
13
|
const RULE_PATH_RE =
|
|
14
|
-
/(?:^|\/)(?:AGENTS(?:\.override)?\.md|CLAUDE(?:\.local)?\.md|GEMINI\.md|SKILL\.md|\.cursor\/rules\/|\.claude\/rules\/|\.agents\/rules\/|\.github\/(?:copilot-instructions\.md|instructions\/)|\.simt
|
|
14
|
+
/(?:^|\/)(?:AGENTS(?:\.override)?\.md|CLAUDE(?:\.local)?\.md|GEMINI\.md|SKILL\.md|\.cursor\/rules\/|\.claude\/rules\/|\.agents\/rules\/|\.github\/(?:copilot-instructions\.md|instructions\/)|\.simt\/profiles\/)/;
|
|
15
15
|
|
|
16
16
|
const FILE_READ_TOOLS = new Set(['Read', 'read_file', 'NotebookRead', 'view_file', 'read']);
|
|
17
17
|
|
package/src/memory/decide.mjs
CHANGED
|
@@ -244,7 +244,7 @@ export async function diffAffectedPaths(root) {
|
|
|
244
244
|
const addLines = (stdout) => {
|
|
245
245
|
for (const line of String(stdout ?? '').split(/\r?\n/)) {
|
|
246
246
|
const trimmed = line.trim();
|
|
247
|
-
if (trimmed && !trimmed.startsWith('.simt
|
|
247
|
+
if (trimmed && !trimmed.startsWith('.simt/')) paths.add(trimmed);
|
|
248
248
|
}
|
|
249
249
|
};
|
|
250
250
|
for (const args of [
|
package/src/memory/lessons.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Deterministic lesson candidate ledger and promotion to `docs/lessons/NNNN-<slug>.md` (plan 0003 R32).
|
|
2
2
|
//
|
|
3
|
-
// Four deterministic candidate sources write to `.simt
|
|
3
|
+
// Four deterministic candidate sources write to `.simt/lessons.jsonl`:
|
|
4
4
|
// 1. `fail-then-fix`: a gate or test that fails and then passes in one session,
|
|
5
5
|
// recorded as `error signature -> fix diff` (where the signature is the sha256
|
|
6
6
|
// hash of the normalised first error line);
|
|
@@ -23,7 +23,7 @@ import { splitFrontmatter } from './decide.mjs';
|
|
|
23
23
|
|
|
24
24
|
const exec = promisify(execFile);
|
|
25
25
|
|
|
26
|
-
export const LESSONS_FILE = '.simt
|
|
26
|
+
export const LESSONS_FILE = '.simt/lessons.jsonl';
|
|
27
27
|
export const LESSONS_DIR = 'docs/lessons';
|
|
28
28
|
export const LESSON_KEYS = new Set(['simt-trigger', 'simt-evidence', 'simt-expires']);
|
|
29
29
|
const DEFAULT_EXPIRES = '2027-04-01';
|
|
@@ -137,7 +137,7 @@ function slugify(text) {
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
/**
|
|
140
|
-
* Read all candidate records from `.simt
|
|
140
|
+
* Read all candidate records from `.simt/lessons.jsonl`.
|
|
141
141
|
*
|
|
142
142
|
* @param {string} root
|
|
143
143
|
* @returns {Promise<{candidates: Array<object>, errors: string[]}>}
|
|
@@ -181,7 +181,7 @@ function sameEvidence(a = [], b = []) {
|
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
/**
|
|
184
|
-
* Append one deterministic candidate to `.simt
|
|
184
|
+
* Append one deterministic candidate to `.simt/lessons.jsonl`, deduplicating by
|
|
185
185
|
* `(kind, signature, evidence)`.
|
|
186
186
|
*
|
|
187
187
|
* @param {string} root
|
|
@@ -239,11 +239,11 @@ async function diffBetweenTrees(root, fromRef, toRef) {
|
|
|
239
239
|
}
|
|
240
240
|
try {
|
|
241
241
|
const [namesOut, diffOut] = await Promise.all([
|
|
242
|
-
exec('git', ['diff', '--name-only', fromRef, toRef, '--', '.', ':(exclude).simt
|
|
242
|
+
exec('git', ['diff', '--name-only', fromRef, toRef, '--', '.', ':(exclude).simt'], {
|
|
243
243
|
cwd: root,
|
|
244
244
|
maxBuffer: 8 << 20,
|
|
245
245
|
}),
|
|
246
|
-
exec('git', ['diff', '--unified=3', fromRef, toRef, '--', '.', ':(exclude).simt
|
|
246
|
+
exec('git', ['diff', '--unified=3', fromRef, toRef, '--', '.', ':(exclude).simt'], {
|
|
247
247
|
cwd: root,
|
|
248
248
|
maxBuffer: 8 << 20,
|
|
249
249
|
}),
|
|
@@ -295,8 +295,8 @@ export async function recordFailThenFix(root, { failEntry, passEntry, failOutput
|
|
|
295
295
|
}
|
|
296
296
|
|
|
297
297
|
/**
|
|
298
|
-
* Scan `.simt
|
|
299
|
-
* ensure each is recorded in `.simt
|
|
298
|
+
* Scan `.simt/ledger.jsonl` for any fail-then-fix sequences in the session and
|
|
299
|
+
* ensure each is recorded in `.simt/lessons.jsonl`.
|
|
300
300
|
*
|
|
301
301
|
* @param {string} root
|
|
302
302
|
* @param {Array<object>} [entries]
|
|
@@ -306,7 +306,7 @@ export async function captureFromLedger(root, entries = null) {
|
|
|
306
306
|
let list = entries;
|
|
307
307
|
if (!Array.isArray(list)) {
|
|
308
308
|
try {
|
|
309
|
-
const raw = await readFile(join(root, '.simt
|
|
309
|
+
const raw = await readFile(join(root, '.simt', 'ledger.jsonl'), 'utf8');
|
|
310
310
|
list = raw
|
|
311
311
|
.split('\n')
|
|
312
312
|
.map((s) => s.trim())
|
|
@@ -339,7 +339,7 @@ export async function captureFromLedger(root, entries = null) {
|
|
|
339
339
|
}
|
|
340
340
|
|
|
341
341
|
/**
|
|
342
|
-
* Capture reverted commits from git history into `.simt
|
|
342
|
+
* Capture reverted commits from git history into `.simt/lessons.jsonl`.
|
|
343
343
|
*
|
|
344
344
|
* @param {string} root
|
|
345
345
|
* @param {{limit?: number}} [options]
|
|
@@ -408,7 +408,7 @@ export async function captureRevertedCommits(root, options = {}) {
|
|
|
408
408
|
}
|
|
409
409
|
|
|
410
410
|
/**
|
|
411
|
-
* Record an ADR-008 `BLOCK` review finding in `.simt
|
|
411
|
+
* Record an ADR-008 `BLOCK` review finding in `.simt/lessons.jsonl`.
|
|
412
412
|
*
|
|
413
413
|
* @param {string} root
|
|
414
414
|
* @param {{summary: string, paths?: string[], evidence?: string[], fixDiff?: string, keywords?: string[]}} finding
|
|
@@ -437,7 +437,7 @@ export async function recordReviewBlock(root, finding) {
|
|
|
437
437
|
}
|
|
438
438
|
|
|
439
439
|
/**
|
|
440
|
-
* Record an agent-time hook denial (R15) in `.simt
|
|
440
|
+
* Record an agent-time hook denial (R15) in `.simt/lessons.jsonl`.
|
|
441
441
|
*
|
|
442
442
|
* @param {string} root
|
|
443
443
|
* @param {{harness?: string, event?: string, policies?: string[], messages?: string[], command?: string, writes?: string[]}} denial
|
|
@@ -612,7 +612,7 @@ async function nextLessonNumber(root) {
|
|
|
612
612
|
}
|
|
613
613
|
|
|
614
614
|
/**
|
|
615
|
-
* Promote a lesson candidate from `.simt
|
|
615
|
+
* Promote a lesson candidate from `.simt/lessons.jsonl` to `docs/lessons/NNNN-<slug>.md`.
|
|
616
616
|
*
|
|
617
617
|
* @param {string} root
|
|
618
618
|
* @param {{id?: string, apply?: boolean, paths?: string[], expires?: string, title?: string}} [options]
|
|
@@ -622,7 +622,7 @@ export async function promoteLesson(root, options = {}) {
|
|
|
622
622
|
await captureFromLedger(root);
|
|
623
623
|
const { candidates } = await readLessonCandidates(root);
|
|
624
624
|
if (candidates.length === 0) {
|
|
625
|
-
return { ok: false, error: 'no lesson candidates in .simt
|
|
625
|
+
return { ok: false, error: 'no lesson candidates in .simt/lessons.jsonl' };
|
|
626
626
|
}
|
|
627
627
|
|
|
628
628
|
let target = null;
|
package/src/name.mjs
CHANGED
|
@@ -45,6 +45,16 @@ export const MARKER_PREFIX_LEGACY = 'simt-dev';
|
|
|
45
45
|
/** The config directory on the adopting engineer's machine. LOCKED, same argument. */
|
|
46
46
|
export const CONFIG_DIR = PROJECT;
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* The per-repository state directory: the index, ledger, benchmark runs and local-model
|
|
50
|
+
* proposals a repository writes for itself. Renamed from `.simt-dev` to `.simt`; the
|
|
51
|
+
* rename is unreleased, so no repository carries the old spelling as a name this tool
|
|
52
|
+
* cannot rewrite. Deliberately not derived from CONFIG_DIR: that is the user-level
|
|
53
|
+
* config directory, and a derivation would let two different on-disk names agree by
|
|
54
|
+
* accident rather than by decision.
|
|
55
|
+
*/
|
|
56
|
+
export const STATE_DIR = '.simt';
|
|
57
|
+
|
|
48
58
|
/**
|
|
49
59
|
* The gate this tool writes into a repository. LOCKED: it is a path in `.github`, and
|
|
50
60
|
* a repo that renames the file silently stops running the gate it was adopted with.
|
|
@@ -92,4 +102,4 @@ export const CLI = 'simt';
|
|
|
92
102
|
export const PACKAGE_NAME = PROJECT;
|
|
93
103
|
|
|
94
104
|
/** The entry point, relative to the package root. LOCKED: `bin` in package.json. */
|
|
95
|
-
export const BIN_REL = 'bin/simt
|
|
105
|
+
export const BIN_REL = 'bin/simt.mjs';
|