@rune-kit/rune 2.17.1 → 2.18.1
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 +12 -4
- package/compiler/__tests__/adapter-model-mapping.test.js +80 -0
- package/compiler/__tests__/adapters.test.js +115 -3
- package/compiler/__tests__/setup.test.js +333 -7
- package/compiler/adapters/aider.js +120 -0
- package/compiler/adapters/codex.js +33 -0
- package/compiler/adapters/copilot.js +126 -0
- package/compiler/adapters/gemini.js +131 -0
- package/compiler/adapters/index.js +10 -0
- package/compiler/adapters/qoder.js +102 -0
- package/compiler/adapters/qwen.js +111 -0
- package/compiler/bin/rune.js +5 -3
- package/compiler/commands/hooks/drift.js +5 -2
- package/compiler/commands/setup.js +193 -4
- package/compiler/emitter.js +38 -41
- package/hooks/session-start/index.cjs +91 -0
- package/package.json +1 -1
- package/skills/asset-creator/SKILL.md +1 -1
- package/skills/audit/SKILL.md +20 -2
- package/skills/autopsy/SKILL.md +173 -2
- package/skills/brainstorm/SKILL.md +24 -1
- package/skills/browser-pilot/SKILL.md +16 -1
- package/skills/debug/SKILL.md +4 -2
- package/skills/deploy/SKILL.md +72 -2
- package/skills/design/SKILL.md +50 -3
- package/skills/launch/SKILL.md +11 -1
- package/skills/marketing/SKILL.md +1 -1
- package/skills/neural-memory/SKILL.md +1 -1
- package/skills/perf/SKILL.md +93 -2
- package/skills/sentinel-env/SKILL.md +2 -2
- package/skills/skill-forge/SKILL.md +47 -1
- package/skills/surgeon/SKILL.md +1 -1
- package/skills/team/SKILL.md +27 -1
|
@@ -62,7 +62,8 @@ export async function checkHookDrift(projectRoot) {
|
|
|
62
62
|
platform: id,
|
|
63
63
|
event: null,
|
|
64
64
|
status: 'mixed-preset',
|
|
65
|
-
message:
|
|
65
|
+
message:
|
|
66
|
+
'settings.json contains both gentle and strict Rune entries — re-run `rune hooks install --preset <gentle|strict>` to converge',
|
|
66
67
|
});
|
|
67
68
|
}
|
|
68
69
|
|
|
@@ -159,7 +160,9 @@ export function formatHookDriftResult(result) {
|
|
|
159
160
|
}
|
|
160
161
|
|
|
161
162
|
lines.push('');
|
|
162
|
-
lines.push(
|
|
163
|
+
lines.push(
|
|
164
|
+
` Summary: ${result.summary.drifted} drifted, ${result.summary.missing} missing, ${result.summary.errors} error(s).`,
|
|
165
|
+
);
|
|
163
166
|
lines.push(' Resolution: re-run `rune hooks install --preset <gentle|strict>` to re-converge.');
|
|
164
167
|
lines.push(' This is a reporter — operator decides what to do with the findings.');
|
|
165
168
|
|
|
@@ -21,11 +21,12 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import { existsSync, readFileSync } from 'node:fs';
|
|
24
|
+
import { cp, readdir, readFile, rm } from 'node:fs/promises';
|
|
24
25
|
import os from 'node:os';
|
|
25
26
|
import path from 'node:path';
|
|
26
27
|
import { createInterface } from 'node:readline';
|
|
27
28
|
import { installHooks } from './hooks/install.js';
|
|
28
|
-
import { TIER_ENV_VARS } from './hooks/tiers.js';
|
|
29
|
+
import { resolveTier, TIER_ENV_VARS } from './hooks/tiers.js';
|
|
29
30
|
|
|
30
31
|
export const WELL_KNOWN_TIER_PATHS = {
|
|
31
32
|
pro: ['D:/Project/Rune/Pro', path.join(os.homedir(), 'rune-pro'), path.join(os.homedir(), 'Project', 'Rune', 'Pro')],
|
|
@@ -38,9 +39,9 @@ export const WELL_KNOWN_TIER_PATHS = {
|
|
|
38
39
|
|
|
39
40
|
/**
|
|
40
41
|
* @param {{ projectRoot: string, runeRoot: string, args: object }} opts
|
|
41
|
-
* @returns {Promise<{ scope: string, tiers: string[], preset: string, written: boolean, files: string[], notes: string[] }>}
|
|
42
|
+
* @returns {Promise<{ scope: string, tiers: string[], preset: string, written: boolean, files: string[], notes: string[], skillResults: Array<{tier: string, installed: string[], skipped: Array<{skill: string, reason: string}>, reason: string|null}> }>}
|
|
42
43
|
*/
|
|
43
|
-
export async function runSetup({ projectRoot, runeRoot
|
|
44
|
+
export async function runSetup({ projectRoot, runeRoot, args = {} }) {
|
|
44
45
|
const detected = detectTiers(projectRoot);
|
|
45
46
|
|
|
46
47
|
// Scope resolution
|
|
@@ -88,16 +89,180 @@ export async function runSetup({ projectRoot, runeRoot: _runeRoot, args = {} })
|
|
|
88
89
|
dry: args.dry,
|
|
89
90
|
});
|
|
90
91
|
|
|
92
|
+
// Install tier skill files into <runeRoot>/skills/ so Claude Code (and any
|
|
93
|
+
// plugin-aware platform) discovers them alongside Free skills. Without this
|
|
94
|
+
// step, paid tiers ship hooks only — `rune:autopilot` returns "Unknown skill"
|
|
95
|
+
// because Pro/skills/autopilot/ is invisible to the plugin runtime.
|
|
96
|
+
const skillResults = [];
|
|
97
|
+
for (const tier of tiers) {
|
|
98
|
+
try {
|
|
99
|
+
const tierManifest = await resolveTier(tier, projectRoot);
|
|
100
|
+
const skillResult = await installTierSkills({
|
|
101
|
+
tier,
|
|
102
|
+
tierManifest,
|
|
103
|
+
runeRoot,
|
|
104
|
+
dry: args.dry,
|
|
105
|
+
});
|
|
106
|
+
skillResults.push(skillResult);
|
|
107
|
+
} catch (err) {
|
|
108
|
+
skillResults.push({ tier, installed: [], skipped: [], reason: err.message });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
91
112
|
return {
|
|
92
113
|
scope,
|
|
93
114
|
targetRoot,
|
|
94
115
|
tiers,
|
|
95
116
|
preset,
|
|
96
117
|
detected,
|
|
118
|
+
skillResults,
|
|
97
119
|
...result,
|
|
98
120
|
};
|
|
99
121
|
}
|
|
100
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Copy a tier's skill directories into the Free plugin's skills/ folder so
|
|
125
|
+
* the Claude Code plugin runtime discovers them with the `rune:` prefix.
|
|
126
|
+
*
|
|
127
|
+
* Idempotent: existing skill directories are SKIPPED (not overwritten). This
|
|
128
|
+
* protects Free skills from being clobbered by a same-named Pro skill, and
|
|
129
|
+
* protects an in-place edit of a previously-installed Pro skill from being
|
|
130
|
+
* stomped on a re-run.
|
|
131
|
+
*
|
|
132
|
+
* Pre-condition: tierManifest.source points at the absolute path of the tier's
|
|
133
|
+
* manifest.json (set by validateManifest). Tier root = grandparent of that path.
|
|
134
|
+
*
|
|
135
|
+
* @param {object} opts
|
|
136
|
+
* @param {string} opts.tier
|
|
137
|
+
* @param {import('./hooks/tiers.js').TierManifest} opts.tierManifest
|
|
138
|
+
* @param {string} opts.runeRoot — target is `<runeRoot>/skills/`
|
|
139
|
+
* @param {boolean} [opts.dry]
|
|
140
|
+
* @returns {Promise<{tier: string, installed: string[], skipped: Array<{skill: string, reason: string}>, reason: string|null}>}
|
|
141
|
+
*/
|
|
142
|
+
export async function installTierSkills({ tier, tierManifest, runeRoot, dry }) {
|
|
143
|
+
if (!tierManifest?.source) {
|
|
144
|
+
return { tier, installed: [], skipped: [], reason: 'tier manifest source missing' };
|
|
145
|
+
}
|
|
146
|
+
// Defensive: validateManifest accepts any string; only locateTierManifest guarantees absolute.
|
|
147
|
+
// Reject relative to keep tierRoot derivation honest.
|
|
148
|
+
if (!path.isAbsolute(tierManifest.source)) {
|
|
149
|
+
return {
|
|
150
|
+
tier,
|
|
151
|
+
installed: [],
|
|
152
|
+
skipped: [],
|
|
153
|
+
reason: `tier manifest source must be absolute (got ${tierManifest.source})`,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
const tierRoot = path.dirname(path.dirname(tierManifest.source));
|
|
157
|
+
const sourceDir = path.join(tierRoot, 'skills');
|
|
158
|
+
if (!existsSync(sourceDir)) {
|
|
159
|
+
return { tier, installed: [], skipped: [], reason: `no skills/ dir at ${sourceDir}` };
|
|
160
|
+
}
|
|
161
|
+
if (!runeRoot) {
|
|
162
|
+
return { tier, installed: [], skipped: [], reason: 'runeRoot not provided' };
|
|
163
|
+
}
|
|
164
|
+
const targetDir = path.join(runeRoot, 'skills');
|
|
165
|
+
if (!existsSync(targetDir)) {
|
|
166
|
+
return { tier, installed: [], skipped: [], reason: `target skills/ missing at ${targetDir}` };
|
|
167
|
+
}
|
|
168
|
+
const installed = [];
|
|
169
|
+
const skipped = [];
|
|
170
|
+
const entries = await readdir(sourceDir, { withFileTypes: true });
|
|
171
|
+
for (const entry of entries) {
|
|
172
|
+
if (!entry.isDirectory()) {
|
|
173
|
+
// Reject symlinks even when their target is a directory — `cp` with default
|
|
174
|
+
// dereference=false would recreate the symlink and let the Claude Code runtime
|
|
175
|
+
// follow it outside runeRoot/skills/ at read time (POSIX symlink-escape).
|
|
176
|
+
if (entry.isSymbolicLink()) {
|
|
177
|
+
skipped.push({ skill: entry.name, reason: 'rejected: symlink (would escape sandbox)' });
|
|
178
|
+
}
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const skillName = entry.name;
|
|
182
|
+
// Guard against path traversal via adversarial directory names (../, /, \).
|
|
183
|
+
// A compromised tier repo with a skill dir like '../../../etc' would otherwise
|
|
184
|
+
// escape runeRoot/skills/ and overwrite arbitrary files. assertSafeTierName
|
|
185
|
+
// in tiers.js covers the tier name but NOT per-skill directory names.
|
|
186
|
+
if (path.basename(skillName) !== skillName || skillName === '.' || skillName === '..') {
|
|
187
|
+
skipped.push({ skill: skillName, reason: 'rejected: unsafe directory name' });
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const src = path.join(sourceDir, skillName);
|
|
191
|
+
const dst = path.join(targetDir, skillName);
|
|
192
|
+
if (existsSync(dst)) {
|
|
193
|
+
const drift = await detectVersionDrift(src, dst);
|
|
194
|
+
skipped.push({ skill: skillName, reason: drift || 'already present' });
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (!dry) {
|
|
198
|
+
try {
|
|
199
|
+
// dereference: true → copy symlink targets as content instead of recreating
|
|
200
|
+
// symlinks at dst. Belt-and-suspenders alongside the isSymbolicLink reject above
|
|
201
|
+
// (nested symlinks inside a skill dir would otherwise still recreate).
|
|
202
|
+
await cp(src, dst, { recursive: true, dereference: true });
|
|
203
|
+
} catch (err) {
|
|
204
|
+
// Clean partial-copy residue so next run isn't silently locked-out by
|
|
205
|
+
// existsSync(dst) on a corrupt half-written directory.
|
|
206
|
+
await rm(dst, { recursive: true, force: true }).catch(() => {});
|
|
207
|
+
skipped.push({ skill: skillName, reason: `copy failed: ${err.message}` });
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
installed.push(skillName);
|
|
212
|
+
}
|
|
213
|
+
return { tier, installed, skipped, reason: null };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Compare source SKILL.md version against installed version. Returns a string
|
|
218
|
+
* describing the drift if source is newer (so user knows an upgrade exists),
|
|
219
|
+
* or null when versions match / cannot be parsed. Never throws — best-effort.
|
|
220
|
+
*
|
|
221
|
+
* @param {string} srcSkillDir
|
|
222
|
+
* @param {string} dstSkillDir
|
|
223
|
+
* @returns {Promise<string|null>}
|
|
224
|
+
*/
|
|
225
|
+
async function detectVersionDrift(srcSkillDir, dstSkillDir) {
|
|
226
|
+
try {
|
|
227
|
+
const [srcVer, dstVer] = await Promise.all([
|
|
228
|
+
readSkillVersion(path.join(srcSkillDir, 'SKILL.md')),
|
|
229
|
+
readSkillVersion(path.join(dstSkillDir, 'SKILL.md')),
|
|
230
|
+
]);
|
|
231
|
+
if (!srcVer || !dstVer) return null;
|
|
232
|
+
if (srcVer === dstVer) return `already present (v${dstVer})`;
|
|
233
|
+
return `stale: installed v${dstVer}, source has v${srcVer} — delete target dir to upgrade`;
|
|
234
|
+
} catch {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Extract version string from a SKILL.md `metadata.version` field. Minimal
|
|
241
|
+
* YAML scan scoped to the `metadata:` block — avoids false positives from
|
|
242
|
+
* multiline description continuation lines that contain `version:` text
|
|
243
|
+
* (e.g. `description: |\n version: 1.0.0 is legacy`). Avoids pulling in a
|
|
244
|
+
* full YAML dependency for one field.
|
|
245
|
+
*
|
|
246
|
+
* Returns null when SKILL.md cannot be read, frontmatter missing, metadata
|
|
247
|
+
* block missing, or version field absent.
|
|
248
|
+
*
|
|
249
|
+
* @param {string} skillMdPath
|
|
250
|
+
* @returns {Promise<string|null>}
|
|
251
|
+
*/
|
|
252
|
+
async function readSkillVersion(skillMdPath) {
|
|
253
|
+
try {
|
|
254
|
+
const content = await readFile(skillMdPath, 'utf-8');
|
|
255
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
256
|
+
if (!fmMatch) return null;
|
|
257
|
+
// Require metadata: header followed by an indented version: line.
|
|
258
|
+
// Indent is preserved so a top-level `version:` (outside metadata) doesn't match.
|
|
259
|
+
const verMatch = fmMatch[1].match(/^metadata:\s*\n(?:\s+[^\n]*\n)*?\s+version:\s*["']?([^"'\s#]+)/m);
|
|
260
|
+
return verMatch ? verMatch[1] : null;
|
|
261
|
+
} catch {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
101
266
|
/**
|
|
102
267
|
* Auto-detect Pro/Business tiers across env vars, sibling paths, and well-known
|
|
103
268
|
* locations. Returns { pro: { path, source } | null, business: { ... } | null }.
|
|
@@ -222,10 +387,34 @@ export function formatSetupResult(result) {
|
|
|
222
387
|
lines.push('');
|
|
223
388
|
lines.push(' Rune Setup Complete');
|
|
224
389
|
lines.push(' ──────────────────');
|
|
225
|
-
lines.push(
|
|
390
|
+
lines.push(
|
|
391
|
+
` Scope: ${result.scope === 'global' ? 'GLOBAL (~/.claude/settings.json)' : `current project (${result.targetRoot})`}`,
|
|
392
|
+
);
|
|
226
393
|
lines.push(` Tiers: Free${result.tiers.length > 0 ? ` + ${result.tiers.join(' + ')}` : ''}`);
|
|
227
394
|
lines.push(` Preset: ${result.preset}`);
|
|
228
395
|
lines.push(` Platforms: ${(result.platforms || []).join(', ') || '—'}`);
|
|
396
|
+
for (const sr of result.skillResults || []) {
|
|
397
|
+
if (sr.installed.length > 0) {
|
|
398
|
+
const preview = sr.installed.slice(0, 3).join(', ');
|
|
399
|
+
const more = sr.installed.length > 3 ? `, +${sr.installed.length - 3}` : '';
|
|
400
|
+
lines.push(` Skills: ${sr.tier}: ${sr.installed.length} installed (${preview}${more})`);
|
|
401
|
+
}
|
|
402
|
+
if (sr.skipped.length > 0) {
|
|
403
|
+
const rejected = sr.skipped.filter((s) => s.reason.startsWith('rejected:'));
|
|
404
|
+
const benign = sr.skipped.length - rejected.length;
|
|
405
|
+
const parts = [];
|
|
406
|
+
if (benign > 0) parts.push(`${benign} already present`);
|
|
407
|
+
if (rejected.length > 0) parts.push(`${rejected.length} rejected`);
|
|
408
|
+
lines.push(` Skipped: ${sr.tier}: ${parts.join(', ')}`);
|
|
409
|
+
// Surface rejection details so the operator can investigate a compromised tier repo.
|
|
410
|
+
for (const r of rejected) {
|
|
411
|
+
lines.push(` ⚠ ${r.skill}: ${r.reason}`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (sr.reason) {
|
|
415
|
+
lines.push(` Skill warn:${sr.tier}: ${sr.reason}`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
229
418
|
if (result.notes?.length) {
|
|
230
419
|
lines.push('');
|
|
231
420
|
lines.push(' Notes:');
|
package/compiler/emitter.js
CHANGED
|
@@ -700,11 +700,44 @@ export async function buildAll({
|
|
|
700
700
|
await writeFile(path.join(outputDir, 'skill-index.json'), `${JSON.stringify(skillIndex, null, 2)}\n`, 'utf-8');
|
|
701
701
|
stats.files.push('skill-index.json');
|
|
702
702
|
|
|
703
|
-
//
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
703
|
+
// Generic extra-files hook: any adapter can emit additional index/bundle files
|
|
704
|
+
// alongside per-skill files (e.g. aider's .aider.conf.yml, qwen's QWEN.md, gemini's GEMINI.md bundle).
|
|
705
|
+
// Hook contract: paths MUST be relative to outputRoot — absolute paths are rejected to prevent
|
|
706
|
+
// accidental writes outside the project tree. Adapters receive a frozen stats snapshot so reads
|
|
707
|
+
// are deterministic regardless of where the hook fires relative to other emit steps.
|
|
708
|
+
if (typeof adapter.generateExtraFiles === 'function') {
|
|
709
|
+
const frozenStats = Object.freeze({ ...stats, files: Object.freeze([...stats.files]) });
|
|
710
|
+
const extras = await adapter.generateExtraFiles({
|
|
711
|
+
parsedSkills,
|
|
712
|
+
stats: frozenStats,
|
|
713
|
+
runeRoot,
|
|
714
|
+
outputRoot,
|
|
715
|
+
outputDir,
|
|
716
|
+
});
|
|
717
|
+
if (Array.isArray(extras)) {
|
|
718
|
+
const outputRootResolved = path.resolve(outputRoot);
|
|
719
|
+
for (const extra of extras) {
|
|
720
|
+
if (!extra || !extra.path || extra.content == null) continue;
|
|
721
|
+
if (path.isAbsolute(extra.path)) {
|
|
722
|
+
stats.errors.push({
|
|
723
|
+
adapter: adapter.name,
|
|
724
|
+
error: `generateExtraFiles must return relative paths, got absolute: ${extra.path}`,
|
|
725
|
+
});
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
const fullPath = path.resolve(outputRootResolved, extra.path);
|
|
729
|
+
if (fullPath !== outputRootResolved && !fullPath.startsWith(outputRootResolved + path.sep)) {
|
|
730
|
+
stats.errors.push({
|
|
731
|
+
adapter: adapter.name,
|
|
732
|
+
error: `generateExtraFiles path escapes outputRoot: ${extra.path}`,
|
|
733
|
+
});
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
await mkdir(path.dirname(fullPath), { recursive: true });
|
|
737
|
+
await writeFile(fullPath, extra.content, 'utf-8');
|
|
738
|
+
stats.files.push(path.relative(outputRoot, fullPath).replaceAll('\\', '/'));
|
|
739
|
+
}
|
|
740
|
+
}
|
|
708
741
|
}
|
|
709
742
|
|
|
710
743
|
// OpenClaw adapter: generate manifest + TypeScript entry point
|
|
@@ -774,42 +807,6 @@ function generateIndex(stats, adapter) {
|
|
|
774
807
|
return lines.join('\n');
|
|
775
808
|
}
|
|
776
809
|
|
|
777
|
-
/**
|
|
778
|
-
* Generate AGENTS.md for Codex (OpenAI convention)
|
|
779
|
-
* Uses dynamic counts from build stats — no hardcoded skill lists
|
|
780
|
-
*/
|
|
781
|
-
function generateAgentsMd(stats, adapter) {
|
|
782
|
-
const lines = [
|
|
783
|
-
'# Rune — Project Configuration',
|
|
784
|
-
'',
|
|
785
|
-
'## Overview',
|
|
786
|
-
'',
|
|
787
|
-
'Rune is an interconnected skill ecosystem for AI coding assistants.',
|
|
788
|
-
`${stats.skillCount} core skills | 5-layer mesh architecture | ${stats.crossRefsResolved} connections | Multi-platform.`,
|
|
789
|
-
'Philosophy: "Less skills. Deeper connections."',
|
|
790
|
-
'',
|
|
791
|
-
`Platform: ${adapter.name}`,
|
|
792
|
-
'',
|
|
793
|
-
'## Skills',
|
|
794
|
-
'',
|
|
795
|
-
`**${stats.skillCount} core skills** + **${stats.packCount} extension packs**`,
|
|
796
|
-
'',
|
|
797
|
-
'## Usage',
|
|
798
|
-
'',
|
|
799
|
-
'Reference skills using the `Skill` tool or delegate to subagents using the `Agent` tool.',
|
|
800
|
-
'',
|
|
801
|
-
'## Skills Directory',
|
|
802
|
-
'',
|
|
803
|
-
`Skills are located in: ${adapter.outputDir}/`,
|
|
804
|
-
'',
|
|
805
|
-
'---',
|
|
806
|
-
'> Rune Skill Mesh — https://github.com/rune-kit/rune',
|
|
807
|
-
'',
|
|
808
|
-
];
|
|
809
|
-
|
|
810
|
-
return lines.join('\n');
|
|
811
|
-
}
|
|
812
|
-
|
|
813
810
|
/**
|
|
814
811
|
* Intent keyword patterns for each skill — extracted from description + Triggers section
|
|
815
812
|
* Maps common user intent words to the skill that handles them
|
|
@@ -69,3 +69,94 @@ if (fs.existsSync(runeDir)) {
|
|
|
69
69
|
} else {
|
|
70
70
|
console.log('[Rune: No .rune/ directory found. Run /rune onboard to set up project context.]');
|
|
71
71
|
}
|
|
72
|
+
|
|
73
|
+
// Tier detection hint (v2.17.1+) — Pro/Business plugins live in private repos
|
|
74
|
+
// and aren't auto-loaded like the Free plugin. If detected at sibling / env /
|
|
75
|
+
// well-known path AND tier hooks aren't already wired in settings.json, nudge
|
|
76
|
+
// user toward `rune setup`. Self-suppressing — once wired, the check fails and
|
|
77
|
+
// the hint stops firing.
|
|
78
|
+
detectTierHint();
|
|
79
|
+
|
|
80
|
+
function detectTierHint() {
|
|
81
|
+
const envVars = { pro: 'RUNE_PRO_ROOT', business: 'RUNE_BUSINESS_ROOT' };
|
|
82
|
+
const wellKnown = {
|
|
83
|
+
pro: [
|
|
84
|
+
'D:/Project/Rune/Pro',
|
|
85
|
+
path.join(os.homedir(), 'rune-pro'),
|
|
86
|
+
path.join(os.homedir(), 'Project', 'Rune', 'Pro'),
|
|
87
|
+
],
|
|
88
|
+
business: [
|
|
89
|
+
'D:/Project/Rune/Business',
|
|
90
|
+
path.join(os.homedir(), 'rune-business'),
|
|
91
|
+
path.join(os.homedir(), 'Project', 'Rune', 'Business'),
|
|
92
|
+
],
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const detected = [];
|
|
96
|
+
for (const tier of ['pro', 'business']) {
|
|
97
|
+
let manifest = null;
|
|
98
|
+
let source = null;
|
|
99
|
+
|
|
100
|
+
const fromEnv = process.env[envVars[tier]];
|
|
101
|
+
if (fromEnv) {
|
|
102
|
+
const m = path.join(fromEnv, 'hooks', 'manifest.json');
|
|
103
|
+
if (fs.existsSync(m)) {
|
|
104
|
+
manifest = m;
|
|
105
|
+
source = `$${envVars[tier]}`;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (!manifest) {
|
|
109
|
+
const m = path.join(cwd, '..', tier === 'pro' ? 'Pro' : 'Business', 'hooks', 'manifest.json');
|
|
110
|
+
if (fs.existsSync(m)) {
|
|
111
|
+
manifest = m;
|
|
112
|
+
source = 'sibling';
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (!manifest) {
|
|
116
|
+
for (const root of wellKnown[tier]) {
|
|
117
|
+
const m = path.join(root, 'hooks', 'manifest.json');
|
|
118
|
+
if (fs.existsSync(m)) {
|
|
119
|
+
manifest = m;
|
|
120
|
+
source = 'well-known';
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (manifest) {
|
|
127
|
+
detected.push({ tier, source, version: readManifestVersion(manifest) });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (detected.length === 0) return;
|
|
132
|
+
|
|
133
|
+
// Suppress hint if any tier hook already wired (project-local OR global)
|
|
134
|
+
const tierEnvRe = /\$\{RUNE_[A-Z][A-Z0-9_]*_ROOT\}/;
|
|
135
|
+
const settingsPaths = [path.join(cwd, '.claude', 'settings.json'), path.join(os.homedir(), '.claude', 'settings.json')];
|
|
136
|
+
for (const settingsPath of settingsPaths) {
|
|
137
|
+
if (!fs.existsSync(settingsPath)) continue;
|
|
138
|
+
try {
|
|
139
|
+
const content = fs.readFileSync(settingsPath, 'utf-8');
|
|
140
|
+
if (tierEnvRe.test(content)) return;
|
|
141
|
+
} catch {
|
|
142
|
+
// ignore unreadable settings.json — fall through to print hint
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
console.log('\n=== Rune Tier Hint ===');
|
|
147
|
+
for (const { tier, source, version } of detected) {
|
|
148
|
+
const cap = tier.charAt(0).toUpperCase() + tier.slice(1);
|
|
149
|
+
console.log(`${cap} detected: ${source} (v${version})`);
|
|
150
|
+
}
|
|
151
|
+
const tierFlag = detected.map((d) => d.tier).join(',');
|
|
152
|
+
console.log(`Wire it: \`npx @rune-kit/rune setup --global --tier ${tierFlag}\``);
|
|
153
|
+
console.log('(adds tier-specific hooks: autopilot circuit-breaker, context-sense, statusline)');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function readManifestVersion(manifestPath) {
|
|
157
|
+
try {
|
|
158
|
+
return JSON.parse(fs.readFileSync(manifestPath, 'utf-8')).version || 'unknown';
|
|
159
|
+
} catch {
|
|
160
|
+
return 'unknown';
|
|
161
|
+
}
|
|
162
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rune-kit/rune",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.18.1",
|
|
4
4
|
"description": "64-skill mesh for AI coding assistants — runtime auto-discipline via native hooks (Claude/Cursor/Windsurf/Antigravity), 5-layer architecture, 215+ connections, multi-platform compiler. v2.17 adds quarantine L3 for prompt-injection advisory on untrusted external content.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: asset-creator
|
|
3
|
-
description: "Creates code-based visual assets — SVG icons, OG image HTML templates, social banners, and icon sets. Outputs files with usage instructions."
|
|
3
|
+
description: "Creates code-based visual assets — SVG icons, OG image HTML templates, social banners, and icon sets. Use when generating visuals as code (vector/HTML), NOT raster images — for raster generation see @rune-pro/media. Outputs files with usage instructions."
|
|
4
4
|
metadata:
|
|
5
5
|
author: runedev
|
|
6
6
|
version: "0.2.0"
|
package/skills/audit/SKILL.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: audit
|
|
3
|
-
description: "Comprehensive project audit — security, dependencies, code quality, architecture, performance, infra, docs, and mesh analytics. Delegates to specialist skills
|
|
3
|
+
description: "Comprehensive project audit — security, dependencies, code quality, architecture, performance, infra, docs, and mesh analytics. Use when needing a full 8-dimension health-score snapshot before a major release, M&A diligence, or quarterly review. Delegates to specialist skills (sentinel, dependency-doctor, perf, autopsy, etc.)."
|
|
4
4
|
metadata:
|
|
5
5
|
author: runedev
|
|
6
|
-
version: "0.
|
|
6
|
+
version: "0.5.0"
|
|
7
7
|
layer: L2
|
|
8
8
|
model: sonnet
|
|
9
9
|
group: quality
|
|
@@ -130,6 +130,24 @@ grep -rn "\.unwrap()" src/ --include="*.rs"
|
|
|
130
130
|
|
|
131
131
|
Merge autopsy report + supplementary findings.
|
|
132
132
|
|
|
133
|
+
**3.5 Zombie Code Detection**
|
|
134
|
+
|
|
135
|
+
Identify code that is effectively dead but hasn't been formally removed. Zombie code increases surface area, confuses contributors, and accumulates security debt.
|
|
136
|
+
|
|
137
|
+
| Signal | Detection Method | Severity |
|
|
138
|
+
|--------|-----------------|----------|
|
|
139
|
+
| No commits in 6+ months | `git log --since="6 months ago" -- <file>` returns empty | MEDIUM |
|
|
140
|
+
| No test coverage | File not imported by any test file (Grep for filename in `**/*.test.*` / `**/*.spec.*`) | MEDIUM |
|
|
141
|
+
| No owner | File not in CODEOWNERS, no author active in last 6 months | LOW |
|
|
142
|
+
| Failing tests referencing the module | Test suite has skipped/failing tests for this module | HIGH |
|
|
143
|
+
| Unpatched CVEs in dependencies only this module uses | Cross-reference dependency-doctor CVE list with per-file imports | HIGH |
|
|
144
|
+
| Orphaned docs | README/docs reference files or APIs that no longer exist | LOW |
|
|
145
|
+
|
|
146
|
+
**Zombie Code Verdict:**
|
|
147
|
+
- 3+ signals on same file/module → flag as **ZOMBIE** in audit report
|
|
148
|
+
- Recommend: archive (move to `_deprecated/`), delete, or assign owner
|
|
149
|
+
- Do NOT auto-delete — present zombie list to user for decision
|
|
150
|
+
|
|
133
151
|
---
|
|
134
152
|
|
|
135
153
|
### Phase 4: Architecture Audit
|