rubyn-code 0.5.1 → 0.8.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 (128) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +137 -4
  3. data/db/migrations/014_multi_agent_upgrade.rb +79 -0
  4. data/lib/rubyn_code/agent/conversation.rb +100 -4
  5. data/lib/rubyn_code/agent/dynamic_tool_schema.rb +1 -1
  6. data/lib/rubyn_code/agent/llm_caller.rb +7 -3
  7. data/lib/rubyn_code/agent/loop.rb +106 -12
  8. data/lib/rubyn_code/agent/response_parser.rb +8 -0
  9. data/lib/rubyn_code/agent/system_prompt_builder.rb +40 -2
  10. data/lib/rubyn_code/agent/tool_processor.rb +13 -1
  11. data/lib/rubyn_code/auth/oauth.rb +1 -1
  12. data/lib/rubyn_code/auth/token_store.rb +49 -4
  13. data/lib/rubyn_code/autonomous/daemon.rb +1 -1
  14. data/lib/rubyn_code/checkpoint/hook.rb +26 -0
  15. data/lib/rubyn_code/checkpoint/manager.rb +109 -0
  16. data/lib/rubyn_code/chisel/debt.rb +65 -0
  17. data/lib/rubyn_code/chisel/inspection.rb +93 -0
  18. data/lib/rubyn_code/chisel.rb +127 -0
  19. data/lib/rubyn_code/cli/commands/agents.rb +31 -0
  20. data/lib/rubyn_code/cli/commands/chisel.rb +52 -0
  21. data/lib/rubyn_code/cli/commands/chisel_audit.rb +19 -0
  22. data/lib/rubyn_code/cli/commands/chisel_debt.rb +28 -0
  23. data/lib/rubyn_code/cli/commands/chisel_gain.rb +30 -0
  24. data/lib/rubyn_code/cli/commands/chisel_review.rb +19 -0
  25. data/lib/rubyn_code/cli/commands/command_template.rb +50 -0
  26. data/lib/rubyn_code/cli/commands/context.rb +30 -1
  27. data/lib/rubyn_code/cli/commands/custom_command.rb +84 -0
  28. data/lib/rubyn_code/cli/commands/custom_loader.rb +100 -0
  29. data/lib/rubyn_code/cli/commands/effort.rb +47 -0
  30. data/lib/rubyn_code/cli/commands/export.rb +174 -0
  31. data/lib/rubyn_code/cli/commands/goal.rb +87 -0
  32. data/lib/rubyn_code/cli/commands/learning.rb +62 -0
  33. data/lib/rubyn_code/cli/commands/loop.rb +58 -0
  34. data/lib/rubyn_code/cli/commands/mcp.rb +49 -12
  35. data/lib/rubyn_code/cli/commands/megaplan.rb +1 -1
  36. data/lib/rubyn_code/cli/commands/registry.rb +14 -9
  37. data/lib/rubyn_code/cli/commands/resume.rb +97 -26
  38. data/lib/rubyn_code/cli/commands/rewind.rb +65 -0
  39. data/lib/rubyn_code/cli/commands/think.rb +47 -0
  40. data/lib/rubyn_code/cli/first_run.rb +1 -1
  41. data/lib/rubyn_code/cli/loop_runner.rb +98 -0
  42. data/lib/rubyn_code/cli/mention_expander.rb +111 -0
  43. data/lib/rubyn_code/cli/renderer.rb +3 -2
  44. data/lib/rubyn_code/cli/repl.rb +68 -15
  45. data/lib/rubyn_code/cli/repl_commands.rb +76 -2
  46. data/lib/rubyn_code/cli/repl_setup.rb +17 -7
  47. data/lib/rubyn_code/cli/stream_formatter.rb +3 -2
  48. data/lib/rubyn_code/cli/version_check.rb +10 -3
  49. data/lib/rubyn_code/config/defaults.rb +14 -1
  50. data/lib/rubyn_code/config/schema.json +9 -0
  51. data/lib/rubyn_code/config/settings.rb +19 -2
  52. data/lib/rubyn_code/context/auto_compact.rb +1 -1
  53. data/lib/rubyn_code/context/manager.rb +29 -12
  54. data/lib/rubyn_code/context/manual_compact.rb +1 -1
  55. data/lib/rubyn_code/debug.rb +11 -5
  56. data/lib/rubyn_code/goal/evaluator.rb +95 -0
  57. data/lib/rubyn_code/hooks/event_map.rb +56 -0
  58. data/lib/rubyn_code/hooks/external_dispatcher.rb +199 -0
  59. data/lib/rubyn_code/hooks/goal_hook.rb +88 -0
  60. data/lib/rubyn_code/hooks/response.rb +83 -0
  61. data/lib/rubyn_code/hooks/runner.rb +61 -3
  62. data/lib/rubyn_code/hooks/settings_json_loader.rb +109 -0
  63. data/lib/rubyn_code/hooks/subprocess_executor.rb +116 -0
  64. data/lib/rubyn_code/ide/handlers/plan_interview_answer_handler.rb +13 -13
  65. data/lib/rubyn_code/ide/handlers/plan_interview_cancel_handler.rb +1 -1
  66. data/lib/rubyn_code/ide/handlers/plan_interview_start_handler.rb +10 -10
  67. data/lib/rubyn_code/ide/handlers/plan_propose_handler.rb +1 -1
  68. data/lib/rubyn_code/ide/handlers/prompt_handler.rb +9 -1
  69. data/lib/rubyn_code/ide/handlers/recover_ci_handler.rb +27 -16
  70. data/lib/rubyn_code/ide/handlers/session_resume_handler.rb +1 -1
  71. data/lib/rubyn_code/index/codebase_index.rb +103 -4
  72. data/lib/rubyn_code/index/prism_extractor.rb +82 -0
  73. data/lib/rubyn_code/learning/injector.rb +1 -2
  74. data/lib/rubyn_code/learning/porter.rb +129 -0
  75. data/lib/rubyn_code/llm/adapters/anthropic.rb +168 -29
  76. data/lib/rubyn_code/llm/adapters/anthropic_streaming.rb +13 -0
  77. data/lib/rubyn_code/llm/adapters/base.rb +2 -1
  78. data/lib/rubyn_code/llm/adapters/openai.rb +2 -2
  79. data/lib/rubyn_code/llm/adapters/openai_message_translator.rb +21 -0
  80. data/lib/rubyn_code/llm/adapters/prompt_caching.rb +5 -1
  81. data/lib/rubyn_code/llm/client.rb +16 -3
  82. data/lib/rubyn_code/llm/image_reader.rb +60 -0
  83. data/lib/rubyn_code/llm/message_builder.rb +21 -1
  84. data/lib/rubyn_code/llm/model_router.rb +4 -4
  85. data/lib/rubyn_code/mcp/client.rb +59 -0
  86. data/lib/rubyn_code/mcp/discovery.rb +93 -0
  87. data/lib/rubyn_code/mcp/server_extras_bridge.rb +110 -0
  88. data/lib/rubyn_code/mcp/sse_transport.rb +2 -1
  89. data/lib/rubyn_code/mcp/tool_bridge.rb +16 -14
  90. data/lib/rubyn_code/megaplan/ci_recovery.rb +3 -3
  91. data/lib/rubyn_code/megaplan/interview_session.rb +8 -3
  92. data/lib/rubyn_code/megaplan/plan_proposer.rb +3 -3
  93. data/lib/rubyn_code/memory/search.rb +9 -5
  94. data/lib/rubyn_code/memory/session_persistence.rb +159 -21
  95. data/lib/rubyn_code/observability/cost_calculator.rb +8 -3
  96. data/lib/rubyn_code/output/diff_renderer.rb +62 -7
  97. data/lib/rubyn_code/protocols/RUBYN.md +0 -3
  98. data/lib/rubyn_code/skills/auto_suggest.rb +70 -2
  99. data/lib/rubyn_code/skills/registry_client.rb +4 -3
  100. data/lib/rubyn_code/sub_agents/agent_type.rb +17 -0
  101. data/lib/rubyn_code/sub_agents/catalog.rb +124 -0
  102. data/lib/rubyn_code/tasks/models.rb +0 -16
  103. data/lib/rubyn_code/teams/agent_registry.rb +120 -0
  104. data/lib/rubyn_code/teams/mailbox.rb +99 -10
  105. data/lib/rubyn_code/teams/manager.rb +83 -5
  106. data/lib/rubyn_code/teams/teammate.rb +3 -14
  107. data/lib/rubyn_code/tools/RUBYN.md +2 -2
  108. data/lib/rubyn_code/tools/ask_user.rb +15 -1
  109. data/lib/rubyn_code/tools/bash.rb +3 -3
  110. data/lib/rubyn_code/tools/code_graph.rb +134 -0
  111. data/lib/rubyn_code/tools/executor.rb +9 -4
  112. data/lib/rubyn_code/tools/spawn_agent.rb +47 -62
  113. data/lib/rubyn_code/tools/spawn_teammate.rb +7 -2
  114. data/lib/rubyn_code/tools/todo_store.rb +55 -0
  115. data/lib/rubyn_code/tools/todo_write.rb +88 -0
  116. data/lib/rubyn_code/tools/web_fetch.rb +1 -1
  117. data/lib/rubyn_code/tools/web_search.rb +4 -1
  118. data/lib/rubyn_code/version.rb +1 -1
  119. data/lib/rubyn_code.rb +57 -9
  120. data/skills/rubyn_self_test.md +462 -14
  121. data/skills/self_test/chisel_smoke.rb +84 -0
  122. data/skills/self_test/fixtures/chisel_sample.rb +64 -0
  123. metadata +45 -6
  124. data/lib/rubyn_code/context/context_budget.rb +0 -183
  125. data/lib/rubyn_code/context/schema_filter.rb +0 -64
  126. data/lib/rubyn_code/learning/shortcut.rb +0 -95
  127. data/lib/rubyn_code/llm/streaming.rb +0 -10
  128. data/lib/rubyn_code/protocols/plan_approval.rb +0 -72
