@rune-kit/rune 2.11.0 → 2.12.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/README.md +58 -1
- package/compiler/__tests__/detect-invariants.test.js +136 -0
- package/compiler/__tests__/doctor-mesh.test.js +229 -0
- package/compiler/__tests__/hook-dispatch.test.js +91 -0
- package/compiler/__tests__/hooks-antigravity.test.js +120 -0
- package/compiler/__tests__/hooks-cursor.test.js +141 -0
- package/compiler/__tests__/hooks-install.test.js +307 -0
- package/compiler/__tests__/hooks-merge.test.js +204 -0
- package/compiler/__tests__/hooks-tiers.test.js +620 -0
- package/compiler/__tests__/hooks-windsurf.test.js +117 -0
- package/compiler/__tests__/inject-claude-md.test.js +152 -0
- package/compiler/__tests__/load-invariants.test.js +408 -0
- package/compiler/__tests__/onboard-invariants.test.js +240 -0
- package/compiler/adapters/hooks/antigravity.js +140 -0
- package/compiler/adapters/hooks/claude.js +166 -0
- package/compiler/adapters/hooks/cursor.js +191 -0
- package/compiler/adapters/hooks/index.js +82 -0
- package/compiler/adapters/hooks/tier-emitter.js +182 -0
- package/compiler/adapters/hooks/windsurf.js +202 -0
- package/compiler/bin/rune.js +196 -6
- package/compiler/commands/hook-dispatch.js +87 -0
- package/compiler/commands/hooks/install.js +120 -0
- package/compiler/commands/hooks/merge.js +211 -0
- package/compiler/commands/hooks/presets.js +116 -0
- package/compiler/commands/hooks/status.js +112 -0
- package/compiler/commands/hooks/tiers.js +323 -0
- package/compiler/commands/hooks/uninstall.js +94 -0
- package/compiler/doctor.js +236 -0
- package/package.json +2 -2
- package/skills/ba/SKILL.md +85 -1
- package/skills/brainstorm/SKILL.md +39 -1
- package/skills/browser-pilot/SKILL.md +1 -0
- package/skills/context-engine/SKILL.md +6 -2
- package/skills/design/SKILL.md +1 -0
- package/skills/docs-seeker/SKILL.md +1 -0
- package/skills/fix/SKILL.md +4 -2
- package/skills/hallucination-guard/SKILL.md +1 -0
- package/skills/journal/SKILL.md +1 -0
- package/skills/logic-guardian/SKILL.md +22 -4
- package/skills/marketing/SKILL.md +62 -1
- package/skills/neural-memory/SKILL.md +13 -16
- package/skills/onboard/SKILL.md +30 -2
- package/skills/onboard/references/invariants-template.md +76 -0
- package/skills/onboard/scripts/detect-invariants.js +439 -0
- package/skills/onboard/scripts/inject-claude-md.js +150 -0
- package/skills/onboard/scripts/onboard-invariants.js +196 -0
- package/skills/perf/SKILL.md +1 -0
- package/skills/plan/SKILL.md +2 -0
- package/skills/preflight/SKILL.md +1 -1
- package/skills/research/SKILL.md +4 -0
- package/skills/review/SKILL.md +4 -2
- package/skills/scope-guard/SKILL.md +4 -1
- package/skills/scout/SKILL.md +6 -0
- package/skills/sentinel/SKILL.md +2 -0
- package/skills/session-bridge/SKILL.md +53 -1
- package/skills/session-bridge/scripts/load-invariants.js +397 -0
- package/skills/slides/SKILL.md +19 -0
- package/skills/team/SKILL.md +2 -1
- package/skills/test/SKILL.md +6 -0
- package/skills/verification/SKILL.md +8 -0
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* load-invariants.js — Parse `.rune/INVARIANTS.md` at session start.
|
|
5
|
+
*
|
|
6
|
+
* Responsibilities:
|
|
7
|
+
* - Read the file if it exists (silent no-op otherwise).
|
|
8
|
+
* - Skip the `## Archived` section entirely (rules there are intentionally
|
|
9
|
+
* retired; including them would re-activate historical noise).
|
|
10
|
+
* - Extract active rules with WHAT / WHERE / WHY + section label.
|
|
11
|
+
* - Produce a token-budgeted preview string (≤ ~500 tokens by default) that
|
|
12
|
+
* session-bridge injects verbatim into the agent's session-start summary.
|
|
13
|
+
* - Detect staleness: if mtime > 30 days old, flag so session-bridge can warn
|
|
14
|
+
* once per session.
|
|
15
|
+
*
|
|
16
|
+
* Returned shape:
|
|
17
|
+
* {
|
|
18
|
+
* loaded: boolean,
|
|
19
|
+
* path: string,
|
|
20
|
+
* stale: boolean, // mtime > 30 days old
|
|
21
|
+
* stats: { danger, critical, state, cross, archivedSkipped, total },
|
|
22
|
+
* rules: Rule[], // [{ section, title, what, where: string[], why }]
|
|
23
|
+
* preview: string, // agent-visible bullet list, capped by budget
|
|
24
|
+
* overflow: number, // rules present but NOT in preview (budget overflow)
|
|
25
|
+
* }
|
|
26
|
+
*
|
|
27
|
+
* Usage as library:
|
|
28
|
+
* import {
|
|
29
|
+
* loadInvariants,
|
|
30
|
+
* matchesInvariant,
|
|
31
|
+
* findMatchingInvariants,
|
|
32
|
+
* } from './load-invariants.js';
|
|
33
|
+
*
|
|
34
|
+
* const result = await loadInvariants({ root, budgetTokens: 500 });
|
|
35
|
+
* // Blast-radius check (used by logic-guardian, Pro autopilot Step 0.5):
|
|
36
|
+
* const hits = findMatchingInvariants('skills/cook/foo.js', result.rules);
|
|
37
|
+
*
|
|
38
|
+
* Usage as CLI:
|
|
39
|
+
* node load-invariants.js --root <project-root> [--json]
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
import { existsSync } from 'node:fs';
|
|
43
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
44
|
+
import path from 'node:path';
|
|
45
|
+
import { parseArgs } from 'node:util';
|
|
46
|
+
|
|
47
|
+
const DEFAULT_BUDGET_TOKENS = 500;
|
|
48
|
+
const STALE_AGE_DAYS = 30;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Rough token estimator. Uses Unicode scalar count (`[...str].length`) rather
|
|
52
|
+
* than `str.length`, because:
|
|
53
|
+
* - `str.length` in JS = UTF-16 code units → emoji outside the BMP count as 2
|
|
54
|
+
* (e.g. `🔒`.length === 2) even though tokenizers treat them as ~1 token.
|
|
55
|
+
* - CJK / Vietnamese text tokenizes closer to 1 char ≈ 1 token, not 1/4 —
|
|
56
|
+
* scalar count gives a safer lower-bound denominator.
|
|
57
|
+
* Not exact tokenization — just a deterministic ceiling that doesn't drift
|
|
58
|
+
* wildly on non-ASCII content.
|
|
59
|
+
*/
|
|
60
|
+
function estimateTokens(str) {
|
|
61
|
+
let scalars = 0;
|
|
62
|
+
for (const _ of str) scalars += 1;
|
|
63
|
+
return Math.ceil(scalars / 3);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Walk `text` line-by-line, tracking triple-backtick / triple-tilde fence
|
|
68
|
+
* state. Yields `{ line, inFence, offset }` for each line. Shared by
|
|
69
|
+
* `stripArchived` and `parseInvariants` so markdown constructs inside fenced
|
|
70
|
+
* code blocks (e.g. a `## Archived` line inside a ```markdown example) never
|
|
71
|
+
* trigger false-positive section breaks or rule headers.
|
|
72
|
+
*/
|
|
73
|
+
function* walkLines(text) {
|
|
74
|
+
let offset = 0;
|
|
75
|
+
let inFence = false;
|
|
76
|
+
let fenceDelim = null;
|
|
77
|
+
while (offset <= text.length) {
|
|
78
|
+
const eol = text.indexOf('\n', offset);
|
|
79
|
+
const lineEnd = eol === -1 ? text.length : eol;
|
|
80
|
+
const line = text.slice(offset, lineEnd);
|
|
81
|
+
const trimmed = line.trimStart();
|
|
82
|
+
if (inFence) {
|
|
83
|
+
if (trimmed.startsWith(fenceDelim)) {
|
|
84
|
+
inFence = false;
|
|
85
|
+
fenceDelim = null;
|
|
86
|
+
}
|
|
87
|
+
yield { line, inFence: true, offset };
|
|
88
|
+
} else {
|
|
89
|
+
if (trimmed.startsWith('```') || trimmed.startsWith('~~~')) {
|
|
90
|
+
inFence = true;
|
|
91
|
+
fenceDelim = trimmed.startsWith('```') ? '```' : '~~~';
|
|
92
|
+
yield { line, inFence: true, offset };
|
|
93
|
+
} else {
|
|
94
|
+
yield { line, inFence: false, offset };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (eol === -1) break;
|
|
98
|
+
offset = eol + 1;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Strip the `## Archived` section (and anything below it) from `text`.
|
|
104
|
+
* Archived rules are intentionally retired — loading them would re-surface
|
|
105
|
+
* historical noise. Fence-aware: `## Archived` inside a code block does NOT
|
|
106
|
+
* terminate active rules.
|
|
107
|
+
*/
|
|
108
|
+
export function stripArchived(text) {
|
|
109
|
+
for (const { line, inFence, offset } of walkLines(text)) {
|
|
110
|
+
if (inFence) continue;
|
|
111
|
+
if (/^## Archived\s*$/.test(line)) {
|
|
112
|
+
return text.slice(0, offset);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return text;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Parse an INVARIANTS.md body (with archived already stripped) into rule
|
|
120
|
+
* objects. Walks `### SectionTitle` and `#### RuleTitle` blocks; expects each
|
|
121
|
+
* rule block to carry `- **WHAT**:`, `- **WHERE**:`, `- **WHY**:` lines.
|
|
122
|
+
* Missing lines are tolerated — they become empty strings / empty arrays.
|
|
123
|
+
*/
|
|
124
|
+
export function parseInvariants(text) {
|
|
125
|
+
const rules = [];
|
|
126
|
+
const SECTION_RE = /^##\s+(Danger Zones|Critical Invariants|State Machine Rules|Cross-File Consistency)\b/;
|
|
127
|
+
const SUBSECTION_RE = /^###\s+(Danger Zones|Critical Invariants|State Machine Rules|Cross-File Consistency)\b/;
|
|
128
|
+
const RULE_HEADER_RE = /^####\s+(.+?)\s*$/;
|
|
129
|
+
const FIELD_RE = /^-\s+\*\*(WHAT|WHERE|WHY)\*\*:\s*(.+)$/;
|
|
130
|
+
|
|
131
|
+
let currentSection = null;
|
|
132
|
+
let current = null;
|
|
133
|
+
|
|
134
|
+
const flush = () => {
|
|
135
|
+
if (current && current.title) rules.push(current);
|
|
136
|
+
current = null;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
for (const { line: raw, inFence } of walkLines(text)) {
|
|
140
|
+
// Fenced code blocks can legitimately contain text that looks like
|
|
141
|
+
// section headers or field lines (e.g. a markdown example inside a WHY).
|
|
142
|
+
// Ignore them while inside a fence.
|
|
143
|
+
if (inFence) continue;
|
|
144
|
+
const line = raw.trimEnd();
|
|
145
|
+
const sectionMatch = SECTION_RE.exec(line) || SUBSECTION_RE.exec(line);
|
|
146
|
+
if (sectionMatch) {
|
|
147
|
+
flush();
|
|
148
|
+
currentSection = sectionLabelToKey(sectionMatch[1]);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const ruleMatch = RULE_HEADER_RE.exec(line);
|
|
152
|
+
if (ruleMatch && currentSection) {
|
|
153
|
+
flush();
|
|
154
|
+
current = {
|
|
155
|
+
section: currentSection,
|
|
156
|
+
title: ruleMatch[1].trim(),
|
|
157
|
+
what: '',
|
|
158
|
+
where: [],
|
|
159
|
+
why: '',
|
|
160
|
+
};
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (!current) continue;
|
|
164
|
+
const fieldMatch = FIELD_RE.exec(line);
|
|
165
|
+
if (!fieldMatch) continue;
|
|
166
|
+
const [, key, value] = fieldMatch;
|
|
167
|
+
if (key === 'WHAT') current.what = value.trim();
|
|
168
|
+
else if (key === 'WHY') current.why = value.trim();
|
|
169
|
+
else if (key === 'WHERE') current.where = extractGlobs(value);
|
|
170
|
+
}
|
|
171
|
+
flush();
|
|
172
|
+
return rules;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function sectionLabelToKey(label) {
|
|
176
|
+
switch (label) {
|
|
177
|
+
case 'Danger Zones':
|
|
178
|
+
return 'danger';
|
|
179
|
+
case 'Critical Invariants':
|
|
180
|
+
return 'critical';
|
|
181
|
+
case 'State Machine Rules':
|
|
182
|
+
return 'state';
|
|
183
|
+
case 'Cross-File Consistency':
|
|
184
|
+
return 'cross';
|
|
185
|
+
default:
|
|
186
|
+
return 'other';
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Convert a minimal glob pattern to a RegExp. Supports the subset of globs
|
|
192
|
+
* seen in INVARIANTS.md `WHERE` entries:
|
|
193
|
+
* - `**` matches any number of path segments (including zero)
|
|
194
|
+
* - `*` matches anything except `/`
|
|
195
|
+
* - `?` matches a single non-`/` character
|
|
196
|
+
* - All other regex metacharacters are escaped literally
|
|
197
|
+
* Paths are normalized to forward slashes before matching.
|
|
198
|
+
*/
|
|
199
|
+
function globToRegExp(glob) {
|
|
200
|
+
const normalized = glob.replace(/\\/g, '/');
|
|
201
|
+
let re = '';
|
|
202
|
+
let i = 0;
|
|
203
|
+
while (i < normalized.length) {
|
|
204
|
+
const c = normalized[i];
|
|
205
|
+
if (c === '*' && normalized[i + 1] === '*') {
|
|
206
|
+
// `**/` = zero or more segments
|
|
207
|
+
if (normalized[i + 2] === '/') {
|
|
208
|
+
re += '(?:.*/)?';
|
|
209
|
+
i += 3;
|
|
210
|
+
} else {
|
|
211
|
+
re += '.*';
|
|
212
|
+
i += 2;
|
|
213
|
+
}
|
|
214
|
+
} else if (c === '*') {
|
|
215
|
+
re += '[^/]*';
|
|
216
|
+
i += 1;
|
|
217
|
+
} else if (c === '?') {
|
|
218
|
+
re += '[^/]';
|
|
219
|
+
i += 1;
|
|
220
|
+
} else if ('.+^$()[]{}|\\'.includes(c)) {
|
|
221
|
+
re += `\\${c}`;
|
|
222
|
+
i += 1;
|
|
223
|
+
} else {
|
|
224
|
+
re += c;
|
|
225
|
+
i += 1;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return new RegExp(`^${re}$`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Return true if `filePath` matches any glob in a rule's `where[]`.
|
|
233
|
+
* Normalizes paths to forward slashes first so Windows and POSIX callers get
|
|
234
|
+
* identical results. Consumers (logic-guardian pre-edit gate, autopilot
|
|
235
|
+
* pre-flight gate) use this to decide whether a planned edit touches an
|
|
236
|
+
* invariant-protected path.
|
|
237
|
+
*/
|
|
238
|
+
export function matchesInvariant(filePath, rule) {
|
|
239
|
+
if (!rule || !Array.isArray(rule.where) || rule.where.length === 0) return false;
|
|
240
|
+
const normalized = filePath.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
241
|
+
for (const glob of rule.where) {
|
|
242
|
+
if (globToRegExp(glob).test(normalized)) return true;
|
|
243
|
+
}
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Return every rule whose WHERE globs match `filePath`. Order preserved from
|
|
249
|
+
* the loaded rules array. Use this when an edit blast radius needs to surface
|
|
250
|
+
* every active invariant that applies.
|
|
251
|
+
*/
|
|
252
|
+
export function findMatchingInvariants(filePath, rules) {
|
|
253
|
+
return rules.filter((rule) => matchesInvariant(filePath, rule));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function extractGlobs(raw) {
|
|
257
|
+
const BACKTICK_RE = /`([^`]+)`/g;
|
|
258
|
+
const globs = [];
|
|
259
|
+
let match;
|
|
260
|
+
BACKTICK_RE.lastIndex = 0;
|
|
261
|
+
while ((match = BACKTICK_RE.exec(raw))) {
|
|
262
|
+
globs.push(match[1].trim());
|
|
263
|
+
}
|
|
264
|
+
// Fallback: if no backticks, split on commas/semicolons.
|
|
265
|
+
if (globs.length === 0) {
|
|
266
|
+
return raw
|
|
267
|
+
.split(/[,;]/)
|
|
268
|
+
.map((s) => s.trim())
|
|
269
|
+
.filter(Boolean);
|
|
270
|
+
}
|
|
271
|
+
return globs;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Render an agent-visible preview. Caps at ~budgetTokens by dropping tail rules
|
|
276
|
+
* and appending an overflow marker. Priority order: danger → critical → state → cross.
|
|
277
|
+
*/
|
|
278
|
+
export function renderPreview(rules, { budgetTokens = DEFAULT_BUDGET_TOKENS } = {}) {
|
|
279
|
+
if (rules.length === 0) {
|
|
280
|
+
return { preview: '', overflow: 0 };
|
|
281
|
+
}
|
|
282
|
+
const priority = { danger: 0, critical: 1, state: 2, cross: 3, other: 4 };
|
|
283
|
+
const ordered = [...rules].sort((a, b) => (priority[a.section] ?? 9) - (priority[b.section] ?? 9));
|
|
284
|
+
|
|
285
|
+
const ICON = { danger: '⚠', critical: '🔒', state: '🔁', cross: '🔗', other: '•' };
|
|
286
|
+
const header = '📎 Active Invariants (.rune/INVARIANTS.md)';
|
|
287
|
+
const lines = [header];
|
|
288
|
+
let tokens = estimateTokens(header);
|
|
289
|
+
let shown = 0;
|
|
290
|
+
|
|
291
|
+
for (const rule of ordered) {
|
|
292
|
+
const icon = ICON[rule.section] ?? '•';
|
|
293
|
+
const wherePreview = rule.where.length > 0 ? rule.where.slice(0, 2).join(', ') : rule.title;
|
|
294
|
+
const line = `${icon} ${wherePreview} — ${rule.what || rule.title}`;
|
|
295
|
+
const cost = estimateTokens(line);
|
|
296
|
+
if (tokens + cost > budgetTokens) break;
|
|
297
|
+
lines.push(line);
|
|
298
|
+
tokens += cost;
|
|
299
|
+
shown += 1;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const overflow = ordered.length - shown;
|
|
303
|
+
if (overflow > 0) {
|
|
304
|
+
lines.push(`…+${overflow} more rule${overflow === 1 ? '' : 's'} in .rune/INVARIANTS.md`);
|
|
305
|
+
}
|
|
306
|
+
return { preview: lines.join('\n'), overflow };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export async function loadInvariants({ root, budgetTokens = DEFAULT_BUDGET_TOKENS } = {}) {
|
|
310
|
+
if (!root) throw new Error('loadInvariants: root is required');
|
|
311
|
+
|
|
312
|
+
const invariantsPath = path.join(root, '.rune', 'INVARIANTS.md');
|
|
313
|
+
const empty = {
|
|
314
|
+
loaded: false,
|
|
315
|
+
count: 0,
|
|
316
|
+
path: invariantsPath,
|
|
317
|
+
stale: false,
|
|
318
|
+
stats: { danger: 0, critical: 0, state: 0, cross: 0, total: 0, archivedSkipped: false },
|
|
319
|
+
rules: [],
|
|
320
|
+
preview: '',
|
|
321
|
+
overflow: 0,
|
|
322
|
+
};
|
|
323
|
+
if (!existsSync(invariantsPath)) return empty;
|
|
324
|
+
|
|
325
|
+
let raw;
|
|
326
|
+
try {
|
|
327
|
+
raw = await readFile(invariantsPath, 'utf8');
|
|
328
|
+
} catch {
|
|
329
|
+
return empty;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const beforeArchive = stripArchived(raw);
|
|
333
|
+
const archivedSkipped = beforeArchive.length !== raw.length;
|
|
334
|
+
const rules = parseInvariants(beforeArchive);
|
|
335
|
+
|
|
336
|
+
const stats = {
|
|
337
|
+
danger: rules.filter((r) => r.section === 'danger').length,
|
|
338
|
+
critical: rules.filter((r) => r.section === 'critical').length,
|
|
339
|
+
state: rules.filter((r) => r.section === 'state').length,
|
|
340
|
+
cross: rules.filter((r) => r.section === 'cross').length,
|
|
341
|
+
total: rules.length,
|
|
342
|
+
archivedSkipped,
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
let stale = false;
|
|
346
|
+
try {
|
|
347
|
+
const st = await stat(invariantsPath);
|
|
348
|
+
const ageDays = (Date.now() - st.mtimeMs) / (1000 * 60 * 60 * 24);
|
|
349
|
+
stale = ageDays > STALE_AGE_DAYS;
|
|
350
|
+
} catch {
|
|
351
|
+
/* stat failure is non-fatal */
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const { preview, overflow } = renderPreview(rules, { budgetTokens });
|
|
355
|
+
|
|
356
|
+
return {
|
|
357
|
+
loaded: rules.length > 0,
|
|
358
|
+
count: rules.length,
|
|
359
|
+
path: invariantsPath,
|
|
360
|
+
stale,
|
|
361
|
+
stats,
|
|
362
|
+
rules,
|
|
363
|
+
preview,
|
|
364
|
+
overflow,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async function main() {
|
|
369
|
+
const { values } = parseArgs({
|
|
370
|
+
options: {
|
|
371
|
+
root: { type: 'string' },
|
|
372
|
+
json: { type: 'boolean', default: false },
|
|
373
|
+
budget: { type: 'string', default: String(DEFAULT_BUDGET_TOKENS) },
|
|
374
|
+
},
|
|
375
|
+
});
|
|
376
|
+
const root = values.root ?? process.cwd();
|
|
377
|
+
const parsedBudget = Number.parseInt(values.budget, 10);
|
|
378
|
+
const budgetTokens = Number.isFinite(parsedBudget) && parsedBudget > 0 ? parsedBudget : DEFAULT_BUDGET_TOKENS;
|
|
379
|
+
const result = await loadInvariants({ root, budgetTokens });
|
|
380
|
+
if (values.json) {
|
|
381
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (!result.loaded) {
|
|
385
|
+
process.stdout.write('No invariants loaded — .rune/INVARIANTS.md missing or empty.\n');
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
process.stdout.write(`${result.preview}\n`);
|
|
389
|
+
if (result.stale) process.stdout.write('\n⚠ Invariants file is stale (>30 days). Run `rune onboard --refresh`.\n');
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith('load-invariants.js')) {
|
|
393
|
+
main().catch((err) => {
|
|
394
|
+
process.stderr.write(`load-invariants: ${err.message}\n`);
|
|
395
|
+
process.exit(1);
|
|
396
|
+
});
|
|
397
|
+
}
|
package/skills/slides/SKILL.md
CHANGED
|
@@ -140,3 +140,22 @@ Slide body content
|
|
|
140
140
|
3. DO NOT require Marp installation — output is standard markdown that Marp can consume
|
|
141
141
|
4. Keep slide count reasonable (5-15 for demos, 10-25 for talks)
|
|
142
142
|
5. Always include speaker notes for non-trivial slides
|
|
143
|
+
|
|
144
|
+
## Returns
|
|
145
|
+
|
|
146
|
+
| Artifact | Format | Location |
|
|
147
|
+
|----------|--------|----------|
|
|
148
|
+
| Slide schema | JSON | `slides.json` (temporary) |
|
|
149
|
+
| Presentation deck | Marp Markdown | `deck.md` or user-specified path |
|
|
150
|
+
|
|
151
|
+
## Done When
|
|
152
|
+
|
|
153
|
+
- Presentation type identified (demo/sprint/talk/tutorial/pitch)
|
|
154
|
+
- JSON schema generated with correct slide types
|
|
155
|
+
- build-deck.js executed (or fallback markdown generated)
|
|
156
|
+
- Output file path presented to user
|
|
157
|
+
- Preview/export commands provided
|
|
158
|
+
|
|
159
|
+
## Cost Profile
|
|
160
|
+
|
|
161
|
+
~500-1500 tokens input, ~300-800 tokens output. Sonnet for quality copy and structure.
|
package/skills/team/SKILL.md
CHANGED
|
@@ -5,7 +5,7 @@ context: fork
|
|
|
5
5
|
agent: general-purpose
|
|
6
6
|
metadata:
|
|
7
7
|
author: runedev
|
|
8
|
-
version: "0.
|
|
8
|
+
version: "1.0.0"
|
|
9
9
|
layer: L1
|
|
10
10
|
model: opus
|
|
11
11
|
group: orchestrator
|
|
@@ -105,6 +105,7 @@ Before decomposing, classify the task into a complexity tier. Each tier defines
|
|
|
105
105
|
|
|
106
106
|
## Called By (inbound)
|
|
107
107
|
|
|
108
|
+
- `scaffold` (L1): decompose scaffolding into parallel workstreams
|
|
108
109
|
- User: `/rune team <task>` direct invocation only
|
|
109
110
|
|
|
110
111
|
---
|
package/skills/test/SKILL.md
CHANGED
|
@@ -307,6 +307,12 @@ Save eval files as `skills/<name>/evals.md`. Each eval is a numbered scenario (E
|
|
|
307
307
|
- `launch` (L1): pre-deployment test suite
|
|
308
308
|
- `safeguard` (L2): writing characterization tests for legacy code
|
|
309
309
|
- `review-intake` (L2): write tests for issues identified during review intake
|
|
310
|
+
- `scaffold` (L1): generate initial test suite for new project
|
|
311
|
+
- `graft` (L2): write integration tests for grafted code
|
|
312
|
+
- `skill-forge` (L2): write tests for new skill functionality
|
|
313
|
+
- `mcp-builder` (L2): write tests for MCP server tools
|
|
314
|
+
- `debug` (L2): write regression test capturing the bug
|
|
315
|
+
- `plan` (L2): reference test requirements in implementation plan
|
|
310
316
|
|
|
311
317
|
## Calls (outbound)
|
|
312
318
|
|
|
@@ -204,6 +204,14 @@ None — pure runner using Bash for all checks. Does not invoke other skills.
|
|
|
204
204
|
- `db` (L2): run migration in test environment
|
|
205
205
|
- `perf` (L2): run benchmark scripts if configured
|
|
206
206
|
- `skill-forge` (L2): verify newly created skill passes lint/type/build checks
|
|
207
|
+
- `team` (L1): verify each parallel workstream before merge
|
|
208
|
+
- `scaffold` (L1): verify scaffolded project builds and passes initial tests
|
|
209
|
+
- `launch` (L1): pre-deploy verification gate
|
|
210
|
+
- `mcp-builder` (L2): verify generated MCP server compiles and starts
|
|
211
|
+
- `preflight` (L2): run verification as part of pre-commit quality gate
|
|
212
|
+
- `logic-guardian` (L2): verify logic invariants hold after changes
|
|
213
|
+
- `dependency-doctor` (L3): verify builds pass after dependency updates
|
|
214
|
+
- `sast` (L3): run verification alongside static analysis
|
|
207
215
|
|
|
208
216
|
## Output Format
|
|
209
217
|
|