Skip to content

Make rbs collection say where the gem list comes from - #3094

Draft
pocke wants to merge 2 commits into
masterfrom
claude/collection-init-agent-friendly
Draft

Make rbs collection say where the gem list comes from#3094
pocke wants to merge 2 commits into
masterfrom
claude/collection-init-agent-friendly

Conversation

@pocke

@pocke pocke commented Aug 22, 2026

Copy link
Copy Markdown
Member

Part of #3093.

Ask a coding agent to set up RBS and Steep in a project and it usually writes a Steepfile that lists every dependency with library, one gem per line. rbs collection exists so that nobody has to maintain that list -- it reads Gemfile.lock, and rbs and Steep load the installed RBS files on their own -- but nothing an agent (or a person) reads on the way there says so. This PR changes what rbs collection prints and writes. The steep init template, the other half of #3093, is soutaro/steep#2269.

init says where the gem list comes from

rbs collection init printed created: rbs_collection.yaml and nothing else: not the next command, not the fact that would stop someone from enumerating gems. The generated rbs_collection.yaml had the same gap -- comments for sources, path and gems[].ignore, none for "the gem list is read from Gemfile.lock". Now:

$ rbs collection init
created: rbs_collection.yaml

rbs collection installs RBS files for the gems in your Gemfile.lock.
Gems in Gemfile.lock don't need to be listed anywhere.

Next steps:
  $ echo "/.gem_rbs_collection/" >> .gitignore
  $ rbs collection install    # writes rbs_collection.lock.yaml; keep it in version control

The generated config opens with the same two facts. Its commented-out gems: example showed only ignore: true; the other half -- adding an entry for a library that Gemfile.lock doesn't carry -- was documented only in docs/collection.md. The example is socket, a non-gem standard library (NONGEM_STDLIBS), rather than pathname, whose types moved to core/pathname.rbs in 8066053, or csv, which is in ALUMNI_STDLIBS and warns when installed.

docs/collection.md gets a quickstart at the top, its cat rbs_collection.yaml block matches the generated file again (it had lost type: git and the local source comment), and the .gitignore paragraph that now duplicates the init output is gone.

The missing-file paths no longer end in a backtrace

exe/rbs has no rescue, so every one of these printed a full backtrace, which reads as "rbs collection is broken" and sends people back to library. init now points at rbs collection install, so they are easier to reach than before.

State Before After
no rbs_collection.yaml (install, update) Errno::ENOENT names rbs collection init
no lock file under --frozen Errno::ENOENT says to run without --frozen, or to run init first when the config is missing too
no Gemfile Bundler::GemfileNotFound names Gemfile.lock
Gemfile but no Gemfile.lock undefined method 'specs' for nil names bundle install

Raising RBS::Collection::Config::CollectionNotAvailable, which #3093 suggested, would keep the backtrace for the same reason, so these write to stderr and return 1 like the rest of run_collection.

Observable change: already exists and should exist to clean used Kernel#puts, which writes to the real $stdout and ignores the injected IO. Both are failures that return 1, so they move to stderr.

Notes

  • Non-gem standard libraries can be loaded either way -- listed under gems: here, or with library in a Steepfile -- so the two templates are not in conflict; each shows the option that belongs to it.
  • Tests cover each of the four paths above, --frozen with neither file, and --frozen with a lock file and no config, which keeps working. steep check does not run in my environment (steep/patch.rbs defines Module#ruby2_keywords, which now collides with core/module.rbs, on master as well); with that one signature line removed it reports No type error detected.

pocke and others added 2 commits August 22, 2026 20:51
Ask a coding agent to set up RBS and Steep in a project and it usually
writes a Steepfile that lists every dependency with `library`, one gem per
line. `rbs collection` exists so that nobody has to maintain that list: it
reads `Gemfile.lock`, and `rbs` and Steep load the installed RBS files on
their own. Agents rarely find it, and the tools we ship are part of why.

`rbs collection init` printed one line, `created: rbs_collection.yaml`. It
named neither the next command nor the fact that would stop someone from
enumerating gems. The generated `rbs_collection.yaml` had the same gap: its
comments explained `sources`, `path` and `gems[].ignore` -- the knobs --
but never said the gem list is read from `Gemfile.lock`. The file is read
right after it is written, so it is a good place to say so.

- `init` now prints what the command does, that gems in `Gemfile.lock` need
  no listing anywhere, and the next steps: `.gitignore` and
  `rbs collection install`. `rbs_collection.lock.yaml` is what pins the RBS
  revisions -- the source is `revision: main` -- so the output says to keep
  it in version control.
- The generated config opens with the same two facts.
- Its commented-out `gems:` example showed only `ignore: true`. The other
  half -- adding an entry for a library that `Gemfile.lock` doesn't carry --
  was documented only in `docs/collection.md`. `socket` is the example: it
  is a non-gem standard library (`NONGEM_STDLIBS`), unlike `pathname`, whose
  types moved to `core/pathname.rbs` in 8066053, and unlike `csv`, which is
  in `ALUMNI_STDLIBS` and warns when installed.
- `docs/collection.md` gets a quickstart, so readers who see only the first
  screen get the two commands. Its `cat rbs_collection.yaml` had drifted
  from the generated file (no `type: git`, no local source comment) and now
  matches it byte for byte. The `.gitignore` paragraph right below the
  `init` output is gone, since that output now says it.

Ref: #3093

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BXtMkmp27LieHgSjNyEWZ8
Running `rbs collection install` before the files it needs exist ended in
an uncaught exception with a full backtrace, because `exe/rbs` has no
rescue. Someone -- or a coding agent -- who sees that concludes that
`rbs collection` is broken and goes back to listing gems with `library`.
`rbs collection init` now points at `rbs collection install`, so these
paths are easier to reach than they were.

Four of them:

- No `rbs_collection.yaml`: `Errno::ENOENT` from `Config.from_path`, for
  both `install` and `update`. Now a message naming `rbs collection init`.
- No `rbs_collection.lock.yaml` under `--frozen`: `Errno::ENOENT` from
  `Installer#initialize`. The message says to run without `--frozen`, or,
  when the config is missing too, to run `rbs collection init` first, so
  that one message is enough to get unstuck.
- No `Gemfile`: `Bundler::GemfileNotFound` from `Bundler.definition`.
- A `Gemfile` but no `Gemfile.lock`: `Bundler.definition` returns, and
  `LockfileGenerator#initialize` dies with `undefined method 'specs' for
  nil` on `definition.locked_gems.specs`. Now a message naming
  `bundle install`.

Raising `RBS::Collection::Config::CollectionNotAvailable`, as the issue
suggested, would keep the backtrace for exactly the same reason, so these
write to `stderr` and return 1 like the rest of `run_collection`.

`already exists` and `should exist to clean` used `Kernel#puts`, which
writes to the real `$stdout` and ignores the injected IO. Both are failures
that return 1, so they go to `stderr` now; this changes which stream they
appear on.

Tests cover each of the four paths, `--frozen` with neither file, and
`--frozen` with a lock file and no config, which keeps working.

Ref: #3093

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BXtMkmp27LieHgSjNyEWZ8
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant