roast-ai 0.4.7 → 0.4.9

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 (311) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +1 -0
  4. data/Gemfile.lock +3 -3
  5. data/README.md +9 -5
  6. data/Rakefile +2 -0
  7. data/dsl/less_simple.rb +112 -0
  8. data/dsl/prototype.rb +17 -0
  9. data/dsl/simple.rb +5 -7
  10. data/dsl/step_communication.rb +18 -0
  11. data/examples/README.md +9 -0
  12. data/examples/available_tools_demo/workflow.yml +1 -1
  13. data/examples/basic_prompt_workflow/workflow.md +1 -0
  14. data/examples/basic_prompt_workflow/workflow.yml +14 -0
  15. data/examples/grading/README.md +1 -26
  16. data/examples/grading/analyze_coverage/prompt.md +1 -1
  17. data/examples/grading/calculate_final_grade.rb +10 -13
  18. data/examples/grading/format_result.rb +5 -8
  19. data/examples/grading/generate_grades/prompt.md +1 -1
  20. data/examples/grading/generate_recommendations/prompt.md +1 -1
  21. data/examples/grading/read_dependencies/prompt.md +0 -1
  22. data/examples/grading/verify_test_helpers/prompt.md +1 -1
  23. data/examples/grading/workflow.md +1 -4
  24. data/examples/grading/workflow.yml +3 -16
  25. data/lib/roast/dsl/cog/config.rb +31 -0
  26. data/lib/roast/dsl/cog/stack.rb +21 -0
  27. data/lib/roast/dsl/cog/store.rb +26 -0
  28. data/lib/roast/dsl/cog.rb +70 -0
  29. data/lib/roast/dsl/cog_execution_context.rb +29 -0
  30. data/lib/roast/dsl/cogs/cmd.rb +55 -0
  31. data/lib/roast/dsl/cogs/graph.rb +53 -0
  32. data/lib/roast/dsl/cogs.rb +65 -0
  33. data/lib/roast/dsl/config_context.rb +54 -0
  34. data/lib/roast/dsl/executor.rb +62 -7
  35. data/lib/roast/dsl/workflow_execution_context.rb +47 -0
  36. data/lib/roast/error.rb +7 -0
  37. data/lib/roast/errors.rb +3 -3
  38. data/lib/roast/graph/edge.rb +25 -0
  39. data/lib/roast/graph/node.rb +40 -0
  40. data/lib/roast/graph/quantum_edge.rb +27 -0
  41. data/lib/roast/graph/threaded_exec.rb +93 -0
  42. data/lib/roast/graph.rb +233 -0
  43. data/lib/roast/resources/api_resource.rb +2 -2
  44. data/lib/roast/resources/url_resource.rb +2 -2
  45. data/lib/roast/tools/apply_diff.rb +1 -1
  46. data/lib/roast/tools/ask_user.rb +1 -1
  47. data/lib/roast/tools/bash.rb +1 -1
  48. data/lib/roast/tools/cmd.rb +2 -2
  49. data/lib/roast/tools/coding_agent.rb +2 -2
  50. data/lib/roast/tools/grep.rb +1 -1
  51. data/lib/roast/tools/read_file.rb +1 -1
  52. data/lib/roast/tools/search_file.rb +1 -1
  53. data/lib/roast/tools/swarm.rb +1 -1
  54. data/lib/roast/tools/update_files.rb +2 -2
  55. data/lib/roast/tools/write_file.rb +1 -1
  56. data/lib/roast/tools.rb +1 -1
  57. data/lib/roast/value_objects/api_token.rb +1 -1
  58. data/lib/roast/value_objects/uri_base.rb +1 -1
  59. data/lib/roast/value_objects/workflow_path.rb +1 -1
  60. data/lib/roast/version.rb +1 -1
  61. data/lib/roast/workflow/base_step.rb +2 -3
  62. data/lib/roast/workflow/base_workflow.rb +38 -2
  63. data/lib/roast/workflow/command_executor.rb +1 -1
  64. data/lib/roast/workflow/configuration_loader.rb +1 -1
  65. data/lib/roast/workflow/error_handler.rb +1 -1
  66. data/lib/roast/workflow/step_executor_registry.rb +1 -1
  67. data/lib/roast/workflow/step_loader.rb +3 -8
  68. data/lib/roast/workflow/workflow_executor.rb +1 -1
  69. data/lib/roast.rb +7 -2
  70. data/sorbet/config +2 -0
  71. data/sorbet/rbi/annotations/.gitattributes +1 -0
  72. data/sorbet/rbi/annotations/activesupport.rbi +495 -0
  73. data/sorbet/rbi/annotations/faraday.rbi +17 -0
  74. data/sorbet/rbi/annotations/minitest.rbi +119 -0
  75. data/sorbet/rbi/annotations/mocha.rbi +34 -0
  76. data/sorbet/rbi/annotations/rainbow.rbi +269 -0
  77. data/sorbet/rbi/annotations/webmock.rbi +9 -0
  78. data/sorbet/rbi/gems/rbs-inline@0.12.0.rbi +2170 -0
  79. data/sorbet/rbi/gems/{rexml@3.4.1.rbi → rexml@3.4.2.rbi} +284 -239
  80. data/sorbet/rbi/shims/lib/roast/dsl/config_context.rbi +11 -0
  81. data/sorbet/rbi/shims/lib/roast/dsl/workflow_execution_context.rbi +11 -0
  82. data/sorbet/rbi/todo.rbi +7 -0
  83. metadata +37 -231
  84. data/CHANGELOG.md +0 -369
  85. data/examples/agent_continue/add_documentation/prompt.md +0 -5
  86. data/examples/agent_continue/add_error_handling/prompt.md +0 -5
  87. data/examples/agent_continue/analyze_codebase/prompt.md +0 -7
  88. data/examples/agent_continue/combined_workflow.yml +0 -24
  89. data/examples/agent_continue/continue_adding_features/prompt.md +0 -4
  90. data/examples/agent_continue/create_integration_tests/prompt.md +0 -3
  91. data/examples/agent_continue/document_with_context/prompt.md +0 -5
  92. data/examples/agent_continue/explore_api/prompt.md +0 -6
  93. data/examples/agent_continue/implement_client/prompt.md +0 -6
  94. data/examples/agent_continue/inline_workflow.yml +0 -20
  95. data/examples/agent_continue/refactor_code/prompt.md +0 -2
  96. data/examples/agent_continue/verify_changes/prompt.md +0 -6
  97. data/examples/agent_continue/workflow.yml +0 -27
  98. data/examples/agent_workflow/README.md +0 -75
  99. data/examples/agent_workflow/apply_refactorings/prompt.md +0 -22
  100. data/examples/agent_workflow/identify_code_smells/prompt.md +0 -15
  101. data/examples/agent_workflow/summarize_improvements/prompt.md +0 -18
  102. data/examples/agent_workflow/workflow.png +0 -0
  103. data/examples/agent_workflow/workflow.yml +0 -16
  104. data/examples/api_workflow/README.md +0 -85
  105. data/examples/api_workflow/fetch_api_data/prompt.md +0 -10
  106. data/examples/api_workflow/generate_report/prompt.md +0 -10
  107. data/examples/api_workflow/prompt.md +0 -10
  108. data/examples/api_workflow/transform_data/prompt.md +0 -10
  109. data/examples/api_workflow/workflow.png +0 -0
  110. data/examples/api_workflow/workflow.yml +0 -30
  111. data/examples/apply_diff_demo/README.md +0 -58
  112. data/examples/apply_diff_demo/apply_simple_change/prompt.md +0 -13
  113. data/examples/apply_diff_demo/create_sample_file/prompt.md +0 -11
  114. data/examples/apply_diff_demo/workflow.yml +0 -24
  115. data/examples/available_tools_demo/workflow.png +0 -0
  116. data/examples/bash_prototyping/README.md +0 -53
  117. data/examples/bash_prototyping/analyze_network/prompt.md +0 -13
  118. data/examples/bash_prototyping/analyze_system/prompt.md +0 -11
  119. data/examples/bash_prototyping/api_testing.png +0 -0
  120. data/examples/bash_prototyping/api_testing.yml +0 -14
  121. data/examples/bash_prototyping/check_processes/prompt.md +0 -11
  122. data/examples/bash_prototyping/generate_report/prompt.md +0 -16
  123. data/examples/bash_prototyping/process_json_response/prompt.md +0 -24
  124. data/examples/bash_prototyping/system_analysis.png +0 -0
  125. data/examples/bash_prototyping/system_analysis.yml +0 -14
  126. data/examples/bash_prototyping/test_public_api/prompt.md +0 -22
  127. data/examples/case_when/README.md +0 -58
  128. data/examples/case_when/detect_language/prompt.md +0 -16
  129. data/examples/case_when/workflow.png +0 -0
  130. data/examples/case_when/workflow.yml +0 -58
  131. data/examples/cmd/README.md +0 -99
  132. data/examples/cmd/analyze_project/prompt.md +0 -57
  133. data/examples/cmd/basic_demo/prompt.md +0 -48
  134. data/examples/cmd/basic_workflow.png +0 -0
  135. data/examples/cmd/basic_workflow.yml +0 -16
  136. data/examples/cmd/check_repository/prompt.md +0 -57
  137. data/examples/cmd/create_and_verify/prompt.md +0 -56
  138. data/examples/cmd/dev_workflow.png +0 -0
  139. data/examples/cmd/dev_workflow.yml +0 -26
  140. data/examples/cmd/explore_project/prompt.md +0 -67
  141. data/examples/cmd/explorer_workflow.png +0 -0
  142. data/examples/cmd/explorer_workflow.yml +0 -21
  143. data/examples/cmd/smart_tool_selection/prompt.md +0 -99
  144. data/examples/coding_agent_with_model.yml +0 -20
  145. data/examples/coding_agent_with_retries.yml +0 -30
  146. data/examples/conditional/README.md +0 -161
  147. data/examples/conditional/check_condition/prompt.md +0 -1
  148. data/examples/conditional/simple_workflow.png +0 -0
  149. data/examples/conditional/simple_workflow.yml +0 -15
  150. data/examples/conditional/workflow.png +0 -0
  151. data/examples/conditional/workflow.yml +0 -23
  152. data/examples/context_management_demo/README.md +0 -43
  153. data/examples/context_management_demo/workflow.yml +0 -42
  154. data/examples/direct_coerce_syntax/README.md +0 -32
  155. data/examples/direct_coerce_syntax/workflow.png +0 -0
  156. data/examples/direct_coerce_syntax/workflow.yml +0 -36
  157. data/examples/dot_notation/README.md +0 -37
  158. data/examples/dot_notation/workflow.png +0 -0
  159. data/examples/dot_notation/workflow.yml +0 -44
  160. data/examples/exit_on_error/README.md +0 -50
  161. data/examples/exit_on_error/analyze_lint_output/prompt.md +0 -9
  162. data/examples/exit_on_error/apply_fixes/prompt.md +0 -2
  163. data/examples/exit_on_error/workflow.png +0 -0
  164. data/examples/exit_on_error/workflow.yml +0 -19
  165. data/examples/grading/js_test_runner +0 -31
  166. data/examples/grading/rb_test_runner +0 -19
  167. data/examples/grading/run_coverage.rb +0 -54
  168. data/examples/grading/workflow.png +0 -0
  169. data/examples/grading/workflow.rb.md +0 -6
  170. data/examples/grading/workflow.ts+tsx.md +0 -6
  171. data/examples/instrumentation.rb +0 -76
  172. data/examples/interpolation/README.md +0 -50
  173. data/examples/interpolation/analyze_file/prompt.md +0 -1
  174. data/examples/interpolation/analyze_patterns/prompt.md +0 -27
  175. data/examples/interpolation/generate_report_for_js/prompt.md +0 -3
  176. data/examples/interpolation/generate_report_for_rb/prompt.md +0 -3
  177. data/examples/interpolation/sample.js +0 -48
  178. data/examples/interpolation/sample.rb +0 -42
  179. data/examples/interpolation/workflow.md +0 -1
  180. data/examples/interpolation/workflow.png +0 -0
  181. data/examples/interpolation/workflow.yml +0 -21
  182. data/examples/iteration/IMPLEMENTATION.md +0 -88
  183. data/examples/iteration/README.md +0 -68
  184. data/examples/iteration/analyze_complexity/prompt.md +0 -22
  185. data/examples/iteration/generate_recommendations/prompt.md +0 -21
  186. data/examples/iteration/generate_report/prompt.md +0 -129
  187. data/examples/iteration/implement_fix/prompt.md +0 -25
  188. data/examples/iteration/prioritize_issues/prompt.md +0 -24
  189. data/examples/iteration/prompts/analyze_file.md +0 -28
  190. data/examples/iteration/prompts/generate_summary.md +0 -24
  191. data/examples/iteration/prompts/update_report.md +0 -29
  192. data/examples/iteration/prompts/write_report.md +0 -22
  193. data/examples/iteration/read_file/prompt.md +0 -9
  194. data/examples/iteration/select_next_issue/prompt.md +0 -25
  195. data/examples/iteration/simple_workflow.md +0 -39
  196. data/examples/iteration/simple_workflow.yml +0 -58
  197. data/examples/iteration/update_fix_count/prompt.md +0 -26
  198. data/examples/iteration/verify_fix/prompt.md +0 -29
  199. data/examples/iteration/workflow.png +0 -0
  200. data/examples/iteration/workflow.yml +0 -42
  201. data/examples/json_handling/README.md +0 -32
  202. data/examples/json_handling/workflow.png +0 -0
  203. data/examples/json_handling/workflow.yml +0 -52
  204. data/examples/mcp/README.md +0 -223
  205. data/examples/mcp/analyze_changes/prompt.md +0 -8
  206. data/examples/mcp/analyze_issues/prompt.md +0 -4
  207. data/examples/mcp/analyze_schema/prompt.md +0 -4
  208. data/examples/mcp/check_data_quality/prompt.md +0 -5
  209. data/examples/mcp/check_documentation/prompt.md +0 -4
  210. data/examples/mcp/create_recommendations/prompt.md +0 -5
  211. data/examples/mcp/database_workflow.png +0 -0
  212. data/examples/mcp/database_workflow.yml +0 -29
  213. data/examples/mcp/env_demo/workflow.png +0 -0
  214. data/examples/mcp/env_demo/workflow.yml +0 -34
  215. data/examples/mcp/fetch_pr_context/prompt.md +0 -4
  216. data/examples/mcp/filesystem_demo/create_test_file/prompt.md +0 -2
  217. data/examples/mcp/filesystem_demo/list_files/prompt.md +0 -6
  218. data/examples/mcp/filesystem_demo/read_with_mcp/prompt.md +0 -7
  219. data/examples/mcp/filesystem_demo/workflow.png +0 -0
  220. data/examples/mcp/filesystem_demo/workflow.yml +0 -38
  221. data/examples/mcp/generate_insights/prompt.md +0 -4
  222. data/examples/mcp/generate_report/prompt.md +0 -6
  223. data/examples/mcp/generate_review/prompt.md +0 -16
  224. data/examples/mcp/github_workflow.png +0 -0
  225. data/examples/mcp/github_workflow.yml +0 -32
  226. data/examples/mcp/multi_mcp_workflow.png +0 -0
  227. data/examples/mcp/multi_mcp_workflow.yml +0 -58
  228. data/examples/mcp/post_review/prompt.md +0 -3
  229. data/examples/mcp/save_report/prompt.md +0 -6
  230. data/examples/mcp/search_issues/prompt.md +0 -2
  231. data/examples/mcp/summarize/prompt.md +0 -1
  232. data/examples/mcp/test_filesystem/prompt.md +0 -6
  233. data/examples/mcp/test_github/prompt.md +0 -8
  234. data/examples/mcp/test_read/prompt.md +0 -1
  235. data/examples/mcp/workflow.png +0 -0
  236. data/examples/mcp/workflow.yml +0 -35
  237. data/examples/no_model_fallback/README.md +0 -17
  238. data/examples/no_model_fallback/analyze_file/prompt.md +0 -1
  239. data/examples/no_model_fallback/analyze_patterns/prompt.md +0 -27
  240. data/examples/no_model_fallback/generate_report_for_md/prompt.md +0 -10
  241. data/examples/no_model_fallback/generate_report_for_rb/prompt.md +0 -3
  242. data/examples/no_model_fallback/sample.rb +0 -42
  243. data/examples/no_model_fallback/workflow.yml +0 -19
  244. data/examples/openrouter_example/README.md +0 -48
  245. data/examples/openrouter_example/analyze_input/prompt.md +0 -16
  246. data/examples/openrouter_example/generate_response/prompt.md +0 -9
  247. data/examples/openrouter_example/workflow.png +0 -0
  248. data/examples/openrouter_example/workflow.yml +0 -12
  249. data/examples/pre_post_processing/README.md +0 -111
  250. data/examples/pre_post_processing/analyze_test_file/prompt.md +0 -23
  251. data/examples/pre_post_processing/improve_test_coverage/prompt.md +0 -17
  252. data/examples/pre_post_processing/optimize_test_performance/prompt.md +0 -25
  253. data/examples/pre_post_processing/post_processing/aggregate_metrics/prompt.md +0 -31
  254. data/examples/pre_post_processing/post_processing/cleanup_environment/prompt.md +0 -28
  255. data/examples/pre_post_processing/post_processing/generate_summary_report/prompt.md +0 -32
  256. data/examples/pre_post_processing/post_processing/output.txt +0 -24
  257. data/examples/pre_post_processing/pre_processing/gather_baseline_metrics/prompt.md +0 -26
  258. data/examples/pre_post_processing/pre_processing/setup_test_environment/prompt.md +0 -11
  259. data/examples/pre_post_processing/validate_changes/prompt.md +0 -24
  260. data/examples/pre_post_processing/workflow.png +0 -0
  261. data/examples/pre_post_processing/workflow.yml +0 -21
  262. data/examples/retry/workflow.yml +0 -23
  263. data/examples/rspec_to_minitest/README.md +0 -68
  264. data/examples/rspec_to_minitest/analyze_spec/prompt.md +0 -30
  265. data/examples/rspec_to_minitest/create_minitest/prompt.md +0 -33
  266. data/examples/rspec_to_minitest/run_and_improve/prompt.md +0 -35
  267. data/examples/rspec_to_minitest/workflow.md +0 -10
  268. data/examples/rspec_to_minitest/workflow.png +0 -0
  269. data/examples/rspec_to_minitest/workflow.yml +0 -40
  270. data/examples/shared_config/README.md +0 -52
  271. data/examples/shared_config/example_with_shared_config/workflow.png +0 -0
  272. data/examples/shared_config/example_with_shared_config/workflow.yml +0 -6
  273. data/examples/shared_config/shared.png +0 -0
  274. data/examples/shared_config/shared.yml +0 -7
  275. data/examples/single_target_prepost/README.md +0 -36
  276. data/examples/single_target_prepost/post_processing/output.txt +0 -27
  277. data/examples/single_target_prepost/pre_processing/gather_dependencies/prompt.md +0 -11
  278. data/examples/single_target_prepost/workflow.png +0 -0
  279. data/examples/single_target_prepost/workflow.yml +0 -20
  280. data/examples/smart_coercion_defaults/README.md +0 -65
  281. data/examples/smart_coercion_defaults/workflow.png +0 -0
  282. data/examples/smart_coercion_defaults/workflow.yml +0 -44
  283. data/examples/step_configuration/README.md +0 -84
  284. data/examples/step_configuration/workflow.png +0 -0
  285. data/examples/step_configuration/workflow.yml +0 -57
  286. data/examples/swarm_example.yml +0 -25
  287. data/examples/tool_config_example/README.md +0 -109
  288. data/examples/tool_config_example/example_step/prompt.md +0 -42
  289. data/examples/tool_config_example/workflow.png +0 -0
  290. data/examples/tool_config_example/workflow.yml +0 -17
  291. data/examples/user_input/README.md +0 -90
  292. data/examples/user_input/funny_name/create_backstory/prompt.md +0 -10
  293. data/examples/user_input/funny_name/workflow.png +0 -0
  294. data/examples/user_input/funny_name/workflow.yml +0 -25
  295. data/examples/user_input/generate_summary/prompt.md +0 -11
  296. data/examples/user_input/simple_input_demo/workflow.png +0 -0
  297. data/examples/user_input/simple_input_demo/workflow.yml +0 -35
  298. data/examples/user_input/survey_workflow.png +0 -0
  299. data/examples/user_input/survey_workflow.yml +0 -71
  300. data/examples/user_input/welcome_message/prompt.md +0 -3
  301. data/examples/user_input/workflow.png +0 -0
  302. data/examples/user_input/workflow.yml +0 -73
  303. data/examples/workflow_generator/README.md +0 -27
  304. data/examples/workflow_generator/analyze_user_request/prompt.md +0 -34
  305. data/examples/workflow_generator/create_workflow_files/prompt.md +0 -32
  306. data/examples/workflow_generator/get_user_input/prompt.md +0 -14
  307. data/examples/workflow_generator/info_from_roast.rb +0 -22
  308. data/examples/workflow_generator/workflow.png +0 -0
  309. data/examples/workflow_generator/workflow.yml +0 -34
  310. data/package-lock.json +0 -6
  311. /data/sorbet/rbi/gems/{rack@2.2.17.rbi → rack@2.2.18.rbi} +0 -0
