roast-ai 0.3.1 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yaml +2 -2
  3. data/.gitignore +1 -0
  4. data/CHANGELOG.md +85 -0
  5. data/CLAUDE.md +106 -9
  6. data/Gemfile +4 -1
  7. data/Gemfile.lock +70 -16
  8. data/README.md +159 -8
  9. data/bin/console +1 -0
  10. data/bin/roast +1 -1
  11. data/claude-swarm.yml +210 -0
  12. data/docs/AGENT_STEPS.md +288 -0
  13. data/docs/VALIDATION.md +178 -0
  14. data/examples/agent_continue/add_documentation/prompt.md +5 -0
  15. data/examples/agent_continue/add_error_handling/prompt.md +5 -0
  16. data/examples/agent_continue/analyze_codebase/prompt.md +7 -0
  17. data/examples/agent_continue/combined_workflow.yml +24 -0
  18. data/examples/agent_continue/continue_adding_features/prompt.md +4 -0
  19. data/examples/agent_continue/create_integration_tests/prompt.md +3 -0
  20. data/examples/agent_continue/document_with_context/prompt.md +5 -0
  21. data/examples/agent_continue/explore_api/prompt.md +6 -0
  22. data/examples/agent_continue/implement_client/prompt.md +6 -0
  23. data/examples/agent_continue/inline_workflow.yml +20 -0
  24. data/examples/agent_continue/refactor_code/prompt.md +2 -0
  25. data/examples/agent_continue/verify_changes/prompt.md +6 -0
  26. data/examples/agent_continue/workflow.yml +27 -0
  27. data/examples/agent_workflow/README.md +75 -0
  28. data/examples/agent_workflow/apply_refactorings/prompt.md +22 -0
  29. data/examples/agent_workflow/identify_code_smells/prompt.md +15 -0
  30. data/examples/agent_workflow/summarize_improvements/prompt.md +18 -0
  31. data/examples/agent_workflow/workflow.png +0 -0
  32. data/examples/agent_workflow/workflow.yml +16 -0
  33. data/examples/api_workflow/workflow.png +0 -0
  34. data/examples/apply_diff_demo/README.md +58 -0
  35. data/examples/apply_diff_demo/apply_simple_change/prompt.md +13 -0
  36. data/examples/apply_diff_demo/create_sample_file/prompt.md +11 -0
  37. data/examples/apply_diff_demo/workflow.yml +24 -0
  38. data/examples/available_tools_demo/README.md +42 -0
  39. data/examples/available_tools_demo/analyze_files/prompt.md +6 -0
  40. data/examples/available_tools_demo/explore_directory/prompt.md +6 -0
  41. data/examples/available_tools_demo/workflow.png +0 -0
  42. data/examples/available_tools_demo/workflow.yml +32 -0
  43. data/examples/available_tools_demo/write_summary/prompt.md +6 -0
  44. data/examples/bash_prototyping/api_testing.png +0 -0
  45. data/examples/bash_prototyping/system_analysis.png +0 -0
  46. data/examples/case_when/detect_language/prompt.md +2 -2
  47. data/examples/case_when/workflow.png +0 -0
  48. data/examples/cmd/basic_workflow.png +0 -0
  49. data/examples/cmd/dev_workflow.png +0 -0
  50. data/examples/cmd/explorer_workflow.png +0 -0
  51. data/examples/conditional/simple_workflow.png +0 -0
  52. data/examples/conditional/workflow.png +0 -0
  53. data/examples/context_management_demo/README.md +43 -0
  54. data/examples/context_management_demo/workflow.yml +42 -0
  55. data/examples/direct_coerce_syntax/workflow.png +0 -0
  56. data/examples/dot_notation/workflow.png +0 -0
  57. data/examples/exit_on_error/workflow.png +0 -0
  58. data/examples/grading/run_coverage.rb +0 -2
  59. data/examples/grading/workflow.png +0 -0
  60. data/examples/interpolation/workflow.png +0 -0
  61. data/examples/interpolation/workflow.yml +1 -1
  62. data/examples/iteration/analyze_complexity/prompt.md +2 -2
  63. data/examples/iteration/generate_recommendations/prompt.md +2 -2
  64. data/examples/iteration/implement_fix/prompt.md +2 -2
  65. data/examples/iteration/prioritize_issues/prompt.md +1 -1
  66. data/examples/iteration/prompts/analyze_file.md +2 -2
  67. data/examples/iteration/prompts/generate_summary.md +1 -1
  68. data/examples/iteration/prompts/update_report.md +3 -3
  69. data/examples/iteration/prompts/write_report.md +3 -3
  70. data/examples/iteration/read_file/prompt.md +2 -2
  71. data/examples/iteration/select_next_issue/prompt.md +2 -2
  72. data/examples/iteration/update_fix_count/prompt.md +4 -4
  73. data/examples/iteration/verify_fix/prompt.md +3 -3
  74. data/examples/iteration/workflow.png +0 -0
  75. data/examples/json_handling/workflow.png +0 -0
  76. data/examples/mcp/README.md +3 -3
  77. data/examples/mcp/analyze_changes/prompt.md +1 -1
  78. data/examples/mcp/database_workflow.png +0 -0
  79. data/examples/mcp/database_workflow.yml +1 -1
  80. data/examples/mcp/env_demo/workflow.png +0 -0
  81. data/examples/mcp/fetch_pr_context/prompt.md +1 -1
  82. data/examples/mcp/filesystem_demo/workflow.png +0 -0
  83. data/examples/mcp/github_workflow.png +0 -0
  84. data/examples/mcp/github_workflow.yml +1 -1
  85. data/examples/mcp/multi_mcp_workflow.png +0 -0
  86. data/examples/mcp/post_review/prompt.md +1 -1
  87. data/examples/mcp/workflow.png +0 -0
  88. data/examples/no_model_fallback/README.md +17 -0
  89. data/examples/no_model_fallback/analyze_file/prompt.md +1 -0
  90. data/examples/no_model_fallback/analyze_patterns/prompt.md +27 -0
  91. data/examples/no_model_fallback/generate_report_for_md/prompt.md +10 -0
  92. data/examples/no_model_fallback/generate_report_for_rb/prompt.md +3 -0
  93. data/examples/no_model_fallback/sample.rb +42 -0
  94. data/examples/no_model_fallback/workflow.yml +19 -0
  95. data/examples/openrouter_example/workflow.png +0 -0
  96. data/examples/pre_post_processing/analyze_test_file/prompt.md +1 -1
  97. data/examples/pre_post_processing/improve_test_coverage/prompt.md +1 -1
  98. data/examples/pre_post_processing/optimize_test_performance/prompt.md +1 -1
  99. data/examples/pre_post_processing/post_processing/aggregate_metrics/prompt.md +2 -2
  100. data/examples/pre_post_processing/post_processing/generate_summary_report/prompt.md +1 -1
  101. data/examples/pre_post_processing/pre_processing/setup_test_environment/prompt.md +1 -1
  102. data/examples/pre_post_processing/validate_changes/prompt.md +2 -2
  103. data/examples/pre_post_processing/workflow.png +0 -0
  104. data/examples/rspec_to_minitest/workflow.png +0 -0
  105. data/examples/shared_config/example_with_shared_config/workflow.png +0 -0
  106. data/examples/shared_config/shared.png +0 -0
  107. data/examples/single_target_prepost/workflow.png +0 -0
  108. data/examples/smart_coercion_defaults/workflow.png +0 -0
  109. data/examples/step_configuration/workflow.png +0 -0
  110. data/examples/swarm_example.yml +25 -0
  111. data/examples/tool_config_example/workflow.png +0 -0
  112. data/examples/user_input/README.md +90 -0
  113. data/examples/user_input/funny_name/create_backstory/prompt.md +10 -0
  114. data/examples/user_input/funny_name/workflow.png +0 -0
  115. data/examples/user_input/funny_name/workflow.yml +26 -0
  116. data/examples/user_input/generate_summary/prompt.md +11 -0
  117. data/examples/user_input/simple_input_demo/workflow.png +0 -0
  118. data/examples/user_input/simple_input_demo/workflow.yml +35 -0
  119. data/examples/user_input/survey_workflow.png +0 -0
  120. data/examples/user_input/survey_workflow.yml +71 -0
  121. data/examples/user_input/welcome_message/prompt.md +3 -0
  122. data/examples/user_input/workflow.png +0 -0
  123. data/examples/user_input/workflow.yml +73 -0
  124. data/examples/workflow_generator/create_workflow_files/prompt.md +1 -1
  125. data/examples/workflow_generator/workflow.png +0 -0
  126. data/lib/roast/errors.rb +6 -4
  127. data/lib/roast/helpers/function_caching_interceptor.rb +0 -2
  128. data/lib/roast/helpers/logger.rb +12 -35
  129. data/lib/roast/helpers/minitest_coverage_runner.rb +0 -1
  130. data/lib/roast/helpers/prompt_loader.rb +0 -2
  131. data/lib/roast/helpers/timeout_handler.rb +91 -0
  132. data/lib/roast/resources/api_resource.rb +0 -4
  133. data/lib/roast/resources/url_resource.rb +0 -3
  134. data/lib/roast/resources.rb +0 -8
  135. data/lib/roast/services/context_threshold_checker.rb +42 -0
  136. data/lib/roast/services/token_counting_service.rb +44 -0
  137. data/lib/roast/tools/apply_diff.rb +128 -0
  138. data/lib/roast/tools/ask_user.rb +0 -2
  139. data/lib/roast/tools/bash.rb +12 -9
  140. data/lib/roast/tools/cmd.rb +29 -12
  141. data/lib/roast/tools/coding_agent.rb +65 -17
  142. data/lib/roast/tools/context_summarizer.rb +108 -0
  143. data/lib/roast/tools/grep.rb +0 -3
  144. data/lib/roast/tools/helpers/coding_agent_message_formatter.rb +1 -4
  145. data/lib/roast/tools/read_file.rb +0 -2
  146. data/lib/roast/tools/search_file.rb +0 -2
  147. data/lib/roast/tools/swarm.rb +124 -0
  148. data/lib/roast/tools/update_files.rb +0 -4
  149. data/lib/roast/tools/write_file.rb +0 -3
  150. data/lib/roast/tools.rb +0 -13
  151. data/lib/roast/value_objects/step_name.rb +14 -3
  152. data/lib/roast/value_objects/workflow_path.rb +0 -2
  153. data/lib/roast/value_objects.rb +4 -4
  154. data/lib/roast/version.rb +1 -1
  155. data/lib/roast/workflow/agent_step.rb +33 -0
  156. data/lib/roast/workflow/api_configuration.rb +0 -4
  157. data/lib/roast/workflow/base_iteration_step.rb +3 -6
  158. data/lib/roast/workflow/base_step.rb +54 -28
  159. data/lib/roast/workflow/base_workflow.rb +43 -23
  160. data/lib/roast/workflow/case_executor.rb +0 -1
  161. data/lib/roast/workflow/case_step.rb +0 -4
  162. data/lib/roast/workflow/command_executor.rb +0 -2
  163. data/lib/roast/workflow/conditional_executor.rb +0 -1
  164. data/lib/roast/workflow/conditional_step.rb +0 -4
  165. data/lib/roast/workflow/configuration.rb +5 -67
  166. data/lib/roast/workflow/configuration_loader.rb +63 -3
  167. data/lib/roast/workflow/configuration_parser.rb +1 -7
  168. data/lib/roast/workflow/context_manager.rb +89 -0
  169. data/lib/roast/workflow/dot_access_hash.rb +16 -1
  170. data/lib/roast/workflow/each_step.rb +1 -1
  171. data/lib/roast/workflow/error_handler.rb +0 -3
  172. data/lib/roast/workflow/expression_evaluator.rb +0 -3
  173. data/lib/roast/workflow/file_state_repository.rb +0 -5
  174. data/lib/roast/workflow/input_executor.rb +41 -0
  175. data/lib/roast/workflow/input_step.rb +163 -0
  176. data/lib/roast/workflow/iteration_executor.rb +0 -2
  177. data/lib/roast/workflow/output_handler.rb +1 -3
  178. data/lib/roast/workflow/output_manager.rb +0 -2
  179. data/lib/roast/workflow/repeat_step.rb +1 -1
  180. data/lib/roast/workflow/replay_handler.rb +1 -4
  181. data/lib/roast/workflow/resource_resolver.rb +0 -3
  182. data/lib/roast/workflow/session_manager.rb +0 -3
  183. data/lib/roast/workflow/sqlite_state_repository.rb +342 -0
  184. data/lib/roast/workflow/state_manager.rb +2 -4
  185. data/lib/roast/workflow/state_repository_factory.rb +36 -0
  186. data/lib/roast/workflow/step_completion_reporter.rb +27 -0
  187. data/lib/roast/workflow/step_executor_coordinator.rb +48 -24
  188. data/lib/roast/workflow/step_executor_factory.rb +0 -5
  189. data/lib/roast/workflow/step_executor_registry.rb +1 -4
  190. data/lib/roast/workflow/step_executor_with_reporting.rb +68 -0
  191. data/lib/roast/workflow/step_executors/hash_step_executor.rb +0 -3
  192. data/lib/roast/workflow/step_executors/parallel_step_executor.rb +0 -3
  193. data/lib/roast/workflow/step_executors/string_step_executor.rb +0 -2
  194. data/lib/roast/workflow/step_factory.rb +56 -0
  195. data/lib/roast/workflow/step_loader.rb +31 -17
  196. data/lib/roast/workflow/step_name_extractor.rb +84 -0
  197. data/lib/roast/workflow/step_orchestrator.rb +3 -2
  198. data/lib/roast/workflow/step_type_resolver.rb +28 -1
  199. data/lib/roast/workflow/validation_command.rb +197 -0
  200. data/lib/roast/workflow/validator.rb +0 -4
  201. data/lib/roast/workflow/validators/base_validator.rb +44 -0
  202. data/lib/roast/workflow/validators/dependency_validator.rb +223 -0
  203. data/lib/roast/workflow/validators/linting_validator.rb +113 -0
  204. data/lib/roast/workflow/validators/schema_validator.rb +90 -0
  205. data/lib/roast/workflow/validators/step_collector.rb +57 -0
  206. data/lib/roast/workflow/validators/validation_orchestrator.rb +52 -0
  207. data/lib/roast/workflow/workflow_executor.rb +11 -20
  208. data/lib/roast/workflow/workflow_initializer.rb +1 -8
  209. data/lib/roast/workflow/workflow_runner.rb +6 -7
  210. data/lib/roast/workflow.rb +0 -15
  211. data/lib/roast/workflow_diagram_generator.rb +298 -0
  212. data/lib/roast.rb +212 -10
  213. data/roast.gemspec +4 -2
  214. data/schema/workflow.json +123 -1
  215. metadata +143 -6
  216. data/lib/roast/helpers.rb +0 -12
