roast-ai 0.3.1 → 0.4.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 (216) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yaml +2 -2
  3. data/.gitignore +1 -0
  4. data/CHANGELOG.md +85 -0
  5. data/CLAUDE.md +106 -9
  6. data/Gemfile +4 -1
  7. data/Gemfile.lock +70 -16
  8. data/README.md +159 -8
  9. data/bin/console +1 -0
  10. data/bin/roast +1 -1
  11. data/claude-swarm.yml +210 -0
  12. data/docs/AGENT_STEPS.md +288 -0
  13. data/docs/VALIDATION.md +178 -0
  14. data/examples/agent_continue/add_documentation/prompt.md +5 -0
  15. data/examples/agent_continue/add_error_handling/prompt.md +5 -0
  16. data/examples/agent_continue/analyze_codebase/prompt.md +7 -0
  17. data/examples/agent_continue/combined_workflow.yml +24 -0
  18. data/examples/agent_continue/continue_adding_features/prompt.md +4 -0
  19. data/examples/agent_continue/create_integration_tests/prompt.md +3 -0
  20. data/examples/agent_continue/document_with_context/prompt.md +5 -0
  21. data/examples/agent_continue/explore_api/prompt.md +6 -0
  22. data/examples/agent_continue/implement_client/prompt.md +6 -0
  23. data/examples/agent_continue/inline_workflow.yml +20 -0
  24. data/examples/agent_continue/refactor_code/prompt.md +2 -0
  25. data/examples/agent_continue/verify_changes/prompt.md +6 -0
  26. data/examples/agent_continue/workflow.yml +27 -0
  27. data/examples/agent_workflow/README.md +75 -0
  28. data/examples/agent_workflow/apply_refactorings/prompt.md +22 -0
  29. data/examples/agent_workflow/identify_code_smells/prompt.md +15 -0
  30. data/examples/agent_workflow/summarize_improvements/prompt.md +18 -0
  31. data/examples/agent_workflow/workflow.png +0 -0
  32. data/examples/agent_workflow/workflow.yml +16 -0
  33. data/examples/api_workflow/workflow.png +0 -0
  34. data/examples/apply_diff_demo/README.md +58 -0
  35. data/examples/apply_diff_demo/apply_simple_change/prompt.md +13 -0
  36. data/examples/apply_diff_demo/create_sample_file/prompt.md +11 -0
  37. data/examples/apply_diff_demo/workflow.yml +24 -0
  38. data/examples/available_tools_demo/README.md +42 -0
  39. data/examples/available_tools_demo/analyze_files/prompt.md +6 -0
  40. data/examples/available_tools_demo/explore_directory/prompt.md +6 -0
  41. data/examples/available_tools_demo/workflow.png +0 -0
  42. data/examples/available_tools_demo/workflow.yml +32 -0
  43. data/examples/available_tools_demo/write_summary/prompt.md +6 -0
  44. data/examples/bash_prototyping/api_testing.png +0 -0
  45. data/examples/bash_prototyping/system_analysis.png +0 -0
  46. data/examples/case_when/detect_language/prompt.md +2 -2
  47. data/examples/case_when/workflow.png +0 -0
  48. data/examples/cmd/basic_workflow.png +0 -0
  49. data/examples/cmd/dev_workflow.png +0 -0
  50. data/examples/cmd/explorer_workflow.png +0 -0
  51. data/examples/conditional/simple_workflow.png +0 -0
  52. data/examples/conditional/workflow.png +0 -0
  53. data/examples/context_management_demo/README.md +43 -0
  54. data/examples/context_management_demo/workflow.yml +42 -0
  55. data/examples/direct_coerce_syntax/workflow.png +0 -0
  56. data/examples/dot_notation/workflow.png +0 -0
  57. data/examples/exit_on_error/workflow.png +0 -0
  58. data/examples/grading/run_coverage.rb +0 -2
  59. data/examples/grading/workflow.png +0 -0
  60. data/examples/interpolation/workflow.png +0 -0
  61. data/examples/interpolation/workflow.yml +1 -1
  62. data/examples/iteration/analyze_complexity/prompt.md +2 -2
  63. data/examples/iteration/generate_recommendations/prompt.md +2 -2
  64. data/examples/iteration/implement_fix/prompt.md +2 -2
  65. data/examples/iteration/prioritize_issues/prompt.md +1 -1
  66. data/examples/iteration/prompts/analyze_file.md +2 -2
  67. data/examples/iteration/prompts/generate_summary.md +1 -1
  68. data/examples/iteration/prompts/update_report.md +3 -3
  69. data/examples/iteration/prompts/write_report.md +3 -3
  70. data/examples/iteration/read_file/prompt.md +2 -2
  71. data/examples/iteration/select_next_issue/prompt.md +2 -2
  72. data/examples/iteration/update_fix_count/prompt.md +4 -4
  73. data/examples/iteration/verify_fix/prompt.md +3 -3
  74. data/examples/iteration/workflow.png +0 -0
  75. data/examples/json_handling/workflow.png +0 -0
  76. data/examples/mcp/README.md +3 -3
  77. data/examples/mcp/analyze_changes/prompt.md +1 -1
  78. data/examples/mcp/database_workflow.png +0 -0
  79. data/examples/mcp/database_workflow.yml +1 -1
  80. data/examples/mcp/env_demo/workflow.png +0 -0
  81. data/examples/mcp/fetch_pr_context/prompt.md +1 -1
  82. data/examples/mcp/filesystem_demo/workflow.png +0 -0
  83. data/examples/mcp/github_workflow.png +0 -0
  84. data/examples/mcp/github_workflow.yml +1 -1
  85. data/examples/mcp/multi_mcp_workflow.png +0 -0
  86. data/examples/mcp/post_review/prompt.md +1 -1
  87. data/examples/mcp/workflow.png +0 -0
  88. data/examples/no_model_fallback/README.md +17 -0
  89. data/examples/no_model_fallback/analyze_file/prompt.md +1 -0
  90. data/examples/no_model_fallback/analyze_patterns/prompt.md +27 -0
  91. data/examples/no_model_fallback/generate_report_for_md/prompt.md +10 -0
  92. data/examples/no_model_fallback/generate_report_for_rb/prompt.md +3 -0
  93. data/examples/no_model_fallback/sample.rb +42 -0
  94. data/examples/no_model_fallback/workflow.yml +19 -0
  95. data/examples/openrouter_example/workflow.png +0 -0
  96. data/examples/pre_post_processing/analyze_test_file/prompt.md +1 -1
  97. data/examples/pre_post_processing/improve_test_coverage/prompt.md +1 -1
  98. data/examples/pre_post_processing/optimize_test_performance/prompt.md +1 -1
  99. data/examples/pre_post_processing/post_processing/aggregate_metrics/prompt.md +2 -2
  100. data/examples/pre_post_processing/post_processing/generate_summary_report/prompt.md +1 -1
  101. data/examples/pre_post_processing/pre_processing/setup_test_environment/prompt.md +1 -1
  102. data/examples/pre_post_processing/validate_changes/prompt.md +2 -2
  103. data/examples/pre_post_processing/workflow.png +0 -0
  104. data/examples/rspec_to_minitest/workflow.png +0 -0
  105. data/examples/shared_config/example_with_shared_config/workflow.png +0 -0
  106. data/examples/shared_config/shared.png +0 -0
  107. data/examples/single_target_prepost/workflow.png +0 -0
  108. data/examples/smart_coercion_defaults/workflow.png +0 -0
  109. data/examples/step_configuration/workflow.png +0 -0
  110. data/examples/swarm_example.yml +25 -0
  111. data/examples/tool_config_example/workflow.png +0 -0
  112. data/examples/user_input/README.md +90 -0
  113. data/examples/user_input/funny_name/create_backstory/prompt.md +10 -0
  114. data/examples/user_input/funny_name/workflow.png +0 -0
  115. data/examples/user_input/funny_name/workflow.yml +26 -0
  116. data/examples/user_input/generate_summary/prompt.md +11 -0
  117. data/examples/user_input/simple_input_demo/workflow.png +0 -0
  118. data/examples/user_input/simple_input_demo/workflow.yml +35 -0
  119. data/examples/user_input/survey_workflow.png +0 -0
  120. data/examples/user_input/survey_workflow.yml +71 -0
  121. data/examples/user_input/welcome_message/prompt.md +3 -0
  122. data/examples/user_input/workflow.png +0 -0
  123. data/examples/user_input/workflow.yml +73 -0
  124. data/examples/workflow_generator/create_workflow_files/prompt.md +1 -1
  125. data/examples/workflow_generator/workflow.png +0 -0
  126. data/lib/roast/errors.rb +6 -4
  127. data/lib/roast/helpers/function_caching_interceptor.rb +0 -2
  128. data/lib/roast/helpers/logger.rb +12 -35
  129. data/lib/roast/helpers/minitest_coverage_runner.rb +0 -1
  130. data/lib/roast/helpers/prompt_loader.rb +0 -2
  131. data/lib/roast/helpers/timeout_handler.rb +91 -0
  132. data/lib/roast/resources/api_resource.rb +0 -4
  133. data/lib/roast/resources/url_resource.rb +0 -3
  134. data/lib/roast/resources.rb +0 -8
  135. data/lib/roast/services/context_threshold_checker.rb +42 -0
  136. data/lib/roast/services/token_counting_service.rb +44 -0
  137. data/lib/roast/tools/apply_diff.rb +128 -0
  138. data/lib/roast/tools/ask_user.rb +0 -2
  139. data/lib/roast/tools/bash.rb +12 -9
  140. data/lib/roast/tools/cmd.rb +29 -12
  141. data/lib/roast/tools/coding_agent.rb +65 -17
  142. data/lib/roast/tools/context_summarizer.rb +108 -0
  143. data/lib/roast/tools/grep.rb +0 -3
  144. data/lib/roast/tools/helpers/coding_agent_message_formatter.rb +1 -4
  145. data/lib/roast/tools/read_file.rb +0 -2
  146. data/lib/roast/tools/search_file.rb +0 -2
  147. data/lib/roast/tools/swarm.rb +124 -0
  148. data/lib/roast/tools/update_files.rb +0 -4
  149. data/lib/roast/tools/write_file.rb +0 -3
  150. data/lib/roast/tools.rb +0 -13
  151. data/lib/roast/value_objects/step_name.rb +14 -3
  152. data/lib/roast/value_objects/workflow_path.rb +0 -2
  153. data/lib/roast/value_objects.rb +4 -4
  154. data/lib/roast/version.rb +1 -1
  155. data/lib/roast/workflow/agent_step.rb +33 -0
  156. data/lib/roast/workflow/api_configuration.rb +0 -4
  157. data/lib/roast/workflow/base_iteration_step.rb +3 -6
  158. data/lib/roast/workflow/base_step.rb +54 -28
  159. data/lib/roast/workflow/base_workflow.rb +43 -23
  160. data/lib/roast/workflow/case_executor.rb +0 -1
  161. data/lib/roast/workflow/case_step.rb +0 -4
  162. data/lib/roast/workflow/command_executor.rb +0 -2
  163. data/lib/roast/workflow/conditional_executor.rb +0 -1
  164. data/lib/roast/workflow/conditional_step.rb +0 -4
  165. data/lib/roast/workflow/configuration.rb +5 -67
  166. data/lib/roast/workflow/configuration_loader.rb +63 -3
  167. data/lib/roast/workflow/configuration_parser.rb +1 -7
  168. data/lib/roast/workflow/context_manager.rb +89 -0
  169. data/lib/roast/workflow/dot_access_hash.rb +16 -1
  170. data/lib/roast/workflow/each_step.rb +1 -1
  171. data/lib/roast/workflow/error_handler.rb +0 -3
  172. data/lib/roast/workflow/expression_evaluator.rb +0 -3
  173. data/lib/roast/workflow/file_state_repository.rb +0 -5
  174. data/lib/roast/workflow/input_executor.rb +41 -0
  175. data/lib/roast/workflow/input_step.rb +163 -0
  176. data/lib/roast/workflow/iteration_executor.rb +0 -2
  177. data/lib/roast/workflow/output_handler.rb +1 -3
  178. data/lib/roast/workflow/output_manager.rb +0 -2
  179. data/lib/roast/workflow/repeat_step.rb +1 -1
  180. data/lib/roast/workflow/replay_handler.rb +1 -4
  181. data/lib/roast/workflow/resource_resolver.rb +0 -3
  182. data/lib/roast/workflow/session_manager.rb +0 -3
  183. data/lib/roast/workflow/sqlite_state_repository.rb +342 -0
  184. data/lib/roast/workflow/state_manager.rb +2 -4
  185. data/lib/roast/workflow/state_repository_factory.rb +36 -0
  186. data/lib/roast/workflow/step_completion_reporter.rb +27 -0
  187. data/lib/roast/workflow/step_executor_coordinator.rb +48 -24
  188. data/lib/roast/workflow/step_executor_factory.rb +0 -5
  189. data/lib/roast/workflow/step_executor_registry.rb +1 -4
  190. data/lib/roast/workflow/step_executor_with_reporting.rb +68 -0
  191. data/lib/roast/workflow/step_executors/hash_step_executor.rb +0 -3
  192. data/lib/roast/workflow/step_executors/parallel_step_executor.rb +0 -3
  193. data/lib/roast/workflow/step_executors/string_step_executor.rb +0 -2
  194. data/lib/roast/workflow/step_factory.rb +56 -0
  195. data/lib/roast/workflow/step_loader.rb +31 -17
  196. data/lib/roast/workflow/step_name_extractor.rb +84 -0
  197. data/lib/roast/workflow/step_orchestrator.rb +3 -2
  198. data/lib/roast/workflow/step_type_resolver.rb +28 -1
  199. data/lib/roast/workflow/validation_command.rb +197 -0
  200. data/lib/roast/workflow/validator.rb +0 -4
  201. data/lib/roast/workflow/validators/base_validator.rb +44 -0
  202. data/lib/roast/workflow/validators/dependency_validator.rb +223 -0
  203. data/lib/roast/workflow/validators/linting_validator.rb +113 -0
  204. data/lib/roast/workflow/validators/schema_validator.rb +90 -0
  205. data/lib/roast/workflow/validators/step_collector.rb +57 -0
  206. data/lib/roast/workflow/validators/validation_orchestrator.rb +52 -0
  207. data/lib/roast/workflow/workflow_executor.rb +11 -20
  208. data/lib/roast/workflow/workflow_initializer.rb +1 -8
  209. data/lib/roast/workflow/workflow_runner.rb +6 -7
  210. data/lib/roast/workflow.rb +0 -15
  211. data/lib/roast/workflow_diagram_generator.rb +298 -0
  212. data/lib/roast.rb +212 -10
  213. data/roast.gemspec +4 -2
  214. data/schema/workflow.json +123 -1
  215. metadata +143 -6
  216. data/lib/roast/helpers.rb +0 -12
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "English"
4
4
  require "roast/helpers/logger"
