gempilot 0.2.2 → 0.2.4

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.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +15 -1
  3. data/CLAUDE.md +2 -9
  4. data/README.md +8 -4
  5. data/data/templates/gem/dotfiles/rubocop.yml.erb +1 -12
  6. data/docs/superpowers/plans/2026-07-20-release-task-hierarchy.md +583 -0
  7. data/docs/superpowers/specs/2026-07-20-release-task-hierarchy-design.md +229 -0
  8. data/issues.rec +74 -3
  9. data/lib/gempilot/cli/commands/console.rb +2 -3
  10. data/lib/gempilot/cli/commands/create.rb +6 -3
  11. data/lib/gempilot/cli/commands/new.rb +2 -3
  12. data/lib/gempilot/cli/commands/release.rb +2 -3
  13. data/lib/gempilot/cli/generator.rb +2 -1
  14. data/lib/gempilot/github_release.rb +0 -2
  15. data/lib/gempilot/origin.rb +52 -0
  16. data/lib/gempilot/project.rb +25 -5
  17. data/lib/gempilot/release_tasks.rb +73 -0
  18. data/lib/gempilot/version.rb +1 -1
  19. data/lib/gempilot/version_task.rb +3 -16
  20. data/vendor/vendored.gemv +0 -0
  21. metadata +8 -14
  22. data/docs/command_kit_comparison.md +0 -249
  23. data/docs/command_kit_reference.md +0 -517
  24. data/docs/plans/2026-02-18-gempilot-add-command.md +0 -718
  25. data/docs/superpowers/plans/2026-04-01-rubocop-new-config.md +0 -838
  26. data/docs/superpowers/plans/2026-04-06-dogfood-inflectable.md +0 -659
  27. data/docs/superpowers/plans/2026-04-06-inflection-tests-and-erb-rename.md +0 -166
  28. data/docs/superpowers/plans/2026-04-06-integrate-version-tools.md +0 -162
  29. data/docs/superpowers/plans/2026-04-06-new-readme.md +0 -185
  30. data/docs/superpowers/plans/2026-06-09-address-review-critiques.md +0 -679
  31. data/docs/version-management-redesign.md +0 -44
  32. data/notes.md +0 -31
