@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
@@ -6,7 +6,7 @@
6
6
  * Commands:
7
7
  * rune init — Interactive setup for a new project
8
8
  * rune build — Compile skills for the configured platform
9
- * rune doctor — Validate compiled output
9
+ * rune doctor — Validate compiled output + mesh integrity (--mesh for mesh only)
10
10
  * rune status — Project dashboard (neofetch-style)
11
11
  * rune visualize — Interactive mesh graph
12
12
  */
@@ -18,8 +18,12 @@ import { createInterface } from 'node:readline';
18
18
  import { fileURLToPath } from 'node:url';
19
19
  import { getAdapter, listPlatforms } from '../adapters/index.js';
20
20
  import { getAllAnalytics } from '../analytics.js';
21
+ import { dispatchHook } from '../commands/hook-dispatch.js';
22
+ import { installHooks } from '../commands/hooks/install.js';
23
+ import { hookStatus } from '../commands/hooks/status.js';
24
+ import { uninstallHooks } from '../commands/hooks/uninstall.js';
21
25
  import { generateDashboardHTML } from '../dashboard.js';
22
- import { formatDoctorResults, runDoctor } from '../doctor.js';
26
+ import { checkMeshIntegrity, formatDoctorResults, formatMeshResults, runDoctor } from '../doctor.js';
23
27
  import { buildAll } from '../emitter.js';
24
28
  import { collectStats, renderStatus, renderStatusJson } from '../status.js';
25
29
  import { collectGraphData, generateMeshHTML } from '../visualizer.js';
@@ -237,6 +241,17 @@ async function cmdBuild(projectRoot, args) {
237
241
  async function cmdDoctor(projectRoot, args) {
238
242
  const config = await readConfig(projectRoot);
239
243
 
244
+ // --mesh flag: run mesh integrity check only
245
+ if (args.mesh) {
246
+ log('');
247
+ const meshResults = await checkMeshIntegrity(RUNE_ROOT);
248
+ log(formatMeshResults(meshResults));
249
+ if (meshResults.errors.length > 0) process.exit(1);
250
+ // Exit with warning code if there are warnings (for CI awareness)
251
+ if (args.strict && meshResults.warnings.length > 0) process.exit(1);
252
+ return;
253
+ }
254
+
240
255
  if (!config) {
241
256
  // No config = CI or fresh clone. Run source-only checks (split packs).
242
257
  log('');
@@ -248,6 +263,12 @@ async function cmdDoctor(projectRoot, args) {
248
263
  runeRoot: RUNE_ROOT,
249
264
  });
250
265
  log(formatDoctorResults(results));
266
+
267
+ // Also run mesh check in source-only mode
268
+ log('');
269
+ const meshResults = await checkMeshIntegrity(RUNE_ROOT);
270
+ log(formatMeshResults(meshResults));
271
+
251
272
  if (!results.healthy) process.exit(1);
252
273
  return;
253
274
  }
@@ -265,6 +286,11 @@ async function cmdDoctor(projectRoot, args) {
265
286
 
266
287
  log(formatDoctorResults(results));
267
288
 
289
+ // Also run mesh check
290
+ log('');
291
+ const meshResults = await checkMeshIntegrity(runeRoot);
292
+ log(formatMeshResults(meshResults));
293
+
268
294
  if (!results.healthy) process.exit(1);
269
295
  }
270
296
 
@@ -403,10 +429,155 @@ async function cmdAnalytics(projectRoot, args) {
403
429
  }
404
430
  }
405
431
 
