roast-ai 0.3.0 → 0.4.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 (127) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/CHANGELOG.md +32 -0
  4. data/CLAUDE.md +52 -1
  5. data/Gemfile +3 -1
  6. data/Gemfile.lock +63 -16
  7. data/README.md +115 -5
  8. data/bin/roast +1 -1
  9. data/claude-swarm.yml +210 -0
  10. data/docs/AGENT_STEPS.md +264 -0
  11. data/examples/agent_workflow/README.md +75 -0
  12. data/examples/agent_workflow/apply_refactorings/prompt.md +22 -0
  13. data/examples/agent_workflow/identify_code_smells/prompt.md +15 -0
  14. data/examples/agent_workflow/summarize_improvements/prompt.md +18 -0
  15. data/examples/agent_workflow/workflow.yml +16 -0
  16. data/examples/available_tools_demo/README.md +42 -0
  17. data/examples/available_tools_demo/analyze_files/prompt.md +6 -0
  18. data/examples/available_tools_demo/explore_directory/prompt.md +6 -0
  19. data/examples/available_tools_demo/workflow.yml +32 -0
  20. data/examples/available_tools_demo/write_summary/prompt.md +6 -0
  21. data/examples/case_when/detect_language/prompt.md +2 -2
  22. data/examples/grading/README.md +71 -0
  23. data/examples/grading/run_coverage.rb +0 -2
  24. data/examples/iteration/analyze_complexity/prompt.md +2 -2
  25. data/examples/iteration/generate_recommendations/prompt.md +2 -2
  26. data/examples/iteration/implement_fix/prompt.md +2 -2
  27. data/examples/iteration/prioritize_issues/prompt.md +1 -1
  28. data/examples/iteration/prompts/analyze_file.md +2 -2
  29. data/examples/iteration/prompts/generate_summary.md +1 -1
  30. data/examples/iteration/prompts/update_report.md +3 -3
  31. data/examples/iteration/prompts/write_report.md +3 -3
  32. data/examples/iteration/read_file/prompt.md +2 -2
  33. data/examples/iteration/select_next_issue/prompt.md +2 -2
  34. data/examples/iteration/update_fix_count/prompt.md +4 -4
  35. data/examples/iteration/verify_fix/prompt.md +3 -3
  36. data/examples/mcp/README.md +3 -3
  37. data/examples/mcp/analyze_changes/prompt.md +1 -1
  38. data/examples/mcp/database_workflow.yml +1 -1
  39. data/examples/mcp/fetch_pr_context/prompt.md +1 -1
  40. data/examples/mcp/github_workflow.yml +1 -1
  41. data/examples/mcp/post_review/prompt.md +1 -1
  42. data/examples/pre_post_processing/analyze_test_file/prompt.md +1 -1
  43. data/examples/pre_post_processing/improve_test_coverage/prompt.md +1 -1
  44. data/examples/pre_post_processing/optimize_test_performance/prompt.md +1 -1
  45. data/examples/pre_post_processing/post_processing/aggregate_metrics/prompt.md +2 -2
  46. data/examples/pre_post_processing/post_processing/generate_summary_report/prompt.md +1 -1
  47. data/examples/pre_post_processing/pre_processing/setup_test_environment/prompt.md +1 -1
  48. data/examples/pre_post_processing/validate_changes/prompt.md +2 -2
  49. data/examples/user_input/README.md +90 -0
  50. data/examples/user_input/funny_name/create_backstory/prompt.md +10 -0
  51. data/examples/user_input/funny_name/workflow.yml +26 -0
  52. data/examples/user_input/generate_summary/prompt.md +11 -0
  53. data/examples/user_input/simple_input_demo/workflow.yml +35 -0
  54. data/examples/user_input/survey_workflow.yml +71 -0
  55. data/examples/user_input/welcome_message/prompt.md +3 -0
  56. data/examples/user_input/workflow.yml +73 -0
  57. data/examples/workflow_generator/create_workflow_files/prompt.md +1 -1
  58. data/lib/roast/errors.rb +6 -4
  59. data/lib/roast/helpers/function_caching_interceptor.rb +0 -2
  60. data/lib/roast/helpers/logger.rb +12 -35
  61. data/lib/roast/helpers/minitest_coverage_runner.rb +0 -1
  62. data/lib/roast/helpers/prompt_loader.rb +0 -2
  63. data/lib/roast/resources/api_resource.rb +0 -4
  64. data/lib/roast/resources/url_resource.rb +0 -3
  65. data/lib/roast/resources.rb +0 -8
  66. data/lib/roast/tools/ask_user.rb +0 -2
  67. data/lib/roast/tools/bash.rb +0 -3
  68. data/lib/roast/tools/cmd.rb +0 -3
  69. data/lib/roast/tools/coding_agent.rb +1 -8
  70. data/lib/roast/tools/grep.rb +0 -3
  71. data/lib/roast/tools/helpers/coding_agent_message_formatter.rb +1 -4
  72. data/lib/roast/tools/read_file.rb +0 -2
  73. data/lib/roast/tools/search_file.rb +0 -2
  74. data/lib/roast/tools/update_files.rb +0 -4
  75. data/lib/roast/tools/write_file.rb +0 -3
  76. data/lib/roast/tools.rb +0 -13
  77. data/lib/roast/value_objects/step_name.rb +14 -3
  78. data/lib/roast/value_objects/workflow_path.rb +0 -2
  79. data/lib/roast/value_objects.rb +4 -4
  80. data/lib/roast/version.rb +1 -1
  81. data/lib/roast/workflow/agent_step.rb +26 -0
  82. data/lib/roast/workflow/api_configuration.rb +0 -4
  83. data/lib/roast/workflow/base_iteration_step.rb +0 -4
  84. data/lib/roast/workflow/base_step.rb +54 -28
  85. data/lib/roast/workflow/base_workflow.rb +2 -21
  86. data/lib/roast/workflow/case_executor.rb +0 -1
  87. data/lib/roast/workflow/case_step.rb +0 -4
  88. data/lib/roast/workflow/command_executor.rb +0 -2
  89. data/lib/roast/workflow/conditional_executor.rb +0 -1
  90. data/lib/roast/workflow/conditional_step.rb +0 -4
  91. data/lib/roast/workflow/configuration.rb +3 -10
  92. data/lib/roast/workflow/configuration_loader.rb +0 -2
  93. data/lib/roast/workflow/configuration_parser.rb +1 -7
  94. data/lib/roast/workflow/dot_access_hash.rb +16 -1
  95. data/lib/roast/workflow/error_handler.rb +0 -3
  96. data/lib/roast/workflow/expression_evaluator.rb +0 -3
  97. data/lib/roast/workflow/file_state_repository.rb +0 -5
  98. data/lib/roast/workflow/input_executor.rb +41 -0
  99. data/lib/roast/workflow/input_step.rb +163 -0
  100. data/lib/roast/workflow/iteration_executor.rb +0 -2
  101. data/lib/roast/workflow/output_handler.rb +0 -2
  102. data/lib/roast/workflow/output_manager.rb +0 -2
  103. data/lib/roast/workflow/prompt_step.rb +1 -1
  104. data/lib/roast/workflow/replay_handler.rb +0 -3
  105. data/lib/roast/workflow/resource_resolver.rb +0 -3
  106. data/lib/roast/workflow/session_manager.rb +0 -3
  107. data/lib/roast/workflow/state_manager.rb +0 -2
  108. data/lib/roast/workflow/step_executor_coordinator.rb +34 -11
  109. data/lib/roast/workflow/step_executor_factory.rb +0 -5
  110. data/lib/roast/workflow/step_executor_registry.rb +1 -4
  111. data/lib/roast/workflow/step_executors/hash_step_executor.rb +0 -3
  112. data/lib/roast/workflow/step_executors/parallel_step_executor.rb +0 -3
  113. data/lib/roast/workflow/step_executors/string_step_executor.rb +0 -2
  114. data/lib/roast/workflow/step_factory.rb +56 -0
  115. data/lib/roast/workflow/step_loader.rb +30 -16
  116. data/lib/roast/workflow/step_orchestrator.rb +3 -2
  117. data/lib/roast/workflow/step_type_resolver.rb +28 -1
  118. data/lib/roast/workflow/validator.rb +0 -4
  119. data/lib/roast/workflow/workflow_executor.rb +0 -16
  120. data/lib/roast/workflow/workflow_initializer.rb +1 -8
  121. data/lib/roast/workflow/workflow_runner.rb +0 -7
  122. data/lib/roast/workflow.rb +0 -15
  123. data/lib/roast.rb +55 -10
  124. data/roast.gemspec +2 -1
  125. data/schema/workflow.json +46 -0
  126. metadata +44 -6
  127. data/lib/roast/helpers.rb +0 -12
