rubyn-code 0.5.0 → 0.7.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 (105) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +182 -11
  3. data/db/migrations/014_multi_agent_upgrade.rb +79 -0
  4. data/lib/rubyn_code/agent/conversation.rb +89 -3
  5. data/lib/rubyn_code/agent/llm_caller.rb +2 -2
  6. data/lib/rubyn_code/agent/loop.rb +49 -9
  7. data/lib/rubyn_code/agent/system_prompt_builder.rb +37 -2
  8. data/lib/rubyn_code/agent/tool_processor.rb +3 -1
  9. data/lib/rubyn_code/auth/oauth.rb +1 -1
  10. data/lib/rubyn_code/auth/token_store.rb +49 -4
  11. data/lib/rubyn_code/checkpoint/hook.rb +26 -0
  12. data/lib/rubyn_code/checkpoint/manager.rb +109 -0
  13. data/lib/rubyn_code/chisel/debt.rb +65 -0
  14. data/lib/rubyn_code/chisel/inspection.rb +93 -0
  15. data/lib/rubyn_code/chisel.rb +127 -0
  16. data/lib/rubyn_code/cli/app.rb +2 -2
  17. data/lib/rubyn_code/cli/commands/agents.rb +31 -0
  18. data/lib/rubyn_code/cli/commands/chisel.rb +52 -0
  19. data/lib/rubyn_code/cli/commands/chisel_audit.rb +19 -0
  20. data/lib/rubyn_code/cli/commands/chisel_debt.rb +28 -0
  21. data/lib/rubyn_code/cli/commands/chisel_gain.rb +30 -0
  22. data/lib/rubyn_code/cli/commands/chisel_review.rb +19 -0
  23. data/lib/rubyn_code/cli/commands/command_template.rb +50 -0
  24. data/lib/rubyn_code/cli/commands/context.rb +3 -1
  25. data/lib/rubyn_code/cli/commands/custom_command.rb +42 -0
  26. data/lib/rubyn_code/cli/commands/custom_loader.rb +69 -0
  27. data/lib/rubyn_code/cli/commands/goal.rb +87 -0
  28. data/lib/rubyn_code/cli/commands/learning.rb +62 -0
  29. data/lib/rubyn_code/cli/commands/loop.rb +58 -0
  30. data/lib/rubyn_code/cli/commands/mcp.rb +18 -5
  31. data/lib/rubyn_code/cli/commands/megaplan.rb +50 -0
  32. data/lib/rubyn_code/cli/commands/registry.rb +14 -9
  33. data/lib/rubyn_code/cli/commands/rewind.rb +65 -0
  34. data/lib/rubyn_code/cli/first_run.rb +1 -1
  35. data/lib/rubyn_code/cli/loop_runner.rb +98 -0
  36. data/lib/rubyn_code/cli/mention_expander.rb +92 -0
  37. data/lib/rubyn_code/cli/renderer.rb +3 -2
  38. data/lib/rubyn_code/cli/repl.rb +37 -14
  39. data/lib/rubyn_code/cli/repl_commands.rb +77 -2
  40. data/lib/rubyn_code/cli/repl_setup.rb +9 -1
  41. data/lib/rubyn_code/cli/setup.rb +13 -0
  42. data/lib/rubyn_code/cli/stream_formatter.rb +3 -2
  43. data/lib/rubyn_code/cli/version_check.rb +10 -3
  44. data/lib/rubyn_code/config/defaults.rb +13 -1
  45. data/lib/rubyn_code/config/schema.json +4 -0
  46. data/lib/rubyn_code/config/settings.rb +17 -2
  47. data/lib/rubyn_code/context/manager.rb +29 -12
  48. data/lib/rubyn_code/debug.rb +11 -5
  49. data/lib/rubyn_code/goal/evaluator.rb +95 -0
  50. data/lib/rubyn_code/hooks/event_map.rb +56 -0
  51. data/lib/rubyn_code/hooks/external_dispatcher.rb +199 -0
  52. data/lib/rubyn_code/hooks/goal_hook.rb +88 -0
  53. data/lib/rubyn_code/hooks/response.rb +83 -0
  54. data/lib/rubyn_code/hooks/runner.rb +61 -3
  55. data/lib/rubyn_code/hooks/settings_json_loader.rb +109 -0
  56. data/lib/rubyn_code/hooks/subprocess_executor.rb +116 -0
  57. data/lib/rubyn_code/ide/handlers/plan_interview_answer_handler.rb +65 -0
  58. data/lib/rubyn_code/ide/handlers/plan_interview_cancel_handler.rb +22 -0
  59. data/lib/rubyn_code/ide/handlers/plan_interview_start_handler.rb +53 -0
  60. data/lib/rubyn_code/ide/handlers/plan_propose_handler.rb +41 -0
  61. data/lib/rubyn_code/ide/handlers/prompt_handler.rb +9 -1
  62. data/lib/rubyn_code/ide/handlers/recover_ci_handler.rb +143 -0
  63. data/lib/rubyn_code/ide/handlers/session_resume_handler.rb +1 -1
  64. data/lib/rubyn_code/ide/handlers.rb +17 -2
  65. data/lib/rubyn_code/ide/protocol.rb +15 -0
  66. data/lib/rubyn_code/ide/server.rb +39 -1
  67. data/lib/rubyn_code/index/codebase_index.rb +39 -1
  68. data/lib/rubyn_code/learning/porter.rb +129 -0
  69. data/lib/rubyn_code/llm/adapters/anthropic.rb +65 -16
  70. data/lib/rubyn_code/llm/adapters/openai.rb +1 -1
  71. data/lib/rubyn_code/llm/adapters/prompt_caching.rb +5 -1
  72. data/lib/rubyn_code/llm/adapters/token_caching.rb +54 -0
  73. data/lib/rubyn_code/llm/model_router.rb +2 -2
  74. data/lib/rubyn_code/mcp/client.rb +59 -0
  75. data/lib/rubyn_code/mcp/server_extras_bridge.rb +110 -0
  76. data/lib/rubyn_code/mcp/sse_transport.rb +2 -1
  77. data/lib/rubyn_code/mcp/tool_bridge.rb +16 -14
  78. data/lib/rubyn_code/megaplan/ci_recovery.rb +104 -0
  79. data/lib/rubyn_code/megaplan/interview_session.rb +250 -0
  80. data/lib/rubyn_code/megaplan/plan_proposer.rb +153 -0
  81. data/lib/rubyn_code/memory/search.rb +9 -5
  82. data/lib/rubyn_code/memory/session_persistence.rb +159 -21
  83. data/lib/rubyn_code/observability/cost_calculator.rb +3 -1
  84. data/lib/rubyn_code/output/diff_renderer.rb +62 -7
  85. data/lib/rubyn_code/skills/auto_suggest.rb +70 -2
  86. data/lib/rubyn_code/skills/registry_client.rb +4 -3
  87. data/lib/rubyn_code/sub_agents/agent_type.rb +17 -0
  88. data/lib/rubyn_code/sub_agents/catalog.rb +124 -0
  89. data/lib/rubyn_code/teams/agent_registry.rb +120 -0
  90. data/lib/rubyn_code/teams/mailbox.rb +99 -10
  91. data/lib/rubyn_code/teams/manager.rb +83 -5
  92. data/lib/rubyn_code/teams/teammate.rb +5 -1
  93. data/lib/rubyn_code/tools/ask_user.rb +15 -1
  94. data/lib/rubyn_code/tools/executor.rb +5 -3
  95. data/lib/rubyn_code/tools/spawn_agent.rb +47 -62
  96. data/lib/rubyn_code/tools/spawn_teammate.rb +7 -2
  97. data/lib/rubyn_code/tools/web_fetch.rb +1 -1
  98. data/lib/rubyn_code/tools/web_search.rb +4 -1
  99. data/lib/rubyn_code/version.rb +1 -1
  100. data/lib/rubyn_code.rb +53 -2
  101. data/skills/megaplan/megaplan.md +156 -0
  102. data/skills/rubyn_self_test.md +322 -14
  103. data/skills/self_test/chisel_smoke.rb +84 -0
  104. data/skills/self_test/fixtures/chisel_sample.rb +64 -0
  105. metadata +49 -4
