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,73 @@
1
+ name: interactive_deployment
2
+ description: Interactive deployment workflow with user confirmations
3
+ model: gpt-4o
4
+
5
+ steps:
6
+ # Collect deployment information
7
+ - input:
8
+ prompt: "Which environment do you want to deploy to?"
9
+ name: environment
10
+ type: choice
11
+ options:
12
+ - development
13
+ - staging
14
+ - production
15
+ required: true
16
+
17
+ - input:
18
+ prompt: "Enter the deployment tag/version:"
19
+ name: deploy_tag
20
+ required: true
21
+ default: "latest"
22
+
23
+ # Show deployment plan
24
+ - bash:
25
+ command: |
26
+ echo "==================================="
27
+ echo " DEPLOYMENT PLAN"
28
+ echo "==================================="
29
+ echo "Environment: {{workflow.output.environment}}"
30
+ echo "Version: {{workflow.output.deploy_tag}}"
31
+ echo "Timestamp: {{Date.today}}"
32
+ echo "==================================="
33
+
34
+ # Confirm deployment
35
+ - input:
36
+ prompt: "Deploy {{workflow.output.deploy_tag}} to {{workflow.output.environment}}?"
37
+ type: boolean
38
+ default: false
39
+ name: confirm_deploy
40
+
41
+ # Execute deployment if confirmed
42
+ - if: "{{workflow.output.confirm_deploy}}"
43
+ then:
44
+ - bash:
45
+ command: echo "🚀 Starting deployment to {{workflow.output.environment}}..."
46
+
47
+ # Simulate deployment steps
48
+ - bash:
49
+ command: |
50
+ echo "🚀 Deploying version {{workflow.output.deploy_tag}} to {{workflow.output.environment}}"
51
+ echo "📦 Pulling Docker image: myapp:{{workflow.output.deploy_tag}}"
52
+ sleep 2
53
+ echo "🔄 Rolling out to {{workflow.output.environment}} cluster"
54
+ sleep 2
55
+ echo "✅ Deployment completed successfully!"
56
+
57
+ # Post-deployment verification
58
+ - input:
59
+ prompt: "Run smoke tests?"
60
+ type: boolean
61
+ default: true
62
+ name: run_tests
63
+
64
+ - if: "{{workflow.output.run_tests}}"
65
+ then:
66
+ - bash:
67
+ command: |
68
+ echo "🧪 Running smoke tests..."
69
+ sleep 1
70
+ echo "✅ All tests passed!"
71
+ else:
72
+ - bash:
73
+ command: echo "❌ Deployment cancelled by user."
@@ -15,7 +15,7 @@ Extract the workflow name from the user input JSON and create the workflow in th
15
15
 
16
16
  Steps to complete:
17
17
 
18
- 1. **Create the main directory**: Use Cmd to create the "{{ workflow_name }}" directory
18
+ 1. **Create the main directory**: Use Cmd to create the "<%= workflow_name %>" directory
19
19
  2. **Create step directories**: Create subdirectories for each workflow step
20
20
  3. **Create workflow.yml**: Write the main workflow configuration file
21
21
  4. **Create step prompt files**: Write each step's prompt.md file
data/lib/roast/errors.rb CHANGED
@@ -1,9 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Roast
4
- # Custom error for API resource not found (404) responses
5
- class ResourceNotFoundError < StandardError; end
4
+ module Errors
5
+ # Custom error for API resource not found (404) responses
6
+ class ResourceNotFoundError < StandardError; end
6
7
 
7
- # Custom error for when API authentication fails
8
- class AuthenticationError < StandardError; end
8
+ # Custom error for when API authentication fails
9
+ class AuthenticationError < StandardError; end
10
+ end
9
11
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "roast/helpers/logger"
4
-
5
3
  module Roast
6
4
  module Helpers
7
5
  # Intercepts function dispatching to add caching capabilities
@@ -1,54 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "logger"
4
- require "forwardable"
5
-
6
3
  module Roast
7
4
  module Helpers
8
5
  # Central logger for the Roast application
9
6
  class Logger
10
- extend Forwardable
11
7
  VALID_LOG_LEVELS = ["DEBUG", "INFO", "WARN", "ERROR", "FATAL"].freeze