@@ -1,10 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "roast/workflow/case_executor"
4
- require "roast/workflow/conditional_executor"
5
- require "roast/workflow/step_executor_factory"
6
- require "roast/workflow/step_type_resolver"
7
-
8
3
  module Roast
9
4
  module Workflow
10
5
  # Coordinates the execution of different types of steps
@@ -29,20 +24,21 @@ module Roast
29
24
 
30
25
  # Execute a list of steps
31
26
  def execute_steps(workflow_steps)
32
- workflow_steps.each do |step|
27
+ workflow_steps.each_with_index do |step, index|
28
+ is_last_step = (index == workflow_steps.length - 1)
33
29
  case step
34
30
  when Hash
35
- execute(step)
31
+ execute(step, is_last_step: is_last_step)
36
32
  when Array
37
- execute(step)
33
+ execute(step, is_last_step: is_last_step)
38
34
  when String
39
- execute(step)
35
+ execute(step, is_last_step: is_last_step)
40
36
  # Handle pause after string steps
41
37
  if @context.workflow.pause_step_name == step
42
38
  Kernel.binding.irb # rubocop:disable Lint/Debugger
43
39
  end
44
40
  else
45
- step_orchestrator.execute_step(step)
41
+ step_orchestrator.execute_step(step, is_last_step: is_last_step)
46
42
  end
