rubik 0.2.0 → 0.3.1

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/ATTRIBUTION.md +4 -0
  3. data/CHANGELOG.md +115 -0
  4. data/README.md +137 -268
  5. data/bin/rubik +9 -0
  6. data/lib/rubik/cli/workspace_trust.rb +78 -0
  7. data/lib/rubik/kernel/init.rb +15 -0
  8. data/lib/rubik/lsp/client.rb +3 -2
  9. data/lib/rubik/orchestrator/auto_apply.rb +1 -1
  10. data/lib/rubik/orchestrator/subagent/activation.rb +29 -4
  11. data/lib/rubik/orchestrator/subagent/builtin_types.rb +117 -0
  12. data/lib/rubik/orchestrator/subagent/dynamic_store.rb +119 -0
  13. data/lib/rubik/orchestrator/subagent/instance.rb +8 -36
  14. data/lib/rubik/orchestrator/subagent/journal.rb +77 -4
  15. data/lib/rubik/orchestrator/subagent/profile_contract.rb +187 -0
  16. data/lib/rubik/orchestrator/subagent/profile_snapshot.rb +72 -0
  17. data/lib/rubik/orchestrator/subagent/reuse.rb +2 -1
  18. data/lib/rubik/orchestrator/subagent/scoped_registry.rb +2 -0
  19. data/lib/rubik/orchestrator/subagent/spawn_transaction.rb +170 -0
  20. data/lib/rubik/orchestrator/subagent/start_gate.rb +98 -0
  21. data/lib/rubik/orchestrator/subagent/supervisor.rb +4 -67
  22. data/lib/rubik/orchestrator/subagent/type_definition.rb +84 -0
  23. data/lib/rubik/orchestrator/subagent/type_registry.rb +139 -159
  24. data/lib/rubik/orchestrator/turn_executor/subagent_channel.rb +4 -2
  25. data/lib/rubik/platform/process.rb +78 -1
  26. data/lib/rubik/prompt/context_builder.rb +54 -20
  27. data/lib/rubik/prompt/doctrine.rb +158 -22
  28. data/lib/rubik/prompt/token_estimator.rb +5 -3
  29. data/lib/rubik/prompt/tool_teachings.rb +19 -12
  30. data/lib/rubik/sandbox/driver/bubblewrap.rb +13 -59
  31. data/lib/rubik/sandbox/driver/docker.rb +14 -60
  32. data/lib/rubik/sandbox/driver/podman.rb +13 -56
  33. data/lib/rubik/sandbox/driver/process.rb +12 -64
  34. data/lib/rubik/security/guard.rb +1 -1
  35. data/lib/rubik/storage/store.rb +52 -11
  36. data/lib/rubik/tools/builtins/define_subagent.rb +69 -0
  37. data/lib/rubik/tools/builtins/storage.rb +8 -5
  38. data/lib/rubik/tools/builtins/subagent.rb +20 -53
  39. data/lib/rubik/tools/builtins/synthesis.rb +6 -1
  40. data/lib/rubik/tools/builtins.rb +1 -0
  41. data/lib/rubik/tools/diagnostic_reporter.rb +2 -1
  42. data/lib/rubik/tools/fuzzy_matcher.rb +39 -19
  43. data/lib/rubik/tools/truncator.rb +54 -21
  44. data/lib/rubik/version.rb +1 -1
  45. metadata +10 -1
@@ -81,29 +81,49 @@ module Rubik
81
81
 
82
82
  # Computes Levenshtein edit distance between two strings using standard dynamic programming.
83
83
  def self.levenshtein_distance(str1, str2)
84
- return DidYouMean::Levenshtein.distance(str1.to_s, str2.to_s) if defined?(DidYouMean::Levenshtein) && DidYouMean::Levenshtein.respond_to?(:distance)
85
-
86
- s1 = str1.to_s.chars
87
- s2 = str2.to_s.chars
88
- return s2.length if s1.empty?
89
- return s1.length if s2.empty?
90
-
91
- d = Array.new(s1.length + 1) { |i| i }
92
-
93
- s2.each_with_index do |char2, j|
94
- new_d = [j + 1]
95
- s1.each_with_index do |char1, i|
84
+ return DidYouMean::Levenshtein.distance(str1.to_s.scrub, str2.to_s.scrub) if defined?(DidYouMean::Levenshtein) && DidYouMean::Levenshtein.respond_to?(:distance)
85
+
86
+ # PERF: Use codepoints instead of String#chars to avoid single-character String allocations,
87
+ # single-buffer array mutation instead of allocating row arrays (new_d), and fast while loops
88
+ # instead of each_with_index/Array#min blocks (~35% faster, zero inner loop allocations).
89
+ # scrub guarantees valid UTF-8 so codepoints never raises on
90
+ # malformed input (the previous .chars path tolerated bad bytes).
91
+ s1 = str1.to_s.scrub.codepoints
92
+ s2 = str2.to_s.scrub.codepoints
93
+ len1 = s1.length
94
+ len2 = s2.length
95
+ return len2 if len1.zero?
96
+ return len1 if len2.zero?
97
+
98
+ d = Array.new(len1 + 1) { |i| i }
99
+
100
+ j = 0
101
+ while j < len2
102
+ char2 = s2[j]
103
+ j += 1
104
+ new_d0 = j
105
+ prev = d[0]
106
+ i = 0
107
+ while i < len1
108
+ char1 = s1[i]
96
109
  cost = char1 == char2 ? 0 : 1