12
8
 
13
- attr_reader :logger, :log_level
14
-
15
- # Delegate info and warn methods to the underlying logger
16
- def_delegators :logger, :info, :warn
17
-
18
- # Create a specialized debug method that ensures proper functionality
19
- def debug(message)
20
- logger.debug(message)
21
- end
22
-
23
- def error(message)
24
- # Add any custom error handling logic here
25
- logger.error(message)
26
- end
27
-
28
- def fatal(message)
29
- # Add any custom fatal error handling logic here
30
- logger.fatal(message)
31
- end
32
-
33
- def initialize(stdout: $stdout, log_level: ENV["ROAST_LOG_LEVEL"] || "INFO")
34
- @log_level = validate_log_level(log_level)
35
- @logger = create_logger(stdout)
36
- end
9
+ delegate_missing_to :@logger
37
10
 
38
- def log_level=(level)
39
- @log_level = validate_log_level(level)
40
- logger.level = ::Logger.const_get(@log_level)
41
- end
11
+ attr_reader :logger
42
12
 
43
13
  class << self
44
- extend Forwardable
14
+ delegate_missing_to :instance
45
15
 
46
16
  def instance
47
17
  @instance ||= new
48
18
  end
49
19
 
50
- # Delegate logging methods to the singleton instance
51
- def_delegators :instance, :debug, :info, :warn, :error, :fatal
20
+ # Override Kernel#warn to ensure proper delegation
21
+ def warn(*args)
22
+ instance.warn(*args)
23
+ end
52
24
 
53
25
  # For testing purposes
54
26
  def reset
@@ -58,6 +30,11 @@ module Roast
58
30
 
59
31
  private
60
32
 
33
+ def initialize(stdout: $stdout, log_level: ENV["ROAST_LOG_LEVEL"] || "INFO")
34
+ @log_level = validate_log_level(log_level)
35
+ @logger = create_logger(stdout)
36
+ end
37
+
61
38
  def validate_log_level(level)
62
39
  level_str = level.to_s.upcase
63
40
  unless VALID_LOG_LEVELS.include?(level_str)
@@ -2,7 +2,6 @@
2
2
 
3
3
  require "coverage"
4
4
  require "minitest"
5
- require "roast/helpers/logger"
6
5
 
7
6
  # Disable the built-in `at_exit` hook for Minitest before anything else
8
7
  module Minitest
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "erb"
4
-
5
3
  module Roast
6
4
  module Helpers
