roast-ai 0.2.1 → 0.2.2

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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yaml +5 -1
  3. data/.gitignore +29 -1
  4. data/CHANGELOG.md +27 -0
  5. data/CLAUDE.md +5 -0
  6. data/Gemfile.lock +3 -4
  7. data/README.md +162 -3
  8. data/examples/grading/generate_recommendations/output.txt +6 -6
  9. data/examples/pre_post_processing/README.md +111 -0
  10. data/examples/pre_post_processing/analyze_test_file/prompt.md +23 -0
  11. data/examples/pre_post_processing/improve_test_coverage/prompt.md +17 -0
  12. data/examples/pre_post_processing/optimize_test_performance/prompt.md +25 -0
  13. data/examples/pre_post_processing/post_processing/aggregate_metrics/prompt.md +31 -0
  14. data/examples/pre_post_processing/post_processing/cleanup_environment/prompt.md +28 -0
  15. data/examples/pre_post_processing/post_processing/generate_summary_report/prompt.md +32 -0
  16. data/examples/pre_post_processing/post_processing/output.txt +24 -0
  17. data/examples/pre_post_processing/pre_processing/gather_baseline_metrics/prompt.md +26 -0
  18. data/examples/pre_post_processing/pre_processing/setup_test_environment/prompt.md +11 -0
  19. data/examples/pre_post_processing/validate_changes/prompt.md +24 -0
  20. data/examples/pre_post_processing/workflow.yml +21 -0
  21. data/examples/single_target_prepost/README.md +36 -0
  22. data/examples/single_target_prepost/post_processing/output.txt +27 -0
  23. data/examples/single_target_prepost/pre_processing/gather_dependencies/prompt.md +11 -0
  24. data/examples/single_target_prepost/workflow.yml +20 -0
  25. data/gemfiles/activesupport7.gemfile +4 -0
  26. data/gemfiles/activesupport8.gemfile +4 -0
  27. data/lib/roast/tools/grep.rb +13 -4
  28. data/lib/roast/tools/search_file.rb +2 -2
  29. data/lib/roast/tools.rb +16 -1
  30. data/lib/roast/value_objects/uri_base.rb +49 -0
  31. data/lib/roast/value_objects.rb +1 -0
  32. data/lib/roast/version.rb +1 -1
  33. data/lib/roast/workflow/api_configuration.rb +9 -1
  34. data/lib/roast/workflow/base_workflow.rb +4 -1
  35. data/lib/roast/workflow/command_executor.rb +5 -2
  36. data/lib/roast/workflow/configuration.rb +4 -2
  37. data/lib/roast/workflow/configuration_loader.rb +14 -0
  38. data/lib/roast/workflow/error_handler.rb +18 -0
  39. data/lib/roast/workflow/expression_evaluator.rb +8 -0
  40. data/lib/roast/workflow/step_executor_coordinator.rb +34 -8
  41. data/lib/roast/workflow/step_loader.rb +15 -2
  42. data/lib/roast/workflow/workflow_execution_context.rb +39 -0
  43. data/lib/roast/workflow/workflow_executor.rb +22 -2
  44. data/lib/roast/workflow/workflow_initializer.rb +11 -2
  45. data/lib/roast/workflow/workflow_runner.rb +127 -5
  46. data/lib/roast/workflow.rb +1 -0
  47. data/lib/roast.rb +7 -1
  48. data/roast.gemspec +1 -1
  49. data/schema/workflow.json +14 -0
  50. metadata +25 -5
