@every-env/compound-plugin 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/{plugins/compound-engineering → .claude}/commands/release-docs.md +0 -1
  2. package/.claude-plugin/marketplace.json +2 -2
  3. package/.github/workflows/ci.yml +1 -1
  4. package/.github/workflows/deploy-docs.yml +3 -3
  5. package/.github/workflows/publish.yml +37 -0
  6. package/README.md +12 -3
  7. package/docs/index.html +13 -13
  8. package/docs/pages/changelog.html +39 -0
  9. package/docs/plans/2026-02-08-feat-convert-local-md-settings-for-opencode-codex-plan.md +143 -0
  10. package/docs/plans/2026-02-08-feat-simplify-plugin-settings-plan.md +195 -0
  11. package/docs/plans/2026-02-09-refactor-dspy-ruby-skill-update-plan.md +104 -0
  12. package/docs/plans/2026-02-12-feat-add-cursor-cli-target-provider-plan.md +306 -0
  13. package/docs/specs/cursor.md +85 -0
  14. package/package.json +1 -1
  15. package/plugins/compound-engineering/.claude-plugin/plugin.json +2 -2
  16. package/plugins/compound-engineering/CHANGELOG.md +38 -0
  17. package/plugins/compound-engineering/README.md +5 -3
  18. package/plugins/compound-engineering/commands/workflows/brainstorm.md +6 -1
  19. package/plugins/compound-engineering/commands/workflows/compound.md +1 -0
  20. package/plugins/compound-engineering/commands/workflows/review.md +23 -21
  21. package/plugins/compound-engineering/commands/workflows/work.md +29 -15
  22. package/plugins/compound-engineering/skills/dspy-ruby/SKILL.md +539 -396
  23. package/plugins/compound-engineering/skills/dspy-ruby/assets/config-template.rb +159 -331
  24. package/plugins/compound-engineering/skills/dspy-ruby/assets/module-template.rb +210 -236
  25. package/plugins/compound-engineering/skills/dspy-ruby/assets/signature-template.rb +173 -95
  26. package/plugins/compound-engineering/skills/dspy-ruby/references/core-concepts.md +552 -143
  27. package/plugins/compound-engineering/skills/dspy-ruby/references/observability.md +366 -0
  28. package/plugins/compound-engineering/skills/dspy-ruby/references/optimization.md +440 -460
  29. package/plugins/compound-engineering/skills/dspy-ruby/references/providers.md +305 -225
  30. package/plugins/compound-engineering/skills/dspy-ruby/references/toolsets.md +502 -0
  31. package/plugins/compound-engineering/skills/setup/SKILL.md +168 -0
  32. package/src/commands/convert.ts +10 -5
  33. package/src/commands/install.ts +10 -5
  34. package/src/converters/claude-to-codex.ts +7 -2
  35. package/src/converters/claude-to-cursor.ts +166 -0
  36. package/src/converters/claude-to-droid.ts +174 -0
  37. package/src/converters/claude-to-opencode.ts +8 -2
  38. package/src/targets/cursor.ts +48 -0
  39. package/src/targets/droid.ts +50 -0
  40. package/src/targets/index.ts +18 -0
  41. package/src/types/cursor.ts +29 -0
  42. package/src/types/droid.ts +20 -0
  43. package/tests/codex-converter.test.ts +62 -0
  44. package/tests/converter.test.ts +61 -0
  45. package/tests/cursor-converter.test.ts +347 -0
  46. package/tests/cursor-writer.test.ts +137 -0
  47. package/tests/droid-converter.test.ts +277 -0
  48. package/tests/droid-writer.test.ts +100 -0
  49. package/plugins/compound-engineering/commands/technical_review.md +0 -8
