@rune-kit/rune 2.16.0 → 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,12 +83,24 @@ _Methodology: Claude Code CLI headless mode (`claude -p --output-format json`),
83
83
 
84
84
  ---
85
85
 
86
- ## What's New (v2.16.0Skill Enrichment + Triage Workflow + Output Modes)
86
+ ## What's New (v2.17.1One-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)
95
+
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.
97
+
98
+
87
99
 
88
100
  - **`debug` v1.2.0 — Step 0: Build Feedback Loop** — 10-rank ladder (failing test → curl → CLI snapshot → headless browser → trace replay → throwaway harness → fuzz → bisection → differential → HITL script). Codifies "the loop is the speed limit" — a fast deterministic pass/fail signal turns debugging into mechanical bisection. Skip if repro is already < 5s and deterministic; > 10 min loop construction triggers 3-Fix Escalation (architecture is the problem).
89
101
  - **`plan` v1.6.0 — Vertical Slice Mode** — tracer-bullet task decomposition. Each task = end-to-end path through schema + API + UI + test, demoable on its own. AFK / HITL classification. Stops "horizontal layer" planning that blocks on the slowest layer.
90
102
  - **`context-engine` v1.2.0 — Caveman Output Mode** — auto-activates on context ORANGE / RED (or `/caveman`). Strips filler / articles / hedging / pleasantries while preserving full technical accuracy (~75% output reduction). Auto-clarity exceptions for security warnings, destructive-action confirmations, multi-step sequences, root-cause diagnosis.
91
- - **`ba` v0.13.0 — Synthesis Mode + Out-of-Scope WRITE** — when prior conversation has rich context (pasted spec, > 1000 words, continuation session), extract Requirements Document with source citations and confirm instead of re-interviewing. Step 1.6 closes the `.out-of-scope/` write loop: explicit mid-elicitation rejections produce a durable `.out-of-scope/<slug>.md` record so future sessions don't re-litigate.
103
+ - **`ba` v1.0.0 — Synthesis Mode + Out-of-Scope WRITE** — when prior conversation has rich context (pasted spec, > 1000 words, continuation session), extract Requirements Document with source citations and confirm instead of re-interviewing. Step 1.6 closes the `.out-of-scope/` write loop: explicit mid-elicitation rejections produce a durable `.out-of-scope/<slug>.md` record so future sessions don't re-litigate.
92
104
  - **`context-pack` v0.3.0 — Agent Brief Variant** — durable handoff format for AFK agents (issue tracker queues, autopilot multi-session work). Behavioral over procedural; type names over file:line. Survives codebase drift between handoff and execution.
93
105
  - **`review-intake` v1.3.0 — Issue Triage Mode** — new mode for issue tracker items (PR Review remains default). State machine: needs-triage → needs-info / ready-for-agent / ready-for-human / wontfix. Repro-first HARD-GATE for bugs (calls `debug` Step 0 if multi-component). Vague issues route to `ba` Synthesis Mode for grilling. AGENT-BRIEF emission for `ready-for-agent`.
94
106
  - **5 new mesh signals** — `output.density.set`, `triage.classified`, `agent.brief.ready`, `outofscope.recorded` + `EXTERNAL_TRIGGER_SIGNALS` whitelist concept (symmetric to `INTENTIONAL_BROADCAST_SIGNALS`).
@@ -189,7 +201,7 @@ cook ───emit:phase.complete────→ session-bridge
189
201
 
190
202
  ## What Rune Is (and Isn't)
191
203
 
192
- 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.
193
205
 
194
206
  | | Rune Provides | Claude Code Provides |
195
207
  |---|---|---|
@@ -197,7 +209,7 @@ Rune started as a **Claude Code plugin** and now compiles to **every major AI ID
197
209
  | **Quality Gates** | preflight + sentinel + review + completion-gate (parallel) | None built-in |
198
210
  | **Domain Knowledge** | 14 extension packs (trading, SaaS, mobile, etc.) | General-purpose |
199
211
  | **Cross-Session State** | .rune/ directory (decisions, conventions, progress) | Conversation only |
200
- | **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 |
201
213
  | **Cost Optimization** | Auto model selection (haiku/sonnet/opus per task) | Single model |
202
214
  | | | |
203
215
  | **Sandbox & Permissions** | — | Claude Code handles this |
@@ -218,6 +230,46 @@ Rune started as a **Claude Code plugin** and now compiles to **every major AI ID
218
230
 
219
231
  ## Install
220
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
+
221
273
  ### Claude Code (Native Plugin)
222
274
 
223
275
  ```bash
@@ -227,7 +279,7 @@ claude plugin add rune-kit/rune
227
279
 
228
280
  Or add manually in `~/.claude/settings.json` under `installed_plugins`.
229
281
 
230
- 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).
231
283
 
232
284
  ### Cursor / Windsurf / Antigravity / Any IDE
233
285
 
@@ -241,14 +293,14 @@ npx @rune-kit/rune init --platform windsurf
241
293
  npx @rune-kit/rune init --platform antigravity
242
294
  ```
243
295
 
244
- 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.
245
297
 
246
298
  ### Platform Comparison
247
299
 
248
300
  | Feature | Claude Code | Cursor / Windsurf / Others |
249
301
  |---------|-------------|---------------------------|
250
- | Skills available | 61/61 | 61/61 |
251
- | 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) |
252
304
  | Workflows & HARD-GATEs | Full | Full |
253
305
  | Extension packs | 14 | 14 |
254
306
  | Subagent parallelism | Native | Sequential fallback |
@@ -592,7 +644,7 @@ See [docs/MULTI-PLATFORM.md](docs/MULTI-PLATFORM.md) for the full architecture.
592
644
  | Doc | What's inside |
593
645
  |-----|---------------|
594
646
  | [`docs/GETTING_STARTED.md`](docs/GETTING_STARTED.md) | Your first 5 minutes with Rune — install to first `/rune cook` |
595
- | [`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 |
596
648
  | [`docs/SIGNALS.md`](docs/SIGNALS.md) | Canonical signal inventory — 25 events, emit/listen graph |
597
649
  | [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) | 5-layer mesh architecture reference |
598
650
  | [`docs/VISION.md`](docs/VISION.md) | Philosophy — what Rune is and isn't |
@@ -605,11 +657,11 @@ See [docs/MULTI-PLATFORM.md](docs/MULTI-PLATFORM.md) for the full architecture.
605
657
  ## Numbers
606
658
 
607
659
  ```
608
- Core Skills: 62 (L0: 1 │ L1: 5 │ L2: 29 │ L3: 27)
609
- Extension Packs: 14 free + 5 pro + 4 business
610
- Mesh Connections: 215+ cross-references
611
- Mesh Signals: 25 signals across 63 skills (emit/listen graph)
612
- 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
613
665
  Platforms: 8 (Claude Code, Cursor, Windsurf, Antigravity, Codex, OpenCode, OpenClaw, Generic)
614
666
  Compiler: ~1400 LOC (parser + 8 transforms + 8 adapters + CLI)
615
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]');