simt-dev 0.3.1 → 0.5.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 +157 -0
- package/README.md +271 -62
- package/package.json +6 -2
- package/rules/core.md +1 -1
- package/src/advice/advice.mjs +229 -0
- package/src/agenthooks/policy.mjs +10 -2
- package/src/agenthooks/prompt.mjs +81 -0
- package/src/agenthooks/render.mjs +138 -17
- package/src/argv.mjs +28 -1
- package/src/bench/grade.mjs +3 -0
- package/src/bench/run.mjs +88 -54
- package/src/bench/tasks.mjs +38 -0
- package/src/bench/variants.mjs +456 -0
- package/src/brief/assemble.mjs +368 -0
- package/src/brief/kind.mjs +134 -0
- package/src/brief/packet.mjs +200 -0
- package/src/commands/agenthook.mjs +84 -5
- package/src/commands/bench.mjs +72 -15
- package/src/commands/brief.mjs +78 -0
- package/src/commands/compare.mjs +10 -2
- package/src/commands/decide.mjs +116 -0
- package/src/commands/distill.mjs +36 -0
- package/src/commands/help.mjs +62 -3
- package/src/commands/index.mjs +29 -1
- package/src/commands/lessons.mjs +127 -0
- package/src/commands/local.mjs +97 -0
- package/src/commands/map.mjs +42 -7
- package/src/commands/repoindex.mjs +4 -1
- package/src/commands/review.mjs +65 -0
- package/src/commands/rules.mjs +17 -2
- package/src/commands/skills.mjs +5 -50
- package/src/commands/skillscompile.mjs +255 -0
- package/src/commands/sync.mjs +15 -0
- package/src/commands/tests.mjs +2 -49
- package/src/commands/why.mjs +23 -25
- package/src/compare/learn.mjs +680 -0
- package/src/decisions/index.mjs +116 -65
- package/src/distill/distill.mjs +246 -0
- package/src/distill/engine.mjs +187 -0
- package/src/federation/client.mjs +188 -0
- package/src/federation/config.mjs +134 -0
- package/src/federation/query.mjs +48 -0
- package/src/graph/compile.mjs +475 -0
- package/src/index/gate.mjs +1 -13
- package/src/index/govern.mjs +23 -11
- package/src/index/map.mjs +251 -17
- package/src/index/persist.mjs +74 -7
- package/src/index/store.mjs +222 -8
- package/src/kernel/variants.mjs +353 -0
- package/src/ledger/index.mjs +36 -1
- package/src/local/client.mjs +482 -0
- package/src/local/config.mjs +269 -0
- package/src/mcp/tools.mjs +138 -1
- package/src/memory/decide.mjs +462 -0
- package/src/memory/lessons.mjs +858 -0
- package/src/projection/lock.mjs +39 -7
- package/src/projection/rules.mjs +32 -8
- package/src/resolve/body.mjs +46 -0
- package/src/review/critic.mjs +509 -0
- package/src/toolwords.mjs +6 -0
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
// Skill compiler, progressive-disclosure splitter, and trigger report (plan 0003 R30).
|
|
2
|
+
//
|
|
3
|
+
// 1. Compiles a skill into the Agent Skills layout (`<name>/SKILL.md` + `references/*.md`)
|
|
4
|
+
// with frontmatter that passes `checkSkill` (review 0002 S44, ADR-069).
|
|
5
|
+
// 2. Flags every skill body over 5,000 tokens (20,000 bytes at 4 B/token) with a
|
|
6
|
+
// deterministic split plan, and splits markdown bodies by section into `references/`
|
|
7
|
+
// so `SKILL.md` stays within the 5,000-token and 500-line budget.
|
|
8
|
+
// 3. Reads R02 transcripts and run records to determine whether the agent opened a skill
|
|
9
|
+
// when the task's gold patch touched its declared `paths`, reporting trigger recall
|
|
10
|
+
// and precision per skill.
|
|
11
|
+
// 4. Builds offline, proposal-only description improvements with provenance (`model`,
|
|
12
|
+
// `promptHash`, `digest`) so model output never lands directly in committed text (P2).
|
|
13
|
+
|
|
14
|
+
import { createHash } from 'node:crypto';
|
|
15
|
+
import { checkSkill, nameProblems, readFrontmatter } from './spec.mjs';
|
|
16
|
+
import { matchesGlob } from '../resolve/resolve.mjs';
|
|
17
|
+
|
|
18
|
+
export const BODY_TOKEN_BUDGET = 5000;
|
|
19
|
+
export const BYTES_PER_TOKEN = 4;
|
|
20
|
+
export const BODY_BYTE_BUDGET = BODY_TOKEN_BUDGET * BYTES_PER_TOKEN;
|
|
21
|
+
export const BODY_LINE_BUDGET = 500;
|
|
22
|
+
|
|
23
|
+
const TARGET_MAIN_BYTES = 12_000;
|
|
24
|
+
const TARGET_MAIN_LINES = 350;
|
|
25
|
+
const TARGET_REF_BYTES = 16_000;
|
|
26
|
+
const SPEC_KEYS = new Set(['name', 'description', 'license', 'compatibility', 'metadata', 'allowed-tools']);
|
|
27
|
+
|
|
28
|
+
function sha256Tag(text) {
|
|
29
|
+
return `sha256-${createHash('sha256').update(String(text ?? ''), 'utf8').digest('hex').slice(0, 16)}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function slugify(title, index) {
|
|
33
|
+
const base = String(title ?? '')
|
|
34
|
+
.toLowerCase()
|
|
35
|
+
.replace(/`+/g, '')
|
|
36
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
37
|
+
.replace(/^-+|-+$/g, '')
|
|
38
|
+
.replace(/-{2,}/g, '-')
|
|
39
|
+
.slice(0, 48);
|
|
40
|
+
const num = String(index + 1).padStart(2, '0');
|
|
41
|
+
return base ? `${num}-${base}` : `${num}-section`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Build a conforming Agent Skills description (30..1024 chars, stating what + when). */
|
|
45
|
+
export function buildConformingDescription(skill = {}, rawDescription = '') {
|
|
46
|
+
const clean = String(rawDescription ?? '').replace(/\s+/g, ' ').trim();
|
|
47
|
+
const summary = String(skill.summary ?? '').replace(/\s+/g, ' ').trim();
|
|
48
|
+
const paths = Array.isArray(skill.paths) && skill.paths.length > 0 ? skill.paths.slice(0, 4).join(', ') : '';
|
|
49
|
+
const keywords = Array.isArray(skill.keywords) && skill.keywords.length > 0 ? skill.keywords.slice(0, 5).join(', ') : '';
|
|
50
|
+
|
|
51
|
+
const whenClause = paths
|
|
52
|
+
? `Use when touching ${paths}${keywords ? ` or working with ${keywords}` : ''}.`
|
|
53
|
+
: keywords
|
|
54
|
+
? `Use when working with ${keywords}.`
|
|
55
|
+
: 'Use when a task matches this capability.';
|
|
56
|
+
|
|
57
|
+
let desc = clean || summary || `Instructions for ${skill.name ?? 'this skill'}.`;
|
|
58
|
+
if (!desc.endsWith('.')) desc += '.';
|
|
59
|
+
if (desc.length < 30 || !/\buse when\b/i.test(desc)) {
|
|
60
|
+
const combined = `${desc} ${whenClause}`.replace(/\s+/g, ' ').trim();
|
|
61
|
+
if (combined.length <= 1024) desc = combined;
|
|
62
|
+
}
|
|
63
|
+
if (desc.length < 30) desc = `${desc} ${whenClause}`.trim();
|
|
64
|
+
if (desc.length > 1024) desc = `${desc.slice(0, 1021).trimEnd()}...`;
|
|
65
|
+
return desc;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function splitMarkdownSections(bodyText) {
|
|
69
|
+
const lines = String(bodyText ?? '').split(/\r?\n/);
|
|
70
|
+
const preamble = [];
|
|
71
|
+
const sections = [];
|
|
72
|
+
let current = null;
|
|
73
|
+
let inFence = false;
|
|
74
|
+
|
|
75
|
+
for (const line of lines) {
|
|
76
|
+
if (line.trim().startsWith('```')) inFence = !inFence;
|
|
77
|
+
const h2 = !inFence ? /^##\s+(.+?)\s*$/.exec(line) : null;
|
|
78
|
+
if (h2) {
|
|
79
|
+
if (current) sections.push(current);
|
|
80
|
+
current = { title: h2[1].trim(), lines: [line] };
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (current) current.lines.push(line);
|
|
84
|
+
else preamble.push(line);
|
|
85
|
+
}
|
|
86
|
+
if (current) sections.push(current);
|
|
87
|
+
|
|
88
|
+
if (sections.length <= 1 && lines.length > BODY_LINE_BUDGET) {
|
|
89
|
+
const chunkSize = 200;
|
|
90
|
+
const bodyLines = sections.length === 1 ? [...preamble, ...sections[0].lines] : preamble;
|
|
91
|
+
const intro = bodyLines.slice(0, 80);
|
|
92
|
+
const rest = bodyLines.slice(80);
|
|
93
|
+
const chunked = [];
|
|
94
|
+
for (let i = 0; i < rest.length; i += chunkSize) {
|
|
95
|
+
const partNum = Math.floor(i / chunkSize) + 1;
|
|
96
|
+
chunked.push({
|
|
97
|
+
title: `Reference Part ${partNum}`,
|
|
98
|
+
lines: [`## Reference Part ${partNum}`, '', ...rest.slice(i, i + chunkSize)],
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return { preamble: intro.join('\n').trim(), sections: chunked };
|
|
102
|
+
}
|
|
103
|
+
return { preamble: preamble.join('\n').trim(), sections };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Compute a deterministic progressive-disclosure split plan for a skill.
|
|
108
|
+
* Every skill over 5,000 tokens (20,000 bytes) or 500 lines is flagged with a split plan.
|
|
109
|
+
*/
|
|
110
|
+
export function planSkillSplit(skill = {}, bodyText = null) {
|
|
111
|
+
const hasBody = typeof bodyText === 'string' && bodyText.length > 0;
|
|
112
|
+
const bodyBytes = hasBody ? Buffer.byteLength(bodyText, 'utf8') : 0;
|
|
113
|
+
const totalBytes = Math.max(Number(skill.bytes ?? 0), bodyBytes);
|
|
114
|
+
const totalTokens = Math.ceil(totalBytes / BYTES_PER_TOKEN);
|
|
115
|
+
const totalLines = hasBody ? bodyText.split(/\r?\n/).length : Math.ceil(totalBytes / 60);
|
|
116
|
+
const overTokens = totalTokens > BODY_TOKEN_BUDGET;
|
|
117
|
+
const flagged = overTokens;
|
|
118
|
+
|
|
119
|
+
if (!flagged) {
|
|
120
|
+
return {
|
|
121
|
+
flagged: false,
|
|
122
|
+
source: hasBody ? 'body' : 'registry',
|
|
123
|
+
reason: null,
|
|
124
|
+
totalBytes,
|
|
125
|
+
totalTokens,
|
|
126
|
+
totalLines,
|
|
127
|
+
mainBytes: bodyBytes || totalBytes,
|
|
128
|
+
mainTokens: Math.ceil((bodyBytes || totalBytes) / BYTES_PER_TOKEN),
|
|
129
|
+
references: [],
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const reason = `${totalTokens} tokens > ${BODY_TOKEN_BUDGET} token budget`;
|
|
134
|
+
|
|
135
|
+
if (!hasBody || bodyBytes <= BODY_BYTE_BUDGET) {
|
|
136
|
+
const mainBytes = hasBody ? bodyBytes : Math.min(totalBytes, TARGET_MAIN_BYTES);
|
|
137
|
+
const mainTokens = Math.ceil(mainBytes / BYTES_PER_TOKEN);
|
|
138
|
+
const remainingBytes = Math.max(1024, totalBytes - mainBytes);
|
|
139
|
+
const topics = [...(skill.capabilities ?? []), ...(skill.keywords ?? []).slice(0, 4)];
|
|
140
|
+
const refCount = Math.max(1, Math.ceil(remainingBytes / TARGET_REF_BYTES));
|
|
141
|
+
const references = [];
|
|
142
|
+
let left = remainingBytes;
|
|
143
|
+
for (let i = 0; i < refCount; i += 1) {
|
|
144
|
+
const share = i === refCount - 1 ? left : Math.min(left, Math.ceil(remainingBytes / refCount));
|
|
145
|
+
left -= share;
|
|
146
|
+
const topic = topics[i] ?? `reference-${i + 1}`;
|
|
147
|
+
references.push({
|
|
148
|
+
path: `references/${slugify(topic, i)}.md`,
|
|
149
|
+
title: `Reference: ${topic}`,
|
|
150
|
+
bytes: share,
|
|
151
|
+
tokens: Math.ceil(share / BYTES_PER_TOKEN),
|
|
152
|
+
lines: Math.max(1, Math.ceil(share / 60)),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
flagged: true,
|
|
157
|
+
source: hasBody ? 'body' : 'registry',
|
|
158
|
+
reason,
|
|
159
|
+
totalBytes,
|
|
160
|
+
totalTokens,
|
|
161
|
+
totalLines,
|
|
162
|
+
mainBytes,
|
|
163
|
+
mainTokens,
|
|
164
|
+
references,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const parsed = readFrontmatter(bodyText);
|
|
169
|
+
const { preamble, sections } = splitMarkdownSections(parsed.body);
|
|
170
|
+
const keptSections = [];
|
|
171
|
+
const movedSections = [];
|
|
172
|
+
let runningBytes = Buffer.byteLength(preamble, 'utf8') + 512;
|
|
173
|
+
let runningLines = preamble.split(/\r?\n/).length + 25;
|
|
174
|
+
|
|
175
|
+
for (const section of sections) {
|
|
176
|
+
const secBytes = Buffer.byteLength(section.lines.join('\n').trim(), 'utf8');
|
|
177
|
+
const secLines = section.lines.length;
|
|
178
|
+
if (
|
|
179
|
+
movedSections.length === 0 &&
|
|
180
|
+
runningBytes + secBytes <= TARGET_MAIN_BYTES &&
|
|
181
|
+
runningLines + secLines <= TARGET_MAIN_LINES &&
|
|
182
|
+
sections.length > 1
|
|
183
|
+
) {
|
|
184
|
+
keptSections.push(section);
|
|
185
|
+
runningBytes += secBytes;
|
|
186
|
+
runningLines += secLines;
|
|
187
|
+
} else {
|
|
188
|
+
movedSections.push(section);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (movedSections.length === 0 && sections.length > 0) movedSections.push(keptSections.pop());
|
|
192
|
+
|
|
193
|
+
const references = movedSections.map((sec, idx) => {
|
|
194
|
+
const content = `# ${sec.title}\n\n${sec.lines.slice(1).join('\n').trim()}\n`;
|
|
195
|
+
const bytes = Buffer.byteLength(content, 'utf8');
|
|
196
|
+
return {
|
|
197
|
+
path: `references/${slugify(sec.title, idx)}.md`,
|
|
198
|
+
title: sec.title,
|
|
199
|
+
bytes,
|
|
200
|
+
tokens: Math.ceil(bytes / BYTES_PER_TOKEN),
|
|
201
|
+
lines: content.split('\n').length,
|
|
202
|
+
content,
|
|
203
|
+
};
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const keptBody = [preamble, ...keptSections.map((s) => s.lines.join('\n').trim())].filter(Boolean).join('\n\n');
|
|
207
|
+
const refIndexLines = [
|
|
208
|
+
'## References',
|
|
209
|
+
'',
|
|
210
|
+
'Progressive-disclosure reference files (load on demand when needed):',
|
|
211
|
+
...references.map((r) => `- [${r.title}](${r.path}) (~${r.tokens} tokens, ${r.bytes} bytes)`),
|
|
212
|
+
];
|
|
213
|
+
const compiledBody = `${keptBody}\n\n${refIndexLines.join('\n')}\n`;
|
|
214
|
+
const mainBytes = Buffer.byteLength(compiledBody, 'utf8');
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
flagged: true,
|
|
218
|
+
source: 'body',
|
|
219
|
+
reason,
|
|
220
|
+
totalBytes,
|
|
221
|
+
totalTokens,
|
|
222
|
+
totalLines,
|
|
223
|
+
mainBytes,
|
|
224
|
+
mainTokens: Math.ceil(mainBytes / BYTES_PER_TOKEN),
|
|
225
|
+
compiledBody,
|
|
226
|
+
references,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function serializeFrontmatter({ name, description, license, compatibility, allowedTools, metadata }) {
|
|
231
|
+
const lines = ['---', `name: ${name}`, `description: ${JSON.stringify(description)}`];
|
|
232
|
+
if (license) lines.push(`license: ${JSON.stringify(String(license))}`);
|
|
233
|
+
if (compatibility) lines.push(`compatibility: ${JSON.stringify(String(compatibility).slice(0, 500))}`);
|
|
234
|
+
if (allowedTools) lines.push(`allowed-tools: ${JSON.stringify(String(allowedTools))}`);
|
|
235
|
+
const metaEntries = Object.entries(metadata ?? {}).filter(([, v]) => v !== undefined && v !== null && v !== '');
|
|
236
|
+
if (metaEntries.length > 0) {
|
|
237
|
+
lines.push('metadata:');
|
|
238
|
+
for (const [k, v] of metaEntries.sort(([a], [b]) => a.localeCompare(b))) {
|
|
239
|
+
lines.push(` ${k}: ${JSON.stringify(String(v))}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
lines.push('---');
|
|
243
|
+
return lines.join('\n');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Compile a skill into Agent Skills conforming `SKILL.md` + optional `references/*.md`. */
|
|
247
|
+
export function compileSkill(skill = {}, rawText = null) {
|
|
248
|
+
const name = String(skill.name ?? '').trim();
|
|
249
|
+
const brokenName = nameProblems(name);
|
|
250
|
+
if (brokenName) throw new Error(`skill "${name}" cannot be compiled: name ${brokenName}`);
|
|
251
|
+
|
|
252
|
+
const parsed = rawText ? readFrontmatter(rawText) : { meta: {}, present: [], body: '' };
|
|
253
|
+
const description = buildConformingDescription(skill, parsed.meta.description);
|
|
254
|
+
const metadata = {};
|
|
255
|
+
for (const [k, v] of Object.entries(parsed.meta.metadata ?? {})) metadata[k] = String(v);
|
|
256
|
+
for (const key of parsed.present) {
|
|
257
|
+
if (!SPEC_KEYS.has(key) && parsed.meta[key] !== undefined) metadata[key] = String(parsed.meta[key]);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const fmBlock = serializeFrontmatter({
|
|
261
|
+
name,
|
|
262
|
+
description,
|
|
263
|
+
license: parsed.meta.license,
|
|
264
|
+
compatibility: parsed.meta.compatibility,
|
|
265
|
+
allowedTools: parsed.meta['allowed-tools'],
|
|
266
|
+
metadata,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const defaultBody = parsed.body.trim() || `# ${name}\n\n${skill.summary ?? description}`;
|
|
270
|
+
const fullCandidate = `${fmBlock}\n\n${defaultBody}\n`;
|
|
271
|
+
const splitPlan = planSkillSplit(skill, rawText ? fullCandidate : null);
|
|
272
|
+
const finalBody = splitPlan.compiledBody ? splitPlan.compiledBody.trim() : defaultBody;
|
|
273
|
+
const mainText = `${fmBlock}\n\n${finalBody}\n`;
|
|
274
|
+
const mainBytes = Buffer.byteLength(mainText, 'utf8');
|
|
275
|
+
const mainTokens = Math.ceil(mainBytes / BYTES_PER_TOKEN);
|
|
276
|
+
const conformance = checkSkill({ directoryName: name, fileName: 'SKILL.md', text: mainText, at: name });
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
name,
|
|
280
|
+
description,
|
|
281
|
+
mainPath: `${name}/SKILL.md`,
|
|
282
|
+
mainText,
|
|
283
|
+
mainBytes,
|
|
284
|
+
mainTokens,
|
|
285
|
+
references: (splitPlan.references ?? []).map((r) => ({
|
|
286
|
+
path: r.path,
|
|
287
|
+
title: r.title,
|
|
288
|
+
bytes: r.bytes,
|
|
289
|
+
tokens: r.tokens,
|
|
290
|
+
lines: r.lines,
|
|
291
|
+
...(r.content ? { content: r.content } : {}),
|
|
292
|
+
})),
|
|
293
|
+
splitPlan: {
|
|
294
|
+
flagged: splitPlan.flagged,
|
|
295
|
+
source: splitPlan.source,
|
|
296
|
+
reason: splitPlan.reason,
|
|
297
|
+
totalBytes: splitPlan.totalBytes,
|
|
298
|
+
totalTokens: splitPlan.totalTokens,
|
|
299
|
+
mainBytes: rawText ? mainBytes : splitPlan.mainBytes,
|
|
300
|
+
mainTokens: rawText ? mainTokens : splitPlan.mainTokens,
|
|
301
|
+
references: (splitPlan.references ?? []).map(({ path, title, bytes, tokens, lines }) => ({
|
|
302
|
+
path,
|
|
303
|
+
title,
|
|
304
|
+
bytes,
|
|
305
|
+
tokens,
|
|
306
|
+
lines,
|
|
307
|
+
})),
|
|
308
|
+
},
|
|
309
|
+
conformance,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Return the sorted names of skills whose declared `paths` globs match any file in `touchedPaths`. */
|
|
314
|
+
export function matchSkillsForPaths(skills = [], touchedPaths = []) {
|
|
315
|
+
const cleanPaths = (touchedPaths ?? []).map((p) => String(p ?? '').replace(/^\/+/, '').trim()).filter(Boolean);
|
|
316
|
+
if (cleanPaths.length === 0) return [];
|
|
317
|
+
const matched = [];
|
|
318
|
+
for (const skill of skills) {
|
|
319
|
+
if (!skill?.name || skill.aliasOf) continue;
|
|
320
|
+
const globs = Array.isArray(skill.paths) ? skill.paths : [];
|
|
321
|
+
if (globs.length > 0 && cleanPaths.some((file) => globs.some((glob) => matchesGlob(file, glob)))) {
|
|
322
|
+
matched.push(skill.name);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return [...new Set(matched)].sort();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** Extract the sorted list of skill names opened by an agent from an R02 transcript. */
|
|
329
|
+
export function extractOpenedSkills(transcriptInput, knownSkillNames = null) {
|
|
330
|
+
const known = knownSkillNames ? new Set(knownSkillNames) : null;
|
|
331
|
+
const opened = new Set();
|
|
332
|
+
const recordIfKnown = (candidate) => {
|
|
333
|
+
const name = String(candidate ?? '').trim().toLowerCase();
|
|
334
|
+
if (!name || nameProblems(name) !== null) return;
|
|
335
|
+
if (!known || known.has(name)) opened.add(name);
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
if (Array.isArray(transcriptInput)) {
|
|
339
|
+
for (const item of transcriptInput) {
|
|
340
|
+
if (typeof item === 'string') recordIfKnown(item);
|
|
341
|
+
else if (item && typeof item === 'object') {
|
|
342
|
+
for (const s of extractOpenedSkills(JSON.stringify(item), known)) opened.add(s);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return [...opened].sort();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const text = typeof transcriptInput === 'string' ? transcriptInput : JSON.stringify(transcriptInput ?? '');
|
|
349
|
+
for (const m of text.matchAll(/(?:^|[\s"'`:=(/])skills\/([a-z0-9-]+)(?:\/SKILL\.md|\.md|\/|[\s"'`),]|$)/gi)) {
|
|
350
|
+
recordIfKnown(m[1]);
|
|
351
|
+
}
|
|
352
|
+
for (const m of text.matchAll(/(?:^|[\s"'`:=(/])([a-z0-9-]+)\/SKILL\.md\b/g)) recordIfKnown(m[1]);
|
|
353
|
+
for (const m of text.matchAll(/\bsimt(?:-dev)?\s+skills\s+show\s+([a-z0-9-]+)\b/gi)) recordIfKnown(m[1]);
|
|
354
|
+
for (const m of text.matchAll(/"(?:skill|skill_name|skillName)"\s*:\s*"([a-z0-9-]+)"/gi)) recordIfKnown(m[1]);
|
|
355
|
+
for (const m of text.matchAll(/"name"\s*:\s*"skills_show"[\s\S]{0,200}?"name"\s*:\s*"([a-z0-9-]+)"/gi)) {
|
|
356
|
+
recordIfKnown(m[1]);
|
|
357
|
+
}
|
|
358
|
+
return [...opened].sort();
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** Compute per-skill trigger recall/precision across R02 runs and attach split plans. */
|
|
362
|
+
export function evaluateSkillTriggers(registry, runs = [], { bodiesBySkill = new Map(), goldPathsByTask = new Map() } = {}) {
|
|
363
|
+
const skills = [...(registry?.skills ?? [])].sort((a, b) => a.name.localeCompare(b.name));
|
|
364
|
+
const knownNames = new Set(skills.map((s) => s.name));
|
|
365
|
+
|
|
366
|
+
const normalizedRuns = (runs ?? []).map((run) => {
|
|
367
|
+
const taskId = String(run?.task ?? '');
|
|
368
|
+
const expectedFromRun = Array.isArray(run?.skills?.expected)
|
|
369
|
+
? run.skills.expected
|
|
370
|
+
: Array.isArray(run?.expectedSkills) ? run.expectedSkills : null;
|
|
371
|
+
const goldPaths = run?.goldFiles ?? goldPathsByTask.get(taskId) ?? [];
|
|
372
|
+
const expected = expectedFromRun
|
|
373
|
+
? expectedFromRun.filter((n) => knownNames.has(n))
|
|
374
|
+
: matchSkillsForPaths(skills, goldPaths);
|
|
375
|
+
const openedFromRun = Array.isArray(run?.skills?.opened)
|
|
376
|
+
? run.skills.opened
|
|
377
|
+
: Array.isArray(run?.openedSkills) ? run.openedSkills : null;
|
|
378
|
+
const opened = openedFromRun
|
|
379
|
+
? openedFromRun.filter((n) => knownNames.has(n))
|
|
380
|
+
: extractOpenedSkills(run?.transcript ?? '', knownNames);
|
|
381
|
+
return { task: taskId, arm: String(run?.arm ?? 'default'), expected: new Set(expected), opened: new Set(opened) };
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
const rows = skills.map((skill) => {
|
|
385
|
+
let tp = 0;
|
|
386
|
+
let fp = 0;
|
|
387
|
+
let fn = 0;
|
|
388
|
+
for (const run of normalizedRuns) {
|
|
389
|
+
const exp = run.expected.has(skill.name);
|
|
390
|
+
const opn = run.opened.has(skill.name);
|
|
391
|
+
if (exp && opn) tp += 1;
|
|
392
|
+
else if (!exp && opn) fp += 1;
|
|
393
|
+
else if (exp && !opn) fn += 1;
|
|
394
|
+
}
|
|
395
|
+
const expected = tp + fn;
|
|
396
|
+
const opened = tp + fp;
|
|
397
|
+
const splitPlan = planSkillSplit(skill, bodiesBySkill.get(skill.name) ?? null);
|
|
398
|
+
return {
|
|
399
|
+
name: skill.name,
|
|
400
|
+
bytes: skill.bytes ?? 0,
|
|
401
|
+
tokens: splitPlan.totalTokens,
|
|
402
|
+
paths: skill.paths ?? [],
|
|
403
|
+
expected,
|
|
404
|
+
opened,
|
|
405
|
+
tp,
|
|
406
|
+
fp,
|
|
407
|
+
fn,
|
|
408
|
+
recall: expected > 0 ? tp / expected : null,
|
|
409
|
+
precision: opened > 0 ? tp / opened : null,
|
|
410
|
+
flagged: splitPlan.flagged,
|
|
411
|
+
splitPlan: {
|
|
412
|
+
flagged: splitPlan.flagged,
|
|
413
|
+
source: splitPlan.source,
|
|
414
|
+
reason: splitPlan.reason,
|
|
415
|
+
totalBytes: splitPlan.totalBytes,
|
|
416
|
+
totalTokens: splitPlan.totalTokens,
|
|
417
|
+
mainBytes: splitPlan.mainBytes,
|
|
418
|
+
mainTokens: splitPlan.mainTokens,
|
|
419
|
+
references: (splitPlan.references ?? []).map(({ path, title, bytes, tokens, lines }) => ({
|
|
420
|
+
path,
|
|
421
|
+
title,
|
|
422
|
+
bytes,
|
|
423
|
+
tokens,
|
|
424
|
+
lines,
|
|
425
|
+
})),
|
|
426
|
+
},
|
|
427
|
+
};
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
return {
|
|
431
|
+
runCount: normalizedRuns.length,
|
|
432
|
+
skillCount: rows.length,
|
|
433
|
+
flaggedCount: rows.filter((r) => r.flagged).length,
|
|
434
|
+
skills: rows,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/** Create a proposal-only description improvement with provenance (Principle P2). */
|
|
439
|
+
export async function proposeSkillDescription(
|
|
440
|
+
skill,
|
|
441
|
+
{ currentDescription = '', triggerStats = null, proposer = null, model = 'deterministic-template' } = {},
|
|
442
|
+
) {
|
|
443
|
+
const prompt = [
|
|
444
|
+
`Skill: ${skill.name}`,
|
|
445
|
+
`Summary: ${skill.summary ?? ''}`,
|
|
446
|
+
`Paths: ${(skill.paths ?? []).join(', ') || '(none)'}`,
|
|
447
|
+
`Keywords: ${(skill.keywords ?? []).join(', ') || '(none)'}`,
|
|
448
|
+
`Capabilities: ${(skill.capabilities ?? []).join(', ') || '(none)'}`,
|
|
449
|
+
`Current description: ${currentDescription || skill.summary || ''}`,
|
|
450
|
+
triggerStats
|
|
451
|
+
? `Trigger stats: expected=${triggerStats.expected}, opened=${triggerStats.opened}, recall=${triggerStats.recall ?? 'n/a'}, precision=${triggerStats.precision ?? 'n/a'}`
|
|
452
|
+
: 'Trigger stats: unmeasured',
|
|
453
|
+
].join('\n');
|
|
454
|
+
|
|
455
|
+
const promptHash = sha256Tag(prompt);
|
|
456
|
+
let candidate = '';
|
|
457
|
+
let usedModel = model;
|
|
458
|
+
if (typeof proposer === 'function') {
|
|
459
|
+
const raw = await proposer({ skill, prompt, triggerStats });
|
|
460
|
+
if (typeof raw === 'string' && raw.trim()) candidate = raw.trim();
|
|
461
|
+
else if (raw && typeof raw.description === 'string') {
|
|
462
|
+
candidate = raw.description.trim();
|
|
463
|
+
if (raw.model) usedModel = String(raw.model);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
const proposedDescription = buildConformingDescription(skill, candidate);
|
|
467
|
+
return {
|
|
468
|
+
schemaVersion: 1,
|
|
469
|
+
skill: skill.name,
|
|
470
|
+
status: 'proposal',
|
|
471
|
+
currentDescription: currentDescription || skill.summary || '',
|
|
472
|
+
proposedDescription,
|
|
473
|
+
provenance: { model: usedModel, promptHash, digest: sha256Tag(proposedDescription) },
|
|
474
|
+
};
|
|
475
|
+
}
|
package/src/index/gate.mjs
CHANGED
|
@@ -9,14 +9,9 @@ import { readFile } from 'node:fs/promises';
|
|
|
9
9
|
import { join } from 'node:path';
|
|
10
10
|
|
|
11
11
|
import { isTestPath } from '../bench/mine.mjs';
|
|
12
|
+
import { langOf } from './store.mjs';
|
|
12
13
|
import { importsOf } from './tests.mjs';
|
|
13
14
|
|
|
14
|
-
const LANG = {
|
|
15
|
-
js: 'javascript', mjs: 'javascript', cjs: 'javascript', jsx: 'javascript',
|
|
16
|
-
ts: 'typescript', tsx: 'typescript', kt: 'kotlin', kts: 'kotlin', java: 'java',
|
|
17
|
-
py: 'python', go: 'go', rs: 'rust', swift: 'swift', dart: 'dart', rb: 'ruby', cs: 'csharp',
|
|
18
|
-
};
|
|
19
|
-
|
|
20
15
|
/**
|
|
21
16
|
* Test files that import one of `changed`, directly or through other files.
|
|
22
17
|
* A changed file that is itself a test is included: the edit is the test.
|
|
@@ -92,10 +87,3 @@ export async function readEntries(root, files) {
|
|
|
92
87
|
export async function readImportEdges(root, files) {
|
|
93
88
|
return importEdges(await readEntries(root, files));
|
|
94
89
|
}
|
|
95
|
-
|
|
96
|
-
function langOf(file) {
|
|
97
|
-
const base = file.slice(file.lastIndexOf('/') + 1);
|
|
98
|
-
const dot = base.lastIndexOf('.');
|
|
99
|
-
if (dot <= 0) return '';
|
|
100
|
-
return LANG[base.slice(dot + 1)] ?? '';
|
|
101
|
-
}
|
package/src/index/govern.mjs
CHANGED
|
@@ -6,22 +6,35 @@
|
|
|
6
6
|
// and a lesson do the same with their own path list. Nothing here is stored: the
|
|
7
7
|
// answer is the records on disk.
|
|
8
8
|
|
|
9
|
+
import { parseDecisionFrontmatter, splitFrontmatter } from '../memory/decide.mjs';
|
|
10
|
+
|
|
9
11
|
const SIBLING = /^\*{0,2}[A-Z][\w-]*(?:[ \t]+[\w-]+){0,3}:\*{0,2}(?:[ \t]|$)/;
|
|
10
12
|
const HEADING = /^#{1,6}\s/;
|
|
11
13
|
const TOOL = /^(?:simt-dev|simt)\b/;
|
|
12
14
|
|
|
13
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Affected tokens of a decision record: reads `simt-affects:` frontmatter first (R34),
|
|
17
|
+
* falling back to backticked tokens of the `Affects:` paragraph (R25).
|
|
18
|
+
*/
|
|
14
19
|
export function affectsTokens(text) {
|
|
15
|
-
const
|
|
20
|
+
const fm = parseDecisionFrontmatter(text);
|
|
21
|
+
const { body } = splitFrontmatter(text);
|
|
22
|
+
const lines = String(body).split('\n');
|
|
16
23
|
const start = lines.findIndex((line) => /^\s*\*{0,2}Affects:\*{0,2}/.test(line));
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
const proseTokens = [];
|
|
25
|
+
if (start >= 0) {
|
|
26
|
+
const parts = [lines[start].replace(/^\s*\*{0,2}Affects:\*{0,2}\s*/, '')];
|
|
27
|
+
for (let i = start + 1; i < lines.length; i += 1) {
|
|
28
|
+
const line = lines[i];
|
|
29
|
+
if (line.trim() === '' || SIBLING.test(line) || HEADING.test(line)) break;
|
|
30
|
+
parts.push(line);
|
|
31
|
+
}
|
|
32
|
+
proseTokens.push(...[...parts.join('\n').matchAll(/`([^`]+)`/g)].map((match) => match[1].trim()).filter(Boolean));
|
|
33
|
+
}
|
|
34
|
+
if (fm.hasAffects) {
|
|
35
|
+
return [...new Set([...fm.affects, ...proseTokens])];
|
|
23
36
|
}
|
|
24
|
-
return
|
|
37
|
+
return proseTokens;
|
|
25
38
|
}
|
|
26
39
|
|
|
27
40
|
/**
|
|
@@ -205,8 +218,7 @@ function extension(file) {
|
|
|
205
218
|
}
|
|
206
219
|
|
|
207
220
|
function frontmatter(text) {
|
|
208
|
-
|
|
209
|
-
return match ? match[1] : '';
|
|
221
|
+
return splitFrontmatter(text).front ?? '';
|
|
210
222
|
}
|
|
211
223
|
|
|
212
224
|
function quotedList(front, key) {
|