@@ -0,0 +1,11 @@
1
+ # Setup Test Environment
2
+
3
+ Prepare the test environment for optimization. Please:
4
+
5
+ 1. Ensure all test dependencies are installed
6
+ 2. Create a backup branch for safety: `test-optimization-backup-{{timestamp}}`
7
+ 3. Set up any necessary test databases or fixtures
8
+ 4. Configure test parallelization settings if available
9
+ 5. Clear any test caches that might affect benchmarking
10
+
11
+ Return a summary of the setup steps completed and any warnings or issues encountered.
@@ -0,0 +1,24 @@
1
+ # Validate Changes
2
+
3
+ Validate the changes made to {{file}}:
4
+
5
+ 1. **Run the updated tests** and ensure they all pass
6
+ 2. **Check coverage metrics** to verify improvements
7
+ 3. **Measure execution time** to confirm performance gains
8
+ 4. **Verify no regressions** were introduced
9
+ 5. **Ensure code style** follows project conventions
10
+
11
+ Store the validation results in the workflow state:
12
+ ```json
13
+ {
14
+ "file": "{{file}}",
15
+ "tests_passed": true,
16
+ "coverage_before": 0.0,
17
+ "coverage_after": 0.0,
18
+ "execution_time_before": 0.0,
19
+ "execution_time_after": 0.0,
20
+ "issues_found": []
21
+ }
22
+ ```
23
+
24
+ If any issues are found, provide recommendations for fixing them.
@@ -0,0 +1,21 @@
1
+ name: test_optimization
2
+ model: gpt-4o
3
+ target: "test/**/*_test.rb"
4
+
5
+ # Pre-processing steps run once before any test files are processed
6
+ pre_processing:
7
+ - gather_baseline_metrics
8
+ - setup_test_environment
9
+
10
+ # Main workflow steps run for each test file
11
+ steps:
12
+ - analyze_test_file
13
+ - improve_test_coverage
14
+ - optimize_test_performance
15
+ - validate_changes
16
+
17
+ # Post-processing steps run once after all test files have been processed
18
+ post_processing:
19
+ - aggregate_metrics
20
+ - generate_summary_report
21
+ - cleanup_environment
@@ -0,0 +1,36 @@
1
+ # Single Target with Pre/Post Processing Example
2
+
3
+ This example demonstrates how pre/post processing works with single-target workflows. Even when analyzing just one file, you can use pre-processing to gather context and post-processing to format results.
4
+
5
+ ## Features Demonstrated
6
+
7
+ 1. **Pre-processing for context gathering** - Analyze dependencies before the main workflow
8
+ 2. **Single-target analysis** - Focus on one specific file
9
+ 3. **Post-processing with output template** - Custom formatting of the final report
10
+
11
+ ## Running the Example
12
+
13
+ ```bash
14
+ roast workflow.yml
15
+ ```
16
+
17
+ This will:
18
+ 1. Run pre-processing to gather dependencies and context
19
+ 2. Analyze the single target file (src/main.rb)
20
+ 3. Apply the post-processing template to format the output
21
+
22
+ ## Output Template
23
+
24
+ The `post_processing/output.txt` template demonstrates how to:
25
+ - Access pre-processing results with `<%= pre_processing[:step_name] %>`
26
+ - Iterate over target results (even for single targets)
27
+ - Include post-processing step outputs
28
+ - Format everything into a professional report
29
+
30
+ ## Use Cases
31
+
32
+ This pattern is ideal for:
33
+ - Deep analysis of critical files
34
+ - Security audits of specific components
35
+ - Performance profiling of key modules
36
+ - Generating documentation for important classes
@@ -0,0 +1,27 @@
1
+ === Code Analysis Report ===
2
+ Generated: <%= Time.now.strftime("%Y-%m-%d %H:%M:%S") %>
3
+
4
+ ## Dependencies & Context
5
+ <%= pre_processing.gather_dependencies %>
6
+
7
+ ## Target File Analysis
8
+ <% targets.each do |file, target| %>
9
+ File: <%= file %>
10
+
11
+ ### Code Quality
12
+ <%= target.output.analyze_code_quality %>
13
+
14
+ ### Identified Improvements
15
+ <%= target.output.identify_improvements %>
16
+
17
+ ### Recommendations
18
+ <%= target.output.generate_recommendations %>
19
+ <% end %>
20
+
21
+ ## Summary
22
+ <%= output.summarize_findings %>
23
+
24
+ ## Action Items
25
+ <%= output.create_action_items %>
26
+
27
+ === End of Report ===
@@ -0,0 +1,11 @@
1
+ # Gather Dependencies
2
+
3
+ Analyze the project structure to understand the dependencies and context for <%= file %>.
4
+
5
+ Tasks:
6
+ 1. List all files that import or depend on the target file
7
+ 2. Identify the key modules and classes used
8
+ 3. Note any external dependencies or libraries
9
+ 4. Summarize the architectural context
10
+
11
+ Provide a structured summary that will help with the main analysis.
@@ -0,0 +1,20 @@
1
+ name: analyze_codebase
2
+ description: Analyze a single codebase with pre/post processing support
3
+ model: gpt-4o
4
+ target: "src/main.rb"
5
+
6
+ # Pre-processing: Gather context before analyzing the main file
7
+ pre_processing:
8
+ - gather_dependencies
9
+ - setup_analysis_tools
10
+
11
+ # Main workflow: Analyze the target file
12
+ steps:
13
+ - analyze_code_quality
14
+ - identify_improvements
15
+ - generate_recommendations
16
+
17
+ # Post-processing: Generate final report
18
+ post_processing:
19
+ - summarize_findings
20
+ - create_action_items
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ eval_gemfile "../Gemfile"
4
+ gem "activesupport", "~> 7.0"
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ eval_gemfile "../Gemfile"
4
+ gem "activesupport", "~> 8.0"
@@ -28,10 +28,19 @@ module Roast
28
28
 
