@rune-kit/rune 2.4.0 → 2.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/README.md +36 -17
- package/compiler/__tests__/pack-split.test.js +141 -1
- package/compiler/__tests__/parser.test.js +147 -55
- package/compiler/__tests__/scripts-bundling.test.js +10 -11
- package/compiler/__tests__/skill-index.test.js +218 -0
- package/compiler/adapters/antigravity.js +71 -57
- package/compiler/bin/rune.js +355 -355
- package/compiler/doctor.js +11 -1
- package/compiler/emitter.js +678 -466
- package/compiler/parser.js +267 -247
- package/hooks/hooks.json +12 -0
- package/hooks/intent-router/index.cjs +108 -0
- package/hooks/pre-tool-guard/index.cjs +177 -68
- package/package.json +63 -63
- package/skills/brainstorm/SKILL.md +2 -0
- package/skills/cook/SKILL.md +661 -648
- package/skills/debug/SKILL.md +394 -392
- package/skills/deploy/SKILL.md +2 -0
- package/skills/fix/SKILL.md +283 -281
- package/skills/onboard/SKILL.md +7 -0
- package/skills/plan/SKILL.md +344 -342
- package/skills/preflight/SKILL.md +362 -360
- package/skills/review/SKILL.md +491 -489
- package/skills/scout/SKILL.md +1 -0
- package/skills/sentinel/SKILL.md +319 -299
- package/skills/session-bridge/SKILL.md +1 -0
- package/skills/team/SKILL.md +1 -0
- package/skills/test/SKILL.md +587 -585
- package/skills/verification/SKILL.md +1 -0
- package/skills/watchdog/SKILL.md +2 -0
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<p align="center">
|
|
6
6
|
<strong>Less skills. Deeper connections.</strong><br>
|
|
7
7
|
A lean, interconnected skill ecosystem for AI coding assistants.<br>
|
|
8
|
-
|
|
8
|
+
61 skills · 200+ mesh connections · 8 platforms · MIT
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
Most skill ecosystems are either **too many isolated skills** (540+ that don't talk to each other) or **rigid pipelines** (A → B → C, if B fails everything stops).
|
|
24
24
|
|
|
25
|
-
Rune is a **mesh** —
|
|
25
|
+
Rune is a **mesh** — 61 skills with 200+ connections across a 5-layer architecture. Skills call each other bidirectionally, forming resilient workflows that adapt when things go wrong.
|
|
26
26
|
|
|
27
27
|
```
|
|
28
28
|
Pipeline: A → B → C → D (B fails = stuck)
|
|
@@ -82,19 +82,34 @@ _Methodology: Claude Code CLI headless mode (`claude -p --output-format json`),
|
|
|
82
82
|
|
|
83
83
|
---
|
|
84
84
|
|
|
85
|
-
## What's New (v2.
|
|
85
|
+
## What's New (v2.6.0)
|
|
86
86
|
|
|
87
|
-
- **
|
|
88
|
-
- **
|
|
89
|
-
- **
|
|
90
|
-
- **
|
|
91
|
-
- **
|
|
92
|
-
- **
|
|
93
|
-
- **
|
|
87
|
+
- **Mesh Signals** — event-driven skill communication via frontmatter. Skills declare `emit` and `listen` signals; compiler builds a signal graph in `skill-index.json`. 17 signals across 15 core skills. Completes the spec-kit trilogy: Tier Override → Mesh Contract → Mesh Signals
|
|
88
|
+
- **Signal Validation** — `scripts/validate-signals.js` checks orphan listeners (hard error), unlistened emitters (warning), signal naming conventions
|
|
89
|
+
- **Mesh Contract** (v2.5.0) — `.rune/contract.md` project-level invariants enforced by cook + sentinel as hard gates
|
|
90
|
+
- **Compiled Intent Mesh** (v2.5.0) — compile-time `skill-index.json` with intent keywords, mesh graph, chain predictions
|
|
91
|
+
- **Tier Override** — Pro/Business packs override Free packs with skill-level merging
|
|
92
|
+
- **Scripts Bundling** — compiler copies `scripts/` directories, resolves `{scripts_dir}` placeholders
|
|
93
|
+
- **566 Tests** — compiler + signals + hooks + scripts validation
|
|
94
|
+
|
|
95
|
+
### Signal Graph
|
|
96
|
+
|
|
97
|
+
Skills communicate through declarative signals — no runtime event bus, just metadata for discovery, validation, and routing:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
scout ──emit:codebase.scanned──→ plan, brainstorm
|
|
101
|
+
fix ────emit:code.changed──────→ test, sentinel, review, preflight, verification
|
|
102
|
+
test ───emit:tests.passed──────→ deploy
|
|
103
|
+
test ───emit:tests.failed──────→ debug
|
|
104
|
+
sentinel─emit:security.passed──→ deploy
|
|
105
|
+
debug ──emit:bug.diagnosed─────→ fix
|
|
106
|
+
deploy ─emit:deploy.complete───→ watchdog
|
|
107
|
+
cook ───emit:phase.complete────→ session-bridge
|
|
108
|
+
```
|
|
94
109
|
|
|
95
110
|
## What Rune Is (and Isn't)
|
|
96
111
|
|
|
97
|
-
Rune started as a **Claude Code plugin** and now compiles to **every major AI IDE**. Same
|
|
112
|
+
Rune started as a **Claude Code plugin** and now compiles to **every major AI IDE**. Same 61 skills, same mesh connections, same workflows — zero knowledge loss across platforms.
|
|
98
113
|
|
|
99
114
|
| | Rune Provides | Claude Code Provides |
|
|
100
115
|
|---|---|---|
|
|
@@ -118,7 +133,7 @@ Rune started as a **Claude Code plugin** and now compiles to **every major AI ID
|
|
|
118
133
|
| CI quality gates | `verification` skill: lint + typecheck + tests + build (actual commands, not LLM review) |
|
|
119
134
|
| Memory / state | `session-bridge` + `journal`: cross-session decisions, conventions, ADRs, module health |
|
|
120
135
|
| Multi-model strategy | Every skill has assigned model: haiku (scan), sonnet (code), opus (architecture) |
|
|
121
|
-
| Agent specialization |
|
|
136
|
+
| Agent specialization | 61 specialized skills with dedicated roles (architect, coder, reviewer, scanner, researcher, BA, scaffolder) — each runs as a Task agent via Claude Code |
|
|
122
137
|
| Security scanning | `sentinel`: OWASP patterns, secret scanning, dependency audit. `sast`: static analysis |
|
|
123
138
|
|
|
124
139
|
## Install
|
|
@@ -146,13 +161,13 @@ npx @rune-kit/rune init --platform windsurf
|
|
|
146
161
|
npx @rune-kit/rune init --platform antigravity
|
|
147
162
|
```
|
|
148
163
|
|
|
149
|
-
This compiles all
|
|
164
|
+
This compiles all 61 skills into your IDE's rules format. Same knowledge, same workflows.
|
|
150
165
|
|
|
151
166
|
### Platform Comparison
|
|
152
167
|
|
|
153
168
|
| Feature | Claude Code | Cursor / Windsurf / Others |
|
|
154
169
|
|---------|-------------|---------------------------|
|
|
155
|
-
| Skills available |
|
|
170
|
+
| Skills available | 61/61 | 61/61 |
|
|
156
171
|
| Mesh connections | 200+ (programmatic) | 200+ (rule references) |
|
|
157
172
|
| Workflows & HARD-GATEs | Full | Full |
|
|
158
173
|
| Extension packs | 14 | 14 |
|
|
@@ -229,8 +244,9 @@ This compiles all 59 skills into your IDE's rules format. Same knowledge, same w
|
|
|
229
244
|
║ Delivery: deploy │ marketing │ incident │ docs ║
|
|
230
245
|
║ Rescue: autopsy │ safeguard │ surgeon ║
|
|
231
246
|
║ Security: adversary ║
|
|
247
|
+
║ Velocity: retro ║
|
|
232
248
|
╠══════════════════════════════════════════════════════╣
|
|
233
|
-
║ L3: UTILITIES (
|
|
249
|
+
║ L3: UTILITIES (27) ║
|
|
234
250
|
║ Stateless, pure capabilities ║
|
|
235
251
|
║ ║
|
|
236
252
|
║ Knowledge: research │ docs-seeker │ trend-scout ║
|
|
@@ -249,6 +265,8 @@ This compiles all 59 skills into your IDE's rules format. Same knowledge, same w
|
|
|
249
265
|
║ Documents: doc-processor ║
|
|
250
266
|
║ Security: sentinel-env ║
|
|
251
267
|
║ Memory: neural-memory ║
|
|
268
|
+
║ Packs: context-pack ║
|
|
269
|
+
║ Slides: slides ║
|
|
252
270
|
╠══════════════════════════════════════════════════════╣
|
|
253
271
|
║ L4: EXTENSION PACKS (14) ║
|
|
254
272
|
║ Domain-specific, install what you need ║
|
|
@@ -426,13 +444,14 @@ See [docs/MULTI-PLATFORM.md](docs/MULTI-PLATFORM.md) for the full architecture.
|
|
|
426
444
|
## Numbers
|
|
427
445
|
|
|
428
446
|
```
|
|
429
|
-
Core Skills:
|
|
447
|
+
Core Skills: 61 (L0: 1 │ L1: 5 │ L2: 28 │ L3: 27)
|
|
430
448
|
Extension Packs: 14 free + 4 pro + 4 business
|
|
431
449
|
Mesh Connections: 200+ cross-references
|
|
450
|
+
Mesh Signals: 17 signals across 15 skills (emit/listen graph)
|
|
432
451
|
Connections/Skill: 3.4 avg
|
|
433
452
|
Platforms: 8 (Claude Code, Cursor, Windsurf, Antigravity, Codex, OpenCode, OpenClaw, Generic)
|
|
434
453
|
Compiler: ~1400 LOC (parser + 8 transforms + 8 adapters + CLI)
|
|
435
|
-
|
|
454
|
+
Tests: 566 (compiler + signals + hooks + scripts)
|
|
436
455
|
Pack Depth: 22 packs total (14 free + 4 pro + 4 business, all free packs rated Deep)
|
|
437
456
|
```
|
|
438
457
|
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import assert from 'node:assert';
|
|
2
2
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
3
5
|
import path from 'node:path';
|
|
4
|
-
import { test } from 'node:test';
|
|
6
|
+
import { describe, test } from 'node:test';
|
|
5
7
|
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { getAdapter } from '../adapters/index.js';
|
|
9
|
+
import { buildAll } from '../emitter.js';
|
|
6
10
|
import { parsePack } from '../parser.js';
|
|
7
11
|
|
|
8
12
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -143,3 +147,139 @@ test('parsePack: real backend PACK.md parses as split (post-split)', () => {
|
|
|
143
147
|
assert.strictEqual(parsed.layer, 'L4');
|
|
144
148
|
assert.ok(parsed.sections.size > 0);
|
|
145
149
|
});
|
|
150
|
+
|
|
151
|
+
// --- Integration: buildAll auto-discovers split pack skill files ---
|
|
152
|
+
|
|
153
|
+
describe('buildAll split pack auto-discovery', () => {
|
|
154
|
+
async function createTempWithSplitPack() {
|
|
155
|
+
const tmp = path.join(tmpdir(), `rune-split-test-${Date.now()}`);
|
|
156
|
+
const skillsDir = path.join(tmp, 'skills', 'test-skill');
|
|
157
|
+
const extDir = path.join(tmp, 'extensions', 'test-pack');
|
|
158
|
+
const extSkillsDir = path.join(extDir, 'skills');
|
|
159
|
+
|
|
160
|
+
await mkdir(skillsDir, { recursive: true });
|
|
161
|
+
await mkdir(extSkillsDir, { recursive: true });
|
|
162
|
+
|
|
163
|
+
// Minimal core skill (required for build)
|
|
164
|
+
await writeFile(
|
|
165
|
+
path.join(skillsDir, 'SKILL.md'),
|
|
166
|
+
[
|
|
167
|
+
'---',
|
|
168
|
+
'name: test-skill',
|
|
169
|
+
'description: "A test skill"',
|
|
170
|
+
'layer: L3',
|
|
171
|
+
'group: utility',
|
|
172
|
+
'connections: []',
|
|
173
|
+
'tags: [test]',
|
|
174
|
+
'---',
|
|
175
|
+
'',
|
|
176
|
+
'# test-skill',
|
|
177
|
+
'',
|
|
178
|
+
'Hello.',
|
|
179
|
+
].join('\n'),
|
|
180
|
+
'utf-8',
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
// Split pack WITHOUT skills: array in frontmatter
|
|
184
|
+
await writeFile(
|
|
185
|
+
path.join(extDir, 'PACK.md'),
|
|
186
|
+
[
|
|
187
|
+
'---',
|
|
188
|
+
'name: "@rune/test-pack"',
|
|
189
|
+
'description: "Test pack"',
|
|
190
|
+
'metadata:',
|
|
191
|
+
' version: "0.1.0"',
|
|
192
|
+
' format: split',
|
|
193
|
+
'---',
|
|
194
|
+
'',
|
|
195
|
+
'# @rune/test-pack',
|
|
196
|
+
'',
|
|
197
|
+
'Pack index body.',
|
|
198
|
+
].join('\n'),
|
|
199
|
+
'utf-8',
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
// Two skill files in skills/ subdir
|
|
203
|
+
await writeFile(
|
|
204
|
+
path.join(extSkillsDir, 'alpha.md'),
|
|
205
|
+
[
|
|
206
|
+
'---',
|
|
207
|
+
'name: "alpha"',
|
|
208
|
+
'description: "Alpha skill"',
|
|
209
|
+
'model: sonnet',
|
|
210
|
+
'---',
|
|
211
|
+
'',
|
|
212
|
+
'# alpha',
|
|
213
|
+
'',
|
|
214
|
+
'Alpha body content here.',
|
|
215
|
+
].join('\n'),
|
|
216
|
+
'utf-8',
|
|
217
|
+
);
|
|
218
|
+
await writeFile(
|
|
219
|
+
path.join(extSkillsDir, 'beta.md'),
|
|
220
|
+
[
|
|
221
|
+
'---',
|
|
222
|
+
'name: "beta"',
|
|
223
|
+
'description: "Beta skill"',
|
|
224
|
+
'model: sonnet',
|
|
225
|
+
'---',
|
|
226
|
+
'',
|
|
227
|
+
'# beta',
|
|
228
|
+
'',
|
|
229
|
+
'Beta body content here.',
|
|
230
|
+
].join('\n'),
|
|
231
|
+
'utf-8',
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
return tmp;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
test('auto-discovers skill files from skills/ subdir when manifest is empty', async () => {
|
|
238
|
+
const tmp = await createTempWithSplitPack();
|
|
239
|
+
try {
|
|
240
|
+
const outputRoot = path.join(tmp, 'out');
|
|
241
|
+
const adapter = getAdapter('cursor');
|
|
242
|
+
const stats = await buildAll({ runeRoot: tmp, outputRoot, adapter });
|
|
243
|
+
|
|
244
|
+
assert.strictEqual(stats.packCount, 1, 'should build 1 pack');
|
|
245
|
+
|
|
246
|
+
// Read the compiled pack output
|
|
247
|
+
const packOutput = await readFile(path.join(outputRoot, adapter.outputDir, 'rune-ext-test-pack.mdc'), 'utf-8');
|
|
248
|
+
|
|
249
|
+
// Should contain the pack index body
|
|
250
|
+
assert.ok(packOutput.includes('Pack index body'), 'missing pack index body');
|
|
251
|
+
|
|
252
|
+
// Should contain BOTH auto-discovered skill bodies
|
|
253
|
+
assert.ok(packOutput.includes('Alpha body content here'), 'missing alpha skill body');
|
|
254
|
+
assert.ok(packOutput.includes('Beta body content here'), 'missing beta skill body');
|
|
255
|
+
} finally {
|
|
256
|
+
await rm(tmp, { recursive: true, force: true });
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test('real ai-ml pack includes skill content after build', async () => {
|
|
261
|
+
const aimlPath = path.join(EXTENSIONS_DIR, 'ai-ml', 'PACK.md');
|
|
262
|
+
if (!existsSync(aimlPath)) {
|
|
263
|
+
console.log(' skip: ai-ml PACK.md not found');
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const tmp = path.join(tmpdir(), `rune-aiml-test-${Date.now()}`);
|
|
268
|
+
const runeRoot = path.resolve(__dirname, '../..');
|
|
269
|
+
const adapter = getAdapter('cursor');
|
|
270
|
+
await buildAll({ runeRoot, outputRoot: tmp, adapter });
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
const packOutput = await readFile(path.join(tmp, adapter.outputDir, 'rune-ext-ai-ml.mdc'), 'utf-8');
|
|
274
|
+
|
|
275
|
+
// Should be significantly longer than just the index body (~99 lines without skills)
|
|
276
|
+
const lineCount = packOutput.split('\n').length;
|
|
277
|
+
assert.ok(lineCount > 200, `ai-ml pack output too short (${lineCount} lines) — skills likely not included`);
|
|
278
|
+
|
|
279
|
+
// Should contain content from individual skill files
|
|
280
|
+
assert.ok(packOutput.includes('ai-agents') || packOutput.includes('AI agent'), 'missing ai-agents skill content');
|
|
281
|
+
} finally {
|
|
282
|
+
await rm(tmp, { recursive: true, force: true });
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
});
|
|
@@ -1,55 +1,147 @@
|
|
|
1
|
-
import assert from 'node:assert';
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { test } from 'node:test';
|
|
5
|
-
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import { parseSkill } from '../parser.js';
|
|
7
|
-
|
|
8
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
-
const SKILLS_DIR = path.resolve(__dirname, '../../skills');
|
|
10
|
-
|
|
11
|
-
test('parse cook SKILL.md', () => {
|
|
12
|
-
const content = readFileSync(path.join(SKILLS_DIR, 'cook/SKILL.md'), 'utf-8');
|
|
13
|
-
const parsed = parseSkill(content, 'cook/SKILL.md');
|
|
14
|
-
|
|
15
|
-
console.log('name:', JSON.stringify(parsed.name));
|
|
16
|
-
console.log('description:', JSON.stringify(parsed.description?.substring(0, 50)));
|
|
17
|
-
console.log('layer:', parsed.layer);
|
|
18
|
-
console.log('model:', parsed.model);
|
|
19
|
-
console.log('group:', parsed.group);
|
|
20
|
-
console.log('contextFork:', parsed.contextFork);
|
|
21
|
-
console.log('agentType:', parsed.agentType);
|
|
22
|
-
console.log('crossRefs:', parsed.crossRefs.length);
|
|
23
|
-
console.log('toolRefs:', parsed.toolRefs.length);
|
|
24
|
-
console.log('hardGates:', parsed.hardGates.length);
|
|
25
|
-
console.log('sections:', [...parsed.sections.keys()]);
|
|
26
|
-
console.log('frontmatter keys:', Object.keys(parsed.frontmatter));
|
|
27
|
-
|
|
28
|
-
assert.strictEqual(parsed.name, 'cook');
|
|
29
|
-
assert.strictEqual(parsed.layer, 'L1');
|
|
30
|
-
assert.ok(parsed.crossRefs.length > 0);
|
|
31
|
-
assert.ok(parsed.hardGates.length > 0);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test('parse fix SKILL.md', () => {
|
|
35
|
-
const content = readFileSync(path.join(SKILLS_DIR, 'fix/SKILL.md'), 'utf-8');
|
|
36
|
-
const parsed = parseSkill(content, 'fix/SKILL.md');
|
|
37
|
-
|
|
38
|
-
console.log('fix name:', JSON.stringify(parsed.name));
|
|
39
|
-
console.log('fix layer:', parsed.layer);
|
|
40
|
-
|
|
41
|
-
assert.strictEqual(parsed.name, 'fix');
|
|
42
|
-
assert.strictEqual(parsed.layer, 'L2');
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test('parse verification SKILL.md', () => {
|
|
46
|
-
const content = readFileSync(path.join(SKILLS_DIR, 'verification/SKILL.md'), 'utf-8');
|
|
47
|
-
const parsed = parseSkill(content, 'verification/SKILL.md');
|
|
48
|
-
|
|
49
|
-
console.log('verification name:', JSON.stringify(parsed.name));
|
|
50
|
-
console.log('verification layer:', parsed.layer);
|
|
51
|
-
console.log('verification toolRefs:', parsed.toolRefs.length);
|
|
52
|
-
|
|
53
|
-
assert.strictEqual(parsed.name, 'verification');
|
|
54
|
-
assert.strictEqual(parsed.layer, 'L3');
|
|
55
|
-
});
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { describe, test } from 'node:test';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { parseSkill } from '../parser.js';
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const SKILLS_DIR = path.resolve(__dirname, '../../skills');
|
|
10
|
+
|
|
11
|
+
test('parse cook SKILL.md', () => {
|
|
12
|
+
const content = readFileSync(path.join(SKILLS_DIR, 'cook/SKILL.md'), 'utf-8');
|
|
13
|
+
const parsed = parseSkill(content, 'cook/SKILL.md');
|
|
14
|
+
|
|
15
|
+
console.log('name:', JSON.stringify(parsed.name));
|
|
16
|
+
console.log('description:', JSON.stringify(parsed.description?.substring(0, 50)));
|
|
17
|
+
console.log('layer:', parsed.layer);
|
|
18
|
+
console.log('model:', parsed.model);
|
|
19
|
+
console.log('group:', parsed.group);
|
|
20
|
+
console.log('contextFork:', parsed.contextFork);
|
|
21
|
+
console.log('agentType:', parsed.agentType);
|
|
22
|
+
console.log('crossRefs:', parsed.crossRefs.length);
|
|
23
|
+
console.log('toolRefs:', parsed.toolRefs.length);
|
|
24
|
+
console.log('hardGates:', parsed.hardGates.length);
|
|
25
|
+
console.log('sections:', [...parsed.sections.keys()]);
|
|
26
|
+
console.log('frontmatter keys:', Object.keys(parsed.frontmatter));
|
|
27
|
+
|
|
28
|
+
assert.strictEqual(parsed.name, 'cook');
|
|
29
|
+
assert.strictEqual(parsed.layer, 'L1');
|
|
30
|
+
assert.ok(parsed.crossRefs.length > 0);
|
|
31
|
+
assert.ok(parsed.hardGates.length > 0);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('parse fix SKILL.md', () => {
|
|
35
|
+
const content = readFileSync(path.join(SKILLS_DIR, 'fix/SKILL.md'), 'utf-8');
|
|
36
|
+
const parsed = parseSkill(content, 'fix/SKILL.md');
|
|
37
|
+
|
|
38
|
+
console.log('fix name:', JSON.stringify(parsed.name));
|
|
39
|
+
console.log('fix layer:', parsed.layer);
|
|
40
|
+
|
|
41
|
+
assert.strictEqual(parsed.name, 'fix');
|
|
42
|
+
assert.strictEqual(parsed.layer, 'L2');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('parse verification SKILL.md', () => {
|
|
46
|
+
const content = readFileSync(path.join(SKILLS_DIR, 'verification/SKILL.md'), 'utf-8');
|
|
47
|
+
const parsed = parseSkill(content, 'verification/SKILL.md');
|
|
48
|
+
|
|
49
|
+
console.log('verification name:', JSON.stringify(parsed.name));
|
|
50
|
+
console.log('verification layer:', parsed.layer);
|
|
51
|
+
console.log('verification toolRefs:', parsed.toolRefs.length);
|
|
52
|
+
|
|
53
|
+
assert.strictEqual(parsed.name, 'verification');
|
|
54
|
+
assert.strictEqual(parsed.layer, 'L3');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('signals parsing', () => {
|
|
58
|
+
test('parses emit and listen from metadata', () => {
|
|
59
|
+
const content = [
|
|
60
|
+
'---',
|
|
61
|
+
'name: alpha',
|
|
62
|
+
'description: "Test skill"',
|
|
63
|
+
'metadata:',
|
|
64
|
+
' layer: L2',
|
|
65
|
+
' emit: code.changed, tests.passed',
|
|
66
|
+
' listen: plan.ready',
|
|
67
|
+
'---',
|
|
68
|
+
'',
|
|
69
|
+
'# alpha',
|
|
70
|
+
].join('\n');
|
|
71
|
+
|
|
72
|
+
const parsed = parseSkill(content);
|
|
73
|
+
assert.ok(parsed.signals, 'signals should not be null');
|
|
74
|
+
assert.deepStrictEqual(parsed.signals.emit, ['code.changed', 'tests.passed']);
|
|
75
|
+
assert.deepStrictEqual(parsed.signals.listen, ['plan.ready']);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('emit only — no listen', () => {
|
|
79
|
+
const content = [
|
|
80
|
+
'---',
|
|
81
|
+
'name: beta',
|
|
82
|
+
'description: "Emitter only"',
|
|
83
|
+
'metadata:',
|
|
84
|
+
' layer: L3',
|
|
85
|
+
' emit: deploy.complete',
|
|
86
|
+
'---',
|
|
87
|
+
'',
|
|
88
|
+
'# beta',
|
|
89
|
+
].join('\n');
|
|
90
|
+
|
|
91
|
+
const parsed = parseSkill(content);
|
|
92
|
+
assert.ok(parsed.signals);
|
|
93
|
+
assert.deepStrictEqual(parsed.signals.emit, ['deploy.complete']);
|
|
94
|
+
assert.deepStrictEqual(parsed.signals.listen, []);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('listen only — no emit', () => {
|
|
98
|
+
const content = [
|
|
99
|
+
'---',
|
|
100
|
+
'name: gamma',
|
|
101
|
+
'description: "Listener only"',
|
|
102
|
+
'metadata:',
|
|
103
|
+
' layer: L3',
|
|
104
|
+
' listen: code.changed',
|
|
105
|
+
'---',
|
|
106
|
+
'',
|
|
107
|
+
'# gamma',
|
|
108
|
+
].join('\n');
|
|
109
|
+
|
|
110
|
+
const parsed = parseSkill(content);
|
|
111
|
+
assert.ok(parsed.signals);
|
|
112
|
+
assert.deepStrictEqual(parsed.signals.emit, []);
|
|
113
|
+
assert.deepStrictEqual(parsed.signals.listen, ['code.changed']);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('no signals — returns null', () => {
|
|
117
|
+
const content = [
|
|
118
|
+
'---',
|
|
119
|
+
'name: delta',
|
|
120
|
+
'description: "No signals"',
|
|
121
|
+
'metadata:',
|
|
122
|
+
' layer: L3',
|
|
123
|
+
'---',
|
|
124
|
+
'',
|
|
125
|
+
'# delta',
|
|
126
|
+
].join('\n');
|
|
127
|
+
|
|
128
|
+
const parsed = parseSkill(content);
|
|
129
|
+
assert.strictEqual(parsed.signals, null);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('real cook skill has signals', () => {
|
|
133
|
+
const content = readFileSync(path.join(SKILLS_DIR, 'cook/SKILL.md'), 'utf-8');
|
|
134
|
+
const parsed = parseSkill(content, 'cook/SKILL.md');
|
|
135
|
+
assert.ok(parsed.signals, 'cook should have signals');
|
|
136
|
+
assert.ok(parsed.signals.emit.includes('phase.complete'));
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('real test skill has emit and listen', () => {
|
|
140
|
+
const content = readFileSync(path.join(SKILLS_DIR, 'test/SKILL.md'), 'utf-8');
|
|
141
|
+
const parsed = parseSkill(content, 'test/SKILL.md');
|
|
142
|
+
assert.ok(parsed.signals, 'test should have signals');
|
|
143
|
+
assert.ok(parsed.signals.emit.includes('tests.passed'));
|
|
144
|
+
assert.ok(parsed.signals.emit.includes('tests.failed'));
|
|
145
|
+
assert.ok(parsed.signals.listen.includes('code.changed'));
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -138,11 +138,7 @@ describe('buildAll with scripts', () => {
|
|
|
138
138
|
|
|
139
139
|
// Minimal .claude-plugin for openclaw
|
|
140
140
|
await mkdir(path.join(tmp, '.claude-plugin'), { recursive: true });
|
|
141
|
-
await writeFile(
|
|
142
|
-
path.join(tmp, '.claude-plugin', 'plugin.json'),
|
|
143
|
-
JSON.stringify({ version: '0.0.1' }),
|
|
144
|
-
'utf-8',
|
|
145
|
-
);
|
|
141
|
+
await writeFile(path.join(tmp, '.claude-plugin', 'plugin.json'), JSON.stringify({ version: '0.0.1' }), 'utf-8');
|
|
146
142
|
|
|
147
143
|
return tmp;
|
|
148
144
|
}
|
|
@@ -168,7 +164,10 @@ describe('buildAll with scripts', () => {
|
|
|
168
164
|
assert.ok(mdc.includes('.cursor/rules/rune-test-slide-scripts/build-deck.js'), 'resolved path missing');
|
|
169
165
|
|
|
170
166
|
// Plain skill: no scripts dir created
|
|
171
|
-
assert.ok(
|
|
167
|
+
assert.ok(
|
|
168
|
+
!existsSync(path.join(outputRoot, adapter.outputDir, 'rune-test-plain-scripts')),
|
|
169
|
+
'plain skill should not have scripts dir',
|
|
170
|
+
);
|
|
172
171
|
} finally {
|
|
173
172
|
await rm(tmp, { recursive: true, force: true });
|
|
174
173
|
}
|
|
@@ -189,10 +188,7 @@ describe('buildAll with scripts', () => {
|
|
|
189
188
|
assert.ok(existsSync(path.join(scriptsOut, 'helper.py')), 'helper.py missing');
|
|
190
189
|
|
|
191
190
|
// Placeholder resolved
|
|
192
|
-
const md = await readFile(
|
|
193
|
-
path.join(outputRoot, adapter.outputDir, 'rune-test-slide', 'SKILL.md'),
|
|
194
|
-
'utf-8',
|
|
195
|
-
);
|
|
191
|
+
const md = await readFile(path.join(outputRoot, adapter.outputDir, 'rune-test-slide', 'SKILL.md'), 'utf-8');
|
|
196
192
|
assert.ok(!md.includes('{scripts_dir}'), 'placeholder should be resolved');
|
|
197
193
|
assert.ok(md.includes('.codex/skills/rune-test-slide/scripts/build-deck.js'), 'resolved path missing');
|
|
198
194
|
} finally {
|
|
@@ -254,7 +250,10 @@ describe('buildAll with scripts', () => {
|
|
|
254
250
|
await buildAll({ runeRoot: tmp, outputRoot, adapter });
|
|
255
251
|
|
|
256
252
|
const original = await readFile(path.join(tmp, 'skills', 'test-slide', 'scripts', 'build-deck.js'), 'utf-8');
|
|
257
|
-
const copied = await readFile(
|
|
253
|
+
const copied = await readFile(
|
|
254
|
+
path.join(outputRoot, adapter.outputDir, 'rune-test-slide-scripts', 'build-deck.js'),
|
|
255
|
+
'utf-8',
|
|
256
|
+
);
|
|
258
257
|
assert.strictEqual(copied, original, 'script content should be identical');
|
|
259
258
|
} finally {
|
|
260
259
|
await rm(tmp, { recursive: true, force: true });
|