@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 +68 -16
- 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/ba/SKILL.md +1 -1
- 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,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.
|
|
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)
|
|
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`
|
|
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
|
|
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** |
|
|
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
|
|
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 |
|
|
251
|
-
| Mesh connections |
|
|
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
|
|
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:
|
|
609
|
-
Extension Packs: 14 free +
|
|
610
|
-
Mesh Connections:
|
|
611
|
-
Mesh Signals:
|
|
612
|
-
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
|
|
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,
|
|
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]');
|