rubik 0.4.1 → 0.5.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.
- checksums.yaml +4 -4
- data/ATTRIBUTION.md +1 -1
- data/CHANGELOG.md +118 -1
- data/lib/rubik/ast/cache_store.rb +69 -5
- data/lib/rubik/ast/extraction_result.rb +6 -0
- data/lib/rubik/ast/graph.rb +32 -5
- data/lib/rubik/ast/ruby_extractor.rb +8 -2
- data/lib/rubik/buffer/apply_contract.rb +6 -0
- data/lib/rubik/buffer/manager.rb +37 -27
- data/lib/rubik/config/defaults.rb +7 -1
- data/lib/rubik/mcp/client.rb +4 -0
- data/lib/rubik/mcp/manager.rb +51 -35
- data/lib/rubik/mcp/proxy_tool.rb +17 -0
- data/lib/rubik/mcp/transport/stdio.rb +142 -17
- data/lib/rubik/orchestrator/budget_manager.rb +2 -0
- data/lib/rubik/orchestrator/loop.rb +1 -0
- data/lib/rubik/orchestrator/shell_deps.rb +4 -0
- data/lib/rubik/orchestrator/turn_executor/user_channel.rb +2 -0
- data/lib/rubik/plugin/manager.rb +39 -15
- data/lib/rubik/plugin/package_inventory.rb +11 -1
- data/lib/rubik/plugin/storage.rb +5 -4
- data/lib/rubik/prompt/doctrine.rb +10 -15
- data/lib/rubik/prompt/tool_teachings.rb +29 -35
- data/lib/rubik/repair/loop.rb +29 -15
- data/lib/rubik/repl/commands/agent_context.rb +1 -0
- data/lib/rubik/repl/commands/system.rb +2 -0
- data/lib/rubik/repl/turn_recorder.rb +3 -0
- data/lib/rubik/security/redactor.rb +13 -9
- data/lib/rubik/storage/vakanuvis/gate.rb +13 -8
- data/lib/rubik/storage/vakanuvis/harness.rb +115 -112
- data/lib/rubik/storage/vakanuvis/orientation.rb +277 -0
- data/lib/rubik/storage/vakanuvis/service.rb +5 -1
- data/lib/rubik/storage/vakanuvis/vocabulary.rb +33 -2
- data/lib/rubik/storage/vakanuvis.rb +8 -1
- data/lib/rubik/tools/builtins/lifecycle.rb +42 -47
- data/lib/rubik/tools/builtins/vakanuvis.rb +13 -15
- data/lib/rubik/ui/capability.rb +19 -5
- data/lib/rubik/version.rb +1 -1
- metadata +2 -1
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../layout"
|
|
4
|
+
|
|
5
|
+
module Rubik
|
|
6
|
+
module Storage
|
|
7
|
+
module Vakanuvis
|
|
8
|
+
# Soğuk-başlangıç ve sıkıştırma sonrası yönelim enjeksiyonu (issue #340):
|
|
9
|
+
# Sistem istemi (system prompt) hiç şişirilmeden, oturumun ilk model
|
|
10
|
+
# çağrısından önce ve her bağlam sıkıştırması / budaması tamamlandığında
|
|
11
|
+
# kök konuşma geçmişine kısa, deterministik bir `role: "user"` yönelim
|
|
12
|
+
# iletisi eklenir. Kart yoksa (N == 0), kronikçi kapalıysa veya
|
|
13
|
+
# `subagents.vakanuvis.orientation == false` ise hiçbir şey enjekte
|
|
14
|
+
# edilmez. Kanca `ctx` içine tersiyle (`on_dispose`) kaydedilir (§1A).
|
|
15
|
+
module Orientation
|
|
16
|
+
DEFAULT_MAX_TOPICS = 10
|
|
17
|
+
HOOK_KEY = :vakanuvis_orientation_hook
|
|
18
|
+
TAG_OPEN = "<vakanuvis_orientation>"
|
|
19
|
+
TAG_CLOSE = "</vakanuvis_orientation>"
|
|
20
|
+
DEPTH_HINT = "Ask `vakanuvis(action: 'recall', query: '<2-3 terms>')` for depth."
|
|
21
|
+
|
|
22
|
+
# Oturum-içi durum ve terslenebilir kanca taşıyıcısı.
|
|
23
|
+
class Hook
|
|
24
|
+
attr_reader :harness
|
|
25
|
+
|
|
26
|
+
def initialize(harness, config: nil)
|
|
27
|
+
@harness = harness
|
|
28
|
+
@config = config
|
|
29
|
+
@session_oriented = false
|
|
30
|
+
@disposed = false
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def dispose!
|
|
34
|
+
@disposed = true
|
|
35
|
+
@session_oriented = false
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def disposed?
|
|
39
|
+
@disposed
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def reset_session!
|
|
43
|
+
@session_oriented = false
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def enabled?(override_config = nil)
|
|
47
|
+
return false if @disposed || @harness.nil? || @harness.stopped?
|
|
48
|
+
|
|
49
|
+
cfg = override_config || @config || @harness.config
|
|
50
|
+
return false unless Service.enabled?(cfg)
|
|
51
|
+
|
|
52
|
+
Layout.config_get(cfg, "subagents.vakanuvis.orientation", true) != false
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def max_topics(override_config = nil)
|
|
56
|
+
cfg = override_config || @config || @harness.config
|
|
57
|
+
raw = Layout.config_get(cfg, "subagents.vakanuvis.orientation_max_topics", DEFAULT_MAX_TOPICS)
|
|
58
|
+
val = raw.to_i
|
|
59
|
+
val.positive? ? val : DEFAULT_MAX_TOPICS
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def build_message(override_config = nil)
|
|
63
|
+
return nil unless enabled?(override_config)
|
|
64
|
+
|
|
65
|
+
excerpt = Orientation.format_excerpt(@harness, limit: max_topics(override_config))
|
|
66
|
+
return nil unless excerpt
|
|
67
|
+
|
|
68
|
+
{ role: "user", content: excerpt }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def inject_session_start!(history, config: nil)
|
|
72
|
+
return nil unless history.is_a?(Array)
|
|
73
|
+
|
|
74
|
+
if history.any? { |m| Orientation.orientation_message?(m) }
|
|
75
|
+
@session_oriented = true
|
|
76
|
+
return nil
|
|
77
|
+
end
|
|
78
|
+
return nil if @session_oriented
|
|
79
|
+
return nil unless enabled?(config)
|
|
80
|
+
|
|
81
|
+
msg = build_message(config)
|
|
82
|
+
return nil unless msg
|
|
83
|
+
|
|
84
|
+
@session_oriented = true
|
|
85
|
+
Orientation.deduplicate_and_insert_start!(history, msg)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def inject_after_compaction!(history, config: nil)
|
|
89
|
+
return nil unless history.is_a?(Array)
|
|
90
|
+
|
|
91
|
+
Orientation.strip_orientation_messages!(history)
|
|
92
|
+
return nil unless enabled?(config)
|
|
93
|
+
|
|
94
|
+
msg = build_message(config)
|
|
95
|
+
return nil unless msg
|
|
96
|
+
|
|
97
|
+
@session_oriented = true
|
|
98
|
+
Orientation.insert_after_compaction!(history, msg)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
module_function
|
|
103
|
+
|
|
104
|
+
# Kancayı `ctx` (veya child sub-context) içine tersiyle kaydeder (§1A).
|
|
105
|
+
# Kronikçi devre dışı bırakıldığında veya child dispose edildiğinde
|
|
106
|
+
# kanca otomatik olarak sökülür; reload döngülerinde yetim kanca kalmaz.
|
|
107
|
+
def register_hook(ctx, harness, config: nil, inverse_ctx: nil)
|
|
108
|
+
return nil unless ctx.respond_to?(:provide) && harness
|
|
109
|
+
|
|
110
|
+
cfg = config || harness.config
|
|
111
|
+
hook = Hook.new(harness, config: cfg)
|
|
112
|
+
ctx.provide(HOOK_KEY, hook)
|
|
113
|
+
target_ctx = inverse_ctx || ctx
|
|
114
|
+
cfg_inverse = wire_config_observer(ctx, hook, cfg)
|
|
115
|
+
if target_ctx.respond_to?(:on_dispose)
|
|
116
|
+
target_ctx.on_dispose do
|
|
117
|
+
cfg_inverse&.call
|
|
118
|
+
hook.dispose!
|
|
119
|
+
ctx.unprovide(HOOK_KEY) if ctx.respond_to?(:unprovide) && ctx.get(HOOK_KEY).equal?(hook)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
hook
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def wire_config_observer(ctx, hook, cfg)
|
|
126
|
+
return nil unless cfg.respond_to?(:on_change)
|
|
127
|
+
|
|
128
|
+
cfg.on_change("subagents.vakanuvis") do |_key, _val, _old|
|
|
129
|
+
next if hook.disposed?
|
|
130
|
+
|
|
131
|
+
if hook.enabled?(cfg)
|
|
132
|
+
ctx.provide(HOOK_KEY, hook) unless ctx.get(HOOK_KEY).equal?(hook)
|
|
133
|
+
elsif ctx.respond_to?(:unprovide) && ctx.get(HOOK_KEY).equal?(hook)
|
|
134
|
+
ctx.unprovide(HOOK_KEY)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Coeffect üzerinden kancayı veya doğrudan VAKANUIS harness'ini çözer.
|
|
140
|
+
# VAKANUIS coeffect'i karşılanmamışsa nil döner.
|
|
141
|
+
def resolve_hook(context_or_manager, config: nil)
|
|
142
|
+
ctx = extract_context(context_or_manager)
|
|
143
|
+
return nil unless ctx
|
|
144
|
+
|
|
145
|
+
harness = Rubik::Coeffects.get(ctx, Rubik::Coeffects::VAKANUIS)
|
|
146
|
+
return nil unless harness.is_a?(Harness) && !harness.stopped?
|
|
147
|
+
|
|
148
|
+
cfg = config || harness.config
|
|
149
|
+
return nil unless Service.enabled?(cfg)
|
|
150
|
+
return nil if Layout.config_get(cfg, "subagents.vakanuvis.orientation", true) == false
|
|
151
|
+
|
|
152
|
+
hook = Rubik::Coeffects.get(ctx, HOOK_KEY)
|
|
153
|
+
return hook if hook.is_a?(Hook) && !hook.disposed? && hook.harness.equal?(harness)
|
|
154
|
+
|
|
155
|
+
register_hook(ctx, harness, config: cfg)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def reset_session!(context_or_manager)
|
|
159
|
+
ctx = extract_context(context_or_manager)
|
|
160
|
+
return false unless ctx
|
|
161
|
+
|
|
162
|
+
hook = Rubik::Coeffects.get(ctx, HOOK_KEY)
|
|
163
|
+
hook.reset_session! if hook.is_a?(Hook)
|
|
164
|
+
true
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def inject_session_start!(history, context_or_manager, config: nil)
|
|
168
|
+
hook = resolve_hook(context_or_manager, config: config)
|
|
169
|
+
return nil unless hook
|
|
170
|
+
|
|
171
|
+
hook.inject_session_start!(history, config: config)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def inject_after_compaction!(history, context_or_manager, config: nil)
|
|
175
|
+
strip_orientation_messages!(history) if history.is_a?(Array)
|
|
176
|
+
hook = resolve_hook(context_or_manager, config: config)
|
|
177
|
+
return nil unless hook
|
|
178
|
+
|
|
179
|
+
hook.inject_after_compaction!(history, config: config)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Aktif mem.* kartlarını [ref, row] sırasıyla listeler.
|
|
183
|
+
# Dönen çift: [lines_array, total_active_cards_count].
|
|
184
|
+
def topic_lines(store, limit: nil)
|
|
185
|
+
return [[], 0] unless store
|
|
186
|
+
|
|
187
|
+
bound = store.refs.select { |ref, _| Vocabulary.memory_ref?(ref) }
|
|
188
|
+
cards = store.list(type: "memory").select do |s|
|
|
189
|
+
s.status == Vocabulary::STATUS_ACTIVE && bound.value?(s.id)
|
|
190
|
+
end
|
|
191
|
+
return [[], 0] if cards.empty?
|
|
192
|
+
|
|
193
|
+
all_lines = cards.sort_by { |s| [bound.key(s.id), s.row] }.filter_map do |s|
|
|
194
|
+
record = store.get(type: "memory", id: s.id)
|
|
195
|
+
next unless record # list→get arası silme yarışı (review L2)
|
|
196
|
+
|
|
197
|
+
"- #{bound.key(s.id)} — #{s.title} — #{first_body_line(record)} — #{store.uri_for('memory', s.id)}"
|
|
198
|
+
end
|
|
199
|
+
total = all_lines.size
|
|
200
|
+
shown = limit ? all_lines.first(limit.to_i) : all_lines
|
|
201
|
+
[shown, total]
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def first_body_line(record)
|
|
205
|
+
slice = record["body"].to_s.lines.map(&:strip).reject(&:empty?).first.to_s.slice(0, Harness::RECALL_BODY_SLICE)
|
|
206
|
+
slice.empty? ? "(no body)" : slice
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Aktif mem.* kartlarından deterministik [ref, row] sırasıyla en fazla
|
|
210
|
+
# `limit` satır üretir. Aktif kart yoksa (N == 0) nil döner.
|
|
211
|
+
def format_excerpt(harness, limit: DEFAULT_MAX_TOPICS)
|
|
212
|
+
store = harness.respond_to?(:store) ? harness.store : nil
|
|
213
|
+
lines, total = topic_lines(store, limit: limit)
|
|
214
|
+
return nil if lines.empty?
|
|
215
|
+
|
|
216
|
+
count_label = total > lines.size ? "#{lines.size} of #{total}" : lines.size.to_s
|
|
217
|
+
"#{TAG_OPEN}\nRecorded topics (#{count_label}):\n#{lines.join("\n")}\n#{DEPTH_HINT}\n#{TAG_CLOSE}"
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def orientation_message?(msg)
|
|
221
|
+
return false unless msg.is_a?(Hash)
|
|
222
|
+
|
|
223
|
+
role = (msg[:role] || msg["role"]).to_s
|
|
224
|
+
return false unless role == "user"
|
|
225
|
+
|
|
226
|
+
content = (msg[:content] || msg["content"]).to_s
|
|
227
|
+
content.include?(TAG_OPEN) && content.include?(DEPTH_HINT)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def strip_orientation_messages!(history)
|
|
231
|
+
return history unless history.is_a?(Array)
|
|
232
|
+
|
|
233
|
+
history.reject! { |msg| orientation_message?(msg) }
|
|
234
|
+
history
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Oturum başında: sistem mesajı varsa hemen ardına (ilk user mesajından
|
|
238
|
+
# önce), yoksa dizinin başına yerleştirir. Eski kopyaları temizler.
|
|
239
|
+
def deduplicate_and_insert_start!(history, msg)
|
|
240
|
+
strip_orientation_messages!(history)
|
|
241
|
+
idx = first_non_system_index(history)
|
|
242
|
+
history.insert(idx, msg)
|
|
243
|
+
msg
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Sıkıştırma sonrasında: eski yönelim iletilerini siler ve yeni yönelim
|
|
247
|
+
# iletisini sistem mesajlarının hemen ardına deterministik olarak ekler.
|
|
248
|
+
def insert_after_compaction!(history, msg)
|
|
249
|
+
strip_orientation_messages!(history)
|
|
250
|
+
idx = first_non_system_index(history)
|
|
251
|
+
history.insert(idx, msg)
|
|
252
|
+
msg
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def first_non_system_index(history)
|
|
256
|
+
idx = history.index do |m|
|
|
257
|
+
role = m.is_a?(Hash) ? (m[:role] || m["role"]).to_s : ""
|
|
258
|
+
role != "system"
|
|
259
|
+
end
|
|
260
|
+
idx || history.size
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def extract_context(context_or_manager)
|
|
264
|
+
return nil unless context_or_manager
|
|
265
|
+
|
|
266
|
+
if context_or_manager.respond_to?(:context)
|
|
267
|
+
context_or_manager.context
|
|
268
|
+
elsif context_or_manager.respond_to?(:runtime_manager) && context_or_manager.runtime_manager.respond_to?(:context)
|
|
269
|
+
context_or_manager.runtime_manager.context
|
|
270
|
+
else
|
|
271
|
+
context_or_manager
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
end
|
|
@@ -57,7 +57,11 @@ module Rubik
|
|
|
57
57
|
)
|
|
58
58
|
# Ömür (§2.7): child ctx'in cascade inverse'i worker'ı kapatır;
|
|
59
59
|
# kuyruk FIFO'su drain olur, yarım kronik kalmaz.
|
|
60
|
-
child&.on_dispose
|
|
60
|
+
child&.on_dispose do
|
|
61
|
+
harness.shutdown
|
|
62
|
+
ctx.unprovide(Rubik::Coeffects::VAKANUIS) if ctx.respond_to?(:unprovide) && ctx.get(Rubik::Coeffects::VAKANUIS).equal?(harness)
|
|
63
|
+
end
|
|
64
|
+
Orientation.register_hook(ctx, harness, config: config, inverse_ctx: child)
|
|
61
65
|
harness.start!
|
|
62
66
|
ctx.provide(Rubik::Coeffects::VAKANUIS, harness)
|
|
63
67
|
context_or_manager.reconcile_all if context_or_manager.respond_to?(:reconcile_all)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
3
5
|
module Rubik
|
|
4
6
|
module Storage
|
|
5
7
|
module Vakanuvis
|
|
@@ -36,16 +38,45 @@ module Rubik
|
|
|
36
38
|
|
|
37
39
|
module_function
|
|
38
40
|
|
|
41
|
+
def transliterate(text)
|
|
42
|
+
s = text.to_s.dup
|
|
43
|
+
s.gsub!(/[çÇ]/, "c")
|
|
44
|
+
s.gsub!(/[ğĞ]/, "g")
|
|
45
|
+
s.gsub!(/[ıİ]/, "i")
|
|
46
|
+
s.gsub!(/[öÖ]/, "o")
|
|
47
|
+
s.gsub!(/[şŞ]/, "s")
|
|
48
|
+
s.gsub!(/[üÜ]/, "u")
|
|
49
|
+
s.unicode_normalize(:nfd).gsub(/\p{M}/, "")
|
|
50
|
+
end
|
|
51
|
+
|
|
39
52
|
# Deterministik slug: aynı metin ⇒ aynı slug ⇒ aynı ref (digest
|
|
40
53
|
# makbuzlarının ve mem./wiki. ref'lerin yeniden oynatılabilirliği
|
|
41
54
|
# buna dayanır). Yalnızca-rakam slug'a "n" öneki zorunlu: Store'un
|
|
42
55
|
# çözümleyicisi "wiki.42" gibi iki parçalı tip.satır formlarını ref
|
|
43
56
|
# alias'tan ÖNCE dener — çarpışma yanlış satırı ezmesi demekti.
|
|
57
|
+
# Unicode dönüştürme ve kısaltma/boşluk durumunda deterministik
|
|
58
|
+
# hash eki (issue #269) çakışmaları önler.
|
|
44
59
|
def slug(text)
|
|
45
|
-
|
|
60
|
+
raw = text.to_s
|
|
61
|
+
return "untitled" if raw.strip.empty?
|
|
62
|
+
|
|
63
|
+
trans = transliterate(raw)
|
|
64
|
+
lossy = trans.match?(/[^\x00-\x7F]/)
|
|
65
|
+
|
|
66
|
+
s = trans.downcase.gsub(/[^a-z0-9]+/, "-")
|
|
46
67
|
s = s.delete_prefix("-") while s.start_with?("-")
|
|
47
68
|
s = s.delete_suffix("-") while s.end_with?("-")
|
|
48
|
-
|
|
69
|
+
|
|
70
|
+
hash = Digest::SHA256.hexdigest(raw)[0, 6]
|
|
71
|
+
|
|
72
|
+
return "untitled-#{hash}" if s.empty?
|
|
73
|
+
|
|
74
|
+
if s.length > SLUG_MAX || lossy
|
|
75
|
+
prefix = s[0, SLUG_MAX - 7].to_s
|
|
76
|
+
prefix = prefix.delete_suffix("-") while prefix.end_with?("-")
|
|
77
|
+
s = prefix.empty? ? "untitled-#{hash}" : "#{prefix}-#{hash}"
|
|
78
|
+
end
|
|
79
|
+
|
|
49
80
|
s = s.delete_suffix("-") while s.end_with?("-")
|
|
50
81
|
s = "n#{s}" if s.match?(/\A\d+\z/)
|
|
51
82
|
s.empty? ? "untitled" : s
|
|
@@ -50,10 +50,16 @@ module Rubik
|
|
|
50
50
|
# gereği notice YOK — sadece dmesg/stderr düzlemi.
|
|
51
51
|
SUBSYSTEM = :storage
|
|
52
52
|
|
|
53
|
+
# Konu-adlı uyarıların SON İZİ buradır: iki havuz da patlarsa bile
|
|
54
|
+
# mesaj stderr'e düşer — kayıp iz yok (review #346 F2).
|
|
53
55
|
def log_via(dmesg, level, message)
|
|
54
56
|
dmesg.respond_to?(:log) ? dmesg.log(SUBSYSTEM, level, message) : Rubik::Log.send(level, SUBSYSTEM, message)
|
|
55
57
|
rescue StandardError
|
|
56
|
-
|
|
58
|
+
begin
|
|
59
|
+
Rubik::Log.warn(SUBSYSTEM, message)
|
|
60
|
+
rescue StandardError
|
|
61
|
+
warn("[vakanuvis] #{message}")
|
|
62
|
+
end
|
|
57
63
|
end
|
|
58
64
|
|
|
59
65
|
def content_unchanged?(existing, title, body, tags, links, status)
|
|
@@ -74,4 +80,5 @@ require_relative "vakanuvis/wiki_skeleton"
|
|
|
74
80
|
require_relative "vakanuvis/distill_gate"
|
|
75
81
|
require_relative "vakanuvis/gate"
|
|
76
82
|
require_relative "vakanuvis/harness"
|
|
83
|
+
require_relative "vakanuvis/orientation"
|
|
77
84
|
require_relative "vakanuvis/service"
|
|
@@ -21,8 +21,7 @@ module Rubik
|
|
|
21
21
|
type: "object",
|
|
22
22
|
properties: {
|
|
23
23
|
reason: { type: "string", description: "Optional explanation of why buffer is being applied (e.g. 'prior to running tests')" },
|
|
24
|
-
force: { type: "boolean",
|
|
25
|
-
description: "If true, overwrites disk files even if modified externally by formatters (resolves [CONFLICT])" }
|
|
24
|
+
force: { type: "boolean", description: "If true, overwrites disk files even if modified externally by formatters (resolves [CONFLICT])" }
|
|
26
25
|
}
|
|
27
26
|
}
|
|
28
27
|
)
|
|
@@ -42,8 +41,7 @@ module Rubik
|
|
|
42
41
|
"(buffer.model_can_apply: false). The human operator will review and apply changes.")
|
|
43
42
|
end
|
|
44
43
|
|
|
45
|
-
force = [
|
|
46
|
-
[true, "true"].include?(args["overwrite"]) || [true, "true"].include?(args[:overwrite])
|
|
44
|
+
force = [args["force"], args[:force], args["overwrite"], args[:overwrite]].intersect?([true, "true"])
|
|
47
45
|
policy = force ? "overwrite" : nil
|
|
48
46
|
|
|
49
47
|
if @buffer_manager.dirty?
|
|
@@ -66,16 +64,24 @@ module Rubik
|
|
|
66
64
|
"To force overwrite with your buffer changes, call: apply_buffer(force: true)\n" \
|
|
67
65
|
"Call read_file(path: \"#{c}\", reload: true) to inspect external changes or resolve."
|
|
68
66
|
end
|
|
67
|
+
skipped = @buffer_manager.respond_to?(:last_skipped) ? (@buffer_manager.last_skipped || []) : []
|
|
68
|
+
skipped.each do |s|
|
|
69
|
+
path = s.is_a?(Hash) ? s[:path] : s.to_s
|
|
70
|
+
reason = s.is_a?(Hash) ? s[:reason] : "invalid path"
|
|
71
|
+
messages << "[SKIPPED] #{path}: discarded as invalid path (#{reason}) and NOT written to disk."
|
|
72
|
+
end
|
|
69
73
|
|
|
70
|
-
if applied.empty?
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
74
|
+
if applied.empty?
|
|
75
|
+
if conflicts.empty? && skipped.empty?
|
|
76
|
+
# Inside the dirty? branch with zero applies and zero conflicts,
|
|
77
|
+
# every buffered entry was discarded as an invalid path
|
|
78
|
+
# (apply!'s garbage-skip) — the edits are NOT on disk, and citing
|
|
79
|
+
# an earlier apply here would be a lie.
|
|
80
|
+
"No unapplied buffer changes to write: the buffered entries were discarded as invalid paths and are NOT on disk. " \
|
|
81
|
+
"Re-check the target paths and buffer them again."
|
|
82
|
+
else
|
|
83
|
+
ToolOutput.error(messages.join("\n"))
|
|
84
|
+
end
|
|
79
85
|
else
|
|
80
86
|
messages.join("\n")
|
|
81
87
|
end
|
|
@@ -102,31 +108,18 @@ module Rubik
|
|
|
102
108
|
params_schema: {
|
|
103
109
|
type: "object",
|
|
104
110
|
properties: {
|
|
105
|
-
target: {
|
|
106
|
-
|
|
107
|
-
enum: ["lsp", "test_runner", "linter", "command"],
|
|
108
|
-
description: "Runtime component type to repair or adapt"
|
|
109
|
-
},
|
|
110
|
-
language_or_type: {
|
|
111
|
-
type: "string",
|
|
112
|
-
description: "Language or service identifier (e.g., 'ruby', 'python', 'rust', 'go', 'zig', 'c', 'bash')"
|
|
113
|
-
},
|
|
111
|
+
target: { type: "string", enum: ["lsp", "test_runner", "linter", "command"], description: "Runtime component type to repair or adapt" },
|
|
112
|
+
language_or_type: { type: "string", description: "Language or service identifier (e.g., 'ruby', 'python', 'rust', 'go', 'zig', 'c', 'bash')" },
|
|
114
113
|
proposed_command: {
|
|
115
114
|
oneOf: [{ type: "string" },
|
|
116
|
-
{ type: "object", properties: { executable: { type: "string" },
|
|
117
|
-
args: { type: "array", items: { type: "string" } } },
|
|
115
|
+
{ type: "object", properties: { executable: { type: "string" }, args: { type: "array", items: { type: "string" } } },
|
|
118
116
|
required: ["executable", "args"], additionalProperties: false },
|
|
119
117
|
{ type: "array", items: { type: "string" }, minItems: 1 }],
|
|
120
118
|
description: "Replacement command: a legacy string or, for LSP, {executable, args} / argv array. Omit to probe automatically."
|
|
121
119
|
},
|
|
122
|
-
probe_args: {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
},
|
|
126
|
-
auto_probe: {
|
|
127
|
-
type: "boolean",
|
|
128
|
-
description: "Whether to automatically probe system binaries and candidate commands (default: true)"
|
|
129
|
-
}
|
|
120
|
+
probe_args: { type: "array", items: { type: "string" },
|
|
121
|
+
description: "Explicit non-mutating probe arguments appended to proposed command; required for unknown executables" },
|
|
122
|
+
auto_probe: { type: "boolean", description: "Whether to automatically probe system binaries and candidate commands (default: true)" }
|
|
130
123
|
},
|
|
131
124
|
required: ["target", "language_or_type"]
|
|
132
125
|
}
|
|
@@ -154,25 +147,17 @@ module Rubik
|
|
|
154
147
|
else
|
|
155
148
|
proposed_cmd = raw_command.to_s.strip
|
|
156
149
|
end
|
|
157
|
-
auto_probe =
|
|
158
|
-
args[:auto_probe].nil? || args[:auto_probe]
|
|
159
|
-
else
|
|
160
|
-
args["auto_probe"]
|
|
161
|
-
end
|
|
150
|
+
auto_probe = args.fetch("auto_probe", args.fetch(:auto_probe, true))
|
|
162
151
|
|
|
163
152
|
return ToolOutput.error("Target and language_or_type must be provided.") if target.empty? || lang.empty?
|
|
164
153
|
|
|
165
154
|
# 1. Automatic Probing if proposed_cmd is not specified or auto_probe is requested
|
|
166
155
|
if !command_provided?(proposed_cmd) && auto_probe
|
|
167
156
|
candidate = case target
|
|
168
|
-
when "lsp"
|
|
169
|
-
|
|
170
|
-
when "
|
|
171
|
-
|
|
172
|
-
when "linter"
|
|
173
|
-
Rubik::Runtime::Discovery.discover_formatters(root_dir: @root_dir, lang: lang).first
|
|
174
|
-
when "command"
|
|
175
|
-
Rubik::Runtime::Discovery.binary_available?(lang) ? lang : nil
|
|
157
|
+
when "lsp" then Rubik::Runtime::Discovery.probe_lsp(lang, root_dir: @root_dir)
|
|
158
|
+
when "test_runner" then Rubik::Runtime::Discovery.discover_test_runners(root_dir: @root_dir, lang: lang).first
|
|
159
|
+
when "linter" then Rubik::Runtime::Discovery.discover_formatters(root_dir: @root_dir, lang: lang).first
|
|
160
|
+
when "command" then Rubik::Runtime::Discovery.binary_available?(lang) ? lang : nil
|
|
176
161
|
end
|
|
177
162
|
|
|
178
163
|
if candidate
|
|
@@ -232,8 +217,18 @@ module Rubik
|
|
|
232
217
|
@repair_scopes.delete([root_ctx, service_key]) if @repair_scopes[[root_ctx, service_key]].equal?(promotion_ctx)
|
|
233
218
|
unless root_ctx.disposed?
|
|
234
219
|
if previous_services.key?(service_key)
|
|
235
|
-
|
|
236
|
-
|
|
220
|
+
if target == "lsp"
|
|
221
|
+
restored = @lsp_manager&.clients&.[](lang)
|
|
222
|
+
active = restored && (!restored.respond_to?(:alive?) || restored.alive?) &&
|
|
223
|
+
(!restored.respond_to?(:running?) || restored.running?)
|
|
224
|
+
if active
|
|
225
|
+
root_ctx.provide(service_key, restored)
|
|
226
|
+
else
|
|
227
|
+
root_ctx.unprovide(service_key)
|
|
228
|
+
end
|
|
229
|
+
else
|
|
230
|
+
root_ctx.provide(service_key, previous_services[service_key])
|
|
231
|
+
end
|
|
237
232
|
else
|
|
238
233
|
root_ctx.unprovide(service_key)
|
|
239
234
|
end
|
|
@@ -24,15 +24,12 @@ module Rubik
|
|
|
24
24
|
DECLARATION = Declaration.new(
|
|
25
25
|
name: "vakanuvis", aliases: ["chronicler"],
|
|
26
26
|
description: "Ask the background chronicler (compiled knowledge in rubik://storage): " \
|
|
27
|
-
"'recall' (deterministic digest over memory+wiki with source links — ask at task start " \
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"evidence layer; memory cards are the durable conclusions you want recalled by default. " \
|
|
34
|
-
"Hygiene: same topic, same session = one note — update it (or retire it with forget; " \
|
|
35
|
-
"superseded rows keep a trace).",
|
|
27
|
+
"'recall' (deterministic digest over memory+wiki with source links — ask at task start), " \
|
|
28
|
+
"'record' (persist durable knowledge as a memory card; the ONE path for memory writes), " \
|
|
29
|
+
"'forget' (supersede/purge a mem.* card or a note), 'digest' (queue an async wiki compilation; " \
|
|
30
|
+
"returns a deterministic receipt, never blocks on the LLM), 'topics' (list recorded memory topics " \
|
|
31
|
+
"— orient before the first query), 'digest_status' (is my queued digest pending, in flight, or done?). " \
|
|
32
|
+
"Notes (annal.*) are the chronicler's evidence layer; memory cards are the durable conclusions you want recalled by default.",
|
|
36
33
|
risk: :write, archetypes: [], write_grant: false,
|
|
37
34
|
maestro_blocked: false, # storage emsali: kronikçi düzlemi kökte doğrudan
|
|
38
35
|
params_schema: {
|
|
@@ -42,9 +39,11 @@ module Rubik
|
|
|
42
39
|
type: "string", enum: ["recall", "record", "forget", "digest", "topics", "digest_status"],
|
|
43
40
|
description: "Operation (default: 'recall')."
|
|
44
41
|
},
|
|
45
|
-
query: {
|
|
46
|
-
|
|
47
|
-
|
|
42
|
+
query: {
|
|
43
|
+
type: "string",
|
|
44
|
+
description: "recall/digest: knowledge query. Recall is keyword-AND — use 2-3 core terms (every " \
|
|
45
|
+
"space-separated term must literally appear); retry with other-language synonyms when empty."
|
|
46
|
+
},
|
|
48
47
|
types: {
|
|
49
48
|
type: "array", items: { type: "string", enum: ["memory", "wiki", "note"] },
|
|
50
49
|
description: "recall: entity scopes to search (default: memory + wiki)."
|
|
@@ -52,9 +51,8 @@ module Rubik
|
|
|
52
51
|
topic: { type: "string", description: "record: card topic — mints ref mem.<topic-slug> (upsert by topic)." },
|
|
53
52
|
body: {
|
|
54
53
|
type: "string",
|
|
55
|
-
description: "record: card body (markdown; end it with a Keywords: line of 5-10 TR+EN synonyms
|
|
56
|
-
"
|
|
57
|
-
"from the body at write time)."
|
|
54
|
+
description: "record: card body (markdown; end it with a Keywords: line of 5-10 TR+EN synonyms if you " \
|
|
55
|
+
"can — a missing Keywords: line is auto-extracted from the body at write time)."
|
|
58
56
|
},
|
|
59
57
|
tags: {
|
|
60
58
|
type: "array", items: { type: "string" },
|
data/lib/rubik/ui/capability.rb
CHANGED
|
@@ -10,12 +10,14 @@ module Rubik
|
|
|
10
10
|
# Resolution order:
|
|
11
11
|
# 1. RUBIK_TEST=1 -> never (deterministic test output)
|
|
12
12
|
# 2. NO_COLOR set -> never (https://no-color.org)
|
|
13
|
-
# 3. CLICOLOR_FORCE
|
|
13
|
+
# 3. CLICOLOR_FORCE set (not 0/false) -> always (CI with color-capable logs)
|
|
14
14
|
# 4. ui.color config: always|never -> explicit override
|
|
15
15
|
# 5. output not a TTY -> never (pipe/redirect safety)
|
|
16
16
|
# 6. TERM=dumb -> never
|
|
17
17
|
# 7. Windows legacy conhost -> try enabling VT, else never
|
|
18
18
|
module Capability
|
|
19
|
+
FALSY_ENV_VALUES = ["0", "false", "no", "off"].freeze
|
|
20
|
+
|
|
19
21
|
module_function
|
|
20
22
|
|
|
21
23
|
# Strips ANSI SGR escape sequences from a string. Used on the recording
|
|
@@ -31,13 +33,16 @@ module Rubik
|
|
|
31
33
|
case resolve_mode(io, config)
|
|
32
34
|
when :always then true
|
|
33
35
|
when :never then false
|
|
34
|
-
else
|
|
36
|
+
else
|
|
37
|
+
real_io = unwrap_tee(io)
|
|
38
|
+
real_io.respond_to?(:tty?) ? real_io.tty? : false
|
|
35
39
|
end
|
|
36
40
|
end
|
|
37
41
|
|
|
38
42
|
# True when the target can animate a spinner line (real terminal only).
|
|
39
43
|
def spinner?(io, config = nil)
|
|
40
|
-
|
|
44
|
+
real_io = unwrap_tee(io)
|
|
45
|
+
color?(io, config) && real_io.respond_to?(:tty?) && real_io.tty? && interactive_env?
|
|
41
46
|
end
|
|
42
47
|
|
|
43
48
|
def interactive_env?
|
|
@@ -68,17 +73,26 @@ module Rubik
|
|
|
68
73
|
io.targets.compact.find(&:tty?)
|
|
69
74
|
end
|
|
70
75
|
|
|
76
|
+
def clicolor_force?
|
|
77
|
+
val = ENV.fetch("CLICOLOR_FORCE", nil)
|
|
78
|
+
return false if val.nil? || val.empty?
|
|
79
|
+
|
|
80
|
+
!FALSY_ENV_VALUES.include?(val.downcase)
|
|
81
|
+
end
|
|
82
|
+
|
|
71
83
|
def resolve_mode(io, config)
|
|
72
84
|
return :never if ENV["RUBIK_TEST"] == "1"
|
|
73
85
|
return :never if ENV["NO_COLOR"] && !ENV["NO_COLOR"].to_s.empty?
|
|
74
|
-
return :always if
|
|
86
|
+
return :always if clicolor_force?
|
|
75
87
|
|
|
76
88
|
cfg_mode = config&.fetch("ui.color", "auto").to_s.downcase if config
|
|
77
89
|
return :always if cfg_mode == "always"
|
|
78
90
|
return :never if cfg_mode == "never"
|
|
79
91
|
|
|
80
92
|
return :never if ENV["TERM"] && ENV["TERM"].downcase == "dumb"
|
|
81
|
-
|
|
93
|
+
|
|
94
|
+
real_io = unwrap_tee(io)
|
|
95
|
+
return :never unless real_io.respond_to?(:tty?) && real_io.tty?
|
|
82
96
|
|
|
83
97
|
enable_windows_virtual_terminal_if_needed
|
|
84
98
|
:auto
|
data/lib/rubik/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rubik
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Özgür Ulukır
|
|
@@ -721,6 +721,7 @@ files:
|
|
|
721
721
|
- lib/rubik/storage/vakanuvis/gate.rb
|
|
722
722
|
- lib/rubik/storage/vakanuvis/harness.rb
|
|
723
723
|
- lib/rubik/storage/vakanuvis/memory_steward.rb
|
|
724
|
+
- lib/rubik/storage/vakanuvis/orientation.rb
|
|
724
725
|
- lib/rubik/storage/vakanuvis/service.rb
|
|
725
726
|
- lib/rubik/storage/vakanuvis/vocabulary.rb
|
|
726
727
|
- lib/rubik/storage/vakanuvis/wiki_skeleton.rb
|