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.
- checksums.yaml +4 -4
- data/ATTRIBUTION.md +1 -1
- data/CHANGELOG.md +102 -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 +99 -69
- data/lib/rubik/config/defaults.rb +17 -20
- data/lib/rubik/config.rb +0 -6
- data/lib/rubik/context/git_lister.rb +3 -1
- data/lib/rubik/llm/anthropic.rb +5 -2
- data/lib/rubik/llm/client.rb +102 -72
- 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/goal_ledger/patch.rb +48 -30
- data/lib/rubik/orchestrator/lsp_feedback.rb +20 -4
- data/lib/rubik/orchestrator/subagent/reuse.rb +2 -2
- data/lib/rubik/orchestrator/subagent/spawn_transaction.rb +60 -31
- data/lib/rubik/platform/process.rb +6 -6
- data/lib/rubik/platform/sanitize.rb +37 -16
- data/lib/rubik/prompt/doctrine.rb +4 -0
- data/lib/rubik/repl/commands/agent_context.rb +126 -104
- data/lib/rubik/repl/commands/agent_execution.rb +183 -155
- data/lib/rubik/repl/commands/agent_models.rb +112 -87
- data/lib/rubik/repl/commands/agent_providers.rb +190 -156
- data/lib/rubik/repl/commands/buffer.rb +122 -88
- data/lib/rubik/repl/commands/lsp.rb +212 -179
- 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 +266 -246
- data/lib/rubik/repl/shell.rb +10 -1
- data/lib/rubik/runtime/discovery.rb +12 -6
- data/lib/rubik/runtime/overlay_storage.rb +59 -2
- data/lib/rubik/sandbox/driver/docker.rb +2 -2
- data/lib/rubik/session/replayer.rb +1 -2
- data/lib/rubik/storage/store.rb +3 -2
- 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/tools/fuzzy_matcher.rb +27 -11
- data/lib/rubik/version.rb +1 -1
- metadata +2 -1
|
@@ -48,7 +48,10 @@ module Rubik
|
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
def manifest_path(dir)
|
|
51
|
-
|
|
51
|
+
ensure_valid_overlay_dir!(dir)
|
|
52
|
+
path = File.join(dir, ".manifest.json")
|
|
53
|
+
ensure_contained_path!(path, dir)
|
|
54
|
+
path
|
|
52
55
|
end
|
|
53
56
|
|
|
54
57
|
def load_manifest(dir)
|
|
@@ -56,7 +59,7 @@ module Rubik
|
|
|
56
59
|
return {} unless File.exist?(path)
|
|
57
60
|
|
|
58
61
|
JSON.parse(File.read(path))
|
|
59
|
-
rescue JSON::ParserError
|
|
62
|
+
rescue JSON::ParserError, ArgumentError
|
|
60
63
|
{}
|
|
61
64
|
end
|
|
62
65
|
|
|
@@ -73,6 +76,7 @@ module Rubik
|
|
|
73
76
|
end
|
|
74
77
|
|
|
75
78
|
def record_hash(dir, filename, scope)
|
|
79
|
+
ensure_valid_overlay_dir!(dir)
|
|
76
80
|
if manifest_state(dir) == :corrupt
|
|
77
81
|
# Bozuk manifesti sessizce "heal" etmek (yalnızca yeni kaydı yazmak) diğer
|
|
78
82
|
# kayıtlı overlay'leri tahliye eder; bunun yerine kaydı reddet.
|
|
@@ -80,6 +84,7 @@ module Rubik
|
|
|
80
84
|
end
|
|
81
85
|
|
|
82
86
|
path = File.join(dir, filename)
|
|
87
|
+
ensure_contained_path!(path, dir)
|
|
83
88
|
hash = Digest::SHA256.hexdigest(normalize_content(File.binread(path)))
|
|
84
89
|
manifest = load_manifest(dir)
|
|
85
90
|
manifest[filename] = {
|
|
@@ -93,6 +98,11 @@ module Rubik
|
|
|
93
98
|
def verify_integrity(file_path, dir, bootstrap_eligible: false)
|
|
94
99
|
return :trusted if trust_mode?
|
|
95
100
|
|
|
101
|
+
unless safe_path_under?(file_path, dir)
|
|
102
|
+
warn "[SECURITY] Path traversal attempt detected for overlay '#{file_path}'. Skipping."
|
|
103
|
+
return :rejected
|
|
104
|
+
end
|
|
105
|
+
|
|
96
106
|
basename = File.basename(file_path)
|
|
97
107
|
manifest = load_manifest(dir)
|
|
98
108
|
current_content = File.exist?(file_path) ? File.binread(file_path) : ""
|
|
@@ -140,6 +150,8 @@ module Rubik
|
|
|
140
150
|
|
|
141
151
|
filename = "#{clean_name.gsub(':', '_')}.rb"
|
|
142
152
|
file_path = File.join(target_dir, filename)
|
|
153
|
+
ensure_contained_path!(file_path, target_dir)
|
|
154
|
+
|
|
143
155
|
header = <<~RUBY
|
|
144
156
|
# frozen_string_literal: true
|
|
145
157
|
#{COMMAND_SENTINEL} !#{clean_name}
|
|
@@ -164,6 +176,7 @@ module Rubik
|
|
|
164
176
|
target_dir = scope.to_sym == :global ? global_commands_dir : project_commands_dir
|
|
165
177
|
filename = "#{clean_name.gsub(':', '_')}.rb"
|
|
166
178
|
file_path = File.join(target_dir, filename)
|
|
179
|
+
ensure_contained_path!(file_path, target_dir)
|
|
167
180
|
|
|
168
181
|
raise "cannot delete '#{filename}': .manifest.json in #{target_dir} is corrupt; delete it and re-record overlays" if manifest_state(target_dir) == :corrupt
|
|
169
182
|
|
|
@@ -190,8 +203,10 @@ module Rubik
|
|
|
190
203
|
|
|
191
204
|
file_path = cmd_meta[:path]
|
|
192
205
|
cmd_name = cmd_meta[:name]
|
|
206
|
+
target_dir = cmd_meta[:scope].to_sym == :global ? global_commands_dir : project_commands_dir
|
|
193
207
|
|
|
194
208
|
begin
|
|
209
|
+
ensure_contained_path!(file_path, target_dir)
|
|
195
210
|
code = File.read(file_path)
|
|
196
211
|
eval_command(shell, cmd_name, code, cmd_meta[:scope])
|
|
197
212
|
loaded << cmd_name
|
|
@@ -249,6 +264,8 @@ module Rubik
|
|
|
249
264
|
|
|
250
265
|
filename = "#{clean_name}.rb"
|
|
251
266
|
file_path = File.join(target_dir, filename)
|
|
267
|
+
ensure_contained_path!(file_path, target_dir)
|
|
268
|
+
|
|
252
269
|
schema_json = params_schema ? JSON.dump(params_schema) : "{}"
|
|
253
270
|
header = <<~RUBY
|
|
254
271
|
# frozen_string_literal: true
|
|
@@ -278,6 +295,7 @@ module Rubik
|
|
|
278
295
|
target_dir = scope.to_sym == :global ? global_tools_dir : project_tools_dir
|
|
279
296
|
filename = "#{clean_name}.rb"
|
|
280
297
|
file_path = File.join(target_dir, filename)
|
|
298
|
+
ensure_contained_path!(file_path, target_dir)
|
|
281
299
|
|
|
282
300
|
raise "cannot delete '#{filename}': .manifest.json in #{target_dir} is corrupt; delete it and re-record overlays" if manifest_state(target_dir) == :corrupt
|
|
283
301
|
|
|
@@ -301,6 +319,10 @@ module Rubik
|
|
|
301
319
|
list_tools.each do |t_meta|
|
|
302
320
|
next if t_meta[:integrity] == :rejected
|
|
303
321
|
|
|
322
|
+
file_path = t_meta[:path]
|
|
323
|
+
target_dir = t_meta[:scope].to_sym == :global ? global_tools_dir : project_tools_dir
|
|
324
|
+
ensure_contained_path!(file_path, target_dir)
|
|
325
|
+
|
|
304
326
|
code = File.read(t_meta[:path])
|
|
305
327
|
tool_inst = eval_tool(t_meta[:name], code, t_meta[:scope])
|
|
306
328
|
if tool_inst
|
|
@@ -357,6 +379,41 @@ module Rubik
|
|
|
357
379
|
|
|
358
380
|
private
|
|
359
381
|
|
|
382
|
+
def ensure_valid_overlay_dir!(dir)
|
|
383
|
+
allowed = [
|
|
384
|
+
[project_commands_dir, @root_dir],
|
|
385
|
+
[global_commands_dir, @home_dir],
|
|
386
|
+
[project_tools_dir, @root_dir],
|
|
387
|
+
[global_tools_dir, @home_dir]
|
|
388
|
+
]
|
|
389
|
+
valid = allowed.any? do |allowed_dir, trusted_root|
|
|
390
|
+
Platform.path_equal?(dir, allowed_dir) && Platform.resolved_path_under?(dir, trusted_root)
|
|
391
|
+
end
|
|
392
|
+
return if valid
|
|
393
|
+
|
|
394
|
+
raise ArgumentError, "Invalid overlay directory: #{dir}"
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def ensure_contained_path!(path, parent_dir)
|
|
398
|
+
raise ArgumentError, "Path traversal attempt detected: #{path}" unless safe_path_under?(path, parent_dir)
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
def safe_path_under?(path, parent_dir)
|
|
402
|
+
return false if path.nil? || parent_dir.nil?
|
|
403
|
+
|
|
404
|
+
ensure_valid_overlay_dir!(parent_dir)
|
|
405
|
+
|
|
406
|
+
# Lexical containment check first
|
|
407
|
+
return false unless Platform.path_under?(path, parent_dir)
|
|
408
|
+
|
|
409
|
+
# If path exists or parent exists, also check resolved path containment
|
|
410
|
+
return Platform.resolved_path_under?(path, parent_dir) if File.exist?(path) || File.exist?(parent_dir)
|
|
411
|
+
|
|
412
|
+
true
|
|
413
|
+
rescue StandardError
|
|
414
|
+
false
|
|
415
|
+
end
|
|
416
|
+
|
|
360
417
|
# Bootstrap kabul taramasında kullanılacak guard: kwarg'la geçilen örnek
|
|
361
418
|
# (üretimde service catalog'un tek operatör-policy guard'ı) ya da lazy
|
|
362
419
|
# default. Lazy çözüm bilinçli: overlay_storage.rb standalone require
|
|
@@ -14,7 +14,7 @@ module Rubik
|
|
|
14
14
|
DEFAULT_IMAGE = "rubik-sandbox:latest"
|
|
15
15
|
|
|
16
16
|
def available?
|
|
17
|
-
!Rubik::Platform.which("docker").nil? && system("docker ps
|
|
17
|
+
!Rubik::Platform.which("docker").nil? && system("docker", "ps", out: File::NULL, err: File::NULL)
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def run(cmd, timeout: 15, chdir: Dir.pwd, allow_network: false, memory_limit_mb: 512, cpus: "1.0")
|
|
@@ -48,7 +48,7 @@ module Rubik
|
|
|
48
48
|
error_message: "Docker sandbox error"
|
|
49
49
|
)
|
|
50
50
|
ensure
|
|
51
|
-
system("docker rm -f
|
|
51
|
+
system("docker", "rm", "-f", container_name, out: File::NULL, err: File::NULL)
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
{
|
|
@@ -18,9 +18,8 @@ module Rubik
|
|
|
18
18
|
def replay_file(session_file_path)
|
|
19
19
|
return false unless File.exist?(session_file_path)
|
|
20
20
|
|
|
21
|
-
lines = File.readlines(session_file_path)
|
|
22
21
|
@shell&.instance_variable_set(:@replaying, true)
|
|
23
|
-
|
|
22
|
+
File.foreach(session_file_path) do |line|
|
|
24
23
|
next if line.strip.empty?
|
|
25
24
|
|
|
26
25
|
event = begin
|
data/lib/rubik/storage/store.rb
CHANGED
|
@@ -499,10 +499,11 @@ module Rubik
|
|
|
499
499
|
# (elle düzenleme: "42", ["x"]) tüm tipi zehirlemesin.
|
|
500
500
|
skipped = 0
|
|
501
501
|
records = File.foreach(path, encoding: "UTF-8").filter_map do |line|
|
|
502
|
-
|
|
502
|
+
line.strip!
|
|
503
|
+
next if line.empty?
|
|
503
504
|
|
|
504
505
|
parsed = begin
|
|
505
|
-
JSON.parse(line.scrub)
|
|
506
|
+
JSON.parse(line.valid_encoding? ? line : line.scrub)
|
|
506
507
|
rescue JSON::ParserError
|
|
507
508
|
nil
|
|
508
509
|
end
|
|
@@ -238,6 +238,10 @@ module Rubik
|
|
|
238
238
|
# talebi olarak sunar. Tümü geçerse sayfanın eski gap notu
|
|
239
239
|
# supersede edilir (öz-onarım kapanışı). Düzlem 1 sayfa metnini
|
|
240
240
|
# ASLA düzenlemez — düzeltme daima bir sonraki derlemeye aittir.
|
|
241
|
+
def active_recall_hits(store, **)
|
|
242
|
+
store.search_ranked(**).reject { |record| record["status"] == Vocabulary::STATUS_SUPERSEDED }
|
|
243
|
+
end
|
|
244
|
+
|
|
241
245
|
def run_recall_smoke(gate, proposal, page_id)
|
|
242
246
|
checks = proposal.fetch("checks", [])
|
|
243
247
|
return if checks.empty?
|
|
@@ -247,8 +251,8 @@ module Rubik
|
|
|
247
251
|
# koşulmalı — ilk RECALL_LIMIT kayıt. 6. sıradaki sayfa maestro'ya
|
|
248
252
|
# görünmüyorsa iska sayılmalı (öz-onarım döngüsü kör kalmasın).
|
|
249
253
|
missed = checks.reject do |check|
|
|
250
|
-
gate.store
|
|
251
|
-
|
|
254
|
+
active_recall_hits(gate.store, query: check, type: Harness::RECALL_TYPES)
|
|
255
|
+
.first(Harness::RECALL_LIMIT).any? { |r| r["id"] == page_id }
|
|
252
256
|
end
|
|
253
257
|
if missed.empty?
|
|
254
258
|
resolved = gate.store.get(type: "note", id: gap_ref)
|
|
@@ -21,10 +21,15 @@ module Rubik
|
|
|
21
21
|
# supervisor, event_bus ve sözlü girdiler dışarıdan verilir —
|
|
22
22
|
# Harness'e ASLA geri dokunmaz.
|
|
23
23
|
class Gate
|
|
24
|
+
DEFAULT_MAX_DIGEST_JOBS = 100
|
|
24
25
|
attr_reader :store, :supervisor, :dmesg, :session_slug, :max_steps, :model
|
|
25
26
|
|
|
26
27
|
def initialize(store:, max_steps:, session_slug:, supervisor: nil, dmesg: nil,
|
|
27
|
-
event_bus: nil, enabled: true, model: nil)
|
|
28
|
+
event_bus: nil, enabled: true, model: nil, max_digest_jobs: DEFAULT_MAX_DIGEST_JOBS)
|
|
29
|
+
raise ArgumentError, "max_digest_jobs must be a positive integer" unless max_digest_jobs.is_a?(Integer) && max_digest_jobs.positive?
|
|
30
|
+
|
|
31
|
+
@max_digest_jobs = max_digest_jobs
|
|
32
|
+
@digest_targets = {} # accepted targets retain their slot through harvest/retry
|
|
28
33
|
@store = store
|
|
29
34
|
@supervisor = supervisor
|
|
30
35
|
@dmesg = dmesg
|
|
@@ -37,7 +42,7 @@ module Rubik
|
|
|
37
42
|
@distillers = {}
|
|
38
43
|
@lock = Mutex.new
|
|
39
44
|
# v2 §4-V5: kirli işareti — son başarılı derlemeden bu yana KALICI
|
|
40
|
-
# içerik (memory+wiki+note
|
|
45
|
+
# içerik (memory+wiki+note içerik ayak izi) değişmediyse toplu
|
|
41
46
|
# damıtıcı doğmaz. Turn satırları ayak izini değiştirmez: saf
|
|
42
47
|
# turn akışında derleme boşa yakılmaz.
|
|
43
48
|
@compile_footprint = nil
|
|
@@ -54,19 +59,22 @@ module Rubik
|
|
|
54
59
|
target = Vocabulary.wiki_ref(query)
|
|
55
60
|
# v2 §4-V5: aynı hedefli iş zaten kuyrukta ya da uçuştaysa makbuz
|
|
56
61
|
# 'already queued' varyantına düşer — çift doğum yok.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
busy
|
|
62
|
+
admission = @lock.synchronize do
|
|
63
|
+
next :duplicate if @digest_targets.key?(target)
|
|
64
|
+
next :full if @digest_targets.size >= @max_digest_jobs
|
|
65
|
+
|
|
66
|
+
@pending_digests << { query: query, target: target, transcript_uris: source_transcript_uris }
|
|
67
|
+
@digest_targets[target] = true
|
|
68
|
+
:accepted
|
|
65
69
|
end
|
|
66
|
-
if
|
|
70
|
+
if admission == :duplicate
|
|
67
71
|
return "Digest already queued for '#{target}' (#{AnnalWriter.one_line(query, 60)}): the chronicler is " \
|
|
68
72
|
"compiling it — recall it later with vakanuvis(action: 'recall', query: '...')."
|
|
69
73
|
end
|
|
74
|
+
if admission == :full
|
|
75
|
+
return "Digest not accepted for '#{target}': capacity #{@max_digest_jobs} is full. " \
|
|
76
|
+
"Existing jobs are preserved; retry after a digest finishes."
|
|
77
|
+
end
|
|
70
78
|
|
|
71
79
|
spawned = spawn_job!(batch: false)
|
|
72
80
|
DistillGate.receipt(query: query, target: target, spawned: !spawned.nil?)
|
|
@@ -122,6 +130,7 @@ module Rubik
|
|
|
122
130
|
# gerçek değeri yaz (future harvest sembolleri sessiz kalmasın).
|
|
123
131
|
Vakanuvis.log_via(@dmesg, :warn, "distiller ended with unexpected outcome=#{outcome.inspect} — dropped, store untouched")
|
|
124
132
|
end
|
|
133
|
+
@lock.synchronize { @digest_targets.delete(job[:target]) } unless [:incomplete, :crashed].include?(outcome)
|
|
125
134
|
outcome
|
|
126
135
|
end
|
|
127
136
|
|
|
@@ -213,7 +222,7 @@ module Rubik
|
|
|
213
222
|
end
|
|
214
223
|
|
|
215
224
|
def restore_job(job)
|
|
216
|
-
@pending_digests.
|
|
225
|
+
@pending_digests.push(job) if job[:target]
|
|
217
226
|
end
|
|
218
227
|
|
|
219
228
|
def confirm(inst_id, job)
|
|
@@ -224,11 +233,23 @@ module Rubik
|
|
|
224
233
|
@lock.synchronize { @compile_footprint.nil? || @compile_footprint != footprint }
|
|
225
234
|
end
|
|
226
235
|
|
|
227
|
-
# Kalıcı içerik ayak izi:
|
|
228
|
-
#
|
|
229
|
-
#
|
|
236
|
+
# Kalıcı içerik ayak izi: sabit id + içerik hash'i + durum.
|
|
237
|
+
# Annal'ın yalnız turn satırları ayıklanır; diğer not değişiklikleri
|
|
238
|
+
# ve mevcut kart/sayfa güncellemeleri derleyiciyi yeniden kirletir.
|
|
230
239
|
def footprint
|
|
231
|
-
["memory", "wiki", "note"].
|
|
240
|
+
["memory", "wiki", "note"].flat_map do |type|
|
|
241
|
+
@store.list(type: type).sort_by(&:id).map do |summary|
|
|
242
|
+
record = @store.get(type: type, id: summary.id)
|
|
243
|
+
next [type, summary.id, nil] unless record
|
|
244
|
+
|
|
245
|
+
hash = record["hash"]
|
|
246
|
+
if type == "note" && (Vocabulary::ANNAL_TAGS - Array(record["tags"])).empty?
|
|
247
|
+
body = record["body"].to_s.lines.grep_v(/\A- \[\d+\] turn .*\(event=turn\.\d+\.[^)]*\)\s*\z/).join.rstrip
|
|
248
|
+
hash = Digest::SHA256.hexdigest([record["title"], body, record["tags"], record["links"]].to_json)
|
|
249
|
+
end
|
|
250
|
+
[type, summary.id, hash, record["status"]]
|
|
251
|
+
end
|
|
252
|
+
end
|
|
232
253
|
end
|
|
233
254
|
|
|
234
255
|
# Kaynak disiplini (v2 §4-V4): yalnız status=completed transcript
|
|
@@ -63,7 +63,9 @@ module Rubik
|
|
|
63
63
|
# (kurucu enjeksiyonu — gate Harness'e geri dokunmaz).
|
|
64
64
|
@gate = DistillGate::Gate.new(store: @store, supervisor: @supervisor, dmesg: @dmesg,
|
|
65
65
|
event_bus: @event_bus, enabled: @distiller_enabled,
|
|
66
|
-
max_steps: @max_steps, model: @model, session_slug: @session_slug
|
|
66
|
+
max_steps: @max_steps, model: @model, session_slug: @session_slug,
|
|
67
|
+
max_digest_jobs: fetch_cfg("subagents.vakanuvis.max_digest_jobs",
|
|
68
|
+
DistillGate::Gate::DEFAULT_MAX_DIGEST_JOBS))
|
|
67
69
|
end
|
|
68
70
|
|
|
69
71
|
class << self
|
|
@@ -90,9 +92,13 @@ module Rubik
|
|
|
90
92
|
# 2 saniyede durmazsa (yavaş bir flush/spawn içinde) kill gelir ve
|
|
91
93
|
# işlenemeyen olay sayısı dmesg'e düşer — sessiz kayıp yok.
|
|
92
94
|
def shutdown
|
|
93
|
-
@monitor.synchronize
|
|
94
|
-
|
|
95
|
-
|
|
95
|
+
@monitor.synchronize do
|
|
96
|
+
return if @stopped
|
|
97
|
+
|
|
98
|
+
@stopped = true
|
|
99
|
+
@busy += 1 # STOP işaretinin de işleniş sayılır
|
|
100
|
+
@queue.push(STOP)
|
|
101
|
+
end
|
|
96
102
|
@worker&.join(2)
|
|
97
103
|
if @worker&.alive?
|
|
98
104
|
Vakanuvis.log_via(@dmesg, :warn, "worker did not stop in 2s — killing with #{queue_size} event(s) unprocessed")
|
|
@@ -114,17 +120,19 @@ module Rubik
|
|
|
114
120
|
# ---- olay giriş kapıları (kworker thread'inde çağrılır) -----------
|
|
115
121
|
|
|
116
122
|
def enqueue_event(kind, payload)
|
|
117
|
-
return false if stopped?
|
|
118
|
-
|
|
119
123
|
unless payload.is_a?(Hash)
|
|
120
124
|
Vakanuvis.log_via(@dmesg, :warn, "discarded non-hash #{kind} event (#{payload.class})")
|
|
121
125
|
return false
|
|
122
126
|
end
|
|
123
127
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
+
@monitor.synchronize do
|
|
129
|
+
return false if @stopped
|
|
130
|
+
|
|
131
|
+
drop_oldest! if @queue.size >= @max_queue
|
|
132
|
+
@busy += 1
|
|
133
|
+
@queue.push([kind, payload])
|
|
134
|
+
true
|
|
135
|
+
end
|
|
128
136
|
end
|
|
129
137
|
|
|
130
138
|
def queue_size = @queue.size
|
|
@@ -142,7 +150,7 @@ module Rubik
|
|
|
142
150
|
|
|
143
151
|
allowed = Array(types).map(&:to_s)
|
|
144
152
|
allowed = RECALL_TYPES if allowed.empty?
|
|
145
|
-
hits = @store
|
|
153
|
+
hits = DistillGate.active_recall_hits(@store, query: q, type: allowed).first(RECALL_LIMIT)
|
|
146
154
|
unless hits.empty?
|
|
147
155
|
lines = hits.map { |r| recall_line(r) }
|
|
148
156
|
header = "Recalled #{hits.size} entit#{hits.size == 1 ? 'y' : 'ies'} for '#{AnnalWriter.one_line(q, 80)}':"
|
|
@@ -186,8 +194,8 @@ module Rubik
|
|
|
186
194
|
"recall shows compiled pages; a submitted digest appears here until it compiles."
|
|
187
195
|
end
|
|
188
196
|
|
|
189
|
-
"Digest queue: #{targets[:queued].size} queued (#{targets[:queued]
|
|
190
|
-
"#{targets[:in_flight].size} in flight (#{targets[:in_flight]
|
|
197
|
+
"Digest queue: #{targets[:queued].size} queued (#{digest_target_summary(targets[:queued])}), " \
|
|
198
|
+
"#{targets[:in_flight].size} in flight (#{digest_target_summary(targets[:in_flight])})."
|
|
191
199
|
end
|
|
192
200
|
|
|
193
201
|
# Soğuk-başlangıç yönelimi (issue #99 P2): ilk sorgu sözcük dağı
|
|
@@ -214,6 +222,12 @@ module Rubik
|
|
|
214
222
|
|
|
215
223
|
private
|
|
216
224
|
|
|
225
|
+
def digest_target_summary(targets)
|
|
226
|
+
shown = targets.first(10)
|
|
227
|
+
shown << "+#{targets.size - 10} targets" if targets.size > 10
|
|
228
|
+
shown.join(", ")
|
|
229
|
+
end
|
|
230
|
+
|
|
217
231
|
# v2 §4-V2 + issue #99 P2: AND boşluğu tek tip körlük değil — en
|
|
218
232
|
# yakın kısmi eşleşmeler dürüstçe "(fallback)" damgasıyla sunulur
|
|
219
233
|
# (gerçek eşleşme değildir), transcript izleri kapsam ipucu olarak
|
|
@@ -226,9 +240,8 @@ module Rubik
|
|
|
226
240
|
# Süpersede edilmiş sayılar "en yakın kısmi eşleşme" olarak
|
|
227
241
|
# sunulmaz (review M1) — emekli bir sayfayı okumaya davet etmek,
|
|
228
242
|
# issue #100'ün "iki kopya, biri bozuk" bulgusunun aynısıdır.
|
|
229
|
-
relaxed = @store
|
|
230
|
-
|
|
231
|
-
.first(RECALL_FALLBACK_LIMIT)
|
|
243
|
+
relaxed = DistillGate.active_recall_hits(@store, query: q, type: allowed, require_all: false)
|
|
244
|
+
.first(RECALL_FALLBACK_LIMIT)
|
|
232
245
|
transcripts = @store.search(query: q, type: "transcript")
|
|
233
246
|
if relaxed.empty? && transcripts.empty?
|
|
234
247
|
return "No chronicled knowledge matches '#{AnnalWriter.one_line(q, 80)}'. Recall is keyword-AND: retry with " \
|
|
@@ -319,13 +332,15 @@ module Rubik
|
|
|
319
332
|
end
|
|
320
333
|
|
|
321
334
|
def handle_subagent(payload)
|
|
322
|
-
# Önce kronik
|
|
323
|
-
#
|
|
324
|
-
# katman LLM katmanının hatasından zarar görmez.
|
|
335
|
+
# Önce kronik, sonra hasat; iki düzlemin hataları birbirini
|
|
336
|
+
# engellemez. Annal yazılamasa da digest tamamlanır/geri kuyruğa alınır.
|
|
325
337
|
seq = next_seq
|
|
326
338
|
line = AnnalWriter.subagent_line(seq, payload)
|
|
327
|
-
|
|
328
|
-
|
|
339
|
+
begin
|
|
340
|
+
append_session([[event_key(payload), line]])
|
|
341
|
+
ensure
|
|
342
|
+
@gate.completion_event(payload)
|
|
343
|
+
end
|
|
329
344
|
maintain if flush_due?
|
|
330
345
|
end
|
|
331
346
|
|
|
@@ -136,10 +136,12 @@ module Rubik
|
|
|
136
136
|
# `storage put type=memory` ile açılmış) satırları disipline
|
|
137
137
|
# sokar — başlıktan slug türet, en yeni satıra mem.<slug> ref'i
|
|
138
138
|
# bağla (bir sonraki record() artık O satırı günceller), aynı
|
|
139
|
-
# slug'a düşen eski kopyaları supersede et.
|
|
139
|
+
# slug'a düşen eski kopyaları supersede et. Mevcut canonical ref
|
|
140
|
+
# her zaman korunur; kaçak kopyalar onun lehine emekli edilir. Deterministik: sıra
|
|
140
141
|
# ve seçim yalnızca [row] üzerinden.
|
|
141
142
|
def sweep_rogue_memory(store)
|
|
142
|
-
|
|
143
|
+
bindings = store.refs.select { |ref, _id| Vocabulary.memory_ref?(ref) }
|
|
144
|
+
bound_ids = bindings.values
|
|
143
145
|
rows = store.list(type: "memory").reject do |s|
|
|
144
146
|
bound_ids.include?(s.id) || s.status == Vocabulary::STATUS_SUPERSEDED
|
|
145
147
|
end
|
|
@@ -147,11 +149,15 @@ module Rubik
|
|
|
147
149
|
|
|
148
150
|
swept = 0
|
|
149
151
|
rows.group_by { |s| Vocabulary.slug(s.title) }.each do |slug, group|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
152
|
+
ref = Vocabulary.memory_ref(slug)
|
|
153
|
+
winner_id = bindings[ref]
|
|
154
|
+
unless winner_id
|
|
155
|
+
winner_id = group.max_by(&:row).id
|
|
156
|
+
store.set_ref(ref, winner_id)
|
|
157
|
+
end
|
|
158
|
+
group.reject { |s| s.id == winner_id }.each do |s|
|
|
153
159
|
rogue = store.get(type: "memory", id: s.id)
|
|
154
|
-
Vakanuvis.supersede_record(store, rogue, by:
|
|
160
|
+
Vakanuvis.supersede_record(store, rogue, by: winner_id) if rogue
|
|
155
161
|
end
|
|
156
162
|
swept += group.size
|
|
157
163
|
end
|
|
@@ -112,8 +112,12 @@ module Rubik
|
|
|
112
112
|
description: "Language or service identifier (e.g., 'ruby', 'python', 'rust', 'go', 'zig', 'c', 'bash')"
|
|
113
113
|
},
|
|
114
114
|
proposed_command: {
|
|
115
|
-
type: "string",
|
|
116
|
-
|
|
115
|
+
oneOf: [{ type: "string" },
|
|
116
|
+
{ type: "object", properties: { executable: { type: "string" },
|
|
117
|
+
args: { type: "array", items: { type: "string" } } },
|
|
118
|
+
required: ["executable", "args"], additionalProperties: false },
|
|
119
|
+
{ type: "array", items: { type: "string" }, minItems: 1 }],
|
|
120
|
+
description: "Replacement command: a legacy string or, for LSP, {executable, args} / argv array. Omit to probe automatically."
|
|
117
121
|
},
|
|
118
122
|
probe_args: {
|
|
119
123
|
type: "array", items: { type: "string" },
|
|
@@ -142,7 +146,14 @@ module Rubik
|
|
|
142
146
|
def execute(args = {})
|
|
143
147
|
target = (args["target"] || args[:target]).to_s.downcase.strip
|
|
144
148
|
lang = (args["language_or_type"] || args[:language_or_type]).to_s.downcase.strip
|
|
145
|
-
|
|
149
|
+
raw_command = args["proposed_command"] || args[:proposed_command]
|
|
150
|
+
if raw_command.is_a?(Hash) || raw_command.is_a?(Array)
|
|
151
|
+
return ToolOutput.error("Structured command argv is supported only for LSP repairs.") unless target == "lsp"
|
|
152
|
+
|
|
153
|
+
proposed_cmd = raw_command
|
|
154
|
+
else
|
|
155
|
+
proposed_cmd = raw_command.to_s.strip
|
|
156
|
+
end
|
|
146
157
|
auto_probe = if args["auto_probe"].nil?
|
|
147
158
|
args[:auto_probe].nil? || args[:auto_probe]
|
|
148
159
|
else
|
|
@@ -152,7 +163,7 @@ module Rubik
|
|
|
152
163
|
return ToolOutput.error("Target and language_or_type must be provided.") if target.empty? || lang.empty?
|
|
153
164
|
|
|
154
165
|
# 1. Automatic Probing if proposed_cmd is not specified or auto_probe is requested
|
|
155
|
-
if proposed_cmd
|
|
166
|
+
if !command_provided?(proposed_cmd) && auto_probe
|
|
156
167
|
candidate = case target
|
|
157
168
|
when "lsp"
|
|
158
169
|
Rubik::Runtime::Discovery.probe_lsp(lang, root_dir: @root_dir)
|
|
@@ -179,7 +190,7 @@ module Rubik
|
|
|
179
190
|
end
|
|
180
191
|
end
|
|
181
192
|
|
|
182
|
-
return ToolOutput.error("No command provided or discovered for #{target} (#{lang}).")
|
|
193
|
+
return ToolOutput.error("No command provided or discovered for #{target} (#{lang}).") unless command_provided?(proposed_cmd)
|
|
183
194
|
|
|
184
195
|
# 2. Pre-flight Verification in Isolated Sub-Context (ctx.extend())
|
|
185
196
|
root_ctx = @runtime_manager&.context || Rubik::Runtime::Context.new
|
|
@@ -313,11 +324,11 @@ module Rubik
|
|
|
313
324
|
end
|
|
314
325
|
|
|
315
326
|
if @config.nil? && ["lsp", "test_runner", "linter"].include?(target)
|
|
316
|
-
return "⚠️ Promoted '#{proposed_cmd}' for #{target} (#{lang}) — no config is attached, " \
|
|
327
|
+
return "⚠️ Promoted '#{display_command(proposed_cmd)}' for #{target} (#{lang}) — no config is attached, " \
|
|
317
328
|
"so the command is active for this session only and was not saved."
|
|
318
329
|
end
|
|
319
330
|
|
|
320
|
-
"✨ Successfully verified and promoted runtime service for #{target} (#{lang}): '#{proposed_cmd}'. " \
|
|
331
|
+
"✨ Successfully verified and promoted runtime service for #{target} (#{lang}): '#{display_command(proposed_cmd)}'. " \
|
|
321
332
|
"Service is now active in the live context."
|
|
322
333
|
end
|
|
323
334
|
|
|
@@ -326,6 +337,22 @@ module Rubik
|
|
|
326
337
|
def verify_binary(target, lang, proposed_cmd, sub_ctx, probe_args: nil)
|
|
327
338
|
Rubik::Runtime::RepairProbe.new(root_dir: @root_dir).verify(target, lang, proposed_cmd, sub_ctx, probe_args: probe_args)
|
|
328
339
|
end
|
|
340
|
+
|
|
341
|
+
def command_provided?(command)
|
|
342
|
+
if command.is_a?(String)
|
|
343
|
+
!command.strip.empty?
|
|
344
|
+
elsif command.is_a?(Hash) || command.is_a?(Array)
|
|
345
|
+
!Rubik::LSP::ServerCommand.argv(command).empty?
|
|
346
|
+
else
|
|
347
|
+
false
|
|
348
|
+
end
|
|
349
|
+
rescue ArgumentError
|
|
350
|
+
false
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def display_command(command)
|
|
354
|
+
command.is_a?(Hash) || command.is_a?(Array) ? Rubik::LSP::ServerCommand.display(command) : command.to_s
|
|
355
|
+
end
|
|
329
356
|
end
|
|
330
357
|
|
|
331
358
|
class FinishTool < BaseTool
|
|
@@ -39,16 +39,32 @@ module Rubik
|
|
|
39
39
|
@lsp_manager.sync_buffer_file_if_needed(path, bm) if bm && @lsp_manager.respond_to?(:sync_buffer_file_if_needed)
|
|
40
40
|
|
|
41
41
|
if path.empty?
|
|
42
|
-
all = @lsp_manager.
|
|
43
|
-
total_issues = all.values.
|
|
42
|
+
all = @lsp_manager.all_diagnostic_snapshots
|
|
43
|
+
total_issues = all.values.sum { |snapshot| snapshot[:diagnostics].size }
|
|
44
|
+
pending = all.values.count { |snapshot| snapshot[:state] == :pending }
|
|
45
|
+
unversioned = all.values.count { |snapshot| snapshot[:state] == :unversioned }
|
|
44
46
|
if total_issues.zero?
|
|
45
|
-
|
|
47
|
+
if pending.positive?
|
|
48
|
+
"LSP analysis pending for #{pending} workspace file(s); no clean result is available yet."
|
|
49
|
+
elsif unversioned.positive?
|
|
50
|
+
"No diagnostics were reported for #{unversioned} workspace file(s), but the server omitted document versions; freshness is unverified."
|
|
51
|
+
elsif all.empty?
|
|
52
|
+
"No active LSP diagnostics have been published for open workspace documents."
|
|
53
|
+
else
|
|
54
|
+
"No LSP diagnostics were reported for #{all.size} analyzed workspace file(s)."
|
|
55
|
+
end
|
|
46
56
|
else
|
|
47
57
|
lines = ["=== Active Workspace LSP Diagnostics (#{total_issues} issue(s)) ==="]
|
|
48
|
-
all.each do |fpath,
|
|
58
|
+
all.each do |fpath, snapshot|
|
|
59
|
+
diags = snapshot[:diagnostics]
|
|
49
60
|
next if diags.empty?
|
|
50
61
|
|
|
51
|
-
|
|
62
|
+
freshness = if snapshot[:state] == :pending
|
|
63
|
+
"PENDING; previous results may be stale"
|
|
64
|
+
else
|
|
65
|
+
(snapshot[:state] == :unversioned ? "VERSION UNVERIFIED" : "current")
|
|
66
|
+
end
|
|
67
|
+
lines << "\nFile: #{fpath} (#{diags.size}; #{freshness}):"
|
|
52
68
|
diags.each do |d|
|
|
53
69
|
sev = d["severity"] == 1 ? "ERROR" : "WARN"
|
|
54
70
|
start_line = (d.dig("range", "start", "line") || 0) + 1
|
|
@@ -58,11 +74,23 @@ module Rubik
|
|
|
58
74
|
lines.join("\n")
|
|
59
75
|
end
|
|
60
76
|
else
|
|
61
|
-
|
|
77
|
+
snapshot = @lsp_manager.diagnostics_snapshot_for(path)
|
|
78
|
+
diags = snapshot[:diagnostics]
|
|
62
79
|
if diags.empty?
|
|
63
|
-
|
|
80
|
+
message = case snapshot[:state]
|
|
81
|
+
when :pending then "LSP analysis is pending for '#{path}'; no clean result is available yet."
|
|
82
|
+
when :unversioned then "No LSP diagnostics were reported for '#{path}', but the server omitted document versions; freshness is unverified."
|
|
83
|
+
when :fresh then "No LSP diagnostics or errors for '#{path}'."
|
|
84
|
+
else "No LSP diagnostic result is available for '#{path}'."
|
|
85
|
+
end
|
|
86
|
+
append_buffer_tip(message, path)
|
|
64
87
|
else
|
|
65
|
-
|
|
88
|
+
freshness = if snapshot[:state] == :pending
|
|
89
|
+
"pending; previous results may be stale"
|
|
90
|
+
else
|
|
91
|
+
(snapshot[:state] == :unversioned ? "version unverified" : "current")
|
|
92
|
+
end
|
|
93
|
+
lines = ["=== LSP Diagnostics for #{path} (#{diags.size} issue(s); #{freshness}) ==="]
|
|
66
94
|
diags.each do |d|
|
|
67
95
|
sev = case d["severity"]
|
|
68
96
|
when 1 then "ERROR"
|