rails-hyperdrive 0.3.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 (55) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +295 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +229 -0
  5. data/Rakefile +6 -0
  6. data/SECURITY.md +43 -0
  7. data/config/routes.rb +5 -0
  8. data/lib/generators/hyperdrive/content_sync_support.rb +54 -0
  9. data/lib/generators/hyperdrive/discover/discover_generator.rb +103 -0
  10. data/lib/generators/hyperdrive/gitignore_support.rb +28 -0
  11. data/lib/generators/hyperdrive/install/USAGE +22 -0
  12. data/lib/generators/hyperdrive/install/install_generator.rb +168 -0
  13. data/lib/generators/hyperdrive/install/templates/initializer.rb.tt +3 -0
  14. data/lib/generators/hyperdrive/install_summary.rb +70 -0
  15. data/lib/generators/hyperdrive/sync/USAGE +21 -0
  16. data/lib/generators/hyperdrive/sync/sync_generator.rb +40 -0
  17. data/lib/generators/hyperdrive/sync_runner.rb +70 -0
  18. data/lib/rails/hyperdrive/artifact_status.rb +86 -0
  19. data/lib/rails/hyperdrive/audit_header.rb +83 -0
  20. data/lib/rails/hyperdrive/auto_install.rb +98 -0
  21. data/lib/rails/hyperdrive/bundler_artifact_discovery.rb +320 -0
  22. data/lib/rails/hyperdrive/claude_md_import.rb +69 -0
  23. data/lib/rails/hyperdrive/companion_discovery.rb +232 -0
  24. data/lib/rails/hyperdrive/console_executor.rb +64 -0
  25. data/lib/rails/hyperdrive/data/gem_categories.yml +52 -0
  26. data/lib/rails/hyperdrive/drift_verdict.rb +42 -0
  27. data/lib/rails/hyperdrive/eager_footprint.rb +48 -0
  28. data/lib/rails/hyperdrive/engine.rb +22 -0
  29. data/lib/rails/hyperdrive/index_document.rb +49 -0
  30. data/lib/rails/hyperdrive/install_layout.rb +45 -0
  31. data/lib/rails/hyperdrive/install_pipeline.rb +415 -0
  32. data/lib/rails/hyperdrive/install_plan.rb +78 -0
  33. data/lib/rails/hyperdrive/install_shell.rb +43 -0
  34. data/lib/rails/hyperdrive/lock_file.rb +171 -0
  35. data/lib/rails/hyperdrive/mcp_server.rb +80 -0
  36. data/lib/rails/hyperdrive/resources/skill.rb +63 -0
  37. data/lib/rails/hyperdrive/resources/stack_profile.rb +32 -0
  38. data/lib/rails/hyperdrive/safety/rack_middleware.rb +54 -0
  39. data/lib/rails/hyperdrive/skill_template.rb +52 -0
  40. data/lib/rails/hyperdrive/sql_safety.rb +28 -0
  41. data/lib/rails/hyperdrive/stack_profile.rb +176 -0
  42. data/lib/rails/hyperdrive/tools/base.rb +39 -0
  43. data/lib/rails/hyperdrive/tools/describe_app.rb +21 -0
  44. data/lib/rails/hyperdrive/tools/list_models.rb +76 -0
  45. data/lib/rails/hyperdrive/tools/list_routes.rb +33 -0
  46. data/lib/rails/hyperdrive/tools/locate_source.rb +86 -0
  47. data/lib/rails/hyperdrive/tools/lookup_doc.rb +60 -0
  48. data/lib/rails/hyperdrive/tools/run_ruby.rb +31 -0
  49. data/lib/rails/hyperdrive/tools/run_sql.rb +49 -0
  50. data/lib/rails/hyperdrive/tools/tail_logs.rb +65 -0
  51. data/lib/rails/hyperdrive/version.rb +5 -0
  52. data/lib/rails/hyperdrive.rb +37 -0
  53. data/lib/rails-hyperdrive.rb +2 -0
  54. data/lib/tasks/hyperdrive.rake +22 -0
  55. metadata +161 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7577ebf92af02fb8bb079a8b32c8751744340390f0f0facc0e682bc31aa6d43d
4
+ data.tar.gz: 990f940533691baf570a17113806fcd274300b56ee171a22195921ea9ddeb6bd
5
+ SHA512:
6
+ metadata.gz: af48e37db907f10bf2a74d8303aa57b5cd04bd44d7ab97834bc197d97f39cf77cef87116217ce8d773d95d5ae6d93b6e216cbec275b3886bdb7c9b32cbe356af
7
+ data.tar.gz: 99144761cd64cf3bbed57caf0eaa0ab691511d7246e7add30e8fdf94bcd2f1e857c0b1612b5b95033a9336ca71faf80bd9bdae3db9813c3377dea3efdf6986c7
data/CHANGELOG.md ADDED
@@ -0,0 +1,295 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.3.0] - 2026-08-04
11
+
12
+ ### Added
13
+
14
+ - `bundler-rails-hyperdrive`, a Bundler plugin gem co-located in this repository
15
+ (`bundler-rails-hyperdrive/`). Once registered, it runs after every
16
+ `bundle install` in development and installs the artifacts that newly
17
+ bundled companion gems ship — additive only (it never overwrites or
18
+ deletes a file) — and reports upgraded or orphaned artifacts with a
19
+ pointer to `bin/rails hyperdrive:sync`. It resolves rails-hyperdrive from
20
+ the application's bundle at runtime (supported range `>= 0.2`), stays
21
+ silent outside development, and never fails a `bundle install`.
22
+ - `hyperdrive:init` now registers the plugin by appending
23
+ `plugin "bundler-rails-hyperdrive"` to the application's Gemfile. Idempotent: an
24
+ existing directive (with any options) is left alone, and an app without a
25
+ Gemfile gets a skip status.
26
+ - **Gem-conditional skill content.** A multi-file skill can now condition parts
27
+ of itself on the app's bundle, through two complementary mechanisms evaluated
28
+ at discovery time:
29
+ - A `conditional:` map in `SKILL.md` frontmatter gates individual supporting
30
+ files per gem. Keys are dir-relative shipped paths; values reuse the
31
+ artifact-level `gem:`/`versions:` forms (single target, comma-separated
32
+ string, YAML list, per-target `versions:` map, `"*"`), with `versions:`
33
+ optional (omitted = unconstrained). A gated file installs only when a
34
+ listed target is bundled at a satisfying version, and disappears on the
35
+ next `hyperdrive:sync` when its gate closes (unedited copies only). A
36
+ malformed condition fails open — the file installs and the problem is
37
+ reported as a discovery warning.
38
+ - Files named `*.md.erb` in a skill directory — including `SKILL.md.erb` in
39
+ place of `SKILL.md` — are rendered at install time with a sealed helper
40
+ binding (`gem?`, `any_gem?`, `gem_version`) over the resolved bundle, and
41
+ install as plain `.md`. A template that fails to render is skipped with a
42
+ warning (the whole skill, for `SKILL.md.erb`); a plain file always beats a
43
+ template rendering to the same path.
44
+
45
+ - **Multi-file skills.** A companion skill directory can now ship more than
46
+ `SKILL.md`: every other file in it (nested however deep — `workflows/`,
47
+ `references/`, `examples/`, …) installs as a **supporting file** under
48
+ `.claude/skills/<name>/`, preserving the relative layout, so `SKILL.md` can
49
+ reference it with directory-relative links (which survive cross-source
50
+ collision postfixing, since that renames the whole directory). Supporting
51
+ files carry no frontmatter contract and no audit header — they land
52
+ byte-identical to what the gem ships, with provenance and a per-file sha
53
+ recorded in `.hyperdrive/lock.yml` under the new `skill_support` kind. The
54
+ full drift state machine applies per file (unedited upgrades rewritten,
55
+ local edits skipped on sync / restored with `--overwrite`, deletions
56
+ reinstalled), a supporting file the gem stops shipping is cleaned up when
57
+ unedited (warned about and left when edited), disabling a skill removes its
58
+ unedited supporting files too, and the install summary shows them as a
59
+ `(+N files)` count on the skill's line.
60
+
61
+ - `hyperdrive:sync` — content-only actualization. Refreshes skills, guidelines,
62
+ `index.md`, and `.hyperdrive/lock.yml` to match the current bundle, and
63
+ touches no bootstrap artifact (`.mcp.json`, the engine mount, the
64
+ optional initializer, the `.gitignore` rule). Locally-edited files are
65
+ preserved by default (skip + warn); pass `--overwrite` to restore the
66
+ gem-shipped content. Works with or without a prior `hyperdrive:init`.
67
+ - An artifact's `gem:` frontmatter field can now declare **several** targets, as
68
+ either a comma-separated string (`gem: "sidekiq, solid_queue"`) or a YAML list.
69
+ The artifact installs when **any** listed target is in the bundle at a
70
+ satisfying version, so one guideline or skill can cover interchangeable
71
+ libraries instead of shipping as near-identical per-target copies. `"*"`
72
+ anywhere in the list makes the artifact universal.
73
+ - `versions:` accepts a **map keyed by gem name** alongside the existing single
74
+ requirement, for target sets that do not share a version cycle:
75
+
76
+ ```yaml
77
+ gem: [sidekiq, solid_queue]
78
+ versions:
79
+ sidekiq: ">= 7.0"
80
+ solid_queue: ">= 1.0"
81
+ ```
82
+
83
+ A single requirement still applies to every listed target. Targets absent from
84
+ the map are unconstrained.
85
+ - Content installation is now callable without a booted Rails application.
86
+ `Rails::Hyperdrive::InstallPipeline` takes an explicit application root and
87
+ performs the whole install — skills, guidelines, `index.md`, the `CLAUDE.md`
88
+ import line, and `.hyperdrive/lock.yml` — so any process that can
89
+ see the app's bundle can run it. `hyperdrive:init` and `hyperdrive:update`
90
+ call the same pipeline, and their behaviour is unchanged.
91
+ - `Rails::Hyperdrive::ArtifactStatus` compares what the bundle offers against
92
+ what `.hyperdrive/lock.yml` records, classifying every artifact as
93
+ `installed`, `missing`, `outdated`, or `orphaned`.
94
+ - `Rails::Hyperdrive::AutoInstall.run` tops up an already-initialized
95
+ application with artifacts the lockfile does not record yet, and returns
96
+ everything it deliberately left alone. Installation is strictly additive: it
97
+ can create a file that does not exist, and it will never overwrite one, so a
98
+ locally-edited artifact is safe. Upgraded and orphaned artifacts are reported
99
+ for `hyperdrive:update` to handle. It writes only when the environment reads
100
+ as development from `ENV` directly (no Rails to ask), `CI` is unset, and the
101
+ bundle is not frozen, and it reports errors rather than raising them.
102
+
103
+ - `hyperdrive:init` / `hyperdrive:update` now check the *combined* eager
104
+ footprint against a budget, not just per-file size. When the guidelines
105
+ listed in `index.md` exceed ~10,000 tokens, the footprint line is followed
106
+ by a warning naming the two largest contributors, so it is
107
+ clear what to trim or which line to drop from `index.md` to opt a guideline
108
+ out. Individually reasonable guidelines could previously clear every per-file
109
+ check and still add up to real context-window pressure with nothing flagging
110
+ it. The install still proceeds — this is a warning, not a gate.
111
+
112
+ - `hyperdrive:init` / `hyperdrive:update` now list every installed artifact in
113
+ the final summary, grouped under the source gem and version that shipped it,
114
+ instead of reporting bare skill and guideline counts. The listing is built
115
+ from `.hyperdrive/lock.yml`, so it reflects what the app ends up with —
116
+ including files left unchanged, files skipped as locally modified, and
117
+ orphans whose source gem has left the bundle — rather than only what the run
118
+ wrote. `--skip-content` prints no listing, as before.
119
+
120
+ - `hyperdrive:init` / `hyperdrive:update` now warn when git ignores an install
121
+ destination. Installed artifacts are git-tracked on purpose: reviewing the
122
+ diff is how you see what a companion gem added to the agent's context. An app
123
+ that gitignores `.claude/` empties that diff while the artifacts still reach
124
+ the agent, and nothing previously said so. The check asks git whether each
125
+ destination is ignored, so patterns, negations, and per-repository excludes
126
+ are all honored; outside a git repository, or without git installed, it stays
127
+ silent.
128
+
129
+ - Per-artifact opt-out via a `disabled:` list in `.hyperdrive/lock.yml`, keyed by
130
+ artifact type (`skills:` / `guidelines:`). A listed artifact is never installed,
131
+ and one already on disk is removed on the next `hyperdrive:init` /
132
+ `hyperdrive:sync` — but only when the file still matches the content the lock
133
+ recorded, so a locally-modified artifact is reported and left in place instead.
134
+ Disabling a guideline also drops its line from `index.md`. For a name shipped by
135
+ more than one companion gem, the shipped name disables every variant and the
136
+ `--<source-gem>` postfixed name disables one. The list is hand-edited: the
137
+ generator reads it, carries it forward, and writes the empty scaffold on every
138
+ install so the key is discoverable. Previously the only control was
139
+ `--skip-content`, which suppresses all installed content at once.
140
+
141
+ ### Changed
142
+
143
+ - **BREAKING:** the eager-content chain is now companion-driven.
144
+ `.claude/hyperdrive/index.md` and the `@.claude/hyperdrive/index.md` line in
145
+ `CLAUDE.md` are created only once a companion gem ships a guideline, and are
146
+ removed again when the last one leaves the bundle. A zero-companion
147
+ `hyperdrive:init` now writes `.mcp.json`, the `.gitignore` rule, the optional
148
+ initializer, the engine mount, and `.hyperdrive/lock.yml` — and nothing into
149
+ the agent's context window. On tear-down, `CLAUDE.md` is deleted only when it
150
+ is byte-identical to the file hyperdrive created; otherwise only hyperdrive's
151
+ own import line is removed and every other byte is left alone. An import line
152
+ you deleted by hand is still never re-added. `hyperdrive:sync` performs the
153
+ tear-down; the `bundler-rails-hyperdrive` plugin (additive) never removes anything.
154
+ Because a `bundle install` must not edit `CLAUDE.md`, the plugin now says so
155
+ when it installs the first guideline into an app that has no import line yet:
156
+ the guideline is on disk but out of context until you run
157
+ `bin/rails hyperdrive:sync`.
158
+ - **BREAKING:** `hyperdrive:init` no longer accepts `--update` /
159
+ `--force-install`; it always preserves locally-edited files (skip + warn).
160
+ The conflict warning and the AutoInstall nudge now point at
161
+ `hyperdrive:sync`.
162
+ - `hyperdrive:init`'s summary now reports the mounted MCP server and its tool
163
+ count.
164
+ - `bundle install` (through the `bundler-rails-hyperdrive` plugin) no longer parses
165
+ `Gemfile.lock`.
166
+
167
+ ### Removed
168
+
169
+ - **BREAKING:** `.claude/hyperdrive/stack.md` is no longer generated. Ask the
170
+ running server what the app's stack is instead — the `describe_app` MCP tool
171
+ and the `hyperdrive://stack-profile` resource answer it live, from the
172
+ resolved bundle, and can never go stale the way a written-once file does.
173
+
174
+ **Manual migration:** an app installed before this release keeps
175
+ `.claude/hyperdrive/stack.md` on disk, and `hyperdrive:init` / `hyperdrive:sync`
176
+ will report it as an orphan on every run. Delete the file and its
177
+ `.hyperdrive/lock.yml` entry by hand.
178
+ - **BREAKING:** `hyperdrive:update` is removed (not deprecated) — use
179
+ `bin/rails hyperdrive:sync --overwrite`. Note the default flip: the routine
180
+ refresh (`hyperdrive:sync`) now preserves locally-edited files; overwriting
181
+ them is opt-in via `--overwrite`.
182
+
183
+ ### Fixed
184
+
185
+ - The MCP endpoint no longer 403s local requests whose `Origin` port differs
186
+ from the server's (e.g. `Origin: http://localhost` against
187
+ `127.0.0.1:3000`). The `mcp` gem's `StreamableHTTPTransport` gained
188
+ default-on DNS-rebinding protection requiring a same-origin `host:port`
189
+ match; it is now disabled in favor of the engine's own Rack middleware,
190
+ which allows any `localhost` / `127.0.0.1` / `[::1]` origin regardless of
191
+ port. The `mcp` dependency floor moves to `~> 0.25` accordingly.
192
+
193
+ - `.hyperdrive/lock.yml` now round-trips top-level keys it does not recognize.
194
+ Reading it selected only the known keys and writing it rebuilt the document from
195
+ scratch, so anything hand-added to the file was erased on the next run. Entries
196
+ under `files:` remain fully generated.
197
+
198
+ - `BundlerArtifactDiscovery#version_matches?` now correctly parses the documented
199
+ comma-separated single-string form of `versions:` (e.g. `">= 7.0, < 9.0"`).
200
+ Previously, `Gem::Requirement.new` rejected that form with `BadRequirementError`
201
+ (subclass of `ArgumentError`), which was caught and silently treated as a
202
+ version mismatch — installation would skip the artifact with a misleading
203
+ "does not satisfy" warning. The YAML list form (`versions: [">= 7.0", "< 9.0"]`)
204
+ was unaffected and continues to work.
205
+
206
+ - `hyperdrive:init` / `hyperdrive:update` now merge the `rails-hyperdrive` entry
207
+ into an existing `.mcp.json` instead of rendering a template over the whole
208
+ file. Any other MCP servers you have configured, and any sibling top-level
209
+ keys, are preserved; only `mcpServers["rails-hyperdrive"]` is managed. The
210
+ write is also silent — previously a project with a pre-existing `.mcp.json`
211
+ hit Thor's interactive `Overwrite? [Ynaqdhm]` prompt, which stalls CI and
212
+ agent runs and then discarded the other servers anyway. A re-run that changes
213
+ nothing writes nothing, and a `.mcp.json` that cannot be parsed is left
214
+ byte-for-byte intact with a warning rather than overwritten.
215
+
216
+ ### Changed
217
+
218
+ - **BREAKING:** the `gem:` value reported for each installed skill by the
219
+ `describe_app` MCP tool and the `hyperdrive://stack-profile` resource is now an
220
+ **array** of every target that matched, rather than a single string. A
221
+ single-target artifact reports a one-element array. Consumers reading that
222
+ field as a string need updating.
223
+
224
+ - Documented the companion gem contract rules the installer enforces but the
225
+ README omitted or misstated: the `hyperdrive_skills_dir` gemspec metadata
226
+ override (an additional skill root, not a replacement; `..` segments ignored),
227
+ the permissive failure model and where its warnings surface, within-gem
228
+ collapsing of same-`name:` artifacts, and both accepted forms of `versions:`.
229
+ Corrected the description of `name:` — it determines the installed path rather
230
+ than merely matching the file or directory stem.
231
+
232
+ - **BREAKING:** renamed the three companion-gem gemspec metadata keys
233
+ `hyperdrive_targets`, `hyperdrive_artifacts`, and `hyperdrive_skills_dir` to
234
+ `rails_hyperdrive_targets`, `rails_hyperdrive_artifacts`, and
235
+ `rails_hyperdrive_skills_dir`. The old spellings are no longer read, with no
236
+ deprecated alias. Gemspec metadata is a single flat namespace shared by every
237
+ published gem, and the unprefixed `hyperdrive_*` prefix belongs to the
238
+ pre-existing, unrelated `hyperdrive` gem on rubygems — the contract is now
239
+ namespaced by the gem that defines it. Companion gems must update their
240
+ gemspecs to the new keys.
241
+
242
+ - **BREAKING:** `hyperdrive:discover` now finds companions by the metadata they
243
+ declare rather than by their name. It queries rubygems for gems declaring
244
+ `rails_hyperdrive_targets` instead of searching for the `rails-hyperdrive-`
245
+ name prefix, so a companion published under its author's own namespace is
246
+ suggested on the same terms as a purpose-built one. The `rails-hyperdrive-`
247
+ prefix remains a recommended naming convention and no longer plays any part in
248
+ discovery. Target matching, version gating, caching, and the never-raise
249
+ offline behaviour are unchanged, and artifact installation was never
250
+ name-filtered, so nothing about `hyperdrive:init` / `hyperdrive:update`
251
+ changes.
252
+
253
+ - **BREAKING:** renamed the `hyperdrive:init` / `hyperdrive:update` flag
254
+ `--skip-skills` to `--skip-content`. The old name is removed outright, with no
255
+ deprecated alias — it never described what the flag does. The flag skips *all*
256
+ installed content, not just skills: skills, guidelines, `index.md`,
257
+ the `CLAUDE.md` import line, and `.hyperdrive/lock.yml`, leaving only
258
+ `.mcp.json`, the discover-cache `.gitignore` rule, the optional initializer,
259
+ and the engine mount. Writing no lockfile is intentional and unchanged: the
260
+ lock is a manifest of installed content, and a later `hyperdrive:init` or
261
+ `hyperdrive:update` reconstructs the full state from scratch. Update any
262
+ scripts or CI invocations passing `--skip-skills`.
263
+
264
+ - Dropped the `< 8.1` upper cap on the `railties` and `activerecord` runtime
265
+ dependencies — both are now floor-only (`>= 7.2`), so the gem installs against
266
+ Rails 8.1 and later without waiting on a new release. The gem uses only stable
267
+ public Rails APIs and degrades gracefully per-tool, so the cap was conservative
268
+ rather than load-bearing. CI now exercises Rails 8.1 in place of 8.0.
269
+
270
+ ## [0.2.0] - 2026-05-29
271
+
272
+ ### Added
273
+
274
+ - `hyperdrive:discover` — read-only, networked command that suggests uninstalled
275
+ `rails-hyperdrive-*` companion gems for the app's stack. Queries the rubygems
276
+ search API, matches each companion's declared `hyperdrive_targets` against
277
+ `Gemfile.lock`, and prints the `bundle add` lines to run. Results cache to
278
+ `.hyperdrive/discover_cache.json` (24h TTL; `--refresh` busts it); offline or
279
+ rate-limited runs fall back to a stale cache or report "unavailable" without
280
+ failing. Ships dormant — returns nothing until companion gems exist on rubygems.
281
+ - `hyperdrive:init` now adds a `.gitignore` rule for the discover cache.
282
+
283
+ ## [0.1.0] - 2026-05-29
284
+
285
+ ### Added
286
+
287
+ - Initial release of `rails-hyperdrive`: a dev-only Rails engine that mounts an MCP
288
+ server at `/_hyperdrive/mcp` exposing introspection tools for AI coding agents.
289
+ - `hyperdrive:init` generator that installs architecture skills and auto-discovers
290
+ per-gem skills.
291
+
292
+ [Unreleased]: https://github.com/rails-hyperdrive/rails-hyperdrive/compare/v0.3.0...HEAD
293
+ [0.3.0]: https://github.com/rails-hyperdrive/rails-hyperdrive/releases/tag/v0.3.0
294
+ [0.2.0]: https://github.com/rails-hyperdrive/rails-hyperdrive/releases/tag/v0.2.0
295
+ [0.1.0]: https://github.com/rails-hyperdrive/rails-hyperdrive/releases/tag/v0.1.0
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bakaface
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,229 @@
1
+ # Rails Hyperdrive
2
+
3
+ > Dev-only Rails engine that bootstraps an MCP server + skills/guidelines for AI coding agents (Claude Code first).
4
+
5
+ Rails Hyperdrive mounts an [MCP (Model Context Protocol)](https://modelcontextprotocol.io) server at `http://localhost:3000/_hyperdrive/mcp` in development, exposing **8 introspection tools** so AI agents stop guessing — they can eval Ruby, query the DB (read-only), tail logs, list models and routes, locate source, fetch docs, and snapshot the stack.
6
+
7
+ It also ships a `hyperdrive:init` generator that discovers and installs **two artifact types** that companion gems ship under a documented contract:
8
+
9
+ - **Skills** — lazy, model-invoked via Claude Code's native description matcher. Procedural ("how to write an idempotent Sidekiq job"). Installed to `.claude/skills/<name>/SKILL.md`.
10
+ - **Guidelines** — eager, always in context via `@`-include from `CLAUDE.md`. Declarative ("this app uses Pundit, not CanCanCan"). Installed to `.claude/hyperdrive/guidelines/<name>.md`.
11
+
12
+ **rails-hyperdrive is the mechanism; companion gems are the content.** rails-hyperdrive itself ships no skills or guidelines — only the contract and the discovery/install engine. Content comes from companion gems, conventionally named `rails-hyperdrive-<library>` (e.g. `rails-hyperdrive-sidekiq`) following the [RuboCop ecosystem](https://github.com/rubocop/rubocop) precedent.
13
+
14
+ Built on the official [`mcp` gem](https://github.com/modelcontextprotocol/ruby-sdk). MIT-licensed.
15
+
16
+ ---
17
+
18
+ ## Golden path
19
+
20
+ ```bash
21
+ # 1. Add the dev gem
22
+ $ bundle add rails-hyperdrive --group=development
23
+
24
+ # 2. (Optional) Add a companion gem for your stack
25
+ $ bundle add rails-hyperdrive-sidekiq --group=development
26
+
27
+ # 3. Run the generator
28
+ $ bin/rails hyperdrive:init
29
+
30
+ create .mcp.json
31
+ insert config/routes.rb
32
+ create .claude/hyperdrive/guidelines/jobs-sidekiq.md
33
+ create .claude/skills/sidekiq-idempotency/SKILL.md
34
+ create .claude/hyperdrive/index.md
35
+ create CLAUDE.md
36
+ create .hyperdrive/lock.yml
37
+ eager 1 guideline(s), ~240 tokens always in context
38
+
39
+ done hyperdrive initialized
40
+ Mount: /_hyperdrive (in config/routes.rb)
41
+ Server: 8 MCP tools at http://localhost:3000/_hyperdrive/mcp
42
+ Installed 1 skill, 1 guideline
43
+
44
+ rails-hyperdrive-sidekiq@1.2.0
45
+ skill sidekiq-idempotency
46
+ guideline jobs-sidekiq
47
+
48
+ # 4. Start the dev server
49
+ $ bin/dev
50
+
51
+ # 5. Open Claude Code in the project directory
52
+ # → Claude Code reads .mcp.json, connects to http://localhost:3000/_hyperdrive/mcp
53
+ # → agent has 8 tools, the eager guidelines (via CLAUDE.md), and the lazy skills
54
+ ```
55
+
56
+ `hyperdrive:init` also registers the `bundler-rails-hyperdrive` Bundler plugin in your Gemfile. From then on, `bundle add rails-hyperdrive-<library>` lands the companion's artifacts on that very `bundle install` — no explicit sync. The plugin is additive only (it never touches an existing file); version bumps and orphaned artifacts are only reported, with a pointer to `bin/rails hyperdrive:sync`.
57
+
58
+ Run `bin/rails hyperdrive:sync` any time (e.g. after `bundle update` or adding a companion gem) to refresh installed content to the current bundle. It touches no bootstrap artifact and leaves locally-modified files untouched (skip + warn); pass `--overwrite` to restore them to the gem-shipped content.
59
+
60
+ Run `hyperdrive:discover` to find companion gems published for your stack that you haven't installed yet — it queries rubygems (read-only, results cached for 24h; `--refresh` re-queries) and prints the `bundle add` lines to run, then run `bin/rails hyperdrive:sync`. It never touches your Gemfile or makes network calls unless you invoke it.
61
+
62
+ ---
63
+
64
+ ## What ships
65
+
66
+ ### MCP tools (8)
67
+
68
+ | # | Tool | Purpose |
69
+ |---|------|---------|
70
+ | 1 | `run_ruby` | Eval Ruby in the booted Rails process, with timeout + output capture |
71
+ | 2 | `run_sql` | Read-only SQL via the AR connection (refuses non-SELECT) |
72
+ | 3 | `tail_logs` | Tail the last N lines of a log under `log/` (defaults to `log/<env>.log`) |
73
+ | 4 | `list_models` | List Active Record model classes with columns/validations/associations |
74
+ | 5 | `locate_source` | Resolve `Const` / `Const#method` / `Const.method` / `dep:<gem>` to a file:line |
75
+ | 6 | `lookup_doc` | Look up RDoc for a symbol (via `ri`) |
76
+ | 7 | `describe_app` | Snapshot: Rails/Ruby/DB versions + full `StackProfile` |
77
+ | 8 | `list_routes` | All routes: HTTP verb, path, controller#action, named route |
78
+
79
+ ### Resources
80
+
81
+ - `hyperdrive://stack-profile` — JSON of the resolved `StackProfile`
82
+ - `hyperdrive://skills/{name}` — markdown body of each installed skill
83
+
84
+ ### Install layout
85
+
86
+ ```
87
+ CLAUDE.md # user-owned; ONE injected line: @.claude/hyperdrive/index.md
88
+ .claude/hyperdrive/
89
+ index.md # managed aggregator: @guidelines/<name>.md
90
+ guidelines/<name>.md # companion-shipped, frontmatter stripped, audit-headered
91
+ .claude/skills/<name>/
92
+ SKILL.md # companion-shipped, frontmatter kept, audit-headered
93
+ <supporting files> # optional companion-shipped extras, installed as shipped (*.md.erb rendered)
94
+ .hyperdrive/lock.yml # git-tracked manifest (source gem, version, content hash)
95
+ ```
96
+
97
+ `CLAUDE.md` and `index.md` are the **eager chain** — they exist only because a companion gem ships a guideline, and both go when the last one leaves the bundle (the guideline file itself is left on disk and reported as an orphan). With no companion gems installed, `hyperdrive:init` writes `.mcp.json`, the `.gitignore` rule, the optional initializer, the engine mount, and `.hyperdrive/lock.yml` — and nothing into the agent's context window.
98
+
99
+ Everything a companion gem contributes lands here git-tracked, so a diff is where you review what it added — the install summary names each artifact's source gem and version, and every SKILL.md and guideline carries the same provenance in an audit header. A skill's supporting files carry no header — they install byte-identical to the install-ready body (the shipped bytes; for `*.md.erb` templates, the rendered output), and their provenance and content hash live in `.hyperdrive/lock.yml` alone. `hyperdrive:init` warns if your app gitignores these paths, since that empties the diff without changing what reaches the agent. The `hyperdrive:discover` cache is the one file rails-hyperdrive adds to `.gitignore`.
100
+
101
+ ### Turning off a single artifact
102
+
103
+ A companion gem you want for one skill but not another doesn't have to be all-or-nothing. Add the artifact's name to the `disabled:` list in `.hyperdrive/lock.yml` — it is written empty on every install, so the shape is already there:
104
+
105
+ ```yaml
106
+ disabled:
107
+ skills:
108
+ - sidekiq-idempotency
109
+ guidelines:
110
+ - jobs-sidekiq
111
+ ```
112
+
113
+ A disabled artifact is never installed, and one already on disk is removed on the next `hyperdrive:init` or `hyperdrive:sync` — but only if you haven't edited it. A locally-modified file is reported and left alone, for you to delete when you're ready. Disabling a skill removes its shipped supporting files under the same per-file rule; files you created yourself in the skill directory survive and keep the directory alive. Disabling a guideline also drops its line from `index.md`, so it leaves eager context along with the file.
114
+
115
+ The list is yours to edit; the generator only reads it and carries it forward. Delete a name to get the artifact back on the next run. When two companion gems ship the same artifact name, both install under a `<name>--<source-gem>` suffix — the plain name disables both, the suffixed name disables one.
116
+
117
+ To skip installed content wholesale instead, pass `--skip-content`.
118
+
119
+ ### Companion gem contract
120
+
121
+ A companion gem ships artifacts under:
122
+
123
+ ```
124
+ <gem-source>/lib/<gem_name>/hyperdrive/skills/<name>/SKILL.md # skill (dir-per-skill)
125
+ <gem-source>/lib/<gem_name>/hyperdrive/guidelines/<name>.md # guideline (flat file)
126
+ ```
127
+
128
+ Skills may ship under an additional root declared in gemspec metadata:
129
+
130
+ ```ruby
131
+ spec.metadata["rails_hyperdrive_skills_dir"] = "extra/skills" # optional; relative to the gem root
132
+ ```
133
+
134
+ That root is searched **in addition to** the convention path, never instead of it, so an override never hides skills already shipped at the convention path. A value containing a `..` segment is ignored. Guidelines have no override — they are found only at the convention path.
135
+
136
+ A skill is a **directory**, and it may ship more than `SKILL.md`. Everything else in the skill directory — nested however you like (`workflows/`, `references/`, `examples/`, …) — installs alongside it as **supporting files**, preserving the relative layout under `.claude/skills/<name>/`. Reference them from `SKILL.md` with directory-relative links; a cross-source name collision renames the whole installed directory, so those links keep working. Supporting files carry no frontmatter contract and no audit header — they install byte-identical to the install-ready body, which is what the gem ships (markdown, code, or binary alike) except for `*.md.erb` templates, which install as their rendered output. Each is tracked per file in `.hyperdrive/lock.yml`, so local edits are preserved on sync exactly like any other installed file. `SKILL.md` frontmatter remains the skill's sole schema surface. Guidelines stay single-file.
137
+
138
+ Every artifact carries four required YAML frontmatter fields:
139
+
140
+ ```yaml
141
+ ---
142
+ name: jobs-sidekiq # kebab-case; determines the install path
143
+ description: Background job conventions for Sidekiq.
144
+ gem: sidekiq # TARGET gem(s), resolved + version-matched in the bundle
145
+ versions: ">= 7.0, < 9.0" # Gem::Requirement matched against the target gem
146
+ ---
147
+ ```
148
+
149
+ `name:` is the artifact's identity, not a label — it is what the installer writes to disk (`.claude/skills/<name>/SKILL.md`, `.claude/hyperdrive/guidelines/<name>.md`). Keep it equal to the file or directory stem: if the two disagree the install still succeeds, but the artifact lands under `name:`. Within one gem, two artifacts of the same type declaring the same `name:` collapse to a single installed file; which one survives is not a guarantee to build on, so give each a distinct `name:`.
150
+
151
+ `versions:` accepts a single comma-separated string (`">= 7.0, < 9.0"`), a YAML list (`[">= 7.0", "< 9.0"]`), or — for multi-target artifacts — a map keyed by gem name.
152
+
153
+ `gem:` names the **targets** (each must be present in the bundle; its resolved version is matched against `versions:`). Use `railties` for "every Rails app" or the quoted `"*"` for "always applicable" (it must be quoted — bare `*` is a YAML alias and the file is skipped). `hyperdrive:init` discovers every such file across the bundle, version-matches it, and installs it with an audit header naming `source`, `sha256`, and `installed_at`. Guidelines are installed with their frontmatter stripped (they are `@`-included eagerly). When two gems ship a same-named artifact, both install, each postfixed by source gem.
154
+
155
+ One artifact can cover several interchangeable libraries — write `gem:` as a comma-separated string or a YAML list, and it installs when **any** listed target is bundled at a satisfying version. `"*"` anywhere in the list makes the artifact universal. Give `versions:` a map keyed by gem name when the targets do not share a version cycle; targets the map omits are unconstrained.
156
+
157
+ ```yaml
158
+ ---
159
+ name: jobs-conventions
160
+ description: Background job conventions.
161
+ gem: [sidekiq, solid_queue, good_job]
162
+ versions:
163
+ sidekiq: ">= 7.0"
164
+ solid_queue: ">= 1.0"
165
+ ---
166
+ ```
167
+
168
+ Draw the listed targets from the gem's own `hyperdrive_targets` (below): the gem-level declaration decides whether a companion is suggested at all, and an artifact naming a target the gemspec omits is unreachable for apps that have only that target.
169
+
170
+ #### Gem-conditional skill content
171
+
172
+ A multi-file skill can condition parts of itself on the app's bundle, so one skill tree serves apps with different gem sets. Both mechanisms are evaluated at discovery time, against the same resolved bundle that gates whole artifacts.
173
+
174
+ **Per-file gating.** A `conditional:` map in `SKILL.md` frontmatter gates individual supporting files. Keys are dir-relative shipped paths; values take the same `gem:`/`versions:` forms as the artifact-level fields (single target, comma-separated string, YAML list, per-target `versions:` map, `"*"`), and the file installs when **any** listed target is bundled at a satisfying version. Unlike the artifact level, `versions:` is optional here — omitted means unconstrained. Files the map doesn't mention install unconditionally, and the supporting files themselves stay byte-identical to upstream — the condition lives entirely out-of-band.
175
+
176
+ ```yaml
177
+ ---
178
+ name: layered-rails
179
+ description: Layered architecture conventions.
180
+ gem: railties
181
+ versions: ">= 7.2"
182
+ conditional:
183
+ references/gems/alba.md:
184
+ gem: alba
185
+ references/gems/jobs.md:
186
+ gem: [sidekiq, solid_queue]
187
+ versions:
188
+ sidekiq: ">= 7.0"
189
+ ---
190
+ ```
191
+
192
+ A malformed condition **fails open**: the file installs unconditionally and the problem is reported with the other discovery warnings — a surplus reference file is harmless, a missing one breaks links from `SKILL.md`. A key naming no shipped file, or naming `SKILL.md` itself (the artifact-level `gem:`/`versions:` gate the whole skill), is warned about and ignored. The `conditional:` key ships through to the installed frontmatter unchanged.
193
+
194
+ **ERB-templated markdown.** A file named `*.md.erb` in a skill directory — including `SKILL.md.erb` in place of `SKILL.md` — is rendered at install time and lands as plain `.md` (the `.erb` suffix is dropped; a `SKILL.md.erb` defines a skill exactly like `SKILL.md`, with frontmatter read from the rendered output). Templates see a sealed binding of exactly three helpers over the resolved bundle, nothing else:
195
+
196
+ - `gem?("name")` / `gem?("name", ">= 2.0")` — is the gem bundled (at a satisfying version)?
197
+ - `any_gem?("a", "b", …)` — is any of these bundled?
198
+ - `gem_version("name")` — the resolved version as a String, or `nil`.
199
+
200
+ Rendering uses ERB's trim mode, so `<%- if gem?("alba") -%>` … `<%- end -%>` control lines leave no blank lines behind. A template that fails to render is skipped with a warning (the whole skill, when it's `SKILL.md.erb`); when a plain file and a template would land at the same path, the plain file wins with a warning. `conditional:` keys refer to templates by their shipped `x.md.erb` name and gate them before rendering. Guidelines get no ERB support.
201
+
202
+ Use ERB sparingly — condition reference manuals via `conditional:` and wrap link-table rows that point at gated files, but keep "consider adopting gem X" recommendations unconditional. An all-wrapped `.md.erb` renders to an empty file; to omit a file entirely, gate it with `conditional:` instead.
203
+
204
+ Gated files appear and disappear as the bundle changes: `hyperdrive:init`/`hyperdrive:sync` install newly gated-in files and remove unedited gated-out ones. The auto top-up after `bundle install` adds newly gated-in files but never removes anything and never rewrites an already-installed file, so removals and re-rendered template output wait for the next `hyperdrive:sync`.
205
+
206
+ Discovery never raises. An artifact with missing or malformed frontmatter, a missing required field, no declared target in the bundle, or every bundled target resolving outside `versions:` is skipped, and the reason is collected. `hyperdrive:init` and `hyperdrive:sync` print the collected reasons at the end of the run, under a yellow `warn` line reading `discovery skipped N artifact(s):`. A companion whose artifacts all fail therefore installs nothing and reports it only there — read that section first when a gem you expected to contribute produces no files.
207
+
208
+ To be discoverable by `hyperdrive:discover` **before** it is installed, a companion also declares gemspec metadata (read remotely from rubygems, so the frontmatter inside the gem isn't visible yet):
209
+
210
+ ```ruby
211
+ spec.metadata["rails_hyperdrive_targets"] = "sidekiq" # required; comma-sep, or "*" for always-applicable
212
+ spec.metadata["rails_hyperdrive_artifacts"] = "guideline,skill" # optional; presentational hint
213
+ ```
214
+
215
+ `rails_hyperdrive_targets` is what makes a gem discoverable: `hyperdrive:discover` searches rubygems for gems declaring it, so a companion is found by what it declares rather than by what it is named. Naming it `rails-hyperdrive-<library>` is a recommended convention — it makes the gem legible in a Gemfile — but it plays no part in discovery, and a companion published under your own namespace is found on the same terms.
216
+
217
+ `rails_hyperdrive_targets` is a coarse pre-install hint — it is never reconciled against the frontmatter `gem:`; once the gem is bundled, the frontmatter alone governs what installs.
218
+
219
+ ---
220
+
221
+ ## Safety
222
+
223
+ Rails Hyperdrive is **dev-only**. The engine refuses to handle requests outside `Rails.env.development?` and enforces an origin allowlist (`localhost`, `127.0.0.1`, `[::1]`). See [SECURITY.md](SECURITY.md).
224
+
225
+ ---
226
+
227
+ ## License
228
+
229
+ MIT — see [LICENSE.txt](LICENSE.txt).
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
data/SECURITY.md ADDED
@@ -0,0 +1,43 @@
1
+ # Security Model
2
+
3
+ `rails-hyperdrive` is a **development-only** tool. Its security model is designed for a single developer running a Rails app on their own machine — nothing more. Read this before exposing your dev server to a network you don't fully control.
4
+
5
+ ## Threat model in one paragraph
6
+
7
+ Rails Hyperdrive mounts an MCP server that exposes Ruby eval, raw SQL (read-only guardrail, not a sandbox), log tailing, and source-code introspection. It is intended to be reached from `http://localhost:3000/_hyperdrive/mcp` by a local AI coding agent (Claude Code) running on the same workstation. There is no authentication. If the dev server is reachable by another process, user, or host, that party can read everything in your Rails app and run arbitrary Ruby in your dev environment.
8
+
9
+ ## Three layers of defense
10
+
11
+ All three layers key off `Rails::Hyperdrive.dev_mode?` (which is `Rails.env.development?`).
12
+
13
+ 1. **Engine load-time warning.** The engine loads in any environment so a production process doesn't crash if `rails-hyperdrive` slips into the wrong Bundler group, but it logs a warning at boot when not in development.
14
+
15
+ 2. **Rack middleware** (`Rails::Hyperdrive::Safety::RackMiddleware`) sits in front of the MCP transport on every request:
16
+ - Returns `403` when `Rails.env` is not `development`.
17
+ - Returns `403` when the `Origin` header is set and its host is not in the allowlist (`localhost`, `127.0.0.1`, `[::1]`). Requests with no `Origin` (e.g. `curl`, `Rack::Test`) pass through.
18
+
19
+ 3. **Per-tool `with_dev_guard`** in `Rails::Hyperdrive::Tools::Base` catches direct in-process invocations (tests, rake tasks) that would bypass the transport entirely.
20
+
21
+ ## What the Origin allowlist does and doesn't do
22
+
23
+ The allowlist blocks DNS-rebinding attacks: an attacker can't trick a victim's browser into POSTing JSON-RPC to `http://localhost:3000/_hyperdrive/mcp` from an attacker-controlled origin, because the browser would attach `Origin: http://evil.example` and we'd 403 it.
24
+
25
+ It does **not** authenticate anything. Any local process that can speak HTTP to your dev server can call every tool. If you run untrusted code on the same workstation, do not run `rails-hyperdrive`.
26
+
27
+ ## What `run_sql` actually is
28
+
29
+ A best-effort regex-based gate that rejects mutating SQL at the parser level. It is **not** a sandbox. A determined attacker with `run_ruby` access can trivially bypass it (`run_ruby` runs arbitrary Ruby in the Rails process). The SQL guard exists to keep a confused AI from running `DELETE FROM users` by accident, not to enforce a privilege boundary.
30
+
31
+ ## Network exposure is your responsibility
32
+
33
+ Rails Hyperdrive does not bind to a network interface — that is Puma's job. The default `bin/rails server` binding behavior varies by Rails version, operating system, and your `config/puma.rb`. To be safe:
34
+
35
+ - Bind Puma to `127.0.0.1` (the default in recent Rails versions), not `0.0.0.0`.
36
+ - Do not run `bin/rails server -b 0.0.0.0` on a network you share with untrusted parties.
37
+ - Do not forward port 3000 in Docker / `kubectl port-forward` / `ngrok` / Tailscale / VPN unless you understand who can reach it.
38
+
39
+ If you must expose the dev server, put it behind your own authenticating proxy and remove `rails-hyperdrive` from the bundle first.
40
+
41
+ ## Reporting a vulnerability
42
+
43
+ Open a GitHub issue describing the impact. Do not include credentials or proprietary code. For sensitive reports, email the gem author listed in `rails-hyperdrive.gemspec`.
data/config/routes.rb ADDED
@@ -0,0 +1,5 @@
1
+ require "rails/hyperdrive/mcp_server"
2
+
3
+ Rails::Hyperdrive::Engine.routes.draw do
4
+ mount Rails::Hyperdrive::McpServer.rack_app => "/mcp", as: :mcp
5
+ end