@@ -0,0 +1,233 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Roast
5
+ class Graph
6
+ class Error < StandardError; end
7
+ class AddEdgeError < Error; end
8
+ class EdgeTopologyError < Error; end
9
+
10
+ #: (Symbol) { (Graph) -> void } -> void
11
+ def subgraph(name, &block)
12
+ subgraph = Graph.new
13
+ block.call(subgraph)
14
+ nodes[name] = Node.new(name, executable: subgraph)
15
+ end
16
+
17
+ #: (Symbol) { () -> void } -> void
18
+ def node(name, &block)
19
+ nodes[name] = Node.new(name, executable: block)
20
+ end
21
+
22
+ #: (from: Symbol | Array[Symbol], ?to: Symbol | Array[Symbol] | nil) ?{ () -> void } -> void
23
+ def edge(from:, to: nil, &block)
24
+ from_nodes = from.is_a?(Array) ? from.map { |from_node| nodes[from_node] } : [nodes[from]].compact
25
+
26
+ if from_nodes.empty?
27
+ raise AddEdgeError, "Cannot create edge from #{from.inspect} to #{to.inspect} because #{from.inspect} does not exist"
28
+ end
29
+
30
+ if block.nil? && !to.nil?
31
+ to_nodes = to.is_a?(Array) ? to.map { |to_node| nodes[to_node] } : [nodes[to]].compact
32
+
33
+ if to_nodes.empty?
34
+ raise AddEdgeError, "Cannot create edge from #{from.inspect} to #{to.inspect} because #{to.inspect} does not exist"
35
+ end
36
+
37
+ from_nodes.each do |from_node|
38
+ to_nodes.each do |to_node|
39
+ insert_edge(Edge.new(T.must(from_node), T.must(to_node)))
40
+ end
41
+ end
42
+ elsif !block.nil? && to.nil?
43
+ from_nodes.each do |from_node|
44
+ quantum_edges[T.must(from_node).name] = QuantumEdge.new(T.must(from_node), T.must(block))
45
+ end
46
+ elsif !block.nil? && !to.nil?
47
+ raise AddEdgeError, "Must provide either a to node or a block, not both"
48
+ else
49
+ raise AddEdgeError, "Must provide either a to node or a block"
50
+ end
51
+ end
52
+
53
+ #: (?Hash[untyped, untyped]?) -> void
54
+ def execute(init_state = nil)
55
+ return if nodes.empty?
56
+
57
+ # HACK: Move the DONE node to the end of the nodes array.
58
+ # In reality we should have a separate "ordered" representation of the nodes, and we store the
59
+ # main thing as a set.
60
+ nodes[:DONE] = T.must(nodes.delete(:DONE))
61
+
62
+ self.state = init_state unless init_state.nil?
63
+
64
+ current_nodes = T.let([T.must(nodes.values.first)], T::Array[Roast::Graph::Node])
65
+
66
+ until current_nodes.any? { |node| T.must(node).done? }
67
+ if current_nodes.size == 1
68
+ T.must(current_nodes.first).execute(state)
69
+ else
70
+ ThreadedExec.new(current_nodes, state).async_execute
71
+ end
72
+
73
+ current_nodes = find_next(current_nodes)
74
+ end
75
+ end
76
+
77
+ private
78
+
79
+ #: () -> Hash[Symbol, Node]
80
+ def nodes
81
+ @nodes ||= { START: Node.new(:START), DONE: Node.new(:DONE) }
82
+ end
83
+
84
+ #: () -> Hash[Symbol, Array[Edge]]
85
+ def edges
86
+ @edges ||= {}
87
+ end
88
+
89
+ #: () -> Hash[Symbol, Roast::Graph::QuantumEdge]
90
+ def quantum_edges
91
+ @quantum_edges ||= {}
92
+ end
93
+
94
+ #: () -> Hash
95
+ def state
96
+ @state ||= {}
97
+ end
98
+
99
+ #: (Hash) -> void
100
+ def state=(new_state)
101
+ raise Error, "State already set, cannot set it again" if !@state.nil? && !new_state.nil?
102
+
103
+ @state = new_state
104
+ end
105
+
106
+ #: (Array[Node]) -> Array[Node]
107
+ def find_next(current_nodes)
108
+ raise Error, "Somehow got an empty array of nodes" if current_nodes.empty?
109
+
110
+ collapse_quantum_edges(current_nodes)
111
+
112
+ next_edges = if current_nodes.size == 1
113
+ next_edges_for_node(T.must(current_nodes.first))
114
+ elsif current_nodes.size > 1
115
+ next_edges_for_nodes(current_nodes)
116
+ end
117
+
118
+ if next_edges.nil?
119
+ raise EdgeTopologyError, "No next edges found for #{current_nodes.map(&:name).join(", ")}, please define edges for this node"
120
+ end
121
+
122
+ next_nodes = next_edges.map(&:to_node).uniq
123
+
124
+ # If we're doing paralell nodes, we need to lookahead to ensuer we join back at the same node.
125
+ if next_nodes.size > 1
126
+ raise EdgeTopologyError, "Parallel execution many to many nodes is not supported" if current_nodes.size != 1
127
+
128
+ collapse_quantum_edges(next_nodes)
129
+ raise_unless_all_point_to_same_next_node?(next_nodes)
130
+ end
131
+
132
+ next_nodes
133
+ end
134
+
135
+ #: (Array[Node]) -> bool
136
+ def raise_unless_all_point_to_same_next_node?(nodes)
137
+ next_nodes = nodes.map { |node| edges_from(node) }.compact.flatten.map(&:to_node).uniq
138
+ # TODO: Deal with when next_nodes here is empty, should be generic "if you define any edges, you must define them all"
139
+ if next_nodes.size > 1
140
+ Roast::Helpers::Logger.info("Next nodes: #{next_nodes.map(&:name).join(", ")}")
141
+ raise EdgeTopologyError, "Parallel nodes #{nodes.map(&:name).join(", ")} have different next nodes: #{next_nodes.inspect}"
142
+ end
143
+
144
+ true
145
+ end
146
+
147
+ #: (Array[Node]) -> void
148
+ def collapse_quantum_edges(current_nodes)
149
+ curr_quantum_edges = current_nodes.map do |node|
150
+ quantum_edges[node.name]
151
+ end.compact
152
+
153
+ return if curr_quantum_edges.empty?
154
+
155
+ if curr_quantum_edges.size > 1
156
+ raise EdgeTopologyError, <<~MANY_Q_EDGES
157
+ Multiple quantum edges for nodes:
158
+ Nodes: #{current_nodes.map(&:name).join(", ")}"
159
+ Quantum Edges: #{quantum_edges.inspect}
160
+ MANY_Q_EDGES
161
+ end
162
+
163
+ edges = curr_quantum_edges.map do |quantum_edge|
164
+ quantum_edge.collapse(state, nodes)
165
+ end
166
+
167
+ edges.flatten.each do |edge|
168
+ insert_edge(edge)
169
+ end
170
+ end
171
+
172
+ #: (Node) -> Array[Edge]
173
+ def next_edges_for_node(current_node)
174
+ next_edges = edges_from(current_node)
175
+ # If the user never defined any edges, we'll just use the next node in the file.
176
+ next_edges ||= [edge_from_next_loaded(current_node.name)].compact if edges.empty?
177
+ T.must(next_edges)
178
+ end
179
+
180
+ #: (Array[Node]) -> Array[Edge]
181
+ def next_edges_for_nodes(current_nodes)
182
+ maybe_next_edges = current_nodes.map { |node| edges_from(node) }.compact.flatten
183
+
184
+ # Verify there are same number of edges as nodes.
185
+ if maybe_next_edges.size != current_nodes.size
186
+ raise EdgeTopologyError, <<~WRONG_NUM_EDGES
187
+ Parallel nodes have different numbers of edges:
188
+ Next Edges: #{maybe_next_edges}
189
+ Parallel nodes: #{current_nodes.map(&:name).join(", ")}
190
+ WRONG_NUM_EDGES
191
+ end
192
+
193
+ # Verify all the edges go to the same place.
194
+ uniq_to_nodes = maybe_next_edges.map(&:to_node).uniq!
195
+ if T.must(uniq_to_nodes).size != 1
196
+ # TODO: Present which edges are going to different places.
197
+ raise EdgeTopologyError, <<~WRONG_NUM_EDGES
198
+ Parallel nodes end up at different nodes:
199
+ Next Edges: #{maybe_next_edges}
200
+ Parallel nodes: #{current_nodes.map(&:name).join(", ")}
201
+ WRONG_NUM_EDGES
202
+ end
203
+
204
+ maybe_next_edges
205
+ end
206
+
207
+ #: (Node) -> Array[Edge]?
208
+ def edges_from(from_node)
209
+ edges[from_node.name]
210
+ end
211
+
212
+ #: (Edge) -> void
213
+ def insert_edge(edge)
214
+ edges[edge.from_node.name] ||= []
215
+ T.must(edges[edge.from_node.name]) << edge
216
+ end
217
+
218
+ #: (Symbol) -> Edge?
219
+ def edge_from_next_loaded(current_node_name)
220
+ next_node = next_loaded_node(current_node_name)
221
+ return if next_node.nil?
222
+
223
+ Edge.new(T.must(nodes[current_node_name]), next_node)
224
+ end
225
+
226
+ #: (Symbol) -> Node?
227
+ def next_loaded_node(current_node_name)
228
+ current_index = nodes.keys.index(current_node_name)
229
+ next_index = (T.must(current_index) + 1)
230
+ nodes.values[next_index]
231
+ end
232
+ end
233
+ end
@@ -74,7 +74,7 @@ module Roast
74
74
 
