rails-hyperdrive 0.4.0 → 0.6.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b3fbb6e0845e6194c6b33334e2bd03db6629cd61a8bfd973f8be3ce7b82fd1be
4
- data.tar.gz: 8f259097744b8729ef71756df833809ff315305bf52a822ca1e40fa94298bc26
3
+ metadata.gz: f1a89d616d54ce6f324e1105894dd01741a0d42e9887bb018fceea00aafbe6f1
4
+ data.tar.gz: b279e9c105a9cbb75c7bffc8a46224f55ff94b6a5412a335db3ba4fe9c3f3812
5
5
  SHA512:
6
- metadata.gz: 3986cc09cecd60a44ecc9899be526c3db981d635a8bc458de956965b227fe51a2d3d4efc8e6da03f8016fc83320d59235d975a2ccdb22d4e86d6d75d7296e970
7
- data.tar.gz: 4524e73cd275471bc2e555ff6845b31b3e4b96f75839bd5629b59611522e85d3dd224c4cc5b197f9a38d1ecf66a6fa9961d9ea60567c9626bb90ff46cc2825de
6
+ metadata.gz: 891f3bcced1126be5b1eb48d456e369b05dcb87cda0f87c82b5d27d7eeea9e57c4ed39ca7549f5a573b13ef800efe7b56f75c39c6cfb544f15cfb1518a9b0d82
7
+ data.tar.gz: 218fe629a71711e1915af9ad10a1cc58923ee9dc41ab16514224191526d19f249ed2df6658c5d281c25b1fa9b6430cfd75c163ee4b9d669853164f5889427780
data/CHANGELOG.md CHANGED
@@ -7,6 +7,105 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.6.0] - 2026-08-17
11
+
12
+ ### Changed
13
+
14
+ - `run_sql` now labels an over-cap result `(showing first 100 of <total> rows)`
15
+ instead of `(<total> rows, truncated)`, which read as though rows beyond the
16
+ count shown had been dropped from a smaller set.
17
+ - The connection check printed under `hyperdrive:init`'s "Next steps" is now a
18
+ JSON-RPC `tools/list` POST carrying the `Content-Type` and `Accept` headers
19
+ the endpoint requires. The previous bare `curl <url>` was a GET, which the
20
+ stateless MCP transport answers with 405 — the suggested check read as a
21
+ failure against a perfectly working install.
22
+
23
+ ### Removed
24
+
25
+ - **BREAKING:** `Rails::Hyperdrive.configure` /
26
+ `Rails::Hyperdrive::Configuration`, and the `config/initializers/hyperdrive.rb`
27
+ initializer that `hyperdrive:init --mount-at` used to write. The
28
+ initializer's only setting, `config.mount_at`, was read by nothing — the
29
+ live mount is the `mount Rails::Hyperdrive::Engine` line the generator
30
+ writes into `config/routes.rb`, and `.mcp.json` records the URL — so
31
+ editing it never moved the endpoint. The `--mount-at` flag is unchanged.
32
+
33
+ **Manual migration:** delete `config/initializers/hyperdrive.rb` if an
34
+ earlier `hyperdrive:init --mount-at` wrote one. The file calls
35
+ `Rails::Hyperdrive.configure` unguarded, so with the gem in the
36
+ `:development` group it raises `NameError` on any boot that excludes that
37
+ group (e.g. a production deploy) — deleting it also removes that hazard.
38
+
39
+ ## [0.5.0] - 2026-08-15
40
+
41
+ ### Added
42
+
43
+ - Gem-root manifest: a companion gem now declares artifact gating in a
44
+ `hyperdrive.yml` at its root (or at the path named by a new
45
+ `rails_hyperdrive_manifest` gemspec metadata key; `..` segments or a blank
46
+ value fall back to the conventional path). Top-level `gem:`/`versions:` are
47
+ gem-wide defaults; `skills:` entries (keyed by skill-dir relpath from the
48
+ skills root) and `guidelines:` entries (keyed by filename) override them per
49
+ key, and per-file `conditional:` gating nests inside `skills:` entries. All
50
+ keys optional; malformed gating fails open (warn + install ungated, never
51
+ skip, never raise), and an entry keying no shipped artifact warns — the
52
+ staleness signal for gating detached from content. Shipping a manifest (file
53
+ or metadata key) is also a companion opt-in signal, so a skills.sh-format
54
+ skill repo integrates with zero modification to its skill files.
55
+
56
+ - Template/content pairing for skills, so one companion repo can serve
57
+ `npx skills` / git-clone consumers and rails-hyperdrive at once without
58
+ giving up bundle-conditioned content. A skill directory holding a static
59
+ `SKILL.md` (the universal face: generated definition plus all supporting
60
+ files) pairs with a `SKILL.md.erb` master at the same relative path under a
61
+ templates root — declared via the new `rails_hyperdrive_skill_templates_dir`
62
+ gemspec metadata key, defaulting to the convention path
63
+ `lib/<gem_name>/hyperdrive/skills`. The pair discovers as one skill:
64
+ hyperdrive renders the template against the app's bundle and installs the
65
+ supporting files from the content directory, never reading the static
66
+ `SKILL.md`. Unpaired layouts — every existing companion — behave exactly as
67
+ before.
68
+ - Canonical-render rake tasks for companion repos:
69
+ `require "rails/hyperdrive/skill_tasks"` in the Rakefile provides
70
+ `rake hyperdrive:skills:render` (generate each template's static `SKILL.md`
71
+ with the fail-open canonical binding — every gem present, `gem_version`
72
+ `nil`; the generated face is the rendered template verbatim — with gating
73
+ in the gem-root manifest there are no installer keys to strip) and
74
+ `rake hyperdrive:skills:check` (fail listing stale generated
75
+ files — the CI freshness gate). Rails-free; rails-hyperdrive as a
76
+ development dependency suffices.
77
+ - A gem's top-level `skills/` directory is now scanned as a default skills
78
+ root for opted-in companions (convention-path artifacts, a
79
+ `rails_hyperdrive_skills_dir`, `rails_hyperdrive_skill_templates_dir`, or
80
+ `rails_hyperdrive_targets` metadata key, or a lockfile `enabled:` entry are
81
+ the opt-in signals). Roots are deduplicated
82
+ by expanded path, so a companion already declaring
83
+ `rails_hyperdrive_skills_dir: "skills"` sees identical results.
84
+ - A hand-editable `enabled:` list in `.hyperdrive/lock.yml` (gem names,
85
+ mirroring `disabled:`): naming a gem there treats it as an opted-in
86
+ companion, so its top-level `skills/` content installs through the normal
87
+ pipeline. `disabled:` still wins for individual artifacts.
88
+ - `hyperdrive:init` and `hyperdrive:sync` now surface bundled gems that ship
89
+ skills.sh-style `skills/*/SKILL.md` content without opting in as companions
90
+ — report-only, with a pointer to the `enabled:` list; nothing is installed
91
+ until the user opts in.
92
+
93
+ ### Changed
94
+
95
+ - The artifact frontmatter contract is relaxed to the skills.sh base
96
+ contract: only `name` and `description` are required. `gem:` and
97
+ `versions:` are now optional narrowing keys — a missing `gem:` means
98
+ universal (`"*"`), a missing `versions:` means unconstrained. Artifacts
99
+ declaring all four fields behave exactly as before.
100
+ - **Breaking:** gating moved entirely to the gem-root manifest. The
101
+ frontmatter `gem:`/`versions:`/`conditional:` keys are no longer read —
102
+ they are ordinary unknown keys, silently ignored and installed verbatim —
103
+ and installer-key stripping is removed: skill bodies now install, and
104
+ render canonically, byte-identical to their shipped (or ERB-rendered)
105
+ content, frontmatter included. Guideline frontmatter is still stripped on
106
+ install. A companion gating via frontmatter must move those keys into
107
+ `hyperdrive.yml`.
108
+
10
109
  ## [0.4.0] - 2026-08-08
