roast-ai 0.2.3 → 0.3.1

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 (97) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -0
  3. data/CLAUDE.md +3 -1
  4. data/Gemfile +1 -0
  5. data/Gemfile.lock +15 -10
  6. data/README.md +214 -20
  7. data/bin/roast +1 -1
  8. data/examples/bash_prototyping/README.md +53 -0
  9. data/examples/bash_prototyping/analyze_network/prompt.md +13 -0
  10. data/examples/bash_prototyping/analyze_system/prompt.md +11 -0
  11. data/examples/bash_prototyping/api_testing.yml +14 -0
  12. data/examples/bash_prototyping/check_processes/prompt.md +11 -0
  13. data/examples/bash_prototyping/generate_report/prompt.md +16 -0
  14. data/examples/bash_prototyping/process_json_response/prompt.md +24 -0
  15. data/examples/bash_prototyping/system_analysis.yml +14 -0
  16. data/examples/bash_prototyping/test_public_api/prompt.md +22 -0
  17. data/examples/cmd/README.md +99 -0
  18. data/examples/cmd/analyze_project/prompt.md +57 -0
  19. data/examples/cmd/basic_demo/prompt.md +48 -0
  20. data/examples/cmd/basic_workflow.yml +17 -0
  21. data/examples/cmd/check_repository/prompt.md +57 -0
  22. data/examples/cmd/create_and_verify/prompt.md +56 -0
  23. data/examples/cmd/dev_workflow.yml +26 -0
  24. data/examples/cmd/explore_project/prompt.md +67 -0
  25. data/examples/cmd/explorer_workflow.yml +21 -0
  26. data/examples/cmd/smart_tool_selection/prompt.md +99 -0
  27. data/examples/grading/README.md +71 -0
  28. data/examples/grading/read_dependencies/prompt.md +4 -2
  29. data/examples/grading/run_coverage.rb +9 -0
  30. data/examples/grading/workflow.yml +0 -2
  31. data/examples/mcp/README.md +223 -0
  32. data/examples/mcp/analyze_changes/prompt.md +8 -0
  33. data/examples/mcp/analyze_issues/prompt.md +4 -0
  34. data/examples/mcp/analyze_schema/prompt.md +4 -0
  35. data/examples/mcp/check_data_quality/prompt.md +5 -0
  36. data/examples/mcp/check_documentation/prompt.md +4 -0
  37. data/examples/mcp/create_recommendations/prompt.md +5 -0
  38. data/examples/mcp/database_workflow.yml +29 -0
  39. data/examples/mcp/env_demo/workflow.yml +34 -0
  40. data/examples/mcp/fetch_pr_context/prompt.md +4 -0
  41. data/examples/mcp/filesystem_demo/create_test_file/prompt.md +2 -0
  42. data/examples/mcp/filesystem_demo/list_files/prompt.md +6 -0
  43. data/examples/mcp/filesystem_demo/read_with_mcp/prompt.md +7 -0
  44. data/examples/mcp/filesystem_demo/workflow.yml +38 -0
  45. data/examples/mcp/generate_insights/prompt.md +4 -0
  46. data/examples/mcp/generate_report/prompt.md +6 -0
  47. data/examples/mcp/generate_review/prompt.md +16 -0
  48. data/examples/mcp/github_workflow.yml +32 -0
  49. data/examples/mcp/multi_mcp_workflow.yml +58 -0
  50. data/examples/mcp/post_review/prompt.md +3 -0
  51. data/examples/mcp/save_report/prompt.md +6 -0
  52. data/examples/mcp/search_issues/prompt.md +2 -0
  53. data/examples/mcp/summarize/prompt.md +1 -0
  54. data/examples/mcp/test_filesystem/prompt.md +6 -0
  55. data/examples/mcp/test_github/prompt.md +8 -0
  56. data/examples/mcp/test_read/prompt.md +1 -0
  57. data/examples/mcp/workflow.yml +35 -0
  58. data/examples/shared_config/README.md +52 -0
  59. data/examples/shared_config/example_with_shared_config/workflow.yml +6 -0
  60. data/examples/shared_config/shared.yml +7 -0
  61. data/examples/step_configuration/README.md +0 -3
  62. data/examples/step_configuration/workflow.yml +0 -3
  63. data/examples/tool_config_example/README.md +109 -0
  64. data/examples/tool_config_example/example_step/prompt.md +42 -0
  65. data/examples/tool_config_example/workflow.yml +17 -0
  66. data/examples/workflow_generator/workflow.yml +0 -1
  67. data/lib/roast/helpers/function_caching_interceptor.rb +0 -4
  68. data/lib/roast/helpers/prompt_loader.rb +0 -1
  69. data/lib/roast/tools/bash.rb +62 -0
  70. data/lib/roast/tools/cmd.rb +121 -34
  71. data/lib/roast/tools/coding_agent.rb +86 -7
  72. data/lib/roast/tools/helpers/coding_agent_message_formatter.rb +87 -0
  73. data/lib/roast/tools/search_file.rb +13 -1
  74. data/lib/roast/tools.rb +5 -5
  75. data/lib/roast/version.rb +1 -1
  76. data/lib/roast/workflow/base_step.rb +29 -21
  77. data/lib/roast/workflow/base_workflow.rb +8 -10
  78. data/lib/roast/workflow/configuration.rb +68 -3
  79. data/lib/roast/workflow/configuration_loader.rb +63 -4
  80. data/lib/roast/workflow/configuration_parser.rb +0 -3
  81. data/lib/roast/workflow/error_handler.rb +0 -1
  82. data/lib/roast/workflow/file_state_repository.rb +0 -1
  83. data/lib/roast/workflow/iteration_executor.rb +0 -1
  84. data/lib/roast/workflow/output_manager.rb +0 -1
  85. data/lib/roast/workflow/prompt_step.rb +1 -1
  86. data/lib/roast/workflow/step_executor_coordinator.rb +5 -3
  87. data/lib/roast/workflow/step_executors/hash_step_executor.rb +1 -1
  88. data/lib/roast/workflow/step_loader.rb +35 -8
  89. data/lib/roast/workflow/step_orchestrator.rb +4 -2
  90. data/lib/roast/workflow/workflow_execution_context.rb +0 -2
  91. data/lib/roast/workflow/workflow_executor.rb +1 -3
  92. data/lib/roast/workflow/workflow_initializer.rb +66 -2
  93. data/lib/roast/workflow/workflow_runner.rb +1 -2
  94. data/lib/roast.rb +8 -0
  95. data/package-lock.json +6 -0
  96. data/roast.gemspec +2 -1
  97. metadata +73 -3