5
+ require "roast/helpers/timeout_handler"
5
6
 
6
7
  module Roast
7
8
  module Tools
@@ -65,6 +66,11 @@ module Roast
65
66
  description: "Arguments to pass to the #{command} command",
66
67
  required: false,
67
68
  },
69
+ timeout: {
70
+ type: "integer",
71
+ description: "Timeout in seconds (optional, default: 30)",
72
+ required: false,
73
+ },
68
74
  ) do |params|
69
75
  full_command = if params[:args].nil? || params[:args].empty?
70
76
  command
@@ -72,7 +78,7 @@ module Roast
72
78
  "#{command} #{params[:args]}"
73
79
  end
74
80
 
75
- Roast::Tools::Cmd.execute_allowed_command(full_command, command)
81
+ Roast::Tools::Cmd.execute_allowed_command(full_command, command, params[:timeout])
76
82
  end
77
83
  end
78
84
  end
@@ -83,15 +89,16 @@ module Roast
83
89
  end
84
90
  end
85
91
 
86
- def execute_allowed_command(full_command, command_prefix)
92
+ def execute_allowed_command(full_command, command_prefix, timeout = 30)
87
93
  Roast::Helpers::Logger.info("🔧 Running command: #{full_command}\n")
88
- execute_command(full_command, command_prefix)
94
+
95
+ execute_command(full_command, command_prefix, timeout)
89
96
  rescue StandardError => e