@@ -5,7 +5,7 @@ module RubynCode
5
5
  class Executor
6
6
  attr_reader :project_root, :output_compressor, :file_cache
7
7
  attr_accessor :llm_client, :background_worker, :on_agent_status, :db, :ask_user_callback,
8
- :codebase_index, :ide_client
8
+ :codebase_index, :ide_client, :todo_store
9
9
 
10
10
  def initialize(project_root:, ide_client: nil)
11
11
  @project_root = File.expand_path(project_root)
@@ -56,12 +56,17 @@ module RubynCode
56
56
 
57
57
  def build_tool(tool_name)
58
58
  tool_class = Registry.get(tool_name)
59
- # IDE-aware tools accept an ide_client parameter.
60
- if @ide_client && tool_class.method(:new).parameters.any? { |_, name| name == :ide_client }
59
+ # IDE-aware tools accept an ide_client parameter. Inspect #initialize,
60
+ # not .method(:new) the latter reports Class#new's own [[:rest]]
61
+ # signature and never forwards to the constructor.
62
+ if @ide_client && tool_class.instance_method(:initialize).parameters.any? { |_, name| name == :ide_client }
61
63
  tool = tool_class.new(project_root: project_root, ide_client: @ide_client)
62
64
  else
63
65
  tool = tool_class.new(project_root: project_root)