@@ -10,7 +10,6 @@ This example demonstrates how to configure various step types in Roast workflows
10
10
  All step types support the following configuration options:
11
11
 
12
12
  - `model`: The AI model to use (e.g., "gpt-4o", "claude-3-opus")
13
- - `loop`: Whether to enable auto-looping (true/false)
14
13
  - `print_response`: Whether to print the response to stdout (true/false)
15
14
  - `json`: Whether to expect a JSON response (true/false)
16
15
  - `params`: Additional parameters to pass to the model (e.g., temperature, max_tokens)
@@ -30,7 +29,6 @@ Inline prompts can be configured in two ways:
30
29
  ```yaml
31
30
  analyze the code:
32
31
  model: gpt-4o
33
- loop: false
34
32
  print_response: true
35
33
  ```
36
34
 
@@ -52,7 +50,6 @@ each:
52
50
  each: "{{files}}"
53
51
  as: file
54
52
  model: gpt-3.5-turbo
55
- loop: false
56
53
  steps:
57
54
  - process {{file}}
58
55
 
@@ -3,12 +3,10 @@ description: Demonstrates how to configure various step types including inline p
3
3
 
4
4
  # Global configuration that applies to all steps
5
5
  model: openai/gpt-4o-mini
6
- loop: true
7
6
 
8
7
  # Configuration for specific steps
9
8
  summarize the code:
10
9
  model: claude-3-opus # Override global model
11
- loop: false # Disable auto-loop for this step
12
10
  print_response: true # Print the response
13
11
  json: false # Don't expect JSON response
14
12
  params:
@@ -25,7 +23,6 @@ each:
25
23
  each: "{{files}}"
