roast-ai 1.0.2 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.claude/commands/docs/write-comments.md +1 -1
- data/.rubocop.yml +12 -1
- data/Gemfile +2 -2
- data/Gemfile.lock +149 -34
- data/README.md +56 -3
- data/examples/agent_with_multiple_prompts.rb +27 -0
- data/examples/custom_logging.rb +4 -2
- data/examples/demo/Gemfile.lock +49 -15
- data/examples/plugin-gem-example/Gemfile.lock +19 -15
- data/examples/simple_chat.rb +1 -1
- data/examples/simple_pi_agent.rb +18 -0
- data/internal/rubocop/cop/roast/no_test_class_nesting.rb +126 -0
- data/internal/rubocop/rubocop-roast.yml +6 -0
- data/internal/workflows/maintenance/branch_docs_impact.rb +97 -0
- data/internal/workflows/maintenance/deprecated_models_docs_updater.rb +78 -0
- data/lib/roast/cog/config.rb +1 -1
- data/lib/roast/cog/output.rb +2 -1
- data/lib/roast/cog/registry.rb +3 -3
- data/lib/roast/cog_input_manager.rb +28 -7
- data/lib/roast/cogs/agent/config.rb +2 -2
- data/lib/roast/cogs/agent/input.rb +20 -22
- data/lib/roast/cogs/agent/providers/claude/claude_invocation.rb +13 -5
- data/lib/roast/cogs/agent/providers/claude/messages/result_message.rb +1 -1
- data/lib/roast/cogs/agent/providers/claude/tool_result.rb +344 -4
- data/lib/roast/cogs/agent/providers/claude/tool_use.rb +356 -1
- data/lib/roast/cogs/agent/providers/claude.rb +16 -3
- data/lib/roast/cogs/agent/providers/pi/messages/tool_call_message.rb +60 -0
- data/lib/roast/cogs/agent/providers/pi/messages/tool_result_message.rb +57 -0
- data/lib/roast/cogs/agent/providers/pi/pi_invocation.rb +352 -0
- data/lib/roast/cogs/agent/providers/pi.rb +41 -0
- data/lib/roast/cogs/agent/stats.rb +29 -0
- data/lib/roast/cogs/agent/usage.rb +22 -0
- data/lib/roast/cogs/agent.rb +5 -6
- data/lib/roast/cogs/chat/config.rb +28 -2
- data/lib/roast/cogs/chat.rb +82 -10
- data/lib/roast/event.rb +1 -0
- data/lib/roast/event_monitor.rb +35 -3
- data/lib/roast/log.rb +21 -0
- data/lib/roast/log_formatter.rb +9 -7
- data/lib/roast/version.rb +1 -1
- data/lib/roast.rb +1 -3
- data/roast-ai.gemspec +2 -1
- data/sorbet/rbi/gems/activesupport@8.0.2.rbi +549 -383
- data/sorbet/rbi/gems/addressable@2.8.7.rbi +46 -44
- data/sorbet/rbi/gems/ast@2.4.3.rbi +7 -6
- data/sorbet/rbi/gems/async@2.34.0.rbi +21 -3
- data/sorbet/rbi/gems/benchmark@0.4.1.rbi +7 -7
- data/sorbet/rbi/gems/bigdecimal@3.2.2.rbi +198 -1
- data/sorbet/rbi/gems/concurrent-ruby@1.3.5.rbi +405 -328
- data/sorbet/rbi/gems/console@1.34.2.rbi +2 -2
- data/sorbet/rbi/gems/docile@1.4.1.rbi +30 -30
- data/sorbet/rbi/gems/drb@2.2.3.rbi +25 -25
- data/sorbet/rbi/gems/erubi@1.13.1.rbi +2 -0
- data/sorbet/rbi/gems/faraday-net_http@3.4.2.rbi +2 -77
- data/sorbet/rbi/gems/faraday-retry@2.3.2.rbi +2 -57
- data/sorbet/rbi/gems/faraday@2.14.1.rbi +382 -75
- data/sorbet/rbi/gems/guard-compat@1.2.1.rbi +1 -110
- data/sorbet/rbi/gems/guard-minitest@2.4.6.rbi +0 -139
- data/sorbet/rbi/gems/guard@2.19.1.rbi +38 -38
- data/sorbet/rbi/gems/hashdiff@1.2.0.rbi +3 -3
- data/sorbet/rbi/gems/i18n@1.14.7.rbi +53 -29
- data/sorbet/rbi/gems/io-event@1.14.0.rbi +67 -10
- data/sorbet/rbi/gems/json@2.18.1.rbi +227 -5
- data/sorbet/rbi/gems/lint_roller@1.1.0.rbi +83 -0
- data/sorbet/rbi/gems/listen@3.9.0.rbi +7 -7
- data/sorbet/rbi/gems/logger@1.7.0.rbi +3 -3
- data/sorbet/rbi/gems/lumberjack@1.2.10.rbi +21 -21
- data/sorbet/rbi/gems/marcel@1.1.0.rbi +1 -1
- data/sorbet/rbi/gems/minitest-rg@5.3.0.rbi +0 -96
- data/sorbet/rbi/gems/minitest@5.25.5.rbi +1 -16
- data/sorbet/rbi/gems/net-http@0.9.1.rbi +27 -19
- data/sorbet/rbi/gems/netrc@0.11.0.rbi +18 -0
- data/sorbet/rbi/gems/notiffany@0.1.3.rbi +20 -20
- data/sorbet/rbi/gems/ostruct@0.6.2.rbi +149 -15
- data/sorbet/rbi/gems/parser@3.3.8.0.rbi +141 -139
- data/sorbet/rbi/gems/prism@1.4.0.rbi +922 -864
- data/sorbet/rbi/gems/public_suffix@6.0.2.rbi +56 -35
- data/sorbet/rbi/gems/racc@1.8.1.rbi +10 -2
- data/sorbet/rbi/gems/rainbow@3.1.1.rbi +12 -12
- data/sorbet/rbi/gems/rake@13.3.0.rbi +219 -318
- data/sorbet/rbi/gems/{rbi@0.3.6.rbi → rbi@0.3.9.rbi} +612 -2267
- data/sorbet/rbi/gems/{rbs@3.9.4.rbi → rbs@4.0.0.dev.5.rbi} +2013 -680
- data/sorbet/rbi/gems/regexp_parser@2.10.0.rbi +151 -113
- data/sorbet/rbi/gems/require-hooks@0.2.3.rbi +110 -0
- data/sorbet/rbi/gems/rexml@3.4.2.rbi +24 -51
- data/sorbet/rbi/gems/rubocop-ast@1.45.1.rbi +506 -815
- data/sorbet/rbi/gems/rubocop-sorbet@0.10.5.rbi +16 -16
- data/sorbet/rbi/gems/rubocop@1.77.0.rbi +2692 -2327
- data/sorbet/rbi/gems/ruby-progressbar@1.13.0.rbi +8 -8
- data/sorbet/rbi/gems/ruby_llm@1.8.2.rbi +38 -23
- data/sorbet/rbi/gems/securerandom@0.4.1.rbi +1 -1
- data/sorbet/rbi/gems/simplecov-html@0.13.2.rbi +2 -131
- data/sorbet/rbi/gems/simplecov@0.22.0.rbi +28 -127
- data/sorbet/rbi/gems/{spoom@1.6.3.rbi → spoom@1.7.11.rbi} +1139 -2246
- data/sorbet/rbi/gems/sqlite3@2.9.0.rbi +91 -1
- data/sorbet/rbi/gems/{tapioca@0.16.11.rbi → tapioca@0.17.10.rbi} +721 -835
- data/sorbet/rbi/gems/thor@1.4.0.rbi +53 -53
- data/sorbet/rbi/gems/tsort@0.2.0.rbi +393 -0
- data/sorbet/rbi/gems/type_toolkit@0.0.5.rbi +49 -0
- data/sorbet/rbi/gems/tzinfo@2.0.6.rbi +144 -143
- data/sorbet/rbi/gems/uri@1.1.1.rbi +7 -7
- data/sorbet/rbi/gems/vcr@6.3.1.rbi +53 -36
- data/sorbet/rbi/gems/webmock@3.25.1.rbi +38 -13
- data/sorbet/rbi/gems/zeitwerk@2.7.3.rbi +39 -272
- data/sorbet/rbi/shims/lib/roast/execution_context.rbi +3 -3
- data/tutorial/01_your_first_workflow/README.md +9 -5
- data/tutorial/01_your_first_workflow/configured_chat.rb +1 -1
- data/tutorial/02_chaining_cogs/README.md +2 -2
- data/tutorial/02_chaining_cogs/code_review.rb +1 -1
- data/tutorial/02_chaining_cogs/session_resumption.rb +1 -1
- data/tutorial/03_targets_and_params/README.md +1 -1
- data/tutorial/04_configuration_options/README.md +2 -2
- data/tutorial/08_iterative_workflows/README.md +1 -1
- data/tutorial/README.md +1 -1
- metadata +39 -17
- data/docs/AGENT_STEPS.md +0 -288
- data/docs/INSTRUMENTATION.md +0 -243
- data/docs/ITERATION_SYNTAX.md +0 -147
- data/docs/VALIDATION.md +0 -178
- data/lib/roast/nil_assertions.rb +0 -23
- /data/internal/documentation/{architectural-notes.md → comments/architectural-notes.md} +0 -0
- /data/internal/documentation/{doc-comments-external.md → comments/doc-comments-external.md} +0 -0
- /data/internal/documentation/{doc-comments-internal.md → comments/doc-comments-internal.md} +0 -0
- /data/internal/documentation/{doc-comments.md → comments/doc-comments.md} +0 -0
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: ../..
|
|
3
3
|
specs:
|
|
4
|
-
roast-ai (1.0
|
|
4
|
+
roast-ai (1.2.0)
|
|
5
5
|
activesupport (~> 8.0)
|
|
6
6
|
async (>= 2.34)
|
|
7
7
|
rainbow (>= 3.0.0)
|
|
8
|
-
ruby_llm (>= 1.
|
|
8
|
+
ruby_llm (>= 1.13)
|
|
9
|
+
type_toolkit (>= 0.0.5)
|
|
9
10
|
zeitwerk (>= 2.6)
|
|
10
11
|
|
|
11
12
|
PATH
|
|
@@ -17,7 +18,7 @@ PATH
|
|
|
17
18
|
GEM
|
|
18
19
|
remote: https://rubygems.org/
|
|
19
20
|
specs:
|
|
20
|
-
activesupport (8.0.
|
|
21
|
+
activesupport (8.0.4.1)
|
|
21
22
|
base64
|
|
22
23
|
benchmark (>= 0.3)
|
|
23
24
|
bigdecimal
|
|
@@ -26,7 +27,7 @@ GEM
|
|
|
26
27
|
drb
|
|
27
28
|
i18n (>= 1.6, < 2)
|
|
28
29
|
logger (>= 1.4.2)
|
|
29
|
-
minitest (>= 5.1)
|
|
30
|
+
minitest (>= 5.1, < 6)
|
|
30
31
|
securerandom (>= 0.3)
|
|
31
32
|
tzinfo (~> 2.0, >= 2.0.5)
|
|
32
33
|
uri (>= 0.13.1)
|
|
@@ -38,19 +39,19 @@ GEM
|
|
|
38
39
|
metrics (~> 0.12)
|
|
39
40
|
traces (~> 0.18)
|
|
40
41
|
base64 (0.3.0)
|
|
41
|
-
benchmark (0.
|
|
42
|
-
bigdecimal (
|
|
43
|
-
concurrent-ruby (1.3.
|
|
44
|
-
connection_pool (
|
|
42
|
+
benchmark (0.5.0)
|
|
43
|
+
bigdecimal (4.1.2)
|
|
44
|
+
concurrent-ruby (1.3.7)
|
|
45
|
+
connection_pool (3.0.2)
|
|
45
46
|
console (1.34.3)
|
|
46
47
|
fiber-annotation
|
|
47
48
|
fiber-local (~> 1.1)
|
|
48
49
|
json
|
|
49
50
|
date (3.4.1)
|
|
50
51
|
drb (2.2.3)
|
|
51
|
-
erb (
|
|
52
|
+
erb (6.0.1.1)
|
|
52
53
|
event_stream_parser (1.0.0)
|
|
53
|
-
faraday (2.14.
|
|
54
|
+
faraday (2.14.2)
|
|
54
55
|
faraday-net_http (>= 2.0, < 3.5)
|
|
55
56
|
json
|
|
56
57
|
logger
|
|
@@ -64,7 +65,7 @@ GEM
|
|
|
64
65
|
fiber-local (1.1.0)
|
|
65
66
|
fiber-storage
|
|
66
67
|
fiber-storage (1.0.1)
|
|
67
|
-
i18n (1.14.
|
|
68
|
+
i18n (1.14.8)
|
|
68
69
|
concurrent-ruby (~> 1.0)
|
|
69
70
|
io-console (0.8.1)
|
|
70
71
|
io-event (1.14.2)
|
|
@@ -72,7 +73,7 @@ GEM
|
|
|
72
73
|
pp (>= 0.6.0)
|
|
73
74
|
rdoc (>= 4.0.0)
|
|
74
75
|
reline (>= 0.4.2)
|
|
75
|
-
json (2.
|
|
76
|
+
json (2.19.5)
|
|
76
77
|
language_server-protocol (3.17.0.5)
|
|
77
78
|
lint_roller (1.1.0)
|
|
78
79
|
logger (1.7.0)
|
|
@@ -118,21 +119,24 @@ GEM
|
|
|
118
119
|
parser (>= 3.3.7.2)
|
|
119
120
|
prism (~> 1.4)
|
|
120
121
|
ruby-progressbar (1.13.0)
|
|
121
|
-
ruby_llm (1.
|
|
122
|
+
ruby_llm (1.15.0)
|
|
122
123
|
base64
|
|
123
124
|
event_stream_parser (~> 1)
|
|
124
125
|
faraday (>= 1.10.0)
|
|
125
126
|
faraday-multipart (>= 1)
|
|
126
127
|
faraday-net_http (>= 1)
|
|
127
128
|
faraday-retry (>= 1)
|
|
128
|
-
marcel (~> 1
|
|
129
|
-
ruby_llm-schema (~> 0
|
|
129
|
+
marcel (~> 1)
|
|
130
|
+
ruby_llm-schema (~> 0)
|
|
130
131
|
zeitwerk (~> 2)
|
|
131
132
|
ruby_llm-schema (0.2.5)
|
|
132
133
|
securerandom (0.4.1)
|
|
133
134
|
stringio (3.1.7)
|
|
134
135
|
traces (0.18.2)
|
|
135
136
|
tsort (0.2.0)
|
|
137
|
+
type_toolkit (0.0.5)
|
|
138
|
+
lint_roller
|
|
139
|
+
rubocop (>= 1.72.0)
|
|
136
140
|
tzinfo (2.0.6)
|
|
137
141
|
concurrent-ruby (~> 1.0)
|
|
138
142
|
unicode-display_width (3.2.0)
|
data/examples/simple_chat.rb
CHANGED
|
@@ -21,6 +21,6 @@ execute do
|
|
|
21
21
|
# Ask a question with a template prompt. You can pass variables to it as you would an ERB template
|
|
22
22
|
chat { template("examples/prompts/simple_prompt.md.erb", { lake_answer: chat!(:lake).response }) }
|
|
23
23
|
|
|
24
|
-
# Shorthand template syntax - searches prompts/
|
|
24
|
+
# Shorthand template syntax - searches prompts/ and templates/ directories automatically
|
|
25
25
|
chat { template("simple_prompt", { lake_answer: chat!(:lake).response }) }
|
|
26
26
|
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
#: self as Roast::Workflow
|
|
5
|
+
|
|
6
|
+
config do
|
|
7
|
+
agent do
|
|
8
|
+
provider :pi
|
|
9
|
+
model "anthropic/claude-haiku-4-5-20251001"
|
|
10
|
+
append_system_prompt "Always respond in haiku form"
|
|
11
|
+
show_prompt!
|
|
12
|
+
dump_raw_agent_messages_to "tmp/pi-messages.log"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
execute do
|
|
17
|
+
agent { "What is the world's largest lake?" }
|
|
18
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# typed: false
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module RuboCop
|
|
5
|
+
module Cop
|
|
6
|
+
module Roast
|
|
7
|
+
# Prevents nesting classes or modules inside reopened (non-test) class
|
|
8
|
+
# definitions in test files.
|
|
9
|
+
#
|
|
10
|
+
# When a class is reopened in a test file (e.g., `class Agent < Cog`) and
|
|
11
|
+
# contains nested class or module definitions, IDE test runners like
|
|
12
|
+
# RubyMine fail to discover test suites. Use `::` scoping instead.
|
|
13
|
+
#
|
|
14
|
+
# The cop walks the entire subtree of the offending class, so deeply
|
|
15
|
+
# nested structures (e.g., `class Agent < Cog; module Providers; ...`)
|
|
16
|
+
# are caught even when the nested definitions are not direct children.
|
|
17
|
+
#
|
|
18
|
+
# Classes nested inside test classes are exempt — helper stubs and
|
|
19
|
+
# fixtures defined inside a test suite are perfectly fine.
|
|
20
|
+
#
|
|
21
|
+
# @example Bad — reopened class with nested module
|
|
22
|
+
# class Agent < Cog
|
|
23
|
+
# module Providers
|
|
24
|
+
# class Claude::MessageTest < ActiveSupport::TestCase
|
|
25
|
+
# # ...
|
|
26
|
+
# end
|
|
27
|
+
# end
|
|
28
|
+
# end
|
|
29
|
+
#
|
|
30
|
+
# @example Bad — reopened class with nested test class
|
|
31
|
+
# class Agent < Cog
|
|
32
|
+
# class ConfigTest < ActiveSupport::TestCase
|
|
33
|
+
# # ...
|
|
34
|
+
# end
|
|
35
|
+
# end
|
|
36
|
+
#
|
|
37
|
+
# @example Good — :: scoping, no class reopening
|
|
38
|
+
# module Agent::Providers
|
|
39
|
+
# class Claude::MessageTest < ActiveSupport::TestCase
|
|
40
|
+
# # ...
|
|
41
|
+
# end
|
|
42
|
+
# end
|
|
43
|
+
#
|
|
44
|
+
# @example Good — :: scoped test class
|
|
45
|
+
# class Agent::ConfigTest < ActiveSupport::TestCase
|
|
46
|
+
# # ...
|
|
47
|
+
# end
|
|
48
|
+
#
|
|
49
|
+
# @example Good — helper class inside a test class
|
|
50
|
+
# class Agent::OutputTest < ActiveSupport::TestCase
|
|
51
|
+
# class FakeAdapter
|
|
52
|
+
# def call; end
|
|
53
|
+
# end
|
|
54
|
+
# end
|
|
55
|
+
#
|
|
56
|
+
class NoTestClassNesting < Base
|
|
57
|
+
MSG = "Do not nest classes or modules inside reopened class `%<parent>s` in test files. " \
|
|
58
|
+
"Use `::` scoping instead (e.g., `class %<parent>s::Nested` or `module %<parent>s::Nested`)."
|
|
59
|
+
|
|
60
|
+
# @!method test_base_class?(node)
|
|
61
|
+
def_node_matcher :test_base_class?, <<~PATTERN
|
|
62
|
+
{
|
|
63
|
+
(const (const {nil? cbase} :ActiveSupport) :TestCase)
|
|
64
|
+
(const (const {nil? cbase} :Minitest) :Test)
|
|
65
|
+
(const {nil? cbase} :Minitest)
|
|
66
|
+
}
|
|
67
|
+
PATTERN
|
|
68
|
+
|
|
69
|
+
def on_class(node)
|
|
70
|
+
# Test classes are allowed to contain nested definitions (helpers, stubs)
|
|
71
|
+
return if test_class?(node)
|
|
72
|
+
|
|
73
|
+
# Classes nested inside a test class are helpers — leave them alone
|
|
74
|
+
return if inside_test_class?(node)
|
|
75
|
+
|
|
76
|
+
# Flag if this non-test class contains any nested class or module
|
|
77
|
+
return unless contains_nested_definitions?(node)
|
|
78
|
+
|
|
79
|
+
message = format(MSG, parent: node.identifier.const_name)
|
|
80
|
+
add_offense(node.loc.keyword.join(node.identifier.source_range), message: message)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def test_class?(node)
|
|
86
|
+
node.parent_class && test_base_class?(node.parent_class)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Returns true if any ancestor of +node+ is a test class.
|
|
90
|
+
def inside_test_class?(node)
|
|
91
|
+
current = node.parent
|
|
92
|
+
while current
|
|
93
|
+
return true if current.class_type? && test_class?(current)
|
|
94
|
+
|
|
95
|
+
current = current.parent
|
|
96
|
+
end
|
|
97
|
+
false
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Returns true if +node+ contains any nested class or module definition
|
|
101
|
+
# at any depth, excluding those sheltered inside an intermediate test class.
|
|
102
|
+
def contains_nested_definitions?(node)
|
|
103
|
+
node.each_descendant(:class, :module) do |descendant|
|
|
104
|
+
next if sheltered_by_test_class?(descendant, node)
|
|
105
|
+
|
|
106
|
+
return true
|
|
107
|
+
end
|
|
108
|
+
false
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Returns true if there is a test class between +descendant+ and +stop_at+
|
|
112
|
+
# in the ancestor chain — meaning the descendant is a helper inside a test
|
|
113
|
+
# class and should not count as a problematic nested definition.
|
|
114
|
+
def sheltered_by_test_class?(descendant, stop_at)
|
|
115
|
+
current = descendant.parent
|
|
116
|
+
while current && current != stop_at
|
|
117
|
+
return true if current.class_type? && test_class?(current)
|
|
118
|
+
|
|
119
|
+
current = current.parent
|
|
120
|
+
end
|
|
121
|
+
false
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
#: self as Roast::Workflow
|
|
5
|
+
|
|
6
|
+
# Gets the committed diff of the current branch vs origin/main, then analyzes it for potential
|
|
7
|
+
# documentation impacts, and optionally applies fixes. This is meant to be run as a pre-merge check, to
|
|
8
|
+
# catch any potential documentation issues before they get merged into main.
|
|
9
|
+
#
|
|
10
|
+
# Accepts a `--fix` flag to apply any suggested fixes in place. Otherwise, it just reports the analysis
|
|
11
|
+
# and recommended fixes without applying them.
|
|
12
|
+
|
|
13
|
+
config do
|
|
14
|
+
agent do
|
|
15
|
+
provider :claude
|
|
16
|
+
model "claude-opus-4-7"
|
|
17
|
+
quiet!
|
|
18
|
+
end
|
|
19
|
+
chat do
|
|
20
|
+
provider :openai
|
|
21
|
+
model "gpt-5"
|
|
22
|
+
quiet!
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
execute do
|
|
27
|
+
cmd(:diff) do
|
|
28
|
+
merge_base = %x(git merge-base origin/main HEAD).strip
|
|
29
|
+
fail!("could not determine merge-base with origin/main — run `git fetch origin main` first") if merge_base.empty?
|
|
30
|
+
"git diff #{merge_base} HEAD"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
agent(:analyzer) do
|
|
34
|
+
skip! if cmd!(:diff).text.strip.empty?
|
|
35
|
+
fail!("diff too large (#{cmd!(:diff).text.bytesize} bytes) to analyze — narrow the branch or exclude generated files") if cmd!(:diff).text.bytesize > 500_000
|
|
36
|
+
<<~PROMPT
|
|
37
|
+
You are checking whether a git diff makes any existing documentation stale.
|
|
38
|
+
|
|
39
|
+
Rules:
|
|
40
|
+
- Use ONLY the diff below. Do not run any commands.
|
|
41
|
+
- Be thorough about finding real issues — do not be conservative.
|
|
42
|
+
- But do NOT speculate about docs you cannot see in the diff.
|
|
43
|
+
- No markdown headers, no preamble, no caveats, no "limitations" sections.
|
|
44
|
+
|
|
45
|
+
Output format — pick exactly one:
|
|
46
|
+
|
|
47
|
+
If nothing in the diff affects existing docs, output a single line:
|
|
48
|
+
No documentation impact.
|
|
49
|
+
|
|
50
|
+
Otherwise, output one block per affected doc, separated by blank lines:
|
|
51
|
+
<doc/path.md>
|
|
52
|
+
Stale because: <one sentence>
|
|
53
|
+
Fix: <one sentence>
|
|
54
|
+
|
|
55
|
+
--- DIFF START ---
|
|
56
|
+
#{cmd!(:diff).text}
|
|
57
|
+
--- DIFF END ---
|
|
58
|
+
PROMPT
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
chat(:report) do
|
|
62
|
+
skip! if cmd!(:diff).text.strip.empty?
|
|
63
|
+
<<~PROMPT
|
|
64
|
+
Based on the following analysis, summarize the impact of the changes in this branch on the project's documentation.
|
|
65
|
+
Highlight any significant improvements or regressions, and provide recommendations for any additional documentation updates that may be necessary.
|
|
66
|
+
Do not suggest updates that are not needed.
|
|
67
|
+
Do not be verbose. Be super concise.
|
|
68
|
+
|
|
69
|
+
Analysis:
|
|
70
|
+
#{agent!(:analyzer).response}
|
|
71
|
+
PROMPT
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
agent(:fixer) do
|
|
75
|
+
skip! unless arg?(:fix)
|
|
76
|
+
skip! if cmd!(:diff).text.strip.empty?
|
|
77
|
+
skip! if agent!(:analyzer).response.strip == "No documentation impact."
|
|
78
|
+
<<~PROMPT
|
|
79
|
+
Apply the documentation fixes suggested in the analysis below. Edit the
|
|
80
|
+
affected files in place. Do not modify any code files; docs only.
|
|
81
|
+
|
|
82
|
+
#{agent!(:analyzer).response}
|
|
83
|
+
PROMPT
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
ruby(:output) do
|
|
87
|
+
if cmd!(:diff).text.strip.empty?
|
|
88
|
+
puts "No changes vs origin/main — nothing to analyze."
|
|
89
|
+
else
|
|
90
|
+
files = cmd!(:diff).out.scan(%r{^diff --git a/.+ b/(.+)$}).flatten
|
|
91
|
+
puts "Files considered (#{files.size}):"
|
|
92
|
+
files.each { |f| puts " #{f}" }
|
|
93
|
+
puts "ANALYSIS:\n#{chat!(:report).response}"
|
|
94
|
+
puts(agent?(:fixer) ? "Fixes applied." : "(Next time, run with `-- fix` to auto-apply suggested edits)")
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
#: self as Roast::Workflow
|
|
5
|
+
|
|
6
|
+
# Looks at the Anthropic (https://platform.claude.com/docs/en/about-claude/model-deprecations) and
|
|
7
|
+
# OpenAI (https://developers.openai.com/api/docs/deprecations) model-deprecation pages and updates all
|
|
8
|
+
# docs, doc comments and code references to deprecated/retired models to reflect those changes. This is
|
|
9
|
+
# meant to be run periodically to keep all the references to models up to date.
|
|
10
|
+
|
|
11
|
+
config do
|
|
12
|
+
agent do
|
|
13
|
+
provider :claude
|
|
14
|
+
model "claude-opus-4-8"
|
|
15
|
+
quiet!
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
execute do
|
|
20
|
+
agent(:model_verifier) do
|
|
21
|
+
<<~PROMPT
|
|
22
|
+
Check both of these pages for deprecated/retired models and their suggested replacements:
|
|
23
|
+
- Anthropic (Claude): https://platform.claude.com/docs/en/about-claude/model-deprecations
|
|
24
|
+
- OpenAI: https://developers.openai.com/api/docs/deprecations
|
|
25
|
+
|
|
26
|
+
Include every deprecated or retired model that has a recommended replacement, from both pages.
|
|
27
|
+
Only include models — ignore deprecated API endpoints, tools, or other non-model features.
|
|
28
|
+
|
|
29
|
+
Output ONLY a JSON object of exactly this shape, with no surrounding prose:
|
|
30
|
+
{"outdated": [{"model": "<outdated_model>", "replacement": "<replacement_model>"}]}
|
|
31
|
+
|
|
32
|
+
If there are none, output {"outdated": []}.
|
|
33
|
+
PROMPT
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
agent(:model_finder) do
|
|
37
|
+
<<~PROMPT
|
|
38
|
+
You are a code search engine. Search through the codebase and documentation for any references to
|
|
39
|
+
the outdated models below, including test files, and list every place where they are mentioned.
|
|
40
|
+
|
|
41
|
+
The input is a JSON object of the form:
|
|
42
|
+
{"outdated": [{"model": "<outdated_model>", "replacement": "<replacement_model>"}]}
|
|
43
|
+
|
|
44
|
+
INPUT:
|
|
45
|
+
#{agent!(:model_verifier).response}
|
|
46
|
+
|
|
47
|
+
Output ONLY a JSON object of exactly this shape, with no surrounding prose, carrying the
|
|
48
|
+
replacement through for each model you find a reference to:
|
|
49
|
+
{"references": [{"model": "<outdated_model>", "replacement": "<replacement_model>", "locations": ["<file_path>:<line_number>"]}]}
|
|
50
|
+
|
|
51
|
+
If you find no references, output {"references": []}.
|
|
52
|
+
PROMPT
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
agent(:updater) do |my|
|
|
56
|
+
finder = agent!(:model_finder)
|
|
57
|
+
skip! if finder.json![:references].blank?
|
|
58
|
+
my.session = finder.session
|
|
59
|
+
<<~PROMPT
|
|
60
|
+
For each reference you found of an outdated model, update the reference to use the suggested replacement model instead. Make sure to update all types of references, including documentation, doc comments and code references. Output the list of updated references in the following format:
|
|
61
|
+
<outdated_model_1> -> <replacement_model_1>:
|
|
62
|
+
- <file_path>:<line_number>
|
|
63
|
+
<outdated_model_2> -> <replacement_model_2>:
|
|
64
|
+
- <file_path>:<line_number>
|
|
65
|
+
...
|
|
66
|
+
PROMPT
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
ruby(:output) do
|
|
70
|
+
if agent?(:updater)
|
|
71
|
+
puts "[OUTDATED MODELS & REPLACEMENTS]\n #{agent!(:model_verifier).response}"
|
|
72
|
+
puts "[REFERENCES IN CODEBASE]\n #{agent!(:model_finder).response}"
|
|
73
|
+
puts "[UPDATED REFERENCES]\n #{agent!(:updater).response}"
|
|
74
|
+
else
|
|
75
|
+
puts "No references to outdated models found — nothing to update."
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
data/lib/roast/cog/config.rb
CHANGED
data/lib/roast/cog/output.rb
CHANGED
|
@@ -241,8 +241,9 @@ module Roast
|
|
|
241
241
|
lines.reverse.each do |line|
|
|
242
242
|
# Look for numbers with various separators, formats, and currency symbols (very permissive)
|
|
243
243
|
# Matches: 123, 1,234, 1_234, 1 234, 1.23, -1.23, 1.23e10, 1.23e-10
|
|
244
|
+
# Reversed so that the last number on the line is tried first (matching the bottom-up line order)
|
|
244
245
|
matches = line.scan(/-?[\d\s$¢£€¥.,_]+(?:[eE][+-]?\d+)?/) #: as Array[String]
|
|
245
|
-
candidates.concat(matches.map(&:strip))
|
|
246
|
+
candidates.concat(matches.map(&:strip).reverse)
|
|
246
247
|
end
|
|
247
248
|
|
|
248
249
|
candidates.compact.uniq
|
data/lib/roast/cog/registry.rb
CHANGED
|
@@ -51,13 +51,13 @@ module Roast
|
|
|
51
51
|
#
|
|
52
52
|
#: (singleton(Roast::Cog)) -> void
|
|
53
53
|
def use(cog_class)
|
|
54
|
-
|
|
55
|
-
cogs[
|
|
54
|
+
name, klass = create_registration(cog_class)
|
|
55
|
+
cogs[name] = klass
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
private
|
|
59
59
|
|
|
60
|
-
#: (singleton(Roast::Cog)) ->
|
|
60
|
+
#: (singleton(Roast::Cog)) -> [Symbol, singleton(Cog)]
|
|
61
61
|
def create_registration(cog_class)
|
|
62
62
|
cog_class_name = cog_class.name
|
|
63
63
|
raise CouldNotDeriveCogNameError if cog_class_name.nil?
|
|
@@ -159,7 +159,7 @@ module Roast
|
|
|
159
159
|
# Supports both relative shorthand paths like "greeting" and full absolute paths.
|
|
160
160
|
#
|
|
161
161
|
# @param path [String, Pathname] The template path to resolve. Can be:
|
|
162
|
-
# - Shorthand name: "greeting" -> searches for prompts/greeting.md.erb
|
|
162
|
+
# - Shorthand name: "greeting" -> searches for prompts/greeting.md.erb or templates/greeting.md.erb
|
|
163
163
|
# - With extension: "template.erb" -> searches for template.erb
|
|
164
164
|
# - Absolute path: "/full/path/to/template.erb" -> uses as-is
|
|
165
165
|
# @param args [Hash] Template variables for ERB interpolation
|
|
@@ -175,13 +175,14 @@ module Roast
|
|
|
175
175
|
# 1. Absolute path as-is (if absolute)
|
|
176
176
|
# 2-4. Workflow directory: path, path.erb, path.md.erb
|
|
177
177
|
# 5-7. Workflow directory prompts/: prompts/path, prompts/path.erb, prompts/path.md.erb
|
|
178
|
-
# 8-10.
|
|
179
|
-
# 11-13. Current directory
|
|
178
|
+
# 8-10. Workflow directory templates/: templates/path, templates/path.erb, templates/path.md.erb
|
|
179
|
+
# 11-13. Current directory: path, path.erb, path.md.erb
|
|
180
|
+
# 14-16. Current directory prompts/: prompts/path, prompts/path.erb, prompts/path.md.erb
|
|
181
|
+
# 17-19. Current directory templates/: templates/path, templates/path.erb, templates/path.md.erb
|
|
182
|
+
# 20-22. Tilde-expanded path: path, path.erb, path.md.erb
|
|
180
183
|
#
|
|
181
184
|
#: (String | Pathname, ?Hash) -> String
|
|
182
185
|
def template(path, args = {})
|
|
183
|
-
# NOTE: Pathname does not expand ~ for home directory automatically.
|
|
184
|
-
# This is tracked in issue https://github.com/Shopify/roast/issues/663.
|
|
185
186
|
path = Pathname.new(path) unless path.is_a?(Pathname)
|
|
186
187
|
|
|
187
188
|
# Priority stack of places to look for a matching file
|
|
@@ -201,17 +202,37 @@ module Roast
|
|
|
201
202
|
candidate_paths << workflow_dir / "prompts" / "#{path}.erb"
|
|
202
203
|
candidate_paths << workflow_dir / "prompts" / "#{path}.md.erb"
|
|
203
204
|
|
|
204
|
-
# 8-10. Relative to
|
|
205
|
+
# 8-10. Relative to workflow directory templates folder
|
|
206
|
+
candidate_paths << workflow_dir / "templates" / path
|
|
207
|
+
candidate_paths << workflow_dir / "templates" / "#{path}.erb"
|
|
208
|
+
candidate_paths << workflow_dir / "templates" / "#{path}.md.erb"
|
|
209
|
+
|
|
210
|
+
# 11-13. Relative to current working directory
|
|
205
211
|
pwd = Pathname.pwd
|
|
206
212
|
candidate_paths << pwd / path
|
|
207
213
|
candidate_paths << pwd / "#{path}.erb"
|
|
208
214
|
candidate_paths << pwd / "#{path}.md.erb"
|
|
209
215
|
|
|
210
|
-
#
|
|
216
|
+
# 14-16. Relative to current working directory prompts folder
|
|
211
217
|
candidate_paths << pwd / "prompts" / path
|
|
212
218
|
candidate_paths << pwd / "prompts" / "#{path}.erb"
|
|
213
219
|
candidate_paths << pwd / "prompts" / "#{path}.md.erb"
|
|
214
220
|
|
|
221
|
+
# 17-19. Relative to current working directory templates folder
|
|
222
|
+
candidate_paths << pwd / "templates" / path
|
|
223
|
+
candidate_paths << pwd / "templates" / "#{path}.erb"
|
|
224
|
+
candidate_paths << pwd / "templates" / "#{path}.md.erb"
|
|
225
|
+
|
|
226
|
+
# 20-22. Tilde expanded path
|
|
227
|
+
begin
|
|
228
|
+
expanded_path = Pathname.new(File.expand_path(path))
|
|
229
|
+
candidate_paths << expanded_path
|
|
230
|
+
candidate_paths << Pathname.new(File.expand_path("#{expanded_path}.erb"))
|
|
231
|
+
candidate_paths << Pathname.new(File.expand_path("#{expanded_path}.md.erb"))
|
|
232
|
+
rescue ArgumentError
|
|
233
|
+
# File.expand_path raises when expanding ~something/foo (assuming "something" is not a real user).
|
|
234
|
+
# Nothing to do here, falls back to other candidate paths without tilde expansion.
|
|
235
|
+
end
|
|
215
236
|
# Use the first path that exists
|
|
216
237
|
resolved_path = candidate_paths.find(&:exist?)
|
|
217
238
|
|
|
@@ -5,7 +5,7 @@ module Roast
|
|
|
5
5
|
module Cogs
|
|
6
6
|
class Agent < Cog
|
|
7
7
|
class Config < Cog::Config
|
|
8
|
-
VALID_PROVIDERS = [:claude].freeze #: Array[Symbol]
|
|
8
|
+
VALID_PROVIDERS = [:claude, :pi].freeze #: Array[Symbol]
|
|
9
9
|
|
|
10
10
|
# Configure the cog to use a specified provider when invoking an agent
|
|
11
11
|
#
|
|
@@ -51,7 +51,7 @@ module Roast
|
|
|
51
51
|
def valid_provider!
|
|
52
52
|
provider = @values[:provider] || VALID_PROVIDERS.first
|
|
53
53
|
unless VALID_PROVIDERS.include?(provider)
|
|
54
|
-
raise
|
|
54
|
+
raise InvalidConfigError, "'#{provider}' is not a valid provider. Available providers include: #{VALID_PROVIDERS.join(", ")}"
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
provider
|
|
@@ -9,10 +9,14 @@ module Roast
|
|
|
9
9
|
# The agent cog requires a prompt that will be sent to the agent for processing.
|
|
10
10
|
# Optionally, a session identifier can be provided to maintain context across multiple invocations.
|
|
11
11
|
class Input < Cog::Input
|
|
12
|
-
# The
|
|
12
|
+
# The prompts to send to the agent for processing
|
|
13
13
|
#
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
# When multiple prompts are specified, each subsequent prompt is passed to the agent as soon as it completes
|
|
15
|
+
# the previous one, in the same session throughout. This can be useful for helping to ensure the agent produces
|
|
16
|
+
# final outputs in the form you desire after performing a long and complex task.
|
|
17
|
+
#
|
|
18
|
+
#: Array[String]
|
|
19
|
+
attr_accessor :prompts
|
|
16
20
|
|
|
17
21
|
# Optional session identifier for maintaining conversation context
|
|
18
22
|
#
|
|
@@ -28,7 +32,7 @@ module Roast
|
|
|
28
32
|
#: () -> void
|
|
29
33
|
def initialize
|
|
30
34
|
super
|
|
31
|
-
@
|
|
35
|
+
@prompts = [] #: Array[String]
|
|
32
36
|
end
|
|
33
37
|
|
|
34
38
|
# Validate that the input has all required parameters
|
|
@@ -37,41 +41,35 @@ module Roast
|
|
|
37
41
|
#
|
|
38
42
|
# #### See Also
|
|
39
43
|
# - `coerce`
|
|
40
|
-
# - `valid_prompt!`
|
|
41
44
|
#
|
|
42
45
|
#: () -> void
|
|
43
46
|
def validate!
|
|
44
|
-
|
|
47
|
+
raise Cog::Input::InvalidInputError, "At least one prompt is required" unless prompts.present?
|
|
48
|
+
raise Cog::Input::InvalidInputError, "Blank prompts are not allowed" if prompts.any?(&:blank?)
|
|
45
49
|
end
|
|
46
50
|
|
|
47
51
|
# Coerce the input from the return value of the input block
|
|
48
52
|
#
|
|
49
53
|
# If the input block returns a String, it will be used as the prompt value.
|
|
54
|
+
# If the input block returns an Array of Strings, the first will be used as the prompt and the
|
|
55
|
+
# rest will be used as finalizers.
|
|
50
56
|
#
|
|
51
57
|
# #### See Also
|
|
52
58
|
# - `validate!`
|
|
53
59
|
#
|
|
54
60
|
#: (untyped) -> void
|
|
55
61
|
def coerce(input_return_value)
|
|
56
|
-
|
|
57
|
-
|
|
62
|
+
case input_return_value
|
|
63
|
+
when String
|
|
64
|
+
self.prompts = [input_return_value]
|
|
65
|
+
when Array
|
|
66
|
+
self.prompts = input_return_value.map(&:to_s)
|
|
58
67
|
end
|
|
59
68
|
end
|
|
60
69
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
#
|
|
65
|
-
# #### See Also
|
|
66
|
-
# - `prompt`
|
|
67
|
-
# - `validate!`
|
|
68
|
-
#
|
|
69
|
-
#: () -> String
|
|
70
|
-
def valid_prompt!
|
|
71
|
-
valid_prompt = @prompt
|
|
72
|
-
raise Cog::Input::InvalidInputError, "'prompt' is required" unless valid_prompt.present?
|
|
73
|
-
|
|
74
|
-
valid_prompt
|
|
70
|
+
#: (String) -> void
|
|
71
|
+
def prompt=(prompt)
|
|
72
|
+
@prompts = [prompt]
|
|
75
73
|
end
|
|
76
74
|
end
|
|
77
75
|
end
|