75
75
  # Consider 2xx and 3xx as success
76
76
  response.code.to_i < 400
77
- rescue StandardError => e
77
+ rescue Roast::Error => e
78
78
  # Log the error but don't crash
79
79
  Roast::Helpers::Logger.error("Error checking API existence: #{e.message}")
80
80
  false
@@ -96,7 +96,7 @@ module Roast
96
96
  begin
97
97
  uri = URI.parse(target)
98
98
  Net::HTTP.get(uri)
99
- rescue StandardError => e
99
+ rescue Roast::Error => e
100
100
  # Log the error but don't crash
101
101
  Roast::Helpers::Logger.error("Error fetching API contents: #{e.message}")
102
102
  nil
@@ -23,7 +23,7 @@ module Roast
23
23
 
24
24
  # Consider 2xx and 3xx as success
25
25
  response.code.to_i < 400
26
- rescue StandardError => e
26
+ rescue Roast::Error => e
27
27
  # Log the error but don't crash
28
28
  Roast::Helpers::Logger.error("Error checking URL existence: #{e.message}")
29
29
  false
@@ -36,7 +36,7 @@ module Roast
36
36
  begin
37
37
  uri = URI.parse(target)
38
38
  Net::HTTP.get(uri)
39
- rescue StandardError => e
39
+ rescue Roast::Error => e
40
40
  # Log the error but don't crash