47
43
  end
48
44
  end
@@ -63,6 +59,8 @@ module Roast
63
59
  when StepTypeResolver::COMMAND_STEP
64
60
  # Command steps should also go through interpolation
65
61
  execute_string_step(step, options)
62
+ when StepTypeResolver::AGENT_STEP
63
+ execute_agent_step(step, options)
66
64
  when StepTypeResolver::GLOB_STEP
67
65
  execute_glob_step(step)
68
66
  when StepTypeResolver::ITERATION_STEP
@@ -71,6 +69,8 @@ module Roast
71
69
  execute_conditional_step(step)
72
70
  when StepTypeResolver::CASE_STEP
73
71
  execute_case_step(step)
72
+ when StepTypeResolver::INPUT_STEP
73
+ execute_input_step(step)
74
74
  when StepTypeResolver::HASH_STEP
75
75
  execute_hash_step(step)
76
76
  when StepTypeResolver::PARALLEL_STEP
@@ -117,6 +117,15 @@ module Roast
117
117
  )
118
118
  end
119
119
 
120
+ def input_executor
121
+ @input_executor ||= dependencies[:input_executor] || InputExecutor.new(
122
+ context.workflow,
123
+ context.context_path,
124
+ dependencies[:state_manager] || dependencies[:workflow_executor].state_manager,
125
+ workflow_executor,
126
+ )
127
+ end
128
+
120
129
  def step_orchestrator
121
130
  dependencies[:step_orchestrator]
122
131
  end
@@ -171,6 +180,15 @@ module Roast
171
180
  end
172
181
  end
173
182
 
183
+ def execute_agent_step(step, options = {})
184
+ # Extract the step name without the ^ prefix
185
+ step_name = StepTypeResolver.extract_name(step)
186
+
187
+ # Load and execute the agent step
188
+ exit_on_error = options.fetch(:exit_on_error, context.exit_on_error?(step))
189
+ step_orchestrator.execute_step(step_name, exit_on_error:, step_key: options[:step_key], agent_type: :coding_agent)
190
+ end
191
+
174
192
  def execute_glob_step(step)
175
193
  Dir.glob(step).join("\n")
176
194
  end
@@ -196,6 +214,10 @@ module Roast
196
214
  case_executor.execute_case(step)
197
215
  end
198
216
 
217
+ def execute_input_step(step)
218
+ input_executor.execute_input(step["input"])
219
+ end
220
+
199
221
  def execute_hash_step(step)
200
222
  name, command = step.to_a.flatten
201
223
  interpolated_name = interpolator.interpolate(name)
@@ -235,7 +257,8 @@ module Roast
235
257
  def execute_standard_step(step, options)
