rails-ai-context 5.32.2 → 5.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1491 -0
  3. data/CONTEXT.md +27 -3
  4. data/README.md +16 -12
  5. data/Rakefile +6 -0
  6. data/app/controllers/rails_ai_context/mcp_controller.rb +112 -84
  7. data/config/routes.rb +2 -3
  8. data/docs/ARCHITECTURE.md +7 -5
  9. data/docs/CLI.md +79 -6
  10. data/docs/COMPATIBILITY.md +75 -1
  11. data/docs/CONFIGURATION.md +30 -11
  12. data/docs/CUSTOM_TOOLS.md +2 -1
  13. data/docs/FAQ.md +16 -6
  14. data/docs/GUIDE.md +117 -71
  15. data/docs/INTROSPECTORS.md +10 -8
  16. data/docs/QUICKSTART.md +3 -3
  17. data/docs/RECIPES.md +1 -1
  18. data/docs/SECURITY.md +109 -25
  19. data/docs/SETUP.md +30 -6
  20. data/docs/STANDALONE.md +10 -3
  21. data/docs/TOOLS.md +152 -34
  22. data/docs/TROUBLESHOOTING.md +90 -12
  23. data/docs/adr/0003-shared-tool-cache-semantics.md +8 -0
  24. data/docs/adr/0005-workspace-mcp-entries.md +39 -0
  25. data/exe/rails-ai-context +733 -104
  26. data/lib/generators/rails_ai_context/install/install_generator.rb +341 -125
  27. data/lib/rails_ai_context/app_kind.rb +18 -5
  28. data/lib/rails_ai_context/ast_cache.rb +7 -0
  29. data/lib/rails_ai_context/base_mixins.rb +8 -8
  30. data/lib/rails_ai_context/boot_manager.rb +22 -3
  31. data/lib/rails_ai_context/change_watch.rb +14 -11
  32. data/lib/rails_ai_context/cli/app_root.rb +401 -0
  33. data/lib/rails_ai_context/cli/entry_boot.rb +58 -22
  34. data/lib/rails_ai_context/cli/tool_runner.rb +47 -21
  35. data/lib/rails_ai_context/code_reloader.rb +115 -1
  36. data/lib/rails_ai_context/concern_membership.rb +3 -1
  37. data/lib/rails_ai_context/concern_paths.rb +8 -3
  38. data/lib/rails_ai_context/configuration.rb +41 -9
  39. data/lib/rails_ai_context/context_file_report.rb +19 -2
  40. data/lib/rails_ai_context/database_file.rb +26 -0
  41. data/lib/rails_ai_context/doctor.rb +1122 -184
  42. data/lib/rails_ai_context/engine.rb +18 -1
  43. data/lib/rails_ai_context/erb_source.rb +19 -0
  44. data/lib/rails_ai_context/executable.rb +21 -0
  45. data/lib/rails_ai_context/facts_formatter.rb +5 -2
  46. data/lib/rails_ai_context/file_walk.rb +61 -10
  47. data/lib/rails_ai_context/fingerprinter/snapshot.rb +234 -0
  48. data/lib/rails_ai_context/fingerprinter.rb +40 -12
  49. data/lib/rails_ai_context/gem_lock.rb +186 -27
  50. data/lib/rails_ai_context/git_ignore.rb +22 -0
  51. data/lib/rails_ai_context/install/ai_tool.rb +31 -5
  52. data/lib/rails_ai_context/install/cleanup.rb +21 -0
  53. data/lib/rails_ai_context/install/program.rb +209 -35
  54. data/lib/rails_ai_context/install/selection_record.rb +258 -97
  55. data/lib/rails_ai_context/install/surface.rb +6 -1
  56. data/lib/rails_ai_context/install/validation_hook.rb +282 -0
  57. data/lib/rails_ai_context/install/workspace.rb +166 -0
  58. data/lib/rails_ai_context/install_mode.rb +71 -13
  59. data/lib/rails_ai_context/introspectors/action_mailbox_introspector.rb +1 -1
  60. data/lib/rails_ai_context/introspectors/action_presence.rb +11 -6
  61. data/lib/rails_ai_context/introspectors/action_source.rb +182 -0
  62. data/lib/rails_ai_context/introspectors/action_text_introspector.rb +1 -2
  63. data/lib/rails_ai_context/introspectors/active_record_settings.rb +1 -1
  64. data/lib/rails_ai_context/introspectors/active_storage_introspector.rb +1 -3
  65. data/lib/rails_ai_context/introspectors/active_support_introspector.rb +3 -3
  66. data/lib/rails_ai_context/introspectors/admin_resources.rb +1 -1
  67. data/lib/rails_ai_context/introspectors/anyway_configs.rb +1 -1
  68. data/lib/rails_ai_context/introspectors/api_introspector.rb +3 -3
  69. data/lib/rails_ai_context/introspectors/auth_introspector.rb +15 -10
  70. data/lib/rails_ai_context/introspectors/call_site_expansion.rb +24 -1
  71. data/lib/rails_ai_context/introspectors/component_introspector.rb +3 -2
  72. data/lib/rails_ai_context/introspectors/controller_introspector.rb +387 -60
  73. data/lib/rails_ai_context/introspectors/convention_introspector.rb +1 -1
  74. data/lib/rails_ai_context/introspectors/database_stats_introspector.rb +15 -3
  75. data/lib/rails_ai_context/introspectors/devops_introspector.rb +3 -1
  76. data/lib/rails_ai_context/introspectors/eager_load.rb +201 -9
  77. data/lib/rails_ai_context/introspectors/engine_introspector.rb +1 -1
  78. data/lib/rails_ai_context/introspectors/env_config_introspector.rb +1 -1
  79. data/lib/rails_ai_context/introspectors/env_references.rb +10 -1
  80. data/lib/rails_ai_context/introspectors/frontend_framework_introspector.rb +1 -1
  81. data/lib/rails_ai_context/introspectors/gem_introspector.rb +23 -10
  82. data/lib/rails_ai_context/introspectors/grape_endpoints.rb +1 -1
  83. data/lib/rails_ai_context/introspectors/habtm_join_tables.rb +1 -1
  84. data/lib/rails_ai_context/introspectors/helper_names.rb +1 -1
  85. data/lib/rails_ai_context/introspectors/i18n_introspector.rb +3 -3
  86. data/lib/rails_ai_context/introspectors/job_introspector.rb +8 -6
  87. data/lib/rails_ai_context/introspectors/listeners/base_listener.rb +12 -8
  88. data/lib/rails_ai_context/introspectors/listeners/callbacks_listener.rb +78 -4
  89. data/lib/rails_ai_context/introspectors/listeners/config_assignment_listener.rb +1 -0
  90. data/lib/rails_ai_context/introspectors/listeners/env_access_listener.rb +10 -1
  91. data/lib/rails_ai_context/introspectors/listeners/foreign_evals_listener.rb +61 -0
  92. data/lib/rails_ai_context/introspectors/listeners/macros_listener.rb +13 -4
  93. data/lib/rails_ai_context/introspectors/listeners/migration_dsl_listener.rb +2 -1
  94. data/lib/rails_ai_context/introspectors/listeners/migration_replay_listener.rb +3 -2
  95. data/lib/rails_ai_context/introspectors/listeners/routes_dsl_listener.rb +29 -8
  96. data/lib/rails_ai_context/introspectors/listeners/schema_dsl_listener.rb +2 -2
  97. data/lib/rails_ai_context/introspectors/listeners/scopes_listener.rb +88 -12
  98. data/lib/rails_ai_context/introspectors/middleware_introspector.rb +2 -2
  99. data/lib/rails_ai_context/introspectors/migration_introspector.rb +27 -0
  100. data/lib/rails_ai_context/introspectors/migration_replay/helpers.rb +1 -1
  101. data/lib/rails_ai_context/introspectors/migration_replay/statements.rb +83 -27
  102. data/lib/rails_ai_context/introspectors/migration_replay.rb +15 -14
  103. data/lib/rails_ai_context/introspectors/model_introspector.rb +231 -23
  104. data/lib/rails_ai_context/introspectors/n_plus_one_scan.rb +620 -0
  105. data/lib/rails_ai_context/introspectors/performance_introspector.rb +11 -174
  106. data/lib/rails_ai_context/introspectors/route_introspector.rb +10 -1
  107. data/lib/rails_ai_context/introspectors/schema_conventions.rb +23 -3
  108. data/lib/rails_ai_context/introspectors/schema_introspector.rb +66 -16
  109. data/lib/rails_ai_context/introspectors/schema_reader.rb +4 -3
  110. data/lib/rails_ai_context/introspectors/seeds_introspector.rb +1 -1
  111. data/lib/rails_ai_context/introspectors/source_scan.rb +39 -9
  112. data/lib/rails_ai_context/introspectors/stimulus_introspector.rb +15 -6
  113. data/lib/rails_ai_context/introspectors/test_introspector.rb +1 -1
  114. data/lib/rails_ai_context/introspectors/turbo_introspector.rb +131 -12
  115. data/lib/rails_ai_context/introspectors/view_introspector.rb +4 -4
  116. data/lib/rails_ai_context/introspectors/view_template_introspector.rb +15 -1
  117. data/lib/rails_ai_context/legacy_cleanup.rb +17 -4
  118. data/lib/rails_ai_context/live_reload.rb +20 -20
  119. data/lib/rails_ai_context/mcp_config_generator.rb +916 -109
  120. data/lib/rails_ai_context/mcp_edge.rb +182 -32
  121. data/lib/rails_ai_context/middleware.rb +5 -1
  122. data/lib/rails_ai_context/migration_status.rb +88 -25
  123. data/lib/rails_ai_context/output_guard.rb +48 -2
  124. data/lib/rails_ai_context/package_json.rb +1 -1
  125. data/lib/rails_ai_context/path_resolver.rb +46 -13
  126. data/lib/rails_ai_context/payload.rb +92 -8
  127. data/lib/rails_ai_context/pending_migrations.rb +5 -1
  128. data/lib/rails_ai_context/portable_path.rb +3 -2
  129. data/lib/rails_ai_context/presets.rb +15 -11
  130. data/lib/rails_ai_context/redaction.rb +262 -51
  131. data/lib/rails_ai_context/resources.rb +55 -20
  132. data/lib/rails_ai_context/route_coverage.rb +8 -0
  133. data/lib/rails_ai_context/safe_file.rb +14 -1
  134. data/lib/rails_ai_context/safe_path.rb +101 -4
  135. data/lib/rails_ai_context/schema_adapter.rb +8 -0
  136. data/lib/rails_ai_context/schema_version.rb +9 -0
  137. data/lib/rails_ai_context/serializers/claude_rules_serializer.rb +88 -33
  138. data/lib/rails_ai_context/serializers/compact_serializer_helper.rb +36 -34
  139. data/lib/rails_ai_context/serializers/context_file_serializer.rb +21 -29
  140. data/lib/rails_ai_context/serializers/copilot_instructions_serializer.rb +8 -6
  141. data/lib/rails_ai_context/serializers/copilot_serializer.rb +2 -7
  142. data/lib/rails_ai_context/serializers/cursor_rules_serializer.rb +19 -16
  143. data/lib/rails_ai_context/serializers/guide_examples.rb +142 -0
  144. data/lib/rails_ai_context/serializers/markdown_serializer.rb +146 -30
  145. data/lib/rails_ai_context/serializers/opencode_rules_serializer.rb +38 -15
  146. data/lib/rails_ai_context/serializers/section_facts.rb +44 -1
  147. data/lib/rails_ai_context/serializers/section_marker_writer.rb +94 -12
  148. data/lib/rails_ai_context/serializers/stack_overview_helper.rb +24 -12
  149. data/lib/rails_ai_context/serializers/tool_guide_helper.rb +244 -129
  150. data/lib/rails_ai_context/server.rb +304 -71
  151. data/lib/rails_ai_context/tasks/rails_ai_context.rake +447 -372
  152. data/lib/rails_ai_context/tools/analyze_feature.rb +72 -18
  153. data/lib/rails_ai_context/tools/argument_schema.rb +49 -0
  154. data/lib/rails_ai_context/tools/base_tool.rb +367 -46
  155. data/lib/rails_ai_context/tools/brakeman_guard.rb +24 -0
  156. data/lib/rails_ai_context/tools/dependency_graph.rb +63 -28
  157. data/lib/rails_ai_context/tools/diagnose.rb +218 -11
  158. data/lib/rails_ai_context/tools/generate_test.rb +339 -39
  159. data/lib/rails_ai_context/tools/get_callbacks.rb +24 -13
  160. data/lib/rails_ai_context/tools/get_component_catalog.rb +1 -1
  161. data/lib/rails_ai_context/tools/get_concern.rb +91 -27
  162. data/lib/rails_ai_context/tools/get_config.rb +13 -4
  163. data/lib/rails_ai_context/tools/get_context.rb +71 -14
  164. data/lib/rails_ai_context/tools/get_controllers.rb +179 -44
  165. data/lib/rails_ai_context/tools/get_conventions.rb +28 -6
  166. data/lib/rails_ai_context/tools/get_edit_context.rb +27 -14
  167. data/lib/rails_ai_context/tools/get_env.rb +18 -9
  168. data/lib/rails_ai_context/tools/get_frontend_stack.rb +3 -1
  169. data/lib/rails_ai_context/tools/get_gems.rb +1 -1
  170. data/lib/rails_ai_context/tools/get_helper_methods.rb +10 -6
  171. data/lib/rails_ai_context/tools/get_i18n.rb +1 -1
  172. data/lib/rails_ai_context/tools/get_job_pattern.rb +3 -2
  173. data/lib/rails_ai_context/tools/get_mailers.rb +3 -3
  174. data/lib/rails_ai_context/tools/get_model_details.rb +30 -10
  175. data/lib/rails_ai_context/tools/get_partial_interface.rb +242 -23
  176. data/lib/rails_ai_context/tools/get_routes.rb +29 -8
  177. data/lib/rails_ai_context/tools/get_schema.rb +52 -19
  178. data/lib/rails_ai_context/tools/get_service_pattern.rb +24 -10
  179. data/lib/rails_ai_context/tools/get_stimulus.rb +37 -1
  180. data/lib/rails_ai_context/tools/get_test_info.rb +18 -4
  181. data/lib/rails_ai_context/tools/get_turbo_map.rb +221 -58
  182. data/lib/rails_ai_context/tools/get_view.rb +14 -14
  183. data/lib/rails_ai_context/tools/migration_advisor.rb +355 -48
  184. data/lib/rails_ai_context/tools/onboard.rb +52 -8
  185. data/lib/rails_ai_context/tools/performance_check.rb +5 -3
  186. data/lib/rails_ai_context/tools/query.rb +914 -86
  187. data/lib/rails_ai_context/tools/read_logs.rb +69 -15
  188. data/lib/rails_ai_context/tools/review_changes.rb +215 -53
  189. data/lib/rails_ai_context/tools/runtime_info.rb +51 -1
  190. data/lib/rails_ai_context/tools/safe_call.rb +35 -14
  191. data/lib/rails_ai_context/tools/search_code.rb +243 -67
  192. data/lib/rails_ai_context/tools/search_docs.rb +4 -4
  193. data/lib/rails_ai_context/tools/security_scan.rb +143 -33
  194. data/lib/rails_ai_context/tools/session_context.rb +39 -5
  195. data/lib/rails_ai_context/tools/validate.rb +259 -40
  196. data/lib/rails_ai_context/tools/validate_semantics.rb +130 -56
  197. data/lib/rails_ai_context/version.rb +1 -1
  198. data/lib/rails_ai_context/vfs.rb +67 -40
  199. data/lib/rails_ai_context/view_file.rb +41 -3
  200. data/lib/rails_ai_context/watcher.rb +51 -13
  201. data/lib/rails_ai_context.rb +21 -0
  202. data/server.json +1 -1
  203. metadata +14 -1
