@rune-kit/rune 2.14.0 → 2.16.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 +31 -11
- package/compiler/__tests__/adapter-model-mapping.test.js +152 -0
- package/compiler/__tests__/context-preview-signal.test.js +166 -0
- package/compiler/__tests__/oracle-bundle-format.test.js +210 -0
- package/compiler/__tests__/oracle-pending-schema.test.js +209 -0
- package/compiler/__tests__/skill-description-quality.test.js +149 -0
- package/compiler/adapters/antigravity.js +17 -1
- package/compiler/adapters/claude.js +4 -0
- package/compiler/adapters/codex.js +16 -1
- package/compiler/adapters/cursor.js +4 -0
- package/compiler/adapters/generic.js +14 -1
- package/compiler/adapters/openclaw.js +15 -1
- package/compiler/adapters/opencode.js +17 -1
- package/compiler/adapters/windsurf.js +4 -0
- package/package.json +2 -1
- package/skills/adversary/SKILL.md +61 -1
- package/skills/adversary/evals.md +79 -0
- package/skills/adversary/references/context-bundle-format.md +122 -0
- package/skills/adversary/references/oracle-mode.md +116 -0
- package/skills/asset-creator/SKILL.md +1 -1
- package/skills/audit/SKILL.md +2 -1
- package/skills/autopsy/SKILL.md +1 -1
- package/skills/ba/SKILL.md +73 -5
- package/skills/ba/references/synthesis-mode.md +105 -0
- package/skills/brainstorm/SKILL.md +1 -1
- package/skills/browser-pilot/SKILL.md +1 -1
- package/skills/completion-gate/SKILL.md +1 -1
- package/skills/constraint-check/SKILL.md +1 -1
- package/skills/context-engine/SKILL.md +119 -2
- package/skills/context-engine/references/caveman-mode.md +107 -0
- package/skills/context-engine/references/preview-gate.md +139 -0
- package/skills/context-pack/SKILL.md +22 -1
- package/skills/context-pack/references/agent-brief.md +194 -0
- package/skills/cook/SKILL.md +3 -2
- package/skills/db/SKILL.md +1 -1
- package/skills/debug/SKILL.md +22 -6
- package/skills/debug/references/feedback-loop-ladder.md +62 -0
- package/skills/dependency-doctor/SKILL.md +1 -1
- package/skills/design/SKILL.md +1 -1
- package/skills/doc-processor/SKILL.md +1 -1
- package/skills/docs/SKILL.md +1 -1
- package/skills/docs-seeker/SKILL.md +1 -1
- package/skills/fix/SKILL.md +4 -3
- package/skills/git/SKILL.md +1 -1
- package/skills/hallucination-guard/SKILL.md +1 -1
- package/skills/integrity-check/SKILL.md +1 -1
- package/skills/journal/SKILL.md +1 -1
- package/skills/logic-guardian/SKILL.md +1 -1
- package/skills/marketing/SKILL.md +1 -1
- package/skills/mcp-builder/SKILL.md +1 -1
- package/skills/onboard/SKILL.md +1 -1
- package/skills/perf/SKILL.md +1 -1
- package/skills/plan/SKILL.md +10 -5
- package/skills/plan/references/vertical-slice.md +79 -0
- package/skills/preflight/SKILL.md +1 -1
- package/skills/research/SKILL.md +1 -1
- package/skills/retro/SKILL.md +1 -1
- package/skills/review/SKILL.md +2 -2
- package/skills/review-intake/SKILL.md +27 -7
- package/skills/review-intake/references/issue-triage.md +202 -0
- package/skills/safeguard/SKILL.md +1 -1
- package/skills/scaffold/SKILL.md +1 -1
- package/skills/sentinel/SKILL.md +1 -1
- package/skills/sentinel-env/SKILL.md +1 -1
- package/skills/session-bridge/SKILL.md +91 -4
- package/skills/session-bridge/references/detach-protocol.md +111 -0
- package/skills/skill-forge/SKILL.md +1 -1
- package/skills/slides/SKILL.md +1 -1
- package/skills/surgeon/SKILL.md +3 -1
- package/skills/team/SKILL.md +5 -0
- package/skills/trend-scout/SKILL.md +1 -1
- package/skills/video-creator/SKILL.md +1 -1
- package/skills/watchdog/SKILL.md +1 -1
- package/skills/worktree/SKILL.md +1 -1
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Oracle-pending record schema (v2.15+) — verify the JSON schema written by
|
|
3
|
+
* session-bridge detach mode. The pending file in .rune/oracle-pending/<id>.json
|
|
4
|
+
* is the rendezvous point between adversary (dispatcher) and primary agent
|
|
5
|
+
* (reattach poller).
|
|
6
|
+
*
|
|
7
|
+
* Validates:
|
|
8
|
+
* - Required fields present + correct type
|
|
9
|
+
* - sessionId pattern: oracle-<unix-ms>-<rand>
|
|
10
|
+
* - bundleHash pattern: sha256:<hex>
|
|
11
|
+
* - status enum: pending | complete | failed
|
|
12
|
+
* - sourceSkill enum: debug | fix | manual
|
|
13
|
+
* - Idempotency: same bundleHash returns same sessionId
|
|
14
|
+
* - Timeout transition: pending → failed when now > timeoutAt
|
|
15
|
+
* - Cleanup: records older than 24h dropped
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import assert from 'node:assert';
|
|
19
|
+
import { describe, test } from 'node:test';
|
|
20
|
+
|
|
21
|
+
const SESSION_ID_PATTERN = /^oracle-\d+-[a-z0-9]+$/;
|
|
22
|
+
const BUNDLE_HASH_PATTERN = /^sha256:[a-f0-9]{8,64}$/;
|
|
23
|
+
const STATUS_VALUES = new Set(['pending', 'complete', 'failed']);
|
|
24
|
+
const SOURCE_SKILL_VALUES = new Set(['debug', 'fix', 'manual']);
|
|
25
|
+
|
|
26
|
+
function isIso8601(s) {
|
|
27
|
+
if (typeof s !== 'string') return false;
|
|
28
|
+
const d = new Date(s);
|
|
29
|
+
return !Number.isNaN(d.getTime()) && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(s);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function validatePendingRecord(rec) {
|
|
33
|
+
const errors = [];
|
|
34
|
+
if (typeof rec.sessionId !== 'string' || !SESSION_ID_PATTERN.test(rec.sessionId)) errors.push('invalid_sessionId');
|
|
35
|
+
if (!isIso8601(rec.dispatchedAt)) errors.push('invalid_dispatchedAt');
|
|
36
|
+
if (typeof rec.triggerSignal !== 'string') errors.push('invalid_triggerSignal');
|
|
37
|
+
if (!SOURCE_SKILL_VALUES.has(rec.sourceSkill)) errors.push('invalid_sourceSkill');
|
|
38
|
+
if (typeof rec.targetModel !== 'string') errors.push('invalid_targetModel');
|
|
39
|
+
if (typeof rec.bundleHash !== 'string' || !BUNDLE_HASH_PATTERN.test(rec.bundleHash))
|
|
40
|
+
errors.push('invalid_bundleHash');
|
|
41
|
+
if (!STATUS_VALUES.has(rec.status)) errors.push('invalid_status');
|
|
42
|
+
if (!isIso8601(rec.timeoutAt)) errors.push('invalid_timeoutAt');
|
|
43
|
+
|
|
44
|
+
// Conditional fields
|
|
45
|
+
if (rec.status === 'complete') {
|
|
46
|
+
if (rec.responseId === null || typeof rec.responseId !== 'string') errors.push('missing_responseId_when_complete');
|
|
47
|
+
if (rec.responseExcerpt === null || typeof rec.responseExcerpt !== 'string')
|
|
48
|
+
errors.push('missing_responseExcerpt_when_complete');
|
|
49
|
+
if (typeof rec.responseExcerpt === 'string' && rec.responseExcerpt.length > 500)
|
|
50
|
+
errors.push('responseExcerpt_too_long');
|
|
51
|
+
}
|
|
52
|
+
if (rec.status === 'pending') {
|
|
53
|
+
if (rec.responseId !== null) errors.push('responseId_must_be_null_when_pending');
|
|
54
|
+
if (rec.responseExcerpt !== null) errors.push('responseExcerpt_must_be_null_when_pending');
|
|
55
|
+
}
|
|
56
|
+
return { valid: errors.length === 0, errors };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function timeoutCheck(record, now) {
|
|
60
|
+
if (record.status !== 'pending') return record;
|
|
61
|
+
if (new Date(now).getTime() >= new Date(record.timeoutAt).getTime()) {
|
|
62
|
+
return { ...record, status: 'failed' };
|
|
63
|
+
}
|
|
64
|
+
return record;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function cleanupExpired(records, now, ttlMs = 24 * 60 * 60 * 1000) {
|
|
68
|
+
return records.filter((r) => new Date(now).getTime() - new Date(r.dispatchedAt).getTime() <= ttlMs);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function buildPending(overrides = {}) {
|
|
72
|
+
return {
|
|
73
|
+
sessionId: 'oracle-1714234500-abc123',
|
|
74
|
+
dispatchedAt: '2026-04-27T12:34:56Z',
|
|
75
|
+
triggerSignal: 'agent.stuck',
|
|
76
|
+
sourceSkill: 'debug',
|
|
77
|
+
targetModel: 'claude-opus-4-7',
|
|
78
|
+
bundleHash: 'sha256:9f3a4b5c6d7e8f90',
|
|
79
|
+
status: 'pending',
|
|
80
|
+
timeoutAt: '2026-04-27T12:44:56Z',
|
|
81
|
+
responseId: null,
|
|
82
|
+
responseExcerpt: null,
|
|
83
|
+
...overrides,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
describe('Pending record schema', () => {
|
|
88
|
+
test('valid pending record passes', () => {
|
|
89
|
+
const result = validatePendingRecord(buildPending());
|
|
90
|
+
assert.strictEqual(result.valid, true, `unexpected errors: ${result.errors.join(', ')}`);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('valid complete record passes', () => {
|
|
94
|
+
const result = validatePendingRecord(
|
|
95
|
+
buildPending({
|
|
96
|
+
status: 'complete',
|
|
97
|
+
responseId: 'resp_xyz789',
|
|
98
|
+
responseExcerpt: 'Root cause: missing await on session refresh in middleware/auth.ts:47',
|
|
99
|
+
}),
|
|
100
|
+
);
|
|
101
|
+
assert.strictEqual(result.valid, true, `unexpected errors: ${result.errors.join(', ')}`);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('valid failed record passes', () => {
|
|
105
|
+
const result = validatePendingRecord(buildPending({ status: 'failed' }));
|
|
106
|
+
assert.strictEqual(result.valid, true);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('rejects invalid sessionId pattern', () => {
|
|
110
|
+
const result = validatePendingRecord(buildPending({ sessionId: 'NotAnOracleId' }));
|
|
111
|
+
assert.ok(result.errors.includes('invalid_sessionId'));
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('rejects invalid bundleHash pattern', () => {
|
|
115
|
+
const result = validatePendingRecord(buildPending({ bundleHash: 'md5:abc123' }));
|
|
116
|
+
assert.ok(result.errors.includes('invalid_bundleHash'));
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('rejects unknown status', () => {
|
|
120
|
+
const result = validatePendingRecord(buildPending({ status: 'in_flight' }));
|
|
121
|
+
assert.ok(result.errors.includes('invalid_status'));
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('rejects unknown sourceSkill', () => {
|
|
125
|
+
const result = validatePendingRecord(buildPending({ sourceSkill: 'review' }));
|
|
126
|
+
assert.ok(result.errors.includes('invalid_sourceSkill'));
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('complete record without responseId is invalid', () => {
|
|
130
|
+
const result = validatePendingRecord(buildPending({ status: 'complete' }));
|
|
131
|
+
assert.ok(result.errors.includes('missing_responseId_when_complete'));
|
|
132
|
+
assert.ok(result.errors.includes('missing_responseExcerpt_when_complete'));
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('responseExcerpt > 500 chars rejected', () => {
|
|
136
|
+
const result = validatePendingRecord(
|
|
137
|
+
buildPending({ status: 'complete', responseId: 'r', responseExcerpt: 'x'.repeat(501) }),
|
|
138
|
+
);
|
|
139
|
+
assert.ok(result.errors.includes('responseExcerpt_too_long'));
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('pending record with responseId set is invalid', () => {
|
|
143
|
+
const result = validatePendingRecord(buildPending({ responseId: 'r1' }));
|
|
144
|
+
assert.ok(result.errors.includes('responseId_must_be_null_when_pending'));
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('Idempotency', () => {
|
|
149
|
+
test('same bundleHash returns same sessionId on duplicate dispatch', () => {
|
|
150
|
+
const existing = buildPending({ sessionId: 'oracle-1714234500-abc' });
|
|
151
|
+
const inbox = [existing];
|
|
152
|
+
|
|
153
|
+
const dispatch = (bundleHash) => {
|
|
154
|
+
const match = inbox.find((r) => r.bundleHash === bundleHash && r.status === 'pending');
|
|
155
|
+
if (match) return match.sessionId;
|
|
156
|
+
const newId = `oracle-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
157
|
+
inbox.push(buildPending({ sessionId: newId, bundleHash }));
|
|
158
|
+
return newId;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const id1 = dispatch(existing.bundleHash);
|
|
162
|
+
const id2 = dispatch(existing.bundleHash);
|
|
163
|
+
assert.strictEqual(id1, id2);
|
|
164
|
+
assert.strictEqual(id1, existing.sessionId);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe('Timeout transition', () => {
|
|
169
|
+
test('pending record becomes failed when now > timeoutAt', () => {
|
|
170
|
+
const record = buildPending({ timeoutAt: '2026-04-27T12:00:00Z' });
|
|
171
|
+
const after = timeoutCheck(record, '2026-04-27T13:00:00Z');
|
|
172
|
+
assert.strictEqual(after.status, 'failed');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('pending stays pending when now < timeoutAt', () => {
|
|
176
|
+
const record = buildPending({ timeoutAt: '2026-04-27T13:00:00Z' });
|
|
177
|
+
const after = timeoutCheck(record, '2026-04-27T12:30:00Z');
|
|
178
|
+
assert.strictEqual(after.status, 'pending');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('complete records not affected by timeout check', () => {
|
|
182
|
+
const record = buildPending({
|
|
183
|
+
status: 'complete',
|
|
184
|
+
responseId: 'r',
|
|
185
|
+
responseExcerpt: 'done',
|
|
186
|
+
timeoutAt: '2026-04-27T12:00:00Z',
|
|
187
|
+
});
|
|
188
|
+
const after = timeoutCheck(record, '2026-04-27T20:00:00Z');
|
|
189
|
+
assert.strictEqual(after.status, 'complete');
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe('Cleanup orphaned records', () => {
|
|
194
|
+
test('records >24h old are dropped', () => {
|
|
195
|
+
const records = [
|
|
196
|
+
buildPending({ sessionId: 'oracle-1-fresh', dispatchedAt: '2026-04-27T11:00:00Z' }),
|
|
197
|
+
buildPending({ sessionId: 'oracle-2-stale', dispatchedAt: '2026-04-25T11:00:00Z' }),
|
|
198
|
+
];
|
|
199
|
+
const remaining = cleanupExpired(records, '2026-04-27T12:00:00Z');
|
|
200
|
+
assert.strictEqual(remaining.length, 1);
|
|
201
|
+
assert.strictEqual(remaining[0].sessionId, 'oracle-1-fresh');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test('records <24h old are kept', () => {
|
|
205
|
+
const records = [buildPending({ dispatchedAt: '2026-04-26T13:00:00Z' })];
|
|
206
|
+
const remaining = cleanupExpired(records, '2026-04-27T12:00:00Z');
|
|
207
|
+
assert.strictEqual(remaining.length, 1);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill description quality (v2.15+) — lint validation that every SKILL.md
|
|
3
|
+
* description is YAML-safe, well-formed, and ambiguous-name skills carry an
|
|
4
|
+
* explicit "Use when…" routing hint.
|
|
5
|
+
*
|
|
6
|
+
* Validates:
|
|
7
|
+
* - All descriptions are double-quoted (YAML safety)
|
|
8
|
+
* - All descriptions end with `.` or `?`
|
|
9
|
+
* - All descriptions are 30-500 chars
|
|
10
|
+
* - Specific ambiguous-name skills include "Use when…" clause
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import assert from 'node:assert';
|
|
14
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
15
|
+
import { dirname, join, resolve } from 'node:path';
|
|
16
|
+
import { describe, test } from 'node:test';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
|
|
19
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const SKILLS_DIR = resolve(__dirname, '../..', 'skills');
|
|
21
|
+
|
|
22
|
+
function listSkills() {
|
|
23
|
+
return readdirSync(SKILLS_DIR).filter((name) => {
|
|
24
|
+
const p = join(SKILLS_DIR, name);
|
|
25
|
+
return statSync(p).isDirectory() && readdirSync(p).includes('SKILL.md');
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function readDescriptionLine(name) {
|
|
30
|
+
const text = readFileSync(join(SKILLS_DIR, name, 'SKILL.md'), 'utf8');
|
|
31
|
+
const lines = text.split(/\r?\n/);
|
|
32
|
+
for (const line of lines.slice(0, 20)) {
|
|
33
|
+
if (line.startsWith('description:')) return line;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function extractDescriptionValue(line) {
|
|
39
|
+
const m = line.match(/^description:\s+"(.+)"\s*$/);
|
|
40
|
+
if (m) return m[1].replace(/\\"/g, '"');
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const AMBIGUOUS_SKILLS_REQUIRING_USE_WHEN = [
|
|
45
|
+
'ba',
|
|
46
|
+
'completion-gate',
|
|
47
|
+
'constraint-check',
|
|
48
|
+
'doc-processor',
|
|
49
|
+
'integrity-check',
|
|
50
|
+
'logic-guardian',
|
|
51
|
+
'onboard',
|
|
52
|
+
'preflight',
|
|
53
|
+
'sentinel-env',
|
|
54
|
+
'watchdog',
|
|
55
|
+
'worktree',
|
|
56
|
+
'hallucination-guard',
|
|
57
|
+
'mcp-builder',
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
describe('Skill description format', () => {
|
|
61
|
+
test('every description is double-quoted (YAML-safe)', () => {
|
|
62
|
+
const skills = listSkills();
|
|
63
|
+
const violations = [];
|
|
64
|
+
for (const name of skills) {
|
|
65
|
+
const line = readDescriptionLine(name);
|
|
66
|
+
if (!line) {
|
|
67
|
+
violations.push(`${name}: no description line found`);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (!line.match(/^description:\s+"/)) {
|
|
71
|
+
violations.push(`${name}: description is not double-quoted`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
assert.strictEqual(violations.length, 0, `Unquoted descriptions:\n${violations.join('\n')}`);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('every description ends with terminal punctuation', () => {
|
|
78
|
+
const skills = listSkills();
|
|
79
|
+
const violations = [];
|
|
80
|
+
for (const name of skills) {
|
|
81
|
+
const line = readDescriptionLine(name);
|
|
82
|
+
const val = extractDescriptionValue(line);
|
|
83
|
+
if (!val) continue;
|
|
84
|
+
if (!val.match(/[.?]$/)) {
|
|
85
|
+
violations.push(`${name}: description does not end with . or ?`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
assert.strictEqual(violations.length, 0, `Bad endings:\n${violations.join('\n')}`);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('every description is 30-500 chars', () => {
|
|
92
|
+
const skills = listSkills();
|
|
93
|
+
const violations = [];
|
|
94
|
+
for (const name of skills) {
|
|
95
|
+
const line = readDescriptionLine(name);
|
|
96
|
+
const val = extractDescriptionValue(line);
|
|
97
|
+
if (!val) continue;
|
|
98
|
+
if (val.length < 30) violations.push(`${name}: description too short (${val.length} chars)`);
|
|
99
|
+
if (val.length > 500) violations.push(`${name}: description too long (${val.length} chars)`);
|
|
100
|
+
}
|
|
101
|
+
assert.strictEqual(violations.length, 0, `Length violations:\n${violations.join('\n')}`);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('Ambiguous-name skills include "Use when…" routing hint', () => {
|
|
106
|
+
test('all 13 ambiguous-name skills explicitly state "Use when…"', () => {
|
|
107
|
+
const violations = [];
|
|
108
|
+
for (const name of AMBIGUOUS_SKILLS_REQUIRING_USE_WHEN) {
|
|
109
|
+
const line = readDescriptionLine(name);
|
|
110
|
+
const val = extractDescriptionValue(line);
|
|
111
|
+
if (!val) {
|
|
112
|
+
violations.push(`${name}: description not parseable`);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (!/Use when/i.test(val)) {
|
|
116
|
+
violations.push(`${name}: missing "Use when…" clause`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
assert.strictEqual(violations.length, 0, `Ambiguous-name skills missing "Use when…":\n${violations.join('\n')}`);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('Description integrity', () => {
|
|
124
|
+
test('YAML escape sequences are well-formed (no orphan \\")', () => {
|
|
125
|
+
const skills = listSkills();
|
|
126
|
+
const violations = [];
|
|
127
|
+
for (const name of skills) {
|
|
128
|
+
const line = readDescriptionLine(name);
|
|
129
|
+
if (!line) continue;
|
|
130
|
+
// Parse the quoted value: count unescaped quotes inside the value
|
|
131
|
+
const inner = line.replace(/^description:\s+"/, '').replace(/"\s*$/, '');
|
|
132
|
+
// Every \" should be escaping a quote; bare " inside is invalid
|
|
133
|
+
let i = 0;
|
|
134
|
+
let bareQuotes = 0;
|
|
135
|
+
while (i < inner.length) {
|
|
136
|
+
if (inner[i] === '\\' && inner[i + 1] === '"') {
|
|
137
|
+
i += 2;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (inner[i] === '"') bareQuotes++;
|
|
141
|
+
i++;
|
|
142
|
+
}
|
|
143
|
+
if (bareQuotes > 0) {
|
|
144
|
+
violations.push(`${name}: ${bareQuotes} unescaped " inside description value`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
assert.strictEqual(violations.length, 0, `Bare quotes:\n${violations.join('\n')}`);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -8,10 +8,22 @@
|
|
|
8
8
|
* Antigravity project context: AGENTS.md (+ CLAUDE.md fallback)
|
|
9
9
|
* Antigravity skills dir: .agents/skills/
|
|
10
10
|
* Antigravity skill format: .agents/skills/{name}/SKILL.md
|
|
11
|
+
*
|
|
12
|
+
* MODEL TIER MAPPING (v2.15+):
|
|
13
|
+
* Skill frontmatter `model: opus|sonnet|haiku` (Anthropic naming) is
|
|
14
|
+
* translated to Gemini provider-correct model names. Antigravity supports
|
|
15
|
+
* both Gemini and Claude — defaulting to Gemini family since it is the
|
|
16
|
+
* native model. Unknown tier values pass through.
|
|
11
17
|
*/
|
|
12
18
|
|
|
13
19
|
import { BRANDING_FOOTER } from '../transforms/branding.js';
|
|
14
20
|
|
|
21
|
+
const MODEL_MAP = {
|
|
22
|
+
opus: 'gemini-3-pro',
|
|
23
|
+
sonnet: 'gemini-3-flash',
|
|
24
|
+
haiku: 'gemini-3-flash-lite',
|
|
25
|
+
};
|
|
26
|
+
|
|
15
27
|
const TOOL_MAP = {
|
|
16
28
|
Read: 'read the file',
|
|
17
29
|
Write: 'write/create the file',
|
|
@@ -46,7 +58,11 @@ export default {
|
|
|
46
58
|
|
|
47
59
|
generateHeader(skill) {
|
|
48
60
|
const desc = (skill.description || '').replace(/"/g, '\\"');
|
|
49
|
-
|
|
61
|
+
const lines = ['---', `name: rune-${skill.name}`, `description: "${desc}"`];
|
|
62
|
+
const translatedModel = skill.model ? MODEL_MAP[skill.model] || skill.model : null;
|
|
63
|
+
if (translatedModel) lines.push(`model: ${translatedModel}`);
|
|
64
|
+
lines.push('---', '', '');
|
|
65
|
+
return lines.join('\n');
|
|
50
66
|
},
|
|
51
67
|
|
|
52
68
|
generateFooter() {
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
* Claude Code Adapter (Passthrough)
|
|
3
3
|
*
|
|
4
4
|
* No transformation needed — source IS the output.
|
|
5
|
+
*
|
|
6
|
+
* MODEL TIER MAPPING (v2.15+):
|
|
7
|
+
* No-op. Claude Code's Anthropic backend understands `model: opus|sonnet|haiku`
|
|
8
|
+
* natively, so no translation is required (unlike codex/antigravity/opencode).
|
|
5
9
|
*/
|
|
6
10
|
|
|
7
11
|
export default {
|
|
@@ -8,10 +8,21 @@
|
|
|
8
8
|
* Codex project context: AGENTS.md (equivalent to CLAUDE.md)
|
|
9
9
|
* Codex skills dir: .codex/skills/
|
|
10
10
|
* Codex skill format: .codex/skills/{name}/SKILL.md
|
|
11
|
+
*
|
|
12
|
+
* MODEL TIER MAPPING (v2.15+):
|
|
13
|
+
* Skill frontmatter `model: opus|sonnet|haiku` (Anthropic naming) is
|
|
14
|
+
* translated to Codex/OpenAI provider-correct model names so the field
|
|
15
|
+
* is meaningful in the compiled output. Unknown tier values pass through.
|
|
11
16
|
*/
|
|
12
17
|
|
|
13
18
|
import { BRANDING_FOOTER } from '../transforms/branding.js';
|
|
14
19
|
|
|
20
|
+
const MODEL_MAP = {
|
|
21
|
+
opus: 'gpt-5-pro',
|
|
22
|
+
sonnet: 'gpt-5',
|
|
23
|
+
haiku: 'gpt-5-mini',
|
|
24
|
+
};
|
|
25
|
+
|
|
15
26
|
const TOOL_MAP = {
|
|
16
27
|
Read: 'read the file',
|
|
17
28
|
Write: 'write/create the file',
|
|
@@ -47,7 +58,11 @@ export default {
|
|
|
47
58
|
|
|
48
59
|
generateHeader(skill) {
|
|
49
60
|
const desc = (skill.description || '').replace(/"/g, '\\"');
|
|
50
|
-
|
|
61
|
+
const lines = ['---', `name: rune-${skill.name}`, `description: "${desc}"`];
|
|
62
|
+
const translatedModel = skill.model ? MODEL_MAP[skill.model] || skill.model : null;
|
|
63
|
+
if (translatedModel) lines.push(`model: ${translatedModel}`);
|
|
64
|
+
lines.push('---', '', '');
|
|
65
|
+
return lines.join('\n');
|
|
51
66
|
},
|
|
52
67
|
|
|
53
68
|
generateFooter() {
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Emits .mdc rule files for .cursor/rules/ directory.
|
|
5
5
|
* Uses @file references for cross-skill mesh.
|
|
6
|
+
*
|
|
7
|
+
* MODEL TIER MAPPING (v2.15+):
|
|
8
|
+
* No-op. Cursor's Anthropic API integration understands `model: opus|sonnet|haiku`
|
|
9
|
+
* natively. No translation required.
|
|
6
10
|
*/
|
|
7
11
|
|
|
8
12
|
import { BRANDING_FOOTER } from '../transforms/branding.js';
|
|
@@ -3,10 +3,21 @@
|
|
|
3
3
|
*
|
|
4
4
|
* For unknown or future platforms. Emits to .ai/rules/ directory.
|
|
5
5
|
* Uses the most portable format possible.
|
|
6
|
+
*
|
|
7
|
+
* MODEL TIER MAPPING (v2.15+):
|
|
8
|
+
* Provider-agnostic — emits semantic tier hints in the markdown header.
|
|
9
|
+
* Skill frontmatter `model: opus|sonnet|haiku` is translated to
|
|
10
|
+
* `tier:heavy|mid|light`. The consuming platform resolves the tier.
|
|
6
11
|
*/
|
|
7
12
|
|
|
8
13
|
import { BRANDING_FOOTER } from '../transforms/branding.js';
|
|
9
14
|
|
|
15
|
+
const MODEL_MAP = {
|
|
16
|
+
opus: 'tier:heavy',
|
|
17
|
+
sonnet: 'tier:mid',
|
|
18
|
+
haiku: 'tier:light',
|
|
19
|
+
};
|
|
20
|
+
|
|
10
21
|
const TOOL_MAP = {
|
|
11
22
|
Read: 'read the file',
|
|
12
23
|
Write: 'write/create the file',
|
|
@@ -37,7 +48,9 @@ export default {
|
|
|
37
48
|
},
|
|
38
49
|
|
|
39
50
|
generateHeader(skill) {
|
|
40
|
-
|
|
51
|
+
const translatedModel = skill.model ? MODEL_MAP[skill.model] || skill.model : null;
|
|
52
|
+
const modelSuffix = translatedModel ? ` | model: ${translatedModel}` : '';
|
|
53
|
+
return `# rune-${skill.name}\n\n> Rune ${skill.layer} Skill | ${skill.group}${modelSuffix}\n\n`;
|
|
41
54
|
},
|
|
42
55
|
|
|
43
56
|
generateFooter() {
|
|
@@ -26,10 +26,22 @@
|
|
|
26
26
|
*
|
|
27
27
|
* Reference codex-imagen repo (darkamenosa/codex-imagen) documents the
|
|
28
28
|
* de-facto in-the-wild convention this adapter formalizes.
|
|
29
|
+
*
|
|
30
|
+
* MODEL TIER MAPPING (v2.15+):
|
|
31
|
+
* OpenClaw is provider-agnostic — emits semantic tier hints in the
|
|
32
|
+
* markdown header. Skill frontmatter `model: opus|sonnet|haiku` is
|
|
33
|
+
* translated to `tier:heavy|mid|light`. The OpenClaw runtime resolves
|
|
34
|
+
* the tier to its configured provider model.
|
|
29
35
|
*/
|
|
30
36
|
|
|
31
37
|
import { BRANDING_FOOTER } from '../transforms/branding.js';
|
|
32
38
|
|
|
39
|
+
const MODEL_MAP = {
|
|
40
|
+
opus: 'tier:heavy',
|
|
41
|
+
sonnet: 'tier:mid',
|
|
42
|
+
haiku: 'tier:light',
|
|
43
|
+
};
|
|
44
|
+
|
|
33
45
|
const TOOL_MAP = {
|
|
34
46
|
Read: 'read_file',
|
|
35
47
|
Write: 'write_file',
|
|
@@ -60,7 +72,9 @@ export default {
|
|
|
60
72
|
},
|
|
61
73
|
|
|
62
74
|
generateHeader(skill) {
|
|
63
|
-
|
|
75
|
+
const translatedModel = skill.model ? MODEL_MAP[skill.model] || skill.model : null;
|
|
76
|
+
const modelSuffix = translatedModel ? ` | model: ${translatedModel}` : '';
|
|
77
|
+
return `# rune-${skill.name}\n\n> Rune ${skill.layer} Skill | ${skill.group}${modelSuffix}\n\n`;
|
|
64
78
|
},
|
|
65
79
|
|
|
66
80
|
generateFooter() {
|
|
@@ -16,10 +16,22 @@
|
|
|
16
16
|
*
|
|
17
17
|
* @see https://opencode.ai/docs/skills/
|
|
18
18
|
* @see https://opencode.ai/docs/agents/
|
|
19
|
+
*
|
|
20
|
+
* MODEL TIER MAPPING (v2.15+):
|
|
21
|
+
* OpenCode is provider-agnostic — emits semantic tier hints rather than
|
|
22
|
+
* concrete model names. Skill frontmatter `model: opus|sonnet|haiku` is
|
|
23
|
+
* translated to `tier:heavy|mid|light`. The OpenCode IDE resolves the
|
|
24
|
+
* tier to its configured provider model.
|
|
19
25
|
*/
|
|
20
26
|
|
|
21
27
|
import { BRANDING_FOOTER } from '../transforms/branding.js';
|
|
22
28
|
|
|
29
|
+
const MODEL_MAP = {
|
|
30
|
+
opus: 'tier:heavy',
|
|
31
|
+
sonnet: 'tier:mid',
|
|
32
|
+
haiku: 'tier:light',
|
|
33
|
+
};
|
|
34
|
+
|
|
23
35
|
const TOOL_MAP = {
|
|
24
36
|
Read: 'read the file',
|
|
25
37
|
Write: 'write/create the file',
|
|
@@ -55,7 +67,11 @@ export default {
|
|
|
55
67
|
|
|
56
68
|
generateHeader(skill) {
|
|
57
69
|
const desc = (skill.description || '').replace(/"/g, '\\"');
|
|
58
|
-
|
|
70
|
+
const lines = ['---', `name: rune-${skill.name}`, `description: "${desc}"`];
|
|
71
|
+
const translatedModel = skill.model ? MODEL_MAP[skill.model] || skill.model : null;
|
|
72
|
+
if (translatedModel) lines.push(`model: ${translatedModel}`);
|
|
73
|
+
lines.push('---', '', '');
|
|
74
|
+
return lines.join('\n');
|
|
59
75
|
},
|
|
60
76
|
|
|
61
77
|
generateFooter() {
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Emits .md rule files for .windsurf/rules/ directory.
|
|
5
5
|
* Uses prose references for cross-skill mesh (no @file support).
|
|
6
|
+
*
|
|
7
|
+
* MODEL TIER MAPPING (v2.15+):
|
|
8
|
+
* No-op. Windsurf's Anthropic API integration understands `model: opus|sonnet|haiku`
|
|
9
|
+
* natively. No translation required.
|
|
6
10
|
*/
|
|
7
11
|
|
|
8
12
|
import { BRANDING_FOOTER } from '../transforms/branding.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rune-kit/rune",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.16.0",
|
|
4
4
|
"description": "63-skill mesh for AI coding assistants — runtime auto-discipline via native hooks (Claude/Cursor/Windsurf/Antigravity), 5-layer architecture, 215+ connections, multi-platform compiler.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"format": "biome format --write .",
|
|
17
17
|
"ci": "biome check . && node --test compiler/__tests__/*.test.js scripts/__tests__/*.test.js && node compiler/bin/rune.js doctor",
|
|
18
18
|
"version-check": "node scripts/version-sync-check.js",
|
|
19
|
+
"version-bump": "node scripts/bump-version.js",
|
|
19
20
|
"prepublishOnly": "node scripts/version-sync-check.js"
|
|
20
21
|
},
|
|
21
22
|
"keywords": [
|