ruby_coded 0.3.1 → 0.4.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +20 -0
  3. data/README.md +113 -1
  4. data/lib/ruby_coded/chat/app.rb +35 -34
  5. data/lib/ruby_coded/chat/bridge_common/auto_switch.rb +45 -0
  6. data/lib/ruby_coded/chat/bridge_common/mode_transitions.rb +70 -0
  7. data/lib/ruby_coded/chat/bridge_common/tool_flow.rb +61 -0
  8. data/lib/ruby_coded/chat/bridge_common.rb +31 -0
  9. data/lib/ruby_coded/chat/bridge_factory.rb +61 -0
  10. data/lib/ruby_coded/chat/codex_bridge/error_handling.rb +1 -20
  11. data/lib/ruby_coded/chat/codex_bridge/request_builder.rb +3 -15
  12. data/lib/ruby_coded/chat/codex_bridge/tool_approval.rb +8 -32
  13. data/lib/ruby_coded/chat/codex_bridge/tool_handling.rb +3 -34
  14. data/lib/ruby_coded/chat/codex_bridge.rb +31 -46
  15. data/lib/ruby_coded/chat/command_handler/agent_commands.rb +3 -3
  16. data/lib/ruby_coded/chat/command_handler/plan_commands.rb +1 -1
  17. data/lib/ruby_coded/chat/command_handler/skill_commands.rb +87 -0
  18. data/lib/ruby_coded/chat/command_handler.rb +3 -0
  19. data/lib/ruby_coded/chat/llm_bridge/chat_configuration.rb +38 -0
  20. data/lib/ruby_coded/chat/llm_bridge/plan_mode.rb +3 -32
  21. data/lib/ruby_coded/chat/llm_bridge/tool_call_handling.rb +9 -50
  22. data/lib/ruby_coded/chat/llm_bridge.rb +31 -65
  23. data/lib/ruby_coded/chat/prompt_builder.rb +66 -0
  24. data/lib/ruby_coded/chat/renderer/chat_panel.rb +48 -62
  25. data/lib/ruby_coded/chat/renderer/chat_panel_cache.rb +35 -0
  26. data/lib/ruby_coded/chat/renderer/chat_panel_formatting.rb +72 -0
  27. data/lib/ruby_coded/chat/renderer/chat_panel_layout.rb +81 -0
  28. data/lib/ruby_coded/chat/renderer/chat_panel_sections.rb +93 -0
  29. data/lib/ruby_coded/chat/renderer/chat_panel_thinking.rb +20 -34
  30. data/lib/ruby_coded/chat/renderer/chat_panel_thinking_render.rb +49 -0
  31. data/lib/ruby_coded/chat/renderer/rich_text.rb +92 -0
  32. data/lib/ruby_coded/chat/renderer/rich_text_inline.rb +81 -0
  33. data/lib/ruby_coded/chat/renderer.rb +12 -0
  34. data/lib/ruby_coded/chat/runtime_mode.rb +97 -0
  35. data/lib/ruby_coded/commands/core_provider.rb +7 -0
  36. data/lib/ruby_coded/commands.rb +11 -0
  37. data/lib/ruby_coded/plugins.rb +10 -1
  38. data/lib/ruby_coded/skills/catalog.rb +103 -0
  39. data/lib/ruby_coded/skills/markdown_loader.rb +112 -0
  40. data/lib/ruby_coded/skills/prompt_formatter.rb +42 -0
  41. data/lib/ruby_coded/skills/skill_definition.rb +29 -0
  42. data/lib/ruby_coded/skills.rb +18 -0
  43. data/lib/ruby_coded/tools/base_tool.rb +14 -0
  44. data/lib/ruby_coded/tools/delete_path_tool.rb +5 -8
  45. data/lib/ruby_coded/tools/edit_file_tool.rb +6 -8
  46. data/lib/ruby_coded/tools/execution_pipeline.rb +60 -0
  47. data/lib/ruby_coded/tools/execution_policy.rb +89 -0
  48. data/lib/ruby_coded/tools/write_file_tool.rb +6 -9
  49. data/lib/ruby_coded/version.rb +1 -1
  50. data/lib/ruby_coded.rb +1 -0
  51. metadata +25 -2
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyCoded
4
+ module Skills
5
+ # Formats active skills for prompt injection.
6
+ module PromptFormatter
7
+ def self.append(base_instructions, skills)
8
+ skills = Array(skills)
9
+ return base_instructions if skills.empty?
10
+
11
+ <<~PROMPT
12
+ #{base_instructions}
13
+
14
+ ## Active project skills
15
+
16
+ Apply the following project-local skills when relevant. If a skill conflicts with higher-priority system instructions or the user's explicit request, follow the higher-priority instruction.
17
+
18
+ #{format_skills(skills)}
19
+ PROMPT
20
+ end
21
+
22
+ def self.format_skills(skills)
23
+ skills.map { |skill| format_skill(skill) }.join("\n\n")
24
+ end
25
+
26
+ def self.format_skill(skill)
27
+ (["### #{skill.name}"] + skill_metadata_lines(skill) + ["", skill.content.to_s]).join("\n")
28
+ end
29
+
30
+ def self.skill_metadata_lines(skill)
31
+ description = skill.description.to_s
32
+ trigger = skill.trigger.to_s
33
+ [
34
+ (description unless description.empty?),
35
+ "Modes: #{skill.modes.join(", ")}",
36
+ ("Tags: #{skill.tags.join(", ")}" if skill.tags.any?),
37
+ ("Trigger: #{trigger}" unless trigger.empty?)
38
+ ].compact
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyCoded
4
+ module Skills
5
+ # Normalized project-local skill metadata.
6
+ class SkillDefinition
7
+ ATTRIBUTES = %i[name description modes content path priority tags trigger].freeze
8
+
9
+ attr_reader(*ATTRIBUTES)
10
+
11
+ def initialize(**attrs)
12
+ ATTRIBUTES.each { |attr| instance_variable_set(ivar(attr), attrs[attr]) }
13
+ @modes = Array(@modes).map(&:to_s)
14
+ @tags = Array(@tags).map(&:to_s)
15
+ @priority = (@priority || 0).to_i
16
+ end
17
+
18
+ def applies_to_mode?(mode)
19
+ @modes.include?(mode.to_s)
20
+ end
21
+
22
+ private
23
+
24
+ def ivar(name)
25
+ :"@#{name}"
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Skills are **automatic instruction overlays** applied to the
4
+ # system prompt for a given mode.
5
+ #
6
+ # The user does not invoke a skill directly; the PromptBuilder
7
+ # consults the Skills::Catalog for the active RuntimeMode and
8
+ # appends the relevant skills to the final instructions. Skills
9
+ # are defined as markdown files under `.rubycoded/skills`.
10
+ #
11
+ # See also:
12
+ # - RubyCoded::Commands — explicit user-invoked slash actions.
13
+ # - RubyCoded::Plugins — code-level behavioral extensions.
14
+
15
+ require_relative "skills/skill_definition"
16
+ require_relative "skills/markdown_loader"
17
+ require_relative "skills/catalog"
18
+ require_relative "skills/prompt_formatter"
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "ruby_llm"
4
4
  require_relative "tool_rejected_error"