29
29
  def call(string)
30
30
  Roast::Helpers::Logger.info("🔍 Grepping for string: #{string}\n")
31
- # Escape regex special characters in strings with curly braces
32
- # Example: "import {render}" becomes "import \{render\}"
33
- escaped_string = string.gsub(/(\{|\})/, '\\\\\\1')
34
- %x(rg -C 4 --trim --color=never --heading -F -- "#{escaped_string}" . | head -n #{MAX_RESULT_LINES})
31
+
32
+ # Use Open3 to safely pass the string as an argument, avoiding shell injection
33
+ require "open3"
34
+ cmd = ["rg", "-C", "4", "--trim", "--color=never", "--heading", "-F", "--", string, "."]
35
+ stdout, _stderr, _status = Open3.capture3(*cmd)
36
+
37
+ # Limit output to MAX_RESULT_LINES
38
+ lines = stdout.lines
39
+ if lines.size > MAX_RESULT_LINES
40
+ lines.first(MAX_RESULT_LINES).join + "\n... (truncated to #{MAX_RESULT_LINES} lines)"
41
+ else
42
+ stdout
43
+ end
35
44
  rescue StandardError => e
36
45
  "Error grepping for string: #{e.message}".tap do |error_message|
37
46
  Roast::Helpers::Logger.error(error_message + "\n")
@@ -29,9 +29,9 @@ module Roast
29
29
  Roast::Helpers::Logger.info("🔍 Searching for: '#{glob_pattern}' in '#{File.expand_path(path)}'\n")
30
30
  search_for(glob_pattern, path).then do |results|
31
31
  return "No results found for #{glob_pattern} in #{path}" if results.empty?
32
- return read_contents(results.first) if results.size == 1
32
+ return read_contents(File.join(path, results.first)) if results.size == 1
33
33
 
34
- results.join("\n") # purposely give the AI list of actual paths so that it can read without searching first
34
+ results.map { |result| File.join(path, result) }.join("\n") # purposely give the AI list of actual paths so that it can read without searching first
35
35
  end
36
36
  rescue StandardError => e
37
37
  "Error searching for '#{glob_pattern}' in '#{path}': #{e.message}".tap do |error_message|
data/lib/roast/tools.rb CHANGED
@@ -52,7 +52,22 @@ module Roast
52
52
  # Hook that runs on any exit (including crashes and unhandled exceptions)
53
53
  at_exit do
54
54
  if $ERROR_INFO && !$ERROR_INFO.is_a?(SystemExit) # If exiting due to unhandled exception
55
- puts "\n\nExiting due to error: #{$ERROR_INFO.class}: #{$ERROR_INFO.message}\n"
55
+ # Print a more user-friendly message based on the error type
56
+ case $ERROR_INFO
57
+ when Roast::Workflow::CommandExecutor::CommandExecutionError
58
+ puts "\n\n🛑 Workflow stopped due to command failure."
59
+ puts " To continue execution despite command failures, you can:"
60
+ puts " - Fix the failing command"
61
+ puts " - Run with --verbose to see command output"
62
+ puts " - Modify your workflow to handle errors gracefully"
63
+ when Roast::Workflow::WorkflowExecutor::StepExecutionError
64
+ puts "\n\n🛑 Workflow stopped due to step failure."
65
+ puts " Check the error message above for details."
66
+ else
67
+ puts "\n\n🛑 Workflow stopped due to an unexpected error:"
68
+ puts " #{$ERROR_INFO.class}: #{$ERROR_INFO.message}"
69
+ end
70
+ puts "\nFor debugging, you can run with --verbose for more details."
56
71
  # Temporary disable the debugger to fix directory issues
57
72
  # context.instance_eval { binding.irb }
58
73
  end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Roast
4
+ module ValueObjects
5
+ # Value object representing a URI base with validation
6
+ class UriBase
7
+ class InvalidUriBaseError < StandardError; end
8
+
9
+ attr_reader :value
10
+
11
+ def initialize(value)
12
+ @value = value&.to_s
13
+ validate!
14
+ freeze
15
+ end
16
+
17
+ def present?
18
+ !blank?
19
+ end
20
+
21
+ def blank?
22
+ @value.nil? || @value.strip.empty?
23
+ end
24
+
25
+ def to_s
26
+ @value
27
+ end
28
+
29
+ def ==(other)
30
+ return false unless other.is_a?(UriBase)
31
+
32
+ value == other.value
33
+ end
34
+ alias_method :eql?, :==
35
+
36
+ def hash
37
+ [self.class, @value].hash
38
+ end
39
+
40
+ private
41
+
42
+ def validate!
43
+ return if @value.nil? # Allow nil URI base, just not empty strings
44
+
45
+ raise InvalidUriBaseError, "URI base cannot be an empty string" if @value.strip.empty?
46
+ end
47
+ end
48
+ end
49
+ end
@@ -3,3 +3,4 @@
3
3
  require "roast/value_objects/api_token"
4
4
  require "roast/value_objects/step_name"
5
5
  require "roast/value_objects/workflow_path"
6
+ require "roast/value_objects/uri_base"
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.2.1"
4
+ VERSION = "0.2.2"
5
5
  end
@@ -2,12 +2,13 @@
2
2
 
3
3
  require "roast/factories/api_provider_factory"
4
4
  require "roast/workflow/resource_resolver"
5
+ require "roast/value_objects/uri_base"
5
6
 
6
7
  module Roast
7
8
  module Workflow
8
9
  # Handles API-related configuration including tokens and providers
9
10
  class ApiConfiguration
10
- attr_reader :api_token, :api_provider
11
+ attr_reader :api_token, :api_provider, :uri_base
11
12
 
12
13
  def initialize(config_hash)
13
14
  @config_hash = config_hash
@@ -37,6 +38,7 @@ module Roast
37
38
  def process_api_configuration
38
39
  extract_api_token
39
40
  extract_api_provider
41
+ extract_uri_base
40
42
  end
41
43
 
42
44
  def extract_api_token
@@ -49,6 +51,12 @@ module Roast
49
51
  @api_provider = Roast::Factories::ApiProviderFactory.from_config(@config_hash)
50
52
  end
51
53
 
54
+ def extract_uri_base
55
+ if @config_hash["uri_base"]
56
+ @uri_base = ResourceResolver.process_shell_command(@config_hash["uri_base"])
57
+ end
58
+ end
59
+
52
60
  def environment_token
53
61
  if openai?
54
62
  ENV["OPENAI_API_KEY"]
@@ -27,10 +27,12 @@ module Roast
27
27
  :configuration,
28
28
  :model
29
29
 
30
+ attr_reader :pre_processing_data
31
+
30
32
  delegate :api_provider, :openai?, to: :configuration
31
33
  delegate :output, :output=, :append_to_final_output, :final_output, to: :output_manager
32
34
 
33
- def initialize(file = nil, name: nil, context_path: nil, resource: nil, session_name: nil, configuration: nil)
35
+ def initialize(file = nil, name: nil, context_path: nil, resource: nil, session_name: nil, configuration: nil, pre_processing_data: nil)
34
36
  @file = file
35
37
  @name = name || self.class.name.underscore.split("/").last
36
38
  @context_path = context_path || ContextPathResolver.resolve(self.class)
@@ -38,6 +40,7 @@ module Roast
38
40
  @session_name = session_name || @name
39
41
  @session_timestamp = nil
40
42
  @configuration = configuration
43
+ @pre_processing_data = pre_processing_data ? DotAccessHash.new(pre_processing_data).freeze : nil
41
44
 
42
45
  # Initialize managers
43
46
  @output_manager = OutputManager.new
@@ -6,7 +6,7 @@ module Roast
6
6
  module Workflow
7
7
  class CommandExecutor
8
8
  class CommandExecutionError < StandardError
9
- attr_reader :command, :exit_status, :original_error
9
+ attr_reader :command, :exit_status, :original_error, :output
10
10
 
11
11
  def initialize(message, command:, exit_status: nil, original_error: nil)
12
12
  @command = command
@@ -56,11 +56,14 @@ module Roast
56
56
  return output if success
57
57
 
58
58
  if exit_on_error
59
- raise CommandExecutionError.new(
59
+ error = CommandExecutionError.new(
60
60
  "Command exited with non-zero status (#{exit_status})",
61
61
  command: command,
62
62
  exit_status: exit_status,
63
63
  )
64
+ # Store the output in the error
65
+ error.instance_variable_set(:@output, output)
66
+ raise error
64
67
  else
65
68
  @logger.warn("Command '#{command}' exited with non-zero status (#{exit_status}), continuing execution")
66
69
  output + "\n[Exit status: #{exit_status}]"
@@ -11,10 +11,10 @@ module Roast
11
11
  # Encapsulates workflow configuration data and provides structured access
12
12
  # to the configuration settings
13
13
  class Configuration
14
- attr_reader :config_hash, :workflow_path, :name, :steps, :tools, :function_configs, :model, :resource
14
+ attr_reader :config_hash, :workflow_path, :name, :steps, :pre_processing, :post_processing, :tools, :function_configs, :model, :resource
15
15
  attr_accessor :target
16
16
 
17
- delegate :api_provider, :openrouter?, :openai?, to: :api_configuration
17
+ delegate :api_provider, :openrouter?, :openai?, :uri_base, to: :api_configuration
18
18
 
19
19
  # Delegate api_token to effective_token for backward compatibility
20
20
  def api_token
@@ -30,6 +30,8 @@ module Roast
30
30
  # Extract basic configuration values
31
31
  @name = ConfigurationLoader.extract_name(@config_hash, workflow_path)
32
32
  @steps = ConfigurationLoader.extract_steps(@config_hash)
33
+ @pre_processing = ConfigurationLoader.extract_pre_processing(@config_hash)
34
+ @post_processing = ConfigurationLoader.extract_post_processing(@config_hash)
33
35
  @tools = ConfigurationLoader.extract_tools(@config_hash)
34
36
  @function_configs = ConfigurationLoader.extract_functions(@config_hash)
35
37
  @model = ConfigurationLoader.extract_model(@config_hash)
@@ -32,6 +32,20 @@ module Roast
32
32
  config_hash["steps"] || []
33
33
  end
34
34
 
35
+ # Extract pre-processing steps from the configuration
36
+ # @param config_hash [Hash] The configuration hash
37
+ # @return [Array] The pre_processing array or empty array
38
+ def extract_pre_processing(config_hash)
39
+ config_hash["pre_processing"] || []
40
+ end
41
+
42
+ # Extract post-processing steps from the configuration
43
+ # @param config_hash [Hash] The configuration hash
44
+ # @return [Array] The post_processing array or empty array
45
+ def extract_post_processing(config_hash)
46
+ config_hash["post_processing"] || []
47
+ end
48
+
35
49
  # Extract tools from the configuration
36
50
  # @param config_hash [Hash] The configuration hash
37
51
  # @return [Array] The tools array or empty array
@@ -85,6 +85,24 @@ module Roast
85
85
  execution_time: execution_time,
86
86
  })
