@rune-kit/rune 2.6.0 → 2.7.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 +17 -6
- package/compiler/__tests__/executive-dashboards.test.js +285 -0
- package/compiler/__tests__/inject.test.js +128 -0
- package/compiler/__tests__/orchestrators.test.js +151 -0
- package/compiler/__tests__/org-templates.test.js +447 -0
- package/compiler/__tests__/parser.test.js +201 -147
- package/compiler/__tests__/skill-index.test.js +218 -218
- package/compiler/__tests__/status.test.js +336 -0
- package/compiler/__tests__/templates.test.js +245 -0
- package/compiler/__tests__/visualizer.test.js +325 -0
- package/compiler/adapters/antigravity.js +71 -71
- package/compiler/bin/rune.js +444 -355
- package/compiler/doctor.js +272 -1
- package/compiler/emitter.js +939 -678
- package/compiler/parser.js +498 -267
- package/compiler/status.js +342 -0
- package/compiler/visualizer.js +622 -0
- package/package.json +1 -1
- package/skills/autopsy/SKILL.md +48 -1
- package/skills/completion-gate/SKILL.md +26 -1
- package/skills/context-engine/SKILL.md +93 -2
- package/skills/cook/SKILL.md +137 -4
- package/skills/debug/SKILL.md +16 -1
- package/skills/docs/SKILL.md +28 -3
- package/skills/fix/SKILL.md +2 -1
- package/skills/mcp-builder/SKILL.md +53 -1
- package/skills/onboard/SKILL.md +51 -1
- package/skills/perf/SKILL.md +34 -1
- package/skills/plan/SKILL.md +29 -1
- package/skills/preflight/SKILL.md +35 -1
- package/skills/retro/SKILL.md +95 -1
- package/skills/review/SKILL.md +45 -1
- package/skills/scope-guard/SKILL.md +1 -0
- package/skills/sentinel/SKILL.md +35 -1
- package/skills/sentinel/references/policy-driven-constraints.md +424 -0
- package/skills/session-bridge/SKILL.md +57 -2
- package/skills/session-bridge/references/evolutionary-memory-patterns.md +312 -0
- package/skills/team/SKILL.md +15 -1
package/compiler/parser.js
CHANGED
|
@@ -1,267 +1,498 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SKILL.md Parser
|
|
3
|
-
*
|
|
4
|
-
* Parses SKILL.md files into a structured intermediate representation (IR).
|
|
5
|
-
* Extracts frontmatter, cross-references, tool references, HARD-GATE blocks, and sections.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Metadata fields that should be parsed as comma-separated arrays
|
|
10
|
-
* e.g. emit: code.changed, tests.passed → ["code.changed", "tests.passed"]
|
|
11
|
-
*/
|
|
12
|
-
const COMMA_LIST_FIELDS = new Set(['emit', 'listen']);
|
|
13
|
-
|
|
14
|
-
const CROSS_REF_PATTERN = /`?rune:([a-z][\w-]*)`?/g;
|
|
15
|
-
const TOOL_REF_PATTERN = /`(Read|Write|Edit|Glob|Grep|Bash|TodoWrite|Skill|Agent)`/g;
|
|
16
|
-
const HARD_GATE_PATTERN = /<HARD-GATE>([\s\S]*?)<\/HARD-GATE>/g;
|
|
17
|
-
const _SECTION_PATTERN = /^## (.+)$/gm;
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Parse YAML-like frontmatter from SKILL.md
|
|
21
|
-
* Handles nested metadata block
|
|
22
|
-
*/
|
|
23
|
-
function parseFrontmatter(content) {
|
|
24
|
-
// Normalize line endings to \n for cross-platform compatibility
|
|
25
|
-
const normalized = content.replace(/\r\n/g, '\n');
|
|
26
|
-
const match = normalized.match(/^---\n([\s\S]*?)\n---/);
|
|
27
|
-
if (!match) return { frontmatter: {}, body: normalized };
|
|
28
|
-
|
|
29
|
-
const raw = match[1];
|
|
30
|
-
const body = normalized.slice(match[0].length).trim();
|
|
31
|
-
const frontmatter = {};
|
|
32
|
-
|
|
33
|
-
let currentIndent = null;
|
|
34
|
-
let nestedKey = null;
|
|
35
|
-
const _nestedObj = {};
|
|
36
|
-
|
|
37
|
-
for (const line of raw.split('\n')) {
|
|
38
|
-
const trimmed = line.trim();
|
|
39
|
-
if (!trimmed) continue;
|
|
40
|
-
|
|
41
|
-
// Detect nested block (e.g., metadata:)
|
|
42
|
-
if (/^\w+:\s*$/.test(trimmed)) {
|
|
43
|
-
nestedKey = trimmed.replace(':', '').trim();
|
|
44
|
-
frontmatter[nestedKey] = {};
|
|
45
|
-
currentIndent = 'nested';
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (currentIndent === 'nested' && line.startsWith(' ')) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
//
|
|
54
|
-
if (
|
|
55
|
-
frontmatter[nestedKey]
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* SKILL.md Parser
|
|
3
|
+
*
|
|
4
|
+
* Parses SKILL.md files into a structured intermediate representation (IR).
|
|
5
|
+
* Extracts frontmatter, cross-references, tool references, HARD-GATE blocks, and sections.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Metadata fields that should be parsed as comma-separated arrays
|
|
10
|
+
* e.g. emit: code.changed, tests.passed → ["code.changed", "tests.passed"]
|
|
11
|
+
*/
|
|
12
|
+
const COMMA_LIST_FIELDS = new Set(['emit', 'listen']);
|
|
13
|
+
|
|
14
|
+
const CROSS_REF_PATTERN = /`?rune:([a-z][\w-]*)`?/g;
|
|
15
|
+
const TOOL_REF_PATTERN = /`(Read|Write|Edit|Glob|Grep|Bash|TodoWrite|Skill|Agent)`/g;
|
|
16
|
+
const HARD_GATE_PATTERN = /<HARD-GATE>([\s\S]*?)<\/HARD-GATE>/g;
|
|
17
|
+
const _SECTION_PATTERN = /^## (.+)$/gm;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Parse YAML-like frontmatter from SKILL.md
|
|
21
|
+
* Handles nested metadata block
|
|
22
|
+
*/
|
|
23
|
+
function parseFrontmatter(content) {
|
|
24
|
+
// Normalize line endings to \n for cross-platform compatibility
|
|
25
|
+
const normalized = content.replace(/\r\n/g, '\n');
|
|
26
|
+
const match = normalized.match(/^---\n([\s\S]*?)\n---/);
|
|
27
|
+
if (!match) return { frontmatter: {}, body: normalized };
|
|
28
|
+
|
|
29
|
+
const raw = match[1];
|
|
30
|
+
const body = normalized.slice(match[0].length).trim();
|
|
31
|
+
const frontmatter = {};
|
|
32
|
+
|
|
33
|
+
let currentIndent = null;
|
|
34
|
+
let nestedKey = null;
|
|
35
|
+
const _nestedObj = {};
|
|
36
|
+
|
|
37
|
+
for (const line of raw.split('\n')) {
|
|
38
|
+
const trimmed = line.trim();
|
|
39
|
+
if (!trimmed) continue;
|
|
40
|
+
|
|
41
|
+
// Detect nested block (e.g., metadata:)
|
|
42
|
+
if (/^\w+:\s*$/.test(trimmed)) {
|
|
43
|
+
nestedKey = trimmed.replace(':', '').trim();
|
|
44
|
+
frontmatter[nestedKey] = {};
|
|
45
|
+
currentIndent = 'nested';
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (currentIndent === 'nested' && line.startsWith(' ')) {
|
|
50
|
+
// YAML list item (e.g. " - value")
|
|
51
|
+
const listMatch = trimmed.match(/^-\s+(.+)$/);
|
|
52
|
+
if (listMatch) {
|
|
53
|
+
// Convert nested block to array if not already
|
|
54
|
+
if (!Array.isArray(frontmatter[nestedKey])) {
|
|
55
|
+
frontmatter[nestedKey] = [];
|
|
56
|
+
}
|
|
57
|
+
frontmatter[nestedKey].push(listMatch[1].replace(/^["']|["']$/g, ''));
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const kvMatch = trimmed.match(/^(\w[\w-]*):\s*(.+)$/);
|
|
62
|
+
if (kvMatch) {
|
|
63
|
+
const rawValue = kvMatch[2].replace(/^["']|["']$/g, '');
|
|
64
|
+
// Comma-separated list fields → parse as array
|
|
65
|
+
if (COMMA_LIST_FIELDS.has(kvMatch[1])) {
|
|
66
|
+
frontmatter[nestedKey][kvMatch[1]] = rawValue
|
|
67
|
+
.split(',')
|
|
68
|
+
.map((s) => s.trim())
|
|
69
|
+
.filter(Boolean);
|
|
70
|
+
} else {
|
|
71
|
+
frontmatter[nestedKey][kvMatch[1]] = rawValue;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Top-level key-value
|
|
78
|
+
currentIndent = null;
|
|
79
|
+
nestedKey = null;
|
|
80
|
+
const kvMatch = trimmed.match(/^(\w[\w-]*):\s*(.+)$/);
|
|
81
|
+
if (kvMatch) {
|
|
82
|
+
const value = kvMatch[2].replace(/^["']|["']$/g, '');
|
|
83
|
+
// Comma-separated list fields at top level too (Pro/Business packs)
|
|
84
|
+
if (COMMA_LIST_FIELDS.has(kvMatch[1])) {
|
|
85
|
+
frontmatter[kvMatch[1]] = value
|
|
86
|
+
.split(',')
|
|
87
|
+
.map((s) => s.trim())
|
|
88
|
+
.filter(Boolean);
|
|
89
|
+
} else {
|
|
90
|
+
frontmatter[kvMatch[1]] = value;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { frontmatter, body };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Extract all rune:<name> cross-references from body
|
|
100
|
+
*/
|
|
101
|
+
export function extractCrossRefs(body) {
|
|
102
|
+
const refs = [];
|
|
103
|
+
const lines = body.split('\n');
|
|
104
|
+
|
|
105
|
+
for (let i = 0; i < lines.length; i++) {
|
|
106
|
+
const line = lines[i];
|
|
107
|
+
let match;
|
|
108
|
+
const regex = new RegExp(CROSS_REF_PATTERN.source, 'g');
|
|
109
|
+
|
|
110
|
+
while ((match = regex.exec(line)) !== null) {
|
|
111
|
+
refs.push({
|
|
112
|
+
raw: match[0],
|
|
113
|
+
skillName: match[1],
|
|
114
|
+
line: i + 1,
|
|
115
|
+
context: line.trim(),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return refs;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Extract all Claude Code tool references from body
|
|
125
|
+
*/
|
|
126
|
+
export function extractToolRefs(body) {
|
|
127
|
+
const refs = [];
|
|
128
|
+
const lines = body.split('\n');
|
|
129
|
+
|
|
130
|
+
for (let i = 0; i < lines.length; i++) {
|
|
131
|
+
const line = lines[i];
|
|
132
|
+
let match;
|
|
133
|
+
const regex = new RegExp(TOOL_REF_PATTERN.source, 'g');
|
|
134
|
+
|
|
135
|
+
while ((match = regex.exec(line)) !== null) {
|
|
136
|
+
refs.push({
|
|
137
|
+
raw: match[0],
|
|
138
|
+
toolName: match[1],
|
|
139
|
+
line: i + 1,
|
|
140
|
+
context: line.trim(),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return refs;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Extract HARD-GATE blocks
|
|
150
|
+
*/
|
|
151
|
+
function extractHardGates(body) {
|
|
152
|
+
const gates = [];
|
|
153
|
+
let match;
|
|
154
|
+
const regex = new RegExp(HARD_GATE_PATTERN.source, 'gs');
|
|
155
|
+
|
|
156
|
+
while ((match = regex.exec(body)) !== null) {
|
|
157
|
+
gates.push(match[1].trim());
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return gates;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Extract ## sections as a Map<sectionName, content>
|
|
165
|
+
*/
|
|
166
|
+
function extractSections(body) {
|
|
167
|
+
const sections = new Map();
|
|
168
|
+
const lines = body.split('\n');
|
|
169
|
+
let currentSection = null;
|
|
170
|
+
let currentContent = [];
|
|
171
|
+
|
|
172
|
+
for (const line of lines) {
|
|
173
|
+
const sectionMatch = line.match(/^## (.+)$/);
|
|
174
|
+
if (sectionMatch) {
|
|
175
|
+
if (currentSection) {
|
|
176
|
+
sections.set(currentSection, currentContent.join('\n').trim());
|
|
177
|
+
}
|
|
178
|
+
currentSection = sectionMatch[1];
|
|
179
|
+
currentContent = [];
|
|
180
|
+
} else if (currentSection) {
|
|
181
|
+
currentContent.push(line);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (currentSection) {
|
|
186
|
+
sections.set(currentSection, currentContent.join('\n').trim());
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return sections;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Parse a SKILL.md file content into structured IR
|
|
194
|
+
*
|
|
195
|
+
* @param {string} content - raw SKILL.md content
|
|
196
|
+
* @param {string} [filePath] - optional file path for error reporting
|
|
197
|
+
* @returns {ParsedSkill}
|
|
198
|
+
*/
|
|
199
|
+
export function parseSkill(content, filePath = '') {
|
|
200
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
201
|
+
|
|
202
|
+
const metadata = frontmatter.metadata || {};
|
|
203
|
+
|
|
204
|
+
// Extract signals — emit/listen arrays from metadata or top-level (Pro/Business packs)
|
|
205
|
+
const emit = Array.isArray(metadata.emit) ? metadata.emit : Array.isArray(frontmatter.emit) ? frontmatter.emit : [];
|
|
206
|
+
const listen = Array.isArray(metadata.listen)
|
|
207
|
+
? metadata.listen
|
|
208
|
+
: Array.isArray(frontmatter.listen)
|
|
209
|
+
? frontmatter.listen
|
|
210
|
+
: [];
|
|
211
|
+
const signals = emit.length > 0 || listen.length > 0 ? { emit, listen } : null;
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
name: frontmatter.name || '',
|
|
215
|
+
description: frontmatter.description || '',
|
|
216
|
+
layer: metadata.layer || 'L2',
|
|
217
|
+
model: metadata.model || 'sonnet',
|
|
218
|
+
group: metadata.group || 'general',
|
|
219
|
+
signals,
|
|
220
|
+
contextFork: frontmatter.context === 'fork',
|
|
221
|
+
agentType: frontmatter.agent || null,
|
|
222
|
+
body,
|
|
223
|
+
crossRefs: extractCrossRefs(body),
|
|
224
|
+
toolRefs: extractToolRefs(body),
|
|
225
|
+
hardGates: extractHardGates(body),
|
|
226
|
+
sections: extractSections(body),
|
|
227
|
+
filePath,
|
|
228
|
+
frontmatter,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Parse a PACK.md extension file (same format, slightly different metadata)
|
|
234
|
+
* Supports both monolith format (all skills inline) and split format (skills in separate files)
|
|
235
|
+
*
|
|
236
|
+
* Split format detection: metadata.format === "split" OR metadata.skills array present
|
|
237
|
+
*/
|
|
238
|
+
export function parsePack(content, filePath = '') {
|
|
239
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
240
|
+
const metadata = frontmatter.metadata || {};
|
|
241
|
+
|
|
242
|
+
// Detect split format
|
|
243
|
+
const isSplit = metadata.format === 'split' || Array.isArray(metadata.skills);
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
name: frontmatter.name || '',
|
|
247
|
+
description: frontmatter.description || '',
|
|
248
|
+
version: metadata.version || frontmatter.version || '1.0.0',
|
|
249
|
+
layer: 'L4',
|
|
250
|
+
group: 'extension',
|
|
251
|
+
isSplit,
|
|
252
|
+
skillManifest: isSplit ? parseSkillManifest(metadata.skills || []) : [],
|
|
253
|
+
body,
|
|
254
|
+
crossRefs: extractCrossRefs(body),
|
|
255
|
+
toolRefs: extractToolRefs(body),
|
|
256
|
+
hardGates: extractHardGates(body),
|
|
257
|
+
sections: extractSections(body),
|
|
258
|
+
filePath,
|
|
259
|
+
frontmatter,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Parse the skills manifest from split PACK.md frontmatter
|
|
265
|
+
*
|
|
266
|
+
* @param {Array} skills - array of skill entries from frontmatter metadata.skills
|
|
267
|
+
* @returns {Array<{name: string, file: string, model: string, description: string}>}
|
|
268
|
+
*/
|
|
269
|
+
/**
|
|
270
|
+
* Parse a workflow template file (templates/*.md)
|
|
271
|
+
*
|
|
272
|
+
* Templates have the same frontmatter structure as skills but with additional
|
|
273
|
+
* template-specific fields: domain, chain, connections[]
|
|
274
|
+
*
|
|
275
|
+
* @param {string} content - raw template file content
|
|
276
|
+
* @param {string} [filePath] - optional file path for error reporting
|
|
277
|
+
* @returns {ParsedTemplate}
|
|
278
|
+
*/
|
|
279
|
+
export function parseTemplate(content, filePath = '') {
|
|
280
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
281
|
+
|
|
282
|
+
// Parse signals — can be nested object { emit: "...", listen: "..." } or flat
|
|
283
|
+
const signalsRaw = frontmatter.signals || {};
|
|
284
|
+
const emit =
|
|
285
|
+
typeof signalsRaw.emit === 'string'
|
|
286
|
+
? signalsRaw.emit
|
|
287
|
+
.split(',')
|
|
288
|
+
.map((s) => s.trim())
|
|
289
|
+
.filter(Boolean)
|
|
290
|
+
: Array.isArray(signalsRaw.emit)
|
|
291
|
+
? signalsRaw.emit
|
|
292
|
+
: [];
|
|
293
|
+
const listen =
|
|
294
|
+
typeof signalsRaw.listen === 'string'
|
|
295
|
+
? signalsRaw.listen
|
|
296
|
+
.split(',')
|
|
297
|
+
.map((s) => s.trim())
|
|
298
|
+
.filter(Boolean)
|
|
299
|
+
: Array.isArray(signalsRaw.listen)
|
|
300
|
+
? signalsRaw.listen
|
|
301
|
+
: [];
|
|
302
|
+
|
|
303
|
+
// Parse connections array — supports both comma-separated string and YAML array
|
|
304
|
+
let connections = [];
|
|
305
|
+
if (frontmatter.connections) {
|
|
306
|
+
if (typeof frontmatter.connections === 'string') {
|
|
307
|
+
connections = frontmatter.connections
|
|
308
|
+
.split(',')
|
|
309
|
+
.map((s) => s.trim())
|
|
310
|
+
.filter(Boolean);
|
|
311
|
+
} else if (Array.isArray(frontmatter.connections)) {
|
|
312
|
+
connections = frontmatter.connections;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
name: frontmatter.name || '',
|
|
318
|
+
pack: frontmatter.pack || '',
|
|
319
|
+
version: frontmatter.version || '1.0.0',
|
|
320
|
+
description: frontmatter.description || '',
|
|
321
|
+
domain: frontmatter.domain || '',
|
|
322
|
+
chain: frontmatter.chain || 'standard',
|
|
323
|
+
signals: { emit, listen },
|
|
324
|
+
connections,
|
|
325
|
+
body,
|
|
326
|
+
crossRefs: extractCrossRefs(body),
|
|
327
|
+
sections: extractSections(body),
|
|
328
|
+
filePath,
|
|
329
|
+
frontmatter,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Parse organization config from .rune/org/org.md
|
|
335
|
+
* Extracts teams, roles, policies, approval flows, and governance level.
|
|
336
|
+
*/
|
|
337
|
+
export function parseOrgConfig(content, filePath = '') {
|
|
338
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
339
|
+
|
|
340
|
+
const teams = parseMarkdownTable(body, 'Teams');
|
|
341
|
+
const roles = parseMarkdownTable(body, 'Roles');
|
|
342
|
+
const policies = parseOrgPolicies(body);
|
|
343
|
+
const approvalFlows = parseApprovalFlows(body);
|
|
344
|
+
const governanceLevel = parseGovernanceLevel(body);
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
name: frontmatter.name || '',
|
|
348
|
+
description: frontmatter.description || '',
|
|
349
|
+
version: frontmatter.version || '1.0.0',
|
|
350
|
+
tier: frontmatter.tier || 'business',
|
|
351
|
+
teams,
|
|
352
|
+
roles,
|
|
353
|
+
policies,
|
|
354
|
+
approvalFlows,
|
|
355
|
+
governanceLevel,
|
|
356
|
+
body,
|
|
357
|
+
filePath,
|
|
358
|
+
frontmatter,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Parse a Markdown table under a ## heading into array of objects.
|
|
364
|
+
* Handles | col1 | col2 | format with header row + separator row + data rows.
|
|
365
|
+
*/
|
|
366
|
+
function parseMarkdownTable(body, sectionName) {
|
|
367
|
+
// Find the section
|
|
368
|
+
const sectionPattern = new RegExp(`## ${sectionName}\\s*\\n([\\s\\S]*?)(?=\\n## |$)`);
|
|
369
|
+
const sectionMatch = body.match(sectionPattern);
|
|
370
|
+
if (!sectionMatch) return [];
|
|
371
|
+
|
|
372
|
+
const sectionContent = sectionMatch[1];
|
|
373
|
+
const lines = sectionContent.split('\n').filter((l) => l.trim().startsWith('|'));
|
|
374
|
+
if (lines.length < 3) return []; // need header + separator + at least 1 row
|
|
375
|
+
|
|
376
|
+
const parseRow = (line) =>
|
|
377
|
+
line
|
|
378
|
+
.split('|')
|
|
379
|
+
.slice(1, -1)
|
|
380
|
+
.map((cell) => cell.trim());
|
|
381
|
+
|
|
382
|
+
const headers = parseRow(lines[0]).map((h) => h.toLowerCase().replace(/\s+/g, '_'));
|
|
383
|
+
// Skip separator row (lines[1])
|
|
384
|
+
const rows = [];
|
|
385
|
+
for (let i = 2; i < lines.length; i++) {
|
|
386
|
+
const cells = parseRow(lines[i]);
|
|
387
|
+
if (cells.length === 0) continue;
|
|
388
|
+
const row = {};
|
|
389
|
+
headers.forEach((h, idx) => {
|
|
390
|
+
row[h] = cells[idx] || '';
|
|
391
|
+
});
|
|
392
|
+
rows.push(row);
|
|
393
|
+
}
|
|
394
|
+
return rows;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Parse ### subsections under ## Policies into a map of policy name → rules array.
|
|
399
|
+
*/
|
|
400
|
+
function parseOrgPolicies(body) {
|
|
401
|
+
const policiesPattern = /## Policies\s*\n([\s\S]*?)(?=\n## [^#]|$)/;
|
|
402
|
+
const policiesMatch = body.match(policiesPattern);
|
|
403
|
+
if (!policiesMatch) return {};
|
|
404
|
+
|
|
405
|
+
// Prepend \n so first ### subsection is also captured by split
|
|
406
|
+
const policiesContent = `\n${policiesMatch[1].trimStart()}`;
|
|
407
|
+
const policies = {};
|
|
408
|
+
const subSections = policiesContent.split(/\n### /);
|
|
409
|
+
|
|
410
|
+
for (let i = 1; i < subSections.length; i++) {
|
|
411
|
+
const lines = subSections[i].split('\n');
|
|
412
|
+
const name = lines[0].trim().toLowerCase().replace(/\s+/g, '_');
|
|
413
|
+
const rules = lines
|
|
414
|
+
.slice(1)
|
|
415
|
+
.filter((l) => l.trim().startsWith('- **'))
|
|
416
|
+
.map((l) => {
|
|
417
|
+
const match = l.match(/- \*\*(.+?)\*\*:\s*(.+)/);
|
|
418
|
+
if (!match) return null;
|
|
419
|
+
return {
|
|
420
|
+
key: match[1].trim().toLowerCase().replace(/\s+/g, '_'),
|
|
421
|
+
value: match[2].trim(),
|
|
422
|
+
};
|
|
423
|
+
})
|
|
424
|
+
.filter(Boolean);
|
|
425
|
+
policies[name] = rules;
|
|
426
|
+
}
|
|
427
|
+
return policies;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Parse ### subsections under ## Approval Flows into named flow strings.
|
|
432
|
+
*/
|
|
433
|
+
function parseApprovalFlows(body) {
|
|
434
|
+
const flowsPattern = /## Approval Flows\s*\n([\s\S]*?)(?=\n## [^#]|$)/;
|
|
435
|
+
const flowsMatch = body.match(flowsPattern);
|
|
436
|
+
if (!flowsMatch) return {};
|
|
437
|
+
|
|
438
|
+
// Prepend \n so first ### subsection is also captured by split
|
|
439
|
+
const flowsContent = `\n${flowsMatch[1].trimStart()}`;
|
|
440
|
+
const flows = {};
|
|
441
|
+
const subSections = flowsContent.split(/\n### /);
|
|
442
|
+
|
|
443
|
+
for (let i = 1; i < subSections.length; i++) {
|
|
444
|
+
const lines = subSections[i].split('\n');
|
|
445
|
+
const name = lines[0].trim().toLowerCase().replace(/\s+/g, '_');
|
|
446
|
+
// Extract content between ``` blocks
|
|
447
|
+
const codeMatch = subSections[i].match(/```\n([\s\S]*?)```/);
|
|
448
|
+
if (codeMatch) {
|
|
449
|
+
flows[name] = codeMatch[1].trim();
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return flows;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Parse ## Governance Level section for governance mode and settings.
|
|
457
|
+
*/
|
|
458
|
+
function parseGovernanceLevel(body) {
|
|
459
|
+
const govPattern = /## Governance Level\s*\n([\s\S]*?)(?=\n## |$)/;
|
|
460
|
+
const govMatch = body.match(govPattern);
|
|
461
|
+
if (!govMatch) return { level: 'unknown', settings: [] };
|
|
462
|
+
|
|
463
|
+
const content = govMatch[1];
|
|
464
|
+
// Extract bold level: **Minimal**, **Moderate**, **Maximum**
|
|
465
|
+
const levelMatch = content.match(/\*\*(\w+)\*\*/);
|
|
466
|
+
const level = levelMatch ? levelMatch[1].toLowerCase() : 'unknown';
|
|
467
|
+
|
|
468
|
+
// Extract bullet settings
|
|
469
|
+
const settings = content
|
|
470
|
+
.split('\n')
|
|
471
|
+
.filter((l) => l.trim().startsWith('- '))
|
|
472
|
+
.map((l) => l.trim().replace(/^- /, ''));
|
|
473
|
+
|
|
474
|
+
return { level, settings };
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function parseSkillManifest(skills) {
|
|
478
|
+
if (!Array.isArray(skills)) return [];
|
|
479
|
+
|
|
480
|
+
return skills.map((skill) => {
|
|
481
|
+
// Handle both string format ("skill-name") and object format ({name, file, model, description})
|
|
482
|
+
if (typeof skill === 'string') {
|
|
483
|
+
return {
|
|
484
|
+
name: skill,
|
|
485
|
+
file: `skills/${skill}.md`,
|
|
486
|
+
model: 'sonnet',
|
|
487
|
+
description: '',
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return {
|
|
492
|
+
name: skill.name || '',
|
|
493
|
+
file: skill.file || `skills/${skill.name}.md`,
|
|
494
|
+
model: skill.model || 'sonnet',
|
|
495
|
+
description: skill.description || '',
|
|
496
|
+
};
|
|
497
|
+
});
|
|
498
|
+
}
|