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,393 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Agentf
4
+ module CLI
5
+ # CLI subcommand for memory operations.
6
+ # Refactored from bin/agentf-memory with bug fixes:
7
+ # - show_help now prints output (finding #6)
8
+ # - extract_limit removes consumed args (finding #8)
9
+ # - search_memories extracts limit before joining query (finding #7)
10
+ # - parse_single_option removes consumed args (finding #9)
11
+ # - @json_output is a proper boolean (finding #10)
12
+ # - by_type accepts business_intent and feature_intent (finding #11)
13
+ class Memory
14
+ include ArgParser
15
+
16
+ VALID_EPISODE_TYPES = %w[pitfall lesson success business_intent feature_intent].freeze
17
+
18
+ def initialize(reviewer: nil, memory: nil)
19
+ @reviewer = reviewer || Commands::MemoryReviewer.new
20
+ @memory = memory || Agentf::Memory::RedisMemory.new
21
+ @json_output = false
22
+ end
23
+
24
+ def run(args)
25
+ @json_output = !args.delete("--json").nil?
26
+ command = args.shift || "help"
27
+
28
+ case command
29
+ when "recent", "list"
30
+ list_memories(args)
31
+ when "pitfalls"
32
+ list_pitfalls(args)
33
+ when "lessons"
34
+ list_lessons(args)
35
+ when "successes"
36
+ list_successes(args)
37
+ when "intents"
38
+ list_intents(args)
39
+ when "business-intents"
40
+ list_business_intents(args)
41
+ when "feature-intents"
42
+ list_feature_intents(args)
43
+ when "add-business-intent"
44
+ add_business_intent(args)
45
+ when "add-feature-intent"
46
+ add_feature_intent(args)
47
+ when "add-lesson"
48
+ add_episode("lesson", args)
49
+ when "add-success"
50
+ add_episode("success", args)
51
+ when "add-pitfall"
52
+ add_episode("pitfall", args)
53
+ when "tags"
54
+ list_tags
55
+ when "search"
56
+ search_memories(args)
57
+ when "summary", "stats"
58
+ show_summary
59
+ when "by-tag"
60
+ by_tag(args)
61
+ when "by-agent"
62
+ by_agent(args)
63
+ when "by-type"
64
+ by_type(args)
65
+ when "help", "--help", "-h"
66
+ show_help
67
+ else
68
+ $stderr.puts "Unknown memory command: #{command}"
69
+ $stderr.puts
70
+ show_help
71
+ exit 1
72
+ end
73
+ end
74
+
75
+ private
76
+
77
+ def list_memories(args)
78
+ limit = extract_limit(args)
79
+ result = @reviewer.get_recent_memories(limit: limit)
80
+ output(result)
81
+ end
82
+
83
+ def list_pitfalls(args)
84
+ limit = extract_limit(args)
85
+ result = @reviewer.get_pitfalls(limit: limit)
86
+ output(result)
87
+ end
88
+
89
+ def list_lessons(args)
90
+ limit = extract_limit(args)
91
+ result = @reviewer.get_lessons(limit: limit)
92
+ output(result)
93
+ end
94
+
95
+ def list_successes(args)
96
+ limit = extract_limit(args)
97
+ result = @reviewer.get_successes(limit: limit)
98
+ output(result)
99
+ end
100
+
101
+ def list_intents(args)
102
+ limit = extract_limit(args)
103
+ kind = args.shift
104
+
105
+ result = case kind
106
+ when "business"
107
+ @reviewer.get_business_intents(limit: limit)
108
+ when "feature"
109
+ @reviewer.get_feature_intents(limit: limit)
110
+ else
111
+ business = @reviewer.get_business_intents(limit: limit)
112
+ feature = @reviewer.get_feature_intents(limit: limit)
113
+ merge_memory_results(business, feature, limit: limit)
114
+ end
115
+
116
+ output(result)
117
+ end
118
+
119
+ def list_business_intents(args)
120
+ limit = extract_limit(args)
121
+ output(@reviewer.get_business_intents(limit: limit))
122
+ end
123
+
124
+ def list_feature_intents(args)
125
+ limit = extract_limit(args)
126
+ output(@reviewer.get_feature_intents(limit: limit))
127
+ end
128
+
129
+ def add_business_intent(args)
130
+ title = args.shift
131
+ description = args.shift
132
+
133
+ if title.to_s.empty? || description.to_s.empty?
134
+ $stderr.puts "Error: add-business-intent requires <title> <description>"
135
+ exit 1
136
+ end
137
+
138
+ tags = parse_list_option(args, "--tags=")
139
+ constraints = parse_list_option(args, "--constraints=")
140
+ priority = parse_integer_option(args, "--priority=", default: 1)
141
+
142
+ intent_id = @memory.store_business_intent(
143
+ title: title,
144
+ description: description,
145
+ tags: tags,
146
+ constraints: constraints,
147
+ priority: priority
148
+ )
149
+
150
+ if @json_output
151
+ puts JSON.generate({ "id" => intent_id, "type" => "business_intent", "status" => "stored" })
152
+ else
153
+ puts "Stored business intent: #{intent_id}"
154
+ end
155
+ end
156
+
157
+ def add_feature_intent(args)
158
+ title = args.shift
159
+ description = args.shift
160
+
161
+ if title.to_s.empty? || description.to_s.empty?
162
+ $stderr.puts "Error: add-feature-intent requires <title> <description>"
163
+ exit 1
164
+ end
165
+
166
+ tags = parse_list_option(args, "--tags=")
167
+ acceptance_criteria = parse_list_option(args, "--acceptance=")
168
+ non_goals = parse_list_option(args, "--non-goals=")
169
+ related_task_id = parse_single_option(args, "--task=")
170
+
171
+ intent_id = @memory.store_feature_intent(
172
+ title: title,
173
+ description: description,
174
+ tags: tags,
175
+ acceptance_criteria: acceptance_criteria,
176
+ non_goals: non_goals,
177
+ related_task_id: related_task_id
178
+ )
179
+
180
+ if @json_output
181
+ puts JSON.generate({ "id" => intent_id, "type" => "feature_intent", "status" => "stored" })
182
+ else
183
+ puts "Stored feature intent: #{intent_id}"
184
+ end
185
+ end
186
+
187
+ def add_episode(type, args)
188
+ title = args.shift
189
+ description = args.shift
190
+
191
+ if title.to_s.empty? || description.to_s.empty?
192
+ $stderr.puts "Error: add-#{type} requires <title> <description>"
193
+ exit 1
194
+ end
195
+
196
+ tags = parse_list_option(args, "--tags=")
197
+ context = parse_single_option(args, "--context=").to_s
198
+ agent = parse_single_option(args, "--agent=") || "SPECIALIST"
199
+ code_snippet = parse_single_option(args, "--code=").to_s
200
+
201
+ intent_id = @memory.store_episode(
202
+ type: type,
203
+ title: title,
204
+ description: description,
205
+ context: context,
206
+ tags: tags,
207
+ agent: agent,
208
+ code_snippet: code_snippet
209
+ )
210
+
211
+ if @json_output
212
+ puts JSON.generate({ "id" => intent_id, "type" => type, "status" => "stored" })
213
+ else
214
+ puts "Stored #{type}: #{intent_id}"
215
+ end
216
+ end
217
+
218
+ def list_tags
219
+ result = @reviewer.get_all_tags
220
+ if @json_output
221
+ puts JSON.generate(result)
222
+ return
223
+ end
224
+
225
+ if result["tags"].empty?
226
+ puts "No tags found."
227
+ else
228
+ puts "Tags (#{result["count"]}):"
229
+ result["tags"].each { |tag| puts " - #{tag}" }
230
+ end
231
+ end
232
+
233
+ def search_memories(args)
234
+ # Extract limit BEFORE joining remaining args as query (fixes finding #7)
235
+ limit = extract_limit(args)
236
+ query = args.join(" ")
237
+
238
+ if query.empty?
239
+ $stderr.puts "Error: search requires a query string"
240
+ exit 1
241
+ end
242
+
243
+ result = @reviewer.search(query, limit: limit)
244
+ output(result)
245
+ end
246
+
247
+ def show_summary
248
+ result = @reviewer.get_summary
249
+ if @json_output
250
+ puts JSON.generate(result)
251
+ return
252
+ end
253
+
254
+ puts "Memory Summary for project: #{result["project"]}"
255
+ puts "-" * 40
256
+ puts "Total memories: #{result["total_memories"]}"
257
+ puts ""
258
+ puts "By type:"
259
+ result["by_type"].each { |type, count| puts " #{type}: #{count}" }
260
+ puts ""
261
+ puts "By agent:"
262
+ result["by_agent"].each { |agent, count| puts " #{agent}: #{count}" }
263
+ puts ""
264
+ puts "Unique tags: #{result["unique_tags"]}"
265
+ end
266
+
267
+ def by_tag(args)
268
+ tag = args.shift
269
+ if tag.nil? || tag.empty?
270
+ $stderr.puts "Error: by-tag requires a tag name"
271
+ exit 1
272
+ end
273
+ limit = extract_limit(args)
274
+ result = @reviewer.get_by_tag(tag, limit: limit)
275
+ output(result)
276
+ end
277
+
278
+ def by_agent(args)
279
+ agent = args.shift
280
+ if agent.nil? || agent.empty?
281
+ $stderr.puts "Error: by-agent requires an agent name"
282
+ exit 1
283
+ end
284
+ limit = extract_limit(args)
285
+ result = @reviewer.get_by_agent(agent, limit: limit)
286
+ output(result)
287
+ end
288
+
289
+ def by_type(args)
290
+ type = args.shift
291
+ unless VALID_EPISODE_TYPES.include?(type)
292
+ $stderr.puts "Error: type must be one of: #{VALID_EPISODE_TYPES.join(", ")}"
293
+ exit 1
294
+ end
295
+ limit = extract_limit(args)
296
+ result = @reviewer.get_by_type(type, limit: limit)
297
+ output(result)
298
+ end
299
+
300
+ def merge_memory_results(*results, limit:)
301
+ entries = results.flat_map { |result| result["memories"] || [] }
302
+ sorted = entries.sort_by { |entry| -(entry["created_at_unix"] || 0) }
303
+ { "memories" => sorted.first(limit), "count" => [sorted.length, limit].min }
304
+ end
305
+
306
+ def output(result)
307
+ if result["error"]
308
+ if @json_output
309
+ puts JSON.generate({ "error" => result["error"] })
310
+ else
311
+ $stderr.puts "Error: #{result["error"]}"
312
+ end
313
+ exit 1
314
+ end
315
+
316
+ if @json_output
317
+ puts JSON.generate(result)
318
+ return
319
+ end
320
+
321
+ if result["count"] == 0
322
+ puts "No memories found."
323
+ return
324
+ end
325
+
326
+ memories = result["memories"] || result.values.first
327
+
328
+ memories.each do |mem|
329
+ puts format_memory(mem)
330
+ puts "-" * 40
331
+ end
332
+ end
333
+
334
+ def format_memory(mem)
335
+ <<~OUTPUT
336
+ [#{mem["type"]&.upcase}] #{mem["title"]}
337
+ #{mem["created_at"]} by #{mem["agent"]}
338
+ #{mem["description"]}
339
+ #{format_code(mem["code_snippet"]) unless mem["code_snippet"].to_s.empty?}
340
+ Tags: #{mem["tags"]&.join(", ") || "none"}
341
+ OUTPUT
342
+ end
343
+
344
+ def format_code(snippet)
345
+ return "" if snippet.to_s.empty?
346
+
347
+ "\n```\n#{snippet.strip}\n```"
348
+ end
349
+
350
+ def show_help
351
+ puts <<~HELP
352
+ Usage: agentf memory <command> [options]
353
+
354
+ Commands:
355
+ recent, list List recent memories (default: 10)
356
+ pitfalls List pitfalls (things that went wrong)
357
+ lessons List lessons learned
358
+ successes List successes
359
+ intents [kind] List intents (kind: business|feature)
360
+ business-intents List business intents
361
+ feature-intents List feature intents
362
+ add-business-intent Store business intent
363
+ add-feature-intent Store feature intent
364
+ add-lesson Store lesson memory
365
+ add-success Store success memory
366
+ add-pitfall Store pitfall memory
367
+ tags List all unique tags
368
+ search <query> Search memories by keyword
369
+ summary, stats Show summary statistics
370
+ by-tag <tag> Get memories with specific tag
371
+ by-agent <agent> Get memories from specific agent
372
+ by-type <type> Get memories by type (#{VALID_EPISODE_TYPES.join("|")})
373
+
374
+ Options:
375
+ -n <count> Limit number of results (default: 10)
376
+ --json Output in JSON format
377
+
378
+ Examples:
379
+ agentf memory recent -n 5
380
+ agentf memory pitfalls
381
+ agentf memory intents business -n 5
382
+ agentf memory add-business-intent "Reliability" "Prioritize uptime" --tags=ops,platform --constraints="No downtime;No vendor lock-in"
383
+ agentf memory add-feature-intent "Agent handoff" "Improve orchestrator continuity" --acceptance="Keeps context;Preserves task state"
384
+ agentf memory add-lesson "Refactor strategy" "Extracted adapter seam" --agent=ARCHITECT --tags=architecture
385
+ agentf memory add-success "Provider install works" "Installed copilot + opencode manifests" --agent=SPECIALIST
386
+ agentf memory search "react"
387
+ agentf memory by-tag "performance"
388
+ agentf memory summary
389
+ HELP
390
+ end
391
+ end
392
+ end
393
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Agentf
4
+ module CLI
5
+ class Metrics
6
+ include ArgParser
7
+
8
+ def initialize(metrics: nil)
9
+ @metrics = metrics || Commands::Metrics.new
10
+ @json_output = false
11
+ end
12
+
13
+ def run(args)
14
+ @json_output = !args.delete("--json").nil?
15
+ command = args.shift || "summary"
16
+
17
+ case command
18
+ when "summary"
19
+ run_summary(args)
20
+ when "parity"
21
+ run_parity(args)
22
+ when "help", "--help", "-h"
23
+ show_help
24
+ else
25
+ emit_error("Unknown metrics command: #{command}")
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def run_summary(args)
32
+ limit = extract_limit(args)
33
+ emit(@metrics.summary(limit: limit))
34
+ end
35
+
36
+ def run_parity(args)
37
+ limit = extract_limit(args)
38
+ emit(@metrics.provider_parity(limit: limit))
39
+ end
40
+
41
+ def emit(payload)
42
+ if payload["error"]
43
+ emit_error(payload["error"])
44
+ return
45
+ end
46
+
47
+ if @json_output
48
+ puts JSON.generate(payload)
49
+ return
50
+ end
51
+
52
+ if payload.key?("completion_rate_gap")
53
+ puts "Provider Parity (#{payload['project']})"
54
+ puts "- opencode runs: #{payload['opencode_runs']}"
55
+ puts "- copilot runs: #{payload['copilot_runs']}"
56
+ puts "- completion rate gap: #{payload['completion_rate_gap']}"
57
+ puts "- approval rate gap: #{payload['approval_rate_gap']}"
58
+ puts "- security issue rate gap: #{payload['security_issue_rate_gap']}"
59
+ puts "- avg agents gap: #{payload['avg_agents_gap']}"
60
+ return
61
+ end
62
+
63
+ puts "Workflow Metrics Summary (#{payload['project']})"
64
+ puts "- total runs: #{payload['total_runs']}"
65
+ puts "- completion rate: #{payload['completion_rate']}"
66
+ puts "- approval rate: #{payload['approval_rate']}"
67
+ puts "- failure rate: #{payload['failure_rate']}"
68
+ puts "- security issue rate: #{payload['security_issue_rate']}"
69
+ puts "- avg agents executed: #{payload['avg_agents_executed']}"
70
+ puts "- contract adherence rate: #{payload['contract_adherence_rate']}"
71
+ puts "- contract blocked runs: #{payload['contract_blocked_runs']}"
72
+ puts "- policy violation rate: #{payload['policy_violation_rate']}"
73
+ end
74
+
75
+ def emit_error(message)
76
+ if @json_output
77
+ puts JSON.generate({ "error" => message })
78
+ else
79
+ $stderr.puts "Error: #{message}"
80
+ end
81
+ exit 1
82
+ end
83
+
84
+ def show_help
85
+ puts <<~HELP
86
+ Usage: agentf metrics <command> [options]
87
+
88
+ Commands:
89
+ summary Show workflow success metrics summary
90
+ parity Compare OpenCode vs Copilot metric gaps
91
+
92
+ Options:
93
+ -n <count> Number of recent metric records to evaluate (default: 10)
94
+ --json Output in JSON format
95
+
96
+ Examples:
97
+ agentf metrics summary -n 100
98
+ agentf metrics parity --json
99
+ HELP
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "arg_parser"
4
+ require_relative "memory"
5
+ require_relative "code"
6
+ require_relative "install"
7
+ require_relative "update"
8
+ require_relative "metrics"
9
+ require_relative "architecture"
10
+
11
+ module Agentf
12
+ module CLI
13
+ # Top-level subcommand router for the unified `agentf` CLI.
14
+ #
15
+ # Usage:
16
+ # agentf memory recent -n 5
17
+ # agentf code glob "**/*.rb"
18
+ # agentf install --provider opencode,copilot
19
+ # agentf version
20
+ # agentf help
21
+ class Router
22
+ SUBCOMMANDS = %w[memory code metrics architecture install update mcp-server version help].freeze
23
+
24
+ def run(args)
25
+ subcommand = args.shift || "help"
26
+
27
+ case subcommand
28
+ when "memory"
29
+ Memory.new.run(args)
30
+ when "code"
31
+ Code.new.run(args)
32
+ when "install"
33
+ Install.new.run(args)
34
+ when "metrics"
35
+ if Agentf.config.metrics_enabled
36
+ Metrics.new.run(args)
37
+ else
38
+ $stderr.puts "Metrics are disabled. Set AGENTF_METRICS_ENABLED=true to enable."
39
+ exit 1
40
+ end
41
+ when "architecture"
42
+ Architecture.new.run(args)
43
+ when "update"
44
+ Update.new.run(args)
45
+ when "mcp-server"
46
+ start_mcp_server
47
+ when "version", "--version", "-v"
48
+ puts "agentf #{Agentf::VERSION}"
49
+ when "help", "--help", "-h"
50
+ show_help
51
+ else
52
+ $stderr.puts "Unknown command: #{subcommand}"
53
+ $stderr.puts
54
+ show_help
55
+ exit 1
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ def start_mcp_server
62
+ require_relative "../mcp/server"
63
+ Agentf::MCP::Server.new.run
64
+ end
65
+
66
+ def show_help
67
+ puts <<~HELP
68
+ Usage: agentf <command> [subcommand] [options]
69
+
70
+ Commands:
71
+ memory Manage agent memory (lessons, pitfalls, successes, intents)
72
+ code Explore codebase (glob, grep, tree, related files)
73
+ metrics Show workflow success and provider parity metrics
74
+ architecture Analyze architecture layers and violations
75
+ install Generate provider manifests (agents, commands, tools)
76
+ update Regenerate manifests when gem version changes
77
+ mcp-server Start MCP server over stdio (for Copilot integration)
78
+ version Show version
79
+
80
+ Global Options:
81
+ --json Output in JSON format (supported by memory and code)
82
+ --help Show help for any command
83
+
84
+ Env:
85
+ AGENTF_METRICS_ENABLED=true|false Enable/disable workflow metrics capture and CLI
86
+ AGENTF_WORKFLOW_CONTRACT_ENABLED=true|false Enable/disable workflow contract checks
87
+ AGENTF_WORKFLOW_CONTRACT_MODE=advisory|enforcing|off Contract behavior mode
88
+ AGENTF_DEFAULT_PACK=generic|rails_standard|rails_37signals|rails_feature_spec
89
+ AGENTF_GEM_PATH=/path/to/gem Path to agentf gem (for OpenCode plugin binary resolution)
90
+
91
+ Examples:
92
+ agentf memory recent -n 5
93
+ agentf memory add-lesson "Title" "Description" --agent=ARCHITECT
94
+ agentf code glob "lib/**/*.rb"
95
+ agentf code grep "def execute" --file-pattern=*.rb
96
+ agentf install --provider opencode,copilot --scope local
97
+ agentf metrics summary -n 100
98
+ agentf metrics parity --json
99
+ agentf architecture analyze
100
+ agentf architecture review --json
101
+ agentf update
102
+ agentf update --force --provider=opencode,copilot
103
+ agentf mcp-server
104
+ agentf version
105
+
106
+ Run 'agentf <command> help' for detailed help on a command.
107
+ HELP
108
+ end
109
+ end
110
+ end
111
+ end