64
66
  end
67
+ if @todo_store && tool_class.instance_method(:initialize).parameters.any? { |_, name| name == :store }
68
+ tool.instance_variable_set(:@store, @todo_store)
69
+ end
65
70
  inject_dependencies(tool, tool_name)
66
71
  tool
67
72
  end
@@ -140,7 +145,7 @@ module RubynCode
140
145
  path = resolve_cache_path(params)
141
146
  return unless path&.end_with?('.rb')
142
147
 
143
- @codebase_index.update!
148
+ @codebase_index.update_file!(path)
144
149
  rescue StandardError => e
145
150
  RubynCode::Debug.warn("CodebaseIndex incremental update failed: #{e.message}")
146
151
  end
@@ -9,8 +9,9 @@ module RubynCode
9
9
  TOOL_NAME = 'spawn_agent'
10
10
  DESCRIPTION = 'Spawn an isolated sub-agent to handle a task. The sub-agent gets its own ' \
11
11
  "fresh context, works independently, and returns only a summary. Use 'explore' " \
12
- "type for research/reading, 'worker' type for writing code/files. The sub-agent " \
13
- 'shares the filesystem but not your conversation.'
12
+ "type for research/reading, 'worker' type for writing code/files, or the name " \
13
+ 'of any custom agent defined in .rubyn-code/agents/. The sub-agent shares the ' \
14
+ 'filesystem but not your conversation.'
14
15
  PARAMETERS = {
15
16
  prompt: {
16
17
  type: :string,
@@ -19,76 +20,73 @@ module RubynCode
19
20
  },
20
21
  agent_type: {
21
22
  type: :string,
22
- description: "Type of agent: 'explore' (read-only) or 'worker' (full write access). Default: explore",
23
- required: false,
24
- enum: %w[explore worker]
23
+ description: "Agent type: 'explore' (read-only), 'worker' (full write access), or a " \
24
+ 'custom agent name from .rubyn-code/agents/. Default: explore',
25
+ required: false
25
26
  }
26
27
  }.freeze
27
28
  RISK_LEVEL = :execute
28
29
 
30
+ READ_TOOLS = %w[read_file glob grep bash load_skill memory_search].freeze
31
+ BLOCKED_TOOLS = %w[spawn_agent send_message read_inbox compact memory_write].freeze
32
+
29
33
  # These get injected by the executor or the REPL
30
34
  attr_writer :llm_client, :on_status
31
35
 
32
36
  def execute(prompt:, agent_type: 'explore')
33
- type = agent_type.to_sym
37
+ agent = resolve_agent(agent_type)
34
38
  callback = @on_status || method(:default_status)
35
39
  @tool_count = 0
36
40
 
37
- callback.call(:started, "Spawning #{type} agent...")
41
+ callback.call(:started, "Spawning #{agent.name} agent...")
38
42
 
39
- tools = tools_for_type(type)
40
- result, hit_limit = run_sub_agent(
41
- prompt: prompt, tools: tools, type: type, callback: callback
42
- )
43
+ tools = tools_for(agent)
44
+ result, hit_limit = run_sub_agent(prompt: prompt, tools: tools, agent: agent, callback: callback)
43
45
 
44
46
  callback.call(:done, "Agent finished (#{@tool_count} tool calls).")
45
47
 
46
48
  summary = RubynCode::SubAgents::Summarizer.call(result, max_length: 3000)
47
- format_agent_result(type, summary, hit_limit)
49
+ format_agent_result(agent.name, summary, hit_limit)
48
50
  end
49
51
 
50
52
  private
51
53
 
52
- def format_agent_result(type, summary, hit_limit)
54
+ # Resolve the requested type via the catalog, falling back to explore
55
+ # for an unknown name so a typo degrades gracefully instead of erroring.
56
+ def resolve_agent(agent_type)
57
+ catalog = RubynCode::SubAgents::Catalog.new(project_root: project_root)
58
+ catalog.get(agent_type) || catalog.get('explore')
59
+ end
60
+
61
+ def format_agent_result(name, summary, hit_limit)
53
62
  if hit_limit