41
41
  Roast::Helpers::Logger.error("Error fetching URL contents: #{e.message}")
42
42
  nil
@@ -64,7 +64,7 @@ module Roast
64
64
  Roast::Helpers::Logger.info(cancel_msg + "\n")
65
65
  cancel_msg
66
66
  end
67
- rescue StandardError => e
67
+ rescue Roast::Error => e
68
68
  error_message = "Error applying diff: #{e.message}"
69
69
  Roast::Helpers::Logger.error(error_message + "\n")
70
70
  Roast::Helpers::Logger.debug(e.backtrace.join("\n") + "\n") if ENV["DEBUG"]
@@ -28,7 +28,7 @@ module Roast
28
28
 
29
29
  Roast::Helpers::Logger.info("User responded: #{response}\n")
30
30
  response
31
- rescue StandardError => e
31
+ rescue Roast::Error => e
32
32
  "Error getting user input: #{e.message}".tap do |error_message|
33
33
  Roast::Helpers::Logger.error(error_message + "\n")
34
34
  Roast::Helpers::Logger.debug(e.backtrace.join("\n") + "\n") if ENV["DEBUG"]
@@ -43,7 +43,7 @@ module Roast
43
43
  rescue Timeout::Error => e
44
44
  Roast::Helpers::Logger.error(e.message + "\n")
