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