rails-hyperdrive 0.9.0 → 0.9.1
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 +22 -1
- data/README.md +6 -14
- data/lib/generators/hyperdrive/install/install_generator.rb +10 -4
- data/lib/generators/hyperdrive/sync/USAGE +6 -4
- data/lib/generators/hyperdrive/sync_runner.rb +2 -1
- data/lib/rails/hyperdrive/resolve/prompt.md.erb +3 -3
- data/lib/rails/hyperdrive/sidecar_resolver.rb +23 -5
- data/lib/rails/hyperdrive/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8d3970fe19124e7d291ca3ae41b4b97b3ca9b2fe4634d2f4806b58142a01ec87
|
|
4
|
+
data.tar.gz: 0d8b0cc84e29b59e917a1b7a1c1fb92d8b5a54560ab47294d906f56fb4bcaa3f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 14031a19b195ee3802a88bbe54af5851495463e079b45fda9d9747fda7c9606d512eee8eb376fb191b21cb1e5db586a7b32bb15bc9181251032cf60c6d91a4f6
|
|
7
|
+
data.tar.gz: 9ef4f32b7b801102e4348a2a4297cf502b7185ce0ec2af0984d9782ed4cd6a33208adb7287a9ee2e3b719d41ac23d2281f8b9769f9800ff499a84bda47b2e3c8
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.9.1] - 2026-09-06
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **`hyperdrive:sync --resolve` no longer deletes a sidecar on a bare exit 0.**
|
|
15
|
+
The command must also have changed `$MERGED`, matching `git mergetool`'s
|
|
16
|
+
default of not trusting the exit code alone. Exit 0 with the live file
|
|
17
|
+
untouched is reported as unresolved — `command exited 0 but wrote nothing`
|
|
18
|
+
(`command exited 0 but $MERGED is missing` when the command deleted it) — and
|
|
19
|
+
leaves the live file, the sidecar, and the lockfile exactly as they were, so
|
|
20
|
+
an agent that was denied every write no longer loses the pending upstream
|
|
21
|
+
delivery.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- **The suggested Claude Code resolver command is now `claude -p $PROMPT
|
|
26
|
+
--allowedTools Read,Edit,Write`.** `--permission-mode acceptEdits` denies
|
|
27
|
+
writes under `.claude/`, where every hyperdrive artifact lives, and cannot read
|
|
28
|
+
`$BASE`, which sits outside the app.
|
|
29
|
+
|
|
10
30
|
## [0.9.0] - 2026-09-05
|
|
11
31
|
|
|
12
32
|
### Added
|
|
@@ -813,7 +833,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
813
833
|
- `hyperdrive:init` generator that installs architecture skills and auto-discovers
|
|
814
834
|
per-gem skills.
|
|
815
835
|
|
|
816
|
-
[Unreleased]: https://github.com/rails-hyperdrive/rails-hyperdrive/compare/v0.9.
|
|
836
|
+
[Unreleased]: https://github.com/rails-hyperdrive/rails-hyperdrive/compare/v0.9.1...HEAD
|
|
837
|
+
[0.9.1]: https://github.com/rails-hyperdrive/rails-hyperdrive/releases/tag/v0.9.1
|
|
817
838
|
[0.9.0]: https://github.com/rails-hyperdrive/rails-hyperdrive/releases/tag/v0.9.0
|
|
818
839
|
[0.8.0]: https://github.com/rails-hyperdrive/rails-hyperdrive/releases/tag/v0.8.0
|
|
819
840
|
[0.7.0]: https://github.com/rails-hyperdrive/rails-hyperdrive/releases/tag/v0.7.0
|
data/README.md
CHANGED
|
@@ -119,7 +119,7 @@ With no companion gems, `hyperdrive:init` sets up just the plumbing (`.mcp.json`
|
|
|
119
119
|
| `--merge` | Rewritten with a git three-way merge (base = the gem version you last installed, ours = your file, theirs = the new upstream) when it applies cleanly | Kept | `--sidecar`, whenever git cannot produce a clean result |
|
|
120
120
|
| `--sidecar` | Untouched; the new upstream body is written next to it as `<file>.new` | Kept, byte-for-byte | — |
|
|
121
121
|
| `--overwrite` | Restored to the gem-shipped content | Discarded | — |
|
|
122
|
-
| `--resolve` | Delivers as `--sidecar` (or as `--merge`, when both are given), then hands each unresolved `<file>.new` to a command you configure; exit 0 deletes the sidecar | Up to your command | — |
|
|
122
|
+
| `--resolve` | Delivers as `--sidecar` (or as `--merge`, when both are given), then hands each unresolved `<file>.new` to a command you configure; exit 0 after the command changed the file deletes the sidecar | Up to your command | — |
|
|
123
123
|
|
|
124
124
|
`--merge`, `--sidecar`, and `--overwrite` are mutually exclusive. `--resolve` stacks on top: alone it means "sidecar, then resolve", `--merge --resolve` means "git merges what it can, your command takes the rest", and `--overwrite --resolve` is rejected because an overwrite leaves nothing to resolve. Every combination accepts `--dry-run`, which prints the plan and writes nothing.
|
|
125
125
|
|
|
@@ -129,12 +129,12 @@ A sidecar is inert (Claude Code loads only `SKILL.md` and the `index.md` `@`-lin
|
|
|
129
129
|
|
|
130
130
|
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.
|
|
131
131
|
|
|
132
|
-
**`--resolve`: hand the sidecars to a tool.** `--resolve` automates that last step, `git mergetool` style. After delivery it hands every unresolved `<file>.new` — from this run or left over from an earlier one — to the command named in `.hyperdrive/config.yml`, and deletes the sidecar when that command exits 0. The gem ships no command of its own, so nothing runs that you did not name:
|
|
132
|
+
**`--resolve`: hand the sidecars to a tool.** `--resolve` automates that last step, `git mergetool` style. After delivery it hands every unresolved `<file>.new` — from this run or left over from an earlier one — to the command named in `.hyperdrive/config.yml`, and deletes the sidecar when that command exits 0 having changed the file. The gem ships no command of its own, so nothing runs that you did not name:
|
|
133
133
|
|
|
134
134
|
```yaml
|
|
135
135
|
# .hyperdrive/config.yml
|
|
136
136
|
resolve:
|
|
137
|
-
command: claude -p --
|
|
137
|
+
command: claude -p $PROMPT --allowedTools Read,Edit,Write
|
|
138
138
|
```
|
|
139
139
|
|
|
140
140
|
```sh
|
|
@@ -143,6 +143,8 @@ bin/rails hyperdrive:sync --merge --resolve # git merges what it can, your too
|
|
|
143
143
|
git diff # then you review
|
|
144
144
|
```
|
|
145
145
|
|
|
146
|
+
That form grants the tools rather than setting `--permission-mode acceptEdits`, because `acceptEdits` denies writes under `.claude/`, where every artifact lives, and cannot read `$BASE`, which is outside the app; `$PROMPT` comes first because `--allowedTools` takes every following argument.
|
|
147
|
+
|
|
146
148
|
The command is split with shell word rules and run with no shell, from the app root. Each argument gets these placeholders substituted, and every one is also exported as an environment variable (`$LOCAL` → `HYPERDRIVE_LOCAL`, and so on), so a wrapper script needs no argument parsing:
|
|
147
149
|
|
|
148
150
|
| Placeholder | Value |
|
|
@@ -156,19 +158,9 @@ The command is split with shell word rules and run with no shell, from the app r
|
|
|
156
158
|
| `$KIND` | `skill`, `guideline`, `agent`, `command`, or `skill_support` |
|
|
157
159
|
| `$PROMPT` | Ready-made instructions for an agent, naming the paths above |
|
|
158
160
|
|
|
159
|
-
`$BASE` is a temporary file outside the app, removed as soon as the command returns, so a tool that is confined to the project directory needs it granted explicitly:
|
|
160
|
-
|
|
161
|
-
```sh
|
|
162
|
-
#!/usr/bin/env bash
|
|
163
|
-
# bin/hyperdrive-resolve, used as `command: bin/hyperdrive-resolve`
|
|
164
|
-
args=(-p --permission-mode acceptEdits)
|
|
165
|
-
[ -n "$HYPERDRIVE_BASE" ] && args+=(--add-dir "$(dirname "$HYPERDRIVE_BASE")")
|
|
166
|
-
exec claude "${args[@]}" "$HYPERDRIVE_PROMPT"
|
|
167
|
-
```
|
|
168
|
-
|
|
169
161
|
Replace the shipped prompt with your own ERB template — same placeholders, as lower-case locals (`local`, `remote`, `base`, `merged`, `source`, `previous_source`, `kind`) — with `resolve.prompt: .hyperdrive/resolve-prompt.md.erb`.
|
|
170
162
|
|
|
171
|
-
Exit 0 is
|
|
163
|
+
Exit 0 is honored only when the command changed `$MERGED` — git's own `mergetool` default (`trustExitCode = false`). Exit 0 with the file untouched is reported as `command exited 0 but wrote nothing` and, like any other exit — including a command that is not on your PATH — leaves the live file, the sidecar, and the lockfile exactly as they were, with the reason printed, so a failed resolve is just an ordinary unresolved sidecar. `--resolve` runs only on an explicit `hyperdrive:sync`, never during `bundle install`. `--dry-run` prints what it would hand off and runs nothing. A sidecar you edited yourself is never handed to the command.
|
|
172
164
|
|
|
173
165
|
**`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.
|
|
174
166
|
|
|
@@ -40,10 +40,10 @@ module Rails
|
|
|
40
40
|
# must write), $SOURCE, $PREVIOUS_SOURCE, $KIND, and $PROMPT (the
|
|
41
41
|
# instructions to give an agent) are substituted per argument, and each
|
|
42
42
|
# is also exported as HYPERDRIVE_LOCAL, HYPERDRIVE_REMOTE, and so on.
|
|
43
|
-
# Exit 0
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
# built-in one.
|
|
43
|
+
# Exit 0 marks the sidecar resolved only when the command changed the
|
|
44
|
+
# file; exit 0 with the file untouched, or any other exit, leaves the
|
|
45
|
+
# file, the sidecar, and the lock untouched. prompt: names your own
|
|
46
|
+
# ERB template, relative to the app root, replacing the built-in one.
|
|
47
47
|
disabled:
|
|
48
48
|
skills: []
|
|
49
49
|
guidelines: []
|
|
@@ -53,6 +53,12 @@ module Rails
|
|
|
53
53
|
# resolve:
|
|
54
54
|
# command: <your tool> $PROMPT
|
|
55
55
|
# prompt: .hyperdrive/resolve-prompt.md.erb
|
|
56
|
+
#
|
|
57
|
+
# For Claude Code:
|
|
58
|
+
# command: claude -p $PROMPT --allowedTools Read,Edit,Write
|
|
59
|
+
# Grant the tools rather than --permission-mode acceptEdits, which
|
|
60
|
+
# refuses writes under .claude/ and cannot read $BASE; $PROMPT comes
|
|
61
|
+
# first because --allowedTools takes every following argument.
|
|
56
62
|
YAML
|
|
57
63
|
|
|
58
64
|
# No templates are rendered; source_root exists so Rails resolves the
|
|
@@ -30,10 +30,12 @@ Description:
|
|
|
30
30
|
|
|
31
31
|
--resolve does that step for you, git-mergetool style: after delivery it
|
|
32
32
|
hands every unresolved <file>.new to the command in resolve.command in
|
|
33
|
-
.hyperdrive/config.yml and deletes the sidecar when that command exits 0
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
.hyperdrive/config.yml and deletes the sidecar when that command exits 0
|
|
34
|
+
after changing the file; exit 0 with the file untouched, or any other exit,
|
|
35
|
+
leaves everything in place. On its own it delivers to sidecars; with
|
|
36
|
+
--merge, git merges first and the command takes the rest. With no command
|
|
37
|
+
configured it stops before syncing anything. A sidecar you edited yourself
|
|
38
|
+
is never handed off.
|
|
37
39
|
|
|
38
40
|
Touches no bootstrap artifact: .mcp.json, the engine mount, the Gemfile
|
|
39
41
|
plugin directive, .gitignore, and .hyperdrive/config.yml belong to
|
|
@@ -22,7 +22,8 @@ module Rails
|
|
|
22
22
|
command: <your tool> $PROMPT
|
|
23
23
|
|
|
24
24
|
Placeholders: $LOCAL $REMOTE $BASE $MERGED $SOURCE $PREVIOUS_SOURCE $KIND $PROMPT
|
|
25
|
-
(also exported as HYPERDRIVE_* environment variables). Exit 0 marks the sidecar resolved
|
|
25
|
+
(also exported as HYPERDRIVE_* environment variables). Exit 0 marks the sidecar resolved
|
|
26
|
+
only when the command changed $MERGED.
|
|
26
27
|
MSG
|
|
27
28
|
|
|
28
29
|
def initialize(shell:, root: nil)
|
|
@@ -41,7 +41,7 @@ Rules:
|
|
|
41
41
|
- Keep the YAML frontmatter valid, and leave the `name:` line exactly as REMOTE
|
|
42
42
|
has it.
|
|
43
43
|
<% end -%>
|
|
44
|
-
- If you are not confident the result is correct,
|
|
45
|
-
|
|
44
|
+
- If you are not confident the result is correct, leave MERGED untouched; an
|
|
45
|
+
unchanged MERGED is treated as unresolved.
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
The file counts as resolved only if MERGED changes.
|
|
@@ -9,8 +9,9 @@ require "rails/hyperdrive/resolve_prompt"
|
|
|
9
9
|
module Rails
|
|
10
10
|
module Hyperdrive
|
|
11
11
|
# Hands each unresolved <dest>.new sidecar to the user's resolver command,
|
|
12
|
-
# git-mergetool style, and deletes the sidecar when it exits 0
|
|
13
|
-
# writes the lock: the sidecar's absence is the whole
|
|
12
|
+
# git-mergetool style, and deletes the sidecar when it exits 0 and changed
|
|
13
|
+
# the file. Nothing here writes the lock: the sidecar's absence is the whole
|
|
14
|
+
# resolution signal.
|
|
14
15
|
class SidecarResolver
|
|
15
16
|
TOKENS = %w[LOCAL REMOTE BASE MERGED SOURCE PREVIOUS_SOURCE KIND PROMPT].freeze
|
|
16
17
|
TOKEN_PATTERN = /\$(#{TOKENS.sort_by { |t| -t.length }.join("|")})\b/
|
|
@@ -105,11 +106,10 @@ module Rails
|
|
|
105
106
|
values = values_for(candidate, base: base)
|
|
106
107
|
argv = substitute(tokens, values, base: base)
|
|
107
108
|
@shell.say_status :resolve, "#{candidate.dest} via #{argv.first}", :blue
|
|
109
|
+
before = merged_bytes(candidate)
|
|
108
110
|
_out, err, status = Open3.capture3(env_for(values), *argv, chdir: @root)
|
|
109
111
|
if status.success?
|
|
110
|
-
|
|
111
|
-
# sidecar goes even if the tool never wrote $MERGED.
|
|
112
|
-
resolved(candidate)
|
|
112
|
+
verify(candidate, before)
|
|
113
113
|
else
|
|
114
114
|
unresolved(candidate, "exit #{status.exitstatus}#{detail(err)}")
|
|
115
115
|
end
|
|
@@ -118,6 +118,24 @@ module Rails
|
|
|
118
118
|
unresolved(candidate, first_line(e.message))
|
|
119
119
|
end
|
|
120
120
|
|
|
121
|
+
# An exit status is not proof: a tool can be denied every write and still
|
|
122
|
+
# exit 0, so the file itself has to show the resolution.
|
|
123
|
+
def verify(candidate, before)
|
|
124
|
+
after = merged_bytes(candidate)
|
|
125
|
+
if after.nil?
|
|
126
|
+
unresolved(candidate, "command exited 0 but $MERGED is missing")
|
|
127
|
+
elsif after == before
|
|
128
|
+
unresolved(candidate, "command exited 0 but wrote nothing")
|
|
129
|
+
else
|
|
130
|
+
resolved(candidate)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def merged_bytes(candidate)
|
|
135
|
+
path = abs(candidate.dest)
|
|
136
|
+
File.file?(path) ? File.binread(path) : nil
|
|
137
|
+
end
|
|
138
|
+
|
|
121
139
|
def resolved(candidate)
|
|
122
140
|
@shell.remove_file candidate.sidecar
|
|
123
141
|
@outcome.resolved << candidate.dest
|