@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.
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +11 -3
- package/agents/audit.md +1 -1
- package/agents/cook.md +1 -1
- package/agents/journal.md +1 -1
- package/agents/reviewer.md +23 -1
- package/agents/session-bridge.md +1 -1
- package/agents/skill-forge.md +1 -1
- package/agents/skill-router.md +1 -1
- package/agents/trend-scout.md +1 -1
- package/agents/worktree.md +1 -1
- package/compiler/__tests__/skill-attribution.test.js +109 -0
- 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
- package/skills/audit/SKILL.md +3 -3
- package/skills/completion-gate/SKILL.md +1 -1
- package/skills/constraint-check/SKILL.md +1 -1
- package/skills/context-engine/SKILL.md +2 -2
- package/skills/cook/SKILL.md +2 -2
- package/skills/cook/references/loop-detection.md +1 -1
- package/skills/cook/references/mid-run-signals.md +1 -1
- package/skills/debug/SKILL.md +2 -2
- package/skills/dependency-doctor/SKILL.md +1 -1
- package/skills/design/SKILL.md +1 -1
- package/skills/docs/SKILL.md +1 -1
- package/skills/docs-seeker/SKILL.md +3 -0
- package/skills/hallucination-guard/SKILL.md +2 -2
- package/skills/incident/SKILL.md +1 -1
- package/skills/integrity-check/SKILL.md +1 -1
- package/skills/launch/SKILL.md +1 -1
- package/skills/preflight/SKILL.md +35 -9
- package/skills/rescue/SKILL.md +1 -1
- package/skills/research/SKILL.md +1 -1
- package/skills/review/SKILL.md +135 -84
- package/skills/review/references/rules/config.md +63 -0
- package/skills/review/references/rules/default.md +57 -0
- package/skills/review/references/rules/go.md +65 -0
- package/skills/review/references/rules/index.md +43 -0
- package/skills/review/references/rules/python.md +64 -0
- package/skills/review/references/rules/rust.md +65 -0
- package/skills/review/references/rules/sql.md +65 -0
- package/skills/review/references/rules/ts-js.md +80 -0
- package/skills/sast/SKILL.md +1 -1
- package/skills/scout/SKILL.md +3 -0
- package/skills/sentinel/SKILL.md +29 -6
- package/skills/skill-router/SKILL.md +1 -1
- package/skills/team/SKILL.md +2 -2
- package/skills/verification/SKILL.md +3 -4
- package/skills/watchdog/SKILL.md +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rune",
|
|
3
|
-
"version": "2.
|
|
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.
|
|
106
|
+
## What's New (v2.30.1 — Hooks That Land)
|
|
107
107
|
|
|
108
|
-
> **v2.
|
|
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.
|
|
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
|
-
-
|
|
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:
|
|
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:
|
|
4
|
+
model: haiku
|
|
5
5
|
subagent_type: general-purpose
|
|
6
6
|
---
|
|
7
7
|
|
package/agents/reviewer.md
CHANGED
|
@@ -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
|
|
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.
|
package/agents/session-bridge.md
CHANGED
|
@@ -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:
|
|
4
|
+
model: haiku
|
|
5
5
|
subagent_type: general-purpose
|
|
6
6
|
---
|
|
7
7
|
|
package/agents/skill-forge.md
CHANGED
|
@@ -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:
|
|
4
|
+
model: opus
|
|
5
5
|
subagent_type: general-purpose
|
|
6
6
|
---
|
|
7
7
|
|
package/agents/skill-router.md
CHANGED
|
@@ -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:
|
|
4
|
+
model: sonnet
|
|
5
5
|
subagent_type: general-purpose
|
|
6
6
|
---
|
|
7
7
|
|
package/agents/trend-scout.md
CHANGED
|
@@ -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:
|
|
4
|
+
model: haiku
|
|
5
5
|
subagent_type: general-purpose
|
|
6
6
|
---
|
|
7
7
|
|
package/agents/worktree.md
CHANGED
|
@@ -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:
|
|
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
|
+
});
|
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.
|
|
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": {
|