rubik 0.3.3 → 0.3.5

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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/ATTRIBUTION.md +1 -1
  3. data/CHANGELOG.md +102 -0
  4. data/lib/rubik/adapter/process.rb +11 -8
  5. data/lib/rubik/ast/go_extractor.rb +9 -2
  6. data/lib/rubik/ast/rust_extractor.rb +9 -2
  7. data/lib/rubik/ast/typescript_extractor.rb +13 -7
  8. data/lib/rubik/buffer/manager.rb +99 -69
  9. data/lib/rubik/config/defaults.rb +17 -20
  10. data/lib/rubik/config.rb +0 -6
  11. data/lib/rubik/context/git_lister.rb +3 -1
  12. data/lib/rubik/llm/anthropic.rb +5 -2
  13. data/lib/rubik/llm/client.rb +102 -72
  14. data/lib/rubik/llm/openai_compatible.rb +83 -68
  15. data/lib/rubik/lsp/client.rb +320 -96
  16. data/lib/rubik/lsp/manager.rb +83 -12
  17. data/lib/rubik/lsp/server_command.rb +102 -0
  18. data/lib/rubik/orchestrator/goal_ledger/patch.rb +48 -30
  19. data/lib/rubik/orchestrator/lsp_feedback.rb +20 -4
  20. data/lib/rubik/orchestrator/subagent/reuse.rb +2 -2
  21. data/lib/rubik/orchestrator/subagent/spawn_transaction.rb +60 -31
  22. data/lib/rubik/platform/process.rb +6 -6
  23. data/lib/rubik/platform/sanitize.rb +37 -16
  24. data/lib/rubik/prompt/doctrine.rb +4 -0
  25. data/lib/rubik/repl/commands/agent_context.rb +126 -104
  26. data/lib/rubik/repl/commands/agent_execution.rb +183 -155
  27. data/lib/rubik/repl/commands/agent_models.rb +112 -87
  28. data/lib/rubik/repl/commands/agent_providers.rb +190 -156
  29. data/lib/rubik/repl/commands/buffer.rb +122 -88
  30. data/lib/rubik/repl/commands/lsp.rb +212 -179
  31. data/lib/rubik/repl/commands/mcp.rb +120 -92
  32. data/lib/rubik/repl/commands/session.rb +90 -59
  33. data/lib/rubik/repl/commands/workspace.rb +266 -246
  34. data/lib/rubik/repl/shell.rb +10 -1
  35. data/lib/rubik/runtime/discovery.rb +12 -6
  36. data/lib/rubik/runtime/overlay_storage.rb +59 -2
  37. data/lib/rubik/sandbox/driver/docker.rb +2 -2
  38. data/lib/rubik/session/replayer.rb +1 -2
  39. data/lib/rubik/storage/store.rb +3 -2
  40. data/lib/rubik/storage/vakanuvis/distill_gate.rb +6 -2
  41. data/lib/rubik/storage/vakanuvis/gate.rb +37 -16
  42. data/lib/rubik/storage/vakanuvis/harness.rb +36 -21
  43. data/lib/rubik/storage/vakanuvis/memory_steward.rb +12 -6
  44. data/lib/rubik/tools/builtins/lifecycle.rb +34 -7
  45. data/lib/rubik/tools/builtins/lsp.rb +36 -8
  46. data/lib/rubik/tools/fuzzy_matcher.rb +27 -11
  47. data/lib/rubik/version.rb +1 -1
  48. metadata +2 -1
@@ -16,6 +16,27 @@ module Rubik
16
16
 
17
17
  def self.register(shell)
18
18
  shell.register_command("history", description: "List recent session history", audience: :both) do |args, ctx|
19
+ run_history(args, ctx)
20
+ end
21
+
22
+ # Plan 91 — explicit multi-line paste mode: collects lines until a
23
+ # lone "." line, then dispatches the joined text as one prompt turn.
24
+ shell.register_command("paste", description: "Multi-line paste mode (finish with lone '.')") do |_args, ctx|
25
+ run_paste(ctx)
26
+ end
27
+
28
+ shell.register_command("replay", description: "Replay session file") do |args, ctx|
29
+ run_replay(args, ctx)
30
+ end
31
+
32
+ shell.register_command("session", description: "Manage recorded sessions (list|resume|info|prune)",
33
+ usage: USAGE_SESSION) do |args, ctx|
34
+ dispatch_session(args, ctx)
35
+ end
36
+ end
37
+
38
+ class << self
39
+ def run_history(args, ctx)
19
40
  limit = (args.first || 20).to_i
