rubik 0.3.4 → 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.
- checksums.yaml +4 -4
- data/ATTRIBUTION.md +1 -1
- data/CHANGELOG.md +54 -0
- data/lib/rubik/adapter/process.rb +11 -8
- data/lib/rubik/ast/go_extractor.rb +9 -2
- data/lib/rubik/ast/rust_extractor.rb +9 -2
- data/lib/rubik/ast/typescript_extractor.rb +13 -7
- data/lib/rubik/buffer/manager.rb +78 -66
- data/lib/rubik/config/defaults.rb +1 -0
- data/lib/rubik/context/git_lister.rb +3 -1
- data/lib/rubik/llm/anthropic.rb +5 -2
- data/lib/rubik/llm/openai_compatible.rb +83 -68
- data/lib/rubik/lsp/client.rb +320 -96
- data/lib/rubik/lsp/manager.rb +83 -12
- data/lib/rubik/lsp/server_command.rb +102 -0
- data/lib/rubik/orchestrator/lsp_feedback.rb +20 -4
- data/lib/rubik/orchestrator/subagent/spawn_transaction.rb +60 -31
- data/lib/rubik/repl/commands/agent_context.rb +126 -104
- data/lib/rubik/repl/commands/agent_execution.rb +83 -68
- data/lib/rubik/repl/commands/agent_models.rb +112 -87
- data/lib/rubik/repl/commands/buffer.rb +122 -88
- data/lib/rubik/repl/commands/mcp.rb +120 -92
- data/lib/rubik/repl/commands/session.rb +90 -59
- data/lib/rubik/repl/commands/workspace.rb +110 -94
- data/lib/rubik/repl/shell.rb +10 -1
- data/lib/rubik/runtime/discovery.rb +12 -6
- data/lib/rubik/storage/vakanuvis/distill_gate.rb +6 -2
- data/lib/rubik/storage/vakanuvis/gate.rb +37 -16
- data/lib/rubik/storage/vakanuvis/harness.rb +36 -21
- data/lib/rubik/storage/vakanuvis/memory_steward.rb +12 -6
- data/lib/rubik/tools/builtins/lifecycle.rb +34 -7
- data/lib/rubik/tools/builtins/lsp.rb +36 -8
- data/lib/rubik/version.rb +1 -1
- metadata +2 -1
|
@@ -110,99 +110,124 @@ module Rubik
|
|
|
110
110
|
end
|
|
111
111
|
end
|
|
112
112
|
|
|
113
|
+
private_class_method def self.handle_list_models(ctx)
|
|
114
|
+
mode = ctx.config.fetch("router.mode", "auto")
|
|
115
|
+
def_spec = ctx.config.model_spec(:default)
|
|
116
|
+
fast_spec = ctx.config.model_spec(:fast)
|
|
117
|
+
adv_spec = ctx.config.model_spec(:advanced)
|
|
118
|
+
|
|
119
|
+
def_limit = ctx.llm_registry.context_limit_for(def_spec["provider"], def_spec["name"])
|
|
120
|
+
fast_limit = ctx.llm_registry.context_limit_for(fast_spec["provider"], fast_spec["name"])
|
|
121
|
+
adv_limit = ctx.llm_registry.context_limit_for(adv_spec["provider"], adv_spec["name"])
|
|
122
|
+
|
|
123
|
+
def_ep = def_spec["endpoint"] || def_spec["host"] || "cloud"
|
|
124
|
+
fast_ep = fast_spec["endpoint"] || fast_spec["host"] || "cloud"
|
|
125
|
+
adv_ep = adv_spec["endpoint"] || adv_spec["host"] || "cloud"
|
|
126
|
+
|
|
127
|
+
ctx.out.puts "Model Router Configuration (Mode: #{mode}):"
|
|
128
|
+
ctx.out.puts " • default : #{def_spec['provider']}/#{def_spec['name']} [Context: #{def_limit} tokens] (#{def_ep})"
|
|
129
|
+
ctx.out.puts " • fast : #{fast_spec['provider']}/#{fast_spec['name']} [Context: #{fast_limit} tokens] (#{fast_ep})"
|
|
130
|
+
ctx.out.puts " • advanced : #{adv_spec['provider']}/#{adv_spec['name']} [Context: #{adv_limit} tokens] (#{adv_ep})"
|
|
131
|
+
ctx.out.puts "Global Config : #{ctx.config.global_config_path}"
|
|
132
|
+
ctx.out.puts "Project Config : #{ctx.config.project_config_path}"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
private_class_method def self.handle_route_command(clean_args, ctx, scope)
|
|
136
|
+
mode = clean_args[1].to_s.downcase
|
|
137
|
+
if ["auto", "manual"].include?(mode)
|
|
138
|
+
res = ctx.config.set("router.mode", mode, persist: scope)
|
|
139
|
+
return if Base.report_persist_failure(ctx, res, scope)
|
|
140
|
+
|
|
141
|
+
ctx.out.puts "Router mode set to #{mode} [#{Base.scope_label(scope)}]."
|
|
142
|
+
else
|
|
143
|
+
ctx.out.puts "Usage: !model route <auto|manual>"
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
private_class_method def self.handle_add_model(clean_args, ctx, scope)
|
|
148
|
+
p_name = clean_args[1]&.downcase
|
|
149
|
+
m_name = clean_args[2]
|
|
150
|
+
if p_name.nil? || p_name.empty? || m_name.nil? || m_name.empty?
|
|
151
|
+
ctx.out.puts "Usage: !model add <provider_name> <model_name> [--context=LIMIT] [--project|--global]"
|
|
152
|
+
else
|
|
153
|
+
context_arg = clean_args.find { |a| a.start_with?("--context=") }
|
|
154
|
+
c_limit = context_arg ? context_arg.split("=", 2).last.to_i : 32_768
|
|
155
|
+
res = ctx.config.upsert_provider_model(p_name, m_name, context_limit: c_limit, persist: scope)
|
|
156
|
+
ctx.refresh_llm_registry!
|
|
157
|
+
if res[:ok]
|
|
158
|
+
ctx.out.puts "Added model '#{m_name}' (#{c_limit} tokens) to provider '#{p_name}' [#{Base.scope_label(scope)}]."
|
|
159
|
+
elsif res[:reason] == :unknown_provider
|
|
160
|
+
ctx.out.puts "Provider '#{p_name}' not found. Add provider first via !provider add."
|
|
161
|
+
else
|
|
162
|
+
Base.report_persist_failure(ctx, res, scope)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
private_class_method def self.handle_context_command(clean_args, ctx, scope)
|
|
168
|
+
if clean_args.size == 2 && clean_args[1] =~ /^\d+$/
|
|
169
|
+
def_spec = ctx.config.model_spec(:default)
|
|
170
|
+
update_context_limit(ctx, scope, def_spec["provider"], def_spec["name"],
|
|
171
|
+
clean_args[1].to_i, "default model #{def_spec['provider']}/#{def_spec['name']}")
|
|
172
|
+
elsif clean_args.size == 3 && ["default", "fast", "advanced"].include?(clean_args[1].downcase) && clean_args[2] =~ /^\d+$/
|
|
173
|
+
role = clean_args[1].downcase.to_sym
|
|
174
|
+
spec = ctx.config.model_spec(role)
|
|
175
|
+
update_context_limit(ctx, scope, spec["provider"], spec["name"],
|
|
176
|
+
clean_args[2].to_i, "#{role} model #{spec['provider']}/#{spec['name']}")
|
|
177
|
+
elsif clean_args.size == 4 && clean_args[3] =~ /^\d+$/
|
|
178
|
+
update_context_limit(ctx, scope, clean_args[1].downcase, clean_args[2],
|
|
179
|
+
clean_args[3].to_i, "#{clean_args[1].downcase}/#{clean_args[2]}")
|
|
180
|
+
else
|
|
181
|
+
ctx.out.puts "Usage: !model context <limit> | !model context <default|fast|advanced> <limit> | " \
|
|
182
|
+
"!model context <provider> <model> <limit> [--project|--global]"
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
private_class_method def self.handle_set_role_command(clean_args, ctx, scope)
|
|
187
|
+
role = clean_args[1].to_s.downcase
|
|
188
|
+
model_args = clean_args[2..]
|
|
189
|
+
if model_args.nil? || model_args.empty?
|
|
190
|
+
ctx.out.puts "Usage: !model set <default|fast|advanced> [<provider>] <model_name> [--project|--global]"
|
|
191
|
+
else
|
|
192
|
+
prov, mod = parse_model_args(model_args, ctx)
|
|
193
|
+
res = ctx.config.set_model_spec(role, provider: prov, name: mod, persist: scope)
|
|
194
|
+
ctx.resync_llm_route!
|
|
195
|
+
return if Base.report_persist_failure(ctx, res, scope)
|
|
196
|
+
|
|
197
|
+
limit = ctx.llm_registry.context_limit_for(prov, mod)
|
|
198
|
+
ctx.out.puts "Set model role '#{role}' to #{prov}/#{mod} [Context: #{limit} tokens] [#{Base.scope_label(scope)}]."
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
private_class_method def self.handle_switch_default_model(clean_args, ctx, scope)
|
|
203
|
+
prov, mod = parse_model_args(clean_args, ctx)
|
|
204
|
+
res = ctx.config.set_model_spec(:default, provider: prov, name: mod, persist: scope)
|
|
205
|
+
ctx.resync_llm_route!
|
|
206
|
+
return if Base.report_persist_failure(ctx, res, scope)
|
|
207
|
+
|
|
208
|
+
limit = ctx.llm_registry.context_limit_for(prov, mod)
|
|
209
|
+
ctx.out.puts "Switched default model to #{prov}/#{mod} [Context: #{limit} tokens] [#{Base.scope_label(scope)}]."
|
|
210
|
+
end
|
|
211
|
+
|
|
113
212
|
private_class_method def self.register_model_command(shell)
|
|
114
213
|
shell.register_command("model",
|
|
115
214
|
audience: :both,
|
|
116
215
|
description: "Switch or view LLM provider/model roles (default, fast, advanced) with auto-persistence") do |args, ctx|
|
|
117
216
|
scope, clean_args = Base.parse_scope(args)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
fast_ep = fast_spec["endpoint"] || fast_spec["host"] || "cloud"
|
|
131
|
-
adv_ep = adv_spec["endpoint"] || adv_spec["host"] || "cloud"
|
|
132
|
-
|
|
133
|
-
ctx.out.puts "Model Router Configuration (Mode: #{mode}):"
|
|
134
|
-
ctx.out.puts " • default : #{def_spec['provider']}/#{def_spec['name']} [Context: #{def_limit} tokens] (#{def_ep})"
|
|
135
|
-
ctx.out.puts " • fast : #{fast_spec['provider']}/#{fast_spec['name']} [Context: #{fast_limit} tokens] (#{fast_ep})"
|
|
136
|
-
ctx.out.puts " • advanced : #{adv_spec['provider']}/#{adv_spec['name']} [Context: #{adv_limit} tokens] (#{adv_ep})"
|
|
137
|
-
ctx.out.puts "Global Config : #{ctx.config.global_config_path}"
|
|
138
|
-
ctx.out.puts "Project Config : #{ctx.config.project_config_path}"
|
|
139
|
-
elsif clean_args.first.downcase == "route"
|
|
140
|
-
mode = clean_args[1].to_s.downcase
|
|
141
|
-
if ["auto", "manual"].include?(mode)
|
|
142
|
-
res = ctx.config.set("router.mode", mode, persist: scope)
|
|
143
|
-
next if Base.report_persist_failure(ctx, res, scope)
|
|
144
|
-
|
|
145
|
-
ctx.out.puts "Router mode set to #{mode} [#{Base.scope_label(scope)}]."
|
|
146
|
-
else
|
|
147
|
-
ctx.out.puts "Usage: !model route <auto|manual>"
|
|
148
|
-
end
|
|
149
|
-
elsif clean_args.first.downcase == "add"
|
|
150
|
-
p_name = clean_args[1]&.downcase
|
|
151
|
-
m_name = clean_args[2]
|
|
152
|
-
if p_name.nil? || p_name.empty? || m_name.nil? || m_name.empty?
|
|
153
|
-
ctx.out.puts "Usage: !model add <provider_name> <model_name> [--context=LIMIT] [--project|--global]"
|
|
154
|
-
else
|
|
155
|
-
context_arg = clean_args.find { |a| a.start_with?("--context=") }
|
|
156
|
-
c_limit = context_arg ? context_arg.split("=", 2).last.to_i : 32_768
|
|
157
|
-
res = ctx.config.upsert_provider_model(p_name, m_name, context_limit: c_limit, persist: scope)
|
|
158
|
-
ctx.refresh_llm_registry!
|
|
159
|
-
if res[:ok]
|
|
160
|
-
ctx.out.puts "Added model '#{m_name}' (#{c_limit} tokens) to provider '#{p_name}' [#{Base.scope_label(scope)}]."
|
|
161
|
-
elsif res[:reason] == :unknown_provider
|
|
162
|
-
ctx.out.puts "Provider '#{p_name}' not found. Add provider first via !provider add."
|
|
163
|
-
else
|
|
164
|
-
Base.report_persist_failure(ctx, res, scope)
|
|
165
|
-
end
|
|
166
|
-
end
|
|
167
|
-
elsif ["context", "limit"].include?(clean_args.first.downcase)
|
|
168
|
-
if clean_args.size == 2 && clean_args[1] =~ /^\d+$/
|
|
169
|
-
def_spec = ctx.config.model_spec(:default)
|
|
170
|
-
update_context_limit(ctx, scope, def_spec["provider"], def_spec["name"],
|
|
171
|
-
clean_args[1].to_i, "default model #{def_spec['provider']}/#{def_spec['name']}")
|
|
172
|
-
elsif clean_args.size == 3 && ["default", "fast", "advanced"].include?(clean_args[1].downcase) && clean_args[2] =~ /^\d+$/
|
|
173
|
-
role = clean_args[1].downcase.to_sym
|
|
174
|
-
spec = ctx.config.model_spec(role)
|
|
175
|
-
update_context_limit(ctx, scope, spec["provider"], spec["name"],
|
|
176
|
-
clean_args[2].to_i, "#{role} model #{spec['provider']}/#{spec['name']}")
|
|
177
|
-
elsif clean_args.size == 4 && clean_args[3] =~ /^\d+$/
|
|
178
|
-
update_context_limit(ctx, scope, clean_args[1].downcase, clean_args[2],
|
|
179
|
-
clean_args[3].to_i, "#{clean_args[1].downcase}/#{clean_args[2]}")
|
|
180
|
-
else
|
|
181
|
-
ctx.out.puts "Usage: !model context <limit> | !model context <default|fast|advanced> <limit> | " \
|
|
182
|
-
"!model context <provider> <model> <limit> [--project|--global]"
|
|
183
|
-
end
|
|
184
|
-
elsif clean_args.first.downcase == "set"
|
|
185
|
-
role = clean_args[1].to_s.downcase
|
|
186
|
-
model_args = clean_args[2..]
|
|
187
|
-
if model_args.nil? || model_args.empty?
|
|
188
|
-
ctx.out.puts "Usage: !model set <default|fast|advanced> [<provider>] <model_name> [--project|--global]"
|
|
189
|
-
else
|
|
190
|
-
prov, mod = parse_model_args(model_args, ctx)
|
|
191
|
-
res = ctx.config.set_model_spec(role, provider: prov, name: mod, persist: scope)
|
|
192
|
-
ctx.resync_llm_route!
|
|
193
|
-
next if Base.report_persist_failure(ctx, res, scope)
|
|
194
|
-
|
|
195
|
-
limit = ctx.llm_registry.context_limit_for(prov, mod)
|
|
196
|
-
ctx.out.puts "Set model role '#{role}' to #{prov}/#{mod} [Context: #{limit} tokens] [#{Base.scope_label(scope)}]."
|
|
197
|
-
end
|
|
217
|
+
subcmd = clean_args.first&.downcase
|
|
218
|
+
|
|
219
|
+
if clean_args.empty? || subcmd == "list"
|
|
220
|
+
handle_list_models(ctx)
|
|
221
|
+
elsif subcmd == "route"
|
|
222
|
+
handle_route_command(clean_args, ctx, scope)
|
|
223
|
+
elsif subcmd == "add"
|
|
224
|
+
handle_add_model(clean_args, ctx, scope)
|
|
225
|
+
elsif ["context", "limit"].include?(subcmd)
|
|
226
|
+
handle_context_command(clean_args, ctx, scope)
|
|
227
|
+
elsif subcmd == "set"
|
|
228
|
+
handle_set_role_command(clean_args, ctx, scope)
|
|
198
229
|
else
|
|
199
|
-
|
|
200
|
-
res = ctx.config.set_model_spec(:default, provider: prov, name: mod, persist: scope)
|
|
201
|
-
ctx.resync_llm_route!
|
|
202
|
-
next if Base.report_persist_failure(ctx, res, scope)
|
|
203
|
-
|
|
204
|
-
limit = ctx.llm_registry.context_limit_for(prov, mod)
|
|
205
|
-
ctx.out.puts "Switched default model to #{prov}/#{mod} [Context: #{limit} tokens] [#{Base.scope_label(scope)}]."
|
|
230
|
+
handle_switch_default_model(clean_args, ctx, scope)
|
|
206
231
|
end
|
|
207
232
|
end
|
|
208
233
|
end
|
|
@@ -16,6 +16,14 @@ module Rubik
|
|
|
16
16
|
USAGE_AUTOAPPLY = "Usage: !autoapply <manual | on_test | on_finish | always | status> [--project|--global]"
|
|
17
17
|
|
|
18
18
|
def self.register(shell)
|
|
19
|
+
register_diff_command(shell)
|
|
20
|
+
register_apply_command(shell)
|
|
21
|
+
register_undo_command(shell)
|
|
22
|
+
register_rollback_command(shell)
|
|
23
|
+
register_autoapply_command(shell)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private_class_method def self.register_diff_command(shell)
|
|
19
27
|
shell.register_command(
|
|
20
28
|
"diff",
|
|
21
29
|
description: "Preview unapplied buffer changes (staged edits that are NOT yet on disk). " \
|
|
@@ -23,111 +31,137 @@ module Rubik
|
|
|
23
31
|
"For ground-truth file content, use read_file; for git history, use run_command with git. " \
|
|
24
32
|
"(colorized viewer on terminals)",
|
|
25
33
|
audience: :both
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
target_path = args.first
|
|
29
|
-
if target_path && !target_path.empty?
|
|
30
|
-
rel = begin
|
|
31
|
-
ctx.buffer_manager.sanitize_path(target_path)
|
|
32
|
-
rescue
|
|
33
|
-
target_path
|
|
34
|
-
end
|
|
35
|
-
if ctx.buffer_manager.dirty_files.key?(rel)
|
|
36
|
-
full_path = File.expand_path(rel, ctx.buffer_manager.root_dir)
|
|
37
|
-
orig_content = File.exist?(full_path) ? File.read(full_path) : ""
|
|
38
|
-
new_content = ctx.buffer_manager.read(rel) || ""
|
|
39
|
-
Rubik::UI::DiffViewer.render(ctx, theme, Rubik::Buffer::Diff.unified_diff(rel, orig_content, new_content))
|
|
40
|
-
else
|
|
41
|
-
ctx.out.puts "No unapplied buffer changes for '#{target_path}'."
|
|
42
|
-
end
|
|
43
|
-
else
|
|
44
|
-
diff_text = ctx.buffer_manager.diff_all
|
|
45
|
-
if diff_text.empty?
|
|
46
|
-
ctx.out.puts "No unapplied buffer changes."
|
|
47
|
-
else
|
|
48
|
-
Rubik::UI::DiffViewer.render(ctx, theme, diff_text)
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
end
|
|
34
|
+
) { |args, ctx| handle_diff(args, ctx) }
|
|
35
|
+
end
|
|
52
36
|
|
|
37
|
+
private_class_method def self.register_apply_command(shell)
|
|
53
38
|
shell.register_command("apply", description: "Apply dirty buffer changes to disk") do |args, ctx|
|
|
54
|
-
|
|
55
|
-
begin
|
|
56
|
-
if target_path && !target_path.empty?
|
|
57
|
-
applied = ctx.buffer_manager.apply!(target_path)
|
|
58
|
-
conflicts = ctx.buffer_manager.last_conflicts || []
|
|
59
|
-
if applied.empty? && conflicts.empty?
|
|
60
|
-
ctx.out.puts "No unapplied buffer changes for '#{target_path}'."
|
|
61
|
-
else
|
|
62
|
-
ctx.sync_context_and_ast(applied) unless applied.empty?
|
|
63
|
-
ctx.out.puts "Applied changes to file: #{applied.first}" unless applied.empty?
|
|
64
|
-
end
|
|
65
|
-
else
|
|
66
|
-
applied = ctx.buffer_manager.apply!
|
|
67
|
-
conflicts = ctx.buffer_manager.last_conflicts || []
|
|
68
|
-
if applied.empty? && conflicts.empty?
|
|
69
|
-
ctx.out.puts "No unapplied buffer changes."
|
|
70
|
-
else
|
|
71
|
-
ctx.sync_context_and_ast(applied) unless applied.empty?
|
|
72
|
-
ctx.out.puts "Applied changes to #{applied.size} file(s): #{applied.join(', ')}" unless applied.empty?
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
conflicts.each do |c|
|
|
76
|
-
ctx.out.puts "[CONFLICT] #{c}: disk content changed since last read — skipped to prevent data loss. Re-read the file to resolve."
|
|
77
|
-
end
|
|
78
|
-
rescue Rubik::Buffer::ApplyFailedError => e
|
|
79
|
-
# Partial apply (issue #146): the report names what DID reach
|
|
80
|
-
# disk before the failure — a mixed outcome must not be
|
|
81
|
-
# presented as a plain success or a plain failure.
|
|
82
|
-
lines = Rubik::Buffer::ApplyContract.failure_lines(e, sync: ->(paths) { ctx.sync_context_and_ast(paths) })
|
|
83
|
-
ctx.out.puts "❌ #{lines.first}"
|
|
84
|
-
lines[1..].each { |line| ctx.out.puts line }
|
|
85
|
-
end
|
|
39
|
+
handle_apply(args, ctx)
|
|
86
40
|
end
|
|
41
|
+
end
|
|
87
42
|
|
|
43
|
+
private_class_method def self.register_undo_command(shell)
|
|
88
44
|
shell.register_command("undo", description: "Revert the last snapshot", audience: :both) do |_args, ctx|
|
|
89
|
-
|
|
90
|
-
ctx.out.puts "Undo successful."
|
|
91
|
-
else
|
|
92
|
-
ctx.out.puts "No snapshots to undo."
|
|
93
|
-
end
|
|
45
|
+
handle_undo(ctx)
|
|
94
46
|
end
|
|
47
|
+
end
|
|
95
48
|
|
|
49
|
+
private_class_method def self.register_rollback_command(shell)
|
|
96
50
|
shell.register_command("rollback", description: "Rollback to a specific snapshot ID", usage: USAGE_ROLLBACK, audience: :both) do |args, ctx|
|
|
97
|
-
|
|
98
|
-
if sid.nil? || sid.empty?
|
|
99
|
-
ctx.out.puts USAGE_ROLLBACK
|
|
100
|
-
elsif ctx.snapshot_store.rollback(sid, ctx.buffer_manager)
|
|
101
|
-
ctx.out.puts "Rollback to snapshot #{sid} successful."
|
|
102
|
-
else
|
|
103
|
-
ctx.out.puts "Snapshot #{sid} not found."
|
|
104
|
-
end
|
|
51
|
+
handle_rollback(args, ctx)
|
|
105
52
|
end
|
|
53
|
+
end
|
|
106
54
|
|
|
55
|
+
private_class_method def self.register_autoapply_command(shell)
|
|
107
56
|
shell.register_command("autoapply",
|
|
108
57
|
description: "Get or set buffer auto-apply policy (manual|on_test|on_finish|always)",
|
|
109
58
|
usage: USAGE_AUTOAPPLY, audience: :both) do |args, ctx|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
ctx.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
59
|
+
handle_autoapply(args, ctx)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private_class_method def self.handle_diff(args, ctx)
|
|
64
|
+
theme = ctx.theme
|
|
65
|
+
target_path = args.first
|
|
66
|
+
if target_path && !target_path.empty?
|
|
67
|
+
rel = begin
|
|
68
|
+
ctx.buffer_manager.sanitize_path(target_path)
|
|
69
|
+
rescue
|
|
70
|
+
target_path
|
|
71
|
+
end
|
|
72
|
+
if ctx.buffer_manager.dirty_files.key?(rel)
|
|
73
|
+
full_path = File.expand_path(rel, ctx.buffer_manager.root_dir)
|
|
74
|
+
orig_content = File.exist?(full_path) ? File.read(full_path) : ""
|
|
75
|
+
new_content = ctx.buffer_manager.read(rel) || ""
|
|
76
|
+
Rubik::UI::DiffViewer.render(ctx, theme, Rubik::Buffer::Diff.unified_diff(rel, orig_content, new_content))
|
|
77
|
+
else
|
|
78
|
+
ctx.out.puts "No unapplied buffer changes for '#{target_path}'."
|
|
79
|
+
end
|
|
80
|
+
else
|
|
81
|
+
diff_text = ctx.buffer_manager.diff_all
|
|
82
|
+
if diff_text.empty?
|
|
83
|
+
ctx.out.puts "No unapplied buffer changes."
|
|
126
84
|
else
|
|
127
|
-
ctx
|
|
85
|
+
Rubik::UI::DiffViewer.render(ctx, theme, diff_text)
|
|
128
86
|
end
|
|
129
87
|
end
|
|
130
88
|
end
|
|
89
|
+
|
|
90
|
+
private_class_method def self.handle_apply(args, ctx)
|
|
91
|
+
target_path = args.first
|
|
92
|
+
begin
|
|
93
|
+
if target_path && !target_path.empty?
|
|
94
|
+
applied = ctx.buffer_manager.apply!(target_path)
|
|
95
|
+
conflicts = ctx.buffer_manager.last_conflicts || []
|
|
96
|
+
if applied.empty? && conflicts.empty?
|
|
97
|
+
ctx.out.puts "No unapplied buffer changes for '#{target_path}'."
|
|
98
|
+
else
|
|
99
|
+
ctx.sync_context_and_ast(applied) unless applied.empty?
|
|
100
|
+
ctx.out.puts "Applied changes to file: #{applied.first}" unless applied.empty?
|
|
101
|
+
end
|
|
102
|
+
else
|
|
103
|
+
applied = ctx.buffer_manager.apply!
|
|
104
|
+
conflicts = ctx.buffer_manager.last_conflicts || []
|
|
105
|
+
if applied.empty? && conflicts.empty?
|
|
106
|
+
ctx.out.puts "No unapplied buffer changes."
|
|
107
|
+
else
|
|
108
|
+
ctx.sync_context_and_ast(applied) unless applied.empty?
|
|
109
|
+
ctx.out.puts "Applied changes to #{applied.size} file(s): #{applied.join(', ')}" unless applied.empty?
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
conflicts.each do |c|
|
|
113
|
+
ctx.out.puts "[CONFLICT] #{c}: disk content changed since last read — skipped to prevent data loss. Re-read the file to resolve."
|
|
114
|
+
end
|
|
115
|
+
rescue Rubik::Buffer::ApplyFailedError => e
|
|
116
|
+
# Partial apply (issue #146): the report names what DID reach
|
|
117
|
+
# disk before the failure — a mixed outcome must not be
|
|
118
|
+
# presented as a plain success or a plain failure.
|
|
119
|
+
lines = Rubik::Buffer::ApplyContract.failure_lines(e, sync: ->(paths) { ctx.sync_context_and_ast(paths) })
|
|
120
|
+
ctx.out.puts "❌ #{lines.first}"
|
|
121
|
+
lines[1..].each { |line| ctx.out.puts line }
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
private_class_method def self.handle_undo(ctx)
|
|
126
|
+
if ctx.snapshot_store.undo(ctx.buffer_manager)
|
|
127
|
+
ctx.out.puts "Undo successful."
|
|
128
|
+
else
|
|
129
|
+
ctx.out.puts "No snapshots to undo."
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
private_class_method def self.handle_rollback(args, ctx)
|
|
134
|
+
sid = args.first
|
|
135
|
+
if sid.nil? || sid.empty?
|
|
136
|
+
ctx.out.puts USAGE_ROLLBACK
|
|
137
|
+
elsif ctx.snapshot_store.rollback(sid, ctx.buffer_manager)
|
|
138
|
+
ctx.out.puts "Rollback to snapshot #{sid} successful."
|
|
139
|
+
else
|
|
140
|
+
ctx.out.puts "Snapshot #{sid} not found."
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
private_class_method def self.handle_autoapply(args, ctx)
|
|
145
|
+
scope, clean_args = Base.parse_scope(args)
|
|
146
|
+
sub = clean_args.first&.downcase
|
|
147
|
+
|
|
148
|
+
if sub.nil? || sub == "status"
|
|
149
|
+
cur = ctx.config.fetch("buffer.auto_apply", "manual")
|
|
150
|
+
can_m = ctx.config.fetch("buffer.model_can_apply", true)
|
|
151
|
+
ctx.out.puts "Buffer Auto-Apply Policy:"
|
|
152
|
+
ctx.out.puts " • Mode : #{cur} (manual | on_test | on_finish | always)"
|
|
153
|
+
ctx.out.puts " • Model Can Apply : #{can_m}"
|
|
154
|
+
ctx.out.puts " • Prompt Indicator : #{ctx.config.fetch('buffer.prompt_indicator', true)}"
|
|
155
|
+
ctx.out.puts " • Reminder Interval: #{ctx.config.fetch('buffer.reminder_interval', 3)} turns"
|
|
156
|
+
elsif ["manual", "on_test", "on_finish", "always"].include?(sub)
|
|
157
|
+
res = ctx.config.set("buffer.auto_apply", sub, persist: scope)
|
|
158
|
+
return if Base.report_persist_failure(ctx, res, scope)
|
|
159
|
+
|
|
160
|
+
ctx.out.puts "Buffer auto-apply policy set to '#{sub}' [#{Base.scope_label(scope)}]."
|
|
161
|
+
else
|
|
162
|
+
ctx.out.puts USAGE_AUTOAPPLY
|
|
163
|
+
end
|
|
164
|
+
end
|
|
131
165
|
end
|
|
132
166
|
end
|
|
133
167
|
end
|