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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7577ebf92af02fb8bb079a8b32c8751744340390f0f0facc0e682bc31aa6d43d
4
- data.tar.gz: 990f940533691baf570a17113806fcd274300b56ee171a22195921ea9ddeb6bd
3
+ metadata.gz: 11be45047a967ade290022f1aa91cd023bf6085e0c84b4f6c08807d9b29f446b
4
+ data.tar.gz: 68205915ef162cc1006685361cb14e1690dcd82b666fbab12a54fdf7d5f26e2a
5
5
  SHA512:
6
- metadata.gz: af48e37db907f10bf2a74d8303aa57b5cd04bd44d7ab97834bc197d97f39cf77cef87116217ce8d773d95d5ae6d93b6e216cbec275b3886bdb7c9b32cbe356af
7
- data.tar.gz: 99144761cd64cf3bbed57caf0eaa0ab691511d7246e7add30e8fdf94bcd2f1e857c0b1612b5b95033a9336ca71faf80bd9bdae3db9813c3377dea3efdf6986c7
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.3.0...HEAD
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
- # Rails Hyperdrive
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
- > Dev-only Rails engine that bootstraps an MCP server + skills/guidelines for AI coding agents (Claude Code first).
5
+ # Rails Hyperdrive
4
6
 
5
- Rails Hyperdrive mounts an [MCP (Model Context Protocol)](https://modelcontextprotocol.io) server at `http://localhost:3000/_hyperdrive/mcp` in development, exposing **8 introspection tools** so AI agents stop guessing — they can eval Ruby, query the DB (read-only), tail logs, list models and routes, locate source, fetch docs, and snapshot the stack.
7
+ **Live introspection and stack-matched knowledge for AI coding agents, straight from your Rails app.**
6
8
 
7
- It also ships a `hyperdrive:init` generator that discovers and installs **two artifact types** that companion gems ship under a documented contract:
9
+ [![Gem Version](https://img.shields.io/gem/v/rails-hyperdrive)](https://rubygems.org/gems/rails-hyperdrive)
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
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE.txt)
8
12
 
9
- - **Skills** lazy, model-invoked via Claude Code's native description matcher. Procedural ("how to write an idempotent Sidekiq job"). Installed to `.claude/skills/<name>/SKILL.md`.
10
- - **Guidelines** — eager, always in context via `@`-include from `CLAUDE.md`. Declarative ("this app uses Pundit, not CanCanCan"). Installed to `.claude/hyperdrive/guidelines/<name>.md`.
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
- **rails-hyperdrive is the mechanism; companion gems are the content.** rails-hyperdrive itself ships no skills or guidelines only the contract and the discovery/install engine. Content comes from companion gems, conventionally named `rails-hyperdrive-<library>` (e.g. `rails-hyperdrive-sidekiq`) following the [RuboCop ecosystem](https://github.com/rubocop/rubocop) precedent.
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
- Built on the official [`mcp` gem](https://github.com/modelcontextprotocol/ruby-sdk). MIT-licensed.
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
- ## Golden path
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
- `hyperdrive:init` also registers the `bundler-rails-hyperdrive` Bundler plugin in your Gemfile. From then on, `bundle add rails-hyperdrive-<library>` lands the companion's artifacts on that very `bundle install` — no explicit sync. The plugin is additive only (it never touches an existing file); version bumps and orphaned artifacts are only reported, with a pointer to `bin/rails hyperdrive:sync`.
57
-
58
- Run `bin/rails hyperdrive:sync` any time (e.g. after `bundle update` or adding a companion gem) to refresh installed content to the current bundle. It touches no bootstrap artifact and leaves locally-modified files untouched (skip + warn); pass `--overwrite` to restore them to the gem-shipped content.
59
-
60
- Run `hyperdrive:discover` to find companion gems published for your stack that you haven't installed yet — it queries rubygems (read-only, results cached for 24h; `--refresh` re-queries) and prints the `bundle add` lines to run, then run `bin/rails hyperdrive:sync`. It never touches your Gemfile or makes network calls unless you invoke it.
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 ships
64
+ ## What your agent gets
65
65
 
66
- ### MCP tools (8)
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 + full `StackProfile` |
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
- ### Resources
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
- - `hyperdrive://stack-profile` JSON of the resolved `StackProfile`
82
- - `hyperdrive://skills/{name}` — markdown body of each installed skill
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
- ### Install layout
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, audit-headered
122
+ guidelines/<name>.md # companion-shipped, frontmatter stripped
91
123
  .claude/skills/<name>/
92
- SKILL.md # companion-shipped, frontmatter kept, audit-headered
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
- `CLAUDE.md` and `index.md` are the **eager chain** they exist only because a companion gem ships a guideline, and both go when the last one leaves the bundle (the guideline file itself is left on disk and reported as an orphan). With no companion gems installed, `hyperdrive:init` writes `.mcp.json`, the `.gitignore` rule, the optional initializer, the engine mount, and `.hyperdrive/lock.yml` and nothing into the agent's context window.
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
- Everything a companion gem contributes lands here git-tracked, so a diff is where you review what it added — the install summary names each artifact's source gem and version, and every SKILL.md and guideline carries the same provenance in an audit header. A skill's supporting files carry no header — they install byte-identical to the install-ready body (the shipped bytes; for `*.md.erb` templates, the rendered output), and their provenance and content hash live in `.hyperdrive/lock.yml` alone. `hyperdrive:init` warns if your app gitignores these paths, since that empties the diff without changing what reaches the agent. The `hyperdrive:discover` cache is the one file rails-hyperdrive adds to `.gitignore`.
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
- ### Companion gem contract
155
+ ### Opting into a gem's bundled skills
120
156
 
121
- A companion gem ships artifacts under:
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
- <gem-source>/lib/<gem_name>/hyperdrive/skills/<name>/SKILL.md # skill (dir-per-skill)
125
- <gem-source>/lib/<gem_name>/hyperdrive/guidelines/<name>.md # guideline (flat file)
159
+ ```yaml
160
+ enabled:
161
+ - foo
126
162
  ```
127
163
 
128
- Skills may ship under an additional root declared in gemspec metadata:
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
- ```ruby
131
- spec.metadata["rails_hyperdrive_skills_dir"] = "extra/skills" # optional; relative to the gem root
132
- ```
133
-
134
- That root is searched **in addition to** the convention path, never instead of it, so an override never hides skills already shipped at the convention path. A value containing a `..` segment is ignored. Guidelines have no override — they are found only at the convention path.
166
+ ---
135
167
 
136
- A skill is a **directory**, and it may ship more than `SKILL.md`. Everything else in the skill directory — nested however you like (`workflows/`, `references/`, `examples/`, …) — installs alongside it as **supporting files**, preserving the relative layout under `.claude/skills/<name>/`. Reference them from `SKILL.md` with directory-relative links; a cross-source name collision renames the whole installed directory, so those links keep working. Supporting files carry no frontmatter contract and no audit header — they install byte-identical to the install-ready body, which is what the gem ships (markdown, code, or binary alike) except for `*.md.erb` templates, which install as their rendered output. Each is tracked per file in `.hyperdrive/lock.yml`, so local edits are preserved on sync exactly like any other installed file. `SKILL.md` frontmatter remains the skill's sole schema surface. Guidelines stay single-file.
168
+ ## Safety
137
169
 
138
- Every artifact carries four required YAML frontmatter fields:
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
- `versions:` accepts a single comma-separated string (`">= 7.0, < 9.0"`), a YAML list (`[">= 7.0", "< 9.0"]`), or — for multi-target artifacts — a map keyed by gem name.
174
+ ## Build a companion gem
152
175
 
153
- `gem:` names the **targets** (each must be present in the bundle; its resolved version is matched against `versions:`). Use `railties` for "every Rails app" or the quoted `"*"` for "always applicable" (it must be quoted — bare `*` is a YAML alias and the file is skipped). `hyperdrive:init` discovers every such file across the bundle, version-matches it, and installs it with an audit header naming `source`, `sha256`, and `installed_at`. Guidelines are installed with their frontmatter stripped (they are `@`-included eagerly). When two gems ship a same-named artifact, both install, each postfixed by source gem.
176
+ Ship markdown, declare what it targets, publish. That's the whole contract:
154
177
 
155
- One artifact can cover several interchangeable libraries — write `gem:` as a comma-separated string or a YAML list, and it installs when **any** listed target is bundled at a satisfying version. `"*"` anywhere in the list makes the artifact universal. Give `versions:` a map keyed by gem name when the targets do not share a version cycle; targets the map omits are unconstrained.
156
-
157
- ```yaml
158
- ---
159
- name: jobs-conventions
160
- description: Background job conventions.
161
- gem: [sidekiq, solid_queue, good_job]
162
- versions:
163
- sidekiq: ">= 7.0"
164
- solid_queue: ">= 1.0"
165
- ---
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
- Draw the listed targets from the gem's own `hyperdrive_targets` (below): the gem-level declaration decides whether a companion is suggested at all, and an artifact naming a target the gemspec omits is unreachable for apps that have only that target.
169
-
170
- #### Gem-conditional skill content
171
-
172
- A multi-file skill can condition parts of itself on the app's bundle, so one skill tree serves apps with different gem sets. Both mechanisms are evaluated at discovery time, against the same resolved bundle that gates whole artifacts.
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
- **Per-file gating.** A `conditional:` map in `SKILL.md` frontmatter gates individual supporting files. Keys are dir-relative shipped paths; values take the same `gem:`/`versions:` forms as the artifact-level fields (single target, comma-separated string, YAML list, per-target `versions:` map, `"*"`), and the file installs when **any** listed target is bundled at a satisfying version. Unlike the artifact level, `versions:` is optional here — omitted means unconstrained. Files the map doesn't mention install unconditionally, and the supporting files themselves stay byte-identical to upstream — the condition lives entirely out-of-band.
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: layered-rails
179
- description: Layered architecture conventions.
180
- gem: railties
181
- versions: ">= 7.2"
182
- conditional:
183
- references/gems/alba.md:
184
- gem: alba
185
- references/gems/jobs.md:
186
- gem: [sidekiq, solid_queue]
187
- versions:
188
- sidekiq: ">= 7.0"
189
+ name: jobs-sidekiq # kebab-case; determines the install path
190
+ description: Background job conventions for Sidekiq.
189
191
  ---
190
192
  ```
191
193
 
192
- A malformed condition **fails open**: the file installs unconditionally and the problem is reported with the other discovery warnings a surplus reference file is harmless, a missing one breaks links from `SKILL.md`. A key naming no shipped file, or naming `SKILL.md` itself (the artifact-level `gem:`/`versions:` gate the whole skill), is warned about and ignored. The `conditional:` key ships through to the installed frontmatter unchanged.
193
-
194
- **ERB-templated markdown.** A file named `*.md.erb` in a skill directory — including `SKILL.md.erb` in place of `SKILL.md` — is rendered at install time and lands as plain `.md` (the `.erb` suffix is dropped; a `SKILL.md.erb` defines a skill exactly like `SKILL.md`, with frontmatter read from the rendered output). Templates see a sealed binding of exactly three helpers over the resolved bundle, nothing else:
195
-
196
- - `gem?("name")` / `gem?("name", ">= 2.0")` — is the gem bundled (at a satisfying version)?
197
- - `any_gem?("a", "b", …)` — is any of these bundled?
198
- - `gem_version("name")` — the resolved version as a String, or `nil`.
199
-
200
- Rendering uses ERB's trim mode, so `<%- if gem?("alba") -%>` … `<%- end -%>` control lines leave no blank lines behind. A template that fails to render is skipped with a warning (the whole skill, when it's `SKILL.md.erb`); when a plain file and a template would land at the same path, the plain file wins with a warning. `conditional:` keys refer to templates by their shipped `x.md.erb` name and gate them before rendering. Guidelines get no ERB support.
201
-
202
- Use ERB sparingly — condition reference manuals via `conditional:` and wrap link-table rows that point at gated files, but keep "consider adopting gem X" recommendations unconditional. An all-wrapped `.md.erb` renders to an empty file; to omit a file entirely, gate it with `conditional:` instead.
203
-
204
- Gated files appear and disappear as the bundle changes: `hyperdrive:init`/`hyperdrive:sync` install newly gated-in files and remove unedited gated-out ones. The auto top-up after `bundle install` adds newly gated-in files but never removes anything and never rewrites an already-installed file, so removals and re-rendered template output wait for the next `hyperdrive:sync`.
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
- Discovery never raises. An artifact with missing or malformed frontmatter, a missing required field, no declared target in the bundle, or every bundled target resolving outside `versions:` is skipped, and the reason is collected. `hyperdrive:init` and `hyperdrive:sync` print the collected reasons at the end of the run, under a yellow `warn` line reading `discovery skipped N artifact(s):`. A companion whose artifacts all fail therefore installs nothing and reports it only there — read that section first when a gem you expected to contribute produces no files.
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
- To be discoverable by `hyperdrive:discover` **before** it is installed, a companion also declares gemspec metadata (read remotely from rubygems, so the frontmatter inside the gem isn't visible yet):
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"] = "sidekiq" # required; comma-sep, or "*" for always-applicable
212
- spec.metadata["rails_hyperdrive_artifacts"] = "guideline,skill" # optional; presentational hint
210
+ spec.metadata["rails_hyperdrive_targets"] = "sidekiq"
213
211
  ```
214
212
 
215
- `rails_hyperdrive_targets` is what makes a gem discoverable: `hyperdrive:discover` searches rubygems for gems declaring it, so a companion is found by what it declares rather than by what it is named. Naming it `rails-hyperdrive-<library>` is a recommended convention it makes the gem legible in a Gemfile but it plays no part in discovery, and a companion published under your own namespace is found on the same terms.
216
-
217
- `rails_hyperdrive_targets` is a coarse pre-install hint — it is never reconciled against the frontmatter `gem:`; once the gem is bundled, the frontmatter alone governs what installs.
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 ruleslives in [docs/COMPANION_GEMS.md](docs/COMPANION_GEMS.md).
218
214
 
219
215
  ---
220
216
 
221
- ## Safety
222
-
223
- Rails Hyperdrive is **dev-only**. The engine refuses to handle requests outside `Rails.env.development?` and enforces an origin allowlist (`localhost`, `127.0.0.1`, `[::1]`). See [SECURITY.md](SECURITY.md).
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 with audit headers (skills to .claude/skills/, guidelines to
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
- (pass --overwrite to restore gem-shipped content).
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 with audit headers (skills to .claude/skills/, guidelines to
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). Pass --overwrite
13
- to restore them to the gem-shipped content.
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
- runner.install(mode: options[:overwrite] ? :overwrite : :preserve)
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(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 }