roast-ai 0.4.0 → 0.4.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 (125) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yaml +2 -2
  3. data/.gitignore +1 -0
  4. data/CHANGELOG.md +103 -0
  5. data/CLAUDE.md +55 -9
  6. data/Gemfile.lock +19 -10
  7. data/README.md +69 -3
  8. data/bin/console +1 -0
  9. data/docs/AGENT_STEPS.md +33 -9
  10. data/docs/VALIDATION.md +178 -0
  11. data/examples/agent_continue/add_documentation/prompt.md +5 -0
  12. data/examples/agent_continue/add_error_handling/prompt.md +5 -0
  13. data/examples/agent_continue/analyze_codebase/prompt.md +7 -0
  14. data/examples/agent_continue/combined_workflow.yml +24 -0
  15. data/examples/agent_continue/continue_adding_features/prompt.md +4 -0
  16. data/examples/agent_continue/create_integration_tests/prompt.md +3 -0
  17. data/examples/agent_continue/document_with_context/prompt.md +5 -0
  18. data/examples/agent_continue/explore_api/prompt.md +6 -0
  19. data/examples/agent_continue/implement_client/prompt.md +6 -0
  20. data/examples/agent_continue/inline_workflow.yml +20 -0
  21. data/examples/agent_continue/refactor_code/prompt.md +2 -0
  22. data/examples/agent_continue/verify_changes/prompt.md +6 -0
  23. data/examples/agent_continue/workflow.yml +27 -0
  24. data/examples/agent_workflow/workflow.png +0 -0
  25. data/examples/api_workflow/workflow.png +0 -0
  26. data/examples/apply_diff_demo/README.md +58 -0
  27. data/examples/apply_diff_demo/apply_simple_change/prompt.md +13 -0
  28. data/examples/apply_diff_demo/create_sample_file/prompt.md +11 -0
  29. data/examples/apply_diff_demo/workflow.yml +24 -0
  30. data/examples/available_tools_demo/workflow.png +0 -0
  31. data/examples/bash_prototyping/api_testing.png +0 -0
  32. data/examples/bash_prototyping/system_analysis.png +0 -0
  33. data/examples/case_when/workflow.png +0 -0
  34. data/examples/cmd/basic_workflow.png +0 -0
  35. data/examples/cmd/dev_workflow.png +0 -0
  36. data/examples/cmd/explorer_workflow.png +0 -0
  37. data/examples/conditional/simple_workflow.png +0 -0
  38. data/examples/conditional/workflow.png +0 -0
  39. data/examples/context_management_demo/README.md +43 -0
  40. data/examples/context_management_demo/workflow.yml +42 -0
  41. data/examples/direct_coerce_syntax/workflow.png +0 -0
  42. data/examples/dot_notation/workflow.png +0 -0
  43. data/examples/exit_on_error/workflow.png +0 -0
  44. data/examples/grading/rb_test_runner +1 -1
  45. data/examples/grading/workflow.png +0 -0
  46. data/examples/interpolation/workflow.png +0 -0
  47. data/examples/interpolation/workflow.yml +1 -1
  48. data/examples/iteration/workflow.png +0 -0
  49. data/examples/json_handling/workflow.png +0 -0
  50. data/examples/mcp/database_workflow.png +0 -0
  51. data/examples/mcp/env_demo/workflow.png +0 -0
  52. data/examples/mcp/filesystem_demo/workflow.png +0 -0
  53. data/examples/mcp/github_workflow.png +0 -0
  54. data/examples/mcp/multi_mcp_workflow.png +0 -0
  55. data/examples/mcp/workflow.png +0 -0
  56. data/examples/no_model_fallback/README.md +17 -0
  57. data/examples/no_model_fallback/analyze_file/prompt.md +1 -0
  58. data/examples/no_model_fallback/analyze_patterns/prompt.md +27 -0
  59. data/examples/no_model_fallback/generate_report_for_md/prompt.md +10 -0
  60. data/examples/no_model_fallback/generate_report_for_rb/prompt.md +3 -0
  61. data/examples/no_model_fallback/sample.rb +42 -0
  62. data/examples/no_model_fallback/workflow.yml +19 -0
  63. data/examples/openrouter_example/workflow.png +0 -0
  64. data/examples/pre_post_processing/workflow.png +0 -0
  65. data/examples/rspec_to_minitest/workflow.png +0 -0
  66. data/examples/shared_config/example_with_shared_config/workflow.png +0 -0
  67. data/examples/shared_config/shared.png +0 -0
  68. data/examples/single_target_prepost/workflow.png +0 -0
  69. data/examples/smart_coercion_defaults/workflow.png +0 -0
  70. data/examples/step_configuration/workflow.png +0 -0
  71. data/examples/swarm_example.yml +25 -0
  72. data/examples/tool_config_example/workflow.png +0 -0
  73. data/examples/user_input/funny_name/workflow.png +0 -0
  74. data/examples/user_input/simple_input_demo/workflow.png +0 -0
  75. data/examples/user_input/survey_workflow.png +0 -0
  76. data/examples/user_input/workflow.png +0 -0
  77. data/examples/workflow_generator/workflow.png +0 -0
  78. data/lib/roast/errors.rb +3 -0
  79. data/lib/roast/helpers/timeout_handler.rb +91 -0
  80. data/lib/roast/services/context_threshold_checker.rb +42 -0
  81. data/lib/roast/services/token_counting_service.rb +44 -0
  82. data/lib/roast/tools/apply_diff.rb +128 -0
  83. data/lib/roast/tools/bash.rb +15 -9
  84. data/lib/roast/tools/cmd.rb +32 -12
  85. data/lib/roast/tools/coding_agent.rb +65 -10
  86. data/lib/roast/tools/context_summarizer.rb +108 -0
  87. data/lib/roast/tools/swarm.rb +124 -0
  88. data/lib/roast/version.rb +1 -1
  89. data/lib/roast/workflow/agent_step.rb +9 -2
  90. data/lib/roast/workflow/base_iteration_step.rb +3 -2
  91. data/lib/roast/workflow/base_workflow.rb +41 -2
  92. data/lib/roast/workflow/command_executor.rb +3 -1
  93. data/lib/roast/workflow/configuration.rb +2 -1
  94. data/lib/roast/workflow/configuration_loader.rb +63 -1
  95. data/lib/roast/workflow/configuration_parser.rb +2 -0
  96. data/lib/roast/workflow/context_manager.rb +89 -0
  97. data/lib/roast/workflow/each_step.rb +1 -1
  98. data/lib/roast/workflow/input_step.rb +2 -0
  99. data/lib/roast/workflow/interpolator.rb +23 -1
  100. data/lib/roast/workflow/output_handler.rb +1 -1
  101. data/lib/roast/workflow/repeat_step.rb +1 -1
  102. data/lib/roast/workflow/replay_handler.rb +1 -1
  103. data/lib/roast/workflow/sqlite_state_repository.rb +342 -0
  104. data/lib/roast/workflow/state_manager.rb +2 -2
  105. data/lib/roast/workflow/state_repository_factory.rb +36 -0
  106. data/lib/roast/workflow/step_completion_reporter.rb +27 -0
  107. data/lib/roast/workflow/step_executor_coordinator.rb +19 -18
  108. data/lib/roast/workflow/step_executor_with_reporting.rb +68 -0
  109. data/lib/roast/workflow/step_loader.rb +1 -1
  110. data/lib/roast/workflow/step_name_extractor.rb +84 -0
  111. data/lib/roast/workflow/validation_command.rb +197 -0
  112. data/lib/roast/workflow/validators/base_validator.rb +44 -0
  113. data/lib/roast/workflow/validators/dependency_validator.rb +223 -0
  114. data/lib/roast/workflow/validators/linting_validator.rb +113 -0
  115. data/lib/roast/workflow/validators/schema_validator.rb +90 -0
  116. data/lib/roast/workflow/validators/step_collector.rb +57 -0
  117. data/lib/roast/workflow/validators/validation_orchestrator.rb +52 -0
  118. data/lib/roast/workflow/workflow_executor.rb +11 -4
  119. data/lib/roast/workflow/workflow_initializer.rb +80 -0
  120. data/lib/roast/workflow/workflow_runner.rb +6 -0
  121. data/lib/roast/workflow_diagram_generator.rb +298 -0
  122. data/lib/roast.rb +158 -0
  123. data/roast.gemspec +4 -1
  124. data/schema/workflow.json +77 -1
  125. metadata +129 -1
