roast-ai 0.4.0 → 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 (118) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yaml +2 -2
  3. data/CHANGELOG.md +65 -0
  4. data/CLAUDE.md +55 -9
  5. data/Gemfile +1 -0
  6. data/Gemfile.lock +8 -1
  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/workflow.png +0 -0
  45. data/examples/interpolation/workflow.png +0 -0
  46. data/examples/interpolation/workflow.yml +1 -1
  47. data/examples/iteration/workflow.png +0 -0
  48. data/examples/json_handling/workflow.png +0 -0
  49. data/examples/mcp/database_workflow.png +0 -0
  50. data/examples/mcp/env_demo/workflow.png +0 -0
  51. data/examples/mcp/filesystem_demo/workflow.png +0 -0
  52. data/examples/mcp/github_workflow.png +0 -0
  53. data/examples/mcp/multi_mcp_workflow.png +0 -0
  54. data/examples/mcp/workflow.png +0 -0
  55. data/examples/no_model_fallback/README.md +17 -0
  56. data/examples/no_model_fallback/analyze_file/prompt.md +1 -0
  57. data/examples/no_model_fallback/analyze_patterns/prompt.md +27 -0
  58. data/examples/no_model_fallback/generate_report_for_md/prompt.md +10 -0
  59. data/examples/no_model_fallback/generate_report_for_rb/prompt.md +3 -0
  60. data/examples/no_model_fallback/sample.rb +42 -0
  61. data/examples/no_model_fallback/workflow.yml +19 -0
  62. data/examples/openrouter_example/workflow.png +0 -0
  63. data/examples/pre_post_processing/workflow.png +0 -0
  64. data/examples/rspec_to_minitest/workflow.png +0 -0
  65. data/examples/shared_config/example_with_shared_config/workflow.png +0 -0
  66. data/examples/shared_config/shared.png +0 -0
  67. data/examples/single_target_prepost/workflow.png +0 -0
  68. data/examples/smart_coercion_defaults/workflow.png +0 -0
  69. data/examples/step_configuration/workflow.png +0 -0
  70. data/examples/swarm_example.yml +25 -0
  71. data/examples/tool_config_example/workflow.png +0 -0
  72. data/examples/user_input/funny_name/workflow.png +0 -0
  73. data/examples/user_input/simple_input_demo/workflow.png +0 -0
  74. data/examples/user_input/survey_workflow.png +0 -0
  75. data/examples/user_input/workflow.png +0 -0
  76. data/examples/workflow_generator/workflow.png +0 -0
  77. data/lib/roast/helpers/timeout_handler.rb +91 -0
  78. data/lib/roast/services/context_threshold_checker.rb +42 -0
  79. data/lib/roast/services/token_counting_service.rb +44 -0
  80. data/lib/roast/tools/apply_diff.rb +128 -0
  81. data/lib/roast/tools/bash.rb +15 -9
  82. data/lib/roast/tools/cmd.rb +32 -12
  83. data/lib/roast/tools/coding_agent.rb +64 -9
  84. data/lib/roast/tools/context_summarizer.rb +108 -0
  85. data/lib/roast/tools/swarm.rb +124 -0
  86. data/lib/roast/version.rb +1 -1
  87. data/lib/roast/workflow/agent_step.rb +9 -2
  88. data/lib/roast/workflow/base_iteration_step.rb +3 -2
  89. data/lib/roast/workflow/base_workflow.rb +41 -2
  90. data/lib/roast/workflow/configuration.rb +2 -1
  91. data/lib/roast/workflow/configuration_loader.rb +63 -1
  92. data/lib/roast/workflow/context_manager.rb +89 -0
  93. data/lib/roast/workflow/each_step.rb +1 -1
  94. data/lib/roast/workflow/output_handler.rb +1 -1
  95. data/lib/roast/workflow/repeat_step.rb +1 -1
  96. data/lib/roast/workflow/replay_handler.rb +1 -1
  97. data/lib/roast/workflow/sqlite_state_repository.rb +342 -0
  98. data/lib/roast/workflow/state_manager.rb +2 -2
  99. data/lib/roast/workflow/state_repository_factory.rb +36 -0
  100. data/lib/roast/workflow/step_completion_reporter.rb +27 -0
  101. data/lib/roast/workflow/step_executor_coordinator.rb +19 -18
  102. data/lib/roast/workflow/step_executor_with_reporting.rb +68 -0
  103. data/lib/roast/workflow/step_loader.rb +1 -1
  104. data/lib/roast/workflow/step_name_extractor.rb +84 -0
  105. data/lib/roast/workflow/validation_command.rb +197 -0
  106. data/lib/roast/workflow/validators/base_validator.rb +44 -0
  107. data/lib/roast/workflow/validators/dependency_validator.rb +223 -0
  108. data/lib/roast/workflow/validators/linting_validator.rb +113 -0
  109. data/lib/roast/workflow/validators/schema_validator.rb +90 -0
  110. data/lib/roast/workflow/validators/step_collector.rb +57 -0
  111. data/lib/roast/workflow/validators/validation_orchestrator.rb +52 -0
  112. data/lib/roast/workflow/workflow_executor.rb +11 -4
  113. data/lib/roast/workflow/workflow_runner.rb +6 -0
  114. data/lib/roast/workflow_diagram_generator.rb +298 -0
  115. data/lib/roast.rb +157 -0
  116. data/roast.gemspec +2 -1
  117. data/schema/workflow.json +77 -1
  118. metadata +101 -1
