@rune-kit/rune 2.26.0 → 2.26.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -2
- package/hooks/hooks.json +5 -5
- package/hooks/lib/hook-output.cjs +102 -0
- package/hooks/post-session-reflect/index.cjs +5 -0
- package/hooks/pre-compact/index.cjs +6 -0
- package/hooks/pre-tool-guard/index.cjs +27 -1
- package/hooks/session-start/index.cjs +5 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -103,11 +103,19 @@ Plus **9 domain packs** (product, sales, data-science, support, growth, media, p
|
|
|
103
103
|
|
|
104
104
|
---
|
|
105
105
|
|
|
106
|
-
## What's New (v2.26.
|
|
106
|
+
## What's New (v2.26.2 — Hook Output Contract)
|
|
107
|
+
|
|
108
|
+
> **v2.26.2 (2026-07-22):** The other half of the Codex wiring fix — v2.26.1 made the hook matchers fire, this makes the hooks **succeed**. Codex parses hook stdout as JSON and reports anything else as `hook: <Event> Failed`, discarding the output; Rune's hooks printed bare `[Rune: ...]` lines, so every hook that loaded on Codex ran, exited 0, and had its output thrown away. Hooks now emit the envelope both runtimes accept — `hookSpecificOutput.additionalContext` for context events, `systemMessage` otherwise — which is Claude Code's documented contract too, not a Codex branch. Verified live against codex-cli 0.145: the same hook goes `SessionStart Failed` → `SessionStart Completed`.
|
|
109
|
+
|
|
110
|
+
### Previous (v2.26.1 — Codex Wiring)
|
|
111
|
+
|
|
112
|
+
> **v2.26.1 (2026-07-22):** Rune's runtime hooks were silently inert on **Codex CLI**. `hooks/hooks.json` is loaded by both Claude Code and Codex — Codex reads `<plugin>/hooks/hooks.json`, the same path, and maps the event names — but every tool matcher named only Claude's tools. Codex has no `Read`, `Write`, `Edit` or `Bash` tool; it issues `shell_command`, `exec`, `apply_patch`, `view_image`, `spawn_agent`. So the privacy gate and the secret scanner matched nothing and never fired. Matchers now name both platforms' tools (plain alternation — Claude behaviour is byte-for-byte unchanged), and `pre-tool-guard` reads the target path out of a Codex `apply_patch` payload (`*** Update File: <path>`), which is what makes it an actual gate there instead of a no-op. Note that Codex does not support `async` hooks yet and skips them, so Rune's six background hooks stay inactive on Codex — `async` is kept because Claude Code honours it.
|
|
113
|
+
|
|
114
|
+
#### Earlier (v2.26.0 — Motion Craft)
|
|
107
115
|
|
|
108
116
|
> **v2.26.0 (2026-07-18):** Rune's UI mesh gains a deep **motion authority**. New reference `skills/design/MOTION-CRAFT.md` is the canonical source for animation decisions: the *should-it-animate* frequency gate (never animate keyboard/100+-per-day actions), easing decision tree with strong custom curves, per-element duration budgets (UI under 300ms, modals/drawers exempt to 500ms), physicality rules (never `scale(0)`, origin-aware popovers, press feedback), spring physics (damping/response, velocity handoff, momentum projection, rubber-banding), interruptibility (transitions vs keyframes, `@starting-style`), motion performance, reduced-motion, and a reverse-lookup vocabulary glossary. `design` (v0.8.0) loads it whenever a domain involves motion and gains an advisory motion-audit mode. `review` (v1.5.0) adds **Motion Craft Checks** — an advisory lens that fires only on motion diffs, flagging `ease-in` on UI, `scale(0)` entrances, animation on high-frequency actions, layout-property animation, and more, citing MOTION-CRAFT for exact fixes. `perf` (v0.6.0) adds **Step 5.5 Motion Performance** — GPU-property, Framer-Motion-shorthand, and recalc-storm detection ranked in the Cost Impact Hierarchy. Advisory-first throughout (no new HARD-GATEs) — enrichment only, mesh unchanged at 66 skills.
|
|
109
117
|
|
|
110
|
-
|
|
118
|
+
#### Earlier (v2.25.0 — Council)
|
|
111
119
|
|
|
112
120
|
> **v2.25.0 (2026-07-11):** New **`council`** L3 primitive — genuinely decorrelated multi-perspective gathering. Detects installed AI CLIs, fans a question across distinct model families (external CLIs when present, subagents when not), gates off-topic/malformed voices, normalizes claims (anchor-first + shared-evidence arbiter fallback — prose similarity never counts), and arbitrates inline with an honest `NO_DECORRELATION` stamp when fewer than 2 real model families answered. Wired into 4 consumers: `adversary` (CRITICAL-tier plans), `review` (high-blast-radius diffs), `brainstorm` (Design-It-Twice candidate judgment), `problem-solver` (high-stakes conclusions) — every integration explicitly refuses to launder a same-family run as independent confirmation. **Live-dogfooded same day**: a real 3-voice self-test (external `grok` dispatch + 2 subagent fallbacks) independently converged on a real gap — "confirmed" `model_family` was CLI-brand identity, not verified backend identity, letting two CLIs redirected to the same backend (BYOK/proxy override) defeat the gate — fixed before shipping. **Pro**: `council-cockpit` (separate repo) adds a read-only live panel + cost-aware allocation + history-informed family weighting, PRE-council advisory only — Free never depends on Pro. Mesh now 66 skills / 208 connections / 45 signals.
|
|
113
121
|
|
package/hooks/hooks.json
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
],
|
|
27
27
|
"PreToolUse": [
|
|
28
28
|
{
|
|
29
|
-
"matcher": "Read|Write|Edit",
|
|
29
|
+
"matcher": "Read|Write|Edit|apply_patch|view_image",
|
|
30
30
|
"hooks": [
|
|
31
31
|
{
|
|
32
32
|
"type": "command",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
]
|
|
47
47
|
},
|
|
48
48
|
{
|
|
49
|
-
"matcher": "Bash",
|
|
49
|
+
"matcher": "Bash|shell_command",
|
|
50
50
|
"hooks": [
|
|
51
51
|
{
|
|
52
52
|
"type": "command",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
],
|
|
59
59
|
"PostToolUse": [
|
|
60
60
|
{
|
|
61
|
-
"matcher": "Edit|Write",
|
|
61
|
+
"matcher": "Edit|Write|apply_patch",
|
|
62
62
|
"hooks": [
|
|
63
63
|
{
|
|
64
64
|
"type": "command",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
]
|
|
74
74
|
},
|
|
75
75
|
{
|
|
76
|
-
"matcher": "Skill|Task|Agent",
|
|
76
|
+
"matcher": "Skill|Task|Agent|spawn_agent",
|
|
77
77
|
"hooks": [
|
|
78
78
|
{
|
|
79
79
|
"type": "command",
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
]
|
|
84
84
|
},
|
|
85
85
|
{
|
|
86
|
-
"matcher": "mcp__.*|WebFetch|Read",
|
|
86
|
+
"matcher": "mcp__.*|WebFetch|Read|web_search",
|
|
87
87
|
"hooks": [
|
|
88
88
|
{
|
|
89
89
|
"type": "command",
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Hook stdout envelope — the one format both runtimes accept.
|
|
4
|
+
//
|
|
5
|
+
// Claude Code tolerates bare text on a hook's stdout. Codex CLI does not: it
|
|
6
|
+
// parses stdout as its `HookUniversalOutputWire` JSON and reports anything else
|
|
7
|
+
// as `hook: <Event> Failed`, discarding the output. Every Rune hook that printed
|
|
8
|
+
// a plain `[Rune: ...]` line was therefore failing on Codex — the hooks loaded,
|
|
9
|
+
// ran, exited 0, and had their output thrown away.
|
|
10
|
+
//
|
|
11
|
+
// Both runtimes accept the same envelope, so emitting it is not a Codex-specific
|
|
12
|
+
// branch — it is simply the correct output contract:
|
|
13
|
+
//
|
|
14
|
+
// {"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"…"}}
|
|
15
|
+
// {"systemMessage":"…"}
|
|
16
|
+
//
|
|
17
|
+
// Verified against codex-cli 0.145 (schema read from the binary: decision,
|
|
18
|
+
// reason, continue, stopReason, suppressOutput, systemMessage, and
|
|
19
|
+
// hookSpecificOutput.{hookEventName,additionalContext,permissionDecision,…};
|
|
20
|
+
// unknown keys are rejected) and Claude Code's documented hook output.
|
|
21
|
+
//
|
|
22
|
+
// Buffer-then-emit, because the envelope is ONE JSON object: a hook that prints
|
|
23
|
+
// three lines must accumulate them and emit once at exit, not three times.
|
|
24
|
+
|
|
25
|
+
/** Events where the model-facing payload is `additionalContext`. */
|
|
26
|
+
const CONTEXT_EVENTS = new Set(['SessionStart', 'UserPromptSubmit']);
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Collects a hook's output and emits a single valid envelope.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} hookEventName Claude/Codex event name, e.g. 'SessionStart'
|
|
32
|
+
* @returns {{line: (text: string) => void, emit: () => void, isEmpty: () => boolean}}
|
|
33
|
+
*/
|
|
34
|
+
function outputBuffer(hookEventName) {
|
|
35
|
+
const parts = [];
|
|
36
|
+
return {
|
|
37
|
+
/** Queue one line of output. Empty/blank input is ignored. */
|
|
38
|
+
line(text) {
|
|
39
|
+
if (typeof text !== 'string') return;
|
|
40
|
+
const trimmed = text.replace(/\s+$/, '');
|
|
41
|
+
if (trimmed.trim()) parts.push(trimmed);
|
|
42
|
+
},
|
|
43
|
+
isEmpty() {
|
|
44
|
+
return parts.length === 0;
|
|
45
|
+
},
|
|
46
|
+
/** Write the envelope. Emits nothing at all when there is nothing to say. */
|
|
47
|
+
emit() {
|
|
48
|
+
if (parts.length === 0) return;
|
|
49
|
+
const text = parts.join('\n');
|
|
50
|
+
const payload = CONTEXT_EVENTS.has(hookEventName)
|
|
51
|
+
? { hookSpecificOutput: { hookEventName, additionalContext: text } }
|
|
52
|
+
: { systemMessage: text };
|
|
53
|
+
process.stdout.write(`${JSON.stringify(payload)}\n`);
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* One-shot form for hooks with a single message.
|
|
60
|
+
*
|
|
61
|
+
* @param {string} hookEventName
|
|
62
|
+
* @param {string} text
|
|
63
|
+
*/
|
|
64
|
+
function emit(hookEventName, text) {
|
|
65
|
+
const buf = outputBuffer(hookEventName);
|
|
66
|
+
buf.line(text);
|
|
67
|
+
buf.emit();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Route a hook's existing `console.log` calls into one envelope.
|
|
72
|
+
*
|
|
73
|
+
* Hooks build their message across many prints and several early `process.exit`
|
|
74
|
+
* paths. Rewriting each print site into an explicit buffer would mean touching
|
|
75
|
+
* every branch — and missing one silently reintroduces the bare-text bug. So
|
|
76
|
+
* capture at the boundary instead: the message logic stays exactly as written,
|
|
77
|
+
* and the envelope is emitted once on exit no matter which branch got there.
|
|
78
|
+
*
|
|
79
|
+
* `console.error` is untouched — stderr is not parsed as the output contract.
|
|
80
|
+
*
|
|
81
|
+
* @param {string} hookEventName
|
|
82
|
+
* @returns {{buffer: ReturnType<typeof outputBuffer>, restore: () => void}}
|
|
83
|
+
*/
|
|
84
|
+
function captureConsole(hookEventName) {
|
|
85
|
+
const buffer = outputBuffer(hookEventName);
|
|
86
|
+
const original = console.log;
|
|
87
|
+
console.log = (...args) => {
|
|
88
|
+
buffer.line(args.map((a) => (typeof a === 'string' ? a : String(a))).join(' '));
|
|
89
|
+
};
|
|
90
|
+
const restore = () => {
|
|
91
|
+
console.log = original;
|
|
92
|
+
};
|
|
93
|
+
// Fires for a natural end AND for process.exit(), which is how the BLOCK
|
|
94
|
+
// paths leave — those messages must survive too.
|
|
95
|
+
process.on('exit', () => {
|
|
96
|
+
restore();
|
|
97
|
+
buffer.emit();
|
|
98
|
+
});
|
|
99
|
+
return { buffer, restore };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = { outputBuffer, emit, captureConsole, CONTEXT_EVENTS };
|
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const os = require('os');
|
|
8
|
+
const { captureConsole } = require('../lib/hook-output.cjs');
|
|
9
|
+
// Hook stdout is a JSON contract, not free text (Codex rejects bare lines and
|
|
10
|
+
// discards the output). Capture the prints below and emit one envelope on exit.
|
|
11
|
+
captureConsole('Stop');
|
|
12
|
+
|
|
8
13
|
|
|
9
14
|
const cwd = process.cwd();
|
|
10
15
|
const hash = Buffer.from(cwd).toString('base64url').slice(0, 16);
|
|
@@ -9,6 +9,12 @@ const fs = require('fs');
|
|
|
9
9
|
const path = require('path');
|
|
10
10
|
const os = require('os');
|
|
11
11
|
|
|
12
|
+
const { captureConsole } = require('../lib/hook-output.cjs');
|
|
13
|
+
// Hook stdout is a JSON contract, not free text (Codex rejects bare lines and
|
|
14
|
+
// discards the output). Capture the prints below and emit one envelope on exit.
|
|
15
|
+
captureConsole('PreCompact');
|
|
16
|
+
|
|
17
|
+
|
|
12
18
|
const cwd = process.cwd();
|
|
13
19
|
const runeDir = path.join(cwd, '.rune');
|
|
14
20
|
|
|
@@ -15,6 +15,12 @@
|
|
|
15
15
|
const fs = require('fs');
|
|
16
16
|
const path = require('path');
|
|
17
17
|
|
|
18
|
+
const { captureConsole } = require('../lib/hook-output.cjs');
|
|
19
|
+
// Hook stdout is a JSON contract, not free text (Codex rejects bare lines and
|
|
20
|
+
// discards the output). Capture the prints below and emit one envelope on exit.
|
|
21
|
+
captureConsole('PreToolUse');
|
|
22
|
+
|
|
23
|
+
|
|
18
24
|
/**
|
|
19
25
|
* Append a structured gate-outcome record to .rune/metrics/gate-outcomes.jsonl
|
|
20
26
|
* relative to process.cwd(). Best-effort: any error is silently swallowed so
|
|
@@ -50,6 +56,26 @@ function appendGateOutcome(gate, outcome, detail) {
|
|
|
50
56
|
}
|
|
51
57
|
}
|
|
52
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Path a Codex `apply_patch` call is about to write.
|
|
61
|
+
*
|
|
62
|
+
* Codex has no Edit/Write tool — file writes arrive as one `apply_patch` call
|
|
63
|
+
* whose payload is raw patch text, so there is no `file_path` to read. The
|
|
64
|
+
* target is in the patch header. Without this the privacy gate is a no-op on
|
|
65
|
+
* Codex: every write sails past.
|
|
66
|
+
*
|
|
67
|
+
* @param {string} toolName
|
|
68
|
+
* @param {object} toolInput
|
|
69
|
+
* @returns {string} target path, or '' when this is not a patch we understand
|
|
70
|
+
*/
|
|
71
|
+
function patchTargetPath(toolName, toolInput) {
|
|
72
|
+
if (toolName !== 'apply_patch') return '';
|
|
73
|
+
const patch = typeof toolInput === 'string' ? toolInput : toolInput.input || toolInput.patch || '';
|
|
74
|
+
if (typeof patch !== 'string') return '';
|
|
75
|
+
const match = patch.match(/^\*\*\* (?:Update|Add|Delete) File:\s*(.+)$/m);
|
|
76
|
+
return match ? match[1].trim() : '';
|
|
77
|
+
}
|
|
78
|
+
|
|
53
79
|
// Read tool_input from Claude Code hook stdin
|
|
54
80
|
let input = '';
|
|
55
81
|
process.stdin.setEncoding('utf-8');
|
|
@@ -66,7 +92,7 @@ process.stdin.on('end', () => {
|
|
|
66
92
|
process.exit(0);
|
|
67
93
|
}
|
|
68
94
|
|
|
69
|
-
const filePath = toolInput.file_path || toolInput.path ||
|
|
95
|
+
const filePath = toolInput.file_path || toolInput.path || patchTargetPath(toolName, toolInput);
|
|
70
96
|
if (!filePath) process.exit(0);
|
|
71
97
|
|
|
72
98
|
const basename = path.basename(filePath);
|
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const os = require('os');
|
|
7
|
+
const { captureConsole } = require('../lib/hook-output.cjs');
|
|
8
|
+
// Hook stdout is a JSON contract, not free text (Codex rejects bare lines and
|
|
9
|
+
// discards the output). Capture the prints below and emit one envelope on exit.
|
|
10
|
+
captureConsole('SessionStart');
|
|
11
|
+
|
|
7
12
|
|
|
8
13
|
const cwd = process.cwd();
|
|
9
14
|
const runeDir = path.join(cwd, '.rune');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rune-kit/rune",
|
|
3
|
-
"version": "2.26.
|
|
3
|
+
"version": "2.26.2",
|
|
4
4
|
"description": "65-skill mesh for AI coding assistants — runtime auto-discipline via native hooks (Claude/Cursor/Windsurf/Antigravity), 5-layer architecture, 204 connections + 43 signals, multi-platform compiler. converge (L3) scans spec vs code so dead-button/frontend-only implementations can't ship.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|