rubik 0.1.2 → 0.1.3
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/CHANGELOG.md +29 -0
- data/lib/rubik/repl/commands/agent_execution.rb +45 -12
- data/lib/rubik/repl/input_execution.rb +22 -6
- data/lib/rubik/security/redactor.rb +15 -3
- 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: a3548e6b569bb11a7c6c6349bca95300d93cd375c65860095ff18d362cd289ed
|
|
4
|
+
data.tar.gz: 207e5ed22d5180f7b219b546d0322ac2f597703dec1157ec44da48feb899ab1e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a6b485c11a72537a00fbe813889f74b92e18edaa1445ec59bc3834bf6e56c377cc60ac628f059878e6d3fc0a1e27d4f63fa1f6161bac84513af4b3640a37c4b6
|
|
7
|
+
data.tar.gz: 228ce20578fc055bde12ffe0883343f21b74a9b499e718c1e953fc40c9e5054d3c21a27f5e3b986890efba08d3ae23ec77a37498fd197e37a3449de5524d0943
|
data/CHANGELOG.md
CHANGED
|
@@ -10,8 +10,37 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
10
10
|
|
|
11
11
|
Nothing yet.
|
|
12
12
|
|
|
13
|
+
## [0.1.3] - 2026-10-12
|
|
14
|
+
|
|
15
|
+
A robustness release straight from a field incident: the `!goal` loop
|
|
16
|
+
no longer strands state when the provider fails mid-goal, and the
|
|
17
|
+
redactor gets measurably cheaper on secret-free input.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- **`!goal` survives provider errors (#105):** an api read-timeout
|
|
22
|
+
`ClientError` escaped the goal handler (only `Interrupt` was
|
|
23
|
+
rescued), leaving `GoalState` stuck in `:running`, the ledger
|
|
24
|
+
unfinalized and the session silent while the batch spinner kept
|
|
25
|
+
cycling with the cursor hidden. One `fail_goal` channel now serves
|
|
26
|
+
the `:error` return plus `ClientError`/`StandardError` rescues
|
|
27
|
+
(`Interrupt` still means `:stopped`); both command-dispatch branches
|
|
28
|
+
kill the leaked spinner instead of masking the real failure; and
|
|
29
|
+
goal steps record their tool calls to the session like free-text
|
|
30
|
+
turns.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- **Redactor short-circuits secret-free lines (#104):** pre-compiled
|
|
35
|
+
quick-check constants plus a trigger check before scrubbing cut
|
|
36
|
+
allocations by ~68% on a mixed redact workload (172,940 -> 54,864
|
|
37
|
+
objects per 20x2000-line pass) with byte-for-byte output parity;
|
|
38
|
+
invalid-UTF-8 input stays total (masked/scrubbed) instead of raising
|
|
39
|
+
ArgumentError.
|
|
40
|
+
|
|
13
41
|
## [0.1.2] - 2026-10-11
|
|
14
42
|
|
|
43
|
+
|
|
15
44
|
The Anthropic correctness release: the provider now speaks the modern
|
|
16
45
|
extended-thinking contract, so pinning any current Claude model works
|
|
17
46
|
instead of failing with HTTP 400.
|
|
@@ -127,6 +127,16 @@ module Rubik
|
|
|
127
127
|
# Adım sayacı rubik://goal/state'e aksın: olaylar bir adım
|
|
128
128
|
# içinde tekrarlanabilir, GoalState monotoniktir (zararsız).
|
|
129
129
|
goal_state.update_steps(step_info[:step], step_info[:max_steps]) if step_info[:step].is_a?(Integer)
|
|
130
|
+
# Session teşhis edilebilirliği (field incident 2026-10-12):
|
|
131
|
+
# saatler süren !goal koşuları session dosyasına tek satır
|
|
132
|
+
# düşürüyordu — free-text turn simetrisinde tool adımları
|
|
133
|
+
# kayıtçıya akar; render ve LLM geçmişi değişmez.
|
|
134
|
+
case step_info[:type]&.to_sym
|
|
135
|
+
when :tool_call
|
|
136
|
+
ctx.recorder&.record_tool_call(step_info[:tool], step_info[:args], call_id: step_info[:call_id])
|
|
137
|
+
when :tool_result
|
|
138
|
+
ctx.recorder&.record_tool_result(step_info[:tool], step_info[:output], status: step_info[:status] || :ok, call_id: step_info[:call_id])
|
|
139
|
+
end
|
|
130
140
|
ctx.render_step_event(step_info)
|
|
131
141
|
}
|
|
132
142
|
)
|
|
@@ -143,8 +153,28 @@ module Rubik
|
|
|
143
153
|
end
|
|
144
154
|
|
|
145
155
|
# Pruner apply seam: hedef döngüsü de aynı sınırda tohumlanır —
|
|
146
|
-
# aşamalı
|
|
156
|
+
# aşamalı budama planı varsa goal promptundan ÖNCE uygulanır.
|
|
147
157
|
ctx.apply_staged_prune_plan!
|
|
158
|
+
|
|
159
|
+
# Tek hata kanalı (field incident 2026-10-12): eskiden yalnızca
|
|
160
|
+
# normal :error dönüşü başarısızlık eşlemesini yapıyordu; loop'tan
|
|
161
|
+
# kaçan bir ClientError (sağlayıcı read timeout) goal'u kayıtsız
|
|
162
|
+
# 'running' bırakıyor, ledger terminal görmiyor ve session
|
|
163
|
+
# dosyası sessiz kalıyordu. Rescue dalları da aynı kapanış
|
|
164
|
+
# sırasını buradan çalıştırır. Kayıtçı '!goal ...' user_turn'unu
|
|
165
|
+
# komut girişinde çoktan yazdı — assistant_response olmadan
|
|
166
|
+
# session restore EŞLEŞMEMİŞ bir 'Goal: ...' turu hidrate ederdi.
|
|
167
|
+
# Adım sayısı: loop'un truncation hatası :steps'siz döner —
|
|
168
|
+
# GoalState monotonik sayacı otoriter yedek.
|
|
169
|
+
fail_goal = lambda do |reason, tokens: nil, steps: nil|
|
|
170
|
+
sanitized = Rubik::Platform.safe_utf8(reason.to_s)
|
|
171
|
+
steps_used = steps || goal_state.step_count
|
|
172
|
+
goal_state.fail!(sanitized, tokens: tokens)
|
|
173
|
+
finalize_ledger.call(:failed)
|
|
174
|
+
ctx.out.puts "\n❌ Goal could not be completed: #{sanitized}"
|
|
175
|
+
record_goal_pair.call("[Goal attempt ended after #{steps_used} steps: #{sanitized}]", tokens: tokens)
|
|
176
|
+
end
|
|
177
|
+
|
|
148
178
|
ctx.active_loop = orchestrator
|
|
149
179
|
begin
|
|
150
180
|
res = orchestrator.run(goal_text, goal_mode: true, conversation_history: ctx.conversation_history)
|
|
@@ -170,6 +200,16 @@ module Rubik
|
|
|
170
200
|
finalize_ledger.call(:stopped)
|
|
171
201
|
record_goal_pair.call("[Goal attempt interrupted after #{goal_state.step_count} steps]")
|
|
172
202
|
raise
|
|
203
|
+
rescue Rubik::LLM::ClientError => e
|
|
204
|
+
# execute_free_text simetrisi: sağlayıcı hatası (read timeout,
|
|
205
|
+
# HTTP 4xx) kaçarsa da goal terminal görür. Yeniden yükseltmez —
|
|
206
|
+
# spinner temizliği dispatch katmanına (execute_command) aittir.
|
|
207
|
+
# next: rescue sonrası akış res'siz `if res[:ok]`'a düşmesin.
|
|
208
|
+
fail_goal.call(e.message)
|
|
209
|
+
next
|
|
210
|
+
rescue StandardError => e
|
|
211
|
+
fail_goal.call("#{e.class}: #{e.message}")
|
|
212
|
+
next
|
|
173
213
|
ensure
|
|
174
214
|
ctx.active_loop = nil
|
|
175
215
|
end
|
|
@@ -197,17 +237,10 @@ module Rubik
|
|
|
197
237
|
ctx.out.puts "\nTip: Use `!diff` to review changes and `!apply` to commit to disk." if ctx.buffer_manager.dirty?
|
|
198
238
|
end
|
|
199
239
|
else
|
|
200
|
-
# Başarısızlık eşlemesi
|
|
201
|
-
#
|
|
202
|
-
#
|
|
203
|
-
|
|
204
|
-
# GoalState monotonik sayacı otoriter yedek.
|
|
205
|
-
sanitized = Rubik::Platform.safe_utf8(res[:error].to_s)
|
|
206
|
-
steps_used = res[:steps] || goal_state.step_count
|
|
207
|
-
goal_state.fail!(sanitized, tokens: res[:tokens])
|
|
208
|
-
finalize_ledger.call(:failed)
|
|
209
|
-
ctx.out.puts "\n❌ Goal could not be completed: #{sanitized}"
|
|
210
|
-
record_goal_pair.call("[Goal attempt ended after #{steps_used} steps: #{sanitized}]", tokens: res[:tokens])
|
|
240
|
+
# Başarısızlık eşlemesi yukarıdaki tek hata kanalından akar:
|
|
241
|
+
# GoalState → ledger finalize → kayıtçı çifti, istisna yoluyla
|
|
242
|
+
# aynı sırayla.
|
|
243
|
+
fail_goal.call(res[:error], tokens: res[:tokens], steps: res[:steps])
|
|
211
244
|
end
|
|
212
245
|
end
|
|
213
246
|
end
|
|
@@ -56,6 +56,11 @@ module Rubik
|
|
|
56
56
|
rescue Interrupt
|
|
57
57
|
Rubik::REPL::InterruptHandler.restore_console_after_interrupt
|
|
58
58
|
@output.puts "\n⚠️ [Rubik] Komut kullanıcı tarafından iptal edildi (^C)."
|
|
59
|
+
ensure
|
|
60
|
+
# !goal dahil lifecycle komutları tracked dal ile aynı spinner
|
|
61
|
+
# garantisini taşır (field incident 2026-10-12: !goal'ten kaçan
|
|
62
|
+
# ClientError'da öksüz spinner terminali kilitli gösterdi).
|
|
63
|
+
kill_command_spinner!
|
|
59
64
|
end
|
|
60
65
|
else
|
|
61
66
|
captured = StringIO.new
|
|
@@ -70,7 +75,7 @@ module Rubik
|
|
|
70
75
|
@output.puts "\n⚠️ [Rubik] Komut kullanıcı tarafından iptal edildi (^C)."
|
|
71
76
|
ensure
|
|
72
77
|
# A command (!plan/!goal) may leave a live spinner if it died mid-wait.
|
|
73
|
-
|
|
78
|
+
kill_command_spinner!
|
|
74
79
|
end
|
|
75
80
|
output_text = captured.string.strip
|
|
76
81
|
if !cmd_interrupted && !output_text.empty?
|
|
@@ -297,11 +302,7 @@ module Rubik
|
|
|
297
302
|
rescue StandardError => e
|
|
298
303
|
warn "[rubik] markdown tail flush failed: #{e.class}: #{e.message}"
|
|
299
304
|
end
|
|
300
|
-
|
|
301
|
-
renderer&.spinner&.kill
|
|
302
|
-
rescue StandardError => e
|
|
303
|
-
warn "[rubik] spinner cleanup failed: #{e.class}: #{e.message}"
|
|
304
|
-
end
|
|
305
|
+
kill_spinner!(renderer&.spinner)
|
|
305
306
|
flush_subagent_notices! if respond_to?(:flush_subagent_notices!)
|
|
306
307
|
# Session review B3: notices stranded in the ended loop's inbox never
|
|
307
308
|
# reached the model channel and owed no wake — an idle maestro learned
|
|
@@ -318,6 +319,21 @@ module Rubik
|
|
|
318
319
|
end
|
|
319
320
|
reap_dead_kernel_units! if respond_to?(:reap_dead_kernel_units!)
|
|
320
321
|
end
|
|
322
|
+
|
|
323
|
+
# Shared spinner teardown. Cleanup must never mask the in-flight
|
|
324
|
+
# exception (AGENTS.md §5): a spinner whose tick thread died holding
|
|
325
|
+
# the mutex raises ThreadError from synchronize — report it to stderr
|
|
326
|
+
# and continue so the real failure surfaces.
|
|
327
|
+
def kill_spinner!(spinner)
|
|
328
|
+
spinner&.kill
|
|
329
|
+
rescue StandardError => e
|
|
330
|
+
warn "[rubik] spinner cleanup failed: #{e.class}: #{e.message}"
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# Command-dispatch spinner cleanup (both branches).
|
|
334
|
+
def kill_command_spinner!
|
|
335
|
+
kill_spinner!(@ui_renderer&.spinner)
|
|
336
|
+
end
|
|
321
337
|
end
|
|
322
338
|
end
|
|
323
339
|
end
|
|
@@ -20,6 +20,11 @@ module Rubik
|
|
|
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
|
+
# PERF: Pre-compiled constants to avoid re-allocation inside hot loops
|
|
24
|
+
URL_PATTERN = %r{(https?://[^:]+:)([^@]+)@}
|
|
25
|
+
URL_MASK = "\\1#{MASK_STRING}@".freeze
|
|
26
|
+
QUICK_CHECK_REGEX = %r{sk-|AKIA|ghp_|github_pat_|AIza|bearer|PRIVATE KEY|://[^:]+:|password|passwd|api_key|secret|token}i
|
|
27
|
+
|
|
23
28
|
# Anahtar-maskeleme desenleri (Session::Recorder#mask_payload ile aynı
|
|
24
29
|
# aile): sır görünümlü anahtarların değerleri desen eşleşmese bile
|
|
25
30
|
# maskelenir; sayaç anahtarları (tokens, max_tokens) istisnadır.
|
|
@@ -33,10 +38,17 @@ module Rubik
|
|
|
33
38
|
# scrub: redact ham geçmişe (tool çıktısı, prompt) dokunur — geçersiz
|
|
34
39
|
# UTF-8 gsub!'ı ArgumentError ile düşürür; total olmak maskenin
|
|
35
40
|
# kendisi bir arıza kaynağı olmasın demektir.
|
|
36
|
-
result = text.dup.scrub
|
|
41
|
+
result = text.valid_encoding? ? text.dup : text.scrub
|
|
42
|
+
|
|
43
|
+
# PERF: kısa devre — sır tetikleyicisi yoksa 11 gsub turu atlanır.
|
|
44
|
+
# Tetik testi scrub'lanmış sonuç üstünde yapılır: match? ham geçersiz
|
|
45
|
+
# UTF-8'de ArgumentError düşürür; scrub sonrası metin hep geçerlidir
|
|
46
|
+
# ve desenler zaten bu metne uygulanıyordu, dolayısıyla eşdeğerdir.
|
|
47
|
+
return result unless result.match?(QUICK_CHECK_REGEX)
|
|
48
|
+
|
|
37
49
|
PATTERNS.each do |pattern|
|
|
38
|
-
if pattern ==
|
|
39
|
-
result.gsub!(pattern,
|
|
50
|
+
if pattern == URL_PATTERN
|
|
51
|
+
result.gsub!(pattern, URL_MASK)
|
|
40
52
|
else
|
|
41
53
|
result.gsub!(pattern, MASK_STRING)
|
|
42
54
|
end
|
data/lib/rubik/version.rb
CHANGED