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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b2a834f37a237a640a01631dc04ab991791e87535b90cb5fc7e44f69e24dccd0
4
- data.tar.gz: 03dd055200f9407f86719da8a221207b1b0a5987e12657284399ce5d8fae2840
3
+ metadata.gz: 8d3970fe19124e7d291ca3ae41b4b97b3ca9b2fe4634d2f4806b58142a01ec87
4
+ data.tar.gz: 0d8b0cc84e29b59e917a1b7a1c1fb92d8b5a54560ab47294d906f56fb4bcaa3f
5
5
  SHA512:
6
- metadata.gz: cc5dcb6940b29b6bf182e704f9419d336616fe999dc214325697d3b6a9aa4673fca9b003ba4a621eb348ea0be34dc892f648a1e6882add7cf261b7710a29d597
7
- data.tar.gz: b810742750f5bd8a747a1c27fee799d80164bb306ad69ad4ff0445a8a6deefdde1171922058ee325d55b8df2a1b187a2e0e3fb2ab2ec41a204ccbce50bdca228
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.0...HEAD
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 --permission-mode acceptEdits $PROMPT
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 a promise: the sidecar is deleted on the strength of it, whether or not `$MERGED` was written. 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.
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 means resolved and the sidecar is deleted; any other exit
44
- # leaves the file, the sidecar, and the lock untouched. prompt: names
45
- # your own ERB template, relative to the app root, replacing the
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
- On its own it delivers to sidecars; with --merge, git merges first and the
35
- command takes the rest. With no command configured it stops before syncing
36
- anything. A sidecar you edited yourself is never handed off.
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, change nothing and exit with a
45
- non-zero status.
44
+ - If you are not confident the result is correct, leave MERGED untouched; an
45
+ unchanged MERGED is treated as unresolved.
46
46
 
47
- Exit 0 only when MERGED holds the reconciled file.
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. Nothing here
13
- # writes the lock: the sidecar's absence is the whole resolution signal.
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
- # Exit 0 is the tool's assertion that the file is resolved, so the
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
@@ -1,5 +1,5 @@
1
1
  module Rails
2
2
  module Hyperdrive
3
- VERSION = "0.9.0"
3
+ VERSION = "0.9.1"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-hyperdrive
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.9.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bakaface