@rune-kit/rune 2.16.1 → 2.18.0
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__/adapter-model-mapping.test.js +80 -0
- package/compiler/__tests__/adapters.test.js +115 -3
- 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/aider.js +120 -0
- package/compiler/adapters/codex.js +33 -0
- package/compiler/adapters/copilot.js +126 -0
- package/compiler/adapters/gemini.js +131 -0
- package/compiler/adapters/index.js +10 -0
- package/compiler/adapters/openclaw.js +1 -1
- package/compiler/adapters/qoder.js +102 -0
- package/compiler/adapters/qwen.js +111 -0
- package/compiler/bin/rune.js +65 -4
- package/compiler/commands/hooks/drift.js +170 -0
- package/compiler/commands/hooks/presets.js +11 -1
- package/compiler/commands/setup.js +242 -0
- package/compiler/doctor.js +48 -2
- package/compiler/emitter.js +38 -41
- 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/hooks/session-start/index.cjs +91 -0
- package/package.json +2 -2
- package/skills/asset-creator/SKILL.md +1 -1
- package/skills/audit/SKILL.md +20 -2
- package/skills/autopsy/SKILL.md +173 -2
- package/skills/brainstorm/SKILL.md +24 -1
- package/skills/browser-pilot/SKILL.md +16 -1
- package/skills/debug/SKILL.md +4 -2
- package/skills/deploy/SKILL.md +72 -2
- package/skills/design/SKILL.md +50 -3
- package/skills/integrity-check/SKILL.md +2 -0
- package/skills/launch/SKILL.md +11 -1
- package/skills/marketing/SKILL.md +1 -1
- package/skills/neural-memory/SKILL.md +1 -1
- package/skills/perf/SKILL.md +93 -2
- 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/skills/sentinel-env/SKILL.md +2 -2
- package/skills/skill-forge/SKILL.md +47 -1
- package/skills/surgeon/SKILL.md +1 -1
- package/skills/team/SKILL.md +27 -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 · 13 platforms · MIT
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
@@ -16,14 +16,14 @@
|
|
|
16
16
|
</p>
|
|
17
17
|
|
|
18
18
|
<p align="center">
|
|
19
|
-
<strong>Claude Code</strong> (native plugin) · <strong>Cursor</strong> · <strong>Windsurf</strong> · <strong>Google Antigravity</strong> · <strong>OpenAI Codex</strong> · <strong>OpenCode</strong> · any AI IDE
|
|
19
|
+
<strong>Claude Code</strong> (native plugin) · <strong>Cursor</strong> · <strong>Windsurf</strong> · <strong>Google Antigravity</strong> · <strong>OpenAI Codex</strong> · <strong>OpenCode</strong> · <strong>Aider</strong> · <strong>GitHub Copilot CLI</strong> · <strong>Gemini CLI</strong> · <strong>Qoder</strong> · <strong>Qwen Coder</strong> · any AI IDE
|
|
20
20
|
</p>
|
|
21
21
|
|
|
22
22
|
## Why Rune?
|
|
23
23
|
|
|
24
24
|
Most skill ecosystems are either **too many isolated skills** (540+ that don't talk to each other) or **rigid pipelines** (A → B → C, if B fails everything stops).
|
|
25
25
|
|
|
26
|
-
Rune is a **mesh** —
|
|
26
|
+
Rune is a **mesh** — 64 skills with 203 connections + 40 signals across a 5-layer architecture. Skills call each other bidirectionally, forming resilient workflows that adapt when things go wrong.
|
|
27
27
|
|
|
28
28
|
```
|
|
29
29
|
Pipeline: A → B → C → D (B fails = stuck)
|
|
@@ -83,7 +83,19 @@ _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.18.0 — Cross-Platform Reach + Discipline Tightening)
|
|
87
|
+
|
|
88
|
+
> **v2.18.0 (2026-05-15):** Compiler grew from 8 → 13 platforms with five new adapters: **Aider** (per-skill `aider/rules/` + auto-generated `.aider.conf.yml` `read:` array), **GitHub Copilot CLI** (`.github/instructions/*.instructions.md` w/ documented `applyTo` YAML), **Gemini CLI** (bundled `GEMINI.md` for single-file context), **Qoder** (`.qoder/rules/` + AGENTS.md), **Qwen Coder** (`qwen/skills/` + `QWEN.md` with `@import`). New `adapter.generateExtraFiles()` hook with path-traversal guard + frozen stats snapshot — replaces ad-hoc adapter special-cases (codex AGENTS.md migrated). Discipline tightening: `design` v0.6.0 adds Step 2.9 Rules 4/5/6 (measurable constraints, no #000/#fff/lorem ipsum, CJK-first font stack); `skill-forge` v1.9.0 adds soft `examples/` convention for output-format skills; `sentinel-env` v0.4.0 expands Tier 8 binary detection (Bun, Cargo, Deno, Volta, asdf, proto). New `CONTRIBUTING.md` "What we don't accept" non-goals section. Source: graft from `nexu-io/html-anything` (Apache-2.0). CI 1435/1435.
|
|
89
|
+
|
|
90
|
+
### Previous (v2.17.1 — One-Command Setup Wizard)
|
|
91
|
+
|
|
92
|
+
> **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.
|
|
93
|
+
|
|
94
|
+
### Previous (v2.17.0 — Quarantine + Hook Drift Reporter)
|
|
95
|
+
|
|
96
|
+
> **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.
|
|
97
|
+
|
|
98
|
+
### Previous (v2.16.1 — Skill Enrichment + Triage Workflow + Output Modes)
|
|
87
99
|
|
|
88
100
|
> **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
101
|
|
|
@@ -193,7 +205,7 @@ cook ───emit:phase.complete────→ session-bridge
|
|
|
193
205
|
|
|
194
206
|
## What Rune Is (and Isn't)
|
|
195
207
|
|
|
196
|
-
Rune started as a **Claude Code plugin** and now compiles to **every major AI IDE**. Same
|
|
208
|
+
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
209
|
|
|
198
210
|
| | Rune Provides | Claude Code Provides |
|
|
199
211
|
|---|---|---|
|
|
@@ -201,7 +213,7 @@ Rune started as a **Claude Code plugin** and now compiles to **every major AI ID
|
|
|
201
213
|
| **Quality Gates** | preflight + sentinel + review + completion-gate (parallel) | None built-in |
|
|
202
214
|
| **Domain Knowledge** | 14 extension packs (trading, SaaS, mobile, etc.) | General-purpose |
|
|
203
215
|
| **Cross-Session State** | .rune/ directory (decisions, conventions, progress) | Conversation only |
|
|
204
|
-
| **Mesh Resilience** |
|
|
216
|
+
| **Mesh Resilience** | 203 skill connections + 40 mesh signals, fail-loud-route-around | Linear execution |
|
|
205
217
|
| **Cost Optimization** | Auto model selection (haiku/sonnet/opus per task) | Single model |
|
|
206
218
|
| | | |
|
|
207
219
|
| **Sandbox & Permissions** | — | Claude Code handles this |
|
|
@@ -222,6 +234,46 @@ Rune started as a **Claude Code plugin** and now compiles to **every major AI ID
|
|
|
222
234
|
|
|
223
235
|
## Install
|
|
224
236
|
|
|
237
|
+
### One-Command Setup (recommended)
|
|
238
|
+
|
|
239
|
+
After installing the plugin, run the wizard once to wire hooks the way you want them — pick scope, pick tiers, done:
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
npx @rune-kit/rune setup
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
The wizard auto-detects what you have:
|
|
246
|
+
|
|
247
|
+
```
|
|
248
|
+
Rune Setup Wizard
|
|
249
|
+
──────────────────
|
|
250
|
+
Free version: 2.18.0 (cached)
|
|
251
|
+
Pro detected: sibling (../Pro) (v1.1.0)
|
|
252
|
+
Business: not detected
|
|
253
|
+
|
|
254
|
+
Where to install hooks?
|
|
255
|
+
[c] Current project — D:/MyProject/.claude/settings.json
|
|
256
|
+
[g] Global — ~/.claude/settings.json
|
|
257
|
+
(every Claude Code session, regardless of project)
|
|
258
|
+
|
|
259
|
+
Scope [c/g] (default c): g
|
|
260
|
+
Install Pro tier? [Y/n]: y
|
|
261
|
+
Preset [g/s] (default g): g
|
|
262
|
+
|
|
263
|
+
✓ Wired 5 hooks to ~/.claude/settings.json
|
|
264
|
+
Verify: rune doctor --hooks
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**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).
|
|
268
|
+
|
|
269
|
+
**Non-interactive mode** (CI / scripted):
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
npx @rune-kit/rune setup --here --preset gentle --tier pro
|
|
273
|
+
npx @rune-kit/rune setup --global --preset strict --tier pro,business
|
|
274
|
+
npx @rune-kit/rune setup --here --no-tier --dry # preview without writing
|
|
275
|
+
```
|
|
276
|
+
|
|
225
277
|
### Claude Code (Native Plugin)
|
|
226
278
|
|
|
227
279
|
```bash
|
|
@@ -231,7 +283,7 @@ claude plugin add rune-kit/rune
|
|
|
231
283
|
|
|
232
284
|
Or add manually in `~/.claude/settings.json` under `installed_plugins`.
|
|
233
285
|
|
|
234
|
-
Full mesh: subagents, hooks, adaptive routing, mesh analytics.
|
|
286
|
+
Full mesh: subagents, hooks, adaptive routing, mesh analytics. **Run `npx @rune-kit/rune setup` afterward to wire hooks** (see One-Command Setup above).
|
|
235
287
|
|
|
236
288
|
### Cursor / Windsurf / Antigravity / Any IDE
|
|
237
289
|
|
|
@@ -245,14 +297,14 @@ npx @rune-kit/rune init --platform windsurf
|
|
|
245
297
|
npx @rune-kit/rune init --platform antigravity
|
|
246
298
|
```
|
|
247
299
|
|
|
248
|
-
This compiles all
|
|
300
|
+
This compiles all 64 skills into your IDE's rules format. Same knowledge, same workflows.
|
|
249
301
|
|
|
250
302
|
### Platform Comparison
|
|
251
303
|
|
|
252
304
|
| Feature | Claude Code | Cursor / Windsurf / Others |
|
|
253
305
|
|---------|-------------|---------------------------|
|
|
254
|
-
| Skills available |
|
|
255
|
-
| Mesh connections |
|
|
306
|
+
| Skills available | 64/64 | 64/64 |
|
|
307
|
+
| Mesh connections | 203 sync + 40 signals (programmatic) | 203 sync + 40 signals (rule references) |
|
|
256
308
|
| Workflows & HARD-GATEs | Full | Full |
|
|
257
309
|
| Extension packs | 14 | 14 |
|
|
258
310
|
| Subagent parallelism | Native | Sequential fallback |
|
|
@@ -596,7 +648,7 @@ See [docs/MULTI-PLATFORM.md](docs/MULTI-PLATFORM.md) for the full architecture.
|
|
|
596
648
|
| Doc | What's inside |
|
|
597
649
|
|-----|---------------|
|
|
598
650
|
| [`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
|
|
651
|
+
| [`docs/SKILLS.md`](docs/SKILLS.md) | All 64 skills, searchable by intent and layer |
|
|
600
652
|
| [`docs/SIGNALS.md`](docs/SIGNALS.md) | Canonical signal inventory — 25 events, emit/listen graph |
|
|
601
653
|
| [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) | 5-layer mesh architecture reference |
|
|
602
654
|
| [`docs/VISION.md`](docs/VISION.md) | Philosophy — what Rune is and isn't |
|
|
@@ -609,11 +661,11 @@ See [docs/MULTI-PLATFORM.md](docs/MULTI-PLATFORM.md) for the full architecture.
|
|
|
609
661
|
## Numbers
|
|
610
662
|
|
|
611
663
|
```
|
|
612
|
-
Core Skills:
|
|
613
|
-
Extension Packs: 14 free +
|
|
614
|
-
Mesh Connections:
|
|
615
|
-
Mesh Signals:
|
|
616
|
-
Connections/Skill: 3.
|
|
664
|
+
Core Skills: 64 (L0: 1 │ L1: 5 │ L2: 30 │ L3: 28)
|
|
665
|
+
Extension Packs: 14 free + 6 pro + 4 business
|
|
666
|
+
Mesh Connections: 203 sync calls (rune doctor)
|
|
667
|
+
Mesh Signals: 40 signals · 50 emit/listen edges (rune doctor)
|
|
668
|
+
Connections/Skill: 3.2 avg
|
|
617
669
|
Platforms: 8 (Claude Code, Cursor, Windsurf, Antigravity, Codex, OpenCode, OpenClaw, Generic)
|
|
618
670
|
Compiler: ~1400 LOC (parser + 8 transforms + 8 adapters + CLI)
|
|
619
671
|
Tests: 1,152+ (compiler + signals + status + visualizer + hooks + scripts + tier-hooks)
|
|
@@ -131,6 +131,86 @@ describe('Anthropic-native adapters are no-op for model field', () => {
|
|
|
131
131
|
});
|
|
132
132
|
});
|
|
133
133
|
|
|
134
|
+
describe('qoder adapter model mapping (provider-agnostic)', () => {
|
|
135
|
+
const qoder = getAdapter('qoder');
|
|
136
|
+
|
|
137
|
+
test('opus translates to tier:heavy', () => {
|
|
138
|
+
const header = qoder.generateHeader({ ...baseSkill, model: 'opus' });
|
|
139
|
+
assert.match(header, /model: tier:heavy/);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('omitted model produces no model line', () => {
|
|
143
|
+
const header = qoder.generateHeader({ ...baseSkill });
|
|
144
|
+
assert.doesNotMatch(header, /model:/);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('copilot adapter model mapping (tier-hint comment)', () => {
|
|
149
|
+
const copilot = getAdapter('copilot');
|
|
150
|
+
|
|
151
|
+
test('opus emits tier-hint:tier:heavy as comment (Copilot ignores model field)', () => {
|
|
152
|
+
const header = copilot.generateHeader({ ...baseSkill, model: 'opus' });
|
|
153
|
+
assert.match(header, /tier-hint: tier:heavy/);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('omitted model produces no tier-hint line', () => {
|
|
157
|
+
const header = copilot.generateHeader({ ...baseSkill });
|
|
158
|
+
assert.doesNotMatch(header, /tier-hint/);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe('aider adapter model mapping (inline header)', () => {
|
|
163
|
+
const aider = getAdapter('aider');
|
|
164
|
+
|
|
165
|
+
test('opus appears as tier:heavy inline', () => {
|
|
166
|
+
const header = aider.generateHeader({ ...baseSkill, model: 'opus' });
|
|
167
|
+
assert.match(header, /tier:heavy/);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('omitted model produces no tier line', () => {
|
|
171
|
+
const header = aider.generateHeader({ ...baseSkill });
|
|
172
|
+
assert.doesNotMatch(header, /tier:/);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe('qwen adapter model mapping (Qwen family)', () => {
|
|
177
|
+
const qwen = getAdapter('qwen');
|
|
178
|
+
|
|
179
|
+
test('opus translates to qwen3-coder-plus', () => {
|
|
180
|
+
const header = qwen.generateHeader({ ...baseSkill, model: 'opus' });
|
|
181
|
+
assert.match(header, /qwen3-coder-plus/);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('sonnet translates to qwen3-coder', () => {
|
|
185
|
+
const header = qwen.generateHeader({ ...baseSkill, model: 'sonnet' });
|
|
186
|
+
assert.match(header, /qwen3-coder(?!-)/);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('haiku translates to qwen3-coder-flash', () => {
|
|
190
|
+
const header = qwen.generateHeader({ ...baseSkill, model: 'haiku' });
|
|
191
|
+
assert.match(header, /qwen3-coder-flash/);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe('gemini adapter model mapping (Gemini family)', () => {
|
|
196
|
+
const gemini = getAdapter('gemini');
|
|
197
|
+
|
|
198
|
+
test('opus translates to gemini-2.5-pro', () => {
|
|
199
|
+
const header = gemini.generateHeader({ ...baseSkill, model: 'opus' });
|
|
200
|
+
assert.match(header, /gemini-2\.5-pro/);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('sonnet translates to gemini-2.5-flash', () => {
|
|
204
|
+
const header = gemini.generateHeader({ ...baseSkill, model: 'sonnet' });
|
|
205
|
+
assert.match(header, /gemini-2\.5-flash/);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test('haiku translates to gemini-2.0-flash-lite', () => {
|
|
209
|
+
const header = gemini.generateHeader({ ...baseSkill, model: 'haiku' });
|
|
210
|
+
assert.match(header, /gemini-2\.0-flash-lite/);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
134
214
|
describe('cross-adapter consistency', () => {
|
|
135
215
|
test('all 5 non-Anthropic adapters emit a model line for opus skills', () => {
|
|
136
216
|
const adapterNames = ['codex', 'antigravity', 'opencode', 'openclaw', 'generic'];
|
|
@@ -4,9 +4,23 @@ import { getAdapter, listPlatforms } from '../adapters/index.js';
|
|
|
4
4
|
|
|
5
5
|
// --- Adapter registry ---
|
|
6
6
|
|
|
7
|
-
test('listPlatforms returns all
|
|
7
|
+
test('listPlatforms returns all 13 platform adapters', () => {
|
|
8
8
|
const adapters = listPlatforms();
|
|
9
|
-
const expected = [
|
|
9
|
+
const expected = [
|
|
10
|
+
'claude',
|
|
11
|
+
'cursor',
|
|
12
|
+
'windsurf',
|
|
13
|
+
'antigravity',
|
|
14
|
+
'codex',
|
|
15
|
+
'opencode',
|
|
16
|
+
'openclaw',
|
|
17
|
+
'generic',
|
|
18
|
+
'aider',
|
|
19
|
+
'copilot',
|
|
20
|
+
'gemini',
|
|
21
|
+
'qoder',
|
|
22
|
+
'qwen',
|
|
23
|
+
];
|
|
10
24
|
for (const name of expected) {
|
|
11
25
|
assert.ok(adapters.includes(name), `missing adapter: ${name}`);
|
|
12
26
|
}
|
|
@@ -35,7 +49,19 @@ const REQUIRED_METHODS = [
|
|
|
35
49
|
|
|
36
50
|
const REQUIRED_PROPS = ['name', 'outputDir', 'fileExtension', 'skillPrefix', 'skillSuffix'];
|
|
37
51
|
|
|
38
|
-
const ADAPTER_NAMES = [
|
|
52
|
+
const ADAPTER_NAMES = [
|
|
53
|
+
'cursor',
|
|
54
|
+
'windsurf',
|
|
55
|
+
'antigravity',
|
|
56
|
+
'codex',
|
|
57
|
+
'opencode',
|
|
58
|
+
'generic',
|
|
59
|
+
'aider',
|
|
60
|
+
'copilot',
|
|
61
|
+
'gemini',
|
|
62
|
+
'qoder',
|
|
63
|
+
'qwen',
|
|
64
|
+
];
|
|
39
65
|
|
|
40
66
|
for (const adapterName of ADAPTER_NAMES) {
|
|
41
67
|
describe(`${adapterName} adapter contract`, () => {
|
|
@@ -107,3 +133,89 @@ test('codex adapter uses skill directories', () => {
|
|
|
107
133
|
assert.strictEqual(codex.useSkillDirectories, true);
|
|
108
134
|
assert.strictEqual(codex.skillFileName, 'SKILL.md');
|
|
109
135
|
});
|
|
136
|
+
|
|
137
|
+
// --- New v2.18 adapters: shape + generateExtraFiles contract ---
|
|
138
|
+
|
|
139
|
+
test('qoder adapter targets .qoder/rules and emits AGENTS.md', async () => {
|
|
140
|
+
const qoder = getAdapter('qoder');
|
|
141
|
+
assert.strictEqual(qoder.outputDir, '.qoder/rules');
|
|
142
|
+
assert.strictEqual(qoder.useSkillDirectories, false);
|
|
143
|
+
assert.strictEqual(typeof qoder.generateExtraFiles, 'function');
|
|
144
|
+
const extras = await qoder.generateExtraFiles({ stats: { skillCount: 5, packCount: 1, files: [] } });
|
|
145
|
+
assert.ok(extras.some((e) => e.path === 'AGENTS.md' && e.content.includes('Rune')));
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('copilot adapter targets .github/instructions with .instructions.md ext', async () => {
|
|
149
|
+
const copilot = getAdapter('copilot');
|
|
150
|
+
assert.strictEqual(copilot.outputDir, '.github/instructions');
|
|
151
|
+
assert.strictEqual(copilot.fileExtension, '.instructions.md');
|
|
152
|
+
const header = copilot.generateHeader({ name: 'cook', layer: 'L1', group: 'orchestrator', description: 'Test' });
|
|
153
|
+
// Per docs.github.com Copilot CLI custom-instructions spec, only `applyTo` is a
|
|
154
|
+
// documented frontmatter field. Description and tier-hint must live in the body.
|
|
155
|
+
assert.ok(header.includes('applyTo: "**"'));
|
|
156
|
+
assert.ok(!/^description:/m.test(header.split('---')[1] || ''), 'description must NOT appear in frontmatter');
|
|
157
|
+
const extras = await copilot.generateExtraFiles({ stats: { skillCount: 5, packCount: 1, files: [] } });
|
|
158
|
+
assert.ok(extras.some((e) => e.path === '.github/copilot-instructions.md'));
|
|
159
|
+
assert.ok(extras.some((e) => e.path === 'AGENTS.md'));
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('codex adapter migrated to generateExtraFiles for AGENTS.md (no longer special-cased in emitter)', async () => {
|
|
163
|
+
const codex = getAdapter('codex');
|
|
164
|
+
assert.strictEqual(typeof codex.generateExtraFiles, 'function');
|
|
165
|
+
const extras = await codex.generateExtraFiles({
|
|
166
|
+
stats: { skillCount: 64, packCount: 14, crossRefsResolved: 287, files: [] },
|
|
167
|
+
});
|
|
168
|
+
const agentsMd = extras.find((e) => e.path === 'AGENTS.md');
|
|
169
|
+
assert.ok(agentsMd, 'codex must emit AGENTS.md via generateExtraFiles');
|
|
170
|
+
assert.ok(agentsMd.content.includes('Rune'));
|
|
171
|
+
assert.ok(agentsMd.content.includes('64 core skills'));
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('emitter rejects absolute paths from generateExtraFiles (path-traversal guard)', async () => {
|
|
175
|
+
// Smoke: verify both qoder and gemini return relative paths only (no absolute).
|
|
176
|
+
for (const name of ['qoder', 'copilot', 'aider', 'qwen', 'gemini', 'codex']) {
|
|
177
|
+
const adapter = getAdapter(name);
|
|
178
|
+
const extras = await adapter.generateExtraFiles({
|
|
179
|
+
stats: { skillCount: 1, packCount: 0, crossRefsResolved: 0, files: ['rune-cook.md'] },
|
|
180
|
+
outputDir: '/tmp/test',
|
|
181
|
+
});
|
|
182
|
+
for (const extra of extras || []) {
|
|
183
|
+
assert.ok(!extra.path.startsWith('/'), `${name} returned absolute path: ${extra.path}`);
|
|
184
|
+
assert.ok(!/^[A-Z]:[\\/]/.test(extra.path), `${name} returned Windows absolute path: ${extra.path}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('aider adapter emits .aider.conf.yml with read array', async () => {
|
|
190
|
+
const aider = getAdapter('aider');
|
|
191
|
+
assert.strictEqual(aider.outputDir, 'aider/rules');
|
|
192
|
+
const extras = await aider.generateExtraFiles({
|
|
193
|
+
stats: { skillCount: 2, packCount: 0, files: ['rune-cook.md', 'rune-fix.md', 'index.md'] },
|
|
194
|
+
});
|
|
195
|
+
const conf = extras.find((e) => e.path === '.aider.conf.yml');
|
|
196
|
+
assert.ok(conf);
|
|
197
|
+
assert.ok(conf.content.includes('read:'));
|
|
198
|
+
assert.ok(conf.content.includes('aider/rules/rune-cook.md'));
|
|
199
|
+
assert.ok(conf.content.includes('aider/rules/rune-fix.md'));
|
|
200
|
+
// index.md must NOT be in the read array
|
|
201
|
+
assert.ok(!conf.content.includes('aider/rules/index.md'));
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test('qwen adapter emits QWEN.md with @import lines', async () => {
|
|
205
|
+
const qwen = getAdapter('qwen');
|
|
206
|
+
assert.strictEqual(qwen.outputDir, 'qwen/skills');
|
|
207
|
+
const extras = await qwen.generateExtraFiles({
|
|
208
|
+
stats: { skillCount: 2, packCount: 0, files: ['rune-cook.md', 'rune-fix.md'] },
|
|
209
|
+
});
|
|
210
|
+
const qwenMd = extras.find((e) => e.path === 'QWEN.md');
|
|
211
|
+
assert.ok(qwenMd);
|
|
212
|
+
assert.ok(qwenMd.content.includes('@qwen/skills/rune-cook.md'));
|
|
213
|
+
assert.ok(qwenMd.content.includes('@qwen/skills/rune-fix.md'));
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test('gemini adapter declares generateExtraFiles for bundled GEMINI.md', () => {
|
|
217
|
+
const gemini = getAdapter('gemini');
|
|
218
|
+
assert.strictEqual(gemini.outputDir, 'gemini/skills');
|
|
219
|
+
assert.strictEqual(typeof gemini.generateExtraFiles, 'function');
|
|
220
|
+
// Full bundle test happens in pipeline test (needs filesystem) — contract checked here.
|
|
221
|
+
});
|
|
@@ -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
|
|