90
97
  handle_error(e)
91
98
  end
92
99
 
93
100
  # Legacy method for backward compatibility
94
- def call(command, config = {})
101
+ def call(command, config = {}, timeout: 30)
95
102
  Roast::Helpers::Logger.info("🔧 Running command: #{command}\n")
96
103
 
97
104
  allowed_commands = config[CONFIG_ALLOWED_COMMANDS] || DEFAULT_ALLOWED_COMMANDS
@@ -99,7 +106,8 @@ module Roast
99
106
  return validation_result unless validation_result.nil?
100
107
 
101
108
  command_prefix = command.split(" ").first
102
- execute_command(command, command_prefix)
109
+
110
+ execute_command(command, command_prefix, timeout)
103
111
  rescue StandardError => e
104
112
  handle_error(e)
105
113
  end
@@ -130,16 +138,25 @@ module Roast
130
138
  configuration&.tool_config("Roast::Tools::Cmd") || {}
131
139
  end
132
140
 
133
- def execute_command(command, command_prefix)
134
- result = if command_prefix == "dev"
135
- # Use bash -l -c to ensure we get a login shell with all environment variables
136
- full_command = "bash -l -c '#{command.gsub("'", "\\'")}'"
137
- IO.popen(full_command, chdir: Dir.pwd, &:read)
141
+ def execute_command(command, command_prefix, timeout)
142
+ timeout = Roast::Helpers::TimeoutHandler.validate_timeout(timeout)
143
+
144
+ full_command = if command_prefix == "dev"
145
+ "bash -l -c '#{command.gsub("'", "\\'")}'"
138
146
  else