@@ -0,0 +1,223 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Roast
4
+ module Workflow
5
+ module Validators
6
+ # Validates dependencies: tools, step references, and resources
7
+ class DependencyValidator < BaseValidator
8
+ def initialize(parsed_yaml, workflow_path = nil, step_collector: nil)
9
+ super(parsed_yaml, workflow_path)
10
+ @step_collector = step_collector || StepCollector.new(parsed_yaml)
11
+ end
12
+
13
+ def validate
14
+ validate_tool_dependencies
15
+ validate_step_references
16
+ validate_resource_dependencies
17
+ end
18
+
19
+ private
20
+
21
+ def validate_tool_dependencies
22
+ return unless @parsed_yaml["tools"]
23
+
24
+ tools = extract_all_tools(@parsed_yaml["tools"])
25
+
26
+ tools.each do |tool|
27
+ next if tool_available?(tool)
28
+
29
+ add_error(
30
+ type: :tool_dependency,
31
+ tool: tool,
32
+ message: "Tool '#{tool}' is not available",
33
+ suggestion: suggest_tool_fix(tool),
34
+ )
35
+ end
36
+ end
37
+
38
+ def validate_step_references
39
+ all_steps = @step_collector.all_steps
40
+ step_names = all_steps.map { |s| extract_step_name(s) }.compact.uniq
41
+
42
+ check_step_references_in_config(@parsed_yaml, step_names)
43
+ end
44
+
45
+ def validate_resource_dependencies
46
+ # Validate file resources if target is specified
47
+ if @parsed_yaml["target"] && @workflow_path
48
+ validate_target_resource(@parsed_yaml["target"])
49
+ end
50
+
51
+ # Validate prompt files exist
52
+ validate_prompt_files if @workflow_path
53
+ end
54
+
55
+ def tool_available?(tool_name)
56
+ # Check if it's an MCP tool first
57
+ tools_config = @parsed_yaml["tools"] || []
58
+ tools_config.each do |tool_entry|
59
+ if tool_entry.is_a?(Hash) && tool_entry.keys.include?(tool_name)
60
+ # It's an MCP tool configuration
61
+ return true
62
+ end
63
+ end
64
+
65
+ # Check if tool module exists
66
+ begin
67
+ tool_name.constantize
68
+ true
69
+ rescue NameError
70
+ false
71
+ end
72
+ end
73
+
74
+ def suggest_tool_fix(tool)
75
+ # Suggest similar tools or common fixes
76
+ available_tools = [
77
+ "Roast::Tools::Bash",
78
+ "Roast::Tools::Cmd",
79
+ "Roast::Tools::ReadFile",
80
+ "Roast::Tools::WriteFile",
81
+ "Roast::Tools::UpdateFiles",
82
+ "Roast::Tools::SearchFile",
83
+ "Roast::Tools::Grep",
84
+ "Roast::Tools::AskUser",
85
+ "Roast::Tools::CodingAgent",
86
+ ]
87
+
88
+ # Simple similarity check
89
+ tool_base = tool.split("::").last&.downcase || tool.downcase
90
+ suggestions = available_tools.select do |t|
91
+ t_base = t.split("::").last&.downcase || ""
92
+ t_base.include?(tool_base) || tool_base.include?(t_base)
93
+ end
94
+
95
+ if suggestions.any?
96
+ "Did you mean: #{suggestions.join(", ")}?"
97
+ else
98
+ "Ensure the tool module exists or check the tool name spelling"
99
+ end
100
+ end
101
+
102
+ def extract_all_tools(tools_config)
103
+ tools = []
104
+ tools_config.each do |tool_entry|
105
+ case tool_entry
106
+ when String
107
+ tools << tool_entry
108
+ when Hash
109
+ tool_entry.each_key do |tool_name|
110
+ tools << tool_name
111
+ end
112
+ end
113
+ end
114
+ tools
115
+ end
116
+
117
+ def extract_step_name(step)
118
+ case step
119
+ when String
120
+ step
121
+ when Hash
122
+ # Complex step types don't have simple names
123
+ nil
124
+ end
125
+ end
126
+
127
+ def check_step_references_in_config(config, valid_step_names)
128
+ # Check steps array
129
+ ["steps", "pre_processing", "post_processing"].each do |key|
130
+ if config[key].is_a?(Array)
131
+ check_step_references(config[key], valid_step_names)
132
+ end
133
+ end
134
+ end
135
+
136
+ def check_step_references(steps, valid_step_names, path = [])
137
+ steps.each_with_index do |step, index|
138
+ current_path = path + [index]
139
+
140
+ case step
141
+ when Hash
142
+ # Check conditions that might reference steps
143
+ ["if", "unless", "case"].each do |condition_key|
144
+ next unless step[condition_key]
145
+
146
+ condition = step[condition_key]
147
+ next unless condition.is_a?(String) && !condition.include?("{{") && !condition.include?("$(")
148
+
149
+ # Check if it looks like a step reference (snake_case identifier)
150
+ # and is not a known boolean value
151
+ next unless condition.match?(/^[a-z_]+$/) && !["true", "false", "yes", "no", "on", "off"].include?(condition)
152
+
153
+ # This looks like it could be a step reference
154
+ next if valid_step_names.include?(condition)
155
+
156
+ add_error(
157
+ type: :step_reference,
158
+ message: "Step '#{condition}' referenced in #{condition_key} condition does not exist",
159
+ suggestion: "Ensure step '#{condition}' is defined before it's referenced",
160
+ )
161
+ end
162
+
163
+ # Check nested steps
164
+ ["then", "else", "steps"].each do |key|
165
+ if step[key].is_a?(Array)
166
+ check_step_references(step[key], valid_step_names, current_path + [key])
167
+ end
168
+ end
169
+
170
+ # Check case/when branches
171
+ if step["when"].is_a?(Hash)
172
+ step["when"].each do |when_value, when_steps|
173
+ if when_steps.is_a?(Array)
174
+ check_step_references(when_steps, valid_step_names, current_path + ["when", when_value])
175
+ end
176
+ end
177
+ end
178
+ when Array
179
+ check_step_references(step, valid_step_names, current_path)
180
+ end
181
+ end
182
+ end
183
+
184
+ def validate_target_resource(target)
185
+ return unless @workflow_path
186
+
187
+ workflow_dir = File.dirname(@workflow_path)
188
+
189
+ # If target is a glob pattern or shell command, skip file validation
190
+ return if target.include?("*") || target.start_with?("$(")
191
+
192
+ target_path = File.expand_path(target, workflow_dir)
193
+ unless File.exist?(target_path)
194
+ add_warning(
195
+ type: :resource,
196
+ message: "Target file '#{target}' does not exist",
197
+ suggestion: "Ensure the target file exists or use a glob pattern",
198
+ )
199
+ end
200
+ end
201
+
202
+ def validate_prompt_files
203
+ workflow_dir = File.dirname(@workflow_path)
204
+ all_steps = @step_collector.all_steps
205
+
206
+ all_steps.each do |step|
207
+ next unless step.is_a?(String)
208
+
209
+ # Check if corresponding prompt file exists
210
+ prompt_path = File.join(workflow_dir, step, "prompt.md")
211
+ next if File.exist?(prompt_path)
212
+
213
+ add_warning(
214
+ type: :resource,
215
+ message: "Prompt file missing for step '#{step}'",
216
+ suggestion: "Create file: #{prompt_path}",
217
+ )
218
+ end
219
+ end
220
+ end
221
+ end
222
+ end
223
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Roast
4
+ module Workflow
5
+ module Validators
6
+ # Validates workflow configuration for best practices and common issues
7
+ class LintingValidator < BaseValidator
8
+ # Configurable thresholds
9
+ MAX_STEPS = 20
10
+ MAX_NESTING_DEPTH = 5
11
+
12
+ def initialize(parsed_yaml, workflow_path = nil, step_collector: nil)
13
+ super(parsed_yaml, workflow_path)
14
+ @step_collector = step_collector || StepCollector.new(parsed_yaml)
15
+ end
16
+
17
+ def validate
18
+ lint_naming_conventions
19
+ lint_step_complexity
20
+ lint_common_mistakes
21
+ end
22
+
23
+ private
24
+
25
+ def lint_naming_conventions
26
+ # Check workflow name
27
+ if @parsed_yaml["name"].nil? || @parsed_yaml["name"].empty?
28
+ add_warning(
29
+ type: :naming,
30
+ message: "Workflow should have a descriptive name",
31
+ suggestion: "Add a 'name' field to your workflow configuration",
32
+ )
33
+ end
34
+
35
+ # Check step naming
36
+ all_steps = @step_collector.all_steps
37
+ all_steps.each do |step|
38
+ next unless step.is_a?(String) && !step.match?(/^[a-z_]+$/)
39
+
40
+ add_warning(
41
+ type: :naming,
42
+ step: step,
43
+ message: "Step name '#{step}' should use snake_case",
44
+ suggestion: "Rename to '#{step.downcase.gsub(/[^a-z0-9]/, "_")}'",
45
+ )
46
+ end
47
+ end
48
+
49
+ def lint_step_complexity
50
+ # Check for overly complex workflows
51
+ all_steps = @step_collector.all_steps
52
+ if all_steps.size > MAX_STEPS
53
+ add_warning(
54
+ type: :complexity,
55
+ message: "Workflow has #{all_steps.size} steps, consider breaking it into smaller workflows",
56
+ suggestion: "Use sub-workflows or modularize complex logic",
57
+ )
58
+ end
59
+
60
+ # Check for deeply nested conditions
61
+ check_nesting_depth(@parsed_yaml["steps"] || [])
62
+ end
63
+
64
+ def lint_common_mistakes
65
+ # Missing error handling
66
+ if !@parsed_yaml["exit_on_error"] && !error_handling?
67
+ add_warning(
68
+ type: :error_handling,
69
+ message: "No error handling configured",
70
+ suggestion: "Consider adding 'exit_on_error: true' or error handling steps",
71
+ )
72
+ end
73
+ end
74
+
75
+ def check_nesting_depth(steps, depth = 0)
76
+ steps.each do |step|
77
+ next unless step.is_a?(Hash)
78
+
79
+ current_depth = depth + 1
80
+
81
+ if current_depth > MAX_NESTING_DEPTH
82
+ add_warning(
83
+ type: :complexity,
84
+ message: "Excessive nesting depth (#{current_depth} levels)",
85
+ suggestion: "Consider extracting nested logic into separate steps or workflows",
86
+ )
87
+ end
88
+
89
+ # Check nested steps
90
+ ["steps", "then", "else", "true", "false"].each do |key|
91
+ check_nesting_depth(step[key], current_depth) if step[key].is_a?(Array)
92
+ end
93
+
94
+ # Check case/when branches
95
+ next unless step["when"]
96
+
97
+ step["when"].each_value do |when_steps|
98
+ check_nesting_depth(when_steps, current_depth) if when_steps.is_a?(Array)
99
+ end
100
+ end
101
+ end
102
+
103
+ def error_handling?
104
+ # Check if workflow has any error handling mechanisms
105
+ all_steps = @step_collector.all_steps
106
+ all_steps.any? do |step|
107
+ step.is_a?(Hash) && (step["rescue"] || step["ensure"])
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Roast
4
+ module Workflow
5
+ module Validators
6
+ # Validates workflow configuration against JSON schema
7
+ class SchemaValidator < BaseValidator
8
+ attr_reader :parsed_yaml
9
+
10
+ def initialize(yaml_content, workflow_path = nil) # rubocop:disable Lint/MissingSuper
11
+ @yaml_content = yaml_content&.strip || ""
12
+ @workflow_path = workflow_path
13
+ @errors = []
14
+ @warnings = []
15
+
16
+ begin
17
+ @parsed_yaml = @yaml_content.empty? ? {} : YAML.safe_load(@yaml_content)
18
+ rescue Psych::SyntaxError => e
19
+ @errors << format_yaml_error(e)
20
+ @parsed_yaml = {}
21
+ end
22
+ end
23
+
24
+ def validate
25
+ if @parsed_yaml.empty?
26
+ @errors << {
27
+ type: :empty_configuration,
28
+ message: "Workflow configuration is empty",
29
+ suggestion: "Provide a valid workflow configuration with required fields: name, tools, and steps",
30
+ }
31
+ return
32
+ end
33
+
34
+ validator = Validator.new(@yaml_content)
35
+ unless validator.valid?
36
+ validator.errors.each do |error|
37
+ @errors << format_schema_error(error)
38
+ end
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def format_yaml_error(error)
45
+ {
46
+ type: :yaml_syntax,
47
+ message: "YAML syntax error: #{error.message}",
48
+ line: error.line,
49
+ column: error.column,
50
+ suggestion: "Check YAML syntax at line #{error.line}, column #{error.column}",
51
+ }
52
+ end
53
+
54
+ def format_schema_error(error)
55
+ # Parse JSON Schema error and make it more user-friendly
56
+ if error.include?("did not contain a required property")
57
+ # Extract property name from error
58
+ match = error.match(/required property of '([^']+)'/)
59
+ if match
60
+ property = match[1]
61
+ {
62
+ type: :schema,
63
+ message: "Missing required field: '#{property}'",
64
+ suggestion: "Add '#{property}' to your workflow configuration",
65
+ }
66
+ else
67
+ {
68
+ type: :schema,
69
+ message: error,
70
+ suggestion: "Check the required fields in your workflow configuration",
71
+ }
72
+ end
73
+ elsif error.include?("does not match")
74
+ {
75
+ type: :schema,
76
+ message: error,
77
+ suggestion: "Check the workflow schema documentation for valid values",
78
+ }
79
+ else
80
+ {
81
+ type: :schema,
82
+ message: error,
83
+ suggestion: "Refer to the workflow schema for correct configuration structure",
84
+ }
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Roast
4
+ module Workflow
5
+ module Validators
6
+ # Collects and caches all steps from a workflow configuration
7
+ class StepCollector
8
+ def initialize(parsed_yaml)
9
+ @parsed_yaml = parsed_yaml
10
+ @all_steps = nil
11
+ end
12
+
13
+ def all_steps
14
+ @all_steps ||= collect_all_steps(@parsed_yaml)
15
+ end
16
+
17
+ private
18
+
19
+ def collect_all_steps(config, steps = [])
20
+ # Recursively collect all steps from the configuration
21
+ ["steps", "pre_processing", "post_processing"].each do |key|
22
+ if config[key]
23
+ steps.concat(extract_steps_from_array(config[key]))
24
+ end
25
+ end
26
+ steps
27
+ end
28
+
29
+ def extract_steps_from_array(steps_array, collected = [])
30
+ steps_array.each do |step|
31
+ case step
32
+ when String
33
+ collected << step
34
+ when Hash
35
+ if step["steps"]
36
+ collected.concat(extract_steps_from_array(step["steps"]))
37
+ end
38
+ # Handle conditional steps
39
+ ["then", "else", "true", "false"].each do |branch|
40
+ if step[branch]
41
+ collected.concat(extract_steps_from_array(step[branch]))
42
+ end
43
+ end
44
+ # Handle case/when steps
45
+ step["when"]&.each_value do |when_steps|
46
+ collected.concat(extract_steps_from_array(when_steps))
47
+ end
48
+ when Array
49
+ collected.concat(extract_steps_from_array(step))
50
+ end
51
+ end
52
+ collected
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Roast
4
+ module Workflow
5
+ module Validators
6
+ # Orchestrates all validators and aggregates results
7
+ class ValidationOrchestrator
8
+ attr_reader :errors, :warnings
9
+
10
+ def initialize(yaml_content, workflow_path = nil)
11
+ @yaml_content = yaml_content
12
+ @workflow_path = workflow_path
13
+ @errors = []
14
+ @warnings = []
15
+ end
16
+
17
+ def valid?
18
+ # First run schema validation
19
+ schema_validator = SchemaValidator.new(@yaml_content, @workflow_path)
20
+
21
+ unless schema_validator.valid?
22
+ @errors = schema_validator.errors
23
+ @warnings = schema_validator.warnings
24
+ return false
25
+ end
26
+
27
+ parsed_yaml = schema_validator.parsed_yaml
28
+
29
+ # If schema is valid, run other validators
30
+ if @errors.empty?
31
+ step_collector = StepCollector.new(parsed_yaml)
32
+
33
+ # Run dependency validation
34
+ dependency_validator = DependencyValidator.new(parsed_yaml, @workflow_path, step_collector: step_collector)
35
+ dependency_validator.validate
36
+ @errors.concat(dependency_validator.errors)
37
+ @warnings.concat(dependency_validator.warnings)
38
+
39
+ # Run linting only if no errors
40
+ if @errors.empty?
41
+ linting_validator = LintingValidator.new(parsed_yaml, @workflow_path, step_collector: step_collector)
42
+ linting_validator.validate
43
+ @warnings.concat(linting_validator.warnings)
44
+ end
45
+ end
46
+
47
+ @errors.empty?
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -66,13 +66,13 @@ module Roast
66
66
  @step_loader = step_loader || StepLoader.new(workflow, config_hash, context_path, phase: phase)