87
87
 
88
+ # Print user-friendly error message based on error type
89
+ case error
90
+ when StepLoader::StepNotFoundError
91
+ $stderr.puts "\n❌ Step not found: '#{step_name}'"
92
+ $stderr.puts " Please check that the step exists in your workflow's steps directory."
93
+ $stderr.puts " Looking for: steps/#{step_name}.rb or steps/#{step_name}/prompt.md"
94
+ when NoMethodError
95
+ if error.message.include?("undefined method")
96
+ $stderr.puts "\n❌ Step error: '#{step_name}'"
97
+ $stderr.puts " The step file exists but may be missing the 'call' method."
98
+ $stderr.puts " Error: #{error.message}"
99
+ end
100
+ else
101
+ $stderr.puts "\n❌ Step failed: '#{step_name}'"
102
+ $stderr.puts " Error: #{error.message}"
103
+ $stderr.puts " This may be an issue with the step's implementation."
104
+ end
105
+
88
106
  # Wrap the original error with context about which step failed
89
107
  raise WorkflowExecutor::StepExecutionError.new(
90
108
  "Failed to execute step '#{step_name}': #{error.message}",
@@ -33,6 +33,14 @@ module Roast
33
33
  begin
34
34
  output = executor.execute(cmd, exit_on_error: false)
35
35
 
36
+ # Print command output in verbose mode
37
+ if @workflow.verbose
38
+ $stderr.puts "Evaluating command: #{cmd}"
39
+ $stderr.puts "Command output:"
40
+ $stderr.puts output
41
+ $stderr.puts
42
+ end
43
+
36
44
  if for_condition
37
45
  # For conditions, we care about the exit status (success = true)
38
46
  # Check if output contains exit status marker
@@ -132,16 +132,42 @@ module Roast
132
132
  error_handler.with_error_handling(step, resource_type: resource_type) do
133
133
  $stderr.puts "Executing: #{step} (Resource type: #{resource_type || "unknown"})"
134
134
 
135
- output = command_executor.execute(step, exit_on_error: exit_on_error)
135
+ begin
136
+ output = command_executor.execute(step, exit_on_error: exit_on_error)
137
+
138
+ # Print command output in verbose mode
139
+ workflow = context.workflow
140
+ if workflow.verbose
141
+ $stderr.puts "Command output:"
142
+ $stderr.puts output
143
+ $stderr.puts
144
+ end
136
145
 
137
- # Add to transcript
138
- workflow = context.workflow
139
- workflow.transcript << {
140
- user: "I just executed the following command: ```\n#{step}\n```\n\nHere is the output:\n\n```\n#{output}\n```",
141
- }
142
- workflow.transcript << { assistant: "Noted, thank you." }
146
+ # Add to transcript
147
+ workflow.transcript << {
148
+ user: "I just executed the following command: ```\n#{step}\n```\n\nHere is the output:\n\n```\n#{output}\n```",
149
+ }
150
+ workflow.transcript << { assistant: "Noted, thank you." }
151
+
152
+ output
153
+ rescue CommandExecutor::CommandExecutionError => e
154
+ # Print user-friendly error message
155
+ $stderr.puts "\n❌ Command failed: #{step}"
156
+ $stderr.puts " Exit status: #{e.exit_status}" if e.exit_status
157
+
158
+ # Show command output if available
159
+ if e.respond_to?(:output) && e.output && !e.output.strip.empty?
160
+ $stderr.puts " Command output:"
161
+ e.output.strip.split("\n").each do |line|
162
+ $stderr.puts " #{line}"
163
+ end
164
+ elsif workflow && !workflow.verbose
165
+ $stderr.puts " To see the command output, run with --verbose flag."
166
+ end
143
167
 
144
- output
168
+ $stderr.puts " This typically means the command returned an error.\n"
169
+ raise
170
+ end
145
171
  end
146
172
  end
147
173
 
@@ -25,11 +25,11 @@ module Roast
25
25
  class StepNotFoundError < StepLoaderError; end
26
26
  class StepExecutionError < StepLoaderError; end
27
27
 
28
- attr_reader :context
28
+ attr_reader :context, :phase
29
29
 
30
30
  delegate :workflow, :config_hash, :context_path, to: :context
31
31
 
32
- def initialize(workflow, config_hash, context_path)
32
+ def initialize(workflow, config_hash, context_path, phase: :steps)
33
33
  # Support both old and new initialization patterns
34
34
  @context = if workflow.is_a?(WorkflowContext)
35
35
  workflow
@@ -40,6 +40,7 @@ module Roast
40
40
  context_path: context_path,
41
41
  )