139
- IO.popen(command, chdir: Dir.pwd, &:read)
147
+ command
140
148
  end
141
149
 
142
- format_output(command, result, $CHILD_STATUS.exitstatus)
150
+ result, exit_status = Roast::Helpers::TimeoutHandler.call(
151
+ full_command,
152
+ timeout: timeout,
153
+ working_directory: Dir.pwd,
154
+ )
155
+
156
+ format_output(command, result, exit_status)
157
+ rescue Timeout::Error => e
158
+ Roast::Helpers::Logger.error(e.message + "\n")
159
+ e.message
143
160
  end
144
161
 
145
162
  def format_output(command, result, exit_status)
@@ -1,12 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "roast/helpers/logger"
4
- require "roast/tools/helpers/coding_agent_message_formatter"
5
- require "json"
6
- require "open3"
7
- require "tempfile"
8
- require "securerandom"
9
-
10
3
  module Roast
11
4
  module Tools
12
5
  module CodingAgent
@@ -26,10 +19,16 @@ module Roast
26
19
  base.class_eval do
27
20
  function(
28
21
  :coding_agent,
29
- "AI-powered coding agent that runs Claude Code CLI with the given prompt",
22
+ "AI-powered coding agent that runs an instance of the Claude Code agent with the given prompt. If the agent is iterating on previous work, set continue to true.",
30
23
  prompt: { type: "string", description: "The prompt to send to Claude Code" },
24
+ include_context_summary: { type: "boolean", description: "Whether to set a summary of the current workflow context as system directive (default: false)", required: false },
25
+ continue: { type: "boolean", description: "Whether to continue where the previous coding agent left off or start with a fresh context (default: false, start fresh)", required: false },
31
26
  ) do |params|
