roast-ai 0.1.6 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yaml +1 -1
  3. data/CHANGELOG.md +48 -0
  4. data/CLAUDE.md +20 -0
  5. data/Gemfile +1 -0
  6. data/Gemfile.lock +11 -6
  7. data/README.md +225 -13
  8. data/bin/roast +27 -0
  9. data/docs/INSTRUMENTATION.md +42 -1
  10. data/docs/ITERATION_SYNTAX.md +119 -0
  11. data/examples/conditional/README.md +161 -0
  12. data/examples/conditional/check_condition/prompt.md +1 -0
  13. data/examples/conditional/simple_workflow.yml +15 -0
  14. data/examples/conditional/workflow.yml +23 -0
  15. data/examples/dot_notation/README.md +37 -0
  16. data/examples/dot_notation/workflow.yml +44 -0
  17. data/examples/exit_on_error/README.md +50 -0
  18. data/examples/exit_on_error/analyze_lint_output/prompt.md +9 -0
  19. data/examples/exit_on_error/apply_fixes/prompt.md +2 -0
  20. data/examples/exit_on_error/workflow.yml +19 -0
  21. data/examples/grading/workflow.yml +5 -1
  22. data/examples/iteration/IMPLEMENTATION.md +88 -0
  23. data/examples/iteration/README.md +68 -0
  24. data/examples/iteration/analyze_complexity/prompt.md +22 -0
  25. data/examples/iteration/generate_recommendations/prompt.md +21 -0
  26. data/examples/iteration/generate_report/prompt.md +129 -0
  27. data/examples/iteration/implement_fix/prompt.md +25 -0
  28. data/examples/iteration/prioritize_issues/prompt.md +24 -0
  29. data/examples/iteration/prompts/analyze_file.md +28 -0
  30. data/examples/iteration/prompts/generate_summary.md +24 -0
  31. data/examples/iteration/prompts/update_report.md +29 -0
  32. data/examples/iteration/prompts/write_report.md +22 -0
  33. data/examples/iteration/read_file/prompt.md +9 -0
  34. data/examples/iteration/select_next_issue/prompt.md +25 -0
  35. data/examples/iteration/simple_workflow.md +39 -0
  36. data/examples/iteration/simple_workflow.yml +58 -0
  37. data/examples/iteration/update_fix_count/prompt.md +26 -0
  38. data/examples/iteration/verify_fix/prompt.md +29 -0
  39. data/examples/iteration/workflow.yml +42 -0
  40. data/examples/openrouter_example/workflow.yml +2 -2
  41. data/examples/workflow_generator/README.md +27 -0
  42. data/examples/workflow_generator/analyze_user_request/prompt.md +34 -0
  43. data/examples/workflow_generator/create_workflow_files/prompt.md +32 -0
  44. data/examples/workflow_generator/get_user_input/prompt.md +14 -0
  45. data/examples/workflow_generator/info_from_roast.rb +22 -0
  46. data/examples/workflow_generator/workflow.yml +35 -0
  47. data/lib/roast/errors.rb +9 -0
  48. data/lib/roast/factories/api_provider_factory.rb +61 -0
  49. data/lib/roast/helpers/function_caching_interceptor.rb +1 -1
  50. data/lib/roast/helpers/minitest_coverage_runner.rb +1 -1
  51. data/lib/roast/helpers/prompt_loader.rb +50 -1
  52. data/lib/roast/resources/base_resource.rb +7 -0
  53. data/lib/roast/resources.rb +6 -6
  54. data/lib/roast/tools/ask_user.rb +40 -0
  55. data/lib/roast/tools/cmd.rb +1 -1
  56. data/lib/roast/tools/search_file.rb +1 -1
  57. data/lib/roast/tools/update_files.rb +413 -0
  58. data/lib/roast/tools.rb +12 -1
  59. data/lib/roast/value_objects/api_token.rb +49 -0
  60. data/lib/roast/value_objects/step_name.rb +39 -0
  61. data/lib/roast/value_objects/workflow_path.rb +77 -0
  62. data/lib/roast/value_objects.rb +5 -0
  63. data/lib/roast/version.rb +1 -1
  64. data/lib/roast/workflow/api_configuration.rb +61 -0
  65. data/lib/roast/workflow/base_iteration_step.rb +165 -0
  66. data/lib/roast/workflow/base_step.rb +4 -24
  67. data/lib/roast/workflow/base_workflow.rb +76 -73
  68. data/lib/roast/workflow/command_executor.rb +88 -0
  69. data/lib/roast/workflow/conditional_executor.rb +50 -0
  70. data/lib/roast/workflow/conditional_step.rb +96 -0
  71. data/lib/roast/workflow/configuration.rb +35 -158
  72. data/lib/roast/workflow/configuration_loader.rb +78 -0
  73. data/lib/roast/workflow/configuration_parser.rb +13 -248
  74. data/lib/roast/workflow/context_path_resolver.rb +43 -0
  75. data/lib/roast/workflow/dot_access_hash.rb +198 -0
  76. data/lib/roast/workflow/each_step.rb +86 -0
  77. data/lib/roast/workflow/error_handler.rb +97 -0
  78. data/lib/roast/workflow/expression_utils.rb +36 -0
  79. data/lib/roast/workflow/file_state_repository.rb +3 -2
  80. data/lib/roast/workflow/interpolator.rb +34 -0
  81. data/lib/roast/workflow/iteration_executor.rb +85 -0
  82. data/lib/roast/workflow/llm_boolean_coercer.rb +55 -0
  83. data/lib/roast/workflow/output_handler.rb +35 -0
  84. data/lib/roast/workflow/output_manager.rb +77 -0
  85. data/lib/roast/workflow/parallel_executor.rb +49 -0
  86. data/lib/roast/workflow/repeat_step.rb +75 -0
  87. data/lib/roast/workflow/replay_handler.rb +123 -0
  88. data/lib/roast/workflow/resource_resolver.rb +77 -0
  89. data/lib/roast/workflow/session_manager.rb +6 -2
  90. data/lib/roast/workflow/state_manager.rb +97 -0
  91. data/lib/roast/workflow/step_executor_coordinator.rb +205 -0
  92. data/lib/roast/workflow/step_executor_factory.rb +47 -0
  93. data/lib/roast/workflow/step_executor_registry.rb +79 -0
  94. data/lib/roast/workflow/step_executors/base_step_executor.rb +23 -0
  95. data/lib/roast/workflow/step_executors/hash_step_executor.rb +43 -0
  96. data/lib/roast/workflow/step_executors/parallel_step_executor.rb +54 -0
  97. data/lib/roast/workflow/step_executors/string_step_executor.rb +29 -0
  98. data/lib/roast/workflow/step_finder.rb +97 -0
  99. data/lib/roast/workflow/step_loader.rb +154 -0
  100. data/lib/roast/workflow/step_orchestrator.rb +45 -0
  101. data/lib/roast/workflow/step_runner.rb +23 -0
  102. data/lib/roast/workflow/step_type_resolver.rb +117 -0
  103. data/lib/roast/workflow/workflow_context.rb +60 -0
  104. data/lib/roast/workflow/workflow_executor.rb +90 -209
  105. data/lib/roast/workflow/workflow_initializer.rb +112 -0
  106. data/lib/roast/workflow/workflow_runner.rb +87 -0
  107. data/lib/roast/workflow.rb +3 -0
  108. data/lib/roast.rb +96 -3
  109. data/roast.gemspec +3 -1
  110. data/schema/workflow.json +85 -0
  111. metadata +112 -4
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "roast/value_objects/step_name"
4
+ require "roast/workflow/workflow_context"
5
+ require "roast/workflow/base_step"
6
+ require "roast/workflow/prompt_step"
7
+
8
+ module Roast
9
+ module Workflow
10
+ # Handles loading and instantiation of workflow steps
11
+ class StepLoader
12
+ DEFAULT_MODEL = "openai/gpt-4o-mini"
13
+
14
+ # Custom exception classes
15
+ class StepLoaderError < StandardError
16
+ attr_reader :step_name, :original_error
17
+
18
+ def initialize(message, step_name: nil, original_error: nil)
19
+ @step_name = step_name
20
+ @original_error = original_error
21
+ super(message)
22
+ end
23
+ end
24
+
25
+ class StepNotFoundError < StepLoaderError; end
26
+ class StepExecutionError < StepLoaderError; end
27
+
28
+ attr_reader :context
29
+
30
+ delegate :workflow, :config_hash, :context_path, to: :context
31
+
32
+ def initialize(workflow, config_hash, context_path)
33
+ # Support both old and new initialization patterns
34
+ @context = if workflow.is_a?(WorkflowContext)
35
+ workflow
36
+ else
37
+ WorkflowContext.new(
38
+ workflow: workflow,
39
+ config_hash: config_hash,
40
+ context_path: context_path,
41
+ )
42
+ end
43
+ end
44
+
45
+ # Finds and loads a step by name
46
+ #
47
+ # @param step_name [String, StepName] The name of the step to load
48
+ # @return [BaseStep] The loaded step instance
49
+ def load(step_name)
50
+ name = step_name.is_a?(Roast::ValueObjects::StepName) ? step_name : Roast::ValueObjects::StepName.new(step_name)
51
+
52
+ # First check for a prompt step (contains spaces)
53
+ if name.plain_text?
54
+ step = Roast::Workflow::PromptStep.new(workflow, name: name.to_s, auto_loop: false)
55
+ configure_step(step, name.to_s)
56
+ return step
57
+ end
58
+
59
+ # Look for Ruby file in various locations
60
+ step_file_path = find_step_file(name.to_s)
61
+ if step_file_path
62
+ return load_ruby_step(step_file_path, name.to_s)
63
+ end
64
+
65
+ # Look for step directory
66
+ step_directory = find_step_directory(name.to_s)
67
+ unless step_directory
68
+ raise StepNotFoundError.new("Step directory or file not found: #{name}", step_name: name.to_s)
69
+ end
70
+
71
+ create_step_instance(Roast::Workflow::BaseStep, name.to_s, step_directory)
72
+ end
73
+
74
+ private
75
+
76
+ # Find a Ruby step file in various locations
77
+ def find_step_file(step_name)
78
+ # Check in context path
79
+ rb_file_path = File.join(context_path, "#{step_name}.rb")
80
+ return rb_file_path if File.file?(rb_file_path)
81
+
82
+ # Check in shared directory
83
+ shared_rb_path = File.expand_path(File.join(context_path, "..", "shared", "#{step_name}.rb"))
84
+ return shared_rb_path if File.file?(shared_rb_path)
85
+
86
+ nil
87
+ end
88
+
89
+ # Find a step directory
90
+ def find_step_directory(step_name)
91
+ # Check in context path
92
+ step_path = File.join(context_path, step_name)
93
+ return step_path if File.directory?(step_path)
94
+
95
+ # Check in shared directory
96
+ shared_path = File.expand_path(File.join(context_path, "..", "shared", step_name))
97
+ return shared_path if File.directory?(shared_path)
98
+
99
+ nil
100
+ end
101
+
102
+ # Load a Ruby step from a file
103
+ def load_ruby_step(file_path, step_name)
104
+ $stderr.puts "Requiring step file: #{file_path}"
105
+
106
+ begin
107
+ require file_path
108
+ rescue LoadError => e
109
+ raise StepNotFoundError.new("Failed to load step file: #{e.message}", step_name: step_name, original_error: e)
110
+ rescue SyntaxError => e
111
+ raise StepExecutionError.new("Syntax error in step file: #{e.message}", step_name: step_name, original_error: e)
112
+ end
113
+
114
+ step_class = step_name.classify.constantize
115
+ context = File.dirname(file_path)
116
+ create_step_instance(step_class, step_name, context)
117
+ end
118
+
119
+ # Create and configure a step instance
120
+ def create_step_instance(step_class, step_name, context_path)
121
+ step = step_class.new(workflow, name: step_name, context_path: context_path)
122
+ configure_step(step, step_name)
123
+ step
124
+ end
125
+
126
+ # Configure a step instance with settings from config_hash
127
+ def configure_step(step, step_name)
128
+ step_config = config_hash[step_name]
129
+
130
+ # Always set the model
131
+ step.model = determine_model(step_config)
132
+
133
+ # Pass resource to step if supported
134
+ step.resource = workflow.resource if step.respond_to?(:resource=)
135
+
136
+ # Apply additional configuration if present
137
+ apply_step_configuration(step, step_config) if step_config.present?
138
+ end
139
+
140
+ # Determine which model to use for the step
141
+ def determine_model(step_config)
142
+ step_config&.dig("model") || config_hash["model"] || DEFAULT_MODEL
143
+ end
144
+
145
+ # Apply configuration settings to a step
146
+ def apply_step_configuration(step, step_config)
147
+ step.print_response = step_config["print_response"] if step_config["print_response"].present?
148
+ step.auto_loop = step_config["loop"] if step_config["loop"].present?
149
+ step.json = step_config["json"] if step_config["json"].present?
150
+ step.params = step_config["params"] if step_config["params"].present?
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Roast
4
+ module Workflow
5
+ # Handles the orchestration of step execution, managing the flow and control
6
+ # of individual steps without knowing how to execute them
7
+ #
8
+ # This class is specifically for executing CUSTOM steps defined in the workflow's
9
+ # step directory (e.g., steps/*.rb files). It loads and executes Ruby step files
10
+ # that define a `call` method.
11
+ #
12
+ # The primary method execute_step is used by StepExecutorCoordinator for
13
+ # executing custom Ruby steps.
14
+ #
15
+ # TODO: Consider renaming this class to CustomStepOrchestrator to clarify its purpose
16
+ class StepOrchestrator
17
+ def initialize(workflow, step_loader, state_manager, error_handler, workflow_executor)
18
+ @workflow = workflow
19
+ @step_loader = step_loader
20
+ @state_manager = state_manager
21
+ @error_handler = error_handler
22
+ @workflow_executor = workflow_executor
23
+ end
24
+
25
+ def execute_step(name, exit_on_error: true)
26
+ resource_type = @workflow.respond_to?(:resource) ? @workflow.resource&.type : nil
27
+
28
+ @error_handler.with_error_handling(name, resource_type: resource_type) do
29
+ $stderr.puts "Executing: #{name} (Resource type: #{resource_type || "unknown"})"
30
+
31
+ step_object = @step_loader.load(name)
32
+ step_result = step_object.call
33
+
34
+ # Store result in workflow output
35
+ @workflow.output[name] = step_result
36
+
37
+ # Save state after each step
38
+ @state_manager.save_state(name, step_result)
39
+
40
+ step_result
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Roast
4
+ module Workflow
5
+ # Interface for running workflow steps.
6
+ # This abstraction breaks the circular dependency between executors and the workflow.
7
+ class StepRunner
8
+ def initialize(coordinator)
9
+ @coordinator = coordinator
10
+ end
11
+
12
+ # Execute a list of steps
13
+ def execute_steps(steps)
14
+ @coordinator.execute_steps(steps)
15
+ end
16
+
17
+ # Execute a single step
18
+ def execute_step(step, options = {})
19
+ @coordinator.execute_step(step, options)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Roast
4
+ module Workflow
5
+ # Determines the type of a step and how it should be executed
6
+ class StepTypeResolver
7
+ # Step type constants
8
+ COMMAND_STEP = :command
9
+ GLOB_STEP = :glob
10
+ ITERATION_STEP = :iteration
11
+ CONDITIONAL_STEP = :conditional
12
+ HASH_STEP = :hash
13
+ PARALLEL_STEP = :parallel
14
+ STRING_STEP = :string
15
+ STANDARD_STEP = :standard
16
+
17
+ # Special step names for iterations
18
+ ITERATION_STEPS = ["repeat", "each"].freeze
19
+
20
+ # Special step names for conditionals
21
+ CONDITIONAL_STEPS = ["if", "unless"].freeze
22
+
23
+ class << self
24
+ # Resolve the type of a step
25
+ # @param step [String, Hash, Array] The step to analyze
26
+ # @param context [WorkflowContext] The workflow context
27
+ # @return [Symbol] The step type
28
+ def resolve(step, context = nil)
29
+ case step
30
+ when String
31
+ resolve_string_step(step, context)
32
+ when Hash
33
+ resolve_hash_step(step)
34
+ when Array
35
+ PARALLEL_STEP
36
+ else
37
+ STANDARD_STEP
38
+ end
39
+ end
40
+
41
+ # Check if a step is a command step
42
+ # @param step [String] The step to check
43
+ # @return [Boolean] true if it's a command step
44
+ def command_step?(step)
45
+ step.is_a?(String) && step.start_with?("$(")
46
+ end
47
+
48
+ # Check if a step is a glob pattern
49
+ # @param step [String] The step to check
50
+ # @param context [WorkflowContext, nil] The workflow context
51
+ # @return [Boolean] true if it's a glob pattern
52
+ def glob_step?(step, context = nil)
53
+ return false unless step.is_a?(String) && step.include?("*")
54
+
55
+ # Only treat as glob if we don't have a resource
56
+ context.nil? || !context.has_resource?
57
+ end
58
+
59
+ # Check if a step is an iteration step
60
+ # @param step [Hash] The step to check
61
+ # @return [Boolean] true if it's an iteration step
62
+ def iteration_step?(step)
63
+ return false unless step.is_a?(Hash)
64
+
65
+ step_name = step.keys.first
66
+ ITERATION_STEPS.include?(step_name)
67
+ end
68
+
69
+ # Check if a step is a conditional step
70
+ # @param step [Hash] The step to check
71
+ # @return [Boolean] true if it's a conditional step
72
+ def conditional_step?(step)
73
+ return false unless step.is_a?(Hash)
74
+
75
+ step_name = step.keys.first
76
+ CONDITIONAL_STEPS.include?(step_name)
77
+ end
78
+
79
+ # Extract the step name from various step formats
80
+ # @param step [String, Hash, Array] The step
81
+ # @return [String, nil] The step name or nil
82
+ def extract_name(step)
83
+ case step
84
+ when String
85
+ step
86
+ when Hash
87
+ step.keys.first
88
+ when Array
89
+ nil # Parallel steps don't have a single name
90
+ end
91
+ end
92
+
93
+ private
94
+
95
+ def resolve_string_step(step, context)
96
+ if command_step?(step)
97
+ COMMAND_STEP
98
+ elsif glob_step?(step, context)
99
+ GLOB_STEP
100
+ else
101
+ STRING_STEP
102
+ end
103
+ end
104
+
105
+ def resolve_hash_step(step)
106
+ if iteration_step?(step)
107
+ ITERATION_STEP
108
+ elsif conditional_step?(step)
109
+ CONDITIONAL_STEP
110
+ else
111
+ HASH_STEP
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Roast
4
+ module Workflow
5
+ # Encapsulates common workflow execution context parameters
6
+ # Reduces data clump anti-pattern by grouping related parameters
7
+ class WorkflowContext
8
+ attr_reader :workflow, :config_hash, :context_path
9
+
10
+ # Initialize the workflow context
11
+ # @param workflow [BaseWorkflow] The workflow instance
12
+ # @param config_hash [Hash] The workflow configuration hash
13
+ # @param context_path [String] The context directory path
14
+ def initialize(workflow:, config_hash:, context_path:)
15
+ @workflow = workflow
16
+ @config_hash = config_hash
17
+ @context_path = context_path
18
+ freeze
19
+ end
20
+
21
+ # Create a new context with updated workflow
22
+ # @param new_workflow [BaseWorkflow] The new workflow instance
23
+ # @return [WorkflowContext] A new context with the updated workflow
24
+ def with_workflow(new_workflow)
25
+ self.class.new(
26
+ workflow: new_workflow,
27
+ config_hash: config_hash,
28
+ context_path: context_path,
29
+ )
30
+ end
31
+
32
+ # Check if the workflow has a resource
33
+ # @return [Boolean] true if workflow responds to resource and has one
34
+ def has_resource?
35
+ workflow.respond_to?(:resource) && workflow.resource
36
+ end
37
+
38
+ # Get the resource type from the workflow
39
+ # @return [Symbol, nil] The resource type or nil
40
+ def resource_type
41
+ has_resource? ? workflow.resource.type : nil
42
+ end
43
+
44
+ # Get configuration for a specific step
45
+ # @param step_name [String] The name of the step
46
+ # @return [Hash] The step configuration or empty hash
47
+ def step_config(step_name)
48
+ config_hash[step_name] || {}
49
+ end
50
+
51
+ # Check if a step should exit on error
52
+ # @param step_name [String] The name of the step
53
+ # @return [Boolean] true if the step should exit on error (default true)
54
+ def exit_on_error?(step_name)
55
+ config = step_config(step_name)
56
+ config.is_a?(Hash) ? config.fetch("exit_on_error", true) : true
57
+ end
58
+ end
59
+ end
60
+ end