data/docs/GUIDE.md CHANGED
@@ -102,8 +102,8 @@ No Gemfile entry, no initializer, no files in your project besides config and co
102
102
  a. All of the above
103
103
  ```
104
104
 
105
- There is no "none" choice. An empty answer, or one with no valid number in it, prints `No tools selected - defaulting to all.` and selects all five.
106
- 2. **Offers to clean up tools you dropped.** Only on a re-run, and only when the last recorded selection (the `config.ai_tools` line in the initializer, else `.rails-ai-context.yml`) has a tool you did not pick this time. It lists them and asks `Remove their generated files?` with `y` (all), `n` (keep, the default) or numbers like `1,2`. A yes removes only what the gem generated: the rule files it names inside the tool's rules directory (the directory goes too when nothing else is left in it), and the block between the `rails-ai-context` markers in a root file such as `CLAUDE.md`. A rule file you wrote by hand, your own lines around the markers, a root file with no markers and a symlink are left alone. It also keeps any file a remaining tool shares (`AGENTS.md`), and takes only the `rails-ai-context` entry out of the tool's MCP config.
105
+ On a re-run the menu is followed by the selection recorded last time, as `Currently selected: 1,2 (Claude Code, Cursor)`. There is no "none" choice. An empty answer selects all five, and so does one with no valid number in it, which says so (`"9" names no tool - selecting all.`).
106
+ 2. **Offers to clean up tools you dropped.** Only on a re-run, and only when the last recorded selection (the `config.ai_tools` line in the initializer, else `.rails-ai-context.yml`) has a tool you did not pick this time. It lists them and asks `Remove their generated files?` with `y` (all), `n` (keep, the default) or numbers like `1,2`. A yes removes only what the gem generated: the rule files it names inside the tool's rules directory (the directory goes too when nothing else is left in it), and the block between the `rails-ai-context` markers in a root file such as `CLAUDE.md`. A rule file you wrote by hand, your own lines around the markers, a root file with no markers and a symlink are left alone. It keeps any file shared with a tool whose files stay (`AGENTS.md`) - one still selected, or one dropped whose files you kept by leaving its number out - and says so, and takes only the `rails-ai-context` entry out of the tool's MCP config - a config left with no other entry is deleted, and named as removed. A tool's dot-directory the cleanup emptied (`.cursor/`, `.github/`, `.codex/`, `.vscode/`) goes too; one that still holds a file of yours stays.
107
107
  3. **Asks what to write**, then `Enter number (default: 1):`
108
108
 
109
109
  ```
