rubyn-code 0.1.0 → 0.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.
Files changed (159) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +269 -467
  3. data/db/migrations/009_create_teams.sql +6 -6
  4. data/db/migrations/011_fix_mailbox_messages_columns.rb +35 -0
  5. data/db/migrations/012_expand_mailbox_message_types.rb +37 -0
  6. data/exe/rubyn-code +1 -1
  7. data/lib/rubyn_code/agent/RUBYN.md +17 -0
  8. data/lib/rubyn_code/agent/conversation.rb +68 -19
  9. data/lib/rubyn_code/agent/loop.rb +312 -54
  10. data/lib/rubyn_code/agent/loop_detector.rb +6 -6
  11. data/lib/rubyn_code/auth/RUBYN.md +19 -0
  12. data/lib/rubyn_code/auth/oauth.rb +40 -35
  13. data/lib/rubyn_code/auth/server.rb +16 -12
  14. data/lib/rubyn_code/auth/token_store.rb +22 -22
  15. data/lib/rubyn_code/autonomous/RUBYN.md +14 -0
  16. data/lib/rubyn_code/autonomous/daemon.rb +115 -79
  17. data/lib/rubyn_code/autonomous/idle_poller.rb +4 -8
  18. data/lib/rubyn_code/autonomous/task_claimer.rb +11 -11
  19. data/lib/rubyn_code/background/RUBYN.md +13 -0
  20. data/lib/rubyn_code/background/notifier.rb +0 -2
  21. data/lib/rubyn_code/background/worker.rb +60 -15
  22. data/lib/rubyn_code/cli/RUBYN.md +30 -0
  23. data/lib/rubyn_code/cli/app.rb +85 -9
  24. data/lib/rubyn_code/cli/commands/RUBYN.md +133 -0
  25. data/lib/rubyn_code/cli/commands/base.rb +53 -0
  26. data/lib/rubyn_code/cli/commands/budget.rb +24 -0
  27. data/lib/rubyn_code/cli/commands/clear.rb +16 -0
  28. data/lib/rubyn_code/cli/commands/compact.rb +21 -0
  29. data/lib/rubyn_code/cli/commands/context.rb +44 -0
  30. data/lib/rubyn_code/cli/commands/context_info.rb +56 -0
  31. data/lib/rubyn_code/cli/commands/cost.rb +23 -0
  32. data/lib/rubyn_code/cli/commands/diff.rb +30 -0
  33. data/lib/rubyn_code/cli/commands/doctor.rb +112 -0
  34. data/lib/rubyn_code/cli/commands/help.rb +41 -0
  35. data/lib/rubyn_code/cli/commands/model.rb +37 -0
  36. data/lib/rubyn_code/cli/commands/plan.rb +22 -0
  37. data/lib/rubyn_code/cli/commands/quit.rb +17 -0
  38. data/lib/rubyn_code/cli/commands/registry.rb +64 -0
  39. data/lib/rubyn_code/cli/commands/resume.rb +51 -0
  40. data/lib/rubyn_code/cli/commands/review.rb +26 -0
  41. data/lib/rubyn_code/cli/commands/skill.rb +32 -0
  42. data/lib/rubyn_code/cli/commands/spawn.rb +24 -0
  43. data/lib/rubyn_code/cli/commands/tasks.rb +32 -0
  44. data/lib/rubyn_code/cli/commands/tokens.rb +76 -0
  45. data/lib/rubyn_code/cli/commands/undo.rb +17 -0
  46. data/lib/rubyn_code/cli/commands/version.rb +16 -0
  47. data/lib/rubyn_code/cli/daemon_runner.rb +129 -0
  48. data/lib/rubyn_code/cli/input_handler.rb +20 -23
  49. data/lib/rubyn_code/cli/renderer.rb +25 -27
  50. data/lib/rubyn_code/cli/repl.rb +161 -194
  51. data/lib/rubyn_code/cli/setup.rb +117 -0
  52. data/lib/rubyn_code/cli/spinner.rb +40 -40
  53. data/lib/rubyn_code/cli/stream_formatter.rb +29 -28
  54. data/lib/rubyn_code/cli/version_check.rb +94 -0
  55. data/lib/rubyn_code/config/RUBYN.md +14 -0
  56. data/lib/rubyn_code/config/defaults.rb +28 -19
  57. data/lib/rubyn_code/config/project_config.rb +7 -9
  58. data/lib/rubyn_code/config/settings.rb +3 -3
  59. data/lib/rubyn_code/context/RUBYN.md +20 -0
  60. data/lib/rubyn_code/context/auto_compact.rb +7 -7
  61. data/lib/rubyn_code/context/compactor.rb +2 -2
  62. data/lib/rubyn_code/context/context_collapse.rb +45 -0
  63. data/lib/rubyn_code/context/manager.rb +20 -3
  64. data/lib/rubyn_code/context/manual_compact.rb +7 -7
  65. data/lib/rubyn_code/context/micro_compact.rb +12 -12
  66. data/lib/rubyn_code/db/RUBYN.md +40 -0
  67. data/lib/rubyn_code/db/connection.rb +13 -13
  68. data/lib/rubyn_code/db/migrator.rb +67 -27
  69. data/lib/rubyn_code/db/schema.rb +6 -6
  70. data/lib/rubyn_code/debug.rb +74 -0
  71. data/lib/rubyn_code/hooks/RUBYN.md +17 -0
  72. data/lib/rubyn_code/hooks/built_in.rb +9 -9
  73. data/lib/rubyn_code/hooks/registry.rb +5 -5
  74. data/lib/rubyn_code/hooks/runner.rb +1 -1
  75. data/lib/rubyn_code/hooks/user_hooks.rb +16 -16
  76. data/lib/rubyn_code/learning/RUBYN.md +16 -0
  77. data/lib/rubyn_code/learning/extractor.rb +22 -22
  78. data/lib/rubyn_code/learning/injector.rb +17 -18
  79. data/lib/rubyn_code/learning/instinct.rb +18 -14
  80. data/lib/rubyn_code/llm/RUBYN.md +15 -0
  81. data/lib/rubyn_code/llm/client.rb +121 -55
  82. data/lib/rubyn_code/llm/message_builder.rb +19 -15
  83. data/lib/rubyn_code/llm/streaming.rb +80 -50
  84. data/lib/rubyn_code/mcp/RUBYN.md +21 -0
  85. data/lib/rubyn_code/mcp/client.rb +25 -24
  86. data/lib/rubyn_code/mcp/config.rb +7 -7
  87. data/lib/rubyn_code/mcp/sse_transport.rb +27 -26
  88. data/lib/rubyn_code/mcp/stdio_transport.rb +22 -19
  89. data/lib/rubyn_code/mcp/tool_bridge.rb +32 -32
  90. data/lib/rubyn_code/memory/RUBYN.md +17 -0
  91. data/lib/rubyn_code/memory/models.rb +3 -3
  92. data/lib/rubyn_code/memory/search.rb +17 -17
  93. data/lib/rubyn_code/memory/session_persistence.rb +49 -34
  94. data/lib/rubyn_code/memory/store.rb +17 -17
  95. data/lib/rubyn_code/observability/RUBYN.md +19 -0
  96. data/lib/rubyn_code/observability/budget_enforcer.rb +16 -15
  97. data/lib/rubyn_code/observability/cost_calculator.rb +3 -3
  98. data/lib/rubyn_code/observability/token_counter.rb +1 -1
  99. data/lib/rubyn_code/observability/usage_reporter.rb +35 -35
  100. data/lib/rubyn_code/output/RUBYN.md +11 -0
  101. data/lib/rubyn_code/output/diff_renderer.rb +6 -6
  102. data/lib/rubyn_code/output/formatter.rb +4 -4
  103. data/lib/rubyn_code/permissions/RUBYN.md +17 -0
  104. data/lib/rubyn_code/permissions/prompter.rb +8 -8
  105. data/lib/rubyn_code/protocols/RUBYN.md +14 -0
  106. data/lib/rubyn_code/protocols/interrupt_handler.rb +1 -1
  107. data/lib/rubyn_code/protocols/plan_approval.rb +9 -9
  108. data/lib/rubyn_code/protocols/shutdown_handshake.rb +9 -11
  109. data/lib/rubyn_code/skills/RUBYN.md +19 -0
  110. data/lib/rubyn_code/skills/catalog.rb +7 -7
  111. data/lib/rubyn_code/skills/document.rb +15 -15
  112. data/lib/rubyn_code/skills/loader.rb +6 -8
  113. data/lib/rubyn_code/sub_agents/RUBYN.md +12 -0
  114. data/lib/rubyn_code/sub_agents/runner.rb +15 -15
  115. data/lib/rubyn_code/sub_agents/summarizer.rb +1 -1
  116. data/lib/rubyn_code/tasks/RUBYN.md +13 -0
  117. data/lib/rubyn_code/tasks/dag.rb +12 -16
  118. data/lib/rubyn_code/tasks/manager.rb +24 -24
  119. data/lib/rubyn_code/tasks/models.rb +4 -4
  120. data/lib/rubyn_code/teams/RUBYN.md +14 -0
  121. data/lib/rubyn_code/teams/mailbox.rb +38 -18
  122. data/lib/rubyn_code/teams/manager.rb +19 -19
  123. data/lib/rubyn_code/teams/teammate.rb +3 -4
  124. data/lib/rubyn_code/tools/RUBYN.md +38 -0
  125. data/lib/rubyn_code/tools/background_run.rb +9 -11
  126. data/lib/rubyn_code/tools/base.rb +54 -3
  127. data/lib/rubyn_code/tools/bash.rb +16 -34
  128. data/lib/rubyn_code/tools/bundle_add.rb +10 -12
  129. data/lib/rubyn_code/tools/bundle_install.rb +9 -11
  130. data/lib/rubyn_code/tools/compact.rb +10 -9
  131. data/lib/rubyn_code/tools/db_migrate.rb +17 -15
  132. data/lib/rubyn_code/tools/edit_file.rb +12 -12
  133. data/lib/rubyn_code/tools/executor.rb +9 -4
  134. data/lib/rubyn_code/tools/git_commit.rb +29 -34
  135. data/lib/rubyn_code/tools/git_diff.rb +17 -18
  136. data/lib/rubyn_code/tools/git_log.rb +17 -19
  137. data/lib/rubyn_code/tools/git_status.rb +18 -20
  138. data/lib/rubyn_code/tools/glob.rb +7 -9
  139. data/lib/rubyn_code/tools/grep.rb +11 -9
  140. data/lib/rubyn_code/tools/load_skill.rb +7 -7
  141. data/lib/rubyn_code/tools/memory_search.rb +13 -12
  142. data/lib/rubyn_code/tools/memory_write.rb +14 -12
  143. data/lib/rubyn_code/tools/rails_generate.rb +16 -16
  144. data/lib/rubyn_code/tools/read_file.rb +8 -7
  145. data/lib/rubyn_code/tools/read_inbox.rb +5 -5
  146. data/lib/rubyn_code/tools/registry.rb +2 -2
  147. data/lib/rubyn_code/tools/review_pr.rb +55 -55
  148. data/lib/rubyn_code/tools/run_specs.rb +20 -19
  149. data/lib/rubyn_code/tools/schema.rb +9 -11
  150. data/lib/rubyn_code/tools/send_message.rb +10 -10
  151. data/lib/rubyn_code/tools/spawn_agent.rb +51 -23
  152. data/lib/rubyn_code/tools/spawn_teammate.rb +21 -21
  153. data/lib/rubyn_code/tools/task.rb +28 -28
  154. data/lib/rubyn_code/tools/web_fetch.rb +46 -31
  155. data/lib/rubyn_code/tools/web_search.rb +64 -66
  156. data/lib/rubyn_code/tools/write_file.rb +7 -6
  157. data/lib/rubyn_code/version.rb +1 -1
  158. data/lib/rubyn_code.rb +136 -105
  159. metadata +94 -21
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "yaml"
3
+ require 'yaml'
4
4
 