45
45
  e.message
46
- rescue StandardError => e
46
+ rescue Roast::Error => e
47
47
  handle_error(e)
48
48
  end
49
49
 
@@ -93,7 +93,7 @@ module Roast
93
93
  Roast::Helpers::Logger.info("🔧 Running command: #{full_command}\n")
94
94
 
95
95
  execute_command(full_command, command_prefix, timeout)
96
- rescue StandardError => e
96
+ rescue Roast::Error => e
97
97
  handle_error(e)
98
98
  end
99
99
 
@@ -108,7 +108,7 @@ module Roast
108
108
  command_prefix = command.split(" ").first
109
109
 
110
110
  execute_command(command, command_prefix, timeout)
111
- rescue StandardError => e
111
+ rescue Roast::Error => e
112
112
  handle_error(e)
113
113
  end
114
114
 
@@ -7,7 +7,7 @@ module Roast
7
7
  extend self
8
8
  include Roast::Helpers::MetadataAccess
9
9
 
10
- class CodingAgentError < StandardError; end
10
+ class CodingAgentError < Roast::Error; end
11
11
 
12
12
  CONFIG_CODING_AGENT_COMMAND = "coding_agent_command"
13
13
  private_constant :CONFIG_CODING_AGENT_COMMAND
@@ -59,7 +59,7 @@ module Roast
59
59
  Roast::Helpers::Logger.debug(e.backtrace.join("\n") + "\n") if ENV["DEBUG"]
