@rune-kit/rune 2.27.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 (52) hide show
  1. package/.codex-plugin/plugin.json +14 -0
  2. package/README.md +76 -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/design/SKILL.md +33 -6
  44. package/skills/doc-processor/SKILL.md +2 -2
  45. package/skills/hallucination-guard/SKILL.md +1 -0
  46. package/skills/journal/SKILL.md +1 -0
  47. package/skills/problem-solver/SKILL.md +32 -1
  48. package/skills/retro/SKILL.md +2 -2
  49. package/skills/session-bridge/SKILL.md +1 -0
  50. package/skills/session-bridge/scripts/load-invariants.js +1 -1
  51. package/skills/verification/SKILL.md +42 -1
  52. package/skills/video-creator/SKILL.md +1 -1
@@ -397,6 +397,65 @@ describe('runSetup — Pro skill installation (regression: rune:autopilot Unknow
397
397
 
398
398
  assert.deepStrictEqual(result.skillResults, []);
399
399
  });
400
+
401
+ test('installs Codex tier skills into project .agents/skills', async () => {
402
+ const projectRoot = path.join(tmpRoot, 'codex-project');
403
+ await mkdir(path.join(projectRoot, '.codex'), { recursive: true });
404
+ await seedTier(projectRoot, 'pro', { skills: ['autopilot'] });
405
+ const runeRoot = await seedFakeRuneRoot(tmpRoot);
406
+
407
+ const result = await runSetup({
408
+ projectRoot,
409
+ runeRoot,
410
+ args: { here: true, platform: 'codex', preset: 'gentle', tier: 'pro' },
411
+ });
412
+
413
+ assert.strictEqual(result.skillTarget.source, 'codex-project');
414
+ assert.deepStrictEqual(result.platforms, ['codex']);
415
+ assert.ok(existsSync(path.join(projectRoot, '.codex', 'hooks.json')));
416
+ const installed = path.join(projectRoot, '.agents', 'skills', 'rune-autopilot', 'SKILL.md');
417
+ assert.ok(existsSync(installed));
418
+ const content = readFileSync(installed, 'utf-8');
419
+ assert.match(content, /^name: rune-autopilot$/m);
420
+ assert.doesNotMatch(content, /^context: fork$/m);
421
+ assert.doesNotMatch(content, /^agent: general-purpose$/m);
422
+ });
423
+
424
+ test('installs tier skills into every detected non-Claude platform using native adapters', async () => {
425
+ const projectRoot = path.join(tmpRoot, 'multi-project');
426
+ await mkdir(path.join(projectRoot, '.cursor'), { recursive: true });
427
+ await mkdir(path.join(projectRoot, '.windsurf'), { recursive: true });
428
+ await seedTier(projectRoot, 'business', { skills: ['quarterly-review'] });
429
+ const runeRoot = await seedFakeRuneRoot(tmpRoot);
430
+
431
+ const result = await runSetup({
432
+ projectRoot,
433
+ runeRoot,
434
+ args: { here: true, platform: 'all', preset: 'gentle', tier: 'business' },
435
+ });
436
+
437
+ assert.deepStrictEqual(result.platforms.sort(), ['cursor', 'windsurf']);
438
+ assert.ok(existsSync(path.join(projectRoot, '.cursor', 'skills', 'rune-quarterly-review', 'SKILL.md')));
439
+ assert.ok(existsSync(path.join(projectRoot, '.windsurf', 'skills', 'rune-quarterly-review', 'SKILL.md')));
440
+ assert.deepStrictEqual(result.skillResults.map((entry) => entry.platform).sort(), ['cursor', 'windsurf']);
441
+ });
442
+
443
+ test('Codex dry-run reports tier skills on a fresh project without creating directories', async () => {
444
+ const projectRoot = path.join(tmpRoot, 'fresh-codex-project');
445
+ await mkdir(projectRoot, { recursive: true });
446
+ await seedTier(projectRoot, 'business', { skills: ['launch-product'] });
447
+ const runeRoot = await seedFakeRuneRoot(tmpRoot);
448
+
449
+ const result = await runSetup({
450
+ projectRoot,
451
+ runeRoot,
452
+ args: { here: true, platform: 'codex', preset: 'gentle', tier: 'business', dry: true },
453
+ });
454
+
455
+ assert.deepStrictEqual(result.skillResults[0].installed, ['launch-product']);
456
+ assert.strictEqual(result.skillResults[0].reason, null);
457
+ assert.ok(!existsSync(path.join(projectRoot, '.agents')));
458
+ });
400
459
  });