5
+ require_relative "execution_pipeline"
5
6
 
6
7
  module RubyCoded
7
8
  module Tools
@@ -28,6 +29,19 @@ module RubyCoded
28
29
 
29
30
  private
30
31
 
32
+ # Runs the shared execution pipeline (path validation, project-root
33
+ # guard, filesystem error normalization) around the given block.
34
+ # Prefer this over calling validate_path! directly for tools that
35
+ # need the full pipeline; use validate_path! when only path
36
+ # resolution is required (e.g. read-only tools with custom flows).
37
+ def run_pipeline(path: nil, forbid_root: false, &)
38
+ pipeline.call(path: path, forbid_root: forbid_root, &)
39
+ end
40
+
41
+ def pipeline
42
+ @pipeline ||= ExecutionPipeline.new(project_root: @project_root)
43
+ end
44
+
31
45
  def resolve_path(relative_path)
32
46
  expanded = File.expand_path(relative_path, @project_root)
33
47
  File.realpath(expanded)
@@ -15,14 +15,11 @@ module RubyCoded
15
15
  end
16
16
 
17
17
  def execute(path:)
18
- full_path = validate_path!(path)
19
- return full_path if full_path.is_a?(Hash)
20
- return { error: "Path not found: #{path}" } unless File.exist?(full_path)
21
- return { error: "Cannot delete the project root" } if full_path == @project_root
22
-
23
- perform_delete(path, full_path)
24
- rescue SystemCallError => e
25
- { error: "Failed to delete #{path}: #{e.message}" }
18
+ run_pipeline(path: path, forbid_root: true) do |full_path|
19
+ next { error: "Path not found: #{path}" } unless File.exist?(full_path)
20
+
21
+ perform_delete(path, full_path)
22
+ end
26
23
  end
27
24
 
28
25
  private
@@ -16,14 +16,12 @@ module RubyCoded
16
16
  end
17
17
 
