agentf 0.3.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 (47) hide show
  1. checksums.yaml +7 -0
  2. data/bin/agentf +8 -0
  3. data/lib/agentf/agent_policy.rb +54 -0
  4. data/lib/agentf/agents/architect.rb +67 -0
  5. data/lib/agentf/agents/base.rb +53 -0
  6. data/lib/agentf/agents/debugger.rb +75 -0
  7. data/lib/agentf/agents/designer.rb +69 -0
  8. data/lib/agentf/agents/documenter.rb +58 -0
  9. data/lib/agentf/agents/explorer.rb +65 -0
  10. data/lib/agentf/agents/reviewer.rb +64 -0
  11. data/lib/agentf/agents/security.rb +84 -0
  12. data/lib/agentf/agents/specialist.rb +68 -0
  13. data/lib/agentf/agents/tester.rb +79 -0
  14. data/lib/agentf/agents.rb +19 -0
  15. data/lib/agentf/cli/architecture.rb +83 -0
  16. data/lib/agentf/cli/arg_parser.rb +50 -0
  17. data/lib/agentf/cli/code.rb +165 -0
  18. data/lib/agentf/cli/install.rb +112 -0
  19. data/lib/agentf/cli/memory.rb +393 -0
  20. data/lib/agentf/cli/metrics.rb +103 -0
  21. data/lib/agentf/cli/router.rb +111 -0
  22. data/lib/agentf/cli/update.rb +204 -0
  23. data/lib/agentf/commands/architecture.rb +183 -0
  24. data/lib/agentf/commands/debugger.rb +238 -0
  25. data/lib/agentf/commands/designer.rb +179 -0
  26. data/lib/agentf/commands/explorer.rb +208 -0
  27. data/lib/agentf/commands/memory_reviewer.rb +186 -0
  28. data/lib/agentf/commands/metrics.rb +272 -0
  29. data/lib/agentf/commands/security_scanner.rb +98 -0
  30. data/lib/agentf/commands/tester.rb +232 -0
  31. data/lib/agentf/commands.rb +17 -0
  32. data/lib/agentf/context_builder.rb +35 -0
  33. data/lib/agentf/installer.rb +580 -0
  34. data/lib/agentf/mcp/server.rb +310 -0
  35. data/lib/agentf/memory.rb +530 -0
  36. data/lib/agentf/packs.rb +74 -0
  37. data/lib/agentf/service/providers.rb +158 -0
  38. data/lib/agentf/tools/component_spec.rb +28 -0
  39. data/lib/agentf/tools/error_analysis.rb +19 -0
  40. data/lib/agentf/tools/file_match.rb +21 -0
  41. data/lib/agentf/tools/test_template.rb +17 -0
  42. data/lib/agentf/tools.rb +12 -0
  43. data/lib/agentf/version.rb +5 -0
  44. data/lib/agentf/workflow_contract.rb +158 -0
  45. data/lib/agentf/workflow_engine.rb +424 -0
  46. data/lib/agentf.rb +87 -0
  47. metadata +164 -0