236
258
  exit_on_error = options.fetch(:exit_on_error, true)
237
259
  step_key = options[:step_key]
238
- step_orchestrator.execute_step(step, exit_on_error: exit_on_error, step_key: step_key)
260
+ is_last_step = options[:is_last_step]
261
+ step_orchestrator.execute_step(step, exit_on_error:, step_key:, is_last_step:)
239
262
  end
240
263
 
241
264
  def validate_each_step!(step)
@@ -1,10 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "roast/workflow/step_executor_registry"
4
- require "roast/workflow/step_executors/hash_step_executor"
5
- require "roast/workflow/step_executors/parallel_step_executor"
6
- require "roast/workflow/step_executors/string_step_executor"
7
-
8
3
  module Roast
9
4
  module Workflow
10
5
  # Factory for creating step executors - now delegates to registry
@@ -43,10 +43,7 @@ module Roast
43
43
  def clear!
44
44
  @executors.clear
45
45
  @type_matchers.clear
46
- # Reset the factory's defaults flag if it's defined
47
- if defined?(StepExecutorFactory)
48
- StepExecutorFactory.instance_variable_set(:@defaults_registered, false)
49
- end
46
+ StepExecutorFactory.instance_variable_set(:@defaults_registered, false)
50
47
  end
51
48
 
52
49
  # Get all registered executors (useful for debugging)
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "roast/workflow/step_executors/base_step_executor"
4
- require "roast/workflow/step_runner"
5
-
6
3
  module Roast
7
4
  module Workflow
8
5
  module StepExecutors
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "roast/workflow/step_executors/base_step_executor"
4
- require "roast/workflow/step_runner"
5
-
6
3
  module Roast
7
4
  module Workflow
8
5
  module StepExecutors
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "roast/workflow/step_executors/base_step_executor"
4
-
5
3
  module Roast
6
4
  module Workflow
7
5
  module StepExecutors
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Roast
4
+ module Workflow
5
+ # Factory for creating step instances based on step characteristics
6
+ class StepFactory
7
+ class << self
8
+ # Create a step instance based on the step type and characteristics
9
+ #
10
+ # @param workflow [BaseWorkflow] The workflow instance
11
+ # @param step_name [String, StepName] The name of the step
12
+ # @param options [Hash] Additional options for step creation
13
+ # @return [BaseStep] The appropriate step instance
14
+ def create(workflow, step_name, options = {})
15
+ name = normalize_step_name(step_name)
16
+
17
+ # Determine the step class based on characteristics
18
+ step_class = determine_step_class(name, options)
19
+
20
+ # Create the step instance with appropriate parameters
21
+ build_step_instance(step_class, workflow, name, options)
22
+ end
23
+
24
+ private
25
+
26
+ def normalize_step_name(step_name)
27
+ step_name.is_a?(Roast::ValueObjects::StepName) ? step_name : Roast::ValueObjects::StepName.new(step_name)
28
+ end
29
+
30
+ def determine_step_class(name, options)
31
+ # Check if this is an agent step (indicated by special processing needs)
32
+ if options[:agent_type] == :coding_agent
33
+ Roast::Workflow::AgentStep
34
+ elsif name.plain_text?
35
+ # Plain text steps are always prompt steps
36
+ options[:agent_type] == :coding_agent ? Roast::Workflow::AgentStep : Roast::Workflow::PromptStep
37
+ else
38
+ # Default to BaseStep for directory-based steps
39
+ Roast::Workflow::BaseStep
40
+ end
41
+ end
42
+
43
+ def build_step_instance(step_class, workflow, name, options)
44
+ step_params = {
45
+ name:,
46
+ }
47
+
48
+ # Add context path if provided
49
+ step_params[:context_path] = options[:context_path] if options[:context_path]
50
+
51
+ step_class.new(workflow, **step_params)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -1,10 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "roast/value_objects/step_name"
4
- require "roast/workflow/workflow_context"
5
- require "roast/workflow/base_step"
6
- require "roast/workflow/prompt_step"
7
-
8
3
  module Roast
9
4
  module Workflow
10
5
  # Handles loading and instantiation of workflow steps
@@ -47,8 +42,9 @@ module Roast
47
42
  #
48
43
  # @param step_name [String, StepName] The name of the step to load