54
- "## Sub-Agent Result (#{type}) — INCOMPLETE (reached #{@tool_count} tool calls)\n\n" \
63
+ "## Sub-Agent Result (#{name}) — INCOMPLETE (reached #{@tool_count} tool calls)\n\n" \
55
64
  'The sub-agent ran out of turns before finishing. Here is what it accomplished so far:' \
56
65
  "\n\n#{summary}"
57
66
  else
58
- "## Sub-Agent Result (#{type})\n\n#{summary}"
59
- end
60
- end
61
-
62
- def max_iterations_for(type)
63
- if type == :explore
64
- Config::Defaults::MAX_EXPLORE_AGENT_ITERATIONS
65
- else
66
- Config::Defaults::MAX_SUB_AGENT_ITERATIONS
67
+ "## Sub-Agent Result (#{name})\n\n#{summary}"
67
68
  end
68
69
  end
69
70
 
70
71
  # Returns [result_text, hit_limit] tuple
71
- def run_sub_agent(prompt:, tools:, type:, callback:)
72
+ def run_sub_agent(prompt:, tools:, agent:, callback:)
72
73
  conversation = RubynCode::Agent::Conversation.new
73
74
  conversation.add_user_message(prompt)
74
75
 
75
- max_iterations = max_iterations_for(type)
76
76
  iteration = 0
77
77
  last_text = nil
78
78
 
79
79
  loop do
80
- return finish_at_limit(conversation, type, last_text) if iteration >= max_iterations
80
+ return finish_at_limit(conversation, agent, last_text) if iteration >= agent.max_iterations
81
81
 
82
- last_text, done = process_iteration(
83
- conversation, tools, type, callback, last_text
84
- )
82
+ last_text, done = process_iteration(conversation, tools, agent, callback, last_text)
85
83
  return [last_text || '', false] if done
86
84
 
87
85
  iteration += 1
88
86
  end
89
87
  end
90
88
 
91
- def finish_at_limit(conversation, type, last_text)
89
+ def finish_at_limit(conversation, agent, last_text)
92
90
  conversation.add_user_message(
93
91
  'You have reached your turn limit. Summarize everything you found or ' \
94
92
  'accomplished so far. Be thorough — this is your last chance to report back.'
@@ -96,17 +94,17 @@ module RubynCode
96
94
  response = @llm_client.chat(
97
95
  messages: conversation.to_api_format,
98
96
  tools: [],
99
- system: sub_agent_system_prompt(type)
97
+ system: agent.system_prompt
100
98
  )
101
99
  summary = extract_text(response)
102
100
  [summary.empty? ? (last_text || '') : summary, true]
103
101
  end
104
102
 
105
- def process_iteration(conversation, tools, type, callback, last_text)
103
+ def process_iteration(conversation, tools, agent, callback, last_text)
106
104
  response = @llm_client.chat(
107
105
  messages: conversation.to_api_format,
108
106
  tools: tools,
109
- system: sub_agent_system_prompt(type)
107
+ system: agent.system_prompt
110
108
  )
111
109
 
112
110
  content = response_content(response)
@@ -117,22 +115,22 @@ module RubynCode
117
115
  conversation.add_assistant_message(content)
118
116
  return [last_text, true] if tool_calls.empty?
119
117
 
120
- execute_sub_agent_tools(tool_calls, conversation, type, callback)
118
+ execute_sub_agent_tools(tool_calls, conversation, agent, callback)
121
119
  [last_text, false]
122
120
  end
123
121
 
124
- def execute_sub_agent_tools(tool_calls, conversation, type, callback)
122
+ def execute_sub_agent_tools(tool_calls, conversation, agent, callback)
125
123
  tool_calls.each do |tc|
126
124
  name, input, id = extract_tool_call(tc)
127
125
  @tool_count += 1
128
126
  callback.call(:tool, name.to_s)
129
127
 
130
- run_single_tool(name, input, id, conversation, type)
128
+ run_single_tool(name, input, id, conversation, agent)
131
129
  end
132
130
  end
133
131
 
134
- def run_single_tool(name, input, id, conversation, type)
135
- if %w[spawn_agent].include?(name)
132
+ def run_single_tool(name, input, id, conversation, agent)
133
+ if name == 'spawn_agent'
136
134
  conversation.add_tool_result(
137
135
  id, name, 'Error: Sub-agents cannot spawn other agents.', is_error: true
138
136
  )
@@ -140,9 +138,9 @@ module RubynCode
140
138
  end
141
139
 
142
140
  tool_class = RubynCode::Tools::Registry.get(name)
143
- if type == :explore && tool_class.risk_level != :read
141
+ if agent.read_only? && tool_class.risk_level != :read
144
142
  conversation.add_tool_result(
145
- id, name, 'Error: Explore agents can only use read-only tools.', is_error: true
143
+ id, name, "Error: #{agent.name} agents can only use read-only tools.", is_error: true
146
144
  )
147
145
  return
148
146
  end
@@ -154,31 +152,18 @@ module RubynCode
154
152
  conversation.add_tool_result(id, name, "Error: #{e.message}", is_error: true)
155
153
  end
156
154
 
157
- def tools_for_type(type)
155
+ # The tool allowlist: explicit list from a custom agent, else the
156
+ # access-based default (read-only set or everything-minus-blocked).
157
+ def tools_for(agent)
158
158
  all_tools = RubynCode::Tools::Registry.tool_definitions
159
- blocked = %w[spawn_agent send_message read_inbox compact memory_write]
160
-
161
- if type == :explore
162
- read_tools = %w[read_file glob grep bash load_skill memory_search]
163
- all_tools.select { |t| read_tools.include?(t[:name]) }
164
- else
165
- all_tools.reject { |t| blocked.include?(t[:name]) }
166
- end
167
- end
168
159
 
169
- def sub_agent_system_prompt(type)
170
- base = 'You are a Rubyn sub-agent. Complete your task efficiently and ' \
171
- 'return a clear summary of what you found or did.'
172
-
173
- case type
174
- when :explore
175
- "#{base}\nYou have read-only access. Search, read files, and analyze. " \
176
- 'Do NOT attempt to write or modify anything.'
177
- when :worker
178
- "#{base}\nYou have full read/write access. Make the changes needed, " \
179
- 'run tests if appropriate, and report what you did.'
160
+ if agent.tool_names && !agent.tool_names.empty?
161
+ allowed = agent.tool_names - BLOCKED_TOOLS
162
+ all_tools.select { |t| allowed.include?(t[:name]) }
163
+ elsif agent.read_only?
164
+ all_tools.select { |t| READ_TOOLS.include?(t[:name]) }
180
165
  else
181
- base
166
+ all_tools.reject { |t| BLOCKED_TOOLS.include?(t[:name]) }
182
167
  end
183
168
  end
184
169
 
@@ -25,13 +25,18 @@ module RubynCode
25
25
  type: :string,
26
26
  description: 'Initial task or instruction for the teammate',
27
27
  required: true
28
+ },
29
+ parent_agent_id: {
30
+ type: :string,
31
+ description: 'ID of the parent agent spawning this teammate (for lineage tracking)',
32
+ required: false
28
33
  }
