rubyn-code 0.5.0 → 0.5.1

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.
@@ -21,7 +21,7 @@ module RubynCode
21
21
  :permission_mode
22
22
  attr_reader :ide_client
23
23
 
24
- def initialize(permission_mode: :default, yolo: false)
24
+ def initialize(permission_mode: :default, yolo: false, workspace_path: nil)
25
25
  @permission_mode = yolo ? :bypass : permission_mode.to_sym
26
26
  @running = false
27
27
  @write_mutex = Mutex.new
@@ -33,10 +33,45 @@ module RubynCode
33
33
  @session_persistence = nil
34
34
  @tool_output_adapter = nil
35
35
  @ide_client = Client.new(self)
36
+ @interview_sessions = {}
37
+
38
+ apply_initial_workspace(workspace_path)
36
39
 
37
40
  Handlers.register_all(self)
38
41
  end
39
42
 
43
+ # ── Interview session registry ──────────────────────────────────
44
+ # Owned by the IDE Server so handlers can look up the same session
45
+ # across start / answer / cancel JSON-RPC calls.
46
+
47
+ def register_interview_session(session)
48
+ @interview_sessions[session.session_id] = session
49
+ end
50
+
51
+ def lookup_interview_session(session_id)
52
+ @interview_sessions[session_id]
53
+ end
54
+
55
+ def drop_interview_session(session_id)
56
+ @interview_sessions.delete(session_id)
57
+ end
58
+
59
+ # Adopt a workspace path supplied on the command line (--dir). Done
60
+ # before the `initialize` JSON-RPC handshake so tools that resolve
61
+ # their project_root at construction time don't fall back to Dir.pwd
62
+ # — which in some launch contexts (Docker, double-clicked VS Code on
63
+ # macOS) is something useless like `/app` or `/`.
64
+ def apply_initial_workspace(path)
65
+ return unless path && !path.empty?
66
+
67
+ if Dir.exist?(path)
68
+ Dir.chdir(path)
69
+ @workspace_path = path
70
+ else
71
+ warn "[IDE::Server] --dir path does not exist, ignoring: #{path}"
72
+ end
73
+ end
74
+
40
75
  # Backward-compatible reader: true when permission_mode is :bypass.
41
76
  def yolo
42
77
  @permission_mode == :bypass
@@ -107,6 +142,9 @@ module RubynCode
107
142
  end
108
143
 
109
144
  dispatch(msg)
145
+ rescue Protocol::JsonRpcError => e
146
+ id = msg.is_a?(Hash) ? msg['id'] : nil
147
+ write(Protocol.error(id, e.code, e.message)) if id
110
148
  rescue StandardError => e
111
149
  warn "[IDE::Server] error handling message: #{e.message}"
