rails-ai-context 5.21.3 → 5.25.0

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 (154) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +593 -0
  3. data/CONTEXT.md +53 -1
  4. data/CONTRIBUTING.md +8 -0
  5. data/README.md +195 -467
  6. data/docs/ARCHITECTURE.md +26 -4
  7. data/docs/CLI.md +23 -0
  8. data/docs/COMPATIBILITY.md +31 -14
  9. data/docs/CONFIGURATION.md +4 -4
  10. data/docs/FAQ.md +1 -1
  11. data/docs/GUIDE.md +9 -7
  12. data/docs/INTROSPECTORS.md +7 -6
  13. data/docs/TOOLS.md +4 -1
  14. data/exe/rails-ai-context +90 -370
  15. data/gemfiles/mcp_floor.gemfile +13 -0
  16. data/gemfiles/prism_floor.gemfile +1 -10
  17. data/gemfiles/prism_head.gemfile +1 -10
  18. data/gemfiles/rails.gemfile +34 -0
  19. data/lib/generators/rails_ai_context/install/install_generator.rb +43 -154
  20. data/lib/rails_ai_context/action_filters.rb +145 -0
  21. data/lib/rails_ai_context/app_kind.rb +2 -7
  22. data/lib/rails_ai_context/ast_cache.rb +13 -10
  23. data/lib/rails_ai_context/boot_manager.rb +17 -1
  24. data/lib/rails_ai_context/change_watch.rb +62 -0
  25. data/lib/rails_ai_context/cli/entry_boot.rb +214 -0
  26. data/lib/rails_ai_context/cli/tool_runner.rb +1 -1
  27. data/lib/rails_ai_context/concern_membership.rb +72 -0
  28. data/lib/rails_ai_context/configuration.rb +102 -15
  29. data/lib/rails_ai_context/doctor.rb +27 -20
  30. data/lib/rails_ai_context/engine.rb +6 -0
  31. data/lib/rails_ai_context/facts_formatter.rb +8 -11
  32. data/lib/rails_ai_context/fingerprinter.rb +71 -24
  33. data/lib/rails_ai_context/gem_lock.rb +97 -0
  34. data/lib/rails_ai_context/hydrators/controller_hydrator.rb +2 -9
  35. data/lib/rails_ai_context/hydrators/model_hints.rb +28 -0
  36. data/lib/rails_ai_context/hydrators/schema_hint_builder.rb +1 -8
  37. data/lib/rails_ai_context/hydrators/view_hydrator.rb +2 -9
  38. data/lib/rails_ai_context/install/ai_tool.rb +10 -1
  39. data/lib/rails_ai_context/install/cleanup.rb +1 -0
  40. data/lib/rails_ai_context/install/initializer_file.rb +32 -0
  41. data/lib/rails_ai_context/install/program.rb +149 -0
  42. data/lib/rails_ai_context/install/selection_record.rb +68 -0
  43. data/lib/rails_ai_context/install_mode.rb +3 -7
  44. data/lib/rails_ai_context/introspector.rb +21 -2
  45. data/lib/rails_ai_context/introspectors/action_resolver.rb +263 -0
  46. data/lib/rails_ai_context/introspectors/action_text_introspector.rb +2 -6
  47. data/lib/rails_ai_context/introspectors/active_storage_introspector.rb +6 -18
  48. data/lib/rails_ai_context/introspectors/active_support_introspector.rb +1 -0
  49. data/lib/rails_ai_context/introspectors/api_introspector.rb +26 -36
  50. data/lib/rails_ai_context/introspectors/asset_pipeline_introspector.rb +9 -13
  51. data/lib/rails_ai_context/introspectors/auth_introspector.rb +18 -45
  52. data/lib/rails_ai_context/introspectors/config_introspector.rb +17 -22
  53. data/lib/rails_ai_context/introspectors/controller_introspector.rb +80 -179
  54. data/lib/rails_ai_context/introspectors/convention_introspector.rb +37 -75
  55. data/lib/rails_ai_context/introspectors/declared_constant.rb +96 -0
  56. data/lib/rails_ai_context/introspectors/eager_load.rb +65 -0
  57. data/lib/rails_ai_context/introspectors/frontend_framework_introspector.rb +6 -4
  58. data/lib/rails_ai_context/introspectors/gem_introspector.rb +12 -35
  59. data/lib/rails_ai_context/introspectors/i18n_introspector.rb +93 -11
  60. data/lib/rails_ai_context/introspectors/job_introspector.rb +90 -149
  61. data/lib/rails_ai_context/introspectors/listeners/method_call_listener.rb +31 -0
  62. data/lib/rails_ai_context/introspectors/listeners/methods_listener.rb +37 -5
  63. data/lib/rails_ai_context/introspectors/listeners/migration_dsl_listener.rb +11 -0
  64. data/lib/rails_ai_context/introspectors/middleware_introspector.rb +14 -1
  65. data/lib/rails_ai_context/introspectors/migration_introspector.rb +32 -35
  66. data/lib/rails_ai_context/introspectors/migration_replay.rb +272 -0
  67. data/lib/rails_ai_context/introspectors/model_introspector.rb +177 -100
  68. data/lib/rails_ai_context/introspectors/multi_database_introspector.rb +13 -21
  69. data/lib/rails_ai_context/introspectors/performance_introspector.rb +14 -40
  70. data/lib/rails_ai_context/introspectors/route_introspector.rb +3 -0
  71. data/lib/rails_ai_context/introspectors/schema_conventions.rb +101 -0
  72. data/lib/rails_ai_context/introspectors/schema_introspector.rb +21 -525
  73. data/lib/rails_ai_context/introspectors/schema_reader.rb +69 -2
  74. data/lib/rails_ai_context/introspectors/security_introspector.rb +3 -6
  75. data/lib/rails_ai_context/introspectors/source_scan.rb +80 -0
  76. data/lib/rails_ai_context/introspectors/structure_sql_reader.rb +196 -0
  77. data/lib/rails_ai_context/introspectors/test_introspector.rb +7 -17
  78. data/lib/rails_ai_context/introspectors/turbo_introspector.rb +276 -120
  79. data/lib/rails_ai_context/introspectors/view_introspector.rb +2 -1
  80. data/lib/rails_ai_context/introspectors/view_template_introspector.rb +19 -0
  81. data/lib/rails_ai_context/legacy_cleanup.rb +5 -0
  82. data/lib/rails_ai_context/live_reload.rb +37 -50
  83. data/lib/rails_ai_context/mcp_config_generator.rb +2 -2
  84. data/lib/rails_ai_context/mcp_edge.rb +25 -3
  85. data/lib/rails_ai_context/middleware.rb +1 -13
  86. data/lib/rails_ai_context/payload.rb +178 -0
  87. data/lib/rails_ai_context/pending_migrations.rb +60 -0
  88. data/lib/rails_ai_context/presets.rb +34 -4
  89. data/lib/rails_ai_context/redaction.rb +32 -1
  90. data/lib/rails_ai_context/resources.rb +6 -13
  91. data/lib/rails_ai_context/route_coverage.rb +30 -1
  92. data/lib/rails_ai_context/safe_path.rb +72 -0
  93. data/lib/rails_ai_context/schema_version.rb +13 -0
  94. data/lib/rails_ai_context/serializers/claude_rules_serializer.rb +17 -22
  95. data/lib/rails_ai_context/serializers/compact_serializer_helper.rb +29 -49
  96. data/lib/rails_ai_context/serializers/context_file_serializer.rb +30 -3
  97. data/lib/rails_ai_context/serializers/copilot_instructions_serializer.rb +15 -20
  98. data/lib/rails_ai_context/serializers/copilot_serializer.rb +19 -27
  99. data/lib/rails_ai_context/serializers/cursor_rules_serializer.rb +15 -20
  100. data/lib/rails_ai_context/serializers/markdown_serializer.rb +65 -88
  101. data/lib/rails_ai_context/serializers/opencode_rules_serializer.rb +15 -24
  102. data/lib/rails_ai_context/serializers/opencode_serializer.rb +5 -29
  103. data/lib/rails_ai_context/serializers/section_facts.rb +66 -0
  104. data/lib/rails_ai_context/serializers/section_marker_writer.rb +11 -1
  105. data/lib/rails_ai_context/serializers/stack_overview_helper.rb +67 -79
  106. data/lib/rails_ai_context/serializers/test_command_detection.rb +1 -2
  107. data/lib/rails_ai_context/serializers/tool_guide_helper.rb +1 -2
  108. data/lib/rails_ai_context/server.rb +4 -12
  109. data/lib/rails_ai_context/tasks/rails_ai_context.rake +61 -185
  110. data/lib/rails_ai_context/tools/analyze_feature.rb +37 -53
  111. data/lib/rails_ai_context/tools/base_tool.rb +68 -24
  112. data/lib/rails_ai_context/tools/dependency_graph.rb +3 -4
  113. data/lib/rails_ai_context/tools/diagnose.rb +27 -21
  114. data/lib/rails_ai_context/tools/generate_test.rb +12 -4
  115. data/lib/rails_ai_context/tools/get_api.rb +65 -12
  116. data/lib/rails_ai_context/tools/get_callbacks.rb +3 -4
  117. data/lib/rails_ai_context/tools/get_concern.rb +36 -136
  118. data/lib/rails_ai_context/tools/get_config.rb +16 -14
  119. data/lib/rails_ai_context/tools/get_context.rb +62 -123
  120. data/lib/rails_ai_context/tools/get_controllers.rb +50 -181
  121. data/lib/rails_ai_context/tools/get_edit_context.rb +9 -37
  122. data/lib/rails_ai_context/tools/get_env.rb +6 -35
  123. data/lib/rails_ai_context/tools/get_frontend_stack.rb +12 -15
  124. data/lib/rails_ai_context/tools/get_helper_methods.rb +7 -23
  125. data/lib/rails_ai_context/tools/get_i18n.rb +50 -5
  126. data/lib/rails_ai_context/tools/get_job_pattern.rb +96 -165
  127. data/lib/rails_ai_context/tools/get_model_details.rb +33 -70
  128. data/lib/rails_ai_context/tools/get_partial_interface.rb +16 -40
  129. data/lib/rails_ai_context/tools/get_routes.rb +5 -9
  130. data/lib/rails_ai_context/tools/get_schema.rb +33 -33
  131. data/lib/rails_ai_context/tools/get_service_pattern.rb +2 -2
  132. data/lib/rails_ai_context/tools/get_stimulus.rb +5 -22
  133. data/lib/rails_ai_context/tools/get_test_info.rb +49 -23
  134. data/lib/rails_ai_context/tools/get_turbo_map.rb +113 -454
  135. data/lib/rails_ai_context/tools/get_view.rb +29 -144
  136. data/lib/rails_ai_context/tools/migration_advisor.rb +6 -10
  137. data/lib/rails_ai_context/tools/onboard.rb +90 -113
  138. data/lib/rails_ai_context/tools/performance_check.rb +5 -3
  139. data/lib/rails_ai_context/tools/read_logs.rb +13 -43
  140. data/lib/rails_ai_context/tools/review_changes.rb +4 -7
  141. data/lib/rails_ai_context/tools/runtime_info.rb +23 -11
  142. data/lib/rails_ai_context/tools/safe_call.rb +4 -4
  143. data/lib/rails_ai_context/tools/search_code.rb +19 -8
  144. data/lib/rails_ai_context/tools/search_docs.rb +3 -8
  145. data/lib/rails_ai_context/tools/validate.rb +8 -45
  146. data/lib/rails_ai_context/tools/validate_semantics.rb +37 -34
  147. data/lib/rails_ai_context/version.rb +1 -1
  148. data/lib/rails_ai_context/vfs.rb +44 -102
  149. data/lib/rails_ai_context/view_file.rb +59 -0
  150. data/lib/rails_ai_context/watcher.rb +17 -32
  151. data/lib/rails_ai_context.rb +12 -8
  152. data/server.json +1 -1
  153. metadata +26 -4
  154. data/lib/rails_ai_context/source_line.rb +0 -208
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8afb263c0004a822a803eee097300e87dcb5a3a2de27d996a3cc5f11ea801051
4
- data.tar.gz: 462a8ca8d7f4a66cd617af71322356908f513aa88ac7aec2046f32cf4279805f
3
+ metadata.gz: d80b8bd17147a84fb1f8ce3028b7e1291af63c0d639663704eec171069e8f1b7
4
+ data.tar.gz: 4ce32bd09b90f16b62f70621642d6db35258c3a47892196e1fdb6d329366b4ed
5
5
  SHA512:
6
- metadata.gz: 2f48e61abb15f942c9a3b41eced790ce41618f091e337d84c1d598486e74e3a58e3331c9acf971ad2fea0d0a0bb8a909caef4c6b9fd3196fba51dbe318532e97
7
- data.tar.gz: 07e4b2759758da12d51805c0861e8a101fcdefdff55cba0db7714c6464473e486842fbd93c55b6480c8ce7b87325138fa18eb0920c38e219f0df3556a1ec0b19
6
+ metadata.gz: 53ecb39c98209ddd59050431dca51b448c848e7f6e4d93fe092523ee87b014e6279aa25d97b283e57c66368eb0b315baaa4b75d0ea349d21bc6b0b1109ec0d2d
7
+ data.tar.gz: 25c7854ce8e4e4c1e16996e13a2031dc35d6cfee245d46515a48833c4fdcc48d35eb3a38b5365cfa4518847096340a104dc79dbfda2dfc314565a1508ea5dd7b
data/CHANGELOG.md CHANGED
@@ -5,6 +5,599 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [5.25.0] - 2026-09-03
9
+
10
+ ### Added
11
+
12
+ - **`sprockets-rails`, `sprockets`, `sorcery` and `clearance` join the notable
13
+ gem table**, so `rails_get_gems` lists them and the `rails_get_config` assets
14
+ and auth lines name them from the lockfile rather than from a loaded
15
+ constant.
16
+
17
+ ### Fixed
18
+
19
+ Defects found by a second survey round over the whole surface, and the
20
+ duplicated mechanisms behind them.
21
+
22
+ - **`max_view_total_size` and `max_view_file_size` said they capped view
23
+ reads.** Only `doctor` reads either one, as the threshold for its view-size
24
+ warning; the config comments and the docs rows now say that.
25
+ - **`excluded_concerns` hid a concern from a model's list but not from the
26
+ catalogue.** `rails_get_concern` and the `rails_get_active_support` concern
27
+ registry still listed and counted a concern the key names; all three now
28
+ apply the same predicate.
29
+ - **An in-Gemfile app's `.rails-ai-context.yml` was inert.** The generated
30
+ initializer holds a `configure` block, and the YAML was skipped whenever one
31
+ had run, so a booted command took the defaults while `--no-boot` read the
32
+ file and the two disagreed (45 tools against 43 with `skip_tools` set).
33
+ Precedence is a merge: the file is applied once, before
34
+ `config/initializers`, so an initializer may assign a key or edit it in
35
+ place and both survive; a later call from the standalone binary or the
36
+ CLI's boot path is a no-op. A block wins the keys it assigns wherever it
37
+ lives, so one in `config/application.rb` or an environment file, which runs
38
+ before the load, keeps them too.
39
+ - **The static tier named the app after its directory.** `onboard` and every
40
+ generated context file were headed "mastodon" for an app that declares
41
+ `module Mastodon`. The name now comes from the module enclosing
42
+ `class Application < Rails::Application` in `config/application.rb`, and
43
+ falls back to the directory only when that file names nothing.
44
+ - **The static tier counted every class under `app/models` as a model.**
45
+ Namespace modules, form objects, filters and plain service classes were
46
+ listed and rendered as models of a table (Mastodon answered 195 models
47
+ against 117 booted). A static model is now a class whose superclass chain
48
+ reaches `ApplicationRecord`, a namespaced `*ApplicationRecord` or
49
+ `ActiveRecord::Base`, STI subclasses included.
50
+ - **An unreadable `.rails-ai-context.yml` aborted the boot.** A directory or a
51
+ file the process cannot read at that path raised out of the engine
52
+ initializer; it now warns and keeps the defaults, the way broken YAML does.
53
+ - **A model under a per-connection abstract base was missing from the static
54
+ list.** The multi-database shape - `class AnimalsRecord < ApplicationRecord;
55
+ self.abstract_class = true` in its own file, then `class Dog <
56
+ AnimalsRecord` - dropped every model on that connection from the static
57
+ list, the count and the schema listing. An abstract base is still walked so
58
+ chains resolve through it; it is left out of the result, as before.
59
+ - **`rails_get_api` stated a filesystem finding for a section nobody had
60
+ read.** In the static tier the whole section but the mode was declared
61
+ unavailable, and nothing read that declaration, so an app with
62
+ `app/controllers/api/v1/` was told "not detected (no app/controllers/api/v*
63
+ directories)". Every detection in that section is a file read, so the
64
+ static tier now answers all of them, and a key a section does name as
65
+ unanswered renders as `[UNAVAILABLE: ...]` rather than as a negative
66
+ finding.
67
+ - **An existing `.ai-context.json` holding anything but a JSON object
68
+ aborted the whole generation run.** The skip check reads it as a file to
69
+ replace.
70
+ - **`rails_get_schema`'s table heading named every model on the table**, so
71
+ an STI table with thirty subclasses filled the cheap summary with one
72
+ heading. It names five and counts the rest, the way the file's other lists
73
+ do.
74
+ - **`rails_get_test_info` blamed the app root for a name it refused as
75
+ sensitive.** The refusal names what the check covers: the name leaves the
76
+ app root or names a sensitive file.
77
+ - **`rails_onboard`'s Getting Started block told the reader to `cd` into the
78
+ app's class name underscored**, which is not the directory the clone lands
79
+ in. It names the app directory.
80
+ - **`init` refused a tree with app source but no `config/environment.rb`
81
+ after it had already written the config files**, leaving it half set up
82
+ with no `CLAUDE.md`. Every command that can serve the static tier now
83
+ reads such a tree, the way `--no-boot` already did.
84
+ - **A tree with app source but no `config/environment.rb` printed a boot
85
+ failure before falling back to static analysis.** No boot can succeed
86
+ without that file, so the static tier now takes over at once and its banner
87
+ names the missing file; `doctor` refuses the same tree with that reason.
88
+ The banner calls that tree static mode rather than a boot failure, since
89
+ no boot ran.
90
+ - **`rails_runtime_info` and `rails_query` told a tree with no
91
+ `config/environment.rb` to fix a boot failure or drop `--no-boot`**, neither
92
+ of which had happened. Such a tree now reads "This tree has no
93
+ `config/environment.rb`; add one (or run from the app root) for runtime
94
+ data."
95
+ - **A source path was contained without a separator and against an
96
+ unresolved root.** The frontend framework introspector's own containment
97
+ check let `/app-old` pass for `/app`. The pre-v5.8.1 bug, still in one
98
+ place.
99
+ - **`rails_get_env` printed Dockerfile `ENV` and `ARG` values verbatim at
100
+ detail full.** Every default now leaves through the redaction gate, and a
101
+ surrounding quote pair is stripped, so `ENV FOO="bar"` prints `bar`.
102
+ - **`rails_read_logs` matched the `search` term before redacting**, so a
103
+ hidden value could be probed a character at a time. The search runs on the
104
+ redacted text now.
105
+ - **The middleware introspector fabricated an empty stack in the static
106
+ tier.** It declares an alternate source and answers only its file facts,
107
+ rather than reporting an app with no middleware.
108
+ - **`rails_get_config` and `rails-ai-context facts` read gem keys no
109
+ introspector emits**, so the Auth line and the Key Dependencies section
110
+ never rendered at all.
111
+ - **Jobs and mailers carry `file:`.** `rails_get_job_pattern` reads the
112
+ carried file instead of camelizing a basename, and its listing comes from
113
+ the payload, so a job in a pack lists.
114
+ - **The auth introspector keyed Devise models by basename**, so
115
+ `app/models/admin/user.rb` overwrote `User`.
116
+ - **An out-of-order migration merge was reported as none pending** by the
117
+ migrations section while the schema section counted it. Both derive from
118
+ one applied set now, and an unknown applied set answers no pending key
119
+ rather than "everything" or "nothing".
120
+ - **Substring lockfile scans reported `bugsnag-capistrano` as Bugsnag** and
121
+ `database_cleaner-redis` as database_cleaner. One lockfile reader with
122
+ exact names answers every gem question, across the GEM, GIT and PATH
123
+ sections.
124
+ - **The hydrators warned about a model they had just resolved in another
125
+ spelling**, and emitted one hint block twice.
126
+ - **The channel and mailer eager loads had no per-constant recovery**, so a
127
+ single unloadable file emptied the list.
128
+ - **`YAML.safe_load` refused the `&default` anchors** every stock webpacker
129
+ or shakapacker config carries, so the source path fell back to the
130
+ convention.
131
+ - **VFS: `controllers/admin/posts` fell into the action handler** and
132
+ `routes/PostsController` returned zero routes.
133
+ - **`Payload.section` accepted a refused (`unavailable`) section**, so a
134
+ static-tier context file rendered a bare heading.
135
+ - **Every generated file counts the same controller set.**
136
+ `config.excluded_controllers` is honoured everywhere, not only by
137
+ `rails_get_controllers`.
138
+ - **The doctor counted models and controllers under `app/` only.** Packs and
139
+ engines count, and the freshness check reads the one watch scope.
140
+ - **The watcher stopped noticing `config/routes.rb` and `db/schema.rb`.** The
141
+ fingerprint, the watcher and the doctor share one scope covering `config`
142
+ and `db` whole, and the fingerprint mark is taken before the read it
143
+ protects.
144
+ - **The diagnose tool's git probe ran outside a repository** and leaked its
145
+ stderr into the response.
146
+ - **`rails_get_stimulus` re-read every controller file** to recompute a
147
+ lifecycle the payload already carried.
148
+ - **MCP config removal wrote non-atomically**, the dead `AstCache.invalidate`
149
+ is gone, and the doctor checks that `.codex/config.toml` is gitignored.
150
+ - **Composing tools decided whether a sub-tool answered by scraping its prose
151
+ for "not found".** A real answer whose body mentioned those words was
152
+ dropped. Responses carry an empty marker in MCP `meta` now.
153
+ - **`rails_get_test_info` guarded a caller-supplied name with its own
154
+ containment.** It reads through the shared guard.
155
+ - **The asset pipeline introspector's literal `none` was rendered as if it
156
+ named a pipeline**, so every generated context file carried
157
+ `- Assets: none`. The line shows only the parts that name something, and
158
+ disappears when there are none.
159
+ - **`rails_get_view`'s ivar list counted an `@` inside an email address and a
160
+ `@@class_variable`.** Neither is an instance variable.
161
+ - **A controller ivar compared, not assigned, was reported as set.** `return
162
+ unless @post == current_user` no longer names `@post`; `||=`, `+=`, `<<=` and
163
+ `>>=` still count.
164
+ - **`rails_get_context` resolved an action name case-sensitively** while
165
+ `rails_get_controllers` did not, so `action: "Show"` skipped the ivar
166
+ cross-check.
167
+ - **The ivar cross-check ran with no view templates section**, reporting every
168
+ controller ivar as unused in the view. It is skipped instead.
169
+ - **`rails_get_turbo_map` printed a Turbo Stream response as a Ruby hash.**
170
+ It renders `PostsController#create`, the way the file's other sections name
171
+ a controller action.
172
+ - **A second `context` run rewrote the generated files** for their timestamp
173
+ alone, so a repo that commits the context files saw a diff from a run that
174
+ found nothing new. One rule now covers them all: a file that differs only
175
+ in its `generated_at` key, or in the full-mode header's `> Generated:`
176
+ line, is skipped.
177
+ - **The schema listing named one model per table.** A table an STI child or a
178
+ namespaced second model shares listed whichever came first in the payload,
179
+ which could be the emptier one. Every model on the table is listed now,
180
+ the one carrying the most detail first.
181
+ - **Every static-tier context file carried `Rails [UNAVAILABLE: app not
182
+ booted]` mid-sentence.** The version comes from the lockfile, and the
183
+ marker is left for a tree whose lockfile does not carry rails.
184
+ - **`rails-ai-context preset " FULL "` exited 1 with the listing.** A preset
185
+ name is matched whatever its case or padding.
186
+ - **A `--no-boot` run that found no app printed the doctor hint**, which asks
187
+ for a boot error that never happened. `doctor` and `init` also accept a
188
+ tree that has app source but no `config/environment.rb`, and the refusal
189
+ names that file instead of telling the user to go to the app root they are
190
+ standing in.
191
+
192
+ ### Changed
193
+
194
+ - **`RailsAiContext.generate_context(format: nil)` means the recorded AI-tool
195
+ selection**, and all of them when nothing is recorded. It was `:all`.
196
+ - **Static-tier migration names are the class-style name** (`CreatePosts`),
197
+ matching what the booted tier reports.
198
+ - **Names in the turbo, auth, attachments and multi-database sections carry
199
+ their namespace** (`Admin::User`, not `User`), and packs and in-repo
200
+ engines count everywhere the app's source is walked.
201
+ - **The `.env.example` reader judges a value by its shape and length only.** A
202
+ default in Ruby source or a Dockerfile is condemned by a secret-shaped name
203
+ as well.
204
+ - **Turbo wiring lives in the introspector payload with file and line** -
205
+ frames, model broadcasts, explicit broadcasts and stream subscriptions -
206
+ and `rails_get_turbo_map` renders it. A `turbo_frame_tag dom_id(@post,
207
+ :edit)` frame renders as that call, and a symbol `turbo_stream_from :posts`
208
+ now pairs with its broadcast. A subscription argument that carries its own
209
+ commas renders whole: `turbo_stream_from [current_user, :notifications]` and
210
+ `dom_id(@post, :x)` keep every character they were written with.
211
+ - **The public-methods lists come from the parser.** `rails_get_concern`,
212
+ `rails_get_helper_methods`, `rails_get_model_details` and
213
+ `rails_get_controllers` read `private def` and `class_methods do`
214
+ correctly.
215
+ - **The standalone binary boots through `CLI::EntryBoot`**, and presets run
216
+ through `Presets.run` in both the binary and the rake task.
217
+ - **`rails_get_controllers` and the controller-action resource decide which
218
+ filters apply through one module, `ActionFilters`.** Every inherited filter
219
+ carries the `(from Parent)` annotation, after_action and
220
+ except-constrained ones included; a filter constrained with `except:` shows
221
+ that constraint; `skip_after_action` and `skip_around_action` count as
222
+ skips, and a skipped filter is struck through in the whole-controller view
223
+ as well as the per-action one.
224
+ - **A config value longer than 40 characters is filtered on every path.** The
225
+ Ruby-source and Dockerfile readers capped at 30 before; `.env.example` was
226
+ already 40.
227
+ - **`schema[:pending_migrations]`, reachable through the `rails://schema`
228
+ resource, is a list of `{ version:, name: }` entries** rather than version
229
+ strings.
230
+ - **`rails-ai-context://routes/{controller}` prefers an exact route-key
231
+ match**, so `routes/posts` no longer also returns `admin/posts`.
232
+ - **`unavailable_sections`, where a section carries it, is a list of
233
+ section keys**, not a reason string. The middleware section's static
234
+ answer is the one that still emits it.
235
+ - **The models resource template advertises
236
+ `rails-ai-context://models/{name}`**, and every scheme resolves through the
237
+ VFS rather than an exact-match legacy reader. `rails://models/{name}` is
238
+ still accepted.
239
+ - **`controllers[:controllers]` can carry `{ error: "unreadable" }` entries**
240
+ for a file over the size cap or otherwise unreadable, and the listing and
241
+ the count include them.
242
+
243
+ ### Removed
244
+
245
+ - **`SourceLine`, the hand-rolled Ruby lexer.** Every source read goes through
246
+ Prism.
247
+ - **`Fingerprinter.reset_gem_lib_fingerprint!` and `AstCache.invalidate`** -
248
+ no callers.
249
+ - **Support for the mcp gem below 0.13.** The gemspec floor is `>= 0.13`, the
250
+ first release whose tool responses carry `meta`.
251
+ - **`Presets.names` and `Presets.fetch`** - the listing and the run read
252
+ `DEFINITIONS` directly.
253
+ - **`SchemaHintBuilder.build_many`** - no callers.
254
+ - **`Fingerprinter.changed?`** - no callers; `stale?` answers the question.
255
+ - **The public constant `ChangeWatch::WATCH_DIRS`.**
256
+ - **`RailsAiContext.configured_via_block?`** - no callers once the config file
257
+ applies once per configuration and a block wins its keys by name.
258
+
259
+ ## [5.24.0] - 2026-08-17
260
+
261
+ ### Fixed
262
+
263
+ Defects found by a QA round against GitLab, OpenProject, Canvas LMS,
264
+ Discourse and Mastodon. Every one of them exits 0.
265
+
266
+ - **The commands that read the app degrade the way `tool` does.** `context`,
267
+ `inspect`, `facts`, `preset`, `watch` and `init` called a bare boot guard,
268
+ so on a repo you have just cloned - the case an agent most needs a
269
+ `CLAUDE.md` for, and the case least likely to boot - every tool answered
270
+ and the command that writes the files exited 1 having written nothing.
271
+ `init` was worse: it writes its config files first, so a boot failure left
272
+ the app half set up and called that a failure. They allow the static tier
273
+ now and each takes `--no-boot`; `doctor` still fails, because diagnosing
274
+ the boot is its job. Writing under `--no-boot` then exposed its own bug -
275
+ the context writer asked `Rails.application` for the output directory,
276
+ which raises `NameError` on the path where Rails is never loaded at all.
277
+ - **`tool --list` reads the app's config when the app cannot boot.** Boot is
278
+ what normally loads `.rails-ai-context.yml`, so the listing fell back to
279
+ the gem's defaults: on Mastodon it advertised 45 tools while the MCP server
280
+ offered 43 and the CLI itself answered `Unknown tool 'query'` for one it
281
+ had just listed.
282
+ - **`search_extensions` is documented as the fallback's list, which is what
283
+ it is.** Making ripgrep honour it did make the two backends agree, and it
284
+ cost the reach that makes the tool useful: on Mastodon `gem 'devise'` went
285
+ from 5 results to none, because a Gemfile carries no listed extension - and
286
+ the same for a Rakefile, a `.md`, a `.sql`. The docs, the attr comment and
287
+ the line the install generator writes now say plainly that the list is the
288
+ Ruby fallback's and that ripgrep searches every file.
289
+ - **A table the replay cannot name is not reported.** Canvas has a migration
290
+ that calls `create_table table_name do |t|` with a local computed at run
291
+ time; the replay kept the entry under a nil key, and the first serializer
292
+ to sort the table names took the whole context run down. Reachable only
293
+ once `context` could enter the static tier at all.
294
+ - **Coverage says nothing when there is nothing to measure against.** With a
295
+ `default_locale` the app configures but ships no file for, every locale
296
+ scored zero and all of them were named as untranslated.
297
+ - **A mixin in a nested concerns directory is not a model.** The skip only saw
298
+ the top-level `concerns/` that Rails autoloads, so OpenProject's
299
+ `app/models/queries/operators/concerns` contributed four mixins to a model
300
+ count of 978 where the app has 974. A nested `concerns/` is an ordinary
301
+ namespace, though, so the directory name alone does not decide it: a file
302
+ there that declares a class is a model like any other.
303
+ - **`docs/COMPATIBILITY.md` describes the static tier the gem actually has.**
304
+ It said 6 introspectors answer without a booted app and "the other 34 have
305
+ no static path", naming eight examples - all of which answer. The real
306
+ split is 23 files-only, 9 alternate-source and 8 runtime-only: 32 of 40.
307
+ A guard spec derives all three lists from `INTROSPECTOR_MAP`.
308
+
309
+ - **A controller is named by the constant its source declares.** Zeitwerk
310
+ resolves a path through the app's own inflector, which the static tier
311
+ never loads, so camelizing the path invented `Activitypub::` for the 12
312
+ controllers Mastodon declares as `ActivityPub::` and `Oauth::` for the 4
313
+ it declares as `OAuth::` - names that appear nowhere in an app that
314
+ registers those acronyms, and a `NameError` for anyone who uses one. The
315
+ path stays the answer where the source does not carry the whole name
316
+ (`application_cable/channel.rb`) and where the declared name does not name
317
+ that file, since Prism recovers a syntax error into a partial tree.
318
+ - **An interceptor is told from a mailer by its framework hook.**
319
+ `app/mailers` is also where ActionMailer interceptors live, and every `.rb`
320
+ under it counted: OpenProject's `Interceptors::DefaultHeaders` arrived as a
321
+ mailer with `delivering_email` - a hook the framework calls - listed as an
322
+ email an agent could send, 2 of its 11 entries. What an interceptor has and
323
+ a mailer does not is one of `delivering_email`, `previewing_email` or
324
+ `delivered_email`; requiring a class instead would have dropped GitLab's 20
325
+ `Emails::*` modules, which hold every notification it sends.
326
+ - **Migration replay reads what a migration does, not what it undoes.** A
327
+ `def down` says how to reverse the change, so replaying it alongside `up`
328
+ cancelled the migration out. On a migrations-only app the ordinary
329
+ up-creates/down-drops pair erased two tables OpenProject really has (35
330
+ to 37), and the reverse pair would have invented one it dropped. The same
331
+ now holds for the block spellings, `reversible { |dir| dir.down { ... } }`
332
+ and `revert`, and for the `t.timestamps` inside them - those were found by
333
+ a separate walk over the whole file, so a down body's timestamps landed on
334
+ the last table created on the way up.
335
+ - **i18n coverage groups the locales below its own rounding floor instead of
336
+ listing them.** A language-name lookup table under `config/locales`
337
+ contributes a top-level key per language, and Rails does load each as an
338
+ available locale. Scoring them produced a row each saying every key was
339
+ missing: on Discourse, 138 of 186 coverage rows described a translation
340
+ effort nobody had started. Those 138 are now summarised in one line, with a
341
+ shared key count stated once. The rows still exist - asking for one by name
342
+ answers with its numbers, so a translation genuinely started below the floor
343
+ is not written off as untranslated. The available list still matches a
344
+ booted Rails, and a locale's keys are read from its own file and any shared
345
+ file together, so one that lives outside the naming convention is scored.
346
+ - **A recorded tool selection no longer switches off `.ai-context.json`.**
347
+ The install generator always records one, and every later `rails
348
+ ai:context` passed that list to the serializer. The machine artifact is
349
+ not an AI tool and no install menu offers it, so it silently stopped being
350
+ written on the one install path the docs describe - while the generator
351
+ went on adding it to `.gitignore`.
352
+ - **`skip_tools` accepts the symbol spelling its neighbours use.** Every
353
+ other key in the generated initializer takes symbols, and `%i[]` here
354
+ skipped nothing at all: both readers compare against a tool name, which is
355
+ a String.
356
+
357
+ - **A controller carries the file it was read from.** Reading the name from
358
+ the source fixed the name and broke every path derived from it:
359
+ `ActivityPub::CollectionsController`.underscore is `activity_pub/...` and
360
+ the file is under `activitypub/`, so `rails_get_context` answered "Could
361
+ not extract source code" for a file that exists and asked the routes for a
362
+ controller key Rails does not use. Both tiers emit `file:` - the booted one
363
+ asks Ruby where the class was defined - and the consumers read it through
364
+ `Payload`.
365
+ - **The locale files are indexed once, not once per locale.** Asking every
366
+ file about every locale is O(locales x files): on Discourse, 187 over 108,
367
+ it took the i18n answer from 4.6 seconds to four and a half minutes. Each
368
+ entry also records the locales it serves, so asking for one by name finds
369
+ its files even when the filename spells the gem rather than the locale.
370
+
371
+ - **Every consumer that turns a controller name back into a path reads the
372
+ file it was read from.** Carrying `file:` fixed six of them and left five
373
+ answering a confident negative, which is the answer an agent acts on
374
+ without checking. On Mastodon's 18 inflected controllers: `rails_test_info`
375
+ reported 16 specs that exist as missing, `rails_get_routes` reported 4
376
+ routes that exist as absent, `rails_get_view` 2 views, and
377
+ `rails_generate_test` wrote a spec whose body was `skip "no routes found"`
378
+ for a controller with a route. `rails_analyze_feature` listed them as
379
+ untested. `Payload.controller_route_key` is the one derivation now, and a
380
+ spec drives all five tools with an inflected name.
381
+ - **The install path's files require their own stdlib.** `init` loads six
382
+ files before Rails and before the entry file, on purpose - so the entry
383
+ file's `require "set"` never runs for them. `legacy_cleanup`'s `[...].to_set`
384
+ raised `NoMethodError` on Ruby 3.1, where `Set` is not autoloaded, and 3.2+
385
+ hid it: only the CI matrix ever saw it.
386
+ - **`rails_runtime_info` reports cache numbers, not the cache object.** The
387
+ MemoryStore branch passed `cache.inspect` straight through, so the answer
388
+ carried `#<ActiveSupport::Cache::MemoryStore entries=0, size=0, options={...}>`
389
+ - the shape this gem has shipped as a defect before. Entries and size are
390
+ now facts, and a store that answers `#stats` gets its hash rendered as pairs
391
+ rather than a Ruby literal.
392
+ - **A model's table comes from its file, not from its name.** Naming a model by
393
+ the constant its source declares made `underscore` stop being the way back:
394
+ `OAuthClientConfig` underscores to `o_auth_client_config`, and the file is
395
+ `oauth_client_config.rb`. `rails_model_details` reported the table as
396
+ `o_auth_client_configs`, which no app has, so the columns section vanished
397
+ and the schema hint pointed at nothing - on Canvas that hit
398
+ `OAuthClientConfig`, `OAuthRequest`, `AuthenticationProvider::OAuth` and
399
+ `OAuth2`, on OpenProject `OAuthClient` and `OAuthClientToken`. The file's own
400
+ name already carries the inflection, because Zeitwerk resolved the constant
401
+ from it. `rails_generate_test`, `rails_get_callbacks`, `rails_test_info` and
402
+ the five `rails_validate` checks that key models by path read it too.
403
+ - **An abstract base class is not a model in the static tier either.** A
404
+ booted Rails rejects `abstract_class?`, and a namespaced base class is one:
405
+ GitLab's `Ci::ApplicationRecord`, `PackageMetadata::ApplicationRecord` and
406
+ `SecApplicationRecord` were counted, so the same app answered 895 models
407
+ without a boot and 888 with one. OpenProject: 974 against 971.
408
+ - **A model carries its file too, and is named by the constant its source
409
+ declares.** `rails_model_details` rebuilt `app/models/<underscored>.rb` from
410
+ the name in four places, so for a model in a pack or an engine the custom
411
+ validations, the source-defined methods, the method signatures and the class
412
+ structure all went quietly missing from an answer that otherwise looked
413
+ complete. The static tier also camelized the path into the name, so an app
414
+ registering an inflection got a constant it does not have - and one the
415
+ booted tier, which reads the real class, would never agree with.
416
+ - **`rails_validate_semantics` checks the views under an inflected directory.**
417
+ A view directory names the route key, so camelizing `app/views/oauth/` back
418
+ gave a constant the app never declares and the undefined-ivar check stopped
419
+ running for that whole tree without saying so.
420
+ - **A test-gap claim is not made from a scan that stopped early.**
421
+ `rails_analyze_feature` scans the first 500 files per glob; Discourse has
422
+ 1,672 specs, so the scan never reached the ones covering the feature and
423
+ every controller in it was reported as having no test. The Tests section
424
+ disclosed the cap, the gaps section asserted through it.
425
+ - **"Global before_actions" in the generated files means global.** The scan
426
+ matched `skip_before_action` too, and ignored `only:` / `except:` / `if:`.
427
+ Mastodon's `CLAUDE.md` listed five, of which three were false - including
428
+ `verify_authenticity_token`, which that controller skips.
429
+
430
+ ### Added
431
+
432
+ - **`--no-boot` on `context`, `inspect`, `facts`, `preset`, `watch` and
433
+ `init`.** The static tier was reachable from `tool` and `serve` only.
434
+ - **`--no-boot` reads a repo that has source but no `config/`.** An engine
435
+ keeps its dummy app under `spec/dummy`, so the guard against describing an
436
+ empty directory has to measure source, not boot files.
437
+
438
+ ### Changed
439
+
440
+ - **The generated initializer has one indent from top to bottom.** The AI
441
+ Tools section was written at the configure body's indent and every section
442
+ after it flush, so the file an app commits changed indent two lines in and
443
+ stayed there.
444
+
445
+ ## [5.23.0] - 2026-08-16
446
+
447
+ ### Fixed
448
+
449
+ - **`AstCache.parse_string` caches, as its name always said.** Keyed by
450
+ content digest, sharing the store and its eviction; oversize sources
451
+ bypass instead of raising. A controller was parsed up to ten times per
452
+ static run because every extractor received the text and re-parsed it.
453
+ - **`rails_validate` says which checks it skipped.** When the AST parse
454
+ fails, the three rules with no regex twin used to vanish silently and a
455
+ partially-checked file read as clean; the response now names them. The
456
+ tool also gains its first spec file.
457
+ - **A downstream app exception is the app's again.** The middleware's
458
+ rescue used to convert any error from the app behind it - non-MCP
459
+ requests included - into an MCP error frame. `McpEdge.rack_call` now owns
460
+ the whole Rack-shaped request (path match, session scope, dispatch,
461
+ containment) for the middleware and the standalone server, and the
462
+ pass-through branch runs outside the rescue.
463
+
464
+ ### Changed
465
+
466
+ - **The cache fingerprint watches what the resolvers read.** Packs,
467
+ engines, `extra_app_paths`, every concern home, the `Gemfile`,
468
+ `config/locales` and `config/environments` now feed the fingerprint, so
469
+ an edit there cannot serve a stale answer that looks fresh.
470
+ - **One change-detection loop.** `ChangeWatch` owns the watch list, the
471
+ Listen wiring, the fingerprint gate and the code reload; `Watcher` and
472
+ `LiveReload` keep only their reactions (regenerate files; refresh caches
473
+ and notify clients) and their own missing-`listen` policy.
474
+ - **The initializer guard and the `tool_mode` line each have one home.**
475
+ `Install::InitializerFile` holds the guard patterns the generator writes
476
+ and the doctor diagnoses; `SelectionRecord.write_tool_mode` replaces the
477
+ rake task's hand-rolled three-branch rewriter, and only rewrites an
478
+ uncommented line, so the generated commented-out default stays a comment.
479
+ - **Split-rule targets derive from the `Install::AiTool` table.** The
480
+ rules serializers read `rules_dir` (and the AGENTS.md pair) from the
481
+ table doctor and cleanup already read, so a moved path changes every
482
+ surface at once.
483
+ - **The stack helper writes through `SafeFile.atomic_write`** instead of a
484
+ private copy of it, and its app-tree scans take a root, so they are
485
+ covered by specs for the first time.
486
+ - **Every serializer is proven against a context real introspectors
487
+ produced** (`IntrospectedFixture`), so no serializer can read a shape
488
+ production does not make - the class of defect that kept the engines and
489
+ turbo sections dead while hand-built fixtures stayed green.
490
+
491
+ ## [5.22.0] - 2026-08-16
492
+
493
+ ### Fixed
494
+
495
+ - **A class nested inside a controller, mailer, model or helper file no
496
+ longer contributes its methods to the host's interface.** "Which public
497
+ methods are this class's own" had five transcriptions with five filter
498
+ sets; they now share `ActionResolver`, which filters listener output by the
499
+ owner field the listener always emitted, walks app-owned ancestors, and
500
+ falls back to reflection with the #136 base subtraction. Mailers had kept
501
+ the pre-#136 reflection answer, so a public helper on a non-abstract
502
+ `ApplicationMailer` arrived as a deliverable action; they subtract now, and
503
+ static mailer actions apply the same underscore rule as controllers. (#149)
504
+
505
+ - **Every generated context file states each fact through one renderer.**
506
+ `SectionFacts` owns the models line, the database line, the associations
507
+ list (eight copies, three of them in tools) and the warnings section -
508
+ which now renders in AGENTS.md and .github/copilot-instructions.md too, so
509
+ a half-failed run cannot look clean in two of four files. AGENTS.md stops
510
+ shadowing the shared architecture and footer renderers with poorer copies,
511
+ and its controller list goes through the shared renderer with named
512
+ actions as a depth choice rather than a fork. `Payload` is the reading
513
+ side of the introspection hash: list readers whose key pairs a spec pins
514
+ against the producing introspector's real output, so the next renamed key
515
+ fails a test instead of silently emptying a section. (#151)
516
+ - **API-only apps are told "does not apply", not "not found".**
517
+ `rails_get_helper_methods` and `rails_get_frontend_stack` now consult the
518
+ api-only note like the other eleven call sites. The two hand-rolled
519
+ paginations (`rails_get_schema`, `rails_get_stimulus`) go through the
520
+ shared `paginate`, one offset/limit behavior across all fourteen paginated
521
+ surfaces. (#152)
522
+ - **The interactive install is one program with three voices.** The
523
+ generator, the standalone binary and the rake task each held a full copy
524
+ of the prompts, the removed-tool cleanup, the gitignore append and the
525
+ MCP-config write - about 470 lines that had already drifted: two labels
526
+ for one mode, a menu with file lists in two entries and bare names in the
527
+ third, a byte-identical gitignore block three times. `Install::Program`
528
+ now owns the steps and the wording; each entry supplies its say/ask
529
+ surface and keeps only its closing instructions. The rake menu shows each
530
+ tool's files, and every entry uses one mode label.
531
+ - **Configuration loads the same way on every entry point.** The engine now
532
+ runs `auto_load!` at boot, so `.rails-ai-context.yml` works on the rake
533
+ tasks, the middleware and the engine controller the way the docs always
534
+ said - initializer first, the loader steps aside when a configure block
535
+ ran. `Configuration#ai_tools` answers from `SelectionRecord` when nothing
536
+ set it, so `rails ai:context` stops re-asking a question the record
537
+ already answers, and the recorded `tool_mode` reaches in-app surfaces
538
+ through the record's new reader. A bad YAML value now warns and keeps the
539
+ default the way bad syntax always did, instead of dying raw out of the
540
+ CLI. (#147, #148)
541
+ - **The three static schema sources follow one set of conventions, and every
542
+ schema question can be asked of any of them.** `SchemaReader.for(root)`
543
+ chooses schema.rb, structure.sql or migration replay behind one
544
+ question-level seam, so introspectors asking "does this table carry this
545
+ column" now get answers on structure.sql and migrations-only apps instead
546
+ of nothing. The migration replay is repaired: a replayed `create_table`
547
+ seeds its implicit `id`, `change_column_null` applies in both directions
548
+ (the listener now captures the positional boolean), and `t.references` is
549
+ typed per adapter instead of hardcoded bigint. The conventions live in
550
+ `SchemaConventions`, shared by all three sources the way #140's
551
+ foreign-key fix pioneered; the SQL parser and the replay engine moved to
552
+ `StructureSqlReader` and `MigrationReplay`, and the schema introspector
553
+ halves in size, keeping only presentation. (#150)
554
+ - **Every surface answers "which concerns count" and "how many routes" the
555
+ same way.** Five renderers carried five concern filter sets:
556
+ `rails_get_callbacks` hid every namespaced concern unless it started with
557
+ "App", `rails_analyze_feature` hid all of them, and two ignored the
558
+ `excluded_concerns` config. `ConcernMembership` now decides the payload
559
+ sense once, at the introspector seam, for both tiers - the static
560
+ controller answer goes through `MixinsListener`, so `prepend` counts and a
561
+ singleton-class `include` does not, exactly as booted. Route populations
562
+ (app vs framework split, PUT/PATCH dedup) moved beside the coverage suffix
563
+ into `RouteCoverage`, replacing four hand-kept computations, one of them a
564
+ wholesale copy. CLAUDE.md's per-model concern list now names the concerns
565
+ the app defines, namespaced ones included.
566
+ - **Mounted engines render again in `rails_onboard` and the context files.**
567
+ `EngineIntrospector` emits `:mounted_engines`; the tool read `:engines` then
568
+ `:mounted`, and the stack line read `:mounted`, so both fell back to "none"
569
+ forever. Both now read the introspector's keys and the element's real
570
+ `engine:`/`path:` shape, and the spec fixtures use that shape instead of one
571
+ production never produced. (#144)
572
+ - **The Hotwire stack line and `rails_onboard`'s Real-Time and Frontend
573
+ sections read turbo keys that exist.** `turbo[:frames]`, `[:streams]` and
574
+ `[:broadcasts]` were never emitted (`:turbo_frames`, `:turbo_streams` and
575
+ `:model_broadcasts` are), so the line was dead everywhere and Real-Time
576
+ always fell through to its view-scan fallback. (#145)
577
+ - **`rails_get_context` no longer offers a Related Services section it could
578
+ never fill.** The `:services` key it read has no introspector behind it; the
579
+ dead branch is gone. (#146)
580
+ - **`rails ai:context` stops re-prompting a YAML-configured app on every
581
+ run.** The rake path now loads `.rails-ai-context.yml` the way the
582
+ standalone binary always did; the initializer still wins when its configure
583
+ block ran. (#147)
584
+ - **The standalone binary's `--format` help names opencode and codex.** A
585
+ guard spec pins the help text to `Install::AiTool`, which the Thor class
586
+ body cannot ask at definition time. (#153)
587
+ - **`rails ai:serve` and `rails ai:serve_http` boot under the same timeout
588
+ and rescue as the binary.** A hanging initializer used to hang the task
589
+ forever where `rails-ai-context serve` gives up at 60s; both tasks now boot
590
+ through `BootManager.guard`, and `RAILS_AI_CONTEXT_BOOT_TIMEOUT` is parsed
591
+ in one place. (#154)
592
+ - **doctor's tool list and `rails_search_code`'s exclusion list derive from
593
+ `Install::AiTool`.** Both were hand-typed restatements sitting below the
594
+ ownership spec's detection floor; the search exclusions now also cover the
595
+ generated `app/models/AGENTS.md` pair they previously missed. (#155)
596
+ - **An e2e example asserts `call` and `static_call` answer the same keys.**
597
+ The declaration contract was already enforced (ADR-0002); the shape contract
598
+ was not, so a key added to the booted tier could silently vanish from the
599
+ static one. (#156)
600
+
8
601
  ## [5.21.3] - 2026-08-12
9
602
 
10
603
  ### Fixed