11
110
 
12
111
  ### Added
@@ -353,7 +452,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
353
452
  - `hyperdrive:init` generator that installs architecture skills and auto-discovers
354
453
  per-gem skills.
355
454
 
356
- [Unreleased]: https://github.com/rails-hyperdrive/rails-hyperdrive/compare/v0.4.0...HEAD
455
+ [Unreleased]: https://github.com/rails-hyperdrive/rails-hyperdrive/compare/v0.6.0...HEAD
456
+ [0.6.0]: https://github.com/rails-hyperdrive/rails-hyperdrive/releases/tag/v0.6.0
457
+ [0.5.0]: https://github.com/rails-hyperdrive/rails-hyperdrive/releases/tag/v0.5.0
357
458
  [0.4.0]: https://github.com/rails-hyperdrive/rails-hyperdrive/releases/tag/v0.4.0
358
459
  [0.3.0]: https://github.com/rails-hyperdrive/rails-hyperdrive/releases/tag/v0.3.0
359
460
  [0.2.0]: https://github.com/rails-hyperdrive/rails-hyperdrive/releases/tag/v0.2.0
data/README.md CHANGED
@@ -10,12 +10,16 @@
10
10
  [![CI](https://github.com/rails-hyperdrive/rails-hyperdrive/actions/workflows/ci.yml/badge.svg)](https://github.com/rails-hyperdrive/rails-hyperdrive/actions/workflows/ci.yml)
11
11
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE.txt)
12
12
 
13
- Rails Hyperdrive is a development-only Rails engine for working on Rails apps with AI coding agents. It gives the agent two things it can't get from source alone live answers from the booted app, and guidance specific to the gems and versions in the bundle:
13
+ Rails Hyperdrive is a development-only Rails engine for working on Rails apps with AI coding agents. It gives the agent two things it can't get from source alone: live answers from the booted app, and guidance specific to the gems and versions in the bundle.
14
14
 
15
15
  - **Live introspection.** The engine mounts an [MCP (Model Context Protocol)](https://modelcontextprotocol.io) server at `http://localhost:3000/_hyperdrive/mcp` with **8 tools** that answer from the running app itself: eval Ruby, query the DB (read-only), tail logs, list models and routes, jump to source, look up docs, snapshot the stack. The agent asks the router instead of grepping `routes.rb`, and reads the live schema instead of replaying migrations.
16
16
  - **Stack-specific knowledge.** `bin/rails hyperdrive:init` discovers **skills** and **guidelines** shipped by companion gems and installs only the ones matching your Gemfile: guidance targeting Sidekiq, for example, lands only if your app bundles Sidekiq, at a version the guidance covers.
17
17
 
18
- **rails-hyperdrive is the mechanism; companion gems are the content.** The gem 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.
18
+ **rails-hyperdrive is the mechanism; companion gems are the content.** The gem itself ships no skills or guidelines, only the contract and the discovery/install engine. Content reaches your app three ways:
19
+
20
+ - **Native support**: the library gem itself ships a top-level `skills/` directory in the [skills.sh](https://www.skills.sh) layout and opts in as a hyperdrive companion. The preferred route when the maintainer is on board: one gem, one source of truth.
21
+ - **Adopted skill repos**: an existing skills.sh skill repo packaged as a gem, content untouched, gating declared on the side.
22
+ - **Dedicated companion gems**: third-party guidance for a library that ships none itself. A name like `rails-hyperdrive-<library>` or `<library>-skills` keeps the gem legible in a Gemfile, but naming plays no part in discovery.
19
23
 
20
24
  ---
21
25
 
@@ -57,7 +61,7 @@ $ bin/dev
57
61
  # → agent has 8 tools, the eager guidelines (via CLAUDE.md), and the lazy skills
58
62
  ```
59
63
 
60
- That's it. No API keys, no config files to write, no per-project setup beyond the generator.
64
+ The generated `.mcp.json` points at `http://localhost:3000<mount>/mcp`. If your dev server runs on another port, edit the URL there.
61
65
 
62
66
  ---
63
67
 
@@ -76,38 +80,38 @@ That's it. No API keys, no config files to write, no per-project setup beyond th
76
80
  | 7 | `describe_app` | Snapshot: Rails/Ruby/DB versions + direct gem dependencies |
77
81
  | 8 | `list_routes` | All routes: HTTP verb, path, controller#action, named route |
78
82
 
79
- Plus two MCP resources: `hyperdrive://stack-profile` (JSON snapshot of your resolved stack) and `hyperdrive://skills/{name}` (the markdown body of each installed skill).
83
+ Plus two MCP resources: `hyperdrive://stack-profile` (JSON snapshot of your resolved stack) and `hyperdrive://skills/{name}` (the markdown body of each installed skill). The skill list is enumerated at server boot and the stack snapshot is memoized per process, so a newly installed skill or a changed bundle reaches these two only after a dev-server restart.
80
84
 
81
85
  ### Two kinds of knowledge
82
86
 
83
87
  Companion gems ship two artifact types, tuned for how agents consume context:
84
88
 
85
- - **Skills** *lazy*. Loaded on demand via Claude Code's native description matcher. Procedural knowledge: "how to write an idempotent Sidekiq job". Installed to `.claude/skills/<name>/SKILL.md`, optionally with supporting files (references, examples, workflows) alongside.
86
- - **Guidelines** *eager*. Always in context via a single `@`-include from `CLAUDE.md`. Declarative facts: "this app uses ActionPolicy, not Pundit". Installed to `.claude/hyperdrive/guidelines/<name>.md`.
89
+ - **Skills**: *lazy*. Loaded on demand via Claude Code's native description matcher. Procedural knowledge: "how to write an idempotent Sidekiq job". Installed to `.claude/skills/<name>/SKILL.md`, optionally with supporting files (references, examples, workflows) alongside.
90
+ - **Guidelines**: *eager*. Always in context via a single `@`-include from `CLAUDE.md`. Declarative facts: "this app uses ActionPolicy, not Pundit". Installed to `.claude/hyperdrive/guidelines/<name>.md`.
87
91
 
88
- Every artifact declares which gem it targets and at which versions, so what installs is exactly what matches your `Gemfile.lock` nothing generic, nothing stale.
92
+ A companion gem declares in its manifest which gem each artifact targets and at which versions, so what lands in your app is what matches your `Gemfile.lock`, and nothing aimed at a library or version you don't run.
89
93
 
90
- With no companion gems, `hyperdrive:init` sets up just the server plumbing (`.mcp.json`, the engine mount, the lockfile) and puts **nothing** into your agent's context window. Zero context cost until you opt in.
94
+ With no companion gems, `hyperdrive:init` sets up just the server plumbing (`.mcp.json`, the engine mount, the lockfile) and puts **nothing** into your agent's context window.
91
95
 
92
96
  ---
93
97
 
94
98
  ## Staying in sync
95
99
 
96
- **After `bundle install` automatic.** `hyperdrive:init` registers the [`bundler-rails-hyperdrive`](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 extra command. The plugin is additive only (it never touches an existing file); version bumps and orphaned artifacts are only reported, with a pointer to `hyperdrive:sync`.
100
+ **After `bundle install`: automatic.** `hyperdrive:init` registers the [`bundler-rails-hyperdrive`](bundler-rails-hyperdrive/) Bundler plugin in your Gemfile. From then on, adding a companion gem lands its artifacts on that very `bundle install`, with no extra command to run. The plugin is additive only (it never touches an existing file); version bumps and orphaned artifacts are only reported, with a pointer to `hyperdrive:sync`.
97
101
 
98
- **`bin/rails hyperdrive:sync` on demand.** Run it any time (e.g. after `bundle update`) to refresh installed content to the current bundle. It touches no bootstrap artifact and leaves locally-modified files untouched (skip + warn). When you *have* edited an installed file and its gem ships a new version, three mutually-exclusive flags reconcile the two:
102
+ **`bin/rails hyperdrive:sync`: on demand.** Run it any time (e.g. after `bundle update`) to refresh installed content to the current bundle. It touches no bootstrap artifact and leaves locally modified files untouched (skip + warn). When you *have* edited an installed file and its gem ships a new version, three mutually exclusive flags reconcile the two:
99
103
 
100
104
  | Strategy | What happens to the live file | What happens to your edits |
101
105
  |---|---|---|
102
- | `--merge` | Rewritten with a git three-way merge when it applies cleanly; otherwise untouched and the upstream lands as a `--sidecar` delivery | Kept a merge that would need conflict markers falls back to the sidecar instead, so nothing half-merged ever goes live |
106
+ | `--merge` | Rewritten with a git three-way merge when it applies cleanly; otherwise untouched and the upstream lands as a `--sidecar` delivery | Kept: a merge that would need conflict markers falls back to the sidecar, so nothing half-merged ever goes live |
103
107
  | `--sidecar` | Untouched; the new upstream body is written next to it as `<file>.new` | Kept, byte-for-byte |
104
108
  | `--overwrite` | Restored to the gem-shipped content | Discarded |
105
109
 
106
- A sidecar is inert Claude Code loads only `SKILL.md` and the `index.md` `@`-lines, never a `.new` file and it shows up in `git status` as your prompt to resolve. Resolve it by folding what you want into the live file and deleting the `.new`, or `mv <file>.new <file>` to accept the upstream wholesale. Either way the lockfile already records that delivery, so the next sync doesn't re-offer the same version (and a leftover sidecar you haven't touched is cleaned up once the live file catches up). `--merge` needs the previously installed gem version still present on disk to reconstruct the merge ancestor; when it isn't (CI, after `gem cleanup`), it degrades to the sidecar with a note saying why.
110
+ A sidecar is inert (Claude Code loads only `SKILL.md` and the `index.md` `@`-lines, never a `.new` file), and it shows up in `git status` as your prompt to resolve. Resolve it by folding what you want into the live file and deleting the `.new`, or `mv <file>.new <file>` to accept the upstream wholesale. Either way the lockfile already records that delivery, so the next sync doesn't re-offer the same version (and a leftover sidecar you haven't touched is cleaned up once the live file catches up). `--merge` needs the previously installed gem version still present on disk to reconstruct the merge ancestor; when it isn't (CI, after `gem cleanup`), it degrades to the sidecar with a note saying why.
107
111
 
108
112
  The sidecar pair is also how an AI coding agent reconciles for you, with no extra machinery: run `bin/rails hyperdrive:sync --sidecar`, have the agent merge the live/`.new` pair semantically (it has both full texts), then delete the sidecar.
109
113
 
110
- **`bin/rails hyperdrive:discover` find what you're missing.** Queries rubygems for companion gems published for your stack that you haven't installed yet, and prints the `bundle add` lines to run. Read-only, results cached for 24h (`--refresh` re-queries), and it never touches your Gemfile or makes network calls unless you invoke it.
114
+ **`bin/rails hyperdrive:discover`: find what you're missing.** Queries rubygems for companion gems published for your stack that you haven't installed yet, and prints the `bundle add` lines to run. It is read-only, caches results for 24h (`--refresh` re-queries), and never touches your Gemfile or makes network calls unless you invoke it.
111
115
 
112
116
  ---
113
117
 
@@ -121,22 +125,22 @@ CLAUDE.md # user-owned; ONE injected line: @.claude
121
125
  index.md # managed aggregator: @guidelines/<name>.md
122
126
  guidelines/<name>.md # companion-shipped, frontmatter stripped
123
127
  .claude/skills/<name>/
124
- SKILL.md # companion-shipped, frontmatter kept minus installer keys
125
- <supporting files> # optional companion-shipped extras, installed as shipped (*.md.erb rendered)
128
+ SKILL.md # companion-shipped, installed verbatim (frontmatter included)
129
+ <supporting files> # optional extras (references/, examples/, …), installed as shipped (*.md.erb rendered)
126
130
  .hyperdrive/lock.yml # git-tracked manifest (source gem, version, content hash)
127
131
  ```
128
132
 
129
- A `git diff` is where you review what a companion gem added. The install summary names each artifact's source gem and version, and every installed file is hashed and attributed to its source in the git-tracked `.hyperdrive/lock.yml` the files themselves land byte-identical to what the gem ships, with nothing injected. `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`.
133
+ A `git diff` is where you review what a companion gem added. The install summary names each artifact's source gem and version, and every installed file is hashed and attributed to its source in the git-tracked `.hyperdrive/lock.yml`. The files themselves land byte-identical to what the gem ships, with nothing injected. `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`.
130
134
 
131
- `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).
135
+ `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).
132
136
 
133
137
  ### Your edits win
134
138
 
135
- Installed files are yours to modify. The lockfile hash tells the installer whether a file is still gem-pristine: unedited files are refreshed on upgrade, edited files are skipped with a warning never silently overwritten. `hyperdrive:sync --overwrite` is the explicit way back to gem-shipped content.
139
+ Installed files are yours to modify. The lockfile hash tells the installer whether a file is still gem-pristine: unedited files are refreshed on upgrade, edited files are skipped with a warning, never silently overwritten. `hyperdrive:sync --overwrite` is the explicit way back to gem-shipped content.
136
140
 
137
141
  ### Turning off a single artifact
138
142
 
139
- 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:
143
+ 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:
140
144
 
141
145
  ```yaml
142
146
  disabled:
@@ -146,52 +150,78 @@ disabled:
146
150
  - jobs-sidekiq
147
151
  ```
148
152
 
149
- 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.
153
+ 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.
150
154
 
151
- 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.
155
+ 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.
152
156
 
153
157
  To skip installed content wholesale instead, pass `--skip-content` to `hyperdrive:init`.
154
158
 
159
+ ### Opting into a gem's bundled skills
160
+
161
+ Ordinary gems (not built as hyperdrive companions) sometimes ship a top-level `skills/` directory of skills.sh-style skills. Those are never installed automatically: `hyperdrive:init` and `hyperdrive:sync` only report them, e.g. `gem 'foo' ships 2 skills.sh skill(s)`. To install them, name the gem in the `enabled:` list in `.hyperdrive/lock.yml` and re-run `hyperdrive:sync`:
162
+
163
+ ```yaml
164
+ enabled:
165
+ - foo
166
+ ```
167
+
168
+ An enabled gem is treated as a companion from then on: its skills install through the normal pipeline (including on `bundle install`), and `disabled:` still wins for any individual artifact. The list is hand-edited like `disabled:` and survives every rewrite of the lockfile.
169
+
155
170
  ---
156
171
 
157
172
  ## Safety
158
173
 
159
- Rails Hyperdrive is **dev-only**, enforced in depth: the engine refuses to handle requests outside `Rails.env.development?`, enforces an origin allowlist (`localhost`, `127.0.0.1`, `[::1]`), and every tool re-checks the dev guard on invocation. `run_sql` accepts read-only statements and refuses anything else. See [SECURITY.md](SECURITY.md).
174
+ Rails Hyperdrive is **dev-only**, enforced in depth: the engine refuses requests outside `Rails.env.development?`, applies an origin allowlist (`localhost`, `127.0.0.1`, `[::1]`), and every tool re-checks the dev guard on invocation. `run_sql` accepts read-only statements and refuses anything else. See [SECURITY.md](SECURITY.md).
160
175
 
161
176
  ---
162
177
 
163
178
  ## Build a companion gem
164
179
 
165
- Ship markdown at a convention path, declare what it targets, publish. That's the whole contract:
180
+ Ship markdown, declare what it targets, publish. That's the whole contract:
166
181
 
167
182
  ```
168
- lib/<gem_name>/hyperdrive/skills/<name>/SKILL.md # skill (dir-per-skill, may ship supporting files)
183
+ skills/<name>/SKILL.md # skill (dir-per-skill, may ship supporting files)
169
184
  lib/<gem_name>/hyperdrive/guidelines/<name>.md # guideline (flat file)
170
185
  ```
171
186
 
187
+ Top-level `skills/` is the recommended home for skill content: it is the tool-agnostic face of your gem, readable by skills.sh and plain git-clone consumers as well as hyperdrive, and it is scanned by default. `lib/<gem_name>/hyperdrive/` is the hyperdrive-specific root: guidelines, and ERB skill templates (`SKILL.md.erb`, which must stay out of `skills/` so raw ERB never reaches generic consumers). Plain skills shipped under it remain scanned as well.
188
+
189
+ Frontmatter is pure skills.sh: only `name` and `description` are read, so a skill repo's content integrates without modification:
190
+
172
191
  ```yaml
173
192
  ---
174
193
  name: jobs-sidekiq # kebab-case; determines the install path
175
194
  description: Background job conventions for Sidekiq.
176
- gem: sidekiq # TARGET gem(s), resolved + version-matched in the bundle
177
- versions: ">= 7.0, < 9.0" # Gem::Requirement matched against the target gem
178
195
  ---
179
196
  ```
180
197
 
181
- And to be suggested by `hyperdrive:discover` before anyone installs you:
198
+ Gating (which bundles an artifact installs into) lives in a `hyperdrive.yml` manifest at the gem root (or at the path named by a `rails_hyperdrive_manifest` gemspec metadata key), never in the content. Every key is optional; no manifest (or an empty one) means everything installs universally:
199
+
200
+ ```yaml
201
+ gem: sidekiq # gem-wide default: TARGET gem(s), resolved + version-matched in the bundle
202
+ versions: ">= 7.0, < 9.0" # gem-wide default: Gem::Requirement matched against the target gem
203
+ skills: # per-skill overrides, keyed by skill dir relative to its skills root
204
+ jobs-sidekiq:
205
+ versions: ">= 8.0"
206
+ guidelines: # per-guideline overrides, keyed by filename
207
+ jobs.md:
208
+ gem: sidekiq
209
+ ```
210
+
211
+ Shipping a `hyperdrive.yml` (or declaring `rails_hyperdrive_manifest`) opts your gem in as a companion. Also declare your targets in gemspec metadata. That opts your gem in too, and it is the pre-install targeting signal (how `hyperdrive:discover` suggests you before anyone installs you):
182
212
 
183
213
  ```ruby
184
214
  spec.metadata["rails_hyperdrive_targets"] = "sidekiq"
185
215
  ```
186
216
 
187
- The full contract multi-target artifacts, multi-file skills, per-file gem gating, ERB-templated content, collision and dedup rules lives in [docs/COMPANION_GEMS.md](docs/COMPANION_GEMS.md).
217
+ [docs/COMPANION_GEMS.md](docs/COMPANION_GEMS.md) has the full contract: multi-target artifacts, multi-file skills, per-file gem gating, ERB-templated content, the template/content paired layout that also serves `npx skills` and git-clone consumers, and the collision and dedup rules.
188
218
 
189
219
  ---
190
220
 
191
221
  ## Requirements
192
222
 
193
- Ruby ≥ 3.2, Rails ≥ 7.2. Tested against Rails 7.2 and 8.1 on Ruby 3.23.4.
223
+ Ruby ≥ 3.2, Rails ≥ 7.2. Tested against Rails 7.2 and 8.1 on Ruby 3.2-3.4.
194
224
 
195
225
  ## License
196
226
 
197
- MIT see [LICENSE.txt](LICENSE.txt).
227
+ MIT. See [LICENSE.txt](LICENSE.txt).
@@ -22,6 +22,8 @@ module Rails
22
22
  GEMFILE = "Gemfile".freeze
23
23
  BUNDLER_PLUGIN = "bundler-rails-hyperdrive".freeze
24
24
 
25
+ # No templates are rendered; source_root exists so Rails resolves the
26
+ # sibling USAGE file for `--help`.
25
27
  source_root File.expand_path("templates", __dir__)
26
28
 
27
29
  class_option :mount_at, type: :string, default: DEFAULT_MOUNT_AT, desc: "Engine mount path."
@@ -77,11 +79,6 @@ module Rails
77
79
  append_to_file GEMFILE, "#{prefix}plugin #{BUNDLER_PLUGIN.inspect}\n"
78
80
  end
79
81
 
80
- def write_initializer
81
- return if mount_path == DEFAULT_MOUNT_AT
82
- template "initializer.rb.tt", "config/initializers/hyperdrive.rb"
83
- end
84
-
85
82
  def mount_engine
86
83
  routes_file = "config/routes.rb"
87
84
  unless File.exist?(::Rails.root.join(routes_file))
@@ -117,10 +114,16 @@ module Rails
117
114
  say " Next steps:"
118
115
  say " 1. bin/rails server"
119
116
  say " 2. Open Claude Code in this directory; it will read .mcp.json"
120
- say " 3. Verify the connection: curl http://localhost:3000#{mount_path}/mcp"
117
+ say " 3. Verify the connection: #{connection_check_command}"
121
118
  end
122
119
 
123
120
  no_tasks do
121
+ def connection_check_command
122
+ "curl -s http://localhost:3000#{mount_path}/mcp " \
123
+ "-H 'Content-Type: application/json' -H 'Accept: application/json' " \
124
+ "-d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\"}'"
125
+ end
126
+
124
127
  def mcp_json_on_disk
125
128
  abs = ::Rails.root.join(MCP_JSON_PATH)
126
129
  File.exist?(abs) ? File.read(abs) : nil
@@ -6,7 +6,7 @@ module Rails
6
6
  module Generators
7
7
  module Hyperdrive
8
8
  # Content-only by contract: no step may write a bootstrap artifact
9
- # (.mcp.json, the engine mount, the initializer, the .gitignore rule).
9
+ # (.mcp.json, the engine mount, the .gitignore rule).
10
10
  class SyncGenerator < ::Rails::Generators::Base
11
11
  include ContentSyncSupport
12
12
 
@@ -1,7 +1,9 @@
1
1
  require "thor"
2
2
  require "rails/hyperdrive"
3
3
  require "rails/hyperdrive/bundler_artifact_discovery"
4
+ require "rails/hyperdrive/install_layout"
4
5
  require "rails/hyperdrive/install_pipeline"
6
+ require "rails/hyperdrive/lock_file"
5
7
  require "generators/hyperdrive/install_summary"
6
8
 
7
9
  module Rails
@@ -29,7 +31,9 @@ module Rails
29
31
  end
30
32
 
31
33
  def discover_artifacts(skip: false)
32
- @artifacts ||= skip ? [] : ::Rails::Hyperdrive::BundlerArtifactDiscovery.discover(warnings: warnings)
34
+ @artifacts ||= skip ? [] : ::Rails::Hyperdrive::BundlerArtifactDiscovery.discover(
35
+ warnings: warnings, enabled_gems: enabled_gems, notices: notices
36
+ )
33
37
  end
34
38
 
35
39
  def install(mode:)
@@ -38,7 +42,8 @@ module Rails
38
42
  shell: @shell,
39
43
  artifacts: discover_artifacts,
40
44
  mode: mode,
41
- warnings: warnings
45
+ warnings: warnings,
46
+ notices: notices
42
47
  )
43
48
  @pipeline.call
44
49
  end
@@ -59,6 +64,16 @@ module Rails
59
64
  @warnings ||= []
60
65
  end
61
66
 
67
+ def notices
68
+ @notices ||= []
69
+ end
70
+
71
+ def enabled_gems
72
+ @enabled_gems ||= ::Rails::Hyperdrive::LockFile.load(
73
+ File.join(root, ::Rails::Hyperdrive::InstallLayout::LOCK_PATH)
74
+ ).enabled_gems
75
+ end
76
+
62
77
  def lock_entries
63
78
  entries = []
64
79
  @pipeline&.lock&.each_entry { |e| entries << e }
@@ -4,6 +4,7 @@ require "rails/hyperdrive/bundler_artifact_discovery"
4
4
  require "rails/hyperdrive/install_layout"
5
5
  require "rails/hyperdrive/install_pipeline"
6
6
  require "rails/hyperdrive/install_shell"
7
+ require "rails/hyperdrive/lock_file"
7
8
 
8
9
  module Rails
9
10
  module Hyperdrive
@@ -46,7 +47,8 @@ module Rails
46
47
  return skip(:not_development) unless development?(env)
47
48
  return skip(:not_initialized) unless File.exist?(File.join(root, InstallLayout::LOCK_PATH))
48
49
 
49
- artifacts = BundlerArtifactDiscovery.discover
50
+ enabled = LockFile.load(File.join(root, InstallLayout::LOCK_PATH)).enabled_gems
51
+ artifacts = BundlerArtifactDiscovery.discover(enabled_gems: enabled)
50
52
  status = ArtifactStatus.compare(root: root, artifacts: artifacts)
51
53
 
52
54
  installed = []