5
5
  module RubynCode
6
6
  module Hooks
@@ -26,8 +26,8 @@ module RubynCode
26
26
  # @return [void]
27
27
  def self.load!(registry, project_root:)
28
28
  paths = [
29
- File.join(project_root, ".rubyn-code", "hooks.yml"),
30
- File.join(Config::Defaults::HOME_DIR, "hooks.yml")
29
+ File.join(project_root, '.rubyn-code', 'hooks.yml'),
30
+ File.join(Config::Defaults::HOME_DIR, 'hooks.yml')
31
31
  ]
32
32
 
33
33
  paths.each do |path|
@@ -42,8 +42,8 @@ module RubynCode
42
42
  private
43
43
 
44
44
  def register_hooks(registry, config)
45
- register_pre_tool_use_hooks(registry, config["pre_tool_use"] || [])
46
- register_post_tool_use_hooks(registry, config["post_tool_use"] || [])
45
+ register_pre_tool_use_hooks(registry, config['pre_tool_use'] || [])
46
+ register_post_tool_use_hooks(registry, config['post_tool_use'] || [])
47
47
  end
48
48
 
49
49
  def register_pre_tool_use_hooks(registry, hook_configs)
@@ -51,9 +51,9 @@ module RubynCode
51
51
  registry.on(:pre_tool_use) do |tool_name:, tool_input:, **|