@@ -0,0 +1,310 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mcp"
4
+ require "json"
5
+
6
+ module Agentf
7
+ module MCP
8
+ # Pure-Ruby MCP server for Agentf.
9
+ #
10
+ # Replaces the Node.js mcp-server/ sidecar. Runs over stdio and exposes
11
+ # the same 9 tools the Node.js version did, but calls Ruby classes
12
+ # directly instead of shelling out to CLI binaries.
13
+ #
14
+ # Guardrails (env vars):
15
+ # AGENTF_MCP_ALLOWED_TOOLS - comma-separated allowlist, or * for all
16
+ # AGENTF_MCP_ALLOW_WRITES - true/false, controls memory write tools
17
+ # AGENTF_MCP_MAX_ARG_LENGTH - max length per string argument
18
+ class Server
19
+ KNOWN_TOOLS = %w[
20
+ code_glob
21
+ code_grep
22
+ code_tree
23
+ code_related_files
24
+ architecture_analyze_layers
25
+ memory_recent
26
+ memory_search
27
+ memory_add_lesson
28
+ memory_add_success
29
+ memory_add_pitfall
30
+ ].freeze
31
+
32
+ WRITE_TOOLS = Set.new(%w[
33
+ memory_add_lesson
34
+ memory_add_success
35
+ memory_add_pitfall
36
+ ]).freeze
37
+
38
+ attr_reader :server, :guardrails
39
+
40
+ def initialize(explorer: nil, reviewer: nil, memory: nil, env: ENV)
41
+ @explorer = explorer || Agentf::Commands::Explorer.new
42
+ @architecture = Agentf::Commands::Architecture.new
43
+ @reviewer = reviewer || Agentf::Commands::MemoryReviewer.new
44
+ @memory = memory || Agentf::Memory::RedisMemory.new
45
+ @guardrails = build_guardrails(env)
46
+ @server = build_server
47
+ end
48
+
49
+ # Start the stdio read loop (blocks until stdin closes).
50
+ def run
51
+ @server.run
52
+ end
53
+
54
+ private
55
+
56
+ # ── Guardrails ──────────────────────────────────────────────
57
+
58
+ def build_guardrails(env)
59
+ {
60
+ allowed_tools: parse_allowed_tools(env.fetch("AGENTF_MCP_ALLOWED_TOOLS", nil)),
61
+ allow_writes: parse_boolean_env(env.fetch("AGENTF_MCP_ALLOW_WRITES", nil), true),
62
+ max_arg_length: parse_integer_env(env.fetch("AGENTF_MCP_MAX_ARG_LENGTH", nil), 4096)
63
+ }
64
+ end
65
+
66
+ def parse_allowed_tools(value)
67
+ return Set.new(KNOWN_TOOLS) if value.nil? || value.strip.empty?
68
+
69
+ requested = value.split(",").map(&:strip).reject(&:empty?)
70
+ return Set.new(KNOWN_TOOLS) if requested.include?("*")
71
+
72
+ unknown = requested - KNOWN_TOOLS
73
+ raise ArgumentError, "Unknown tool(s) in AGENTF_MCP_ALLOWED_TOOLS: #{unknown.join(", ")}" unless unknown.empty?
74
+
75
+ Set.new(requested)
76
+ end
77
+
78
+ def parse_boolean_env(value, default)
79
+ return default if value.nil? || value.strip.empty?
80
+
81
+ case value.strip.downcase
82
+ when "1", "true", "yes", "on" then true
83
+ when "0", "false", "no", "off" then false
84
+ else default
85
+ end
86
+ end
87
+
88
+ def parse_integer_env(value, default, min: 1)
89
+ return default if value.nil? || value.to_s.strip.empty?
90
+
91
+ parsed = Integer(value.to_s.strip, 10)
92
+ parsed >= min ? parsed : default
93
+ rescue ArgumentError
94
+ default
95
+ end
96
+
97
+ def assert_tool_allowed!(tool_name)
98
+ return if @guardrails[:allowed_tools].include?(tool_name)
99
+
100
+ raise "Tool not allowed: #{tool_name}"
101
+ end
102
+
103
+ def assert_write_allowed!(tool_name)
104
+ return unless WRITE_TOOLS.include?(tool_name)
105
+ return if @guardrails[:allow_writes]
106
+
107
+ raise "Write tools are disabled: #{tool_name}"
108
+ end
109
+
110
+ def assert_valid_string_args!(**args)
111
+ max = @guardrails[:max_arg_length]
112
+ args.each do |key, value|
113
+ next unless value.is_a?(String)
114
+
115
+ raise "Argument '#{key}' exceeds max length of #{max}" if value.length > max
116
+ end
117
+ end
118
+
119
+ # ── Server construction ─────────────────────────────────────
120
+
121
+ def build_server
122
+ s = ::MCP::Server.new(name: "agentf", version: Agentf::VERSION)
123
+ register_code_tools(s)
124
+ register_memory_tools(s)
125
+ register_architecture_tools(s)
126
+ s
127
+ end
128
+
129
+ # ── Code tools ─────────────────────────────────────────────
130
+
131
+ def register_code_tools(s)
132
+ explorer = @explorer
133
+ mcp_server = self
134
+
135
+ s.tool("code_glob") do
136
+ description "Find files using project glob patterns."
137
+ argument :pattern, String, required: true, description: "Glob pattern, e.g. lib/**/*.rb"
138
+ argument :types, Array, required: false, items: String, description: "File extensions to filter, e.g. [\"rb\",\"py\"]"
139
+ call do |args|
140
+ mcp_server.send(:guard!, "code_glob", **args)
141
+ file_types = args[:types]&.empty? ? nil : args[:types]
142
+ results = explorer.glob(args[:pattern], file_types: file_types)
143
+ JSON.generate(pattern: args[:pattern], matches: results, count: results.length)
144
+ end
145
+ end
146
+
147
+ s.tool("code_grep") do
148
+ description "Search file contents with regex."
149
+ argument :pattern, String, required: true, description: "Regex or text to search"
150
+ argument :file_pattern, String, required: false, description: "Include pattern, e.g. *.rb"
151
+ argument :context_lines, Integer, required: false, description: "Context lines (0-20)"
152
+ call do |args|
153
+ mcp_server.send(:guard!, "code_grep", **args)
154
+ ctx = args[:context_lines] || 2
155
+ matches = explorer.grep(args[:pattern], file_pattern: args[:file_pattern], context_lines: ctx)
156
+ serialized = matches.map { |m| m.respond_to?(:to_h) ? m.to_h : m }
157
+ JSON.generate(pattern: args[:pattern], matches: serialized, count: serialized.length)
158
+ end
159
+ end
160
+
161
+ s.tool("code_tree") do
162
+ description "Get directory tree structure."
163
+ argument :depth, Integer, required: false, description: "Max traversal depth (1-10)"
164
+ call do |args|
165
+ mcp_server.send(:guard!, "code_tree", **args)
166
+ max_depth = args[:depth] || 3
167
+ tree = explorer.get_file_tree(max_depth: max_depth)
168
+ JSON.generate(max_depth: max_depth, tree: tree)
169
+ end
170
+ end
171
+
172
+ s.tool("code_related_files") do
173
+ description "Find imports and related files for a target file."
174
+ argument :target_file, String, required: true, description: "Workspace-relative file path"
175
+ call do |args|
176
+ mcp_server.send(:guard!, "code_related_files", **args)
177
+ related = explorer.find_related_files(args[:target_file])
178
+ JSON.generate(target_file: args[:target_file], related: related)
179
+ end
180
+ end
181
+ end
182
+
183
+ # ── Memory tools ───────────────────────────────────────────
184
+
185
+ def register_memory_tools(s)
186
+ reviewer = @reviewer
187
+ memory = @memory
188
+ mcp_server = self
189
+
190
+ s.tool("memory_recent") do
191
+ description "Get recent memories from Redis."
192
+ argument :limit, Integer, required: false, description: "How many memories to return (1-100)"
193
+ call do |args|
194
+ mcp_server.send(:guard!, "memory_recent", **args)
195
+ result = reviewer.get_recent_memories(limit: args[:limit] || 10)
196
+ JSON.generate(result)
197
+ end
198
+ end
199
+
200
+ s.tool("memory_search") do
201
+ description "Search memories by keyword."
202
+ argument :query, String, required: true, description: "Search query"
203
+ argument :limit, Integer, required: false, description: "How many results to return (1-100)"
204
+ call do |args|
205
+ mcp_server.send(:guard!, "memory_search", **args)
206
+ result = reviewer.search(args[:query], limit: args[:limit] || 10)
207
+ JSON.generate(result)
208
+ end
209
+ end
210
+
211
+ s.tool("memory_add_lesson") do
212
+ description "Store a lesson memory in Redis."
213
+ argument :title, String, required: true, description: "Lesson title"
214
+ argument :description, String, required: true, description: "Lesson description"
215
+ argument :agent, String, required: false, description: "Agent name"
216
+ argument :tags, Array, required: false, items: String, description: "Tags"
217
+ argument :context, String, required: false, description: "Context"
218
+ call do |args|
219
+ mcp_server.send(:guard!, "memory_add_lesson", **args)
220
+ id = memory.store_episode(
221
+ type: "lesson",
222
+ title: args[:title],
223
+ description: args[:description],
224
+ agent: args[:agent] || "SPECIALIST",
225
+ tags: args[:tags] || [],
226
+ context: args[:context].to_s,
227
+ code_snippet: ""
228
+ )
229
+ JSON.generate(id: id, type: "lesson", status: "stored")
230
+ end
231
+ end
232
+
233
+ s.tool("memory_add_success") do
234
+ description "Store a success memory in Redis."
235
+ argument :title, String, required: true, description: "Success title"
236
+ argument :description, String, required: true, description: "Success description"
237
+ argument :agent, String, required: false, description: "Agent name"
238
+ argument :tags, Array, required: false, items: String, description: "Tags"
239
+ argument :context, String, required: false, description: "Context"
240
+ call do |args|
241
+ mcp_server.send(:guard!, "memory_add_success", **args)
242
+ id = memory.store_episode(
243
+ type: "success",
244
+ title: args[:title],
245
+ description: args[:description],
246
+ agent: args[:agent] || "SPECIALIST",
247
+ tags: args[:tags] || [],
248
+ context: args[:context].to_s,
249
+ code_snippet: ""
250
+ )
251
+ JSON.generate(id: id, type: "success", status: "stored")
252
+ end
253
+ end
254
+
255
+ s.tool("memory_add_pitfall") do
256
+ description "Store a pitfall memory in Redis."
257
+ argument :title, String, required: true, description: "Pitfall title"
258
+ argument :description, String, required: true, description: "Pitfall description"
259
+ argument :agent, String, required: false, description: "Agent name"
260
+ argument :tags, Array, required: false, items: String, description: "Tags"
261
+ argument :context, String, required: false, description: "Context"
262
+ call do |args|
263
+ mcp_server.send(:guard!, "memory_add_pitfall", **args)
264
+ id = memory.store_episode(
265
+ type: "pitfall",
266
+ title: args[:title],
267
+ description: args[:description],
268
+ agent: args[:agent] || "SPECIALIST",
269
+ tags: args[:tags] || [],
270
+ context: args[:context].to_s,
271
+ code_snippet: ""
272
+ )
273
+ JSON.generate(id: id, type: "pitfall", status: "stored")
274
+ end
275
+ end
276
+ end
277
+
278
+ def register_architecture_tools(s)
279
+ architecture = @architecture
280
+ mcp_server = self
281
+
282
+ s.tool("architecture_analyze_layers") do
283
+ description "Analyze architecture layers, review violations, or create gradual adoption plans."
284
+ argument :mode, String, required: false, description: "analyze|review|gradual"
285
+ argument :limit, Integer, required: false, description: "Maximum violations to return for review mode"
286
+ argument :goal, String, required: false, description: "Adoption goal for gradual mode"
287
+ call do |args|
288
+ mcp_server.send(:guard!, "architecture_analyze_layers", **args)
289
+ case (args[:mode] || "analyze").to_s
290
+ when "review"
291
+ JSON.generate(architecture.review_layer_violations(limit: args[:limit] || 20))
292
+ when "gradual"
293
+ JSON.generate(architecture.plan_gradual_adoption(goal: args[:goal] || "improve architecture boundaries"))
294
+ else
295
+ JSON.generate(architecture.analyze_layers)
296
+ end
297
+ end
298
+ end
299
+ end
300
+
301
+ # ── Shared guard helper ────────────────────────────────────
302
+
303
+ def guard!(tool_name, **args)
304
+ assert_tool_allowed!(tool_name)
305
+ assert_write_allowed!(tool_name)
306
+ assert_valid_string_args!(**args)
307
+ end
308
+ end
309
+ end
310
+ end