26
24
  as: file
27
25
  model: gpt-3.5-turbo # Use a faster model for iteration
28
- loop: false # Don't loop on each iteration
29
26
  steps:
30
27
  - process {{file}}
31
28
 
@@ -0,0 +1,109 @@
1
+ # Tool Configuration Example
2
+
3
+ This example demonstrates how to configure tools with specific settings in Roast workflows.
4
+
5
+ ## Overview
6
+
7
+ Starting with this update, Roast supports configuring tools with specific settings directly in the workflow YAML file. This is particularly useful for tools like `Roast::Tools::Cmd` where you might want to restrict which commands can be executed.
8
+
9
+ ## Configuration Syntax
10
+
11
+ Tools can be configured in two ways:
12
+
13
+ ### 1. Simple String Format (No Configuration)
14
+ ```yaml
15
+ tools:
16
+ - Roast::Tools::ReadFile
17
+ - Roast::Tools::WriteFile
18
+ - Roast::Tools::Grep
19
+ ```
20
+
21
+ ### 2. Hash Format (With Configuration)
22
+ ```yaml
23
+ tools:
24
+ - Roast::Tools::Cmd:
25
+ allowed_commands:
26
+ - ls
27
+ - pwd
28
+ - echo
29
+ ```
30
+
31
+ ### 3. Mixed Format
32
+ You can mix both formats in the same workflow:
33
+
34
+ ```yaml
35
+ tools:
36
+ - Roast::Tools::ReadFile
37
+ - Roast::Tools::Cmd:
38
+ allowed_commands:
39
+ - ls
40
+ - pwd
41
+ - ruby
42
+ - sed
43
+ - Roast::Tools::WriteFile
44
+ - Roast::Tools::SearchFile
45
+ ```
46
+
47
+ ## Example: Configuring Allowed Commands
48
+
49
+ The `Roast::Tools::Cmd` tool now supports an `allowed_commands` configuration that restricts which commands can be executed:
50
+
51
+ ```yaml
52
+ tools:
53
+ - Roast::Tools::Cmd:
54
+ allowed_commands:
55
+ - ls
56
+ - pwd
57
+ - echo
58
+ - cat
59
+ - ruby
60
+ - rake
61
+ ```
62
+
63
+ ### Enhanced Command Configuration with Descriptions
64
+
65
+ You can also provide custom descriptions for commands to help the LLM understand their purpose:
66
+
67
+ ```yaml
68
+ tools:
69
+ - Roast::Tools::Cmd:
70
+ allowed_commands:
71
+ - ls
72
+ - pwd
73
+ - name: echo
74
+ description: "echo command - output text to stdout, supports > for file redirection"
75
+ - name: cat
76
+ description: "cat command - display file contents, concatenate files, works with pipes"
77
+ ```
78
+
79
+ This mixed format allows you to:
80
+ - Use simple strings for commands with good default descriptions
81
+ - Provide custom descriptions for commands that need more context
82
+ - Help the LLM make better decisions about which command to use
83
+
84
+ With this configuration:
85
+ - ✅ `ls -la` will work
86
+ - ✅ `echo "Hello World"` will work
87
+ - ❌ `rm file.txt` will be rejected (not in allowed list)
88
+ - ❌ `git status` will be rejected (not in allowed list)
89
+
90
+ ## Default Behavior
91
+
92
+ If no configuration is provided for `Roast::Tools::Cmd`, it uses the default allowed commands:
93
+ - pwd
94
+ - find
95
+ - ls
96
+ - rake
97
+ - ruby
98
+ - dev
99
+ - mkdir
100
+
101
+ ## Running the Example
102
+
103
+ To run this example workflow:
104
+
105
+ ```bash
106
+ bin/roast execute examples/tool_config_example/workflow.yml
107
+ ```
108
+
109
+ The workflow will validate the tool configuration by executing various commands and demonstrating which ones are allowed and which are rejected based on the configuration.
@@ -0,0 +1,42 @@
1
+ # Tool Configuration Validation
2
+
3
+ Execute the following commands using the cmd tool:
4
+
5
+ 1. `ls -la`
6
+ 2. `pwd`
7
+ 3. `echo "Hello from configured commands!"`
8
+ 4. `git status`
9
+
10
+ RESPONSE FORMAT
11
+ You must respond in JSON format within <json> XML tags.
12
+
13
+ <json>
14
+ {
15
+ "commands": [
16
+ {
17
+ "command": "ls -la",
18
+ "exit_status": 0,
19
+ "output": "total 208\ndrwxr-xr-x@ 31 user staff...",
20
+ "success": true
21
+ },
22
+ {
23
+ "command": "pwd",
24
+ "exit_status": 0,
25
+ "output": "/Users/user/project",
26
+ "success": true
27
+ },
28
+ {
29
+ "command": "echo \"Hello from configured commands!\"",
30
+ "exit_status": 0,
31
+ "output": "Hello from configured commands!",
32
+ "success": true
33
+ },
34
+ {
35
+ "command": "git status",
36
+ "exit_status": null,
37
+ "output": "Error: Command not allowed. Only commands starting with ls, pwd, echo, cat are permitted.",
38
+ "success": false
39
+ }
40
+ ]
41
+ }
42
+ </json>
@@ -0,0 +1,17 @@
1
+ name: Tool Configuration Example
2
+ model: default
3
+ tools:
4
+ - Roast::Tools::ReadFile
5
+ - Roast::Tools::Cmd:
6
+ allowed_commands:
7
+ - ls
8
+ - pwd
9
+ - name: echo
10
+ description: "echo command - output text to stdout, supports > for file redirection"
11
+ - name: cat
12
+ description: "cat command - display file contents, concatenate files, works with pipes"
13
+ - Roast::Tools::WriteFile
14
+
15
+ steps:
16
+ - example_step
17
+
@@ -23,7 +23,6 @@ steps:
23
23
  get_user_input:
24
24
  print_response: false
25
25
  json: true
26
- auto_loop: false
27
26
 
28
27
  analyze_user_request:
29
28
  print_response: true
@@ -1,9 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support"
4
- require "active_support/isolated_execution_state"
5
- require "active_support/cache"
6
- require "active_support/notifications"
7
3
  require "roast/helpers/logger"
8
4
 
9
5
  module Roast
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/core_ext/string"
4
3
  require "erb"
5
4
 
6
5
  module Roast
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "English"
4
+ require "roast/helpers/logger"
5
+
6
+ module Roast
7
+ module Tools
8
+ module Bash
9
+ extend self
10
+
11
+ class << self
12
+ def included(base)
13
+ base.class_eval do
14
+ function(
15
+ :bash,
16
+ "Execute any bash command without restrictions. ⚠️ WARNING: Use only in trusted environments!",
17
+ command: { type: "string", description: "The bash command to execute" },
18
+ ) do |params|
19
+ Roast::Tools::Bash.call(params[:command])
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ def call(command)
26
+ Roast::Helpers::Logger.info("🚀 Executing bash command: #{command}\n")
27
+
28
+ # Show warning unless explicitly disabled
29
+ if ENV["ROAST_BASH_WARNINGS"] != "false"
30
+ Roast::Helpers::Logger.warn("⚠️ WARNING: Unrestricted bash execution - use with caution!\n")
31
+ end
32
+
33
+ # Execute the command without any restrictions
34
+ result = ""
35
+ IO.popen("#{command} 2>&1", chdir: Dir.pwd) do |io|
36
+ result = io.read
37
+ end
38
+
39
+ exit_status = $CHILD_STATUS.exitstatus
40
+
41
+ format_output(command, result, exit_status)
42
+ rescue StandardError => e
43
+ handle_error(e)
44
+ end
45
+
46
+ private
47
+
48
+ def format_output(command, result, exit_status)
49
+ "Command: #{command}\n" \
50
+ "Exit status: #{exit_status}\n" \
51
+ "Output:\n#{result}"
52
+ end
53
+
54
+ def handle_error(error)
55
+ error_message = "Error running command: #{error.message}"
56
+ Roast::Helpers::Logger.error("#{error_message}\n")
57
+ Roast::Helpers::Logger.debug("#{error.backtrace.join("\n")}\n") if ENV["DEBUG"]
58
+ error_message
59
+ end
60
+ end
61
+ end
62
+ end
@@ -8,64 +8,151 @@ module Roast
8
8
  module Cmd