29
34
  }.freeze
30
35
  RISK_LEVEL = :execute
31
36
 
32
37
  attr_writer :llm_client, :on_status, :db
33
38
 
34
- def execute(name:, role:, prompt:)
39
+ def execute(name:, role:, prompt:, parent_agent_id: nil)
35
40
  callback = @on_status || method(:default_status)
36
41
 
37
42
  raise Error, 'LLM client not available' unless @llm_client
@@ -40,7 +45,7 @@ module RubynCode
40
45
  mailbox = Teams::Mailbox.new(@db)
41
46
  manager = Teams::Manager.new(@db, mailbox: mailbox)
42
47
 
43
- teammate = manager.spawn(name: name, role: role)
48
+ teammate = manager.spawn(name: name, role: role, parent_agent_id: parent_agent_id)
44
49
  callback.call(:started, "Spawning teammate '#{name}' as #{role}...")
45
50
 
46
51
  Thread.new do
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubynCode
4
+ module Tools
5
+ # Shared, thread-safe checklist store. The Agent::Loop owns one instance and
6
+ # exposes it to (a) the renderer so the user sees what's in flight, and
7
+ # (b) the TodoWrite tool so the model can mutate it.
8
+ class TodoStore
9
+ include MonitorMixin
10
+
11
+ Item = Data.define(:content, :status, :active_form)
12
+
13
+ def initialize
14
+ super
15
+ @items = []
16
+ end
17
+
18
+ def replace(items)
19
+ synchronize do
20
+ @items = items.map do |item|
21
+ Item.new(
22
+ content: item['content'] || item[:content],
23
+ status: item['status'] || item[:status],
24
+ active_form: (item['active_form'] || item[:active_form]).to_s
25
+ )
26
+ end
27
+ end
28
+ end
29
+
30
+ def current
31
+ synchronize { @items.map(&:to_h) }
32
+ end
33
+
34
+ def clear
35
+ synchronize { @items = [] }
36
+ end
37
+
38
+ def empty?
39
+ current.empty?
40
+ end
41
+
42
+ def render
43
+ current.map do |item|
44
+ mark =
45
+ case item[:status]
46
+ when 'completed' then '☑'
47
+ when 'in_progress' then '[~]'
48
+ else '[ ]'
49
+ end
50
+ "#{mark} #{item[:content]}"
51
+ end.join("\n")
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require_relative 'registry'
5
+
6
+ module RubynCode
7
+ module Tools
8
+ # Update the in-turn task checklist. The model uses this to keep the user
9
+ # informed of progress while it works. The store is shared between the
10
+ # Agent::Loop (which exposes it for the renderer) and this tool.
11
+ class TodoWrite < Base
12
+ TOOL_NAME = 'TodoWrite'
13
+ DESCRIPTION = 'Update the in-turn task checklist. ' \
14
+ 'Use this to keep the user informed of what you plan to do, ' \
15
+ 'what you are currently working on, and what you have finished.'
16
+ PARAMETERS = {
17
+ todos: { type: :array, required: true,
18
+ description: 'The full set of tasks currently on the checklist. ' \
19
+ 'Replace the existing list with this. Each task is ' \
20
+ '{ "content": "...", "status": "pending|in_progress|completed", ' \
21
+ '"active_form": "..." }.' }
22
+ }.freeze
23
+ RISK_LEVEL = :read
24
+ REQUIRES_CONFIRMATION = false
25
+
26
+ VALID_STATUS = %w[pending in_progress completed].freeze
27
+
28
+ def initialize(project_root:, store: nil)
29
+ super(project_root: project_root)
30
+ @store = store
31
+ end
32
+
33
+ def execute(todos:)
34
+ items = Array(todos)
35
+ validated = []
36
+
37
+ items.each_with_index do |item, i|
38
+ return "TodoWrite: item #{i} is not a hash — got #{item.class}" unless item.is_a?(Hash)
39
+
40
+ content = item[:content] || item['content']
41
+ status = (item[:status] || item['status']).to_s
42
+ active_form = item[:active_form] || item['active_form']
43
+
44
+ return "TodoWrite: item #{i} missing 'content'" if content.to_s.empty?
45
+ unless VALID_STATUS.include?(status)
46
+ return "TodoWrite: item #{i} status must be one of #{VALID_STATUS.join('/')} (got #{status.inspect})"
47
+ end
48
+
49
+ validated << {
50
+ 'content' => content.to_s,
51
+ 'status' => status,
52
+ 'active_form' => active_form.to_s.empty? ? content.to_s : active_form.to_s
53
+ }
54
+ end
55
+
56
+ @store&.replace(validated)
57
+ format(validated)
58
+ end
59
+
60
+ def self.summarize(_output, args)
61
+ count = Array(args[:todos] || args['todos']).size
62
+ if count.zero?
63
+ 'cleared checklist'
64
+ else
65
+ "checklist: #{count} item#{'s' unless count == 1}"
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ def format(items)
72
+ return 'Checklist cleared.' if items.empty?
73
+
74
+ items.map do |item|
75
+ mark =
76
+ case item['status']
77
+ when 'completed' then '[x]'
78
+ when 'in_progress' then '[~]'
79
+ else '[ ]'
80
+ end
81
+ "#{mark} #{item['content']}"
82
+ end.join("\n")
83
+ end
84
+ end
85
+
86
+ Registry.register(TodoWrite)
87
+ end
88
+ end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'faraday'
4
3
  require_relative 'base'