@@ -113,11 +113,11 @@ No Gemfile entry, no initializer, no files in your project besides config and co
113
113
  ```
114
114
 
115
115
  Choices 1 and 3 set `tool_mode` to `:mcp`, choice 2 sets it to `:cli`. Choice 3 also sets `context_files` to `false`. Any other answer counts as 1.
116
- 4. **Writes the MCP config for each selected tool** (`.mcp.json`, `.cursor/mcp.json`, `.vscode/mcp.json`, `opencode.json`, `.codex/config.toml`). Skipped for choice 2. An existing file is merged: only the `rails-ai-context` entry is added or replaced.
117
- 5. **Creates or updates `config/initializers/rails_ai_context.rb`.** A new file gets your three answers as live lines (`config.ai_tools`, `config.tool_mode`, `config.context_files`) and every other option commented out at its default, wrapped in `if defined?(RailsAiContext) && RailsAiContext.respond_to?(:configure)`. In an existing file it rewrites those three lines, appends any config section the file lacks, adds the guard if it is missing, and leaves the rest alone.
118
- 6. **Writes `.rails-ai-context.yml`** with `ai_tools`, `tool_mode` and `context_files`. Other keys already in the file are kept.
116
+ 4. **Writes the MCP config for each selected tool** (`.mcp.json`, `.cursor/mcp.json`, `.vscode/mcp.json`, `opencode.json`, `.codex/config.toml`). Skipped for choice 2, which instead lists any config that still starts `rails-ai-context` and asks `Remove rails-ai-context from them? (y/N)`: CLI mode starts no MCP server. A yes takes out only the gem's entries, as step 2 does. An existing file is merged: only the `rails-ai-context` entry is added or replaced. One that does not parse as JSON, or holds comments a rewrite would drop, is left as it is, with a warning that names the entry to add by hand. Each file is read as its client reads it. VS Code's and OpenCode's take JSONC, comments and trailing commas (read under json 2.9 or later); Claude Code reads `.mcp.json` as plain JSON, and Cursor's `.cursor/mcp.json` is held to plain JSON too, so a comment or a trailing comma in either is reported as one the client cannot read, and the file left as it is. In `.codex/config.toml` the server's table is found however its name is quoted, and replaced where it stands; an entry of the gem's written as an inline table or with dotted keys is left as it is, with a warning, since a table added beside it would declare the server twice, which Codex refuses.
117
+ 5. **Creates or updates `config/initializers/rails_ai_context.rb`.** A new file gets your three answers as live lines (`config.ai_tools`, `config.tool_mode`, `config.context_files`) and every other option commented out at its default, wrapped in `if defined?(RailsAiContext) && RailsAiContext.respond_to?(:configure)`; it passes the omakase Rubocop a new Rails app runs. In an existing file it rewrites those three lines where the value changed - keeping the line's indentation and any note of your own after it - appends any config section the file lacks, adds the guard if it is missing, and leaves the rest alone.
118
+ 6. **Writes `.rails-ai-context.yml`** with `ai_tools`, `tool_mode` and `context_files`. Only a key whose value changed is rewritten, where it stands; your comments and other keys are kept, and a run that changes no value leaves the file as it was.
119
119
  7. **Adds lines to `.gitignore`**, if the file exists: `.ai-context.json` (not for choice 3) and `.codex/config.toml` (always, because it holds this machine's Ruby paths). A line that is already there is not added again.
120
- 8. **Asks about a pre-commit hook:** `Install a pre-commit hook that validates Rails references? (y/N)`. Only `y` installs it. The hook runs `rails 'ai:tool[validate]'` on the staged `.rb` and `.erb` files. No question is asked when the app has no `.git` directory or when the hook is already installed. When another `.git/hooks/pre-commit` exists it prints `Skipped pre-commit hook (existing hook found - add manually)` and moves on.
120
+ 8. **Asks about a pre-commit hook:** `Install a pre-commit hook that checks staged Ruby, ERB and JavaScript files for syntax errors? (y/N)`. Only `y` installs it. The hook runs `bundle exec rails-ai-context tool validate` (`rails-ai-context tool validate` in a standalone install) on the staged `.rb`, `.erb`, `.js`, `.mjs` and `.cjs` files and stops a commit whose files do not parse (JavaScript is parsed by `node`, as [`rails_validate`](TOOLS.md#rails_validate) describes); the Rails-reference checks (`level=rails`) are left to the tool, as Brakeman would slow every commit. An app that cannot boot (an initializer that needs a variable the shell lacks, say) is still checked, from its source, and the hook shows why it did not boot; it shows whatever else the check prints to stderr too, so a check that cannot run says why. Every staged name is read whole, one with characters outside ASCII included; a name holding a comma, which the files list cannot carry, is named and left unchecked. It goes where git runs hooks (`git rev-parse --git-path hooks`), so an app checked out as a submodule, an app inside a monorepo and a repo with `core.hooksPath` set all get it where git will run it. The hook names the apps it covers and validates each one's staged files (deleted ones aside) from inside that app; installing in a second app of the same monorepo adds it to the hook, unless the hook was changed by hand, which it says, and for an app below the top of the repository the question names the repository. A hook an earlier version wrote, and nobody changed since, is brought up to date without asking again: the oldest handed validate the files a commit deletes too, and the commit failed on them, and later ones ran `rails 'ai:tool[validate]'`, which has to boot the app, with its errors thrown away, so an app that could not boot failed every commit with no reason given. Any other hook that runs `rails-ai-context` - one changed by hand, or a team's own that calls it among other checks - is left as it is, with a message. No hook is offered for an app the repository does not track yet (an app under a dotfiles repository at `$HOME`, say), nor into a `core.hooksPath` outside the repository, which every repository using it shares; both say why. No question is asked outside a git repository or when the hook already covers the app. When another `pre-commit` hook exists there it prints `Skipped pre-commit hook (existing hook found - add manually)` and moves on.
121
121
  9. **Generates the context files** for the selected tools. Skipped for choice 3. If files that v5.0.0 stopped generating are still there (`.claude/rules/rails-ui-patterns.md`, `.claude/rules/rails-accessibility.md`, `.cursor/rules/rails-ui-patterns.mdc`, `.github/instructions/rails-ui-patterns.instructions.md`), it lists them first and asks `Delete them? [y/N]:`. Without a terminal, or under `--defaults`, it prints the `rm` command and does not ask.
122
122
  10. **Prints a summary:** the files each selected tool got and the commands to run next.
123
123
 
@@ -126,12 +126,14 @@ Two flags change the prompts:
126
126
  - `--defaults` answers every prompt with its default: all five tools, keep dropped tools' files, choice 1, no hook, keep legacy files. Piping an empty stdin does the same.
127
127
  - `--mcp-only` skips the step 3 question and acts as choice 3. Steps 1, 2 and 8 still ask.
128
128
 
129
+ Run from a mountable engine's root, where `bin/rails` boots `test/dummy`, the install goes into the engine's root - the folder an editor is opened at, and the one every `rails-ai-context` command run there reads: the MCP configs, `.rails-ai-context.yml`, the `.gitignore` lines and the context files, which are generated from the engine's source the way its MCP server reads it there. No initializer is written, since Rails runs an engine's `config/initializers` in every app that mounts it and the gemspec ships them; one an earlier install left there is named, with a note to delete it. Step 8 offers no hook at the engine's root (its rake tasks run in the dummy app, as `app:ai:*`), and the summary lists the commands that work at the engine's root, `bundle exec rails-ai-context ...`. The context files written there name the same commands for their CLI fallback and the server, as do the tools' own hints. `bundle exec rails-ai-context doctor` at the engine's root diagnoses the dummy app and reads this install, as `bin/rails app:ai:doctor` does.
130
+
129
131
  A team that wants no AI tool files in the repo can skip the generator. The gem needs none of its output: `rails 'ai:tool[NAME]'`, `rails ai:serve`, `rails ai:doctor` and the `rails-ai-context` binary all work on the defaults with no initializer, no `.rails-ai-context.yml` and no MCP config. `rails ai:context` is the one to leave alone in that setup, since it writes context files. To make that safe too, set `config.ai_tools = []` in an initializer: an empty list means no tool, so nothing is written and nothing is asked.
130
132
 
131
133
  Two other entry points ask the same step 1 and step 3 questions:
132
134
 
133
- - `rails-ai-context init` (standalone) asks both, then offers the step 2 and step 9 cleanups, writes `.rails-ai-context.yml`, the MCP configs and the `.gitignore` lines, and generates the context files. On an MCP-only run it skips the step 9 question, as the generator does. It never creates the initializer (in one that exists it updates `config.ai_tools`, `config.tool_mode` and `config.context_files`), never offers the pre-commit hook, and has `--mcp-only` but no `--defaults`.
134
- - `rails ai:context` asks step 1 only when no tool selection is recorded, and step 3 only when no `tool_mode` is recorded. It then writes `.rails-ai-context.yml`, the MCP configs and the `.gitignore` lines before generating. It does not create the initializer or offer the hook.
135
+ - `rails-ai-context init` (standalone) asks both, then the step 2 cleanup question, step 4's question about MCP configs when the answer was CLI mode, and the step 9 question. Only then does it boot the app, so that every gem the app locks loads at the app's pin, and write: the cleanup, `.rails-ai-context.yml`, the MCP configs, the `.gitignore` lines and the context files. An MCP-only run skips the step 9 question, as the generator does, and boots nothing. It never creates the initializer (in one that exists it updates `config.ai_tools`, `config.tool_mode` and `config.context_files`), never offers the pre-commit hook, and has `--mcp-only` but no `--defaults`. Typed outside the app (`init --app-path ../shop`), it names the files it wrote from there, as `../shop/.mcp.json`. Run in a folder that holds several apps, it asks once and sets them all up: each app's own `.rails-ai-context.yml` and context files, and one MCP server per app in the folder's configs ([`init`](CLI.md#init-standalone-only)).
136
+ - `rails ai:context` asks step 1 only when no tool selection is recorded, and step 3 only when no `tool_mode` is recorded. It then writes `.rails-ai-context.yml`, the MCP configs and the `.gitignore` lines before generating. It does not create the initializer or offer the hook. `rails-ai-context context` brings the MCP configs of a recorded selection up to date the same way, and `rails ai:context:<tool>` writes that tool's MCP config along with its files.
135
137
 
136
138
  ---
137
139
 
@@ -146,8 +148,9 @@ rails ai:context
146
148
  ```