@@ -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
@@ -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.0'
4
+ VERSION = '0.7.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
@@ -49,6 +54,7 @@ module RubynCode
49
54
  autoload :Base, 'rubyn_code/llm/adapters/base'
50
55
  autoload :JsonParsing, 'rubyn_code/llm/adapters/json_parsing'
51
56
  autoload :PromptCaching, 'rubyn_code/llm/adapters/prompt_caching'
57
+ autoload :TokenCaching, 'rubyn_code/llm/adapters/token_caching'
52
58
  autoload :Anthropic, 'rubyn_code/llm/adapters/anthropic'
53
59
  autoload :AnthropicCompatible, 'rubyn_code/llm/adapters/anthropic_compatible'
54
60
  autoload :AnthropicStreaming, 'rubyn_code/llm/adapters/anthropic_streaming'
@@ -153,6 +159,14 @@ module RubynCode
153
159
  module SubAgents
154
160
  autoload :Runner, 'rubyn_code/sub_agents/runner'
155
161
  autoload :Summarizer, 'rubyn_code/sub_agents/summarizer'
162
+ autoload :AgentType, 'rubyn_code/sub_agents/agent_type'
163
+ autoload :Catalog, 'rubyn_code/sub_agents/catalog'
164
+ end
165
+
166
+ # Checkpoints (/rewind)
167
+ module Checkpoint
168
+ autoload :Manager, 'rubyn_code/checkpoint/manager'
169
+ autoload :Hook, 'rubyn_code/checkpoint/hook'
156
170
  end
