@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 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
- 63 skills · 215+ mesh connections · 8 platforms · MIT
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** — 63 skills with 215+ connections across a 5-layer architecture. Skills call each other bidirectionally, forming resilient workflows that adapt when things go wrong.
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.16.1 — Skill Enrichment + Triage Workflow + Output Modes)
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 63 skills, same mesh connections, same workflows — zero knowledge loss across platforms.
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** | 215+ skill connections, fail-loud-route-around | Linear execution |
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 63 skills into your IDE's rules format. Same knowledge, same workflows.
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 | 61/61 | 61/61 |
255
- | Mesh connections | 215+ (programmatic) | 215+ (rule references) |
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 63 skills, searchable by intent and layer |
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: 62 (L0: 1 │ L1: 5 │ L2: 29 │ L3: 27)
613
- Extension Packs: 14 free + 5 pro + 4 business
614
- Mesh Connections: 215+ cross-references
615
- Mesh Signals: 25 signals across 63 skills (emit/listen graph)
616
- Connections/Skill: 3.4 avg
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, 4);
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. 215+ connections, 14 extension packs.`,
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: {
@@ -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
- const result = await installHooks(projectRoot, {
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: args.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(' init Interactive setup (auto-detects platform)');
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]');