32
- Roast::Tools::CodingAgent.call(params[:prompt])
27
+ Roast::Tools::CodingAgent.call(
28
+ params[:prompt],
29
+ include_context_summary: params[:include_context_summary].presence || false,
30
+ continue: params[:continue].presence || false,
31
+ )
33
32
  end
34
33
  end
35
34
  end
@@ -40,9 +39,9 @@ module Roast
40
39
  end
41
40
  end
42
41
 
43
- def call(prompt)
42
+ def call(prompt, include_context_summary: false, continue: false)
44
43
  Roast::Helpers::Logger.info("🤖 Running CodingAgent\n")
45
- run_claude_code(prompt)
44
+ run_claude_code(prompt, include_context_summary:, continue:)
46
45
  rescue StandardError => e
47
46
  "Error running CodingAgent: #{e.message}".tap do |error_message|
48
47
  Roast::Helpers::Logger.error(error_message + "\n")
@@ -52,7 +51,7 @@ module Roast
52
51
 
53
52
  private
54
53
 
55
- def run_claude_code(prompt)
54
+ def run_claude_code(prompt, include_context_summary:, continue:)
56
55
  Roast::Helpers::Logger.debug("🤖 Executing Claude Code CLI with prompt: #{prompt}\n")
57
56
 
58
57
  # Create a temporary file with a unique name
@@ -62,14 +61,21 @@ module Roast
62
61
  temp_file = Tempfile.new(["claude_prompt_#{timestamp}_#{pid}_#{random_id}", ".txt"])
63
62
 
64
63
  begin
64
+ # Prepare the final prompt with context summary if requested
65
+ final_prompt = prepare_prompt(prompt, include_context_summary)
66
+
65
67
  # Write the prompt to the file
66
- temp_file.write(prompt)
68
+ temp_file.write(final_prompt)
67
69
  temp_file.close
68
70
 
71
+ # Build the command with continue option if specified
72
+ base_command = claude_code_command
73
+ command_to_run = build_command(base_command, continue:)
74
+
69
75
  # 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}"
76
+ expect_json_output = command_to_run.include?("--output-format stream-json") ||
77
+ command_to_run.include?("--output-format json")
78
+ command = "cat #{temp_file.path} | #{command_to_run}"
73
79
  result = ""
74
80
 
75
81
  Open3.popen3(command) do |stdin, stdout, stderr, wait_thread|
@@ -110,7 +116,7 @@ module Roast
110
116
  def handle_intermediate_message(json)
111
117
  case json["type"]
112
118
  when "assistant", "user"
113
- CodingAgentMessageFormatter.format_messages(json).each(&method(:log_message))
119
+ Roast::Tools::Helpers::CodingAgentMessageFormatter.format_messages(json).each(&method(:log_message))
114
120
  when "result", "system"
115
121
  # Ignore these message types
116
122
  else
@@ -143,6 +149,48 @@ module Roast
143
149
  def claude_code_command
144
150
  CodingAgent.configured_command || ENV["CLAUDE_CODE_COMMAND"] || "claude -p --verbose --output-format stream-json"
145
151
  end
152
+
153
+ def build_command(base_command, continue:)
154
+ return base_command unless continue
155
+
156
+ # Add --continue flag to the command
157
+ # If the command already has flags, insert --continue after 'claude'
158
+ if base_command.start_with?("claude ")
159
+ base_command.sub("claude ", "claude --continue ")
160
+ else
161
+ # Fallback for non-standard commands
162
+ "#{base_command} --continue"
163
+ end
164
+ end
165
+
166
+ def prepare_prompt(prompt, include_context_summary)
167
+ return prompt unless include_context_summary
168
+
169
+ context_summary = generate_context_summary(prompt)
170
+ return prompt if context_summary.blank? || context_summary == "No relevant information found in the workflow context."
171
+
172
+ # Prepend context summary as a system directive
173
+ <<~PROMPT
174
+ <system>
175
+ #{context_summary}
176
+ </system>
177
+
178
+ #{prompt}
179
+ PROMPT
180
+ end
181
+
182
+ def generate_context_summary(agent_prompt)
183
+ # Access the current workflow context if available
184
+ workflow_context = Thread.current[:workflow_context]
185
+ return unless workflow_context
186
+
187
+ # Use ContextSummarizer to generate an intelligent summary
188
+ summarizer = ContextSummarizer.new
189
+ summarizer.generate_summary(workflow_context, agent_prompt)
190
+ rescue => e
191
+ Roast::Helpers::Logger.debug("Failed to generate context summary: #{e.message}\n")
192
+ nil
193
+ end
146
194
  end