18
18
  def execute(path:, old_text:, new_text:)
19
- full_path = validate_path!(path)
20
- return full_path if full_path.is_a?(Hash)
21
- return { error: "File not found: #{path}" } unless File.exist?(full_path)
22
- return { error: "Not a file: #{path}" } unless File.file?(full_path)
23
-
24
- apply_edit(path, full_path, old_text, new_text)
25
- rescue SystemCallError => e
26
- { error: "Failed to edit #{path}: #{e.message}" }
19
+ run_pipeline(path: path) do |full_path|
20
+ next { error: "File not found: #{path}" } unless File.exist?(full_path)
21
+ next { error: "Not a file: #{path}" } unless File.file?(full_path)
22
+
23
+ apply_edit(path, full_path, old_text, new_text)
24
+ end
27
25
  end
28
26
 
29
27
  private
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyCoded
4
+ module Tools
5
+ # Standardizes the internal execution flow shared by side-effecting tools:
6
+ # 1. validate input (project-relative path if given)
7
+ # 2. resolve to an absolute path inside the project root
8
+ # 3. optionally forbid operating on the project root itself
9
+ # 4. execute the tool's business logic (via block)
10
+ # 5. normalize filesystem errors into a structured response
11
+ #
12
+ # Policy, risk assessment and user approval intentionally live outside
13
+ # this pipeline: they are handled by Tools::ExecutionPolicy and by the
14
+ # bridges (BridgeCommon::ToolFlow), which sit above tool execution.
15
+ class ExecutionPipeline
16
+ def initialize(project_root:)
17
+ @project_root = File.realpath(project_root)
18
+ end
19
+
20
+ # Runs the pipeline. Yields the resolved full_path (or nil if `path`
21
+ # was not provided) to the caller-supplied block, which returns the
22
+ # tool result — either a success value (string/hash without :error)
23
+ # or an error hash of the form { error: "..." }.
24
+ #
25
+ # Any SystemCallError raised inside the block is normalized to an
26
+ # error hash.
27
+ def call(path: nil, forbid_root: false)
28
+ full_path = nil
29
+ if path
30
+ full_path = resolve_and_validate(path)
31
+ return full_path if full_path.is_a?(Hash)
32
+ return { error: "Cannot operate on the project root" } if forbid_root && full_path == @project_root
33
+ end
34
+
35
+ yield(full_path)
36
+ rescue SystemCallError => e
37
+ { error: "Filesystem error: #{e.message}" }
38
+ end
39
+
40
+ private
41
+
42
+ def resolve_and_validate(relative_path)
43
+ expanded = File.expand_path(relative_path, @project_root)
44
+ full_path = begin
45
+ File.realpath(expanded)
46
+ rescue Errno::ENOENT
47
+ expanded
48
+ end
49
+
50
+ return full_path if inside_project?(full_path)
51
+
52
+ { error: "Path is outside the project directory. Only paths within #{@project_root} are allowed." }
53
+ end
54
+
55
+ def inside_project?(full_path)
56
+ full_path.start_with?(@project_root)
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_tool"
4
+ require_relative "agent_iteration_limit_error"
5
+
6
+ module RubyCoded
7
+ module Tools
8
+ # Centralizes runtime safety policy for tool execution:
9
+ #
10
+ # - risk lookup (delegated to the registry),
11
+ # - call budgeting (write vs total rounds, warning threshold),
12
+ # - confirmation requirement based on risk, mode, and auto-approve.
13
+ #
14
+ # Both LLMBridge and CodexBridge share a policy instance so
15
+ # limits and approval rules stay consistent across backends.
16
+ class ExecutionPolicy
17
+ MAX_WRITE_TOOL_ROUNDS = 50
18
+ MAX_TOTAL_TOOL_ROUNDS = 200
19
+ WARNING_THRESHOLD_RATIO = 0.8
20
+
21
+ def initialize(state:, registry:)
22
+ @state = state
23
+ @registry = registry
24
+ reset_counters!
25
+ end
26
+
27
+ def reset_counters!
28
+ @tool_call_count = 0
29
+ @write_tool_call_count = 0
30
+ end
31
+
32
+ def risk_for(tool_name)
33
+ @registry.risk_level_for(tool_name)
34
+ end
35
+
36
+ # Records the call, enforces hard limits, and returns the risk level.
37
+ # Raises AgentIterationLimitError when the total budget is exceeded.
38
+ def register_call!(tool_name)
39
+ risk = risk_for(tool_name)
40
+ @tool_call_count += 1
41
+ @write_tool_call_count += 1 unless risk == BaseTool::SAFE_RISK
42
+ enforce_limits!
43
+ risk
44
+ end
45
+
46
+ def warn_if_approaching_limit!
47
+ threshold = warning_threshold
48
+ return unless @tool_call_count == threshold
49
+
50
+ remaining = MAX_TOTAL_TOOL_ROUNDS - threshold
51
+ @state.add_message(:system,
52
+ "Approaching total tool call limit: #{remaining} calls remaining. " \
53
+ "Prioritize completing the most important work.")
54
+ end
55
+
56
+ # True when the user must confirm this call before it runs.
57
+ def requires_confirmation?(risk, mode)
58
+ return false if risk == BaseTool::SAFE_RISK
59
+ return false if @state.auto_approve_tools?
60
+
61
+ mode.requires_confirmation?
62
+ end
63
+
64
+ def risk_label_for(risk)
65
+ risk == BaseTool::DANGEROUS_RISK ? "DANGEROUS" : "WRITE"
66
+ end
67
+
68
+ private
69
+
70
+ def warning_threshold
71
+ (MAX_TOTAL_TOOL_ROUNDS * WARNING_THRESHOLD_RATIO).to_i
72
+ end
73
+
74
+ def enforce_limits!
75
+ if @write_tool_call_count >= MAX_WRITE_TOOL_ROUNDS
76
+ @write_tool_call_count = 0
77
+ @state.add_message(:system,
78
+ "Write tool call budget (#{MAX_WRITE_TOOL_ROUNDS}) reached — auto-resetting counter.")
79
+ end
80
+
81
+ return unless @tool_call_count > MAX_TOTAL_TOOL_ROUNDS
82
+
83
+ raise AgentIterationLimitError,
84
+ "Reached maximum of #{MAX_TOTAL_TOOL_ROUNDS} total tool calls. " \
85
+ "Send a new message to continue, or use /agent on to reset counters."
86
+ end
87
+ end
88
+ end
89
+ end
@@ -15,16 +15,13 @@ module RubyCoded
15
15
  end
