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 +4 -4
- data/CHANGELOG.md +102 -1
- data/README.md +60 -30
- data/lib/generators/hyperdrive/install/install_generator.rb +9 -6
- data/lib/generators/hyperdrive/sync/sync_generator.rb +1 -1
- data/lib/generators/hyperdrive/sync_runner.rb +17 -2
- data/lib/rails/hyperdrive/auto_install.rb +3 -1
- data/lib/rails/hyperdrive/bundler_artifact_discovery.rb +172 -82
- data/lib/rails/hyperdrive/canonical_skill_render.rb +105 -0
- data/lib/rails/hyperdrive/gem_manifest.rb +175 -0
- data/lib/rails/hyperdrive/install_pipeline.rb +27 -2
- data/lib/rails/hyperdrive/lock_file.rb +15 -0
- data/lib/rails/hyperdrive/skill_tasks.rb +27 -0
- data/lib/rails/hyperdrive/skill_template.rb +25 -0
- data/lib/rails/hyperdrive/stack_profile.rb +20 -1
- data/lib/rails/hyperdrive/tools/run_sql.rb +1 -1
- data/lib/rails/hyperdrive/version.rb +1 -1
- data/lib/rails/hyperdrive.rb +0 -22
- metadata +4 -2
- data/lib/generators/hyperdrive/install/templates/initializer.rb.tt +0 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f1a89d616d54ce6f324e1105894dd01741a0d42e9887bb018fceea00aafbe6f1
|
|
4
|
+
data.tar.gz: b279e9c105a9cbb75c7bffc8a46224f55ff94b6a5412a335db3ba4fe9c3f3812
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
[](https://github.com/rails-hyperdrive/rails-hyperdrive/actions/workflows/ci.yml)
|
|
11
11
|
[](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
|
|
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
|
|
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
|
-
|
|
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
|
|
86
|
-
- **Guidelines
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
125
|
-
<supporting files> # optional
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
180
|
+
Ship markdown, declare what it targets, publish. That's the whole contract:
|
|
166
181
|
|
|
167
182
|
```
|
|
168
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.2
|
|
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
|
|
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:
|
|
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
|
|
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(
|
|
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
|
-
|
|
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 = []
|