49
44
  # @param step_key [String] The configuration key for the step (optional)
45
+ # @param options [Hash] Additional options for step loading
50
46
  # @return [BaseStep] The loaded step instance
51
- def load(step_name, step_key: nil)
47
+ def load(step_name, step_key: nil, is_last_step: nil, **options)
52
48
  name = step_name.is_a?(Roast::ValueObjects::StepName) ? step_name : Roast::ValueObjects::StepName.new(step_name)
53
49
 
54
50
  # Get step config for per-step path
@@ -57,17 +53,17 @@ module Roast
57
53
 
58
54
  # First check for a prompt step (contains spaces)
59
55
  if name.plain_text?
60
- step = Roast::Workflow::PromptStep.new(workflow, name: name.to_s)
56
+ step = StepFactory.create(workflow, name, options)
61
57
  # Use step_key for configuration if provided, otherwise use name
62
58
  config_key = step_key || name.to_s
63
- configure_step(step, config_key)
59
+ configure_step(step, config_key, is_last_step:)
64
60
  return step
65
61
  end
66
62
 
67
63
  # Look for Ruby file in various locations
68
64
  step_file_path = find_step_file(name.to_s, per_step_path)
69
65
  if step_file_path
70
- return load_ruby_step(step_file_path, name.to_s)
66
+ return load_ruby_step(step_file_path, name.to_s, is_last_step:)
71
67
  end
72
68
 
73
69
  # Look for step directory
@@ -76,7 +72,10 @@ module Roast
76
72
  raise StepNotFoundError.new("Step directory or file not found: #{name}", step_name: name.to_s)
77
73
  end
78
74
 
79
- create_step_instance(Roast::Workflow::BaseStep, name.to_s, step_directory)
75
+ # Use factory to create the appropriate step instance
76
+ step = StepFactory.create(workflow, name, options.merge(context_path: step_directory))
77
+ configure_step(step, name.to_s, is_last_step:)
78
+ step
80
79
  end
81
80
 
82
81
  private
@@ -141,7 +140,7 @@ module Roast
141
140
  end
142
141
 
143
142
  # Load a Ruby step from a file
144
- def load_ruby_step(file_path, step_name)
143
+ def load_ruby_step(file_path, step_name, is_last_step: nil)
145
144
  $stderr.puts "Requiring step file: #{file_path}"
146
145
 
147
146
  begin
@@ -154,18 +153,24 @@ module Roast
154
153
 
155
154
  step_class = step_name.classify.constantize
156
155
  context = File.dirname(file_path)
157
- create_step_instance(step_class, step_name, context)
156
+ # For Ruby steps, we instantiate the specific class directly
157
+ # Convert step_name to StepName value object
158
+ step_name_obj = Roast::ValueObjects::StepName.new(step_name)
159
+ step = step_class.new(workflow, name: step_name_obj, context_path: context)
160
+ configure_step(step, step_name, is_last_step:)
161
+ step
158
162
  end
159
163
 
160
164
  # Create and configure a step instance
161
- def create_step_instance(step_class, step_name, context_path)
162
- step = step_class.new(workflow, name: step_name, context_path: context_path)
163
- configure_step(step, step_name)
165
+ def create_step_instance(step_class, step_name, context_path, options = {})
166
+ is_last_step = options[:is_last_step]
167
+ step = StepFactory.create(workflow, step_name, options.merge(context_path: context_path))
168
+ configure_step(step, step_name, is_last_step:)
164
169
  step
165
170
  end
166
171
 
167
172
  # Configure a step instance with settings from config_hash
168
- def configure_step(step, step_name)
173
+ def configure_step(step, step_name, is_last_step: nil)
169
174
  step_config = config_hash[step_name]
170
175
 
171
176
  # Always set the model
@@ -176,6 +181,11 @@ module Roast
176
181
 
177
182
  # Apply additional configuration if present
178
183
  apply_step_configuration(step, step_config) if step_config.present?
184
+
185
+ # Set print_response to true for the last step if not already configured
186
+ if is_last_step && !step_config&.key?("print_response")
187
+ step.print_response = true
188
+ end
179
189
  end
180
190
 
181
191
  # Determine which model to use for the step
@@ -189,6 +199,10 @@ module Roast
189
199
  step.json = step_config["json"] if step_config.key?("json")