@@ -1,326 +1,300 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Example DSPy Module Template
4
- # This template demonstrates best practices for creating composable modules
5
-
6
- # Basic module with single predictor
7
- class BasicModule < DSPy::Module
3
+ # =============================================================================
4
+ # DSPy.rb Module Template v0.34.3 API
5
+ #
6
+ # Modules orchestrate predictors, tools, and business logic.
7
+ #
8
+ # Key patterns:
9
+ # - Use .call() to invoke (not .forward())
10
+ # - Access results with result.field (not result[:field])
11
+ # - Use DSPy::Tools::Base for tools (not DSPy::Tool)
12
+ # - Use lifecycle callbacks (before/around/after) for cross-cutting concerns
13
+ # - Use DSPy.with_lm for temporary model overrides
14
+ # - Use configure_predictor for fine-grained agent control
15
+ # =============================================================================
16
+
17
+ # --- Basic Module ---
18
+
19
+ class BasicClassifier < DSPy::Module
8
20
  def initialize
9
21
  super
10
- # Initialize predictor with signature
11
- @predictor = DSPy::Predict.new(ExampleSignature)
22
+ @predictor = DSPy::Predict.new(ClassificationSignature)
12
23
  end
13
24
 
14
- def forward(input_hash)
15
- # Forward pass through the predictor
16
- @predictor.forward(input_hash)
25
+ def forward(text:)
26
+ @predictor.call(text: text)
17
27
  end
18
28
  end
19
29
 
20
- # Module with Chain of Thought reasoning
21
- class ChainOfThoughtModule < DSPy::Module
30
+ # Usage:
31
+ # classifier = BasicClassifier.new
32
+ # result = classifier.call(text: "This is a test")
33
+ # result.category # => "technical"
34
+ # result.confidence # => 0.95
35
+
36
+ # --- Module with Chain of Thought ---
37
+
38
+ class ReasoningClassifier < DSPy::Module
22
39
  def initialize
23
40
  super
24
- # ChainOfThought automatically adds reasoning to output
25
- @predictor = DSPy::ChainOfThought.new(EmailClassificationSignature)
41
+ @predictor = DSPy::ChainOfThought.new(ClassificationSignature)
26
42
  end
27
43
 
28
- def forward(email_subject:, email_body:)
29
- result = @predictor.forward(
30
- email_subject: email_subject,
31
- email_body: email_body
32
- )
44
+ def forward(text:)
45
+ result = @predictor.call(text: text)
46
+ # ChainOfThought adds result.reasoning automatically
47
+ result
48
+ end
49
+ end
50
+
51
+ # --- Module with Lifecycle Callbacks ---
52
+
53
+ class InstrumentedModule < DSPy::Module
54
+ before :setup_metrics
55
+ around :manage_context
56
+ after :log_completion
33
57
 
34
- # Result includes :reasoning field automatically
35
- {
36
- category: result[:category],
37
- priority: result[:priority],
38
- reasoning: result[:reasoning],
39
- confidence: calculate_confidence(result)
40
- }
58
+ def initialize
59
+ super
60
+ @predictor = DSPy::Predict.new(AnalysisSignature)
61
+ @start_time = nil
62
+ end
63
+
64
+ def forward(query:)
65
+ @predictor.call(query: query)
41
66
  end
42
67
 
43
68
  private
44
69
 
45
- def calculate_confidence(result)
46
- # Add custom logic to calculate confidence
47
- # For example, based on reasoning length or specificity
48
- result[:confidence] || 0.8
70
+ # Runs before forward
71
+ def setup_metrics
72
+ @start_time = Time.now
73
+ Rails.logger.info "Starting prediction"
49
74
  end
75
+
76
+ # Wraps forward — must call yield
77
+ def manage_context
78
+ load_user_context
79
+ result = yield
80
+ save_updated_context(result)
81
+ result
82
+ end
83
+
84
+ # Runs after forward completes
85
+ def log_completion
86
+ duration = Time.now - @start_time
87
+ Rails.logger.info "Prediction completed in #{duration}s"
88
+ end
89
+
90
+ def load_user_context = nil
91
+ def save_updated_context(_result) = nil
50
92
  end