16
16
 
17
17
  def execute(path:, content:)
18
- full_path = validate_path!(path)
19
- return full_path if full_path.is_a?(Hash)
18
+ run_pipeline(path: path) do |full_path|
19
+ dir = File.dirname(full_path)
20
+ FileUtils.mkdir_p(dir) unless File.directory?(dir)
20
21
 
21
- dir = File.dirname(full_path)
22
- FileUtils.mkdir_p(dir) unless File.directory?(dir)
23
-
24
- File.write(full_path, content)
25
- "File written: #{path} (#{content.bytesize} bytes)"
26
- rescue SystemCallError => e
27
- { error: "Failed to write #{path}: #{e.message}" }
22
+ File.write(full_path, content)
23
+ "File written: #{path} (#{content.bytesize} bytes)"
24
+ end
28
25
  end
29
26
  end
30
27
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  # This module contains the version of the RubyCoded gem
4
4
  module RubyCoded
5
- VERSION = "0.3.1"
5
+ VERSION = "0.4.0"
6
6
 
7
7
  def self.gem_version
8
8
  Gem::Version.new(VERSION).freeze
data/lib/ruby_coded.rb CHANGED
@@ -6,6 +6,7 @@ require_relative "ruby_coded/auth/auth_manager"
6
6
  require_relative "ruby_coded/initializer"
7
7
  require_relative "ruby_coded/plugins"
8
8
  require_relative "ruby_coded/commands"
9
+ require_relative "ruby_coded/skills"
9
10
 
10
11
  raise "This gem requires Ruby 3.3.0 or higher" if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.3.0")
11
12
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_coded
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cesar Rodriguez
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-06-28 00:00:00.000000000 Z
11
+ date: 2026-07-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -127,6 +127,11 @@ files:
127
127
  - lib/ruby_coded/chat/app/event_dispatch.rb
128
128
  - lib/ruby_coded/chat/app/login_handler.rb
129
129
  - lib/ruby_coded/chat/app/oauth_handler.rb
130
+ - lib/ruby_coded/chat/bridge_common.rb
131
+ - lib/ruby_coded/chat/bridge_common/auto_switch.rb
132
+ - lib/ruby_coded/chat/bridge_common/mode_transitions.rb
133
+ - lib/ruby_coded/chat/bridge_common/tool_flow.rb
134
+ - lib/ruby_coded/chat/bridge_factory.rb
130
135
  - lib/ruby_coded/chat/codex_bridge.rb
131
136
  - lib/ruby_coded/chat/codex_bridge/error_handling.rb
132
137
  - lib/ruby_coded/chat/codex_bridge/request_builder.rb