401
460
 
402
461
  describe('formatSetupResult', () => {
@@ -2,6 +2,7 @@ import assert from 'node:assert';
2
2
  import path from 'node:path';
3
3
  import { describe, test } from 'node:test';
4
4
  import { fileURLToPath } from 'node:url';
5
+ import { checkMeshIntegrity } from '../doctor.js';
5
6
  import { collectStats, renderStatus, renderStatusJson } from '../status.js';
6
7
 
7
8
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -42,6 +43,11 @@ describe('collectStats', () => {
42
43
  assert.ok(parseFloat(stats.avgConnections) > 1);
43
44
  });
44
45
 
46
+ test('uses the same canonical outbound connection count as doctor', async () => {
47
+ const [stats, mesh] = await Promise.all([collectStats(RUNE_ROOT), checkMeshIntegrity(RUNE_ROOT)]);
48
+ assert.strictEqual(stats.totalConnections, mesh.stats.connections);
49
+ });
50
+
45
51
  test('discovers free packs', async () => {
46
52
  const stats = await collectStats(RUNE_ROOT);
47
53
  assert.ok(stats.freePacks.length >= 14);
@@ -203,7 +209,7 @@ describe('renderStatus', () => {
203
209
  if (stats.bizPacks.length > 0) {
204
210
  const output = renderStatus(stats);
205
211
  assert.ok(output.includes('Business Packs'));
206
- assert.ok(output.includes('@rune-biz/'));
212
+ assert.ok(output.includes('@rune-pro/'));
207
213
  }
208
214
  });
209
215
 
@@ -11,7 +11,7 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
11
11
  import { tmpdir } from 'node:os';
12
12
  import path from 'node:path';
13
13
  import { describe, test } from 'node:test';
14
- import { discoverTieredPacks } from '../emitter.js';
14
+ import { discoverTieredPacks, discoverTieredSkills } from '../emitter.js';
15
15
 
16
16
  /**
17
17
  * Create a temp directory structure with PACK.md files for testing
@@ -197,3 +197,44 @@ describe('discoverTieredPacks', () => {
197
197
  }
198
198
  });
199
199
  });
200
+
201
+ describe('discoverTieredSkills', () => {
202
+ test('includes standalone Pro and Business skills and applies tier priority', async () => {
203
+ const root = mkdtempSync(path.join(tmpdir(), 'rune-tier-skills-'));
204
+ try {
205
+ const freeSkills = path.join(root, 'Free', 'skills');
206
+ const proExtensions = path.join(root, 'Pro', 'extensions');
207
+ const businessExtensions = path.join(root, 'Business', 'extensions');
208
+ for (const [skillsDir, names] of [
209
+ [freeSkills, ['cook', 'shared']],
210
+ [path.join(root, 'Pro', 'skills'), ['autopilot', 'shared']],
211
+ [path.join(root, 'Business', 'skills'), ['launch-product', 'shared']],
212
+ ]) {
213
+ for (const name of names) {
214
+ const dir = path.join(skillsDir, name);
215
+ mkdirSync(dir, { recursive: true });
216
+ writeFileSync(path.join(dir, 'SKILL.md'), `---\nname: ${name}\n---\n# ${name}\n`);
217
+ }
218
+ }
219
+ mkdirSync(proExtensions, { recursive: true });
220
+ mkdirSync(businessExtensions, { recursive: true });
221
+
222
+ const skills = await discoverTieredSkills(freeSkills, {
223
+ pro: proExtensions,
224
+ business: businessExtensions,
225
+ });
226
+
227
+ assert.deepStrictEqual(
228
+ skills.map(({ name, tier }) => [name, tier]),
229
+ [
230
+ ['autopilot', 'pro'],
231
+ ['cook', 'free'],
232
+ ['launch-product', 'business'],
233
+ ['shared', 'business'],
234
+ ],
235
+ );
236
+ } finally {
237
+ rmSync(root, { recursive: true, force: true });
238
+ }
239
+ });
240
+ });
@@ -2,6 +2,7 @@ import assert from 'node:assert';
2
2
  import { describe, test } from 'node:test';
3
3
  import { addBranding } from '../transforms/branding.js';
4
4
  import { transformCrossReferences } from '../transforms/cross-references.js';
5
+ import { transformSubagents } from '../transforms/subagents.js';
5
6
  import { transformToolNames } from '../transforms/tool-names.js';
6
7
 
7
8
  // --- Mock adapter ---
@@ -75,6 +76,20 @@ describe('transformToolNames', () => {
75
76
  });
76
77
  });
77
78
 
79
+ describe('transformSubagents', () => {
80
+ const input = 'Launch 3 parallel agents as independent Task agents.';
81
+
82
+ test('preserves native Codex subagent instructions', () => {
83
+ assert.strictEqual(transformSubagents(input, { name: 'codex' }), input);
84
+ });
85
+
86
+ test('keeps sequential fallback for platforms without native subagents', () => {
87
+ const result = transformSubagents(input, { name: 'generic' });
88
+ assert.notStrictEqual(result, input);
89
+ assert.match(result, /sequentially/);
90
+ });
91
+ });
92
+
78
93
  // --- Branding transform ---
79
94
 
80
95
  describe('addBranding', () => {
@@ -21,18 +21,21 @@
21
21
  * Codex GPT-5.6 tiers (verified against codex-cli 0.144.1 + developers.openai.com/codex/models):
22
22
  * sol = flagship/complex → opus
23
23
  * terra = balanced everyday → sonnet
24
- * luna = fast/affordable → haiku
24
+ * terra at low effort = fast/affordable → haiku
25
25
  * Codex also exposes `model_reasoning_effort = minimal|low|medium|high|xhigh`
26
26
  * (a config.toml key, not per-skill frontmatter) — Rune surfaces the tier→effort
27
27
  * suggestion in AGENTS.md rather than emitting a speculative per-skill field.
28
28
  */
29
29
 
30
+ import { existsSync } from 'node:fs';
31
+ import { readFile } from 'node:fs/promises';
32
+ import path from 'node:path';
30
33
  import { BRANDING_FOOTER } from '../transforms/branding.js';
31
34
 
32
35
  const MODEL_MAP = {
33
36
  opus: 'gpt-5.6-sol',
34
37
  sonnet: 'gpt-5.6-terra',
35
- haiku: 'gpt-5.6-luna',
38
+ haiku: 'gpt-5.6-terra',
36
39
  };
37
40
 
38
41
  // Suggested config.toml `model_reasoning_effort` per tier (global key, documented in AGENTS.md).
@@ -78,8 +81,6 @@ export default {
78
81
  generateHeader(skill) {
79
82
  const desc = (skill.description || '').replace(/"/g, '\\"');
80
83
  const lines = ['---', `name: rune-${skill.name}`, `description: "${desc}"`];
81
- const translatedModel = skill.model ? MODEL_MAP[skill.model] || skill.model : null;
82
- if (translatedModel) lines.push(`model: ${translatedModel}`);
83
84
  lines.push('---', '', '');
84
85
  return lines.join('\n');
85
86
  },
@@ -102,25 +103,27 @@ export default {
102
103
 
103
104
  // Codex (and OpenAI's AGENTS.md convention generally) reads AGENTS.md at the
104
105
  // project root for high-level context. Migrated from emitter.js v2.18.
105
- generateExtraFiles({ stats }) {
106
- const agentsMd = [
106
+ async generateExtraFiles({ stats, outputRoot }) {
107
+ const runeBlock = [
108
+ '<!-- rune:codex:start -->',
107
109
  '# Rune — Project Configuration',
108
110
  '',
109
111
  '## Overview',
110
112
  '',
111
113
  'Rune is an interconnected skill ecosystem for AI coding assistants.',
112
- `${stats.skillCount} core skills | 5-layer mesh architecture | ${stats.crossRefsResolved} connections | Multi-platform.`,
114
+ `${stats.coreSkillCount ?? stats.skillCount} core skills${stats.tierSkillCount ? ` + ${stats.tierSkillCount} tier skills` : ''} | 5-layer mesh architecture | ${stats.crossRefsResolved} connections | Multi-platform.`,
113
115
  'Philosophy: "Less skills. Deeper connections."',
114
116
  '',
115
117
  'Platform: codex',
116
118
  '',
117
119
  '## Skills',
118
120
  '',
119
- `**${stats.skillCount} core skills** + **${stats.packCount} extension packs**`,
121
+ `**${stats.coreSkillCount ?? stats.skillCount} core skills**${stats.tierSkillCount ? ` + **${stats.tierSkillCount} tier skills**` : ''} + **${stats.packCount} extension packs**`,
120
122
  '',
121
123
  '## Usage',
122
124
  '',
123
- 'Reference skills using the `Skill` tool or delegate to subagents using the `Agent` tool.',
125
+ 'Mention a skill as `$rune-<name>` or let Codex activate it from the task description.',
126
+ 'When an applicable Rune skill requests delegation, Codex may run bounded subagents in parallel.',
124
127
  '',
125
128
  '## Skills Directory',
126
129
  '',
@@ -134,12 +137,86 @@ export default {
134
137
  `- sonnet → \`${MODEL_MAP.sonnet}\` (\`model_reasoning_effort = "${REASONING_EFFORT_MAP.sonnet}"\`) — code / edit / review`,
135
138
  `- haiku → \`${MODEL_MAP.haiku}\` (\`model_reasoning_effort = "${REASONING_EFFORT_MAP.haiku}"\`) — scan / read-only`,
136
139
  '',
137
- 'Set `model` + `model_reasoning_effort` in `~/.codex/config.toml` to match the tier of the task.',
140
+ 'Generated project-scoped roles live in `.codex/agents/` so spawned agents can apply these settings natively.',
138
141
  '',
139
142
  '---',
140
143
  '> Rune Skill Mesh — https://github.com/rune-kit/rune',
144
+ '<!-- rune:codex:end -->',
141
145
  '',
142
146
  ].join('\n');
143
- return [{ path: 'AGENTS.md', content: agentsMd }];
147
+
148
+ const agentsMd = await mergeAgentsMd(outputRoot, runeBlock);
149
+ return [
150
+ { path: 'AGENTS.md', content: agentsMd },
151
+ {
152
+ path: '.codex/agents/rune-heavy.toml',
153
+ content: renderAgentRole({
154
+ name: 'rune_heavy',
155
+ description: 'Rune worker for architecture, security, planning, and other high-complexity tasks.',
156
+ model: MODEL_MAP.opus,
157
+ effort: REASONING_EFFORT_MAP.opus,
158
+ instructions:
159
+ 'Follow the applicable Rune skill exactly. Prioritize correctness, explicit assumptions, impact analysis, and complete verification.',
160
+ }),
161
+ },
162
+ {
163
+ path: '.codex/agents/rune-standard.toml',
164
+ content: renderAgentRole({
165
+ name: 'rune_standard',
166
+ description: 'Rune worker for implementation, debugging, review, and everyday engineering tasks.',
167
+ model: MODEL_MAP.sonnet,
168
+ effort: REASONING_EFFORT_MAP.sonnet,
169
+ instructions:
170
+ 'Follow the applicable Rune skill exactly. Keep changes scoped, preserve user work, and verify behavior before reporting completion.',
171
+ }),
172
+ },
173
+ {
174
+ path: '.codex/agents/rune-fast.toml',
175
+ content: renderAgentRole({
176
+ name: 'rune_fast',
177
+ description: 'Read-heavy Rune worker for scouting, dependency checks, and focused evidence gathering.',
178
+ model: MODEL_MAP.haiku,
179
+ effort: REASONING_EFFORT_MAP.haiku,
180
+ instructions:
181
+ 'Stay read-only unless the parent explicitly requests edits. Return concise evidence with file and line references.',
182
+ }),
183
+ },
184
+ ];
144
185
  },
145
186
  };
187
+
188
+ const RUNE_AGENTS_START = '<!-- rune:codex:start -->';
189
+ const RUNE_AGENTS_END = '<!-- rune:codex:end -->';
190
+
191
+ async function mergeAgentsMd(outputRoot, runeBlock) {
192
+ if (!outputRoot) return runeBlock;
193
+
194
+ const agentsPath = path.join(outputRoot, 'AGENTS.md');
195
+ if (!existsSync(agentsPath)) return runeBlock;
196
+
197
+ const existing = await readFile(agentsPath, 'utf-8');
198
+ const start = existing.indexOf(RUNE_AGENTS_START);
199
+ const end = existing.indexOf(RUNE_AGENTS_END);
200
+ if (start !== -1 && end > start) {
201
+ const after = end + RUNE_AGENTS_END.length;
202
+ return `${existing.slice(0, start)}${runeBlock}${existing.slice(after)}`;
203
+ }
204
+
205
+ // Migrate files produced by Rune versions before managed markers existed.
206
+ if (existing.trimStart().startsWith('# Rune — Project Configuration')) return runeBlock;
207
+
208
+ return `${existing.trimEnd()}\n\n${runeBlock}`;
209
+ }
210
+
211
+ function renderAgentRole({ name, description, model, effort, instructions }) {
212
+ return [
213
+ `name = "${name}"`,
214
+ `description = "${description}"`,
215
+ `model = "${model}"`,
216
+ `model_reasoning_effort = "${effort}"`,
217
+ 'developer_instructions = """',
218
+ instructions,
219
+ '"""',
220
+ '',
221
+ ].join('\n');
222
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * OpenAI Codex hooks adapter.
3
+ *
4
+ * Target: `.codex/hooks.json` — Codex-native lifecycle hooks. Codex currently
5
+ * skips handlers marked `async`, so Rune emits every command synchronously and
6
+ * relies on the preset's `--gentle` flag for warn-only behavior.
7
+ */
8
+
9
+ import { existsSync } from 'node:fs';
10
+ import { readFile } from 'node:fs/promises';
11
+ import path from 'node:path';
12
+ import {
13
+ appendHookBlock,
14
+ detectPreset,
15
+ stripHooksBySkill,
16
+ stripRuneHooks,
17
+ summarizeRuneHooks,
18
+ } from '../../commands/hooks/merge.js';
19
+ import { buildPreset, WIRED_SKILLS } from '../../commands/hooks/presets.js';
20
+
21
+ export const id = 'codex';
22
+ export const HOOKS_REL_PATH = '.codex/hooks.json';
23
+
24
+ const RUNE_DESCRIPTION = 'Rune lifecycle hooks for OpenAI Codex.';
25
+
26
+ export function detect(projectRoot) {
27
+ return existsSync(path.join(projectRoot, '.codex'));
28
+ }
29
+
30
+ export async function emit({ preset, projectRoot, tierManifests = [] }) {
31
+ if (preset === 'off' && tierManifests.length === 0) return uninstall({ projectRoot });
32
+ if (preset !== 'off' && preset !== 'strict' && preset !== 'gentle') {
33
+ throw new Error(`codex adapter: invalid preset '${preset}'`);
34
+ }
35
+
36
+ const hooksPath = path.join(projectRoot, HOOKS_REL_PATH);
37
+ const existing = await readJson(hooksPath);
38
+ let merged = stripRuneHooks(existing);
39
+ const notes = [
40
+ 'Codex requires review/trust for new or changed project hooks. Open `/hooks` after installation.',
41
+ 'Codex does not execute async command hooks; Rune emitted synchronous handlers with preset-controlled behavior.',
42
+ ];
43
+
44
+ if (!merged.description) merged.description = RUNE_DESCRIPTION;
45
+ if (preset !== 'off') {
46
+ merged = appendHookBlock(merged, normalizeBlock(buildPreset(preset)));
47
+ }
48
+
49
+ for (const manifest of tierManifests) {
50
+ const overrideKeys = Object.keys(manifest.overrides || {});
51
+ if (overrideKeys.length > 0) merged = stripHooksBySkill(merged, overrideKeys);
52
+
53
+ const tierBlock = buildCodexTierBlock(manifest);
54
+ if (Object.keys(tierBlock.hooks).length > 0) {
55
+ merged = appendHookBlock(merged, tierBlock);
56
+ }
57
+ if (tierBlock.skipped.length > 0) {
58
+ notes.push(`${manifest.tier}: skipped Codex-unsupported entries (${tierBlock.skipped.join(', ')}).`);
59
+ }
60
+ }
61
+
62
+ return {
63
+ files: [{ path: hooksPath, content: `${JSON.stringify(merged, null, 2)}\n` }],
64
+ notes,
65
+ };
66
+ }
67
+
68
+ export async function uninstall({ projectRoot }) {
69
+ const hooksPath = path.join(projectRoot, HOOKS_REL_PATH);
70
+ if (!existsSync(hooksPath)) {
71
+ return { files: [], notes: ['no .codex/hooks.json — nothing to uninstall'] };
72
+ }
73
+
74
+ const existing = await readJson(hooksPath);
75
+ const stripped = stripRuneHooks(existing);
76
+ const keys = Object.keys(stripped);
77
+ const isRuneOnlyShell = keys.length === 1 && keys[0] === 'description' && stripped.description === RUNE_DESCRIPTION;
78
+
79
+ return {
80
+ files: [{ path: hooksPath, content: isRuneOnlyShell ? null : `${JSON.stringify(stripped, null, 2)}\n` }],
81
+ notes: [],
82
+ };
83
+ }
84
+
85
+ export async function status(projectRoot) {
86
+ const hooksPath = path.join(projectRoot, HOOKS_REL_PATH);
87
+ if (!existsSync(hooksPath)) {
88
+ return {
89
+ installed: false,
90
+ preset: null,
91
+ wired: [],
92
+ missing: [...WIRED_SKILLS],
93
+ notes: ['no .codex/hooks.json'],
94
+ };
95
+ }
96
+
97
+ const settings = await readJson(hooksPath);
98
+ const summary = summarizeRuneHooks(settings);
99
+ const preset = detectPreset(settings);
100
+ const wired = Array.from(new Set(Object.values(summary.events).flat()));
101
+ return {
102
+ installed: summary.total > 0,
103
+ preset: preset === 'none' ? null : preset,
104
+ wired,
105
+ missing: WIRED_SKILLS.filter((skill) => !wired.includes(skill)),
106
+ events: summary.events,
107
+ notes: ['Use `/hooks` in Codex to inspect the current trust state.'],
108
+ };
109
+ }
110
+
111
+ function normalizeBlock(block) {
112
+ const hooks = {};
113
+ for (const [event, groups] of Object.entries(block.hooks || {})) {
114
+ hooks[event] = groups.map((group) => {
115
+ const normalized = {
116
+ ...group,
117
+ hooks: group.hooks.map(normalizeCommand),
118
+ };
119
+ if (event === 'UserPromptSubmit' || event === 'Stop') delete normalized.matcher;
120
+ return normalized;
121
+ });
122
+ }
123
+ return { hooks };
124
+ }
125
+
126
+ function normalizeCommand(entry) {
127
+ const { async: _unsupported, ...normalized } = entry;
128
+ const commandWindows = toWindowsCommand(normalized.command);
129
+ if (commandWindows !== normalized.command) normalized.commandWindows = commandWindows;
130
+ return normalized;
131
+ }
132
+
133
+ function buildCodexTierBlock(manifest) {
134
+ const hooks = {};
135
+ const skipped = [];
136
+
137
+ for (const entry of manifest.entries) {
138
+ if (entry.event === 'statusLine' || entry.claudeOnly || entry.platforms?.codex === 'unsupported') {
139
+ skipped.push(entry.id || entry.skill);
140
+ continue;
141
+ }
142
+
143
+ const event = entry.event;
144
+ const matcher = entry.matcher || '.*';
145
+ if (!hooks[event]) hooks[event] = [];
146
+ let group = hooks[event].find((candidate) => candidate.matcher === matcher);
147
+ if (!group) {
148
+ group = { matcher, hooks: [] };
149
+ hooks[event].push(group);
150
+ }
151
+ group.hooks.push(
152
+ normalizeCommand({
153
+ type: 'command',
154
+ command: entry.command,
155
+ }),
156
+ );
157
+ }
158
+
159
+ return { ...normalizeBlock({ hooks }), skipped };
160
+ }
161
+
162
+ function toWindowsCommand(command) {
163
+ if (typeof command !== 'string') return command;
164
+ return command.replace(/\$\{([A-Z][A-Z0-9_]*)\}/g, '%$1%');
165
+ }
166
+
167
+ async function readJson(hooksPath) {
168
+ if (!existsSync(hooksPath)) return {};
169
+ const raw = await readFile(hooksPath, 'utf-8');
170
+ if (!raw.trim()) return {};
171
+ try {
172
+ return JSON.parse(raw);
173
+ } catch (err) {
174
+ throw new Error(
175
+ `${hooksPath} is not valid JSON — fix it manually or delete the file and re-run \`rune hooks install --platform codex\`. (${err.message})`,
176
+ );
177
+ }
178
+ }
@@ -15,11 +15,13 @@
15
15
 
16
16
  import * as antigravity from './antigravity.js';
17
17
  import * as claude from './claude.js';
18
+ import * as codex from './codex.js';
18
19
  import * as cursor from './cursor.js';
19
20
  import * as windsurf from './windsurf.js';
20
21
 
21
22
  export const ADAPTERS = Object.freeze({
22
23
  claude,
24
+ codex,
23
25
  cursor,
24
26
  windsurf,
25
27
  antigravity,
@@ -55,6 +57,14 @@ export const CAPABILITIES = Object.freeze({
55
57
  stop: true,
56
58
  notes: 'Full hook parity — PreToolUse/PostToolUse/Stop all supported.',
57
59
  },
60
+ codex: {
61
+ maturity: 'stable',
62
+ preToolEdit: true,
63
+ preToolBash: true,
64
+ postToolEdit: true,
65
+ stop: true,
66
+ notes: 'Native hooks via `.codex/hooks.json`; operator trust is required before first execution.',
67
+ },
58
68
  cursor: {
59
69
  maturity: 'beta',
60
70
  preToolEdit: 'rule-injection',
@@ -105,7 +105,7 @@ export default {
105
105
  id: 'rune',
106
106
  name: 'Rune',
107
107
  kind: 'skills',
108
- description: `${skills.length}-skill mesh for AI coding assistants. Routes all code tasks through specialized skills. 203 sync connections + 40 async signals, 14 extension packs.`,
108
+ description: `${skills.length}-skill mesh for AI coding assistants. Routes all code tasks through specialized skills. 248 sync connections + 45 async signals, 14 extension packs.`,
109
109
  version: pluginJson.version || '0.0.0',
110
110
  skills: ['./skills'],
111
111
  artifactConvention: {
@@ -170,7 +170,7 @@ export default {
170
170
 
171
171
  > Less skills. Deeper connections.
172
172
 
173
- **${skills.length}-skill mesh** for AI coding assistants — 5-layer architecture, 215+ connections, 14 extension packs.
173
+ **${skills.length}-skill mesh** for AI coding assistants — 5-layer architecture, 248 connections + 45 signals, 14 extension packs.
174
174
 
175
175
  ## Install
176
176
 
@@ -63,9 +63,10 @@ function detectPlatform(projectRoot) {
63
63
  if (existsSync(path.join(projectRoot, '.claude-plugin'))) return 'claude';
64
64
  if (existsSync(path.join(projectRoot, '.cursor'))) return 'cursor';
65
65
  if (existsSync(path.join(projectRoot, '.windsurf'))) return 'windsurf';
66
- if (existsSync(path.join(projectRoot, '.agents'))) return 'antigravity';
67
- if (existsSync(path.join(projectRoot, '.openclaw'))) return 'openclaw';
68
66
  if (existsSync(path.join(projectRoot, '.codex'))) return 'codex';
67
+ if (existsSync(path.join(projectRoot, '.agents', 'skills'))) return 'codex';
68
+ if (existsSync(path.join(projectRoot, '.antigravity'))) return 'antigravity';
69
+ if (existsSync(path.join(projectRoot, '.openclaw'))) return 'openclaw';
69
70
  if (existsSync(path.join(projectRoot, '.opencode'))) return 'opencode';
70
71
  return null;
71
72
  }
@@ -655,7 +656,7 @@ async function cmdHooks(projectRoot, args, subcommand) {
655
656
  ' status [--platform <name>|all] [--tier pro|business] Show active preset, wired skills, tier coverage',
656
657
  );
657
658
  log('');
658
- log(' Platforms: claude, cursor, windsurf, antigravity (auto-detected if omitted)');
659
+ log(' Platforms: claude, codex, cursor, windsurf, antigravity (auto-detected if omitted)');
659
660
  log(' Tiers: pro, business — requires $RUNE_PRO_ROOT / $RUNE_BUSINESS_ROOT env var or monorepo sibling.');
660
661
  log(' Options:');
661
662
  log(' --dry Preview changes without writing');
@@ -899,7 +900,7 @@ async function main() {
899
900
  log(' [--days <n>] Lookback window for gate counts (default: 30)');
900
901
  log(' hooks Install/uninstall/status for multi-platform auto-discipline');
901
902
  log(
902
- ' hooks install [--preset gentle|strict|off] [--platform claude|cursor|windsurf|antigravity|all] [--global]',
903
+ ' hooks install [--preset gentle|strict|off] [--platform claude|codex|cursor|windsurf|antigravity|all] [--global]',
903
904
  );
904
905
  log(' hooks uninstall [--platform <name>|all]');
905
906
  log(' hooks status [--platform <name>|all]');
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `rune hooks install [--preset strict|gentle|off] [--platform claude|cursor|windsurf|antigravity|all]`
2
+ * `rune hooks install [--preset strict|gentle|off] [--platform claude|codex|cursor|windsurf|antigravity|all]`
3
3
  *
4
4
  * Writes Rune-managed hook/rule/workflow entries for one or more platforms.
5
5
  * Idempotent: re-running replaces existing Rune entries, preserves user entries.
@@ -41,7 +41,7 @@ export async function installHooks(projectRoot, args = {}) {
41
41
  results: [],
42
42
  written: false,
43
43
  notes: [
44
- 'No target platform detected. Create `.claude/`, `.cursor/`, `.windsurf/`, or `.antigravity/` first, or pass `--platform <name>`.',
44
+ 'No target platform detected. Create `.claude/`, `.codex/`, `.cursor/`, `.windsurf/`, or `.antigravity/` first, or pass `--platform <name>`.',
45
45
  ...tierNotes,
46
46
  ],
47
47
  };
@@ -34,7 +34,7 @@ export async function hookStatus(projectRoot, runeRoot, args = {}) {
34
34
  tiers,
35
35
  missingInRepo,
36
36
  notes: [
37
- 'No target platform detected. Create `.claude/`, `.cursor/`, `.windsurf/`, or `.antigravity/` first, or pass `--platform <name>`.',
37
+ 'No target platform detected. Create `.claude/`, `.codex/`, `.cursor/`, `.windsurf/`, or `.antigravity/` first, or pass `--platform <name>`.',
38
38
  ],
39
39
  };
40
40
  }