157
171
 
158
172
  # Layer 7: Tasks
@@ -162,6 +176,13 @@ module RubynCode
162
176
  autoload :Models, 'rubyn_code/tasks/models'
163
177
  end
164
178
 
179
+ # Layer 7b: Megaplan
180
+ module Megaplan
181
+ autoload :PlanProposer, 'rubyn_code/megaplan/plan_proposer'
182
+ autoload :InterviewSession, 'rubyn_code/megaplan/interview_session'
183
+ autoload :CiRecovery, 'rubyn_code/megaplan/ci_recovery'
184
+ end
185
+
165
186
  # Layer 8: Background
166
187
  module Background
167
188
  autoload :Worker, 'rubyn_code/background/worker'
@@ -174,6 +195,7 @@ module RubynCode
174
195
  autoload :Manager, 'rubyn_code/teams/manager'
175
196
  autoload :Mailbox, 'rubyn_code/teams/mailbox'
176
197
  autoload :Teammate, 'rubyn_code/teams/teammate'
198
+ autoload :AgentRegistry, 'rubyn_code/teams/agent_registry'
177
199
  end
178
200
 
179
201
  # Layer 10: Protocols
@@ -215,6 +237,17 @@ module RubynCode
215
237
  autoload :Runner, 'rubyn_code/hooks/runner'
216
238
  autoload :BuiltIn, 'rubyn_code/hooks/built_in'
217
239
  autoload :UserHooks, 'rubyn_code/hooks/user_hooks'
240
+ autoload :GoalHook, 'rubyn_code/hooks/goal_hook'
241
+ autoload :EventMap, 'rubyn_code/hooks/event_map'
242
+ autoload :Response, 'rubyn_code/hooks/response'
243
+ autoload :SettingsJsonLoader, 'rubyn_code/hooks/settings_json_loader'
244
+ autoload :SubprocessExecutor, 'rubyn_code/hooks/subprocess_executor'
245
+ autoload :ExternalDispatcher, 'rubyn_code/hooks/external_dispatcher'
246
+ end
247
+
248
+ # Session goals (/goal)
249
+ module Goal
250
+ autoload :Evaluator, 'rubyn_code/goal/evaluator'
218
251
  end
219
252
 
220
253
  # Layer 15: MCP
@@ -223,6 +256,7 @@ module RubynCode
223
256
  autoload :StdioTransport, 'rubyn_code/mcp/stdio_transport'