52
52
  next unless matches?(hook_config, tool_name, tool_input)
53
53
 
54
- case hook_config["action"]
55
- when "deny"
56
- { deny: true, reason: hook_config["reason"] || "Blocked by hooks.yml" }
54
+ case hook_config['action']
55
+ when 'deny'
56
+ { deny: true, reason: hook_config['reason'] || 'Blocked by hooks.yml' }
57
57
  end
58
58
  end
59
59
  end
@@ -62,12 +62,12 @@ module RubynCode
62
62
  def register_post_tool_use_hooks(registry, hook_configs)
63
63
  hook_configs.each do |hook_config|
64
64
  registry.on(:post_tool_use) do |tool_name:, result:, **|
65
- next result unless hook_config["tool"].nil? || hook_config["tool"] == tool_name
65
+ next result unless hook_config['tool'].nil? || hook_config['tool'] == tool_name
66
66
 
67
- if hook_config["action"] == "log"
68
- log_dir = ".rubyn-code"
69
- FileUtils.mkdir_p(log_dir) unless Dir.exist?(log_dir)
70
- File.open(File.join(log_dir, "audit.log"), "a") do |f|
67
+ if hook_config['action'] == 'log'
68
+ log_dir = '.rubyn-code'
69
+ FileUtils.mkdir_p(log_dir)
70
+ File.open(File.join(log_dir, 'audit.log'), 'a') do |f|
71
71
  f.puts "[#{Time.now}] #{tool_name}: #{result.to_s[0..200]}"
