simt-dev 0.4.0 → 0.6.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.
- package/CHANGELOG.md +170 -0
- package/README.md +300 -82
- 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/package.json +8 -4
- package/src/advice/advice.mjs +229 -0
- package/src/agenthooks/render.mjs +87 -21
- package/src/argv.mjs +25 -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 +4 -18
- package/src/bench/tasks.mjs +36 -0
- package/src/bench/variants.mjs +1 -3
- package/src/brief/assemble.mjs +24 -4
- package/src/brief/packet.mjs +4 -1
- package/src/ci/dependabot.mjs +1 -1
- package/src/ci/workflow.mjs +40 -12
- package/src/commands/agenthook.mjs +6 -2
- package/src/commands/bench.mjs +35 -3
- package/src/commands/ci.mjs +1 -1
- package/src/commands/compare.mjs +1 -1
- package/src/commands/distill.mjs +36 -0
- 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 +54 -4
- package/src/commands/hooks.mjs +1 -1
- package/src/commands/index.mjs +29 -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/review.mjs +65 -0
- package/src/commands/rules.mjs +1 -1
- package/src/commands/skills.mjs +3 -34
- 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 +6 -3
- 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 +246 -0
- package/src/distill/engine.mjs +187 -0
- 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 +52 -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/resolve/body.mjs +46 -0
- package/src/review/critic.mjs +509 -0
- 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
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';
|
package/src/package-version.mjs
CHANGED
|
@@ -3,14 +3,16 @@
|
|
|
3
3
|
// Three distinct things that must not be confused:
|
|
4
4
|
// - packageVersion() is the version of the binary that is RUNNING. It is read from
|
|
5
5
|
// the package this code lives in, so it is correct whether the binary was resolved
|
|
6
|
-
// from node_modules, a global install, or a
|
|
6
|
+
// from node_modules, the release binary in `.simt/bin`, a global install, or a
|
|
7
|
+
// source checkout.
|
|
7
8
|
// - hasNpm(root) is the KIND of repository: does it have a package.json? That decides
|
|
8
|
-
// how it obtains simt-dev at all — pinned as a devDependency where one exists,
|
|
9
|
-
//
|
|
9
|
+
// how it obtains simt-dev at all — pinned as a devDependency where one exists, the
|
|
10
|
+
// release binary into the repository where there is no node_modules to pin in
|
|
11
|
+
// (ADR-098).
|
|
10
12
|
// - the pin is the version a repository recorded when it adopted simt-dev, stored in
|
|
11
|
-
// the manifest's projection.version. It is what
|
|
12
|
-
// pins the node_modules copy for us, but nothing pins a
|
|
13
|
-
// compares the running version against the pin and refuses on a mismatch.
|
|
13
|
+
// the manifest's projection.version. It is what an install outside node_modules
|
|
14
|
+
// must match: npm pins the node_modules copy for us, but nothing pins a binary, so
|
|
15
|
+
// the gate compares the running version against the pin and refuses on a mismatch.
|
|
14
16
|
|
|
15
17
|
import { readFileSync } from 'node:fs';
|
|
16
18
|
import { stat } from 'node:fs/promises';
|
|
@@ -38,7 +40,7 @@ export function packageVersion() {
|
|
|
38
40
|
|
|
39
41
|
/**
|
|
40
42
|
* Whether a repository has a package.json, i.e. whether it pins simt-dev as a
|
|
41
|
-
* devDependency (npm) or must
|
|
43
|
+
* devDependency (npm) or must download the release binary (ADR-098). Async because the render
|
|
42
44
|
* call sites that feed it (init, hooks install, the compare stale check) are all async,
|
|
43
45
|
* and a missing file is an answer (false), not an error.
|
|
44
46
|
*/
|
|
@@ -56,9 +58,9 @@ export async function hasNpm(root) {
|
|
|
56
58
|
* simt-dev. Returns null when there is nothing to refuse, otherwise a message that
|
|
57
59
|
* names both versions and the one command that fixes it.
|
|
58
60
|
*
|
|
59
|
-
* This is the gate's fail-closed guard against a stale
|
|
60
|
-
* without npm,
|
|
61
|
-
* it should be is the pin. A node_modules copy is pinned by npm itself, so its mismatch
|
|
61
|
+
* This is the gate's fail-closed guard against a stale binary: for a repository
|
|
62
|
+
* without npm, the copy is the release binary in `.simt/bin` (ADR-098) and the only
|
|
63
|
+
* thing that knows which version it should be is the pin. A node_modules copy is pinned by npm itself, so its mismatch
|
|
62
64
|
* is a stale manifest, which `upgrade` refreshes — the remedy is the same either way.
|
|
63
65
|
*
|
|
64
66
|
* A manifest with no recorded pin (adopted before the field existed) is skipped, not
|