224
257
  autoload :SSETransport, 'rubyn_code/mcp/sse_transport'
225
258
  autoload :ToolBridge, 'rubyn_code/mcp/tool_bridge'
259
+ autoload :ServerExtrasBridge, 'rubyn_code/mcp/server_extras_bridge'
226
260
  autoload :Config, 'rubyn_code/mcp/config'
227
261
  end
228
262
 
@@ -233,6 +267,7 @@ module RubynCode
233
267
  autoload :InstinctMethods, 'rubyn_code/learning/instinct'
234
268
  autoload :Injector, 'rubyn_code/learning/injector'
235
269
  autoload :Shortcut, 'rubyn_code/learning/shortcut'
270
+ autoload :Porter, 'rubyn_code/learning/porter'
236
271
  end
237
272
 
238
273
  # IDE (VS Code extension server)
@@ -264,6 +299,8 @@ module RubynCode
264
299
  autoload :Setup, 'rubyn_code/cli/setup'
265
300
  autoload :FirstRun, 'rubyn_code/cli/first_run'
266
301
  autoload :DaemonRunner, 'rubyn_code/cli/daemon_runner'
302
+ autoload :LoopRunner, 'rubyn_code/cli/loop_runner'
303
+ autoload :MentionExpander, 'rubyn_code/cli/mention_expander'
267
304
  autoload :VersionCheck, 'rubyn_code/cli/version_check'
268
305
 
269
306
  # Slash Command System
@@ -296,6 +333,20 @@ module RubynCode
296
333
  autoload :InstallSkills, 'rubyn_code/cli/commands/install_skills'
297
334
  autoload :RemoveSkills, 'rubyn_code/cli/commands/remove_skills'
298
335
  autoload :Skills, 'rubyn_code/cli/commands/skills'
336
+ autoload :Megaplan, 'rubyn_code/cli/commands/megaplan'
337
+ autoload :Goal, 'rubyn_code/cli/commands/goal'
338
+ autoload :Loop, 'rubyn_code/cli/commands/loop'
339
+ autoload :Agents, 'rubyn_code/cli/commands/agents'
340
+ autoload :CommandTemplate, 'rubyn_code/cli/commands/command_template'
341
+ autoload :CustomCommand, 'rubyn_code/cli/commands/custom_command'
342
+ autoload :CustomLoader, 'rubyn_code/cli/commands/custom_loader'
343
+ autoload :Learning, 'rubyn_code/cli/commands/learning'
344
+ autoload :Rewind, 'rubyn_code/cli/commands/rewind'
345
+ autoload :Chisel, 'rubyn_code/cli/commands/chisel'
346
+ autoload :ChiselReview, 'rubyn_code/cli/commands/chisel_review'
347
+ autoload :ChiselAudit, 'rubyn_code/cli/commands/chisel_audit'
348
+ autoload :ChiselDebt, 'rubyn_code/cli/commands/chisel_debt'
349
+ autoload :ChiselGain, 'rubyn_code/cli/commands/chisel_gain'
299
350
  end
300
351
  end
301
352
 