60
60
  end
61
61
  Roast::Helpers::Logger.error("🤖 CodingAgent did not complete successfully after multiple retries")
62
- rescue StandardError => e
62
+ rescue Roast::Error => e
63
63
  "🤖 Error running CodingAgent: #{e.message}".tap do |error_message|
64
64
  Roast::Helpers::Logger.error(error_message + "\n")
65
65
  Roast::Helpers::Logger.debug(e.backtrace.join("\n") + "\n") if ENV["DEBUG"]
@@ -47,7 +47,7 @@ module Roast
47
47
  else
48
48
  stdout
49
49
  end
50
- rescue StandardError => e
50
+ rescue Roast::Error => e
51
51
  "Error grepping for string: #{e.message}".tap do |error_message|
52
52
  Roast::Helpers::Logger.error(error_message + "\n")
53
53
  Roast::Helpers::Logger.debug(e.backtrace.join("\n") + "\n") if ENV["DEBUG"]
@@ -39,7 +39,7 @@ module Roast
39
39
  else
40
40
  File.read(path)
41
41
  end
42
- rescue StandardError => e
42
+ rescue Roast::Error => e
43
43
  "Error reading file: #{e.message}".tap do |error_message|
44
44
  Roast::Helpers::Logger.error(error_message + "\n")
