@rune-kit/rune 2.29.1 → 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.
Files changed (58) hide show
  1. package/.codex-plugin/plugin.json +1 -1
  2. package/README.md +11 -3
  3. package/agents/audit.md +1 -1
  4. package/agents/cook.md +1 -1
  5. package/agents/journal.md +1 -1
  6. package/agents/reviewer.md +23 -1
  7. package/agents/session-bridge.md +1 -1
  8. package/agents/skill-forge.md +1 -1
  9. package/agents/skill-router.md +1 -1
  10. package/agents/trend-scout.md +1 -1
  11. package/agents/worktree.md +1 -1
  12. package/compiler/__tests__/skill-attribution.test.js +109 -0
  13. package/compiler/emitter.js +1 -1
  14. package/hooks/context-watch/index.cjs +5 -8
  15. package/hooks/hooks.json +1 -1
  16. package/hooks/intent-router/index.cjs +9 -6
  17. package/hooks/lib/hook-output.cjs +7 -1
  18. package/hooks/lib/hook-stdin.cjs +52 -0
  19. package/hooks/metrics-collector/index.cjs +5 -8
  20. package/hooks/pre-tool-guard/index.cjs +6 -6
  21. package/hooks/quarantine/index.cjs +12 -21
  22. package/package.json +4 -3
  23. package/skill-index.json +2187 -0
  24. package/skills/audit/SKILL.md +3 -3
  25. package/skills/completion-gate/SKILL.md +1 -1
  26. package/skills/constraint-check/SKILL.md +1 -1
  27. package/skills/context-engine/SKILL.md +2 -2
  28. package/skills/cook/SKILL.md +2 -2
  29. package/skills/cook/references/loop-detection.md +1 -1
  30. package/skills/cook/references/mid-run-signals.md +1 -1
  31. package/skills/debug/SKILL.md +2 -2
  32. package/skills/dependency-doctor/SKILL.md +1 -1
  33. package/skills/design/SKILL.md +1 -1
  34. package/skills/docs/SKILL.md +1 -1
  35. package/skills/docs-seeker/SKILL.md +3 -0
  36. package/skills/hallucination-guard/SKILL.md +2 -2
  37. package/skills/incident/SKILL.md +1 -1
  38. package/skills/integrity-check/SKILL.md +1 -1
  39. package/skills/launch/SKILL.md +1 -1
  40. package/skills/preflight/SKILL.md +35 -9
  41. package/skills/rescue/SKILL.md +1 -1
  42. package/skills/research/SKILL.md +1 -1
  43. package/skills/review/SKILL.md +135 -84
  44. package/skills/review/references/rules/config.md +63 -0
  45. package/skills/review/references/rules/default.md +57 -0
  46. package/skills/review/references/rules/go.md +65 -0
  47. package/skills/review/references/rules/index.md +43 -0
  48. package/skills/review/references/rules/python.md +64 -0
  49. package/skills/review/references/rules/rust.md +65 -0
  50. package/skills/review/references/rules/sql.md +65 -0
  51. package/skills/review/references/rules/ts-js.md +80 -0
  52. package/skills/sast/SKILL.md +1 -1
  53. package/skills/scout/SKILL.md +3 -0
  54. package/skills/sentinel/SKILL.md +29 -6
  55. package/skills/skill-router/SKILL.md +1 -1
  56. package/skills/team/SKILL.md +2 -2
  57. package/skills/verification/SKILL.md +3 -4
  58. package/skills/watchdog/SKILL.md +1 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rune",
