@rune-kit/rune 2.28.0 → 2.29.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/.codex-plugin/plugin.json +14 -0
- package/README.md +72 -42
- package/compiler/__tests__/adapter-model-mapping.test.js +22 -24
- package/compiler/__tests__/adapters.test.js +4 -1
- package/compiler/__tests__/doctor-mesh.test.js +55 -1
- package/compiler/__tests__/governance-collector.test.js +1 -0
- package/compiler/__tests__/hooks-codex.test.js +85 -0
- package/compiler/__tests__/hooks-doctor-tier.test.js +3 -5
- package/compiler/__tests__/hooks-drift.test.js +2 -2
- package/compiler/__tests__/hooks-install.test.js +2 -2
- package/compiler/__tests__/hooks-tiers.test.js +3 -4
- package/compiler/__tests__/setup.test.js +59 -0
- package/compiler/__tests__/status.test.js +7 -1
- package/compiler/__tests__/tier-override.test.js +42 -1
- package/compiler/__tests__/transforms.test.js +15 -0
- package/compiler/adapters/codex.js +88 -11
- package/compiler/adapters/hooks/codex.js +178 -0
- package/compiler/adapters/hooks/index.js +10 -0
- package/compiler/adapters/openclaw.js +2 -2
- package/compiler/bin/rune.js +5 -4
- package/compiler/commands/hooks/install.js +2 -2
- package/compiler/commands/hooks/status.js +1 -1
- package/compiler/commands/setup.js +101 -28
- package/compiler/doctor.js +12 -6
- package/compiler/emitter.js +46 -3
- package/compiler/governance-collector.js +3 -2
- package/compiler/status.js +4 -7
- package/compiler/transforms/branding.js +2 -2
- package/compiler/transforms/subagents.js +3 -3
- package/hooks/codex-hooks.json +96 -0
- package/hooks/context-watch/index.cjs +8 -5
- package/hooks/intent-router/index.cjs +3 -0
- package/hooks/lib/hook-output.cjs +11 -3
- package/hooks/post-session-reflect/index.cjs +65 -24
- package/hooks/pre-compact/index.cjs +10 -4
- package/hooks/pre-tool-guard/index.cjs +38 -36
- package/hooks/run-hook +1 -1
- package/hooks/run-hook.cmd +1 -1
- package/hooks/secrets-scan/index.cjs +18 -2
- package/package.json +3 -2
- package/skills/browser-pilot/SKILL.md +1 -0
- package/skills/completion-gate/SKILL.md +5 -5
- package/skills/doc-processor/SKILL.md +2 -2
- package/skills/hallucination-guard/SKILL.md +1 -0
- package/skills/journal/SKILL.md +1 -0
- package/skills/retro/SKILL.md +2 -2
- package/skills/session-bridge/SKILL.md +1 -0
- package/skills/session-bridge/scripts/load-invariants.js +1 -1
- package/skills/video-creator/SKILL.md +1 -1
|
@@ -21,10 +21,14 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
24
|
-
import { cp, readdir, readFile, rm } from 'node:fs/promises';
|
|
24
|
+
import { cp, mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
25
25
|
import os from 'node:os';
|
|
26
26
|
import path from 'node:path';
|
|
27
27
|
import { createInterface } from 'node:readline';
|
|
28
|
+
import { getAdapter as getCompilerAdapter } from '../adapters/index.js';
|
|
29
|
+
import { parseSkill } from '../parser.js';
|
|
30
|
+
import { transformSkill } from '../transformer.js';
|
|
31
|
+
import { resolveScriptsPath } from '../transforms/scripts-path.js';
|
|
28
32
|
import { installHooks } from './hooks/install.js';
|
|
29
33
|
import { resolveTier, TIER_ENV_VARS } from './hooks/tiers.js';
|
|
30
34
|
|
|
@@ -103,9 +107,10 @@ export async function runSetup({ projectRoot, runeRoot, args = {}, skillTarget:
|
|
|
103
107
|
// Determine target root
|
|
104
108
|
const targetRoot = scope === 'global' ? os.homedir() : projectRoot;
|
|
105
109
|
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
|
|
110
|
+
// Global defaults to Claude for backward compatibility, but an explicit
|
|
111
|
+
// Codex target installs user-wide hooks under ~/.codex and skills under
|
|
112
|
+
// ~/.agents/skills.
|
|
113
|
+
const platform = args.platform || (scope === 'global' ? 'claude' : undefined);
|
|
109
114
|
|
|
110
115
|
// Set tier env vars from detection so installer can resolve
|
|
111
116
|
for (const tier of tiers) {
|
|
@@ -130,23 +135,44 @@ export async function runSetup({ projectRoot, runeRoot, args = {}, skillTarget:
|
|
|
130
135
|
// Claude Code plugin cache when present, runeRoot otherwise. Without this
|
|
131
136
|
// step, paid tiers ship hooks only — `rune:autopilot` returns "Unknown skill"
|
|
132
137
|
// because Pro/skills/autopilot/ is invisible to the plugin runtime.
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
138
|
+
const skillPlatforms = result.platforms || [];
|
|
139
|
+
const skillTargets = skillPlatforms.map((skillPlatform) => {
|
|
140
|
+
if (skillPlatform === 'claude') {
|
|
141
|
+
if (typeof skillTargetOverride === 'string') {
|
|
142
|
+
return { platform: skillPlatform, root: skillTargetOverride, source: 'override' };
|
|
143
|
+
}
|
|
144
|
+
if (skillTargetOverride) return { platform: skillPlatform, ...skillTargetOverride };
|
|
145
|
+
return { platform: skillPlatform, ...resolveSkillInstallRoot(runeRoot) };
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
platform: skillPlatform,
|
|
149
|
+
root: targetRoot,
|
|
150
|
+
source: scope === 'global' ? `${skillPlatform}-user` : `${skillPlatform}-project`,
|
|
151
|
+
};
|
|
152
|
+
});
|
|
153
|
+
const skillTarget = skillTargets[0] || null;
|
|
137
154
|
const skillResults = [];
|
|
138
|
-
for (const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
155
|
+
for (const skillTargetEntry of skillTargets) {
|
|
156
|
+
for (const tier of tiers) {
|
|
157
|
+
try {
|
|
158
|
+
const tierManifest = await resolveTier(tier, projectRoot);
|
|
159
|
+
const skillResult = await installTierSkills({
|
|
160
|
+
tier,
|
|
161
|
+
tierManifest,
|
|
162
|
+
runeRoot: skillTargetEntry.root,
|
|
163
|
+
platform: skillTargetEntry.platform,
|
|
164
|
+
dry: args.dry,
|
|
165
|
+
});
|
|
166
|
+
skillResults.push({ platform: skillTargetEntry.platform, ...skillResult });
|
|
167
|
+
} catch (err) {
|
|
168
|
+
skillResults.push({
|
|
169
|
+
platform: skillTargetEntry.platform,
|
|
170
|
+
tier,
|
|
171
|
+
installed: [],
|
|
172
|
+
skipped: [],
|
|
173
|
+
reason: err.message,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
150
176
|
}
|
|
151
177
|
}
|
|
152
178
|
|
|
@@ -158,6 +184,7 @@ export async function runSetup({ projectRoot, runeRoot, args = {}, skillTarget:
|
|
|
158
184
|
detected,
|
|
159
185
|
skillResults,
|
|
160
186
|
skillTarget,
|
|
187
|
+
skillTargets,
|
|
161
188
|
...result,
|
|
162
189
|
};
|
|
163
190
|
}
|
|
@@ -177,11 +204,12 @@ export async function runSetup({ projectRoot, runeRoot, args = {}, skillTarget:
|
|
|
177
204
|
* @param {object} opts
|
|
178
205
|
* @param {string} opts.tier
|
|
179
206
|
* @param {import('./hooks/tiers.js').TierManifest} opts.tierManifest
|
|
180
|
-
* @param {string} opts.runeRoot —
|
|
207
|
+
* @param {string} opts.runeRoot — Claude plugin root, or project/user root for other platforms
|
|
208
|
+
* @param {string} [opts.platform='claude']
|
|
181
209
|
* @param {boolean} [opts.dry]
|
|
182
210
|
* @returns {Promise<{tier: string, installed: string[], skipped: Array<{skill: string, reason: string}>, reason: string|null}>}
|
|
183
211
|
*/
|
|
184
|
-
export async function installTierSkills({ tier, tierManifest, runeRoot, dry }) {
|
|
212
|
+
export async function installTierSkills({ tier, tierManifest, runeRoot, platform = 'claude', dry }) {
|
|
185
213
|
if (!tierManifest?.source) {
|
|
186
214
|
return { tier, installed: [], skipped: [], reason: 'tier manifest source missing' };
|
|
187
215
|
}
|
|
@@ -203,9 +231,24 @@ export async function installTierSkills({ tier, tierManifest, runeRoot, dry }) {
|
|
|
203
231
|
if (!runeRoot) {
|
|
204
232
|
return { tier, installed: [], skipped: [], reason: 'runeRoot not provided' };
|
|
205
233
|
}
|
|
206
|
-
const
|
|
234
|
+
const adapter = platform === 'claude' ? null : getCompilerAdapter(platform);
|
|
235
|
+
if (adapter && !adapter.useSkillDirectories) {
|
|
236
|
+
return {
|
|
237
|
+
tier,
|
|
238
|
+
installed: [],
|
|
239
|
+
skipped: [],
|
|
240
|
+
reason: `platform '${platform}' does not use directory-based Agent Skills`,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
const targetDir = adapter ? path.join(runeRoot, adapter.outputDir) : path.join(runeRoot, 'skills');
|
|
207
244
|
if (!existsSync(targetDir)) {
|
|
208
|
-
|
|
245
|
+
if (dry) {
|
|
246
|
+
// Dry-run should describe what would be installed even on a fresh target.
|
|
247
|
+
} else if (adapter) {
|
|
248
|
+
await mkdir(targetDir, { recursive: true });
|
|
249
|
+
} else {
|
|
250
|
+
return { tier, installed: [], skipped: [], reason: `target skills/ missing at ${targetDir}` };
|
|
251
|
+
}
|
|
209
252
|
}
|
|
210
253
|
const installed = [];
|
|
211
254
|
const skipped = [];
|
|
@@ -230,7 +273,33 @@ export async function installTierSkills({ tier, tierManifest, runeRoot, dry }) {
|
|
|
230
273
|
continue;
|
|
231
274
|
}
|
|
232
275
|
const src = path.join(sourceDir, skillName);
|
|
233
|
-
|
|
276
|
+
let parsed = null;
|
|
277
|
+
let compiledOutput = null;
|
|
278
|
+
let targetSkillName = skillName;
|
|
279
|
+
if (adapter) {
|
|
280
|
+
try {
|
|
281
|
+
const skillPath = path.join(src, 'SKILL.md');
|
|
282
|
+
const content = await readFile(skillPath, 'utf-8');
|
|
283
|
+
parsed = parseSkill(content, skillPath);
|
|
284
|
+
targetSkillName = `${adapter.skillPrefix || ''}${parsed.name}${adapter.skillSuffix || ''}`;
|
|
285
|
+
if (path.basename(targetSkillName) !== targetSkillName) {
|
|
286
|
+
skipped.push({ skill: skillName, reason: 'rejected: compiled skill name is unsafe' });
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
const { header, body: rawBody, footer } = transformSkill(parsed, adapter);
|
|
290
|
+
const scriptsSource = path.join(src, 'scripts');
|
|
291
|
+
const scriptsPath =
|
|
292
|
+
existsSync(scriptsSource) && typeof adapter.scriptsDir === 'function'
|
|
293
|
+
? path.join(adapter.outputDir, adapter.scriptsDir(parsed.name)).replaceAll('\\', '/')
|
|
294
|
+
: null;
|
|
295
|
+
const body = scriptsPath ? resolveScriptsPath(rawBody, scriptsPath) : rawBody;
|
|
296
|
+
compiledOutput = [header, body, footer].filter(Boolean).join('\n');
|
|
297
|
+
} catch (err) {
|
|
298
|
+
skipped.push({ skill: skillName, reason: `compile failed for ${platform}: ${err.message}` });
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
const dst = path.join(targetDir, targetSkillName);
|
|
234
303
|
if (existsSync(dst)) {
|
|
235
304
|
const drift = await detectVersionDrift(src, dst);
|
|
236
305
|
skipped.push({ skill: skillName, reason: drift || 'already present' });
|
|
@@ -242,6 +311,9 @@ export async function installTierSkills({ tier, tierManifest, runeRoot, dry }) {
|
|
|
242
311
|
// symlinks at dst. Belt-and-suspenders alongside the isSymbolicLink reject above
|
|
243
312
|
// (nested symlinks inside a skill dir would otherwise still recreate).
|
|
244
313
|
await cp(src, dst, { recursive: true, dereference: true });
|
|
314
|
+
if (compiledOutput !== null) {
|
|
315
|
+
await writeFile(path.join(dst, adapter.skillFileName || 'SKILL.md'), compiledOutput, 'utf-8');
|
|
316
|
+
}
|
|
245
317
|
} catch (err) {
|
|
246
318
|
// Clean partial-copy residue so next run isn't silently locked-out by
|
|
247
319
|
// existsSync(dst) on a corrupt half-written directory.
|
|
@@ -436,10 +508,11 @@ export function formatSetupResult(result) {
|
|
|
436
508
|
lines.push(` Preset: ${result.preset}`);
|
|
437
509
|
lines.push(` Platforms: ${(result.platforms || []).join(', ') || '—'}`);
|
|
438
510
|
for (const sr of result.skillResults || []) {
|
|
511
|
+
const platformLabel = sr.platform ? `/${sr.platform}` : '';
|
|
439
512
|
if (sr.installed.length > 0) {
|
|
440
513
|
const preview = sr.installed.slice(0, 3).join(', ');
|
|
441
514
|
const more = sr.installed.length > 3 ? `, +${sr.installed.length - 3}` : '';
|
|
442
|
-
lines.push(` Skills: ${sr.tier}: ${sr.installed.length} installed (${preview}${more})`);
|
|
515
|
+
lines.push(` Skills: ${sr.tier}${platformLabel}: ${sr.installed.length} installed (${preview}${more})`);
|
|
443
516
|
}
|
|
444
517
|
if (sr.skipped.length > 0) {
|
|
445
518
|
const rejected = sr.skipped.filter((s) => s.reason.startsWith('rejected:'));
|
|
@@ -447,14 +520,14 @@ export function formatSetupResult(result) {
|
|
|
447
520
|
const parts = [];
|
|
448
521
|
if (benign > 0) parts.push(`${benign} already present`);
|
|
449
522
|
if (rejected.length > 0) parts.push(`${rejected.length} rejected`);
|
|
450
|
-
lines.push(` Skipped: ${sr.tier}: ${parts.join(', ')}`);
|
|
523
|
+
lines.push(` Skipped: ${sr.tier}${platformLabel}: ${parts.join(', ')}`);
|
|
451
524
|
// Surface rejection details so the operator can investigate a compromised tier repo.
|
|
452
525
|
for (const r of rejected) {
|
|
453
526
|
lines.push(` ⚠ ${r.skill}: ${r.reason}`);
|
|
454
527
|
}
|
|
455
528
|
}
|
|
456
529
|
if (sr.reason) {
|
|
457
|
-
lines.push(` Skill warn:${sr.tier}: ${sr.reason}`);
|
|
530
|
+
lines.push(` Skill warn:${sr.tier}${platformLabel}: ${sr.reason}`);
|
|
458
531
|
}
|
|
459
532
|
}
|
|
460
533
|
if (result.notes?.length) {
|
package/compiler/doctor.js
CHANGED
|
@@ -742,7 +742,7 @@ export async function checkMeshIntegrity(runeRoot) {
|
|
|
742
742
|
/**
|
|
743
743
|
* Parse skill connections from SKILL.md content
|
|
744
744
|
*/
|
|
745
|
-
function parseSkillConnections(content, skillName) {
|
|
745
|
+
export function parseSkillConnections(content, skillName) {
|
|
746
746
|
const result = {
|
|
747
747
|
name: skillName,
|
|
748
748
|
calls: [],
|
|
@@ -777,13 +777,15 @@ function parseSkillConnections(content, skillName) {
|
|
|
777
777
|
}
|
|
778
778
|
|
|
779
779
|
// Extract Calls section
|
|
780
|
-
const callsMatch = content.match(/## Calls \(outbound
|
|
780
|
+
const callsMatch = content.match(/## Calls \(outbound(?: connections)?\)\s*\r?\n([\s\S]*?)(?=\r?\n## |\r?\n---|$)/);
|
|
781
781
|
if (callsMatch) {
|
|
782
782
|
const lines = callsMatch[1].split('\n');
|
|
783
783
|
for (const line of lines) {
|
|
784
784
|
// Match patterns like: - `scout` (L2): scan codebase
|
|
785
|
-
// Or:
|
|
786
|
-
const match =
|
|
785
|
+
// Or table rows like: | 1 | `scout` | L2 | scan codebase |
|
|
786
|
+
const match =
|
|
787
|
+
line.match(/^-\s*`?([a-z][\w-]*)`?\s*\((?:L[0-4]|ext)\)/i) ||
|
|
788
|
+
line.match(/^\|(?:[^|]*\|)*?\s*`?([a-z][\w-]*)`?\s*\|\s*(?:L[0-4]|ext)\s*\|/i);
|
|
787
789
|
if (match) {
|
|
788
790
|
const skillRef = match[1].toLowerCase();
|
|
789
791
|
const reason = line
|
|
@@ -796,11 +798,15 @@ function parseSkillConnections(content, skillName) {
|
|
|
796
798
|
}
|
|
797
799
|
|
|
798
800
|
// Extract Called By section
|
|
799
|
-
const calledByMatch = content.match(
|
|
801
|
+
const calledByMatch = content.match(
|
|
802
|
+
/## Called By \(inbound(?: connections)?\)\s*\r?\n([\s\S]*?)(?=\r?\n## |\r?\n---|$)/,
|
|
803
|
+
);
|
|
800
804
|
if (calledByMatch) {
|
|
801
805
|
const lines = calledByMatch[1].split('\n');
|
|
802
806
|
for (const line of lines) {
|
|
803
|
-
const match =
|
|
807
|
+
const match =
|
|
808
|
+
line.match(/^-\s*`?([a-z][\w-]*)`?\s*\((?:L[0-4]|ext)\)/i) ||
|
|
809
|
+
line.match(/^\|(?:[^|]*\|)*?\s*`?([a-z][\w-]*)`?\s*\|\s*(?:L[0-4]|ext)\s*\|/i);
|
|
804
810
|
if (match) {
|
|
805
811
|
const skillRef = match[1].toLowerCase();
|
|
806
812
|
result.calledBy.push({ skill: skillRef });
|
package/compiler/emitter.js
CHANGED
|
@@ -19,6 +19,7 @@ import { resolveScriptsPath } from './transforms/scripts-path.js';
|
|
|
19
19
|
* @returns {Promise<string[]>} array of SKILL.md file paths
|
|
20
20
|
*/
|
|
21
21
|
async function discoverSkills(skillsDir) {
|
|
22
|
+
if (!existsSync(skillsDir)) return [];
|
|
22
23
|
const entries = await readdir(skillsDir, { withFileTypes: true });
|
|
23
24
|
const paths = [];
|
|
24
25
|
|
|
@@ -33,6 +34,38 @@ async function discoverSkills(skillsDir) {
|
|
|
33
34
|
return paths.sort();
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Discover standalone skills across Free and paid tiers.
|
|
39
|
+
* Business > Pro > Free when the same skill directory name exists.
|
|
40
|
+
*
|
|
41
|
+
* Tier sources point at `<tier>/extensions`; standalone tier skills live in
|
|
42
|
+
* the sibling `<tier>/skills` directory.
|
|
43
|
+
*
|
|
44
|
+
* @param {string} freeSkillsDir
|
|
45
|
+
* @param {Object<string, string>} [tierSources]
|
|
46
|
+
* @returns {Promise<Array<{path: string, tier: string, name: string}>>}
|
|
47
|
+
*/
|
|
48
|
+
export async function discoverTieredSkills(freeSkillsDir, tierSources = {}) {
|
|
49
|
+
const skillMap = new Map();
|
|
50
|
+
|
|
51
|
+
async function scanDir(skillsDir, tier) {
|
|
52
|
+
for (const skillPath of await discoverSkills(skillsDir)) {
|
|
53
|
+
const name = path.basename(path.dirname(skillPath));
|
|
54
|
+
const priority = TIER_PRIORITY[tier] ?? 0;
|
|
55
|
+
const existing = skillMap.get(name);
|
|
56
|
+
if (!existing || priority > existing.priority) {
|
|
57
|
+
skillMap.set(name, { path: skillPath, tier, name, priority });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
await scanDir(freeSkillsDir, 'free');
|
|
63
|
+
if (tierSources.pro) await scanDir(path.join(path.dirname(tierSources.pro), 'skills'), 'pro');
|
|
64
|
+
if (tierSources.business) await scanDir(path.join(path.dirname(tierSources.business), 'skills'), 'business');
|
|
65
|
+
|
|
66
|
+
return [...skillMap.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
67
|
+
}
|
|
68
|
+
|
|
36
69
|
/**
|
|
37
70
|
* Discover all PACK.md files in the extensions directory
|
|
38
71
|
*
|
|
@@ -435,10 +468,15 @@ export async function buildAll({
|
|
|
435
468
|
// Ensure output directory exists
|
|
436
469
|
await mkdir(outputDir, { recursive: true });
|
|
437
470
|
|
|
438
|
-
const skillPaths = await discoverSkills(skillsDir);
|
|
439
|
-
|
|
440
471
|
// Tier-aware pack discovery: if tierSources provided, resolve overrides
|
|
441
472
|
const hasTiers = tierSources && (tierSources.pro || tierSources.business);
|
|
473
|
+
const skillEntries = hasTiers
|
|
474
|
+
? await discoverTieredSkills(skillsDir, tierSources)
|
|
475
|
+
: (await discoverSkills(skillsDir)).map((skillPath) => ({
|
|
476
|
+
path: skillPath,
|
|
477
|
+
tier: 'free',
|
|
478
|
+
name: path.basename(path.dirname(skillPath)),
|
|
479
|
+
}));
|
|
442
480
|
const packEntries = hasTiers
|
|
443
481
|
? await discoverTieredPacks(extensionsDir, tierSources, enabledPacks)
|
|
444
482
|
: (await discoverPacks(extensionsDir, enabledPacks)).map((p) => ({
|
|
@@ -450,6 +488,8 @@ export async function buildAll({
|
|
|
450
488
|
const stats = {
|
|
451
489
|
platform: adapter.name,
|
|
452
490
|
skillCount: 0,
|
|
491
|
+
coreSkillCount: 0,
|
|
492
|
+
tierSkillCount: 0,
|
|
453
493
|
packCount: 0,
|
|
454
494
|
crossRefsResolved: 0,
|
|
455
495
|
toolRefsResolved: 0,
|
|
@@ -471,7 +511,8 @@ export async function buildAll({
|
|
|
471
511
|
// Build skills — collect parsed data for skill-index + openclaw reuse
|
|
472
512
|
const parsedSkills = [];
|
|
473
513
|
|
|
474
|
-
for (const
|
|
514
|
+
for (const skillEntry of skillEntries) {
|
|
515
|
+
const skillPath = skillEntry.path;
|
|
475
516
|
try {
|
|
476
517
|
const content = await readFile(skillPath, 'utf-8');
|
|
477
518
|
const parsed = parseSkill(content, skillPath);
|
|
@@ -553,6 +594,8 @@ export async function buildAll({
|
|
|
553
594
|
|
|
554
595
|
parsedSkills.push(parsed);
|
|
555
596
|
stats.skillCount++;
|
|
597
|
+
if (skillEntry.tier === 'free') stats.coreSkillCount++;
|
|
598
|
+
else stats.tierSkillCount++;
|
|
556
599
|
stats.crossRefsResolved += parsed.crossRefs.length;
|
|
557
600
|
stats.toolRefsResolved += parsed.toolRefs.length;
|
|
558
601
|
stats.files.push(displayName);
|
|
@@ -312,8 +312,9 @@ async function assembleCompliance(runeRoot) {
|
|
|
312
312
|
continue; // skip unreadable pack
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
-
//
|
|
316
|
-
|
|
315
|
+
// Business is the entitlement tier; paid pack manifests retain the
|
|
316
|
+
// canonical @rune-pro/* namespace for mesh compatibility.
|
|
317
|
+
const packName = packDir.replace(/^pro-/, '@rune-pro/');
|
|
317
318
|
|
|
318
319
|
// Extract obligations from ## Constraints. Real Business PACK.md files use
|
|
319
320
|
// numbered ("1. MUST …") OR bulleted ("- MUST …") lists, so accept both.
|
package/compiler/status.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { existsSync } from 'node:fs';
|
|
9
9
|
import { readdir, readFile } from 'node:fs/promises';
|
|
10
10
|
import path from 'node:path';
|
|
11
|
+
import { parseSkillConnections } from './doctor.js';
|
|
11
12
|
import { parseSkill } from './parser.js';
|
|
12
13
|
|
|
13
14
|
// ─── Constants ───
|
|
@@ -73,6 +74,7 @@ export async function collectStats(runeRoot, tierSources = {}) {
|
|
|
73
74
|
const layers = { L0: 0, L1: 0, L2: 0, L3: 0 };
|
|
74
75
|
const skillNames = [];
|
|
75
76
|
let signalCount = 0;
|
|
77
|
+
let totalConnections = 0;
|
|
76
78
|
const signalMap = { emitters: {}, listeners: {} };
|
|
77
79
|
const parsedSkills = [];
|
|
78
80
|
|
|
@@ -85,6 +87,7 @@ export async function collectStats(runeRoot, tierSources = {}) {
|
|
|
85
87
|
|
|
86
88
|
const content = await readFile(skillFile, 'utf-8');
|
|
87
89
|
const parsed = parseSkill(content, skillFile);
|
|
90
|
+
totalConnections += parseSkillConnections(content, entry.name).calls.length;
|
|
88
91
|
parsedSkills.push(parsed);
|
|
89
92
|
skillNames.push(parsed.name);
|
|
90
93
|
|
|
@@ -106,11 +109,6 @@ export async function collectStats(runeRoot, tierSources = {}) {
|
|
|
106
109
|
signalCount = allSignals.size;
|
|
107
110
|
}
|
|
108
111
|
|
|
109
|
-
// Count connections
|
|
110
|
-
let totalConnections = 0;
|
|
111
|
-
for (const skill of parsedSkills) {
|
|
112
|
-
totalConnections += new Set((skill.crossRefs ?? []).map((r) => r.skillName)).size;
|
|
113
|
-
}
|
|
114
112
|
const avgConnections = parsedSkills.length > 0 ? (totalConnections / parsedSkills.length).toFixed(1) : '0';
|
|
115
113
|
|
|
116
114
|
// Count free packs
|
|
@@ -302,8 +300,7 @@ export function renderStatusJson(stats, { version = '', platform = '', projectNa
|
|
|
302
300
|
|
|
303
301
|
function formatPackName(dirName, tier = 'free') {
|
|
304
302
|
const baseName = dirName.replace(/^(pro|business)-/, '');
|
|
305
|
-
if (tier === 'business') return `@rune-
|
|
306
|
-
if (tier === 'pro') return `@rune-pro/${baseName}`.padEnd(26);
|
|
303
|
+
if (tier === 'business' || tier === 'pro') return `@rune-pro/${baseName}`.padEnd(26);
|
|
307
304
|
return `@rune/${baseName}`.padEnd(26);
|
|
308
305
|
}
|
|
309
306
|
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
export const BRANDING_FOOTER = [
|
|
13
13
|
'',
|
|
14
14
|
'---',
|
|
15
|
-
'> **Rune Skill Mesh** —
|
|
15
|
+
'> **Rune Skill Mesh** — 66 skills, 248 connections + 45 signals, 14 extension packs',
|
|
16
16
|
'> [Landing Page](https://rune-kit.github.io/rune) · [Source](https://github.com/rune-kit/rune) (MIT)',
|
|
17
|
-
'> **Rune Pro** ($49 lifetime) —
|
|
17
|
+
'> **Rune Pro** ($49 lifetime) — 9 domain packs + context intelligence → [rune-kit/rune-pro](https://github.com/rune-kit/rune-pro)',
|
|
18
18
|
'> **Rune Business** ($149 lifetime) — finance, legal, HR, enterprise-search packs → [rune-kit/rune-business](https://github.com/rune-kit/rune-business)',
|
|
19
19
|
].join('\n');
|
|
20
20
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Subagent Transform
|
|
3
3
|
*
|
|
4
|
-
* Converts parallel agent/subagent execution instructions to sequential workflow
|
|
5
|
-
*
|
|
4
|
+
* Converts parallel agent/subagent execution instructions to sequential workflow
|
|
5
|
+
* only on platforms without native multi-agent support.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
const PARALLEL_PATTERNS = [
|
|
@@ -23,7 +23,7 @@ const PARALLEL_PATTERNS = [
|
|
|
23
23
|
* @returns {string} transformed body
|
|
24
24
|
*/
|
|
25
25
|
export function transformSubagents(body, adapter) {
|
|
26
|
-
if (adapter.name === 'claude') return body;
|
|
26
|
+
if (adapter.name === 'claude' || adapter.name === 'codex') return body;
|
|
27
27
|
|
|
28
28
|
let result = body;
|
|
29
29
|
for (const { pattern, replace } of PARALLEL_PATTERNS) {
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "Rune lifecycle hooks optimized for OpenAI Codex.",
|
|
3
|
+
"hooks": {
|
|
4
|
+
"SessionStart": [
|
|
5
|
+
{
|
|
6
|
+
"matcher": "startup|resume|clear|compact",
|
|
7
|
+
"hooks": [
|
|
8
|
+
{
|
|
9
|
+
"type": "command",
|
|
10
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cjs\" session-start"
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"UserPromptSubmit": [
|
|
16
|
+
{
|
|
17
|
+
"hooks": [
|
|
18
|
+
{
|
|
19
|
+
"type": "command",
|
|
20
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cjs\" intent-router"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"PreToolUse": [
|
|
26
|
+
{
|
|
27
|
+
"matcher": "Read|Write|Edit|apply_patch|view_image",
|
|
28
|
+
"hooks": [
|
|
29
|
+
{
|
|
30
|
+
"type": "command",
|
|
31
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cjs\" pre-tool-guard"
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"matcher": ".*",
|
|
37
|
+
"hooks": [
|
|
38
|
+
{
|
|
39
|
+
"type": "command",
|
|
40
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cjs\" context-watch"
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"matcher": "Bash|shell_command",
|
|
46
|
+
"hooks": [
|
|
47
|
+
{
|
|
48
|
+
"type": "command",
|
|
49
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cjs\" secrets-scan"
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
],
|
|
54
|
+
"PostToolUse": [
|
|
55
|
+
{
|
|
56
|
+
"matcher": "Skill|Task|Agent|spawn_agent",
|
|
57
|
+
"hooks": [
|
|
58
|
+
{
|
|
59
|
+
"type": "command",
|
|
60
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cjs\" metrics-collector"
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"matcher": "mcp__.*|WebFetch|Read|web_search",
|
|
66
|
+
"hooks": [
|
|
67
|
+
{
|
|
68
|
+
"type": "command",
|
|
69
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cjs\" quarantine"
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
],
|
|
74
|
+
"PreCompact": [
|
|
75
|
+
{
|
|
76
|
+
"matcher": ".*",
|
|
77
|
+
"hooks": [
|
|
78
|
+
{
|
|
79
|
+
"type": "command",
|
|
80
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cjs\" pre-compact"
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
],
|
|
85
|
+
"Stop": [
|
|
86
|
+
{
|
|
87
|
+
"hooks": [
|
|
88
|
+
{
|
|
89
|
+
"type": "command",
|
|
90
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cjs\" post-session-reflect"
|
|
91
|
+
}
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
]
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
|
|
14
14
|
const fs = require('fs');
|
|
15
15
|
const { stateFile } = require('../lib/context-key.cjs');
|
|
16
|
+
const { captureConsole } = require('../lib/hook-output.cjs');
|
|
17
|
+
|
|
18
|
+
captureConsole('PreToolUse');
|
|
16
19
|
|
|
17
20
|
// The counter file is keyed by Claude Code session_id (parsed from stdin in the
|
|
18
21
|
// handler below) so it resets per session and never bleeds across sessions or
|
|
@@ -53,7 +56,9 @@ process.stdin.on('end', () => {
|
|
|
53
56
|
// Ensure fields exist (upgrade from old format)
|
|
54
57
|
if (!state.toolCounts) state.toolCounts = {};
|
|
55
58
|
if (!state.sessionStart) state.sessionStart = new Date().toISOString();
|
|
56
|
-
if (
|
|
59
|
+
if (sessionId) {
|
|
60
|
+
state.sessionId = sessionId;
|
|
61
|
+
} else if (!state.sessionId) {
|
|
57
62
|
const s = state.sessionStart;
|
|
58
63
|
state.sessionId = `s-${s.slice(0, 10).replace(/-/g, '')}-${s.slice(11, 19).replace(/:/g, '')}`;
|
|
59
64
|
}
|
|
@@ -61,7 +66,8 @@ process.stdin.on('end', () => {
|
|
|
61
66
|
// First run or corrupted — start fresh
|
|
62
67
|
const now = new Date().toISOString();
|
|
63
68
|
state.sessionStart = now;
|
|
64
|
-
state.sessionId =
|
|
69
|
+
state.sessionId = sessionId
|
|
70
|
+
|| `s-${now.slice(0, 10).replace(/-/g, '')}-${now.slice(11, 19).replace(/:/g, '')}`;
|
|
65
71
|
}
|
|
66
72
|
|
|
67
73
|
// Increment total (drives context-pressure warnings) and per-tool distribution.
|
|
@@ -88,9 +94,6 @@ process.stdin.on('end', () => {
|
|
|
88
94
|
state.lastWarning = count;
|
|
89
95
|
}
|
|
90
96
|
|
|
91
|
-
// Pass through stdin to stdout (required for PreToolUse hooks)
|
|
92
|
-
if (stdinData) process.stdout.write(stdinData);
|
|
93
|
-
|
|
94
97
|
// Persist
|
|
95
98
|
try {
|
|
96
99
|
fs.writeFileSync(counterFile, JSON.stringify(state));
|
|
@@ -79,16 +79,24 @@ function emit(hookEventName, text) {
|
|
|
79
79
|
* `console.error` is untouched — stderr is not parsed as the output contract.
|
|
80
80
|
*
|
|
81
81
|
* @param {string} hookEventName
|
|
82
|
+
* @param {{captureError?: boolean}} [options]
|
|
82
83
|
* @returns {{buffer: ReturnType<typeof outputBuffer>, restore: () => void}}
|
|
83
84
|
*/
|
|
84
|
-
function captureConsole(hookEventName) {
|
|
85
|
+
function captureConsole(hookEventName, options = {}) {
|
|
85
86
|
const buffer = outputBuffer(hookEventName);
|
|
86
|
-
const
|
|
87
|
+
const originalLog = console.log;
|
|
88
|
+
const originalError = console.error;
|
|
87
89
|
console.log = (...args) => {
|
|
88
90
|
buffer.line(args.map((a) => (typeof a === 'string' ? a : String(a))).join(' '));
|
|
89
91
|
};
|
|
92
|
+
if (options.captureError) {
|
|
93
|
+
console.error = (...args) => {
|
|
94
|
+
buffer.line(args.map((a) => (typeof a === 'string' ? a : String(a))).join(' '));
|
|
95
|
+
};
|
|
96
|
+
}
|
|
90
97
|
const restore = () => {
|
|
91
|
-
console.log =
|
|
98
|
+
console.log = originalLog;
|
|
99
|
+
console.error = originalError;
|
|
92
100
|
};
|
|
93
101
|
// Fires for a natural end AND for process.exit(), which is how the BLOCK
|
|
94
102
|
// paths leave — those messages must survive too.
|