9
9
  extend self
10
10
 
11
+ DEFAULT_ALLOWED_COMMANDS = [
12
+ { "name" => "pwd", "description" => "pwd command - print current working directory path" },
13
+ { "name" => "find", "description" => "find command - search for files/directories using patterns like -name '*.rb'" },
14
+ { "name" => "ls", "description" => "ls command - list directory contents with options like -la, -R" },
15
+ { "name" => "rake", "description" => "rake command - run Ruby tasks defined in Rakefile" },
16
+ { "name" => "ruby", "description" => "ruby command - execute Ruby code or scripts, supports -e for inline code" },
17
+ { "name" => "dev", "description" => "Shopify dev CLI - development environment tool with subcommands" },
18
+ { "name" => "mkdir", "description" => "mkdir command - create directories, supports -p for parent directories" },
19
+ ].freeze
20
+
21
+ CONFIG_ALLOWED_COMMANDS = "allowed_commands"
22
+ private_constant :DEFAULT_ALLOWED_COMMANDS, :CONFIG_ALLOWED_COMMANDS
23
+
11
24
  class << self
12
25
  # Add this method to be included in other classes
13
26
  def included(base)
27
+ @base_class = base
28
+ end
29
+
30
+ # Called after configuration is loaded
31
+ def post_configuration_setup(base, config = {})
32
+ allowed_commands = config[CONFIG_ALLOWED_COMMANDS] || DEFAULT_ALLOWED_COMMANDS
33
+
34
+ allowed_commands.each do |command_entry|
35
+ case command_entry
36
+ when String
37
+ register_command_function(base, command_entry, nil)
38
+ when Hash
39
+ command_name = command_entry["name"] || command_entry[:name]
40
+ description = command_entry["description"] || command_entry[:description]
41
+
42
+ if command_name.nil?
43
+ raise ArgumentError, "Command configuration must include 'name' field"
44
+ end
45
+
46
+ register_command_function(base, command_name, description)
47
+ else
48
+ raise ArgumentError, "Invalid command configuration format: #{command_entry.inspect}"
49
+ end
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ def register_command_function(base, command, custom_description = nil)
56
+ function_name = command.to_sym
57
+ description = custom_description || generate_command_description(command)
58
+
14
59
  base.class_eval do
15
60
  function(
16
- :cmd,
17
- 'Run a command in the current working directory (e.g. "ls", "rake", "ruby"). ' \
18
- "You may use this tool to execute tests and verify if they pass.",
19
- command: { type: "string", description: "The command to run in a bash shell." },
61
+ function_name,
62
+ description,
63
+ args: {
64
+ type: "string",
65
+ description: "Arguments to pass to the #{command} command",
66
+ required: false,
67
+ },
20
68
  ) do |params|
21
- Roast::Tools::Cmd.call(params[:command])
69
+ full_command = if params[:args].nil? || params[:args].empty?
70
+ command
71
+ else
72
+ "#{command} #{params[:args]}"
73
+ end
74
+
75
+ Roast::Tools::Cmd.execute_allowed_command(full_command, command)
22
76
  end
23
77
  end
24
78
  end
79
+
80
+ def generate_command_description(command)
81
+ default_cmd = DEFAULT_ALLOWED_COMMANDS.find { |cmd| cmd["name"] == command }
82
+ default_cmd ? default_cmd["description"] : "Execute the #{command} command"
83
+ end
84
+ end
85
+
86
+ def execute_allowed_command(full_command, command_prefix)
87
+ Roast::Helpers::Logger.info("🔧 Running command: #{full_command}\n")
88
+ execute_command(full_command, command_prefix)
89
+ rescue StandardError => e
90
+ handle_error(e)
25
91
  end
26
92
 
27
- def call(command)
93
+ # Legacy method for backward compatibility
94
+ def call(command, config = {})
28
95
  Roast::Helpers::Logger.info("🔧 Running command: #{command}\n")
29
96
 
