@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,240 @@
1
+ import assert from 'node:assert';
2
+ import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import path from 'node:path';
5
+ import { describe, test } from 'node:test';
6
+ import {
7
+ collectPointerGlobs,
8
+ loadTemplate,
9
+ mergeInvariantsContent,
10
+ runOnboardInvariants,
11
+ } from '../../skills/onboard/scripts/onboard-invariants.js';
12
+
13
+ describe('mergeInvariantsContent', () => {
14
+ test('seeds from template when existing is empty', () => {
15
+ const template = '# Project Invariants\n\n## Auto-detected (new)\n\n<!-- placeholder -->\n';
16
+ const detected = {
17
+ danger: [{ title: 'core', what: '10 files', where: ['src/core/**'], why: 'hub' }],
18
+ critical: [],
19
+ state: [],
20
+ cross: [],
21
+ };
22
+ const merged = mergeInvariantsContent({ existing: '', autoDetected: detected, template });
23
+ assert.ok(merged.seeded);
24
+ assert.ok(merged.content.includes('# Project Invariants'));
25
+ assert.ok(merged.content.includes('#### core'));
26
+ });
27
+
28
+ test('replaces ONLY the auto-detected block on re-run', () => {
29
+ const first = mergeInvariantsContent({
30
+ existing: '',
31
+ autoDetected: {
32
+ danger: [{ title: 'v1', what: 'old', where: ['src/v1/**'], why: 'first' }],
33
+ critical: [],
34
+ state: [],
35
+ cross: [],
36
+ },
37
+ template: '# Project Invariants\n\n## Auto-detected (new)\n\n<!-- placeholder -->\n',
38
+ });
39
+
40
+ const userEdited = first.content.replace(
41
+ '# Project Invariants',
42
+ '# Project Invariants\n\n## Danger Zones\n\n#### UserRule\n- **WHAT**: custom\n- **WHERE**: `src/custom/**`\n- **WHY**: user-added\n',
43
+ );
44
+
45
+ const second = mergeInvariantsContent({
46
+ existing: userEdited,
47
+ autoDetected: {
48
+ danger: [{ title: 'v2', what: 'new', where: ['src/v2/**'], why: 'second' }],
49
+ critical: [],
50
+ state: [],
51
+ cross: [],
52
+ },
53
+ template: '',
54
+ });
55
+
56
+ assert.ok(second.replaced);
57
+ assert.ok(second.content.includes('UserRule'), 'user edits must be preserved');
58
+ assert.ok(second.content.includes('#### v2'), 'new detection must appear');
59
+ assert.ok(!second.content.includes('#### v1'), 'old detection must be replaced');
60
+ });
61
+
62
+ test('appends Auto-detected header if missing from existing file', () => {
63
+ const existing = '# Project Invariants\n\n## Danger Zones\n\n#### legacy\n';
64
+ const merged = mergeInvariantsContent({
65
+ existing,
66
+ autoDetected: {
67
+ danger: [{ title: 'fresh', what: 'n', where: ['src/x/**'], why: 'y' }],
68
+ critical: [],
69
+ state: [],
70
+ cross: [],
71
+ },
72
+ template: '',
73
+ });
74
+ assert.ok(merged.appended);
75
+ assert.ok(merged.content.includes('## Auto-detected (new)'));
76
+ assert.ok(merged.content.includes('legacy'), 'existing user content preserved');
77
+ });
78
+
79
+ test('renders placeholder line when no detections', () => {
80
+ const merged = mergeInvariantsContent({
81
+ existing: '',
82
+ autoDetected: { danger: [], critical: [], state: [], cross: [] },
83
+ template: '# X\n\n## Auto-detected (new)\n\n<!-- placeholder -->\n',
84
+ });
85
+ assert.match(merged.content, /No new detections on this run/);
86
+ });
87
+
88
+ test('Phase 2 C1: code-fence with `## ` inside auto-detected block does not corrupt merge', () => {
89
+ // User manually edits the file and embeds a markdown example showing a `## `
90
+ // line inside a fenced code block (e.g. illustrating markdown syntax).
91
+ // The old regex-based boundary scan would treat that `## ` as a real section
92
+ // header and drop everything after it. This test locks in the line-walking
93
+ // fence-aware boundary detection.
94
+ const existing = [
95
+ '# Project Invariants',
96
+ '',
97
+ '## Danger Zones',
98
+ '',
99
+ '#### PreservedRule',
100
+ '- **WHAT**: do not touch',
101
+ '',
102
+ '## Auto-detected (new)',
103
+ '',
104
+ '```markdown',
105
+ '## This heading is inside a fence',
106
+ 'example content',
107
+ '```',
108
+ '',
109
+ '#### OldDetection',
110
+ '',
111
+ '## Notes (user section)',
112
+ '',
113
+ 'should survive too',
114
+ '',
115
+ ].join('\n');
116
+
117
+ const merged = mergeInvariantsContent({
118
+ existing,
119
+ autoDetected: {
120
+ danger: [{ title: 'NewDetection', what: 'fresh', where: ['src/fresh/**'], why: 'new' }],
121
+ critical: [],
122
+ state: [],
123
+ cross: [],
124
+ },
125
+ template: '',
126
+ });
127
+
128
+ assert.ok(merged.replaced);
129
+ assert.ok(merged.content.includes('PreservedRule'), 'content above Auto-detected preserved');
130
+ assert.ok(merged.content.includes('NewDetection'), 'new detection inserted');
131
+ assert.ok(!merged.content.includes('OldDetection'), 'old auto-detected content replaced');
132
+ assert.ok(merged.content.includes('Notes (user section)'), 'real `## ` section after block survives');
133
+ assert.ok(merged.content.includes('should survive too'), 'body of real section survives');
134
+ });
135
+ });
136
+
137
+ describe('collectPointerGlobs', () => {
138
+ test('extracts and dedupes globs from danger + critical buckets', () => {
139
+ const globs = collectPointerGlobs({
140
+ danger: [{ where: ['src/a/**'] }, { where: ['src/b/**'] }, { where: ['src/a/**'] }],
141
+ critical: [{ where: ['src/b/**'] }, { where: ['src/c/**'] }],
142
+ state: [],
143
+ cross: [],
144
+ });
145
+ assert.deepStrictEqual(globs.sort(), ['src/a/**', 'src/b/**', 'src/c/**']);
146
+ });
147
+ });
148
+
149
+ describe('loadTemplate', () => {
150
+ test('reads the bundled template file', async () => {
151
+ const content = await loadTemplate();
152
+ assert.ok(content.includes('Project Invariants'));
153
+ assert.ok(content.includes('Auto-detected (new)'));
154
+ });
155
+ });
156
+
157
+ describe('runOnboardInvariants — end-to-end', () => {
158
+ test('seeds INVARIANTS.md and injects CLAUDE.md pointer', async () => {
159
+ const root = await mkdtemp(path.join(tmpdir(), 'rune-onboard-'));
160
+ try {
161
+ await mkdir(path.join(root, 'src', 'core'), { recursive: true });
162
+ for (let i = 0; i < 12; i++) {
163
+ await writeFile(path.join(root, 'src', 'core', `file${i}.ts`), `export const X${i} = ${i};\n`, 'utf8');
164
+ }
165
+ await writeFile(path.join(root, 'CLAUDE.md'), '# Fresh Project\n', 'utf8');
166
+
167
+ const result = await runOnboardInvariants({ root });
168
+
169
+ assert.strictEqual(result.invariants.action, 'seeded');
170
+ assert.ok(['created', 'updated'].includes(result.claudeMd.action));
171
+
172
+ const invariants = await readFile(path.join(root, '.rune', 'INVARIANTS.md'), 'utf8');
173
+ assert.ok(invariants.includes('Project Invariants'));
174
+
175
+ const claudeMd = await readFile(path.join(root, 'CLAUDE.md'), 'utf8');
176
+ assert.ok(claudeMd.includes('@rune-invariants-pointer:start'));
177
+ assert.ok(claudeMd.includes('.rune/INVARIANTS.md'));
178
+ } finally {
179
+ await rm(root, { recursive: true, force: true });
180
+ }
181
+ });
182
+
183
+ test('Phase 2 H1: SKILL.md / PACK.md directories surface as danger zones', async () => {
184
+ const root = await mkdtemp(path.join(tmpdir(), 'rune-onboard-skills-'));
185
+ try {
186
+ // Seed an AI-skills-repo shape: directories containing SKILL.md but no
187
+ // SOURCE_EXTS files. Old detector (exts-only scan) missed these entirely.
188
+ await mkdir(path.join(root, 'skills', 'skill-router'), { recursive: true });
189
+ await mkdir(path.join(root, 'skills', 'cook'), { recursive: true });
190
+ await mkdir(path.join(root, 'extensions', 'ui'), { recursive: true });
191
+ await writeFile(path.join(root, 'skills', 'skill-router', 'SKILL.md'), '# Router\n', 'utf8');
192
+ await writeFile(path.join(root, 'skills', 'cook', 'SKILL.md'), '# Cook\n', 'utf8');
193
+ await writeFile(path.join(root, 'extensions', 'ui', 'PACK.md'), '# UI Pack\n', 'utf8');
194
+ await writeFile(path.join(root, 'CLAUDE.md'), '# P\n', 'utf8');
195
+
196
+ const result = await runOnboardInvariants({ root });
197
+ assert.strictEqual(result.invariants.action, 'seeded');
198
+
199
+ const invariants = await readFile(path.join(root, '.rune', 'INVARIANTS.md'), 'utf8');
200
+ const hasSkillRouter = invariants.includes('skills/skill-router');
201
+ const hasCook = invariants.includes('skills/cook');
202
+ const hasUiPack = invariants.includes('extensions/ui');
203
+ assert.ok(
204
+ hasSkillRouter || hasCook || hasUiPack,
205
+ 'at least one signal-file directory must surface as a danger zone',
206
+ );
207
+ assert.ok(invariants.includes('skill/pack artifact') || invariants.includes('High-risk keyword'));
208
+ } finally {
209
+ await rm(root, { recursive: true, force: true });
210
+ }
211
+ });
212
+
213
+ test('second run preserves user edits above Auto-detected', async () => {
214
+ const root = await mkdtemp(path.join(tmpdir(), 'rune-onboard-'));
215
+ try {
216
+ await mkdir(path.join(root, 'src', 'core'), { recursive: true });
217
+ for (let i = 0; i < 10; i++) {
218
+ await writeFile(path.join(root, 'src', 'core', `f${i}.ts`), `export const Y${i} = ${i};\n`, 'utf8');
219
+ }
220
+ await writeFile(path.join(root, 'CLAUDE.md'), '# P\n', 'utf8');
221
+
222
+ await runOnboardInvariants({ root });
223
+
224
+ const invariantsPath = path.join(root, '.rune', 'INVARIANTS.md');
225
+ const original = await readFile(invariantsPath, 'utf8');
226
+ const edited = original.replace(
227
+ '## Auto-detected (new)',
228
+ '## Danger Zones\n\n#### UserRule\n- **WHAT**: sacred\n- **WHERE**: `src/sacred/**`\n- **WHY**: must-not-touch\n\n## Auto-detected (new)',
229
+ );
230
+ await writeFile(invariantsPath, edited, 'utf8');
231
+
232
+ await runOnboardInvariants({ root });
233
+ const after = await readFile(invariantsPath, 'utf8');
234
+ assert.ok(after.includes('UserRule'), 'user section must survive re-run');
235
+ assert.ok(after.includes('## Auto-detected (new)'));
236
+ } finally {
237
+ await rm(root, { recursive: true, force: true });
238
+ }
239
+ });
240
+ });
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Google Antigravity hooks adapter (experimental).
3
+ *
4
+ * Antigravity uses `.antigravity/rules/*.md` for persistent agent context.
5
+ * There is no tool-level hook primitive yet, so we mirror Cursor's pattern:
6
+ * emit rule files that cascade into the agent's prompt. This is the weakest
7
+ * fidelity tier — documented so users know what they're getting.
8
+ */
9
+
10
+ import { existsSync } from 'node:fs';
11
+ import { readdir, readFile } from 'node:fs/promises';
12
+ import path from 'node:path';
13
+ import { platformEntries, renderTierRule, unsupportedEntries } from './tier-emitter.js';
14
+
15
+ export const id = 'antigravity';
16
+
17
+ const RULES_REL_DIR = '.antigravity/rules';
18
+ const RUNE_PREFIX = 'rune-';
19
+ const AUTO_SIGNATURE = '@rune-kit/rune hook-dispatch';
20
+
21
+ export function detect(projectRoot) {
22
+ return existsSync(path.join(projectRoot, '.antigravity'));
23
+ }
24
+
25
+ export async function emit({ preset, projectRoot, tierManifests = [] }) {
26
+ if (preset === 'off' && tierManifests.length === 0) return uninstall({ projectRoot });
27
+ if (preset !== 'off' && preset !== 'strict' && preset !== 'gentle') {
28
+ throw new Error(`antigravity adapter: invalid preset '${preset}'`);
29
+ }
30
+
31
+ const mode = preset === 'strict' ? 'BLOCK' : 'WARN';
32
+ const rulesDir = path.join(projectRoot, RULES_REL_DIR);
33
+
34
+ const rules = [
35
+ { name: 'preflight', description: 'Run preflight review before any source edit.' },
36
+ { name: 'sentinel', description: 'Security review before shell / infra edits.' },
37
+ { name: 'dependency-doctor', description: 'Dependency audit after manifest edits.' },
38
+ ];
39
+
40
+ const files =
41
+ preset === 'off'
42
+ ? []
43
+ : rules.map((r) => ({
44
+ path: path.join(rulesDir, `${RUNE_PREFIX}${r.name}.md`),
45
+ content: renderRule(r, mode),
46
+ }));
47
+
48
+ const notes =
49
+ preset === 'off'
50
+ ? []
51
+ : [
52
+ 'Antigravity support is experimental — rule-injection only, no true hook.',
53
+ `Preset: ${preset} (${mode}). \`completion-gate\` must be invoked manually.`,
54
+ ];
55
+
56
+ for (const manifest of tierManifests) {
57
+ for (const entry of platformEntries(manifest, 'antigravity')) {
58
+ const content = renderTierRule(entry, 'antigravity', { mode, tier: manifest.tier });
59
+ if (!content) continue;
60
+ files.push({
61
+ path: path.join(rulesDir, `${RUNE_PREFIX}${manifest.tier}-${entry.id}.md`),
62
+ content,
63
+ });
64
+ }
65
+ const skipped = unsupportedEntries(manifest, 'antigravity');
66
+ if (skipped.length > 0) {
67
+ notes.push(
68
+ `antigravity: ${manifest.tier} skipped ${skipped.length} entr${skipped.length === 1 ? 'y' : 'ies'} (${skipped.map((e) => e.id).join(', ')}) — platform-unsupported or Claude-only.`,
69
+ );
70
+ }
71
+ }
72
+
73
+ return { files, notes };
74
+ }
75
+
76
+ export async function uninstall({ projectRoot }) {
77
+ const rulesDir = path.join(projectRoot, RULES_REL_DIR);
78
+ if (!existsSync(rulesDir)) return { files: [], notes: [] };
79
+ const entries = await readdir(rulesDir, { withFileTypes: true });
80
+ const runeFiles = entries.filter((e) => e.isFile() && e.name.startsWith(RUNE_PREFIX) && e.name.endsWith('.md'));
81
+ const files = [];
82
+ for (const file of runeFiles) {
83
+ const abs = path.join(rulesDir, file.name);
84
+ const content = await readFile(abs, 'utf-8');
85
+ if (content.includes(AUTO_SIGNATURE) || content.includes('rune-managed: true')) {
86
+ files.push({ path: abs, content: null });
87
+ }
88
+ }
89
+ return { files, notes: files.length === 0 ? ['no Rune-managed antigravity rules'] : [] };
90
+ }
91
+
92
+ export async function status(projectRoot) {
93
+ const rulesDir = path.join(projectRoot, RULES_REL_DIR);
94
+ if (!existsSync(rulesDir)) {
95
+ return {
96
+ installed: false,
97
+ preset: null,
98
+ wired: [],
99
+ missing: ['preflight', 'sentinel', 'dependency-doctor'],
100
+ notes: ['no .antigravity/rules directory'],
101
+ };
102
+ }
103
+ const entries = await readdir(rulesDir, { withFileTypes: true });
104
+ const runeFiles = entries.filter((e) => e.isFile() && e.name.startsWith(RUNE_PREFIX) && e.name.endsWith('.md'));
105
+ const wired = runeFiles.map((f) => f.name.replace(RUNE_PREFIX, '').replace('.md', ''));
106
+ const expected = ['preflight', 'sentinel', 'dependency-doctor'];
107
+ const missing = expected.filter((s) => !wired.includes(s));
108
+
109
+ let preset = null;
110
+ for (const file of runeFiles) {
111
+ const content = await readFile(path.join(rulesDir, file.name), 'utf-8');
112
+ if (/^mode: BLOCK$/m.test(content)) {
113
+ preset = 'strict';
114
+ break;
115
+ }
116
+ if (/^mode: WARN$/m.test(content)) preset = preset ?? 'gentle';
117
+ }
118
+
119
+ return { installed: runeFiles.length > 0, preset, wired, missing, notes: ['experimental — rule-injection only'] };
120
+ }
121
+
122
+ function renderRule(rule, mode) {
123
+ return [
124
+ '---',
125
+ `description: ${rule.description}`,
126
+ 'rune-managed: true',
127
+ `rune-skill: ${rule.name}`,
128
+ `mode: ${mode}`,
129
+ '---',
130
+ '',
131
+ `# Rune ${rule.name}`,
132
+ '',
133
+ `${rule.description} Mode: ${mode}.`,
134
+ '',
135
+ 'Refer to the Rune skill SKILL.md for the full checklist — apply those checks mentally before each matching edit.',
136
+ '',
137
+ `_Auto-generated by \`rune hooks install\` (${AUTO_SIGNATURE})._`,
138
+ '',
139
+ ].join('\n');
140
+ }
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Claude Code hooks adapter.
3
+ *
4
+ * Target: `.claude/settings.json` — native hook primitive (PreToolUse /
5
+ * PostToolUse / Stop). This is the stable reference adapter; other adapters
6
+ * degrade gracefully against Claude's capabilities.
7
+ */
8
+
9
+ import { existsSync } from 'node:fs';
10
+ import { readFile } from 'node:fs/promises';
11
+ import path from 'node:path';
12
+ import {
13
+ appendHookBlock,
14
+ detectPreset,
15
+ stripHooksBySkill,
16
+ stripRuneHooks,
17
+ summarizeRuneHooks,
18
+ } from '../../commands/hooks/merge.js';
19
+ import { buildPreset, SETTINGS_REL_PATH, WIRED_SKILLS } from '../../commands/hooks/presets.js';
20
+ import { buildTierBlock } from './tier-emitter.js';
21
+
22
+ export const id = 'claude';
23
+
24
+ export function detect(projectRoot) {
25
+ return existsSync(path.join(projectRoot, '.claude'));
26
+ }
27
+
28
+ export async function emit({ preset, projectRoot, tierManifests = [] }) {
29
+ if (preset === 'off' && tierManifests.length === 0) return uninstall({ projectRoot });
30
+ if (preset !== 'off' && preset !== 'strict' && preset !== 'gentle') {
31
+ throw new Error(`claude adapter: invalid preset '${preset}'`);
32
+ }
33
+
34
+ const settingsPath = path.join(projectRoot, SETTINGS_REL_PATH);
35
+ const existing = await readJson(settingsPath);
36
+
37
+ // Strip ONCE up-front — preset + tier layers then merge additively without
38
+ // the next layer wiping the previous.
39
+ let merged = stripRuneHooks(existing);
40
+ // Clear any existing Rune-managed statusLine so re-install is idempotent.
41
+ if (merged.statusLine?.command && isRuneStatusLine(merged.statusLine.command)) {
42
+ const { statusLine: _unused, ...rest } = merged;
43
+ merged = rest;
44
+ }
45
+ const notes = [];
46
+
47
+ if (preset !== 'off') {
48
+ merged = appendHookBlock(merged, buildPreset(preset));
49
+ }
50
+
51
+ for (const manifest of tierManifests) {
52
+ // Apply `overrides` — strip any surviving entries whose extracted skill name
53
+ // matches an override key. Example: Pro's `context-sense` replaces the older
54
+ // `context-watch` hook, so a pre-migration settings.json that still carries
55
+ // a `context-watch` entry gets cleaned up here.
56
+ const overrideKeys = Object.keys(manifest.overrides || {});
57
+ if (overrideKeys.length > 0) {
58
+ const before = JSON.stringify(merged.hooks || {});
59
+ merged = stripHooksBySkill(merged, overrideKeys);
60
+ const after = JSON.stringify(merged.hooks || {});
61
+ if (before !== after) {
62
+ notes.push(
63
+ `claude: applied ${manifest.tier} overrides — stripped legacy entr${overrideKeys.length === 1 ? 'y' : 'ies'} (${overrideKeys.join(', ')}).`,
64
+ );
65
+ }
66
+ }
67
+
68
+ const tierBlock = buildTierBlock(manifest, 'claude');
69
+ if (tierBlock.hooks && Object.keys(tierBlock.hooks).length > 0) {
70
+ merged = appendHookBlock(merged, { hooks: tierBlock.hooks });
71
+ }
72
+ if (tierBlock.statusLine) {
73
+ const existingStatus = merged.statusLine;
74
+ if (!existingStatus || isRuneStatusLine(existingStatus.command)) {
75
+ merged = { ...merged, statusLine: tierBlock.statusLine };
76
+ } else {
77
+ notes.push(
78
+ `claude: user-owned statusLine detected — skipping Rune tier statusLine ('${tierBlock.statusLine.command}'). Remove your statusLine and re-install to adopt Rune's.`,
79
+ );
80
+ }
81
+ }
82
+ if (tierBlock.notes?.length) notes.push(...tierBlock.notes);
83
+ }
84
+
85
+ return {
86
+ files: [
87
+ {
88
+ path: settingsPath,
89
+ content: `${JSON.stringify(merged, null, 2)}\n`,
90
+ },
91
+ ],
92
+ notes,
93
+ };
94
+ }
95
+
96
+ export async function uninstall({ projectRoot }) {
97
+ const settingsPath = path.join(projectRoot, SETTINGS_REL_PATH);
98
+ if (!existsSync(settingsPath)) {
99
+ return { files: [], notes: ['no .claude/settings.json — nothing to uninstall'] };
100
+ }
101
+ const existing = await readJson(settingsPath);
102
+ const stripped = stripRuneHooks(existing);
103
+ // statusLine with Rune hook-dispatch or a rune-managed tier command is Rune-owned — strip it.
104
+ if (stripped.statusLine?.command && isRuneStatusLine(stripped.statusLine.command)) {
105
+ delete stripped.statusLine;
106
+ }
107
+ return {
108
+ files: [
109
+ {
110
+ path: settingsPath,
111
+ content: `${JSON.stringify(stripped, null, 2)}\n`,
112
+ },
113
+ ],
114
+ notes: [],
115
+ };
116
+ }
117
+
118
+ const RUNE_TIER_STATUSLINE_RE = /\$\{RUNE_[A-Z][A-Z0-9_]*_ROOT\}/;
119
+
120
+ function isRuneStatusLine(command) {
121
+ if (typeof command !== 'string') return false;
122
+ // Match the installer's own output shapes only — not any command that happens
123
+ // to contain the substring "rune-pulse" (a user alias could legitimately contain it).
124
+ // Accepts: (1) npx @rune-kit/rune ..., (2) tier-rendered `${RUNE_*_ROOT}/hooks/...`.
125
+ if (RUNE_TIER_STATUSLINE_RE.test(command)) return true;
126
+ return /(^|\s)npx(\s+--yes)?\s+@rune-kit\/rune\b/.test(command);
127
+ }
128
+
129
+ export async function status(projectRoot) {
130
+ const settingsPath = path.join(projectRoot, SETTINGS_REL_PATH);
131
+ if (!existsSync(settingsPath)) {
132
+ return {
133
+ installed: false,
134
+ preset: null,
135
+ wired: [],
136
+ missing: [...WIRED_SKILLS],
137
+ notes: ['no .claude/settings.json'],
138
+ };
139
+ }
140
+ const settings = await readJson(settingsPath);
141
+ const summary = summarizeRuneHooks(settings);
142
+ const preset = detectPreset(settings);
143
+ const wired = Array.from(new Set(Object.values(summary.events).flat()));
144
+ const missing = WIRED_SKILLS.filter((s) => !wired.includes(s));
145
+ return {
146
+ installed: summary.total > 0,
147
+ preset: preset === 'none' ? null : preset,
148
+ wired,
149
+ missing,
150
+ events: summary.events,
151
+ notes: [],
152
+ };
153
+ }
154
+
155
+ async function readJson(settingsPath) {
156
+ if (!existsSync(settingsPath)) return {};
157
+ const raw = await readFile(settingsPath, 'utf-8');
158
+ if (!raw.trim()) return {};
159
+ try {
160
+ return JSON.parse(raw);
161
+ } catch (err) {
162
+ throw new Error(
163
+ `${settingsPath} is not valid JSON — fix it manually or delete the file and re-run \`rune hooks install\`. (${err.message})`,
164
+ );
165
+ }
166
+ }