190
200
  step.params = step_config["params"] if step_config.key?("params")
191
201
  step.coerce_to = step_config["coerce_to"].to_sym if step_config.key?("coerce_to")
202
+
203
+ if step_config.key?("available_tools")
204
+ step.available_tools = step_config["available_tools"]
205
+ end
192
206
  end
193
207
  end
194
208
  end
@@ -22,7 +22,7 @@ module Roast
22
22
  @workflow_executor = workflow_executor
23
23
  end
24
24
 
25
- def execute_step(name, exit_on_error: true, step_key: nil)
25
+ def execute_step(name, exit_on_error: true, step_key: nil, **options)
26
26
  resource_type = @workflow.respond_to?(:resource) ? @workflow.resource&.type : nil
27
27
 
28
28
  @error_handler.with_error_handling(name, resource_type: resource_type) do
@@ -30,7 +30,8 @@ module Roast
30
30
 
31
31
  # Use step_key for loading if provided, otherwise use name
32
32
  load_key = step_key || name
33
- step_object = @step_loader.load(name, step_key: load_key)
33
+ is_last_step = options[:is_last_step]
34
+ step_object = @step_loader.load(name, step_key: load_key, is_last_step:, **options)
34
35
  step_result = step_object.call
35
36
 
36
37
  # Store result in workflow output
@@ -10,10 +10,12 @@ module Roast
10
10
  ITERATION_STEP = :iteration
11
11
  CONDITIONAL_STEP = :conditional
12
12
  CASE_STEP = :case
13
+ INPUT_STEP = :input
13
14
  HASH_STEP = :hash
14
15
  PARALLEL_STEP = :parallel
15
16
  STRING_STEP = :string
16
17
  STANDARD_STEP = :standard
18
+ AGENT_STEP = :agent
17
19
 
18
20
  # Special step names for iterations
19
21
  ITERATION_STEPS = ["repeat", "each"].freeze
@@ -24,6 +26,9 @@ module Roast
24
26
  # Special step name for case statements
25
27
  CASE_STEPS = ["case"].freeze
26
28
 
29
+ # Special step name for input steps
30
+ INPUT_STEPS = ["input"].freeze
31
+
27
32
  class << self
28
33
  # Resolve the type of a step
29
34
  # @param step [String, Hash, Array] The step to analyze
@@ -49,6 +54,13 @@ module Roast
49
54
  step.is_a?(String) && step.start_with?("$(")
50
55
  end
51
56
 
57
+ # Check if a step is an agent step
58
+ # @param step [String] The step to check
59
+ # @return [Boolean] true if it's an agent step
60
+ def agent_step?(step)
61
+ step.is_a?(String) && step.start_with?("^")
62
+ end
63
+
52
64
  # Check if a step is a glob pattern
53
65
  # @param step [String] The step to check
54
66
  # @param context [WorkflowContext, nil] The workflow context
@@ -90,13 +102,24 @@ module Roast
90
102
  CASE_STEPS.include?(step_name)
91
103
  end
92
104
 
105
+ # Check if a step is an input step
106
+ # @param step [Hash] The step to check
107
+ # @return [Boolean] true if it's an input step
108
+ def input_step?(step)
109
+ return false unless step.is_a?(Hash)
110
+
111
+ step_name = step.keys.first
112
+ INPUT_STEPS.include?(step_name)
113
+ end
114
+
93
115
  # Extract the step name from various step formats
94
116
  # @param step [String, Hash, Array] The step
95
117
  # @return [String, nil] The step name or nil
96
118
  def extract_name(step)
97
119
  case step
98
120
  when String
99
- step
121
+ # Strip ^ prefix for agent steps
122
+ agent_step?(step) ? step[1..] : step
100
123
  when Hash
101
124
  step.keys.first
102
125
  when Array
@@ -109,6 +132,8 @@ module Roast
109
132
  def resolve_string_step(step, context)
110
133
  if command_step?(step)
111
134
  COMMAND_STEP
135
+ elsif agent_step?(step)
136
+ AGENT_STEP
112
137
  elsif glob_step?(step, context)
113
138
  GLOB_STEP
114
139
  else
@@ -123,6 +148,8 @@ module Roast
123
148
  CONDITIONAL_STEP
124
149
  elsif case_step?(step)