@@ -0,0 +1,6 @@
1
+ Explore the GitHub API and document:
2
+ - Key endpoints for repository management
3
+ - Authentication requirements
4
+ - Rate limiting considerations
5
+
6
+ Format as a structured summary.
@@ -0,0 +1,6 @@
1
+ Create a Ruby client for the GitHub API that includes:
2
+ - Basic authentication
3
+ - Repository listing
4
+ - Repository creation
5
+
6
+ Use the information from the API exploration.
@@ -0,0 +1,20 @@
1
+ description: Example workflow using inline agent prompts with continuation
2
+
3
+ # This example demonstrates using inline agent prompts (direct text after ^)
4
+ # combined with the new parameters
5
+
6
+ steps:
7
+ # Start with a fresh context
8
+ - ^Create a simple Ruby class for managing a todo list with add, remove, and list methods
9
+
10
+ # Continue from previous session to add more features
11
+ - ^continue_adding_features
12
+
13
+ # Use context summary to understand what was built
14
+ - ^document_with_context
15
+
16
+ continue_adding_features:
17
+ continue: true # This continues from the previous agent session
18
+
19
+ document_with_context:
20
+ include_context_summary: true # This will include context about previous steps
@@ -0,0 +1,2 @@
1
+ Now implement the refactoring suggestions you identified.
2
+ Start with the highest priority items.
@@ -0,0 +1,6 @@
1
+ Review all the changes made by the coding agent steps.
2
+ Provide a summary of:
3
+ - What was analyzed
4
+ - What refactorings were applied
5
+ - What documentation was added
6
+ - Any potential issues or concerns
@@ -0,0 +1,27 @@
1
+ description: Example workflow demonstrating agent continuation and context summary features
2
+
3
+ # This example shows how to use the new coding agent parameters:
4
+ # - continue: true - continues from previous agent session
5
+ # - include_context_summary: true - includes workflow context in the prompt
6
+
7
+ target: "**/*.rb"
8
+
9
+ steps:
10
+ # First agent step - starts fresh
11
+ - ^analyze_codebase
12
+
13
+ # Second agent step - continues from previous session
14
+ - ^refactor_code
15
+
16
+ # Third agent step - includes context summary from previous steps
17
+ - ^add_documentation
18
+
19
+ # Final verification step
20
+ - verify_changes
21
+
22
+ # Step configurations
23
+ refactor_code:
24
+ continue: true # Continue from the previous agent session
25
+
26
+ add_documentation:
27
+ include_context_summary: true # Include workflow context in the prompt
Binary file
@@ -0,0 +1,58 @@
1
+ # Apply Diff Demo
2
+
3
+ This example demonstrates the `apply_diff` tool, which shows users a colored diff of proposed changes and applies them only after user confirmation.
4
+
5
+ ## What this workflow does
6
+
7
+ 1. **Creates a sample file** - Generates `hello.txt` with simple text content
8
+ 2. **Applies a simple change** - Uses `apply_diff` to modify the greeting and ask for user confirmation
9
+
10
+ ## Key features demonstrated
11
+
12
+ - **Interactive approval** - The `apply_diff` tool shows a clear, colored diff and waits for user confirmation
13
+ - **Safe modifications** - Changes are only applied when the user explicitly approves them
14
+ - **Colored visualization** - Diff format shows exactly what will be changed with:
15
+ - **Red** lines starting with `-` for removed content
16
+ - **Green** lines starting with `+` for added content
17
+ - **Cyan** line numbers and context (`@@` lines)
18
+ - **Bold** diff headers
19
+ - **Optional descriptions** - You can provide context about why a change is being made
20
+
21
+ ## Running the workflow
22
+
23
+ ```bash
24
+ bin/roast examples/apply_diff_demo/workflow.yml
25
+ ```
26
+
27
+ ## Expected interaction
28
+
29
+ When you run this workflow, you'll see:
30
+
31
+ 1. The workflow creates a simple `hello.txt` file
32
+ 2. It proposes changing "Hello World!" to "Hello, Apply Diff Demo!"
33
+ 3. It shows you a colored diff of the proposed change:
34
+ ```
35
+ 📝 Proposed change for hello.txt:
36
+ Description: Update greeting to be more specific to the demo
37
+
38
+ diff --git a/hello.txt b/hello.txt
39
+ index 1234567..abcdefg 100644
40
+ --- a/hello.txt
41
+ +++ b/hello.txt
42
+ @@ -1,3 +1,3 @@
43
+ -Hello World!
44
+ +Hello, Apply Diff Demo!
45
+ This is a demo file.
46
+ We will modify this file in the next step.
47
+ ```
48
+ 4. It asks for your confirmation: `Apply this change? (y/n)`
49
+ 5. If you say "y", it applies the change; if "n", it cancels
50
+ 6. Finally, it reads the file again to show the result
51
+
52
+ ## Tools used
53
+
54
+ - `Roast::Tools::WriteFile` - Creates the initial sample file
55
+ - `Roast::Tools::ReadFile` - Reads files to show results
56
+ - `Roast::Tools::ApplyDiff` - Shows colored diffs and applies changes with user confirmation
57
+
58
+ This pattern is useful for any workflow where you want to make targeted changes to files but give users control over what actually gets applied.
@@ -0,0 +1,13 @@
1
+ # Apply Simple Change
2
+
3
+ Now use the apply_diff function to modify the `hello.txt` file. Let's change the greeting from "Hello World!" to "Hello, Apply Diff Demo!".
4
+
5
+ Use the apply_diff function with:
6
+ - `file_path`: "hello.txt"
7
+ - `old_content`: "Hello World!"
8
+ - `new_content`: "Hello, Apply Diff Demo!"
9
+ - `description`: "Update greeting to be more specific to the demo"
10
+
11
+ This will show the user a colored diff of the proposed change and ask for their confirmation before applying it.
12
+
13
+ After the change is applied (or declined), read the file again to show the final result.
@@ -0,0 +1,11 @@
1
+ # Create Sample File
2
+
3
+ Create a simple text file called `hello.txt` with the following content:
4
+
5
+ ```
6
+ Hello World!
7
+ This is a demo file.
8
+ We will modify this file in the next step.
9
+ ```
10
+
11
+ Use the write_file function to create this file.
@@ -0,0 +1,24 @@
1
+ # Apply Diff Demo
2
+ #
3
+ # This workflow demonstrates the apply_diff tool which shows users a diff
4
+ # and applies changes based on their confirmation. It's useful for making
5
+ # targeted changes to files with user approval.
6
+
7
+ name: Apply Diff Demo
8
+ model: gpt-4o-mini
9
+
10
+ tools:
11
+ - Roast::Tools::WriteFile
12
+ - Roast::Tools::ReadFile
13
+ - Roast::Tools::ApplyDiff
14
+
15
+ steps:
16
+ - create_sample_file
17
+ - apply_simple_change
18
+
19
+ # Step configurations
20
+ create_sample_file:
21
+ print_response: false
22
+
23
+ apply_simple_change:
24
+ print_response: true
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,43 @@
1
+ # Context Management Demo
2
+
3
+ This example demonstrates Roast's automatic context management feature, which helps prevent workflow failures when conversation history exceeds the LLM's context window.
4
+
5
+ ## Features Demonstrated
6
+
7
+ 1. **Automatic Token Tracking**: Monitors token usage throughout workflow execution
8
+ 2. **Configurable Thresholds**: Set when to trigger warnings or compaction
9
+ 3. **Context Preservation**: Specify critical steps to retain during compaction
10
+
11
+ ## Configuration
12
+
13
+ ```yaml
14
+ context_management:
15
+ enabled: true # Enable context management
16
+ strategy: auto # Compaction strategy (auto, summarize, prune, none)
17
+ threshold: 0.8 # Trigger at 80% of context window
18
+ max_tokens: 10000 # Override default limit (for demo purposes)
19
+ retain_steps: # Steps to always keep in full
20
+ - analyze_requirements
21
+ - generate_summary
22
+ ```
23
+
24
+ ## Running the Demo
25
+
26
+ ```bash
27
+ roast execute context_management_demo
28
+ ```
29
+
30
+ The workflow intentionally generates verbose responses to demonstrate how context management handles large amounts of text without failing.
31
+
32
+ ## What to Observe
33
+
34
+ 1. **Token Usage Warnings**: Watch for warnings as the context approaches limits
35
+ 2. **Automatic Handling**: The workflow continues even with large outputs
36
+ 3. **Preserved Context**: Critical steps remain accessible throughout execution
37
+
38
+ ## Customization
39
+
40
+ Try modifying the configuration:
41
+ - Lower `max_tokens` to trigger compaction sooner
42
+ - Change `strategy` to test different compaction approaches
43
+ - Add more steps to `retain_steps` to preserve additional context
@@ -0,0 +1,42 @@
1
+ name: Context Management Demo
2
+ tools:
3
+ - Roast::Tools::ReadFile
4
+ - Roast::Tools::WriteFile
5
+
6
+ # Context management configuration
7
+ context_management:
8
+ enabled: true
9
+ strategy: auto
10
+ threshold: 0.8 # Trigger compaction at 80% of context window
11
+ max_tokens: 10000 # Demo with smaller limit for testing
12
+ retain_steps:
13
+ - analyze_requirements
14
+ - generate_summary
15
+
16
+ steps:
17
+ - analyze_requirements: |
18
+ Analyze this text and list the key requirements:
19
+
20
+ We need a system that can:
21
+ 1. Process customer orders
22
+ 2. Track inventory levels
23
+ 3. Generate reports
24
+ 4. Handle refunds
25
+ 5. Integrate with payment systems
26
+
27
+ - expand_details: |
28
+ For each requirement from the previous step, provide detailed implementation notes,
29
+ technical considerations, and potential challenges. Be very thorough and verbose
30
+ to help test the context management system.
31
+
32
+ - generate_more_context: |
33
+ Now describe the database schema needed for this system. Include all tables,
34
+ relationships, indexes, and data types. Be extremely detailed.
35
+
36
+ - add_api_design: |
37
+ Design a complete REST API for this system. Include all endpoints, request/response
38
+ formats, authentication, and error handling. Provide examples for each endpoint.
39
+
40
+ - generate_summary: |
41
+ Create a concise executive summary of the system design. Focus on the key decisions
42
+ and trade-offs made during the design process.
Binary file
Binary file
Binary file
Binary file
@@ -1,5 +1,5 @@
1
1
  name: interpolation_example