3
- "version": "2.29.1",
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,19 @@ Plus **9 domain packs** (product, sales, data-science, support, growth, media, p
103
103
 
104
104
  ---
105
105
 
106
- ## What's New (v2.29.1 — One-Command Update)
106
+ ## What's New (v2.30.1 — Hooks That Land)
107
107
 
108
- > **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 configno 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,638 tests.
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
109
 
110
- ### Previous (v2.29.0 — Codex Native)
110
+ ### Previous (v2.30.0 — Tier Restored)
111
+
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.
113
+
114
+ #### Earlier (v2.29.1 — One-Command Update)
115
+
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.
117
+
118
+ #### Earlier (v2.29.0 — Codex Native)
111
119
 
112
120
  > **v2.29.0 (2026-07-23):** Codex stops being a compile target that happens to work and becomes a **first-class native runtime**. The Codex adapter now emits everything current Codex supports natively — `.agents/skills/`, project-scoped agent TOML (`.codex/agents/rune-{heavy,standard,fast}.toml`), a dedicated synchronous hook adapter targeting `.codex/hooks.json` (Codex silently skips async handlers), MCP config, and tier-aware compilation so Free/Pro/Business stacks resolve exactly as on Claude Code. The mesh validator was fixed at the root — `Calls (outbound)` is now the single authoritative edge inventory, acknowledged by the target's `Called By` — moving the canonical count from 209 to **248 connections** (same mesh, honest count; doctor now fails CI on any stale claim). Doctor also grew a cross-tier audit: Business metadata (28 pack skills, 4 orchestrators), $149 pricing, 13-platform count, and JSON-schema validation of the new `docs/config-schema.json` + hooks manifest schema. All XLabs remote-MCP references now go through `XLABS_MCP_TOKEN` — plaintext bearer values are forbidden. 1,615 tests pass.
113
121
 
package/agents/audit.md CHANGED
@@ -25,7 +25,7 @@ You are the **audit** skill — Rune's comprehensive project auditor.
25
25
 
26
26
  **Critical Rules:**
27
27
  - MUST complete all 8 phases (explicitly state if skipped)
28
- - Only findings >80% confidence (no speculation)
28
+ - Drop a finding only on direct counter-evidence, never on uncertainty — an unconfirmable finding is reported and typed `ASSUMED` with its unchecked premise named
29
29
  - Include at least 3 positive findings
30
30
  - Delegate Phase 1 to dependency-doctor, Phase 2 to sentinel
31
31
 
package/agents/cook.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: cook
3
3
  description: "Feature implementation orchestrator — handles 70% of requests. Full TDD cycle: understand → plan → test → implement → verify → commit. Use for ANY code modification (features, bugs, refactors, security)."
4
- model: opus
4
+ model: sonnet
5
5
  subagent_type: general-purpose
6
6
  ---
7
7
 
package/agents/journal.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: journal
3
3
  description: "Persistent state tracking across sessions — records decisions with rationale, progress, risks, ADRs to .rune/ files. Called by surgeon, deploy, rescue for cross-session continuity."
4
- model: sonnet
4
+ model: haiku
5
5
  subagent_type: general-purpose
6
6
  ---
7
7
 
@@ -9,7 +9,7 @@ You are the **reviewer** subagent — a code review specialist spawned by other
9
9
 
10
10
  ## Operating Rules
11
11
 
12
- 1. **Every finding MUST have file:line reference** — vague observations are rejected
12
+ 1. **Every finding MUST carry a verbatim evidence snippet** — vague observations are rejected, and so are line numbers produced from memory (see Evidence & Anchoring below)
13
13
  2. Check all 5 areas: correctness, security, performance, conventions, test coverage
14
14
  3. Severity levels: CRITICAL (blocks merge) → HIGH → MEDIUM → LOW
15
15
  4. Never rubber-stamp — if zero issues found, look harder (default-suspicious mindset)
@@ -17,4 +17,26 @@ You are the **reviewer** subagent — a code review specialist spawned by other
17
17
  6. Include at least 1 positive note (what's well-designed)
18
18
  7. Verdict: APPROVE / REQUEST CHANGES / NEEDS DISCUSSION
19
19
 
20
+ ## Falsification Pass (before reporting)
21
+
22
+ **Falsify, not verify.** Do not filter findings by how confident you feel — you cannot calibrate that. Filter by disproof:
23
+
24
+ - **DROP** only when the code you read holds **direct counter-evidence** against the finding's key claim. Discard it silently.
25
+ - **KEEP** when the finding rests on context outside the diff that you actually read — that context is evidence.
26
+ - **KEEP** when you can neither confirm nor disprove it. "Unsure" is not grounds to drop.
27
+
28
+ Type every surviving finding `OBSERVED` (you read the defect), `DERIVED` (follows from what you read, state the mechanism), or `ASSUMED` (rests on an unverified premise — name it). **`ASSUMED` findings are never CRITICAL.**
29
+
30
+ ## Evidence & Anchoring
31
+
32
+ Report the snippet, not the line number. A line number recalled from a long context drifts, and a correct finding pointing at the wrong line is unactionable.
33
+
34
+ - **Copy** the offending lines verbatim from the file — no rewriting, no re-indenting, diff markers (`+`/`-`) stripped, max 5 lines. A finding needing more than 5 lines to show is a design comment: report it without evidence at MEDIUM or below.
35
+ - **Anchor** each finding before reporting: `Grep` the exact snippet in its file → hit, use that line. Miss → retry once with whitespace normalised and the first and last lines dropped.
36
+ - **`UNANCHORED`** on a second miss: downgrade one severity level, report as `path (unanchored)` with the snippet shown inline. Never drop it — a failed `Grep` is not counter-evidence.
37
+
38
+ ## Strict Focus
39
+
40
+ Read anything you need for context. Report findings **only** about files in the stated review scope — an out-of-scope finding is dropped, or noted as a one-line follow-up in the report footer with no severity.
41
+
20
42
  You do NOT fix code. You identify issues with evidence. The parent skill decides next steps.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: session-bridge
3
3
  description: "Cross-session context persistence — auto-saves decisions, conventions, progress, instincts to .rune/ files. Loads + integrity-checks at session start. Auto-triggered before compaction."
4
- model: sonnet
4
+ model: haiku
5
5
  subagent_type: general-purpose
6
6
  ---
7
7
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: skill-forge
3
3
  description: "The skill that builds skills — TDD-driven: baseline test → write skill → verify → refactor → integrate into mesh. Use for creating or editing Rune skills."
4
- model: sonnet
4
+ model: opus
5
5
  subagent_type: general-purpose
6
6
  ---
7
7
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: skill-router
3
3
  description: "Meta-enforcement layer — routes EVERY agent action through the correct skill before any code is written. Always active. Prevents 'just this once' bypasses."
4
- model: opus
4
+ model: sonnet
5
5
  subagent_type: general-purpose
6
6
  ---
7
7
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: trend-scout
3
3
  description: "Market intelligence — competitor analysis, technology trends, community sentiment. Scans Product Hunt, GitHub Trending, HN, Reddit. Use for positioning, not API docs."
4
- model: sonnet
4
+ model: haiku
5
5
  subagent_type: general-purpose
6
6
  ---
7
7
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: worktree
3
3
  description: "Git worktree lifecycle — create isolated workspaces for parallel development, manage branches, cleanup. Max 3 active worktrees. Called by team for parallel streams."
4
- model: sonnet
4
+ model: haiku
5
5
  subagent_type: general-purpose
6
6
  ---
7
7
 
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Attribution hygiene — skills and agents must not carry graft-source credits.
3
+ *
4
+ * Rune skills present their guidance as Rune's own. A `> From <repo> (N★): "…"`
5
+ * blockquote leaks where a pattern was grafted from, dates the file with a star
6
+ * count, and reads as borrowed authority. The prose stays; the credit line goes.
7
+ *
8
+ * Guards the patterns removed in a2df7b5 so they cannot drift back in.
9
+ *
10
+ * Legitimate credits are NOT attribution: a license obligation for redistributed
11
+ * data (skills/design), a descriptive "Source patterns:" lead-in, and a filename
12
+ * cited inside a config example all stay. Each is asserted explicitly below so a
13
+ * future tightening of the rules cannot silently outlaw them.
14
+ */
15
+
16
+ import assert from 'node:assert';
17
+ import { readdirSync, readFileSync, statSync } from 'node:fs';
18
+ import { dirname, join, relative, resolve } from 'node:path';
19
+ import { describe, test } from 'node:test';
20
+ import { fileURLToPath } from 'node:url';
21
+
22
+ const __dirname = dirname(fileURLToPath(import.meta.url));
23
+ const REPO_ROOT = resolve(__dirname, '../..');
24
+ const SCAN_DIRS = ['skills', 'agents'];
25
+
26
+ /**
27
+ * Each rule matches a single line. Keep them shape-based: a rule that keys on a
28
+ * repo name rots the moment a different repo is grafted.
29
+ */
30
+ const FORBIDDEN = [
31
+ { name: 'blockquote "> From <repo>"', re: /^\s*>\s*From\s+\S/ },
32
+ { name: 'blockquote "> Inspired by"', re: /^\s*>\s*Inspired by\s/ },
33
+ { name: 'blockquote "> Source:"', re: /^\s*>\s*Source:/ },
34
+ { name: 'bare "Source: … ★"', re: /^\s*Source:\s.*★/ },
35
+ { name: 'blockquote carrying a star count', re: /^\s*>\s.*★/ },
36
+ ];
37
+
38
+ /** Lines that look adjacent to the rules above and must keep passing. */
39
+ const MUST_PASS = [
40
+ 'Design intelligence data from [UI/UX Pro Max](https://github.com/nextlevelbuilder/ui-ux-pro-max-skill) (MIT, 42.8k★).',
41
+ '> Source patterns: production-proven PostgreSQL scaling strategies.',
42
+ 'Source: compliance-soc2.yaml',
43
+ ];
44
+
45
+ function walkMarkdown(dir) {
46
+ const out = [];
47
+ for (const entry of readdirSync(dir)) {
48
+ const full = join(dir, entry);
49
+ if (statSync(full).isDirectory()) {
50
+ out.push(...walkMarkdown(full));
51
+ } else if (entry.endsWith('.md')) {
52
+ out.push(full);
53
+ }
54
+ }
55
+ return out;
56
+ }
57
+
58
+ function findViolations(text, file) {
59
+ const hits = [];
60
+ text.split(/\r?\n/).forEach((line, i) => {
61
+ for (const rule of FORBIDDEN) {
62
+ if (rule.re.test(line)) {
63
+ hits.push(`${file}:${i + 1} — ${rule.name} → ${line.trim()}`);
64
+ }
65
+ }
66
+ });
67
+ return hits;
68
+ }
69
+
70
+ describe('attribution hygiene', () => {
71
+ test('no source-attribution lines in skills/ or agents/', () => {
72
+ const violations = [];
73
+ for (const dirName of SCAN_DIRS) {
74
+ const dir = join(REPO_ROOT, dirName);
75
+ for (const file of walkMarkdown(dir)) {
76
+ violations.push(...findViolations(readFileSync(file, 'utf8'), relative(REPO_ROOT, file)));
77
+ }
78
+ }
79
+
80
+ assert.deepStrictEqual(
81
+ violations,
82
+ [],
83
+ `Attribution lines found — delete the credit, keep the prose:\n${violations.join('\n')}`,
84
+ );
85
+ });
86
+
87
+ test('rules fire on the exact lines removed in a2df7b5', () => {
88
+ const removed = [
89
+ '> From goclaw (nextlevelbuilder/goclaw, 832★): "Compact during run, not just at session boundary."',
90
+ '> From superpowers (obra/superpowers, 84k★): "Each fix revealing new problems elsewhere = structural issue."',
91
+ '> Inspired by CLI-Anything (HKUDS/CLI-Anything, 14.5k★): "Never trust exit 0."',
92
+ 'Source: goclaw (832★) — SHA256-based loop detection distinguishes true stuck loops.',
93
+ ];
94
+
95
+ for (const line of removed) {
96
+ assert.ok(
97
+ FORBIDDEN.some((rule) => rule.re.test(line)),
98
+ `no rule caught a known attribution line: ${line}`,
99
+ );
100
+ }
101
+ });
102
+
103
+ test('legitimate credits and lookalikes are not flagged', () => {
104
+ for (const line of MUST_PASS) {
105
+ const caught = FORBIDDEN.filter((rule) => rule.re.test(line)).map((rule) => rule.name);
106
+ assert.deepStrictEqual(caught, [], `false positive on a legitimate line: ${line}`);
107
+ }
108
+ });
109
+ });
@@ -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
- process.stdin.setEncoding('utf-8');
35
- process.stdin.on('data', chunk => { stdinData += chunk; });
36
- process.stdin.on('end', () => {
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
@@ -19,7 +19,7 @@
19
19
  {
20
20
  "type": "command",
21
21
  "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cjs\" intent-router",
22
- "async": true
22
+ "async": false
23
23
  }
24
24
  ]
25
25
  }
@@ -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 Claude Code hook stdin
17
- let input = '';
18
- process.stdin.setEncoding('utf-8');
19
- process.stdin.on('data', (chunk) => { input += chunk; });
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(`${JSON.stringify(payload)}\n`);
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
- process.stdin.setEncoding('utf-8');
21
- process.stdin.on('data', chunk => { stdinData += chunk; });
22
- process.stdin.on('end', () => {
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 Claude Code hook stdin
80
- let input = '';
81
- process.stdin.setEncoding('utf-8');
82
- process.stdin.on('data', (chunk) => { input += chunk; });
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
- let stdinBuf = '';
53
- process.stdin.setEncoding('utf-8');
54
- process.stdin.on('data', (chunk) => {
55
- stdinBuf += chunk;
56
- // Cap stdin at 1MB — Claude Code never sends payloads this large
57
- if (stdinBuf.length > 1_000_000) {
58
- process.stdin.destroy();
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
- // If stdin is a TTY (no input), exit clean
73
- if (process.stdin.isTTY) {
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.29.1",
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": {