42
42
  end
43
+ @phase = phase
43
44
  end
44
45
 
45
46
  # Finds and loads a step by name
@@ -75,6 +76,12 @@ module Roast
75
76
 
76
77
  # Find a Ruby step file in various locations
77
78
  def find_step_file(step_name)
79
+ # Check in phase-specific directory first
80
+ if phase != :steps
81
+ phase_rb_path = File.join(context_path, phase.to_s, "#{step_name}.rb")
82
+ return phase_rb_path if File.file?(phase_rb_path)
83
+ end
84
+
78
85
  # Check in context path
79
86
  rb_file_path = File.join(context_path, "#{step_name}.rb")
80
87
  return rb_file_path if File.file?(rb_file_path)
@@ -88,6 +95,12 @@ module Roast
88
95
 
89
96
  # Find a step directory
90
97
  def find_step_directory(step_name)
98
+ # Check in phase-specific directory first
99
+ if phase != :steps
100
+ phase_step_path = File.join(context_path, phase.to_s, step_name)
101
+ return phase_step_path if File.directory?(phase_step_path)
102
+ end
103
+
91
104
  # Check in context path
92
105
  step_path = File.join(context_path, step_name)
93
106
  return step_path if File.directory?(step_path)
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/string/inflections"
4
+
5
+ module Roast
6
+ module Workflow
7
+ # Manages execution context across pre-processing, target workflows, and post-processing phases
8
+ class WorkflowExecutionContext
9
+ attr_reader :pre_processing_output, :target_outputs
10
+
11
+ def initialize
12
+ @pre_processing_output = OutputManager.new
13
+ @target_outputs = {}
14
+ end
15
+
16
+ # Add output from a target workflow execution
17
+ def add_target_output(target, output_manager)
18
+ target_key = generate_target_key(target)
19
+ @target_outputs[target_key] = output_manager
20
+ end
21
+
22
+ # Get all data as a hash for post-processing
23
+ def to_h
24
+ {
25
+ pre_processing: @pre_processing_output.to_h,
26
+ targets: @target_outputs.transform_values(&:to_h),
27
+ }
28
+ end
29
+
30
+ private
31
+
32
+ def generate_target_key(target)
33
+ return "default" unless target
34
+
35
+ target.to_s.parameterize
36
+ end
37
+ end
38
+ end
39
+ end
@@ -48,10 +48,30 @@ module Roast
48
48
 
