@rune-kit/rune 2.30.1 → 2.30.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rune",
3
- "version": "2.30.1",
3
+ "version": "2.30.2",
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.1Hooks That Land)
106
+ ## What's New (v2.30.2Blocked, With a Reason)
107
+
108
+ > **v2.30.2 (2026-07-29):** Finishes the hook I/O sweep, plus the fault that sweep uncovered. Claude Code shows the model **only stderr** for an exit-2 block, and `pre-tool-guard` explained itself on stdout — so a blocked read surfaced as `hook error: […]: No stderr output`. The block worked; the model just never learned why, and could only guess whether to stop or route around it. New `emitBlock()` writes the reason to stderr (Claude Code) and the envelope to stdout (Codex), then exits 2 — verified end-to-end by the model quoting the reason back. Three hooks v2.30.1 missed are also fixed: `quarantine` hand-built its envelope and shipped it with `process.stdout.write` right before `process.exit(0)`, while `auto-format` and `typecheck` printed bare `console.log` with no envelope at all (unflushed on Claude Code, rejected outright by Codex). Two new repo-wide gate tests — no hook writes stdout directly, no hook reads stdin asynchronously — so a new hook cannot reintroduce either. 1,651 tests.
109
+
110
+ ### Previous (v2.30.1 — Hooks That Land)
107
111
 
108
112
  > **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
113
 
110
- ### Previous (v2.30.0 — Tier Restored)
114
+ #### Earlier (v2.30.0 — Tier Restored)
111
115
 
112
116
  > **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.
113
117
 
@@ -5,8 +5,11 @@
5
5
  // Silent pass-through if not applicable.
6
6
 
7
7
  const { execSync } = require('child_process');
8
+ const { captureConsole } = require('../lib/hook-output.cjs');
8
9
  const path = require('path');
9
10
 
11
+ captureConsole('PostToolUse');
12
+
10
13
  const input = JSON.parse(process.env.CLAUDE_TOOL_INPUT || '{}');
11
14
  const filePath = input.file_path || input.filePath || '';
12
15
 
@@ -113,4 +113,25 @@ function captureConsole(hookEventName, options = {}) {
113
113
  return { buffer, restore };
114
114
  }
115
115
 
116
- module.exports = { outputBuffer, emit, captureConsole, CONTEXT_EVENTS };
116
+ /**
117
+ * Emit a BLOCK reason, then exit 2.
118
+ *
119
+ * A blocking hook has to speak on two channels, because the runtimes read
120
+ * different ones. Claude Code takes the reason for an exit-2 block from STDERR —
121
+ * a hook that explains itself on stdout gets `hook error: […]: No stderr output`
122
+ * shown to the model, which learns that it was blocked but never why. Codex reads
123
+ * the stdout envelope. Write both; neither runtime is confused by the other's.
124
+ *
125
+ * @param {string} hookEventName
126
+ * @param {string} text human-readable reason
127
+ */
128
+ function emitBlock(hookEventName, text) {
129
+ const reason = text.trim();
130
+ if (!reason) process.exit(2);
131
+
132
+ fs.writeSync(2, `${reason}\n`);
133
+ fs.writeSync(1, `${JSON.stringify({ systemMessage: reason, hookSpecificOutput: { hookEventName } })}\n`);
134
+ process.exit(2);
135
+ }
136
+
137
+ module.exports = { outputBuffer, emit, emitBlock, captureConsole, CONTEXT_EVENTS };
@@ -15,7 +15,7 @@
15
15
  const fs = require('fs');
16
16
  const path = require('path');
17
17
 
18
- const { captureConsole } = require('../lib/hook-output.cjs');
18
+ const { captureConsole, emitBlock } = require('../lib/hook-output.cjs');
19
19
  const { readStdinSync } = require('../lib/hook-stdin.cjs');
20
20
  // Hook stdout is a JSON contract, not free text (Codex rejects bare lines and
21
21
  // discards the output). Capture the prints below and emit one envelope on exit.
@@ -158,10 +158,16 @@ const input = readStdinSync();
158
158
  // at this layer and intentionally remain uncaptured (see GAP-1 in governance-collector.js).
159
159
  appendGateOutcome('privacy-mesh', 'blocked', `file matched BLOCK-tier pattern: ${basename}`);
160
160
 
161
- console.log(`\n🚫 [Rune privacy-mesh] BLOCKED: ${filePath}`);
162
- console.log(' This file matches a BLOCK-tier pattern (private keys, certificates).');
163
- console.log(' Override: add path to .rune/privacy.json "allow" list if intentional.\n');
164
- process.exit(2); // Exit code 2 = BLOCK
161
+ // Exit code 2 = BLOCK. The reason must go to stderr — that is the only
162
+ // channel Claude Code shows the model for a blocked call.
163
+ emitBlock(
164
+ 'PreToolUse',
165
+ [
166
+ `🚫 [Rune privacy-mesh] BLOCKED: ${filePath}`,
167
+ ' This file matches a BLOCK-tier pattern (private keys, certificates).',
168
+ ' Override: add path to .rune/privacy.json "allow" list if intentional.',
169
+ ].join('\n'),
170
+ );
165
171
  }
166
172
 
167
173
  const isWarned = warnPatterns.some((p) => p.test(basename) || p.test(normalized));
@@ -96,7 +96,9 @@ function main(raw) {
96
96
  additionalContext: notice,
97
97
  },
98
98
  };
99
- process.stdout.write(`${JSON.stringify(output)}\n`);
99
+ // fs.writeSync, not process.stdout.write — process.exit() follows immediately
100
+ // below, and a piped stdout is not guaranteed to flush before the process ends.
101
+ fs.writeSync(1, `${JSON.stringify(output)}\n`);
100
102
 
101
103
  writeTelemetry({ tool: toolName, decision: 'emit', source: decision.source, session_id: sessionId });
102
104
  clearTimeout(timeoutHandle);
@@ -5,9 +5,12 @@
5
5
  // Async hook — doesn't block the agent, just warns.
6
6
 
7
7
  const { execSync } = require('child_process');
8
+ const { captureConsole } = require('../lib/hook-output.cjs');
8
9
  const fs = require('fs');
9
10
  const path = require('path');
10
11
 
12
+ captureConsole('PostToolUse');
13
+
11
14
  const input = JSON.parse(process.env.CLAUDE_TOOL_INPUT || '{}');
12
15
  const filePath = input.file_path || input.filePath || '';
13
16
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rune-kit/rune",
3
- "version": "2.30.1",
3
+ "version": "2.30.2",
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": {