@supersuit/superskill 0.1.0 → 0.2.1
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 +20 -0
- package/SPEC.md +1 -1
- package/package.json +5 -1
- package/src/frontmatter.mjs +91 -36
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.1 (2026-09-28)
|
|
4
|
+
|
|
5
|
+
- An unquoted value that is only a comment now reads as empty, which is what YAML means. Before
|
|
6
|
+
this, `source: # TODO` came back as the string "# TODO" and a list item written as `- # none
|
|
7
|
+
yet` came back as "# none yet", so a downstream linter counted a comment placeholder as a
|
|
8
|
+
filled-in field and passed specs it should have failed. A comment-only value followed by a
|
|
9
|
+
more-indented block still opens that nested map or list, exactly as it did with no comment.
|
|
10
|
+
Quoted values are untouched: `key: "# literal"` still reads as "# literal".
|
|
11
|
+
|
|
12
|
+
## 0.2.0 (2026-09-28)
|
|
13
|
+
|
|
14
|
+
- The frontmatter reader reads nesting: maps inside maps, lists of maps, lists inside maps, and
|
|
15
|
+
block scalars at any depth. Until now it stopped at one level and silently ignored anything
|
|
16
|
+
deeper, so a list of maps came back empty. Values still stay strings, and no skill in two real
|
|
17
|
+
corpora (295 skills) changed level. It is exported as `parseYamlSubset` so other standards in
|
|
18
|
+
this family (hyperspecification first) read their files with this one reader instead of a
|
|
19
|
+
second copy.
|
|
20
|
+
- `metadata-string-map` now names a metadata value that is itself a map; before, the reader
|
|
21
|
+
dropped it and the rule never saw it.
|
|
22
|
+
|
|
3
23
|
## 0.1.0 (2026-09-28)
|
|
4
24
|
|
|
5
25
|
- `reference-says-when`: a link from SKILL.md to an instruction file must say when to read it. Long-skill fixes now point at step files (`steps/<step>.md`).
|
package/SPEC.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# The superskill standard
|
|
2
2
|
|
|
3
|
-
**Version 0.1
|
|
3
|
+
**Version 0.2.1** (2026-09-28). The reference checker is `@supersuit/superskill`; where this
|
|
4
4
|
document and the checker disagree, the checker has a bug.
|
|
5
5
|
|
|
6
6
|
A **superskill** runs on frontier intelligence, is checked against examples a person approved,
|
package/package.json
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supersuit/superskill",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Score any agent skill folder as skill, tested, or superskill. An open standard and a zero-dependency CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": { "superskill": "bin/superskill.mjs" },
|
|
7
|
+
"exports": {
|
|
8
|
+
"./yaml": "./src/frontmatter.mjs",
|
|
9
|
+
"./package.json": "./package.json"
|
|
10
|
+
},
|
|
7
11
|
"files": ["bin/", "src/", "SPEC.md", "README.md", "CHANGELOG.md", "LICENSE"],
|
|
8
12
|
"scripts": { "test": "node --test test/*.test.mjs" },
|
|
9
13
|
"engines": { "node": ">=20" },
|
package/src/frontmatter.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
// A small YAML frontmatter reader for the subset SKILL.md files use: scalars,
|
|
2
|
-
// strings, folded (>) and literal (|) blocks,
|
|
1
|
+
// A small YAML frontmatter reader for the subset SKILL.md files and hyperspecs use: scalars,
|
|
2
|
+
// quoted strings, folded (>) and literal (|) blocks, maps and lists nested to any depth by
|
|
3
|
+
// indentation, and inline lists.
|
|
3
4
|
// Values stay strings; rules decide what a string means. Zero dependencies on purpose.
|
|
4
5
|
|
|
5
6
|
export function parseSkillFile(text) {
|
|
@@ -22,55 +23,109 @@ export function parseSkillFile(text) {
|
|
|
22
23
|
|
|
23
24
|
const indentOf = (l) => l.length - l.trimStart().length;
|
|
24
25
|
|
|
26
|
+
// Nesting, added in 0.2.0: maps inside maps, lists of maps, lists inside maps, block scalars
|
|
27
|
+
// at any depth, read by indentation. Until then the reader stopped at one level and IGNORED
|
|
28
|
+
// anything deeper, so a list of maps came back as nothing and no error said so. Values still
|
|
29
|
+
// stay strings. A stray indented line at the top level is still tolerated, as before.
|
|
30
|
+
// 0.2.1: an unquoted value or list item that is only a comment (`key: # TODO`, `- # none yet`)
|
|
31
|
+
// reads as empty, the same as no value at all, which is what YAML means. A comment-only value
|
|
32
|
+
// that is followed by a more-indented block still opens that nested map or list, exactly as
|
|
33
|
+
// `key:` with nothing after it does.
|
|
25
34
|
export function parseYamlSubset(lines) {
|
|
35
|
+
return parseMap(lines, 0, 0, true)[0];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const isBlank = (l) => !l.trim() || l.trimStart().startsWith("#");
|
|
39
|
+
const isItem = (t) => t === "-" || t.startsWith("- ");
|
|
40
|
+
const KEY = /^([^:\s"'][^:]*?|"[^"]*"|'[^']*')\s*:(?:\s+(.*)|\s*)$/;
|
|
41
|
+
|
|
42
|
+
function nextContent(lines, i) {
|
|
43
|
+
while (i < lines.length && isBlank(lines[i])) i++;
|
|
44
|
+
return i;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseMap(lines, i, indent, top = false) {
|
|
26
48
|
const out = {};
|
|
27
|
-
let i = 0;
|
|
28
49
|
while (i < lines.length) {
|
|
50
|
+
if (isBlank(lines[i])) { i++; continue; }
|
|
29
51
|
const line = lines[i];
|
|
30
|
-
|
|
31
|
-
if (
|
|
32
|
-
|
|
52
|
+
const ind = indentOf(line);
|
|
53
|
+
if (ind < indent) break;
|
|
54
|
+
if (ind > indent) {
|
|
55
|
+
if (top) { i++; continue; } // stray indented line at the top: tolerate, as before
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
const t = line.trim();
|
|
59
|
+
if (isItem(t)) break;
|
|
60
|
+
const m = t.match(KEY);
|
|
33
61
|
if (!m) throw new Error(`cannot read line ${i + 1}: ${line.slice(0, 60)}`);
|
|
34
62
|
const key = m[1].trim().replace(/^["']|["']$/g, "");
|
|
35
63
|
const rest = (m[2] ?? "").trim();
|
|
36
64
|
i++;
|
|
37
65
|
if (/^[>|][+-]?$/.test(rest)) {
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
while (block.length && !block[block.length - 1].trim()) block.pop();
|
|
41
|
-
const min = Math.min(...block.filter((l) => l.trim()).map(indentOf));
|
|
42
|
-
const stripped = block.map((l) => l.slice(Number.isFinite(min) ? min : 0));
|
|
43
|
-
out[key] = rest[0] === "|" ? stripped.join("\n") : foldLines(stripped);
|
|
44
|
-
continue;
|
|
66
|
+
const [v, next] = readBlock(lines, i, ind, rest[0]);
|
|
67
|
+
out[key] = v; i = next; continue;
|
|
45
68
|
}
|
|
46
|
-
if (rest === "") {
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (!items.length) out[key] = "";
|
|
54
|
-
else if (items[0].trim().startsWith("- ") || items[0].trim() === "-") out[key] = items.map((l) => scalar(l.trim().replace(/^-\s*/, "")));
|
|
55
|
-
else {
|
|
56
|
-
const map = {};
|
|
57
|
-
const base = indentOf(items[0]);
|
|
58
|
-
for (const l of items) {
|
|
59
|
-
if (indentOf(l) !== base) continue; // deeper nesting is outside the subset; ignored
|
|
60
|
-
const mm = l.trim().match(/^([^:]+?)\s*:\s*(.*)$/);
|
|
61
|
-
if (mm) map[mm[1].trim().replace(/^["']|["']$/g, "")] = scalar(mm[2]);
|
|
62
|
-
}
|
|
63
|
-
out[key] = map;
|
|
69
|
+
if (rest === "" || rest.startsWith("#")) {
|
|
70
|
+
const j = nextContent(lines, i);
|
|
71
|
+
if (j < lines.length) {
|
|
72
|
+
const ci = indentOf(lines[j]);
|
|
73
|
+
const ct = lines[j].trim();
|
|
74
|
+
if (ci > ind) { [out[key], i] = isItem(ct) ? parseList(lines, j, ci) : parseMap(lines, j, ci); continue; }
|
|
75
|
+
if (ci === ind && isItem(ct)) { [out[key], i] = parseList(lines, j, ci); continue; }
|
|
64
76
|
}
|
|
65
|
-
continue;
|
|
77
|
+
out[key] = ""; continue;
|
|
66
78
|
}
|
|
67
|
-
|
|
68
|
-
|
|
79
|
+
out[key] = inlineOrScalar(rest);
|
|
80
|
+
}
|
|
81
|
+
return [out, i];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function parseList(lines, i, indent) {
|
|
85
|
+
const out = [];
|
|
86
|
+
while (i < lines.length) {
|
|
87
|
+
if (isBlank(lines[i])) { i++; continue; }
|
|
88
|
+
const line = lines[i];
|
|
89
|
+
const ind = indentOf(line);
|
|
90
|
+
if (ind < indent) break;
|
|
91
|
+
if (ind > indent) { i++; continue; }
|
|
92
|
+
const t = line.trim();
|
|
93
|
+
if (!isItem(t)) break;
|
|
94
|
+
const content = t === "-" ? "" : t.slice(1).trimStart();
|
|
95
|
+
const at = line.indexOf(content, ind + 1); // where the item's content starts on the line
|
|
96
|
+
i++;
|
|
97
|
+
if (content === "" || content.startsWith("#")) {
|
|
98
|
+
const j = nextContent(lines, i);
|
|
99
|
+
if (j < lines.length && indentOf(lines[j]) > ind) {
|
|
100
|
+
const ci = indentOf(lines[j]);
|
|
101
|
+
let v; [v, i] = isItem(lines[j].trim()) ? parseList(lines, j, ci) : parseMap(lines, j, ci);
|
|
102
|
+
out.push(v);
|
|
103
|
+
} else out.push("");
|
|
69
104
|
continue;
|
|
70
105
|
}
|
|
71
|
-
|
|
106
|
+
if (KEY.test(content) && !/^\[.*\]$/.test(content)) {
|
|
107
|
+
// "- key: value" opens a map whose keys sit where this content starts.
|
|
108
|
+
const sub = [" ".repeat(at) + content, ...lines.slice(i)];
|
|
109
|
+
const [v, used] = parseMap(sub, 0, at);
|
|
110
|
+
out.push(v); i += used - 1; continue;
|
|
111
|
+
}
|
|
112
|
+
out.push(inlineOrScalar(content));
|
|
72
113
|
}
|
|
73
|
-
return out;
|
|
114
|
+
return [out, i];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function readBlock(lines, i, keyIndent, style) {
|
|
118
|
+
const block = [];
|
|
119
|
+
while (i < lines.length && (lines[i].trim() === "" || indentOf(lines[i]) > keyIndent)) { block.push(lines[i]); i++; }
|
|
120
|
+
while (block.length && !block[block.length - 1].trim()) block.pop();
|
|
121
|
+
const min = Math.min(...block.filter((l) => l.trim()).map(indentOf));
|
|
122
|
+
const stripped = block.map((l) => l.slice(Number.isFinite(min) ? min : 0));
|
|
123
|
+
return [style === "|" ? stripped.join("\n") : foldLines(stripped), i];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function inlineOrScalar(rest) {
|
|
127
|
+
if (rest.startsWith("[") && rest.endsWith("]")) return splitInline(rest.slice(1, -1)).map(scalar).filter((s) => s !== "");
|
|
128
|
+
return scalar(rest);
|
|
74
129
|
}
|
|
75
130
|
|
|
76
131
|
function foldLines(lines) {
|