rails-ai-context 5.32.2 → 5.34.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 (203) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1491 -0
  3. data/CONTEXT.md +27 -3
  4. data/README.md +16 -12
  5. data/Rakefile +6 -0
  6. data/app/controllers/rails_ai_context/mcp_controller.rb +112 -84
  7. data/config/routes.rb +2 -3
  8. data/docs/ARCHITECTURE.md +7 -5
  9. data/docs/CLI.md +79 -6
  10. data/docs/COMPATIBILITY.md +75 -1
  11. data/docs/CONFIGURATION.md +30 -11
  12. data/docs/CUSTOM_TOOLS.md +2 -1
  13. data/docs/FAQ.md +16 -6
  14. data/docs/GUIDE.md +117 -71
  15. data/docs/INTROSPECTORS.md +10 -8
  16. data/docs/QUICKSTART.md +3 -3
  17. data/docs/RECIPES.md +1 -1
  18. data/docs/SECURITY.md +109 -25
  19. data/docs/SETUP.md +30 -6
  20. data/docs/STANDALONE.md +10 -3
  21. data/docs/TOOLS.md +152 -34
  22. data/docs/TROUBLESHOOTING.md +90 -12
  23. data/docs/adr/0003-shared-tool-cache-semantics.md +8 -0
  24. data/docs/adr/0005-workspace-mcp-entries.md +39 -0
  25. data/exe/rails-ai-context +733 -104
  26. data/lib/generators/rails_ai_context/install/install_generator.rb +341 -125
  27. data/lib/rails_ai_context/app_kind.rb +18 -5
  28. data/lib/rails_ai_context/ast_cache.rb +7 -0
  29. data/lib/rails_ai_context/base_mixins.rb +8 -8
  30. data/lib/rails_ai_context/boot_manager.rb +22 -3
  31. data/lib/rails_ai_context/change_watch.rb +14 -11
  32. data/lib/rails_ai_context/cli/app_root.rb +401 -0
  33. data/lib/rails_ai_context/cli/entry_boot.rb +58 -22
  34. data/lib/rails_ai_context/cli/tool_runner.rb +47 -21
  35. data/lib/rails_ai_context/code_reloader.rb +115 -1
  36. data/lib/rails_ai_context/concern_membership.rb +3 -1
  37. data/lib/rails_ai_context/concern_paths.rb +8 -3
  38. data/lib/rails_ai_context/configuration.rb +41 -9
  39. data/lib/rails_ai_context/context_file_report.rb +19 -2
  40. data/lib/rails_ai_context/database_file.rb +26 -0
  41. data/lib/rails_ai_context/doctor.rb +1122 -184
  42. data/lib/rails_ai_context/engine.rb +18 -1
  43. data/lib/rails_ai_context/erb_source.rb +19 -0
  44. data/lib/rails_ai_context/executable.rb +21 -0
  45. data/lib/rails_ai_context/facts_formatter.rb +5 -2
  46. data/lib/rails_ai_context/file_walk.rb +61 -10
  47. data/lib/rails_ai_context/fingerprinter/snapshot.rb +234 -0
  48. data/lib/rails_ai_context/fingerprinter.rb +40 -12
  49. data/lib/rails_ai_context/gem_lock.rb +186 -27
  50. data/lib/rails_ai_context/git_ignore.rb +22 -0
  51. data/lib/rails_ai_context/install/ai_tool.rb +31 -5
  52. data/lib/rails_ai_context/install/cleanup.rb +21 -0
  53. data/lib/rails_ai_context/install/program.rb +209 -35
  54. data/lib/rails_ai_context/install/selection_record.rb +258 -97
  55. data/lib/rails_ai_context/install/surface.rb +6 -1
  56. data/lib/rails_ai_context/install/validation_hook.rb +282 -0
  57. data/lib/rails_ai_context/install/workspace.rb +166 -0
  58. data/lib/rails_ai_context/install_mode.rb +71 -13
  59. data/lib/rails_ai_context/introspectors/action_mailbox_introspector.rb +1 -1
  60. data/lib/rails_ai_context/introspectors/action_presence.rb +11 -6
  61. data/lib/rails_ai_context/introspectors/action_source.rb +182 -0
  62. data/lib/rails_ai_context/introspectors/action_text_introspector.rb +1 -2
  63. data/lib/rails_ai_context/introspectors/active_record_settings.rb +1 -1
  64. data/lib/rails_ai_context/introspectors/active_storage_introspector.rb +1 -3
  65. data/lib/rails_ai_context/introspectors/active_support_introspector.rb +3 -3
  66. data/lib/rails_ai_context/introspectors/admin_resources.rb +1 -1
  67. data/lib/rails_ai_context/introspectors/anyway_configs.rb +1 -1
  68. data/lib/rails_ai_context/introspectors/api_introspector.rb +3 -3
  69. data/lib/rails_ai_context/introspectors/auth_introspector.rb +15 -10
  70. data/lib/rails_ai_context/introspectors/call_site_expansion.rb +24 -1
  71. data/lib/rails_ai_context/introspectors/component_introspector.rb +3 -2
  72. data/lib/rails_ai_context/introspectors/controller_introspector.rb +387 -60
  73. data/lib/rails_ai_context/introspectors/convention_introspector.rb +1 -1
  74. data/lib/rails_ai_context/introspectors/database_stats_introspector.rb +15 -3
  75. data/lib/rails_ai_context/introspectors/devops_introspector.rb +3 -1
  76. data/lib/rails_ai_context/introspectors/eager_load.rb +201 -9
  77. data/lib/rails_ai_context/introspectors/engine_introspector.rb +1 -1
  78. data/lib/rails_ai_context/introspectors/env_config_introspector.rb +1 -1
  79. data/lib/rails_ai_context/introspectors/env_references.rb +10 -1
  80. data/lib/rails_ai_context/introspectors/frontend_framework_introspector.rb +1 -1
  81. data/lib/rails_ai_context/introspectors/gem_introspector.rb +23 -10
  82. data/lib/rails_ai_context/introspectors/grape_endpoints.rb +1 -1
  83. data/lib/rails_ai_context/introspectors/habtm_join_tables.rb +1 -1
  84. data/lib/rails_ai_context/introspectors/helper_names.rb +1 -1
  85. data/lib/rails_ai_context/introspectors/i18n_introspector.rb +3 -3
  86. data/lib/rails_ai_context/introspectors/job_introspector.rb +8 -6
  87. data/lib/rails_ai_context/introspectors/listeners/base_listener.rb +12 -8
  88. data/lib/rails_ai_context/introspectors/listeners/callbacks_listener.rb +78 -4
  89. data/lib/rails_ai_context/introspectors/listeners/config_assignment_listener.rb +1 -0
  90. data/lib/rails_ai_context/introspectors/listeners/env_access_listener.rb +10 -1
  91. data/lib/rails_ai_context/introspectors/listeners/foreign_evals_listener.rb +61 -0
  92. data/lib/rails_ai_context/introspectors/listeners/macros_listener.rb +13 -4
  93. data/lib/rails_ai_context/introspectors/listeners/migration_dsl_listener.rb +2 -1
  94. data/lib/rails_ai_context/introspectors/listeners/migration_replay_listener.rb +3 -2
  95. data/lib/rails_ai_context/introspectors/listeners/routes_dsl_listener.rb +29 -8
  96. data/lib/rails_ai_context/introspectors/listeners/schema_dsl_listener.rb +2 -2
  97. data/lib/rails_ai_context/introspectors/listeners/scopes_listener.rb +88 -12
  98. data/lib/rails_ai_context/introspectors/middleware_introspector.rb +2 -2
  99. data/lib/rails_ai_context/introspectors/migration_introspector.rb +27 -0
  100. data/lib/rails_ai_context/introspectors/migration_replay/helpers.rb +1 -1
  101. data/lib/rails_ai_context/introspectors/migration_replay/statements.rb +83 -27
  102. data/lib/rails_ai_context/introspectors/migration_replay.rb +15 -14
  103. data/lib/rails_ai_context/introspectors/model_introspector.rb +231 -23
  104. data/lib/rails_ai_context/introspectors/n_plus_one_scan.rb +620 -0
  105. data/lib/rails_ai_context/introspectors/performance_introspector.rb +11 -174
  106. data/lib/rails_ai_context/introspectors/route_introspector.rb +10 -1
  107. data/lib/rails_ai_context/introspectors/schema_conventions.rb +23 -3
  108. data/lib/rails_ai_context/introspectors/schema_introspector.rb +66 -16
  109. data/lib/rails_ai_context/introspectors/schema_reader.rb +4 -3
  110. data/lib/rails_ai_context/introspectors/seeds_introspector.rb +1 -1
  111. data/lib/rails_ai_context/introspectors/source_scan.rb +39 -9
  112. data/lib/rails_ai_context/introspectors/stimulus_introspector.rb +15 -6
  113. data/lib/rails_ai_context/introspectors/test_introspector.rb +1 -1
  114. data/lib/rails_ai_context/introspectors/turbo_introspector.rb +131 -12
  115. data/lib/rails_ai_context/introspectors/view_introspector.rb +4 -4
  116. data/lib/rails_ai_context/introspectors/view_template_introspector.rb +15 -1
  117. data/lib/rails_ai_context/legacy_cleanup.rb +17 -4
  118. data/lib/rails_ai_context/live_reload.rb +20 -20
  119. data/lib/rails_ai_context/mcp_config_generator.rb +916 -109
  120. data/lib/rails_ai_context/mcp_edge.rb +182 -32
  121. data/lib/rails_ai_context/middleware.rb +5 -1
  122. data/lib/rails_ai_context/migration_status.rb +88 -25
  123. data/lib/rails_ai_context/output_guard.rb +48 -2
  124. data/lib/rails_ai_context/package_json.rb +1 -1
  125. data/lib/rails_ai_context/path_resolver.rb +46 -13
  126. data/lib/rails_ai_context/payload.rb +92 -8
  127. data/lib/rails_ai_context/pending_migrations.rb +5 -1
  128. data/lib/rails_ai_context/portable_path.rb +3 -2
  129. data/lib/rails_ai_context/presets.rb +15 -11
  130. data/lib/rails_ai_context/redaction.rb +262 -51
  131. data/lib/rails_ai_context/resources.rb +55 -20
  132. data/lib/rails_ai_context/route_coverage.rb +8 -0
  133. data/lib/rails_ai_context/safe_file.rb +14 -1
  134. data/lib/rails_ai_context/safe_path.rb +101 -4
  135. data/lib/rails_ai_context/schema_adapter.rb +8 -0
  136. data/lib/rails_ai_context/schema_version.rb +9 -0
  137. data/lib/rails_ai_context/serializers/claude_rules_serializer.rb +88 -33
  138. data/lib/rails_ai_context/serializers/compact_serializer_helper.rb +36 -34
  139. data/lib/rails_ai_context/serializers/context_file_serializer.rb +21 -29
  140. data/lib/rails_ai_context/serializers/copilot_instructions_serializer.rb +8 -6
  141. data/lib/rails_ai_context/serializers/copilot_serializer.rb +2 -7
  142. data/lib/rails_ai_context/serializers/cursor_rules_serializer.rb +19 -16
  143. data/lib/rails_ai_context/serializers/guide_examples.rb +142 -0
  144. data/lib/rails_ai_context/serializers/markdown_serializer.rb +146 -30
  145. data/lib/rails_ai_context/serializers/opencode_rules_serializer.rb +38 -15
  146. data/lib/rails_ai_context/serializers/section_facts.rb +44 -1
  147. data/lib/rails_ai_context/serializers/section_marker_writer.rb +94 -12
  148. data/lib/rails_ai_context/serializers/stack_overview_helper.rb +24 -12
  149. data/lib/rails_ai_context/serializers/tool_guide_helper.rb +244 -129
  150. data/lib/rails_ai_context/server.rb +304 -71
  151. data/lib/rails_ai_context/tasks/rails_ai_context.rake +447 -372
  152. data/lib/rails_ai_context/tools/analyze_feature.rb +72 -18
  153. data/lib/rails_ai_context/tools/argument_schema.rb +49 -0
  154. data/lib/rails_ai_context/tools/base_tool.rb +367 -46
  155. data/lib/rails_ai_context/tools/brakeman_guard.rb +24 -0
  156. data/lib/rails_ai_context/tools/dependency_graph.rb +63 -28
  157. data/lib/rails_ai_context/tools/diagnose.rb +218 -11
  158. data/lib/rails_ai_context/tools/generate_test.rb +339 -39
  159. data/lib/rails_ai_context/tools/get_callbacks.rb +24 -13
  160. data/lib/rails_ai_context/tools/get_component_catalog.rb +1 -1
  161. data/lib/rails_ai_context/tools/get_concern.rb +91 -27
  162. data/lib/rails_ai_context/tools/get_config.rb +13 -4
  163. data/lib/rails_ai_context/tools/get_context.rb +71 -14
  164. data/lib/rails_ai_context/tools/get_controllers.rb +179 -44
  165. data/lib/rails_ai_context/tools/get_conventions.rb +28 -6
  166. data/lib/rails_ai_context/tools/get_edit_context.rb +27 -14
  167. data/lib/rails_ai_context/tools/get_env.rb +18 -9
  168. data/lib/rails_ai_context/tools/get_frontend_stack.rb +3 -1
  169. data/lib/rails_ai_context/tools/get_gems.rb +1 -1
  170. data/lib/rails_ai_context/tools/get_helper_methods.rb +10 -6
  171. data/lib/rails_ai_context/tools/get_i18n.rb +1 -1
  172. data/lib/rails_ai_context/tools/get_job_pattern.rb +3 -2
  173. data/lib/rails_ai_context/tools/get_mailers.rb +3 -3
  174. data/lib/rails_ai_context/tools/get_model_details.rb +30 -10
  175. data/lib/rails_ai_context/tools/get_partial_interface.rb +242 -23
  176. data/lib/rails_ai_context/tools/get_routes.rb +29 -8
  177. data/lib/rails_ai_context/tools/get_schema.rb +52 -19
  178. data/lib/rails_ai_context/tools/get_service_pattern.rb +24 -10
  179. data/lib/rails_ai_context/tools/get_stimulus.rb +37 -1
  180. data/lib/rails_ai_context/tools/get_test_info.rb +18 -4
  181. data/lib/rails_ai_context/tools/get_turbo_map.rb +221 -58
  182. data/lib/rails_ai_context/tools/get_view.rb +14 -14
  183. data/lib/rails_ai_context/tools/migration_advisor.rb +355 -48
  184. data/lib/rails_ai_context/tools/onboard.rb +52 -8
  185. data/lib/rails_ai_context/tools/performance_check.rb +5 -3
  186. data/lib/rails_ai_context/tools/query.rb +914 -86
  187. data/lib/rails_ai_context/tools/read_logs.rb +69 -15
  188. data/lib/rails_ai_context/tools/review_changes.rb +215 -53
  189. data/lib/rails_ai_context/tools/runtime_info.rb +51 -1
  190. data/lib/rails_ai_context/tools/safe_call.rb +35 -14
  191. data/lib/rails_ai_context/tools/search_code.rb +243 -67
  192. data/lib/rails_ai_context/tools/search_docs.rb +4 -4
  193. data/lib/rails_ai_context/tools/security_scan.rb +143 -33
  194. data/lib/rails_ai_context/tools/session_context.rb +39 -5
  195. data/lib/rails_ai_context/tools/validate.rb +259 -40
  196. data/lib/rails_ai_context/tools/validate_semantics.rb +130 -56
  197. data/lib/rails_ai_context/version.rb +1 -1
  198. data/lib/rails_ai_context/vfs.rb +67 -40
  199. data/lib/rails_ai_context/view_file.rb +41 -3
  200. data/lib/rails_ai_context/watcher.rb +51 -13
  201. data/lib/rails_ai_context.rb +21 -0
  202. data/server.json +1 -1
  203. metadata +14 -1