5
4
  require_relative 'registry'
6
5
 
@@ -76,6 +75,7 @@ module RubynCode
76
75
  end
77
76
 
78
77
  def build_connection
78
+ require 'faraday'
79
79
  Faraday.new do |f|
80
80
  f.options.timeout = 30
81
81
  f.options.open_timeout = 10
@@ -3,7 +3,6 @@
3
3
  require 'open3'
4
4
  require 'cgi'
5
5
  require 'json'
6
- require 'faraday'
7
6
  require_relative 'base'
8
7
  require_relative 'registry'
9
8
 
@@ -112,6 +111,7 @@ module RubynCode
112
111
  end
113
112
 
114
113
  def brave_request(query, num_results)
114
+ require 'faraday'
115
115
  Faraday.get('https://api.search.brave.com/res/v1/web/search') do |req|
116
116
  req.params['q'] = query
117
117
  req.params['count'] = num_results
@@ -135,6 +135,7 @@ module RubynCode
135
135
  end
136
136
 
137
137
  def tavily_request(query, num_results)
138
+ require 'faraday'
138
139
  Faraday.post('https://api.tavily.com/search') do |req|
139
140
  req.headers['Content-Type'] = 'application/json'
140
141
  req.body = JSON.generate(
@@ -156,6 +157,7 @@ module RubynCode
156
157
  end
157
158
 
158
159
  def serpapi_request(query, num_results)
160
+ require 'faraday'
159
161
  Faraday.get('https://serpapi.com/search.json') do |req|
160
162
  req.params['q'] = query
161
163
  req.params['num'] = num_results
@@ -175,6 +177,7 @@ module RubynCode
175
177
  end
176
178
 
177
179
  def google_request(query, num_results)
180
+ require 'faraday'
178
181
  Faraday.get('https://www.googleapis.com/customsearch/v1') do |req|
179
182
  req.params['q'] = query
180
183
  req.params['num'] = [num_results, 10].min
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubynCode
4
- VERSION = '0.5.1'
4
+ VERSION = '0.8.0'
5
5
  end
data/lib/rubyn_code.rb CHANGED
@@ -16,11 +16,16 @@ module RubynCode
16
16
  # rather than a successful tool call returning a string like "denied".
17
17
  class UserDeniedError < Error; end
18
18
 
19
- # Infrastructure
20
- autoload :Config, 'rubyn_code/config/settings'
19
+ # Chisel — opt-in "write the minimum that works" enforcement (off by default)
20
+ autoload :Chisel, 'rubyn_code/chisel'
21
21
 
22
+ # Infrastructure
22
23
  module Config
24
+ autoload :Settings, 'rubyn_code/config/settings'
25
+ autoload :Defaults, 'rubyn_code/config/defaults'
26
+ autoload :ProjectConfig, 'rubyn_code/config/project_config'
23
27
  autoload :ProjectProfile, 'rubyn_code/config/project_profile'
28
+ autoload :Validator, 'rubyn_code/config/validator'
24
29
  end
25
30
 
26
31
  # Database
@@ -42,6 +47,11 @@ module RubynCode
42
47
  module LLM
43
48
  autoload :Client, 'rubyn_code/llm/client'
44
49
  autoload :MessageBuilder, 'rubyn_code/llm/message_builder'
50
+ autoload :TextBlock, 'rubyn_code/llm/message_builder'
51
+ autoload :ThinkingBlock, 'rubyn_code/llm/message_builder'
52
+ autoload :ImageBlock, 'rubyn_code/llm/message_builder'
53
+ autoload :ToolUseBlock, 'rubyn_code/llm/message_builder'
54
+ autoload :ImageReader, 'rubyn_code/llm/image_reader'
45
55
  autoload :ModelRouter, 'rubyn_code/llm/model_router'
46
56
 
47
57
  # Adapters (provider-specific implementations)
@@ -57,9 +67,6 @@ module RubynCode
57
67
  autoload :OpenAICompatible, 'rubyn_code/llm/adapters/openai_compatible'
58
68
  autoload :OpenAIMessageTranslator, 'rubyn_code/llm/adapters/openai_message_translator'
59
69
  end
60
-
61
- # Backward-compat: LLM::Streaming → Adapters::AnthropicStreaming
62
- autoload :Streaming, 'rubyn_code/llm/streaming'
63
70
  end
64
71
 
65
72
  # Layer 1: Agent Loop
@@ -75,6 +82,7 @@ module RubynCode
75
82
  module Tools
76
83
  autoload :Base, 'rubyn_code/tools/base'
77
84
  autoload :Registry, 'rubyn_code/tools/registry'
85
+ autoload :TodoStore, 'rubyn_code/tools/todo_store'
78
86
  autoload :Schema, 'rubyn_code/tools/schema'
79
87
  autoload :Executor, 'rubyn_code/tools/executor'
80
88
  autoload :ReadFile, 'rubyn_code/tools/read_file'
@@ -82,6 +90,7 @@ module RubynCode
82
90
  autoload :EditFile, 'rubyn_code/tools/edit_file'
83
91
  autoload :Glob, 'rubyn_code/tools/glob'
84
92
  autoload :Grep, 'rubyn_code/tools/grep'
93
+ autoload :CodeGraph, 'rubyn_code/tools/code_graph'
85
94
  autoload :Bash, 'rubyn_code/tools/bash'
86
95
  autoload :RailsGenerate, 'rubyn_code/tools/rails_generate'
87
96
  autoload :DbMigrate, 'rubyn_code/tools/db_migrate'
@@ -89,6 +98,7 @@ module RubynCode
89
98
  autoload :BundleInstall, 'rubyn_code/tools/bundle_install'
90
99
  autoload :BundleAdd, 'rubyn_code/tools/bundle_add'
91
100
  autoload :Compact, 'rubyn_code/tools/compact'
101
+ autoload :TodoWrite, 'rubyn_code/tools/todo_write'
92
102
  autoload :LoadSkill, 'rubyn_code/tools/load_skill'
93
103
  autoload :Task, 'rubyn_code/tools/task'
94
104
  autoload :MemorySearch, 'rubyn_code/tools/memory_search'
@@ -127,8 +137,6 @@ module RubynCode
127
137
  autoload :AutoCompact, 'rubyn_code/context/auto_compact'
128
138
  autoload :ManualCompact, 'rubyn_code/context/manual_compact'
129
139
  autoload :ContextCollapse, 'rubyn_code/context/context_collapse'
130
- autoload :ContextBudget, 'rubyn_code/context/context_budget'
131
- autoload :SchemaFilter, 'rubyn_code/context/schema_filter'
132
140
  autoload :DecisionCompactor, 'rubyn_code/context/decision_compactor'
133
141
  end
134
142
 
@@ -153,6 +161,14 @@ module RubynCode
153
161
  module SubAgents
154
162
  autoload :Runner, 'rubyn_code/sub_agents/runner'
155
163
  autoload :Summarizer, 'rubyn_code/sub_agents/summarizer'
164
+ autoload :AgentType, 'rubyn_code/sub_agents/agent_type'
165
+ autoload :Catalog, 'rubyn_code/sub_agents/catalog'
166
+ end
167
+
168
+ # Checkpoints (/rewind)
169
+ module Checkpoint
170
+ autoload :Manager, 'rubyn_code/checkpoint/manager'
171
+ autoload :Hook, 'rubyn_code/checkpoint/hook'
156
172
  end
157
173
 
158
174
  # Layer 7: Tasks
@@ -181,12 +197,12 @@ module RubynCode
181
197
  autoload :Manager, 'rubyn_code/teams/manager'
182
198
  autoload :Mailbox, 'rubyn_code/teams/mailbox'
183
199
  autoload :Teammate, 'rubyn_code/teams/teammate'
200
+ autoload :AgentRegistry, 'rubyn_code/teams/agent_registry'
184
201
  end
185
202
 
186
203
  # Layer 10: Protocols
187
204
  module Protocols
188
205
  autoload :ShutdownHandshake, 'rubyn_code/protocols/shutdown_handshake'
189
- autoload :PlanApproval, 'rubyn_code/protocols/plan_approval'
190
206
  autoload :InterruptHandler, 'rubyn_code/protocols/interrupt_handler'
191
207
  end
192
208
 
@@ -222,6 +238,17 @@ module RubynCode
222
238
  autoload :Runner, 'rubyn_code/hooks/runner'
223
239
  autoload :BuiltIn, 'rubyn_code/hooks/built_in'
224
240
  autoload :UserHooks, 'rubyn_code/hooks/user_hooks'
241
+ autoload :GoalHook, 'rubyn_code/hooks/goal_hook'
242
+ autoload :EventMap, 'rubyn_code/hooks/event_map'
243
+ autoload :Response, 'rubyn_code/hooks/response'
244
+ autoload :SettingsJsonLoader, 'rubyn_code/hooks/settings_json_loader'
245
+ autoload :SubprocessExecutor, 'rubyn_code/hooks/subprocess_executor'
246
+ autoload :ExternalDispatcher, 'rubyn_code/hooks/external_dispatcher'
247
+ end
248
+
249
+ # Session goals (/goal)
250
+ module Goal
251
+ autoload :Evaluator, 'rubyn_code/goal/evaluator'
225
252
  end
226
253
 
227
254
  # Layer 15: MCP
@@ -230,7 +257,9 @@ module RubynCode
230
257
  autoload :StdioTransport, 'rubyn_code/mcp/stdio_transport'
231
258
  autoload :SSETransport, 'rubyn_code/mcp/sse_transport'
232
259
  autoload :ToolBridge, 'rubyn_code/mcp/tool_bridge'
260
+ autoload :ServerExtrasBridge, 'rubyn_code/mcp/server_extras_bridge'
233
261
  autoload :Config, 'rubyn_code/mcp/config'
262
+ autoload :Discovery, 'rubyn_code/mcp/discovery'
234
263
  end
235
264
 
236
265
  # Layer 16: Learning
@@ -239,7 +268,7 @@ module RubynCode
239
268
  autoload :Instinct, 'rubyn_code/learning/instinct'
240
269
  autoload :InstinctMethods, 'rubyn_code/learning/instinct'
241
270
  autoload :Injector, 'rubyn_code/learning/injector'
242
- autoload :Shortcut, 'rubyn_code/learning/shortcut'
271
+ autoload :Porter, 'rubyn_code/learning/porter'
243
272
  end
244
273
 
245
274
  # IDE (VS Code extension server)
@@ -258,6 +287,7 @@ module RubynCode
258
287
  # Codebase Index
259
288
  module Index
260
289
  autoload :CodebaseIndex, 'rubyn_code/index/codebase_index'
290
+ autoload :PrismExtractor, 'rubyn_code/index/prism_extractor'
261
291
  end
262
292
 
263
293
  # CLI
@@ -271,6 +301,8 @@ module RubynCode
271
301
  autoload :Setup, 'rubyn_code/cli/setup'
272
302
  autoload :FirstRun, 'rubyn_code/cli/first_run'
273
303
  autoload :DaemonRunner, 'rubyn_code/cli/daemon_runner'
304
+ autoload :LoopRunner, 'rubyn_code/cli/loop_runner'
305
+ autoload :MentionExpander, 'rubyn_code/cli/mention_expander'
274
306
  autoload :VersionCheck, 'rubyn_code/cli/version_check'
275
307
 
276
308
  # Slash Command System
@@ -304,6 +336,22 @@ module RubynCode
304
336
  autoload :RemoveSkills, 'rubyn_code/cli/commands/remove_skills'
305
337
  autoload :Skills, 'rubyn_code/cli/commands/skills'
306
338
  autoload :Megaplan, 'rubyn_code/cli/commands/megaplan'
339
+ autoload :Goal, 'rubyn_code/cli/commands/goal'
340
+ autoload :Loop, 'rubyn_code/cli/commands/loop'
341
+ autoload :Agents, 'rubyn_code/cli/commands/agents'
342
+ autoload :CommandTemplate, 'rubyn_code/cli/commands/command_template'
343
+ autoload :CustomCommand, 'rubyn_code/cli/commands/custom_command'
344
+ autoload :CustomLoader, 'rubyn_code/cli/commands/custom_loader'
345
+ autoload :Learning, 'rubyn_code/cli/commands/learning'
346
+ autoload :Rewind, 'rubyn_code/cli/commands/rewind'
347
+ autoload :Chisel, 'rubyn_code/cli/commands/chisel'
348
+ autoload :ChiselReview, 'rubyn_code/cli/commands/chisel_review'
349
+ autoload :ChiselAudit, 'rubyn_code/cli/commands/chisel_audit'
350
+ autoload :ChiselDebt, 'rubyn_code/cli/commands/chisel_debt'
351
+ autoload :ChiselGain, 'rubyn_code/cli/commands/chisel_gain'
352
+ autoload :Think, 'rubyn_code/cli/commands/think'
353
+ autoload :Effort, 'rubyn_code/cli/commands/effort'
354
+ autoload :Export, 'rubyn_code/cli/commands/export'
307
355
  end
308
356
  end
309
357