45
45
  Roast::Helpers::Logger.debug(e.backtrace.join("\n") + "\n") if ENV["DEBUG"]
@@ -44,7 +44,7 @@ module Roast
44
44
 
45
45
  results.map { |result| File.join(path, result) }.join("\n") # purposely give the AI list of actual paths so that it can read without searching first
46
46
  end
47
- rescue StandardError => e
47
+ rescue Roast::Error => e
48
48
  "Error searching for '#{glob_pattern}' in '#{path}': #{e.message}".tap do |error_message|
49
49
  Roast::Helpers::Logger.error(error_message + "\n")
50
50
  Roast::Helpers::Logger.debug(e.backtrace.join("\n") + "\n") if ENV["DEBUG"]
@@ -60,7 +60,7 @@ module Roast
60
60
  Roast::Helpers::Logger.info("🐝 Running Claude Swarm with config: #{config_path}\n")
61
61
 
62
62
  execute_swarm(prompt, config_path)
63
- rescue StandardError => e
63
+ rescue Roast::Error => e
64
64
  handle_error(e)
65
65
  end
66
66
 
@@ -70,7 +70,7 @@ module Roast
70
70
 
71
71
  # Apply changes atomically
72
72
  apply_changes(file_changes, base_path, create_files)
73
- rescue StandardError => e
73
+ rescue Roast::Error => e
74
74
  "Error applying patch: #{e.message}".tap do |error_message|
75
75
  Roast::Helpers::Logger.error(error_message + "\n")
76
76
  Roast::Helpers::Logger.debug(e.backtrace.join("\n") + "\n") if ENV["DEBUG"]
@@ -316,7 +316,7 @@ module Roast
316
316
  end
317
317
 
318
318
  "Successfully applied patch to #{modified_files.size} file(s): #{modified_files.join(", ")}"
319
- rescue StandardError => e
319
+ rescue Roast::Error => e
320
320
  # Restore backups if any change fails
321
321
  backup_files.each do |path, content|
322
322
  File.write(path, content) if File.exist?(path)
@@ -49,7 +49,7 @@ module Roast
49
49
  Roast::Helpers::Logger.error(restriction_message)
50
50
  "Error: Path must start with '#{restrict_path}' to use the write_file tool, try again."
51
51
  end
52
- rescue StandardError => e
52
+ rescue Roast::Error => e
53
53
  "Error writing file: #{e.message}".tap do |error_message|
54
54
  Roast::Helpers::Logger.error(error_message + "\n")
55
55
  Roast::Helpers::Logger.debug(e.backtrace.join("\n") + "\n") if ENV["DEBUG"]
data/lib/roast/tools.rb CHANGED
@@ -24,7 +24,7 @@ module Roast
24
24
 
25
25
  #{File.read(file)}
26
26
  PROMPT
27
- rescue StandardError => e
27
+ rescue Roast::Error => e
28
28
  Roast::Helpers::Logger.error("In current directory: #{Dir.pwd}\n")
29
29
  Roast::Helpers::Logger.error("Error reading file #{file}\n")
30
30
 
@@ -5,7 +5,7 @@ module Roast
5
5
  module ValueObjects
6
6
  # Value object representing an API token with validation
7
7
  class ApiToken
8
- class InvalidTokenError < StandardError; end
8
+ class InvalidTokenError < Roast::Error; end
9
9
 
10
10
  attr_reader :value
11
11
 
@@ -5,7 +5,7 @@ module Roast
5
5
  module ValueObjects
6
6
  # Value object representing a URI base with validation
7
7
  class UriBase
8
- class InvalidUriBaseError < StandardError; end
8
+ class InvalidUriBaseError < Roast::Error; end
9
9
 
10
10
  attr_reader :value
11
11
 
@@ -5,7 +5,7 @@ module Roast
5
5
  module ValueObjects
6
6
  # Value object representing a workflow file path with validation and resolution
7
7
  class WorkflowPath
8
- class InvalidPathError < StandardError; end
8
+ class InvalidPathError < Roast::Error; end
9
9
 
10
10
  attr_reader :value
11
11
 
data/lib/roast/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Roast
5
- VERSION = "0.4.7"
5
+ VERSION = "0.4.9"
6
6
  end
@@ -10,10 +10,9 @@ module Roast
10
10
  delegate :append_to_final_output, :transcript, to: :workflow
11
11
  delegate_missing_to :workflow
12
12
 
13
- # TODO: is this really the model we want to default to, and is this the right place to set it?
14
- def initialize(workflow, model: "anthropic:claude-opus-4", name: nil, context_path: nil)
13
+ def initialize(workflow, model: nil, name: nil, context_path: nil)
15
14
  @workflow = workflow
16
- @model = model
15
+ @model = model || workflow.model || StepLoader::DEFAULT_MODEL
17
16
  @name = normalize_name(name)
18
17
  @context_path = context_path || ContextPathResolver.resolve(self.class)
19
18
  @print_response = false
@@ -113,7 +113,7 @@ module Roast
113
113
  log_and_raise_error(error, message, step_model || model, kwargs, execution_time)
114
114
  rescue => e
115
115
  execution_time = Time.now - start_time
116
- log_and_raise_error(e, e.message, step_model || model, kwargs, execution_time)
116
+ log_and_raise_error(e, enhanced_message(e), step_model || model, kwargs, execution_time)
117
117
  end
118
118
 
119
119
  def with_model(model)
@@ -142,7 +142,43 @@ module Roast
142
142
  execution_time: execution_time,
143
143
  })
144
144
 