51
93
 
52
- # Composable module that chains multiple steps
53
- class MultiStepPipeline < DSPy::Module
54
- def initialize
55
- super
56
- # Initialize multiple predictors for different steps
57
- @step1 = DSPy::Predict.new(Step1Signature)
58
- @step2 = DSPy::ChainOfThought.new(Step2Signature)
59
- @step3 = DSPy::Predict.new(Step3Signature)
94
+ # Execution order: before around (before yield) → forward → around (after yield) → after
95
+ # Callbacks are inherited from parent classes and execute in registration order.
96
+
97
+ # --- Module with Tools ---
98
+
99
+ class SearchTool < DSPy::Tools::Base
100
+ tool_name "search"
101
+ tool_description "Search for information by query"
102
+
103
+ sig { params(query: String, max_results: Integer).returns(T::Array[T::Hash[Symbol, String]]) }
104
+ def call(query:, max_results: 5)
105
+ # Implementation here
106
+ [{ title: "Result 1", url: "https://example.com" }]
60
107
  end
108
+ end
109
+
110
+ class FinishTool < DSPy::Tools::Base
111
+ tool_name "finish"
112
+ tool_description "Submit the final answer"
61
113
 
62
- def forward(input)
63
- # Chain predictors together
64
- result1 = @step1.forward(input)
65
- result2 = @step2.forward(result1)
66
- result3 = @step3.forward(result2)
67
-
68
- # Combine results as needed
69
- {
70
- step1_output: result1,
71
- step2_output: result2,
72
- final_result: result3
73
- }
114
+ sig { params(answer: String).returns(String) }
115
+ def call(answer:)
116
+ answer
74
117
  end
75
118
  end
76
119
 
77
- # Module with conditional logic
78
- class ConditionalModule < DSPy::Module
120
+ class ResearchAgent < DSPy::Module
79
121
  def initialize
80
122
  super
81
- @simple_classifier = DSPy::Predict.new(SimpleClassificationSignature)
82
- @complex_analyzer = DSPy::ChainOfThought.new(ComplexAnalysisSignature)
123
+ tools = [SearchTool.new, FinishTool.new]
124
+ @agent = DSPy::ReAct.new(
125
+ ResearchSignature,
126
+ tools: tools,
127
+ max_iterations: 5
128
+ )
83
129
  end
84
130
 
85
- def forward(text:, complexity_threshold: 100)
86
- # Use different predictors based on input characteristics
87
- if text.length < complexity_threshold
88
- @simple_classifier.forward(text: text)
89
- else
90
- @complex_analyzer.forward(text: text)
91
- end
131
+ def forward(question:)
132
+ @agent.call(question: question)
92
133
  end
93
134
  end
94
135
 
95
- # Module with error handling and retry logic
96
- class RobustModule < DSPy::Module
97
- MAX_RETRIES = 3
136
+ # --- Module with Per-Task Model Selection ---
98
137
 
138
+ class SmartRouter < DSPy::Module
99
139
  def initialize
100
140
  super
101
- @predictor = DSPy::Predict.new(RobustSignature)
102
- @logger = Logger.new(STDOUT)
141
+ @classifier = DSPy::Predict.new(RouteSignature)
142
+ @analyzer = DSPy::ChainOfThought.new(AnalysisSignature)
103
143
  end
104
144
 
105
- def forward(input, retry_count: 0)
106
- @logger.info "Processing input: #{input.inspect}"
145
+ def forward(text:)
146
+ # Use fast model for classification
147
+ DSPy.with_lm(fast_model) do
148
+ route = @classifier.call(text: text)
107
149
 