125
150
  CASE_STEP
151
+ elsif input_step?(step)
152
+ INPUT_STEP
126
153
  else
127
154
  HASH_STEP
128
155
  end
@@ -1,9 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "json-schema"
4
- require "json"
5
- require "yaml"
6
-
7
3
  module Roast
8
4
  module Workflow
9
5
  class Validator
@@ -1,21 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "English"
4
-
5
- require "roast/workflow/command_executor"
6
- require "roast/workflow/conditional_executor"
7
- require "roast/workflow/error_handler"
8
- require "roast/workflow/interpolator"
9
- require "roast/workflow/iteration_executor"
10
- require "roast/workflow/parallel_executor"
11
- require "roast/workflow/state_manager"
12
- require "roast/workflow/step_executor_factory"
13
- require "roast/workflow/step_executor_coordinator"
14
- require "roast/workflow/step_loader"
15
- require "roast/workflow/step_orchestrator"
16
- require "roast/workflow/step_type_resolver"
17
- require "roast/workflow/workflow_context"
18
-
19
3
  module Roast
20
4
  module Workflow
21
5
  # Handles the execution of workflow steps, including orchestration and threading
@@ -1,12 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "raix"
4
- require "roast/initializers"
5
- require "roast/helpers/function_caching_interceptor"
6
- require "roast/helpers/logger"
7
- require "roast/workflow/base_workflow"
8
- require "roast/workflow/interpolator"
9
-
10
3
  module Roast
11
4
  module Workflow
12
5
  # Handles initialization of workflow dependencies: initializers, tools, and API clients
@@ -103,7 +96,7 @@ module Roast
103
96
  # Validate the client configuration by making a test API call
104
97
  validate_api_client(client) if client
105
98
  rescue OpenRouter::ConfigurationError, Faraday::UnauthorizedError => e
106
- error = Roast::AuthenticationError.new("API authentication failed: No API token provided or token is invalid")
99
+ error = Roast::Errors::AuthenticationError.new("API authentication failed: No API token provided or token is invalid")
107
100
  error.set_backtrace(e.backtrace)
108
101
 