@@ -0,0 +1,156 @@
1
+ ---
2
+ name: megaplan
3
+ description: Phased project planning. Interview the user one question at a time, then scaffold numbered phase folders (requirements/design/tasks). Trigger phrases include "megaplan", "mega plan", "plan phases", "phase this out", or any feature spanning 3+ PRs.
4
+ tags:
5
+ - planning
6
+ - process
7
+ - phases
8
+ - megaplan
9
+ triggers:
10
+ - megaplan
11
+ - mega plan
12
+ - plan phases
13
+ - phase this out
14
+ ---
15
+
16
+ # Megaplan — Phased Project Planning
17
+
18
+ Ship in vertical slices. Each phase merges cleanly and leaves the trunk working.
19
+
20
+ ## Don't use for
21
+
22
+ - Single-PR features — just do them
23
+ - Pure research / exploration — nothing shippable
24
+ - Work where the *shape* (not just the details) will change weekly — plan something smaller first
25
+
26
+ ## Design principles to apply throughout
27
+
28
+ Hold the work to these when proposing phases and reviewing each `design.md`:
29
+
30
+ - **Vertical slices, not horizontal.** A phase touches every layer it needs to be end-to-end testable. The classic anti-pattern this skill exists to prevent: "Phase 1: all models. Phase 2: all controllers. Phase 3: all views." Trunk is unshippable until Phase 3 and Phase 2 has nothing to test. Instead: "Phase 1: one feature, full-stack, behind a flag."
31
+ - **SOLID, applied lightly.** Single Responsibility and Dependency Inversion are the two that actually bite at the phase-design level. The others usually take care of themselves if those two are clean.
32
+ - **KISS.** Skip the abstraction until you have two concrete callers. Three similar lines beat a premature framework. Inline before you extract.
33
+ - **Justify abstractions.** Every new module/service/class in `design.md` needs a one-sentence reason it exists separately. If you can't write it, inline the code.
34
+
35
+ ## The workflow
36
+
37
+ ### 1. Interview first
38
+
39
+ Don't propose phases until you understand the shape. Walk through the agenda below **one question per turn** — never dump the full list in a single message. The point is to let the operator steer at every step.
40
+
41
+ **How to ask:**
42
+
43
+ - **One topic per turn.** Pick the next unanswered item from the agenda. Ask only about that.
44
+ - **Number the options.** Whenever the question has 2+ plausible answers, present them as a numbered list so the operator can reply with just the number. Include a **recommended** pick (the one you'd default to given what you know so far) and say *why* it's the recommendation in one short line.
45
+ - **Restate locked-in answers at the top of each follow-up.** A running "Decisions so far:" block of one-line bullets, so the operator can spot drift and you can spot contradictions.
46
+ - **Open-ended questions are fine** when no obvious option set exists (e.g. "What's the end-state in user-facing terms?"). Still ask one at a time.
47
+ - **Stop when you're 95% sure of the shape.** Don't run the whole agenda for its own sake — skip topics that are already obvious from context. The agenda is a checklist *for you*, not a script to read aloud.
48
+
49
+ **Agenda (interviewer's checklist, not a dump):**
50
+
51
+ - **Goal** — end state in user-facing terms
52
+ - **Constraints** — deadlines, infra limits, things you can't break
53
+ - **Existing assets** — what's there to build on or rip out
54
+ - **Natural ordering** — dependency sequence (data → API → UI)?
55
+ - **External dependencies** — other teams, third-party APIs, infra access, design review. These reorder phases more than technical concerns do.
56
+ - **Destructive operations** — schema drops, data deletes, deprecations. These need their own phase with an explicit rollback note.
57
+ - **Test strategy** — what coverage is needed?
58
+ - **Done-per-phase** — minimum manual test that proves each phase shipped?
59
+
60
+ ### 2. Propose phases, get agreement
61
+
62
+ A good phase:
63
+ - Is a vertical slice — testable end-to-end at merge time
64
+ - Ships independently — trunk works at every boundary
65
+ - Has a clear definition of done
66
+ - Is roughly 1–3 days of focused work
67
+ - Has a name that survives the PR title. If it adds *and* removes, capture both (e.g. "TX-Only Checkout + Geofencing Removal").
68
+
69
+ A good phase list:
70
+ - 3–8 phases for most projects
71
+ - Ordered by dependency, not priority
72
+ - Destructive operations isolated to their own phase
73
+ - Ends with a phase that visibly delivers the goal
74
+
75
+ Propose as a numbered outline. Let the user reorder, merge, or split before any files exist.
76
+
77
+ ### 3. Scaffold the structure
78
+
79
+ ```
80
+ docs/
81
+ README.md # roadmap tracker
82
+ NN-slug/
83
+ requirements.md # user stories + acceptance criteria
84
+ design.md # architecture + interfaces + test strategy
85
+ tasks.md # numbered checklist
86
+ ```
87
+
88
+ Numbering: zero-padded (`01-`, `02-`). Slugs: kebab-case, ≤4 words.
89
+
90
+ Default: fully scaffold the *current* phase; future phases stay as one-liners in `README.md` until you start them. Later phases mutate based on what's learned early — don't pre-write what you'll have to rewrite.
91
+
92
+ ### 4. Implement phase-by-phase
93
+
94
+ For each phase:
95
+ 1. **Read the running architecture doc first** (`CLAUDE.md` or equivalent). That's how you don't repeat decisions or miss constraints from earlier phases.
96
+ 2. Branch off main: `git checkout -b phase-NN-slug`
97
+ 3. Work `tasks.md` top-to-bottom, checking subtasks as you go
98
+ 4. Commit at section boundaries: `Phase N (M/X): description` — where `M` is the current section number and `X` is the total number of sections in `tasks.md`
99
+ 5. Run full test suite + lint + format at each commit boundary
100
+ 6. When `tasks.md` is fully checked, push and open a PR (see PR description shape below)
101
+ 7. After merge: check the box in `docs/README.md`, update the running architecture doc if anything moved
102
+
103
+ ## File templates
104
+
105
+ ### requirements.md
106
+
107
+ Use RFC 2119 SHALL/SHOULD/MAY language for acceptance criteria. They're contracts — write them as something a QA tester could check.
108
+
109
+ Sections: Overview, Glossary, Requirements (per requirement: user story + numbered SHALL criteria), Out of scope.
110
+
111
+ ### design.md
112
+
113
+ Sections: Overview, Architecture (each component gets Responsibility, Collaborators, "Why not inline?"), Data model changes, Test strategy, Migration / rollout, Future enhancements.
114
+
115
+ Every new abstraction needs a justification line. If you can't answer "why not inline this", inline it.
116
+
117
+ ### tasks.md
118
+
119
+ Sections (`## [ ] N. <name>`) and tasks (`- [ ] N.M ...`) both get checkboxes. A section ticks only when every task under it ticks. Reference requirements by ID (`refs Req 1.1`) so coverage gaps are visible. Always end with a Validation section that includes the manual smoke flow.
120
+
121
+ ## PR description shape
122
+
123
+ Three bullets. Don't pad them.
124
+
125
+ ```
126
+ ## What shipped
127
+ - <user-facing capability or removal>
128
+
129
+ ## What proves it
130
+ - <new tests, smoke flow, manual check>
131
+
132
+ ## What's deferred
133
+ - <link to later phase, or "none">
134
+ ```
135
+
136
+ ## Patterns and pitfalls
137
+
138
+ **Patterns to keep:**
139
+ - **Branch per phase:** `phase-NN-slug` — disposable, one PR per branch
140
+ - **Squash-merge:** one phase = one commit on main, full PR description preserved
141
+ - **Plan before code:** `requirements.md` is finalized and `design.md` is sketched before any task in `tasks.md` gets implemented.
142
+ - **Semantic test anchors** so later phases don't break earlier tests
143
+ - **One running architecture doc** kept current — read it before each phase, update it after
144
+
145
+ **When to break the rules:**
146
+ - **Phase grew mid-stream?** Split it. Add `NN-a-slug` / `NN-b-slug` or renumber.
147
+ - **Later phase invalidates an earlier requirement?** Update the earlier doc with a "Superseded by Phase N" note.
148
+ - **Phase ships nothing user-visible** (e.g. refactor prep)? Still its own PR — but say so in the description.
149
+
150
+ **Pitfalls to avoid:**
151
+ - **Horizontal-slice phases** (all models / all controllers / all views). Trunk is unshippable until the last phase merges.
152
+ - **Scaffolding all phases upfront in full detail** — later phases get invalidated by what you learn early.
153
+ - **Phases longer than ~3 days** — the phase should split. Long phases hide scope creep.
154
+ - **Requirements without acceptance criteria** — "Make X work" isn't a requirement; "When <condition>, the system SHALL <observable behavior>" is.
155
+ - **Tasks that don't reference requirements** — if you can't cite which requirement a task serves, the task probably isn't load-bearing.
156
+ - **Destructive operations mixed with feature work** — schema drops, deletes, deprecations belong in their own phase with a rollback note.