20
41
  hist = ctx.recorder.read_history(limit)
21
42
  hist.each_with_index do |item, idx|
@@ -23,14 +44,12 @@ module Rubik
23
44
  end
24
45
  end
25
46
 
26
- # Plan 91 — explicit multi-line paste mode: collects lines until a
27
- # lone "." line, then dispatches the joined text as one prompt turn.
28
- shell.register_command("paste", description: "Multi-line paste mode (finish with lone '.')") do |_args, ctx|
47
+ def run_paste(ctx)
29
48
  ctx.request_paste!
30
49
  ctx.out.puts "[Rubik] Paste modu bir sonraki prompt'ta başlayacak."
31
50
  end
32
51
 
33
- shell.register_command("replay", description: "Replay session file") do |args, ctx|
52
+ def run_replay(args, ctx)
34
53
  path = args.first
35
54
  replayer = ctx.session_replayer
36
55
  if replayer.replay_file(path)
@@ -40,77 +59,85 @@ module Rubik
40
59
  end
41
60
  end
42
61
 
43
- shell.register_command("session", description: "Manage recorded sessions (list|resume|info|prune)",
44
- usage: USAGE_SESSION) do |args, ctx|
62
+ def dispatch_session(args, ctx)
45
63
  sub = args.first&.downcase || "list"
46
64
  root_d = ctx.buffer_manager&.root_dir || Dir.pwd
47
65
  home_d = ctx.config&.home_dir || ENV.fetch("HOME", nil)
48
66
 
49
67
  case sub
50
68
  when "list"
51
- sessions = Rubik::Session::Manager.list_sessions(root_d, home_d, config: ctx.config)
52
- if sessions.empty?
53
- ctx.out.puts "No recorded sessions found for this workspace."
54
- else
55
- ctx.out.puts "=== Recorded Sessions (#{sessions.size}) ==="
56
- sessions.each_with_index do |s, idx|
57
- mark = s[:path] == ctx.recorder.session_file ? " [CURRENT]" : ""
58
- ctx.out.puts " #{idx + 1}. #{s[:id]} (#{s[:scope]}) - #{s[:event_count]} events - Last: \"#{s[:last_prompt]}\"#{mark}"
59
- end
60
- end
69
+ run_list(ctx, root_d, home_d)
61
70
  when "resume", "continue", "load"
62
- target_id = args[1]
63
- if target_id.nil?
64
- picked = interactive_session_pick(ctx)
65
- if picked == :picker_cancelled
66
- ctx.out.puts "[Rubik::Session] Resume iptal edildi."
67
- next
68
- end
69
- target_id = picked || :latest
70
- end
71
- res = ctx.resume_session(target_id)
72
- ctx.out.puts "Resume failed: #{res[:error]}" unless res[:ok]
71
+ run_resume(args[1], ctx)
73
72
  when "info"
74
- ctx.out.puts "Active Session File: #{ctx.recorder.session_file}"
75
- ctx.out.puts "Total Events in Session: #{ctx.recorder.events.size}"
73
+ run_info(ctx)
76
74
  when "prune", "clean", "cleanup"
77
75
  run_prune(args.drop(1), ctx)
78
76
  when "save"
79
- target_name = args[1]
80
- if target_name.nil? || target_name.strip.empty?
81
- ctx.out.puts "Usage: !session save <name>"
82
- else
83
- safe_name = File.basename(target_name.strip.sub(/\.jsonl$/, ""))
84
- if safe_name.empty? || safe_name.start_with?(".")
85
- ctx.out.puts "Error: Invalid session name '#{target_name}'."
86
- next
87
- end
88
-
89
- target_dir = File.join(root_d, ".rubik", "sessions")
90
- FileUtils.mkdir_p(target_dir)
91
- target_file = File.join(target_dir, "#{safe_name}.jsonl")
92
-
93
- if ctx.recorder.session_file && File.exist?(ctx.recorder.session_file)
94
- FileUtils.cp(ctx.recorder.session_file, target_file)
95
- else
96
- File.open(target_file, "w") do |f|
97
- ctx.recorder.events.each { |ev| f.puts(JSON.dump(ev)) }
98
- end
99
- end
100
- ctx.recorder.switch_session_file(target_file, cleanup_empty_current: false)
101
- ctx.out.puts "[Rubik::Session] Session saved to #{target_file}"
102
- end
77
+ run_save(args[1], ctx, root_d)
103
78
  else
