@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.
Files changed (48) hide show
  1. package/README.md +68 -16
  2. package/compiler/__tests__/adapter-model-mapping.test.js +80 -0
  3. package/compiler/__tests__/adapters.test.js +115 -3
  4. package/compiler/__tests__/doctor-mesh.test.js +5 -0
  5. package/compiler/__tests__/hooks-drift.test.js +156 -0
  6. package/compiler/__tests__/hooks-merge.test.js +2 -1
  7. package/compiler/__tests__/setup.test.js +152 -0
  8. package/compiler/adapters/aider.js +120 -0
  9. package/compiler/adapters/codex.js +33 -0
  10. package/compiler/adapters/copilot.js +126 -0
  11. package/compiler/adapters/gemini.js +131 -0
  12. package/compiler/adapters/index.js +10 -0
  13. package/compiler/adapters/openclaw.js +1 -1
  14. package/compiler/adapters/qoder.js +102 -0
  15. package/compiler/adapters/qwen.js +111 -0
  16. package/compiler/bin/rune.js +65 -4
  17. package/compiler/commands/hooks/drift.js +170 -0
  18. package/compiler/commands/hooks/presets.js +11 -1
  19. package/compiler/commands/setup.js +242 -0
  20. package/compiler/doctor.js +48 -2
  21. package/compiler/emitter.js +38 -41
  22. package/compiler/transforms/branding.js +1 -1
  23. package/compiler/transforms/hooks.js +6 -0
  24. package/hooks/hooks.json +10 -0
  25. package/hooks/quarantine/index.cjs +256 -0
  26. package/hooks/session-start/index.cjs +91 -0
  27. package/package.json +2 -2
  28. package/skills/asset-creator/SKILL.md +1 -1
  29. package/skills/audit/SKILL.md +20 -2
  30. package/skills/autopsy/SKILL.md +173 -2
  31. package/skills/brainstorm/SKILL.md +24 -1
  32. package/skills/browser-pilot/SKILL.md +16 -1
  33. package/skills/debug/SKILL.md +4 -2
  34. package/skills/deploy/SKILL.md +72 -2
  35. package/skills/design/SKILL.md +50 -3
  36. package/skills/integrity-check/SKILL.md +2 -0
  37. package/skills/launch/SKILL.md +11 -1
  38. package/skills/marketing/SKILL.md +1 -1
  39. package/skills/neural-memory/SKILL.md +1 -1
  40. package/skills/perf/SKILL.md +93 -2
  41. package/skills/quarantine/SKILL.md +173 -0
  42. package/skills/quarantine/references/quarantine-discipline.md +97 -0
  43. package/skills/quarantine/references/trusted-mcp-allowlist.md +77 -0
  44. package/skills/sentinel/SKILL.md +2 -1
  45. package/skills/sentinel-env/SKILL.md +2 -2
  46. package/skills/skill-forge/SKILL.md +47 -1
  47. package/skills/surgeon/SKILL.md +1 -1
  48. package/skills/team/SKILL.md +27 -1
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Qoder Adapter
3
+ *
4
+ * Emits per-skill rule files into .qoder/rules/ — Qoder's documented project-level
5
+ * rule directory. Qoder also reads AGENTS.md as its project-context file.
6
+ *
7
+ * Qoder rules dir: .qoder/rules/
8
+ * Qoder rule file: .qoder/rules/rune-{name}.md (one file per skill)
9
+ * Qoder project context: AGENTS.md (open AGENTS.md standard)
10
+ *
11
+ * @see https://docs.qoder.com/user-guide/rules
12
+ * @see https://agents.md/
13
+ *
14
+ * MODEL TIER MAPPING (v2.18+):
15
+ * Qoder is provider-agnostic — emits semantic tier hints rather than concrete
16
+ * model names. Qoder IDE resolves the tier to its configured provider model.
17
+ */
18
+
19
+ import { BRANDING_FOOTER } from '../transforms/branding.js';
20
+
21
+ const MODEL_MAP = {
22
+ opus: 'tier:heavy',
23
+ sonnet: 'tier:mid',
24
+ haiku: 'tier:light',
25
+ };
26
+
27
+ const TOOL_MAP = {
28
+ Read: 'read the file',
29
+ Write: 'write/create the file',
30
+ Edit: 'edit the file',
31
+ Glob: 'find files by pattern',
32
+ Grep: 'search file contents',
33
+ Bash: 'run a shell command',
34
+ TodoWrite: 'track task progress',
35
+ Skill: 'follow the referenced rune-{name} rule',
36
+ Agent: 'execute the workflow',
37
+ };
38
+
39
+ export default {
40
+ name: 'qoder',
41
+ outputDir: '.qoder/rules',
42
+ fileExtension: '.md',
43
+ skillPrefix: 'rune-',
44
+ skillSuffix: '',
45
+
46
+ // Qoder rules are flat .md files, not directory-per-skill
47
+ useSkillDirectories: false,
48
+
49
+ transformReference(skillName, raw) {
50
+ const isBackticked = raw.startsWith('`') && raw.endsWith('`');
51
+ const ref = `the rune-${skillName} rule`;
52
+ return isBackticked ? `\`${ref}\`` : ref;
53
+ },
54
+
55
+ transformToolName(toolName) {
56
+ return TOOL_MAP[toolName] || toolName;
57
+ },
58
+
59
+ generateHeader(skill) {
60
+ const desc = (skill.description || '').replace(/"/g, '\\"');
61
+ const lines = ['---', `name: rune-${skill.name}`, `description: "${desc}"`];
62
+ const translatedModel = skill.model ? MODEL_MAP[skill.model] || skill.model : null;
63
+ if (translatedModel) lines.push(`model: ${translatedModel}`);
64
+ lines.push('---', '', '');
65
+ return lines.join('\n');
66
+ },
67
+
68
+ generateFooter() {
69
+ return BRANDING_FOOTER;
70
+ },
71
+
72
+ transformSubagentInstruction(text) {
73
+ return text;
74
+ },
75
+
76
+ scriptsDir(skillName) {
77
+ return `rune-${skillName}-scripts`;
78
+ },
79
+
80
+ postProcess(content) {
81
+ return content.replace(/^context: fork\n/gm, '').replace(/^agent: general-purpose\n/gm, '');
82
+ },
83
+
84
+ // Qoder reads AGENTS.md at the project root as its high-level context file.
85
+ generateExtraFiles({ stats }) {
86
+ const agentsMd = [
87
+ '# Rune — Project Configuration',
88
+ '',
89
+ 'Rune is an interconnected skill ecosystem for AI coding assistants.',
90
+ `${stats.skillCount} core skills + ${stats.packCount} extension packs.`,
91
+ '',
92
+ 'Per-skill rules live under `.qoder/rules/rune-<name>.md`. Qoder loads them automatically.',
93
+ '',
94
+ 'Reference a skill by name (e.g. "follow the rune-cook rule") inside any chat — the rule file is auto-injected.',
95
+ '',
96
+ '---',
97
+ '> Rune Skill Mesh — https://github.com/rune-kit/rune',
98
+ '',
99
+ ].join('\n');
100
+ return [{ path: 'AGENTS.md', content: agentsMd }];
101
+ },
102
+ };
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Qwen Coder Adapter
3
+ *
4
+ * Emits per-skill rule files into qwen/skills/ and a top-level QWEN.md that
5
+ * uses Qwen Code's @import syntax to load each rule file. Qwen Code loads
6
+ * QWEN.md hierarchically (cwd → parent → ~/.qwen/QWEN.md).
7
+ *
8
+ * Qwen rules dir: qwen/skills/
9
+ * Qwen rule file: qwen/skills/rune-{name}.md
10
+ * Qwen project context: QWEN.md at project root (with @path imports)
11
+ *
12
+ * @see https://qwenlm.github.io/qwen-code-docs/en/users/configuration/settings/
13
+ * @see https://github.com/QwenLM/qwen-code
14
+ *
15
+ * MODEL TIER MAPPING (v2.18+):
16
+ * Qwen Coder defaults to Qwen3-Coder family. Anthropic-style tier names
17
+ * translate to Qwen size hints (heavy=qwen3-coder-plus, mid=qwen3-coder,
18
+ * light=qwen3-coder-flash). Hint only — Qwen Code reads model from settings.
19
+ */
20
+
21
+ import { BRANDING_FOOTER } from '../transforms/branding.js';
22
+
23
+ const MODEL_MAP = {
24
+ opus: 'qwen3-coder-plus',
25
+ sonnet: 'qwen3-coder',
26
+ haiku: 'qwen3-coder-flash',
27
+ };
28
+
29
+ const TOOL_MAP = {
30
+ Read: 'read the file',
31
+ Write: 'write/create the file',
32
+ Edit: 'edit the file',
33
+ Glob: 'find files by pattern',
34
+ Grep: 'search file contents',
35
+ Bash: 'run a shell command',
36
+ TodoWrite: 'track task progress',
37
+ Skill: 'follow the imported rune-{name} skill',
38
+ Agent: 'execute the workflow',
39
+ };
40
+
41
+ export default {
42
+ name: 'qwen',
43
+ outputDir: 'qwen/skills',
44
+ fileExtension: '.md',
45
+ skillPrefix: 'rune-',
46
+ skillSuffix: '',
47
+
48
+ // Qwen skills are flat per-skill markdown files imported from QWEN.md.
49
+ useSkillDirectories: false,
50
+
51
+ transformReference(skillName, raw) {
52
+ const isBackticked = raw.startsWith('`') && raw.endsWith('`');
53
+ const ref = `the rune-${skillName} skill`;
54
+ return isBackticked ? `\`${ref}\`` : ref;
55
+ },
56
+
57
+ transformToolName(toolName) {
58
+ return TOOL_MAP[toolName] || toolName;
59
+ },
60
+
61
+ generateHeader(skill) {
62
+ const desc = (skill.description || '').replace(/"/g, '\\"');
63
+ const lines = ['---', `name: rune-${skill.name}`, `description: "${desc}"`];
64
+ const translatedModel = skill.model ? MODEL_MAP[skill.model] || skill.model : null;
65
+ if (translatedModel) lines.push(`# model-hint: ${translatedModel}`);
66
+ lines.push('---', '', '');
67
+ return lines.join('\n');
68
+ },
69
+
70
+ generateFooter() {
71
+ return BRANDING_FOOTER;
72
+ },
73
+
74
+ transformSubagentInstruction(text) {
75
+ return text;
76
+ },
77
+
78
+ scriptsDir(skillName) {
79
+ return `rune-${skillName}-scripts`;
80
+ },
81
+
82
+ postProcess(content) {
83
+ return content.replace(/^context: fork\n/gm, '').replace(/^agent: general-purpose\n/gm, '');
84
+ },
85
+
86
+ // Emit QWEN.md at project root with @import lines for every per-skill file.
87
+ generateExtraFiles({ stats }) {
88
+ const skillFiles = stats.files
89
+ .filter((f) => f.startsWith('rune-') && f.endsWith('.md') && !f.includes('/') && !f.includes('\\'))
90
+ .sort();
91
+
92
+ const qwenMd = [
93
+ '# Rune — Project Configuration',
94
+ '',
95
+ 'Rune is an interconnected skill ecosystem for AI coding assistants.',
96
+ `${stats.skillCount} core skills + ${stats.packCount} extension packs.`,
97
+ '',
98
+ '## Loaded Skills',
99
+ '',
100
+ 'Qwen Code loads each referenced file as part of this project context.',
101
+ '',
102
+ ...skillFiles.map((f) => `@qwen/skills/${f}`),
103
+ '',
104
+ '---',
105
+ '> Rune Skill Mesh — https://github.com/rune-kit/rune',
106
+ '',
107
+ ].join('\n');
108
+
109
+ return [{ path: 'QWEN.md', content: qwenMd }];
110
+ },
111
+ };
@@ -13,15 +13,18 @@
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';
19
20
  import { getAdapter, listPlatforms } from '../adapters/index.js';
20
21
  import { getAllAnalytics } from '../analytics.js';
21
22
  import { dispatchHook } from '../commands/hook-dispatch.js';
23
+ import { checkHookDrift, formatHookDriftResult } from '../commands/hooks/drift.js';
22
24
  import { installHooks } from '../commands/hooks/install.js';
23
25
  import { hookStatus } from '../commands/hooks/status.js';
24
26
  import { uninstallHooks } from '../commands/hooks/uninstall.js';
27
+ import { formatSetupResult, runSetup } from '../commands/setup.js';
25
28
  import { generateDashboardHTML } from '../dashboard.js';
26
29
  import { checkMeshIntegrity, formatDoctorResults, formatMeshResults, runDoctor } from '../doctor.js';
27
30
  import { buildAll } from '../emitter.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,22 @@ 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(
724
+ ' setup Interactive wizard — auto-detect tiers, pick scope, install hooks (recommended for first-time)',
725
+ );
726
+ log(' [--here|--global] [--tier pro,business] [--preset gentle|strict] [--dry]');
727
+ log(' init Interactive setup for build pipeline (auto-detects platform)');
668
728
  log(' build Compile skills for configured platform');
669
729
  log(' doctor Validate compiled output + mesh integrity');
670
730
  log(' --mesh Mesh integrity only (reciprocals, versions, sections)');
731
+ log(' --hooks Hook drift report — compare installed vs canonical preset (reporter, exit 0)');
671
732
  log(' --strict Fail on warnings (for CI)');
672
733
  log(' status Project dashboard (skills, signals, packs, health)');
673
734
  log(' visualize Interactive mesh graph (opens in browser)');
674
735
  log(' analytics Usage analytics dashboard (Business tier)');
675
736
  log(' hooks Install/uninstall/status for multi-platform auto-discipline');
676
737
  log(
677
- ' hooks install [--preset gentle|strict|off] [--platform claude|cursor|windsurf|antigravity|all]',
738
+ ' hooks install [--preset gentle|strict|off] [--platform claude|cursor|windsurf|antigravity|all] [--global]',
678
739
  );
679
740
  log(' hooks uninstall [--platform <name>|all]');
680
741
  log(' hooks status [--platform <name>|all]');
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Hook drift reporter.
3
+ *
4
+ * Compares Rune-managed Free-preset entries in `.claude/settings.json` against
5
+ * the canonical `buildPreset()` output for the detected preset. Flags missing
6
+ * entries (preset wired more than what's installed) and drifted entries
7
+ * (installed command differs from canonical shape).
8
+ *
9
+ * Reporter, NOT a gate — exit 0 always. Operators legitimately edit settings.json
10
+ * (custom user hooks alongside Rune hooks). Auto-fix would be hostile.
11
+ *
12
+ * Scope: Free preset only. Tier-emitted entries (`${RUNE_PRO_ROOT}` /
13
+ * `${RUNE_BUSINESS_ROOT}`) are filtered out — those are checked separately
14
+ * by tier doctor (out of scope for v2.17.0).
15
+ *
16
+ * Use case: diagnostic before users file "skill is broken" issues. Local drift
17
+ * is a common cause of unexplained hook behavior — a check that points at the
18
+ * actual deviation saves a round-trip with the maintainer.
19
+ */
20
+
21
+ import { existsSync } from 'node:fs';
22
+ import { readFile } from 'node:fs/promises';
23
+ import path from 'node:path';
24
+ import { detectPlatforms } from '../../adapters/hooks/index.js';
25
+ import { detectPreset } from './merge.js';
26
+ import { buildPreset, isRuneManaged, SETTINGS_REL_PATH } from './presets.js';
27
+
28
+ const TIER_ENV_RE = /\$\{RUNE_[A-Z][A-Z0-9_]*_ROOT\}/;
29
+
30
+ /**
31
+ * @param {string} projectRoot
32
+ * @returns {Promise<{findings: Array, summary: object, platforms: string[]}>}
33
+ */
34
+ export async function checkHookDrift(projectRoot) {
35
+ const platforms = detectPlatforms(projectRoot);
36
+ const findings = [];
37
+ const checked = [];
38
+
39
+ for (const id of platforms) {
40
+ if (id !== 'claude') continue; // Settings drift is Claude-specific
41
+ const settingsPath = path.join(projectRoot, SETTINGS_REL_PATH);
42
+ if (!existsSync(settingsPath)) continue;
43
+
44
+ let settings;
45
+ try {
46
+ const raw = await readFile(settingsPath, 'utf-8');
47
+ settings = raw.trim() ? JSON.parse(raw) : {};
48
+ } catch (err) {
49
+ findings.push({
50
+ platform: id,
51
+ event: null,
52
+ status: 'error',
53
+ message: `Cannot parse ${SETTINGS_REL_PATH}: ${err.message}`,
54
+ });
55
+ continue;
56
+ }
57
+
58
+ const preset = detectPreset(settings);
59
+ if (preset === 'none') continue; // No Rune hooks installed
60
+ if (preset === 'mixed') {
61
+ findings.push({
62
+ platform: id,
63
+ event: null,
64
+ status: 'mixed-preset',
65
+ message:
66
+ 'settings.json contains both gentle and strict Rune entries — re-run `rune hooks install --preset <gentle|strict>` to converge',
67
+ });
68
+ }
69
+
70
+ // For mixed preset, fall back to gentle for canonical comparison
71
+ const canonicalPreset = preset === 'mixed' ? 'gentle' : preset;
72
+ const canonical = buildPreset(canonicalPreset);
73
+ checked.push({ platform: id, preset });
74
+
75
+ for (const event of ['PreToolUse', 'PostToolUse', 'Stop']) {
76
+ const actualCommands = collectFreePresetCommands(settings.hooks?.[event] || []);
77
+ const canonicalCommands = collectFreePresetCommands(canonical.hooks?.[event] || []);
78
+
79
+ // Missing: canonical entry not present in actual
80
+ for (const cmd of canonicalCommands) {
81
+ if (!actualCommands.includes(cmd)) {
82
+ findings.push({ platform: id, event, status: 'missing', expected: cmd });
83
+ }
84
+ }
85
+
86
+ // Drift: actual entry not in canonical (extra or modified)
87
+ for (const cmd of actualCommands) {
88
+ if (!canonicalCommands.includes(cmd)) {
89
+ findings.push({ platform: id, event, status: 'drift', actual: cmd });
90
+ }
91
+ }
92
+ }
93
+ }
94
+
95
+ const summary = {
96
+ drifted: findings.filter((f) => f.status === 'drift').length,
97
+ missing: findings.filter((f) => f.status === 'missing').length,
98
+ errors: findings.filter((f) => f.status === 'error' || f.status === 'mixed-preset').length,
99
+ };
100
+
101
+ return { findings, summary, platforms: checked };
102
+ }
103
+
104
+ /**
105
+ * Pull Rune-managed Free-preset commands out of a matcher-group list.
106
+ * Tier-emitted entries (`${RUNE_*_ROOT}/...`) are excluded — those are tier-managed,
107
+ * not Free-preset, and have their own check path.
108
+ */
109
+ function collectFreePresetCommands(matcherGroups) {
110
+ const cmds = [];
111
+ if (!Array.isArray(matcherGroups)) return cmds;
112
+ for (const group of matcherGroups) {
113
+ if (!Array.isArray(group?.hooks)) continue;
114
+ for (const entry of group.hooks) {
115
+ if (!isRuneManaged(entry)) continue;
116
+ if (TIER_ENV_RE.test(entry.command)) continue; // tier entry — not our concern here
117
+ cmds.push(entry.command);
118
+ }
119
+ }
120
+ return cmds;
121
+ }
122
+
123
+ /**
124
+ * Format drift result as human-readable lines.
125
+ *
126
+ * @param {Awaited<ReturnType<typeof checkHookDrift>>} result
127
+ * @returns {string}
128
+ */
129
+ export function formatHookDriftResult(result) {
130
+ const lines = [];
131
+ lines.push(' Hook Drift Report');
132
+ lines.push(' ──────────────────');
133
+
134
+ if (result.platforms.length === 0) {
135
+ lines.push('');
136
+ lines.push(' ℹ No installed Rune hooks detected on Claude. Run `rune hooks install --preset gentle` first.');
137
+ return lines.join('\n');
138
+ }
139
+
140
+ for (const { platform, preset } of result.platforms) {
141
+ lines.push(` Platform: ${platform} (preset: ${preset})`);
142
+ }
143
+ lines.push('');
144
+
145
+ if (result.findings.length === 0) {
146
+ lines.push(' ✓ All Rune-managed entries match canonical preset.');
147
+ return lines.join('\n');
148
+ }
149
+
150
+ for (const f of result.findings) {
151
+ if (f.status === 'mixed-preset' || f.status === 'error') {
152
+ lines.push(` ⚠ ${f.platform}: ${f.message}`);
153
+ } else if (f.status === 'missing') {
154
+ lines.push(` ✗ ${f.platform} ${f.event}: missing canonical entry`);
155
+ lines.push(` expected: ${f.expected}`);
156
+ } else if (f.status === 'drift') {
157
+ lines.push(` ✗ ${f.platform} ${f.event}: drift — installed command not in canonical preset`);
158
+ lines.push(` actual: ${f.actual}`);
159
+ }
160
+ }
161
+
162
+ lines.push('');
163
+ lines.push(
164
+ ` Summary: ${result.summary.drifted} drifted, ${result.summary.missing} missing, ${result.summary.errors} error(s).`,
165
+ );
166
+ lines.push(' Resolution: re-run `rune hooks install --preset <gentle|strict>` to re-converge.');
167
+ lines.push(' This is a reporter — operator decides what to do with the findings.');
168
+
169
+ return lines.join('\n');
170
+ }
@@ -81,6 +81,16 @@ export function buildPreset(preset) {
81
81
  },
82
82
  ],
83
83
  },
84
+ {
85
+ matcher: 'mcp__.*|WebFetch|Read',
86
+ hooks: [
87
+ {
88
+ type: 'command',
89
+ command: `${DISPATCH_CMD} quarantine${flag}`,
90
+ async: true,
91
+ },
92
+ ],
93
+ },
84
94
  ],
85
95
  Stop: [
86
96
  {
@@ -101,7 +111,7 @@ export function buildPreset(preset) {
101
111
  /**
102
112
  * Skills wired by presets — used by `rune hooks status` to verify skill existence.
103
113
  */
104
- export const WIRED_SKILLS = ['preflight', 'sentinel', 'dependency-doctor', 'completion-gate'];
114
+ export const WIRED_SKILLS = ['preflight', 'sentinel', 'dependency-doctor', 'completion-gate', 'quarantine'];
105
115
 
106
116
  /**
107
117
  * Detect if a hook command entry is Rune-managed.