147
195
  end
148
196
  end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Roast
4
+ module Tools
5
+ class ContextSummarizer
6
+ include Raix::ChatCompletion
7
+
8
+ attr_reader :model
9
+
10
+ def initialize(model: "o4-mini")
11
+ @model = model
12
+ end
13
+
14
+ # Generate an intelligent summary of the workflow context
15
+ # tailored to what the agent needs to know for its upcoming task
16
+ #
17
+ # @param workflow_context [Object] The workflow context from Thread.current
18
+ # @param agent_prompt [String] The prompt the agent is about to execute
19
+ # @return [String, nil] The generated summary or nil if generation fails
20
+ def generate_summary(workflow_context, agent_prompt)
21
+ return unless workflow_context&.workflow
22
+
23
+ context_data = build_context_data(workflow_context.workflow)
24
+ summary_prompt = build_summary_prompt(context_data, agent_prompt)
25
+
26
+ # Use our own transcript for the summary generation
27
+ self.transcript = []
28
+ prompt(summary_prompt)
29
+
30
+ result = chat_completion
31
+ result&.strip
32
+ rescue => e
33
+ Roast::Helpers::Logger.debug("Failed to generate LLM context summary: #{e.message}\n")
34
+ nil
35
+ end
36
+
37
+ private
38
+
39
+ def build_context_data(workflow)
40
+ data = {}
41
+
42
+ # Add workflow description if available
43
+ if workflow.config && workflow.config["description"]
44
+ data[:workflow_description] = workflow.config["description"]
45
+ end
46
+
47
+ # Add step outputs if available
48
+ if workflow.output && !workflow.output.empty?
49
+ data[:step_outputs] = workflow.output.map do |step_name, output|
50
+ # Include full output for context generation
51
+ { step: step_name, output: output }
52
+ end
53
+ end
54
+
55
+ # Add current working directory
56
+ data[:working_directory] = Dir.pwd
57
+
58
+ # Add workflow name if available
59
+ if workflow.respond_to?(:name)
60
+ data[:workflow_name] = workflow.name
61
+ end
62
+
63
+ data
64
+ end
65
+
66
+ def build_summary_prompt(context_data, agent_prompt)
67
+ prompt_parts = []
68
+
69
+ prompt_parts << "You are preparing a context summary for an AI coding agent (Claude Code) that is about to perform a task."
70
+ prompt_parts << "\nThe agent's upcoming task is:"
71
+ prompt_parts << "```"
72
+ prompt_parts << agent_prompt
73
+ prompt_parts << "```"
74
+
75
+ prompt_parts << "\nBased on the following workflow context, provide a concise summary of ONLY the information that would be relevant for the agent to complete this specific task."
76
+
77
+ if context_data[:workflow_description]
78
+ prompt_parts << "\nWorkflow Description: #{context_data[:workflow_description]}"
79
+ end
80
+
81
+ if context_data[:workflow_name]
82
+ prompt_parts << "\nWorkflow Name: #{context_data[:workflow_name]}"
83
+ end
84
+
85
+ if context_data[:working_directory]
86
+ prompt_parts << "\nWorking Directory: #{context_data[:working_directory]}"
87
+ end
88
+
89
+ if context_data[:step_outputs] && !context_data[:step_outputs].empty?
90
+ prompt_parts << "\nPrevious Step Outputs:"
91
+ context_data[:step_outputs].each do |step_data|
92
+ prompt_parts << "\n### Step: #{step_data[:step]}"
93
+ prompt_parts << "Output: #{step_data[:output]}"
94
+ end
95
+ end
96
+
97
+ prompt_parts << "\n\nGenerate a brief context summary that:"
98
+ prompt_parts << "1. Focuses ONLY on information relevant to the agent's upcoming task"
99
+ prompt_parts << "2. Highlights key findings, decisions, or outputs the agent should be aware of"
100
+ prompt_parts << "3. Is concise and actionable (aim for 3-5 sentences)"
101
+ prompt_parts << "4. Does not repeat information that would be obvious from the agent's prompt"
102
+ prompt_parts << "\nIf there is no relevant context for this task, respond with 'No relevant information found in the workflow context.'"
103
+
104
+ prompt_parts.join("\n")
105
+ end
106
+ end
107
+ end
108
+ end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "roast/helpers/logger"
4
-
5
3
  module Roast