49
49
  delegate :workflow, :config_hash, :context_path, to: :context
50
50
 
51
+ # Initialize a new WorkflowExecutor
52
+ #
53
+ # @param workflow [BaseWorkflow] The workflow instance to execute
54
+ # @param config_hash [Hash] The workflow configuration
55
+ # @param context_path [String] The base path for the workflow
56
+ # @param error_handler [ErrorHandler] Optional custom error handler
57
+ # @param step_loader [StepLoader] Optional custom step loader
58
+ # @param command_executor [CommandExecutor] Optional custom command executor
59
+ # @param interpolator [Interpolator] Optional custom interpolator
60
+ # @param state_manager [StateManager] Optional custom state manager
61
+ # @param iteration_executor [IterationExecutor] Optional custom iteration executor
62
+ # @param conditional_executor [ConditionalExecutor] Optional custom conditional executor
63
+ # @param step_orchestrator [StepOrchestrator] Optional custom step orchestrator
64
+ # @param step_executor_coordinator [StepExecutorCoordinator] Optional custom step executor coordinator
65
+ # @param phase [Symbol] The execution phase - determines where to load steps from
66
+ # Valid values:
67
+ # - :steps (default) - Load steps from the main steps directory
68
+ # - :pre_processing - Load steps from the pre_processing directory
69
+ # - :post_processing - Load steps from the post_processing directory
51
70
  def initialize(workflow, config_hash, context_path,
52
71
  error_handler: nil, step_loader: nil, command_executor: nil,
53
72
  interpolator: nil, state_manager: nil, iteration_executor: nil,
54
- conditional_executor: nil, step_orchestrator: nil, step_executor_coordinator: nil)
73
+ conditional_executor: nil, step_orchestrator: nil, step_executor_coordinator: nil,
74
+ phase: :steps)
55
75
  # Create context object to reduce data clump
56
76
  @context = WorkflowContext.new(
57
77
  workflow: workflow,
@@ -61,7 +81,7 @@ module Roast
61
81
 
62
82
  # Dependencies with defaults
63
83
  @error_handler = error_handler || ErrorHandler.new
64
- @step_loader = step_loader || StepLoader.new(workflow, config_hash, context_path)
84
+ @step_loader = step_loader || StepLoader.new(workflow, config_hash, context_path, phase: phase)
65
85
  @command_executor = command_executor || CommandExecutor.new(logger: @error_handler)
66
86
  @interpolator = interpolator || Interpolator.new(workflow, logger: @error_handler)
67
87
  @state_manager = state_manager || StateManager.new(workflow, logger: @error_handler)