2
- model: anthropic:claude-3-7-sonnet
2
+ model: gpt-4o-mini
3
3
 
4
4
  tools:
5
5
  - Roast::Tools::ReadFile
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,17 @@
1
+ # No Model Fallback Example
2
+
3
+ This example demonstrates the issue where workflows without explicit model specification do not properly fall back to a default model.
4
+
5
+ ## Purpose
6
+
7
+ This workflow is based on the interpolation example but intentionally omits the `model` field to test the fallback behavior.
8
+
9
+ ## Expected Behavior
10
+
11
+ The workflow should fall back to a default model when no model is specified at the workflow level.
12
+
13
+ ## Usage
14
+
15
+ ```bash
16
+ bin/roast examples/no_model_fallback/workflow.yml --file examples/no_model_fallback/sample.rb
17
+ ```
@@ -0,0 +1 @@
1
+ Analyze the file at: <%= workflow.file %>
@@ -0,0 +1,27 @@
1
+ Extract some patterns about this file and return in json format like this:
2
+
3
+ <json>
4
+ {
5
+ "code_patterns": {
6
+ "class_structure": {
7
+ "name": "Calculator",
8
+ "instance_variables": ["@memory"],
9
+ "method_count": 7,
10
+ "method_types": {
11
+ "constructor": ["initialize"],
12
+ "operations": ["add", "subtract", "multiply", "divide"],
13
+ "accessors": ["memory"],
14
+ "utility": ["clear"]
15
+ }
16
+ },
17
+ "error_handling": {
18
+ "techniques": ["conditional raise", "zero check"],
19
+ "examples": ["raise \"Division by zero!\" if number.zero?"]
20
+ },
21
+ "design_patterns": {
22
+ "state": "Uses instance variable to maintain calculator state",
23
+ "command": "Each operation method modifies the internal state"
24
+ }
25
+ }
26
+ }
27
+ </json>
@@ -0,0 +1,10 @@
1
+ Generate a comprehensive report for the markdown file.
2
+
3
+ File content: {{steps.analyze_file}}
4
+
5
+ Patterns found: {{steps.analyze_patterns}}
6
+
7
+ Please create a detailed report that includes:
8
+ 1. Summary of the file content
9
+ 2. Key patterns identified
10
+ 3. Any recommendations or observations
@@ -0,0 +1,3 @@
1
+ Generate a nicely formatted report based on the following metadata:
2
+
3
+ <%= workflow.output[:analyze_patterns] %>
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Sample Ruby file for testing interpolation in workflows
4
+
5
+ class Calculator
6
+ def initialize
7
+ @memory = 0
8
+ end
9
+
10
+ def add(number)
11
+ @memory += number
12
+ end
13
+
14
+ def subtract(number)
15
+ @memory -= number
16
+ end
17
+
18
+ def multiply(number)
19
+ @memory *= number
20
+ end
21
+
22
+ def divide(number)
23
+ raise "Division by zero!" if number.zero?
24
+
25
+ @memory /= number
26
+ end
27
+
28
+ attr_reader :memory
29
+
30
+ def clear
31
+ @memory = 0
32
+ end
33
+ end
34
+
35
+ # Example usage
36
+ if __FILE__ == $PROGRAM_NAME
37
+ calc = Calculator.new
38
+ calc.add(10)
39
+ calc.multiply(2)
40
+ calc.subtract(5)
41
+ puts "Result: #{calc.memory}"
42
+ end
@@ -0,0 +1,19 @@
1
+ name: no_model_fallback_example
2
+
3
+ tools:
4
+ - Roast::Tools::ReadFile
5
+
6
+ steps:
7
+ - analyze_file
8
+ - analyze_patterns
9
+ - generate_report_for_{{File.extname(workflow.file).sub('.', '')}}
10
+ - '$(echo "Processing completed for file: {{File.basename(workflow.file)}}")'
11
+
12
+ analyze_patterns:
13
+ json: true
14
+
15
+ generate_report_for_rb:
16
+ print_response: true
17
+
18
+ generate_report_for_md:
19
+ print_response: true
Binary file
@@ -0,0 +1,25 @@
1
+ name: Swarm Example Workflow
2
+
3
+ # Example workflow demonstrating Roast's integration with Claude Swarm
4
+ # The Swarm tool is available to the LLM, which can choose to use it when appropriate
5
+
6
+ tools:
7
+ - Roast::Tools::Swarm:
8
+ path: ".swarm.yml" # Optional - will use default locations if not specified
9
+
10
+ steps:
11
+ - orchestrate_refactoring: |
12
+ Help me refactor this codebase for better performance. Coordinate multiple
13
+ Claude agents using the swarm configuration to:
14
+ 1. Analyze the current code structure
15
+ 2. Identify performance bottlenecks
16
+ 3. Implement optimizations
17
+ 4. Ensure backward compatibility
18
+
19
+ - specialized_analysis: |
20
+ Now use the specialized swarm configuration at ./specialized-swarm.yml to run
21
+ a comprehensive code analysis that includes:
22
+ - Architecture review
23
+ - Security audit
24
+ - Documentation generation
25
+ - Test coverage analysis
Binary file
@@ -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