97
- new_d << [
98
- d[i + 1] + 1, # deletion
99
- new_d[i] + 1, # insertion
100
- d[i] + cost # substitution
101
- ].min
110
+ del = d[i + 1] + 1
111
+ ins = new_d0 + 1
112
+ sub = prev + cost
113
+
114
+ min_val = del
115
+ min_val = ins if ins < min_val
116
+ min_val = sub if sub < min_val
117
+ new_d0 = min_val
118
+
119
+ prev = d[i + 1]
120
+ d[i + 1] = new_d0
121
+ i += 1
102
122
  end
103
- d = new_d
123
+ d[0] = j
104
124
  end
105
125
 
106
- d.last
126
+ d[len1]
107
127
  end
108
128
 
109
129
  # Finds the closest matching line window in `file_content` for `target_content`
@@ -1,14 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "English"
4
-
5
3
  module Rubik
6
4
  module Tools
7
5
  module Truncator
8
6
  DEFAULT_MAX_CHARS = 12_000
9
7
  DEFAULT_MAX_LINES = 300
10
8
 
11
- def self.truncate(text, max_chars: DEFAULT_MAX_CHARS, max_lines: DEFAULT_MAX_LINES, strategy: :head_tail)
9
+ def self.truncate(text, max_chars: DEFAULT_MAX_CHARS, max_lines: DEFAULT_MAX_LINES, strategy: :head_tail, separator: "\n")
12
10
  str = text.to_s
13
11
  # byteslice truncation below cuts on byte boundaries and can split a
14
12
  # multibyte character, producing invalid UTF-8 that then poisons the
@@ -16,15 +14,14 @@ module Rubik
16
14
  # estimation). Scrub exactly once, after the last byteslice.
17
15
  str = str.scrub("") unless str.valid_encoding?
18
16
 
19
- max_c = (max_chars || DEFAULT_MAX_CHARS).to_i
20
- max_l = (max_lines || DEFAULT_MAX_LINES).to_i
17
+ max_c = [(max_chars || DEFAULT_MAX_CHARS).to_i, 0].max
18
+ max_l = [(max_lines || DEFAULT_MAX_LINES).to_i, 0].max
19
+ sep = separator || "\n"
21
20
 
22
- # PERF: With the default input separator, calculate the line upper bound
21
+ # PERF: With the default newline separator, calculate the line upper bound
23
22
  # using zero-allocation C-level #count("\n") and short-circuit BEFORE
24
- # calling #lines (which allocates an Array of line strings). Keep the
25
- # original #lines behavior when callers customize Ruby's global record
26
- # separator, since String#lines uses $/ when no separator is supplied.
27
- if $INPUT_RECORD_SEPARATOR == "\n"
23
+ # calling #lines (which allocates an Array of line strings).
24
+ if sep == "\n"
28
25
  line_bound = if str.empty?
29
26
  0
30
27
  elsif str.end_with?("\n")
@@ -35,7 +32,8 @@ module Rubik
35
32
  return { text: str, truncated: false, omitted_lines: 0, omitted_chars: 0 } if str.bytesize <= max_c && line_bound <= max_l
36
33
  end
37
34
 
38
- lines = str.lines
35
+ lines = str.lines(sep)
36
+ return { text: str, truncated: false, omitted_lines: 0, omitted_chars: 0 } if str.bytesize <= max_c && lines.size <= max_l
39
37
 
40
38
  strategy_sym = strategy.to_s.downcase.to_sym
41
39
 
@@ -44,7 +42,8 @@ module Rubik
44
42
  kept_lines = lines.first(max_l)
45
43
  kept_str = kept_lines.join
46
44
  kept_str = kept_str.byteslice(0, max_c) if kept_str.bytesize > max_c