108
- begin
109
- result = @predictor.forward(input)
110
- validate_result!(result)
111
- result
112
- rescue DSPy::ValidationError => e
113
- @logger.error "Validation error: #{e.message}"
114
-
115
- if retry_count < MAX_RETRIES
116
- @logger.info "Retrying (#{retry_count + 1}/#{MAX_RETRIES})..."
117
- sleep(2 ** retry_count) # Exponential backoff
118
- forward(input, retry_count: retry_count + 1)
150
+ if route.requires_deep_analysis
151
+ # Switch to powerful model for analysis
152
+ DSPy.with_lm(powerful_model) do
153
+ @analyzer.call(text: text)
154
+ end
119
155
  else
120
- @logger.error "Max retries exceeded"
121
- raise
156
+ route
122
157
  end
123
158
  end
124
159
  end
125
160
 
126
161
  private
127
162
 
128
- def validate_result!(result)
129
- # Add custom validation logic
130
- raise DSPy::ValidationError, "Invalid result" unless result[:category]
131
- raise DSPy::ValidationError, "Low confidence" if result[:confidence] && result[:confidence] < 0.5
163
+ def fast_model
164
+ @fast_model ||= DSPy::LM.new(
165
+ ENV.fetch("DSPY_SELECTOR_MODEL", "ruby_llm/gemini-2.5-flash-lite"),
166
+ structured_outputs: true
167
+ )
168
+ end
169
+
170
+ def powerful_model
171
+ @powerful_model ||= DSPy::LM.new(
172
+ ENV.fetch("DSPY_SYNTHESIZER_MODEL", "ruby_llm/gemini-2.5-flash"),
173
+ structured_outputs: true
174
+ )
132
175
  end
133
176
  end
134
177
 
135
- # Module with ReAct agent and tools
136
- class AgentModule < DSPy::Module
178
+ # --- Module with configure_predictor ---
179
+
180
+ class ConfiguredAgent < DSPy::Module
137
181
  def initialize
138
182
  super
183
+ tools = [SearchTool.new, FinishTool.new]
184
+ @agent = DSPy::ReAct.new(ResearchSignature, tools: tools)
139
185
 
140
- # Define tools for the agent
141
- tools = [
142
- SearchTool.new,
143
- CalculatorTool.new,
144
- DatabaseQueryTool.new
145
- ]
186
+ # Set default model for all internal predictors
187
+ @agent.configure { |c| c.lm = DSPy::LM.new('ruby_llm/gemini-2.5-flash', structured_outputs: true) }
146
188
 
147
- # ReAct provides iterative reasoning and tool usage
148
- @agent = DSPy::ReAct.new(
149
- AgentSignature,
150
- tools: tools,
151
- max_iterations: 5
152
- )
189
+ # Override specific predictor with a more capable model
190
+ @agent.configure_predictor('thought_generator') do |c|
191
+ c.lm = DSPy::LM.new('ruby_llm/claude-sonnet-4-20250514', structured_outputs: true)
192
+ end
153
193
  end
154
194
 
155
- def forward(task:)
156
- # Agent will autonomously use tools to complete the task
157
- @agent.forward(task: task)
195
+ def forward(question:)
196
+ @agent.call(question: question)
158
197
  end
159
198
  end
160
199
 
161
- # Tool definition example
162
- class SearchTool < DSPy::Tool
163
- def call(query:)
164
- # Implement search functionality
165
- results = perform_search(query)
166
- { results: results }
167
- end
168
-
169
- private
200
+ # Available internal predictors by agent type:
201
+ # DSPy::ReAct → thought_generator, observation_processor
202
+ # DSPy::CodeAct → code_generator, observation_processor
203
+ # DSPy::DeepSearch seed_predictor, search_predictor, reader_predictor, reason_predictor
170
204
 
171
- def perform_search(query)
172
- # Actual search implementation
173
- # Could call external API, database, etc.
174
- ["result1", "result2", "result3"]
175
- end
176
- end
205
+ # --- Module with Event Subscriptions ---
177
206
 