7
5
  class PromptLoader
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "timeout"
4
+ require "open3"
5
+
6
+ module Roast
7
+ module Helpers
8
+ # Shared timeout handling logic for command-based tools
9
+ #
10
+ # This class provides centralized timeout functionality for executing shell commands
11
+ # with proper process management and resource cleanup.
12
+ #
13
+ # @example Basic usage
14
+ # output, status = TimeoutHandler.call("echo hello", timeout: 5)
15
+ #
16
+ # @example With custom working directory
17
+ # output, status = TimeoutHandler.call("pwd", timeout: 10, working_directory: "/tmp")
18
+ class TimeoutHandler
19
+ DEFAULT_TIMEOUT = 30
20
+ MAX_TIMEOUT = 300
21
+
22
+ class << self
23
+ # Execute a command with timeout using Open3 with proper process cleanup
24
+ # @param command [String] The command to execute
25
+ # @param timeout [Integer] Timeout in seconds
26
+ # @param working_directory [String] Directory to execute in (default: Dir.pwd)
27
+ # @return [Array<String, Integer>] [output, exit_status]
28
+ # @raise [Timeout::Error] When command exceeds timeout duration
29
+ def call(command, timeout: DEFAULT_TIMEOUT, working_directory: Dir.pwd)
30
+ timeout = validate_timeout(timeout)
31
+ output = ""
32
+ exit_status = nil
33
+ wait_thr = nil
34
+
35
+ begin
36
+ Timeout.timeout(timeout) do
37
+ stdin, stdout, stderr, wait_thr = Open3.popen3(command, chdir: working_directory)
38
+ stdin.close # Prevent hanging on stdin-waiting commands
39
+ output = stdout.read + stderr.read
40
+ wait_thr.join
41
+ exit_status = wait_thr.value.exitstatus
42
+
43
+ [stdout, stderr].each(&:close)
44
+ end
45
+ rescue Timeout::Error
46
+ # Clean up any remaining processes to prevent zombies
47
+ cleanup_process(wait_thr) if wait_thr&.alive?
48
+ raise Timeout::Error, "Command '#{command}' in '#{working_directory}' timed out after #{timeout} seconds"
49
+ end
50
+
51
+ [output, exit_status]
52
+ end
53
+
54
+ # Validate and normalize timeout value
55
+ # @param timeout [Integer, nil] Raw timeout value
56
+ # @return [Integer] Validated timeout between 1 and MAX_TIMEOUT
57
+ def validate_timeout(timeout)
58
+ return DEFAULT_TIMEOUT if timeout.nil? || timeout <= 0
59
+
60
+ [timeout, MAX_TIMEOUT].min
61
+ end
62
+
63
+ private
64
+
65
+ # Clean up process on timeout to prevent zombie processes
66
+ # @param wait_thr [Process::Waiter] The process thread to clean up
67
+ def cleanup_process(wait_thr)
68
+ return unless wait_thr&.alive?
69
+
70
+ pid = wait_thr.pid
71
+ # First try graceful termination
72
+ Process.kill("TERM", pid)
73
+ sleep(0.1)
74
+
75
+ # Force kill if still alive
76
+ if wait_thr.alive?
77
+ Process.kill("KILL", pid)
78
+ end
79
+ rescue Errno::ESRCH
80
+ # Process already terminated, which is fine
81
+ rescue Errno::EPERM
82
+ # Permission denied - process may be owned by different user
83
+ Roast::Helpers::Logger.debug("Could not kill process #{pid}: Permission denied")
84
+ rescue => e
85
+ # Catch any other unexpected errors during cleanup
86
+ Roast::Helpers::Logger.debug("Unexpected error during process cleanup: #{e.message}")
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -1,9 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "json"
4
- require "net/http"
5
- require "uri"
6
-
7
3
  module Roast
8
4
  module Resources
9
5
  # Resource implementation for API endpoints using Fetch API-style format
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "net/http"
4
- require "uri"
5
-
6
3
  module Roast
7
4
  module Resources
8
5
  # Resource implementation for URLs
@@ -1,13 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "roast/resources/base_resource"
4
- require "roast/resources/file_resource"
5
- require "roast/resources/directory_resource"
6
- require "roast/resources/url_resource"
7
- require "roast/resources/api_resource"
8
- require "roast/resources/none_resource"
9
- require "uri"
10
-
11
3
  module Roast
12
4
  # The Resources module contains classes for handling different types of resources
