simt-dev 0.2.0 → 0.2.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 +375 -7
- package/README.md +4 -4
- package/docs/decisions/ADR-019-a-declared-command-is-the-runner.md +27 -0
- package/docs/decisions/ADR-059-the-version-a-repository-runs-is-the-one-it-pinned.md +75 -0
- package/docs/decisions/ADR-060-a-repository-with-nothing-to-run-is-adopted-and-says-so.md +131 -0
- package/docs/decisions/ADR-061-the-skill-registry-is-workspace-data.md +113 -0
- package/docs/decisions/ADR-062-a-fresh-adoption-seeds-the-gates-it-can-run.md +94 -0
- package/docs/decisions/ADR-063-the-adoption-escape-hatch-is-opt-in.md +69 -0
- package/docs/decisions/ADR-064-the-context-meter-counts-what-the-tool-wrote.md +107 -0
- package/docs/decisions/ADR-065-a-pack-is-one-shared-rule-set-pinned-by-digest.md +97 -0
- package/docs/decisions/ADR-066-a-monorepo-projects-once-per-package.md +115 -0
- package/docs/decisions/ADR-067-which-harnesses-read-agents-md-is-a-column.md +110 -0
- package/docs/decisions/ADR-068-the-mcp-revision-list-cites-what-was-read.md +111 -0
- package/docs/decisions/ADR-069-the-conformance-lint-checks-the-published-specification.md +90 -0
- package/docs/decisions/ADR-070-the-json-payload-carries-its-own-version.md +91 -0
- package/docs/decisions/ADR-071-a-rule-that-can-be-narrowed-is-written-narrow.md +106 -0
- package/docs/decisions/ADR-072-the-budget-is-a-declaration-beside-the-gates.md +104 -0
- package/docs/decisions/ADR-073-a-kernel-is-a-smaller-projection-the-repository-asks-for.md +92 -0
- package/docs/decisions/README.md +17 -0
- package/package.json +1 -2
- package/rules/core.md +1 -0
- package/rules/stacks/android.md +2 -0
- package/rules/stacks/cloudflare-workers.md +2 -0
- package/rules/stacks/connectiq.md +1 -0
- package/rules/stacks/container.md +2 -0
- package/rules/stacks/dart.md +40 -0
- package/rules/stacks/deno.md +38 -0
- package/rules/stacks/dotnet.md +37 -0
- package/rules/stacks/elixir.md +41 -0
- package/rules/stacks/go.md +36 -0
- package/rules/stacks/jvm.md +2 -0
- package/rules/stacks/node.md +2 -0
- package/rules/stacks/python.md +2 -0
- package/rules/stacks/react-native.md +2 -0
- package/rules/stacks/react-web.md +2 -0
- package/rules/stacks/ruby.md +38 -0
- package/rules/stacks/rust.md +36 -0
- package/rules/stacks/swift.md +39 -0
- package/rules/stacks/typescript.md +2 -0
- package/rules/stacks/uv.md +2 -0
- package/src/argv.mjs +54 -6
- package/src/ci/dependabot.mjs +1 -1
- package/src/ci/workflow.mjs +33 -11
- package/src/commands/compare.mjs +15 -0
- package/src/commands/graph.mjs +58 -25
- package/src/commands/harness.mjs +150 -0
- package/src/commands/help.mjs +37 -5
- package/src/commands/hooks.mjs +173 -57
- package/src/commands/index.mjs +95 -69
- package/src/commands/init.mjs +100 -20
- package/src/commands/mcp.mjs +20 -5
- package/src/commands/rules.mjs +96 -60
- package/src/commands/skills.mjs +189 -86
- package/src/commands/skillsinstall.mjs +20 -5
- package/src/commands/sync.mjs +213 -15
- package/src/commands/tokens.mjs +138 -0
- package/src/compare/hooksstate.mjs +96 -9
- package/src/compare/row.mjs +107 -7
- package/src/decisions/index.mjs +21 -6
- package/src/graph/load.mjs +125 -14
- package/src/graph/spec.mjs +227 -0
- package/src/graph/speccheck.mjs +68 -0
- package/src/hooks/capability.mjs +48 -4
- package/src/hooks/dispatcher.mjs +84 -7
- package/src/hooks/gates.mjs +0 -2
- package/src/hooks/resolve.mjs +24 -25
- package/src/hooks/shim.mjs +29 -5
- package/src/identity.mjs +0 -11
- package/src/mcp/schema.mjs +139 -0
- package/src/mcp/server.mjs +197 -20
- package/src/mcp/tools.mjs +290 -3
- package/src/measure/context.mjs +371 -0
- package/src/monorepo.mjs +339 -0
- package/src/output/index.mjs +86 -9
- package/src/package-version.mjs +74 -0
- package/src/packs.mjs +182 -0
- package/src/projection/conflicts.mjs +56 -5
- package/src/projection/manifest.mjs +335 -25
- package/src/projection/mdc.mjs +4 -13
- package/src/projection/project.mjs +139 -8
- package/src/projection/rules.mjs +308 -27
- package/src/projection/surfaces.mjs +133 -15
- package/src/projection/targets.mjs +90 -7
- package/src/resolve/eval.mjs +64 -0
- package/src/resolve/fingerprint.mjs +1 -0
- package/src/resolve/near.mjs +26 -7
- package/src/resolve/resolve.mjs +1 -1
- package/src/resolve/source.mjs +1 -1
- package/src/workspace.mjs +0 -9
- package/graph/skills.graph.json +0 -1622
package/src/commands/skills.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Implementation sequence steps 4 and 5.
|
|
4
4
|
//
|
|
5
5
|
// Output design (plan 0002 section 6): one fact per line, fixed field order, no
|
|
6
|
-
// wrapping, no
|
|
6
|
+
// wrapping, no ANSI on any stream, bounded by --max-bytes with an explicit
|
|
7
7
|
// TRUNCATED line. The NOT SELECTED block is as important as the resolved block -
|
|
8
8
|
// the agent can see what exists and why it was excluded, and override in one
|
|
9
9
|
// token instead of guessing. Suppressing it is how a discovery tool becomes a
|
|
@@ -14,9 +14,10 @@ import { join } from 'node:path';
|
|
|
14
14
|
import { existsSync } from 'node:fs';
|
|
15
15
|
|
|
16
16
|
import { EXIT, emit, emitJson, fail, isJson, note as emitNote } from '../output/index.mjs';
|
|
17
|
-
import { loadRegistry
|
|
17
|
+
import { loadRegistry } from '../graph/load.mjs';
|
|
18
18
|
import { locateSource, sourceHeldLocally } from '../resolve/source.mjs';
|
|
19
|
-
import {
|
|
19
|
+
import { unknownSkills, scoreCorpus } from '../resolve/eval.mjs';
|
|
20
|
+
import { PACKAGE_ROOT, resolveWorkspace, WORKSPACE_REMEDY } from '../workspace.mjs';
|
|
20
21
|
import { fingerprint } from '../resolve/fingerprint.mjs';
|
|
21
22
|
import { tokenize, rankSkills, closure, selectWithinBudget, resolveAlias, scoreSkill, selectionVerdict, stackCompatible } from '../resolve/resolve.mjs';
|
|
22
23
|
import { nearMatches } from '../resolve/near.mjs';
|
|
@@ -48,6 +49,22 @@ export function parsePathList(value) {
|
|
|
48
49
|
.filter(Boolean);
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Read a list flag in both the forms this tool ships.
|
|
54
|
+
*
|
|
55
|
+
* `--skill` accepts `--skill a --skill b` and `--skill a,b`, and arrives here as an
|
|
56
|
+
* array in the first case and a string in the second. `String(['a','b'])` happens to
|
|
57
|
+
* produce `'a,b'`, so the old one-line reader would have kept working by accident — and
|
|
58
|
+
* would have started throwing the moment a skill name contained the character the
|
|
59
|
+
* join inserts. Both shapes are handled rather than one being relied upon, because
|
|
60
|
+
* relying on it is the same class of bug as the one being fixed here.
|
|
61
|
+
*/
|
|
62
|
+
export function listFlag(value) {
|
|
63
|
+
return (Array.isArray(value) ? value : [value])
|
|
64
|
+
.flatMap((v) => parsePathList(v))
|
|
65
|
+
.filter(Boolean);
|
|
66
|
+
}
|
|
67
|
+
|
|
51
68
|
/** Compact stack label: the tags that actually discriminate, not all of them. */
|
|
52
69
|
function stackLabel(tags = []) {
|
|
53
70
|
const DISCRIMINATING = [
|
|
@@ -75,29 +92,24 @@ const kb = (bytes) => `${(bytes / 1000).toFixed(1)}k`;
|
|
|
75
92
|
// --- tier 0: index ------------------------------------------------------------
|
|
76
93
|
|
|
77
94
|
export async function indexCommand(args, flags) {
|
|
78
|
-
const { registry, errors, ok } = await loadRegistry(
|
|
79
|
-
if (!ok) return fail(errors[0], EXIT.REGISTRY);
|
|
95
|
+
const { registry, errors, ok } = await loadRegistry(null, flags);
|
|
96
|
+
if (!ok) return fail(errors[0], EXIT.REGISTRY, WORKSPACE_REMEDY);
|
|
80
97
|
const workspace = await workspaceLine(flags);
|
|
81
98
|
|
|
82
99
|
if (isJson(flags)) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
},
|
|
97
|
-
null,
|
|
98
|
-
2,
|
|
99
|
-
)}\n`,
|
|
100
|
-
{ maxBytes: flags.maxBytes ?? 0 },
|
|
100
|
+
emitJson(
|
|
101
|
+
{
|
|
102
|
+
ok: true,
|
|
103
|
+
workspace,
|
|
104
|
+
skills: registry.skills.map((s) => ({
|
|
105
|
+
name: s.name,
|
|
106
|
+
summary: s.summary,
|
|
107
|
+
capabilities: s.capabilities,
|
|
108
|
+
stack: s.stack,
|
|
109
|
+
bytes: s.bytes,
|
|
110
|
+
deferred: Boolean(s.deferred),
|
|
111
|
+
})),
|
|
112
|
+
},
|
|
101
113
|
);
|
|
102
114
|
return EXIT.OK;
|
|
103
115
|
}
|
|
@@ -151,20 +163,15 @@ async function emitAll(registry, repoStack, flags, warning) {
|
|
|
151
163
|
const inline = usable.filter((s) => !s.deferred);
|
|
152
164
|
|
|
153
165
|
if (isJson(flags)) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
},
|
|
164
|
-
null,
|
|
165
|
-
2,
|
|
166
|
-
)}\n`,
|
|
167
|
-
{ maxBytes: flags.maxBytes ?? 0 },
|
|
166
|
+
emitJson(
|
|
167
|
+
{
|
|
168
|
+
ok: true,
|
|
169
|
+
all: true,
|
|
170
|
+
repoStack,
|
|
171
|
+
inline: inline.map((s) => s.name),
|
|
172
|
+
deferred: deferred.map((s) => s.name),
|
|
173
|
+
...(warning ? { warning } : {}),
|
|
174
|
+
},
|
|
168
175
|
);
|
|
169
176
|
return EXIT.OK;
|
|
170
177
|
}
|
|
@@ -189,8 +196,8 @@ export async function findCommand(args, flags) {
|
|
|
189
196
|
const query = args.join(' ').trim();
|
|
190
197
|
if (!query) return fail('find needs a query', EXIT.USAGE, `usage: ${CLI} skills find <query>`);
|
|
191
198
|
|
|
192
|
-
const { registry, errors, ok } = await loadRegistry(
|
|
193
|
-
if (!ok) return fail(errors[0], EXIT.REGISTRY);
|
|
199
|
+
const { registry, errors, ok } = await loadRegistry(null, flags);
|
|
200
|
+
if (!ok) return fail(errors[0], EXIT.REGISTRY, WORKSPACE_REMEDY);
|
|
194
201
|
|
|
195
202
|
const tokens = tokenize(query);
|
|
196
203
|
const workspace = await workspaceLine(flags);
|
|
@@ -200,7 +207,10 @@ export async function findCommand(args, flags) {
|
|
|
200
207
|
.sort((a, b) => b.score - a.score || a.skill.name.localeCompare(b.skill.name));
|
|
201
208
|
|
|
202
209
|
if (isJson(flags)) {
|
|
203
|
-
|
|
210
|
+
emitJson(
|
|
211
|
+
{ ok: true, query, workspace, results: results.map((r) => ({ name: r.skill.name, score: r.score, reasons: r.reasons })) },
|
|
212
|
+
{ maxBytes: flags.maxBytes ?? 0 },
|
|
213
|
+
);
|
|
204
214
|
return EXIT.OK;
|
|
205
215
|
}
|
|
206
216
|
|
|
@@ -227,8 +237,8 @@ export async function findCommand(args, flags) {
|
|
|
227
237
|
// --- brief: tier 1 ------------------------------------------------------------
|
|
228
238
|
|
|
229
239
|
export async function briefCommand(args, flags) {
|
|
230
|
-
const { registry, errors, ok } = await loadRegistry(
|
|
231
|
-
if (!ok) return fail(errors[0], EXIT.REGISTRY);
|
|
240
|
+
const { registry, errors, ok } = await loadRegistry(null, flags);
|
|
241
|
+
if (!ok) return fail(errors[0], EXIT.REGISTRY, WORKSPACE_REMEDY);
|
|
232
242
|
|
|
233
243
|
const root = flags.root ?? process.cwd();
|
|
234
244
|
const repo = await fingerprint(root);
|
|
@@ -237,7 +247,7 @@ export async function briefCommand(args, flags) {
|
|
|
237
247
|
const repoStack = repo.signals;
|
|
238
248
|
const paths = flags.paths ? parsePathList(flags.paths) : await dirtyPaths(root);
|
|
239
249
|
const taskTokens = tokenize(flags.task ?? '');
|
|
240
|
-
const forced =
|
|
250
|
+
const forced = listFlag(flags.skill);
|
|
241
251
|
const budget = flags.budget ?? DEFAULT_BUDGET;
|
|
242
252
|
|
|
243
253
|
// A brief with no task and no paths resolves on stack alone, which produces a
|
|
@@ -258,24 +268,19 @@ export async function briefCommand(args, flags) {
|
|
|
258
268
|
// is what `warning` carries. Failing here would tell a caller the tool
|
|
259
269
|
// broke when in fact it is telling them their query was.
|
|
260
270
|
if (flags.all) return emitAll(registry, repoStack, flags, warning);
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
null,
|
|
275
|
-
2,
|
|
276
|
-
)}\n`,
|
|
277
|
-
{ maxBytes: flags.maxBytes ?? 0 },
|
|
278
|
-
);
|
|
271
|
+
emitJson(
|
|
272
|
+
{
|
|
273
|
+
ok: true,
|
|
274
|
+
repo: { root, stack: repoStack },
|
|
275
|
+
workspace,
|
|
276
|
+
task: null,
|
|
277
|
+
budget,
|
|
278
|
+
resolved: [],
|
|
279
|
+
deferred: [],
|
|
280
|
+
excluded: [],
|
|
281
|
+
warning,
|
|
282
|
+
},
|
|
283
|
+
);
|
|
279
284
|
return EXIT.OK;
|
|
280
285
|
}
|
|
281
286
|
emit(
|
|
@@ -316,22 +321,17 @@ export async function briefCommand(args, flags) {
|
|
|
316
321
|
const lines = [];
|
|
317
322
|
|
|
318
323
|
if (isJson(flags)) {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
},
|
|
331
|
-
null,
|
|
332
|
-
2,
|
|
333
|
-
)}\n`,
|
|
334
|
-
{ maxBytes: flags.maxBytes ?? 0 },
|
|
324
|
+
emitJson(
|
|
325
|
+
{
|
|
326
|
+
ok: true,
|
|
327
|
+
repo: { root, stack: repoStack },
|
|
328
|
+
workspace,
|
|
329
|
+
task: flags.task ?? null,
|
|
330
|
+
budget,
|
|
331
|
+
resolved: picked.map((e) => ({ name: e.skill.name, bytes: e.skill.bytes, why: [...e.reasons, ...[...e.via].map((v) => `requires:${v}`)] })),
|
|
332
|
+
deferred: deferred.map((e) => ({ name: e.skill.name, bytes: e.skill.bytes })),
|
|
333
|
+
excluded: excluded.filter((e) => !closureNames.has(e.skill.name)).map((e) => ({ name: e.skill.name, reason: e.reason })),
|
|
334
|
+
},
|
|
335
335
|
);
|
|
336
336
|
return EXIT.OK;
|
|
337
337
|
}
|
|
@@ -432,8 +432,8 @@ export async function showCommand(args, flags) {
|
|
|
432
432
|
const name = args[0];
|
|
433
433
|
if (!name) return fail('show needs a skill name', EXIT.USAGE, `usage: ${CLI} skills show <name>`);
|
|
434
434
|
|
|
435
|
-
const { registry, errors, ok } = await loadRegistry(
|
|
436
|
-
if (!ok) return fail(errors[0], EXIT.REGISTRY);
|
|
435
|
+
const { registry, errors, ok } = await loadRegistry(null, flags);
|
|
436
|
+
if (!ok) return fail(errors[0], EXIT.REGISTRY, WORKSPACE_REMEDY);
|
|
437
437
|
|
|
438
438
|
const skill = registry.skills.find((s) => s.name === name);
|
|
439
439
|
if (!skill) {
|
|
@@ -453,10 +453,7 @@ export async function showCommand(args, flags) {
|
|
|
453
453
|
const body = await sourcePath(skill, workspaceRoot, root);
|
|
454
454
|
const bodyFrom = body === null ? 'none' : body.startsWith(`${root}/`) ? 'repo' : 'workspace';
|
|
455
455
|
if (isJson(flags)) {
|
|
456
|
-
|
|
457
|
-
`${JSON.stringify({ ok: true, ...skill, workspace, body, bodyFrom }, null, 2)}\n`,
|
|
458
|
-
{ maxBytes: flags.maxBytes ?? 0 },
|
|
459
|
-
);
|
|
456
|
+
emitJson({ ok: true, ...skill, workspace, body, bodyFrom }, { maxBytes: flags.maxBytes ?? 0 });
|
|
460
457
|
return EXIT.OK;
|
|
461
458
|
}
|
|
462
459
|
emit(
|
|
@@ -490,8 +487,8 @@ export async function whyCommand(args, flags) {
|
|
|
490
487
|
const name = args[0];
|
|
491
488
|
if (!name) return fail('why needs a skill name', EXIT.USAGE, `usage: ${CLI} skills why <name>`);
|
|
492
489
|
|
|
493
|
-
const { registry, errors, ok } = await loadRegistry(
|
|
494
|
-
if (!ok) return fail(errors[0], EXIT.REGISTRY);
|
|
490
|
+
const { registry, errors, ok } = await loadRegistry(null, flags);
|
|
491
|
+
if (!ok) return fail(errors[0], EXIT.REGISTRY, WORKSPACE_REMEDY);
|
|
495
492
|
|
|
496
493
|
const skill = registry.skills.find((s) => s.name === name);
|
|
497
494
|
if (!skill) {
|
|
@@ -565,6 +562,112 @@ export async function whyCommand(args, flags) {
|
|
|
565
562
|
return EXIT.OK;
|
|
566
563
|
}
|
|
567
564
|
|
|
565
|
+
// --- eval ---------------------------------------------------------------------
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* The gold corpus when the reader names none. A checkout holds one at this fixed path;
|
|
569
|
+
* `test/` is not in the published files, so an installed copy does not. That absence is
|
|
570
|
+
* a fact about where this copy runs, reported as such (ADR-061's tiered resolution is
|
|
571
|
+
* the same shape: explicit path, then the package's own file, then an honest absence).
|
|
572
|
+
*/
|
|
573
|
+
const DEFAULT_CORPUS = join(PACKAGE_ROOT, 'test', 'fixtures', 'eval-corpus.json');
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* `simt skills eval [corpus]` - measure the resolver against the gold corpus.
|
|
577
|
+
*
|
|
578
|
+
* Plan 0002 section 9: "Reports precision and recall and fails on regression." The
|
|
579
|
+
* corpus states, per case, which skills must come back (`select`) and which must not
|
|
580
|
+
* (`reject`); a case that names no selection is exhaustive, and anything it returns is
|
|
581
|
+
* a miss. The exit code rests on `clean`, because the two ratios can both read 1.0
|
|
582
|
+
* while an exhaustive case returns a skill neither list named (see src/resolve/eval.mjs).
|
|
583
|
+
*/
|
|
584
|
+
export async function evalCommand(positional, flags) {
|
|
585
|
+
const corpusPath = positional[0] ?? (existsSync(DEFAULT_CORPUS) ? DEFAULT_CORPUS : null);
|
|
586
|
+
|
|
587
|
+
if (corpusPath === null) {
|
|
588
|
+
return fail(
|
|
589
|
+
`no eval corpus: ${DEFAULT_CORPUS} is not on this machine (test data does not ship in the package)`,
|
|
590
|
+
EXIT.FAIL,
|
|
591
|
+
`run: ${CLI} skills eval <path-to-corpus> (a file of cases with select and reject lists)`,
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
let corpus;
|
|
596
|
+
try {
|
|
597
|
+
corpus = JSON.parse(await readFile(corpusPath, 'utf8'));
|
|
598
|
+
} catch (error) {
|
|
599
|
+
return fail(
|
|
600
|
+
`${corpusPath}: unreadable or invalid JSON (${error.message})`,
|
|
601
|
+
EXIT.FAIL,
|
|
602
|
+
`run: ${CLI} skills eval <path-to-corpus>`,
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
if (!Array.isArray(corpus.cases) || corpus.cases.length === 0) {
|
|
606
|
+
return fail(
|
|
607
|
+
`${corpusPath}: no cases (a corpus is {"cases": [{"name", "repo", "task", "select", "reject"}]})`,
|
|
608
|
+
EXIT.FAIL,
|
|
609
|
+
`run: ${CLI} skills eval <path-to-corpus>`,
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// The corpus is measured against the registry it cites, so it needs the same
|
|
614
|
+
// resolution and the same failure as every other skills command.
|
|
615
|
+
const { registry, errors, ok, path } = await loadRegistry(null, flags);
|
|
616
|
+
if (!ok) return fail(errors[0], EXIT.REGISTRY, WORKSPACE_REMEDY);
|
|
617
|
+
|
|
618
|
+
const unknown = unknownSkills(registry, corpus);
|
|
619
|
+
const score = scoreCorpus(registry, corpus);
|
|
620
|
+
const okRun = unknown.length === 0 && score.clean;
|
|
621
|
+
|
|
622
|
+
if (isJson(flags)) {
|
|
623
|
+
emitJson(
|
|
624
|
+
{
|
|
625
|
+
ok: okRun,
|
|
626
|
+
command: 'skills eval',
|
|
627
|
+
corpus: corpusPath,
|
|
628
|
+
registry: path,
|
|
629
|
+
cases: score.cases.map((c) => ({
|
|
630
|
+
name: c.name,
|
|
631
|
+
selected: c.selected,
|
|
632
|
+
missing: c.missing,
|
|
633
|
+
unexpected: c.unexpected,
|
|
634
|
+
})),
|
|
635
|
+
unknownSkills: unknown,
|
|
636
|
+
precision: score.precision,
|
|
637
|
+
recall: score.recall,
|
|
638
|
+
},
|
|
639
|
+
);
|
|
640
|
+
return okRun ? EXIT.OK : EXIT.FAIL;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const failed = score.cases.filter((c) => c.missing.length > 0 || c.unexpected.length > 0);
|
|
644
|
+
const lines = [
|
|
645
|
+
`corpus ${corpusPath} (${score.cases.length} cases)`,
|
|
646
|
+
`registry ${path}`,
|
|
647
|
+
`${score.cases.length - failed.length} pass, ${failed.length} fail`,
|
|
648
|
+
`precision ${score.precision.toFixed(3)} recall ${score.recall.toFixed(3)}`,
|
|
649
|
+
'',
|
|
650
|
+
];
|
|
651
|
+
|
|
652
|
+
for (const c of score.cases) {
|
|
653
|
+
if (c.missing.length === 0 && c.unexpected.length === 0) {
|
|
654
|
+
lines.push(`PASS ${c.name}`);
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
lines.push(`FAIL ${c.name}`);
|
|
658
|
+
for (const name of c.missing) lines.push(` missed ${name}`);
|
|
659
|
+
for (const name of c.unexpected) lines.push(` unexpected ${name}`);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (unknown.length > 0) {
|
|
663
|
+
lines.push('', `FAIL corpus cites skills the registry does not declare: ${unknown.join(', ')}`);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
lines.push('', okRun ? 'OK the resolver meets the gold corpus' : 'REGRESSION: the resolver no longer meets the gold corpus');
|
|
667
|
+
emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
|
|
668
|
+
return okRun ? EXIT.OK : EXIT.FAIL;
|
|
669
|
+
}
|
|
670
|
+
|
|
568
671
|
// --- bodies -------------------------------------------------------------------
|
|
569
672
|
|
|
570
673
|
/**
|
|
@@ -581,7 +684,7 @@ export async function whyCommand(args, flags) {
|
|
|
581
684
|
* location for a declared source, matched on bytes rather than on hope (ADR-015).
|
|
582
685
|
* The canonical source is still tried first, so a workspace body always wins.
|
|
583
686
|
*/
|
|
584
|
-
|
|
687
|
+
async function sourcePath(skill, workspaceRoot, root) {
|
|
585
688
|
const canonical = skill.sources?.find((s) => s.role === 'canonical') ?? skill.sources?.[0];
|
|
586
689
|
|
|
587
690
|
if (canonical) {
|
|
@@ -24,8 +24,8 @@ import { cp, mkdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
|
24
24
|
import { existsSync } from 'node:fs';
|
|
25
25
|
import { dirname, join } from 'node:path';
|
|
26
26
|
|
|
27
|
-
import { EXIT, emit, fail, isJson, note, writeMode } from '../output/index.mjs';
|
|
28
|
-
import { loadRegistry,
|
|
27
|
+
import { EXIT, emit, emitJson, fail, isJson, note, writeMode } from '../output/index.mjs';
|
|
28
|
+
import { loadRegistry, measureSkill } from '../graph/load.mjs';
|
|
29
29
|
import { fingerprint } from '../resolve/fingerprint.mjs';
|
|
30
30
|
import { readCapability } from '../hooks/capability.mjs';
|
|
31
31
|
import { MANIFEST, buildManifest, readManifest, writeManifest } from '../projection/manifest.mjs';
|
|
@@ -35,6 +35,7 @@ import { nearMatches } from '../resolve/near.mjs';
|
|
|
35
35
|
import { resolveWorkspace, WORKSPACE_REMEDY } from '../workspace.mjs';
|
|
36
36
|
import { CLI } from '../name.mjs';
|
|
37
37
|
import { MANIFEST_FILE } from '../name.mjs';
|
|
38
|
+
import { nameProblems } from '../graph/spec.mjs';
|
|
38
39
|
|
|
39
40
|
/**
|
|
40
41
|
* Where skills live in this repo. The estate already has a convention - a
|
|
@@ -59,8 +60,8 @@ export async function installCommand(args, flags) {
|
|
|
59
60
|
);
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
const { registry, errors, ok } = await loadRegistry(
|
|
63
|
-
if (!ok) return fail(errors[0], EXIT.REGISTRY);
|
|
63
|
+
const { registry, errors, ok } = await loadRegistry(null, flags);
|
|
64
|
+
if (!ok) return fail(errors[0], EXIT.REGISTRY, WORKSPACE_REMEDY);
|
|
64
65
|
|
|
65
66
|
const skill = registry.skills.find((s) => s.name === name);
|
|
66
67
|
if (!skill) {
|
|
@@ -112,6 +113,20 @@ export async function installCommand(args, flags) {
|
|
|
112
113
|
}
|
|
113
114
|
|
|
114
115
|
const dir = skillsDir(root, await dirEntries(root));
|
|
116
|
+
// The Agent Skills specification requires `name` to equal the directory it lives in,
|
|
117
|
+
// and the directory is named after the skill. So a name that breaks the naming rules
|
|
118
|
+
// cannot be installed at all: the client resolves by name, finds a directory the name
|
|
119
|
+
// does not match, and never reads the body this command just copied. Refused here,
|
|
120
|
+
// before anything is written, with the rule broken - because the alternative is a
|
|
121
|
+
// successful install that no client can load.
|
|
122
|
+
const broken = nameProblems(name);
|
|
123
|
+
if (broken !== null) {
|
|
124
|
+
return fail(
|
|
125
|
+
`${name}: ${broken}`,
|
|
126
|
+
EXIT.REGISTRY,
|
|
127
|
+
`rename it in ${skill.source ?? 'the registry'} to lowercase letters, digits and single hyphens, then install again`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
115
130
|
const dest = join(root, dir, name);
|
|
116
131
|
const inplace = await samePath(src, dest);
|
|
117
132
|
const action = inplace ? 'in-place' : await compare(dest, src, measured.flat, name);
|
|
@@ -147,7 +162,7 @@ export async function installCommand(args, flags) {
|
|
|
147
162
|
// combination a script writes. The payload is now emitted once, at the end,
|
|
148
163
|
// after the write it describes.
|
|
149
164
|
const report = () => {
|
|
150
|
-
if (isJson(flags))
|
|
165
|
+
if (isJson(flags)) emitJson(payload);
|
|
151
166
|
};
|
|
152
167
|
const mode = writeMode(flags);
|
|
153
168
|
|