178
- # Module with state management
179
- class StatefulModule < DSPy::Module
180
- attr_reader :history
207
+ class TokenTrackingModule < DSPy::Module
208
+ subscribe 'lm.tokens', :track_tokens, scope: :descendants
181
209
 
182
210
  def initialize
183
211
  super
184
- @predictor = DSPy::ChainOfThought.new(StatefulSignature)
185
- @history = []
212
+ @predictor = DSPy::Predict.new(AnalysisSignature)
213
+ @total_tokens = 0
186
214
  end
187
215
 
188
- def forward(input)
189
- # Process with context from history
190
- context = build_context_from_history
191
- result = @predictor.forward(
192
- input: input,
193
- context: context
194
- )
195
-
196
- # Store in history
197
- @history << {
198
- input: input,
199
- result: result,
200
- timestamp: Time.now
201
- }
202
-
203
- result
216
+ def forward(query:)
217
+ @predictor.call(query: query)
204
218
  end
205
219
 
206
- def reset!
207
- @history.clear
220
+ def track_tokens(_event, attrs)
221
+ @total_tokens += attrs.fetch(:total_tokens, 0)
208
222
  end
209
223
 
210
- private
211
-
212
- def build_context_from_history
213
- @history.last(5).map { |h| h[:result][:summary] }.join("\n")
224
+ def token_usage
225
+ @total_tokens
214
226
  end
215
227
  end
216
228
 
217
- # Module that uses different LLMs for different tasks
218
- class MultiModelModule < DSPy::Module
219
- def initialize
220
- super
229
+ # Module-scoped subscriptions automatically scope to the module instance and descendants.
230
+ # Use scope: :self_only to restrict delivery to the module itself (ignoring children).
221
231
 
222
- # Fast, cheap model for simple classification
223
- @fast_predictor = create_predictor(
224
- 'openai/gpt-4o-mini',
225
- SimpleClassificationSignature
226
- )
232
+ # --- Tool That Wraps a Prediction ---
227
233
 
228
- # Powerful model for complex analysis
229
- @powerful_predictor = create_predictor(
230
- 'anthropic/claude-3-5-sonnet-20241022',
231
- ComplexAnalysisSignature
232
- )
233
- end
234
+ class RerankTool < DSPy::Tools::Base
235
+ tool_name "rerank"
236
+ tool_description "Score and rank search results by relevance"
234
237
 
235
- def forward(input, use_complex: false)
236
- if use_complex
237
- @powerful_predictor.forward(input)
238
- else
239
- @fast_predictor.forward(input)
240
- end
241
- end
238
+ MAX_ITEMS = 200
239
+ MIN_ITEMS_FOR_LLM = 5
242
240
 
243
- private
241
+ sig { params(query: String, items: T::Array[T::Hash[Symbol, T.untyped]]).returns(T::Hash[Symbol, T.untyped]) }
242
+ def call(query:, items: [])
243
+ # Short-circuit: skip LLM for small sets
244
+ return { scored_items: items, reranked: false } if items.size < MIN_ITEMS_FOR_LLM
245
+
246
+ # Cap to prevent token overflow
247
+ capped_items = items.first(MAX_ITEMS)
248
+
249
+ predictor = DSPy::Predict.new(RerankSignature)
250
+ predictor.configure { |c| c.lm = DSPy::LM.new("ruby_llm/gemini-2.5-flash", structured_outputs: true) }
244
251
 
245
- def create_predictor(model, signature)
246
- lm = DSPy::LM.new(model, api_key: ENV["#{model.split('/').first.upcase}_API_KEY"])
247
- DSPy::Predict.new(signature, lm: lm)
252
+ result = predictor.call(query: query, items: capped_items)
253
+ { scored_items: result.scored_items, reranked: true }
254
+ rescue => e
255
+ Rails.logger.warn "[RerankTool] LLM rerank failed: #{e.message}"
256
+ { error: "Rerank failed: #{e.message}", scored_items: items, reranked: false }
248
257
  end
