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,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+ require_relative "../commands"
5
+
6
+ module Agentf
7
+ module Agents
8
+ # Tester Agent - Test generation and execution
9
+ class Tester < Base
10
+ DESCRIPTION = "Automated test generation and execution."
11
+ COMMANDS = %w[detect_framework generate_unit_tests run_tests].freeze
12
+ MEMORY_CONCEPTS = {
13
+ "reads" => [],
14
+ "writes" => ["store_success"],
15
+ "policy" => "Persist test generation outcomes for future reuse."
16
+ }.freeze
17
+
18
+ def self.description
19
+ DESCRIPTION
20
+ end
21
+
22
+ def self.commands
23
+ COMMANDS
24
+ end
25
+
26
+ def self.memory_concepts
27
+ MEMORY_CONCEPTS
28
+ end
29
+
30
+ def self.policy_boundaries
31
+ {
32
+ "always" => ["Produce framework-aware tests", "Verify red/green state when TDD enabled"],
33
+ "ask_first" => ["Changing test framework conventions"],
34
+ "never" => ["Mark passing when command output is uncertain"],
35
+ "required_inputs" => [],
36
+ "required_outputs" => ["test_file"]
37
+ }
38
+ end
39
+
40
+ def initialize(memory, commands: nil)
41
+ super(memory)
42
+ @commands = commands || Agentf::Commands::Tester.new
43
+ end
44
+
45
+ def generate_tests(code_file, test_type: "unit")
46
+ log "Generating #{test_type} tests for: #{code_file}"
47
+
48
+ template = @commands.generate_unit_tests(code_file)
49
+
50
+ memory.store_success(
51
+ title: "Generated #{test_type} tests for #{code_file}",
52
+ description: "Created #{template.test_file} with #{test_type} tests",
53
+ context: "Test framework: #{template.framework}",
54
+ tags: ["testing", test_type, code_file.split(".").last],
55
+ agent: name
56
+ )
57
+
58
+ log "Created: #{template.test_file}"
59
+
60
+ {
61
+ "source_file" => code_file,
62
+ "test_file" => template.test_file,
63
+ "test_type" => test_type,
64
+ "generated_code" => template.test_code
65
+ }
66
+ end
67
+
68
+ def run_tests(test_file)
69
+ log "Running tests: #{test_file}"
70
+
71
+ result = @commands.run_tests(test_file: test_file)
72
+
73
+ log "Tests passed: #{result['passed']}"
74
+
75
+ { "test_file" => test_file, "passed" => result["passed"] }
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Load all agents
4
+ require_relative "agents/base"
5
+ require_relative "agents/architect"
6
+ require_relative "agents/specialist"
7
+ require_relative "agents/reviewer"
8
+ require_relative "agents/documenter"
9
+ require_relative "agents/explorer"
10
+ require_relative "agents/tester"
11
+ require_relative "agents/debugger"
12
+ require_relative "agents/designer"
13
+ require_relative "agents/security"
14
+
15
+ module Agentf
16
+ module Agents
17
+ # All agents are loaded above
18
+ end
19
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Agentf
4
+ module CLI
5
+ class Architecture
6
+ include ArgParser
7
+
8
+ def initialize(architecture: nil)
9
+ @architecture = architecture || Commands::Architecture.new
10
+ @json_output = false
11
+ end
12
+
13
+ def run(args)
14
+ @json_output = !args.delete("--json").nil?
15
+ command = args.shift || "help"
16
+
17
+ payload = case command
18
+ when "analyze"
19
+ @architecture.analyze_layers
20
+ when "callbacks"
21
+ @architecture.analyze_callbacks(limit: extract_limit(args))
22
+ when "gods"
23
+ @architecture.find_god_objects(limit: extract_limit(args))
24
+ when "review"
25
+ @architecture.review_layer_violations(limit: extract_limit(args))
26
+ when "gradual"
27
+ goal = args.join(" ").strip
28
+ @architecture.plan_gradual_adoption(goal: goal.empty? ? "improve architecture boundaries" : goal)
29
+ when "help", "--help", "-h"
30
+ show_help
31
+ return
32
+ else
33
+ emit_error("Unknown architecture command: #{command}")
34
+ end
35
+
36
+ emit(payload)
37
+ end
38
+
39
+ private
40
+
41
+ def emit(payload)
42
+ if @json_output
43
+ puts JSON.generate(payload)
44
+ return
45
+ end
46
+
47
+ puts JSON.pretty_generate(payload)
48
+ end
49
+
50
+ def emit_error(message)
51
+ if @json_output
52
+ puts JSON.generate({ "error" => message })
53
+ else
54
+ $stderr.puts "Error: #{message}"
55
+ end
56
+ exit 1
57
+ end
58
+
59
+ def show_help
60
+ puts <<~HELP
61
+ Usage: agentf architecture <command> [options]
62
+
63
+ Commands:
64
+ analyze Analyze layer distribution
65
+ callbacks Find callback-heavy model files
66
+ gods Find likely god objects
67
+ review Review layer violations
68
+ gradual [goal] Generate gradual adoption plan
69
+
70
+ Options:
71
+ -n <count> Result limit for callbacks/gods/review (default: 10)
72
+ --json Output in JSON format
73
+
74
+ Examples:
75
+ agentf architecture analyze
76
+ agentf architecture callbacks -n 20
77
+ agentf architecture review --json
78
+ agentf architecture gradual "adopt layered rails patterns"
79
+ HELP
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Agentf
4
+ module CLI
5
+ # Shared argument parsing helpers used by all CLI subcommands.
6
+ module ArgParser
7
+ # Extracts a -n <value> flag from args, removes both entries,
8
+ # and returns the integer value. Returns default if not found.
9
+ def extract_limit(args, default: 10)
10
+ idx = args.index("-n")
11
+ return default unless idx && args[idx + 1]
12
+
13
+ limit = args[idx + 1].to_i
14
+ args.delete_at(idx + 1)
15
+ args.delete_at(idx)
16
+ limit
17
+ end
18
+
19
+ # Extracts --prefix=value from args, removes the entry,
20
+ # and returns the value string. Returns nil if not found.
21
+ def parse_single_option(args, prefix)
22
+ idx = args.index { |arg| arg.start_with?(prefix) }
23
+ return nil unless idx
24
+
25
+ args.delete_at(idx).delete_prefix(prefix)
26
+ end
27
+
28
+ # Extracts --prefix=a,b,c from args, removes it,
29
+ # and returns an array of strings. Supports semicolons as delimiters.
30
+ def parse_list_option(args, prefix)
31
+ raw = parse_single_option(args, prefix)
32
+ return [] if raw.to_s.empty?
33
+
34
+ delimiter = raw.include?(";") ? ";" : ","
35
+ raw.split(delimiter).map(&:strip).reject(&:empty?)
36
+ end
37
+
38
+ # Extracts --prefix=N from args, removes it,
39
+ # and returns the integer. Returns default on missing/invalid.
40
+ def parse_integer_option(args, prefix, default: 0)
41
+ raw = parse_single_option(args, prefix)
42
+ return default if raw.to_s.empty?
43
+
44
+ Integer(raw)
45
+ rescue ArgumentError
46
+ default
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Agentf
4
+ module CLI
5
+ # CLI subcommand for code exploration operations.
6
+ # Refactored from bin/agentf-code with fixes:
7
+ # - Human-readable output now shows actual results, not just counts (finding #13)
8
+ # - Argument parsing uses shared ArgParser module
9
+ class Code
10
+ include ArgParser
11
+
12
+ def initialize(explorer: nil)
13
+ @explorer = explorer || Commands::Explorer.new
14
+ @json_output = false
15
+ end
16
+
17
+ def run(args)
18
+ @json_output = !args.delete("--json").nil?
19
+ command = args.shift || "help"
20
+
21
+ case command
22
+ when "glob"
23
+ run_glob(args)
24
+ when "grep"
25
+ run_grep(args)
26
+ when "tree"
27
+ run_tree(args)
28
+ when "related"
29
+ run_related(args)
30
+ when "help", "--help", "-h"
31
+ show_help
32
+ else
33
+ emit_error("Unknown code command: #{command}")
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def run_glob(args)
40
+ pattern = args.shift
41
+ return emit_error("glob requires a pattern") if pattern.to_s.empty?
42
+
43
+ file_types = parse_list_option(args, "--types=")
44
+ file_types = nil if file_types.empty?
45
+
46
+ results = @explorer.glob(pattern, file_types: file_types)
47
+ emit_success("glob", { "pattern" => pattern, "matches" => results, "count" => results.length })
48
+ end
49
+
50
+ def run_grep(args)
51
+ pattern = args.shift
52
+ return emit_error("grep requires a pattern") if pattern.to_s.empty?
53
+
54
+ file_pattern = parse_single_option(args, "--file-pattern=")
55
+ context_lines = parse_integer_option(args, "--context=", default: 2)
56
+
57
+ matches = @explorer.grep(pattern, file_pattern: file_pattern, context_lines: context_lines)
58
+ serialized = matches.map { |match| match.respond_to?(:to_h) ? match.to_h : match }
59
+ emit_success("grep", { "pattern" => pattern, "matches" => serialized, "count" => serialized.length })
60
+ end
61
+
62
+ def run_tree(args)
63
+ max_depth = parse_integer_option(args, "--depth=", default: 3)
64
+ tree = @explorer.get_file_tree(max_depth: max_depth)
65
+ emit_success("tree", { "max_depth" => max_depth, "tree" => tree })
66
+ end
67
+
68
+ def run_related(args)
69
+ target_file = args.shift
70
+ return emit_error("related requires a target file") if target_file.to_s.empty?
71
+
72
+ related = @explorer.find_related_files(target_file)
73
+ emit_success("related", { "target_file" => target_file, "related" => related })
74
+ end
75
+
76
+ def emit_success(command, payload)
77
+ if @json_output
78
+ puts JSON.generate(payload.merge("command" => command))
79
+ else
80
+ format_human_output(command, payload)
81
+ end
82
+ end
83
+
84
+ def format_human_output(command, payload)
85
+ count = payload["count"] || 1
86
+ puts "#{command} -> #{count} results"
87
+
88
+ case command
89
+ when "glob"
90
+ payload["matches"]&.each { |f| puts " #{f}" }
91
+ when "grep"
92
+ payload["matches"]&.each do |m|
93
+ if m.is_a?(Hash)
94
+ puts " #{m["path"]}:#{m["line_number"]} #{m["content"]}"
95
+ else
96
+ puts " #{m}"
97
+ end
98
+ end
99
+ when "tree"
100
+ tree = payload["tree"]
101
+ if tree.is_a?(Hash)
102
+ print_tree(tree)
103
+ else
104
+ puts tree
105
+ end
106
+ when "related"
107
+ related = payload["related"]
108
+ if related.is_a?(Hash)
109
+ related.each do |key, values|
110
+ next unless values.is_a?(Array) && !values.empty?
111
+
112
+ puts " #{key}:"
113
+ values.each { |v| puts " #{v}" }
114
+ end
115
+ end
116
+ end
117
+ end
118
+
119
+ def print_tree(tree, indent: "")
120
+ tree.each do |name, subtree|
121
+ if subtree.is_a?(Hash)
122
+ puts "#{indent}#{name}/"
123
+ print_tree(subtree, indent: "#{indent} ")
124
+ else
125
+ puts "#{indent}#{name}"
126
+ end
127
+ end
128
+ end
129
+
130
+ def emit_error(message)
131
+ if @json_output
132
+ puts JSON.generate({ "error" => message })
133
+ else
134
+ $stderr.puts "Error: #{message}"
135
+ end
136
+ exit 1
137
+ end
138
+
139
+ def show_help
140
+ puts <<~HELP
141
+ Usage: agentf code <command> [options]
142
+
143
+ Commands:
144
+ glob <pattern> Find files matching glob pattern
145
+ grep <pattern> Search file contents with regex
146
+ tree Print directory tree
147
+ related <file> Find related/imported files
148
+
149
+ Options:
150
+ --json Output in JSON format
151
+ --types=rb,py Filter glob by file extensions
152
+ --file-pattern=*.rb Grep include pattern
153
+ --context=2 Grep context lines
154
+ --depth=3 Tree max depth
155
+
156
+ Examples:
157
+ agentf code glob "lib/**/*.rb"
158
+ agentf code grep "WorkflowEngine" --file-pattern=*.rb --json
159
+ agentf code tree --depth=2
160
+ agentf code related lib/agentf/workflow_engine.rb
161
+ HELP
162
+ end
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+
5
+ module Agentf
6
+ module CLI
7
+ # CLI subcommand for generating provider manifests.
8
+ # Wraps the existing Agentf::Installer with CLI argument parsing.
9
+ class Install
10
+ include ArgParser
11
+
12
+ def initialize
13
+ @options = {
14
+ providers: ["opencode"],
15
+ scope: "all",
16
+ global_root: Dir.home,
17
+ local_root: Dir.pwd,
18
+ dry_run: false,
19
+ only_agents: nil,
20
+ only_commands: nil
21
+ }
22
+ end
23
+
24
+ def run(args)
25
+ parse_args(args)
26
+
27
+ if args.include?("help") || args.include?("--help") || args.include?("-h")
28
+ show_help
29
+ return
30
+ end
31
+
32
+ installer = Agentf::Installer.new(
33
+ global_root: @options[:global_root],
34
+ local_root: @options[:local_root],
35
+ dry_run: @options[:dry_run]
36
+ )
37
+
38
+ results = installer.install(
39
+ providers: @options[:providers],
40
+ scope: @options[:scope],
41
+ only_agents: @options[:only_agents],
42
+ only_commands: @options[:only_commands]
43
+ )
44
+
45
+ results.each do |result|
46
+ puts "#{result.fetch("status").upcase}: #{Pathname.new(result.fetch("path")).cleanpath}"
47
+ end
48
+
49
+ puts "\nCompleted #{results.size} manifest operations."
50
+ end
51
+
52
+ private
53
+
54
+ def parse_args(args)
55
+ # Extract --dry-run flag
56
+ @options[:dry_run] = !args.delete("--dry-run").nil?
57
+
58
+ # Extract --provider
59
+ provider_val = parse_single_option(args, "--provider=") || parse_single_option(args, "-p=")
60
+ if provider_val
61
+ providers = provider_val.split(",").map { |item| item.strip.downcase }.reject(&:empty?)
62
+ @options[:providers] = providers == ["all"] ? Agentf::Installer::PROVIDER_LAYOUTS.keys : providers
63
+ end
64
+
65
+ # Extract --scope
66
+ scope_val = parse_single_option(args, "--scope=") || parse_single_option(args, "-s=")
67
+ @options[:scope] = scope_val.downcase if scope_val
68
+
69
+ # Extract --global-root and --local-root
70
+ global_root = parse_single_option(args, "--global-root=")
71
+ @options[:global_root] = File.expand_path(global_root) if global_root
72
+
73
+ local_root = parse_single_option(args, "--local-root=")
74
+ @options[:local_root] = File.expand_path(local_root) if local_root
75
+
76
+ # Extract --agent and --command filters
77
+ agent_val = parse_single_option(args, "--agent=")
78
+ if agent_val
79
+ @options[:only_agents] = agent_val.split(",").map { |item| item.strip.downcase }.reject(&:empty?)
80
+ end
81
+
82
+ command_val = parse_single_option(args, "--command=")
83
+ if command_val
84
+ @options[:only_commands] = command_val.split(",").map { |item| item.strip.downcase }.reject(&:empty?)
85
+ end
86
+ end
87
+
88
+ def show_help
89
+ puts <<~HELP
90
+ Usage: agentf install [options]
91
+
92
+ Generates provider-specific agent and command manifests.
93
+
94
+ Options:
95
+ --provider=LIST Providers: opencode,copilot,all (default: opencode)
96
+ --scope=SCOPE Install scope: global|local|all (default: all)
97
+ --global-root=PATH Root for global installs (default: $HOME)
98
+ --local-root=PATH Root for local installs (default: current directory)
99
+ --agent=LIST Only install specific agents (comma-separated)
100
+ --command=LIST Only install specific commands (comma-separated)
101
+ --dry-run Show planned writes without writing files
102
+
103
+ Examples:
104
+ agentf install
105
+ agentf install --provider=opencode,copilot --scope=local
106
+ agentf install --provider=copilot --dry-run
107
+ agentf install --agent=architect,specialist
108
+ HELP
109
+ end
110
+ end
111
+ end
112
+ end