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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +1287 -0
- data/CONTEXT.md +26 -2
- data/README.md +13 -9
- data/Rakefile +6 -0
- data/app/controllers/rails_ai_context/mcp_controller.rb +114 -84
- data/docs/ARCHITECTURE.md +6 -4
- data/docs/CLI.md +79 -6
- data/docs/COMPATIBILITY.md +65 -1
- data/docs/CONFIGURATION.md +30 -11
- data/docs/CUSTOM_TOOLS.md +2 -1
- data/docs/FAQ.md +16 -6
- data/docs/GUIDE.md +117 -71
- data/docs/INTROSPECTORS.md +8 -7
- data/docs/QUICKSTART.md +3 -3
- data/docs/RECIPES.md +1 -1
- data/docs/SECURITY.md +109 -25
- data/docs/SETUP.md +30 -6
- data/docs/STANDALONE.md +10 -3
- data/docs/TOOLS.md +145 -34
- data/docs/TROUBLESHOOTING.md +90 -12
- data/docs/adr/0003-shared-tool-cache-semantics.md +8 -0
- data/docs/adr/0005-workspace-mcp-entries.md +39 -0
- data/exe/rails-ai-context +733 -104
- data/lib/generators/rails_ai_context/install/install_generator.rb +341 -125
- data/lib/rails_ai_context/ast_cache.rb +7 -0
- data/lib/rails_ai_context/base_mixins.rb +8 -8
- data/lib/rails_ai_context/boot_manager.rb +22 -3
- data/lib/rails_ai_context/change_watch.rb +14 -11
- data/lib/rails_ai_context/cli/app_root.rb +401 -0
- data/lib/rails_ai_context/cli/entry_boot.rb +53 -20
- data/lib/rails_ai_context/cli/tool_runner.rb +47 -21
- data/lib/rails_ai_context/code_reloader.rb +115 -1
- data/lib/rails_ai_context/concern_membership.rb +3 -1
- data/lib/rails_ai_context/concern_paths.rb +8 -3
- data/lib/rails_ai_context/configuration.rb +41 -9
- data/lib/rails_ai_context/context_file_report.rb +19 -2
- data/lib/rails_ai_context/database_file.rb +26 -0
- data/lib/rails_ai_context/doctor.rb +1122 -184
- data/lib/rails_ai_context/engine.rb +11 -1
- data/lib/rails_ai_context/erb_source.rb +19 -0
- data/lib/rails_ai_context/executable.rb +21 -0
- data/lib/rails_ai_context/facts_formatter.rb +5 -2
- data/lib/rails_ai_context/file_walk.rb +61 -10
- data/lib/rails_ai_context/fingerprinter/snapshot.rb +234 -0
- data/lib/rails_ai_context/fingerprinter.rb +40 -12
- data/lib/rails_ai_context/gem_lock.rb +176 -26
- data/lib/rails_ai_context/git_ignore.rb +22 -0
- data/lib/rails_ai_context/install/ai_tool.rb +31 -5
- data/lib/rails_ai_context/install/cleanup.rb +21 -0
- data/lib/rails_ai_context/install/program.rb +209 -35
- data/lib/rails_ai_context/install/selection_record.rb +258 -97
- data/lib/rails_ai_context/install/surface.rb +6 -1
- data/lib/rails_ai_context/install/validation_hook.rb +282 -0
- data/lib/rails_ai_context/install/workspace.rb +166 -0
- data/lib/rails_ai_context/install_mode.rb +71 -13
- data/lib/rails_ai_context/introspectors/action_mailbox_introspector.rb +1 -1
- data/lib/rails_ai_context/introspectors/action_presence.rb +11 -6
- data/lib/rails_ai_context/introspectors/action_source.rb +182 -0
- data/lib/rails_ai_context/introspectors/action_text_introspector.rb +1 -2
- data/lib/rails_ai_context/introspectors/active_storage_introspector.rb +1 -3
- data/lib/rails_ai_context/introspectors/active_support_introspector.rb +3 -3
- data/lib/rails_ai_context/introspectors/admin_resources.rb +1 -1
- data/lib/rails_ai_context/introspectors/anyway_configs.rb +1 -1
- data/lib/rails_ai_context/introspectors/api_introspector.rb +3 -3
- data/lib/rails_ai_context/introspectors/auth_introspector.rb +10 -3
- data/lib/rails_ai_context/introspectors/component_introspector.rb +3 -2
- data/lib/rails_ai_context/introspectors/controller_introspector.rb +56 -8
- data/lib/rails_ai_context/introspectors/convention_introspector.rb +1 -1
- data/lib/rails_ai_context/introspectors/database_stats_introspector.rb +14 -2
- data/lib/rails_ai_context/introspectors/devops_introspector.rb +3 -1
- data/lib/rails_ai_context/introspectors/eager_load.rb +201 -9
- data/lib/rails_ai_context/introspectors/engine_introspector.rb +1 -1
- data/lib/rails_ai_context/introspectors/env_config_introspector.rb +1 -1
- data/lib/rails_ai_context/introspectors/env_references.rb +10 -1
- data/lib/rails_ai_context/introspectors/frontend_framework_introspector.rb +1 -1
- data/lib/rails_ai_context/introspectors/gem_introspector.rb +2 -1
- data/lib/rails_ai_context/introspectors/grape_endpoints.rb +1 -1
- data/lib/rails_ai_context/introspectors/habtm_join_tables.rb +1 -1
- data/lib/rails_ai_context/introspectors/helper_names.rb +1 -1
- data/lib/rails_ai_context/introspectors/i18n_introspector.rb +2 -2
- data/lib/rails_ai_context/introspectors/job_introspector.rb +8 -6
- data/lib/rails_ai_context/introspectors/listeners/callbacks_listener.rb +46 -1
- data/lib/rails_ai_context/introspectors/listeners/env_access_listener.rb +10 -1
- data/lib/rails_ai_context/introspectors/listeners/macros_listener.rb +3 -1
- data/lib/rails_ai_context/introspectors/listeners/routes_dsl_listener.rb +14 -3
- data/lib/rails_ai_context/introspectors/listeners/scopes_listener.rb +88 -12
- data/lib/rails_ai_context/introspectors/middleware_introspector.rb +2 -2
- data/lib/rails_ai_context/introspectors/migration_introspector.rb +27 -0
- data/lib/rails_ai_context/introspectors/migration_replay.rb +2 -2
- data/lib/rails_ai_context/introspectors/model_introspector.rb +91 -5
- data/lib/rails_ai_context/introspectors/n_plus_one_scan.rb +620 -0
- data/lib/rails_ai_context/introspectors/performance_introspector.rb +11 -174
- data/lib/rails_ai_context/introspectors/route_introspector.rb +10 -1
- data/lib/rails_ai_context/introspectors/schema_introspector.rb +57 -4
- data/lib/rails_ai_context/introspectors/seeds_introspector.rb +1 -1
- data/lib/rails_ai_context/introspectors/source_scan.rb +13 -9
- data/lib/rails_ai_context/introspectors/stimulus_introspector.rb +15 -6
- data/lib/rails_ai_context/introspectors/turbo_introspector.rb +130 -11
- data/lib/rails_ai_context/introspectors/view_introspector.rb +3 -3
- data/lib/rails_ai_context/introspectors/view_template_introspector.rb +15 -1
- data/lib/rails_ai_context/legacy_cleanup.rb +17 -4
- data/lib/rails_ai_context/live_reload.rb +20 -20
- data/lib/rails_ai_context/mcp_config_generator.rb +916 -109
- data/lib/rails_ai_context/mcp_edge.rb +185 -32
- data/lib/rails_ai_context/middleware.rb +5 -1
- data/lib/rails_ai_context/migration_status.rb +88 -25
- data/lib/rails_ai_context/output_guard.rb +48 -2
- data/lib/rails_ai_context/package_json.rb +1 -1
- data/lib/rails_ai_context/path_resolver.rb +46 -13
- data/lib/rails_ai_context/payload.rb +55 -7
- data/lib/rails_ai_context/pending_migrations.rb +5 -1
- data/lib/rails_ai_context/portable_path.rb +3 -2
- data/lib/rails_ai_context/presets.rb +15 -11
- data/lib/rails_ai_context/redaction.rb +254 -51
- data/lib/rails_ai_context/resources.rb +55 -20
- data/lib/rails_ai_context/route_coverage.rb +8 -0
- data/lib/rails_ai_context/safe_file.rb +14 -1
- data/lib/rails_ai_context/safe_path.rb +101 -4
- data/lib/rails_ai_context/schema_adapter.rb +8 -0
- data/lib/rails_ai_context/schema_version.rb +9 -0
- data/lib/rails_ai_context/serializers/claude_rules_serializer.rb +86 -32
- data/lib/rails_ai_context/serializers/compact_serializer_helper.rb +36 -34
- data/lib/rails_ai_context/serializers/context_file_serializer.rb +21 -29
- data/lib/rails_ai_context/serializers/copilot_instructions_serializer.rb +8 -6
- data/lib/rails_ai_context/serializers/copilot_serializer.rb +2 -7
- data/lib/rails_ai_context/serializers/cursor_rules_serializer.rb +17 -15
- data/lib/rails_ai_context/serializers/guide_examples.rb +141 -0
- data/lib/rails_ai_context/serializers/markdown_serializer.rb +142 -29
- data/lib/rails_ai_context/serializers/opencode_rules_serializer.rb +36 -14
- data/lib/rails_ai_context/serializers/section_facts.rb +35 -1
- data/lib/rails_ai_context/serializers/section_marker_writer.rb +94 -12
- data/lib/rails_ai_context/serializers/stack_overview_helper.rb +24 -12
- data/lib/rails_ai_context/serializers/tool_guide_helper.rb +244 -129
- data/lib/rails_ai_context/server.rb +304 -71
- data/lib/rails_ai_context/tasks/rails_ai_context.rake +112 -43
- data/lib/rails_ai_context/tools/analyze_feature.rb +72 -18
- data/lib/rails_ai_context/tools/argument_schema.rb +49 -0
- data/lib/rails_ai_context/tools/base_tool.rb +352 -41
- data/lib/rails_ai_context/tools/brakeman_guard.rb +24 -0
- data/lib/rails_ai_context/tools/dependency_graph.rb +63 -28
- data/lib/rails_ai_context/tools/diagnose.rb +218 -11
- data/lib/rails_ai_context/tools/generate_test.rb +339 -39
- data/lib/rails_ai_context/tools/get_callbacks.rb +24 -13
- data/lib/rails_ai_context/tools/get_component_catalog.rb +1 -1
- data/lib/rails_ai_context/tools/get_concern.rb +91 -27
- data/lib/rails_ai_context/tools/get_config.rb +13 -4
- data/lib/rails_ai_context/tools/get_context.rb +71 -14
- data/lib/rails_ai_context/tools/get_controllers.rb +157 -40
- data/lib/rails_ai_context/tools/get_conventions.rb +26 -4
- data/lib/rails_ai_context/tools/get_edit_context.rb +27 -14
- data/lib/rails_ai_context/tools/get_env.rb +18 -9
- data/lib/rails_ai_context/tools/get_frontend_stack.rb +3 -1
- data/lib/rails_ai_context/tools/get_gems.rb +1 -1
- data/lib/rails_ai_context/tools/get_helper_methods.rb +9 -5
- data/lib/rails_ai_context/tools/get_i18n.rb +1 -1
- data/lib/rails_ai_context/tools/get_job_pattern.rb +2 -1
- data/lib/rails_ai_context/tools/get_mailers.rb +3 -3
- data/lib/rails_ai_context/tools/get_model_details.rb +16 -6
- data/lib/rails_ai_context/tools/get_partial_interface.rb +242 -23
- data/lib/rails_ai_context/tools/get_routes.rb +19 -6
- data/lib/rails_ai_context/tools/get_schema.rb +51 -18
- data/lib/rails_ai_context/tools/get_service_pattern.rb +24 -10
- data/lib/rails_ai_context/tools/get_stimulus.rb +37 -1
- data/lib/rails_ai_context/tools/get_test_info.rb +18 -4
- data/lib/rails_ai_context/tools/get_turbo_map.rb +221 -58
- data/lib/rails_ai_context/tools/get_view.rb +14 -14
- data/lib/rails_ai_context/tools/migration_advisor.rb +355 -48
- data/lib/rails_ai_context/tools/onboard.rb +46 -6
- data/lib/rails_ai_context/tools/performance_check.rb +4 -2
- data/lib/rails_ai_context/tools/query.rb +914 -86
- data/lib/rails_ai_context/tools/read_logs.rb +69 -15
- data/lib/rails_ai_context/tools/review_changes.rb +204 -43
- data/lib/rails_ai_context/tools/runtime_info.rb +51 -1
- data/lib/rails_ai_context/tools/safe_call.rb +35 -14
- data/lib/rails_ai_context/tools/search_code.rb +243 -67
- data/lib/rails_ai_context/tools/search_docs.rb +3 -3
- data/lib/rails_ai_context/tools/security_scan.rb +141 -31
- data/lib/rails_ai_context/tools/session_context.rb +38 -4
- data/lib/rails_ai_context/tools/validate.rb +259 -40
- data/lib/rails_ai_context/tools/validate_semantics.rb +129 -55
- data/lib/rails_ai_context/version.rb +1 -1
- data/lib/rails_ai_context/vfs.rb +67 -40
- data/lib/rails_ai_context/view_file.rb +41 -3
- data/lib/rails_ai_context/watcher.rb +51 -13
- data/lib/rails_ai_context.rb +21 -0
- data/server.json +1 -1
- metadata +13 -1
|
@@ -7,6 +7,9 @@ module RailsAiContext
|
|
|
7
7
|
module ToolGuideHelper
|
|
8
8
|
include CountPhrase
|
|
9
9
|
|
|
10
|
+
# A workflow step that runs a tool. A plain string is a step with none.
|
|
11
|
+
WorkflowStep = Struct.new(:text)
|
|
12
|
+
|
|
10
13
|
# Returns the tool invocation example for a given tool call.
|
|
11
14
|
# MCP: rails_analyze_feature(feature:"post")
|
|
12
15
|
# CLI: rails 'ai:tool[analyze_feature]' feature=post
|
|
@@ -25,6 +28,36 @@ module RailsAiContext
|
|
|
25
28
|
RailsAiContext.configuration.tool_mode
|
|
26
29
|
end
|
|
27
30
|
|
|
31
|
+
# A tool named once in prose: its MCP call, or its command where the
|
|
32
|
+
# app serves no MCP. A pointer in a rules file names the one form to
|
|
33
|
+
# use; the workflows below show both in :mcp mode.
|
|
34
|
+
def tool_ref(mcp_name, mcp_params = nil, cli_params = nil)
|
|
35
|
+
if tool_mode == :cli
|
|
36
|
+
"`#{cli_cmd(RailsAiContext::CLI::ToolRunner.short_name(mcp_name), cli_params)}`"
|
|
37
|
+
else
|
|
38
|
+
"`#{mcp_name}#{"(#{mcp_params})" if mcp_params}`"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# "`rails_get_schema` MCP tool", or the command in :cli mode. A
|
|
43
|
+
# parameter follows the name in prose, or goes into the command.
|
|
44
|
+
def tool_named(mcp_name, key = nil, value = nil)
|
|
45
|
+
return tool_ref(mcp_name, nil, ("#{key}=#{value}" if key)) if tool_mode == :cli
|
|
46
|
+
|
|
47
|
+
key ? "`#{mcp_name}` MCP tool with #{param_text(key, value)}" : "`#{mcp_name}` MCP tool"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The tools as a whole, in a sentence: in :cli mode, with how to run one.
|
|
51
|
+
def tools_noun
|
|
52
|
+
tool_mode == :cli ? "introspection tools (`#{cli_cmd("TOOL_NAME", "param=value")}`)" : "MCP tools"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# A parameter as the mode passes it: detail:"summary" to an MCP tool,
|
|
56
|
+
# detail=summary on the command line.
|
|
57
|
+
def param_text(key, value)
|
|
58
|
+
tool_mode == :cli ? "#{key}=#{value}" : "#{key}:\"#{value}\""
|
|
59
|
+
end
|
|
60
|
+
|
|
28
61
|
# True when the app runs in API-only mode (no view layer) - used to swap
|
|
29
62
|
# the view-editing workflow for an API-focused one in the generated guide.
|
|
30
63
|
# Falls back to false (the view workflow) when the includer has no
|
|
@@ -38,9 +71,28 @@ module RailsAiContext
|
|
|
38
71
|
RailsAiContext::Payload.architecture(ctx).include?("api_only")
|
|
39
72
|
end
|
|
40
73
|
|
|
41
|
-
#
|
|
74
|
+
# The tools the server serves: the built-ins skip_tools leaves, then
|
|
75
|
+
# the custom ones. Every count and list in the guide reads this, so none
|
|
76
|
+
# names a tool tools/list lacks. Resolved once per generation run.
|
|
77
|
+
def exposed_tools
|
|
78
|
+
RailsAiContext::RunCache.fetch(:exposed_tools) { RailsAiContext::Server.exposed_tools }
|
|
79
|
+
end
|
|
80
|
+
|
|
42
81
|
def tool_count
|
|
43
|
-
|
|
82
|
+
exposed_tools.size
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Whether the server serves the tool. The guide names no other: a step,
|
|
86
|
+
# rule or pointer whose tool skip_tools removed is left out, or said
|
|
87
|
+
# without it, so nothing sends the AI to a tool tools/list lacks.
|
|
88
|
+
def served?(mcp_name)
|
|
89
|
+
@served_tool_names ||= exposed_tools.to_set { |tool| RailsAiContext::Server.tool_label(tool) }
|
|
90
|
+
@served_tool_names.include?(mcp_name)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# The named tools the server serves, each as tool_ref gives it.
|
|
94
|
+
def served_refs(*mcp_names)
|
|
95
|
+
mcp_names.select { |name| served?(name) }.map { |name| tool_ref(name) }
|
|
44
96
|
end
|
|
45
97
|
|
|
46
98
|
def tools_header
|
|
@@ -53,7 +105,7 @@ module RailsAiContext
|
|
|
53
105
|
[
|
|
54
106
|
"This project has #{count_phrase(tool_count, "introspection tool")}. **MANDATORY - use these instead of reading files.**",
|
|
55
107
|
"They return ground truth from the running app: real schema, real associations, real filters - not guesses.",
|
|
56
|
-
"Read files ONLY when you are about to Edit them.",
|
|
108
|
+
"Run one with `#{cli_cmd("TOOL_NAME", "param=value")}`. Read files ONLY when you are about to Edit them.",
|
|
57
109
|
""
|
|
58
110
|
]
|
|
59
111
|
else
|
|
@@ -80,7 +132,7 @@ module RailsAiContext
|
|
|
80
132
|
"",
|
|
81
133
|
"1. **Verify before you write.** Never reference a column, association, route, helper, method, class, partial, or gem you have NOT verified in THIS project via a tool call in THIS turn. If it's not verified here, verify it now. Never invent names that \"sound right.\"",
|
|
82
134
|
"2. **Mark every assumption.** If you must proceed without verification, prefix the relevant output with `[ASSUMPTION]` and state what you're assuming and why. Silent assumptions are forbidden. \"I'd need to check X first\" is a valid and preferred answer.",
|
|
83
|
-
"3. **Training data describes average Rails. This app isn't average.** When something feels \"obviously\" like standard Rails, query anyway. Factories vs fixtures? Pundit vs CanCan? Devise vs has_secure_password?
|
|
135
|
+
"3. **Training data describes average Rails. This app isn't average.** When something feels \"obviously\" like standard Rails, query anyway. Factories vs fixtures? Pundit vs CanCan? Devise vs has_secure_password? #{stack_check_text} BEFORE scaffolding anything.",
|
|
84
136
|
"4. **Check the inheritance chain before every edit.** Before writing a controller action: inherited `before_action` filters and ancestor classes. Before writing a model method: concerns, includes, STI parents. Inheritance is never flat.",
|
|
85
137
|
"5. **Empty tool output is information, not permission.** \"0 callers found,\" \"no validations,\" or a missing model is a signal to investigate or confirm with the user - not a license to proceed on guesses. Follow `_Next:` hints.",
|
|
86
138
|
"6. **Stale context lies. Re-query after writes.** After any edit, tool output from earlier in this turn may be wrong. Re-query the affected tool before the next write.",
|
|
@@ -88,148 +140,176 @@ module RailsAiContext
|
|
|
88
140
|
]
|
|
89
141
|
end
|
|
90
142
|
|
|
143
|
+
# Rule 3's check before scaffolding: the conventions and gems tools the
|
|
144
|
+
# server serves, else the app's own files.
|
|
145
|
+
def stack_check_text
|
|
146
|
+
refs = served_refs("rails_get_conventions", "rails_get_gems")
|
|
147
|
+
refs.any? ? "Check #{refs.join(" and ")}" : "Check the Gemfile and the code that already does it"
|
|
148
|
+
end
|
|
149
|
+
|
|
91
150
|
def tools_detail_guidance
|
|
92
|
-
|
|
93
|
-
context_tool = tool_mode == :cli ? cli_cmd("context") : "rails_get_context"
|
|
94
|
-
analyze_tool = tool_mode == :cli ? cli_cmd("analyze_feature") : "rails_analyze_feature"
|
|
95
|
-
[
|
|
151
|
+
lines = [
|
|
96
152
|
"### detail parameter - ALWAYS start with summary",
|
|
97
153
|
"",
|
|
98
|
-
"Individual lookup tools accept `#{
|
|
154
|
+
"Individual lookup tools accept `#{param_text("detail", "summary")}`. Use the right level:",
|
|
99
155
|
"- **summary** - first call, orient yourself (table list, model names, route overview)",
|
|
100
156
|
"- **standard** - working detail (columns with types, associations, action source) - DEFAULT",
|
|
101
157
|
"- **full** - only when you need indexes, foreign keys, code snippets, or complete content",
|
|
102
158
|
"",
|
|
103
159
|
"Pattern: summary to find the target → standard to understand it → full only if needed.",
|
|
104
|
-
"",
|
|
105
|
-
"**Do NOT pass `detail` to composite tools** - `#{context_tool}` and `#{analyze_tool}` do not accept it and will return an error.",
|
|
106
160
|
""
|
|
107
161
|
]
|
|
162
|
+
composites = served_refs("rails_get_context", "rails_analyze_feature")
|
|
163
|
+
if composites.any?
|
|
164
|
+
lines << "**Do NOT pass `detail` to composite tools** - #{composites.join(" and ")} #{composites.size > 1 ? "do" : "does"} not accept it and will return an error."
|
|
165
|
+
lines << ""
|
|
166
|
+
end
|
|
167
|
+
lines
|
|
108
168
|
end
|
|
109
169
|
|
|
110
170
|
def tools_power_tool_section
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
"**New to this project?** Get a full walkthrough first:",
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
"",
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
]
|
|
171
|
+
ex = guide_examples
|
|
172
|
+
lines = []
|
|
173
|
+
if served?("rails_onboard")
|
|
174
|
+
lines.push("**New to this project?** Get a full walkthrough first:",
|
|
175
|
+
tool_call("rails_onboard(detail:\"standard\")", cli_cmd("onboard", "detail=standard")), "")
|
|
176
|
+
end
|
|
177
|
+
if served?("rails_get_context")
|
|
178
|
+
lines.push("**#{tool_ref("rails_get_context")} is your power tool** - bundles schema + model + controller + routes + views in ONE call:",
|
|
179
|
+
tool_call("rails_get_context(controller:\"#{ex.controller}\", action:\"#{ex.action}\")", cli_cmd("context", "controller=#{ex.controller} action=#{ex.action}")),
|
|
180
|
+
tool_call("rails_get_context(model:\"#{ex.model}\")", cli_cmd("context", "model=#{ex.model}")),
|
|
181
|
+
tool_call("rails_get_context(feature:\"#{ex.feature}\")", cli_cmd("context", "feature=#{ex.feature}")), "")
|
|
182
|
+
end
|
|
183
|
+
if served?("rails_analyze_feature")
|
|
184
|
+
lines.push("**#{tool_ref("rails_analyze_feature")} for broad discovery** - scans all layers (models, controllers, routes, services, jobs, views, tests):",
|
|
185
|
+
tool_call("rails_analyze_feature(feature:\"authentication\")", cli_cmd("analyze_feature", "feature=authentication")), "")
|
|
186
|
+
end
|
|
187
|
+
return [] if lines.empty?
|
|
188
|
+
|
|
189
|
+
composite = served?("rails_get_context") || served?("rails_analyze_feature")
|
|
190
|
+
lines.push("Use individual tools only when you need deeper detail on a specific layer.", "") if composite
|
|
191
|
+
[ composite ? "### Start here - composite tools save multiple calls" : "### Start here", "", *lines ]
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# The step, or nil when the server does not serve its tool.
|
|
195
|
+
def tool_step(mcp_name, mcp_params, cli_short, cli_params, note = nil)
|
|
196
|
+
return nil unless served?(mcp_name)
|
|
197
|
+
|
|
198
|
+
WorkflowStep.new([ tool_call_inline(mcp_name, mcp_params, cli_short, cli_params), note ].compact.join(" - "))
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# A titled workflow numbered over the steps left. One left with no tool
|
|
202
|
+
# at all says only "read the file and edit it", so it goes too.
|
|
203
|
+
def workflow(title, *steps)
|
|
204
|
+
return [] unless steps.any?(WorkflowStep)
|
|
205
|
+
|
|
206
|
+
texts = steps.compact.map { |step| step.is_a?(WorkflowStep) ? step.text : step }
|
|
207
|
+
[ title, *texts.each_with_index.map { |text, i| "#{i + 1}. #{text}" }, "" ]
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# A one-call example under its title, or nothing when its tool is not served.
|
|
211
|
+
def tool_example(title, mcp_name, mcp_call, cli_short, cli_params)
|
|
212
|
+
served?(mcp_name) ? [ title, tool_call(mcp_call, cli_cmd(cli_short, cli_params)), "" ] : []
|
|
128
213
|
end
|
|
129
214
|
|
|
130
215
|
def tools_workflow_section
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
"",
|
|
216
|
+
ex = guide_examples
|
|
217
|
+
lines = workflow(
|
|
134
218
|
"**Modify a model** (add field, change validation, add scope):",
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
"
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
"",
|
|
219
|
+
tool_step("rails_get_context", "model:\"#{ex.model}\"", "context", "model=#{ex.model}", "schema + associations + validations in one call") ||
|
|
220
|
+
tool_step("rails_get_model_details", "model:\"#{ex.model}\"", "model_details", "model=#{ex.model}", "associations, validations, scopes and enums"),
|
|
221
|
+
"Read the model file, make your edit",
|
|
222
|
+
tool_step("rails_migration_advisor", "action:\"add_column\", table:\"#{ex.table}\", column:\"rating\", type:\"integer\"", "migration_advisor",
|
|
223
|
+
"action=add_column table=#{ex.table} column=rating type=integer", "if schema change needed"),
|
|
224
|
+
tool_step("rails_validate", "files:[\"#{ex.model_file}\"], level:\"rails\"", "validate", "files=#{ex.model_file} level=rails", "EVERY time after editing"),
|
|
225
|
+
tool_step("rails_generate_test", "model:\"#{ex.model}\"", "generate_test", "model=#{ex.model}", "generate tests matching project patterns")
|
|
226
|
+
)
|
|
227
|
+
lines += workflow(
|
|
141
228
|
"**Fix a controller bug:**",
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
"
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
]
|
|
229
|
+
tool_step("rails_get_context", "controller:\"#{ex.controller}\", action:\"#{ex.action}\"", "context",
|
|
230
|
+
"controller=#{ex.controller} action=#{ex.action}", "action source + routes + views + model") ||
|
|
231
|
+
tool_step("rails_get_controllers", "controller:\"#{ex.controller}\", action:\"#{ex.action}\"", "controllers",
|
|
232
|
+
"controller=#{ex.controller} action=#{ex.action}", "action source + inherited filters + render map"),
|
|
233
|
+
"Read the controller file, make your fix",
|
|
234
|
+
tool_step("rails_validate", "files:[\"#{ex.controller_file}\"], level:\"rails\"", "validate", "files=#{ex.controller_file} level=rails")
|
|
235
|
+
)
|
|
236
|
+
lines += api_only? ? api_endpoint_workflow_lines : view_workflow_lines
|
|
237
|
+
lines += tool_example("**Trace a method:**", "rails_search_code",
|
|
238
|
+
"rails_search_code(pattern:\"#{ex.method_name}\", match_type:\"trace\")", "search_code", "pattern=\"#{ex.method_name}\" match_type=trace")
|
|
239
|
+
# Single quotes, as Ruby 3.4 prints the name: a backtick would close the code span.
|
|
240
|
+
lines += tool_example("**Debug an error (one call - gathers context + git + logs + fix):**", "rails_diagnose",
|
|
241
|
+
"rails_diagnose(error:\"NoMethodError: undefined method 'foo' for nil\", file:\"#{ex.model_file}\")",
|
|
242
|
+
"diagnose", "error=\"NoMethodError: undefined method 'foo' for nil\" file=#{ex.model_file}")
|
|
243
|
+
lines += tool_example("**Review changes before merging:**", "rails_review_changes", "rails_review_changes(ref:\"main\")", "review_changes", "ref=main")
|
|
244
|
+
lines += tool_example("**Generate tests matching project patterns:**", "rails_generate_test",
|
|
245
|
+
"rails_generate_test(model:\"#{ex.model}\")", "generate_test", "model=#{ex.model}")
|
|
246
|
+
return [] if lines.empty?
|
|
247
|
+
|
|
248
|
+
[ "### Step-by-step workflows (follow this order)", "", *lines ]
|
|
160
249
|
end
|
|
161
250
|
|
|
162
251
|
# HTML/Hotwire apps get the view-editing workflow.
|
|
163
252
|
def view_workflow_lines
|
|
164
|
-
|
|
253
|
+
ex = guide_examples
|
|
254
|
+
workflow(
|
|
165
255
|
"**Build or modify a view:**",
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
"
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
]
|
|
256
|
+
tool_step("rails_get_view", "controller:\"#{ex.view_controller}\"", "view", "controller=#{ex.view_controller}", "existing templates, partials, Stimulus refs"),
|
|
257
|
+
tool_step("rails_get_partial_interface", "partial:\"#{ex.partial}\"", "partial_interface", "partial=#{ex.partial}", "partial locals contract"),
|
|
258
|
+
tool_step("rails_get_component_catalog", "component:\"#{ex.component}\"", "component_catalog", "component=#{ex.component}", "ViewComponent/Phlex props, slots, previews"),
|
|
259
|
+
"Read the view file, make your edit",
|
|
260
|
+
tool_step("rails_validate", "files:[\"#{ex.view_file}\"]", "validate", "files=#{ex.view_file}")
|
|
261
|
+
)
|
|
173
262
|
end
|
|
174
263
|
|
|
175
264
|
# API-only apps have no view layer - swap in a workflow for modifying
|
|
176
265
|
# a JSON/XML response instead.
|
|
177
266
|
def api_endpoint_workflow_lines
|
|
178
|
-
|
|
267
|
+
ex = guide_examples
|
|
268
|
+
workflow(
|
|
179
269
|
"**Modify a JSON endpoint** (add/change a serialized field, adjust status codes):",
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
"
|
|
183
|
-
"
|
|
184
|
-
""
|
|
185
|
-
|
|
270
|
+
tool_step("rails_get_controllers", "controller:\"#{ex.controller}\", action:\"#{ex.action}\"", "controllers",
|
|
271
|
+
"controller=#{ex.controller} action=#{ex.action}", "action source + strong params + render map"),
|
|
272
|
+
tool_step("rails_get_model_details", "model:\"#{ex.model}\"", "model_details", "model=#{ex.model}", "schema + associations + validations backing the response"),
|
|
273
|
+
"Read the controller file, make your edit",
|
|
274
|
+
tool_step("rails_validate", "files:[\"#{ex.controller_file}\"], level:\"rails\"", "validate", "files=#{ex.controller_file} level=rails")
|
|
275
|
+
)
|
|
186
276
|
end
|
|
187
277
|
|
|
188
278
|
def tools_antipatterns_section
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
"
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
"- **
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
"- **Don't
|
|
199
|
-
|
|
200
|
-
"- **Don't call
|
|
201
|
-
|
|
202
|
-
|
|
279
|
+
lines = [ "### Common mistakes - avoid these", "" ]
|
|
280
|
+
lines << "- **Don't read #{schema_dump_path}** - use #{tool_ref("rails_get_schema")}. It adds [indexed]/[unique] hints you'd miss." if served?("rails_get_schema")
|
|
281
|
+
if served?("rails_get_model_details")
|
|
282
|
+
lines << "- **Don't read model files for reference** - use #{tool_ref("rails_get_model_details")}. It resolves concerns, inherited methods, and implicit belongs_to validations."
|
|
283
|
+
end
|
|
284
|
+
if served?("rails_search_code")
|
|
285
|
+
lines << "- **Prefer #{tool_ref("rails_search_code")} over Grep** for method tracing and cross-layer search. It excludes sensitive files, supports `#{param_text("match_type", "trace")}`, and paginates."
|
|
286
|
+
end
|
|
287
|
+
lines << if served?("rails_get_model_details")
|
|
288
|
+
"- **Don't call tools without a target** - #{tool_ref("rails_get_model_details", "")} without `#{tool_mode == :cli ? "model=" : "model:"}` returns a paginated list, not an error. Always specify what you want."
|
|
289
|
+
else
|
|
290
|
+
"- **Don't call tools without a target** - a lookup without one returns a paginated list, not an error. Always specify what you want."
|
|
291
|
+
end
|
|
292
|
+
if served?("rails_validate")
|
|
293
|
+
lines << "- **Don't skip validation** - run #{tool_ref("rails_validate")} after EVERY edit. It catches syntax errors AND Rails-specific issues (missing partials, bad column refs)."
|
|
294
|
+
end
|
|
295
|
+
lines << "- **Don't ignore cross-references** - tool responses include `_Next:` hints suggesting the best follow-up call. Follow them."
|
|
296
|
+
lines << "- **Don't call `#{param_text("detail", "full")}` first** - start with `summary` to find your target, then drill in. Full responses bury the signal."
|
|
297
|
+
lines << ""
|
|
203
298
|
end
|
|
204
299
|
|
|
205
300
|
def tools_rules_section
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
"2. **NEVER read reference files** - #{schema_dump_path}, config/routes.rb, model files, test files - tools are better",
|
|
213
|
-
"3. **Prefer `#{cli_cmd("search_code")}`** for tracing and cross-layer search - standard search tools are fine for simple targeted lookups",
|
|
214
|
-
"4. **Read files ONLY to Edit them** - not for reference",
|
|
215
|
-
"5. **Validate EVERY edit** - `#{cli_cmd("validate", "files=... level=rails")}`",
|
|
216
|
-
"6. **Follow _Next:_ hints** - tool responses suggest the best follow-up call",
|
|
217
|
-
""
|
|
218
|
-
]
|
|
219
|
-
else
|
|
220
|
-
[
|
|
221
|
-
"### Rules",
|
|
222
|
-
"",
|
|
223
|
-
"1. **Use composite tools first** - `rails_get_context` and `rails_analyze_feature` before individual tools",
|
|
224
|
-
"2. **NEVER read reference files** - #{schema_dump_path}, config/routes.rb, model files, test files - tools are better",
|
|
225
|
-
"3. **Prefer `rails_search_code`** for tracing and cross-layer search - standard search tools are fine for simple targeted lookups",
|
|
226
|
-
"4. **Read files ONLY to Edit them** - not for reference",
|
|
227
|
-
"5. **Validate EVERY edit** - `rails_validate(files:[...], level:\"rails\")`",
|
|
228
|
-
"6. **Follow _Next:_ hints** - tool responses suggest the best follow-up call",
|
|
229
|
-
"7. If MCP tools are not connected, use CLI: `#{cli_cmd("TOOL_NAME", "param=value")}`",
|
|
230
|
-
""
|
|
231
|
-
]
|
|
301
|
+
rules = []
|
|
302
|
+
composites = served_refs("rails_get_context", "rails_analyze_feature")
|
|
303
|
+
rules << "**Use composite tools first** - #{composites.join(" and ")} before individual tools" if composites.any?
|
|
304
|
+
rules << "**NEVER read reference files** - #{schema_dump_path}, config/routes.rb, model files, test files - tools are better"
|
|
305
|
+
if served?("rails_search_code")
|
|
306
|
+
rules << "**Prefer #{tool_ref("rails_search_code")}** for tracing and cross-layer search - standard search tools are fine for simple targeted lookups"
|
|
232
307
|
end
|
|
308
|
+
rules << "**Read files ONLY to Edit them** - not for reference"
|
|
309
|
+
rules << "**Validate EVERY edit** - #{tool_ref("rails_validate", 'files:[...], level:"rails"', "files=... level=rails")}" if served?("rails_validate")
|
|
310
|
+
rules << "**Follow _Next:_ hints** - tool responses suggest the best follow-up call"
|
|
311
|
+
rules << "If MCP tools are not connected, use CLI: `#{cli_cmd("TOOL_NAME", "param=value")}`" unless tool_mode == :cli
|
|
312
|
+
[ "### Rules", "", *rules.each_with_index.map { |rule, i| "#{i + 1}. #{rule}" }, "" ]
|
|
233
313
|
end
|
|
234
314
|
|
|
235
315
|
def tools_table
|
|
@@ -238,12 +318,16 @@ module RailsAiContext
|
|
|
238
318
|
lines
|
|
239
319
|
end
|
|
240
320
|
|
|
241
|
-
#
|
|
242
|
-
#
|
|
321
|
+
# One row per tool the server serves, in the guide's order: a built-in
|
|
322
|
+
# declares its own, and a custom tool, which declares none, follows
|
|
323
|
+
# with the first sentence of its description.
|
|
243
324
|
def tool_rows
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
325
|
+
builtin = exposed_tools.filter_map do |tool|
|
|
326
|
+
row = tool.guide_row if tool.respond_to?(:guide_row)
|
|
327
|
+
[ tool, row ] if row
|
|
328
|
+
end
|
|
329
|
+
custom = (exposed_tools - builtin.map(&:first)).map { |tool| [ tool, custom_tool_row(tool) ] }
|
|
330
|
+
builtin.sort_by { |_tool, row| row.order } + custom
|
|
247
331
|
end
|
|
248
332
|
|
|
249
333
|
def build_tools_table(include_mcp:)
|
|
@@ -276,6 +360,21 @@ module RailsAiContext
|
|
|
276
360
|
lines
|
|
277
361
|
end
|
|
278
362
|
|
|
363
|
+
# What a tools rule file adds to a root file that carries the guide: the
|
|
364
|
+
# detail levels and the table. Claude Code loads CLAUDE.md and its rules
|
|
365
|
+
# on every request, as Copilot does its instructions, so a whole guide in
|
|
366
|
+
# both put the guide and the protocol in front of the AI twice.
|
|
367
|
+
def render_tools_reference(root_file)
|
|
368
|
+
[
|
|
369
|
+
"## Tools (#{tool_count}) - Reference",
|
|
370
|
+
"",
|
|
371
|
+
"How to use them - the protocol, the workflows and the rules - is in #{root_file}. This file is the reference.",
|
|
372
|
+
"",
|
|
373
|
+
*tools_detail_guidance,
|
|
374
|
+
*tools_table
|
|
375
|
+
]
|
|
376
|
+
end
|
|
377
|
+
|
|
279
378
|
# Compact tool guide for root files (CLAUDE.md, AGENTS.md) that have line limits.
|
|
280
379
|
# Includes power tools + workflows + rules + dense tool name list (no table).
|
|
281
380
|
def render_tools_guide_compact
|
|
@@ -292,18 +391,35 @@ module RailsAiContext
|
|
|
292
391
|
lines
|
|
293
392
|
end
|
|
294
393
|
|
|
295
|
-
# Dense one-line
|
|
394
|
+
# Dense one-line listing, from the same rows the table uses: the MCP
|
|
395
|
+
# names, or in :cli mode the names the command takes.
|
|
296
396
|
def tools_name_list
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
397
|
+
cli = tool_mode == :cli
|
|
398
|
+
names = tool_rows.map do |tool, _row|
|
|
399
|
+
cli ? RailsAiContext::CLI::ToolRunner.short_name(tool.tool_name) : tool.tool_name
|
|
400
|
+
end
|
|
401
|
+
lines = [ "### All #{count_phrase(names.size, "tool")}" ]
|
|
402
|
+
lines << "Run one as `#{cli_cmd("NAME", "param=value")}`, NAME being one of:" if cli
|
|
403
|
+
lines << "`#{names.join('` `')}`"
|
|
404
|
+
lines << ""
|
|
303
405
|
end
|
|
304
406
|
|
|
305
407
|
private
|
|
306
408
|
|
|
409
|
+
# The model, controller and files the examples name, the app's own
|
|
410
|
+
# where it has them.
|
|
411
|
+
def guide_examples
|
|
412
|
+
@guide_examples ||= GuideExamples.new(respond_to?(:context) ? context : {})
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
# A row for a tool that declares none: its name and the first sentence
|
|
416
|
+
# of its description, which is all a custom MCP::Tool is sure to have.
|
|
417
|
+
def custom_tool_row(tool)
|
|
418
|
+
description = tool.respond_to?(:description_value) ? tool.description_value.to_s : ""
|
|
419
|
+
summary = description.strip.split(/(?<=\.)\s/).first.to_s.gsub(/\s+/, " ").gsub("|", "\\|")
|
|
420
|
+
RailsAiContext::Tools::BaseTool::GuideRow.new(mcp: tool.tool_name, summary: summary.empty? ? "Custom tool" : summary)
|
|
421
|
+
end
|
|
422
|
+
|
|
307
423
|
# Apps with `config.active_record.schema_format = :sql` dump the schema
|
|
308
424
|
# to db/structure.sql instead of db/schema.rb; guidance that names the
|
|
309
425
|
# wrong file sends the AI hunting for a file that does not exist.
|
|
@@ -330,23 +446,22 @@ module RailsAiContext
|
|
|
330
446
|
# Generate zsh-safe CLI command. In-Gemfile installs go through the rake
|
|
331
447
|
# task (`rails 'ai:tool[name]'`); standalone installs (gem not in the
|
|
332
448
|
# host app's Gemfile) have no rake tasks at all, so they use the CLI
|
|
333
|
-
# binary directly (`rails-ai-context tool name`)
|
|
449
|
+
# binary directly (`rails-ai-context tool name`), and an engine's root,
|
|
450
|
+
# whose tasks run in its dummy app, uses the binary in its bundle.
|
|
334
451
|
def cli_cmd(tool_name, params = nil)
|
|
335
|
-
cmd =
|
|
452
|
+
cmd = RailsAiContext::InstallMode.tool_command(tool_name, form: install_form)
|
|
336
453
|
cmd += " #{params}" if params
|
|
337
454
|
cmd
|
|
338
455
|
end
|
|
339
456
|
|
|
340
457
|
def serve_cmd
|
|
341
|
-
RailsAiContext::InstallMode.command(:serve,
|
|
458
|
+
RailsAiContext::InstallMode.command(:serve, form: install_form)
|
|
342
459
|
end
|
|
343
460
|
|
|
344
461
|
# Delegates to InstallMode (shared with the CLI surfaces), memoized per
|
|
345
462
|
# serializer instance because cli_cmd runs once per documented tool.
|
|
346
|
-
def
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
@standalone_install = RailsAiContext::InstallMode.standalone?
|
|
463
|
+
def install_form
|
|
464
|
+
@install_form ||= RailsAiContext::InstallMode.form
|
|
350
465
|
end
|
|
351
466
|
|
|
352
467
|
# Inline tool call for workflow steps (shorter format).
|