@@ -142,6 +147,7 @@ files:
142
147
  - lib/ruby_coded/chat/command_handler/login_commands.rb
143
148
  - lib/ruby_coded/chat/command_handler/model_commands.rb
144
149
  - lib/ruby_coded/chat/command_handler/plan_commands.rb
150
+ - lib/ruby_coded/chat/command_handler/skill_commands.rb
145
151
  - lib/ruby_coded/chat/command_handler/token_commands.rb
146
152
  - lib/ruby_coded/chat/command_handler/token_formatting.rb
147
153
  - lib/ruby_coded/chat/help.txt
@@ -150,21 +156,31 @@ files:
150
156
  - lib/ruby_coded/chat/input_handler/modal_inputs.rb
151
157
  - lib/ruby_coded/chat/input_handler/normal_mode_input.rb
152
158
  - lib/ruby_coded/chat/llm_bridge.rb
159
+ - lib/ruby_coded/chat/llm_bridge/chat_configuration.rb
153
160
  - lib/ruby_coded/chat/llm_bridge/plan_mode.rb
154
161
  - lib/ruby_coded/chat/llm_bridge/streaming_retries.rb
155
162
  - lib/ruby_coded/chat/llm_bridge/tool_call_handling.rb
156
163
  - lib/ruby_coded/chat/model_filter.rb
157
164
  - lib/ruby_coded/chat/plan_clarification_parser.rb
165
+ - lib/ruby_coded/chat/prompt_builder.rb
158
166
  - lib/ruby_coded/chat/renderer.rb
159
167
  - lib/ruby_coded/chat/renderer/chat_panel.rb
168
+ - lib/ruby_coded/chat/renderer/chat_panel_cache.rb
169
+ - lib/ruby_coded/chat/renderer/chat_panel_formatting.rb
160
170
  - lib/ruby_coded/chat/renderer/chat_panel_input.rb
171
+ - lib/ruby_coded/chat/renderer/chat_panel_layout.rb
172
+ - lib/ruby_coded/chat/renderer/chat_panel_sections.rb
161
173
  - lib/ruby_coded/chat/renderer/chat_panel_thinking.rb
174
+ - lib/ruby_coded/chat/renderer/chat_panel_thinking_render.rb
162
175
  - lib/ruby_coded/chat/renderer/login_flow.rb
163
176
  - lib/ruby_coded/chat/renderer/login_flow_layout.rb
164
177
  - lib/ruby_coded/chat/renderer/model_selector.rb
165
178
  - lib/ruby_coded/chat/renderer/plan_clarifier.rb
166
179
  - lib/ruby_coded/chat/renderer/plan_clarifier_layout.rb
180
+ - lib/ruby_coded/chat/renderer/rich_text.rb
181
+ - lib/ruby_coded/chat/renderer/rich_text_inline.rb
167
182
  - lib/ruby_coded/chat/renderer/status_bar.rb
183
+ - lib/ruby_coded/chat/runtime_mode.rb
168
184
  - lib/ruby_coded/chat/state.rb
169
185
  - lib/ruby_coded/chat/state/context_window.rb
170
186
  - lib/ruby_coded/chat/state/login_flow.rb
@@ -195,6 +211,11 @@ files:
195
211
  - lib/ruby_coded/plugins/command_completion/renderer_extension.rb
196
212
  - lib/ruby_coded/plugins/command_completion/state_extension.rb
197
213
  - lib/ruby_coded/plugins/registry.rb
214
+ - lib/ruby_coded/skills.rb
215
+ - lib/ruby_coded/skills/catalog.rb
216
+ - lib/ruby_coded/skills/markdown_loader.rb
217
+ - lib/ruby_coded/skills/prompt_formatter.rb
218
+ - lib/ruby_coded/skills/skill_definition.rb
198
219
  - lib/ruby_coded/strategies/api_key_strategy.rb
199
220
  - lib/ruby_coded/strategies/base.rb
200
221
  - lib/ruby_coded/strategies/oauth_strategy.rb
@@ -204,6 +225,8 @@ files:
204
225
  - lib/ruby_coded/tools/create_directory_tool.rb
205
226
  - lib/ruby_coded/tools/delete_path_tool.rb
206
227
  - lib/ruby_coded/tools/edit_file_tool.rb
228
+ - lib/ruby_coded/tools/execution_pipeline.rb
229
+ - lib/ruby_coded/tools/execution_policy.rb
207
230
  - lib/ruby_coded/tools/git_add_tool.rb
208
231
  - lib/ruby_coded/tools/git_base_tool.rb
209
232
  - lib/ruby_coded/tools/git_commit_tool.rb