@rune-kit/rune 2.30.0 → 2.30.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +6 -2
- package/compiler/emitter.js +1 -1
- package/hooks/context-watch/index.cjs +5 -8
- package/hooks/hooks.json +1 -1
- package/hooks/intent-router/index.cjs +9 -6
- package/hooks/lib/hook-output.cjs +7 -1
- package/hooks/lib/hook-stdin.cjs +52 -0
- package/hooks/metrics-collector/index.cjs +5 -8
- package/hooks/pre-tool-guard/index.cjs +6 -6
- package/hooks/quarantine/index.cjs +12 -21
- package/package.json +4 -3
- package/skill-index.json +2187 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rune",
|
|
3
|
-
"version": "2.30.
|
|
3
|
+
"version": "2.30.1",
|
|
4
4
|
"description": "66-skill engineering mesh with Codex-native skills, subagents, synchronous lifecycle hooks, security gates, MCP-aware workflows, and project memory.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Rune Contributors"
|
package/README.md
CHANGED
|
@@ -103,11 +103,15 @@ Plus **9 domain packs** (product, sales, data-science, support, growth, media, p
|
|
|
103
103
|
|
|
104
104
|
---
|
|
105
105
|
|
|
106
|
-
## What's New (v2.30.
|
|
106
|
+
## What's New (v2.30.1 — Hooks That Land)
|
|
107
|
+
|
|
108
|
+
> **v2.30.1 (2026-07-29):** Five hooks had been running, exiting 0, and reaching nobody — nothing logged an error, so the hook layer looked healthy while contributing nothing. Two independent faults, each fatal on its own: **`process.stdout.write` from an exit handler is not guaranteed to reach a piped stdout** (most hooks emit via `captureConsole`, which flushes on exit — the envelope was built, "written", and lost; now `fs.writeSync(1, …)`), and **a hook that collects stdin with an async listener has its stdout discarded by Claude Code** even when the write is synchronous (new `hooks/lib/hook-stdin.cjs` reads with `readFileSync(0)`; `intent-router`, `context-watch`, `metrics-collector`, `pre-tool-guard`, `quarantine` converted). Both isolated by differential test — identical hooks, one variable at a time — and confirmed by the model quoting hook-injected context back. Separately, `intent-router` never had an index to read: `skill-index.json` is emitted at build time and Claude Code doesn't compile, so all five candidate paths were missing and it exited silently on every prompt since it shipped. New `scripts/build-skill-index.js` generates it at the plugin root with a `--check` staleness gate in CI. It was also registered `"async": true`, which drops `additionalContext` outright. 1,649 tests.
|
|
109
|
+
|
|
110
|
+
### Previous (v2.30.0 — Tier Restored)
|
|
107
111
|
|
|
108
112
|
> **v2.30.0 (2026-07-29):** Rune's model tier table had quietly stopped applying on Claude Code, and had been wrong in **23 of 66 places** for longer than that. `agents/*.md` is a hand-written parallel copy of each skill's tier; nothing generated it from `skills/` and no check compared the two, so they drifted — `cook` said opus in one file and sonnet in the other, `verification` said sonnet and haiku. `skills/*/SKILL.md` is now the single source of truth, every pair agrees, and a new **`validateAgentSync`** gate fails CI on any future drift, alongside a model-split check and a warning for a top-level `model` that cannot take effect. Where the two disagreed the tier was decided by role, with one rule overriding both old values: **a gate never runs below sonnet** — `completion-gate`, `constraint-check`, `integrity-check`, `hallucination-guard`, and `verification` exist to catch unverified claims, so running them cheaper than the agent they audit defeats the point. Separately: tiering only ever applied through spawned subagents, and on Opus 5 the harness tells the model not to spawn them unless asked — so every tier assignment had become a silent no-op. `scout` and `docs-seeker` now declare `context: fork` + `model: haiku` and route through the Skill tool instead; verified end-to-end on Opus 5 by per-model billing showing `claude-haiku-4-5`. `cook` deliberately declares no model — an orchestrator inherits the session model, because choosing Opus for a session is a deliberate act. 1,647 tests.
|
|
109
113
|
|
|
110
|
-
|
|
114
|
+
#### Earlier (v2.29.1 — One-Command Update)
|
|
111
115
|
|
|
112
116
|
> **v2.29.1 (2026-07-24):** v2.29.0 documented the update flow; this release automates it. New **`rune update`** command — one-shot updater for an already-configured project: `git pull --ff-only` any detected Pro/Business tier repos (env var → sibling dir, same detection as setup; a failed pull **aborts loudly**, never a silent half-update), re-runs the managed setup rewrite in place non-interactively (your installed platforms, preset, and tiers are detected from the existing hook config — no prompts), then verifies with doctor + hook drift and reminds Codex users to re-trust `/hooks` only when `.codex/hooks.json` actually changed. Flags: `--no-pull`, `--preset`, `--tier`, `--dry`. Plus: "Updating" sections in all three tier READMEs. Docs-and-CLI patch — no skill or mesh changes. 1,641 tests.
|
|
113
117
|
|
package/compiler/emitter.js
CHANGED
|
@@ -897,7 +897,7 @@ const INTENT_KEYWORDS = {
|
|
|
897
897
|
* @param {Array} parsedSkills - array of parsed skill objects
|
|
898
898
|
* @returns {object} skill index with graph + intents
|
|
899
899
|
*/
|
|
900
|
-
function generateSkillIndex(parsedSkills) {
|
|
900
|
+
export function generateSkillIndex(parsedSkills) {
|
|
901
901
|
// Build adjacency graph from cross-references
|
|
902
902
|
const graph = {};
|
|
903
903
|
const skills = {};
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
const fs = require('fs');
|
|
15
15
|
const { stateFile } = require('../lib/context-key.cjs');
|
|
16
16
|
const { captureConsole } = require('../lib/hook-output.cjs');
|
|
17
|
+
const { readStdinSync } = require('../lib/hook-stdin.cjs');
|
|
17
18
|
|
|
18
19
|
captureConsole('PreToolUse');
|
|
19
20
|
|
|
@@ -31,9 +32,9 @@ const CRITICAL_THRESHOLD = 120;
|
|
|
31
32
|
|
|
32
33
|
// Read stdin JSON to get tool name (Claude Code passes hook data via stdin)
|
|
33
34
|
let stdinData = '';
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
stdinData = readStdinSync();
|
|
36
|
+
|
|
37
|
+
(() => {
|
|
37
38
|
let toolName = 'unknown';
|
|
38
39
|
let sessionId;
|
|
39
40
|
try {
|
|
@@ -102,8 +103,4 @@ process.stdin.on('end', () => {
|
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
process.exit(0);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
// Handle empty stdin (pipe closed immediately)
|
|
108
|
-
process.stdin.on('error', () => process.exit(0));
|
|
109
|
-
process.stdin.resume();
|
|
106
|
+
})();
|
package/hooks/hooks.json
CHANGED
|
@@ -10,14 +10,14 @@
|
|
|
10
10
|
const fs = require('fs');
|
|
11
11
|
const path = require('path');
|
|
12
12
|
const { captureConsole } = require('../lib/hook-output.cjs');
|
|
13
|
+
const { readStdinSync } = require('../lib/hook-stdin.cjs');
|
|
13
14
|
|
|
14
15
|
captureConsole('UserPromptSubmit');
|
|
15
16
|
|
|
16
|
-
// Read user prompt from
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
process.stdin.on('end', () => {
|
|
17
|
+
// Read user prompt from the hook payload. Must be synchronous — see hook-stdin.cjs.
|
|
18
|
+
const input = readStdinSync();
|
|
19
|
+
|
|
20
|
+
(() => {
|
|
21
21
|
let userPrompt = '';
|
|
22
22
|
try {
|
|
23
23
|
const parsed = JSON.parse(input);
|
|
@@ -34,6 +34,9 @@ process.stdin.on('end', () => {
|
|
|
34
34
|
// Find skill-index.json — check multiple locations
|
|
35
35
|
const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT || path.resolve(__dirname, '../..');
|
|
36
36
|
const candidates = [
|
|
37
|
+
// Uncompiled plugin root (Claude Code serves the repo directly — nothing to
|
|
38
|
+
// compile, so this is the only index that exists on that runtime).
|
|
39
|
+
path.join(pluginRoot, 'skill-index.json'),
|
|
37
40
|
path.join(pluginRoot, '.cursor', 'rules', 'skill-index.json'),
|
|
38
41
|
path.join(pluginRoot, '.windsurf', 'rules', 'skill-index.json'),
|
|
39
42
|
path.join(pluginRoot, 'dist', 'cursor', 'skill-index.json'),
|
|
@@ -108,4 +111,4 @@ process.stdin.on('end', () => {
|
|
|
108
111
|
console.log('');
|
|
109
112
|
|
|
110
113
|
process.exit(0);
|
|
111
|
-
});
|
|
114
|
+
})();
|
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
// Buffer-then-emit, because the envelope is ONE JSON object: a hook that prints
|
|
23
23
|
// three lines must accumulate them and emit once at exit, not three times.
|
|
24
24
|
|
|
25
|
+
const fs = require('node:fs');
|
|
26
|
+
|
|
25
27
|
/** Events where the model-facing payload is `additionalContext`. */
|
|
26
28
|
const CONTEXT_EVENTS = new Set(['SessionStart', 'UserPromptSubmit']);
|
|
27
29
|
|
|
@@ -50,7 +52,11 @@ function outputBuffer(hookEventName) {
|
|
|
50
52
|
const payload = CONTEXT_EVENTS.has(hookEventName)
|
|
51
53
|
? { hookSpecificOutput: { hookEventName, additionalContext: text } }
|
|
52
54
|
: { systemMessage: text };
|
|
53
|
-
process.stdout.write
|
|
55
|
+
// fs.writeSync, not process.stdout.write: most of these emits happen from a
|
|
56
|
+
// `process.on('exit')` handler, where a piped stdout is not guaranteed to
|
|
57
|
+
// flush before the process is gone. The write reports success either way, so
|
|
58
|
+
// the loss is silent — every hook using console.log was landing nowhere.
|
|
59
|
+
fs.writeSync(1, `${JSON.stringify(payload)}\n`);
|
|
54
60
|
},
|
|
55
61
|
};
|
|
56
62
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Hook stdin — read it synchronously, always.
|
|
4
|
+
//
|
|
5
|
+
// A hook that collects stdin with `process.stdin.on('data'/'end')` and writes its
|
|
6
|
+
// result from the 'end' callback has its stdout DISCARDED by Claude Code. The hook
|
|
7
|
+
// runs, the payload arrives, the callback fires, the write succeeds, exit code is 0
|
|
8
|
+
// — and the output never reaches the model. Nothing reports an error, which is why
|
|
9
|
+
// five Rune hooks looked healthy while contributing nothing.
|
|
10
|
+
//
|
|
11
|
+
// Verified by differential test on Claude Code 2.1.220: an identical hook writing
|
|
12
|
+
// the identical envelope is seen by the model when stdin is read with
|
|
13
|
+
// `readFileSync(0)`, and unseen when read with an async listener. Switching the
|
|
14
|
+
// write to `fs.writeSync(1, …)` does not rescue the async form, so this is about
|
|
15
|
+
// when the process settles, not about flushing.
|
|
16
|
+
//
|
|
17
|
+
// So: read stdin synchronously, then do the work on the main tick.
|
|
18
|
+
|
|
19
|
+
const fs = require('node:fs');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Read the hook payload from stdin, synchronously.
|
|
23
|
+
*
|
|
24
|
+
* @returns {string} raw stdin, or '' when there is nothing to read
|
|
25
|
+
*/
|
|
26
|
+
function readStdinSync() {
|
|
27
|
+
try {
|
|
28
|
+
return fs.readFileSync(0, 'utf-8');
|
|
29
|
+
} catch {
|
|
30
|
+
// No stdin attached (manual invocation, or a runtime that omits the payload)
|
|
31
|
+
return '';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Read and parse the payload. Hook payloads are JSON, but a runtime invoking a
|
|
37
|
+
* hook by hand may pipe raw text — callers that want that fallback read `.raw`.
|
|
38
|
+
*
|
|
39
|
+
* @returns {{raw: string, data: object}}
|
|
40
|
+
*/
|
|
41
|
+
function readHookInput() {
|
|
42
|
+
const raw = readStdinSync();
|
|
43
|
+
let data = {};
|
|
44
|
+
try {
|
|
45
|
+
data = JSON.parse(raw);
|
|
46
|
+
} catch {
|
|
47
|
+
data = {};
|
|
48
|
+
}
|
|
49
|
+
return { raw, data };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = { readStdinSync, readHookInput };
|
|
@@ -10,6 +10,7 @@ const fs = require('fs');
|
|
|
10
10
|
const path = require('path');
|
|
11
11
|
const os = require('os');
|
|
12
12
|
const { resolveStateKey, stateFile } = require('../lib/context-key.cjs');
|
|
13
|
+
const { readStdinSync } = require('../lib/hook-stdin.cjs');
|
|
13
14
|
|
|
14
15
|
// metricsFile + watchFile are keyed by the Claude Code session_id (parsed from
|
|
15
16
|
// stdin in the handler) so they reset per session and match the session-keyed
|
|
@@ -17,9 +18,9 @@ const { resolveStateKey, stateFile } = require('../lib/context-key.cjs');
|
|
|
17
18
|
|
|
18
19
|
// Read stdin JSON to get tool input (Claude Code passes hook data via stdin)
|
|
19
20
|
let stdinData = '';
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
stdinData = readStdinSync();
|
|
22
|
+
|
|
23
|
+
(() => {
|
|
23
24
|
let skillName = 'unknown';
|
|
24
25
|
let claudeSessionId; // raw Claude Code session_id — used to key temp files
|
|
25
26
|
|
|
@@ -108,8 +109,4 @@ process.stdin.on('end', () => {
|
|
|
108
109
|
}
|
|
109
110
|
|
|
110
111
|
process.exit(0);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
// Handle empty stdin (pipe closed immediately)
|
|
114
|
-
process.stdin.on('error', () => process.exit(0));
|
|
115
|
-
process.stdin.resume();
|
|
112
|
+
})();
|
|
@@ -16,6 +16,7 @@ const fs = require('fs');
|
|
|
16
16
|
const path = require('path');
|
|
17
17
|
|
|
18
18
|
const { captureConsole } = require('../lib/hook-output.cjs');
|
|
19
|
+
const { readStdinSync } = require('../lib/hook-stdin.cjs');
|
|
19
20
|
// Hook stdout is a JSON contract, not free text (Codex rejects bare lines and
|
|
20
21
|
// discards the output). Capture the prints below and emit one envelope on exit.
|
|
21
22
|
captureConsole('PreToolUse');
|
|
@@ -76,11 +77,10 @@ function patchTargetPaths(toolName, toolInput) {
|
|
|
76
77
|
return Array.from(patch.matchAll(/^\*\*\* (?:Update|Add|Delete) File:\s*(.+)$/gm), (match) => match[1].trim());
|
|
77
78
|
}
|
|
78
79
|
|
|
79
|
-
// Read tool_input from
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
process.stdin.on('end', () => {
|
|
80
|
+
// Read tool_input from the hook payload. Must be synchronous — see hook-stdin.cjs.
|
|
81
|
+
const input = readStdinSync();
|
|
82
|
+
|
|
83
|
+
(() => {
|
|
84
84
|
let toolInput = {};
|
|
85
85
|
let toolName = '';
|
|
86
86
|
try {
|
|
@@ -179,7 +179,7 @@ process.stdin.on('end', () => {
|
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
process.exit(0);
|
|
182
|
-
});
|
|
182
|
+
})();
|
|
183
183
|
|
|
184
184
|
/**
|
|
185
185
|
* Load .rune/privacy.json from project root
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
const fs = require('fs');
|
|
23
23
|
const path = require('path');
|
|
24
24
|
const os = require('os');
|
|
25
|
+
const { readStdinSync } = require('../lib/hook-stdin.cjs');
|
|
25
26
|
|
|
26
27
|
const HOOK_TIMEOUT_MS = 5000;
|
|
27
28
|
|
|
@@ -49,28 +50,18 @@ if (process.env.QUARANTINE_DISABLE === '1') {
|
|
|
49
50
|
process.exit(0);
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
process.exit(0);
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
process.stdin.on('end', () => {
|
|
63
|
-
try {
|
|
64
|
-
main(stdinBuf);
|
|
65
|
-
} catch {
|
|
66
|
-
// Any unexpected error → silent advisory exit (never block tool dispatch)
|
|
67
|
-
process.exit(0);
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
process.stdin.on('error', () => process.exit(0));
|
|
53
|
+
// Read synchronously — see hook-stdin.cjs for why an async listener loses output.
|
|
54
|
+
const stdinBuf = readStdinSync();
|
|
55
|
+
|
|
56
|
+
// Cap stdin at 1MB — Claude Code never sends payloads this large
|
|
57
|
+
if (stdinBuf.length > 1_000_000) {
|
|
58
|
+
process.exit(0);
|
|
59
|
+
}
|
|
71
60
|
|
|
72
|
-
|
|
73
|
-
|
|
61
|
+
try {
|
|
62
|
+
main(stdinBuf);
|
|
63
|
+
} catch {
|
|
64
|
+
// Any unexpected error → silent advisory exit (never block tool dispatch)
|
|
74
65
|
process.exit(0);
|
|
75
66
|
}
|
|
76
67
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rune-kit/rune",
|
|
3
|
-
"version": "2.30.
|
|
3
|
+
"version": "2.30.1",
|
|
4
4
|
"description": "66-skill mesh for AI coding assistants — native lifecycle hooks for Claude Code and Codex, 5-layer architecture, 248 connections + 45 signals, and a 13-platform compiler.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"build": "node compiler/bin/rune.js build",
|
|
11
|
-
"doctor": "node compiler/bin/rune.js doctor && node scripts/version-sync-check.js",
|
|
11
|
+
"doctor": "node compiler/bin/rune.js doctor && node scripts/version-sync-check.js && node scripts/build-skill-index.js --check",
|
|
12
12
|
"test": "node --test compiler/__tests__/*.test.js scripts/__tests__/*.test.js",
|
|
13
13
|
"test:coverage": "c8 --reporter=text --reporter=lcov node --test compiler/__tests__/*.test.js scripts/__tests__/*.test.js",
|
|
14
14
|
"lint": "biome check .",
|
|
@@ -53,7 +53,8 @@
|
|
|
53
53
|
"agents/",
|
|
54
54
|
"hooks/",
|
|
55
55
|
".codex-plugin/",
|
|
56
|
-
"references/"
|
|
56
|
+
"references/",
|
|
57
|
+
"skill-index.json"
|
|
57
58
|
],
|
|
58
59
|
"homepage": "https://rune-kit.github.io/rune",
|
|
59
60
|
"bugs": {
|