6
4
  module Tools
7
5
  module Grep
@@ -30,7 +28,6 @@ module Roast
30
28
  Roast::Helpers::Logger.info("🔍 Grepping for string: #{string}\n")
31
29
 
32
30
  # Use Open3 to safely pass the string as an argument, avoiding shell injection
33
- require "open3"
34
31
  cmd = ["rg", "-C", "4", "--trim", "--color=never", "--heading", "-F", "--", string, "."]
35
32
  stdout, _stderr, _status = Open3.capture3(*cmd)
36
33
 
@@ -1,11 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "json"
4
- require "yaml"
5
-
6
3
  module Roast
7
4
  module Tools
8
- module CodingAgent
5
+ module Helpers
9
6
  module CodingAgentMessageFormatter
10
7
  extend self
11
8
 
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "roast/helpers/logger"
4
-
5
3
  module Roast
6
4
  module Tools
7
5
  module ReadFile
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "roast/helpers/logger"
4
-
5
3
  module Roast
6
4
  module Tools
7
5
  module SearchFile
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "shellwords"
4
+
5
+ module Roast
6
+ module Tools
7
+ module Swarm
8
+ extend self
9
+
10
+ DEFAULT_CONFIG_PATHS = [
11
+ ".swarm.yml",
12
+ "swarm.yml",
13
+ ".swarm/config.yml",
14
+ ].freeze
15
+
16
+ CONFIG_PATH_KEY = "path"
17
+ private_constant :DEFAULT_CONFIG_PATHS, :CONFIG_PATH_KEY
18
+
19
+ class << self
20
+ def included(base)
21
+ base.class_eval do
22
+ function(
23
+ :swarm,
24
+ "Execute Claude Swarm to orchestrate multiple Claude Code instances",
25
+ prompt: {
26
+ type: "string",
27
+ description: "The prompt to send to the swarm agents",
28
+ required: true,
29
+ },
30
+ path: {
31
+ type: "string",
32
+ description: "Path to the swarm configuration file (optional)",
33
+ required: false,
34
+ },
35
+ ) do |params|
36
+ Roast::Tools::Swarm.call(params[:prompt], params[:path])
37
+ end
38
+ end
39
+ end
40
+
41
+ def post_configuration_setup(base, config = {})
42
+ @tool_config = config
43
+ end
44
+
45
+ attr_reader :tool_config
46
+ end
47
+
48
+ def call(prompt, step_path = nil)
49
+ config_path = determine_config_path(step_path)
50
+
51
+ if config_path.nil?
52
+ return "Error: No swarm configuration file found. Please create a .swarm.yml file or specify a path."
53
+ end
54
+
55
+ unless File.exist?(config_path)
56
+ return "Error: Swarm configuration file not found at: #{config_path}"
57
+ end
58
+
59
+ Roast::Helpers::Logger.info("🐝 Running Claude Swarm with config: #{config_path}\n")
60
+
61
+ execute_swarm(prompt, config_path)
62
+ rescue StandardError => e
63
+ handle_error(e)
64
+ end
65
+
66
+ private
67
+
68
+ def determine_config_path(step_path)
69
+ # Priority: step-level path > tool-level path > default locations
70
+
71
+ # 1. Check step-level path
72
+ return step_path if step_path
73
+
74
+ # 2. Check tool-level path from configuration
75
+ if tool_config && tool_config[CONFIG_PATH_KEY]
76
+ return tool_config[CONFIG_PATH_KEY]
77
+ end
78
+
79
+ # 3. Check default locations
80
+ DEFAULT_CONFIG_PATHS.find { |path| File.exist?(path) }
81
+ end
82
+
83
+ def execute_swarm(prompt, config_path)
84
+ # Build the swarm command with proper escaping
85
+ command = build_swarm_command(prompt, config_path)
86
+
87
+ result = ""
88
+
89
+ # Execute the command directly with the prompt included
90
+ IO.popen(command, err: [:child, :out]) do |io|
91
+ result = io.read
92
+ end
93
+
94
+ exit_status = $CHILD_STATUS.exitstatus
95
+
96
+ format_output(command, result, exit_status)
97
+ end
98
+
99
+ def build_swarm_command(prompt, config_path)
100
+ # Build the claude-swarm command with properly escaped arguments
101
+ [
102
+ "claude-swarm",
103
+ "--config",
104
+ config_path,
105
+ "--prompt",
106
+ prompt,
107
+ ].shelljoin
108
+ end
109
+
110
+ def format_output(command, result, exit_status)
111
+ "Command: #{command}\n" \
112
+ "Exit status: #{exit_status}\n" \
113
+ "Output:\n#{result}"
114
+ end
115
+
116
+ def handle_error(error)
117
+ error_message = "Error running swarm: #{error.message}"
118
+ Roast::Helpers::Logger.error("#{error_message}\n")
119
+ Roast::Helpers::Logger.debug("#{error.backtrace.join("\n")}\n") if ENV["DEBUG"]
120
+ error_message
121
+ end
122
+ end
123
+ end
124
+ end
@@ -1,9 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "fileutils"
4
- require "roast/helpers/logger"
5
- require "diff/lcs"
6
-
7
3
  module Roast