data/CHANGELOG.md CHANGED
@@ -5,6 +5,1497 @@ 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.34.0] - 2026-10-12
9
+
10
+ ### Security
11
+
12
+ - **A tracked log's diff hides its credentials.** `rails_review_changes`
13
+ read a `.log` file's diff as source, which filters a Bearer or Basic value
14
+ only at 16 characters, so a short one showed in clear. A log file shown as
15
+ a file, logrotate's dated and `.old` ones and a `.log.bak` included, now
16
+ gets what `rails_read_logs` filters.
17
+ - **A Digest or Token Authorization header is filtered with all its
18
+ parameters.** A Digest header in a log kept its `response=` hash, also
19
+ inside an inspected hash whose value escapes its quotes, and a Token
20
+ header kept every parameter after `token=` (and came out as
21
+ `Authorization: [FILTERED]]` when unquoted).
22
+ - **An Authorization value after an unquoted name and `=` or `=>` is
23
+ filtered.** A log line from an env dump (`HTTP_AUTHORIZATION=Basic ...`)
24
+ or a debug line (`Authorization => Bearer ...`) kept a short credential in
25
+ clear, in `rails_read_logs` and in a tracked log's diff. A switch's value
26
+ (`skip_authorization = true`) stays, and in a query string only the value
27
+ goes.
28
+
29
+ ### Added
30
+
31
+ - **A static model answer names the plugin patch modules it did not
32
+ read.** An app plugin that adds to a core model through its own DSL
33
+ (OpenProject's `patches %i[WorkPackage]`, which includes
34
+ `<Plugin>::Patches::WorkPackagePatch` in `to_prepare`) gave a
35
+ `rails_get_model_details` answer with none of what the patches add and no
36
+ sign anything was left out. A module named `*::Patches::<Model>Patch` in
37
+ the app's, its code roots' or its in-repo gems' `lib` is now named in one
38
+ `[UNAVAILABLE]` line, the way `rails_get_controllers` names an included
39
+ module it did not read.
40
+ - **`rails_get_model_details` names each `has_attached_file` attachment
41
+ under Macros.**
42
+ - **The controller listing marks `permit!` and names a parent's strong
43
+ params.** The listing marks `permit!` as the per-action view did, and
44
+ strong params a parent controller in the app defines are listed with the
45
+ parent named, unless the controller defines an instance method of that
46
+ name itself.
47
+
48
+ ### Changed
49
+
50
+ - **Notable gems are the ones the app names, and a Rails framework only when
51
+ the app loads it.** Every lockfile entry counted, so `onboard`, the gems
52
+ tool and the context files listed `activestorage` for an app that leaves
53
+ `active_storage/engine` commented out (it is in the lock because `rails`
54
+ pulls it in) and `jwt` for an app that only has it through oauth2 and
55
+ googleauth. A notable gem now counts when the Gemfile names it, when a
56
+ gemspec or path gem the Gemfile pulls in depends on it, when it is a Rails
57
+ engine or OmniAuth that another gem brings in (gds-sso mounts OmniAuth in
58
+ the app), or, for a framework `rails` pulls in, when
59
+ config/application.rb loads it (`rails/all`, its railtie, or a
60
+ `config.<framework>` line). Rails 6's `sprockets-rails` is one of those
61
+ frameworks; on Rails 7 and later a gem that pulls it in loads it, so it
62
+ counts.
63
+ - **Static routes leave out a block from a routing method they cannot read,
64
+ and say so.** A route drawn inside a method the app defines (a helper
65
+ that wraps `scope(path: "api/v1", module: "api/v1")`, versionist's
66
+ `api_version`) was drawn at the top level, so `rails_get_routes` gave a
67
+ two-route controller seven routes it does not have and gave the namespaced
68
+ controller none. Such a block is now counted among the constructs not
69
+ expanded, and a controller with no route says one may be missing behind
70
+ them. Blocks that draw in place are drawn as before: Rails'
71
+ `constraints` and `defaults`, Devise's `devise_scope` and
72
+ `authenticate`, and route_translator's `localized`; Rails' `nested` block
73
+ is drawn under its parent's path.
74
+ - **Strong params are the methods that return permitted params.** Any `def
75
+ *_params` was listed, so a method that builds associations counted while
76
+ `article_params_json`, which calls `permit`, did not. A method named
77
+ `*_params` now counts when it calls `permit`, `permit!` or `expect` on
78
+ params (directly, through a local or an ivar, on each item a block walks,
79
+ or on `ActionController::Parameters.new`), or when what it returns comes
80
+ from `super` (`super.merge(...)`), another strong params method (a call
81
+ runs the nearest class's method of that name, so one the controller or a
82
+ parent class defines counts whatever its name, such as Mastodon's
83
+ `params_slice`; any other named `*_params` or `permitted_*`, such as a
84
+ concern's or Pundit's `permitted_attributes(@post)`; and Devise's
85
+ `devise_parameter_sanitizer`), or a call given the whole params
86
+ (`PermittedAttributes::Enterprise.new(params).call`); a `before_action`
87
+ that writes into another one's params is not one. A method of any other
88
+ name counts only when it returns the permitted params themselves, through
89
+ a local, an `if`, a `case` or an explicit `return`, so a helper that
90
+ permits to build a URL, or to read one value, is left out. An action that
91
+ permits inline is left out, a public `*_params` helper is kept, `permit!`
92
+ on a `slice` lists the keys it slices, a computed filter
93
+ (`settings_attributes: UserSettings.keys`) is shown as written, not as a
94
+ scalar, and a key `require` computes is not printed as one.
95
+
96
+ ### Fixed
97
+
98
+ - **A primary key's function default shows.** `id: :uuid, default: -> {
99
+ "gen_random_uuid()" }` on a `create_table` line gave the `id` row an empty
100
+ Default in `rails_get_schema` and `rails_get_model_details`, booted and
101
+ static, which reads as "no default". The booted tier now reads a column's
102
+ `default_function`, leaving out only a serial column's own sequence as
103
+ the dump does, and schema.rb, structure.sql and the migration replay give
104
+ the key the default its line declares.
105
+ - **Two bare `validates_with` lines are both listed.** The second one was
106
+ dropped because the two shared a dedup key with no validator class in it.
107
+ - **A `validates_with` class a concern nests is listed once.** The source
108
+ name `DateValidator` never matched the class Ruby resolves,
109
+ `DateValidation::DateValidator`, so reflection listed it again and the
110
+ validation count was one too high.
111
+ - **A model whose table the database lacks is not marked verified.**
112
+ `rails_get_model_details` printed `[VERIFIED]` and `**Table:** \`tags\``
113
+ for a model with no `tags` table, and `onboard` and the context files
114
+ named the table too. Each now says `` `tags` not found `` where a live
115
+ listing lacks the table, and the tool guide's examples pass over such a
116
+ model. A database read from its dump, a secondary one included, is not
117
+ taken at its word, since a dump can leave out a view, and nor is the
118
+ primary's listing for a model on a connection of its own.
119
+ - **Callbacks on a chain a library defines are listed.** Paperclip's
120
+ `before_file_validate` and `after_post_process` were missing from
121
+ `rails_get_model_details` and `rails_get_callbacks`, booted too, because
122
+ only Active Record's own callback names were read. A call on a chain the
123
+ booted model really has, or one `define_model_callbacks` in the same file
124
+ declares, is now listed.
125
+ - **A macro inside `translation_class.class_eval` is not the model's own.**
126
+ It is listed under "Declared on another class", as one in a concern's
127
+ `translation_class.instance_eval` already was; two declared on one line
128
+ are both listed, and a modifier's condition is not listed as one.
129
+ - **The static tier keeps a model whose parent comes from a gem.**
130
+ `class Tag < ActsAsTaggableOn::Tag` was "Model 'Tag' not found". A class
131
+ in app/models on a parent the app does not define is now a model when a
132
+ database's dump holds a table it would read, one of the parent's names
133
+ (`ahoy_messages` for `EmailMessage < Ahoy::Message`) or its own, the
134
+ primary's first, and, with no schema to ask or another database's dump
135
+ not on disk, when it declares associations or scopes. Wherever the schema
136
+ can be read, an ActiveHash class that declares `has_many` stays out, and a
137
+ class on a framework class (`ActiveRecord::Relation`) is never one. The
138
+ table comes from the parent as Rails' single table inheritance decides, or
139
+ a line says the parent sets it.
140
+ - **A filtered routes answer names the other paths an engine is mounted
141
+ at.** An engine mounted three times showed only its first mount and that
142
+ mount's helpers.
143
+ - **Migration replay applies `change_column`'s options.** `null:`,
144
+ `default:`, `limit:`, `precision:` and `scale:` given to `change_column`
145
+ were dropped, so a column relaxed to `null: true` stayed NOT NULL. The
146
+ replay now records the sizes a migration declares as the dump writes
147
+ them: an integer limit picks smallint or bigint on PostgreSQL and MySQL,
148
+ a datetime keeps any precision but the default 6, MySQL sizes a text or
149
+ blob by its limit, and a size the dump leaves out stays out. A type-only
150
+ `change_column` keeps null and default on PostgreSQL and MySQL and resets
151
+ them on SQLite, as each adapter does.
152
+ - **Migration replay keeps a partial index's condition.** A Symbol
153
+ `where: :schedule_manually` and the `where:` of a `t.references ...,
154
+ index: { where: ... }` were dropped, so a partial index read as a full one.
155
+ An index's `using:`, `order:`, `opclass:` and the rest are kept too.
156
+ - **A gem installed under the app root is not the app's code.** With the
157
+ bundle in vendor/bundle, `onboard` listed a gem's ActiveJob class
158
+ (`Chewy::Strategy::ActiveJob::Worker`) as the app's background job.
159
+ - **The engine works in an app with an `AI` or `MCP` acronym.** The app's
160
+ loader and router camelize with the app's inflections: `inflect.acronym
161
+ "AI"` made Zeitwerk expect `RailsAIContext::McpController` and
162
+ `inflect.acronym "MCP"` `RailsAiContext::MCPController`, so
163
+ `bin/rails zeitwerk:check` and any eager load failed once the gem was in
164
+ the Gemfile; the mounted endpoint answered with an error (a 404 on Rails
165
+ 7.0 after the first request), and a production boot with eager loading
166
+ failed. The controller is now kept out of the app's loader and routed as
167
+ a class, which `bin/rails routes` shows as an inline handler.
168
+ - **`rails ai:context` runs in an app with its own top-level
169
+ `print_result`.** The gem's rake helpers were top-level methods guarded by
170
+ `unless defined?`, so an app's `print_result` in lib/tasks ran instead and
171
+ the task aborted. They now live in a module.
172
+ - **A byte-size label does not depend on the app's I18n.**
173
+ `rails_read_logs` threw its whole answer away when an app's I18n backend
174
+ raised after a failed boot (Discourse's backend queries the database),
175
+ and `rails_runtime_info` and `rails_get_test_info` could do the same.
176
+ - **The static fallback loads after a Bundler.setup that failed part way.**
177
+ An app that activates a gem before `bundler/setup` (Consul's
178
+ `require "sassc-embedded"`) at a version its lock does not pin made
179
+ Bundler raise after it had marked activesupport loaded but before it added
180
+ the load paths, so the standalone CLI and `serve` died with `cannot load
181
+ such file -- active_support/core_ext/string/inflections` instead of
182
+ serving static answers.
183
+ - **The controller count is the app's own, the same from rake and from the
184
+ generator.** A gem's controller was listed whenever something had loaded
185
+ it, which flipflop's dashboard does in a generator boot and not in a rake
186
+ one, so the install wrote "Controllers (99)" and `rails ai:context` wrote
187
+ "(97)" and `ai:doctor` called the files stale. A `*_controller.rb` that
188
+ declares only a module was listed as a controller too, and one that names
189
+ another controller (`GoogleController = OAuth2Controller`) is listed as
190
+ its alias, with its actions.
191
+ - **A module in app/policies is not a policy.** `onboard` counted
192
+ `module Authorizer`, which nests a helper class, among the Pundit
193
+ policies. A policy is now the class its file is named for.
194
+ - **`rails_review_changes` does not count the install's context files as
195
+ code.** `app/models/AGENTS.md` was "1 model" and
196
+ `app/controllers/AGENTS.md` "1 controller".
197
+ - **Statements on one line replay in source order on every platform.** Ruby's
198
+ sort is not stable, so with a dozen statements in a migration a one-line
199
+ `change_table(:t) { |t| t.change ... }` could apply its `t.change` before
200
+ the block opened and lose it, and on Windows it did. A route with many
201
+ paths drew them in the same unordered way.
202
+ - **Ties keep their order in every list.** Where two entries sort equal, the
203
+ answer depended on the platform's sort: `onboard`'s key flows and
204
+ `rails_get_conventions`' key directories could name different controllers
205
+ and folders, `rails_validate` named a different five Brakeman warnings,
206
+ `rails_search_docs` a different top five, and helpers, tables, queues,
207
+ N+1 risks, security warnings, SQLite table stats, shared examples and a
208
+ session's queries came out shuffled. A one-line `create_table` in a
209
+ schema.rb that also holds a `# Could not dump table` comment gave its
210
+ columns to the wrong table.
211
+
212
+ ## [5.33.0] - 2026-10-11
213
+
214
+ ### Security
215
+
216
+ - **Redaction filters a secret passed as a flag, set under a dashed header
217
+ name, or sent after an auth scheme.** Every tool that shows the app's
218
+ source returned `mysql --password=...`, `-- --token=...` and
219
+ `redis-server --requirepass=...` with the value in place, since a dash
220
+ before the name hid it; `{ "X-Api-Key" => "..." }`,
221
+ `req["X-Api-Key"] = "..."` and `"x-auth-token": "..."`, since the dashes
222
+ hid the secret word; and `conn.headers["Authorization"] = "Bearer ..."`
223
+ and `{ "Authorization" => "Basic ..." }`, since "Authorization" is no
224
+ secret word. A flag's value is now filtered as any `NAME=value` is, with
225
+ Redis's `requirepass` and `masterauth` among the password names and a
226
+ regex that names the flag (`/--password=.+/`) left as code; a quoted name
227
+ joined by dashes reads as the name it spells with underscores, so
228
+ `"Content-Type"` and `"X-Request-Id"` stay; and the credential after
229
+ `Bearer`, `Basic` or `Token` goes, the scheme kept, when it is 16 or more
230
+ letters and digits, both, read past a UUID's dashes and base64's `+`,
231
+ `/` and `=`, so `Bearer #{token}`, `Bearer YOUR_TOKEN` and
232
+ `Basic realm=...` stay. `rails_read_logs` kept a `Basic` credential after
233
+ `Authorization: `, the whole header when it was quoted, a hash or JSON
234
+ key or `HTTP_AUTHORIZATION`, and a dashed header key's value in a logged
235
+ hash (`{"X-Api-Key"=>"..."}`); each is filtered now. A Stimulus target,
236
+ action, controller, outlet or class is read as a descriptor, so
237
+ `data: { password_visibility_target: "input" }` keeps its value.
238
+ - **Redaction filters the password Postgres, mysql and redis-cli read from
239
+ the environment, a camel-cased secret name, and a flag's secret given as
240
+ the next word.** A password word counted only as a part of a name between
241
+ underscores, a secret word only in snake case, and a flag's value only
242
+ after `=`, so `PGPASSWORD: ...` in a compose file, `PGPASSWORD=... psql`
243
+ in a script, `ENV["PGPASSWORD"] = "..."`, `MYSQL_PWD` and
244
+ `REDISCLI_AUTH`, `const accessToken = "..."`, `{"clientSecret": "..."}`,
245
+ `secretAccessKey: "..."` and `redis-server --requirepass ...` in a
246
+ Procfile kept their values, in source and in a log line alike. A long
247
+ password word that ends a name now reads with a name joined on before it
248
+ (`PGPASSWORD`, `dbpassword`, `userPassword`; not `ONEPASSWORD_VAULT`),
249
+ the two client variables as password names, a camel-cased name with
250
+ `Secret` or `Token` among its words as a secret's (`accessToken`,
251
+ `jwtSecretKey`), as `apiKey` already was a key's, and the word after a
252
+ secret-named flag, bare or quoted, as its value when it has a digit. So
253
+ `the --password option`, `--token TOKEN`, `--password $DB_PASSWORD`, a
254
+ `--no-password` switch, a path (`--private-key ~/.ssh/id_ed25519`), a
255
+ file or an id (`--token-file`, `--secret-id`) and BuildKit's
256
+ `--secret id=...,env=...` stay as written, as do a switch's value
257
+ (`showPassword=false`) and a value that is its key's own name
258
+ (`currentPassword: "current_password"`).
259
+ - **Redaction reads a long word in time that grows with its length, not its
260
+ square.** A call's receiver was tried from every character of a run of
261
+ letters, digits and dots, so filtering a line holding a 16 KB hex string
262
+ took 1.5 to 2 seconds on Ruby 3.1 and 3.2 and a 64 KB one over 20; on
263
+ Ruby 3.3 and later a 1 MB one took 2.5 seconds, past the one second a
264
+ Rails 8 app gives a regular expression, and the tool answered with that
265
+ error. A name was split at every underscore of a long word holding a
266
+ secret word in many of its parts, and a path check retried from every
267
+ slash. The receiver is now read from where its run starts, a name is taken
268
+ whole, and a path's first segment at once: each of those lines takes a
269
+ fraction of a second.
270
+ - **`rails_get_view` filters the secrets in a controller's full listing.**
271
+ `controller:"posts", detail:"full"` printed each template and partial as
272
+ written, `<% api_key = "..." %>` and a `render` call's `token: "..."`
273
+ among them, while the same template read by `path:` and every layout had
274
+ them filtered. Each file in the listing is now filtered as one read by
275
+ path is, a mailer's views (`controller:"user_mailer"`) included.
276
+ - **`rails_security_scan` shows a warning's code as the file has it.** At
277
+ `detail:"full"` the code was Brakeman's own line, which writes the value
278
+ of each variable and constant it can follow in place of the name: a token
279
+ assigned to `api_token` on the line above, or to a constant in an
280
+ initializer, sat in the `system(...)` call as a bare literal, with no name
281
+ left for the redaction to know it by. The answer now shows the statement
282
+ the warning names, read from its file and filtered as every other slice of
283
+ the app's source is: a call written across lines whole, up to 12 lines,
284
+ and a template's line fenced as the template it is. A warning in a file
285
+ the tools may not read shows no code.
286
+ - **`rails_search_code` in trace mode checks its `path` like every other
287
+ mode.** `match_type: "trace"` joined the path to the root unchecked, so
288
+ `path: "../../elsewhere"` searched outside the app and printed the source
289
+ it found there.
290
+ - **A directory linked in from outside the app's git repository is not
291
+ read.** A walk or a pack follows a directory link only into the work tree
292
+ holding the app (into the app root outside a repository); a pack linked
293
+ from anywhere, a home directory included, used to be read.
294
+ - **`rails_review_changes` hands git a commit, never the caller's ref.**
295
+ The ref went to `git diff` and `git log` as an argument, and one that
296
+ starts with a dash is read as an option: `ref: "--output=<path>"` made
297
+ the read-only tool write the diff to any path the caller named. A ref now
298
+ has to name a commit, and git sees that commit's SHA. A ref that starts
299
+ with a dash is refused, and one that names no commit says so, where it
300
+ answered "No changes found".
301
+ - **`rails_review_changes` filters the secrets in the diffs it shows, and
302
+ shows no line of a sensitive file.** Each file's diff was printed as git
303
+ gave it, so a key, token or password a change added or took out reached
304
+ the caller whole, as no other tool's source did, and so did every line of
305
+ a changed `config/database.yml`, `.env` or key file. A diff's lines now go
306
+ through the same redaction, read as the file they come from, a hunk
307
+ header's line and a removed validation's included, and a file on
308
+ `sensitive_patterns` is listed with its line counts alone.
309
+ - **Redaction reads a secret a YAML or JSON file names on another line,
310
+ and a YAML key written as a symbol.** Heroku's app.json gives a
311
+ variable's value under its name (`"STRIPE_SECRET_KEY": { "value": "..." }`),
312
+ render.yaml and a Kubernetes manifest under a sibling (`- key:
313
+ CLIENT_SECRET` over `value: ...`), and config/sidekiq.yml writes its keys
314
+ as symbols (`:password: ...`); every tool that shows a file kept each of
315
+ those values. In a YAML or JSON file read whole, a `value` under a
316
+ mapping a secret's name names is now filtered, a boolean and the next
317
+ entry's value kept, and a symbol key reads as its name.
318
+ - **The gems section names a git source without its credentials.** A
319
+ Gemfile's `git: "https://user:token@host/..."` went into the gems answer,
320
+ the `rails://gems` resource and `.ai-context.json` as written. The URL now
321
+ passes through the redaction a source line does, so its userinfo, or a
322
+ token format in its place, reads `[FILTERED]`.
323
+ - **The context file names a Procfile's commands with their secrets
324
+ filtered.** Each command went into the DevOps section as written, so
325
+ `redis-server --requirepass ...` or `DB_PASSWORD=... bundle exec sidekiq`
326
+ reached `.ai-context.json`, which `rails ai:context` asks the team to
327
+ commit. A command now passes through the redaction a Procfile line read by
328
+ any tool does.
329
+ - **`rails_get_edit_context` names an endless method without its body, and
330
+ looks for `near` in what it shows.** The line above the code quoted the
331
+ enclosing method's line as written, and an endless method's line is its
332
+ whole body: `def headers = { "Authorization" => "Bearer ..." }` printed
333
+ the credential the code under it had filtered. And `near` was looked for
334
+ in the file as written, so a guess at a secret's start brought back the
335
+ window where a wrong guess said "not found". The label is now built from
336
+ the filtered lines, an endless method named alone, and `near` is matched
337
+ against the lines as they are returned.
338
+ - **`rails_query` never serialises a whole row.** On PostgreSQL,
339
+ `row_to_json(users.*)`, `to_json(u)`, `u::text`, `array_agg(u)`,
340
+ `json_agg(u)`, `hstore(u)`, `ROW(u.*)`, `format('%s', u)` and a bare
341
+ `SELECT u FROM users u` returned every column, password digests and
342
+ tokens included, since no column name appeared for the checks to catch.
343
+ A query is now planned first with `EXPLAIN (VERBOSE, FORMAT JSON)` in the
344
+ same read-only transaction - planning only, nothing runs - and refused
345
+ when the plan reads a whole row of a table that has a sensitive column,
346
+ or uses a sensitive column other than as a plain output column. A
347
+ whole-row read of a table with no sensitive column still works.
348
+ - **A sensitive column cannot leave through an alias, an expression or a
349
+ view.** `SELECT api_token AS c`, `upper(api_token)`,
350
+ `json_build_object('t', api_token)` and `(SELECT api_token FROM users
351
+ LIMIT 1) AS leaked` reached the result under a name the redaction did not
352
+ recognise. One rule now decides what is sensitive - a configured or
353
+ built-in redacted name, an `encrypts` column, or the result heuristic,
354
+ minus `query_allowed_columns` - and a query naming any real sensitive
355
+ column of the schema is refused before it runs, on every adapter, as is
356
+ a column-alias list (`t(a, b, c)`) that would rename a wildcard's
357
+ columns. Rails' own bookkeeping tables are left out of that set, so
358
+ `ar_internal_metadata.key` does not make the word `key` refused, and on
359
+ PostgreSQL a name inside a string literal (`LIKE '%secret%'`, a JSON key)
360
+ is data, allowed only when the query's plan was read and cleared. On PostgreSQL the result is also redacted by where each column
361
+ comes from, so a view that renames `api_token` to `t` returns
362
+ `[FILTERED]`, and a sensitive column passed through a materialized CTE, a
363
+ subquery or a set operation, where its origin cannot be pinned, is
364
+ refused.
365
+ - **`rails_query` refuses every spelling of UNION.** The check matched
366
+ only `UNION SELECT` and `UNION ALL SELECT`; MySQL and MariaDB also take
367
+ `UNION DISTINCT SELECT` and a parenthesised `UNION (SELECT ...)`, and
368
+ either one returned another table's rows under the first SELECT's
369
+ column names - a password digest under `posts.user_id` - past the
370
+ redaction that reads names. Any UNION is refused now, on every adapter,
371
+ with that reason, where it was called an "SQL injection pattern".
372
+ - **Session- and server-affecting functions no longer run under the
373
+ read-only transaction.** `pg_terminate_backend`, `pg_reload_conf`,
374
+ `pg_stat_reset*`, `pg_advisory_lock*`, `set_config`, `pg_notify`,
375
+ `txid_current`, the replication and WAL-control functions, and MySQL's
376
+ `GET_LOCK` / `RELEASE_LOCK` / `IS_FREE_LOCK` passed `SET TRANSACTION READ
377
+ ONLY` and left their effect - a terminated backend, a held advisory lock -
378
+ on the pooled connection. A name list refuses the known ones on every
379
+ adapter, and on PostgreSQL any VOLATILE function outside a short harmless
380
+ list (`random`, `clock_timestamp`, `gen_random_uuid`, `pg_sleep` and its
381
+ variants) is refused from the plan, closing the family without a list
382
+ that has to grow.
383
+ - **The endpoint inside the app refuses outside development and test
384
+ unless the app opts in.** The mounted engine and `auto_mount` answered
385
+ every tool to whoever could reach a production or staging app. Both now
386
+ answer 403 with a JSON-RPC error that names the environment and the
387
+ setting, and log the refusal once per process;
388
+ `config.allow_http_in_production = true` lets them answer, for an
389
+ endpoint behind the app's own authentication. Doctor's "MCP HTTP
390
+ endpoint" check fails `auto_mount` with it on and warns for the engine.
391
+ - **No walk follows a symlink out of the app.** `rails_get_stimulus`
392
+ parsed a controller linked in from outside the app and printed its
393
+ targets and values, and so did the view, layout, partial, helper,
394
+ JavaScript, locale, environment-file, seed and concern scans and the
395
+ booted source reads. A file whose real path is outside the app is now
396
+ neither listed nor read, unless it sits in a directory the app links in,
397
+ such as a pack.
398
+ - **A log, partial or concern refused on policy is an error result.**
399
+ `rails_read_logs file:`, `rails_get_partial_interface partial:` and
400
+ `rails_get_concern name:` answered "not found" for a file that links out
401
+ of the app, and their lists still offered it. They now answer `Path not
402
+ allowed: <name>` with `isError: true` (exit 1 on the CLI), no list offers
403
+ a refused entry, and `rails_diagnose` returns its diagnosis for a refused
404
+ `file:` as an error result.
405
+
406
+ ### Added
407
+
408
+ - **`rails-ai-context init` sets up a folder of apps.** Run in a folder that
409
+ is no app but holds apps one or two levels down - the folder an editor or
410
+ agent is often opened at - it asks once and sets up every app: each keeps
411
+ its own `.rails-ai-context.yml` and context files, generated by the install
412
+ that app's server runs, and the folder's MCP configs get one server per
413
+ app, `rails-ai-context-<app folder>`, pointed at it with `--app-path`
414
+ (#429). Paths stay relative so a committed config works on every machine,
415
+ spelled from `${workspaceFolder}` for Cursor and VS Code, which name the
416
+ folder their config sits in. Each app keeps its own install form, decided
417
+ from its own Gemfile.lock, and an in-Gemfile app's entry sets
418
+ `BUNDLE_GEMFILE` to the Gemfile its bundle reads: its own (a linked one by
419
+ the link's name), else the shared one its `config/boot.rb` names, else the
420
+ one `bundle exec` run inside the app would find. A bare
421
+ `rails-ai-context` entry left in the folder is replaced, a re-run drops
422
+ the entry of an app whose folder is gone or that the gem renamed, removal
423
+ and cleanup take every `rails-ai-context-*` entry that runs the gem's
424
+ server (a hand-made one that does not stays, and so does an entry with no
425
+ command, an HTTP one, under either name), `--app-path` naming a folder of
426
+ apps sets that folder up, and `doctor` inside an app finds the folder's
427
+ config one or two levels up, says when one there does not parse, and
428
+ checks each Codex env snapshot in it. `init` warns when an app declares a
429
+ Ruby other than the one the folder runs. See ADR-0005 for what each AI
430
+ tool does with a server's working directory, and why.
431
+ - **`RAILS_AI_CONTEXT_SERVER_NAME`** names the server, for one process, in
432
+ place of the default `config.server_name`; a name the app configured wins.
433
+ A workspace entry sets it to put its app first: VS Code names every tool
434
+ after the announced name and keeps 13 characters of it, so every app's
435
+ tools would otherwise start `rails-ai-cont`. An app whose bundle pins an
436
+ older gem ignores the variable and starts.
437
+ - **CONFIGURATION.md lists the environment variables the gem reads**, among
438
+ them `RAILS_AI_CONTEXT_BOOT_TIMEOUT`, which appeared only in error
439
+ messages.
440
+
441
+ ### Fixed
442
+
443
+ - **`rails_security_scan` echoes a long file or check name shortened.** A
444
+ 100,000-character `files` entry came back whole in "File(s) not found"
445
+ and in the scope of a clean answer, and a long `checks` entry came back
446
+ whole in brakeman's "Could not find specified check", from the scan in
447
+ this process and the one outside the bundle alike. Each now shows its
448
+ first 80 characters and its length. A check name brakeman does not know
449
+ is answered the same from either scan - "Brakeman scan failed: Could not
450
+ find specified check" - where the scan outside the bundle told the caller
451
+ to add brakeman to the Gemfile - and a `files` path that names nothing is
452
+ answered before the scan, which on a large app takes minutes, not after it.
453
+ - **A call sees an edit inside a directory the app links in from its
454
+ repository.** The check each call makes for edits stopped at the link, while
455
+ the readers walk through it, so with `app/javascript/controllers/shared`
456
+ linked to a monorepo's shared controllers, `rails_get_stimulus` answered
457
+ from before an edit there until `cache_ttl` ran out, 60 seconds by
458
+ default, and `rails_search_code` already read the edit. The check now
459
+ follows such a link as the readers do: only into the app's repository,
460
+ never back up over the tree it walks, each linked directory once, so a
461
+ link that leads round in a circle ends. Where a link leads is asked at
462
+ every check, so one whose directory appears later is followed too. The
463
+ fuller check that runs once `cache_ttl` is out follows a linked directory
464
+ whatever its name, where it asked only a name with no extension
465
+ (`billing.v2` was passed over).
466
+ - **The pre-commit hook no longer stops a commit for JavaScript the app's
467
+ own toolchain compiles.** `rails_validate` failed a Flow file
468
+ (`Typed.js:3:21: SyntaxError: Unexpected token ':'`), a MobX store's
469
+ decorators (`Invalid or unexpected token`) and any syntax a Babel config
470
+ adds, since node parses none of them. A decorator is now skipped as JSX
471
+ is, naming its line. A file that declares Flow (`// @flow`), or one Babel
472
+ compiles - a `babel.config.*` or `.babelrc*` file, or a `"babel"` key in
473
+ `package.json`, in its folder or one above it up to the top of the
474
+ repository - is skipped with node's error named beside it, since that may
475
+ be the toolchain's syntax or the file's own mistake. Type annotations in a
476
+ `.js` file nothing compiles, and a plain syntax error, still fail.
477
+ - **A `doctor` fix typed in a folder inside the app runs there too.** From
478
+ `blog/app/models` the fixes were the ones for the app's root:
479
+ `bin/rails db:prepare` was no file there, and `git rm --cached .env`
480
+ matched nothing. A command now goes to the root first - `cd ../.. &&
481
+ bin/rails db:prepare`, `git -C ../.. rm --cached .env`, `rails-ai-context
482
+ --app-path ../.. init` - and a `.gitignore` to edit, a config to make
483
+ valid and the engine a task runs in are named from where `doctor` was
484
+ typed; the report still names the app's files from its root. The binary
485
+ tells the bundle's copy it hands the run to where it was typed, and
486
+ `rails ai:doctor` reads it from the shell's `PWD`, since Rails' `rails`
487
+ command and rake move up to the root before the task runs.
488
+ - **`doctor` gives the edit by hand the install gives for a config the
489
+ install leaves as it is.** For a `.vscode/mcp.json` holding comments it
490
+ said to run the install, which left the file as it was and said to add
491
+ the entry by hand, and `doctor` then said to run the install again. It
492
+ now names the entry to add, as the install writes it (`Add
493
+ {"servers":{...}} to .vscode/mcp.json by hand`), for VS Code's and
494
+ OpenCode's configs, a workspace's included. For a Codex entry kept as an
495
+ inline table or with dotted keys, a stale env snapshot in one included,
496
+ it names the table to write by hand, where an entry the install had to
497
+ rewrite sent the reader back to the install too; and it asks for a JSON
498
+ config that is not UTF-8 to be saved as UTF-8 before the install runs,
499
+ as the install now does too, where it said to add the entry by hand to a
500
+ file JSON still could not read.
501
+ - **On Windows the MCP configs start the server through `cmd /c`, and
502
+ `doctor` reads a config for the platform it runs on.** A gem's executables
503
+ on Windows are batch files (`bundle.bat`, `rails-ai-context.bat`), which
504
+ Claude Code, Codex and the other clients that start their server without a
505
+ shell do not run, so no entry the install wrote started there. Each entry
506
+ is now `cmd /c bundle exec rails-ai-context serve` (`cmd /c
507
+ rails-ai-context serve` standalone) on Windows - `cmd.exe` is a program
508
+ every client starts, and it runs the batch file - and stays as it was on
509
+ macOS and Linux. A config may be committed, so `doctor` fails a `cmd /c` entry on
510
+ macOS and Linux, where there is no `cmd`, and on Windows warns about a
511
+ bare entry whose command PATH finds only as a `.bat` or `.cmd`; running
512
+ the install again rewrites either for the machine it runs on. A team on
513
+ both keeps the MCP configs out of git, or runs the install on each machine.
514
+ - **The pre-commit hook is written with plain newlines on Windows**, where
515
+ the generator wrote it in text mode, so each line ended in CRLF and the sh
516
+ that Git for Windows runs a hook with read the CR as part of every
517
+ command. The stdio server ends each JSON-RPC frame with a plain newline
518
+ there too, rather than the CRLF a text-mode stdout writes.
519
+ - **`doctor` reads PATH on Windows**, where the variable is named `Path`:
520
+ the copy of the environment it reads from before Bundler's changes is a
521
+ plain Hash, which asked for `PATH` found nothing, so every MCP config's
522
+ command read as not on PATH there.
523
+ - **`doctor` and `search_code` find ripgrep on Windows**, which has no
524
+ `which` to ask, so ripgrep read as missing there and every search took the
525
+ slower Ruby path. PATH is searched in Ruby now, with Windows' `PATHEXT`
526
+ extensions, and `doctor`'s fix names `winget install
527
+ BurntSushi.ripgrep.MSVC`. ripgrep is told to separate paths with `/`, since
528
+ on Windows it joins the files it finds with backslashes, which neither the
529
+ app root nor the sensitive patterns that keep a file out of the answer
530
+ (`config/database.yml`) would match.
531
+ - **A Windows path that starts with a drive is read as absolute.** Every
532
+ check for an absolute path looked for a leading `/`, so on Windows
533
+ `C:/...` passed as relative: the context the app commits could carry a
534
+ path under the developer's home directory, an engine or code root outside
535
+ the app was looked for inside it, and a tool handed `C:/Windows/win.ini`
536
+ did not refuse it as the absolute path it is. A failing tool's `At:` line
537
+ split its frame at the drive letter's colon and so named the file by its
538
+ whole path, and `doctor` named an absolute SQLite path from database.yml
539
+ as though it were relative to the app.
540
+ - **A file whose lines end in CRLF, as a Windows checkout's do, is read and
541
+ written in them.** `search_code` ends a line before its CRLF, so `end$`
542
+ finds a line that ends `end` (it matched no line of such a file) and
543
+ `\s$` no longer matches every one. The initializer's `config.ai_tools`
544
+ line was not found or inserted in a CRLF initializer; a context file's
545
+ managed block was written in LF beside CRLF text, leaving a blank line
546
+ under its END marker on every run; and the CRLF pre-commit hook an
547
+ earlier version wrote on Windows, which sh cannot run, read as changed by
548
+ hand, so the install never rewrote it.
549
+ - **On Windows the stdio server keeps its stdout across Bundler's re-exec.**
550
+ Windows has no `exec`: Ruby starts the new image as a child that inherits
551
+ the standard handles and no other, so the descriptor the server saved its
552
+ real stdout on was lost, and a server whose app pins another Bundler
553
+ answered its client nothing. An exec inside the boot now puts the real
554
+ stdout back first.
555
+ - **On Windows `security_scan` stops a run that does not end**, where
556
+ sending it TERM raised `EINVAL`; the run is stopped with KILL alone there.
557
+ A `search_code` pattern longer than Windows takes on a command line (about
558
+ 32,000 characters) is searched in Ruby, where the error answered with the
559
+ whole command line, pattern and all.
560
+ - **On Windows an app whose `app/` is a directory link holds source.** The
561
+ check for Ruby files under `app/`, which decides that a tree with no
562
+ `config/` is an app to read from source (an engine's root, a command run
563
+ with `--no-boot`), walked in through a trailing separator. That follows a
564
+ link on macOS and Linux; Windows' `lstat` answers for the link itself, so
565
+ the walk found nothing there and the command said no Rails app was found.
566
+ - **"No Rails app found in" names an `--app-path` as it was typed**, as the
567
+ hint under it does, where it named the folder through any symlink on the
568
+ way (`/private/var/...` for `/var/...` on macOS). The warning that an app
569
+ boots against another bundle under `bundle exec` is printed only where
570
+ there is an app to boot.
571
+ - **A stdio server whose app hangs while booting answers its client within
572
+ about 21 seconds, from the static tier.** A stdio client waits about 30
573
+ seconds for `initialize` (Claude Code, the MCP Inspector) and `serve` gave
574
+ the boot 60, so an initializer blocked on a service that was down never
575
+ reached the static tier for a real client. `serve` over stdio and
576
+ `rails ai:serve` now give the boot 20 seconds unless
577
+ `RAILS_AI_CONTEXT_BOOT_TIMEOUT` says otherwise; `rails ai:serve`, which
578
+ has no static tier, exits with the boot's error while the client still
579
+ listens. Other commands keep 60.
580
+ - **A call sees every edit made before it, with live reload or without.**
581
+ Inside `cache_ttl` the cache was served without asking whether files had
582
+ moved, relying on live reload - which needs the `listen` gem a new Rails 8
583
+ app does not bundle, and even then delivers an edit only after its
584
+ 1.5-second debounce. An agent that edited a model and asked about it at
585
+ once was answered from before the edit: a new association missing, a
586
+ syntax error unreported; without live reload an association added to a
587
+ model never appeared until a restart. Every server, the endpoints mounted
588
+ in the app included, now checks the app's files at the start of each tool
589
+ call and resource read and, when they moved, reloads the app's code and
590
+ resets the caches. A call shares only a check that began after it arrived,
591
+ so calls one after another each see an edit made between them. The check
592
+ costs about a millisecond on a small app and about 30 ms at 10,000 files.
593
+ Live reload now only tells clients that files changed.
594
+ - **Reading pending migrations writes nothing to the database.** The pending
595
+ list came from `MigrationContext#open`, which creates `schema_migrations`
596
+ and `ar_internal_metadata` in a database that lacks them. Pending is now
597
+ the migration files minus the versions `schema_migrations` holds. A SQLite
598
+ database whose file is not there is reported missing without connecting,
599
+ since connecting created an empty file that `db:prepare` then migrated
600
+ from nothing instead of loading the schema and seeds.
601
+ - **`doctor`'s secrets check asks git.** Inside a git repository every
602
+ ignore rule counts: any `.gitignore` up to the repository root,
603
+ `.git/info/exclude` and the global excludes. A tracked secret fails as
604
+ committed, with `git rm --cached` and rotating it as the fix. Outside git,
605
+ the app's `.gitignore` is read as before.
606
+ - **`rails_security_scan` runs a brakeman installed outside the app's
607
+ bundle.** It runs the gem's own `bin/brakeman` with the app's Ruby, where
608
+ the scan produced no report while `doctor` passed it. `doctor` runs that
609
+ script's `--version` the same way and warns when it does not answer.
610
+ - **`rails_validate` reads ERB trim tags as Rails does.** `<%-` and `-%>`
611
+ were compiled as a unary minus, so every template using them failed, and
612
+ the installed pre-commit hook blocked the commit. An `if` left open and an
613
+ extra `end` still fail.
614
+ - **An ERB syntax error names the template line it is on.** Errors were
615
+ reported a line low, two with a `frozen_string_literal` comment. A keyword
616
+ left unpaired was blamed on a line past the template's end; it is now
617
+ reported on its own line, and a bracket left open at the end of the
618
+ template.
619
+ - **`rails_validate` says why a path is refused.** `app/models/../models/x.rb`
620
+ and absolute paths were refused as "outside Rails root"; the message now
621
+ names the `..` or the absolute path. A directory is called a directory,
622
+ and only a file other than the input is suggested.
623
+ - **`level:"rails"` stops reporting columns that macros declare.**
624
+ `has_secure_password`, its confirmation and challenge, `has_one_attached`,
625
+ `has_many_attached` and `has_rich_text` were each reported as a missing
626
+ column with a migration to add it, in `validates` and in `permit` lists.
627
+ - **`level:"rails"` knows route helpers that are not rows of the route
628
+ table.** Helpers in app/helpers, `helper_method` names, `direct` routes
629
+ and Rails' own routes such as `rails_blob_url` are no longer reported as
630
+ missing.
631
+ - **An ivar set by an ancestor's or a concern's `before_action` is not
632
+ reported as unset.** The view check now reads the controller, its
633
+ ancestors and the modules they include.
634
+ - **No warning asks for a `.turbo_stream.erb` beside a `turbo_stream_from`
635
+ view.** Nothing renders such a template.
636
+ - **`rails_review_changes` with a ref diffs from the merge base.** A
637
+ validation the ref added after the branch left it no longer reads as one
638
+ the branch removed, and an edited `validates` line is not reported as
639
+ removed.
640
+ - **`rails_review_changes` reviews an app in a subfolder of its repository
641
+ on its own.** Files are named from the app root, with their diffs and the
642
+ right type; another app's files and commits are left out.
643
+ - **`rails_review_changes` reads a migration's table however the call is
644
+ written.** `change_table "orders"` and
645
+ `add_column("orders", :coupon_id, :integer)` now get the "Affects tables"
646
+ line and the missing-index warning. The line reads every schema method
647
+ that takes a table, through Rails 8.1: `remove_columns :users, :name`
648
+ names users, as do `rename_index`, the comment, constraint and index
649
+ methods; `drop_table` names each table it drops, `rename_table` the new
650
+ name too, and `create_join_table :users, :roles` the join table it makes,
651
+ roles_users, and both tables. A call on a `change_table` block's table
652
+ (`t.remove_index :number`) or a commented-out line names no table.
653
+ - **The pre-commit hook no longer blocks every commit when the app cannot
654
+ boot, and shows why validation failed.** It validates through
655
+ `bundle exec rails-ai-context tool validate`, which checks from source
656
+ when the boot fails, and its stderr is shown. Re-running the generator
657
+ upgrades an unchanged earlier hook.
658
+ - **The pre-commit hook checks staged `.js`, `.mjs` and `.cjs` files.** A
659
+ broken Stimulus controller is now blocked.
660
+ - **A file saved mid-edit no longer crashes the server.** With web-console
661
+ in the bundle, saving two files that did not compile made the next
662
+ `rails_get_model_details` segfault in about half the runs: Ruby crashed
663
+ compiling them under web-console's raise hook. A file Prism finds a
664
+ syntax error in is no longer required while introspecting, nor when a
665
+ tool looks a class up by name - the receiver or the exception class
666
+ `rails_diagnose` is given, a mixin `rails_get_concern` checks, a model
667
+ `rails_validate` asks about a callback - nor is one whose class body
668
+ names its constant (a subclass of a broken `ApplicationRecord`). The
669
+ answer gives the error, and the log names each file once.
670
+ - **`rails_get_controllers` names a controller whose file does not
671
+ compile.** A controller left unloaded by a syntax error in its file, or in
672
+ a file its class body needs (its parent controller), was listed with the
673
+ actions and filters its source read as, with no word that Rails cannot
674
+ load it. Its entry now gives the error, in every listing and in its own
675
+ answer, as `rails_get_model_details` does for a model, and so do the
676
+ `controllers/{name}` and `controllers/{name}/{action}` resources, where
677
+ an action read built its answer from the broken source.
678
+ - **A tool call that raises a `SyntaxError` or a `LoadError` answers with an
679
+ error and leaves the server running.** The rescue that turns a tool's
680
+ failure into an error result caught `StandardError` only, so a
681
+ `ScriptError` out of an app file a tool reached went past it and past
682
+ the MCP SDK, and ended a stdio server with every tool in it. It is now
683
+ that call's `Tool ... failed: SyntaxError: ...` answer.
684
+ - **Live reload loads no app code on the watching thread.** Listen's
685
+ callback only marks a reload as pending; the next tool call reloads on its
686
+ own thread. `watch` reloads and regenerates on its main thread and keeps
687
+ going past an error from the app's code.
688
+ - **A server that cannot reload code says so once the code changed.** Under
689
+ `RAILS_ENV=test`, or production with eager loading, every answer ends with
690
+ a note naming the changed files, since what reflection reads (associations,
691
+ enums) is as of boot, and a JSON answer or resource body carries it under
692
+ `_stale_code`. The server instructions no longer promise that reflection
693
+ follows edits there.
694
+ - **A resource read from the static tier says so**, under `_static_tier` in
695
+ its JSON body, as a tool's JSON answer does.
696
+ - **A Ruby file saved twice within one second at the same size is named in
697
+ every answer.** Bootsnap's compile cache knows a file by its size and the
698
+ second of its mtime, so the reload such a save sets off got the code of
699
+ the earlier save, and an answer showing the earlier save's associations
700
+ read as current. Bootsnap has no way to drop one entry, so a server that
701
+ reloads now ends every answer with a note naming the file until it is
702
+ saved in another second, and COMPATIBILITY.md lists the limit and
703
+ `DISABLE_BOOTSNAP_COMPILE_CACHE=1`.
704
+ - **`rails_query` runs a read-only query that names a write keyword only
705
+ inside a string.** `WHERE title LIKE '%update%'`, `= 'put into box'` and
706
+ the `replace()` string function were refused as writes; a keyword or
707
+ `INTO` is now judged outside string literals, and `SELECT ... INTO`,
708
+ `REPLACE INTO`, `INTO OUTFILE` and writes inside a CTE are still refused.
709
+ - **`rails_query` runs `SHOW CREATE TABLE` and `SHOW CREATE VIEW`** on
710
+ MySQL and MariaDB, showing the whole definition, with credentials in it
711
+ redacted.
712
+ `SHOW CREATE USER`, `SHOW CREATE DATABASE` and `SHOW GRANTS` stay refused.
713
+ - **Every command a `doctor` fix gives runs where `doctor` was typed.**
714
+ Typed in an app's parent folder with `--app-path blog`, as `init`'s
715
+ folder-of-apps summary suggests, the fixes did nothing or the wrong thing
716
+ there: `bin/rails db:migrate` was no file, `rails-ai-context init` set up
717
+ every app in the folder, `git rm --cached` found no repository, and a
718
+ `.gitignore` line spelled from the parent folder (`blog/.env.local`)
719
+ ignored nothing in blog's own `.gitignore`. The binary's commands now
720
+ carry `--app-path blog`, the app's other commands run after `cd blog &&`,
721
+ git runs as `git -C blog`, and a `.gitignore` line names `blog/.gitignore`
722
+ with the pattern as that file reads it (`.env.local`).
723
+ - **The install finds a Codex server however its name is spelled, and never
724
+ declares it twice.** A quoted table (`[mcp_servers."rails-ai-context"]`),
725
+ an inline table or dotted keys went unseen: a second table was appended,
726
+ and Codex refused the whole file ("duplicate key") while re-runs said
727
+ "(unchanged)" and `doctor` passed it. A quoted table is now replaced where
728
+ it stands, and two tables become one. An entry of the gem's in another
729
+ form is left as it is, with what to do by hand. `doctor` fails a server
730
+ declared twice and warns about an entry the install does not rewrite.
731
+ - **An MCP config with trailing commas is read as VS Code reads it** (json
732
+ 2.9 or later). Every run left `.vscode/mcp.json` as it was, kept asking
733
+ for the entry by hand after it had been added, and `doctor` failed the
734
+ file.
735
+ - **`init` in a folder holding one app names the folder's files as the
736
+ folder's.** It printed `backend/.mcp.json` for the folder's `.mcp.json`.
737
+ Each app's `.gitignore` is now named by its app, and an app's legacy-file
738
+ `rm -f` line works when pasted where `init` ran.
739
+ - **Removing one of two dropped tools keeps the file the other still
740
+ uses.** Removing OpenCode's files while also dropping, but keeping, Codex
741
+ CLI's deleted `AGENTS.md`.
742
+ - **An MCP config is read the way its client reads it.** Claude Code reads
743
+ `.mcp.json` as plain JSON, so a hand-edited one with a trailing comma or a
744
+ comment loaded no server while the install called it "(unchanged)" and
745
+ `doctor` passed it. Such a file is now reported, by the install and by
746
+ `doctor`, as one Claude Code cannot read; Cursor's `.cursor/mcp.json` is
747
+ held to plain JSON too. VS Code's `.vscode/mcp.json` and OpenCode's
748
+ `opencode.json`, which read JSONC, keep taking comments and trailing
749
+ commas.
750
+ - **A Codex config with a byte that is not UTF-8 in a quoted server name no
751
+ longer stops `init`, or drops `doctor`'s MCP and Codex rows.** Codex
752
+ refuses a config that is not UTF-8 at all, so the install says so and
753
+ `doctor` fails the file.
754
+ - **A directory the app links in from its own repository is read as the
755
+ app's own.** With `app/javascript/controllers` linked to a monorepo
756
+ package every Stimulus controller was missing; the code, view and
757
+ JavaScript walks now follow such a link as Zeitwerk, the view resolver and
758
+ the bundlers do, and an edit inside one refreshes the cache.
759
+ - **A path through a linked-in pack is accepted by every tool that takes a
760
+ path.** `rails_get_edit_context`, `rails_validate` and `rails_search_code`
761
+ refused `packs/billing/app/models/invoice.rb`, which
762
+ `rails_get_model_details` named, and a search of the app never read the
763
+ pack.
764
+ - **`rails_get_partial_interface` names a linked-in pack's partial as the
765
+ app spells it**, finds its render sites, and points at a `rails_get_view`
766
+ path that resolves.
767
+ - **A link that resolves nowhere is never offered back as the file to try**,
768
+ nor listed as a Stimulus controller or a view.
769
+ - **Every tool refuses a path in the same words, and echoes a long one
770
+ shortened.** `Path not allowed: <path>`, with `(sensitive file)` when the
771
+ name is the reason, where some tools said "Access denied" or "outside
772
+ Rails root"; a 100 KB path came back as a 100 KB refusal.
773
+ - **The mounted engine keeps its MCP sessions across a code reload.** Every
774
+ edit in development dropped them (404 "Session not found") and left one
775
+ more session-reaper thread running.
776
+ - **The mounted engine (`mount RailsAiContext::Engine`) no longer deadlocks
777
+ when clients call it while app code reloads in development.** Since 5.16.1
778
+ it answered on a thread `ActionController::Live` starts, which needs a
779
+ share of the lock Rails reloads code under. Once an edit had a reload
780
+ waiting, no new share was granted, and the reload waited for the request,
781
+ which waited for its Live thread: with six clients calling while a file
782
+ changed every 0.3 seconds, every call hung and `rails server` ignored
783
+ SIGTERM, in every run on Rails 8.1 and in three of four longer runs on
784
+ 7.0. The Live thread also kept the database connection a call had used, so
785
+ from Rails 8.0 each call after an edit sat out the reload's ten-second
786
+ wait for it: 10.3 seconds a call, where it now takes 0.4. The engine now
787
+ runs each call on the request's own thread and sends the answer whole; the
788
+ same runs answer every call and stop on SIGTERM. The cost is streaming:
789
+ what a custom tool sends during a call, such as progress or log
790
+ notifications, arrives with its answer, and a request a tool makes of the
791
+ client mid-call (sampling, elicitation, roots) fails at once, saying why,
792
+ where it could only have waited out its timeout. `subscriptions/listen`,
793
+ whose notification stream an answer sent whole cannot keep open, is
794
+ answered as a method the engine does not have. stdio and the standalone
795
+ HTTP server still stream.
796
+ - **The engine and `auto_mount` answer the hosts the app's `config.hosts`
797
+ allows.** The MCP SDK answered only loopback names, so
798
+ `allow_http_in_production` could not serve an app by its own name, and
799
+ `myapp.localhost` was refused in development. Rails' host authorization
800
+ judges the Host now; a cross-origin request is still refused.
801
+ - **A stdio message over 4 MiB is answered, named on stderr, and ends the
802
+ server with status 1**, where it exited 0 with nothing said.
803
+ - **Email addresses in any script are redacted from log lines**
804
+ (`jürgen@example.com`, `anna@münchen.de`).
805
+ - **A missing view, an unknown resource URI and an unknown tool are
806
+ answered with what exists**, in `data.available` and, for a tool, the
807
+ nearest names in the message.
808
+ - **A generated length test sets a value the validation rejects.** A length
809
+ validation with no maximum was tested with `""`, so
810
+ `length: { minimum: 10 }, allow_blank: true` got the one value it allows
811
+ and the test failed on a valid record. The value is now one past the
812
+ maximum (or the `is:` or range end), else one short of the minimum; `""`
813
+ is used only for a minimum of 1 that does not allow blanks.
814
+ - **Generated tests call `FactoryBot.create` where the suite does not
815
+ include FactoryBot's methods.** A bare `create(:article)` works only where
816
+ rails_helper, a support file or test_helper includes
817
+ `FactoryBot::Syntax::Methods`; without it every generated example failed
818
+ with "undefined method `create'".
819
+ - **A controller whose base class inherits `ActionController::API` is an
820
+ API controller with the app unbooted too.**
821
+ `Api::V1::PostsController < BaseController < ActionController::API` read
822
+ as an ordinary controller under `--no-boot`.
823
+ - **`rails_get_partial_interface` reads `render "comments/form"` as the
824
+ comments form.** `posts/_form` took the comment form's call in
825
+ `posts/show`, and its `comment` local.
826
+ - **`rails 'ai:tool[...]'` keeps an argument that contains a bracket.**
827
+ `pattern=params\[:id\]` was dropped, and the answer was "Pattern is
828
+ required".
829
+ - **The stack overview counts the databases it does not name.**
830
+ "Databases: 5 (primary, analytics, cache)" read as the whole list; it now
831
+ ends "...2 more".
832
+ - **A filtered `rails_get_turbo_map` judges each stream against the whole
833
+ app.** A `controller:` filter called a stream unheard, and warned about
834
+ it, when its subscribers were in another controller's views.
835
+ - **`rails_session_context` keeps a conversation's latest 200 queries, each
836
+ long param shortened, and says how many it dropped.** One client could
837
+ grow the server's memory without limit, and an answer echoed a 100 KB
838
+ argument back in full.
839
+ - **Source a tool shows has its secrets filtered, as search results did.**
840
+ `rails_search_code` and `rails_get_edit_context` filtered a literal key
841
+ from the lines they showed, while an action's source in
842
+ `rails_get_controllers` (and the methods and params methods it calls), a
843
+ concern's methods, a callback's body, a template in `rails_get_view` and
844
+ `rails_get_partial_interface`, a test file in `rails_get_test_info`, a
845
+ job's guard clauses, a `rails_security_scan` code line and the
846
+ `controllers/{name}/{action}` and `views/{path}` resources showed the same
847
+ line as written.
848
+ - **`rails_get_controllers` and the `controllers/{name}/{action}` resource
849
+ find an action a concern or a parent controller gives.** Rails counts
850
+ every public method of an `ApplicationController` concern among each
851
+ controller's actions; the action list takes one only where a route names
852
+ it, so a lookup by name of an unrouted one answered "not found". The
853
+ answer now reads it from the file that defines it and names the parent
854
+ controller (`inherited_from` in the resource). A callback, a predicate, a
855
+ method that takes an argument, or one a private def lower down hides is
856
+ still not an action. Under `--no-boot` the action list also takes a routed
857
+ method of a parent's concern, as the booted list did.
858
+ - **An action a concern or a parent controller defines shows its source.**
859
+ `rails_get_controllers(controller:, action:)` read only the controller's
860
+ own file, so an action from an included concern (namespaced ones too) or
861
+ from a base controller answered "Could not extract source code" with an
862
+ absolute path. It now follows Ruby's lookup through the app's modules and
863
+ parent controllers and names the file the source came from, in both
864
+ tiers. The `rails-ai-context://controllers/{name}/{action}` resource,
865
+ described as the action's source and metadata, never carried the source;
866
+ it now has `file`, `lines` and `source`.
867
+ - **A Rails 8 controller's concerns no longer list the route helpers Rails
868
+ mixes in.** `Rails::Engine::LazyRouteSet::MountedHelpers` appeared as a
869
+ concern of every controller in the booted `controllers` payload (the
870
+ `rails://controllers` resource and `.ai-context.json`), and not in the
871
+ static tier's.
872
+ - **`tool --json` and `JSON=1 rails 'ai:tool[...]'` print the envelope and
873
+ nothing else.** Sidekiq's "connecting to Redis" line, and on the rake task
874
+ an initializer's `puts`, landed on stdout ahead of it. What the app prints
875
+ while it boots or a tool runs now goes to stderr, as under `serve`.
876
+ - **An invalid tool argument is reported as one when the app's locale files
877
+ do not load.** With a locale file that did not parse, every invalid
878
+ argument came back as a JSON-RPC internal error with an I18n backtrace on
879
+ stderr, over stdio and HTTP. An exception that still escapes a tool is
880
+ named in one stderr line, its backtrace under `DEBUG`.
881
+ - **A preset prints each tool's header above that tool's answer, on
882
+ stdout**, from the binary and the rake task; the headers went to stderr,
883
+ so a redirect of both streams put them out of order.
884
+ - **A tool `skip_tools` turned off says so** on the CLI, the rake task and
885
+ the MCP server, which answered "Unknown tool" and "Tool not found".
886
+ - **Every command answers `--help` and `-h`**, exit 0, where each failed with
887
+ Thor's "was called with arguments" (`inspect`'s naming `inspect_app`) and
888
+ `tool`/`preset` read the flag as a name. `doctor --no-boot` and
889
+ `serve --transport bogus` are refused in words, the transport before the
890
+ boot. `tool --list` in an app whose boot fails says so in one line. An
891
+ unknown param's suggestion keeps the form it was typed in.
892
+ - **An unknown format is refused before any work, with every valid one named,
893
+ `all` included**: `rails 'ai:context_for[bogus]'` printed "Writing bogus
894
+ context file..." and then a raw `ArgumentError`.
895
+ - **An unknown `context_mode` or `tool_mode` is refused** from
896
+ `CONTEXT_MODE`, the YAML and the initializer, where it was kept and read
897
+ as the default.
898
+ - **The `ai:facts` footer names a command that gives the full JSON**,
899
+ `rails ai:context:json`, since `ai:inspect` prints a text summary, and the
900
+ `ai:inspect` table no longer pairs `.codex/config.toml` with
901
+ `rails ai:context:codex`.
902
+ - **`ruby -W` prints no warning from the gem's own files.**
903
+ - **`doctor` checks that each MCP config's command can start the server.** A
904
+ config passed when it parsed, so after `bundle remove rails-ai-context`
905
+ every `bundle exec rails-ai-context serve` entry still read as valid, an
906
+ in-Gemfile app whose `.mcp.json` ran the bare binary was told it
907
+ "activates via bundler", and a `path:` copy without the executable passed
908
+ while `bundle exec` could not find it. Each entry is now checked without
909
+ starting it: `bundle exec` needs `bundle` on the PATH the client gives the
910
+ server, a lockfile (the one the entry's `BUNDLE_GEMFILE` names, if set)
911
+ that carries the gem, and a copy that lists its executable; a bare
912
+ `rails-ai-context` must be on that PATH, and in an app whose bundle
913
+ carries the gem it is a warning. The fix names the command to rerun, a
914
+ config that holds no rails-ai-context server says so, and a JSON config
915
+ that does not parse is named in the install's words.
916
+ - **`doctor` checks the PATH a Codex env snapshot saved.** Codex starts the
917
+ server with only the env its config sets, and an rbenv or asdf snapshot
918
+ saves `PATH` alone, so checking `GEM_HOME` passed a snapshot whose Ruby
919
+ had been removed. Each snapshot's `PATH` now has to reach the command its
920
+ entry runs, in the app's config and in a folder-of-apps config above it,
921
+ and a failure names the directory that is gone and where to rerun
922
+ `rails-ai-context init`. A saved `GEM_HOME` has to exist too, and so does
923
+ each directory on a saved `GEM_PATH` that the shell's own `GEM_PATH` does
924
+ not name: RubyGems passes over a directory that is not there, and `init`
925
+ saves the shell's `GEM_PATH` again, so a warning about one could never
926
+ clear.
927
+ - **A context file is out of date when a context run would rewrite it.**
928
+ `doctor` compared file times, so touching a model raised "CLAUDE.md may be
929
+ stale" and `rails ai:context`, which leaves unchanged files alone, could
930
+ not clear it; it ignored `output_dir`; and after an upgrade it called
931
+ files stamped with the old version up to date. It now runs the context
932
+ generation into a scratch copy, names the files it would write and why
933
+ (the version that wrote them, or the folders changed since), and the fix
934
+ it names clears it. The app's files are not touched.
935
+ - **`doctor` reads pending migrations in every database.** Only the primary
936
+ was asked, so a pending migration for a second database passed. Each
937
+ database the environment configures is asked through its own connection,
938
+ as `db:migrate:status` does, without writing to it, the message names the
939
+ database, and the Migrations row counts every database's files.
940
+ - **A development database that does not exist fails `doctor`.** It scored
941
+ 93 and `--strict` exited 0. A Database row now fails, naming a database
942
+ that does not exist (fix: `bin/rails db:prepare`) or one that does not
943
+ answer, with the error.
944
+ - **`doctor` reads the bundle, suite and code the tools read.** It reported
945
+ "Gemfile.lock not found" for an app whose `config/boot.rb` names a shared
946
+ Gemfile above it, and in an engine's `test/dummy` it counted only the
947
+ dummy's models, controllers, views and tests and found no migrations. It
948
+ now finds the lockfile as the tools do, counts the engine's code, suite
949
+ and migrations with the dummy's, and names the engine's root as the place
950
+ to run the db task it suggests.
951
+ - **`doctor`'s secrets check is about the files that hold secrets.** Every
952
+ stock app was warned about committing `config/credentials.yml.enc`,
953
+ `config/database.yml`, `config/cable.yml` and `config/storage.yml`. A
954
+ committed key or env file (`config/master.key`,
955
+ `config/credentials/*.key`, `.env*` and the like) still fails; a config
956
+ such as `config/database.yml` warns only when it holds a literal password,
957
+ token or URL with a password, naming the key and line; encrypted
958
+ credentials are not reported.
959
+ - **`doctor` agrees with the boot about a pinned dependency.** With
960
+ `prism 1.3.0` locked, the boot warned that the gem needs `>= 1.4` while the
961
+ Prism parser row passed, and the same held for `mcp` and the MCP server
962
+ row. Both rows now warn with the locked and needed versions and name
963
+ `bundle update <gem>` as the fix.
964
+ - **`rails_validate` checks a Stimulus controller as the ES module it is.**
965
+ On Node 22, `node -c` exits 0 for any `.js` file that imports when its
966
+ package declares no type, broken or not, so every Stimulus controller
967
+ passed. The source now goes to node on stdin with its module type spelled
968
+ out, decided as Node decides it, and nothing is written next to the app.
969
+ A failure names the line, the column and node's message, where it printed
970
+ the first lines of node's output and cut the message off. Without node, a
971
+ file whose brackets balance is listed as skipped rather than passed, and
972
+ JSX is skipped rather than failed. `.mjs` and `.cjs` files are checked too.
973
+ - **`rails_get_callbacks` lists the callbacks turbo-rails' broadcast macros
974
+ declare.** A model with `broadcasts_refreshes`, `broadcasts_to`,
975
+ `broadcasts` or `broadcasts_refreshes_to` read as having none of the commit
976
+ callbacks they add. Each is listed under its event as
977
+ `broadcasts_refreshes (turbo-rails)`, with the method turbo-rails runs at
978
+ `detail:"full"`, and `broadcasts_refreshes` is a macro on
979
+ `rails_get_model_details` too. `detail:"full"` also prints a block or
980
+ lambda callback's declaration, where it printed `[inline_block]` alone.
981
+ - **A scope declared in a loop over a literal list is a scope.**
982
+ `KINDS.each { |k| scope k, -> { ... } }` gave no scope statically, and
983
+ booted the scopes read as class methods. Each element is now a scope,
984
+ tagged `[INFERRED]`.
985
+ - **`rails_diagnose` calls a `NoMethodError` a nil reference only when the
986
+ receiver is nil.** On any other receiver it is an undefined method, and
987
+ booted, the answer names the receiver's closest method; a private method
988
+ is said to be private.
989
+ - **`rails_diagnose` reads an error as a Rails log writes it.**
990
+ `Pundit::AuthorizationNotPerformedError (ProductsController)` came back as
991
+ `Error: Unknown`. Authorization not performed and authorization denied are
992
+ classified, and an error with no rule names the gem or app file that
993
+ defines its class.
994
+ - **`rails_diagnose`'s log correlation finds the request.** It searched the
995
+ log's last 15 lines and printed the matching line alone. It now reads the
996
+ log's last megabyte and shows the latest entry: path, action, parameters,
997
+ status, request id, the error and the first frames, all redacted. It says
998
+ so when there is no entry.
999
+ - **`rails_dependency_graph` counts only an `as:` declaration as implementing
1000
+ a polymorphic interface, and lists foreign-key cycles only.** An
1001
+ association and its inverse were listed as a cycle, sixteen of them on an
1002
+ app with none; when there are none the answer now says so.
1003
+ - **`rails_get_concern` lists private methods and `helper_method`, and says
1004
+ when an include is conditional.** On a booted app, a conditionally
1005
+ included module that is not defined is said never to have been mixed in.
1006
+ - **`rails_get_env` skips YAML comment lines and reads a block's literal
1007
+ default.** A default in a `sensitive_patterns` file is said to be present
1008
+ and unread, where a lone site read as having none.
1009
+ - **`rails_runtime_info` says its pool and cache are the MCP server
1010
+ process's own.** It also names the shared store another environment keeps
1011
+ the app's cache in.
1012
+ - **An in-Gemfile install from a copy outside git has its executable.** The
1013
+ gemspec listed its files with `git ls-files`, which lists nothing outside a
1014
+ checkout, so a `path:` entry pointing at a vendored or unpacked copy left
1015
+ the gem without `rails-ai-context` and every MCP config's
1016
+ `bundle exec rails-ai-context serve` failed to start. Outside a checkout
1017
+ the files on disk are the list.
1018
+ - **`rails_review_changes` shows what a migration's tables hold now.** Each
1019
+ affected table's line was the first line of `rails_get_schema`, which is
1020
+ its heading (`products: ## Table: products`); it is now the table's
1021
+ current columns, or "not in the schema yet" for one the migration
1022
+ creates. The routes line drops the heading marks it carried.
1023
+ - **A folder of apps keeps its MCP configs true as apps come and go**
1024
+ (#429). Folders named outside ASCII all make the name `app`, so the gem
1025
+ numbered its own entries (`rails-ai-context-app-2`) and then never
1026
+ recognised them: when one app went, the other kept a second server, or
1027
+ one naming a folder that was gone. A numbered name of the gem's own is
1028
+ now dropped like any other. An app folder that starts with a dash is
1029
+ written `./-api`, where `--app-path -api` read as an option and the
1030
+ server never started. A folder named in Latin-1 crashed `init` with
1031
+ "invalid byte sequence in UTF-8"; JSON and TOML hold UTF-8 alone, so that
1032
+ app is now left out of the folder's configs, with a message, and the
1033
+ other apps' entries are written. A config holding comments is named for
1034
+ them under json 3 too, which refuses comments json 2 passed over, where
1035
+ it was blamed on a trailing comma.
1036
+ - **Commands the binary prints read as a person would type them.** A path
1037
+ holding a space or a character a shell reads is single-quoted, letters
1038
+ outside ASCII stay as they are (`--app-path 'my apps/шоп'`, where every
1039
+ one was backslashed), and a path that starts with a dash is spelled from
1040
+ the current directory. The several-apps refusal says `<folder> is no
1041
+ Rails app, and holds 2 below it`. The boot-failure hint and the static
1042
+ tier's banner name the doctor command as the binary was run, with its
1043
+ `--app-path` and under `bundle exec` when it ran so: a workspace's
1044
+ server runs with `--app-path`, and a bare `doctor` where its client
1045
+ stands finds the folder of apps.
1046
+ - **The pre-commit hook checks a staged file whatever its name.** git
1047
+ quotes a name holding a character outside ASCII, a quote or a backslash
1048
+ in the list the hook read, so no `.rb` test matched it and the file went
1049
+ unchecked: a broken `db/seeds/brokén.rb` committed. Names are now read
1050
+ NUL-separated. validate takes its files comma-separated, so a name
1051
+ holding a comma is named and left unchecked rather than passed in two
1052
+ halves. With several apps in one hook each app's lines are headed by its
1053
+ name, and the second app's question asks to add it. The question and the
1054
+ hook say what it checks - that the staged Ruby, ERB and JavaScript files
1055
+ parse - where
1056
+ they promised hallucinated columns and missing models, which a commit
1057
+ never checked.
1058
+ - **The binary runs an app's own copy of the gem when its bundle has one.**
1059
+ In an app whose Gemfile carries rails-ai-context at another version or
1060
+ from a `path:` copy, the global binary loaded its own files before the
1061
+ boot and the bundle's over them: "already initialized constant" warnings,
1062
+ and a command that ran half of each copy (`version` said one release, the
1063
+ server announced another). It now runs the command as
1064
+ `bundle exec rails-ai-context` would, from where it was typed, and says
1065
+ so on stderr: that is the copy the app's MCP configs start. Only a run
1066
+ that boots the app does so, since the boot runs the app's bundle anyway:
1067
+ `--no-boot` stays on the installed copy, as bundle exec would evaluate
1068
+ the app's Gemfile and start whatever copy the app names, code nobody has
1069
+ read on a repo just cloned. `init` asks its questions on the installed
1070
+ copy and has the app's copy write only the context files, so an
1071
+ MCP-only setup, by flag or at the prompt, runs nothing of the app's, and
1072
+ the MCP configs it writes are not written again from the bundle's
1073
+ environment. A
1074
+ bundle not installed yet is left alone too (`bundle check` says so), so
1075
+ its failed boot still serves the static tier. The bundle is the one a
1076
+ `BUNDLE_GEMFILE` you set names (a dual boot's `Gemfile.next`), and a
1077
+ command typed in a subdirectory, or in a folder holding the one app,
1078
+ runs from the app root, where a released copy, which does not look for
1079
+ the root, finds the app. Hints that name another command (`doctor`,
1080
+ `facts`' pointer to `inspect`) name it as typed: no `bundle exec` the
1081
+ user did not type, the `--app-path` they did.
1082
+ - **The binary loads no gem the app pins before the app boots.** Run from a
1083
+ subdirectory of an app (#427) or a folder holding one, it asked of every
1084
+ directory it passed whether a non-Rails framework lived there, which
1085
+ parsed the Gemfile with the prism gem; it read config/boot.rb the same
1086
+ way. A Rails 8 app locks prism, json and securerandom, so a version
1087
+ other than the newest installed loaded over ours at the boot: dozens of
1088
+ "already initialized constant" lines, then a mixed library. The Gemfile
1089
+ and config/boot.rb are now read with Ruby's own Ripper before the boot,
1090
+ a directory is checked for a framework only once it holds an app, and
1091
+ json, strscan and securerandom load when first used.
1092
+ - **The install summary names a preset that exists:** it suggested
1093
+ `rails 'ai:preset[arch]'`, which answers "Unknown preset". It is
1094
+ `ai:preset[architecture]`.
1095
+ - **Commands run from a subdirectory of an app find the app** (#427), the way
1096
+ `bin/rails` and Bundler do: the nearest directory above with
1097
+ `config/application.rb`, `config/environment.rb` or a `bin/rails` that
1098
+ boots an app or an engine. A packwerk pack, `$HOME`, `node_modules`,
1099
+ `vendor/bundle` and a gem install are walked past, never stopped at. The
1100
+ app is named on stderr, so `serve` keeps stdout clean.
1101
+ - **Commands run from a folder of apps say which they could read** (#428):
1102
+ one app below is used, several are refused with the exact `--app-path`
1103
+ command for each, and `tool --list` still lists.
1104
+ - **`init --app-path` sets up the app it names** (#424). The config, every MCP
1105
+ config and the `.gitignore` entries went to the current directory and the
1106
+ context files to the named app; from a directory that is no app, it
1107
+ refused outright. The commands it prints afterwards name the app the same
1108
+ way. Under `bundle exec`, a command that reads an app other than the
1109
+ bundle's says it boots against that bundle, and the install form every
1110
+ file names - the MCP command, the commands in context files and tool
1111
+ hints - is read from the app's own Gemfile.lock, not the bundle's.
1112
+ - **`tool --list --app-path` lists the app's custom tools** (#426). It never
1113
+ booted the named app, so tools from its initializer were missing.
1114
+ - **A wrong `--app-path` inside an app names the app**, and a relative one
1115
+ that misses because the client started inside an app names the folder it
1116
+ was written for.
1117
+ - **The install generator offers the pre-commit hook in a submodule, a
1118
+ monorepo app and under `core.hooksPath`** (#425). It looked for a `.git`
1119
+ directory in the app root and skipped the step silently otherwise; the
1120
+ hooks directory is now git's own answer. A monorepo's hook validates each
1121
+ app's staged files from inside that app, from a linked worktree too, and
1122
+ installing in a second app adds it to the same hook. A deleted file no
1123
+ longer blocks the commit: there is nothing left to validate, and an
1124
+ exported `CDPATH` cannot send the hook into another folder of the same
1125
+ name. Running the generator again brings a hook an earlier version wrote,
1126
+ and nobody changed since, up to date. A `core.hooksPath` outside the
1127
+ repository, which every repository using it shares, a repository that does
1128
+ not track the app (a dotfiles repository at `$HOME`), and any other hook
1129
+ that runs `rails-ai-context` are left alone with a message.
1130
+ - **The pre-commit hook starts a `path:` copy of the gem from a submodule
1131
+ or a linked worktree.** git hands every command a hook runs the
1132
+ committing repository's `GIT_DIR` and `GIT_INDEX_FILE`, and the gemspec's
1133
+ `git ls-files` read that repository: in an app kept as a submodule the
1134
+ gem's file list was the app's, and every commit failed with "can't find
1135
+ executable rails-ai-context". The gemspec now asks git about its own
1136
+ directory alone.
1137
+ - **An app with a Gemfile and no lockfile yet is read by what its Gemfile
1138
+ names.** It counted as an in-Gemfile install, so a fresh clone that never
1139
+ named the gem got `bundle exec` commands that could not start. A Gemfile
1140
+ that pulls in others (`eval_gemfile`, `gemspec`) still counts as one.
1141
+ - **Codex's config keeps the tables around the gem's own.** A server section
1142
+ ran on into the next table when no blank line separated them, so updating
1143
+ it replaced the user's table too; a comment above the next table now stays
1144
+ with it, a section a formatter wrapped over several lines is still read as
1145
+ the gem's, values are written as TOML strings rather than Ruby literals, a
1146
+ file with Windows line endings keeps them, a byte order mark no longer
1147
+ hides the first section, and one that is not ASCII is read whatever the
1148
+ locale, by `doctor` too.
1149
+ - **A JSON config the install cannot write back faithfully is left alone**,
1150
+ with the entry to add by hand, said on the terminal: one that does not
1151
+ parse (VS Code's and OpenCode's take trailing commas) was replaced by a
1152
+ file holding only the gem's server, one with comments lost them, and one
1153
+ that is JSON but not an object, or not UTF-8, raised out of the install. A
1154
+ byte order mark at the head of a config is kept, where it cost the file
1155
+ its contents, and an entry of the user's under the gem's own name - the
1156
+ HTTP one SETUP.md describes - is kept in place of the gem's, where it was
1157
+ overwritten. `doctor` says to make such a file valid JSON holding an
1158
+ object before running the install again, where it called one that parsed
1159
+ valid whatever it held.
1160
+ - **In a C or POSIX locale, a container's default, the binary reads paths
1161
+ and files as UTF-8 when they are**, as Rails does once it boots. An app
1162
+ folder or a folder of apps whose path holds characters outside ASCII, a
1163
+ home directory holding them while the binary looked for the app, and an
1164
+ initializer holding them stopped the binary with an encoding error. A
1165
+ folder named in another encoding is read byte by byte, as before.
1166
+ - **A hand-written `app/models/AGENTS.md` or `app/controllers/AGENTS.md`
1167
+ keeps its text.** With OpenCode or Codex selected, `rails ai:context`,
1168
+ `rails-ai-context context`, the installer and `init` replaced the file
1169
+ with the gem's listing without a word. These names are ones teams write
1170
+ by hand, so they now follow the root files' rule: a file the gem
1171
+ generated whole (it carries `> Auto-generated by rails-ai-context. Do not
1172
+ edit manually.`) is rewritten whole, a file holding the markers gets only
1173
+ its block replaced, and any other file keeps its text and gets the
1174
+ listing between markers above it, with a note that claims the block
1175
+ rather than the file. Dropping the AI tool takes out only that block.
1176
+ - **`tool_mode :cli` reaches every generated file.** The root files followed
1177
+ it, but the split rule files and OpenCode's per-directory `AGENTS.md`
1178
+ still said "ALWAYS use MCP tools" and "Use `rails_get_model_details` MCP
1179
+ tool", the protocol's third rule named `rails_get_conventions`, the "All
1180
+ 45 tools" list gave MCP names, and the tools rule files called themselves
1181
+ MCP tools in their Cursor description and Copilot name. In CLI mode each
1182
+ of them names the command now (`rails 'ai:tool[model_details]'
1183
+ model=Name`), the trim note included; MCP mode reads as before.
1184
+ - **A static-tier CLAUDE.md no longer shows an empty Architecture
1185
+ heading.** Without a boot there is no conventions section, so the
1186
+ heading stood over a blank line before the services and jobs it held
1187
+ and read as a section that came up empty; the names now follow it
1188
+ directly, and the blank line only separates them from a pattern list.
1189
+ - **The generated files count every database's tables and migrations.** In
1190
+ an app with a second database, CLAUDE.md, AGENTS.md, `.cursorrules`,
1191
+ copilot-instructions.md and the overview rule files counted the
1192
+ primary's alone ("Migrations: 3 total" with 7 on disk), and
1193
+ `.claude/rules/rails-schema.md` listed only the primary's tables, its
1194
+ `paths:` missing `db/analytics_schema.rb` and `db/analytics_migrate/**`.
1195
+ The Database and Migrations lines now name each other database after the
1196
+ primary (`analytics: SQLite - 3 tables`, `analytics: 4 total, 1 pending`,
1197
+ pending read from that database's dump as `rails_get_schema` reads it),
1198
+ the schema rule lists each database's tables under its own heading and
1199
+ triggers on each one's dump and migrations, and full mode's schema and
1200
+ migrations sections cover them all. A database whose every table is
1201
+ Solid Queue's, Solid Cache's or Solid Cable's, as a Rails 8 app's queue,
1202
+ cache and cable are, is named for its framework in one phrase (`queue:
1203
+ Solid Queue (13 tables)`); its tables, the framework's own, stay out of
1204
+ the schema rule and full mode's dump, and its dump out of the rule's
1205
+ `paths:`.
1206
+ - **Claude Code and Copilot read the tools guide once.** Both load the root
1207
+ file and the always-on tools rule file on every request, and both files
1208
+ carried the whole guide and the protocol, about 95 lines twice. With root
1209
+ files on, `.claude/rules/rails-mcp-tools.md` and
1210
+ `.github/instructions/rails-mcp-tools.instructions.md` now hold what the
1211
+ root file leaves out, the detail levels and the table of every tool, and
1212
+ point to it for the rest; with `generate_root_files` off they still carry
1213
+ the whole guide. Cursor's agent-requested rule is unchanged.
1214
+ - **The models rule files no longer contradict the tools guide.** They said
1215
+ "Read model files for business logic/methods" beside the guide's "NEVER
1216
+ read reference files - ... model files"; they now say to read a model
1217
+ file only when you edit it, the guide's own rule.
1218
+ - **A small `claude_max_lines` no longer cuts the tools guide.** Below
1219
+ about 110 lines the budget was a hard cap that trimmed the file from the
1220
+ end, so at 60 the four compact files kept the data and lost the
1221
+ anti-hallucination protocol and the whole tool reference to one line and
1222
+ the trim note, though the docs said those sections are kept whole. The
1223
+ title block and the Commands, Warnings, Rules and Tools sections are now
1224
+ never cut: the data sections between them go first, down to none, so
1225
+ those sections set the smallest file a budget can give.
1226
+ - **Full mode keeps the anti-hallucination protocol and the tools guide.**
1227
+ `context_mode :full` (`rails ai:context:full`, `CONTEXT_MODE=full`) wrote
1228
+ CLAUDE.md, AGENTS.md and copilot-instructions.md without them, so OpenCode
1229
+ and Codex, which load AGENTS.md alone, got neither, and `tool_mode` had no
1230
+ effect on those files. Each now ends with the compact files' tools guide,
1231
+ protocol included while `anti_hallucination_rules` is on; Copilot's gains
1232
+ the behavioral rules the other two carry. The dump itself says what the
1233
+ compact files already did: column defaults, indexes and foreign keys,
1234
+ each model's concerns, scopes, callbacks, enum values and constants, and
1235
+ the Stimulus controllers. It lists the app's routes as the count has
1236
+ them, each PATCH/PUT pair one entry, and counts the framework's instead of
1237
+ listing ~27 Active Storage, Action Mailbox and Turbo routes; a helper with
1238
+ no methods no longer ends in a bare colon, and the Overview and Puma
1239
+ headings appear only with something under them.
1240
+ - **A full-mode file names the command that regenerates it in full mode.**
1241
+ Its last line said to run `rails ai:context`, which writes compact files
1242
+ again unless `config.context_mode` is `:full`. It now says
1243
+ `CONTEXT_MODE=full rails ai:context`, which keeps the AI tool selection
1244
+ as `rails ai:context:full` does not; a standalone install, whose only
1245
+ way into full mode is `context_mode: full` in `.rails-ai-context.yml`,
1246
+ keeps `rails-ai-context context`. Copilot's full file shares the line.
1247
+ - **Every generated file states the route count the same way.** CLAUDE.md
1248
+ said "12 app routes across 3 routed controllers (39 total incl.
1249
+ framework)" while `.claude/rules/rails-context.md`,
1250
+ `.cursor/rules/rails-project.mdc` and
1251
+ `.github/instructions/rails-context.instructions.md` said "Routes: 39";
1252
+ an app with no routes of its own read "0 app routes" beside "Routes: 27".
1253
+ The rule files now print CLAUDE.md's line, from one place
1254
+ (`RouteCoverage.summary`), as `rails ai:inspect` does.
1255
+ - **The tool guide's examples name the app's own model, controller and
1256
+ files.** Every app's workflows said `rails_get_context(model:"Post")`,
1257
+ `app/models/post.rb`, `PostsController`, `publishable?` and
1258
+ `shared/status_badge`, right beside the rule never to name what a tool
1259
+ call has not verified, so an app of Authors and Books read them as its
1260
+ own. They now use the app's most connected model, its controller, a
1261
+ template and a partial it renders and a method its source defines, and
1262
+ an obvious placeholder (`YourModel`) where the app has none. The tools
1263
+ table's mailers row says `mailer:"X"` like the rest, where it said
1264
+ `UserMailer`.
1265
+ - **The tool guide names tools as `tools/list` does.** It sent the AI to
1266
+ `get_schema` and `get_model_details` and called `get_context` the power
1267
+ tool, none of which exists, and the `rails_diagnose` example put a
1268
+ backtick inside its code span, which cut it in two.
1269
+ - **The generated files count, list and document the tools the server
1270
+ serves.** `config.skip_tools` and `config.custom_tools` reached the server
1271
+ and the CLI but not the context files: with two tools skipped and one
1272
+ added, CLAUDE.md said 45, listed both skipped tools, and its tools table
1273
+ offered `rails 'ai:tool[query]'`, which answers "Unknown tool", while the
1274
+ custom tool went unmentioned. Every count, the name list and the tools
1275
+ table now read the server's own set, a custom tool getting a row with the
1276
+ first sentence of its description, and the installer's and `init`'s
1277
+ summaries count the same. No workflow step, rule, protocol line or
1278
+ pointer names a skipped tool either: it is left out, or said without the
1279
+ tool, so with `rails_get_context` skipped the model workflow starts from
1280
+ `rails_get_model_details`, and with `rails_validate` skipped no file asks
1281
+ the AI to validate with it.
1282
+ - **With `config.output_dir` set, the OpenCode per-directory files say why
1283
+ they were not written.** They are written only into a directory that
1284
+ exists, since OpenCode reads them beside the code, and the run reported
1285
+ `app/models not present` for an app that has one; it is the output
1286
+ directory that lacks it, and the reason now says `no app/models under
1287
+ output_dir`.
1288
+ - **The split rule files end with a newline.** The fourteen files under
1289
+ `.claude/rules/`, `.cursor/rules/`, `.github/instructions/` and
1290
+ `app/*/AGENTS.md` stopped at their last character, so an editor or an
1291
+ end-of-file fixer rewrote each one and git flagged it. Files written by
1292
+ an earlier version are rewritten once.
1293
+ - **A context file whose markers do not pair up is left as it is.** A lost
1294
+ `<!-- END rails-ai-context -->` had a fresh section prepended and the old
1295
+ one kept below it, so CLAUDE.md carried the gem's section twice from then
1296
+ on. With a marker missing, a second pair pasted in or END before BEGIN,
1297
+ nothing says where the gem's section ends, so a run now writes nothing to
1298
+ that file and names it `unpaired rails-ai-context marker, left as it is`.
1299
+ A regenerated section keeps a double backslash, where it was written as
1300
+ one.
1301
+ - **`rails_query` refuses a `DO $$ … $$` block as a statement it does not
1302
+ run**, not as "multiple statements": a semicolon inside a string or a
1303
+ block body is data now, and one between two statements is still caught.
1304
+ - **`rails_query` says when the row limit held rows back.** A capped result
1305
+ gave no sign the query had more; every format now ends with a note naming
1306
+ the limit, in CSV after a blank line so a parser still reads clean rows.
1307
+ - **`rails_query` bounds a MariaDB query with `max_statement_time`.**
1308
+ MariaDB ignores MySQL's `MAX_EXECUTION_TIME` hint, so `query_timeout` did
1309
+ nothing there and `SELECT SLEEP(8)` ran to the end, holding the
1310
+ connection. The statement is now wrapped in MariaDB's own `SET STATEMENT
1311
+ max_statement_time = <seconds> FOR …`, which leaves nothing on the pooled
1312
+ connection, and a cut query says "Query exceeded N second timeout".
1313
+ - **The selection record changes only what moved.** `init`, `rails
1314
+ ai:context` and the generator rewrote `.rails-ai-context.yml` through
1315
+ `to_yaml`, dropping every comment, and re-indented `config.ai_tools` in
1316
+ the initializer. They now edit `ai_tools`, `tool_mode` and
1317
+ `context_files` in place, keep comments, and write nothing when no value
1318
+ changed.
1319
+ - **The generated initializer passes a new Rails app's Rubocop**, and its
1320
+ comments say what the code does.
1321
+ - **The installer writes the context files in the mode it was just given.**
1322
+ A first install in CLI mode wrote files describing MCP tools via `rails
1323
+ ai:serve`.
1324
+ - **Install and context summaries name files from the app root, or from
1325
+ where the command was typed** (`init --app-path ../b` prints
1326
+ `../b/.mcp.json`), never as absolute paths. The `rm -f` line for the
1327
+ v5.0.0 leftover rule files does the same, so pasting it removes them.
1328
+ - **`init` asks every question before it boots the app, and writes nothing
1329
+ until the app's bundle is set up**; an MCP-only setup boots nothing. With
1330
+ an app that pins an older `json`, `init` printed four "already
1331
+ initialized constant" warnings; it prints none.
1332
+ - **`init` and `context` write Codex's environment as the binary was
1333
+ started**, not with the `GEM_HOME` and bundle `bin` directory the app's
1334
+ boot put into the process.
1335
+ - **Dropping an AI tool says what was kept and what was deleted** ("Kept
1336
+ AGENTS.md - Codex CLI uses it too", "Removed opencode.json
1337
+ (rails-ai-context was its only server)"), and a dot-directory the cleanup
1338
+ emptied is removed.
1339
+ - **Switching to CLI mode offers to take the gem out of the MCP configs**,
1340
+ and the CLI-mode summary no longer lists `.codex/config.toml`. The
1341
+ initializer is named whenever one of its lines moved, in `init` and on
1342
+ the first `rails ai:context`, which also records an MCP-only answer where
1343
+ it used to ask again on every run.
1344
+ - **Install summaries say only what applies**: re-runs show the current
1345
+ selection, only tools not yet picked are offered, there is no standalone
1346
+ pitch inside a bundle, the Codex gitignore hint appears only when it
1347
+ applies, an MCP-only install lists no `rails ai:context`, and
1348
+ `--defaults` no longer says "No tools selected". The setup answer is
1349
+ confirmed in the words the question offered it, and an unchanged file
1350
+ reads `(unchanged)` in every line.
1351
+ - **`rails ai:context:<tool>` writes that tool's MCP config in MCP mode**,
1352
+ and the binary's `context` keeps the MCP configs current as `rails
1353
+ ai:context` does: after `bundle remove rails-ai-context`, configs that
1354
+ started `bundle exec` are rewritten to the binary.
1355
+ - **The installer run at a mountable engine's root installs into that
1356
+ root.** It writes no initializer the host apps would run, no context
1357
+ files under `test/dummy`, and no hook calling a task the engine lacks,
1358
+ and it reports its MCP config once. The context files and the tools'
1359
+ hints there name `bundle exec rails-ai-context`, since `rails ai:serve`
1360
+ and `rails 'ai:tool[...]'` exist only in the dummy app. `doctor` reads
1361
+ that install, run as `bin/rails app:ai:doctor` or as
1362
+ `bundle exec rails-ai-context doctor` at the engine's root (which now
1363
+ checks the dummy app `bin/rails` boots instead of refusing), and its fixes
1364
+ name the commands that run there (a command of the dummy app's own, in
1365
+ `test/dummy`). It names every path from where it was
1366
+ typed: at the engine's root, the dummy app's files as `test/dummy/...` and
1367
+ the engine's own as `Gemfile.lock` and `app/views`; typed inside
1368
+ `test/dummy`, from `../..`.
1369
+ - **The standalone binary uses a `listen` installed beside it** for
1370
+ `watch` and for `serve`'s live reload, behind a `listen` the app's bundle
1371
+ locks, and `doctor` checks the same one. Where there is none, a
1372
+ standalone install is told `gem install listen`, and `serve` says each
1373
+ tool call still checks for changed files.
1374
+ - **Routes the app's own route files declare are app routes.**
1375
+ `devise_for` and the `rails/health` check were hidden as framework
1376
+ routes, so `rails_get_routes(controller: "devise/sessions")` answered "No
1377
+ routes"; a controller filter now finds its routes. The static tier draws
1378
+ no `new` or `edit` route for an API-only app, and the routes heading
1379
+ names the actions that skip a filter.
1380
+ - **Model details name the `devise` modules**, and Mongoid model details
1381
+ name the collection and the fields Mongoid adds.
1382
+ - **Next-step hints name only controllers the app has**, and under
1383
+ `tool_mode: :cli` they name the command, not an MCP call.
1384
+ - **N+1 risks follow a loaded collection into the loops and partials that
1385
+ walk it**, instead of matching an association name anywhere in any view,
1386
+ and each risk names its view.
1387
+ - **`rails_get_config` no longer calls a hand-written Authentication
1388
+ concern the Rails 8 generator.**
1389
+ - **The migration advisor warns about what the app's migrations already
1390
+ hold**: a pending migration that adds the column, a colliding migration
1391
+ name, `create_table` on an existing table, `null: false` with no default
1392
+ on a table with rows, and an unknown type ("Did you mean string?").
1393
+ Self-references use `to_table:`, and dropping a column lists the code
1394
+ that names it.
1395
+ - **`rails_get_schema` reads pending migrations from the connection**
1396
+ (an unmigrated database no longer reads "Pending migrations: none"),
1397
+ keeps each model on its own database's table, and marks encrypted, array
1398
+ and generated columns. A MySQL boolean default reads `true` or `false` on
1399
+ Rails 8.0 and 8.1, and SQLite row counts stop at a cap instead of
1400
+ reading every table whole.
1401
+ - **`rails_get_context` counts the instance variables a `before_action`
1402
+ sets.**
1403
+ - **`rails_validate level:"rails"` keeps its Brakeman findings when an app
1404
+ file does not parse**, and `DEBUG=1` no longer floods stderr from
1405
+ Brakeman, in validate and security_scan alike.
1406
+ - **`rails_search_code` shows the first match under a low
1407
+ `max_search_results`**; `definition` and `class` searches read a regex,
1408
+ and `definition` finds a name anywhere in a method name.
1409
+ - **`rails_analyze_feature`** lists Sidekiq workers, a service's own public
1410
+ methods and the files that call it, finds a model's test anywhere in the
1411
+ suite, and writes `after_commit (on: :create)`.
1412
+ - **`rails_get_test_info`'s RSpec template uses shoulda-matchers only when
1413
+ the app bundles it.**
1414
+ - **`rails_generate_test`** gives an existing test file the cases to add,
1415
+ gives a job a job test, signs a controller test in with the app's helper
1416
+ behind a login filter (or says it must), invents no params for a
1417
+ controller with no strong params, and writes a real system test for
1418
+ `type: "system"`.
1419
+ - **`rails_get_partial_interface` finds Turbo Stream, broadcast and
1420
+ jbuilder render sites** and jbuilder locals, lists a partial's render
1421
+ sites by the format each one looks up, and lists no helper as a local;
1422
+ `rails_get_view` lists those partials. A call names a format the way
1423
+ Rails reads one, as `formats:` among the options it renders with: a
1424
+ `format:` passed to the partial, or any key inside `locals:`, is a local,
1425
+ and so is every key of `render "shared/date", date: d, format: :short`.
1426
+ - **Render maps and frame sources keep their closing brackets.**
1427
+ - **`rails_get_turbo_map` wires a stream by the records and names it is
1428
+ built from**, says "can't tell" for what it cannot resolve, and says when
1429
+ a stream filter matches nothing.
1430
+ - **`rails_get_stimulus`'s markup** wires actions to the events the app's
1431
+ views use, leaves out change callbacks and the controller's own helpers,
1432
+ and adds class and outlet attributes.
1433
+ - **`rails_get_controllers`** says when an action needs a controller and
1434
+ which controller a bare name resolved to, answers for
1435
+ ApplicationController, counts template formats, and lists the implicit
1436
+ `format.turbo_stream` render.
1437
+ - **A 100 KB argument is answered at once and echoed shortened**, where
1438
+ `rails_analyze_feature` took 14 seconds and echoed it whole. v5.32.2
1439
+ repeated such an argument whole in 62 answers across 29 tools, among them
1440
+ the not-found and invalid-value answers of `rails_search_docs`,
1441
+ `rails_get_component_catalog`, `rails_get_concern`,
1442
+ `rails_review_changes`, `rails_generate_test`, `rails_get_controllers` and
1443
+ `rails_get_context`; each now shows its first 80 characters and its
1444
+ length. `rails_migration_advisor`, which wrote the argument into the
1445
+ migration twice over, refuses a name longer than 256 characters and
1446
+ options longer than 1,024. A `rails_review_changes` ref too long to hand
1447
+ git at all (128 KB on Linux, some 32,000 characters on Windows) is an
1448
+ unknown ref, where the answer was the error starting git raised.
1449
+ - **`rails_onboard` counts every database the app declares**, naming the
1450
+ framework's own databases in one line; it and
1451
+ `rails_get_frontend_stack` count every broadcast, and onboard includes
1452
+ STI payment subclasses.
1453
+ - **`rails_get_conventions`' create skeleton checks permission the app's
1454
+ way** and keeps controller namespaces.
1455
+ - **`rails_get_edit_context`'s not-found answer no longer offers the same
1456
+ path back.**
1457
+ - **The documented engine mount loads where the gem is not.** README,
1458
+ GUIDE and SETUP showed a mount that raised `NameError` in production
1459
+ with the gem in the development group, and after the gem was removed.
1460
+ They now show `mount RailsAiContext::Engine, at: "/mcp" if
1461
+ defined?(RailsAiContext::Engine)`.
1462
+ - **`serve --transport http` stops on SIGTERM or Ctrl-C every time and
1463
+ exits 0.** About one stop in five hung once a client had connected:
1464
+ Puma's graceful stop ran inside the signal handler and deadlocked. A
1465
+ request still running is cut off after 3 seconds, and `bin/rails
1466
+ ai:serve_http` no longer prints `bin/rails aborted! SignalException`.
1467
+ - **`rails server` stops with an MCP client connected to the mounted
1468
+ engine.** The engine answers the server-push GET with 405 instead of
1469
+ holding a thread per client.
1470
+ - **Each HTTP client keeps its own session record**, and a
1471
+ `rails_session_context` reset clears the caller's alone. The record holds
1472
+ the call the client made, not the tools a composite tool called inside
1473
+ it, and `mark` refuses an empty mark or one naming no tool.
1474
+ - **The HTTP error frame is JSON even when the app's `to_json` raises**
1475
+ (Rails 7.0-8.0 with json 3).
1476
+ - **`auto_mount` answers while a migration is pending**; the mounted
1477
+ engine, inside routing, still does not, as the docs say.
1478
+ - **The `http_bind` warning and docs say what a non-loopback bind
1479
+ serves.**
1480
+ - **`rails_read_logs` works on logs with non-ASCII lines.** On Ruby before
1481
+ 3.4 the redaction patterns raised `RegexpError` on such a line, breaking
1482
+ read_logs and `rails_diagnose`'s log section. `search:` now reaches back
1483
+ through the last 4 MB of a log and says how many lines it searched, where
1484
+ it searched only the last `lines` lines.
1485
+ - **The view resource labels a template by the language of its source**,
1486
+ where every view read was `text/html`.
1487
+ - **A resource read for a name the app does not have fails with JSON-RPC
1488
+ -32602**, naming the URI and the names that exist, where it came back as
1489
+ a successful read holding an `error` key.
1490
+ - **A commented MCP config reads the same under json 3.** json 3 refuses
1491
+ the comments json 2 passed over, so with it - what a new Rails 8.1 app
1492
+ resolves - a `.vscode/mcp.json` holding a comment was reported on every
1493
+ run as a file that does not parse, and doctor flagged it, even when its
1494
+ entries were current. Configs are now read with comments allowed on every
1495
+ json, so a current one is left alone as unchanged, and one that needs
1496
+ new entries is still left as it is, since writing it back would drop the
1497
+ comments.
1498
+
8
1499
  ## [5.32.2] - 2026-10-07
9
1500
 
10
1501
  ### Security