147
149
 
148
150
  - CLAUDE.md, AGENTS.md, .cursorrules and copilot-instructions.md ≤150 non-blank
149
- lines (`claude_max_lines`). Over budget, data lines are cut and the Commands,
150
- Warnings, Rules and MCP-tools sections kept whole
151
+ lines (`claude_max_lines`). Over budget, the data sections are cut first; the
152
+ title and the Commands, Warnings, Rules and Tools sections are never cut, so a
153
+ budget smaller than they are (about 110 lines) leaves just them
151
154
  - Files contain a project overview + MCP tool reference
152
155
  - AI uses MCP tools for detailed data on-demand
153
156
  - **Best for:** all apps, especially large ones (30+ models)
@@ -160,7 +163,9 @@ rails ai:context:full
160
163
  CONTEXT_MODE=full rails ai:context
161
164
  ```
162
165
 
163
- - Dumps everything into the root files CLAUDE.md, AGENTS.md and copilot-instructions.md (schema, all models, all routes, etc.). The split rule files and Cursor's files are the same in both modes
166
+ - Dumps everything into the root files CLAUDE.md, AGENTS.md and copilot-instructions.md: every table with its column types, defaults, indexes and foreign keys; every model with its associations, validations, scopes, callbacks, enum values and constants; the app's own routes (the framework's are counted, not listed); Stimulus controllers; and the rest of the introspection. The split rule files and Cursor's files are the same in both modes
167
+ - Each root file ends with the same tools guide and anti-hallucination protocol the compact files carry, so OpenCode and Codex, which read AGENTS.md alone, get them too
168
+ - A plain `rails ai:context` writes compact files again unless `config.context_mode` is `:full`, so each file's last line names the command that keeps full mode: `CONTEXT_MODE=full rails ai:context`, or for a standalone install `rails-ai-context context`, which reads `context_mode` from `.rails-ai-context.yml`
164
169
  - Can produce thousands of lines for large apps
165
170
  - **Best for:** small apps (<30 models) where the full dump fits in context
166
171
 
@@ -198,10 +203,10 @@ A file whose section has nothing in it is not written. An app with no models get
198
203
  | File | Purpose | Notes |
199
204
  |------|---------|-------|
200
205
  | `CLAUDE.md` | Main context file | ≤150 lines in compact mode. Claude Code reads this automatically. |
201
- | `.claude/rules/rails-schema.md` | Database table listing | Auto-loaded by Claude Code alongside CLAUDE.md. |
206
+ | `.claude/rules/rails-schema.md` | Database table listing | Every database's tables, each database under its own heading when the app has more than one. A database of Solid Queue, Solid Cache or Solid Cable tables only, as Rails 8's queue, cache and cable are, is named in CLAUDE.md instead (`queue: Solid Queue (13 tables)`). Loaded when Claude Code opens a schema dump or a migration. |
202
207
  | `.claude/rules/rails-models.md` | Model listing with associations | Auto-loaded by Claude Code alongside CLAUDE.md. |
203
208
  | `.claude/rules/rails-context.md` | Project context and conventions | Auto-loaded by Claude Code alongside CLAUDE.md. |
204
- | `.claude/rules/rails-mcp-tools.md` | Full MCP tool reference | Parameters, detail levels, pagination, workflow guide. |
209
+ | `.claude/rules/rails-mcp-tools.md` | Tool reference | Detail levels and the table of every tool. Loaded beside CLAUDE.md, which carries the protocol, the workflows and the rules, so they are not read twice; with `generate_root_files` off it carries them itself. |
205
210
  | `.claude/rules/rails-components.md` | View component listing | Written only when the app has view components. |
206
211
 
207
212
  ### OpenCode (3 files)
@@ -209,8 +214,8 @@ A file whose section has nothing in it is not written. An app with no models get
209
214
  | File | Purpose | Notes |
210
215
  |------|---------|-------|
211
216
  | `AGENTS.md` | Main context file | Native OpenCode format. ≤150 lines in compact mode. OpenCode also reads CLAUDE.md as fallback. |
212
- | `app/models/AGENTS.md` | Model reference | Auto-loaded by OpenCode when reading files in `app/models/`. |
213
- | `app/controllers/AGENTS.md` | Controller reference | Auto-loaded by OpenCode when reading files in `app/controllers/`. |
217
+ | `app/models/AGENTS.md` | Model reference | Auto-loaded by OpenCode when reading files in `app/models/`. One you wrote yourself keeps its text, with the listing added between markers. |
218
+ | `app/controllers/AGENTS.md` | Controller reference | Auto-loaded by OpenCode when reading files in `app/controllers/`. One you wrote yourself keeps its text, with the listing added between markers. |
214
219
 
215
220
  ### Cursor (5 files)
216
221
 
@@ -230,7 +235,7 @@ A file whose section has nothing in it is not written. An app with no models get
230
235
  | `.github/instructions/rails-models.instructions.md` | Model context | `applyTo: app/models/**/*.rb` - loaded when editing models. |
231
236
  | `.github/instructions/rails-controllers.instructions.md` | Controller context | `applyTo: app/controllers/**/*.rb` - loaded when editing controllers. |
232
237
  | `.github/instructions/rails-context.instructions.md` | Project context and conventions | `applyTo: **/*` - loaded everywhere. |
233
- | `.github/instructions/rails-mcp-tools.instructions.md` | MCP tool reference | `applyTo: **/*` - loaded everywhere. |
238
+ | `.github/instructions/rails-mcp-tools.instructions.md` | Tool reference | `applyTo: **/*` - loaded everywhere. Detail levels and the table; the protocol and workflows are in copilot-instructions.md, or here too with `generate_root_files` off. |
234
239
 
235
240
  ### Generic (1 file)
236
241
 
@@ -263,14 +268,18 @@ unmarked file was generated with the app running.
263
268
  | `rails ai:context:full` | full | all | Generate all files in full mode |
264
269
  | `rails ai:context:claude` | compact | Claude | CLAUDE.md + .claude/rules/ |
265
270
  | `rails ai:context:opencode` | compact | OpenCode | AGENTS.md + per-directory AGENTS.md |
266
- | `rails ai:context:codex` | compact | Codex | AGENTS.md + per-directory AGENTS.md (same files as OpenCode; `.codex/config.toml` comes from the installer or `rails ai:context`) |
271
+ | `rails ai:context:codex` | compact | Codex | AGENTS.md + per-directory AGENTS.md (same files as OpenCode), and `.codex/config.toml` in MCP mode |
267
272
  | `rails ai:context:cursor` | compact | Cursor | .cursor/rules/ |
268
273
  | `rails ai:context:copilot` | compact | Copilot | copilot-instructions.md + .github/instructions/ |
269
274
  | `rails ai:context:json` | - | JSON | .ai-context.json |
270
275
  | `CONTEXT_MODE=full rails ai:context:claude` | full | Claude | Full dump for Claude only |
271
- | `CONTEXT_MODE=full rails ai:context:cursor` | full | Cursor | Full dump for Cursor only |
276
+ | `CONTEXT_MODE=full rails ai:context:opencode` | full | OpenCode | Full dump in AGENTS.md, which Codex reads too |
272
277
  | `CONTEXT_MODE=full rails ai:context:copilot` | full | Copilot | Full dump for Copilot only |
273
278
 
279
+ Cursor's files are the same in both modes, so `CONTEXT_MODE=full` changes nothing for `rails ai:context:cursor`.
280
+
281
+ Each `rails ai:context:<tool>` adds its tool to the recorded selection and, in MCP mode, writes that tool's MCP config too (`.cursor/mcp.json` for Cursor), since its files tell the AI to call the MCP tools.
282
+
274
283
  ### CLI tools
275
284
 
276
285
  | Command | Description |
@@ -290,7 +299,7 @@ unmarked file was generated with the app running.
290
299
 
291
300
  | Command | Description |
292
301
  |---------|-------------|
293
- | `rails ai:doctor` | Run up to 25 diagnostic checks. Reports pass/warn/fail with fix suggestions. AI readiness score (0-100). `STRICT=1` exits 1 when a check fails. |
302
+ | `rails ai:doctor` | Run the diagnostic checks that apply to the app. Reports pass/warn/fail with fix suggestions. AI readiness score (0-100). `STRICT=1` exits 1 when a check fails. |
294
303
  | `rails ai:watch` | Watch for file changes and auto-regenerate context files. Requires `listen` gem. |
295
304
  | `rails ai:inspect` | Print introspection summary to stdout. Useful for debugging. |
296
305
  | `rails ai:facts` | Print a short schema facts summary (tables, columns, indexes, associations, dependencies). |
@@ -320,7 +329,7 @@ rails-ai-context version # Print version
320
329
  rails-ai-context help # Show all commands
321
330
  ```
322
331
 
323
- Run it from your Rails app root, or pass `--app-path PATH`. Booting needs `config/environment.rb`. When the app cannot boot, or with `--no-boot`, every command except `doctor` reads the source files instead.
332
+ Run it anywhere inside your Rails app, from a folder holding one app, or pass `--app-path PATH` ([how the app is found](CLI.md#tool)). Booting needs `config/environment.rb`. When the app cannot boot, or with `--no-boot`, every command except `doctor` reads the source files instead.
324
333
 
325
334
  **Config:** Standalone mode reads from `.rails-ai-context.yml` (created by `init`), and that is its only config source - the gem is not loaded while `config/initializers` runs. If no config file exists, defaults are used. With the gem in the Gemfile the two merge key by key ([Precedence](CONFIGURATION.md#precedence)).
326
335
 
@@ -393,7 +402,11 @@ end
393
402
  ```
394
403
 
395
404
  - **`:mcp`** - context files show MCP tool syntax (e.g. `rails_get_schema(table: "users")`). CLI tools still available as fallback.
396
- - **`:cli`** - context files show CLI syntax (e.g. `rails 'ai:tool[schema]' table=users`). No MCP server required.
405
+ - **`:cli`** - context files show CLI syntax (e.g. `rails 'ai:tool[schema]' table=users`). No MCP server required. Every file follows it: the root files, the split rule files and OpenCode's per-directory `AGENTS.md`, whose tools rule files then describe the CLI tools.
406
+
407
+ Every tool count and list in the generated files is the set the server serves: the built-in tools `skip_tools` leaves, plus `custom_tools`. A custom tool gets a row in the tools table with the first sentence of its description. A workflow step, rule or pointer whose tool is skipped is left out, or said without the tool (with `rails_get_context` skipped, the model workflow starts from `rails_get_model_details`), so no file sends the AI to a tool the server does not serve.
408
+
409
+ The workflow examples name the app's own most connected model, one of its controllers, a template and a partial it renders. An app with none of one gets an obvious placeholder (`YourModel`, `shared/your_partial`) rather than a name that reads like the app's.
397
410
 
398
411
  The `tool_mode` is selected during `rails generate rails_ai_context:install`.
399
412
 
@@ -482,7 +495,7 @@ Returns all routes: HTTP verbs, paths, controller actions, route names.
482
495
  | `detail` | string | `summary` / `standard` (default) / `full` |
483
496
  | `limit` | integer | Max routes to return. Default: 150 (standard), 200 (full). |
484
497
  | `offset` | integer | Skip routes for pagination. Default: 0. |
485
- | `app_only` | boolean | Filter out internal Rails routes (Active Storage, Action Mailbox, Conductor, etc.). Default: true. |
498
+ | `app_only` | boolean | Leave out the routes Rails' engines and gems draw on their own (Active Storage, Action Mailbox and its conductor, Turbo Native navigation, `rails/info`, `rails/mailers`, `rails/welcome`). The app's own declarations stay, `devise_for` and the health check included. A `controller` filter searches every route. Default: true. |
486
499
 
487
500
  **Examples:**
488
501
 
@@ -681,7 +694,7 @@ rails_get_edit_context(file: "app/controllers/posts_controller.rb", near: "def i
681
694
 
682
695
  ### rails_validate
683
696
 
684
- Validates syntax of multiple files at once (Ruby, ERB, JavaScript). Optionally runs Rails-aware semantic checks.
697
+ Validates syntax of multiple files at once (Ruby, ERB, JavaScript). Optionally runs Rails-aware semantic checks. JavaScript needs `node` on the PATH; without it a file whose brackets balance is listed as skipped, not as OK.
685
698
 
686
699
  **Parameters:**
687
700
 
@@ -714,10 +727,10 @@ Ripgrep-powered regex search across the codebase.
714
727
 
715
728
  | Param | Type | Description |
716
729
  |-------|------|-------------|
717
- | `pattern` | string | **Required.** Regex pattern or method name to search for. |
730
+ | `pattern` | string | **Required.** Regex pattern or method name to search for. With `definition` or `class`, a plain method or constant name matches literally (`valid?`), anything else as a regex (`generate_.*`). |
718
731
  | `path` | string | Subdirectory to search in (e.g. `app/models`, `config`). Default: entire app. |
719
732
  | `file_type` | string | Filter by file extension (e.g. `rb`, `erb`, `js`). Alphanumeric only. |
720
- | `match_type` | string | `any` (default), `definition` (def lines), `class` (class/module lines), `call` (call sites only), `trace` (**full picture** - definition with class context + source code + internal calls + sibling methods + callers with route chain + test coverage separated). |
733
+ | `match_type` | string | `any` (default), `definition` (def lines whose method name contains the pattern), `class` (class/module lines whose name contains it), `call` (call sites only), `trace` (**full picture** - definition with class context + source code + internal calls + sibling methods + callers with route chain + test coverage separated). |
721
734
  | `exact_match` | boolean | Match the pattern literally, whole-word where its edges are word characters. `def reblog?` does not match `def reblog`. Default: false. |
722
735
  | `exclude_tests` | boolean | Exclude test/spec/features directories. Default: false. |
723
736
  | `group_by_file` | boolean | Group results by file with match counts. Default: false. |
@@ -725,7 +738,7 @@ Ripgrep-powered regex search across the codebase.
725
738
  | `limit` | integer | Max lines to return; the default is sized in matches. |
726
739
  | `context_lines` | integer | Lines of context before and after each match (like grep -C). Default: 2, max: 5. |
727
740
 
728
- Smart result limiting, sized in matches: under 10 shows all, 10-100 shows half, over 100 caps at 100. `offset` and `limit` count emitted lines, so a search with context returns more lines than matches. The header says how many matches were found, how many the page shows, and whether the line cap was reached. Once it names the cap, the count covers only the lines it scanned, so it is written `N+`.
741
+ Smart result limiting, sized in matches: under 10 shows all, 10-100 shows half, over 100 caps at 100. `offset` and `limit` count emitted lines, so a search with context returns more lines than matches. The header says how many matches were found, how many the page shows, and whether the line cap was reached. The cap always holds the first match and the context before it, so a `max_search_results` below `context_lines + 1` is raised to that. Once it names the cap, the count covers only the lines it scanned, so it is written `N+`.
729
742
 
730
743
  **Examples:**
731
744
 
@@ -735,7 +748,8 @@ rails_search_code(pattern: "publishable?", match_type: "trace")
735
748
  + sibling methods + app callers with route chain + test coverage (separated)
736
749
 
737
750
  rails_search_code(pattern: "create", match_type: "definition")
738
- → Only `def create` / `def self.create` lines
751
+ → Only def lines whose name contains `create`: `def create`, `def self.create`,
752
+ `def recreate`; add exact_match: true for `def create` alone
739
753
 
740
754
  rails_search_code(pattern: "publishable", match_type: "call")
741
755
  → Only call sites (excludes the definition)
@@ -749,14 +763,14 @@ rails_search_code(pattern: "has_many", group_by_file: true)
749
763
  rails_search_code(pattern: "post", exclude_tests: true)
750
764
  → Skip test/spec directories
751
765
 
752
- rails_search_code(pattern: "activate", match_type: "definition")
753
- → Only `def activate` / `def self.activate` lines (skips method calls)
766
+ rails_search_code(pattern: "generate_.*", match_type: "definition")
767
+ → Only def lines whose name matches the regex (skips method calls)
754
768
 
755
- rails_search_code(pattern: "User", match_type: "class")
756
- → Only `class User` / `module User` definitions
769
+ rails_search_code(pattern: "\\w+Job", match_type: "class")
770
+ → Only class/module definitions whose name matches the regex
757
771
  ```
758
772
 
759
- **Security:** Uses `Open3.capture3` with array arguments (no shell injection). Validates file_type. Blocks path traversal. Respects `excluded_paths` and `sensitive_patterns` config on both backends, and the Ruby fallback reads the ignore files ripgrep reads and ranks them the way ripgrep does: `.rgignore` over `.ignore` over `.gitignore` (each scoped to its own directory) over `.git/info/exclude` over the global excludes file, the file type deciding before depth, which breaks ties only within one type; `.ignore` and `.rgignore` apply outside a git repository too, and a directory the rules ignore is never entered. As ripgrep does, the fallback also reads the ignore files of every directory above the app, the git ones only as far up as the nearest `.git` (a file for a worktree or submodule), so an app nested in a larger repository gets that repository's rules and a repository nested in the app stops the app's `.gitignore`. Patterns match case-sensitively, symlinks are not followed, and a file with a NUL byte in its first 64 KiB is skipped unread, all as in ripgrep, so the two return the same set.
773
+ **Security:** Uses `Open3.capture3` with array arguments (no shell injection). Validates file_type. Blocks path traversal. Respects `excluded_paths` and `sensitive_patterns` config on both backends, and the Ruby fallback reads the ignore files ripgrep reads and ranks them the way ripgrep does: `.rgignore` over `.ignore` over `.gitignore` (each scoped to its own directory) over `.git/info/exclude` over the global excludes file, the file type deciding before depth, which breaks ties only within one type; `.ignore` and `.rgignore` apply outside a git repository too, and a directory the rules ignore is never entered. As ripgrep does, the fallback also reads the ignore files of every directory above the app, the git ones only as far up as the nearest `.git` (a file for a worktree or submodule), so an app nested in a larger repository gets that repository's rules and a repository nested in the app stops the app's `.gitignore`. Patterns match case-sensitively, symlinks are not followed but for a directory linked in from the app's git repository, which both search under the app's name for it, and a file with a NUL byte in its first 64 KiB is skipped unread, all as in ripgrep, so the two return the same set.
760
774
 
761
775
  ### rails_analyze_feature
762
776
 
@@ -786,7 +800,7 @@ rails_analyze_feature(feature: "orders")
786
800
  → Everything related to orders across all layers
787
801
  ```
788
802
 
789
- **Returns:** Markdown with sections for Models (with columns, associations, validations, scopes, enums), Controllers (with actions and filters), Routes, Services (with methods), Jobs (with queue/retry), Views (with partials and Stimulus refs), Stimulus controllers (with targets/values/actions), Tests (with counts), Related models, Concerns, Callbacks, Channels, Mailers, and Environment dependencies. Each section shows match counts.
803
+ **Returns:** Markdown with sections for Models (with columns, associations, validations, scopes, enums), Controllers (with actions and filters), Routes, Services (with their public methods and the files that call them), Jobs and Sidekiq workers (with queue/retry), Views (with partials and Stimulus refs), Stimulus controllers (with targets/values/actions), Tests (with counts), Related models, Concerns, Callbacks, Channels, Mailers, and Environment dependencies. Each section shows match counts.
790
804
 
791
805
  ### rails_security_scan
792
806
 
@@ -841,7 +855,7 @@ rails_get_concern(type: "model")
841
855
  → Lists only model concerns from app/models/concerns/
842
856
  ```
843
857
 
844
- **Returns:** Concern listing or full detail including file path, line count, included/extended modules, macros and DSL usage, public methods, class methods, callbacks, and a list of models or controllers that include the concern. Cross-references to related model/controller tools.
858
+ **Returns:** Concern listing or full detail including file path, line count, included/extended modules (a conditional include with its condition), macros and DSL usage (`helper_method` included), public methods, class methods, private methods, callbacks, and a list of models or controllers that include the concern. The listing counts private methods apart from public ones. Cross-references to related model/controller tools.
845
859
 
846
860
  ### rails_get_callbacks
847
861
 
@@ -873,7 +887,9 @@ Within one type the order is the order Rails runs them, base classes first, a
873
887
  `before_` or `around_` callback with `prepend: true` first, and
874
888
  `after_commit`/`after_rollback` last declared first unless the app runs them in
875
889
  order (`load_defaults 7.1`). A method declared again shows once with the later
876
- declaration's conditions.
890
+ declaration's conditions. A block or lambda callback shows its declaration at
891
+ `detail: "full"`, and turbo-rails' broadcast macros (`broadcasts_refreshes`,
892
+ `broadcasts_to` and the rest) are listed as the commit callbacks they declare.
877
893
 
878
894
  ### rails_get_helper_methods
879
895
 
@@ -1104,7 +1120,7 @@ The legacy `rails://models/{name}` form is still accepted.
1104
1120
 
1105
1121
  ### Dynamic Resource Templates (VFS)
1106
1122
 
1107
- Live resources introspected fresh on every request - zero stale data:
1123
+ Live resources introspected afresh on every request, after the app's code is reloaded for any edit:
1108
1124
 
1109
1125
  | Resource Template | Description |
1110
1126
  |-------------------|-------------|
@@ -1138,7 +1154,11 @@ The install generator (or `rails-ai-context init`) creates per-tool MCP config f
1138
1154
  | OpenCode | `opencode.json` | `mcp` | JSON |
1139
1155
  | Codex CLI | `.codex/config.toml` | `[mcp_servers]` | TOML |
1140
1156
 
1141
- Each file is merge-safe - only the `rails-ai-context` entry is managed, other servers are preserved. When the gem is not in the app's `Gemfile.lock` (standalone install) the entry runs `rails-ai-context serve` with no `bundle exec`.
1157
+ Each file is merge-safe - only the `rails-ai-context` entries are managed, other servers are preserved. When the gem is not in the app's `Gemfile.lock` (standalone install) the entry runs `rails-ai-context serve` with no `bundle exec`.
1158
+
1159
+ On Windows every entry starts the server through `cmd /c`: `"command": "cmd", "args": ["/c", "bundle", "exec", "rails-ai-context", "serve"]` (OpenCode: `"command": ["cmd", "/c", ...]`; Codex: `command = "cmd"`). A gem's executables there are batch files (`bundle.bat`, `rails-ai-context.bat`), which Claude Code, Codex and the other clients that start a server without a shell do not run. An entry written on Windows does not start on macOS or Linux, and one written there may not start on Windows: `doctor` names either, and running the install again rewrites it for the machine it runs on. A team on both keeps the MCP configs out of git, or runs the install on each machine.
1160
+
1161
+ A folder of apps gets one entry per app instead, `rails-ai-context-<app folder>`, each pointed at its app with `--app-path` and announcing its app first through `RAILS_AI_CONTEXT_SERVER_NAME`; see [`init`](CLI.md#init-standalone-only) for the shape each AI tool gets.
1142
1162
 
1143
1163
  **Example: `.mcp.json` (Claude Code)**
1144
1164
  ```json
@@ -1177,6 +1197,9 @@ claude mcp add rails-ai-context -- bundle exec rails ai:serve
1177
1197
 
1178
1198
  # Standalone
1179
1199
  claude mcp add rails-ai-context -- rails-ai-context serve
1200
+
1201
+ # On Windows, through cmd /c
1202
+ claude mcp add rails-ai-context -- cmd /c bundle exec rails-ai-context serve
1180
1203
  ```
1181
1204
 
1182
1205
  ### Claude Desktop
@@ -1195,7 +1218,7 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS)
1195
1218
  }
1196
1219
  ```
1197
1220
 
1198
- Or for standalone: replace `"command": "bundle"` / `"args": ["exec", "rails-ai-context", "serve"]` with `"command": "rails-ai-context"` / `"args": ["serve"]`.
1221
+ Or for standalone: replace `"command": "bundle"` / `"args": ["exec", "rails-ai-context", "serve"]` with `"command": "rails-ai-context"` / `"args": ["serve"]`. On Windows, start either through `cmd /c`: `"command": "cmd"`, with `"/c"` ahead of the rest of the args.
1199
1222
 
1200
1223
  ### Cursor
1201
1224
 
@@ -1232,12 +1255,12 @@ if defined?(RailsAiContext) && RailsAiContext.respond_to?(:configure)
1232
1255
  RailsAiContext.configure do |config|
1233
1256
  config.auto_mount = true
1234
1257
  config.http_path = "/mcp" # default
1235
- config.http_port = 6029 # default
1236
- config.http_bind = "127.0.0.1" # default (localhost only)
1237
1258
  end
1238
1259
  end
1239
1260
  ```
1240
1261
 
1262
+ The endpoint is then served by your app's own server, on its address and port; `http_bind` and `http_port` configure only the standalone process.
1263
+
1241
1264
  Both transports are **read-only** - they expose the same 45 tools and never modify your app.
1242
1265
 
1243
1266
  ### Controller Transport (Alternative)
@@ -1246,10 +1269,14 @@ For tighter Rails integration (authentication, routing, middleware stack), mount
1246
1269
 
1247
1270
  ```ruby
1248
1271
  # config/routes.rb
1249
- mount RailsAiContext::Engine, at: "/mcp"
1272
+ mount RailsAiContext::Engine, at: "/mcp" if defined?(RailsAiContext::Engine)
1250
1273
  ```
1251
1274
 
1252
- This provides a native Rails controller (`RailsAiContext::McpController`) that delegates to the Streamable HTTP transport.
1275
+ The `if` keeps the routes file loading wherever the gem is not: in production, where Bundler skips the `:development` group, and after the gem is removed. An unguarded mount raises `uninitialized constant RailsAiContext::Engine` there, and the app does not boot.
1276
+
1277
+ This provides a native Rails controller (`RailsAiContext::McpController`) that delegates to the Streamable HTTP transport. Outside development and test (production, staging) it refuses every request with a 403, as `auto_mount` does, unless you set `config.allow_http_in_production = true`; see [Security](SECURITY.md#mcp-http-transport).
1278
+
1279
+ The controller runs each call on the request's own thread and sends the answer whole, which keeps calls answering while the app reloads code in development. A custom tool's progress or log notifications therefore arrive together with its answer, and a request a tool makes of the client mid-call (sampling, elicitation, roots) fails at once; stdio and the standalone HTTP server deliver both as they are sent ([Setup](SETUP.md#http-transport-alternative)).
1253
1280
 
1254
1281
  ---
1255
1282
 
@@ -1370,6 +1397,8 @@ if defined?(RailsAiContext) && RailsAiContext.respond_to?(:configure)
1370
1397
 
1371
1398
  # Auto-mount Rack middleware for HTTP MCP
1372
1399
  config.auto_mount = false
1400
+ # Let the mounted engine and auto_mount answer in production
1401
+ config.allow_http_in_production = false
1373
1402
  config.http_path = "/mcp"
1374
1403
  config.http_bind = "127.0.0.1"
1375
1404
  config.http_port = 6029
@@ -1384,7 +1413,7 @@ end
1384
1413
  | `preset` | Symbol | `:full` | Introspector preset (`:full` or `:standard`) |
1385
1414
  | `introspectors` | Array | 40 (full preset) | Which introspectors to run |
1386
1415
  | `context_mode` | Symbol | `:compact` | `:compact` or `:full` |
1387
- | `claude_max_lines` | Integer | `150` | Max non-blank lines in a compact context file's gem-managed block, the `<!-- BEGIN/END rails-ai-context -->` markers included. Over budget, data lines are cut and the Commands, Warnings, Rules and MCP-tools sections kept whole |
1416
+ | `claude_max_lines` | Integer | `150` | Budget for the non-blank lines of a compact context file's gem-managed block, the `<!-- BEGIN/END rails-ai-context -->` markers included. Over budget, the data sections (Stack, Key models, Gems, Architecture) are cut first, down to none; the title and the Commands, Warnings, Rules and Tools sections are never cut, so they set the smallest file a budget can give (about 110 lines with the anti-hallucination protocol) |
1388
1417
  | `max_tool_response_chars` | Integer | `200_000` | Safety cap for MCP tool responses and resource payloads |
1389
1418
  | `cache_ttl` | Integer | `60` | Cache TTL in seconds for introspection results |
1390
1419
  | `custom_tools` | Array | `[]` | Additional MCP tool classes to register alongside built-in tools |
@@ -1394,14 +1423,15 @@ end
1394
1423
  | `excluded_models` | Array | internal Rails models | Models to skip |
1395
1424
  | `excluded_paths` | Array | `node_modules tmp log vendor .git doc docs` | Paths excluded from code search |
1396
1425
  | `sensitive_patterns` | Array | `.env`, `.key`, `.pem`, credentials | File patterns blocked from search and read tools |
1397
- | `output_dir` | String | `nil` (Rails.root) | Where to write context files |
1426
+ | `output_dir` | String | `nil` (Rails.root) | Where to write context files. OpenCode's `app/models/AGENTS.md` and `app/controllers/AGENTS.md` are written only where that directory exists under it |
1398
1427
  | `auto_mount` | Boolean | `false` | Auto-mount HTTP MCP endpoint |
1399
- | `http_path` | String | `"/mcp"` | HTTP endpoint path |
1400
- | `http_bind` | String | `"127.0.0.1"` | HTTP bind address |
1401
- | `http_port` | Integer | `6029` | HTTP server port |
1428
+ | `allow_http_in_production` | Boolean | `false` | Let the mounted engine and `auto_mount` answer in production and every other environment but development and test. Off, they refuse there with a 403. See [Security](SECURITY.md#mcp-http-transport) |
1429
+ | `http_path` | String | `"/mcp"` | HTTP endpoint path (`auto_mount` and the standalone server) |
1430
+ | `http_bind` | String | `"127.0.0.1"` | Address the standalone HTTP server binds. Another address still answers only a loopback `Host` header; see [CONFIGURATION](CONFIGURATION.md#mcp-server) |
1431
+ | `http_port` | Integer | `6029` | Port of the standalone HTTP server |
1402
1432
  | `live_reload` | Symbol/Boolean | `:auto` | `:auto`, `true`, or `false` - enable MCP live reload |
1403
1433
  | `live_reload_debounce` | Float | `1.5` | Debounce interval in seconds for live reload |
1404
- | `server_name` | String | `"rails-ai-context"` | MCP server name |
1434
+ | `server_name` | String | `"rails-ai-context"` | The MCP server name the server announces. When it is left at the default, the `RAILS_AI_CONTEXT_SERVER_NAME` environment variable takes its place for one process, which is how a workspace's entries announce their app |
1405
1435
  | `server_version` | String | gem version | MCP server version. Read-only, there is no setter |
1406
1436
  | `generate_root_files` | Boolean | `true` | Set `false` to generate split rules only: no CLAUDE.md, AGENTS.md, .cursorrules or copilot-instructions.md (`.ai-context.json` is still written) |
1407
1437
  | `anti_hallucination_rules` | Boolean | `true` | Embed 6-rule Anti-Hallucination Protocol in generated context files - set `false` to skip |
@@ -1415,7 +1445,7 @@ end
1415
1445
  | `max_search_results` | Integer | `200` | Max lines a search may emit per call, matches and context together |
1416
1446
  | `max_validate_files` | Integer | `50` | Max files per validate call |
1417
1447
  | `excluded_controllers` | Array | `DeviseController`, etc. | Controller classes hidden from listings |
1418
- | `excluded_route_prefixes` | Array | `action_mailbox/`, `active_storage/`, etc. | Route controller prefixes hidden with `app_only` |
1448
+ | `excluded_route_prefixes` | Array | `action_mailbox/`, `active_storage/`, `rails/conductor/`, `rails/info`, `rails/mailers`, `rails/welcome`, `turbo/` | Route controller prefixes hidden with `app_only`: the routes Rails' engines and gems draw on their own |
1419
1449
  | `excluded_association_names` | Array | 7 framework associations | Framework association names hidden from model output |
1420
1450
  | `excluded_concerns` | Array of Regex or String | framework regex patterns | Patterns for concerns to hide. A YAML list replaces the framework defaults; the initializer's `+=` adds to them |
1421
1451
  | `excluded_filters` | Array | `verify_authenticity_token`, etc. | Framework filter names hidden from controller output |
@@ -1427,7 +1457,7 @@ end
1427
1457
  | `extra_app_paths` | Array | `[]` | More app-root-relative directories that hold Rails app code. Each one's `app/models`, `app/controllers` and `app/views` are scanned |
1428
1458
  | `query_timeout` | Integer | `5` | `rails_query` statement timeout in seconds |
1429
1459
  | `query_row_limit` | Integer | `100` | Max rows `rails_query` returns (1 to 1000) |
1430
- | `query_redacted_columns` | Array | `password_digest`, `encrypted_password`, tokens, etc. | Column names whose values `rails_query` redacts |
1460
+ | `query_redacted_columns` | Array | `password_digest`, `encrypted_password`, tokens, etc. | Column names `rails_query` refuses: a query naming one is rejected before it runs, and a returned column of that name comes back `[FILTERED]` |
1431
1461
  | `query_allowed_columns` | Array | `[]` | Column names to exempt from the built-in sensitive list |
1432
1462
  | `allow_query_in_production` | Boolean | `false` | Allow `rails_query` in production |
1433
1463
  | `log_lines` | Integer | `50` | Default number of lines `rails_read_logs` tails |
@@ -1462,7 +1492,7 @@ Core Rails structure only. Use `config.preset = :standard` for a lighter footpri
1462
1492
  | Introspector | What it discovers |
1463
1493
  |-------------|-------------------|
1464
1494
  | `schema` | Tables, columns, types, indexes, foreign keys, primary keys. Falls back to `db/schema.rb` parsing when no DB connected. |
1465
- | `models` | Associations, validations, scopes, enums, callbacks, concerns, instance methods, class methods. Source-level macros via Prism AST (single-pass, 8 listeners): `has_secure_password`, `encrypts`, `normalizes`, `delegate`, `serialize`, `store`, `generates_token_for`, `has_one_attached`, `has_many_attached`, `has_rich_text`, `broadcasts_to`. Every result tagged `[VERIFIED]` or `[INFERRED]`. |
1495
+ | `models` | Associations, validations, scopes, enums, callbacks, concerns, instance methods, class methods. Source-level macros via Prism AST (single-pass, 8 listeners): `has_secure_password`, `encrypts`, `normalizes`, `delegate`, `serialize`, `store`, `generates_token_for`, `has_one_attached`, `has_many_attached`, `has_rich_text`, `broadcasts`, `broadcasts_to`, `broadcasts_refreshes`, `broadcasts_refreshes_to`. Every result tagged `[VERIFIED]` or `[INFERRED]`. |
1466
1496
  | `routes` | All routes with HTTP verbs, paths, controller actions, route names, API namespaces, mounted engines. |
1467
1497
  | `jobs` | ActiveJob classes with queue names. Mailers with action methods. Action Cable channels. |
1468
1498
  | `gems` | 127 notable gems categorized: auth, jobs, frontend, api, database, files, testing, deploy, monitoring, admin, pagination, search, forms, server, notifications, validation, utilities, services, payments. |
@@ -1650,33 +1680,34 @@ config.introspectors += %i[api]
1650
1680
  rails ai:doctor
1651
1681
  ```
1652
1682
 
1653
- Runs up to 25 checks and reports an AI readiness score (0-100). A check that does not apply to the app prints no row:
1683
+ Runs the checks below and reports an AI readiness score (0-100). A check that does not apply to the app prints no row:
1654
1684
 
1655
1685
  | Check | What it verifies |
1656
1686
  |-------|------------------|
1657
1687
  | Schema | A schema dump file exists |
1658
- | Pending migrations | No migration is pending (fails when one is) |
1659
- | Models | Model files detected |
1688
+ | Pending migrations | No migration is pending in any database the app migrates, each read through its own connection as `db:migrate:status` reads it, and nothing written to it: a database never migrated gets no `schema_migrations` table from doctor (fails when one is pending) |
1689
+ | Database | Shown in place of Pending migrations when a database the app migrates does not exist (fix: `bin/rails db:prepare`) or its server does not answer (fails). A SQLite database whose file is not there is named without connecting to it, since connecting would create an empty one |
1690
+ | Models | Model files detected where the tools read them: `app/models`, packs and in-repo engines, and in an engine's `test/dummy` the engine's own |
1660
1691
  | Routes | `config/routes.rb` exists |
1661
- | Gems | The lockfile exists |
1662
- | Controllers | Controller files detected |
1663
- | Views | Files exist under `app/views` |
1664
- | Tests | A test suite is found |
1665
- | Migrations | Migration files exist |
1666
- | Context files | Generated context files exist and are newer than the code they describe. No row on an MCP-only install |
1692
+ | Gems | The app's lockfile exists, or the one its `config/boot.rb` names (a monorepo's shared bundle, an engine's for its `test/dummy`) |
1693
+ | Controllers | Controller files detected, read as Models are |
1694
+ | Views | Files exist where the tools read views: `app/views`, packs, and in an engine's `test/dummy` the engine's |
1695
+ | Tests | A test suite is found: the app's, or in an engine's `test/dummy` the engine's |
1696
+ | Migrations | Migration files exist, counted over every database the app migrates, and in an engine's `test/dummy` the engine's own |
1697
+ | Context files | Generated context files exist where `config.output_dir` puts them, and a context run would leave them as they are. A file a run would rewrite, because an older version of the gem wrote it or the app changed under it, is named; a file that is only older than the code is not. In an engine's dummy app the install at the engine's root is read, and its own context command runs there, into a copy. No row on an MCP-only install |
1667
1698
  | Initializer guard | Shown only when `config/initializers/rails_ai_context.rb` has no guard, or guards on `defined?(RailsAiContext)` alone |
1668
- | MCP configs | Each selected tool's MCP config file exists and parses. Skipped in CLI-only mode |
1669
- | Codex env snapshot | The `GEM_HOME` saved in `.codex/config.toml` still exists. Only when Codex is selected |
1670
- | MCP server | MCP server can be built |
1699
+ | MCP configs | Each selected tool's MCP config file exists, parses, and holds a rails-ai-context server whose command can start: `bundle exec` only where the app's lockfile carries the gem and the bundle's copy has its executable, the `rails-ai-context` binary only where it is on PATH, and a warning when a config runs that binary in an app whose bundle carries the gem. The command is looked up, never run. Skipped in CLI-only mode |
1700
+ | Codex env snapshot | The `PATH` saved in `.codex/config.toml` (the app's, or a folder of apps' above it) still reaches each server's command, and a saved `GEM_HOME`, and every directory on a saved `GEM_PATH`, still exists - one missing from `GEM_PATH` passes when the shell's own `GEM_PATH` names it too, since RubyGems passes over it and the install would save it again. Only when Codex is selected |
1701
+ | MCP server | MCP server can be built, with an `mcp` gem this gem supports (warns, as the boot does, when the app's bundle pins one outside that range) |
1671
1702
  | Introspector health | Every configured introspector returns data |
1672
1703
  | Preset coverage | The preset covers the features the app has |
1673
1704
  | ripgrep | `rg` binary installed (optional, falls back to Ruby) |
1674
- | Prism parser | Prism is available for AST-based validation |
1675
- | Brakeman | Brakeman is available for `rails_security_scan` (optional) |
1705
+ | Prism parser | Prism is available for AST-based validation, at a version this gem supports (warns, as the boot does, when the app's bundle pins one below it) |
1706
+ | Brakeman | Brakeman is available for `rails_security_scan` (optional): in the app's bundle, or installed on the machine, where the scan runs the gem's own script as a process of its own. Doctor runs that script's `--version` the way the scan runs it, and warns, with what it printed, when it does not answer |
1676
1707
  | Live reload | `listen` gem installed (optional, enables MCP live reload) |
1677
- | MCP stdio hygiene | On a standalone install, gem activation prints nothing on stdout |
1678
- | Secrets in .gitignore | Sensitive files that exist are gitignored |
1679
- | MCP auto_mount | `auto_mount` is not on in production |
1708
+ | MCP stdio hygiene | On a standalone install, or where a config starts the `rails-ai-context` binary itself, gem activation prints nothing on stdout |
1709
+ | Secrets in .gitignore | Secret files that exist are gitignored and not committed: `config/master.key`, `config/credentials/*.key`, every `.env` file, `config/application.yml`, the Codex config and SSH and cloud credentials (fails when one is not ignored, or is committed: the fix is `git rm --cached` and rotating it). `config/database.yml`, `cable.yml`, `storage.yml` and the like warn only when they hold a password, token or key as a literal value. The encrypted `credentials.yml.enc` is committed by design and never reported. Inside a git repository git answers, so a rule in a `.gitignore` above the app (a monorepo's), in `.git/info/exclude` or in the global excludes counts; outside one the app's `.gitignore` is read |
1710
+ | MCP HTTP endpoint | `allow_http_in_production` is off, so the mounted engine and `auto_mount` refuse outside development and test; on, it warns, and with `auto_mount` it fails |
1680
1711
  | Schema file size | The schema file is under 80% of `max_schema_file_size` |
1681
1712
  | View aggregation size | `app/views` templates total under 80% of `max_view_total_size` |
1682
1713
 
@@ -1699,23 +1730,32 @@ Requires the `listen` gem:
1699
1730
  gem "listen", group: :development
1700
1731
  ```
1701
1732
 
1733
+ The standalone binary's `rails-ai-context watch` also finds a `listen` installed with `gem install listen`, the way it finds its own dependencies outside the app's bundle; a `listen` the app's bundle locks still wins.
1734
+
1702
1735
  Watches for changes in: `app/`, `config/`, `db/`, `lib/`, `rakelib/`, `test/`, `spec/`, plus the app directories of packs, in-repo engines and `extra_app_paths`, and regenerates only the files that changed (diff-aware, skips unchanged files).
1703
1736
 
1704
1737
  ---
1705
1738
 
1706
1739
  ## Live Reload (MCP)
1707
1740
 
1708
- When running the MCP server via `rails ai:serve`, **live reload** automatically invalidates tool caches and notifies connected AI clients when files change - so the AI always has fresh context without manual re-querying.
1741
+ When running the MCP server via `rails ai:serve`, **live reload** notifies connected AI clients when files change, so they know to re-query.
1742
+
1743
+ Answers never wait on it. Every tool call and resource read first checks the watched files, with `listen` or without it (a new Rails 8 app does not bundle it), and when one changed it reloads the app's code and drops the caches - so an answer sees every edit made before the call began, one made a moment earlier included, which `listen` would deliver only after its debounce. The check stats each watched directory and file and reads a directory again only when it changed: about a millisecond on a small app, about 30 ms at 10,000 files. Calls made at the same time, over HTTP, wait for one check together; calls one after another each check.
1744
+
1745
+ A server whose app cannot reload code - `RAILS_ENV=test`, or production with eager loading, where `config.enable_reloading` is off - keeps the code it booted with. What reflection reads, such as associations and enums, then stays as of boot (validations and callbacks are read off the source, so they still follow the edit), and once a file under the app's autoload paths changes, every answer ends with a note naming it: restart the server to see the edit.
1746
+
1747
+ One save a reload can miss: Bootsnap's compile cache knows a Ruby file by its size and the second of its mtime, so a file saved twice within one second at the same size can reload as the first save. Every answer then names the file until it is saved again a second later ([Known limits](COMPATIBILITY.md#known-limits)).
1709
1748
 
1710
1749
  ### How it works
1711
1750
 
1712
1751
  1. A background thread watches `app/`, `config/`, `db/`, `lib/`, `rakelib/`, `test/` and `spec/` (plus the app directories of packs, in-repo engines and `extra_app_paths`) for changes
1713
1752
  2. On change (debounced 1.5s), it checks the file fingerprint to avoid false positives
1714
1753
  3. If files truly changed, it:
1715
- - Clears all MCP tool caches
1716
1754
  - Sends `notifications/resources/list_changed` to the AI client
1717
1755
  - Logs a summary of what changed (e.g., "Files changed: 2 models, 1 controller.")
1718
1756
 
1757
+ The code reload and the cache drop are the calls' own: the first call after an edit makes them, on its own thread, when it checks the files. The watching thread loads none of the app's code.
1758
+
1719
1759
  ### Setup
1720
1760
 
1721
1761
  Add the `listen` gem (you may already have it from Watch Mode):
@@ -1725,6 +1765,8 @@ Add the `listen` gem (you may already have it from Watch Mode):
1725
1765
  gem "listen", group: :development
1726
1766
  ```
1727
1767
 
1768
+ The standalone binary's `rails-ai-context serve` also finds a `listen` installed with `gem install listen`, as `watch` does; a `listen` the app's bundle locks still wins.
1769
+
1728
1770
  Live reload is **enabled by default** when the `listen` gem is available. No configuration needed.
1729
1771
 
1730
1772
  ### Configuration
@@ -1784,7 +1826,7 @@ Works in:
1784
1826
  ### MCP server not detected by your AI tool
1785
1827
 
1786
1828
  1. Run `rails ai:doctor` - it checks per-tool MCP config files
1787
- 2. Verify the correct config file exists for your tool (`.mcp.json`, `.cursor/mcp.json`, `.vscode/mcp.json`, `opencode.json`, `.codex/config.toml`)
1829
+ 2. Verify the correct config file exists for your tool (`.mcp.json`, `.cursor/mcp.json`, `.vscode/mcp.json`, `opencode.json`, `.codex/config.toml`) in the folder the tool opened: the app, or the folder of apps `init` set up
1788
1830
  3. Re-run install (`rails generate rails_ai_context:install` or `rails-ai-context init`) to regenerate configs
1789
1831
  4. Restart your AI tool
1790
1832
 
@@ -1833,6 +1875,10 @@ bundle add listen --group development
1833
1875
 
1834
1876
  # Then run
1835
1877
  rails ai:watch
1878
+
1879
+ # Standalone install: no Gemfile change
1880
+ gem install listen
1881
+ rails-ai-context watch
1836
1882
  ```
1837
1883
 
1838
1884
  ### Tool responses show "not available"