112
150
  warn e.backtrace&.first(5)&.join("\n")
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubynCode
4
+ module Megaplan
5
+ # Drives one CI recovery attempt on a Rubyn-opened PR.
6
+ #
7
+ # Receives the failure context the extension's CiWatcher packaged
8
+ # (trimmed log + phase docs + branch + attempt counts) and asks the
9
+ # agent to push a fix commit. Returns a `recovery_outcome` shape:
10
+ #
11
+ # { kind: 'fixed' | 'no_fix' | 'errored', commit_sha?, summary? }
12
+ #
13
+ # 'fixed' means the agent identified and committed a fix on the
14
+ # branch. 'no_fix' means the agent looked but couldn't see an
15
+ # obvious correctness fix in the log (escalate to the human).
16
+ # 'errored' means the agent loop itself crashed.
17
+ class CiRecovery
18
+ class RecoveryError < RubynCode::Error; end
19
+
20
+ SYSTEM_PROMPT = <<~PROMPT.freeze
21
+ You are Rubyn doing CI auto-recovery on a megaplan PR.
22
+
23
+ Read the failing job log, identify the root cause, push a fix
24
+ commit to the existing branch. Keep the diff minimal and focused
25
+ — this is not a refactoring opportunity.
26
+
27
+ If you can't identify a concrete fix from the log, output exactly:
28
+
29
+ NO_FIX_IDENTIFIED: <one-sentence reason>
30
+
31
+ Do not invent fixes. Do not "try something" just to try. A clean
32
+ escalation to the human beats a wrong commit any day.
33
+ PROMPT
34
+
35
+ def initialize(agent_invoker: nil)
36
+ @agent_invoker = agent_invoker || method(:default_agent_invoker)
37
+ end
38
+
39
+ # @param context [Hash] the recovery_ci payload from the extension
40
+ # @return [Hash] recovery_outcome { kind, commit_sha?, summary? }
41
+ def recover(context)
42
+ validate!(context)
43
+ prompt = build_prompt(context)
44
+ result = @agent_invoker.call(prompt, context)
45
+ interpret(result, context)
46
+ rescue StandardError => e
47
+ { 'kind' => 'errored', 'summary' => e.message }
48
+ end
49
+
50
+ private
51
+
52
+ def validate!(context)
53
+ raise ArgumentError, 'context required' unless context.is_a?(Hash)
54
+
55
+ %w[plan_id phase_number branch pr_number trimmed_log attempt_number max_attempts].each do |key|
56
+ raise ArgumentError, "missing #{key}" if context[key].nil?
57
+ end
58
+ end
59
+
60
+ def build_prompt(context)
61
+ phase = context['phase'] || {}
62
+ <<~PROMPT
63
+ Auto-recovery attempt #{context['attempt_number']} of #{context['max_attempts']}.
64
+
65
+ **PR:** ##{context['pr_number']}
66
+ **Branch:** `#{context['branch']}`
67
+ **Failing check:** #{context['failing_check_name'] || 'unknown'}
68
+ **Commit SHA:** #{context['commit_sha']}
69
+
70
+ **Phase #{context['phase_number']} — #{phase['name']}**
71
+ #{phase['summary']}
72
+
73
+ **Trimmed log:**
74
+ ```
75
+ #{context['trimmed_log']}
76
+ ```
77
+
78
+ Fix the failure on the branch above. If you can't identify a fix,
79
+ respond with `NO_FIX_IDENTIFIED: <reason>` instead.
80
+ PROMPT
81
+ end
82
+
83
+ def interpret(result, context)
84
+ text = result.is_a?(Hash) ? (result[:text] || result['text'] || '') : result.to_s
85
+ if text =~ /\bNO_FIX_IDENTIFIED:\s*(.+)$/
86
+ { 'kind' => 'no_fix', 'summary' => Regexp.last_match(1).strip }
87
+ else
88
+ {
89
+ 'kind' => 'fixed',
90
+ 'summary' => 'Agent recovery attempt completed.',
91
+ 'commit_sha' => context['commit_sha']
92
+ }
93
+ end
94
+ end
95
+
96
+ def default_agent_invoker(prompt, _context)
97
+ # Stub for now — real wiring happens in RecoverCiHandler which has
98
+ # an Agent::Loop on hand. The handler injects its own invoker via
99
+ # the constructor.
100
+ raise RecoveryError, 'No agent invoker configured.'
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,245 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'securerandom'
5
+
6
+ # Eager-load so LLM::TextBlock / LLM::ToolUseBlock constants resolve at
7
+ # class definition time — we reference them directly in `assistant_turn_blocks`
8
+ # before any MessageBuilder call has had a chance to trigger autoload.
9
+ require_relative '../llm/message_builder'
10
+
11
+ module RubynCode
12
+ module Megaplan
13
+ # Drives a multi-turn LLM conversation that gathers enough context to
14
+ # produce a megaplan. The model has a small whitelist of READ-ONLY
15
+ # tools (read_file, grep, glob, git_status, git_diff, git_log) so it
16
+ # can inspect the codebase before asking sharper questions — but it
17
+ # cannot edit, run shell mutations, or call any side-effecting tool.
18
+ #
19
+ # Each interview turn ends in one of two JSON shapes:
20
+ #
21
+ # { "question": { "text": "...", "options": ["a", "b"] | null } }
22
+ # { "plan": { "slug": ..., "feature": ..., "phases": [...] } }
23
+ #
24
+ # Validation of the plan payload is delegated to PlanProposer's
25
+ # existing rules so both /megaplan paths stay consistent.
26
+ class InterviewSession
27
+ class InvalidAnswerError < RubynCode::Error; end
28
+ class MalformedResponseError < RubynCode::Error; end
29
+
30
+ Question = Data.define(:id, :text, :options) do
31
+ def open? = options.nil? || options.empty?
32
+ end
33
+
34
+ # The megaplan skill lives in the gem's shared skill catalog
35
+ # (skills/megaplan/megaplan.md) so it's also reachable as
36
+ # `/skill megaplan` from the REPL and the chat. We load the file
37
+ # body directly (skipping the YAML frontmatter) for the system
38
+ # prompt.
39
+ SKILL_PATH = File.expand_path('../../../skills/megaplan/megaplan.md', __dir__)
40
+
41
+ def self.load_skill_body
42
+ raw = File.read(SKILL_PATH)
43
+ raw.sub(/\A---\s*\n.+?\n---\s*\n/m, '')
44
+ end
45
+
46
+ # Whitelist of read-only tools the interviewer may call. Picked from
47
+ # the existing Tools::Registry by name. Anything that writes, runs
48
+ # shell mutations, or spawns sub-agents is intentionally excluded.
49
+ INTERVIEW_TOOLS = %w[
50
+ read_file
51
+ glob
52
+ grep
53
+ git_status
54
+ git_diff
55
+ git_log
56
+ ].freeze
57
+
58
+ # Safety cap on the interview's per-turn tool loop. A well-behaved
59
+ # interviewer should read at most a handful of files before asking
60
+ # its next question; this stops a runaway model from stalling the
61
+ # session indefinitely. Per-turn, not per-session.
62
+ MAX_TOOL_TURNS = 10
63
+
64
+ # Strict output contract bolted on top of the megaplan skill body.
65
+ # The skill teaches *what* a megaplan is and *how* to interview; this
66
+ # contract teaches the LLM the wire format the gem expects on every
67
+ # turn AND that its tool palette is read-only.
68
+ JSON_OUTPUT_CONTRACT = <<~CONTRACT.freeze
69
+ # Output contract (overrides any other formatting instinct)
70
+
71
+ You are an interviewer, not a coding agent. You have a READ-ONLY
72
+ tool palette: `read_file`, `glob`, `grep`, `git_status`, `git_diff`,
73
+ `git_log`. Use them sparingly — only when looking at the code would
74
+ let you ask a SHARPER question (e.g. confirming a column already
75
+ exists before asking about it). You must NOT edit, write, run
76
+ shell mutations, or call any other tool. There are no other tools
77
+ available.
78
+
79
+ After any tool use, your next message must be a single JSON object
80
+ — no markdown fences, no prose before or after — in one of these
81
+ two shapes:
82
+
83
+ { "question": { "text": "<one focused question>", "options": ["a", "b", "c"] | null } }
84
+
85
+ { "plan": { "slug": "<kebab-case>", "feature": "<short description>",
86
+ "phases": [{ "number": 1, "slug": "<kebab>", "name": "<name>",
87
+ "summary": "<one sentence>",
88
+ "requirements_md": "<markdown>",
89
+ "design_md": "<markdown>",
90
+ "tasks_md": "<markdown>" }, ...] } }
91
+
92
+ Interview rules:
93
+ - Ask one question at a time. Never bundle multiple.
94
+ - Prefer numbered options (3-5 choices) when there's an obvious option set.
95
+ - Use null `options` only for genuinely open questions (end-state, constraints prose).
96
+ - Walk the megaplan-skill agenda (goal → constraints → assets → ordering
97
+ → external deps → destructive ops → tests → done-per-phase). Skip
98
+ topics already obvious from context — including anything you've
99
+ confirmed via a read-only tool.
100
+ - Stop interviewing when you're 95% sure of the shape; emit the plan.
101
+
102
+ Plan rules:
103
+ - 1 to 12 phases. Each phase is a vertical slice that ships independently.
104
+ - Trunk works at every phase boundary.
105
+ - tasks_md uses `[ ]` checkboxes; requirements_md uses EARS-style SHALL
106
+ statements when phrasing acceptance criteria.
107
+
108
+ When you emit your final answer for a turn (a question or a plan),
109
+ produce ONLY the JSON object. No prefatory text. No trailing
110
+ commentary. Never produce free-form coding-agent output.
111
+ CONTRACT
112
+
113
+ DEFAULT_INTERVIEW_PROMPT = "#{load_skill_body}\n\n#{JSON_OUTPUT_CONTRACT}".freeze
114
+
115
+ attr_reader :session_id
116
+
117
+ def initialize(llm_client: nil, system_prompt: nil, workspace_path: nil, executor: nil)
118
+ @llm_client = llm_client || LLM::Client.new
119
+ @system_prompt = system_prompt || DEFAULT_INTERVIEW_PROMPT
120
+ @session_id = SecureRandom.uuid
121
+ @history = []
122
+ @last_question = nil
123
+ @workspace_path = workspace_path || Dir.pwd
124
+ @executor = executor || Tools::Executor.new(project_root: @workspace_path)
125
+ end
126
+
127
+ # Returns a Question to ask the user, or a Hash (validated plan payload)
128
+ # if the LLM jumped straight to the plan.
129
+ def start
130
+ ask_llm('Begin the interview. Ask your first question.')
131
+ end
132
+
133
+ # @param question_id [String] echoes back the question's id (anti-race)
134
+ # @param answer_text [String] the user's answer
135
+ # @return [Question, Hash] the next question OR the final plan payload
136
+ def answer(question_id, answer_text)
137
+ raise InvalidAnswerError, 'no question awaiting answer' unless @last_question
138
+ raise InvalidAnswerError, 'wrong question id' unless @last_question.id == question_id
139
+
140
+ @history << { role: 'user', content: answer_text.to_s }
141
+ ask_llm(answer_text.to_s)
142
+ end
143
+
144
+ private
145
+
146
+ def ask_llm(prompt)
147
+ @history << { role: 'user', content: prompt } if @history.empty? || @history.last[:content] != prompt
148
+
149
+ MAX_TOOL_TURNS.times do
150
+ response = @llm_client.chat(
151
+ messages: @history,
152
+ system: @system_prompt,
153
+ tools: interview_tool_definitions
154
+ )
155
+
156
+ tool_calls = response.respond_to?(:tool_calls) ? response.tool_calls : []
157
+ if tool_calls.any?
158
+ @history << assistant_turn_blocks(response)
159
+ @history << tool_results_turn(tool_calls)
160
+ next
161
+ end
162
+
163
+ text = extract_text(response)
164
+ @history << { role: 'assistant', content: text }
165
+ return parse_outcome(text)
166
+ end
167
+
168
+ raise MalformedResponseError,
169
+ "Interview tool loop exceeded #{MAX_TOOL_TURNS} turns without producing a question or plan"
170
+ end
171
+
172
+ def interview_tool_definitions
173
+ @executor.tool_definitions.select do |defn|
174
+ INTERVIEW_TOOLS.include?(defn[:name].to_s)
175
+ end
176
+ end
177
+
178
+ def assistant_turn_blocks(response)
179
+ blocks = response.content.filter_map do |block|
180
+ case block
181
+ when LLM::TextBlock
182
+ { type: 'text', text: block.text }
183
+ when LLM::ToolUseBlock
184
+ { type: 'tool_use', id: block.id, name: block.name, input: block.input }
185
+ end
186
+ end
187
+ { role: 'assistant', content: blocks }
188
+ end
189
+
190
+ def tool_results_turn(tool_calls)
191
+ content = tool_calls.map do |call|
192
+ result = if INTERVIEW_TOOLS.include?(call.name.to_s)
193
+ @executor.execute(call.name, stringify_keys(call.input))
194
+ else
195
+ "Tool '#{call.name}' is not available in interview mode (read-only palette: #{INTERVIEW_TOOLS.join(', ')})."
196
+ end
197
+ { type: 'tool_result', tool_use_id: call.id, content: result.to_s }
198
+ end
199
+ { role: 'user', content: content }
200
+ end
201
+
202
+ def stringify_keys(input)
203
+ return input unless input.is_a?(Hash)
204
+
205
+ input.each_with_object({}) { |(k, v), out| out[k.to_s] = v }
206
+ end
207
+
208
+ # Mirrors PlanProposer#extract_text so both /megaplan paths handle
209
+ # LLM::Response Data objects, Hash legacy shapes, and raw Strings.
210
+ def extract_text(response)
211
+ return response.text if response.respond_to?(:text) && !response.is_a?(String)
212
+ return response[:text] || response['text'] if response.is_a?(Hash)
213
+
214
+ response.to_s
215
+ end
216
+
217
+ def parse_outcome(text)
218
+ cleaned = text.to_s.strip
219
+ .sub(/\A```(?:json)?\s*\n?/, '')
220
+ .sub(/\n?```\s*\z/, '')
221
+ payload = JSON.parse(cleaned)
222
+ if payload.is_a?(Hash) && payload['question']
223
+ q = build_question(payload['question'])
224
+ @last_question = q
225
+ q
226
+ elsif payload.is_a?(Hash) && payload['plan']
227
+ plan = payload['plan']
228
+ PlanProposer.new.validate!(plan)
229
+ @last_question = nil
230
+ plan
231
+ else
232
+ raise MalformedResponseError, 'LLM response is neither a question nor a plan'
233
+ end
234
+ rescue JSON::ParserError => e
235
+ raise MalformedResponseError, "LLM response is not valid JSON: #{e.message}"
236
+ end
237
+
238
+ def build_question(payload)
239
+ options = payload['options']
240
+ options = nil if options.is_a?(Array) && options.empty?
241
+ Question.new(id: SecureRandom.uuid, text: payload['text'].to_s, options: options)
242
+ end
243
+ end
244
+ end
245
+ end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'securerandom'
5
+
6
+ module RubynCode
7
+ module Megaplan
8
+ # Proposes a multi-phase megaplan for a feature description.
9
+ #
10
+ # Asks the LLM to produce a JSON payload that matches the extension's
11
+ # `plan_proposal` shape — one folder per phase, three documents per
12
+ # phase, vertical-slice ordering. The handler validates the response
13
+ # and returns it to the IDE.
14
+ #
15
+ # The LLM call is the slow part (~5-30s); callers should run this
16
+ # off the main JSON-RPC thread.
17
+ class PlanProposer
18
+ class InvalidProposalError < RubynCode::Error; end
19
+
20
+ MAX_PHASES = 12
21
+ DEFAULT_SYSTEM_PROMPT = <<~PROMPT.freeze
22
+ You are a senior Ruby/Rails architect breaking a feature request into a megaplan.
23
+
24
+ A megaplan is a multi-phase development plan where each phase is a
25
+ VERTICAL SLICE that can ship independently. Trunk works at every phase
26
+ boundary. No "scaffolding first, behavior later" — every phase delivers
27
+ a thin, end-to-end working increment.
28
+
29
+ Output a single JSON object with this exact shape:
30
+
31
+ {
32
+ "slug": "kebab-case-feature-slug",
33
+ "feature": "Short feature description",
34
+ "phases": [
35
+ {
36
+ "number": 1,
37
+ "slug": "kebab-case-phase-slug",
38
+ "name": "Human-readable phase name",
39
+ "summary": "One-sentence summary of what this phase ships",
40
+ "requirements_md": "# Phase 1 — <name>: Requirements\\n\\n...",
41
+ "design_md": "# Phase 1 — <name>: Design\\n\\n...",
42
+ "tasks_md": "# Phase 1 — <name>: Tasks\\n\\n## [ ] 1. ...\\n\\n- [ ] 1.1 ..."
43
+ }
44
+ ]
45
+ }
46
+
47
+ Constraints:
48
+ - 1 to 12 phases. Smaller, sharper phases beat fewer mega-phases.
49
+ - Each phase must be a vertical slice.
50
+ - tasks_md is a checklist with `[ ]` boxes (megaplan convention).
51
+ - Every phase needs requirements_md, design_md, tasks_md.
52
+ - Return ONLY the JSON. No markdown fences. No commentary.
53
+ PROMPT
54
+
55
+ def initialize(llm_client: nil, system_prompt: nil, max_phases: MAX_PHASES)
56
+ @llm_client = llm_client || LLM::Client.new
57
+ @system_prompt = system_prompt || DEFAULT_SYSTEM_PROMPT
58
+ @max_phases = max_phases
59
+ end
60
+
61
+ # @param feature [String] the user's feature description
62
+ # @return [Hash] payload with `slug`, `feature`, `phases`
63
+ # @raise [InvalidProposalError] if the LLM response can't be parsed
64
+ def propose(feature)
65
+ raise ArgumentError, 'feature is required' if feature.nil? || feature.strip.empty?
66
+
67
+ response = @llm_client.chat(
68
+ messages: [{ role: 'user', content: feature_prompt(feature) }],
69
+ system: @system_prompt
70
+ )
71
+
72
+ text = extract_text(response)
73
+ payload = parse_payload(text)
74
+ validate!(payload, feature)
75
+ normalize(payload, feature)
76
+ end
77
+
78
+ # Validate a parsed plan_proposal Hash. Public so the interview path
79
+ # (which produces the same shape via a different LLM workflow) can
80
+ # reuse the rule set without reaching into a private method.
81
+ def validate!(payload, _feature = nil)
82
+ raise InvalidProposalError, 'payload is not an object' unless payload.is_a?(Hash)
83
+
84
+ phases = payload['phases']
85
+ raise InvalidProposalError, 'phases must be an array' unless phases.is_a?(Array)
86
+ raise InvalidProposalError, 'phases is empty' if phases.empty?
87
+ raise InvalidProposalError, "too many phases (max #{@max_phases})" if phases.size > @max_phases
88
+
89
+ phases.each_with_index do |phase, idx|
90
+ %w[name summary requirements_md design_md tasks_md].each do |key|
91
+ next unless phase[key].nil? || phase[key].to_s.strip.empty?
92
+
93
+ raise InvalidProposalError, "phase #{idx + 1} missing #{key}"
94
+ end
95
+ end
96
+ end
97
+
98
+ private
99
+
100
+ # LLM::Client#chat returns a `LLM::Response` Data object whose `.text`
101
+ # joins all text blocks. Tests and older callers may pass a String or
102
+ # a Hash — handle all three so the proposer doesn't crash with
103
+ # `#<data ...>` ending up as parser input.
104
+ def extract_text(response)
105
+ return response.text if response.respond_to?(:text) && !response.is_a?(String)
106
+ return response[:text] || response['text'] if response.is_a?(Hash)
107
+
108
+ response.to_s
109
+ end
110
+
111
+ def feature_prompt(feature)
112
+ "Plan this feature as a megaplan:\n\n#{feature.strip}"
113
+ end
114
+
115
+ def parse_payload(text)
116
+ # The LLM can leak fences despite the prompt — strip a leading/trailing
117
+ # ``` block if present.
118
+ cleaned = text.to_s.strip
119
+ cleaned = cleaned.sub(/\A```(?:json)?\s*\n?/, '').sub(/\n?```\s*\z/, '')
120
+ JSON.parse(cleaned)
121
+ rescue JSON::ParserError => e
122
+ raise InvalidProposalError, "LLM response is not valid JSON: #{e.message}"
123
+ end
124
+
125
+ def normalize(payload, feature)
126
+ slug = payload['slug'].to_s.strip
127
+ slug = slugify(feature) if slug.empty?
128
+ phases = payload['phases'].each_with_index.map do |phase, idx|
129
+ {
130
+ 'number' => phase['number'] || idx + 1,
131
+ 'slug' => (phase['slug'].to_s.strip.empty? ? slugify(phase['name']) : phase['slug']),
132
+ 'name' => phase['name'],
133
+ 'summary' => phase['summary'],
134
+ 'requirements_md' => phase['requirements_md'],
135
+ 'design_md' => phase['design_md'],
136
+ 'tasks_md' => phase['tasks_md']
137
+ }
138
+ end
139
+ {
140
+ 'slug' => slug,
141
+ 'feature' => payload['feature'] || feature,
142
+ 'phases' => phases
143
+ }
144
+ end
145
+
146
+ def slugify(text)
147
+ cleaned = text.to_s.downcase.gsub(/[^a-z0-9]+/, '-').gsub(/^-+|-+$/, '')
148
+ cleaned = cleaned[0, 80]
149
+ cleaned.empty? ? 'feature' : cleaned
150
+ end
151
+ end
152
+ end
153
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubynCode
4
- VERSION = '0.5.0'
4
+ VERSION = '0.5.1'
5
5
  end
data/lib/rubyn_code.rb CHANGED
@@ -162,6 +162,13 @@ module RubynCode
162
162
  autoload :Models, 'rubyn_code/tasks/models'
163
163
  end
164
164
 
165
+ # Layer 7b: Megaplan
166
+ module Megaplan
167
+ autoload :PlanProposer, 'rubyn_code/megaplan/plan_proposer'
168
+ autoload :InterviewSession, 'rubyn_code/megaplan/interview_session'
169
+ autoload :CiRecovery, 'rubyn_code/megaplan/ci_recovery'
170
+ end
171
+
165
172
  # Layer 8: Background
166
173
  module Background
167
174
  autoload :Worker, 'rubyn_code/background/worker'
@@ -296,6 +303,7 @@ module RubynCode
296
303
  autoload :InstallSkills, 'rubyn_code/cli/commands/install_skills'
297
304
  autoload :RemoveSkills, 'rubyn_code/cli/commands/remove_skills'
298
305
  autoload :Skills, 'rubyn_code/cli/commands/skills'
306
+ autoload :Megaplan, 'rubyn_code/cli/commands/megaplan'
299
307
  end
300
308
  end
301
309