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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yaml +5 -1
- data/.gitignore +29 -1
- data/CHANGELOG.md +27 -0
- data/CLAUDE.md +5 -0
- data/Gemfile.lock +3 -4
- data/README.md +162 -3
- data/examples/grading/generate_recommendations/output.txt +6 -6
- data/examples/pre_post_processing/README.md +111 -0
- data/examples/pre_post_processing/analyze_test_file/prompt.md +23 -0
- data/examples/pre_post_processing/improve_test_coverage/prompt.md +17 -0
- data/examples/pre_post_processing/optimize_test_performance/prompt.md +25 -0
- data/examples/pre_post_processing/post_processing/aggregate_metrics/prompt.md +31 -0
- data/examples/pre_post_processing/post_processing/cleanup_environment/prompt.md +28 -0
- data/examples/pre_post_processing/post_processing/generate_summary_report/prompt.md +32 -0
- data/examples/pre_post_processing/post_processing/output.txt +24 -0
- data/examples/pre_post_processing/pre_processing/gather_baseline_metrics/prompt.md +26 -0
- data/examples/pre_post_processing/pre_processing/setup_test_environment/prompt.md +11 -0
- data/examples/pre_post_processing/validate_changes/prompt.md +24 -0
- data/examples/pre_post_processing/workflow.yml +21 -0
- data/examples/single_target_prepost/README.md +36 -0
- data/examples/single_target_prepost/post_processing/output.txt +27 -0
- data/examples/single_target_prepost/pre_processing/gather_dependencies/prompt.md +11 -0
- data/examples/single_target_prepost/workflow.yml +20 -0
- data/gemfiles/activesupport7.gemfile +4 -0
- data/gemfiles/activesupport8.gemfile +4 -0
- data/lib/roast/tools/grep.rb +13 -4
- data/lib/roast/tools/search_file.rb +2 -2
- data/lib/roast/tools.rb +16 -1
- data/lib/roast/value_objects/uri_base.rb +49 -0
- data/lib/roast/value_objects.rb +1 -0
- data/lib/roast/version.rb +1 -1
- data/lib/roast/workflow/api_configuration.rb +9 -1
- data/lib/roast/workflow/base_workflow.rb +4 -1
- data/lib/roast/workflow/command_executor.rb +5 -2
- data/lib/roast/workflow/configuration.rb +4 -2
- data/lib/roast/workflow/configuration_loader.rb +14 -0
- data/lib/roast/workflow/error_handler.rb +18 -0
- data/lib/roast/workflow/expression_evaluator.rb +8 -0
- data/lib/roast/workflow/step_executor_coordinator.rb +34 -8
- data/lib/roast/workflow/step_loader.rb +15 -2
- data/lib/roast/workflow/workflow_execution_context.rb +39 -0
- data/lib/roast/workflow/workflow_executor.rb +22 -2
- data/lib/roast/workflow/workflow_initializer.rb +11 -2
- data/lib/roast/workflow/workflow_runner.rb +127 -5
- data/lib/roast/workflow.rb +1 -0
- data/lib/roast.rb +7 -1
- data/roast.gemspec +1 -1
- data/schema/workflow.json +14 -0
- 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
|
data/lib/roast/tools/grep.rb
CHANGED
@@ -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
|
-
|
32
|
-
#
|
33
|
-
|
34
|
-
|
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
|
-
|
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
|
data/lib/roast/value_objects.rb
CHANGED
data/lib/roast/version.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
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
|
-
|
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)
|