72
72
  end
73
73
  end
@@ -78,9 +78,9 @@ module RubynCode
78
78
  end
79
79
 
80
80
  def matches?(config, tool_name, params)
81
- return false if config["tool"] && config["tool"] != tool_name
82
- return false if config["match"] && !params.to_s.include?(config["match"])
83
- return false if config["path"] && !File.fnmatch?(config["path"], params[:path].to_s)
81
+ return false if config['tool'] && config['tool'] != tool_name
82
+ return false if config['match'] && !params.to_s.include?(config['match'])
83
+ return false if config['path'] && !File.fnmatch?(config['path'], params[:path].to_s)
84
84
 
85
85
  true
86
86
  end
@@ -0,0 +1,16 @@
1
+ # Layer 16: Learning
2
+
3
+ Continuous learning from session patterns with confidence decay.
4
+
5
+ ## Classes
6
+
7
+ - **`Extractor`** — Post-session analysis using a cheaper LLM (Haiku). Scans the last 30
8
+ messages for patterns: error resolutions, user corrections, workarounds, debugging
9
+ techniques, project-specific conventions. Persists as instincts.
10
+
11
+ - **`Instinct`** — A learned pattern with a confidence score that decays over time.
12
+ Stored in the `instincts` SQLite table. Higher confidence = more likely to be injected
13
+ into future prompts.
14
+
15
+ - **`Injector`** — Selects relevant instincts (confidence >= 0.3, max 10) and injects
16
+ them into the system prompt for the current session. Filters by project context.
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "json"
4
- require "securerandom"
3
+ require 'json'
4
+ require 'securerandom'
5
5
 