104
79
  ctx.out.puts USAGE_SESSION
105
80
  end
106
81
  end
107
- end
108
82
 
109
- # Interactive session picker (Antigravity /resume style): arrow-key
110
- # selection over recorded sessions on capable TTYs. Returns the
111
- # session id, :picker_cancelled on Ctrl+C (caller aborts the
112
- # resume), or nil in pipes/tests so `resume` falls back to :latest.
113
- class << self
83
+ def run_list(ctx, root_d, home_d)
84
+ sessions = Rubik::Session::Manager.list_sessions(root_d, home_d, config: ctx.config)
85
+ if sessions.empty?
86
+ ctx.out.puts "No recorded sessions found for this workspace."
87
+ else
88
+ ctx.out.puts "=== Recorded Sessions (#{sessions.size}) ==="
89
+ sessions.each_with_index do |s, idx|
90
+ mark = s[:path] == ctx.recorder.session_file ? " [CURRENT]" : ""
91
+ ctx.out.puts " #{idx + 1}. #{s[:id]} (#{s[:scope]}) - #{s[:event_count]} events - Last: \"#{s[:last_prompt]}\"#{mark}"
92
+ end
93
+ end
94
+ end
95
+
96
+ def run_resume(target_id, ctx)
97
+ if target_id.nil?
98
+ picked = interactive_session_pick(ctx)
99
+ if picked == :picker_cancelled
100
+ ctx.out.puts "[Rubik::Session] Resume iptal edildi."
101
+ return
102
+ end
103
+ target_id = picked || :latest
104
+ end
105
+ res = ctx.resume_session(target_id)
106
+ ctx.out.puts "Resume failed: #{res[:error]}" unless res[:ok]
107
+ end
108
+
109
+ def run_info(ctx)
110
+ ctx.out.puts "Active Session File: #{ctx.recorder.session_file}"
111
+ ctx.out.puts "Total Events in Session: #{ctx.recorder.events.size}"
112
+ end
113
+
114
+ def run_save(target_name, ctx, root_d)
115
+ if target_name.nil? || target_name.strip.empty?
116
+ ctx.out.puts "Usage: !session save <name>"
117
+ return
118
+ end
119
+
120
+ safe_name = File.basename(target_name.strip.sub(/\.jsonl$/, ""))
121
+ if safe_name.empty? || safe_name.start_with?(".")
122
+ ctx.out.puts "Error: Invalid session name '#{target_name}'."
123
+ return
124
+ end
125
+
126
+ target_dir = File.join(root_d, ".rubik", "sessions")
127
+ FileUtils.mkdir_p(target_dir)
128
+ target_file = File.join(target_dir, "#{safe_name}.jsonl")
129
+
130
+ if ctx.recorder.session_file && File.exist?(ctx.recorder.session_file)
131
+ FileUtils.cp(ctx.recorder.session_file, target_file)
132
+ else
133
+ File.open(target_file, "w") do |f|
134
+ ctx.recorder.events.each { |ev| f.puts(JSON.dump(ev)) }
135
+ end
136
+ end
137
+ ctx.recorder.switch_session_file(target_file, cleanup_empty_current: false)
138
+ ctx.out.puts "[Rubik::Session] Session saved to #{target_file}"
139
+ end
140
+
114
141
  # `!session prune [days] [--apply] [--all]` — dry-run by default:
115
142
  # prints the blast radius of files that WOULD be deleted and only
116
143
  # unlinks once the operator passes --apply. The active session file
@@ -154,6 +181,10 @@ module Rubik
154
181
  days == days.to_i ? "#{days.to_i}d" : "#{days.round(1)}d"
155
182
  end
156
183
 
184
+ # Interactive session picker (Antigravity /resume style): arrow-key
185
+ # selection over recorded sessions on capable TTYs. Returns the
186
+ # session id, :picker_cancelled on Ctrl+C (caller aborts the
187
+ # resume), or nil in pipes/tests so `resume` falls back to :latest.
157
188
  def interactive_session_pick(ctx)
158
189
  return nil unless Rubik::UI::Capability.interactive?(input: ctx.input, output: ctx.output)
159
190