@@ -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
@@ -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
@@ -82,13 +66,13 @@ module Roast
82
66
  @step_loader = step_loader || StepLoader.new(workflow, config_hash, context_path, phase: phase)
83
67
  @command_executor = command_executor || CommandExecutor.new(logger: @error_handler)
84
68
  @interpolator = interpolator || Interpolator.new(workflow, logger: @error_handler)
85
- @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)
86
70
  @iteration_executor = iteration_executor || IterationExecutor.new(workflow, context_path, @state_manager, config_hash)
87
71
  @conditional_executor = conditional_executor || ConditionalExecutor.new(workflow, context_path, @state_manager, self)
88
72
  @step_orchestrator = step_orchestrator || StepOrchestrator.new(workflow, @step_loader, @state_manager, @error_handler, self)
89
73
 
90
74
  # Initialize coordinator with dependencies
91
- @step_executor_coordinator = step_executor_coordinator || StepExecutorCoordinator.new(
75
+ base_coordinator = step_executor_coordinator || StepExecutorCoordinator.new(
92
76
  context: @context,
93
77
  dependencies: {
94
78
  workflow_executor: self,
@@ -100,6 +84,13 @@ module Roast
100
84
  error_handler: @error_handler,
101
85
  },
102
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
103
94
  end
104
95
 
105
96
  # Logger interface methods for backward compatibility
@@ -127,8 +118,8 @@ module Roast
127
118
  @interpolator.interpolate(text)
128
119
  end
129
120
 
130
- def execute_step(name, exit_on_error: true)
131
- @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:)
132
123
  rescue StepLoader::StepNotFoundError => e
133
124
  raise StepNotFoundError.new(e.message, step_name: e.step_name, original_error: e.original_error)
134
125
  rescue StepLoader::StepExecutionError => e
@@ -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
@@ -174,6 +167,12 @@ module Roast
174
167
  workflow.verbose = @options[:verbose] if @options[:verbose].present?
175
168
  workflow.concise = @options[:concise] if @options[:concise].present?
176
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
177
176
  end
178
177
  end
179
178
 
@@ -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
@@ -0,0 +1,298 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Roast
4
+ class WorkflowDiagramGenerator
5
+ def initialize(workflow_config, workflow_file_path = nil)
6
+ @workflow_config = workflow_config
7
+ @workflow_file_path = workflow_file_path
8
+ @graph = GraphViz.new(:G, type: :digraph)
9
+ @node_counter = 0
10
+ @nodes = {}
11
+ end
12
+
13
+ def generate(custom_output_path = nil)
14
+ configure_graph
15
+ build_graph(@workflow_config.steps)
16
+
17
+ output_path = custom_output_path || generate_output_filename
18
+ @graph.output(png: output_path)
19
+ output_path
20
+ end
21
+
22
+ private
23
+
24
+ def configure_graph
25
+ @graph[:rankdir] = "TB"
26
+ @graph[:fontname] = "Helvetica"
27
+ @graph[:fontsize] = "12"
28
+ @graph[:bgcolor] = "white"
29
+ @graph[:pad] = "0.5"
30
+ @graph[:nodesep] = "0.7"
31
+ @graph[:ranksep] = "0.8"
32
+ @graph[:splines] = "spline"
33
+
34
+ # Default node styling
35
+ @graph.node[:shape] = "box"
36
+ @graph.node[:style] = "rounded,filled"
37
+ @graph.node[:fillcolor] = "#E8F4FD"
38
+ @graph.node[:color] = "#2563EB"
39
+ @graph.node[:fontname] = "Helvetica"
40
+ @graph.node[:fontsize] = "11"
41
+ @graph.node[:fontcolor] = "#1E293B"
42
+ @graph.node[:penwidth] = "1.5"
43
+ @graph.node[:height] = "0.6"
44
+ @graph.node[:margin] = "0.15"
45
+
46
+ # Edge styling
47
+ @graph.edge[:fontname] = "Helvetica"
48
+ @graph.edge[:fontsize] = "10"
49
+ @graph.edge[:color] = "#64748B"
50
+ @graph.edge[:penwidth] = "1.5"
51
+ @graph.edge[:arrowsize] = "0.8"
52
+ end
53
+
54
+ def build_graph(steps, parent_node = nil)
55
+ previous_node = parent_node
56
+
57
+ steps.each do |step|
58
+ current_node = process_step(step)
59
+
60
+ if previous_node && current_node
61
+ @graph.add_edges(previous_node, current_node)
62
+ end
63
+
64
+ previous_node = current_node unless current_node.nil?
65
+ end
66
+
67
+ previous_node
68
+ end
69
+
70
+ def process_step(step)
71
+ case step
72
+ when String
73
+ create_step_node(step)
74
+ when Hash
75
+ process_control_flow(step)
76
+ else
77
+ ::CLI::Kit.logger.warn("Unexpected step type in workflow diagram: #{step.class} - #{step.inspect}")
78
+ nil
79
+ end
80
+ end
81
+
82
+ def create_step_node(step_name)
83
+ node_id = next_node_id
84
+ label = step_name
85
+
86
+ # Check if it's an inline prompt
87
+ @nodes[node_id] = if step_name.start_with?("prompt:")
88
+ @graph.add_nodes(
89
+ node_id,
90
+ label: truncate_label(step_name[7..].strip),
91
+ fillcolor: "#FEF3C7",
92
+ color: "#F59E0B",
93
+ shape: "note",
94
+ fontsize: "10",
95
+ )
96
+ else
97
+ @graph.add_nodes(node_id, label: label)
98
+ end
99
+
100
+ @nodes[node_id]
101
+ end
102
+
103
+ def process_control_flow(control_flow)
104
+ if control_flow.key?("if") || control_flow.key?("unless")
105
+ process_conditional(control_flow)
106
+ elsif control_flow.key?("each") || control_flow.key?("repeat")
107
+ process_loop(control_flow)
108
+ elsif control_flow.key?("input")
109
+ process_input(control_flow)
110
+ elsif control_flow.key?("proceed?")
111
+ process_proceed(control_flow)
112
+ elsif control_flow.key?("case")
113
+ process_case(control_flow)
114
+ else
115
+ ::CLI::Kit.logger.warn("Unexpected control flow structure in workflow diagram: #{control_flow.keys.join(", ")}")
116
+ nil
117
+ end
118
+ end
119
+
120
+ def process_conditional(conditional)
121
+ condition_type = conditional.key?("if") ? "if" : "unless"
122
+ condition = conditional[condition_type]
123
+
124
+ # Create diamond decision node
125
+ decision_id = next_node_id
126
+ decision_node = @graph.add_nodes(
127
+ decision_id,
128
+ label: "#{condition_type}: #{condition}",
129
+ shape: "diamond",
130
+ fillcolor: "#FEE2E2",
131
+ color: "#DC2626",
132
+ fontsize: "10",
133
+ height: "0.8",
134
+ width: "1.2",
135
+ )
136
+
137
+ # Process then branch
138
+ if conditional["then"]
139
+ then_steps = Array(conditional["then"])
140
+ if then_steps.any?
141
+ build_graph(then_steps, decision_node)
142
+ end
143
+ end
144
+
145
+ # Process else branch
146
+ if conditional["else"]
147
+ else_steps = Array(conditional["else"])
148
+ if else_steps.any?
149
+ build_graph(else_steps, decision_node)
150
+ end
151
+ end
152
+
153
+ decision_node
154
+ end
155
+
156
+ def process_loop(loop_control)
157
+ loop_type = loop_control.key?("each") ? "each" : "repeat"
158
+ loop_value = loop_control[loop_type]
159
+
160
+ # Create loop node
161
+ loop_id = next_node_id
162
+ loop_label = loop_type == "each" ? "each: #{loop_value}" : "repeat: #{loop_value}"
163
+ loop_node = @graph.add_nodes(
164
+ loop_id,
165
+ label: loop_label,
166
+ shape: "box3d",
167
+ fillcolor: "#D1FAE5",
168
+ color: "#10B981",
169
+ fontsize: "10",
170
+ penwidth: "2",
171
+ )
172
+
173
+ # Process loop body
174
+ if loop_control["do"]
175
+ loop_steps = Array(loop_control["do"])
176
+ if loop_steps.any?
177
+ last_loop_node = build_graph(loop_steps, loop_node)
178
+ # Add back edge to show loop
179
+ @graph.add_edges(
180
+ last_loop_node,
181
+ loop_node,
182
+ style: "dashed",
183
+ label: "loop",
184
+ color: "#10B981",
185
+ fontcolor: "#10B981",
186
+ arrowhead: "empty",
187
+ )
188
+ end
189
+ end
190
+
191
+ loop_node
192
+ end
193
+
194
+ def process_input(input_control)
195
+ input_id = next_node_id
196
+ label = input_control["input"]
197
+ input_node = @graph.add_nodes(
198
+ input_id,
199
+ label: "input: #{label}",
200
+ shape: "parallelogram",
201
+ fillcolor: "#F3F4F6",
202
+ color: "#6B7280",
203
+ fontsize: "10",
204
+ )
205
+ input_node
206
+ end
207
+
208
+ def process_proceed(proceed_control)
209
+ proceed_id = next_node_id
210
+ proceed_node = @graph.add_nodes(
211
+ proceed_id,
212
+ label: "proceed?",
213
+ shape: "diamond",
214
+ fillcolor: "#FED7AA",
215
+ color: "#EA580C",
216
+ fontsize: "10",
217
+ height: "0.8",
218
+ )
219
+
220
+ # Process do branch if present
221
+ if proceed_control["do"]
222
+ proceed_steps = Array(proceed_control["do"])
223
+ if proceed_steps.any?
224
+ build_graph(proceed_steps, proceed_node)
225
+ end
226
+ end
227
+
228
+ proceed_node
229
+ end
230
+
231
+ def process_case(case_control)
232
+ case_id = next_node_id
233
+ case_node = @graph.add_nodes(
234
+ case_id,
235
+ label: "case: #{case_control["case"]}",
236
+ shape: "diamond",
237
+ fillcolor: "#E9D5FF",
238
+ color: "#9333EA",
239
+ fontsize: "10",
240
+ height: "0.8",
241
+ width: "1.5",
242
+ )
243
+
244
+ # Process when branches
245
+ case_control["when"].each do |condition, steps|
246
+ when_steps = Array(steps)
247
+ next if when_steps.none?
248
+
249
+ first_when_node = process_step(when_steps.first)
250
+ @graph.add_edges(
251
+ case_node,
252
+ first_when_node,
253
+ label: condition.to_s,
254
+ fontcolor: "#9333EA",
255
+ )
256
+
257
+ if when_steps.length > 1
258
+ build_graph(when_steps[1..], first_when_node)
259
+ end
260
+ end
261
+
262
+ case_node
263
+ end
264
+
265
+ def next_node_id
266
+ @node_counter += 1
267
+ "node_#{@node_counter}"
268
+ end
269
+
270
+ def truncate_label(text, max_length = 50)
271
+ return text if text.length <= max_length
272
+
273
+ "#{text[0...max_length]}..."
274
+ end
275
+
276
+ def generate_output_filename
277
+ if @workflow_file_path
278
+ # Get the directory and base name of the workflow file
279
+ dir = File.dirname(@workflow_file_path)
280
+ base = File.basename(@workflow_file_path, ".yml")
281
+
282
+ # Create the diagram filename in the same directory
283
+ File.join(dir, "#{base}.png")
284
+ else
285
+ # Fallback to workflow name if no file path provided
286
+ workflow_name = @workflow_config.name
287
+ sanitized_name = workflow_name
288
+ .downcase
289
+ .gsub(/[^a-z0-9]+/, "_")
290
+ .gsub(/^_|_$/, "")
291
+ .gsub(/_+/, "_")
292
+
293
+ sanitized_name = "workflow" if sanitized_name.empty?
294
+ "#{sanitized_name}_diagram.png"
295
+ end
296
+ end
297
+ end
298
+ end