@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.
Files changed (60) hide show
  1. package/README.md +58 -1
  2. package/compiler/__tests__/detect-invariants.test.js +136 -0
  3. package/compiler/__tests__/doctor-mesh.test.js +229 -0
  4. package/compiler/__tests__/hook-dispatch.test.js +91 -0
  5. package/compiler/__tests__/hooks-antigravity.test.js +120 -0
  6. package/compiler/__tests__/hooks-cursor.test.js +141 -0
  7. package/compiler/__tests__/hooks-install.test.js +307 -0
  8. package/compiler/__tests__/hooks-merge.test.js +204 -0
  9. package/compiler/__tests__/hooks-tiers.test.js +620 -0
  10. package/compiler/__tests__/hooks-windsurf.test.js +117 -0
  11. package/compiler/__tests__/inject-claude-md.test.js +152 -0
  12. package/compiler/__tests__/load-invariants.test.js +408 -0
  13. package/compiler/__tests__/onboard-invariants.test.js +240 -0
  14. package/compiler/adapters/hooks/antigravity.js +140 -0
  15. package/compiler/adapters/hooks/claude.js +166 -0
  16. package/compiler/adapters/hooks/cursor.js +191 -0
  17. package/compiler/adapters/hooks/index.js +82 -0
  18. package/compiler/adapters/hooks/tier-emitter.js +182 -0
  19. package/compiler/adapters/hooks/windsurf.js +202 -0
  20. package/compiler/bin/rune.js +196 -6
  21. package/compiler/commands/hook-dispatch.js +87 -0
  22. package/compiler/commands/hooks/install.js +120 -0
  23. package/compiler/commands/hooks/merge.js +211 -0
  24. package/compiler/commands/hooks/presets.js +116 -0
  25. package/compiler/commands/hooks/status.js +112 -0
  26. package/compiler/commands/hooks/tiers.js +323 -0
  27. package/compiler/commands/hooks/uninstall.js +94 -0
  28. package/compiler/doctor.js +236 -0
  29. package/package.json +2 -2
  30. package/skills/ba/SKILL.md +85 -1
  31. package/skills/brainstorm/SKILL.md +39 -1
  32. package/skills/browser-pilot/SKILL.md +1 -0
  33. package/skills/context-engine/SKILL.md +6 -2
  34. package/skills/design/SKILL.md +1 -0
  35. package/skills/docs-seeker/SKILL.md +1 -0
  36. package/skills/fix/SKILL.md +4 -2
  37. package/skills/hallucination-guard/SKILL.md +1 -0
  38. package/skills/journal/SKILL.md +1 -0
  39. package/skills/logic-guardian/SKILL.md +22 -4
  40. package/skills/marketing/SKILL.md +62 -1
  41. package/skills/neural-memory/SKILL.md +13 -16
  42. package/skills/onboard/SKILL.md +30 -2
  43. package/skills/onboard/references/invariants-template.md +76 -0
  44. package/skills/onboard/scripts/detect-invariants.js +439 -0
  45. package/skills/onboard/scripts/inject-claude-md.js +150 -0
  46. package/skills/onboard/scripts/onboard-invariants.js +196 -0
  47. package/skills/perf/SKILL.md +1 -0
  48. package/skills/plan/SKILL.md +2 -0
  49. package/skills/preflight/SKILL.md +1 -1
  50. package/skills/research/SKILL.md +4 -0
  51. package/skills/review/SKILL.md +4 -2
  52. package/skills/scope-guard/SKILL.md +4 -1
  53. package/skills/scout/SKILL.md +6 -0
  54. package/skills/sentinel/SKILL.md +2 -0
  55. package/skills/session-bridge/SKILL.md +53 -1
  56. package/skills/session-bridge/scripts/load-invariants.js +397 -0
  57. package/skills/slides/SKILL.md +19 -0
  58. package/skills/team/SKILL.md +2 -1
  59. package/skills/test/SKILL.md +6 -0
  60. 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
+ }
@@ -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.
@@ -5,7 +5,7 @@ context: fork
5
5
  agent: general-purpose
6
6
  metadata:
7
7
  author: runedev
8
- version: "0.8.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
  ---
@@ -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