432
+ // ─── Hook Commands ───
433
+
434
+ async function cmdHooks(projectRoot, args, subcommand) {
435
+ if (!subcommand) {
436
+ log('');
437
+ log(' rune hooks — Auto-discipline entry point for AI IDEs');
438
+ log('');
439
+ log(' Subcommands:');
440
+ log(' install [--preset strict|gentle|off] [--platform <name>|all] [--tier pro|business]');
441
+ log(' Wire Rune hooks / rules / workflows');
442
+ log(
443
+ ' uninstall [--platform <name>|all] Remove Rune-managed entries (keeps user entries)',
444
+ );
445
+ log(
446
+ ' status [--platform <name>|all] [--tier pro|business] Show active preset, wired skills, tier coverage',
447
+ );
448
+ log('');
449
+ log(' Platforms: claude, cursor, windsurf, antigravity (auto-detected if omitted)');
450
+ log(' Tiers: pro, business — requires $RUNE_PRO_ROOT / $RUNE_BUSINESS_ROOT env var or monorepo sibling.');
451
+ log(' Options:');
452
+ log(' --dry Preview changes without writing');
453
+ log(' --tier <name>[,<name>] Overlay tier manifest(s) — pass multiple tiers comma-separated.');
454
+ log('');
455
+ return;
456
+ }
457
+
458
+ switch (subcommand) {
459
+ case 'install': {
460
+ const tierArg = parseTierFlag(args.tier);
461
+ const result = await installHooks(projectRoot, {
462
+ preset: args.preset,
463
+ dry: args.dry,
464
+ platform: args.platform,
465
+ tier: tierArg,
466
+ });
467
+ log('');
468
+ if (result.platforms.length === 0) {
469
+ for (const note of result.notes) logStep('·', note);
470
+ log('');
471
+ break;
472
+ }
473
+ const tierSuffix = result.tiers?.length ? ` + tier(s): ${result.tiers.join(', ')}` : '';
474
+ if (result.written) {
475
+ logStep('✓', `Installed preset "${result.preset}"${tierSuffix} across: ${result.platforms.join(', ')}`);
476
+ } else if (args.dry) {
477
+ logStep('◎', `Dry-run — no changes written (platforms: ${result.platforms.join(', ')}${tierSuffix})`);
478
+ }
479
+ for (const r of result.results) {
480
+ log('');
481
+ log(` [${r.platform}]`);
482
+ for (const file of r.files) {
483
+ const rel = path.relative(projectRoot, file.path);
484
+ logStep(file.deleted ? '−' : '→', rel);
485
+ }
486
+ for (const note of r.notes) logStep('·', note);
487
+ }
488
+ log('');
489
+ break;
490
+ }
491
+ case 'uninstall': {
492
+ const result = await uninstallHooks(projectRoot, { dry: args.dry, platform: args.platform });
493
+ log('');
494
+ if (result.platforms.length === 0) {
495
+ for (const note of result.notes) logStep('·', note);
496
+ log('');
497
+ break;
498
+ }
499
+ if (result.written) {
500
+ logStep('✓', `Uninstalled Rune entries across: ${result.platforms.join(', ')}`);
501
+ } else if (args.dry) {
502
+ logStep('◎', `Dry-run — would uninstall across: ${result.platforms.join(', ')}`);
503
+ }
504
+ for (const r of result.results) {
505
+ log('');
506
+ log(` [${r.platform}]`);
507
+ for (const file of r.files) {
508
+ const rel = path.relative(projectRoot, file.path);
509
+ logStep(file.deleted ? '−' : '→', rel);
510
+ }
511
+ for (const note of r.notes) logStep('·', note);
512
+ }
513
+ log('');
514
+ break;
515
+ }
516
+ case 'status': {
517
+ const tierArg = parseTierFlag(args.tier);
518
+ const result = await hookStatus(projectRoot, RUNE_ROOT, { platform: args.platform, tier: tierArg });
519
+ log('');
520
+ if (result.platforms.length === 0) {
521
+ for (const note of result.notes) logStep('·', note);
522
+ log('');
523
+ break;
524
+ }
525
+ for (const r of result.results) {
526
+ log(` [${r.platform}]${r.capability ? ` (${r.capability.maturity})` : ''}`);
527
+ log(` installed: ${r.installed ? 'yes' : 'no'}`);
528
+ log(` preset: ${r.preset ?? 'none'}`);
529
+ if (r.wired.length > 0) log(` wired: ${r.wired.join(', ')}`);
530
+ if (r.missing.length > 0) logStep('⚠', `missing: ${r.missing.join(', ')}`);
531
+ if (r.events) {
532
+ for (const [event, skills] of Object.entries(r.events)) {
533
+ log(` ${event}: ${skills.join(', ')}`);
534
+ }
535
+ }
536
+ for (const note of r.notes) logStep('·', note);
537
+ log('');
538
+ }
539
+ if (result.tiers && result.tiers.length > 0) {
540
+ log(' Tiers:');
541
+ for (const t of result.tiers) {
542
+ if (!t.found) {
543
+ logStep('⚠', `${t.tier}: manifest not found${t.error ? ` (${t.error})` : ''}`);
544
+ } else {
545
+ const req = t.requiresOk ? 'env OK' : `missing env: ${t.requiresMissing.join(', ')}`;
546
+ logStep('·', `${t.tier} v${t.version} — ${t.entries} entries — ${req}`);
547
+ }
548
+ }
549
+ log('');
550
+ }
551
+ if (result.missingInRepo.length > 0) {
552
+ logStep('⚠', `Skills referenced by presets but not found in repo: ${result.missingInRepo.join(', ')}`);
553
+ log('');
554
+ }
555
+ break;
556
+ }
557
+ default:
558
+ log(` ✗ Unknown hooks subcommand: ${subcommand}. Run \`rune hooks\` for help.`);
559
+ process.exit(1);
560
+ }
561
+ }
562
+
406
563
  // ─── Arg Parsing ───