6
6
  module RubynCode
7
7
  module Learning
@@ -23,7 +23,7 @@ module RubynCode
23
23
  project_specific
24
24
  ].freeze
25
25
 
26
- EXTRACTION_PROMPT = <<~PROMPT
26
+ EXTRACTION_PROMPT = <<~PROMPT.freeze
27
27
  Analyze the following conversation between a developer and an AI coding assistant.
28
28
  Extract reusable patterns that could help in future sessions for this project.
29
29
 
@@ -75,22 +75,22 @@ module RubynCode
75
75
  def request_extraction(messages, llm_client)
76
76
  # Serialize conversation into a single user message to avoid
77
77
  # "must end with user message" errors
78
- transcript = messages.map { |m|
79
- role = (m[:role] || m["role"] || "unknown").capitalize
80
- content = m[:content] || m["content"]
78
+ transcript = messages.map do |m|
79
+ role = (m[:role] || m['role'] || 'unknown').capitalize
80
+ content = m[:content] || m['content']
81
81
  text = case content
82
82
  when String then content
83
83
  when Array
84
- content.filter_map { |b|
85
- b.respond_to?(:text) ? b.text : (b[:text] || b["text"])
86
- }.join("\n")
84
+ content.filter_map do |b|
85
+ b.respond_to?(:text) ? b.text : (b[:text] || b['text'])
86
+ end.join("\n")
87
87
  else content.to_s
88
88
  end
89
89
  "#{role}: #{text}"
90
- }.join("\n\n")
90
+ end.join("\n\n")
91
91
 
92
92
  llm_client.chat(
93
- messages: [{ role: "user", content: "#{EXTRACTION_PROMPT}\n\nConversation:\n#{transcript}" }],
93
+ messages: [{ role: 'user', content: "#{EXTRACTION_PROMPT}\n\nConversation:\n#{transcript}" }],
94
94
  max_tokens: 2000
95
95
  )
96
96
  rescue StandardError => e
@@ -104,7 +104,7 @@ module RubynCode
104
104
 
105
105
  instincts.each do |inst|