30
- # Validate the command starts with one of the allowed prefixes
31
- allowed_prefixes = ["pwd", "find", "ls", "rake", "ruby", "dev", "mkdir"]
97
+ allowed_commands = config[CONFIG_ALLOWED_COMMANDS] || DEFAULT_ALLOWED_COMMANDS
98
+ validation_result = validate_command(command, allowed_commands)
99
+ return validation_result unless validation_result.nil?
100
+
32
101
  command_prefix = command.split(" ").first
102
+ execute_command(command, command_prefix)
103
+ rescue StandardError => e
104
+ handle_error(e)
105
+ end
33
106
 
34
- err = "Error: Command not allowed. Only commands starting with #{allowed_prefixes.join(", ")} are permitted."
35
- return err unless allowed_prefixes.any? do |prefix|
36
- command_prefix == prefix
37
- end
107
+ private
108
+
109
+ def validate_command(command, allowed_commands)
110
+ command_prefix = command.split(" ").first
111
+
112
+ # Extract command names from the allowed_commands array
113
+ allowed_command_names = allowed_commands.map do |cmd_entry|
114
+ case cmd_entry
115
+ when String
116
+ cmd_entry
117
+ when Hash
118
+ cmd_entry["name"] || cmd_entry[:name]
119
+ end
120
+ end.compact
38
121
 
39
- # Execute the command in the current working directory
40
- result = ""
122
+ return if allowed_command_names.include?(command_prefix)
123
+
124
+ "Error: Command not allowed. Only commands starting with #{allowed_command_names.join(", ")} are permitted."
125
+ end
41
126
 
42
- # Use a full shell environment for commands, especially for 'dev'
43
- if command_prefix == "dev"
127
+ def extract_tool_config
128
+ return {} unless respond_to?(:configuration)
129
+
130
+ configuration&.tool_config("Roast::Tools::Cmd") || {}
131
+ end
132
+
133
+ def execute_command(command, command_prefix)
134
+ result = if command_prefix == "dev"
44
135
  # Use bash -l -c to ensure we get a login shell with all environment variables
45
136
  full_command = "bash -l -c '#{command.gsub("'", "\\'")}'"
46
- IO.popen(full_command, chdir: Dir.pwd) do |io|
47
- result = io.read
48
- end
137
+ IO.popen(full_command, chdir: Dir.pwd, &:read)
49
138
  else
50
- # For other commands, use the original approach
51
- IO.popen(command, chdir: Dir.pwd) do |io|
52
- result = io.read
53
- end
139
+ IO.popen(command, chdir: Dir.pwd, &:read)
54
140
  end
55
141
 
56
- exit_status = $CHILD_STATUS.exitstatus
142
+ format_output(command, result, $CHILD_STATUS.exitstatus)
143
+ end
57
144
 
58
- # Return the command output along with exit status information
59
- output = "Command: #{command}\n"
60
- output += "Exit status: #{exit_status}\n"
61
- output += "Output:\n#{result}"
145
+ def format_output(command, result, exit_status)
146
+ "Command: #{command}\n" \
147
+ "Exit status: #{exit_status}\n" \
148
+ "Output:\n#{result}"
149
+ end
62
150
 
63
- output
64
- rescue StandardError => e
65
- "Error running command: #{e.message}".tap do |error_message|
66
- Roast::Helpers::Logger.error(error_message + "\n")
67
- Roast::Helpers::Logger.debug(e.backtrace.join("\n") + "\n") if ENV["DEBUG"]
68
- end
151
+ def handle_error(error)
152
+ error_message = "Error running command: #{error.message}"
153
+ Roast::Helpers::Logger.error("#{error_message}\n")
154
+ Roast::Helpers::Logger.debug("#{error.backtrace.join("\n")}\n") if ENV["DEBUG"]
155
+ error_message
69
156
  end
70
157
  end
71
158
  end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "roast/helpers/logger"
4
+ require "roast/tools/helpers/coding_agent_message_formatter"
5
+ require "json"
4
6
  require "open3"
5
7
  require "tempfile"
6
8
  require "securerandom"
@@ -10,7 +12,16 @@ module Roast
10
12
  module CodingAgent
11
13
  extend self