13
5
  # that workflows can operate on. Each resource type implements a common interface.
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Roast
4
+ module Services
5
+ class ContextThresholdChecker
6
+ # Default max tokens if not specified (128k for GPT-4)
7
+ DEFAULT_MAX_TOKENS = 128_000
8
+
9
+ # Warning threshold as percentage of compaction threshold
10
+ WARNING_THRESHOLD_RATIO = 0.9
11
+
12
+ # Critical threshold as percentage of max tokens
13
+ CRITICAL_THRESHOLD_RATIO = 0.95
14
+
15
+ def should_compact?(token_count, threshold, max_tokens)
16
+ max_tokens ||= DEFAULT_MAX_TOKENS
17
+ token_count >= (max_tokens * threshold)
18
+ end
19
+
20
+ def check_warning_threshold(token_count, compaction_threshold, max_tokens)
21
+ max_tokens ||= DEFAULT_MAX_TOKENS
22
+ percentage_used = (token_count.to_f / max_tokens * 100).round
23
+
24
+ if token_count >= (max_tokens * CRITICAL_THRESHOLD_RATIO)
25
+ {
26
+ level: :critical,
27
+ percentage_used: percentage_used,
28
+ tokens_used: token_count,
29
+ max_tokens: max_tokens,
30
+ }
31
+ elsif token_count >= (max_tokens * compaction_threshold * WARNING_THRESHOLD_RATIO)
32
+ {
33
+ level: :approaching_limit,
34
+ percentage_used: percentage_used,
35
+ tokens_used: token_count,
36
+ max_tokens: max_tokens,
37
+ }
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Roast
4
+ module Services
5
+ class TokenCountingService
6
+ # Approximate character-to-token ratio for English text
7
+ # Based on OpenAI's rule of thumb: ~4 characters per token
8
+ CHARS_PER_TOKEN = 4.0
9
+
10
+ # Base token overhead for message structure
11
+ MESSAGE_OVERHEAD_TOKENS = 3
12
+
13
+ def count_messages(messages)
14
+ return 0 if messages.nil? || messages.empty?
15
+
16
+ messages.sum do |message|
17
+ count_message(message)
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def count_message(message)
24
+ return 0 if message.nil?
25
+
26
+ role_tokens = estimate_tokens(message[:role].to_s)
27
+ content_tokens = estimate_tokens(message[:content].to_s)
28
+
29
+ # Don't add overhead for empty messages
30
+ return 0 if role_tokens == 0 && content_tokens == 0
31
+
32
+ # Add overhead for message structure and special tokens
33
+ role_tokens + content_tokens + MESSAGE_OVERHEAD_TOKENS
34
+ end
35
+
36
+ def estimate_tokens(text)
37
+ return 0 if text.nil? || text.empty?
38
+
39
+ # Simple character-based estimation
40
+ (text.length / CHARS_PER_TOKEN).ceil
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cli/ui"
4
+
5
+ module Roast
6
+ module Tools
7
+ module ApplyDiff
8
+ extend self
9
+
10
+ class << self
11
+ def included(base)
12
+ base.class_eval do
13
+ function(
14
+ :apply_diff,
15
+ "Show a diff to the user and apply changes based on their yes/no response",
16
+ file_path: { type: "string", description: "Path to the file to modify" },
17
+ old_content: { type: "string", description: "The current content to be replaced" },
18
+ new_content: { type: "string", description: "The new content to replace with" },
19
+ description: { type: "string", description: "Optional description of the change", required: false },
20
+ ) do |params|
21
+ Roast::Tools::ApplyDiff.call(
22
+ params[:file_path],
23
+ params[:old_content],
24
+ params[:new_content],
25
+ params[:description],
26
+ )
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ def call(file_path, old_content, new_content, description = nil)
33
+ unless File.exist?(file_path)
34
+ error_msg = "File not found: #{file_path}"
35
+ Roast::Helpers::Logger.error(error_msg + "\n")
36
+ return error_msg
37
+ end
38
+
39
+ current_content = File.read(file_path)
40
+ unless current_content.include?(old_content)
41
+ error_msg = "Old content not found in file: #{file_path}"
42
+ Roast::Helpers::Logger.error(error_msg + "\n")
43
+ return error_msg
44
+ end
45
+
46
+ # Show the diff
47
+ show_diff(file_path, old_content, new_content, description)
48
+
49
+ # Ask for confirmation
50
+ prompt_text = "Apply this change? (y/n)"
51
+ response = ::CLI::UI::Prompt.ask(prompt_text)
52
+
53
+ if response.to_s.downcase.start_with?("y")
54
+ # Apply the change
55
+ updated_content = current_content.gsub(old_content, new_content)
56
+ File.write(file_path, updated_content)
57
+
58
+ success_msg = "✅ Changes applied to #{file_path}"
59
+ Roast::Helpers::Logger.info(success_msg + "\n")
60
+ success_msg
61
+ else
62
+ cancel_msg = "❌ Changes cancelled for #{file_path}"
63
+ Roast::Helpers::Logger.info(cancel_msg + "\n")
64
+ cancel_msg
65
+ end
66
+ rescue StandardError => e
67
+ error_message = "Error applying diff: #{e.message}"
68
+ Roast::Helpers::Logger.error(error_message + "\n")
69
+ Roast::Helpers::Logger.debug(e.backtrace.join("\n") + "\n") if ENV["DEBUG"]
70
+ error_message
71
+ end
72
+
73
+ private
74
+
75
+ def show_diff(file_path, old_content, new_content, description)
76
+ require "tmpdir"
77
+
78
+ Roast::Helpers::Logger.info("📝 Proposed change for #{file_path}:\n")
79
+
80
+ if description
81
+ Roast::Helpers::Logger.info("Description: #{description}\n\n")
82
+ end
83
+
84
+ # Create temporary files for git diff
85
+ Dir.mktmpdir do |tmpdir|
86
+ # Write current content with old_content replaced by new_content
87
+ current_content = File.read(file_path)
88
+ updated_content = current_content.gsub(old_content, new_content)
89
+
90
+ # Create temp file with the proposed changes
91
+ temp_file = File.join(tmpdir, File.basename(file_path))
92
+ File.write(temp_file, updated_content)
93
+
94
+ # Run git diff
95
+ diff_output = %x(git diff --no-index --no-prefix "#{file_path}" "#{temp_file}" 2>/dev/null)
96
+
97
+ if diff_output.empty?
98
+ Roast::Helpers::Logger.info("No differences found (files are identical)\n")
99
+ else
100
+ # Clean up the diff output - remove temp file paths and use relative paths with colors
101
+ cleaned_diff = diff_output.lines.map do |line|
102
+ case line
103
+ when /^diff --git /
104
+ ::CLI::UI.fmt("{{bold:diff --git a/#{file_path} b/#{file_path}}}")
105
+ when /^--- /
106
+ ::CLI::UI.fmt("{{red:--- a/#{file_path}}}")
107
+ when /^\+\+\+ /
108
+ ::CLI::UI.fmt("{{green:+++ b/#{file_path}}}")
109
+ when /^@@/
110
+ ::CLI::UI.fmt("{{cyan:#{line.chomp}}}")
111
+ when /^-/
112
+ ::CLI::UI.fmt("{{red:#{line.chomp}}}")
113
+ when /^\+/
114
+ ::CLI::UI.fmt("{{green:#{line.chomp}}}")
115
+ else
116
+ line.chomp
117
+ end
118
+ end.join("\n")
119
+
120
+ Roast::Helpers::Logger.info("#{cleaned_diff}\n")
121
+ end
122
+ end
123
+
124
+ Roast::Helpers::Logger.info("\n")
125
+ end
126
+ end
127
+ end
128
+ end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "roast/helpers/logger"
4
-
5
3
  module Roast
