@rune-kit/rune 2.10.0 → 2.12.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 (240) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +65 -6
  3. package/commands/rune.md +168 -168
  4. package/compiler/__tests__/detect-invariants.test.js +136 -0
  5. package/compiler/__tests__/doctor-mesh.test.js +229 -0
  6. package/compiler/__tests__/hook-dispatch.test.js +91 -0
  7. package/compiler/__tests__/hooks-antigravity.test.js +118 -0
  8. package/compiler/__tests__/hooks-cursor.test.js +139 -0
  9. package/compiler/__tests__/hooks-install.test.js +305 -0
  10. package/compiler/__tests__/hooks-merge.test.js +204 -0
  11. package/compiler/__tests__/hooks-tiers.test.js +519 -0
  12. package/compiler/__tests__/hooks-windsurf.test.js +115 -0
  13. package/compiler/__tests__/inject-claude-md.test.js +152 -0
  14. package/compiler/__tests__/load-invariants.test.js +408 -0
  15. package/compiler/__tests__/onboard-invariants.test.js +240 -0
  16. package/compiler/adapters/hooks/antigravity.js +140 -0
  17. package/compiler/adapters/hooks/claude.js +166 -0
  18. package/compiler/adapters/hooks/cursor.js +191 -0
  19. package/compiler/adapters/hooks/index.js +82 -0
  20. package/compiler/adapters/hooks/tier-emitter.js +182 -0
  21. package/compiler/adapters/hooks/windsurf.js +202 -0
  22. package/compiler/bin/rune.js +196 -6
  23. package/compiler/commands/hook-dispatch.js +87 -0
  24. package/compiler/commands/hooks/install.js +120 -0
  25. package/compiler/commands/hooks/merge.js +211 -0
  26. package/compiler/commands/hooks/presets.js +116 -0
  27. package/compiler/commands/hooks/status.js +112 -0
  28. package/compiler/commands/hooks/tiers.js +221 -0
  29. package/compiler/commands/hooks/uninstall.js +94 -0
  30. package/compiler/doctor.js +236 -0
  31. package/contexts/dev.md +34 -34
  32. package/contexts/research.md +43 -43
  33. package/contexts/review.md +55 -55
  34. package/extensions/ai-ml/PACK.md +88 -88
  35. package/extensions/ai-ml/skills/ai-agents.md +172 -172
  36. package/extensions/ai-ml/skills/code-sandbox.md +187 -187
  37. package/extensions/ai-ml/skills/deep-research.md +146 -146
  38. package/extensions/ai-ml/skills/embedding-search.md +66 -66
  39. package/extensions/ai-ml/skills/fine-tuning-guide.md +74 -74
  40. package/extensions/ai-ml/skills/llm-architect.md +125 -125
  41. package/extensions/ai-ml/skills/llm-integration.md +64 -64
  42. package/extensions/ai-ml/skills/prompt-patterns.md +72 -72
  43. package/extensions/ai-ml/skills/rag-patterns.md +66 -66
  44. package/extensions/ai-ml/skills/web-extraction.md +114 -114
  45. package/extensions/analytics/PACK.md +92 -92
  46. package/extensions/analytics/skills/ab-testing.md +72 -72
  47. package/extensions/analytics/skills/dashboard-patterns.md +83 -83
  48. package/extensions/analytics/skills/data-validation.md +68 -68
  49. package/extensions/analytics/skills/funnel-analysis.md +81 -81
  50. package/extensions/analytics/skills/sql-patterns.md +57 -57
  51. package/extensions/analytics/skills/statistical-analysis.md +79 -79
  52. package/extensions/analytics/skills/tracking-setup.md +71 -71
  53. package/extensions/backend/PACK.md +104 -104
  54. package/extensions/backend/skills/api-patterns.md +84 -84
  55. package/extensions/backend/skills/async-pipeline.md +193 -193
  56. package/extensions/backend/skills/auth-patterns.md +97 -97
  57. package/extensions/backend/skills/background-jobs.md +133 -133
  58. package/extensions/backend/skills/caching-patterns.md +108 -108
  59. package/extensions/backend/skills/cli-generation.md +133 -133
  60. package/extensions/backend/skills/database-patterns.md +87 -87
  61. package/extensions/backend/skills/middleware-patterns.md +104 -104
  62. package/extensions/chrome-ext/PACK.md +93 -93
  63. package/extensions/chrome-ext/skills/cws-preflight.md +143 -143
  64. package/extensions/chrome-ext/skills/cws-publish.md +104 -104
  65. package/extensions/chrome-ext/skills/ext-ai-integration.md +251 -251
  66. package/extensions/chrome-ext/skills/ext-messaging.md +139 -139
  67. package/extensions/chrome-ext/skills/ext-storage.md +133 -133
  68. package/extensions/chrome-ext/skills/mv3-scaffold.md +164 -164
  69. package/extensions/content/PACK.md +96 -96
  70. package/extensions/content/skills/blog-patterns.md +88 -88
  71. package/extensions/content/skills/cms-integration.md +131 -131
  72. package/extensions/content/skills/content-scoring.md +107 -107
  73. package/extensions/content/skills/i18n.md +83 -83
  74. package/extensions/content/skills/mdx-authoring.md +137 -137
  75. package/extensions/content/skills/reference.md +1014 -1014
  76. package/extensions/content/skills/seo-patterns.md +67 -67
  77. package/extensions/content/skills/video-repurpose.md +153 -153
  78. package/extensions/devops/PACK.md +101 -101
  79. package/extensions/devops/skills/chaos-testing.md +67 -67
  80. package/extensions/devops/skills/ci-cd.md +75 -75
  81. package/extensions/devops/skills/docker.md +58 -58
  82. package/extensions/devops/skills/edge-serverless.md +163 -163
  83. package/extensions/devops/skills/infra-as-code.md +158 -158
  84. package/extensions/devops/skills/kubernetes.md +110 -110
  85. package/extensions/devops/skills/monitoring.md +57 -57
  86. package/extensions/devops/skills/server-setup.md +64 -64
  87. package/extensions/devops/skills/ssl-domain.md +42 -42
  88. package/extensions/ecommerce/PACK.md +116 -116
  89. package/extensions/ecommerce/skills/cart-system.md +79 -79
  90. package/extensions/ecommerce/skills/inventory-mgmt.md +102 -102
  91. package/extensions/ecommerce/skills/order-management.md +126 -126
  92. package/extensions/ecommerce/skills/payment-integration.md +472 -472
  93. package/extensions/ecommerce/skills/shopify-dev.md +69 -69
  94. package/extensions/ecommerce/skills/subscription-billing.md +93 -93
  95. package/extensions/ecommerce/skills/tax-compliance.md +117 -117
  96. package/extensions/gamedev/PACK.md +142 -142
  97. package/extensions/gamedev/skills/asset-pipeline.md +74 -74
  98. package/extensions/gamedev/skills/audio-system.md +129 -129
  99. package/extensions/gamedev/skills/camera-system.md +87 -87
  100. package/extensions/gamedev/skills/ecs.md +98 -98
  101. package/extensions/gamedev/skills/game-loops.md +72 -72
  102. package/extensions/gamedev/skills/input-system.md +199 -199
  103. package/extensions/gamedev/skills/multiplayer.md +180 -180
  104. package/extensions/gamedev/skills/particles.md +105 -105
  105. package/extensions/gamedev/skills/physics-engine.md +89 -89
  106. package/extensions/gamedev/skills/scene-management.md +146 -146
  107. package/extensions/gamedev/skills/threejs-patterns.md +90 -90
  108. package/extensions/gamedev/skills/webgl.md +71 -71
  109. package/extensions/mobile/PACK.md +106 -106
  110. package/extensions/mobile/skills/app-store-connect.md +152 -152
  111. package/extensions/mobile/skills/app-store-prep.md +66 -66
  112. package/extensions/mobile/skills/deep-linking.md +109 -109
  113. package/extensions/mobile/skills/flutter.md +60 -60
  114. package/extensions/mobile/skills/ios-build-pipeline.md +142 -142
  115. package/extensions/mobile/skills/native-bridge.md +66 -66
  116. package/extensions/mobile/skills/ota-updates.md +97 -97
  117. package/extensions/mobile/skills/push-notifications.md +111 -111
  118. package/extensions/mobile/skills/react-native.md +82 -82
  119. package/extensions/saas/PACK.md +116 -116
  120. package/extensions/saas/skills/billing-integration.md +200 -200
  121. package/extensions/saas/skills/feature-flags.md +130 -130
  122. package/extensions/saas/skills/multi-tenant.md +103 -103
  123. package/extensions/saas/skills/onboarding-flow.md +139 -139
  124. package/extensions/saas/skills/subscription-flow.md +95 -95
  125. package/extensions/saas/skills/team-management.md +144 -144
  126. package/extensions/security/PACK.md +99 -99
  127. package/extensions/security/skills/api-security.md +140 -140
  128. package/extensions/security/skills/compliance.md +68 -68
  129. package/extensions/security/skills/owasp-audit.md +64 -64
  130. package/extensions/security/skills/pentest-patterns.md +77 -77
  131. package/extensions/security/skills/secret-mgmt.md +65 -65
  132. package/extensions/security/skills/supply-chain.md +65 -65
  133. package/extensions/trading/PACK.md +80 -80
  134. package/extensions/trading/skills/chart-components.md +55 -55
  135. package/extensions/trading/skills/experiment-loop.md +125 -125
  136. package/extensions/trading/skills/fintech-patterns.md +47 -47
  137. package/extensions/trading/skills/indicator-library.md +58 -58
  138. package/extensions/trading/skills/quant-analysis.md +111 -111
  139. package/extensions/trading/skills/realtime-data.md +58 -58
  140. package/extensions/trading/skills/trade-logic.md +104 -104
  141. package/extensions/ui/PACK.md +130 -130
  142. package/extensions/ui/skills/a11y-audit.md +91 -91
  143. package/extensions/ui/skills/animation-patterns.md +127 -127
  144. package/extensions/ui/skills/component-patterns.md +100 -100
  145. package/extensions/ui/skills/design-decision.md +108 -108
  146. package/extensions/ui/skills/design-system.md +68 -68
  147. package/extensions/ui/skills/landing-patterns.md +155 -155
  148. package/extensions/ui/skills/palette-picker.md +173 -173
  149. package/extensions/ui/skills/react-health.md +90 -90
  150. package/extensions/ui/skills/type-system.md +125 -125
  151. package/extensions/ui/skills/web-vitals.md +153 -153
  152. package/extensions/zalo/PACK.md +145 -145
  153. package/extensions/zalo/skills/zalo-oa-mcp.md +317 -317
  154. package/extensions/zalo/skills/zalo-oa-messaging.md +429 -429
  155. package/extensions/zalo/skills/zalo-oa-setup.md +236 -236
  156. package/extensions/zalo/skills/zalo-oa-webhook.md +189 -189
  157. package/extensions/zalo/skills/zalo-personal-messaging.md +194 -194
  158. package/extensions/zalo/skills/zalo-personal-setup.md +153 -153
  159. package/extensions/zalo/skills/zalo-rate-guard.md +219 -219
  160. package/hooks/auto-format/index.cjs +48 -48
  161. package/hooks/hooks.json +111 -111
  162. package/hooks/post-session-reflect/index.cjs +189 -189
  163. package/hooks/pre-compact/index.cjs +95 -95
  164. package/hooks/run-hook.cmd +1 -1
  165. package/hooks/secrets-scan/index.cjs +100 -100
  166. package/hooks/session-start/index.cjs +71 -71
  167. package/hooks/typecheck/index.cjs +65 -65
  168. package/package.json +63 -63
  169. package/references/ui-pro-max-data/LICENSE-UI-PRO-MAX +21 -21
  170. package/references/ui-pro-max-data/charts.csv +26 -26
  171. package/references/ui-pro-max-data/colors.csv +161 -161
  172. package/references/ui-pro-max-data/styles.csv +68 -68
  173. package/references/ui-pro-max-data/typography.csv +74 -74
  174. package/references/ui-pro-max-data/ui-reasoning.csv +162 -162
  175. package/references/ui-pro-max-data/ux-guidelines.csv +99 -99
  176. package/skills/adversary/SKILL.md +283 -283
  177. package/skills/asset-creator/SKILL.md +157 -157
  178. package/skills/audit/SKILL.md +147 -2
  179. package/skills/autopsy/SKILL.md +335 -335
  180. package/skills/ba/SKILL.md +85 -1
  181. package/skills/brainstorm/SKILL.md +380 -342
  182. package/skills/browser-pilot/SKILL.md +169 -168
  183. package/skills/constraint-check/SKILL.md +165 -165
  184. package/skills/context-engine/SKILL.md +408 -404
  185. package/skills/cook/SKILL.md +917 -863
  186. package/skills/db/SKILL.md +273 -273
  187. package/skills/debug/SKILL.md +465 -465
  188. package/skills/dependency-doctor/SKILL.md +265 -235
  189. package/skills/deploy/SKILL.md +274 -231
  190. package/skills/design/DESIGN-REFERENCE.md +365 -365
  191. package/skills/design/SKILL.md +590 -589
  192. package/skills/doc-processor/SKILL.md +254 -254
  193. package/skills/docs/SKILL.md +374 -374
  194. package/skills/docs-seeker/SKILL.md +178 -177
  195. package/skills/fix/SKILL.md +332 -330
  196. package/skills/git/SKILL.md +339 -339
  197. package/skills/hallucination-guard/SKILL.md +220 -219
  198. package/skills/incident/SKILL.md +254 -253
  199. package/skills/integrity-check/SKILL.md +169 -169
  200. package/skills/journal/SKILL.md +241 -240
  201. package/skills/launch/SKILL.md +344 -344
  202. package/skills/logic-guardian/SKILL.md +269 -251
  203. package/skills/marketing/SKILL.md +351 -289
  204. package/skills/mcp-builder/SKILL.md +425 -425
  205. package/skills/neural-memory/SKILL.md +359 -362
  206. package/skills/onboard/SKILL.md +432 -403
  207. package/skills/onboard/references/invariants-template.md +76 -0
  208. package/skills/onboard/scripts/detect-invariants.js +439 -0
  209. package/skills/onboard/scripts/inject-claude-md.js +150 -0
  210. package/skills/onboard/scripts/onboard-invariants.js +194 -0
  211. package/skills/perf/SKILL.md +347 -346
  212. package/skills/plan/SKILL.md +435 -428
  213. package/skills/preflight/SKILL.md +415 -415
  214. package/skills/problem-solver/SKILL.md +380 -284
  215. package/skills/rescue/SKILL.md +474 -474
  216. package/skills/research/SKILL.md +4 -0
  217. package/skills/retro/SKILL.md +3 -1
  218. package/skills/review/SKILL.md +614 -588
  219. package/skills/review-intake/SKILL.md +249 -249
  220. package/skills/safeguard/SKILL.md +200 -200
  221. package/skills/sast/SKILL.md +190 -190
  222. package/skills/scaffold/SKILL.md +328 -287
  223. package/skills/scope-guard/SKILL.md +183 -180
  224. package/skills/scout/SKILL.md +269 -263
  225. package/skills/sentinel/SKILL.md +384 -381
  226. package/skills/sentinel-env/SKILL.md +254 -254
  227. package/skills/sequential-thinking/SKILL.md +234 -234
  228. package/skills/session-bridge/SKILL.md +595 -543
  229. package/skills/session-bridge/scripts/load-invariants.js +397 -0
  230. package/skills/skill-forge/SKILL.md +581 -581
  231. package/skills/skill-router/SKILL.md +3 -0
  232. package/skills/slides/SKILL.md +19 -0
  233. package/skills/surgeon/SKILL.md +215 -215
  234. package/skills/team/SKILL.md +557 -537
  235. package/skills/test/SKILL.md +620 -614
  236. package/skills/trend-scout/SKILL.md +145 -145
  237. package/skills/verification/SKILL.md +334 -326
  238. package/skills/video-creator/SKILL.md +201 -201
  239. package/skills/watchdog/SKILL.md +168 -168
  240. package/skills/worktree/SKILL.md +140 -140
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Tests for doctor mesh integrity checks
3
+ */
4
+
5
+ import assert from 'node:assert';
6
+ import { mkdir, rm, writeFile } from 'node:fs/promises';
7
+ import path from 'node:path';
8
+ import { afterEach, beforeEach, describe, test } from 'node:test';
9
+ import { fileURLToPath } from 'node:url';
10
+ import { checkMeshIntegrity, formatMeshResults } from '../doctor.js';
11
+
12
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
+ const TEST_ROOT = path.join(__dirname, '.test-mesh-skills');
14
+
15
+ describe('checkMeshIntegrity', () => {
16
+ beforeEach(async () => {
17
+ await mkdir(path.join(TEST_ROOT, 'skills', 'skill-a'), { recursive: true });
18
+ await mkdir(path.join(TEST_ROOT, 'skills', 'skill-b'), { recursive: true });
19
+ });
20
+
21
+ afterEach(async () => {
22
+ await rm(TEST_ROOT, { recursive: true, force: true });
23
+ });
24
+
25
+ test('detects missing reciprocal connections', async () => {
26
+ // skill-a calls skill-b, but skill-b doesn't acknowledge
27
+ await writeFile(
28
+ path.join(TEST_ROOT, 'skills', 'skill-a', 'SKILL.md'),
29
+ `---
30
+ name: skill-a
31
+ metadata:
32
+ version: "1.0.0"
33
+ ---
34
+ # skill-a
35
+ ## Calls (outbound)
36
+ - \`skill-b\` (L2): some reason
37
+ ## Called By (inbound)
38
+ None
39
+ ## Sharp Edges
40
+ | Mode | Sev | Mit |
41
+ ## Done When
42
+ - done
43
+ ## Cost Profile
44
+ ~100 tokens
45
+ `,
46
+ );
47
+
48
+ await writeFile(
49
+ path.join(TEST_ROOT, 'skills', 'skill-b', 'SKILL.md'),
50
+ `---
51
+ name: skill-b
52
+ metadata:
53
+ version: "1.0.0"
54
+ ---
55
+ # skill-b
56
+ ## Calls (outbound)
57
+ None
58
+ ## Called By (inbound)
59
+ None
60
+ ## Sharp Edges
61
+ | Mode | Sev | Mit |
62
+ ## Done When
63
+ - done
64
+ ## Cost Profile
65
+ ~100 tokens
66
+ `,
67
+ );
68
+
69
+ const results = await checkMeshIntegrity(TEST_ROOT);
70
+
71
+ assert.strictEqual(results.stats.skills, 2, 'Should find 2 skills');
72
+ assert.strictEqual(results.stats.missingReciprocals, 1, 'Should detect 1 missing reciprocal');
73
+ assert.ok(
74
+ results.warnings.some((w) => w.includes('skill-b') && w.includes('skill-a')),
75
+ 'Should warn that skill-b is missing skill-a in Called By',
76
+ );
77
+ });
78
+
79
+ test('passes when reciprocals are correct', async () => {
80
+ await writeFile(
81
+ path.join(TEST_ROOT, 'skills', 'skill-a', 'SKILL.md'),
82
+ `---
83
+ name: skill-a
84
+ metadata:
85
+ version: "1.0.0"
86
+ ---
87
+ # skill-a
88
+ ## Calls (outbound)
89
+ - \`skill-b\` (L2): some reason
90
+ ## Called By (inbound)
91
+ None
92
+ ## Sharp Edges
93
+ | Mode | Sev | Mit |
94
+ ## Done When
95
+ - done
96
+ ## Cost Profile
97
+ ~100 tokens
98
+ `,
99
+ );
100
+
101
+ await writeFile(
102
+ path.join(TEST_ROOT, 'skills', 'skill-b', 'SKILL.md'),
103
+ `---
104
+ name: skill-b
105
+ metadata:
106
+ version: "1.0.0"
107
+ ---
108
+ # skill-b
109
+ ## Calls (outbound)
110
+ None
111
+ ## Called By (inbound)
112
+ - \`skill-a\` (L2): caller
113
+ ## Sharp Edges
114
+ | Mode | Sev | Mit |
115
+ ## Done When
116
+ - done
117
+ ## Cost Profile
118
+ ~100 tokens
119
+ `,
120
+ );
121
+
122
+ const results = await checkMeshIntegrity(TEST_ROOT);
123
+
124
+ assert.strictEqual(results.stats.missingReciprocals, 0, 'Should have no missing reciprocals');
125
+ assert.strictEqual(
126
+ results.checks.find((c) => c.name === 'Reciprocal connections')?.status,
127
+ 'pass',
128
+ 'Reciprocal check should pass',
129
+ );
130
+ });
131
+
132
+ test('suggests version bump for mature skills', async () => {
133
+ await writeFile(
134
+ path.join(TEST_ROOT, 'skills', 'skill-a', 'SKILL.md'),
135
+ `---
136
+ name: skill-a
137
+ metadata:
138
+ version: "0.9.0"
139
+ ---
140
+ # skill-a
141
+ ## Calls (outbound)
142
+ None
143
+ ## Called By (inbound)
144
+ None
145
+ ## Sharp Edges
146
+ | Mode | Sev | Mit |
147
+ ## Done When
148
+ - done
149
+ ## Cost Profile
150
+ ~100 tokens
151
+ ## Returns
152
+ | Artifact | Format | Location |
153
+ `,
154
+ );
155
+
156
+ const results = await checkMeshIntegrity(TEST_ROOT);
157
+
158
+ assert.ok(
159
+ results.warnings.some((w) => w.includes('skill-a') && w.includes('1.0')),
160
+ 'Should suggest promoting to 1.0',
161
+ );
162
+ });
163
+
164
+ test('detects missing required sections', async () => {
165
+ await writeFile(
166
+ path.join(TEST_ROOT, 'skills', 'skill-a', 'SKILL.md'),
167
+ `---
168
+ name: skill-a
169
+ metadata:
170
+ version: "1.0.0"
171
+ ---
172
+ # skill-a
173
+ ## Calls (outbound)
174
+ None
175
+ ## Called By (inbound)
176
+ None
177
+ `,
178
+ );
179
+
180
+ const results = await checkMeshIntegrity(TEST_ROOT);
181
+
182
+ assert.ok(
183
+ results.warnings.some((w) => w.includes('skill-a') && w.includes('Sharp Edges')),
184
+ 'Should warn about missing Sharp Edges',
185
+ );
186
+ assert.ok(
187
+ results.warnings.some((w) => w.includes('skill-a') && w.includes('Done When')),
188
+ 'Should warn about missing Done When',
189
+ );
190
+ });
191
+ });
192
+
193
+ describe('formatMeshResults', () => {
194
+ test('formats results for console output', () => {
195
+ const results = {
196
+ checks: [
197
+ { name: 'Reciprocal connections', status: 'pass' },
198
+ { name: 'Version maturity', status: 'warn', detail: '2 skills ready for 1.0' },
199
+ ],
200
+ warnings: ['skill-a v0.9.0: consider promoting to 1.0'],
201
+ errors: [],
202
+ stats: { skills: 10, connections: 50, missingReciprocals: 0 },
203
+ };
204
+
205
+ const output = formatMeshResults(results);
206
+
207
+ assert.ok(output.includes('Skills: 10'), 'Should show skill count');
208
+ assert.ok(output.includes('Connections: 50'), 'Should show connection count');
209
+ assert.ok(output.includes('[✓] Reciprocal'), 'Should show pass icon');
210
+ assert.ok(output.includes('[!] Version'), 'Should show warn icon');
211
+ assert.ok(output.includes('skill-a v0.9.0'), 'Should include warnings');
212
+ });
213
+ });
214
+
215
+ describe('integration: real skills', () => {
216
+ test('runs mesh check on actual Rune skills directory', async () => {
217
+ const runeRoot = path.resolve(__dirname, '../..');
218
+ const results = await checkMeshIntegrity(runeRoot);
219
+
220
+ // Should find skills
221
+ assert.ok(results.stats.skills > 50, `Should find 50+ skills, got ${results.stats.skills}`);
222
+
223
+ // Should find connections
224
+ assert.ok(results.stats.connections > 100, `Should find 100+ connections, got ${results.stats.connections}`);
225
+
226
+ // Should not have errors
227
+ assert.strictEqual(results.errors.length, 0, `Should have no errors: ${results.errors.join(', ')}`);
228
+ });
229
+ });
@@ -0,0 +1,91 @@
1
+ import assert from 'node:assert';
2
+ import { PassThrough } from 'node:stream';
3
+ import { describe, test } from 'node:test';
4
+ import { dispatchHook } from '../commands/hook-dispatch.js';
5
+
6
+ function makeIO({ stdinData = '' } = {}) {
7
+ const stdin = new PassThrough();
8
+ stdin.setEncoding = () => {};
9
+ stdin.isTTY = false;
10
+ if (stdinData) {
11
+ stdin.write(stdinData);
12
+ }
13
+ stdin.end();
14
+
15
+ const stdout = new PassThrough();
16
+ const stderr = new PassThrough();
17
+ let out = '';
18
+ let err = '';
19
+ stdout.on('data', (chunk) => {
20
+ out += chunk.toString();
21
+ });
22
+ stderr.on('data', (chunk) => {
23
+ err += chunk.toString();
24
+ });
25
+ return { stdin, stdout, stderr, getOut: () => out, getErr: () => err };
26
+ }
27
+
28
+ describe('dispatchHook', () => {
29
+ test('unknown skill exits 1 in strict mode', async () => {
30
+ const io = makeIO();
31
+ const code = await dispatchHook(['evil-skill'], io);
32
+ assert.strictEqual(code, 1);
33
+ assert.match(io.getErr(), /unknown skill/);
34
+ });
35
+
36
+ test('unknown skill exits 0 in gentle mode', async () => {
37
+ const io = makeIO();
38
+ const code = await dispatchHook(['evil-skill', '--gentle'], io);
39
+ assert.strictEqual(code, 0);
40
+ assert.match(io.getErr(), /unknown skill/);
41
+ });
42
+
43
+ test('missing skill name errors', async () => {
44
+ const io = makeIO();
45
+ const code = await dispatchHook([], io);
46
+ assert.strictEqual(code, 1);
47
+ assert.match(io.getErr(), /missing skill/);
48
+ });
49
+
50
+ test('known skill succeeds and emits advisory line', async () => {
51
+ const io = makeIO({ stdinData: JSON.stringify({ tool_name: 'Edit' }) });
52
+ const code = await dispatchHook(['preflight', '--gentle'], io);
53
+ assert.strictEqual(code, 0);
54
+ assert.match(io.getOut(), /rune-hook: preflight/);
55
+ assert.match(io.getOut(), /advisory/);
56
+ assert.match(io.getOut(), /Edit/);
57
+ });
58
+
59
+ test('strict mode prints enforcing label', async () => {
60
+ const io = makeIO({ stdinData: '{}' });
61
+ const code = await dispatchHook(['sentinel'], io);
62
+ assert.strictEqual(code, 0);
63
+ assert.match(io.getOut(), /enforcing/);
64
+ });
65
+
66
+ test('malformed stdin does not crash', async () => {
67
+ const io = makeIO({ stdinData: 'not json at all' });
68
+ const code = await dispatchHook(['completion-gate', '--gentle'], io);
69
+ assert.strictEqual(code, 0);
70
+ });
71
+
72
+ // T5: invalid skill name (not in allowlist) → exits non-zero, stderr contains rejected name
73
+ test('T5: skill not in allowlist exits 1 in strict mode with name in stderr', async () => {
74
+ const io = makeIO();
75
+ const code = await dispatchHook(['totally-fake-skill'], io);
76
+ assert.strictEqual(code, 1);
77
+ assert.ok(io.getErr().includes('totally-fake-skill'), 'stderr must contain the rejected skill name');
78
+ });
79
+
80
+ // T6: stdin larger than 1MB → truncates or errors gracefully (no OOM)
81
+ test('T6: stdin larger than 1MB is rejected gracefully', async () => {
82
+ // Build a payload just over the 1MB limit
83
+ const big = 'x'.repeat(1_100_000);
84
+ const io = makeIO({ stdinData: big });
85
+ // Should not throw — must return an exit code (0 or 1, gentle or strict)
86
+ const code = await dispatchHook(['preflight', '--gentle'], io);
87
+ assert.ok(typeof code === 'number', 'must return a numeric exit code');
88
+ // gentle mode: still exits 0 even if stdin was discarded
89
+ assert.strictEqual(code, 0);
90
+ });
91
+ });
@@ -0,0 +1,118 @@
1
+ import assert from 'node:assert';
2
+ import { existsSync } from 'node:fs';
3
+ import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import path from 'node:path';
6
+ import { afterEach, beforeEach, describe, test } from 'node:test';
7
+ import * as antigravity from '../adapters/hooks/antigravity.js';
8
+ import { installHooks } from '../commands/hooks/install.js';
9
+ import { hookStatus } from '../commands/hooks/status.js';
10
+ import { uninstallHooks } from '../commands/hooks/uninstall.js';
11
+
12
+ const RUNE_ROOT = path.resolve(import.meta.dirname, '..', '..');
13
+ const RULES_DIR = '.antigravity/rules';
14
+
15
+ let tmpRoot;
16
+
17
+ async function seedAntigravity(root) {
18
+ await mkdir(path.join(root, '.antigravity'), { recursive: true });
19
+ }
20
+
21
+ beforeEach(async () => {
22
+ tmpRoot = await mkdtemp(path.join(tmpdir(), 'rune-antigravity-'));
23
+ });
24
+
25
+ afterEach(async () => {
26
+ if (tmpRoot) await rm(tmpRoot, { recursive: true, force: true });
27
+ });
28
+
29
+ describe('antigravity adapter', () => {
30
+ test('detect() true when .antigravity exists', async () => {
31
+ await seedAntigravity(tmpRoot);
32
+ assert.strictEqual(antigravity.detect(tmpRoot), true);
33
+ });
34
+
35
+ test('emit(gentle) returns 3 rule markdowns with WARN mode', async () => {
36
+ const plan = await antigravity.emit({ preset: 'gentle', projectRoot: tmpRoot });
37
+ assert.strictEqual(plan.files.length, 3);
38
+ for (const file of plan.files) {
39
+ assert.ok(file.content.includes('rune-managed: true'));
40
+ assert.ok(file.content.includes('mode: WARN'));
41
+ }
42
+ });
43
+
44
+ test('emit(strict) renders BLOCK mode', async () => {
45
+ const plan = await antigravity.emit({ preset: 'strict', projectRoot: tmpRoot });
46
+ for (const file of plan.files) {
47
+ assert.ok(file.content.includes('mode: BLOCK'));
48
+ }
49
+ });
50
+
51
+ test('emit() rejects invalid preset', async () => {
52
+ await assert.rejects(antigravity.emit({ preset: 'weird', projectRoot: tmpRoot }), /invalid preset/);
53
+ });
54
+
55
+ test('emit() notes call out experimental status', async () => {
56
+ const plan = await antigravity.emit({ preset: 'gentle', projectRoot: tmpRoot });
57
+ assert.ok(plan.notes.some((n) => n.toLowerCase().includes('experimental')));
58
+ });
59
+
60
+ test('install writes rule files under .antigravity/rules/', async () => {
61
+ await seedAntigravity(tmpRoot);
62
+ const result = await installHooks(tmpRoot, { preset: 'gentle', platform: 'antigravity' });
63
+ assert.strictEqual(result.written, true);
64
+ const files = await readdir(path.join(tmpRoot, RULES_DIR));
65
+ assert.ok(files.includes('rune-preflight.md'));
66
+ assert.ok(files.includes('rune-sentinel.md'));
67
+ assert.ok(files.includes('rune-dependency-doctor.md'));
68
+ });
69
+
70
+ test('idempotent re-install', async () => {
71
+ await seedAntigravity(tmpRoot);
72
+ await installHooks(tmpRoot, { preset: 'gentle', platform: 'antigravity' });
73
+ const first = await readFile(path.join(tmpRoot, RULES_DIR, 'rune-preflight.md'), 'utf-8');
74
+ await installHooks(tmpRoot, { preset: 'gentle', platform: 'antigravity' });
75
+ const second = await readFile(path.join(tmpRoot, RULES_DIR, 'rune-preflight.md'), 'utf-8');
76
+ assert.strictEqual(first, second);
77
+ });
78
+
79
+ test('uninstall removes Rune rules, preserves user rules', async () => {
80
+ await seedAntigravity(tmpRoot);
81
+ const rulesDir = path.join(tmpRoot, RULES_DIR);
82
+ await mkdir(rulesDir, { recursive: true });
83
+ await writeFile(path.join(rulesDir, 'user-rule.md'), '# User rule\n', 'utf-8');
84
+
85
+ await installHooks(tmpRoot, { preset: 'gentle', platform: 'antigravity' });
86
+ await uninstallHooks(tmpRoot, { platform: 'antigravity' });
87
+
88
+ const remaining = await readdir(rulesDir);
89
+ assert.ok(remaining.includes('user-rule.md'), 'user rule must survive');
90
+ assert.ok(!remaining.some((f) => f.startsWith('rune-')), 'no rune-* files should remain');
91
+ });
92
+
93
+ test('uninstall leaves non-managed rune-* files alone', async () => {
94
+ await seedAntigravity(tmpRoot);
95
+ const rulesDir = path.join(tmpRoot, RULES_DIR);
96
+ await mkdir(rulesDir, { recursive: true });
97
+ await writeFile(path.join(rulesDir, 'rune-fake.md'), '---\ndescription: imposter\n---\n\nnot ours\n', 'utf-8');
98
+
99
+ await uninstallHooks(tmpRoot, { platform: 'antigravity' });
100
+ assert.ok(existsSync(path.join(rulesDir, 'rune-fake.md')));
101
+ });
102
+
103
+ test('status after install reports wired skills', async () => {
104
+ await seedAntigravity(tmpRoot);
105
+ await installHooks(tmpRoot, { preset: 'strict', platform: 'antigravity' });
106
+ const result = await hookStatus(tmpRoot, RUNE_ROOT, { platform: 'antigravity' });
107
+ const r = result.results.find((x) => x.platform === 'antigravity');
108
+ assert.strictEqual(r.installed, true);
109
+ assert.strictEqual(r.preset, 'strict');
110
+ assert.ok(r.wired.includes('preflight'));
111
+ });
112
+
113
+ test('status reports not installed when .antigravity missing', async () => {
114
+ const result = await hookStatus(tmpRoot, RUNE_ROOT, { platform: 'antigravity' });
115
+ const r = result.results.find((x) => x.platform === 'antigravity');
116
+ assert.strictEqual(r.installed, false);
117
+ });
118
+ });
@@ -0,0 +1,139 @@
1
+ import assert from 'node:assert';
2
+ import { existsSync } from 'node:fs';
3
+ import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import path from 'node:path';
6
+ import { afterEach, beforeEach, describe, test } from 'node:test';
7
+ import * as cursor from '../adapters/hooks/cursor.js';
8
+ import { installHooks } from '../commands/hooks/install.js';
9
+ import { hookStatus } from '../commands/hooks/status.js';
10
+ import { uninstallHooks } from '../commands/hooks/uninstall.js';
11
+
12
+ const RUNE_ROOT = path.resolve(import.meta.dirname, '..', '..');
13
+ const RULES_DIR = '.cursor/rules';
14
+
15
+ let tmpRoot;
16
+
17
+ async function seedCursor(root) {
18
+ await mkdir(path.join(root, '.cursor'), { recursive: true });
19
+ }
20
+
21
+ beforeEach(async () => {
22
+ tmpRoot = await mkdtemp(path.join(tmpdir(), 'rune-cursor-'));
23
+ });
24
+
25
+ afterEach(async () => {
26
+ if (tmpRoot) await rm(tmpRoot, { recursive: true, force: true });
27
+ });
28
+
29
+ describe('cursor adapter', () => {
30
+ test('detect() true when .cursor exists', async () => {
31
+ await seedCursor(tmpRoot);
32
+ assert.strictEqual(cursor.detect(tmpRoot), true);
33
+ });
34
+
35
+ test('detect() false when .cursor missing', () => {
36
+ assert.strictEqual(cursor.detect(tmpRoot), false);
37
+ });
38
+
39
+ test('emit(gentle) returns 3 .mdc rule files', async () => {
40
+ const plan = await cursor.emit({ preset: 'gentle', projectRoot: tmpRoot });
41
+ assert.strictEqual(plan.files.length, 3);
42
+ const names = plan.files.map((f) => path.basename(f.path)).sort();
43
+ assert.deepStrictEqual(names, ['rune-dependency-doctor.mdc', 'rune-preflight.mdc', 'rune-sentinel.mdc']);
44
+ for (const file of plan.files) {
45
+ assert.ok(file.content.includes('rune-managed: true'));
46
+ assert.ok(file.content.includes('@rune-kit/rune hook-dispatch'));
47
+ }
48
+ });
49
+
50
+ test('emit(strict) renders WARN → BLOCK guidance', async () => {
51
+ const gentle = await cursor.emit({ preset: 'gentle', projectRoot: tmpRoot });
52
+ const strict = await cursor.emit({ preset: 'strict', projectRoot: tmpRoot });
53
+ const gentlePreflight = gentle.files.find((f) => f.path.endsWith('rune-preflight.mdc')).content;
54
+ const strictPreflight = strict.files.find((f) => f.path.endsWith('rune-preflight.mdc')).content;
55
+ assert.ok(gentlePreflight.includes('WARN'));
56
+ assert.ok(strictPreflight.includes('BLOCK'));
57
+ });
58
+
59
+ test('emit(off) delegates to uninstall', async () => {
60
+ await seedCursor(tmpRoot);
61
+ await installHooks(tmpRoot, { preset: 'gentle', platform: 'cursor' });
62
+ const plan = await cursor.emit({ preset: 'off', projectRoot: tmpRoot });
63
+ assert.ok(plan.files.every((f) => f.content === null));
64
+ assert.ok(plan.files.length >= 1);
65
+ });
66
+
67
+ test('emit() rejects invalid preset', async () => {
68
+ await assert.rejects(cursor.emit({ preset: 'loose', projectRoot: tmpRoot }), /invalid preset/);
69
+ });
70
+
71
+ test('install writes .mdc files to .cursor/rules/', async () => {
72
+ await seedCursor(tmpRoot);
73
+ const result = await installHooks(tmpRoot, { preset: 'gentle', platform: 'cursor' });
74
+ assert.strictEqual(result.written, true);
75
+ const rulesDir = path.join(tmpRoot, RULES_DIR);
76
+ const files = await readdir(rulesDir);
77
+ assert.ok(files.includes('rune-preflight.mdc'));
78
+ assert.ok(files.includes('rune-sentinel.mdc'));
79
+ assert.ok(files.includes('rune-dependency-doctor.mdc'));
80
+ });
81
+
82
+ test('idempotent re-install produces identical content', async () => {
83
+ await seedCursor(tmpRoot);
84
+ await installHooks(tmpRoot, { preset: 'gentle', platform: 'cursor' });
85
+ const rulesDir = path.join(tmpRoot, RULES_DIR);
86
+ const first = await readFile(path.join(rulesDir, 'rune-preflight.mdc'), 'utf-8');
87
+ await installHooks(tmpRoot, { preset: 'gentle', platform: 'cursor' });
88
+ const second = await readFile(path.join(rulesDir, 'rune-preflight.mdc'), 'utf-8');
89
+ assert.strictEqual(first, second);
90
+ });
91
+
92
+ test('uninstall removes only Rune-managed .mdc files', async () => {
93
+ await seedCursor(tmpRoot);
94
+ const rulesDir = path.join(tmpRoot, RULES_DIR);
95
+ await mkdir(rulesDir, { recursive: true });
96
+ await writeFile(path.join(rulesDir, 'user-custom.mdc'), '---\ndescription: user\n---\n\n# User rule\n', 'utf-8');
97
+
98
+ await installHooks(tmpRoot, { preset: 'gentle', platform: 'cursor' });
99
+ await uninstallHooks(tmpRoot, { platform: 'cursor' });
100
+
101
+ const remaining = await readdir(rulesDir);
102
+ assert.ok(remaining.includes('user-custom.mdc'), 'user rule must survive');
103
+ assert.ok(!remaining.some((f) => f.startsWith('rune-')), 'no rune-* files should remain');
104
+ });
105
+
106
+ test('uninstall skips files without rune-managed signature', async () => {
107
+ await seedCursor(tmpRoot);
108
+ const rulesDir = path.join(tmpRoot, RULES_DIR);
109
+ await mkdir(rulesDir, { recursive: true });
110
+ // A file that happens to start with "rune-" but is NOT managed by Rune
111
+ await writeFile(
112
+ path.join(rulesDir, 'rune-fake.mdc'),
113
+ '---\ndescription: user imposter\n---\n\nnot ours\n',
114
+ 'utf-8',
115
+ );
116
+
117
+ await uninstallHooks(tmpRoot, { platform: 'cursor' });
118
+ assert.ok(existsSync(path.join(rulesDir, 'rune-fake.mdc')), 'non-managed rune-* file must survive');
119
+ });
120
+
121
+ test('status reports installed preset after install', async () => {
122
+ await seedCursor(tmpRoot);
123
+ await installHooks(tmpRoot, { preset: 'strict', platform: 'cursor' });
124
+ const result = await hookStatus(tmpRoot, RUNE_ROOT, { platform: 'cursor' });
125
+ const r = result.results.find((x) => x.platform === 'cursor');
126
+ assert.strictEqual(r.installed, true);
127
+ assert.strictEqual(r.preset, 'strict');
128
+ assert.ok(r.wired.includes('preflight'));
129
+ assert.ok(r.wired.includes('sentinel'));
130
+ assert.ok(r.wired.includes('dependency-doctor'));
131
+ });
132
+
133
+ test('status reports not installed when .cursor missing', async () => {
134
+ const result = await hookStatus(tmpRoot, RUNE_ROOT, { platform: 'cursor' });
135
+ const r = result.results.find((x) => x.platform === 'cursor');
136
+ assert.strictEqual(r.installed, false);
137
+ assert.ok(r.missing.includes('preflight'));
138
+ });
139
+ });