12
14
 
15
+ class CodingAgentError < StandardError; end
16
+
17
+ CONFIG_CODING_AGENT_COMMAND = "coding_agent_command"
18
+ private_constant :CONFIG_CODING_AGENT_COMMAND
19
+
20
+ @configured_command = nil
21
+
13
22
  class << self
23
+ attr_accessor :configured_command
24
+
14
25
  def included(base)
15
26
  base.class_eval do
16
27
  function(
@@ -22,6 +33,11 @@ module Roast
22
33
  end
23
34
  end
24
35
  end
36
+
37
+ # Called after configuration is loaded
38
+ def post_configuration_setup(base, config = {})
39
+ self.configured_command = config[CONFIG_CODING_AGENT_COMMAND]
40
+ end
25
41
  end
26
42
 
27
43
  def call(prompt)
@@ -50,20 +66,83 @@ module Roast
50
66
  temp_file.write(prompt)
51
67
  temp_file.close
52
68
 
53
- # Run Claude Code CLI using the temp file as input
54
- claude_code_command = ENV.fetch("CLAUDE_CODE_COMMAND", "claude -p")
55
- stdout, stderr, status = Open3.capture3("cat #{temp_file.path} | #{claude_code_command}")
69
+ # Run Claude Code CLI using the temp file as input with streaming output
70
+ expect_json_output = claude_code_command.include?("--output-format stream-json") ||
71
+ claude_code_command.include?("--output-format json")
72
+ command = "cat #{temp_file.path} | #{claude_code_command}"
73
+ result = ""
56
74
 
57
- if status.success?
58
- stdout
59
- else
60
- "Error running ClaudeCode: #{stderr}"
75
+ Open3.popen3(command) do |stdin, stdout, stderr, wait_thread|
76
+ stdin.close
77
+ if expect_json_output
78
+ stdout.each_line do |line|
79
+ json = parse_json(line)
80
+ next unless json
81
+
82
+ handle_intermediate_message(json)
83
+ result += handle_result(json) || ""
84
+ end
85
+ else
86
+ result = stdout.read
87
+ end
88
+
89
+ status = wait_thread.value
90
+ if status.success?
91
+ return result
92
+ else
93
+ error_output = stderr.read
94
+ return "Error running CodingAgent: #{error_output}"
95
+ end
61
96
  end
62
97
  ensure
63
98
  # Always clean up the temp file
64
99
  temp_file.unlink
65
100
  end
66
101
  end
102
+
103
+ def parse_json(line)
104
+ JSON.parse(line)
105
+ rescue JSON::ParserError => e
106
+ Roast::Helpers::Logger.warn("🤖 Error parsing JSON response: #{e}\n")
107
+ nil
108
+ end
109
+
110
+ def handle_intermediate_message(json)
111
+ case json["type"]
112
+ when "assistant", "user"
113
+ CodingAgentMessageFormatter.format_messages(json).each(&method(:log_message))
114
+ when "result", "system"
115
+ # Ignore these message types
116
+ else
117
+ Roast::Helpers::Logger.debug("🤖 Encountered unexpected message type: #{json["type"]}\n")
118
+ end
119
+ end
120
+
121
+ def handle_result(json)
122
+ if json["type"] == "result"
123
+ # NOTE: the format of an error response is { "subtype": "success", "is_error": true }
124
+ if json["is_error"]
125
+ raise CodingAgentError, json["result"]
126
+ elsif json["subtype"] == "success"
127
+ json["result"]
128
+ else
129
+ raise CodingAgentError, "CodingAgent did not complete successfully: #{line}"
130
+ end
131
+ end
132
+ end
133
+
134
+ def log_message(text)
135
+ return if text.blank?
136
+
137
+ text = text.lines.map do |line|
138
+ "\t#{line}"
139
+ end.join
140
+ Roast::Helpers::Logger.info("• " + text.chomp + "\n")
141
+ end
142
+
143
+ def claude_code_command
144
+ CodingAgent.configured_command || ENV["CLAUDE_CODE_COMMAND"] || "claude -p --verbose --output-format stream-json"
145
+ end
67
146
  end
68
147
  end
69
148
  end