6
4
  module Tools
7
5
  module AskUser
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "English"
4
4
  require "roast/helpers/logger"
5
+ require "roast/helpers/timeout_handler"
5
6
 
6
7
  module Roast
7
8
  module Tools
@@ -15,14 +16,15 @@ module Roast
15
16
  :bash,
16
17
  "Execute any bash command without restrictions. ⚠️ WARNING: Use only in trusted environments!",
17
18
  command: { type: "string", description: "The bash command to execute" },
19
+ timeout: { type: "integer", description: "Timeout in seconds (optional, default: 30)", required: false },
18
20
  ) do |params|
19
- Roast::Tools::Bash.call(params[:command])
21
+ Roast::Tools::Bash.call(params[:command], timeout: params[:timeout])
20
22
  end
21
23
  end
22
24
  end
23
25
  end
24
26
 
25
- def call(command)
27
+ def call(command, timeout: 30)
26
28
  Roast::Helpers::Logger.info("🚀 Executing bash command: #{command}\n")
27
29
 
28
30
  # Show warning unless explicitly disabled
@@ -30,15 +32,16 @@ module Roast
30
32
  Roast::Helpers::Logger.warn("⚠️ WARNING: Unrestricted bash execution - use with caution!\n")
31
33
  end
32
34
 
33
- # Execute the command without any restrictions
34
- result = ""
35
- IO.popen("#{command} 2>&1", chdir: Dir.pwd) do |io|
36
- result = io.read
37
- end
38
-
39
- exit_status = $CHILD_STATUS.exitstatus
35
+ result, exit_status = Roast::Helpers::TimeoutHandler.call(
36
+ "#{command} 2>&1",
37
+ timeout: timeout,
38
+ working_directory: Dir.pwd,
39
+ )
40
40
 
41
41
  format_output(command, result, exit_status)
42
+ rescue Timeout::Error => e
43
+ Roast::Helpers::Logger.error(e.message + "\n")
44
+ e.message
42
45
  rescue StandardError => e
43
46
  handle_error(e)
44
47
  end