@@ -0,0 +1,583 @@
1
+ # Release Task Hierarchy 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:** Move GitHub publishing out of the `version:github:*` namespace into the idiomatic `release` / `unrelease` namespace, and fix GitHub releasing (bundler's `already_tagged?` guard silently skips the git push).
6
+
7
+ **Architecture:** A new `Gempilot::Origin` object does an idempotent commit+tag push and replaces bundler's buggy `release:source_control_push`. RubyGems publishing reuses bundler's own `build` / `release:guard_clean` / `release:rubygem_push` leaf tasks via prerequisites; GitHub publishing uses the existing (slimmed) `GithubRelease`. All task wiring lives in a new `Gempilot::ReleaseTasks` module mixed into `VersionTask`. Bare `rake release` is a pure prerequisite composite over both remotes — no arguments, no dispatcher code.
8
+
9
+ **Tech Stack:** Ruby, Rake (`Rake::TaskLib`, `bundler/gem_tasks`), Zeitwerk autoloading, RSpec (unit specs) + Minitest (generator tests), RuboCop (`rubocop-claude`/`-rspec`/`-performance`/`-rake`/`-design`).
10
+
11
+ **Spec:** `docs/superpowers/specs/2026-07-20-release-task-hierarchy-design.md`
12
+
13
+ ---
14
+
15
+ ## Preconditions
16
+
17
+ - Branch `release-task-hierarchy` is checked out.
18
+ - `bundle install` works and the baseline is green:
19
+ - `bundle exec rake spec` → `106 examples, 0 failures`
20
+ - `bundle exec rake test` → `108 runs, 0 failures`
21
+ - `bundle exec rubocop` → `no offenses detected`
22
+ - If `bundle install` fails with `Bundler::Plugin::Index::SourceConflict (Source(s) 'vault' ...)`, align the local plugin index to the installed vault plugin version, then reinstall:
23
+ ```bash
24
+ ruby -e 'f=".bundle/plugin/index"; File.write(f, File.read(f).gsub("bundler-source-vault-0.1.2","bundler-source-vault-0.1.5"))'
25
+ bundle install
26
+ ```
27
+ (`.bundle/` is not tracked by git — this is a local-environment fix only.)
28
+
29
+ ## Final target — task interface
30
+
31
+ | Task | Behavior |
32
+ | --- | --- |
33
+ | `rake release` | Publish current version to all remotes (RubyGems + GitHub) |
34
+ | `rake release:rubygems` | Build + push the gem to RubyGems |
35
+ | `rake release:github` | Push commit + tag, then create the GitHub release |
36
+ | `rake release:list:github` | List GitHub releases |
37
+ | `rake unrelease` | Delete the release from all remotes that support it (GitHub) |
38
+ | `rake unrelease:github` | Delete the GitHub release and remote tag |
39
+
40
+ `version:github:release/unrelease/list` are removed. `version:*` lifecycle tasks and the `version:release`/`version:unrelease` composites are unchanged.
41
+
42
+ ---
43
+
44
+ ## Task 1: `Gempilot::Origin` (idempotent commit + tag push)
45
+
46
+ **Files:**
47
+ - Create: `lib/gempilot/origin.rb`
48
+ - Test: `spec/gempilot/origin_spec.rb`
49
+
50
+ This is the bug fix: an always-push, no-guard replacement for bundler's `release:source_control_push`. Mirrors bundler's `git_push` (push the branch ref, then the tag ref, to the branch's configured remote — defaulting to `origin`).
51
+
52
+ - [ ] **Step 1: Write the failing spec**
53
+
54
+ Create `spec/gempilot/origin_spec.rb`:
55
+
56
+ ```ruby
57
+ require "spec_helper"
58
+
59
+ RSpec.describe Gempilot::Origin do
60
+ subject(:origin) { described_class.new("v1.2.3") }
61
+
62
+ around do |example|
63
+ Dir.mktmpdir("origin_spec") { |dir| Dir.chdir(dir) { example.run } }
64
+ end
65
+
66
+ before do
67
+ system("git", "init", "--quiet", "-b", "main", ".")
68
+ system("git", "config", "user.email", "test@test.com")
69
+ system("git", "config", "user.name", "Test")
70
+ system("git", "commit", "--allow-empty", "--quiet", "-m", "init")
71
+ end
72
+
73
+ describe "#push" do
74
+ before { allow(origin).to receive(:sh) }
75
+
76
+ it "pushes the current branch, then the tag, to the resolved remote", :aggregate_failures do
77
+ origin.push
78
+ expect(origin).to have_received(:sh).with("git", "push", "origin", "refs/heads/main").ordered
79
+ expect(origin).to have_received(:sh).with("git", "push", "origin", "refs/tags/v1.2.3").ordered
80
+ end
81
+ end
82
+
83
+ describe "#push against a real remote" do
84
+ before do
85
+ system("git", "clone", "--quiet", "--bare", ".", "origin.git")
86
+ system("git", "remote", "add", "origin", "origin.git")
87
+ system("git", "tag", "v1.2.3")
88
+ end
89
+
90
+ it "lands the tag on the remote and stays idempotent on re-run", :aggregate_failures do
91
+ origin.push
92
+ expect(`git --git-dir=origin.git tag`.strip).to eq("v1.2.3")
93
+ expect { described_class.new("v1.2.3").push }.not_to raise_error
94
+ end
95
+ end
96
+ end
97
+ ```
98
+
99
+ - [ ] **Step 2: Run the spec, verify it fails**
100
+
101
+ Run: `bundle exec rspec spec/gempilot/origin_spec.rb`
102
+ Expected: FAIL — `uninitialized constant Gempilot::Origin` (or `NameError`).
103
+
104
+ - [ ] **Step 3: Implement `Gempilot::Origin`**
105
+
106
+ Create `lib/gempilot/origin.rb`:
107
+
108
+ ```ruby
109
+ require "open3"
110
+
111
+ module Gempilot
112
+ ## Pushes the current branch and a release tag to the branch's git remote.
113
+ ## Backs the +release:source_control_push+ task. Idempotent: pushing an
114
+ ## already-pushed branch or tag is a no-op, so re-running a release never
115
+ ## fails on an existing tag (unlike bundler's +already_tagged?+ guard, which
116
+ ## skips the push entirely once the tag exists locally).
117
+ class Origin
118
+ include StrictShell
119
+
120
+ attr_reader :tag
121
+
122
+ def initialize(tag)
123
+ @tag = tag
124
+ end
125
+
126
+ def push
127
+ sh "git", "push", remote, "refs/heads/#{branch}"
128
+ sh "git", "push", remote, "refs/tags/#{tag}"
129
+ end
130
+
131
+ private
132
+
133
+ def branch
134
+ @branch ||= capture("git", "rev-parse", "--abbrev-ref", "HEAD")
135
+ end
136
+
137
+ def remote
138
+ @remote ||= configured_remote || "origin"
139
+ end
140
+
141
+ def configured_remote
142
+ out, status = Open3.capture2("git", "config", "--get", "branch.#{branch}.remote")
143
+ out.strip if status.success?
144
+ end
145
+
146
+ def capture(*args)
147
+ out, status = Open3.capture2(*args)
148
+ raise "Command #{args.join(" ").inspect} failed" unless status.success?
149
+
150
+ out.strip
151
+ end
152
+ end
153
+ end
154
+ ```
155
+
156
+ - [ ] **Step 4: Run the spec, verify it passes**
157
+
158
+ Run: `bundle exec rspec spec/gempilot/origin_spec.rb`
159
+ Expected: PASS — `3 examples, 0 failures` (2 `#push` examples across the two describe blocks). RuboCop next.
160
+
161
+ - [ ] **Step 5: Lint the new files**
162
+
163
+ Run: `bundle exec rubocop lib/gempilot/origin.rb spec/gempilot/origin_spec.rb`
164
+ Expected: `no offenses detected`. If any offense appears, fix it and re-run.
165
+
166
+ - [ ] **Step 6: Commit**
167
+
168
+ ```bash
169
+ git add lib/gempilot/origin.rb spec/gempilot/origin_spec.rb
170
+ git commit -m "Add Gempilot::Origin for idempotent commit+tag push"
171
+ ```
172
+
173
+ ---
174
+
175
+ ## Task 2: Slim `GithubRelease#create`
176
+
177
+ **Files:**
178
+ - Modify: `lib/gempilot/github_release.rb`
179
+ - Test: `spec/gempilot/github_release_spec.rb`
180
+
181
+ Pushing is now `Origin`'s job (the `release:source_control_push` prerequisite). `GithubRelease#create` should only call `gh release create`.
182
+
183
+ - [ ] **Step 1: Update the spec to drop the git-push expectations**
184
+
185
+ In `spec/gempilot/github_release_spec.rb`, replace the entire `describe "#create"` block:
186
+
187
+ ```ruby
188
+ describe "#create" do
189
+ it "creates a release with generated notes" do
190
+ release.create
191
+ args = ["gh", "release", "create", "--generate-notes", "--fail-on-no-commits", tag]
192
+ expect(release).to have_received(:sh).with(*args)
193
+ end
194
+ end
195
+ ```
196
+
197
+ (Leave the `#destroy` and `#list` describe blocks unchanged.)
198
+
199
+ - [ ] **Step 2: Run the spec, verify `#create` now fails**
200
+
201
+ Run: `bundle exec rspec spec/gempilot/github_release_spec.rb`
202
+ Expected: The `#create` example passes already (it only asserts the `gh` call), but confirm the whole file is green. If green, the current `create` still issues `git push` calls harmlessly. To make the test *drive* the change, also assert no git push:
203
+
204
+ Add this example inside `describe "#create"`:
205
+
206
+ ```ruby
207
+ it "does not push git refs itself" do
208
+ release.create
209
+ expect(release).not_to have_received(:sh).with("git", any_args)
210
+ end
211
+ ```
212
+
213
+ Run again: `bundle exec rspec spec/gempilot/github_release_spec.rb`
214
+ Expected: FAIL — `does not push git refs itself` fails because `create` still runs `git push`.
215
+
216
+ - [ ] **Step 3: Remove the git push from `create`**
217
+
218
+ In `lib/gempilot/github_release.rb`, change `create` from:
219
+
220
+ ```ruby
221
+ def create
222
+ sh "git", "push"
223
+ sh "git", "push", "--tags"
224
+ sh "gh", "release", "create",
225
+ "--generate-notes", "--fail-on-no-commits",
226
+ tag
227
+ end
228
+ ```
229
+
230
+ to:
231
+
232
+ ```ruby
233
+ def create
234
+ sh "gh", "release", "create",
235
+ "--generate-notes", "--fail-on-no-commits",
236
+ tag
237
+ end
238
+ ```
239
+
240
+ - [ ] **Step 4: Run the spec, verify it passes**
241
+
242
+ Run: `bundle exec rspec spec/gempilot/github_release_spec.rb`
243
+ Expected: PASS — all examples green.
244
+
245
+ - [ ] **Step 5: Lint**
246
+
247
+ Run: `bundle exec rubocop lib/gempilot/github_release.rb spec/gempilot/github_release_spec.rb`
248
+ Expected: `no offenses detected`.
249
+
250
+ - [ ] **Step 6: Commit**
251
+
252
+ ```bash
253
+ git add lib/gempilot/github_release.rb spec/gempilot/github_release_spec.rb
254
+ git commit -m "Slim GithubRelease#create: push is now Origin's job"
255
+ ```
256
+
257
+ ---
258
+
259
+ ## Task 3: `ReleaseTasks` module + rewire `VersionTask`
260
+
261
+ **Files:**
262
+ - Create: `lib/gempilot/release_tasks.rb`
263
+ - Modify: `lib/gempilot/version_task.rb`
264
+ - Test: `spec/gempilot/version_task_spec.rb`
265
+
266
+ The new `release`/`unrelease` task tree, extracted into a module (keeps `VersionTask` under `Metrics/ClassLength`) and `include`d into `VersionTask`.
267
+
268
+ - [ ] **Step 1: Write the failing spec additions**
269
+
270
+ In `spec/gempilot/version_task_spec.rb`, add these two `describe` blocks inside the top-level `RSpec.describe Gempilot::VersionTask do ... end` (e.g. after the existing `describe "version:release"` block, before the final `end`):
271
+
272
+ ```ruby
273
+ describe "release task hierarchy" do
274
+ it "defines the release and unrelease tasks", :aggregate_failures do
275
+ %w[release release:rubygems release:github release:list:github unrelease unrelease:github].each do |name|
276
+ expect(Rake::Task).to be_task_defined(name)
277
+ end
278
+ end
279
+
280
+ it "removes the old version:github tasks", :aggregate_failures do
281
+ %w[version:github:release version:github:unrelease version:github:list].each do |name|
282
+ expect(Rake::Task).not_to be_task_defined(name)
283
+ end
284
+ end
285
+
286
+ it "composes release from the per-remote tasks" do
287
+ expect(Rake::Task["release"].prerequisites).to eq(%w[release:rubygems release:github])
288
+ end
289
+
290
+ it "builds release:rubygems from bundler's own tasks" do
291
+ chain = %w[build release:guard_clean release:source_control_push release:rubygem_push]
292
+ expect(Rake::Task["release:rubygems"].prerequisites).to eq(chain)
293
+ end
294
+
295
+ it "composes unrelease from the github task" do
296
+ expect(Rake::Task["unrelease"].prerequisites).to eq(%w[unrelease:github])
297
+ end
298
+ end
299
+
300
+ describe "release task behavior" do
301
+ let(:origin) { instance_double(Gempilot::Origin, push: nil) }
302
+ let(:github) { instance_double(Gempilot::GithubRelease, create: nil, destroy: nil, list: nil) }
303
+
304
+ before do
305
+ allow(Gempilot::Origin).to receive(:new).and_return(origin)
306
+ allow(Gempilot::GithubRelease).to receive(:new).and_return(github)
307
+ end
308
+
309
+ it "release:source_control_push pushes via Origin" do
310
+ Rake::Task["release:source_control_push"].invoke
311
+ expect(origin).to have_received(:push)
312
+ end
313
+
314
+ it "release:github pushes, then creates the release", :aggregate_failures do
315
+ Rake::Task["release:github"].invoke
316
+ expect(origin).to have_received(:push)
317
+ expect(github).to have_received(:create)
318
+ end
319
+
320
+ it "release:list:github lists releases" do
321
+ Rake::Task["release:list:github"].invoke
322
+ expect(github).to have_received(:list)
323
+ end
324
+
325
+ it "unrelease:github destroys the release" do
326
+ Rake::Task["unrelease:github"].invoke
327
+ expect(github).to have_received(:destroy)
328
+ end
329
+ end
330
+ ```
331
+
332
+ - [ ] **Step 2: Run the spec, verify it fails**
333
+
334
+ Run: `bundle exec rspec spec/gempilot/version_task_spec.rb`
335
+ Expected: FAIL — `release`/`release:github`/etc. are not defined; several examples error with "Don't know how to build task 'release:github'" / `be_task_defined` returns false.
336
+
337
+ - [ ] **Step 3: Create the `ReleaseTasks` module**
338
+
339
+ Create `lib/gempilot/release_tasks.rb`:
340
+
341
+ ```ruby
342
+ module Gempilot
343
+ ## Rake task definitions for publishing a release to RubyGems and GitHub.
344
+ ## Mixed into VersionTask. Assumes +bundler/gem_tasks+ has been required so the
345
+ ## +build+, +release:guard_clean+, and +release:rubygem_push+ tasks exist (the
346
+ ## generated Rakefile guarantees this). Fixes GitHub releasing by replacing
347
+ ## bundler's +already_tagged?+-guarded +release:source_control_push+ with an
348
+ ## idempotent push.
349
+ module ReleaseTasks
350
+ private
351
+
352
+ def define_release_tasks(project)
353
+ override_source_control_push(project)
354
+ define_release_namespace(project)
355
+ define_root_release_task
356
+ define_unrelease_tasks(project)
357
+ end
358
+
359
+ def override_source_control_push(project)
360
+ clear_task "release:source_control_push"
361
+ task("release:source_control_push") { Origin.new(project.version_tag).push }
362
+ end
363
+
364
+ def define_release_namespace(project)
365
+ namespace :release do
366
+ define_rubygems_release
367
+ define_github_release(project)
368
+ define_release_list(project)
369
+ end
370
+ end
371
+
372
+ def define_rubygems_release
373
+ desc "Release the current version to RubyGems"
374
+ task rubygems: %w[build release:guard_clean release:source_control_push release:rubygem_push]
375
+ end
376
+
377
+ def define_github_release(project)
378
+ desc "Create a GitHub release for the current version"
379
+ task github: "release:source_control_push" do
380
+ GithubRelease.new(project.version_tag).create
381
+ end
382
+ end
383
+
384
+ def define_release_list(project)
385
+ namespace :list do
386
+ desc "List GitHub releases"
387
+ task(:github) { GithubRelease.new(project.version_tag).list }
388
+ end
389
+ end
390
+
391
+ def define_root_release_task
392
+ clear_task "release"
393
+ desc "Release the current version to all remotes"
394
+ task release: %w[release:rubygems release:github]
395
+ end
396
+
397
+ def define_unrelease_tasks(project)
398
+ desc "Delete the current release from all remotes that support it"
399
+ task unrelease: %w[unrelease:github]
400
+ define_unrelease_namespace(project)
401
+ end
402
+
403
+ def define_unrelease_namespace(project)
404
+ namespace :unrelease do
405
+ desc "Delete the GitHub release for the current version"
406
+ task(:github) { GithubRelease.new(project.version_tag).destroy }
407
+ end
408
+ end
409
+
410
+ def clear_task(name)
411
+ Rake::Task[name].clear if Rake::Task.task_defined?(name)
412
+ end
413
+ end
414
+ end
415
+ ```
416
+
417
+ - [ ] **Step 4: Rewire `VersionTask` to use the module**
418
+
419
+ In `lib/gempilot/version_task.rb`:
420
+
421
+ First, add the include at the top of the class body (right after `class VersionTask < Rake::TaskLib`):
422
+
423
+ ```ruby
424
+ class VersionTask < Rake::TaskLib
425
+ include ReleaseTasks
426
+
427
+ attr_reader :project
428
+ ```
429
+
430
+ Then change `define_tasks` from:
431
+
432
+ ```ruby
433
+ def define_tasks
434
+ define_version_tasks
435
+ define_version_composite_tasks
436
+ define_github_tasks
437
+ end
438
+ ```
439
+
440
+ to:
441
+
442
+ ```ruby
443
+ def define_tasks
444
+ define_version_tasks
445
+ define_version_composite_tasks
446
+ define_release_tasks(@project)
447
+ end
448
+ ```
449
+
450
+ Then delete the entire `define_github_tasks` method (the `namespace "version:github" do ... end` method) — from `def define_github_tasks` through its closing `end`.
451
+
452
+ - [ ] **Step 5: Run the version_task spec, verify it passes**
453
+
454
+ Run: `bundle exec rspec spec/gempilot/version_task_spec.rb`
455
+ Expected: PASS — all examples green (existing `version:bump`/`version:release` plus the new hierarchy/behavior examples).
456
+
457
+ - [ ] **Step 6: Run the full suite + lint**
458
+
459
+ Run: `bundle exec rake spec && bundle exec rake test && bundle exec rubocop`
460
+ Expected: `rake spec` green, `rake test` green, RuboCop `no offenses detected`. In particular confirm `lib/gempilot/version_task.rb` reports no `Metrics/ClassLength` offense.
461
+
462
+ - [ ] **Step 7: Sanity-check the task tree renders and old tasks are gone**
463
+
464
+ Run (from the repo root — gempilot dogfoods its own `VersionTask`):
465
+ ```bash
466
+ bundle exec rake -T release ; bundle exec rake -T unrelease ; bundle exec rake -AT | grep -c 'version:github' || true
467
+ ```
468
+ Expected: `release`, `release:rubygems`, `release:github`, `release:list:github`, `unrelease`, `unrelease:github` appear; the `version:github` grep count is `0`.
469
+
470
+ - [ ] **Step 8: Commit**
471
+
472
+ ```bash
473
+ git add lib/gempilot/release_tasks.rb lib/gempilot/version_task.rb spec/gempilot/version_task_spec.rb
474
+ git commit -m "Move GitHub publishing into idiomatic release/unrelease namespace"
475
+ ```
476
+
477
+ ---
478
+
479
+ ## Task 4: Update docs
480
+
481
+ **Files:**
482
+ - Modify: `README.md`
483
+ - Modify: `CLAUDE.md`
484
+
485
+ No tests — documentation only. Make the exact edits below.
486
+
487
+ - [ ] **Step 1: Update the README `gempilot release` note**
488
+
489
+ In `README.md`, replace:
490
+
491
+ ```markdown
492
+ Delegates to `rake release` to build and push the gem.
493
+ ```
494
+
495
+ with:
496
+
497
+ ```markdown
498
+ Delegates to `rake release`, which publishes the current version to all remotes
499
+ (RubyGems + GitHub).
500
+ ```
501
+
502
+ - [ ] **Step 2: Update the README task table**
503
+
504
+ In `README.md`, replace these three rows:
505
+
506
+ ```markdown
507
+ | `rake version:github:release` | Push and create a GitHub release |
508
+ | `rake version:github:unrelease` | Delete the GitHub release |
509
+ | `rake version:github:list` | List GitHub releases |
510
+ ```
511
+
512
+ with:
513
+
514
+ ```markdown
515
+ | `rake release` | Publish the current version to all remotes (RubyGems + GitHub) |
516
+ | `rake release:rubygems` | Build and push the gem to RubyGems |
517
+ | `rake release:github` | Push commit + tag, then create the GitHub release |
518
+ | `rake release:list:github` | List GitHub releases |
519
+ | `rake unrelease` | Delete the release from all remotes that support it |
520
+ | `rake unrelease:github` | Delete the GitHub release and remote tag |
521
+ ```
522
+
523
+ (The `rake version:release` and `rake version:unrelease` rows stay.)
524
+
525
+ - [ ] **Step 3: Update CLAUDE.md**
526
+
527
+ In `CLAUDE.md`, replace this bullet:
528
+
529
+ ```markdown
530
+ - Version lifecycle rake tasks installed via `Gempilot::VersionTask.new` (a `Rake::TaskLib`): `version:current/bump/commit/tag/untag/reset/revert`, composite `version:release`/`version:unrelease`, and `version:github:release/unrelease/list`
531
+ ```
532
+
533
+ with:
534
+
535
+ ```markdown
536
+ - Version lifecycle rake tasks installed via `Gempilot::VersionTask.new` (a `Rake::TaskLib`): `version:current/bump/commit/tag/untag/reset/revert` and composite `version:release`/`version:unrelease`
537
+ - Publishing rake tasks (mixed into `VersionTask` via `Gempilot::ReleaseTasks`): `release` (all remotes), `release:rubygems`, `release:github`, `release:list:github`, `unrelease`, `unrelease:github`. These override and reuse bundler's `bundler/gem_tasks` release chain; `release:source_control_push` is replaced with `Gempilot::Origin` to push commit+tag idempotently
538
+ ```
539
+
540
+ - [ ] **Step 4: Verify no stale references remain**
541
+
542
+ Run: `grep -rn "version:github" README.md CLAUDE.md`
543
+ Expected: no output.
544
+
545
+ - [ ] **Step 5: Commit**
546
+
547
+ ```bash
548
+ git add README.md CLAUDE.md docs/superpowers/specs/2026-07-20-release-task-hierarchy-design.md
549
+ git commit -m "Docs: document release/unrelease task hierarchy"
550
+ ```
551
+
552
+ ---
553
+
554
+ ## Task 5: Full verification
555
+
556
+ **Files:** none (verification only).
557
+
558
+ - [ ] **Step 1: Run the complete default suite**
559
+
560
+ Run: `bundle exec rake`
561
+ Expected: `task default: [:test, :spec, :rubocop]` runs all three — Minitest green (`0 failures`), RSpec green (`0 failures`), RuboCop `no offenses detected`.
562
+
563
+ - [ ] **Step 2: Verify Zeitwerk naming for the new files**
564
+
565
+ Run: `bundle exec rake zeitwerk:validate`
566
+ Expected: `Zeitwerk: All files loaded successfully.` (confirms `lib/gempilot/origin.rb` → `Gempilot::Origin` and `lib/gempilot/release_tasks.rb` → `Gempilot::ReleaseTasks` load cleanly under eager load).
567
+
568
+ - [ ] **Step 3: Confirm the working tree is clean and review the branch**
569
+
570
+ Run: `git status` and `git log --oneline master..HEAD`
571
+ Expected: clean working tree; commits for Origin, GithubRelease slim, release/unrelease move, and docs.
572
+
573
+ ---
574
+
575
+ ## Self-review notes (traceability to spec)
576
+
577
+ - **Task hierarchy** (spec §"Task interface"/"Task definitions") → Task 3 (module + rewire) + Task 3 Step 1 structure specs.
578
+ - **Bug fix — idempotent push replacing `already_tagged?`** (spec §"Problem"/`Origin`) → Task 1 (`Origin`, incl. real-remote idempotency regression test) + Task 3 `override_source_control_push`.
579
+ - **`GithubRelease` slimmed** (spec §"Components") → Task 2.
580
+ - **RubyGems reuses bundler leaf tasks** (spec §"Decisions") → Task 3 `define_rubygems_release` + prerequisite spec.
581
+ - **`unrelease:rubygems` errors naturally** (spec §"Error handling") → covered by not defining it; documented, no code.
582
+ - **Docs** (spec §"Docs") → Task 4.
583
+ - **`Rakefile.erb` unchanged** (spec §"Out of scope") → no task; load order already correct.