67
67
  @command_executor = command_executor || CommandExecutor.new(logger: @error_handler)
68
68
  @interpolator = interpolator || Interpolator.new(workflow, logger: @error_handler)
69
- @state_manager = state_manager || StateManager.new(workflow, logger: @error_handler)
69
+ @state_manager = state_manager || StateManager.new(workflow, logger: @error_handler, storage_type: workflow.storage_type)
70
70
  @iteration_executor = iteration_executor || IterationExecutor.new(workflow, context_path, @state_manager, config_hash)
71
71
  @conditional_executor = conditional_executor || ConditionalExecutor.new(workflow, context_path, @state_manager, self)
72
72
  @step_orchestrator = step_orchestrator || StepOrchestrator.new(workflow, @step_loader, @state_manager, @error_handler, self)
73
73
 
74
74
  # Initialize coordinator with dependencies
75
- @step_executor_coordinator = step_executor_coordinator || StepExecutorCoordinator.new(
75
+ base_coordinator = step_executor_coordinator || StepExecutorCoordinator.new(
76
76
  context: @context,
77
77
  dependencies: {
78
78
  workflow_executor: self,
@@ -84,6 +84,13 @@ module Roast
84
84
  error_handler: @error_handler,
85
85
  },
86
86
  )
87
+
88
+ # Only wrap with reporting decorator if workflow has token tracking enabled
89
+ @step_executor_coordinator = if workflow.respond_to?(:context_manager) && workflow.context_manager
90
+ StepExecutorWithReporting.new(base_coordinator, @context)
91
+ else
92
+ base_coordinator
93
+ end
87
94
  end
88
95
 
89
96
  # Logger interface methods for backward compatibility
@@ -111,8 +118,8 @@ module Roast
111
118
  @interpolator.interpolate(text)
112
119
  end
113
120
 
114
- def execute_step(name, exit_on_error: true)
115
- @step_executor_coordinator.execute(name, exit_on_error: exit_on_error)
121
+ def execute_step(name, exit_on_error: true, is_last_step: nil)
122
+ @step_executor_coordinator.execute(name, exit_on_error:, is_last_step:)
116
123
  rescue StepLoader::StepNotFoundError => e
117
124
  raise StepNotFoundError.new(e.message, step_name: e.step_name, original_error: e.original_error)
118
125
  rescue StepLoader::StepExecutionError => e
@@ -10,6 +10,7 @@ module Roast
10
10
 
11
11
  def setup
12
12
  load_roast_initializers
13
+ check_raix_configuration
13
14
  include_tools
14
15
  configure_api_client
15
16
  end
@@ -20,6 +21,85 @@ module Roast
20
21
  Roast::Initializers.load_all
21
22
  end
22
23
 
24
+ def check_raix_configuration
25
+ # Skip check in test environment
26
+ return if ENV["RAILS_ENV"] == "test" || ENV["RACK_ENV"] == "test" || defined?(Minitest)
27
+
28
+ # Only check if the workflow has steps that would need API access
29
+ return if @configuration.steps.empty?
30
+
31
+ # Check if Raix has been configured with the appropriate client
32
+ case @configuration.api_provider
33
+ when :openai
34
+ if Raix.configuration.openai_client.nil?
35
+ warn_about_missing_raix_configuration(:openai)
36
+ end
37
+ when :openrouter
38
+ if Raix.configuration.openrouter_client.nil?
39
+ warn_about_missing_raix_configuration(:openrouter)
40
+ end
41
+ when nil
42
+ # If no api_provider is set but we have steps that might need API access,
43
+ # check if any client is configured
44
+ if Raix.configuration.openai_client.nil? && Raix.configuration.openrouter_client.nil?
45
+ warn_about_missing_raix_configuration(:any)
46
+ end
47
+ end
48
+ end
49
+
50
+ def warn_about_missing_raix_configuration(provider)
51
+ ::CLI::UI.frame_style = :box
52
+ ::CLI::UI::Frame.open("{{red:Raix Configuration Missing}}", color: :red) do
53
+ case provider
54
+ when :openai
55
+ puts ::CLI::UI.fmt("{{yellow:⚠️ Warning: Raix OpenAI client is not configured!}}")
56
+ when :openrouter
57
+ puts ::CLI::UI.fmt("{{yellow:⚠️ Warning: Raix OpenRouter client is not configured!}}")
58
+ else
59
+ puts ::CLI::UI.fmt("{{yellow:⚠️ Warning: Raix is not configured!}}")
60
+ end
61
+ puts
62
+ puts "Roast requires Raix to be properly initialized to make API calls."
63
+ puts ::CLI::UI.fmt("To fix this, create a file at {{cyan:.roast/initializers/raix.rb}} with:")
64
+ puts
65
+ puts ::CLI::UI.fmt("{{cyan:# frozen_string_literal: true}}")
66
+ puts
67
+ puts ::CLI::UI.fmt("{{cyan:require \"raix\"}}")
68
+
69
+ if provider == :openrouter
70
+ puts ::CLI::UI.fmt("{{cyan:require \"open_router\"}}")
71
+ puts
72
+ puts ::CLI::UI.fmt("{{cyan:Raix.configure do |config|}}")
73
+ puts ::CLI::UI.fmt("{{cyan: config.openrouter_client = OpenRouter::Client.new(}}")
74
+ puts ::CLI::UI.fmt("{{cyan: access_token: ENV.fetch(\"OPENROUTER_API_KEY\"),}}")
75
+ puts ::CLI::UI.fmt("{{cyan: uri_base: \"https://openrouter.ai/api/v1\",}}")
76
+ puts ::CLI::UI.fmt("{{cyan: )}}")
77
+ else
78
+ puts ::CLI::UI.fmt("{{cyan:require \"faraday\"}}")
79
+ puts ::CLI::UI.fmt("{{cyan:require \"faraday/retry\"}}")
80
+ puts
81
+ puts ::CLI::UI.fmt("{{cyan: Raix.configure do |config|}}")
82
+ puts ::CLI::UI.fmt("{{cyan: config.openai_client = OpenAI::Client.new(}}")
83
+ puts ::CLI::UI.fmt("{{cyan: access_token: ENV.fetch(\"OPENAI_API_KEY\"),}}")
84
+ puts ::CLI::UI.fmt("{{cyan: uri_base: \"https://api.openai.com/v1\",}}")
85
+ puts ::CLI::UI.fmt("{{cyan: ) do |f|}}")
86
+ puts ::CLI::UI.fmt("{{cyan: f.request(:retry, {}}")
87
+ puts ::CLI::UI.fmt("{{cyan: max: 2,}}")
88
+ puts ::CLI::UI.fmt("{{cyan: interval: 0.05,}}")
89
+ puts ::CLI::UI.fmt("{{cyan: interval_randomness: 0.5,}}")
90
+ puts ::CLI::UI.fmt("{{cyan: backoff_factor: 2,}}")
91
+ puts ::CLI::UI.fmt("{{cyan: })}}")
92
+ puts ::CLI::UI.fmt("{{cyan: end}}")
93
+ end
94
+ puts ::CLI::UI.fmt("{{cyan:end}}")
95
+ puts
96
+ puts "For Shopify users, you need to use the LLM gateway proxy instead."
97
+ puts "Check the #roast slack channel for more information."
98
+ puts
99
+ end
100
+ raise ::CLI::Kit::Abort, "Please configure Raix before running workflows."
101
+ end
102
+
23
103
  def include_tools
24
104
  return unless @configuration.tools.present? || @configuration.mcp_tools.present?
25
105
 
@@ -167,6 +167,12 @@ module Roast
167
167
  workflow.verbose = @options[:verbose] if @options[:verbose].present?
168
168
  workflow.concise = @options[:concise] if @options[:concise].present?
169
169
  workflow.pause_step_name = @options[:pause] if @options[:pause].present?
170
+ # Set storage type based on CLI option (default is SQLite unless --file-storage is used)
171
+ workflow.storage_type = @options[:file_storage] ? "file" : nil
172
+ # Set model from configuration with fallback to default
173
+ workflow.model = @configuration.model || StepLoader::DEFAULT_MODEL
174
+ # Set context management configuration
175
+ workflow.context_management_config = @configuration.context_management
170
176
  end
171
177
  end
172
178