8
4
  module Tools
9
5
  module UpdateFiles
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "fileutils"
4
- require "roast/helpers/logger"
5
-
6
3
  module Roast
7
4
  module Tools
8
5
  module WriteFile
data/lib/roast/tools.rb CHANGED
@@ -1,18 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "English"
4
- require "fileutils"
5
-
6
- require "roast/tools/ask_user"
7
- require "roast/tools/bash"
8
- require "roast/tools/cmd"
9
- require "roast/tools/coding_agent"
10
- require "roast/tools/grep"
11
- require "roast/tools/read_file"
12
- require "roast/tools/search_file"
13
- require "roast/tools/update_files"
14
- require "roast/tools/write_file"
15
-
16
3
  module Roast
17
4
  module Tools
18
5
  extend self
@@ -24,12 +24,23 @@ module Roast
24
24
  @value
25
25
  end
26
26
 
27
+ # Enables implicit conversion to String
28
+ alias_method :to_str, :to_s
29
+
27
30
  def ==(other)
28
- return false unless other.is_a?(StepName)
31
+ case other
32
+ when StepName
33
+ value == other.value
34
+ when String
35
+ value == other
36
+ else
37
+ false
38
+ end
39
+ end
29
40
 
30
- value == other.value
41
+ def eql?(other)
42
+ other.is_a?(StepName) && value == other.value
31
43
  end
32
- alias_method :eql?, :==
33
44
 
34
45
  def hash
35
46
  [self.class, @value].hash
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "pathname"
4
-
5
3
  module Roast
6
4
  module ValueObjects
7
5
  # Value object representing a workflow file path with validation and resolution
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "roast/value_objects/api_token"
4
- require "roast/value_objects/step_name"
5
- require "roast/value_objects/workflow_path"
6
- require "roast/value_objects/uri_base"
3
+ module Roast
4
+ module ValueObjects
5
+ end
6
+ end
data/lib/roast/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Roast
4
- VERSION = "0.3.1"
4
+ VERSION = "0.4.1"
5
5
  end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Roast
4
+ module Workflow
5
+ class AgentStep < BaseStep
6
+ def call
7
+ # For inline prompts (detected by plain text step names), use the name as the prompt
8
+ # For file-based steps, load from the prompt file
9
+ prompt_content = if name.plain_text?
10
+ name.to_s
11
+ else
12
+ read_sidecar_prompt
13
+ end
14
+
15
+ # Extract agent-specific configuration from workflow config
16
+ step_config = workflow.config[name.to_s] || {}
17
+ agent_options = {
18
+ include_context_summary: step_config.fetch("include_context_summary", false),
19
+ continue: step_config.fetch("continue", false),
20
+ }
21
+
22
+ # Call CodingAgent directly with the prompt content and options
23
+ result = Roast::Tools::CodingAgent.call(prompt_content, **agent_options)
24
+
25
+ # Process output if print_response is enabled
26
+ process_output(result, print_response:)
27
+
28
+ # Apply coercion if configured
29
+ apply_coercion(result)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -1,9 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "roast/factories/api_provider_factory"
4
- require "roast/workflow/resource_resolver"
5
- require "roast/value_objects/uri_base"
6
-
7
3
  module Roast
8
4
  module Workflow
9
5
  # Handles API-related configuration including tokens and providers
@@ -1,9 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "roast/workflow/expression_utils"
4
- require "roast/workflow/llm_boolean_coercer"
5
- require "roast/workflow/workflow_executor"
6
-
7
3
  module Roast
8
4
  module Workflow
9
5
  # Base class for iteration steps (RepeatStep and EachStep)
@@ -69,10 +65,11 @@ module Roast
69
65
  executor ||= WorkflowExecutor.new(context, config_hash, context_path)
70
66
  results = []
71
67
 
72
- steps.each do |step|
68
+ steps.each_with_index do |step, index|
69
+ is_last_step = (index == steps.length - 1)
73
70
  result = case step
74
71
  when String
75
- executor.execute_step(step)
72
+ executor.execute_step(step, is_last_step:)
76
73
  when Hash, Array
77
74
  executor.execute_steps([step])
78
75
  end