gempilot 0.3.0 → 0.3.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: 72e379c74f157a19076c82e54b15e2d3581610d11089542ebd32b346971cfdb5
4
- data.tar.gz: e7b8f296a83f2c4b9e24f4bb3fd7757a034b1cca00fa461f4baa3e5c878b6f51
3
+ metadata.gz: 686fd797469009843d44529b0831634baf163cbd007a7d625f9605e340aa021a
4
+ data.tar.gz: f1144a8db6d9a54e8b4cfb146aae7d4d240c92f87a08cf4501589241934cfcbc
5
5
  SHA512:
6
- metadata.gz: bcb608a185c3808045cf017dcf0629d9b6034be93fd8457129728abed9cfd5191288893c214b3b38a46fdbda267676cdb7976ec2013c053a4b7796cd551a9c73
7
- data.tar.gz: 867f02bab2332352a5035db336f5b1073f5d4a4f08f49f75b76b84eb2fdd4d490dba4c9f36fed4b60d0a7844791509dce074d3f13d488b08000e16bc0c70bdc8
6
+ metadata.gz: 5ae0fdd86a89ec6855ae589c6c64fc28d97e71df117f81a35440ca9029333d962171daf080c8d0b24adafe2e83a4e41e2cb0bd989399d8df9cc79dfb2da6d9b0
7
+ data.tar.gz: abcea8380c013d64f9f22d8c82a59ffbb18a24ff5bcd0be017c0a640450cec51afc88fb676c0004f797f0a54404ca24e4acc8a2183cd836719dab0e117532580
@@ -0,0 +1,14 @@
1
+ ---
2
+ name: be-decisive-not-menus
3
+ description: "David wants decisive action — make the clear right move, don't present option-menus or re-ask"
4
+ metadata:
5
+ node_type: memory
6
+ type: feedback
7
+ originSessionId: 8ab20c6a-4c03-4f0a-9acd-e12099699ad8
8
+ ---
9
+
10
+ David wants decisive execution, not deliberation theater. He corrected me repeatedly (and bluntly) this session for presenting choices instead of acting, re-offering things already done, and asking permission for obvious next steps.
11
+
12
+ **Why:** He's the maintainer and knows his codebase; a menu of options he has to adjudicate is friction, not help. "and?" from him means "keep going / do the next obvious thing." "do the right thing" means "use your judgment and execute."
13
+
14
+ **How to apply:** When something's already done, say it's done — don't re-offer it. When there's a clear best move (green reviewed PR → merge it; verified-safe branch cleanup → do it), just make it and report the result. Reserve questions for genuine forks where his answer changes the outcome, and even then lead with a one-word-approvable recommendation instead of a survey. Don't narrate options you won't pursue. Confirm only truly hard-to-reverse or outward-facing actions — and expect him to push you past even those with "just do it."
@@ -0,0 +1,16 @@
1
+ ---
2
+ name: flagship-shared-rubocop-config-state
3
+ description: "Status of issue 79AE436E (shared RuboCop config + eject) — designed 2026-07-20, not yet built"
4
+ metadata:
5
+ node_type: memory
6
+ type: project
7
+ originSessionId: 8ab20c6a-4c03-4f0a-9acd-e12099699ad8
8
+ ---
9
+
10
+ The flagship **79AE436E** ("own shared RuboCop config via `inherit_gem` + eject hatch") is **designed but not implemented** as of 2026-07-20. Design spec committed at `docs/superpowers/specs/2026-07-20-shared-rubocop-config-eject-design.md` on branch `file-issue-own-shared-config` (that branch = the flagship WIP: issue-filing commit + design doc only; the 6 backlog fixes that were briefly on it shipped separately via PR #20 and are now in master).
11
+
12
+ Approved direction: `inherit_gem`; gempilot ships `config/rubocop/{base,minitest,rspec}.yml`; generated `.rubocop.yml` shrinks to near-zero (gem-name excludes become globs `*.gemspec`/`lib/*-*.rb`; drop `TargetRubyVersion` since RuboCop reads it from the gemspec's `required_ruby_version`); new `gempilot eject` inlines config + cuts the link; generated Gemfile pins gempilot.
13
+
14
+ Before/while building: **(1) OPEN decision** — CI-break mitigation policy; **pin** is the recommended default but David has NOT confirmed it. **(2) First step is a SPIKE** — verify RuboCop loads `plugins:` (rubocop-claude etc.) *through* an `inherit_gem` config; if not, the plugins list stays in the local file. See [[gempilot-generated-gem-rubocop-gate]].
15
+
16
+ Other open backlog item: **741FFD05** (reduce deps). The gemspec's 4 runtime deps (`command_kit`, `rake`, `warning`, `zeitwerk`) are all used — leave them; reduction is purely Gemfile dev deps. Recommended: drop `benchmark`/`observer`/`rbs`; keep `irb`/`repl_type_completor` (bin/console), `rdoc` (docs), `debug`. (`warning` is the one narrow runtime dep — only silences the VERSION-reload warnings in `Project#fetch_version`.)
data/CLAUDE.md CHANGED
@@ -10,7 +10,7 @@ A CLI tool for creating and managing Ruby gems, built on CommandKit.
10
10
  - `gempilot create` — Scaffold a new gem (templates in `data/templates/gem/`)
11
11
  - `gempilot new` — Generate a class, module, or command in an existing gem (templates in `data/templates/new/`)
12
12
  - `gempilot destroy` — Remove a class, module, or command from an existing gem
13
- - `gempilot bump` — Bump version in `version.rb` (patch default, or minor/major)
13
+ - `gempilot bump` — Bump version in `version.rb` (patch default, or minor/major/tiny/dev)
14
14
  - `gempilot release` — Thin proxy to `rake release`
15
15
  - `gempilot console` — Thin proxy to `bin/console`
16
16
 
@@ -21,6 +21,7 @@ A CLI tool for creating and managing Ruby gems, built on CommandKit.
21
21
  - Generator module (`lib/gempilot/cli/generator.rb`) provides template rendering via ERB
22
22
  - GemContext module (`lib/gempilot/cli/gem_context.rb`) shared by new, destroy, release, console
23
23
  - `GemConstant` value object (`lib/gempilot/gem_constant.rb`) owns constant→namespace/path resolution for `new`/`destroy`; constants are rooted at the gem module by construction
24
+ - `SegmentedVersion` value object (`lib/gempilot/segmented_version.rb`) owns version parsing and bump arithmetic; every bump moves to the smallest version of the requested shape greater than the current version (RubyGems ordering)
24
25
  - CommandKit::Commands::AutoLoad maps filenames in `commands/` to command names
25
26
 
26
27
  ### Testing
data/README.md CHANGED
@@ -82,8 +82,16 @@ Bump the version in `lib/<gem>/version.rb`.
82
82
  gempilot bump # patch (default)
83
83
  gempilot bump minor
84
84
  gempilot bump major
85
+ gempilot bump tiny # fourth integer for tiny follow-ups: 0.2.0 -> 0.2.0.1
86
+ gempilot bump dev # preview of the next patch: 0.2.0 -> 0.2.1.dev1
85
87
  ```
86
88
 
89
+ A `dev` version previews the next patch, so it always sorts ahead of the
90
+ current release (`0.2.0 < 0.2.1.dev1 < 0.2.1`), and a numeric bump finalizes
91
+ the cycle (`gempilot bump` from `0.2.1.dev3` gives `0.2.1`). A repo holding an
92
+ old-style dev version — one created *after* its base version shipped — needs a
93
+ one-time hand edit of `version.rb` past the last published version.
94
+
87
95
  ### `gempilot release`
88
96
 
89
97
  Delegates to `rake release`, which publishes the current version to all remotes
@@ -113,7 +121,7 @@ Generated gems include rake tasks for the full version lifecycle:
113
121
  | Task | Description |
114
122
  |------|-------------|
115
123
  | `rake version:current` | Display the current version |
116
- | `rake version:bump` | Increment the patch version |
124
+ | `rake version:bump` | Bump the version (patch default; major/minor/patch/tiny/dev) |
117
125
  | `rake version:commit` | Commit the version file change |
118
126
  | `rake version:tag` | Create a git tag for the version |
119
127
  | `rake version:untag` | Delete the version git tag |
@@ -0,0 +1,483 @@
1
+ # Forward-Moving Version Bumps Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Make every `version:bump` segment move strictly forward under RubyGems ordering, add a `tiny` fourth-integer segment, and mark dev GitHub releases as prereleases.
6
+
7
+ **Architecture:** A new `Gempilot::SegmentedVersion` value object (Data class) owns parsing of the three accepted version shapes (`M.m.p`, `M.m.p.N`, `M.m.p.devN`) and all bump arithmetic; `Project::Version#bump` becomes a thin delegation, keeping its `Data.define(:path, :value)` surface untouched. `GithubRelease#create` derives prerelease status from its tag string. Spec: `docs/superpowers/specs/2026-08-09-dev-version-bump-design.md`.
8
+
9
+ **Tech Stack:** Ruby (Zeitwerk autoloading), RSpec (`spec/`, library), Minitest (`test/`, CLI), RuboCop.
10
+
11
+ ## Global Constraints
12
+
13
+ - Semantics rule (from spec): every bump yields the smallest version of the requested shape strictly greater than the current version. Full transition table is in the spec and is reproduced in Task 1's test code — treat that table as normative.
14
+ - Double-quoted strings (`Style/StringLiterals: double_quotes`); NO `# frozen_string_literal:` comments (`EnforcedStyle: never`); trailing commas in multiline literals/arguments.
15
+ - Ruby ≥ 3.4 idioms are fine (`it` block parameter is used throughout the codebase); rdoc `##` doc blocks (never YARD tags), no blank line between `##` block and the definition.
16
+ - Zeitwerk: new file `lib/gempilot/segmented_version.rb` MUST define `Gempilot::SegmentedVersion` (the eager-load spec `spec/zeitwerk_spec.rb` enforces this).
17
+ - Verification commands: `bundle exec rspec <file>` for focused runs, `bundle exec rake default` (= `test` + `spec` + `rubocop`) for the full gate. Baseline before Task 1: 145 examples, 0 failures, 60 files no rubocop offenses.
18
+ - Commit messages: plain imperative, no conventional-commit prefixes (repo style: "Add issue", "Add dev version bump design spec").
19
+
20
+ ---
21
+
22
+ ### Task 1: SegmentedVersion arithmetic + Version#bump rewrite
23
+
24
+ **Files:**
25
+ - Create: `lib/gempilot/segmented_version.rb`
26
+ - Modify: `lib/gempilot/project/version.rb` (full rewrite shown below)
27
+ - Test: `spec/gempilot/project/version_spec.rb` (full rewrite shown below)
28
+ - Test: `spec/gempilot/version_task_spec.rb:35-38` (one expectation flip)
29
+
30
+ **Interfaces:**
31
+ - Consumes: `Gempilot::Project::Version` `Data.define(:path, :value)` (existing), `Data#with` (Ruby core).
32
+ - Produces: `Gempilot::SegmentedVersion.parse(string) → SegmentedVersion` (raises `ArgumentError` on unrecognized shapes), `SegmentedVersion#bump(segment) → SegmentedVersion` for `:major/:minor/:patch/:tiny/:dev` (symbol or string; raises `ArgumentError` otherwise), `SegmentedVersion#to_s → String`. `Version#bump(segment = :patch)` / `#next_version` keep their existing signatures — later tasks rely only on those.
33
+
34
+ - [ ] **Step 1: Rewrite the version spec with the normative transition table**
35
+
36
+ Replace the entire contents of `spec/gempilot/project/version_spec.rb` with:
37
+
38
+ ```ruby
39
+ require "spec_helper"
40
+
41
+ RSpec.describe Gempilot::Project::Version do
42
+ let(:path) { Pathname("lib/my_gem/version.rb") }
43
+
44
+ def version(value)
45
+ described_class.new(path: path, value: value)
46
+ end
47
+
48
+ describe "#tag" do
49
+ it "prepends v to the value" do
50
+ expect(version("1.2.3").tag).to eq("v1.2.3")
51
+ end
52
+ end
53
+
54
+ describe "#bump" do
55
+ transitions = {
56
+ "1.2.3" => { major: "2.0.0", minor: "1.3.0", patch: "1.2.4", tiny: "1.2.3.1", dev: "1.2.4.dev1" },
57
+ "1.2.3.1" => { major: "2.0.0", minor: "1.3.0", patch: "1.2.4", tiny: "1.2.3.2", dev: "1.2.4.dev1" },
58
+ "1.2.4.dev2" => { major: "2.0.0", minor: "1.3.0", patch: "1.2.4", tiny: "1.2.4.1", dev: "1.2.4.dev3" },
59
+ "1.3.0.dev2" => { major: "2.0.0", minor: "1.3.0", patch: "1.3.0", tiny: "1.3.0.1", dev: "1.3.0.dev3" },
60
+ "2.0.0.dev2" => { major: "2.0.0", minor: "2.0.0", patch: "2.0.0", tiny: "2.0.0.1", dev: "2.0.0.dev3" },
61
+ }
62
+
63
+ transitions.each do |from, bumps|
64
+ bumps.each do |segment, to|
65
+ it "bumps #{from} to #{to} for #{segment}" do
66
+ expect(version(from).bump(segment).value).to eq(to)
67
+ end
68
+ end
69
+ end
70
+
71
+ it "moves every transition strictly forward under Gem::Version ordering" do
72
+ transitions.each do |from, bumps|
73
+ bumps.each_value do |to|
74
+ expect(Gem::Version.new(to)).to be > Gem::Version.new(from)
75
+ end
76
+ end
77
+ end
78
+
79
+ it "bumps patch by default" do
80
+ expect(version("1.2.3").bump.value).to eq("1.2.4")
81
+ end
82
+
83
+ it "accepts segment as a string" do
84
+ expect(version("1.0.0").bump("minor").value).to eq("1.1.0")
85
+ end
86
+
87
+ it "raises for an unknown segment" do
88
+ expect { version("1.0.0").bump(:hotfix) }.to raise_error(ArgumentError, /unknown segment/i)
89
+ end
90
+
91
+ it "raises for a two-integer version" do
92
+ expect { version("1.2").bump }.to raise_error(ArgumentError, /cannot parse/i)
93
+ end
94
+
95
+ it "raises for a non-dev prerelease version" do
96
+ expect { version("1.2.3.beta1").bump }.to raise_error(ArgumentError, /cannot parse/i)
97
+ end
98
+ end
99
+
100
+ describe "#next_version" do
101
+ it "finalizes a dev version to its target" do
102
+ expect(version("0.0.4.dev3").next_version.value).to eq("0.0.4")
103
+ end
104
+
105
+ it "bumps the patch of a release version" do
106
+ expect(version("1.0.99").next_version.value).to eq("1.0.100")
107
+ end
108
+ end
109
+ end
110
+ ```
111
+
112
+ - [ ] **Step 2: Flip the stale expectation in the version task spec**
113
+
114
+ In `spec/gempilot/version_task_spec.rb`, the fixture version is `1.0.0.dev3`. Change the `version:bump` default example (currently expects `"1.0.1"`) to:
115
+
116
+ ```ruby
117
+ it "finalizes a dev version to its target by default" do
118
+ Rake::Task["version:bump"].invoke
119
+ expect(version_in_file).to eq("1.0.0")
120
+ end
121
+ ```
122
+
123
+ (The `version:bump[dev]` → `"1.0.0.dev4"` and both `version:release` examples are already correct under the new semantics — do not touch them.)
124
+
125
+ - [ ] **Step 3: Run the specs to verify they fail for the right reason**
126
+
127
+ Run: `bundle exec rspec spec/gempilot/project/version_spec.rb spec/gempilot/version_task_spec.rb --no-color`
128
+ Expected: FAILURES including `expected: "1.2.4.dev1" got: "1.2.3.dev1"` (dev goes backward today), `expected: "1.2.4" got: "1.2.5"` (patch overshoots a dev target), every `:tiny` cell erroring with `ArgumentError: Unknown segment :tiny` (old code rejects the segment), the `"1.2"` example failing with `NoMethodError` instead of `ArgumentError` (old code coerces then crashes on the missing patch integer), the `"1.2.3.beta1"` example failing with "expected ArgumentError but nothing was raised" (old code silently coerces to `1.2.4`), and `expected: "1.0.0" got: "1.0.1"` in the task spec. The `#tag`, string-segment, and unknown-segment examples still pass.
129
+
130
+ - [ ] **Step 4: Create the SegmentedVersion value object**
131
+
132
+ Create `lib/gempilot/segmented_version.rb`:
133
+
134
+ ```ruby
135
+ module Gempilot
136
+ ## Arithmetic over the three version shapes gempilot accepts: release
137
+ ## (+1.2.3+), tiny release (+1.2.3.1+), and dev prerelease (+1.2.3.dev1+).
138
+ ##
139
+ ## Every bump returns the smallest version of the requested shape that is
140
+ ## strictly greater than the current version under RubyGems ordering, so
141
+ ## bumping always moves a project forward: a dev bump previews the next
142
+ ## patch (+1.2.3+ to +1.2.4.dev1+) and numeric bumps finalize a dev cycle
143
+ ## (+1.2.4.dev2+ to +1.2.4+ for +:patch+).
144
+ SegmentedVersion = Data.define(:major, :minor, :patch, :tiny, :dev) do
145
+ def self.parse(string)
146
+ format = /\A(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)(?:\.(?<tiny>\d+)|\.dev(?<dev>\d+))?\z/
147
+ match = format.match(string)
148
+ raise ArgumentError, "cannot parse version: #{string.inspect}" unless match
149
+
150
+ new(**match.named_captures.to_h { |name, digits| [name.to_sym, digits&.to_i] })
151
+ end
152
+
153
+ def bump(segment)
154
+ case segment.to_sym
155
+ when :major then bump_major
156
+ when :minor then bump_minor
157
+ when :patch then bump_patch
158
+ when :tiny then bump_tiny
159
+ when :dev then bump_dev
160
+ else raise ArgumentError, "Unknown segment #{segment.inspect}. Use :major, :minor, :patch, :tiny, or :dev"
161
+ end
162
+ end
163
+
164
+ def to_s
165
+ [major, minor, patch, tiny, ("dev#{dev}" if dev)].compact.join(".")
166
+ end
167
+
168
+ private
169
+
170
+ def bump_major
171
+ return finalize if dev && minor.zero? && patch.zero?
172
+
173
+ with(major: major + 1, minor: 0, patch: 0, tiny: nil, dev: nil)
174
+ end
175
+
176
+ def bump_minor
177
+ return finalize if dev && patch.zero?
178
+
179
+ with(minor: minor + 1, patch: 0, tiny: nil, dev: nil)
180
+ end
181
+
182
+ def bump_patch
183
+ dev ? finalize : with(patch: patch + 1, tiny: nil)
184
+ end
185
+
186
+ def bump_tiny
187
+ with(tiny: (tiny || 0) + 1, dev: nil)
188
+ end
189
+
190
+ def bump_dev
191
+ dev ? with(dev: dev + 1) : with(patch: patch + 1, tiny: nil, dev: 1)
192
+ end
193
+
194
+ def finalize
195
+ with(tiny: nil, dev: nil)
196
+ end
197
+ end.freeze
198
+ end
199
+ ```
200
+
201
+ Notes for the implementer:
202
+ - The regex lives in a method-local (`format`), not a constant — a constant inside the `Data.define` block would trip `Lint/ConstantDefinitionInBlock`.
203
+ - `bump` dispatches to five one-branch private methods to stay under `Metrics/CyclomaticComplexity` (max 7).
204
+ - `finalize` drops the prerelease/tiny suffix — it encodes "the smallest version of a numeric shape greater than a dev version is the dev version's own target".
205
+
206
+ - [ ] **Step 5: Rewrite Version#bump as a delegation**
207
+
208
+ Replace the entire contents of `lib/gempilot/project/version.rb` with:
209
+
210
+ ```ruby
211
+ module Gempilot
212
+ class Project
213
+ ## The project's version file (+path+) and value. Bump arithmetic
214
+ ## delegates to SegmentedVersion, so every bump moves strictly forward
215
+ ## under RubyGems ordering.
216
+ Version = Data.define(:path, :value) do
217
+ def tag
218
+ "v#{value}"
219
+ end
220
+
221
+ def bump(segment = :patch)
222
+ with(value: SegmentedVersion.parse(value).bump(segment).to_s)
223
+ end
224
+
225
+ alias_method :next_version, :bump
226
+ end.freeze
227
+ end
228
+ end
229
+ ```
230
+
231
+ - [ ] **Step 6: Run the specs to verify they pass**
232
+
233
+ Run: `bundle exec rspec spec/gempilot/project/version_spec.rb spec/gempilot/version_task_spec.rb --no-color`
234
+ Expected: PASS, 0 failures (34 + task-spec examples). If `zeitwerk` cannot resolve the new constant, the filename does not match `Gempilot::SegmentedVersion` — fix the filename, not the loader.
235
+
236
+ - [ ] **Step 7: Run the full gate**
237
+
238
+ Run: `bundle exec rake default 2>&1 | tail -8`
239
+ Expected: minitest green, all RSpec examples green (count grows from 145), `no offenses detected`.
240
+
241
+ - [ ] **Step 8: Commit**
242
+
243
+ ```bash
244
+ git add lib/gempilot/segmented_version.rb lib/gempilot/project/version.rb spec/gempilot/project/version_spec.rb spec/gempilot/version_task_spec.rb
245
+ git commit -m "Make version bumps move strictly forward"
246
+ ```
247
+
248
+ ---
249
+
250
+ ### Task 2: `tiny` segment on the rake and CLI surfaces
251
+
252
+ **Files:**
253
+ - Modify: `lib/gempilot/version_task.rb:53` (desc string only)
254
+ - Modify: `lib/gempilot/cli/commands/bump.rb`
255
+ - Test: `spec/gempilot/version_task_spec.rb` (add one example)
256
+ - Test: `test/gempilot/cli/bump_command_test.rb` (add one test)
257
+
258
+ **Interfaces:**
259
+ - Consumes: `Version#bump` accepting `:tiny` (Task 1); rake `version:bump[segment]` forwards the segment string verbatim (existing behavior — no logic change needed there).
260
+ - Produces: `gempilot bump tiny` and `rake version:bump[tiny]` as documented user commands; CLI allowed-segment list `%w[patch minor major tiny dev]`.
261
+
262
+ - [ ] **Step 1: Add the failing rake-task example**
263
+
264
+ In `spec/gempilot/version_task_spec.rb`, inside `describe "version:bump"`, add:
265
+
266
+ ```ruby
267
+ it "bumps the tiny segment" do
268
+ Rake::Task["version:bump"].invoke("tiny")
269
+ expect(version_in_file).to eq("1.0.0.1")
270
+ end
271
+ ```
272
+
273
+ (Fixture is `1.0.0.dev3`; `tiny` finalizes the core and appends `.1`.)
274
+
275
+ - [ ] **Step 2: Add the failing CLI test**
276
+
277
+ In `test/gempilot/cli/bump_command_test.rb`, after `test_bump_dev_invokes_rake`, add:
278
+
279
+ ```ruby
280
+ def test_bump_tiny_invokes_rake
281
+ calls = recorded_system_calls { |cmd| cmd.main(["tiny"]) }
282
+
283
+ assert_includes calls, ["bundle", "exec", "rake", "version:bump[tiny]"]
284
+ end
285
+ ```
286
+
287
+ - [ ] **Step 3: Run both to verify the CLI test fails and the rake example passes**
288
+
289
+ Run: `bundle exec rspec spec/gempilot/version_task_spec.rb --no-color && bundle exec rake test 2>&1 | tail -4`
290
+ Expected: the rake-task example PASSES already (the task forwards any segment and Task 1 taught `Version#bump` about `:tiny`) — it is a pin, not a driver. The minitest CLI test FAILS: `Bump#validate_segment` rejects `tiny` and exits 1 before calling rake.
291
+
292
+ - [ ] **Step 4: Teach the CLI command the tiny segment**
293
+
294
+ In `lib/gempilot/cli/commands/bump.rb` apply these four edits:
295
+
296
+ ```ruby
297
+ description "Bump the gem version (patch by default, or minor/major/tiny/dev)"
298
+
299
+ examples [
300
+ "",
301
+ "patch",
302
+ "minor",
303
+ "major",
304
+ "tiny",
305
+ "dev",
306
+ ]
307
+
308
+ argument :segment, required: false,
309
+ desc: "Version segment to bump: patch (default), minor, major, tiny, or dev"
310
+ ```
311
+
312
+ and in `validate_segment`:
313
+
314
+ ```ruby
315
+ return segment if %w[patch minor major tiny dev].include?(segment)
316
+
317
+ puts colors.red("Unknown segment '#{segment}'. Use patch, minor, major, tiny, or dev.")
318
+ ```
319
+
320
+ - [ ] **Step 5: Update the rake task description**
321
+
322
+ In `lib/gempilot/version_task.rb`, `define_bump_task`:
323
+
324
+ ```ruby
325
+ desc "Bump version (patch default; segments: major, minor, patch, tiny, dev)"
326
+ ```
327
+
328
+ - [ ] **Step 6: Run the full gate**
329
+
330
+ Run: `bundle exec rake default 2>&1 | tail -8`
331
+ Expected: all green, no offenses. (`test_bump_fails_with_invalid_segment` uses `hotfix`, which stays invalid.)
332
+
333
+ - [ ] **Step 7: Commit**
334
+
335
+ ```bash
336
+ git add lib/gempilot/version_task.rb lib/gempilot/cli/commands/bump.rb spec/gempilot/version_task_spec.rb test/gempilot/cli/bump_command_test.rb
337
+ git commit -m "Add tiny segment to bump interfaces"
338
+ ```
339
+
340
+ ---
341
+
342
+ ### Task 3: Mark dev GitHub releases as prereleases
343
+
344
+ **Files:**
345
+ - Modify: `lib/gempilot/github_release.rb`
346
+ - Test: `spec/gempilot/github_release_spec.rb`
347
+
348
+ **Interfaces:**
349
+ - Consumes: `GithubRelease.new(tag)` with tag strings like `"v1.2.4.dev1"` (signature unchanged); `Gem::Version#prerelease?` (rubygems, always loaded).
350
+ - Produces: `gh release create` invoked with `--prerelease` when and only when the tag names a prerelease version.
351
+
352
+ - [ ] **Step 1: Add the failing specs**
353
+
354
+ In `spec/gempilot/github_release_spec.rb`, inside `describe "#create"`, add two contexts after the existing examples:
355
+
356
+ ```ruby
357
+ context "with a dev prerelease tag" do
358
+ let(:tag) { "v1.2.4.dev1" }
359
+
360
+ it "marks the release as a prerelease" do
361
+ release.create
362
+ args = ["gh", "release", "create", "--generate-notes", "--fail-on-no-commits", "--prerelease", tag]
363
+ expect(release).to have_received(:sh).with(*args)
364
+ end
365
+ end
366
+
367
+ context "with a tiny release tag" do
368
+ let(:tag) { "v1.2.3.1" }
369
+
370
+ it "does not mark the release as a prerelease" do
371
+ release.create
372
+ args = ["gh", "release", "create", "--generate-notes", "--fail-on-no-commits", tag]
373
+ expect(release).to have_received(:sh).with(*args)
374
+ end
375
+ end
376
+ ```
377
+
378
+ - [ ] **Step 2: Run to verify the dev context fails**
379
+
380
+ Run: `bundle exec rspec spec/gempilot/github_release_spec.rb --no-color`
381
+ Expected: FAIL — the dev-tag example receives no `--prerelease` argument; the tiny-tag example passes.
382
+
383
+ - [ ] **Step 3: Implement the prerelease flag**
384
+
385
+ In `lib/gempilot/github_release.rb`, replace `#create` and append a private section after `#list`:
386
+
387
+ ```ruby
388
+ def create
389
+ sh "gh", "release", "create",
390
+ "--generate-notes", "--fail-on-no-commits",
391
+ *prerelease_flag,
392
+ tag
393
+ end
394
+ ```
395
+
396
+ ```ruby
397
+ private
398
+
399
+ def prerelease_flag
400
+ Gem::Version.new(tag.delete_prefix("v")).prerelease? ? ["--prerelease"] : []
401
+ end
402
+ ```
403
+
404
+ Also extend the class doc block's first line to:
405
+
406
+ ```ruby
407
+ ## Manages GitHub releases for a version tag. Tags naming a prerelease
408
+ ## version (e.g. +v1.2.4.dev1+) are created as GitHub prereleases.
409
+ ```
410
+
411
+ - [ ] **Step 4: Run the spec, then the full gate**
412
+
413
+ Run: `bundle exec rspec spec/gempilot/github_release_spec.rb --no-color && bundle exec rake default 2>&1 | tail -8`
414
+ Expected: PASS everywhere, no offenses.
415
+
416
+ - [ ] **Step 5: Commit**
417
+
418
+ ```bash
419
+ git add lib/gempilot/github_release.rb spec/gempilot/github_release_spec.rb
420
+ git commit -m "Mark dev GitHub releases as prereleases"
421
+ ```
422
+
423
+ ---
424
+
425
+ ### Task 4: Documentation
426
+
427
+ **Files:**
428
+ - Modify: `README.md` (bump section ~line 77-85; version tasks table row ~line 116)
429
+ - Modify: `CLAUDE.md` (bump command bullet; architecture list)
430
+
431
+ **Interfaces:**
432
+ - Consumes: final semantics from Tasks 1-3.
433
+ - Produces: user-facing docs; no code.
434
+
435
+ - [ ] **Step 1: Update the README bump section**
436
+
437
+ Replace the `gempilot bump` code block and add one paragraph after it:
438
+
439
+ ````markdown
440
+ ```bash
441
+ gempilot bump # patch (default)
442
+ gempilot bump minor
443
+ gempilot bump major
444
+ gempilot bump tiny # fourth integer for tiny follow-ups: 0.2.0 -> 0.2.0.1
445
+ gempilot bump dev # preview of the next patch: 0.2.0 -> 0.2.1.dev1
446
+ ```
447
+
448
+ A `dev` version previews the next patch, so it always sorts ahead of the
449
+ current release (`0.2.0 < 0.2.1.dev1 < 0.2.1`), and a numeric bump finalizes
450
+ the cycle (`gempilot bump` from `0.2.1.dev3` gives `0.2.1`). A repo holding an
451
+ old-style dev version — one created *after* its base version shipped — needs a
452
+ one-time hand edit of `version.rb` past the last published version.
453
+ ````
454
+
455
+ - [ ] **Step 2: Update the README version tasks table row**
456
+
457
+ ```markdown
458
+ | `rake version:bump` | Bump the version (patch default; major/minor/patch/tiny/dev) |
459
+ ```
460
+
461
+ - [ ] **Step 3: Update CLAUDE.md**
462
+
463
+ Change the bump command bullet to:
464
+
465
+ ```markdown
466
+ - `gempilot bump` — Bump version in `version.rb` (patch default, or minor/major/tiny/dev)
467
+ ```
468
+
469
+ and add to the Architecture list, after the `GemConstant` bullet:
470
+
471
+ ```markdown
472
+ - `SegmentedVersion` value object (`lib/gempilot/segmented_version.rb`) owns version parsing and bump arithmetic; every bump moves to the smallest version of the requested shape greater than the current version (RubyGems ordering)
473
+ ```
474
+
475
+ - [ ] **Step 4: Full gate and commit**
476
+
477
+ Run: `bundle exec rake default 2>&1 | tail -8`
478
+ Expected: all green (docs only — this is a regression guard).
479
+
480
+ ```bash
481
+ git add README.md CLAUDE.md
482
+ git commit -m "Update docs for forward-moving version bumps"
483
+ ```
@@ -0,0 +1,163 @@
1
+ # Dev version bump semantics — design
2
+
3
+ **Issue:** `2360FFA4-9388-11F1-8474-FE6CB9572C2F` — "rake version:bump[dev] goes backward rather than forward"
4
+
5
+ **Date:** 2026-08-09
6
+
7
+ ## Problem
8
+
9
+ `Gempilot::Project::Version#bump(:dev)` appends `.dev1` to the *current* version
10
+ (`0.2.0` → `0.2.0.dev1`). RubyGems treats any version with a letter segment as a
11
+ **prerelease of the version it names** (`Gem::Version` rdoc: "Prereleases sort
12
+ between real releases (newest to oldest): 1.0 > 1.0.b1 > 1.0.a.2 > 0.9"), so
13
+ `0.2.0.dev1 < 0.2.0` — the bump moves the project *backward*. Two related gaps:
14
+
15
+ 1. `bump(:patch)` from `X.Y.Z.devN` produces `X.Y.(Z+1)`, overshooting the
16
+ `X.Y.Z` the dev cycle was working toward.
17
+ 2. There is no way to bump a fourth integer (`0.2.0` → `0.2.0.1`), the
18
+ rubygems-legal *non-prerelease* shape for tiny follow-up fixes on a released
19
+ version (the Rails security-release pattern, `6.0.3.1`).
20
+
21
+ ## Decisions
22
+
23
+ - **Dev versions preview the next patch.** `bump(:dev)` from a release version
24
+ yields the next patch's first dev prerelease (`0.2.0` → `0.2.1.dev1`); from a
25
+ dev version it increments the counter (`0.2.1.dev1` → `0.2.1.dev2`). This is
26
+ the only reading consistent with rubygems ordering, and matches ecosystem
27
+ practice (Rails `7.1.0.beta1` precedes `7.1.0`; Bundler's in-repo `.dev`).
28
+ - **One rule governs every bump: move to the smallest version of the requested
29
+ shape that is strictly greater than the current version.** Consequences:
30
+ numeric bumps from a dev version *finalize* the cycle (`0.2.1.dev3` +
31
+ `patch` → `0.2.1`, not `0.2.2`), and `minor`/`major` redirect it
32
+ (`0.2.1.dev3` + `minor` → `0.3.0`).
33
+ - **New `tiny` segment for the fourth integer.** `bump(:tiny)`: `0.2.0` →
34
+ `0.2.0.1` → `0.2.0.2`. Name chosen by elimination: semver's "build metadata"
35
+ is precedence-ignored (and the `Gem::Version` rdoc uses "build" for the third
36
+ integer); `micro` is Python's third digit; `patchlevel` is Ruby-historical but
37
+ long and confusable with `patch`. `tiny` reads as "smaller than patch", which
38
+ is where it sorts. (Old Rails used TINY for the third digit; accepted.)
39
+ - **No dev target argument** (`bump[dev,minor]` → `0.3.0.dev1`) — YAGNI. The
40
+ shape rule already finalizes hand-edited `M.m.0.devN` / `M.0.0.devN` states
41
+ correctly, so a targeted form can be layered on later without breaking
42
+ anything.
43
+ - **Dev GitHub releases are prereleases.** `release:github` passes
44
+ `--prerelease` to `gh release create` when the tag names a prerelease
45
+ version, so `v0.2.1.dev2` does not become the repo's "Latest" release.
46
+ - **Strict version parsing.** `Version#bump` recognizes exactly three forms —
47
+ `M.m.p`, `M.m.p.N`, `M.m.p.devN` — and raises `ArgumentError` for anything
48
+ else, instead of today's silent first-three-integers coercion.
49
+
50
+ ## Semantics
51
+
52
+ Accepted version forms: release `M.m.p`, tiny release `M.m.p.N`, dev prerelease
53
+ `M.m.p.devN` (parse: `/\A(\d+)\.(\d+)\.(\d+)(?:\.(\d+)|\.dev(\d+))?\z/`).
54
+
55
+ Full transition table (rows marked * arise only by hand-editing; the rule is
56
+ total over all three forms regardless of how the version got there):
57
+
58
+ | from ↓ bump → | `major` | `minor` | `patch` | `tiny` | `dev` |
59
+ | ------------- | ------- | ------- | ------- | --------- | ------------ |
60
+ | `1.2.3` | `2.0.0` | `1.3.0` | `1.2.4` | `1.2.3.1` | `1.2.4.dev1` |
61
+ | `1.2.3.1` | `2.0.0` | `1.3.0` | `1.2.4` | `1.2.3.2` | `1.2.4.dev1` |
62
+ | `1.2.4.dev2` | `2.0.0` | `1.3.0` | `1.2.4` | `1.2.4.1` | `1.2.4.dev3` |
63
+ | `1.3.0.dev2`* | `2.0.0` | `1.3.0` | `1.3.0` | `1.3.0.1` | `1.3.0.dev3` |
64
+ | `2.0.0.dev2`* | `2.0.0` | `2.0.0` | `2.0.0` | `2.0.0.1` | `2.0.0.dev3` |
65
+
66
+ All 25 transitions verified strictly increasing under `Gem::Version` (checked
67
+ 2026-08-09 against ruby's installed rubygems).
68
+
69
+ Branch logic per segment, where the numeric core is `M.m.p` and `dev?` means
70
+ the current version is a dev prerelease:
71
+
72
+ - `major`: `dev? && m == 0 && p == 0` → `M.0.0` (finalize); else `(M+1).0.0`
73
+ - `minor`: `dev? && p == 0` → `M.m.0` (finalize); else `M.(m+1).0`
74
+ - `patch`: `dev?` → `M.m.p` (finalize); else `M.m.(p+1)`
75
+ - `tiny`: `M.m.p.(N+1)` where `N` is the current fourth integer, else 0.
76
+ (From a dev version this yields `M.m.p.1` — it implicitly finalizes the
77
+ target and adds a tiny level; forward-moving, documented edge case.)
78
+ - `dev`: `dev?` → `M.m.p.dev(N+1)`; else `M.m.(p+1).dev1`
79
+
80
+ ## Components
81
+
82
+ ### `Gempilot::Project::Version` (modified — `lib/gempilot/project/version.rb`)
83
+
84
+ - Stays `Data.define(:path, :value)` — `write_version!`, `VersionTag`, and all
85
+ call sites are untouched. Parsing is internal to `bump`.
86
+ - `bump(segment)` accepts `:major, :minor, :patch, :tiny, :dev` (symbol or
87
+ string), implements the table above. Unknown segment → `ArgumentError`
88
+ naming the valid segments (as today, plus `tiny`). Unparseable value →
89
+ `ArgumentError` naming the value.
90
+ - `next_version` alias retained; its behavior follows the new `patch` default
91
+ (`0.0.4.dev3` → `0.0.4`, previously `0.0.5`).
92
+
93
+ ### `Gempilot::GithubRelease` (modified — `lib/gempilot/github_release.rb`)
94
+
95
+ - `#create` appends `--prerelease` when the tag (sans leading `v`) parses as a
96
+ prerelease per `Gem::Version#prerelease?`. Self-contained — the
97
+ `initialize(tag)` signature and all call sites are unchanged. `#destroy` /
98
+ `#list` untouched.
99
+
100
+ ### `Gempilot::VersionTask` (modified — `lib/gempilot/version_task.rb`)
101
+
102
+ - Behavior unchanged (it forwards the segment). Only the `version:bump` desc
103
+ string updates: "Bump version (patch default; segments: major, minor, patch,
104
+ tiny, dev)".
105
+
106
+ ### `Gempilot::CLI::Commands::Bump` (modified — `lib/gempilot/cli/commands/bump.rb`)
107
+
108
+ - Allowed segments become `%w[patch minor major tiny dev]`; error message,
109
+ argument desc, command description, and `examples` updated to match.
110
+
111
+ ## Testing (TDD — written before implementation)
112
+
113
+ **`spec/gempilot/project/version_spec.rb` (rewritten)**
114
+
115
+ - The full 5×5 transition table above, each cell an example.
116
+ - A monotonicity property spec: for every table cell,
117
+ `Gem::Version.new(new) > Gem::Version.new(old)` — the direct regression spec
118
+ for this issue.
119
+ - Segment as string; unknown segment `ArgumentError`; unparseable value
120
+ (`"1.2"`, `"1.2.3.beta1"`) `ArgumentError`; `next_version` follows patch
121
+ semantics (`0.0.4.dev3` → `0.0.4`).
122
+
123
+ **`spec/gempilot/version_task_spec.rb` (expectations updated)**
124
+
125
+ Fixture starts at `1.0.0.dev3`:
126
+ - `version:bump` (default patch) → `1.0.0` (finalize; was `1.0.1`).
127
+ - `version:bump[dev]` → `1.0.0.dev4` (unchanged).
128
+ - `version:bump[tiny]` → `1.0.0.1` (new example).
129
+ - `version:release[dev]` → commit "Bump version to 1.0.0.dev4", tag
130
+ `v1.0.0.dev4` (unchanged).
131
+
132
+ **`spec/gempilot/github_release_spec.rb` (extended)**
133
+
134
+ - `create` with `v1.2.4.dev1` includes `--prerelease`; with `v1.2.4` and
135
+ `v1.2.3.1` it does not.
136
+
137
+ **`test/gempilot/cli/bump_command_test.rb` (extended)**
138
+
139
+ - `tiny` accepted as a segment; unknown-segment error message lists it.
140
+
141
+ ## Docs
142
+
143
+ - `README.md` — `gempilot bump` section gains `tiny` and `dev` examples with a
144
+ one-line explanation of dev semantics; the rake tasks table row for
145
+ `version:bump` mentions the segments.
146
+ - `CLAUDE.md` — version lifecycle bullet: "(patch default, or
147
+ minor/major/tiny/dev)".
148
+
149
+ ## Migration
150
+
151
+ A repo sitting on an old-style dev version (e.g. `0.2.0.dev1` created *after*
152
+ `0.2.0` shipped) is already in the broken state this fixes; `bump(:patch)`
153
+ would finalize to the already-released `0.2.0` (and `version:tag` would then
154
+ fail on the existing tag). Recovery is a one-time hand edit of `version.rb`
155
+ past the last published version. Noted in the README dev-example line.
156
+
157
+ ## Out of scope
158
+
159
+ - Targeted dev cycles (`bump[dev,minor]`) — compatible later extension.
160
+ - Other prerelease markers (`pre`, `rc`, `beta`) — `dev` sorts before all of
161
+ them, so they remain addable later.
162
+ - RubyGems prerelease yanking; `data/templates/gem/Rakefile.erb` (no change
163
+ needed — it just instantiates `Gempilot::VersionTask`).
data/issues.rec CHANGED
@@ -110,8 +110,8 @@ Description: e.g.
110
110
  + Zeitwerk::Loader.for_gem.tap do |l|
111
111
  + l.setup
112
112
  + end
113
- +
114
- +
113
+ +
114
+ +
115
115
  Status: closed
116
116
 
117
117
  Id: 5618BB20-4325-11F1-BF5A-FE6CB9572C2F
@@ -212,3 +212,91 @@ Description: the github release related tasks are invoked as follows:
212
212
  +
213
213
  + Also, there is an issue that prevents github releasing from working at all via the old tasks. I get an error every time saying that "tags have already been pushed" or something along those lines. Ensure that is fixed as well
214
214
  Status: closed
215
+
216
+ Id: 986E0100-88F4-11F1-B718-FE6CB9572C2D
217
+ Updated: Sun, 26 Jul 2026 09:19:13 -0400
218
+ Title: Make rbs and the other gems GP installs mri only
219
+ Description: This tool is also used with jruby, and it fails irb install due to rbs
220
+ Status: open
221
+
222
+ Id: 3FFE7616-88F5-11F1-8D3B-FE6CB9572C2D
223
+ Updated: Sun, 26 Jul 2026 09:23:54 -0400
224
+ Title: Add betterleaks to gem setup
225
+ Description: Ensure betterleaks is integrated into the commit and build pipelines, via commit hooks, rake tasks, and/or whatever the idiomatic way is to integrate betterleaks into a rubyproject
226
+ Status: open
227
+
228
+ Id: F6C6EB04-8DA4-11F1-91EF-FA41C3164F3A
229
+ Updated: Sat, 01 Aug 2026 12:31:47 +0000
230
+ Title: release:rubygem_push only handles one gem per version
231
+ Description: rake release:rubygems can publish only one gem per version. A project that publishes several -- platform gems each carrying a compiled executable, say -- either publishes one of them and silently leaves the rest, or, once it replaces the push task with a pkg glob, tries to re-push versions already on rubygems and stops halfway through the release.
232
+ +
233
+ + MECHANISM
234
+ +
235
+ + gempilot, lib/gempilot/release_tasks.rb: ReleaseTasks#define_rubygems_release wires release:rubygems to [build, release:guard_clean, release:source_control_push, release:rubygem_push], and, as the module's own rdoc says, expects bundler/gem_tasks to have supplied build, release:guard_clean and release:rubygem_push.
236
+ +
237
+ + bundler 4.0.17, lib/bundler/gem_helper.rb: GemHelper#install_tasks defines
238
+ +
239
+ + task "release:rubygem_push" => "build" do
240
+ + rubygem_push(built_gem_path) if gem_push?
241
+ + end
242
+ +
243
+ + built_gem_path is the single .gem that #build_gem produced. One build, one gem, one push. Nothing in that chain can carry a second gem, so a project publishing more than one has to replace both build and release:rubygem_push itself.
244
+ +
245
+ + There is no glob in gempilot's own source to go looking for. The glob is what a downstream project ends up writing once it takes over the push, and it is wrong in a way gempilot's own version:bump triggers.
246
+ +
247
+ + REPRODUCE
248
+ +
249
+ + 1. A gem publishing more than one gem per version. aws-session-manager on gempilot 0.3.0 publishes five: x86_64-linux, aarch64-linux, x86_64-darwin and arm64-darwin, each carrying a compiled session-manager-plugin executable, plus the plain ruby gem.
250
+ + 2. It replaces build, and replaces the push with the obvious implementation:
251
+ + task(:rubygem_push) { PACKAGES.glob("*.gem").each { sh "gem", "push", it.to_s } }
252
+ + 3. rake build && rake release:rubygems -- 0.0.1 publishes.
253
+ + 4. rake version:bump && rake build -- pkg/ now holds both 0.0.1 and 0.0.2 gems. Nothing empties pkg/ between versions; bundler never has, and gempilot's version tasks do not either.
254
+ + 5. rake release:rubygems -- gem push rejects the 0.0.1 gems ("Repushing of gem versions is not allowed"), sh raises on the non-zero status, and the release aborts with part of 0.0.2 published and the rest not. Re-running repeats the failure, because the stale gems are still there.
255
+ +
256
+ + FIX APPLIED DOWNSTREAM
257
+ +
258
+ + aws-session-manager stopped globbing and took the list from the specifications its build packages, so that gem:build and the push share one source of truth:
259
+ +
260
+ + PACKAGED = [*BUILDS.map { it.package.to_s }, GENERIC_PACKAGE].freeze
261
+ +
262
+ + namespace :gem do
263
+ + task build: PACKAGED
264
+ + end
265
+ +
266
+ + namespace :release do
267
+ + task(:rubygem_push) { PACKAGED.each { sh "gem", "push", it } }
268
+ + end
269
+ +
270
+ + FIX SUGGESTED HERE
271
+ +
272
+ + gempilot should own release:rubygem_push rather than inherit bundler's, and push every gem in pkg belonging to the version being released. It cannot know how a project builds, but it knows the name and the version, which is enough:
273
+ +
274
+ + def define_rubygem_push(project)
275
+ + clear_task "release:rubygem_push"
276
+ + task "release:rubygem_push" => "build" do
277
+ + packages(project).each { sh "gem", "push", it.to_s }
278
+ + end
279
+ + end
280
+ +
281
+ + def packages(project)
282
+ + stem = "#{project.name}-#{project.version_value}"
283
+ + project.root.glob("pkg/{#{stem}.gem,#{stem}-*.gem}")
284
+ + end
285
+ +
286
+ + Project#name, #version_value and #root already exist in lib/gempilot/project.rb.
287
+ +
288
+ + Two details worth keeping:
289
+ +
290
+ + - The brace glob matches gempilot-0.3.0.gem and gempilot-0.3.0-x86_64-linux.gem, and does not match gempilot-0.3.10.gem. A bare "#{stem}*.gem" would match the latter and push the wrong version.
291
+ + - An empty result should abort. Otherwise a project whose build writes gems elsewhere gets a release that reports success having published nothing, which is worse than the bug being fixed.
292
+ +
293
+ + Single-gem projects are unaffected: pkg holds one gem for the current version, and it gets pushed, as today. Multi-gem projects stop having to reinvent the push.
294
+ +
295
+ + If gempilot would rather not own the task, then the ReleaseTasks rdoc should state the contract instead: a project that replaces build must also replace release:rubygem_push, and must scope the push to the version being released rather than globbing pkg.
296
+ Status: open
297
+
298
+ Id: 2360FFA4-9388-11F1-8474-FE6CB9572C2F
299
+ Updated: Sat, 08 Aug 2026 20:20:34 -0400
300
+ Title: rake version:bump[dev] goes backward rather than forward
301
+ Description: rubygems treats 0.2.0.dev as an *older* version than 0.2.0. The way gempilot handles dev version needs to be rethought. Additionally, how does a user trigger a bump on the fourth integer, e.g. 0.2.0.1?
302
+ Status: open
@@ -8,18 +8,19 @@ module Gempilot
8
8
  include GemContext
9
9
 
10
10
  usage "[options] [SEGMENT]"
11
- description "Bump the gem version (patch by default, or minor/major/dev)"
11
+ description "Bump the gem version (patch by default, or minor/major/tiny/dev)"
12
12
 
13
13
  examples [
14
14
  "",
15
15
  "patch",
16
16
  "minor",
17
17
  "major",
18
+ "tiny",
18
19
  "dev",
19
20
  ]
20
21
 
21
22
  argument :segment, required: false,
22
- desc: "Version segment to bump: patch (default), minor, major, or dev"
23
+ desc: "Version segment to bump: patch (default), minor, major, tiny, or dev"
23
24
 
24
25
  def run(segment = "patch")
25
26
  detect_gem_context
@@ -31,9 +32,9 @@ module Gempilot
31
32
 
32
33
  def validate_segment(segment)
33
34
  segment = segment.downcase
34
- return segment if %w[patch minor major dev].include?(segment)
35
+ return segment if %w[patch minor major tiny dev].include?(segment)
35
36
 
36
- puts colors.red("Unknown segment '#{segment}'. Use patch, minor, major, or dev.")
37
+ puts colors.red("Unknown segment '#{segment}'. Use patch, minor, major, tiny, or dev.")
37
38
  exit 1
38
39
  end
39
40
 
@@ -1,5 +1,6 @@
1
1
  module Gempilot
2
- ## Manages GitHub releases for a version tag.
2
+ ## Manages GitHub releases for a version tag. Tags naming a prerelease
3
+ ## version (e.g. +v1.2.4.dev1+) are created as GitHub prereleases.
3
4
  class GithubRelease
4
5
  include StrictShell
5
6
 
@@ -12,6 +13,7 @@ module Gempilot
12
13
  def create
13
14
  sh "gh", "release", "create",
14
15
  "--generate-notes", "--fail-on-no-commits",
16
+ *prerelease_flag,
15
17
  tag
16
18
  end
17
19
 
@@ -24,5 +26,11 @@ module Gempilot
24
26
  def list
25
27
  sh "gh", "release", "list"
26
28
  end
29
+
30
+ private
31
+
32
+ def prerelease_flag
33
+ Gem::Version.new(tag.delete_prefix("v")).prerelease? ? ["--prerelease"] : []
34
+ end
27
35
  end
28
36
  end
@@ -1,36 +1,15 @@
1
1
  module Gempilot
2
2
  class Project
3
+ ## The project's version file (+path+) and value. Bump arithmetic
4
+ ## delegates to SegmentedVersion, so every bump moves strictly forward
5
+ ## under RubyGems ordering.
3
6
  Version = Data.define(:path, :value) do
4
7
  def tag
5
8
  "v#{value}"
6
9
  end
7
10
 
8
11
  def bump(segment = :patch)
9
- case segment.to_sym
10
- when :major, :minor, :patch then bump_semver(segment)
11
- when :dev then bump_dev
12
- else raise ArgumentError, "Unknown segment #{segment.inspect}. Use :major, :minor, :patch, or :dev"
13
- end
14
- end
15
-
16
- private
17
-
18
- def bump_semver(segment)
19
- major, minor, patch = value.split(".").first(3).map(&:to_i)
20
- new_value = case segment.to_sym
21
- when :major then "#{major + 1}.0.0"
22
- when :minor then "#{major}.#{minor + 1}.0"
23
- when :patch then "#{major}.#{minor}.#{patch + 1}"
24
- end
25
- with(value: new_value)
26
- end
27
-
28
- def bump_dev
29
- if value.match?(/\.dev\d+\z/)
30
- with(value: value.sub(/\d+\z/) { it.to_i + 1 })
31
- else
32
- with(value: "#{value}.dev1")
33
- end
12
+ with(value: SegmentedVersion.parse(value).bump(segment).to_s)
34
13
  end
35
14
 
36
15
  alias_method :next_version, :bump
@@ -0,0 +1,64 @@
1
+ module Gempilot
2
+ ## Arithmetic over the three version shapes gempilot accepts: release
3
+ ## (+1.2.3+), tiny release (+1.2.3.1+), and dev prerelease (+1.2.3.dev1+).
4
+ ##
5
+ ## Every bump returns the smallest version of the requested shape that is
6
+ ## strictly greater than the current version under RubyGems ordering, so
7
+ ## bumping always moves a project forward: a dev bump previews the next
8
+ ## patch (+1.2.3+ to +1.2.4.dev1+) and numeric bumps finalize a dev cycle
9
+ ## (+1.2.4.dev2+ to +1.2.4+ for +:patch+).
10
+ SegmentedVersion = Data.define(:major, :minor, :patch, :tiny, :dev) do
11
+ def self.parse(string)
12
+ format = /\A(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)(?:\.(?<tiny>\d+)|\.dev(?<dev>\d+))?\z/
13
+ match = format.match(string)
14
+ raise ArgumentError, "cannot parse version: #{string.inspect}" unless match
15
+
16
+ new(**match.named_captures.to_h { |name, digits| [name.to_sym, digits&.to_i] })
17
+ end
18
+
19
+ def bump(segment)
20
+ case segment.to_sym
21
+ when :major then bump_major
22
+ when :minor then bump_minor
23
+ when :patch then bump_patch
24
+ when :tiny then bump_tiny
25
+ when :dev then bump_dev
26
+ else raise ArgumentError, "Unknown segment #{segment.inspect}. Use :major, :minor, :patch, :tiny, or :dev"
27
+ end
28
+ end
29
+
30
+ def to_s
31
+ [major, minor, patch, tiny, ("dev#{dev}" if dev)].compact.join(".")
32
+ end
33
+
34
+ private
35
+
36
+ def bump_major
37
+ return finalize if dev && minor.zero? && patch.zero?
38
+
39
+ with(major: major + 1, minor: 0, patch: 0, tiny: nil, dev: nil)
40
+ end
41
+
42
+ def bump_minor
43
+ return finalize if dev && patch.zero?
44
+
45
+ with(minor: minor + 1, patch: 0, tiny: nil, dev: nil)
46
+ end
47
+
48
+ def bump_patch
49
+ dev ? finalize : with(patch: patch + 1, tiny: nil)
50
+ end
51
+
52
+ def bump_tiny
53
+ with(tiny: (tiny || 0) + 1, dev: nil)
54
+ end
55
+
56
+ def bump_dev
57
+ dev ? with(dev: dev + 1) : with(patch: patch + 1, tiny: nil, dev: 1)
58
+ end
59
+
60
+ def finalize
61
+ with(tiny: nil, dev: nil)
62
+ end
63
+ end.freeze
64
+ end
@@ -1,3 +1,3 @@
1
1
  module Gempilot
2
- VERSION = "0.3.0".freeze
2
+ VERSION = "0.3.1".freeze
3
3
  end
@@ -50,7 +50,7 @@ module Gempilot
50
50
  end
51
51
 
52
52
  def define_bump_task(project)
53
- desc "Bump version (patch default, or rake version:bump[minor])"
53
+ desc "Bump version (patch default; segments: major, minor, patch, tiny, dev)"
54
54
  task :bump, [:segment] do |_t, args|
55
55
  segment = (args[:segment] || :patch).to_sym
56
56
  old_version = project.version
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gempilot
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Gillis
@@ -72,6 +72,8 @@ executables:
72
72
  extensions: []
73
73
  extra_rdoc_files: []
74
74
  files:
75
+ - ".claude/memory/be-decisive-not-menus.md"
76
+ - ".claude/memory/flagship-shared-rubocop-config-state.md"
75
77
  - ".claude/skills/using-command-kit/SKILL.md"
76
78
  - ".claude/skills/using-command-kit/cli-example.rb"
77
79
  - ".claude/skills/using-command-kit/generator-pattern.rb"
@@ -107,7 +109,9 @@ files:
107
109
  - data/templates/new/.keep
108
110
  - data/templates/new/command.rb.erb
109
111
  - docs/superpowers/plans/2026-07-20-release-task-hierarchy.md
112
+ - docs/superpowers/plans/2026-08-09-dev-version-bump.md
110
113
  - docs/superpowers/specs/2026-07-20-release-task-hierarchy-design.md
114
+ - docs/superpowers/specs/2026-08-09-dev-version-bump-design.md
111
115
  - exe/gempilot
112
116
  - issues.rec
113
117
  - lib/core_ext/string/inflection_methods.rb
@@ -130,6 +134,7 @@ files:
130
134
  - lib/gempilot/project.rb
131
135
  - lib/gempilot/project/version.rb
132
136
  - lib/gempilot/release_tasks.rb
137
+ - lib/gempilot/segmented_version.rb
133
138
  - lib/gempilot/strict_shell.rb
134
139
  - lib/gempilot/version.rb
135
140
  - lib/gempilot/version_tag.rb
@@ -156,7 +161,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
156
161
  - !ruby/object:Gem::Version
157
162
  version: '0'
158
163
  requirements: []
159
- rubygems_version: 4.0.16
164
+ rubygems_version: 4.0.18
160
165
  specification_version: 4
161
166
  summary: A toolkit for creating, managing, and releasing your own rubygems
162
167
  test_files: []