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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/ATTRIBUTION.md +1 -1
  3. data/CHANGELOG.md +118 -1
  4. data/lib/rubik/ast/cache_store.rb +69 -5
  5. data/lib/rubik/ast/extraction_result.rb +6 -0
  6. data/lib/rubik/ast/graph.rb +32 -5
  7. data/lib/rubik/ast/ruby_extractor.rb +8 -2
  8. data/lib/rubik/buffer/apply_contract.rb +6 -0
  9. data/lib/rubik/buffer/manager.rb +37 -27
  10. data/lib/rubik/config/defaults.rb +7 -1
  11. data/lib/rubik/mcp/client.rb +4 -0
  12. data/lib/rubik/mcp/manager.rb +51 -35
  13. data/lib/rubik/mcp/proxy_tool.rb +17 -0
  14. data/lib/rubik/mcp/transport/stdio.rb +142 -17
  15. data/lib/rubik/orchestrator/budget_manager.rb +2 -0
  16. data/lib/rubik/orchestrator/loop.rb +1 -0
  17. data/lib/rubik/orchestrator/shell_deps.rb +4 -0
  18. data/lib/rubik/orchestrator/turn_executor/user_channel.rb +2 -0
  19. data/lib/rubik/plugin/manager.rb +39 -15
  20. data/lib/rubik/plugin/package_inventory.rb +11 -1
  21. data/lib/rubik/plugin/storage.rb +5 -4
  22. data/lib/rubik/prompt/doctrine.rb +10 -15
  23. data/lib/rubik/prompt/tool_teachings.rb +29 -35
  24. data/lib/rubik/repair/loop.rb +29 -15
  25. data/lib/rubik/repl/commands/agent_context.rb +1 -0
  26. data/lib/rubik/repl/commands/system.rb +2 -0
  27. data/lib/rubik/repl/turn_recorder.rb +3 -0
  28. data/lib/rubik/security/redactor.rb +13 -9
  29. data/lib/rubik/storage/vakanuvis/gate.rb +13 -8
  30. data/lib/rubik/storage/vakanuvis/harness.rb +115 -112
  31. data/lib/rubik/storage/vakanuvis/orientation.rb +277 -0
  32. data/lib/rubik/storage/vakanuvis/service.rb +5 -1
  33. data/lib/rubik/storage/vakanuvis/vocabulary.rb +33 -2
  34. data/lib/rubik/storage/vakanuvis.rb +8 -1
  35. data/lib/rubik/tools/builtins/lifecycle.rb +42 -47
  36. data/lib/rubik/tools/builtins/vakanuvis.rb +13 -15
  37. data/lib/rubik/ui/capability.rb +19 -5
  38. data/lib/rubik/version.rb +1 -1
  39. 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 { harness.shutdown }
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
- s = text.to_s.downcase.gsub(/[^a-z0-9]+/, "-")
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
- s = s[0, SLUG_MAX].to_s
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
- Rubik::Log.warn(SUBSYSTEM, message)
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 = [true, "true"].include?(args["force"]) || [true, "true"].include?(args[: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? && !conflicts.empty?
71
- ToolOutput.error(messages.join("\n"))
72
- elsif applied.empty?
73
- # Inside the dirty? branch with zero applies and zero conflicts,
74
- # every buffered entry was discarded as an invalid path
75
- # (apply!'s garbage-skip) — the edits are NOT on disk, and citing
76
- # an earlier apply here would be a lie.
77
- "No unapplied buffer changes to write: the buffered entries were discarded as invalid paths and are NOT on disk. " \
78
- "Re-check the target paths and buffer them again."
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
- type: "string",
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
- type: "array", items: { type: "string" },
124
- description: "Explicit non-mutating functional probe arguments appended to the exact proposed command; required for unknown executables"
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 = if args["auto_probe"].nil?
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
- Rubik::Runtime::Discovery.probe_lsp(lang, root_dir: @root_dir)
170
- when "test_runner"
171
- Rubik::Runtime::Discovery.discover_test_runners(root_dir: @root_dir, lang: lang).first
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
- restored = target == "lsp" ? @lsp_manager&.clients&.[](lang) : nil
236
- root_ctx.provide(service_key, restored || previous_services[service_key])
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
- "and after compaction), 'record' (persist durable knowledge as a memory card; the ONE " \
29
- "path for memory writes), 'forget' (supersede/purge a mem.* card or a note), 'digest' (queue an " \
30
- "async wiki compilation; returns a deterministic receipt, never blocks on the LLM), " \
31
- "'topics' (list recorded memory topics — orient before the first query), 'digest_status' " \
32
- "(is my queued digest pending, in flight, or done?). Notes (annal.*) are the chronicler's " \
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: { type: "string",
46
- description: "recall/digest: knowledge query. Recall is keyword-AND — every space-separated " \
47
- "term must literally appear; retry with other-language synonyms when empty." },
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 so " \
56
- "keyword-AND recall can find the card — a missing Keywords: line is auto-extracted " \
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" },
@@ -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=1 -> always (CI with color-capable logs)
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 io.respond_to?(:tty?) ? io.tty? : false
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
- color?(io, config) && io.respond_to?(:tty?) && io.tty? && interactive_env?
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 ENV["CLICOLOR_FORCE"] && ENV["CLICOLOR_FORCE"].to_s == "1"
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
- return :never unless io.respond_to?(:tty?) && io.tty?
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rubik
4
- VERSION = "0.4.1"
4
+ VERSION = "0.5.0"
5
5
  end
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.1
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