145
- raise error
145
+ # If we have an enhanced message, create a new error with it
146
+ if message != error.message
147
+ new_error = error.class.new(message)
148
+ new_error.set_backtrace(error.backtrace) if error.backtrace
149
+ raise new_error
150
+ else
151
+ raise error
152
+ end
153
+ end
154
+
155
+ def enhanced_message(error)
156
+ original_message = error.message
157
+
158
+ url, status = if error.respond_to?(:response) && error.response.is_a?(Hash)
159
+ [error.response[:url], error.response[:status]]
160
+ elsif error.respond_to?(:response_status)
161
+ [nil, error.response_status]
162
+ end
163
+
164
+ message = if url && status
165
+ "API call to #{url} failed with status #{status}: #{original_message}"
166
+ elsif status
167
+ "API call failed with status #{status}: #{original_message}"
168
+ else
169
+ original_message
170
+ end
171
+
172
+ message += if error.respond_to?(:response_body)
173
+ body = error.response_body
174
+ error_detail = body.is_a?(Hash) ? body.dig("error", "message") : body.to_s
175
+
176
+ if error_detail && !error_detail.empty? && !message.include?(error_detail)
177
+ " (#{error_detail})"
178
+ end
179
+ end
180
+
181
+ message
146
182
  end
147
183
 
148
184
  def read_sidecar_prompt
@@ -4,7 +4,7 @@
4
4
  module Roast
5
5
  module Workflow
6
6
  class CommandExecutor
7
- class CommandExecutionError < StandardError
7
+ class CommandExecutionError < Roast::Error
8
8
  attr_reader :command, :exit_status, :original_error, :output
9
9
 
10
10
  def initialize(message, command:, exit_status: nil, original_error: nil)
@@ -5,7 +5,7 @@ module Roast
5
5
  module Workflow
6
6
  # Handles loading and parsing of workflow configuration files
7
7
  class ConfigurationLoader
8
- class ValidationError < StandardError; end
8
+ class ValidationError < Roast::Error; end
9
9
 
10
10
  class << self
11
11
  # Load configuration from a YAML file
@@ -27,7 +27,7 @@ module Roast
27
27
  current_attempt += 1
28
28
  result = block.call
29
29
  break
30
- rescue StandardError => e
30
+ rescue Roast::Error => e
31
31
  remaining_attempts = maximum_attempts - current_attempt
32
32
  raise e if remaining_attempts == 0
33
33
 
@@ -6,7 +6,7 @@ module Roast
6
6
  # Registry pattern for step executors - eliminates case statements
7
7
  # and follows Open/Closed Principle
8
8
  class StepExecutorRegistry
9
- class UnknownStepTypeError < StandardError; end
9
+ class UnknownStepTypeError < Roast::Error; end
10
10
 
11
11
  @executors = {}
12
12
  @type_matchers = []
@@ -8,7 +8,7 @@ module Roast
8
8
  DEFAULT_MODEL = "gpt-4o-mini"
9
9
 
10
10
  # Custom exception classes
11
- class StepLoaderError < StandardError
11
+ class StepLoaderError < Roast::Error
12
12
  attr_reader :step_name, :original_error
13
13
 
14
14
  def initialize(message, step_name: nil, original_error: nil)
@@ -208,8 +208,8 @@ module Roast
208
208
  def configure_step(step, step_name, is_last_step: nil)
209
209
  step_config = config_hash[step_name]
210
210
 
211
- # Always set the model
212
- step.model = determine_model(step_config)
211
+ # Only set the model if explicitly specified for this step
212
+ step.model = step_config["model"] if step_config&.key?("model")
213
213
 
214
214
  # Pass resource to step if supported
215
215
  step.resource = workflow.resource if step.respond_to?(:resource=)
@@ -223,11 +223,6 @@ module Roast
223
223
  end
224
224
  end
225
225
 
226
- # Determine which model to use for the step
227
- def determine_model(step_config)
228
- step_config&.dig("model") || config_hash["model"] || DEFAULT_MODEL
229
- end
230
-
231
226
  # Apply configuration settings to a step
232
227
  def apply_step_configuration(step, step_config)
233
228
  step.print_response = step_config["print_response"] if step_config.key?("print_response")
@@ -11,7 +11,7 @@ module Roast
11
11
  # by introducing the StepRunner interface.
12
12
  class WorkflowExecutor
13
13
  # Define custom exception classes for specific error scenarios
14
- class WorkflowExecutorError < StandardError
14
+ class WorkflowExecutorError < Roast::Error
15
15
  attr_reader :step_name, :original_error
16
16
 
17
17
  def initialize(message, step_name: nil, original_error: nil)
data/lib/roast.rb CHANGED
@@ -21,6 +21,7 @@ require "yaml"
21
21
  # Third-party gem requires
22
22
  require "active_support"
23
23
  require "active_support/cache"
24
+ require "active_support/core_ext/array"
24
25
  require "active_support/core_ext/hash/indifferent_access"
25
26
  require "active_support/core_ext/module/delegation"
26
27
  require "active_support/core_ext/string"
@@ -279,7 +280,7 @@ module Roast
279
280
  output_path = generator.generate(options[:output])
280
281
 
281
282
  puts ::CLI::UI.fmt("{{success:✓}} Diagram generated: #{output_path}")
282
- rescue StandardError => e
283
+ rescue Roast::Error => e
283
284
  raise Thor::Error, "Error generating diagram: #{e.message}"
284
285
  end
285
286
 
@@ -327,7 +328,11 @@ module Roast
327
328
  def copy_example(example_name)
328
329
  examples_dir = File.join(Roast::ROOT, "examples")
329
330
  source_path = File.join(examples_dir, example_name)
330
- target_path = File.join(Dir.pwd, example_name)
331
+
332
+ # Always place new workflows in roast/ so `roast list` can find them
333
+ roast_dir = File.join(Dir.pwd, "roast")
334
+ FileUtils.mkdir_p(roast_dir)
335
+ target_path = File.join(roast_dir, example_name)
331
336
 
332
337
  unless File.directory?(source_path)
333
338
  puts "Example '#{example_name}' not found!"
data/sorbet/config CHANGED
@@ -1,5 +1,7 @@
1
1
  --dir
2
2
  .
3
+ --ignore=.git/
4
+ --ignore=.idea/
3
5
  --ignore=tmp/
4
6
  --ignore=vendor/
5
7
  --ignore=test/
@@ -0,0 +1 @@
1
+ **/*.rbi linguist-vendored=true