407
564
 
408
565
  // Flags that require a string value (not boolean)
409
- const VALUE_REQUIRED_FLAGS = new Set(['platform', 'output', 'disable', 'extensions', 'days']);
566
+ const VALUE_REQUIRED_FLAGS = new Set(['platform', 'output', 'disable', 'extensions', 'days', 'preset', 'tier']);
567
+
568
+ function parseTierFlag(raw) {
569
+ if (!raw) return undefined;
570
+ if (Array.isArray(raw)) {
571
+ return raw
572
+ .flatMap((v) => String(v).split(','))
573
+ .map((s) => s.trim())
574
+ .filter(Boolean);
575
+ }
576
+ return String(raw)
577
+ .split(',')
578
+ .map((s) => s.trim())
579
+ .filter(Boolean);
580
+ }
410
581
 
411
582
  function parseArgs(argv) {
412
583
  const args = {};
@@ -431,13 +602,21 @@ function parseArgs(argv) {
431
602
  }
432
603
  }
433
604
 
434
- return { command: positional[0], args };
605
+ return { command: positional[0], subcommand: positional[1], args, positional };
435
606
  }
436
607
 
437
608
  // ─── Main ───
438
609
 
439
610
  async function main() {
440
- const { command, args } = parseArgs(process.argv.slice(2));
611
+ const rawArgv = process.argv.slice(2);
612
+
613
+ // hook-dispatch bypasses parseArgs so flags like --gentle reach the dispatcher verbatim
614
+ if (rawArgv[0] === 'hook-dispatch') {
615
+ const exitCode = await dispatchHook(rawArgv.slice(1));
616
+ process.exit(exitCode);
617
+ }
618
+
619
+ const { command, subcommand, args } = parseArgs(rawArgv);
441
620
  const projectRoot = process.cwd();
442
621
 
443
622
  // Handle --version / -v as flag (not positional command)
@@ -468,6 +647,9 @@ async function main() {
468
647
  case 'dash':
469
648
  await cmdAnalytics(projectRoot, args);
470
649
  break;
650
+ case 'hooks':
651
+ await cmdHooks(projectRoot, args, subcommand);
652
+ break;
471
653
  case 'version':
472
654
  case '--version':
473
655
  case '-v': {
@@ -484,10 +666,18 @@ async function main() {
484
666
  log(' Commands:');
485
667
  log(' init Interactive setup (auto-detects platform)');
486
668
  log(' build Compile skills for configured platform');
487
- log(' doctor Validate compiled output');
669
+ log(' doctor Validate compiled output + mesh integrity');
670
+ log(' --mesh Mesh integrity only (reciprocals, versions, sections)');
671
+ log(' --strict Fail on warnings (for CI)');
488
672
  log(' status Project dashboard (skills, signals, packs, health)');
489
673
  log(' visualize Interactive mesh graph (opens in browser)');
490
674
  log(' analytics Usage analytics dashboard (Business tier)');
675
+ log(' hooks Install/uninstall/status for multi-platform auto-discipline');
676
+ log(
677
+ ' hooks install [--preset gentle|strict|off] [--platform claude|cursor|windsurf|antigravity|all]',
678
+ );
679
+ log(' hooks uninstall [--platform <name>|all]');
680
+ log(' hooks status [--platform <name>|all]');
491
681
  log('');
492
682
  log(' Options:');
493
683
  log(
@@ -0,0 +1,87 @@
1
+ /**
2
+ * `rune hook-dispatch <skill> [--gentle]`
3
+ *
4
+ * Runtime dispatcher invoked by Claude Code hooks. Reads event JSON from stdin,
5
+ * validates the skill name against an allowlist, then forwards to the skill's
6
+ * execution (currently a no-op placeholder that emits a verdict line — skill
7
+ * invocation wiring happens when skills gain a headless mode).
8
+ *
9
+ * In gentle mode: always exits 0, prints advisory line.
10
+ * In strict mode: exits 2 on BLOCK verdict (Claude Code blocks the tool call).
11
+ *
12
+ * Security:
13
+ * - Skill name MUST match the allowlist (no arbitrary shell injection)
14
+ * - Event payload is parsed but never passed to shell commands
15
+ * - Unknown skills fail closed (error in strict, warn in gentle)
16
+ */
17
+
18
+ import { WIRED_SKILLS } from './hooks/presets.js';
19
+
20
+ const ALLOWLIST = new Set(WIRED_SKILLS);
21
+
22
+ /**
23
+ * @param {string[]} argv — positional args after `hook-dispatch`
24
+ * @param {{stdin?: NodeJS.ReadableStream, stdout?: NodeJS.WritableStream, stderr?: NodeJS.WritableStream}} io
25
+ * @returns {Promise<number>} exit code
26
+ */
27
+ export async function dispatchHook(argv, io = {}) {
28
+ const stdout = io.stdout || process.stdout;
29
+ const stderr = io.stderr || process.stderr;
30
+ const stdin = io.stdin || process.stdin;
31
+
32
+ const positional = argv.filter((a) => !a.startsWith('--'));
33
+ const flags = new Set(argv.filter((a) => a.startsWith('--')));
34
+ const skill = positional[0];
35
+ const gentle = flags.has('--gentle');
36
+
37
+ if (!skill) {
38
+ stderr.write('rune hook-dispatch: missing skill name\n');
39
+ return gentle ? 0 : 1;
40
+ }
41
+
42
+ if (!ALLOWLIST.has(skill)) {
43
+ stderr.write(`rune hook-dispatch: unknown skill "${skill}"\n`);
44
+ return gentle ? 0 : 1;
45
+ }
46
+
47
+ // Read event JSON from stdin (best-effort; hooks may pass empty stdin)
48
+ let eventJson = {};
49
+ try {
50
+ const raw = await readStdin(stdin);
51
+ if (raw.trim()) eventJson = JSON.parse(raw);
52
+ } catch {
53
+ // Non-JSON stdin is tolerable — skills may not need the payload
54
+ }
55
+
56
+ // Skill invocation placeholder — skills don't yet expose a headless API.
57
+ // For v1: emit advisory line, pass through. This unblocks hook installation
58
+ // while skill-forge adds `--hook-mode` to preflight/sentinel/etc.
59
+ const mode = gentle ? 'advisory' : 'enforcing';
60
+ stdout.write(`rune-hook: ${skill} [${mode}] — tool=${eventJson?.tool_name || 'unknown'}\n`);
61
+
62
+ // Until skills expose headless verdicts, dispatcher returns neutral success.
63
+ // When skills add `--hook-mode`, extend this to run them and propagate exit codes.
64
+ return 0;
65
+ }
66
+
67
+ function readStdin(stream) {
68
+ return new Promise((resolve, reject) => {
69
+ if (stream.isTTY) {
70
+ resolve('');
71
+ return;
72
+ }
73
+ let buf = '';
74
+ stream.setEncoding('utf-8');
75
+ stream.on('data', (chunk) => {
76
+ buf += chunk;
77
+ if (buf.length > 1_000_000) {
78
+ stream.destroy();
79
+ reject(new Error('stdin too large'));
80
+ }
81
+ });
82
+ stream.on('end', () => resolve(buf));
83
+ stream.on('error', reject);
84
+ // Safety timeout — don't block Claude Code indefinitely
85
+ setTimeout(() => resolve(buf), 500);
86
+ });
87
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * `rune hooks install [--preset strict|gentle|off] [--platform claude|cursor|windsurf|antigravity|all]`
3
+ *
4
+ * Writes Rune-managed hook/rule/workflow entries for one or more platforms.
5
+ * Idempotent: re-running replaces existing Rune entries, preserves user entries.
6
+ */
7
+
8
+ import { existsSync } from 'node:fs';
9
+ import { mkdir, unlink, writeFile } from 'node:fs/promises';
10
+ import path from 'node:path';
11
+ import { ADAPTERS, detectPlatforms, getAdapter, PLATFORM_KEYS } from '../../adapters/hooks/index.js';
12
+ import { checkManifestRequires, resolveTier } from './tiers.js';
13
+
14
+ /**
15
+ * @param {string} projectRoot
16
+ * @param {{preset?: string, dry?: boolean, platform?: string|string[], tier?: string|string[]}} args
17
+ */
18
+ export async function installHooks(projectRoot, args = {}) {
19
+ const preset = args.preset || 'gentle';
20
+ if (preset !== 'strict' && preset !== 'gentle' && preset !== 'off') {
21
+ throw new Error(`Invalid preset: ${preset}. Choose from: strict | gentle | off`);
22
+ }
23
+
24
+ const tierManifests = await resolveTierManifests(projectRoot, args.tier);
25
+ const tierNotes = [];
26
+ for (const manifest of tierManifests) {
27
+ const { ok, missing } = checkManifestRequires(manifest);
28
+ if (!ok) {
29
+ tierNotes.push(
30
+ `${manifest.tier}: missing env var${missing.length === 1 ? '' : 's'} (${missing.join(', ')}) — hooks are written with literal \${${missing[0]}} and will FAIL at runtime until you \`export ${missing.join('=… && export ')}=…\` in your shell.`,
31
+ );
32
+ }
33
+ }
34
+
35
+ const platforms = resolvePlatforms(projectRoot, args.platform);
36
+ if (platforms.length === 0) {
37
+ return {
38
+ preset,
39
+ tiers: tierManifests.map((m) => m.tier),
40
+ platforms: [],
41
+ results: [],
42
+ written: false,
43
+ notes: [
44
+ 'No target platform detected. Create `.claude/`, `.cursor/`, `.windsurf/`, or `.antigravity/` first, or pass `--platform <name>`.',
45
+ ...tierNotes,
46
+ ],
47
+ };
48
+ }
49
+
50
+ const results = [];
51
+ let totalWrites = 0;
52
+ for (const id of platforms) {
53
+ const adapter = getAdapter(id);
54
+ const plan = await adapter.emit({ preset, projectRoot, tierManifests });
55
+ let platformWrites = 0;
56
+ if (!args.dry) {
57
+ for (const file of plan.files) {
58
+ if (file.content === null) {
59
+ if (existsSync(file.path)) {
60
+ await unlink(file.path);
61
+ platformWrites += 1;
62
+ }
63
+ } else {
64
+ await mkdir(path.dirname(file.path), { recursive: true });
65
+ await writeFile(file.path, file.content, 'utf-8');
66
+ platformWrites += 1;
67
+ }
68
+ }
69
+ }
70
+ totalWrites += platformWrites;
71
+ results.push({
72
+ platform: id,
73
+ files: plan.files.map((f) => ({ path: f.path, deleted: f.content === null })),
74
+ notes: plan.notes,
75
+ writes: platformWrites,
76
+ });
77
+ }
78
+
79
+ return {
80
+ preset,
81
+ tiers: tierManifests.map((m) => m.tier),
82
+ platforms,
83
+ results,
84
+ written: !args.dry && totalWrites > 0,
85
+ notes: [...(totalWrites === 0 && !args.dry ? ['no changes to apply (already clean)'] : []), ...tierNotes],
86
+ };
87
+ }
88
+
89
+ /**
90
+ * Resolve requested tiers to loaded manifests.
91
+ * @returns {Promise<import('./tiers.js').TierManifest[]>}
92
+ */
93
+ async function resolveTierManifests(projectRoot, requested) {
94
+ if (!requested) return [];
95
+ const list = Array.isArray(requested) ? requested : [requested];
96
+ const unique = Array.from(new Set(list.filter((t) => typeof t === 'string' && t.length > 0)));
97
+ const manifests = [];
98
+ for (const tier of unique) {
99
+ manifests.push(await resolveTier(tier, projectRoot));
100
+ }
101
+ return manifests;
102
+ }
103
+
104
+ function resolvePlatforms(projectRoot, requested) {
105
+ if (!requested) return detectPlatforms(projectRoot);
106
+ const list = Array.isArray(requested) ? requested : [requested];
107
+ const expanded = [];
108
+ for (const item of list) {
109
+ if (item === 'all') {
110
+ // `all` = every *detected* platform, never force-creates unrelated platform dirs.
111
+ // To install into a platform with no existing directory, pass `--platform <name>` explicitly.
112
+ expanded.push(...detectPlatforms(projectRoot));
113
+ } else if (ADAPTERS[item]) {
114
+ expanded.push(item);
115
+ } else {
116
+ throw new Error(`Unknown platform: ${item}. Choose from: ${PLATFORM_KEYS.join(', ')}, all`);
117
+ }
118
+ }
119
+ return Array.from(new Set(expanded));
120
+ }
@@ -0,0 +1,211 @@
1
+ /**
2
+ * settings.json merger for `rune hooks install/uninstall`.
3
+ *
4
+ * Strategy: detect Rune-managed hook entries by command signature (not JSON
5
+ * comments — settings.json is strict JSON). Remove all Rune entries, then
6
+ * inject new ones from the preset. User-authored hooks in the same event
7
+ * arrays are preserved.
8
+ */
9
+
10
+ import { isRuneManaged } from './presets.js';
11
+
12
+ /**
13
+ * Strip all Rune-managed entries from a settings.json object.
14
+ * Returns a new settings object (no mutation).
15
+ *
16
+ * @param {Object} settings — parsed .claude/settings.json content
17
+ * @returns {Object} settings with Rune hooks removed
18
+ */
19
+ export function stripRuneHooks(settings) {
20
+ if (!settings || typeof settings !== 'object') return {};
21
+ if (!settings.hooks || typeof settings.hooks !== 'object') return settings;
22
+
23
+ const cleanedHooks = {};
24
+ for (const [eventName, matcherGroups] of Object.entries(settings.hooks)) {
25
+ if (!Array.isArray(matcherGroups)) {
26
+ cleanedHooks[eventName] = matcherGroups;
27
+ continue;
28
+ }
29
+
30
+ const cleanedGroups = matcherGroups
31
+ .map((group) => {
32
+ if (!group || !Array.isArray(group.hooks)) return group;
33
+ const cleanedEntries = group.hooks.filter((entry) => !isRuneManaged(entry));
34
+ return { ...group, hooks: cleanedEntries };
35
+ })
36
+ .filter((group) => {
37
+ // Drop groups that became empty after stripping Rune entries
38
+ if (!group || !Array.isArray(group.hooks)) return true;
39
+ return group.hooks.length > 0;
40
+ });
41
+
42
+ if (cleanedGroups.length > 0) {
43
+ cleanedHooks[eventName] = cleanedGroups;
44
+ }
45
+ }
46
+
47
+ const result = { ...settings };
48
+ if (Object.keys(cleanedHooks).length > 0) {
49
+ result.hooks = cleanedHooks;
50
+ } else {
51
+ delete result.hooks;
52
+ }
53
+ return result;
54
+ }
55
+
56
+ /**
57
+ * Merge a preset hook block into existing settings. Existing Rune entries are
58
+ * replaced; user entries in the same events are preserved.
59
+ *
60
+ * @param {Object} settings — existing .claude/settings.json content (or {})
61
+ * @param {Object} preset — output of buildPreset()
62
+ * @returns {Object} merged settings
63
+ */
64
+ export function mergePreset(settings, preset) {
65
+ const stripped = stripRuneHooks(settings || {});
66
+ return appendHookBlock(stripped, preset);
67
+ }
68
+
69
+ /**
70
+ * Append a hook block to pre-stripped settings — does NOT strip Rune entries
71
+ * first. Use this when layering multiple Rune blocks (e.g. preset + tier)
72
+ * after a single upstream `stripRuneHooks` call, to avoid each additional
73
+ * layer wiping the previous one.
74
+ *
75
+ * @param {Object} settings — already-stripped settings (or {})
76
+ * @param {Object} block — { hooks: {...}, ... }
77
+ * @returns {Object} merged settings
78
+ */
79
+ export function appendHookBlock(settings, block) {
80
+ const base = settings && typeof settings === 'object' ? settings : {};
81
+ const existingHooks = base.hooks && typeof base.hooks === 'object' ? base.hooks : {};
82
+ const merged = { ...existingHooks };
83
+
84
+ for (const [eventName, matcherGroups] of Object.entries(block?.hooks || {})) {
85
+ const existing = Array.isArray(merged[eventName]) ? merged[eventName] : [];
86
+ merged[eventName] = mergeEventGroups(existing, matcherGroups);
87
+ }
88
+
89
+ return { ...base, hooks: merged };
90
+ }
91
+
92
+ /**
93
+ * Merge preset matcher groups into existing groups for a single event.
94
+ * If a matcher string already has a user group, append Rune entries into it
95
+ * rather than duplicating the group.
96
+ */
97
+ function mergeEventGroups(existingGroups, presetGroups) {
98
+ const result = existingGroups.map((g) => ({ ...g, hooks: [...(g.hooks || [])] }));
99
+
100
+ for (const presetGroup of presetGroups) {
101
+ const existingIdx = result.findIndex((g) => g.matcher === presetGroup.matcher);
102
+ if (existingIdx >= 0) {
103
+ result[existingIdx].hooks.push(...presetGroup.hooks);
104
+ } else {
105
+ result.push({ ...presetGroup, hooks: [...presetGroup.hooks] });
106
+ }
107
+ }
108
+
109
+ return result;
110
+ }
111
+
112
+ /**
113
+ * Summarize Rune hook entries currently in settings — for `rune hooks status`.
114
+ *
115
+ * @param {Object} settings
116
+ * @returns {{events: Record<string, string[]>, total: number}}
117
+ */
118
+ export function summarizeRuneHooks(settings) {
119
+ const events = {};
120
+ let total = 0;
121
+ if (!settings?.hooks) return { events, total };
122
+
123
+ for (const [eventName, matcherGroups] of Object.entries(settings.hooks)) {
124
+ if (!Array.isArray(matcherGroups)) continue;
125
+ for (const group of matcherGroups) {
126
+ if (!Array.isArray(group?.hooks)) continue;
127
+ for (const entry of group.hooks) {
128
+ if (!isRuneManaged(entry)) continue;
129
+ total += 1;
130
+ const skill = extractSkillFromCommand(entry.command);
131
+ if (!events[eventName]) events[eventName] = [];
132
+ events[eventName].push(skill);
133
+ }
134
+ }
135
+ }
136
+ return { events, total };
137
+ }
138
+
139
+ function extractSkillFromCommand(command) {
140
+ // Expected shapes:
141
+ // "npx --yes @rune-kit/rune hook-dispatch <skill> [--gentle]" (Free preset / dispatch)
142
+ // "node \"${RUNE_*_ROOT}/hooks/run-hook.cjs\" <skill>" (tier entry)
143
+ const dispatch = command.match(/hook-dispatch\s+(\S+)/);
144
+ if (dispatch) return dispatch[1];
145
+ const tier = command.match(/run-hook\.cjs(?:"|')?\s+(\S+)/);
146
+ if (tier) return tier[1];
147
+ return 'unknown';
148
+ }
149
+
150
+ /**
151
+ * Strip entries whose extracted skill name appears in `skillIds` from an
152
+ * already-parsed settings object. Used to honor manifest `overrides`:
153
+ * e.g., Pro's `context-sense` replaces an older `context-watch` entry.
154
+ *
155
+ * @param {Object} settings
156
+ * @param {Iterable<string>} skillIds — skill names to remove
157
+ * @returns {Object} new settings
158
+ */
159
+ export function stripHooksBySkill(settings, skillIds) {
160
+ const targets = new Set(skillIds);
161
+ if (!settings?.hooks || targets.size === 0) return settings;
162
+
163
+ const cleanedHooks = {};
164
+ for (const [eventName, matcherGroups] of Object.entries(settings.hooks)) {
165
+ if (!Array.isArray(matcherGroups)) {
166
+ cleanedHooks[eventName] = matcherGroups;
167
+ continue;
168
+ }
169
+ const cleanedGroups = matcherGroups
170
+ .map((group) => {
171
+ if (!group || !Array.isArray(group.hooks)) return group;
172
+ const filtered = group.hooks.filter((entry) => {
173
+ if (!entry || typeof entry.command !== 'string') return true;
174
+ return !targets.has(extractSkillFromCommand(entry.command));
175
+ });
176
+ return { ...group, hooks: filtered };
177
+ })
178
+ .filter((group) => !group || !Array.isArray(group.hooks) || group.hooks.length > 0);
179
+ if (cleanedGroups.length > 0) cleanedHooks[eventName] = cleanedGroups;
180
+ }
181
+
182
+ const result = { ...settings };
183
+ if (Object.keys(cleanedHooks).length > 0) result.hooks = cleanedHooks;
184
+ else delete result.hooks;
185
+ return result;
186
+ }
187
+
188
+ /**
189
+ * Detect active preset by examining Rune commands in settings.
190
+ * Returns 'gentle' | 'strict' | 'mixed' | 'none'.
191
+ */
192
+ export function detectPreset(settings) {
193
+ if (!settings?.hooks) return 'none';
194
+ let hasGentle = false;
195
+ let hasStrict = false;
196
+ for (const matcherGroups of Object.values(settings.hooks)) {
197
+ if (!Array.isArray(matcherGroups)) continue;
198
+ for (const group of matcherGroups) {
199
+ if (!Array.isArray(group?.hooks)) continue;
200
+ for (const entry of group.hooks) {
201
+ if (!isRuneManaged(entry)) continue;
202
+ if (entry.command.includes('--gentle')) hasGentle = true;
203
+ else hasStrict = true;
204
+ }
205
+ }
206
+ }
207
+ if (hasGentle && hasStrict) return 'mixed';
208
+ if (hasGentle) return 'gentle';
209
+ if (hasStrict) return 'strict';
210
+ return 'none';
211
+ }