249
258
  end
250
259
 
251
- # Module with caching
252
- class CachedModule < DSPy::Module
260
+ # Key patterns for tools wrapping predictions:
261
+ # - Short-circuit LLM calls when unnecessary (small data, trivial cases)
262
+ # - Cap input size to prevent token overflow
263
+ # - Per-tool model selection via configure
264
+ # - Graceful error handling with fallback data
265
+
266
+ # --- Multi-Step Pipeline ---
267
+
268
+ class AnalysisPipeline < DSPy::Module
253
269
  def initialize
254
270
  super
255
- @predictor = DSPy::Predict.new(CachedSignature)
256
- @cache = {}
271
+ @classifier = DSPy::Predict.new(ClassifySignature)
272
+ @analyzer = DSPy::ChainOfThought.new(AnalyzeSignature)
273
+ @summarizer = DSPy::Predict.new(SummarizeSignature)
257
274
  end
258
275
 
259
- def forward(input)
260
- # Create cache key from input
261
- cache_key = create_cache_key(input)
262
-
263
- # Return cached result if available
264
- if @cache.key?(cache_key)
265
- puts "Cache hit for #{cache_key}"
266
- return @cache[cache_key]
267
- end
268
-
269
- # Compute and cache result
270
- result = @predictor.forward(input)
271
- @cache[cache_key] = result
272
- result
276
+ def forward(text:)
277
+ classification = @classifier.call(text: text)
278
+ analysis = @analyzer.call(text: text, category: classification.category)
279
+ @summarizer.call(analysis: analysis.reasoning, category: classification.category)
273
280
  end
281
+ end
274
282
 
275
- def clear_cache!
276
- @cache.clear
277
- end
283
+ # --- Observability with Spans ---
278
284
 
279
- private
285
+ class TracedModule < DSPy::Module
286
+ def initialize
287
+ super
288
+ @predictor = DSPy::Predict.new(AnalysisSignature)
289
+ end
280
290
 
281
- def create_cache_key(input)
282
- # Create deterministic hash from input
283
- Digest::MD5.hexdigest(input.to_s)
291
+ def forward(query:)
292
+ DSPy::Context.with_span(
293
+ operation: "traced_module.analyze",
294
+ "dspy.module" => self.class.name,
295
+ "query.length" => query.length.to_s
296
+ ) do
297
+ @predictor.call(query: query)
298
+ end
284
299
  end
285
300
  end
286
-
287
- # Usage Examples:
288
- #
289
- # Basic usage:
290
- # module = BasicModule.new
291
- # result = module.forward(field_name: "value")
292
- #
293
- # Chain of Thought:
294
- # module = ChainOfThoughtModule.new
295
- # result = module.forward(
296
- # email_subject: "Can't log in",
297
- # email_body: "I'm unable to access my account"
298
- # )
299
- # puts result[:reasoning]
300
- #
301
- # Multi-step pipeline:
302
- # pipeline = MultiStepPipeline.new
303
- # result = pipeline.forward(input_data)
304
- #
305
- # With error handling:
306
- # module = RobustModule.new
307
- # begin
308
- # result = module.forward(input_data)
309
- # rescue DSPy::ValidationError => e
310
- # puts "Failed after retries: #{e.message}"
311
- # end
312
- #
313
- # Agent with tools:
314
- # agent = AgentModule.new
315
- # result = agent.forward(task: "Find the population of Tokyo")
316
- #
317
- # Stateful processing:
318
- # module = StatefulModule.new
319
- # result1 = module.forward("First input")
320
- # result2 = module.forward("Second input") # Has context from first
321
- # module.reset! # Clear history
322
- #
323
- # With caching:
324
- # module = CachedModule.new
325
- # result1 = module.forward(input) # Computes result
326
- # result2 = module.forward(input) # Returns cached result