47
- omitted_lines = [lines.size - kept_lines.size, 0].max
45
+ kept_line_count = kept_str.empty? ? 0 : kept_str.lines(sep).size
46
+ omitted_lines = [lines.size - kept_line_count, 0].max
48
47
  omitted_chars = [str.bytesize - kept_str.bytesize, 0].max
49
48
  notice = "\n... [Truncated: omitted #{omitted_lines} lines / #{omitted_chars} chars] ...\n"
50
49
  { text: finalize(kept_str, notice, prefix: true), truncated: true,
@@ -53,29 +52,63 @@ module Rubik
53
52
  kept_lines = lines.last(max_l)
54
53
  kept_str = kept_lines.join
55
54
  kept_str = kept_str.byteslice(-max_c, max_c) if kept_str.bytesize > max_c
56
- omitted_lines = [lines.size - kept_lines.size, 0].max
55
+ kept_line_count = kept_str.empty? ? 0 : kept_str.lines(sep).size
56
+ omitted_lines = [lines.size - kept_line_count, 0].max
57
57
  omitted_chars = [str.bytesize - kept_str.bytesize, 0].max
58
58
  notice = "\n... [Truncated: omitted #{omitted_lines} lines / #{omitted_chars} chars] ...\n"
59
59
  { text: finalize(kept_str, notice, prefix: false), truncated: true,
60
60
  omitted_lines: omitted_lines, omitted_chars: omitted_chars }
61
61
  else # :head_tail
62
- half_lines = (max_l / 2).clamp(1, 150)
63
- half_chars = (max_c / 2).clamp(1, 6000)
62
+ if max_l <= 1
63
+ kept_lines = lines.first(max_l)
64
+ kept_str = kept_lines.join
65
+ kept_str = kept_str.byteslice(0, max_c) if kept_str.bytesize > max_c
66
+ kept_line_count = kept_str.empty? ? 0 : kept_str.lines(sep).size
67
+ omitted_lines = [lines.size - kept_line_count, 0].max
68
+ omitted_chars = [str.bytesize - kept_str.bytesize, 0].max
69
+ notice = "\n\n... [Truncated: #{omitted_lines} lines / #{omitted_chars} chars omitted to preserve token budget] ...\n\n"
70
+ return { text: finalize(kept_str, notice, prefix: true), truncated: true,
71
+ omitted_lines: omitted_lines, omitted_chars: omitted_chars }
72
+ end
73
+
74
+ head_lines_count = max_l / 2
75
+ tail_lines_count = max_l - head_lines_count
76
+
77
+ head_chars = max_c / 2
78
+ tail_chars = max_c - head_chars
64
79
 
65
- head_lines = lines.first(half_lines)
66
- tail_lines = lines.last(half_lines)
80
+ if lines.size == 1
81
+ head_lines = lines
82
+ tail_lines = lines
83
+ elsif head_lines_count + tail_lines_count >= lines.size
84
+ head_n = [head_lines_count, lines.size / 2].min
85
+ tail_n = [tail_lines_count, lines.size - head_n].min
86
+ head_lines = lines.first(head_n)
87
+ tail_lines = lines.last(tail_n)
88
+ else
89
+ head_lines = lines.first(head_lines_count)
90
+ tail_lines = lines.last(tail_lines_count)
91
+ end
67
92
 
68
93
  head_str = head_lines.join
69
94
  tail_str = tail_lines.join
70
95
 
71
- head_str = head_str.byteslice(0, half_chars) if head_str.bytesize > half_chars
72
- tail_str = tail_str.byteslice(-half_chars, half_chars) if tail_str.bytesize > half_chars
96
+ head_str = head_str.byteslice(0, head_chars) if head_str.bytesize > head_chars
97
+ tail_str = tail_str.byteslice(-tail_chars, tail_chars) if tail_str.bytesize > tail_chars
73
98
 
74
- omitted_lines = [lines.size - (head_lines.size + tail_lines.size), 0].max
99
+ omitted_lines = if lines.size == 1
100
+ 0
101
+ else
102
+ head_k = head_str.empty? ? 0 : head_str.lines(sep).size
103
+ tail_k = tail_str.empty? ? 0 : tail_str.lines(sep).size
104
+ [lines.size - (head_k + tail_k), 0].max
105
+ end
75
106
  omitted_chars = [str.bytesize - (head_str.bytesize + tail_str.bytesize), 0].max
76
107
  notice = "\n\n... [Truncated: #{omitted_lines} lines / #{omitted_chars} chars omitted to preserve token budget] ...\n\n"
77
108
 
78
- { text: finalize(tail_str, finalize(head_str, notice, prefix: true), prefix: false), truncated: true,
109
+ head_final = head_str.valid_encoding? ? head_str : head_str.scrub("")
110
+ tail_final = tail_str.valid_encoding? ? tail_str : tail_str.scrub("")
111
+ { text: "#{head_final}#{notice}#{tail_final}", truncated: true,
79
112
  omitted_lines: omitted_lines, omitted_chars: omitted_chars }
80
113
  end
81
114
  end
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.2.0"
4
+ VERSION = "0.3.1"
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.2.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Özgür Ulukır
@@ -272,6 +272,7 @@ files:
272
272
  - lib/rubik/buffer/manager.rb
273
273
  - lib/rubik/buffer/path_validator.rb
274
274
  - lib/rubik/buffer/snapshot.rb
275
+ - lib/rubik/cli/workspace_trust.rb
275
276
  - lib/rubik/coeffects.rb
276
277
  - lib/rubik/concurrency.rb
277
278
  - lib/rubik/concurrency/boundary.rb
@@ -357,12 +358,14 @@ files:
357
358
  - lib/rubik/orchestrator/stream_manager.rb
358
359
  - lib/rubik/orchestrator/subagent/activation.rb
359
360
  - lib/rubik/orchestrator/subagent/budget.rb
361
+ - lib/rubik/orchestrator/subagent/builtin_types.rb
360
362
  - lib/rubik/orchestrator/subagent/completion.rb
361
363
  - lib/rubik/orchestrator/subagent/concurrency_gate.rb
362
364
  - lib/rubik/orchestrator/subagent/context_compaction.rb
363
365
  - lib/rubik/orchestrator/subagent/conversation_id.rb
364
366
  - lib/rubik/orchestrator/subagent/deferred_spec.rb
365
367
  - lib/rubik/orchestrator/subagent/dependency.rb
368
+ - lib/rubik/orchestrator/subagent/dynamic_store.rb
366
369
  - lib/rubik/orchestrator/subagent/execution_reads.rb
367
370
  - lib/rubik/orchestrator/subagent/instance.rb
368
371
  - lib/rubik/orchestrator/subagent/journal.rb
@@ -371,6 +374,8 @@ files:
371
374
  - lib/rubik/orchestrator/subagent/narration.rb
372
375
  - lib/rubik/orchestrator/subagent/notices.rb
373
376
  - lib/rubik/orchestrator/subagent/operation_tracker.rb
377
+ - lib/rubik/orchestrator/subagent/profile_contract.rb
378
+ - lib/rubik/orchestrator/subagent/profile_snapshot.rb
374
379
  - lib/rubik/orchestrator/subagent/questioner.rb
375
380
  - lib/rubik/orchestrator/subagent/reaper.rb
376
381
  - lib/rubik/orchestrator/subagent/resilience.rb
@@ -378,6 +383,8 @@ files:
378
383
  - lib/rubik/orchestrator/subagent/reuse.rb
379
384
  - lib/rubik/orchestrator/subagent/run_cycle.rb
380
385
  - lib/rubik/orchestrator/subagent/scoped_registry.rb
386
+ - lib/rubik/orchestrator/subagent/spawn_transaction.rb
387
+ - lib/rubik/orchestrator/subagent/start_gate.rb
381
388
  - lib/rubik/orchestrator/subagent/step_runner.rb
382
389
  - lib/rubik/orchestrator/subagent/storage_id.rb
383
390
  - lib/rubik/orchestrator/subagent/supervisor.rb
@@ -386,6 +393,7 @@ files:
386
393
  - lib/rubik/orchestrator/subagent/tool_dispatch.rb
387
394
  - lib/rubik/orchestrator/subagent/transcript_formatter.rb
388
395
  - lib/rubik/orchestrator/subagent/transcript_reader.rb
396
+ - lib/rubik/orchestrator/subagent/type_definition.rb
389
397
  - lib/rubik/orchestrator/subagent/type_registry.rb
390
398
  - lib/rubik/orchestrator/subagent/waiting.rb
391
399
  - lib/rubik/orchestrator/subagent_notice_inbox.rb
@@ -717,6 +725,7 @@ files:
717
725
  - lib/rubik/tools.rb
718
726
  - lib/rubik/tools/base.rb
719
727
  - lib/rubik/tools/builtins.rb
728
+ - lib/rubik/tools/builtins/define_subagent.rb
720
729
  - lib/rubik/tools/builtins/dmesg.rb
721
730
  - lib/rubik/tools/builtins/exec.rb
722
731
  - lib/rubik/tools/builtins/files.rb