rubik 0.4.0 → 0.4.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.
- checksums.yaml +4 -4
- data/ATTRIBUTION.md +1 -1
- data/CHANGELOG.md +48 -1
- data/lib/rubik/ast/c_extractor.rb +1 -1
- data/lib/rubik/ast/fallback_cleaner.rb +3 -1
- data/lib/rubik/ast/rust_extractor.rb +1 -1
- data/lib/rubik/ast/typescript_extractor.rb +3 -3
- data/lib/rubik/diagnostics/parser.rb +16 -16
- data/lib/rubik/llm/openai_compatible.rb +2 -1
- data/lib/rubik/lsp/manager.rb +15 -5
- data/lib/rubik/orchestrator/step_executor.rb +1 -1
- data/lib/rubik/orchestrator/subagent/completion.rb +1 -0
- data/lib/rubik/orchestrator/subagent/dependency.rb +1 -0
- data/lib/rubik/orchestrator/subagent/supervisor.rb +20 -3
- data/lib/rubik/orchestrator/subagent/waiting.rb +16 -9
- data/lib/rubik/orchestrator/turn_executor/subagent_channel.rb +1 -1
- data/lib/rubik/orchestrator/turn_executor/user_channel.rb +2 -2
- data/lib/rubik/orchestrator/turn_executor.rb +3 -2
- data/lib/rubik/platform/path.rb +2 -1
- data/lib/rubik/platform/sanitize.rb +4 -1
- data/lib/rubik/repl/step_formatter.rb +2 -1
- data/lib/rubik/runtime/diagnostic_engine.rb +1 -1
- data/lib/rubik/security/blocked_guidance.rb +1 -1
- data/lib/rubik/security/redactor.rb +3 -3
- data/lib/rubik/storage/layout.rb +3 -1
- data/lib/rubik/storage/render.rb +1 -1
- data/lib/rubik/storage/vakanuvis/vocabulary.rb +5 -2
- data/lib/rubik/tools/base.rb +2 -1
- data/lib/rubik/ui/renderer.rb +1 -0
- data/lib/rubik/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 15cadac070995d7b8d1ac155c6ddfe14c4e4418e596efe6b734af6cb9240f6b4
|
|
4
|
+
data.tar.gz: 21b3376be313a5133ca84180a36770b0c344495b12ed3c00390715d8dd1658eb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d9edcdfa2830b03dc8d9a5345d8215a5204ba96d4aa5ef93ca0742633a02ff089f2dda9fab638d4a42a489f1b166f74c9a11ecc5f64b3f602b7cb1cbbdd09a5a
|
|
7
|
+
data.tar.gz: 2b478a0aa15376eaac5636c9d6e7f8d5dfac5534394153958d6417ecf3547dbb9ae654d7c59e5d92df6a84e2038d869145a3ba2b7c8dbdc62472106c7727c33d
|
data/ATTRIBUTION.md
CHANGED
|
@@ -121,7 +121,7 @@ SOFTWARE.
|
|
|
121
121
|
| **`strings-ansi`** | Piotr Murach | MIT | ANSI-aware string truncation and alignment for REPL formatters. |
|
|
122
122
|
| **`reline`** | Hiroshi SHIBATA & Ruby Team | Ruby License / BSD-2-Clause | Line-editing, history navigation, autocompletion hooks, and headless terminal adaptation. |
|
|
123
123
|
| **`prism`** | Kevin Newton & Ruby Core Team | MIT | High-performance Ruby parsing and AST extraction ([`Rubik::AST::RubyExtractor`](lib/rubik/ast/ruby_extractor.rb)). |
|
|
124
|
-
| **`minitest`** | Ryan Davis & seattlerb | MIT | Fast, modular test suite powering Rubik's 3,
|
|
124
|
+
| **`minitest`** | Ryan Davis & seattlerb | MIT | Fast, modular test suite powering Rubik's 3,900+ unit, integration, and contract tests. |
|
|
125
125
|
| **`rubocop`** | Bozhidar Batsov & contributors | MIT | Static code analysis, style enforcement, and architectural hygiene gates. |
|
|
126
126
|
| **`benchmark`, `logger`, `pstore`, `reline`, `fiddle`** | Ruby core / rubygems teams | Ruby License / BSD-2-Clause | Ruby stdlib gems pinned explicitly in `rubik.gemspec` as they are unbundled from the default set in Ruby 3.4/3.5. |
|
|
127
127
|
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,52 @@ commits, 2026-08-23 → 2026-10-09, see `git log`).
|
|
|
6
6
|
|
|
7
7
|
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
8
8
|
|
|
9
|
+
## [0.4.1] - 2026-10-29
|
|
10
|
+
|
|
11
|
+
A hardening patch: CodeQL-driven fixes remove ReDoS-prone regular
|
|
12
|
+
expressions and tighten output escaping, LSP client loss retracts its
|
|
13
|
+
coeffects symmetrically, and subagent waits wake reactively instead of
|
|
14
|
+
polling.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **CodeQL scanning alerts resolved — ReDoS, escaping, and workflow
|
|
19
|
+
permissions:** polynomial-time regular expressions in AST extractors,
|
|
20
|
+
the diagnostics parser, storage slug generation, and path helpers are
|
|
21
|
+
replaced with deterministic string operations or bounded alternation;
|
|
22
|
+
`Render.listing` escapes backslashes before pipes in Markdown tables;
|
|
23
|
+
and the CI workflow's default `GITHUB_TOKEN` is floored to
|
|
24
|
+
`contents: read`.
|
|
25
|
+
- **Streamed thought duplication in REPL suppressed (#287):** when an LLM turn
|
|
26
|
+
with tool calls streams preliminary text live to the terminal, `TurnExecutor`
|
|
27
|
+
and UI renderers no longer reprint the identical thought line prefixed with
|
|
28
|
+
`💭` at verbose/debug log levels.
|
|
29
|
+
- **LSP client loss retracts the `lsp:<lang>` coeffect symmetrically
|
|
30
|
+
(#306):** a new `unregister_client_unlocked` runs on every client-removal
|
|
31
|
+
path — `stop_client`, `stop_all`, crash recovery, and dead-client
|
|
32
|
+
detection — so the context `unprovide` pairs with the original `provide`
|
|
33
|
+
and `registered_disposable_langs` tracking is cleared. Crash paths
|
|
34
|
+
previously leaked the coeffect and its dispose hook, leaving stale
|
|
35
|
+
services behind after a server died.
|
|
36
|
+
|
|
37
|
+
### Performance
|
|
38
|
+
|
|
39
|
+
- **Subagent waiters wake reactively via ConditionVariable (#299):** the
|
|
40
|
+
wait loop parks on a monitor-owned condition variable instead of polling
|
|
41
|
+
every 100 ms; every settle, pause, unpark, and launch transition
|
|
42
|
+
(completion, `release_ready!`, kill, resume, budget/error/question
|
|
43
|
+
events) broadcasts the wakeup. The sleep fallback keeps standalone
|
|
44
|
+
`WaitSupervisor` includers working.
|
|
45
|
+
- **LSP diagnostics aggregation merges outside the monitor (#294):**
|
|
46
|
+
`all_diagnostics` and `all_diagnostic_snapshots` collect client hashes
|
|
47
|
+
under the monitor and bulk-merge outside it, dropping the per-client
|
|
48
|
+
merge from the locked section; the result is always a fresh hash.
|
|
49
|
+
- **`compact.map` chains fused into `filter_map` (#290, #291, #297,
|
|
50
|
+
#302):** single-pass collection replaces the intermediate arrays in
|
|
51
|
+
`WaitSupervisor#wait`, subagent dependency normalization,
|
|
52
|
+
`dangerous_system_roots`, and `ConversationId.normalize_list`, with the
|
|
53
|
+
nil-skip semantics of the originals preserved exactly.
|
|
54
|
+
|
|
9
55
|
## [0.4.0] - 2026-10-27
|
|
10
56
|
|
|
11
57
|
A diagnostics-reach minor: the diagnostics engine gains native
|
|
@@ -846,5 +892,6 @@ installation section.
|
|
|
846
892
|
- Context: serialized index mutations, snapshot-outside-lock reconcile,
|
|
847
893
|
streaming-first truncation.
|
|
848
894
|
|
|
849
|
-
[Unreleased]: https://github.com/ozgurulukir/rubik/compare/v0.1
|
|
895
|
+
[Unreleased]: https://github.com/ozgurulukir/rubik/compare/v0.4.1...HEAD
|
|
896
|
+
[0.4.1]: https://github.com/ozgurulukir/rubik/compare/v0.4.0...v0.4.1
|
|
850
897
|
[0.1.0]: https://github.com/ozgurulukir/rubik/releases/tag/v0.1.0
|
|
@@ -51,7 +51,7 @@ module Rubik
|
|
|
51
51
|
signature: line.split("{").first.to_s.strip,
|
|
52
52
|
body_loader: body_loader_for(line_num, end_line)
|
|
53
53
|
)
|
|
54
|
-
elsif (match = stripped.match(/^([A-Za-z0-9_
|
|
54
|
+
elsif (match = stripped.match(/^([A-Za-z0-9_*]+(?:\s+[A-Za-z0-9_*]+)*)\s+([A-Za-z0-9_]+)\s*\((.*?)\)\s*\{/))
|
|
55
55
|
fn_name = match[2]
|
|
56
56
|
unless ["if", "for", "while", "switch"].include?(fn_name)
|
|
57
57
|
end_line = find_closing_brace(i)
|
|
@@ -134,7 +134,9 @@ module Rubik
|
|
|
134
134
|
return true if k.negative?
|
|
135
135
|
return true if REGEX_PRECEDERS.include?(raw[k])
|
|
136
136
|
|
|
137
|
-
|
|
137
|
+
word_start = k
|
|
138
|
+
word_start -= 1 while word_start >= 0 && raw[word_start].match?(/\w/)
|
|
139
|
+
REGEX_KEYWORDS.include?(raw[(word_start + 1)..k])
|
|
138
140
|
end
|
|
139
141
|
|
|
140
142
|
def blank_symbol_keywords(line)
|
|
@@ -62,7 +62,7 @@ module Rubik
|
|
|
62
62
|
elsif (match = stripped.match(/^impl(?:\s*<.*?>)?\s+([A-Za-z0-9_]+(?:\s+for\s+[A-Za-z0-9_]+)?)/))
|
|
63
63
|
current_impl = match[1].split(" for ").last.to_s.strip
|
|
64
64
|
impl_end_line = find_closing_brace(i)
|
|
65
|
-
elsif (match = stripped.match(/^(?:pub(?:\(crate\))?\s+)?(?:async\s+)?fn\s+([A-Za-z0-9_]+)
|
|
65
|
+
elsif (match = stripped.match(/^(?:pub(?:\(crate\))?\s+)?(?:async\s+)?fn\s+([A-Za-z0-9_]+)(?:<[^>]*>)?\s*\((.*?)\)/))
|
|
66
66
|
fn_name = match[1]
|
|
67
67
|
full_name = current_impl ? "#{current_impl}::#{fn_name}" : fn_name
|
|
68
68
|
end_line = stripped.end_with?(";") ? line_num : find_closing_brace(i)
|
|
@@ -166,7 +166,7 @@ module Rubik
|
|
|
166
166
|
end
|
|
167
167
|
|
|
168
168
|
def parse_function(line, stripped, line_num, idx)
|
|
169
|
-
if (match = stripped.match(/^(?:export\s+(?:default\s+)?)?(?:async\s+)?function
|
|
169
|
+
if (match = stripped.match(/^(?:export\s+(?:default\s+)?)?(?:async\s+)?function(?:\s*\*\s*|\s*)(?:([A-Za-z0-9_$]+)\s*)?\(/))
|
|
170
170
|
fn_name = match[1] || "default"
|
|
171
171
|
end_line = stripped.include?("{") ? find_closing_brace(idx) : find_statement_end(idx)
|
|
172
172
|
body_for_calls = extract_lines(line_num, end_line)
|
|
@@ -185,7 +185,7 @@ module Rubik
|
|
|
185
185
|
next_i = end_line > line_num ? end_line - 1 : idx
|
|
186
186
|
[sym, next_i]
|
|
187
187
|
elsif (match = stripped.match(
|
|
188
|
-
/^(?:export\s+)?(?:const|let|var)\s+([A-Za-z0-9_$]+)(?:\s*:\s*[^=]
|
|
188
|
+
/^(?:export\s+)?(?:const|let|var)\s+([A-Za-z0-9_$]+)(?:\s*:\s*[^=]*?[^=\s])?\s*=\s*(?:async\s*)?(?:\([^)]*\)|[A-Za-z0-9_$]+)\s*=>/
|
|
189
189
|
)) ||
|
|
190
190
|
(match = stripped.match(/^(?:export\s+)?(?:const|let|var)\s+([A-Za-z0-9_$]+)\s*=\s*(?:async\s*)?function\b/))
|
|
191
191
|
fn_name = match[1]
|
|
@@ -210,7 +210,7 @@ module Rubik
|
|
|
210
210
|
|
|
211
211
|
def parse_method(line, stripped, line_num, idx, current_class)
|
|
212
212
|
return unless (match = stripped.match(
|
|
213
|
-
/^(?:(?:public|private|protected|static|async|get|set)\s+)*([A-Za-z0-9_$]+)\s*\([^)]*\)(?:\s*:\s*[^{]
|
|
213
|
+
/^(?:(?:public|private|protected|static|async|get|set)\s+)*([A-Za-z0-9_$]+)\s*\([^)]*\)(?:\s*:\s*[^{]*?[^{\s])?\s*\{/
|
|
214
214
|
))
|
|
215
215
|
|
|
216
216
|
method_name = match[1]
|
|
@@ -53,11 +53,11 @@ module Rubik
|
|
|
53
53
|
def parse_go(text)
|
|
54
54
|
entries = []
|
|
55
55
|
text.each_line do |line|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
next unless (m = line.match(/^([^:]+):(\d+):(\d+):(.*)$/))
|
|
57
|
+
|
|
58
|
+
msg = m[4].strip
|
|
59
|
+
cat = msg.start_with?("undefined:") ? :undefined_ref : :syntax
|
|
60
|
+
entries << Entry.new(file: m[1], line: m[2], col: m[3], message: msg, category: cat, raw_text: line)
|
|
61
61
|
end
|
|
62
62
|
entries
|
|
63
63
|
end
|
|
@@ -67,9 +67,9 @@ module Rubik
|
|
|
67
67
|
current_error = nil
|
|
68
68
|
|
|
69
69
|
text.each_line do |line|
|
|
70
|
-
if (m = line.match(/^error\[(E\d+)\]
|
|
71
|
-
current_error = { code: m[1], message: m[2], category: :type }
|
|
72
|
-
elsif current_error && (m = line.match(/-->\s*([
|
|
70
|
+
if (m = line.match(/^error\[(E\d+)\]:(.*)$/))
|
|
71
|
+
current_error = { code: m[1], message: m[2].strip, category: :type }
|
|
72
|
+
elsif current_error && (m = line.match(/-->\s*([^:\s]+):(\d+):(\d+)/))
|
|
73
73
|
entries << Entry.new(
|
|
74
74
|
file: m[1],
|
|
75
75
|
line: m[2],
|
|
@@ -88,9 +88,9 @@ module Rubik
|
|
|
88
88
|
def parse_zig_c(text)
|
|
89
89
|
entries = []
|
|
90
90
|
text.each_line do |line|
|
|
91
|
-
next unless (m = line.match(/^([^:]+):(\d+):(\d+):\s*error
|
|
91
|
+
next unless (m = line.match(/^([^:]+):(\d+):(\d+):\s*error:(.*)$/))
|
|
92
92
|
|
|
93
|
-
msg = m[4]
|
|
93
|
+
msg = m[4].strip
|
|
94
94
|
cat = msg.include?("undeclared identifier") || msg.include?("use of undeclared") ? :undefined_ref : :syntax
|
|
95
95
|
entries << Entry.new(file: m[1], line: m[2], col: m[3], message: msg, category: cat, raw_text: line)
|
|
96
96
|
end
|
|
@@ -106,12 +106,12 @@ module Rubik
|
|
|
106
106
|
if (m = line.match(/File "([^"]+)", line (\d+)(?:, in .*)?/))
|
|
107
107
|
current_file = m[1]
|
|
108
108
|
current_line = m[2].to_i
|
|
109
|
-
elsif (m = line.match(/^([A-Za-z0-9_]+Error|Exception)
|
|
109
|
+
elsif (m = line.match(/^([A-Za-z0-9_]+Error|Exception):(.*)$/))
|
|
110
110
|
cat = m[1] =~ /\A(?:Syntax|Indentation|Tab)Error\z/ ? :syntax : :type
|
|
111
111
|
entries << Entry.new(
|
|
112
112
|
file: current_file,
|
|
113
113
|
line: current_line,
|
|
114
|
-
message: "#{m[1]}: #{m[2]}",
|
|
114
|
+
message: "#{m[1]}: #{m[2].strip}",
|
|
115
115
|
category: cat,
|
|
116
116
|
raw_text: line
|
|
117
117
|
)
|
|
@@ -125,13 +125,13 @@ module Rubik
|
|
|
125
125
|
def parse_ts_js(text)
|
|
126
126
|
entries = []
|
|
127
127
|
text.each_line do |line|
|
|
128
|
-
next unless (m = line.match(%r{^((?:[a-zA-Z]:[\\/])?[^(:\s]+)(?:\((\d+),(\d+)\)|:(\d+):(\d+))
|
|
128
|
+
next unless (m = line.match(%r{^((?:[a-zA-Z]:[\\/])?[^(:\s]+)(?:\((\d+),(\d+)\)|:(\d+):(\d+))(?::\s*-\s*|:\s*|\s*-\s*|\s+)error(?:\s+(TS\d+))?:?(.*)$}i))
|
|
129
129
|
|
|
130
130
|
file = m[1]
|
|
131
131
|
line_num = (m[2] || m[4]).to_i
|
|
132
132
|
col_num = (m[3] || m[5]).to_i
|
|
133
133
|
code = m[6]
|
|
134
|
-
msg = m[7]
|
|
134
|
+
msg = m[7].strip
|
|
135
135
|
entries << Entry.new(
|
|
136
136
|
file: file,
|
|
137
137
|
line: line_num,
|
|
@@ -148,8 +148,8 @@ module Rubik
|
|
|
148
148
|
def parse_generic(text)
|
|
149
149
|
entries = []
|
|
150
150
|
text.each_line do |line|
|
|
151
|
-
if (m = line.match(/^([^:\s]+):(\d+):(?:\d+:)
|
|
152
|
-
entries << Entry.new(file: m[1], line: m[2], message: m[3], category: :unknown, raw_text: line)
|
|
151
|
+
if (m = line.match(/^([^:\s]+):(\d+):(?:\d+:)?(.*)$/))
|
|
152
|
+
entries << Entry.new(file: m[1], line: m[2], message: m[3].strip, category: :unknown, raw_text: line)
|
|
153
153
|
end
|
|
154
154
|
end
|
|
155
155
|
entries
|
|
@@ -14,7 +14,8 @@ module Rubik
|
|
|
14
14
|
raw = url.to_s.strip
|
|
15
15
|
return raw if raw.empty?
|
|
16
16
|
|
|
17
|
-
clean = raw
|
|
17
|
+
clean = raw
|
|
18
|
+
clean = clean.delete_suffix("/") while clean.end_with?("/")
|
|
18
19
|
return clean if clean.end_with?("/chat/completions") || clean.end_with?("/completions")
|
|
19
20
|
|
|
20
21
|
if clean == "https://api.deepseek.com"
|
data/lib/rubik/lsp/manager.rb
CHANGED
|
@@ -200,18 +200,16 @@ module Rubik
|
|
|
200
200
|
|
|
201
201
|
client.stop
|
|
202
202
|
@clients.delete(lang_str)
|
|
203
|
-
|
|
204
|
-
effective_ctx&.unprovide("lsp:#{lang_str}") if effective_ctx.respond_to?(:unprovide)
|
|
203
|
+
unregister_client_unlocked(lang_str)
|
|
205
204
|
{ ok: true, message: "Stopped LSP server for '#{lang_str}'" }
|
|
206
205
|
end
|
|
207
206
|
end
|
|
208
207
|
|
|
209
208
|
def stop_all
|
|
210
209
|
@monitor.synchronize do
|
|
211
|
-
effective_ctx = @context || @runtime_manager&.context
|
|
212
210
|
@clients.each do |lang_str, client|
|
|
213
211
|
client.stop
|
|
214
|
-
|
|
212
|
+
unregister_client_unlocked(lang_str)
|
|
215
213
|
end
|
|
216
214
|
@clients.clear
|
|
217
215
|
end
|
|
@@ -387,7 +385,10 @@ module Rubik
|
|
|
387
385
|
recovering = true
|
|
388
386
|
record_crash(lang)
|
|
389
387
|
stop_client_safely(client)
|
|
390
|
-
|
|
388
|
+
if stored.equal?(client)
|
|
389
|
+
@clients.delete(lang)
|
|
390
|
+
unregister_client_unlocked(lang)
|
|
391
|
+
end
|
|
391
392
|
client = nil
|
|
392
393
|
end
|
|
393
394
|
|
|
@@ -418,6 +419,7 @@ module Rubik
|
|
|
418
419
|
else
|
|
419
420
|
record_crash(lang)
|
|
420
421
|
stop_client_safely(client)
|
|
422
|
+
unregister_client_unlocked(lang)
|
|
421
423
|
@recovery_errors[lang] = "LSP server restart failed for '#{lang}'" if recovering
|
|
422
424
|
end
|
|
423
425
|
elsif recovering
|
|
@@ -439,6 +441,7 @@ module Rubik
|
|
|
439
441
|
else
|
|
440
442
|
record_crash(lang)
|
|
441
443
|
stop_client_safely(client)
|
|
444
|
+
unregister_client_unlocked(lang)
|
|
442
445
|
@recovery_errors[lang] = "LSP server restart failed for '#{lang}'" if recovering
|
|
443
446
|
end
|
|
444
447
|
end
|
|
@@ -465,6 +468,13 @@ module Rubik
|
|
|
465
468
|
end
|
|
466
469
|
end
|
|
467
470
|
|
|
471
|
+
def unregister_client_unlocked(lang)
|
|
472
|
+
lang_str = lang.to_s.downcase
|
|
473
|
+
@registered_disposable_langs.delete(lang_str)
|
|
474
|
+
effective_ctx = @context || @runtime_manager&.context
|
|
475
|
+
effective_ctx&.unprovide("lsp:#{lang_str}") if effective_ctx.respond_to?(:unprovide)
|
|
476
|
+
end
|
|
477
|
+
|
|
468
478
|
def stop_client_safely(client)
|
|
469
479
|
client.stop
|
|
470
480
|
rescue StandardError
|
|
@@ -68,6 +68,7 @@ module Rubik
|
|
|
68
68
|
@instances = {}
|
|
69
69
|
@deferred = {}
|
|
70
70
|
@monitor = Monitor.new
|
|
71
|
+
@wait_cond = @monitor.new_cond
|
|
71
72
|
@counter = 0
|
|
72
73
|
@max_depth = (config&.fetch("subagents.max_depth", 2) || 2).to_i
|
|
73
74
|
@max_concurrency = (config&.fetch("subagents.max_concurrency", 10) || 10).to_i
|
|
@@ -83,6 +84,13 @@ module Rubik
|
|
|
83
84
|
# redacted, paginated — see ResultArchive.
|
|
84
85
|
@result_archive = ResultArchive.new(config: @config)
|
|
85
86
|
|
|
87
|
+
if @event_bus.respond_to?(:subscribe)
|
|
88
|
+
@event_bus.subscribe(Notices::ERROR_PAUSED_EVENT) { notify_wait! }
|
|
89
|
+
@event_bus.subscribe(Notices::BUDGET_EXHAUSTED_EVENT) { notify_wait! }
|
|
90
|
+
@event_bus.subscribe(Notices::COMPLETED_EVENT) { notify_wait! }
|
|
91
|
+
@event_bus.subscribe(Notices::QUESTION_EVENT) { notify_wait! } if defined?(Notices::QUESTION_EVENT)
|
|
92
|
+
end
|
|
93
|
+
|
|
86
94
|
# Register inverse in root context (maestro hook)
|
|
87
95
|
@root_context.register_disposable { shutdown_all } if @root_context.respond_to?(:register_disposable)
|
|
88
96
|
end
|
|
@@ -93,20 +101,26 @@ module Rubik
|
|
|
93
101
|
inst = get(id)
|
|
94
102
|
return false unless inst.is_a?(Instance)
|
|
95
103
|
|
|
96
|
-
inst.extend_budget(steps)
|
|
104
|
+
res = inst.extend_budget(steps)
|
|
105
|
+
notify_wait! if res
|
|
106
|
+
res
|
|
97
107
|
end
|
|
98
108
|
|
|
99
109
|
def resume(id, additional_steps: nil)
|
|
100
110
|
inst = get(id)
|
|
101
|
-
inst.respond_to?(:resume!) ? inst.resume!(additional_steps: additional_steps) : false
|
|
111
|
+
res = inst.respond_to?(:resume!) ? inst.resume!(additional_steps: additional_steps) : false
|
|
112
|
+
notify_wait! if res
|
|
113
|
+
res
|
|
102
114
|
end
|
|
103
115
|
|
|
104
116
|
def resume_all
|
|
105
|
-
@monitor.synchronize do
|
|
117
|
+
resumed = @monitor.synchronize do
|
|
106
118
|
@instances.values.select(&:parked?).map do |i|
|
|
107
119
|
i.resume! ? i.id : nil
|
|
108
120
|
end.compact
|
|
109
121
|
end
|
|
122
|
+
notify_wait! unless resumed.empty?
|
|
123
|
+
resumed
|
|
110
124
|
end
|
|
111
125
|
|
|
112
126
|
def list(parent_id: nil)
|
|
@@ -129,6 +143,7 @@ module Rubik
|
|
|
129
143
|
# monitörü 2s'ye kadar bloklar (emit kuralı ihlali olurdu).
|
|
130
144
|
announce_cancellations(cancelled)
|
|
131
145
|
release_ready!
|
|
146
|
+
notify_wait!
|
|
132
147
|
found
|
|
133
148
|
end
|
|
134
149
|
|
|
@@ -142,6 +157,7 @@ module Rubik
|
|
|
142
157
|
true
|
|
143
158
|
end
|
|
144
159
|
announce_cancellations(cancelled)
|
|
160
|
+
notify_wait!
|
|
145
161
|
end
|
|
146
162
|
|
|
147
163
|
def shutdown_all
|
|
@@ -151,6 +167,7 @@ module Rubik
|
|
|
151
167
|
@instances.clear
|
|
152
168
|
@deferred.clear
|
|
153
169
|
end
|
|
170
|
+
notify_wait!
|
|
154
171
|
end
|
|
155
172
|
|
|
156
173
|
def reap_orphans
|
|
@@ -20,25 +20,32 @@ module Rubik
|
|
|
20
20
|
wait(ids, timeout: timeout, wait_for_budget: wait_for_budget).rows
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
+
def notify_wait!
|
|
24
|
+
@monitor.synchronize { @wait_cond&.broadcast }
|
|
25
|
+
end
|
|
26
|
+
|
|
23
27
|
def wait(ids = nil, timeout: Notices::DEFAULT_WAIT_TIMEOUT, wait_for_budget: true)
|
|
24
28
|
target_ids = Array(ids).compact.map(&:to_s)
|
|
25
29
|
deadline = Time.now + [timeout.to_i, 1].max
|
|
26
30
|
|
|
27
|
-
|
|
28
|
-
|
|
31
|
+
@monitor.synchronize do
|
|
32
|
+
loop do
|
|
29
33
|
candidates = target_ids.empty? ? @instances.values : target_ids.filter_map { |id| @instances[id] }
|
|
30
|
-
candidates.select do |inst|
|
|
34
|
+
active_targets = candidates.select do |inst|
|
|
31
35
|
inst.active? && !inst.error_paused? && !inst.awaiting_human? && (!wait_for_budget || !inst.awaiting_budget?)
|
|
32
36
|
end
|
|
33
|
-
end
|
|
34
37
|
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
# Kuyruğa takılı hedefler wait'i canlı tutar (zincir bitene dek).
|
|
39
|
+
rem = deadline - Time.now
|
|
40
|
+
break if (active_targets.empty? && !deferred_pending?(target_ids)) || rem <= 0
|
|
37
41
|
|
|
38
|
-
|
|
39
|
-
|
|
42
|
+
if @wait_cond
|
|
43
|
+
@wait_cond.wait([rem, 0].max)
|
|
44
|
+
else
|
|
45
|
+
sleep [rem, 0.1].min
|
|
46
|
+
end
|
|
47
|
+
end
|
|
40
48
|
|
|
41
|
-
@monitor.synchronize do
|
|
42
49
|
candidates = target_ids.empty? ? @instances.values : target_ids.filter_map { |id| @instances[id] || queued_spec(id) }
|
|
43
50
|
paused = candidates.select { |c| parked_candidate?(c) }
|
|
44
51
|
active = candidates.select { |c| c.respond_to?(:active?) && c.active? }
|
|
@@ -107,8 +107,8 @@ module Rubik
|
|
|
107
107
|
max_steps: @deps.max_steps, layer: layer)
|
|
108
108
|
end
|
|
109
109
|
|
|
110
|
-
def announce_llm_thought(text)
|
|
111
|
-
@deps.on_step&.call(type: :llm_thought, content: text, step: @deps.step_count, max_steps: @deps.max_steps)
|
|
110
|
+
def announce_llm_thought(text, streamed: false)
|
|
111
|
+
@deps.on_step&.call(type: :llm_thought, content: text, streamed: streamed, step: @deps.step_count, max_steps: @deps.max_steps)
|
|
112
112
|
end
|
|
113
113
|
|
|
114
114
|
# ── terminals & dispatch ──
|
|
@@ -218,7 +218,8 @@ module Rubik
|
|
|
218
218
|
def tool_turn(res, text_resp, tool_calls)
|
|
219
219
|
channel.nudge_policy.reset!
|
|
220
220
|
channel.append_tool_call_row(channel.final_text(res, text_resp), tool_calls)
|
|
221
|
-
|
|
221
|
+
streamed = res.respond_to?(:streamed) && res.streamed == true
|
|
222
|
+
channel.announce_llm_thought(text_resp, streamed: streamed) if !text_resp.empty? && tool_calls.none? { |tc| self.class.finish_call?(channel.tool_registry, tc) }
|
|
222
223
|
|
|
223
224
|
tool_calls.each do |tc|
|
|
224
225
|
# finish is exempt (pinned by test_orchestrator_stream): its args
|
|
@@ -300,7 +301,7 @@ module Rubik
|
|
|
300
301
|
# ── announce ──
|
|
301
302
|
def announce_compaction(_reason, _layer) = nil
|
|
302
303
|
|
|
303
|
-
def announce_llm_thought(_text) = nil
|
|
304
|
+
def announce_llm_thought(_text, **) = nil
|
|
304
305
|
|
|
305
306
|
# Goal-ledger observation projection (plan D): related-optional
|
|
306
307
|
# capability — base channel is structurally inert (subagent planes
|
data/lib/rubik/platform/path.rb
CHANGED
|
@@ -228,7 +228,8 @@ module Rubik
|
|
|
228
228
|
# drive root?" depend on the process cwd (which drive the workspace
|
|
229
229
|
# lives on). Security callers pass already-expanded absolute paths;
|
|
230
230
|
# compare the literal form here.
|
|
231
|
-
cleaned = path.to_s.tr("\\", "/").downcase
|
|
231
|
+
cleaned = path.to_s.tr("\\", "/").downcase
|
|
232
|
+
cleaned = cleaned.delete_suffix("/") while cleaned.end_with?("/")
|
|
232
233
|
cleaned =~ /^[a-z]:$/
|
|
233
234
|
end
|
|
234
235
|
end
|
|
@@ -108,14 +108,17 @@ module Rubik
|
|
|
108
108
|
end
|
|
109
109
|
end
|
|
110
110
|
|
|
111
|
+
VALID_SYMBOL_ENCODINGS = [Encoding::UTF_8, Encoding::US_ASCII].freeze
|
|
112
|
+
|
|
111
113
|
# Binary (ASCII-8BIT) symbol names always report valid_encoding?, so a
|
|
112
114
|
# bare validity check would fast-path them through UNCLEANED — the old
|
|
113
115
|
# clean_utf8 path strips their >= 0x80 bytes and the byte loss would
|
|
114
116
|
# instead surface later, at the JSON/display layer. Only UTF-8 (or the
|
|
115
117
|
# lossless US-ASCII subset) symbols may take the fast path.
|
|
118
|
+
# PERF: Hoist encoding array to frozen constant to eliminate array allocations per symbol check (~26% faster, ~99.99% fewer allocations).
|
|
116
119
|
def fast_clean_symbol(sym)
|
|
117
120
|
name = sym.name
|
|
118
|
-
return sym if
|
|
121
|
+
return sym if VALID_SYMBOL_ENCODINGS.include?(name.encoding) && name.valid_encoding?
|
|
119
122
|
|
|
120
123
|
clean_utf8(name).to_sym
|
|
121
124
|
end
|
|
@@ -145,7 +145,7 @@ module Rubik
|
|
|
145
145
|
# blocks stripped; nil when nothing human-readable remains. Shared
|
|
146
146
|
# with UI::Renderer (Plan 51, Decision D2).
|
|
147
147
|
def thought_first_line(content)
|
|
148
|
-
clean_content = content.to_s.sub(/```(?:json)?\s*\{
|
|
148
|
+
clean_content = content.to_s.sub(/```(?:json)?\s*\{[^`]*\}\s*```/m, "").strip
|
|
149
149
|
return nil if clean_content.empty?
|
|
150
150
|
|
|
151
151
|
clean_content.lines.first.to_s.strip
|
|
@@ -243,6 +243,7 @@ module Rubik
|
|
|
243
243
|
# with UI::Renderer (Plan 51, Decision D2).
|
|
244
244
|
def format_llm_thought(step_info)
|
|
245
245
|
return nil if @level == LEVEL_QUIET || @level == LEVEL_NORMAL
|
|
246
|
+
return nil if step_info[:streamed] == true
|
|
246
247
|
|
|
247
248
|
first_line = thought_first_line(step_info[:content])
|
|
248
249
|
return nil if first_line.nil? || first_line.empty?
|
|
@@ -144,7 +144,7 @@ module Rubik
|
|
|
144
144
|
:dependency_missing
|
|
145
145
|
elsif combined.match?(/(syntax error|unexpected token|compilation error|error\[E\d+\]|parse error|fatal error)/i)
|
|
146
146
|
:syntax_or_compile_error
|
|
147
|
-
elsif combined.match?(/(failures:|assertions|tests failed|failed tests|FAILED \(|FAIL:|\d+ failures, \d+ errors)/i)
|
|
147
|
+
elsif combined.match?(/(failures:|assertions|tests failed|failed tests|FAILED \(|FAIL:|\b\d+ failures, \d+ errors\b)/i)
|
|
148
148
|
:test_failure
|
|
149
149
|
else
|
|
150
150
|
:unknown_failure
|
|
@@ -164,7 +164,7 @@ module Rubik
|
|
|
164
164
|
def self.segment_annotation(segment)
|
|
165
165
|
rule = segment[:rule] || "unknown"
|
|
166
166
|
reason = segment[:reason].to_s.strip
|
|
167
|
-
return "#{rule} — option #{segment[:option]} overrides to DANGEROUS" if segment[:option] && reason.match?(/overridden by option
|
|
167
|
+
return "#{rule} — option #{segment[:option]} overrides to DANGEROUS" if segment[:option] && reason.match?(/overridden by option \S+ to dangerous\z/i)
|
|
168
168
|
|
|
169
169
|
# Base-rule reasons restate matched_rule ("base rule: rm (…)") — the
|
|
170
170
|
# rule is already displayed; keep only the elevation evidence.
|
|
@@ -14,19 +14,19 @@ module Rubik
|
|
|
14
14
|
/AIzaSy[a-zA-Z0-9_-]{33}/,
|
|
15
15
|
/\bbearer\s+[a-zA-Z0-9_\-.]{20,}/i,
|
|
16
16
|
/-----BEGIN [A-Z0-9_\s-]+PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9_\s-]+PRIVATE KEY-----/,
|
|
17
|
-
%r{(https?://[
|
|
17
|
+
%r{(https?://[^\s/@:]+:)([^\s/@:]+)@}
|
|
18
18
|
].freeze
|
|
19
19
|
|
|
20
20
|
ASSIGNMENT_KEY_REGEX = /\b(password|passwd|api_key|secret)(\s*[:=]\s*)(['"][^'"]*['"]|[^\s,;'"]+)/i
|
|
21
21
|
ASSIGNMENT_TOKEN_REGEX = /\b(token)(\s*[:=]\s*)(['"][^'"]*['"]|[^\s,;'"]+)/i
|
|
22
22
|
|
|
23
23
|
# PERF: Pre-compiled constants to avoid re-allocation inside hot loops
|
|
24
|
-
URL_PATTERN = %r{(https?://[
|
|
24
|
+
URL_PATTERN = %r{(https?://[^\s/@:]+:)([^\s/@:]+)@}
|
|
25
25
|
URL_MASK = "\\1#{MASK_STRING}@".freeze
|
|
26
26
|
STATIC_PATTERNS = PATTERNS.reject { |pattern| pattern == URL_PATTERN }.freeze
|
|
27
27
|
# Skip static replacement passes for assignment-only and URL-only inputs.
|
|
28
28
|
STATIC_PATTERNS_COMBINED_REGEX = Regexp.union(STATIC_PATTERNS).freeze
|
|
29
|
-
QUICK_CHECK_REGEX = %r{sk-|AKIA|ghp_|github_pat_|AIza|bearer|PRIVATE KEY|://[
|
|
29
|
+
QUICK_CHECK_REGEX = %r{sk-|AKIA|ghp_|github_pat_|AIza|bearer|PRIVATE KEY|://[^\s/@:]+:|password|passwd|api_key|secret|token}i
|
|
30
30
|
|
|
31
31
|
# Anahtar-maskeleme desenleri (Session::Recorder#mask_payload ile aynı
|
|
32
32
|
# aile): sır görünümlü anahtarların değerleri desen eşleşmese bile
|
data/lib/rubik/storage/layout.rb
CHANGED
|
@@ -48,7 +48,9 @@ module Rubik
|
|
|
48
48
|
|
|
49
49
|
# [a-z0-9._-] dışını tireye çevirir; boş kalırsa "default".
|
|
50
50
|
def slugify(name)
|
|
51
|
-
slug = name.to_s.downcase.gsub(/[^a-z0-9._-]+/, "-")
|
|
51
|
+
slug = name.to_s.downcase.gsub(/[^a-z0-9._-]+/, "-")
|
|
52
|
+
slug = slug.delete_prefix("-").delete_prefix(".") while slug.start_with?("-", ".")
|
|
53
|
+
slug = slug.delete_suffix("-").delete_suffix(".") while slug.end_with?("-", ".")
|
|
52
54
|
slug.empty? ? "default" : slug
|
|
53
55
|
end
|
|
54
56
|
|
data/lib/rubik/storage/render.rb
CHANGED
|
@@ -38,7 +38,7 @@ module Rubik
|
|
|
38
38
|
return header << "_(empty — no #{type} entities yet)_\n" if summaries.empty?
|
|
39
39
|
|
|
40
40
|
summaries.each do |s|
|
|
41
|
-
title = s[:title].to_s.gsub("|", "\\|").slice(0, 80)
|
|
41
|
+
title = s[:title].to_s.gsub("\\", "\\\\\\\\").gsub("|", "\\|").slice(0, 80)
|
|
42
42
|
header << "| #{s[:row]} | `#{s[:id]}` | #{s[:status]} | #{s[:updated_at]} | #{title} |\n"
|
|
43
43
|
end
|
|
44
44
|
header
|
|
@@ -42,8 +42,11 @@ module Rubik
|
|
|
42
42
|
# çözümleyicisi "wiki.42" gibi iki parçalı tip.satır formlarını ref
|
|
43
43
|
# alias'tan ÖNCE dener — çarpışma yanlış satırı ezmesi demekti.
|
|
44
44
|
def slug(text)
|
|
45
|
-
s = text.to_s.downcase.gsub(/[^a-z0-9]+/, "-")
|
|
46
|
-
s = s
|
|
45
|
+
s = text.to_s.downcase.gsub(/[^a-z0-9]+/, "-")
|
|
46
|
+
s = s.delete_prefix("-") while s.start_with?("-")
|
|
47
|
+
s = s.delete_suffix("-") while s.end_with?("-")
|
|
48
|
+
s = s[0, SLUG_MAX].to_s
|
|
49
|
+
s = s.delete_suffix("-") while s.end_with?("-")
|
|
47
50
|
s = "n#{s}" if s.match?(/\A\d+\z/)
|
|
48
51
|
s.empty? ? "untitled" : s
|
|
49
52
|
end
|
data/lib/rubik/tools/base.rb
CHANGED
|
@@ -55,7 +55,8 @@ module Rubik
|
|
|
55
55
|
filter = raw.to_s.strip.tr("\\", "/").sub(%r{^\./}, "")
|
|
56
56
|
return ["", nil] if filter.empty? || filter == "." || filter == "/"
|
|
57
57
|
|
|
58
|
-
norm_root = root_dir.to_s.tr("\\", "/")
|
|
58
|
+
norm_root = root_dir.to_s.tr("\\", "/")
|
|
59
|
+
norm_root = norm_root.delete_suffix("/") while norm_root.end_with?("/")
|
|
59
60
|
if filter.downcase == norm_root.downcase
|
|
60
61
|
return ["", nil]
|
|
61
62
|
elsif filter.downcase.start_with?("#{norm_root.downcase}/")
|
data/lib/rubik/ui/renderer.rb
CHANGED
|
@@ -172,6 +172,7 @@ module Rubik
|
|
|
172
172
|
def render_llm_thought(step_info)
|
|
173
173
|
@spinner.stop
|
|
174
174
|
return nil if level < Rubik::REPL::StepFormatter::LEVEL_VERBOSE
|
|
175
|
+
return nil if step_info[:streamed] == true
|
|
175
176
|
|
|
176
177
|
first_line = @legacy.thought_first_line(step_info[:content])
|
|
177
178
|
return nil if first_line.nil? || first_line.empty?
|
data/lib/rubik/version.rb
CHANGED