rails-hyperdrive 0.3.0 → 0.5.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 +137 -1
- data/README.md +96 -102
- data/lib/generators/hyperdrive/install/USAGE +3 -2
- data/lib/generators/hyperdrive/sync/USAGE +17 -3
- data/lib/generators/hyperdrive/sync/sync_generator.rb +16 -1
- data/lib/generators/hyperdrive/sync_runner.rb +17 -2
- data/lib/rails/hyperdrive/ancestor_locator.rb +76 -0
- data/lib/rails/hyperdrive/artifact_status.rb +1 -1
- data/lib/rails/hyperdrive/auto_install.rb +3 -1
- data/lib/rails/hyperdrive/bundler_artifact_discovery.rb +172 -55
- data/lib/rails/hyperdrive/canonical_skill_render.rb +105 -0
- data/lib/rails/hyperdrive/drift_verdict.rb +9 -14
- data/lib/rails/hyperdrive/gem_manifest.rb +175 -0
- data/lib/rails/hyperdrive/install_layout.rb +4 -0
- data/lib/rails/hyperdrive/install_pipeline.rb +212 -31
- data/lib/rails/hyperdrive/lock_file.rb +15 -0
- data/lib/rails/hyperdrive/resources/stack_profile.rb +1 -1
- 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 +31 -40
- data/lib/rails/hyperdrive/three_way_merge.rb +47 -0
- data/lib/rails/hyperdrive/tools/describe_app.rb +1 -1
- data/lib/rails/hyperdrive/version.rb +1 -1
- data/lib/tasks/hyperdrive.rake +1 -1
- metadata +6 -3
- data/lib/rails/hyperdrive/audit_header.rb +0 -83
- data/lib/rails/hyperdrive/data/gem_categories.yml +0 -52
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 11be45047a967ade290022f1aa91cd023bf6085e0c84b4f6c08807d9b29f446b
|
|
4
|
+
data.tar.gz: 68205915ef162cc1006685361cb14e1690dcd82b666fbab12a54fdf7d5f26e2a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 59852c94a50743b1666ab4c8d973b3a34d5178158e57c7b7e20c98e203c53dca22ac6ef505880f0b5b013b5972f9c2aa66b53ec2b1646eab2b6fef487bd4447a
|
|
7
|
+
data.tar.gz: 37165dc674f138b5f29bd408917f0d4d2e5847079f2f346fcc99d4341d2984d01eea3a228bfb434a0351d8c32115762bfbc5937f296a338e511726be4552c3a4
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,140 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.5.0] - 2026-08-15
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Gem-root manifest: a companion gem now declares artifact gating in a
|
|
15
|
+
`hyperdrive.yml` at its root (or at the path named by a new
|
|
16
|
+
`rails_hyperdrive_manifest` gemspec metadata key; `..` segments or a blank
|
|
17
|
+
value fall back to the conventional path). Top-level `gem:`/`versions:` are
|
|
18
|
+
gem-wide defaults; `skills:` entries (keyed by skill-dir relpath from the
|
|
19
|
+
skills root) and `guidelines:` entries (keyed by filename) override them per
|
|
20
|
+
key, and per-file `conditional:` gating nests inside `skills:` entries. All
|
|
21
|
+
keys optional; malformed gating fails open (warn + install ungated, never
|
|
22
|
+
skip, never raise), and an entry keying no shipped artifact warns — the
|
|
23
|
+
staleness signal for gating detached from content. Shipping a manifest (file
|
|
24
|
+
or metadata key) is also a companion opt-in signal, so a skills.sh-format
|
|
25
|
+
skill repo integrates with zero modification to its skill files.
|
|
26
|
+
|
|
27
|
+
- Template/content pairing for skills, so one companion repo can serve
|
|
28
|
+
`npx skills` / git-clone consumers and rails-hyperdrive at once without
|
|
29
|
+
giving up bundle-conditioned content. A skill directory holding a static
|
|
30
|
+
`SKILL.md` (the universal face: generated definition plus all supporting
|
|
31
|
+
files) pairs with a `SKILL.md.erb` master at the same relative path under a
|
|
32
|
+
templates root — declared via the new `rails_hyperdrive_skill_templates_dir`
|
|
33
|
+
gemspec metadata key, defaulting to the convention path
|
|
34
|
+
`lib/<gem_name>/hyperdrive/skills`. The pair discovers as one skill:
|
|
35
|
+
hyperdrive renders the template against the app's bundle and installs the
|
|
36
|
+
supporting files from the content directory, never reading the static
|
|
37
|
+
`SKILL.md`. Unpaired layouts — every existing companion — behave exactly as
|
|
38
|
+
before.
|
|
39
|
+
- Canonical-render rake tasks for companion repos:
|
|
40
|
+
`require "rails/hyperdrive/skill_tasks"` in the Rakefile provides
|
|
41
|
+
`rake hyperdrive:skills:render` (generate each template's static `SKILL.md`
|
|
42
|
+
with the fail-open canonical binding — every gem present, `gem_version`
|
|
43
|
+
`nil`; the generated face carries only the skills.sh frontmatter, with the
|
|
44
|
+
installer-only `gem:`/`versions:`/`conditional:` keys stripped, as they are
|
|
45
|
+
on install) and `rake hyperdrive:skills:check` (fail listing stale generated
|
|
46
|
+
files — the CI freshness gate). Rails-free; rails-hyperdrive as a
|
|
47
|
+
development dependency suffices.
|
|
48
|
+
- A gem's top-level `skills/` directory is now scanned as a default skills
|
|
49
|
+
root for opted-in companions (convention-path artifacts, a
|
|
50
|
+
`rails_hyperdrive_skills_dir`, `rails_hyperdrive_skill_templates_dir`, or
|
|
51
|
+
`rails_hyperdrive_targets` metadata key, or a lockfile `enabled:` entry are
|
|
52
|
+
the opt-in signals). Roots are deduplicated
|
|
53
|
+
by expanded path, so a companion already declaring
|
|
54
|
+
`rails_hyperdrive_skills_dir: "skills"` sees identical results.
|
|
55
|
+
- A hand-editable `enabled:` list in `.hyperdrive/lock.yml` (gem names,
|
|
56
|
+
mirroring `disabled:`): naming a gem there treats it as an opted-in
|
|
57
|
+
companion, so its top-level `skills/` content installs through the normal
|
|
58
|
+
pipeline. `disabled:` still wins for individual artifacts.
|
|
59
|
+
- `hyperdrive:init` and `hyperdrive:sync` now surface bundled gems that ship
|
|
60
|
+
skills.sh-style `skills/*/SKILL.md` content without opting in as companions
|
|
61
|
+
— report-only, with a pointer to the `enabled:` list; nothing is installed
|
|
62
|
+
until the user opts in.
|
|
63
|
+
|
|
64
|
+
### Changed
|
|
65
|
+
|
|
66
|
+
- The artifact frontmatter contract is relaxed to the skills.sh base
|
|
67
|
+
contract: only `name` and `description` are required. `gem:` and
|
|
68
|
+
`versions:` are now optional narrowing keys — a missing `gem:` means
|
|
69
|
+
universal (`"*"`), a missing `versions:` means unconstrained. Artifacts
|
|
70
|
+
declaring all four fields behave exactly as before.
|
|
71
|
+
- **Breaking:** gating moved entirely to the gem-root manifest. The
|
|
72
|
+
frontmatter `gem:`/`versions:`/`conditional:` keys are no longer read —
|
|
73
|
+
they are ordinary unknown keys, silently ignored and installed verbatim —
|
|
74
|
+
and installer-key stripping is removed: skill bodies now install, and
|
|
75
|
+
render canonically, byte-identical to their shipped (or ERB-rendered)
|
|
76
|
+
content, frontmatter included. Guideline frontmatter is still stripped on
|
|
77
|
+
install. A companion gating via frontmatter must move those keys into
|
|
78
|
+
`hyperdrive.yml`.
|
|
79
|
+
|
|
80
|
+
## [0.4.0] - 2026-08-08
|
|
81
|
+
|
|
82
|
+
### Added
|
|
83
|
+
|
|
84
|
+
- `hyperdrive:sync --sidecar`: when an installed file is locally modified and
|
|
85
|
+
its gem ships something new, the new upstream body is delivered next to it
|
|
86
|
+
as `<file>.new` instead of being skipped. The live
|
|
87
|
+
file is never touched; `mv <file>.new <file>` accepts the upstream
|
|
88
|
+
wholesale. The lockfile records the delivered upstream, so the same version
|
|
89
|
+
is offered exactly once, and a delivered-but-unresolved file no longer nags
|
|
90
|
+
from `bundle install`.
|
|
91
|
+
- `hyperdrive:sync --merge`: same as `--sidecar`, but first attempts a git
|
|
92
|
+
three-way merge of the local edits with the upstream change, using the
|
|
93
|
+
previously installed gem version (found in the installed gem directories,
|
|
94
|
+
content-verified against the lock) as the ancestor. Only a clean merge is
|
|
95
|
+
written to the live file — a conflict, a missing ancestor, a missing `git`,
|
|
96
|
+
binary content, or an earlier delivery still unresolved at `<file>.new` all
|
|
97
|
+
fall back to the sidecar delivery, so conflict markers never reach a live
|
|
98
|
+
file and a pending delivery is never merged over.
|
|
99
|
+
- Leftover sidecars are swept: whenever a sync writes or verifies the live
|
|
100
|
+
file, a `<file>.new` still matching a delivered upstream is removed, and
|
|
101
|
+
one you edited is warned about and left alone.
|
|
102
|
+
|
|
103
|
+
### Changed
|
|
104
|
+
|
|
105
|
+
- **BREAKING:** the stack profile (`describe_app` MCP tool and the
|
|
106
|
+
`hyperdrive://stack-profile` resource) no longer categorizes gems into
|
|
107
|
+
`test` / `jobs` / `frontend` / `auth` / `authz` / `db_gems` buckets. Those
|
|
108
|
+
keys are replaced by `direct_dependencies` — the app's declared gems (the
|
|
109
|
+
lockfile's `DEPENDENCIES` section) with their resolved versions. The old
|
|
110
|
+
buckets matched a hand-curated gem list against the *resolved* dependency
|
|
111
|
+
set, so every Rails app reported transitive `minitest` as a chosen test
|
|
112
|
+
framework
|
|
113
|
+
([#2](https://github.com/rails-hyperdrive/rails-hyperdrive/issues/2));
|
|
114
|
+
raw direct dependencies carry no such editorializing and
|
|
115
|
+
need no curated list to go stale. `rails`, `ruby`, `database`, and
|
|
116
|
+
`gem_skills` are unchanged.
|
|
117
|
+
|
|
118
|
+
- Installed skills no longer carry the installer-only frontmatter keys
|
|
119
|
+
(`gem:`, `versions:`, `conditional:`). They are discovery-time inputs with
|
|
120
|
+
no post-install reader, and the `conditional:` map referenced shipped paths
|
|
121
|
+
that gating and ERB rendering could leave pointing at files absent from
|
|
122
|
+
disk — dead weight in the agent's context window at every skill invocation.
|
|
123
|
+
The installed frontmatter now holds only what the runtime reads (`name:`,
|
|
124
|
+
`description:`, any extra keys like `allowed-tools:`). Because the
|
|
125
|
+
install-ready body changes, the next
|
|
126
|
+
`hyperdrive:sync` rewrites each unedited installed skill once; locally
|
|
127
|
+
edited skills are skipped with the usual warning.
|
|
128
|
+
- The skip warning for a locally-modified file now names all three
|
|
129
|
+
reconciliation flags (`--merge`, `--sidecar`, `--overwrite`).
|
|
130
|
+
- Orphan reports now say "no longer shipped by \<source\>" instead of
|
|
131
|
+
"source \<source\> no longer in bundle" — an artifact is also orphaned when
|
|
132
|
+
its gem is still bundled but stopped shipping it.
|
|
133
|
+
|
|
134
|
+
### Removed
|
|
135
|
+
|
|
136
|
+
- Audit headers. Installed skills and guidelines no longer carry the
|
|
137
|
+
`# hyperdrive: source=...` / `<!-- hyperdrive: ... -->` comment block —
|
|
138
|
+
every installed file now lands byte-identical to its install-ready body
|
|
139
|
+
(sidecar `.new` deliveries and merge results included). The header
|
|
140
|
+
duplicated what the git-tracked `.hyperdrive/lock.yml` already records per
|
|
141
|
+
file (`source`, `source_sha`, `installed_at`) and was loaded into the
|
|
142
|
+
agent's context on every skill invocation (and eagerly, for guidelines).
|
|
143
|
+
|
|
10
144
|
## [0.3.0] - 2026-08-04
|
|
11
145
|
|
|
12
146
|
### Added
|
|
@@ -289,7 +423,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
289
423
|
- `hyperdrive:init` generator that installs architecture skills and auto-discovers
|
|
290
424
|
per-gem skills.
|
|
291
425
|
|
|
292
|
-
[Unreleased]: https://github.com/rails-hyperdrive/rails-hyperdrive/compare/v0.
|
|
426
|
+
[Unreleased]: https://github.com/rails-hyperdrive/rails-hyperdrive/compare/v0.5.0...HEAD
|
|
427
|
+
[0.5.0]: https://github.com/rails-hyperdrive/rails-hyperdrive/releases/tag/v0.5.0
|
|
428
|
+
[0.4.0]: https://github.com/rails-hyperdrive/rails-hyperdrive/releases/tag/v0.4.0
|
|
293
429
|
[0.3.0]: https://github.com/rails-hyperdrive/rails-hyperdrive/releases/tag/v0.3.0
|
|
294
430
|
[0.2.0]: https://github.com/rails-hyperdrive/rails-hyperdrive/releases/tag/v0.2.0
|
|
295
431
|
[0.1.0]: https://github.com/rails-hyperdrive/rails-hyperdrive/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -1,21 +1,25 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/rails-hyperdrive/rails-hyperdrive/main/docs/logo-wide.png" alt="" width="480">
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
# Rails Hyperdrive
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
**Live introspection and stack-matched knowledge for AI coding agents, straight from your Rails app.**
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
[](https://rubygems.org/gems/rails-hyperdrive)
|
|
10
|
+
[](https://github.com/rails-hyperdrive/rails-hyperdrive/actions/workflows/ci.yml)
|
|
11
|
+
[](LICENSE.txt)
|
|
8
12
|
|
|
9
|
-
|
|
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`.
|
|
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:
|
|
11
14
|
|
|
12
|
-
|
|
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
|
+
- **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.
|
|
13
17
|
|
|
14
|
-
|
|
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.
|
|
15
19
|
|
|
16
20
|
---
|
|
17
21
|
|
|
18
|
-
##
|
|
22
|
+
## Quick start
|
|
19
23
|
|
|
20
24
|
```bash
|
|
21
25
|
# 1. Add the dev gem
|
|
@@ -53,17 +57,13 @@ $ bin/dev
|
|
|
53
57
|
# → agent has 8 tools, the eager guidelines (via CLAUDE.md), and the lazy skills
|
|
54
58
|
```
|
|
55
59
|
|
|
56
|
-
|
|
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.
|
|
60
|
+
That's it. No API keys, no config files to write, no per-project setup beyond the generator.
|
|
61
61
|
|
|
62
62
|
---
|
|
63
63
|
|
|
64
|
-
## What
|
|
64
|
+
## What your agent gets
|
|
65
65
|
|
|
66
|
-
### MCP tools
|
|
66
|
+
### 8 MCP tools
|
|
67
67
|
|
|
68
68
|
| # | Tool | Purpose |
|
|
69
69
|
|---|------|---------|
|
|
@@ -73,30 +73,66 @@ Run `hyperdrive:discover` to find companion gems published for your stack that y
|
|
|
73
73
|
| 4 | `list_models` | List Active Record model classes with columns/validations/associations |
|
|
74
74
|
| 5 | `locate_source` | Resolve `Const` / `Const#method` / `Const.method` / `dep:<gem>` to a file:line |
|
|
75
75
|
| 6 | `lookup_doc` | Look up RDoc for a symbol (via `ri`) |
|
|
76
|
-
| 7 | `describe_app` | Snapshot: Rails/Ruby/DB versions +
|
|
76
|
+
| 7 | `describe_app` | Snapshot: Rails/Ruby/DB versions + direct gem dependencies |
|
|
77
77
|
| 8 | `list_routes` | All routes: HTTP verb, path, controller#action, named route |
|
|
78
78
|
|
|
79
|
-
|
|
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).
|
|
80
|
+
|
|
81
|
+
### Two kinds of knowledge
|
|
82
|
+
|
|
83
|
+
Companion gems ship two artifact types, tuned for how agents consume context:
|
|
84
|
+
|
|
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`.
|
|
87
|
+
|
|
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.
|
|
89
|
+
|
|
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.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Staying in sync
|
|
95
|
+
|
|
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`.
|
|
97
|
+
|
|
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:
|
|
80
99
|
|
|
81
|
-
|
|
82
|
-
|
|
100
|
+
| Strategy | What happens to the live file | What happens to your edits |
|
|
101
|
+
|---|---|---|
|
|
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 |
|
|
103
|
+
| `--sidecar` | Untouched; the new upstream body is written next to it as `<file>.new` | Kept, byte-for-byte |
|
|
104
|
+
| `--overwrite` | Restored to the gem-shipped content | Discarded |
|
|
83
105
|
|
|
84
|
-
|
|
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.
|
|
107
|
+
|
|
108
|
+
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
|
+
|
|
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.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## You stay in charge
|
|
115
|
+
|
|
116
|
+
### Everything lands git-tracked
|
|
85
117
|
|
|
86
118
|
```
|
|
87
119
|
CLAUDE.md # user-owned; ONE injected line: @.claude/hyperdrive/index.md
|
|
88
120
|
.claude/hyperdrive/
|
|
89
121
|
index.md # managed aggregator: @guidelines/<name>.md
|
|
90
|
-
guidelines/<name>.md # companion-shipped, frontmatter stripped
|
|
122
|
+
guidelines/<name>.md # companion-shipped, frontmatter stripped
|
|
91
123
|
.claude/skills/<name>/
|
|
92
|
-
SKILL.md # companion-shipped, frontmatter
|
|
124
|
+
SKILL.md # companion-shipped, installed verbatim (frontmatter included)
|
|
93
125
|
<supporting files> # optional companion-shipped extras, installed as shipped (*.md.erb rendered)
|
|
94
126
|
.hyperdrive/lock.yml # git-tracked manifest (source gem, version, content hash)
|
|
95
127
|
```
|
|
96
128
|
|
|
97
|
-
`
|
|
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`.
|
|
130
|
+
|
|
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).
|
|
98
132
|
|
|
99
|
-
|
|
133
|
+
### Your edits win
|
|
134
|
+
|
|
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.
|
|
100
136
|
|
|
101
137
|
### Turning off a single artifact
|
|
102
138
|
|
|
@@ -114,115 +150,73 @@ A disabled artifact is never installed, and one already on disk is removed on th
|
|
|
114
150
|
|
|
115
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.
|
|
116
152
|
|
|
117
|
-
To skip installed content wholesale instead, pass `--skip-content`.
|
|
153
|
+
To skip installed content wholesale instead, pass `--skip-content` to `hyperdrive:init`.
|
|
118
154
|
|
|
119
|
-
###
|
|
155
|
+
### Opting into a gem's bundled skills
|
|
120
156
|
|
|
121
|
-
|
|
157
|
+
Ordinary gems (not built as hyperdrive companions) sometimes ship a top-level `skills/` directory of [skills.sh](https://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`:
|
|
122
158
|
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
|
|
159
|
+
```yaml
|
|
160
|
+
enabled:
|
|
161
|
+
- foo
|
|
126
162
|
```
|
|
127
163
|
|
|
128
|
-
|
|
164
|
+
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.
|
|
129
165
|
|
|
130
|
-
|
|
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.
|
|
166
|
+
---
|
|
135
167
|
|
|
136
|
-
|
|
168
|
+
## Safety
|
|
137
169
|
|
|
138
|
-
|
|
170
|
+
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).
|
|
139
171
|
|
|
140
|
-
```yaml
|
|
141
172
|
---
|
|
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
173
|
|
|
151
|
-
|
|
174
|
+
## Build a companion gem
|
|
152
175
|
|
|
153
|
-
|
|
176
|
+
Ship markdown, declare what it targets, publish. That's the whole contract:
|
|
154
177
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
---
|
|
178
|
+
```
|
|
179
|
+
skills/<name>/SKILL.md # skill (dir-per-skill, may ship supporting files)
|
|
180
|
+
lib/<gem_name>/hyperdrive/guidelines/<name>.md # guideline (flat file)
|
|
166
181
|
```
|
|
167
182
|
|
|
168
|
-
|
|
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.
|
|
183
|
+
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. `lib/<gem_name>/hyperdrive/` is hyperdrive-specific machinery: guidelines (no skills.sh analogue), ERB skill templates (`SKILL.md.erb` — raw ERB must not reach generic consumers, so keep it out of `skills/`), and legacy static skills (still scanned for back-compat).
|
|
173
184
|
|
|
174
|
-
|
|
185
|
+
Frontmatter is pure [skills.sh](https://skills.sh) — only `name` and `description` are read, so a skill repo's content integrates without modification:
|
|
175
186
|
|
|
176
187
|
```yaml
|
|
177
188
|
---
|
|
178
|
-
name:
|
|
179
|
-
description:
|
|
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
|
+
name: jobs-sidekiq # kebab-case; determines the install path
|
|
190
|
+
description: Background job conventions for Sidekiq.
|
|
189
191
|
---
|
|
190
192
|
```
|
|
191
193
|
|
|
192
|
-
|
|
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`.
|
|
194
|
+
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:
|
|
205
195
|
|
|
206
|
-
|
|
196
|
+
```yaml
|
|
197
|
+
gem: sidekiq # gem-wide default: TARGET gem(s), resolved + version-matched in the bundle
|
|
198
|
+
versions: ">= 7.0, < 9.0" # gem-wide default: Gem::Requirement matched against the target gem
|
|
199
|
+
skills: # per-skill overrides, keyed by skill dir relative to its skills root
|
|
200
|
+
jobs-sidekiq:
|
|
201
|
+
versions: ">= 8.0"
|
|
202
|
+
guidelines: # per-guideline overrides, keyed by filename
|
|
203
|
+
jobs.md:
|
|
204
|
+
gem: sidekiq
|
|
205
|
+
```
|
|
207
206
|
|
|
208
|
-
|
|
207
|
+
Shipping a `hyperdrive.yml` (or declaring `rails_hyperdrive_manifest`) opts your gem in as a companion. Also declare your targets in gemspec metadata — it opts your gem in too, and it is the pre-install targeting signal (how `hyperdrive:discover` suggests you before anyone installs you):
|
|
209
208
|
|
|
210
209
|
```ruby
|
|
211
|
-
spec.metadata["rails_hyperdrive_targets"]
|
|
212
|
-
spec.metadata["rails_hyperdrive_artifacts"] = "guideline,skill" # optional; presentational hint
|
|
210
|
+
spec.metadata["rails_hyperdrive_targets"] = "sidekiq"
|
|
213
211
|
```
|
|
214
212
|
|
|
215
|
-
|
|
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.
|
|
213
|
+
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, collision and dedup rules — lives in [docs/COMPANION_GEMS.md](docs/COMPANION_GEMS.md).
|
|
218
214
|
|
|
219
215
|
---
|
|
220
216
|
|
|
221
|
-
##
|
|
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).
|
|
217
|
+
## Requirements
|
|
224
218
|
|
|
225
|
-
|
|
219
|
+
Ruby ≥ 3.2, Rails ≥ 7.2. Tested against Rails 7.2 and 8.1 on Ruby 3.2–3.4.
|
|
226
220
|
|
|
227
221
|
## License
|
|
228
222
|
|
|
@@ -4,7 +4,7 @@ Description:
|
|
|
4
4
|
merging into any servers already configured there
|
|
5
5
|
- mounts Rails::Hyperdrive::Engine at /_hyperdrive in config/routes.rb (idempotent)
|
|
6
6
|
- discovers companion-gem skills + guidelines in the bundle and installs
|
|
7
|
-
them
|
|
7
|
+
them (skills to .claude/skills/, guidelines to
|
|
8
8
|
.claude/hyperdrive/guidelines/)
|
|
9
9
|
- maintains the .claude/hyperdrive/index.md aggregator and injects one
|
|
10
10
|
@-include line into CLAUDE.md — both only while a companion gem ships
|
|
@@ -13,7 +13,8 @@ Description:
|
|
|
13
13
|
|
|
14
14
|
Re-running re-syncs content and leaves locally-modified files untouched
|
|
15
15
|
(skip + warn). Routine content refresh is bin/rails hyperdrive:sync
|
|
16
|
-
(
|
|
16
|
+
(see its --merge, --sidecar, and --overwrite flags for reconciling
|
|
17
|
+
locally-modified files).
|
|
17
18
|
|
|
18
19
|
Examples:
|
|
19
20
|
bin/rails hyperdrive:init
|
|
@@ -2,20 +2,34 @@ Description:
|
|
|
2
2
|
Syncs Rails Hyperdrive content to match the current bundle (run after
|
|
3
3
|
`bundle update` or after adding a companion gem):
|
|
4
4
|
- discovers companion-gem skills + guidelines in the bundle and installs
|
|
5
|
-
them
|
|
5
|
+
them (skills to .claude/skills/, guidelines to
|
|
6
6
|
.claude/hyperdrive/guidelines/)
|
|
7
7
|
- maintains the .claude/hyperdrive/index.md aggregator and the single
|
|
8
8
|
@-include line in CLAUDE.md — both only while a companion gem ships a
|
|
9
9
|
guideline, and both removed when the last one goes
|
|
10
10
|
- tracks everything in .hyperdrive/lock.yml
|
|
11
11
|
|
|
12
|
-
Locally-modified files are left untouched (skip + warn).
|
|
13
|
-
|
|
12
|
+
Locally-modified files are left untouched by default (skip + warn). Three
|
|
13
|
+
mutually-exclusive strategies reconcile them with a new upstream:
|
|
14
|
+
--merge three-way-merge the upstream change into the live file
|
|
15
|
+
(clean merges only; anything else falls back to --sidecar,
|
|
16
|
+
so no conflict marker ever lands in a live file)
|
|
17
|
+
--sidecar write the new upstream body to <file>.new next to the live
|
|
18
|
+
file, leaving the live file untouched
|
|
19
|
+
--overwrite restore the live file to the gem-shipped content,
|
|
20
|
+
discarding local edits
|
|
21
|
+
|
|
22
|
+
Resolving a sidecar: fold what you want from <file>.new into the live
|
|
23
|
+
file, then delete <file>.new — or `mv <file>.new <file>` to accept the
|
|
24
|
+
upstream wholesale. Either way the next sync is quiet: the lock already
|
|
25
|
+
records the delivered upstream, so the same version is never re-offered.
|
|
14
26
|
|
|
15
27
|
Touches no bootstrap artifact: .mcp.json, the engine mount, the optional
|
|
16
28
|
initializer, and .gitignore belong to hyperdrive:init and are left alone.
|
|
17
29
|
|
|
18
30
|
Examples:
|
|
19
31
|
bin/rails hyperdrive:sync
|
|
32
|
+
bin/rails hyperdrive:sync --merge
|
|
33
|
+
bin/rails hyperdrive:sync --sidecar
|
|
20
34
|
bin/rails hyperdrive:sync --overwrite
|
|
21
35
|
bin/rails hyperdrive:sync --dry-run
|
|
@@ -15,8 +15,17 @@ module Rails
|
|
|
15
15
|
source_root File.expand_path("templates", __dir__)
|
|
16
16
|
|
|
17
17
|
class_option :overwrite, type: :boolean, default: false, desc: "Restore locally-modified managed files to the gem-shipped content."
|
|
18
|
+
class_option :merge, type: :boolean, default: false, desc: "Three-way-merge upstream changes into locally-modified files; falls back to sidecar delivery."
|
|
19
|
+
class_option :sidecar, type: :boolean, default: false, desc: "Deliver upstream changes for locally-modified files to <file>.new sidecars."
|
|
18
20
|
class_option :dry_run, type: :boolean, default: false, desc: "Show what would change; write nothing."
|
|
19
21
|
|
|
22
|
+
def verify_options
|
|
23
|
+
chosen = %i[overwrite merge sidecar].select { |flag| options[flag] }
|
|
24
|
+
return if chosen.size <= 1
|
|
25
|
+
raise Thor::Error,
|
|
26
|
+
"hyperdrive: #{chosen.map { |flag| "--#{flag}" }.join(" and ")} are mutually exclusive; pick one"
|
|
27
|
+
end
|
|
28
|
+
|
|
20
29
|
def verify_environment
|
|
21
30
|
runner.verify_environment!
|
|
22
31
|
end
|
|
@@ -26,7 +35,13 @@ module Rails
|
|
|
26
35
|
end
|
|
27
36
|
|
|
28
37
|
def sync_content
|
|
29
|
-
|
|
38
|
+
mode =
|
|
39
|
+
if options[:overwrite] then :overwrite
|
|
40
|
+
elsif options[:merge] then :merge
|
|
41
|
+
elsif options[:sidecar] then :sidecar
|
|
42
|
+
else :preserve
|
|
43
|
+
end
|
|
44
|
+
runner.install(mode: mode)
|
|
30
45
|
end
|
|
31
46
|
|
|
32
47
|
def print_summary
|
|
@@ -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 }
|