@rune-kit/rune 2.16.1 → 2.17.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/README.md +63 -15
- package/compiler/__tests__/doctor-mesh.test.js +5 -0
- package/compiler/__tests__/hooks-drift.test.js +156 -0
- package/compiler/__tests__/hooks-merge.test.js +2 -1
- package/compiler/__tests__/setup.test.js +152 -0
- package/compiler/adapters/openclaw.js +1 -1
- package/compiler/bin/rune.js +63 -4
- package/compiler/commands/hooks/drift.js +167 -0
- package/compiler/commands/hooks/presets.js +11 -1
- package/compiler/commands/setup.js +240 -0
- package/compiler/doctor.js +48 -2
- package/compiler/transforms/branding.js +1 -1
- package/compiler/transforms/hooks.js +6 -0
- package/hooks/hooks.json +10 -0
- package/hooks/quarantine/index.cjs +256 -0
- package/package.json +2 -2
- package/skills/integrity-check/SKILL.md +2 -0
- package/skills/quarantine/SKILL.md +173 -0
- package/skills/quarantine/references/quarantine-discipline.md +97 -0
- package/skills/quarantine/references/trusted-mcp-allowlist.md +77 -0
- package/skills/sentinel/SKILL.md +2 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<p align="center">
|
|
6
6
|
<strong>Less skills. Deeper connections.</strong><br>
|
|
7
7
|
A lean, interconnected skill ecosystem for AI coding assistants.<br>
|
|
8
|
-
|
|
8
|
+
64 skills · 203 connections · 40 signals · 8 platforms · MIT
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
Most skill ecosystems are either **too many isolated skills** (540+ that don't talk to each other) or **rigid pipelines** (A → B → C, if B fails everything stops).
|
|
25
25
|
|
|
26
|
-
Rune is a **mesh** —
|
|
26
|
+
Rune is a **mesh** — 64 skills with 203 connections + 40 signals across a 5-layer architecture. Skills call each other bidirectionally, forming resilient workflows that adapt when things go wrong.
|
|
27
27
|
|
|
28
28
|
```
|
|
29
29
|
Pipeline: A → B → C → D (B fails = stuck)
|
|
@@ -83,7 +83,15 @@ _Methodology: Claude Code CLI headless mode (`claude -p --output-format json`),
|
|
|
83
83
|
|
|
84
84
|
---
|
|
85
85
|
|
|
86
|
-
## What's New (v2.
|
|
86
|
+
## What's New (v2.17.1 — One-Command Setup Wizard)
|
|
87
|
+
|
|
88
|
+
> **v2.17.1 (2026-05-06):** New `rune setup` interactive wizard collapses the multi-step `cd <project> && export RUNE_PRO_ROOT && rune hooks install --preset gentle --tier pro` workflow into one command — auto-detects Pro/Business tiers across env var / sibling / well-known paths, asks for scope (current project / global) + preset, installs hooks. New `--global` flag on `rune hooks install` writes to `~/.claude/settings.json` (every Claude Code session, regardless of project). Non-interactive mode via `--here` / `--global` / `--tier` / `--preset` / `--dry` flags. Anti-paywall — wizard ships in Free, NOT Pro/Business (tier-agnostic infrastructure UX). Doc sweep: README "One-Command Setup", HOOKS.md restructure, agent skill-routing row for "set up rune". CI 1376/1376.
|
|
89
|
+
|
|
90
|
+
### Previous (v2.17.0 — Quarantine + Hook Drift Reporter)
|
|
91
|
+
|
|
92
|
+
> **v2.17.0 (2026-05-06):** New L3 skill `quarantine` ships a PostToolUse advisory hook for untrusted external content (MCP user-content, WebFetch, upload Reads). Honest scope: hook lands `[QUARANTINE-NOTICE]` in next-turn `additionalContext`, biasing the model to treat prior external content as data — NOT structural defense. Layered against `permissions.deny` (egress) + `integrity-check` (state). Default trusted-MCP allowlist (linear / github / jira / atlassian / Drive / neural-memory) skips advisory; operator extends at `~/.claude/quarantine.d/trusted-mcp-allowlist.txt`. Per-session disable via `QUARANTINE_DISABLE=1`. Wired into `rune hooks install --preset gentle|strict`. New `rune doctor --hooks` drift reporter. CI 1367/1367.
|
|
93
|
+
|
|
94
|
+
### Previous (v2.16.1 — Skill Enrichment + Triage Workflow + Output Modes)
|
|
87
95
|
|
|
88
96
|
> **v2.16.1 patch (2026-05-02):** `ba` v0.13.0 → v1.0.0 first stable major (no functional changes — maturity stamp). Doc/signal hygiene: CLAUDE.md "Current Wave" synced; 4 terminal-observability signals whitelisted in `validate-signals.js`. CI 1349/1349.
|
|
89
97
|
|
|
@@ -193,7 +201,7 @@ cook ───emit:phase.complete────→ session-bridge
|
|
|
193
201
|
|
|
194
202
|
## What Rune Is (and Isn't)
|
|
195
203
|
|
|
196
|
-
Rune started as a **Claude Code plugin** and now compiles to **every major AI IDE**. Same
|
|
204
|
+
Rune started as a **Claude Code plugin** and now compiles to **every major AI IDE**. Same 64 skills, same mesh connections, same workflows — zero knowledge loss across platforms.
|
|
197
205
|
|
|
198
206
|
| | Rune Provides | Claude Code Provides |
|
|
199
207
|
|---|---|---|
|
|
@@ -201,7 +209,7 @@ Rune started as a **Claude Code plugin** and now compiles to **every major AI ID
|
|
|
201
209
|
| **Quality Gates** | preflight + sentinel + review + completion-gate (parallel) | None built-in |
|
|
202
210
|
| **Domain Knowledge** | 14 extension packs (trading, SaaS, mobile, etc.) | General-purpose |
|
|
203
211
|
| **Cross-Session State** | .rune/ directory (decisions, conventions, progress) | Conversation only |
|
|
204
|
-
| **Mesh Resilience** |
|
|
212
|
+
| **Mesh Resilience** | 203 skill connections + 40 mesh signals, fail-loud-route-around | Linear execution |
|
|
205
213
|
| **Cost Optimization** | Auto model selection (haiku/sonnet/opus per task) | Single model |
|
|
206
214
|
| | | |
|
|
207
215
|
| **Sandbox & Permissions** | — | Claude Code handles this |
|
|
@@ -222,6 +230,46 @@ Rune started as a **Claude Code plugin** and now compiles to **every major AI ID
|
|
|
222
230
|
|
|
223
231
|
## Install
|
|
224
232
|
|
|
233
|
+
### One-Command Setup (recommended)
|
|
234
|
+
|
|
235
|
+
After installing the plugin, run the wizard once to wire hooks the way you want them — pick scope, pick tiers, done:
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
npx @rune-kit/rune setup
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
The wizard auto-detects what you have:
|
|
242
|
+
|
|
243
|
+
```
|
|
244
|
+
Rune Setup Wizard
|
|
245
|
+
──────────────────
|
|
246
|
+
Free version: 2.17.1 (cached)
|
|
247
|
+
Pro detected: sibling (../Pro) (v1.1.0)
|
|
248
|
+
Business: not detected
|
|
249
|
+
|
|
250
|
+
Where to install hooks?
|
|
251
|
+
[c] Current project — D:/MyProject/.claude/settings.json
|
|
252
|
+
[g] Global — ~/.claude/settings.json
|
|
253
|
+
(every Claude Code session, regardless of project)
|
|
254
|
+
|
|
255
|
+
Scope [c/g] (default c): g
|
|
256
|
+
Install Pro tier? [Y/n]: y
|
|
257
|
+
Preset [g/s] (default g): g
|
|
258
|
+
|
|
259
|
+
✓ Wired 5 hooks to ~/.claude/settings.json
|
|
260
|
+
Verify: rune doctor --hooks
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
**What does the wizard do?** It writes Rune-managed entries to `.claude/settings.json` (project-local OR global) so Claude Code auto-fires `preflight`, `sentinel`, `dependency-doctor`, `completion-gate`, and `quarantine` at the right moments. With `--tier pro`, it also wires `loop-circuit-breaker` (auto-engages only in autopilot sessions).
|
|
264
|
+
|
|
265
|
+
**Non-interactive mode** (CI / scripted):
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
npx @rune-kit/rune setup --here --preset gentle --tier pro
|
|
269
|
+
npx @rune-kit/rune setup --global --preset strict --tier pro,business
|
|
270
|
+
npx @rune-kit/rune setup --here --no-tier --dry # preview without writing
|
|
271
|
+
```
|
|
272
|
+
|
|
225
273
|
### Claude Code (Native Plugin)
|
|
226
274
|
|
|
227
275
|
```bash
|
|
@@ -231,7 +279,7 @@ claude plugin add rune-kit/rune
|
|
|
231
279
|
|
|
232
280
|
Or add manually in `~/.claude/settings.json` under `installed_plugins`.
|
|
233
281
|
|
|
234
|
-
Full mesh: subagents, hooks, adaptive routing, mesh analytics.
|
|
282
|
+
Full mesh: subagents, hooks, adaptive routing, mesh analytics. **Run `npx @rune-kit/rune setup` afterward to wire hooks** (see One-Command Setup above).
|
|
235
283
|
|
|
236
284
|
### Cursor / Windsurf / Antigravity / Any IDE
|
|
237
285
|
|
|
@@ -245,14 +293,14 @@ npx @rune-kit/rune init --platform windsurf
|
|
|
245
293
|
npx @rune-kit/rune init --platform antigravity
|
|
246
294
|
```
|
|
247
295
|
|
|
248
|
-
This compiles all
|
|
296
|
+
This compiles all 64 skills into your IDE's rules format. Same knowledge, same workflows.
|
|
249
297
|
|
|
250
298
|
### Platform Comparison
|
|
251
299
|
|
|
252
300
|
| Feature | Claude Code | Cursor / Windsurf / Others |
|
|
253
301
|
|---------|-------------|---------------------------|
|
|
254
|
-
| Skills available |
|
|
255
|
-
| Mesh connections |
|
|
302
|
+
| Skills available | 64/64 | 64/64 |
|
|
303
|
+
| Mesh connections | 203 sync + 40 signals (programmatic) | 203 sync + 40 signals (rule references) |
|
|
256
304
|
| Workflows & HARD-GATEs | Full | Full |
|
|
257
305
|
| Extension packs | 14 | 14 |
|
|
258
306
|
| Subagent parallelism | Native | Sequential fallback |
|
|
@@ -596,7 +644,7 @@ See [docs/MULTI-PLATFORM.md](docs/MULTI-PLATFORM.md) for the full architecture.
|
|
|
596
644
|
| Doc | What's inside |
|
|
597
645
|
|-----|---------------|
|
|
598
646
|
| [`docs/GETTING_STARTED.md`](docs/GETTING_STARTED.md) | Your first 5 minutes with Rune — install to first `/rune cook` |
|
|
599
|
-
| [`docs/SKILLS.md`](docs/SKILLS.md) | All
|
|
647
|
+
| [`docs/SKILLS.md`](docs/SKILLS.md) | All 64 skills, searchable by intent and layer |
|
|
600
648
|
| [`docs/SIGNALS.md`](docs/SIGNALS.md) | Canonical signal inventory — 25 events, emit/listen graph |
|
|
601
649
|
| [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) | 5-layer mesh architecture reference |
|
|
602
650
|
| [`docs/VISION.md`](docs/VISION.md) | Philosophy — what Rune is and isn't |
|
|
@@ -609,11 +657,11 @@ See [docs/MULTI-PLATFORM.md](docs/MULTI-PLATFORM.md) for the full architecture.
|
|
|
609
657
|
## Numbers
|
|
610
658
|
|
|
611
659
|
```
|
|
612
|
-
Core Skills:
|
|
613
|
-
Extension Packs: 14 free +
|
|
614
|
-
Mesh Connections:
|
|
615
|
-
Mesh Signals:
|
|
616
|
-
Connections/Skill: 3.
|
|
660
|
+
Core Skills: 64 (L0: 1 │ L1: 5 │ L2: 30 │ L3: 28)
|
|
661
|
+
Extension Packs: 14 free + 6 pro + 4 business
|
|
662
|
+
Mesh Connections: 203 sync calls (rune doctor)
|
|
663
|
+
Mesh Signals: 40 signals · 50 emit/listen edges (rune doctor)
|
|
664
|
+
Connections/Skill: 3.2 avg
|
|
617
665
|
Platforms: 8 (Claude Code, Cursor, Windsurf, Antigravity, Codex, OpenCode, OpenClaw, Generic)
|
|
618
666
|
Compiler: ~1400 LOC (parser + 8 transforms + 8 adapters + CLI)
|
|
619
667
|
Tests: 1,152+ (compiler + signals + status + visualizer + hooks + scripts + tier-hooks)
|
|
@@ -223,6 +223,11 @@ describe('integration: real skills', () => {
|
|
|
223
223
|
// Should find connections
|
|
224
224
|
assert.ok(results.stats.connections > 100, `Should find 100+ connections, got ${results.stats.connections}`);
|
|
225
225
|
|
|
226
|
+
// Should find signals (emit/listen mesh — separate from sync calls)
|
|
227
|
+
// Free core scan only — Pro/Business packs scanned separately by validate-signals.js
|
|
228
|
+
assert.ok(results.stats.signals > 20, `Should find 20+ signals, got ${results.stats.signals}`);
|
|
229
|
+
assert.ok(results.stats.signalEdges > 0, `Should find signal edges, got ${results.stats.signalEdges}`);
|
|
230
|
+
|
|
226
231
|
// Should not have errors
|
|
227
232
|
assert.strictEqual(results.errors.length, 0, `Should have no errors: ${results.errors.join(', ')}`);
|
|
228
233
|
});
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { afterEach, beforeEach, describe, test } from 'node:test';
|
|
6
|
+
import { checkHookDrift, formatHookDriftResult } from '../commands/hooks/drift.js';
|
|
7
|
+
import { buildPreset, SETTINGS_REL_PATH } from '../commands/hooks/presets.js';
|
|
8
|
+
|
|
9
|
+
let tmpRoot;
|
|
10
|
+
|
|
11
|
+
async function seedClaude(root, settings) {
|
|
12
|
+
await mkdir(path.join(root, '.claude'), { recursive: true });
|
|
13
|
+
if (settings) {
|
|
14
|
+
await writeFile(path.join(root, SETTINGS_REL_PATH), `${JSON.stringify(settings, null, 2)}\n`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
beforeEach(async () => {
|
|
19
|
+
tmpRoot = await mkdtemp(path.join(tmpdir(), 'rune-drift-'));
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(async () => {
|
|
23
|
+
if (tmpRoot) await rm(tmpRoot, { recursive: true, force: true });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('checkHookDrift', () => {
|
|
27
|
+
test('returns empty findings when settings.json matches canonical gentle preset', async () => {
|
|
28
|
+
await seedClaude(tmpRoot, buildPreset('gentle'));
|
|
29
|
+
const result = await checkHookDrift(tmpRoot);
|
|
30
|
+
assert.strictEqual(result.findings.length, 0);
|
|
31
|
+
assert.strictEqual(result.summary.drifted, 0);
|
|
32
|
+
assert.strictEqual(result.summary.missing, 0);
|
|
33
|
+
assert.strictEqual(result.platforms.length, 1);
|
|
34
|
+
assert.strictEqual(result.platforms[0].preset, 'gentle');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('returns empty findings for canonical strict preset', async () => {
|
|
38
|
+
await seedClaude(tmpRoot, buildPreset('strict'));
|
|
39
|
+
const result = await checkHookDrift(tmpRoot);
|
|
40
|
+
assert.strictEqual(result.findings.length, 0);
|
|
41
|
+
assert.strictEqual(result.platforms[0].preset, 'strict');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('flags drifted entry when installed command differs from canonical', async () => {
|
|
45
|
+
const baseline = buildPreset('gentle');
|
|
46
|
+
// Mutate one command to simulate operator hand-edit
|
|
47
|
+
baseline.hooks.PreToolUse[0].hooks[0].command = 'npx --yes @rune-kit/rune hook-dispatch preflight --custom-flag';
|
|
48
|
+
await seedClaude(tmpRoot, baseline);
|
|
49
|
+
const result = await checkHookDrift(tmpRoot);
|
|
50
|
+
const drift = result.findings.filter((f) => f.status === 'drift');
|
|
51
|
+
const missing = result.findings.filter((f) => f.status === 'missing');
|
|
52
|
+
assert.ok(drift.length >= 1, 'should flag the modified entry as drifted');
|
|
53
|
+
assert.ok(missing.length >= 1, 'canonical entry no longer present → missing');
|
|
54
|
+
assert.match(drift[0].actual, /--custom-flag/);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('flags missing entry when canonical has more than installed', async () => {
|
|
58
|
+
const partial = buildPreset('gentle');
|
|
59
|
+
// Remove one Stop entry to simulate partial install
|
|
60
|
+
partial.hooks.Stop = [];
|
|
61
|
+
await seedClaude(tmpRoot, partial);
|
|
62
|
+
const result = await checkHookDrift(tmpRoot);
|
|
63
|
+
const missing = result.findings.filter((f) => f.status === 'missing' && f.event === 'Stop');
|
|
64
|
+
assert.strictEqual(missing.length, 1);
|
|
65
|
+
assert.match(missing[0].expected, /completion-gate/);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('ignores tier-emitted entries (${RUNE_PRO_ROOT})', async () => {
|
|
69
|
+
const settings = buildPreset('gentle');
|
|
70
|
+
// Inject a Pro-tier entry alongside Free preset entries
|
|
71
|
+
settings.hooks.PostToolUse.push({
|
|
72
|
+
matcher: 'Edit|Write',
|
|
73
|
+
hooks: [
|
|
74
|
+
{
|
|
75
|
+
type: 'command',
|
|
76
|
+
command: 'node "${RUNE_PRO_ROOT}/hooks/run-hook.cjs" context-sense',
|
|
77
|
+
async: false,
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
});
|
|
81
|
+
await seedClaude(tmpRoot, settings);
|
|
82
|
+
const result = await checkHookDrift(tmpRoot);
|
|
83
|
+
// Tier entry should NOT be flagged as drift — it's tier-managed, not Free-preset
|
|
84
|
+
const tierDrift = result.findings.find(
|
|
85
|
+
(f) => f.status === 'drift' && typeof f.actual === 'string' && f.actual.includes('RUNE_PRO_ROOT'),
|
|
86
|
+
);
|
|
87
|
+
assert.strictEqual(tierDrift, undefined);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('returns empty when settings.json absent', async () => {
|
|
91
|
+
await mkdir(path.join(tmpRoot, '.claude'), { recursive: true });
|
|
92
|
+
const result = await checkHookDrift(tmpRoot);
|
|
93
|
+
assert.strictEqual(result.findings.length, 0);
|
|
94
|
+
assert.strictEqual(result.platforms.length, 0);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('returns empty when no Rune hooks installed (preset = none)', async () => {
|
|
98
|
+
await seedClaude(tmpRoot, { env: { FOO: 'bar' } }); // user settings, no Rune hooks
|
|
99
|
+
const result = await checkHookDrift(tmpRoot);
|
|
100
|
+
assert.strictEqual(result.findings.length, 0);
|
|
101
|
+
assert.strictEqual(result.platforms.length, 0);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('reports parse error when settings.json is malformed', async () => {
|
|
105
|
+
await mkdir(path.join(tmpRoot, '.claude'), { recursive: true });
|
|
106
|
+
await writeFile(path.join(tmpRoot, SETTINGS_REL_PATH), '{ not valid json');
|
|
107
|
+
const result = await checkHookDrift(tmpRoot);
|
|
108
|
+
const errors = result.findings.filter((f) => f.status === 'error');
|
|
109
|
+
assert.strictEqual(errors.length, 1);
|
|
110
|
+
assert.match(errors[0].message, /Cannot parse/);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('flags mixed preset (both gentle and strict installed)', async () => {
|
|
114
|
+
// Mix gentle PreToolUse with strict Stop
|
|
115
|
+
const gentle = buildPreset('gentle');
|
|
116
|
+
const strict = buildPreset('strict');
|
|
117
|
+
const mixed = {
|
|
118
|
+
hooks: {
|
|
119
|
+
PreToolUse: gentle.hooks.PreToolUse,
|
|
120
|
+
Stop: strict.hooks.Stop,
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
await seedClaude(tmpRoot, mixed);
|
|
124
|
+
const result = await checkHookDrift(tmpRoot);
|
|
125
|
+
const mixedFinding = result.findings.find((f) => f.status === 'mixed-preset');
|
|
126
|
+
assert.ok(mixedFinding, 'should flag mixed-preset');
|
|
127
|
+
assert.match(mixedFinding.message, /gentle and strict/);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe('formatHookDriftResult', () => {
|
|
132
|
+
test('clean preset → all-match line', () => {
|
|
133
|
+
const out = formatHookDriftResult({
|
|
134
|
+
findings: [],
|
|
135
|
+
summary: { drifted: 0, missing: 0, errors: 0 },
|
|
136
|
+
platforms: [{ platform: 'claude', preset: 'gentle' }],
|
|
137
|
+
});
|
|
138
|
+
assert.match(out, /All Rune-managed entries match canonical preset/);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('no platforms → install hint', () => {
|
|
142
|
+
const out = formatHookDriftResult({ findings: [], summary: { drifted: 0, missing: 0, errors: 0 }, platforms: [] });
|
|
143
|
+
assert.match(out, /No installed Rune hooks detected/);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('drift finding renders actual command', () => {
|
|
147
|
+
const out = formatHookDriftResult({
|
|
148
|
+
findings: [{ platform: 'claude', event: 'PreToolUse', status: 'drift', actual: 'custom-cmd' }],
|
|
149
|
+
summary: { drifted: 1, missing: 0, errors: 0 },
|
|
150
|
+
platforms: [{ platform: 'claude', preset: 'gentle' }],
|
|
151
|
+
});
|
|
152
|
+
assert.match(out, /drift/);
|
|
153
|
+
assert.match(out, /custom-cmd/);
|
|
154
|
+
assert.match(out, /Summary: 1 drifted/);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
@@ -169,10 +169,11 @@ describe('summarizeRuneHooks', () => {
|
|
|
169
169
|
test('extracts skill names per event', () => {
|
|
170
170
|
const merged = mergePreset({}, buildPreset('gentle'));
|
|
171
171
|
const summary = summarizeRuneHooks(merged);
|
|
172
|
-
assert.strictEqual(summary.total,
|
|
172
|
+
assert.strictEqual(summary.total, 5);
|
|
173
173
|
assert.ok(summary.events.PreToolUse.includes('preflight'));
|
|
174
174
|
assert.ok(summary.events.PreToolUse.includes('sentinel'));
|
|
175
175
|
assert.ok(summary.events.PostToolUse.includes('dependency-doctor'));
|
|
176
|
+
assert.ok(summary.events.PostToolUse.includes('quarantine'));
|
|
176
177
|
assert.ok(summary.events.Stop.includes('completion-gate'));
|
|
177
178
|
});
|
|
178
179
|
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { afterEach, beforeEach, describe, test } from 'node:test';
|
|
6
|
+
import { detectTiers, formatSetupResult, runSetup } from '../commands/setup.js';
|
|
7
|
+
|
|
8
|
+
let tmpRoot;
|
|
9
|
+
|
|
10
|
+
async function seedClaude(root) {
|
|
11
|
+
await mkdir(path.join(root, '.claude'), { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function seedTier(root, tier) {
|
|
15
|
+
const dir = path.join(root, '..', tier === 'pro' ? 'Pro' : 'Business', 'hooks');
|
|
16
|
+
await mkdir(dir, { recursive: true });
|
|
17
|
+
await writeFile(
|
|
18
|
+
path.join(dir, 'manifest.json'),
|
|
19
|
+
JSON.stringify({
|
|
20
|
+
tier,
|
|
21
|
+
version: '1.0.0',
|
|
22
|
+
minFreeVersion: '2.0.0',
|
|
23
|
+
requires: [tier === 'pro' ? 'RUNE_PRO_ROOT' : 'RUNE_BUSINESS_ROOT'],
|
|
24
|
+
entries: [],
|
|
25
|
+
}),
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
beforeEach(async () => {
|
|
30
|
+
tmpRoot = await mkdtemp(path.join(tmpdir(), 'rune-setup-'));
|
|
31
|
+
await mkdir(path.join(tmpRoot, 'project'), { recursive: true });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(async () => {
|
|
35
|
+
if (tmpRoot) await rm(tmpRoot, { recursive: true, force: true });
|
|
36
|
+
delete process.env.RUNE_PRO_ROOT;
|
|
37
|
+
delete process.env.RUNE_BUSINESS_ROOT;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('detectTiers', () => {
|
|
41
|
+
test('returns null for both when no tier present', () => {
|
|
42
|
+
const projectRoot = path.join(tmpRoot, 'project');
|
|
43
|
+
const result = detectTiers(projectRoot, { wellKnownPaths: { pro: [], business: [] } });
|
|
44
|
+
assert.strictEqual(result.pro, null);
|
|
45
|
+
assert.strictEqual(result.business, null);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('detects Pro via sibling path', async () => {
|
|
49
|
+
const projectRoot = path.join(tmpRoot, 'project');
|
|
50
|
+
await seedTier(projectRoot, 'pro');
|
|
51
|
+
const result = detectTiers(projectRoot, { wellKnownPaths: { pro: [], business: [] } });
|
|
52
|
+
assert.ok(result.pro);
|
|
53
|
+
assert.match(result.pro.source, /sibling/);
|
|
54
|
+
assert.strictEqual(result.pro.version, '1.0.0');
|
|
55
|
+
assert.strictEqual(result.business, null);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('detects Business via sibling path', async () => {
|
|
59
|
+
const projectRoot = path.join(tmpRoot, 'project');
|
|
60
|
+
await seedTier(projectRoot, 'business');
|
|
61
|
+
const result = detectTiers(projectRoot, { wellKnownPaths: { pro: [], business: [] } });
|
|
62
|
+
assert.ok(result.business);
|
|
63
|
+
assert.match(result.business.source, /sibling/);
|
|
64
|
+
assert.strictEqual(result.pro, null);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('env var takes precedence over sibling', async () => {
|
|
68
|
+
const projectRoot = path.join(tmpRoot, 'project');
|
|
69
|
+
await seedTier(projectRoot, 'pro');
|
|
70
|
+
// Env var pointing at a different location
|
|
71
|
+
const envRoot = path.join(tmpRoot, 'env-pro');
|
|
72
|
+
await mkdir(path.join(envRoot, 'hooks'), { recursive: true });
|
|
73
|
+
await writeFile(
|
|
74
|
+
path.join(envRoot, 'hooks', 'manifest.json'),
|
|
75
|
+
JSON.stringify({ tier: 'pro', version: '2.0.0', minFreeVersion: '2.0.0', requires: [], entries: [] }),
|
|
76
|
+
);
|
|
77
|
+
process.env.RUNE_PRO_ROOT = envRoot;
|
|
78
|
+
const result = detectTiers(projectRoot, { wellKnownPaths: { pro: [], business: [] } });
|
|
79
|
+
assert.match(result.pro.source, /\$RUNE_PRO_ROOT/);
|
|
80
|
+
assert.strictEqual(result.pro.version, '2.0.0');
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('runSetup (non-interactive)', () => {
|
|
85
|
+
test('--here installs to current project, no tiers when none detected', async () => {
|
|
86
|
+
const projectRoot = path.join(tmpRoot, 'project');
|
|
87
|
+
await seedClaude(projectRoot);
|
|
88
|
+
const result = await runSetup({
|
|
89
|
+
projectRoot,
|
|
90
|
+
runeRoot: path.resolve(path.dirname(new URL(import.meta.url).pathname), '..', '..'),
|
|
91
|
+
args: { here: true, preset: 'gentle', 'no-tier': true },
|
|
92
|
+
});
|
|
93
|
+
assert.strictEqual(result.scope, 'current');
|
|
94
|
+
assert.strictEqual(result.preset, 'gentle');
|
|
95
|
+
assert.deepStrictEqual(result.tiers, []);
|
|
96
|
+
assert.strictEqual(result.targetRoot, projectRoot);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('--tier flag bypasses prompt', async () => {
|
|
100
|
+
const projectRoot = path.join(tmpRoot, 'project');
|
|
101
|
+
await seedClaude(projectRoot);
|
|
102
|
+
await seedTier(projectRoot, 'pro');
|
|
103
|
+
const result = await runSetup({
|
|
104
|
+
projectRoot,
|
|
105
|
+
runeRoot: path.resolve(path.dirname(new URL(import.meta.url).pathname), '..', '..'),
|
|
106
|
+
args: { here: true, preset: 'gentle', tier: 'pro' },
|
|
107
|
+
});
|
|
108
|
+
assert.deepStrictEqual(result.tiers, ['pro']);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('--dry does not write files', async () => {
|
|
112
|
+
const projectRoot = path.join(tmpRoot, 'project');
|
|
113
|
+
await seedClaude(projectRoot);
|
|
114
|
+
const result = await runSetup({
|
|
115
|
+
projectRoot,
|
|
116
|
+
runeRoot: path.resolve(path.dirname(new URL(import.meta.url).pathname), '..', '..'),
|
|
117
|
+
args: { here: true, preset: 'gentle', 'no-tier': true, dry: true },
|
|
118
|
+
});
|
|
119
|
+
assert.strictEqual(result.written, false);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('formatSetupResult', () => {
|
|
124
|
+
test('renders summary with scope and tiers', () => {
|
|
125
|
+
const out = formatSetupResult({
|
|
126
|
+
scope: 'current',
|
|
127
|
+
targetRoot: '/path/to/project',
|
|
128
|
+
tiers: ['pro'],
|
|
129
|
+
preset: 'gentle',
|
|
130
|
+
platforms: ['claude'],
|
|
131
|
+
written: true,
|
|
132
|
+
notes: [],
|
|
133
|
+
});
|
|
134
|
+
assert.match(out, /Setup Complete/);
|
|
135
|
+
assert.match(out, /Tiers:.*Free.*pro/);
|
|
136
|
+
assert.match(out, /current project/);
|
|
137
|
+
assert.match(out, /rune doctor --hooks/);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('renders global scope label when scope=global', () => {
|
|
141
|
+
const out = formatSetupResult({
|
|
142
|
+
scope: 'global',
|
|
143
|
+
targetRoot: '/home/user',
|
|
144
|
+
tiers: [],
|
|
145
|
+
preset: 'gentle',
|
|
146
|
+
platforms: ['claude'],
|
|
147
|
+
written: true,
|
|
148
|
+
notes: [],
|
|
149
|
+
});
|
|
150
|
+
assert.match(out, /GLOBAL/);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
@@ -105,7 +105,7 @@ export default {
|
|
|
105
105
|
id: 'rune',
|
|
106
106
|
name: 'Rune',
|
|
107
107
|
kind: 'skills',
|
|
108
|
-
description: `${skills.length}-skill mesh for AI coding assistants. Routes all code tasks through specialized skills.
|
|
108
|
+
description: `${skills.length}-skill mesh for AI coding assistants. Routes all code tasks through specialized skills. 203 sync connections + 40 async signals, 14 extension packs.`,
|
|
109
109
|
version: pluginJson.version || '0.0.0',
|
|
110
110
|
skills: ['./skills'],
|
|
111
111
|
artifactConvention: {
|
package/compiler/bin/rune.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { existsSync } from 'node:fs';
|
|
15
15
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
16
|
+
import os from 'node:os';
|
|
16
17
|
import path from 'node:path';
|
|
17
18
|
import { createInterface } from 'node:readline';
|
|
18
19
|
import { fileURLToPath } from 'node:url';
|
|
@@ -24,6 +25,8 @@ import { hookStatus } from '../commands/hooks/status.js';
|
|
|
24
25
|
import { uninstallHooks } from '../commands/hooks/uninstall.js';
|
|
25
26
|
import { generateDashboardHTML } from '../dashboard.js';
|
|
26
27
|
import { checkMeshIntegrity, formatDoctorResults, formatMeshResults, runDoctor } from '../doctor.js';
|
|
28
|
+
import { checkHookDrift, formatHookDriftResult } from '../commands/hooks/drift.js';
|
|
29
|
+
import { runSetup, formatSetupResult } from '../commands/setup.js';
|
|
27
30
|
import { buildAll } from '../emitter.js';
|
|
28
31
|
import { collectStats, renderStatus, renderStatusJson } from '../status.js';
|
|
29
32
|
import { collectGraphData, generateMeshHTML } from '../visualizer.js';
|
|
@@ -241,6 +244,14 @@ async function cmdBuild(projectRoot, args) {
|
|
|
241
244
|
async function cmdDoctor(projectRoot, args) {
|
|
242
245
|
const config = await readConfig(projectRoot);
|
|
243
246
|
|
|
247
|
+
// --hooks flag: run hook drift report only (reporter, exit 0 always)
|
|
248
|
+
if (args.hooks) {
|
|
249
|
+
log('');
|
|
250
|
+
const driftResult = await checkHookDrift(projectRoot);
|
|
251
|
+
log(formatHookDriftResult(driftResult));
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
244
255
|
// --mesh flag: run mesh integrity check only
|
|
245
256
|
if (args.mesh) {
|
|
246
257
|
log('');
|
|
@@ -294,6 +305,45 @@ async function cmdDoctor(projectRoot, args) {
|
|
|
294
305
|
if (!results.healthy) process.exit(1);
|
|
295
306
|
}
|
|
296
307
|
|
|
308
|
+
async function cmdSetup(projectRoot, args) {
|
|
309
|
+
log('');
|
|
310
|
+
log(' Rune Setup Wizard');
|
|
311
|
+
log(' ──────────────────');
|
|
312
|
+
log(` Free version: ${await readVersion()} (cached)`);
|
|
313
|
+
|
|
314
|
+
// Print detected tiers banner
|
|
315
|
+
const { detectTiers } = await import('../commands/setup.js');
|
|
316
|
+
const detected = detectTiers(projectRoot);
|
|
317
|
+
if (detected.pro) {
|
|
318
|
+
log(` Pro detected: ${detected.pro.source} (v${detected.pro.version})`);
|
|
319
|
+
} else {
|
|
320
|
+
log(' Pro: not detected');
|
|
321
|
+
}
|
|
322
|
+
if (detected.business) {
|
|
323
|
+
log(` Business: ${detected.business.source} (v${detected.business.version})`);
|
|
324
|
+
} else {
|
|
325
|
+
log(' Business: not detected');
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
try {
|
|
329
|
+
const result = await runSetup({ projectRoot, runeRoot: RUNE_ROOT, args });
|
|
330
|
+
log(formatSetupResult(result));
|
|
331
|
+
} catch (err) {
|
|
332
|
+
log('');
|
|
333
|
+
log(` ✗ Setup failed: ${err.message}`);
|
|
334
|
+
process.exit(1);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function readVersion() {
|
|
339
|
+
try {
|
|
340
|
+
const pkg = JSON.parse(await readFile(path.join(RUNE_ROOT, 'package.json'), 'utf-8'));
|
|
341
|
+
return pkg.version;
|
|
342
|
+
} catch {
|
|
343
|
+
return 'unknown';
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
297
347
|
async function cmdStatus(projectRoot, args) {
|
|
298
348
|
const config = await readConfig(projectRoot);
|
|
299
349
|
const tierSources = resolveTierSources(config?.tiers, projectRoot);
|
|
@@ -458,10 +508,13 @@ async function cmdHooks(projectRoot, args, subcommand) {
|
|
|
458
508
|
switch (subcommand) {
|
|
459
509
|
case 'install': {
|
|
460
510
|
const tierArg = parseTierFlag(args.tier);
|
|
461
|
-
|
|
511
|
+
// --global writes to ~/.claude/settings.json (covers every Claude Code session)
|
|
512
|
+
const targetRoot = args.global ? os.homedir() : projectRoot;
|
|
513
|
+
const platform = args.global ? 'claude' : args.platform;
|
|
514
|
+
const result = await installHooks(targetRoot, {
|
|
462
515
|
preset: args.preset,
|
|
463
516
|
dry: args.dry,
|
|
464
|
-
platform
|
|
517
|
+
platform,
|
|
465
518
|
tier: tierArg,
|
|
466
519
|
});
|
|
467
520
|
log('');
|
|
@@ -636,6 +689,9 @@ async function main() {
|
|
|
636
689
|
case 'doctor':
|
|
637
690
|
await cmdDoctor(projectRoot, args);
|
|
638
691
|
break;
|
|
692
|
+
case 'setup':
|
|
693
|
+
await cmdSetup(projectRoot, args);
|
|
694
|
+
break;
|
|
639
695
|
case 'status':
|
|
640
696
|
await cmdStatus(projectRoot, args);
|
|
641
697
|
break;
|
|
@@ -664,17 +720,20 @@ async function main() {
|
|
|
664
720
|
log(' Rune CLI — Skill mesh for AI coding assistants');
|
|
665
721
|
log('');
|
|
666
722
|
log(' Commands:');
|
|
667
|
-
log('
|
|
723
|
+
log(' setup Interactive wizard — auto-detect tiers, pick scope, install hooks (recommended for first-time)');
|
|
724
|
+
log(' [--here|--global] [--tier pro,business] [--preset gentle|strict] [--dry]');
|
|
725
|
+
log(' init Interactive setup for build pipeline (auto-detects platform)');
|
|
668
726
|
log(' build Compile skills for configured platform');
|
|
669
727
|
log(' doctor Validate compiled output + mesh integrity');
|
|
670
728
|
log(' --mesh Mesh integrity only (reciprocals, versions, sections)');
|
|
729
|
+
log(' --hooks Hook drift report — compare installed vs canonical preset (reporter, exit 0)');
|
|
671
730
|
log(' --strict Fail on warnings (for CI)');
|
|
672
731
|
log(' status Project dashboard (skills, signals, packs, health)');
|
|
673
732
|
log(' visualize Interactive mesh graph (opens in browser)');
|
|
674
733
|
log(' analytics Usage analytics dashboard (Business tier)');
|
|
675
734
|
log(' hooks Install/uninstall/status for multi-platform auto-discipline');
|
|
676
735
|
log(
|
|
677
|
-
' hooks install [--preset gentle|strict|off] [--platform claude|cursor|windsurf|antigravity|all]',
|
|
736
|
+
' hooks install [--preset gentle|strict|off] [--platform claude|cursor|windsurf|antigravity|all] [--global]',
|
|
678
737
|
);
|
|
679
738
|
log(' hooks uninstall [--platform <name>|all]');
|
|
680
739
|
log(' hooks status [--platform <name>|all]');
|