@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.
Files changed (49) hide show
  1. package/.codex-plugin/plugin.json +14 -0
  2. package/README.md +72 -42
  3. package/compiler/__tests__/adapter-model-mapping.test.js +22 -24
  4. package/compiler/__tests__/adapters.test.js +4 -1
  5. package/compiler/__tests__/doctor-mesh.test.js +55 -1
  6. package/compiler/__tests__/governance-collector.test.js +1 -0
  7. package/compiler/__tests__/hooks-codex.test.js +85 -0
  8. package/compiler/__tests__/hooks-doctor-tier.test.js +3 -5
  9. package/compiler/__tests__/hooks-drift.test.js +2 -2
  10. package/compiler/__tests__/hooks-install.test.js +2 -2
  11. package/compiler/__tests__/hooks-tiers.test.js +3 -4
  12. package/compiler/__tests__/setup.test.js +59 -0
  13. package/compiler/__tests__/status.test.js +7 -1
  14. package/compiler/__tests__/tier-override.test.js +42 -1
  15. package/compiler/__tests__/transforms.test.js +15 -0
  16. package/compiler/adapters/codex.js +88 -11
  17. package/compiler/adapters/hooks/codex.js +178 -0
  18. package/compiler/adapters/hooks/index.js +10 -0
  19. package/compiler/adapters/openclaw.js +2 -2
  20. package/compiler/bin/rune.js +5 -4
  21. package/compiler/commands/hooks/install.js +2 -2
  22. package/compiler/commands/hooks/status.js +1 -1
  23. package/compiler/commands/setup.js +101 -28
  24. package/compiler/doctor.js +12 -6
  25. package/compiler/emitter.js +46 -3
  26. package/compiler/governance-collector.js +3 -2
  27. package/compiler/status.js +4 -7
  28. package/compiler/transforms/branding.js +2 -2
  29. package/compiler/transforms/subagents.js +3 -3
  30. package/hooks/codex-hooks.json +96 -0
  31. package/hooks/context-watch/index.cjs +8 -5
  32. package/hooks/intent-router/index.cjs +3 -0
  33. package/hooks/lib/hook-output.cjs +11 -3
  34. package/hooks/post-session-reflect/index.cjs +65 -24
  35. package/hooks/pre-compact/index.cjs +10 -4
  36. package/hooks/pre-tool-guard/index.cjs +38 -36
  37. package/hooks/run-hook +1 -1
  38. package/hooks/run-hook.cmd +1 -1
  39. package/hooks/secrets-scan/index.cjs +18 -2
  40. package/package.json +3 -2
  41. package/skills/browser-pilot/SKILL.md +1 -0
  42. package/skills/completion-gate/SKILL.md +5 -5
  43. package/skills/doc-processor/SKILL.md +2 -2
  44. package/skills/hallucination-guard/SKILL.md +1 -0
  45. package/skills/journal/SKILL.md +1 -0
  46. package/skills/retro/SKILL.md +2 -2
  47. package/skills/session-bridge/SKILL.md +1 -0
  48. package/skills/session-bridge/scripts/load-invariants.js +1 -1
  49. 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
- // For global scope, claude is the only meaningful platform (cursor/windsurf
107
- // configs typically live per-project). Force claude.
108
- const platform = scope === 'global' ? 'claude' : args.platform;
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 skillTarget =
134
- typeof skillTargetOverride === 'string'
135
- ? { root: skillTargetOverride, source: 'override' }
136
- : (skillTargetOverride ?? resolveSkillInstallRoot(runeRoot));
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 tier of tiers) {
139
- try {
140
- const tierManifest = await resolveTier(tier, projectRoot);
141
- const skillResult = await installTierSkills({
142
- tier,
143
- tierManifest,
144
- runeRoot: skillTarget.root,
145
- dry: args.dry,
146
- });
147
- skillResults.push(skillResult);
148
- } catch (err) {
149
- skillResults.push({ tier, installed: [], skipped: [], reason: err.message });
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 — target is `<runeRoot>/skills/`
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 targetDir = path.join(runeRoot, 'skills');
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
- return { tier, installed: [], skipped: [], reason: `target skills/ missing at ${targetDir}` };
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
- const dst = path.join(targetDir, skillName);
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) {
@@ -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\)\s*\n([\s\S]*?)(?=\n## |\n---|Z)/);
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: - scout (L2): scan codebase
786
- const match = line.match(/^-\s*`?([a-z][\w-]*)`?\s*\(L\d\)/i);
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(/## Called By \(inbound\)\s*\n([\s\S]*?)(?=\n## |\n---|Z)/);
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 = line.match(/^-\s*`?([a-z][\w-]*)`?\s*\(L\d\)/i);
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 });
@@ -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 skillPath of skillPaths) {
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
- // Derive a human-readable pack name from the directory name.
316
- const packName = packDir.replace(/^pro-/, '@rune-business/');
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.
@@ -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-biz/${baseName}`.padEnd(26);
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** — 64 skills, 204 connections + 40 signals, 14 extension packs',
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) — product, sales, data-science, support packs → [rune-kit/rune-pro](https://github.com/rune-kit/rune-pro)',
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
- * On non-Claude platforms, there is no multi-agent support.
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 (!state.sessionId) {
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 = `s-${now.slice(0, 10).replace(/-/g, '')}-${now.slice(11, 19).replace(/:/g, '')}`;
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));
@@ -9,6 +9,9 @@
9
9
 
10
10
  const fs = require('fs');
11
11
  const path = require('path');
12
+ const { captureConsole } = require('../lib/hook-output.cjs');
13
+
14
+ captureConsole('UserPromptSubmit');
12
15
 
13
16
  // Read user prompt from Claude Code hook stdin
14
17
  let input = '';
@@ -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 original = console.log;
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 = original;
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.