106
106
  db.execute(
107
- "INSERT INTO instincts (id, project_path, pattern, context_tags, confidence, decay_rate, times_applied, times_helpful, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
107
+ 'INSERT INTO instincts (id, project_path, pattern, context_tags, confidence, decay_rate, times_applied, times_helpful, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
108
108
  [
109
109
  SecureRandom.uuid,
110
110
  inst[:project_path],
@@ -147,15 +147,15 @@ module RubynCode
147
147
  block = response.content.find { |b| b.respond_to?(:text) }
148
148
  block&.text
149
149
  elsif response.is_a?(Hash)
150
- response.dig("content", 0, "text")
150
+ response.dig('content', 0, 'text')
151
151
  end
152
152
  end
153
153
 
154
154
  def normalize_pattern(raw, project_path)
155
- type = raw["type"].to_s
156
- pattern = raw["pattern"].to_s.strip
157
- context_tags = Array(raw["context_tags"]).map(&:to_s)
158
- confidence = raw["confidence"].to_f
155
+ type = raw['type'].to_s
156
+ pattern = raw['pattern'].to_s.strip
157
+ context_tags = Array(raw['context_tags']).map(&:to_s)
158
+ confidence = raw['confidence'].to_f
159
159
 
160
160
  return nil if pattern.empty?
161
161
  return nil unless VALID_TYPES.include?(type)
@@ -177,11 +177,11 @@ module RubynCode
177
177
  # Project-specific knowledge decays slower; workarounds decay faster.
178
178
  def decay_rate_for_type(type)
179
179
  case type
180
- when "project_specific" then 0.02
181
- when "error_resolution" then 0.03
182
- when "debugging_technique" then 0.04
183
- when "user_correction" then 0.05
184
- when "workaround" then 0.07
180
+ when 'project_specific' then 0.02
181
+ when 'error_resolution' then 0.03
182
+ when 'debugging_technique' then 0.04
183
+ when 'user_correction' then 0.05
184
+ when 'workaround' then 0.07
185
185
  else 0.05
186
186
  end
187
187
  end
@@ -1,8 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "json"
4
- require "time"
5
- require "set"
3
+ require 'json'
4
+ require 'time'
6
5
 
7
6
  module RubynCode
8
7
  module Learning
@@ -15,7 +14,7 @@ module RubynCode
15
14
  # Default maximum number of instincts to inject.
16
15
  DEFAULT_MAX_INSTINCTS = 10
17
16
 
18
- INSTINCTS_TABLE = "instincts"
17
+ INSTINCTS_TABLE = 'instincts'
19
18
 
20
19
  class << self
21
20
  # Queries and formats relevant instincts for system prompt injection.
@@ -27,7 +26,7 @@ module RubynCode
27
26
  # @return [String] formatted instincts block, or empty string if none found
28
27
  def call(db:, project_path:, context_tags: [], max_instincts: DEFAULT_MAX_INSTINCTS)
29
28
  rows = fetch_instincts(db, project_path)
30
- return "" if rows.empty?
29
+ return '' if rows.empty?
31
30
 
32
31
  instincts = rows.map { |row| row_to_instinct(row) }
33
32
 
@@ -46,7 +45,7 @@ module RubynCode
46
45
  .sort_by { |inst| -inst.confidence }
47
46
  .first(max_instincts)
48
47
 
49
- return "" if instincts.empty?
48
+ return '' if instincts.empty?
50
49
 
51
50
  format_instincts(instincts)
52
51
  end
@@ -65,16 +64,16 @@ module RubynCode
65
64
 
66
65
  def row_to_instinct(row)
67
66
  Instinct.new(
68
- id: row["id"],
69
- project_path: row["project_path"],
70
- pattern: row["pattern"],
71
- context_tags: parse_tags(row["context_tags"]),
72
- confidence: row["confidence"].to_f,
73
- decay_rate: row["decay_rate"].to_f,
74
- times_applied: row["times_applied"].to_i,
75
- times_helpful: row["times_helpful"].to_i,
76
- created_at: parse_time(row["created_at"]),
77
- updated_at: parse_time(row["updated_at"])
67
+ id: row['id'],
68
+ project_path: row['project_path'],
69
+ pattern: row['pattern'],
70
+ context_tags: parse_tags(row['context_tags']),
71
+ confidence: row['confidence'].to_f,
72
+ decay_rate: row['decay_rate'].to_f,
73
+ times_applied: row['times_applied'].to_i,
74
+ times_helpful: row['times_helpful'].to_i,
75
+ created_at: parse_time(row['created_at']),
76
+ updated_at: parse_time(row['updated_at'])
78
77
  )
79
78
  end
80
79
 
@@ -84,7 +83,7 @@ module RubynCode
84
83
  begin
85
84
  JSON.parse(tags)
86
85
  rescue JSON::ParserError
87
- tags.split(",").map(&:strip)
86
+ tags.split(',').map(&:strip)
88
87
  end
89
88
  when Array
90
89
  tags
@@ -111,7 +110,7 @@ module RubynCode
111
110
  # @param tags [Array<String>] required context tags
112
111
  # @return [Array<Instinct>] filtered instincts
113
112
  def filter_by_tags(instincts, tags)
114
- tag_set = tags.map(&:downcase).to_set
113
+ tag_set = tags.to_set(&:downcase)
115
114
 
116
115
  instincts.select do |inst|
117
116
  inst_tags = inst.context_tags.map(&:downcase)
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "time"
3
+ require 'time'
4
4
 
5
5
  module RubynCode
6
6
  module Learning
@@ -44,10 +44,10 @@ module RubynCode
44
44
 
45
45
  # Confidence label thresholds, checked in descending order.
46
46
  CONFIDENCE_LABELS = [
47
- [0.9, "near-certain"],
48
- [0.7, "confident"],
49
- [0.5, "moderate"],
50
- [0.3, "tentative"]
47
+ [0.9, 'near-certain'],
48
+ [0.7, 'confident'],
49
+ [0.5, 'moderate'],
50
+ [0.3, 'tentative']
51
51
  ].freeze
52
52
 
53
53
  class << self
@@ -105,7 +105,7 @@ module RubynCode
105
105
  return label if score >= threshold
106
106
  end
107
107
 
108
- "unreliable"
108
+ 'unreliable'
109
109
  end
110
110
 
111
111
  # Reinforces an instinct in the database by updating confidence
@@ -120,7 +120,7 @@ module RubynCode
120
120
 
121
121
  if helpful
122
122
  db.execute(
123
- "UPDATE instincts SET confidence = MIN(1.0, confidence + 0.1 * (1.0 - confidence)), times_applied = times_applied + 1, times_helpful = times_helpful + 1, updated_at = ? WHERE id = ?",
123
+ 'UPDATE instincts SET confidence = MIN(1.0, confidence + 0.1 * (1.0 - confidence)), times_applied = times_applied + 1, times_helpful = times_helpful + 1, updated_at = ? WHERE id = ?',
124
124
  [now, instinct_id]
125
125
  )
126
126
  else
@@ -141,25 +141,29 @@ module RubynCode
141
141
  # @return [void]
142
142
  def decay_all(db, project_path:)
143
143
  rows = db.query(
144
- "SELECT id, confidence, decay_rate, updated_at FROM instincts WHERE project_path = ?",
144
+ 'SELECT id, confidence, decay_rate, updated_at FROM instincts WHERE project_path = ?',
145
145
  [project_path]
146
146
  ).to_a
147
147
 
148
148
  now = Time.now
149
149
  rows.each do |row|
150
- updated_at = Time.parse(row["updated_at"].to_s) rescue Time.now
150
+ updated_at = begin
151
+ Time.parse(row['updated_at'].to_s)
152
+ rescue StandardError
153
+ Time.now
154
+ end
151
155
  elapsed_days = (now - updated_at).to_f / 86_400
152
156
  next if elapsed_days <= 0
153
157
 
154
- decay_factor = Math.exp(-row["decay_rate"].to_f * elapsed_days)
155
- new_confidence = (row["confidence"].to_f * decay_factor).clamp(MIN_CONFIDENCE, 1.0)
158
+ decay_factor = Math.exp(-row['decay_rate'].to_f * elapsed_days)
159
+ new_confidence = (row['confidence'].to_f * decay_factor).clamp(MIN_CONFIDENCE, 1.0)
156
160
 
157
161
  if new_confidence <= MIN_CONFIDENCE
158
- db.execute("DELETE FROM instincts WHERE id = ?", [row["id"]])
162
+ db.execute('DELETE FROM instincts WHERE id = ?', [row['id']])
159
163
  else
160
164
  db.execute(
161
- "UPDATE instincts SET confidence = ? WHERE id = ?",
162
- [new_confidence, row["id"]]
165
+ 'UPDATE instincts SET confidence = ? WHERE id = ?',
166
+ [new_confidence, row['id']]
163
167
  )
164
168
  end
165
169
  end
@@ -0,0 +1,15 @@
1
+ # LLM Layer
2
+
3
+ Faraday-based Claude API client with streaming support.
4
+
5
+ ## Classes
6
+
7
+ - **`Client`** — Sends messages to the Claude API. Handles auth headers (OAuth bearer or API key),
8
+ model selection, system prompts, and tool definitions. Returns parsed response or streams.
9
+
10
+ - **`Streaming`** — SSE stream parser for Claude's streaming API. Buffers partial events,
11
+ emits content blocks and tool_use blocks as they arrive. Feeds into `CLI::StreamFormatter`.
12
+
13
+ - **`MessageBuilder`** — Constructs the messages array for the API. Handles system prompt
14
+ injection, tool result formatting, and context window limits. Knows about Claude's
15
+ message format (role/content pairs, tool_use/tool_result blocks).