109
102
  ActiveSupport::Notifications.instrument("roast.workflow.start.error", {
@@ -1,12 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "erb"
4
- require "roast/workflow/replay_handler"
5
- require "roast/workflow/workflow_executor"
6
- require "roast/workflow/output_handler"
7
- require "roast/workflow/base_workflow"
8
- require "roast/workflow/dot_access_hash"
9
-
10
3
  module Roast
11
4
  module Workflow
12
5
  # Handles running workflows for files/targets and orchestrating execution
@@ -1,20 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "roast/workflow/base_step"
4
- require "roast/workflow/prompt_step"
5
- require "roast/workflow/base_iteration_step"
6
- require "roast/workflow/repeat_step"
7
- require "roast/workflow/each_step"
8
- require "roast/workflow/base_workflow"
9
- require "roast/workflow/configuration"
10
- require "roast/workflow/workflow_execution_context"
11
- require "roast/workflow/workflow_executor"
12
- require "roast/workflow/configuration_parser"
13
- require "roast/workflow/validator"
14
- require "roast/workflow/state_repository"
15
- require "roast/workflow/session_manager"
16
- require "roast/workflow/file_state_repository"
17
-
18
3
  module Roast
19
4
  module Workflow
20
5
  end
data/lib/roast.rb CHANGED
@@ -1,24 +1,43 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Standard library requires
4
+ require "digest"
5
+ require "English"
6
+ require "erb"
7
+ require "fileutils"
8
+ require "json"
9
+ require "logger"
10
+ require "net/http"
11
+ require "open3"
12
+ require "pathname"
13
+ require "securerandom"
14
+ require "tempfile"
15
+ require "uri"
16
+ require "yaml"
17
+
18
+ # Third-party gem requires
3
19
  require "active_support"
4
20
  require "active_support/cache"
5
- require "active_support/notifications"
6
21
  require "active_support/core_ext/hash/indifferent_access"
22
+ require "active_support/core_ext/module/delegation"
7
23
  require "active_support/core_ext/string"
8
24
  require "active_support/core_ext/string/inflections"
9
- require "active_support/core_ext/module/delegation"
10
25
  require "active_support/isolated_execution_state"
11
- require "fileutils"
26
+ require "active_support/notifications"
12
27
  require "cli/ui"
28
+ require "diff/lcs"
29
+ require "json-schema"
13
30
  require "raix"
31
+ require "raix/chat_completion"
32
+ require "raix/function_dispatch"
14
33
  require "thor"
15
- require "roast/errors"
16
- require "roast/helpers"
17
- require "roast/initializers"
18
- require "roast/resources"
19
- require "roast/tools"
20
- require "roast/version"
21
- require "roast/workflow"
34
+
35
+ # Autoloading setup
36
+ require "zeitwerk"
37
+
38
+ # Set up Zeitwerk autoloader
39
+ loader = Zeitwerk::Loader.for_gem
40
+ loader.setup
22
41
 
23
42
  module Roast
24
43
  ROOT = File.expand_path("../..", __FILE__)
@@ -63,6 +82,32 @@ module Roast
63
82
  end
64
83
  end
65
84
 
85
+ desc "list", "List workflows visible to Roast and their source"
86
+ def list
87
+ roast_dir = File.join(Dir.pwd, "roast")
88
+
89
+ unless File.directory?(roast_dir)
90
+ raise Thor::Error, "No roast/ directory found in current path"
91
+ end
92
+
93
+ workflow_files = Dir.glob(File.join(roast_dir, "**/workflow.yml")).sort
94
+
95
+ if workflow_files.empty?
96
+ raise Thor::Error, "No workflow.yml files found in roast/ directory"
97
+ end
98
+
99
+ puts "Available workflows:"
100
+ puts
101
+
102
+ workflow_files.each do |file|
103
+ workflow_name = File.dirname(file.sub("#{roast_dir}/", ""))
104
+ puts " #{workflow_name} (from project)"
105
+ end
106
+
107
+ puts
108
+ puts "Run a workflow with: roast execute <workflow_name>"
109
+ end
110
+
66
111
  private
67
112
 
68
113
  def show_example_picker
data/roast.gemspec CHANGED
@@ -37,11 +37,12 @@ Gem::Specification.new do |spec|
37
37
  spec.require_paths = ["lib"]
38
38
 
39
39
  spec.add_dependency("activesupport", ">= 7.0")
40
- spec.add_dependency("cli-ui")
40
+ spec.add_dependency("cli-ui", "2.3.0")
41
41
  spec.add_dependency("diff-lcs", "~> 1.5")
42
42
  spec.add_dependency("faraday-retry")
43
43
  spec.add_dependency("json-schema")
44
44
  spec.add_dependency("open_router", "~> 0.3")
45
45
  spec.add_dependency("raix", "~> 1.0")
46
46
  spec.add_dependency("thor", "~> 1.3")
47
+ spec.add_dependency("zeitwerk", "~> 2.6")
47
48
  end
data/schema/workflow.json CHANGED
@@ -206,6 +206,52 @@
206
206
  }
207
207
  },
208
208
  "required": ["case", "when"]
209
+ },
210
+ {
211
+ "type": "object",
212
+ "properties": {
213
+ "input": {
214
+ "type": "object",
215
+ "properties": {
216
+ "prompt": {
217
+ "type": "string",
218
+ "description": "The prompt text to display to the user"
219
+ },
220
+ "name": {
221
+ "type": "string",
222
+ "description": "Optional name to store the input value in workflow state"
223
+ },
224
+ "type": {
225
+ "type": "string",
226
+ "enum": ["text", "boolean", "choice", "password"],
227
+ "default": "text",
228
+ "description": "The type of input to collect"
229
+ },
230
+ "required": {
231
+ "type": "boolean",
232
+ "default": false,
233
+ "description": "Whether the input is required"
234
+ },
235
+ "default": {
236
+ "description": "Default value for the input"
237
+ },
238
+ "timeout": {
239
+ "type": "number",
240
+ "description": "Optional timeout in seconds"
241
+ },
242
+ "options": {
243
+ "type": "array",
244
+ "items": {
245
+ "type": "string"
246
+ },
247
+ "description": "Options for choice type inputs"
248
+ }
249
+ },
250
+ "required": ["prompt"],
251
+ "additionalProperties": false
252
+ }
253
+ },
254
+ "required": ["input"]
209
255
  }
210
256
  ]
211
257
  }