rubik 0.1.1 → 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/ATTRIBUTION.md +10 -1
- data/CHANGELOG.md +58 -0
- data/README.md +16 -1
- data/lib/rubik/llm/anthropic.rb +42 -6
- data/lib/rubik/llm/registry.rb +4 -4
- 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/ATTRIBUTION.md
CHANGED
|
@@ -54,6 +54,15 @@ SOFTWARE.
|
|
|
54
54
|
- **Spatial Composability:** Reactive Coeffect Dependency DAG ($\Sigma \vdash C : \tau$) implemented in [`Rubik::Runtime::ComponentFiber`](lib/rubik/runtime/fiber.rb) and [`Rubik::Runtime::Manager`](lib/rubik/runtime/manager.rb).
|
|
55
55
|
- Zero-downtime hot reloading, atomic rollback on activation failure, and leak-free lifecycle management.
|
|
56
56
|
|
|
57
|
+
### SKILL.state (Structured Execution State)
|
|
58
|
+
- **Paper:** *"SKILL.state: Scalable Long-Horizon Agent Skills"* ([arXiv:2608.26263](https://arxiv.org/abs/2608.26263))
|
|
59
|
+
- **Authors:** Sanket Badhe, Priyanka Tiwari, Jonghyun Chung
|
|
60
|
+
- **Rubik Implementation:**
|
|
61
|
+
- The structured goal ledger ([`orchestrator/goal_ledger/`](lib/rubik/orchestrator/goal_ledger/), 2026-10-10): the `!goal` loop replaces append-only conversation memory with an explicit, mutable, rev-guarded execution state Σ (objective, tasks, findings, decisions, open questions, environment notes) — the model sees the current state, not an ever-growing history.
|
|
62
|
+
- Validated JSON-patch intake (RFC 6902 subset) with the `:syntax` / `:schema` / `:conflict` rejection taxonomy and bounded-retry model feedback (`GoalLedger::Patch` / `Schema` / `Validator`).
|
|
63
|
+
- The paper's world-drift recovery, coded as tri-state FS re-verification: declarative `verify` assertions are re-checked at step boundaries; a failed assertion flips the task to `stale` with an internal rev bump, so the `:conflict` the model then hits IS the drift signal (`GoalLedger::Verifier` / `VerifySpec` / `DriftNote`).
|
|
64
|
+
- Observation projection: banked tool results compact into per-row `<absorbed_into_ledger>` tombstones while the last K turns stay raw (`GoalLedger::ObservationProjection`).
|
|
65
|
+
|
|
57
66
|
---
|
|
58
67
|
|
|
59
68
|
## 🌳 3. AST Parsing & Multi-Language Ecosystem
|
|
@@ -112,7 +121,7 @@ SOFTWARE.
|
|
|
112
121
|
| **`strings-ansi`** | Piotr Murach | MIT | ANSI-aware string truncation and alignment for REPL formatters. |
|
|
113
122
|
| **`reline`** | Hiroshi SHIBATA & Ruby Team | Ruby License / BSD-2-Clause | Line-editing, history navigation, autocompletion hooks, and headless terminal adaptation. |
|
|
114
123
|
| **`prism`** | Kevin Newton & Ruby Core Team | MIT | High-performance Ruby parsing and AST extraction ([`Rubik::AST::RubyExtractor`](lib/rubik/ast/ruby_extractor.rb)). |
|
|
115
|
-
| **`minitest`** | Ryan Davis & seattlerb | MIT | Fast, modular test suite powering Rubik's
|
|
124
|
+
| **`minitest`** | Ryan Davis & seattlerb | MIT | Fast, modular test suite powering Rubik's 3,400+ unit, integration, and contract tests. |
|
|
116
125
|
| **`rubocop`** | Bozhidar Batsov & contributors | MIT | Static code analysis, style enforcement, and architectural hygiene gates. |
|
|
117
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. |
|
|
118
127
|
|
data/CHANGELOG.md
CHANGED
|
@@ -10,8 +10,66 @@ 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
|
+
|
|
41
|
+
## [0.1.2] - 2026-10-11
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
The Anthropic correctness release: the provider now speaks the modern
|
|
45
|
+
extended-thinking contract, so pinning any current Claude model works
|
|
46
|
+
instead of failing with HTTP 400.
|
|
47
|
+
|
|
48
|
+
### Fixed
|
|
49
|
+
|
|
50
|
+
- **Anthropic provider speaks the modern thinking contract:** the
|
|
51
|
+
default model pin moves from the stale `claude-3-7-sonnet-20250219`
|
|
52
|
+
to `claude-sonnet-5-5`, and the extended-thinking payload follows the
|
|
53
|
+
model line ? 4.6+ and the 5.x/Fable models get the adaptive form
|
|
54
|
+
(`thinking: {type: "adaptive"}` with an `output_config` effort),
|
|
55
|
+
3-7-sonnet through the 4.5 line keep the `budget_tokens` math, and
|
|
56
|
+
pre-thinking lines get nothing attached. 0.1.1 sent the legacy form
|
|
57
|
+
to every Anthropic model, which Claude 4.7+ rejects with HTTP 400.
|
|
58
|
+
|
|
59
|
+
### Changed
|
|
60
|
+
|
|
61
|
+
- Extended thinking auto-enables on every thinking-capable Anthropic
|
|
62
|
+
model line (previously only claude-3-7-sonnet); `thinking: false`
|
|
63
|
+
still disables it everywhere and `thinking_budget_ratio` shapes the
|
|
64
|
+
legacy form only. Requests that carry thinking still force
|
|
65
|
+
`temperature` to 1.0.
|
|
66
|
+
- README quick start documents the native Anthropic provider
|
|
67
|
+
(`ANTHROPIC_API_KEY` + `!model anthropic/<model>`) next to the
|
|
68
|
+
OpenAI-compatible example.
|
|
69
|
+
|
|
13
70
|
## [0.1.1] - 2026-10-11
|
|
14
71
|
|
|
72
|
+
|
|
15
73
|
The Linux surface release: CI now proves Linux on Ruby 3.4 and 4.0
|
|
16
74
|
(`linux-test` is a required check), and the host seams that surface
|
|
17
75
|
exposed are fixed. Ships the structured goal ledger, the default-on
|
data/README.md
CHANGED
|
@@ -11,7 +11,7 @@ English | [Türkçe](README.tr.md)
|
|
|
11
11
|
[](AGENTS.md)
|
|
12
12
|
[](COMMANDS.md)
|
|
13
13
|
[](CONFIG.md)
|
|
14
|
-
[]()
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
@@ -31,6 +31,12 @@ Requires **Ruby 3.4+**:
|
|
|
31
31
|
gem install rubik
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
+
The installer puts the `rubik` launcher into RubyGems' bin directory (already on your `PATH`), so you can start it from any project directory:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
rubik
|
|
38
|
+
```
|
|
39
|
+
|
|
34
40
|
or with Bundler:
|
|
35
41
|
|
|
36
42
|
```ruby
|
|
@@ -73,6 +79,9 @@ ln -sf $(pwd)/bin/rubik ~/.local/bin/rubik
|
|
|
73
79
|
rubik
|
|
74
80
|
```
|
|
75
81
|
|
|
82
|
+
> [!NOTE]
|
|
83
|
+
> **The gem and a clone can coexist.** On POSIX the symlink always runs your clone: `bin/rubik` resolves `lib/` from its own real path, so a `git pull` is live on the very next launch — even while the `rubik` gem stays installed. The gem-installed `rubik` keeps serving the last released version until you run `gem update rubik`. On Windows, whichever `bin` folder comes first in `PATH` wins — `where.exe rubik` shows the winner.
|
|
84
|
+
|
|
76
85
|
#### 🪟 Windows (PowerShell / Command Prompt)
|
|
77
86
|
|
|
78
87
|
Rubik ships with built-in `.cmd`, `.bat`, and `.ps1` wrappers for Windows:
|
|
@@ -105,6 +114,12 @@ rubik > !provider add deepseek openai_compatible https://api.deepseek.com/v1/cha
|
|
|
105
114
|
rubik > !model deepseek/deepseek-coder
|
|
106
115
|
```
|
|
107
116
|
|
|
117
|
+
Anthropic talks over its **native Messages API** (not an OpenAI shim): the `anthropic` provider is registered out of the box — set `ANTHROPIC_API_KEY` and pick a model:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
rubik > !model anthropic/claude-sonnet-5-5
|
|
121
|
+
```
|
|
122
|
+
|
|
108
123
|
### First Session Example
|
|
109
124
|
|
|
110
125
|
```text
|
data/lib/rubik/llm/anthropic.rb
CHANGED
|
@@ -9,7 +9,32 @@ module Rubik
|
|
|
9
9
|
class AnthropicProvider < BaseProvider
|
|
10
10
|
attr_reader :name, :model, :formatter, :headers, :api_key, :api_key_env
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
# Extended-thinking support by model line (Anthropic docs, 2026-10):
|
|
13
|
+
# :adaptive — 4.6+ and the 5.x/Fable lines: `thinking: {type: "adaptive"}`
|
|
14
|
+
# with an `output_config` effort; the legacy budget_tokens form is
|
|
15
|
+
# REJECTED with a 400 from 4.7 onward.
|
|
16
|
+
# :legacy — claude-3-7-sonnet through the 4.5 line: the manual
|
|
17
|
+
# `thinking: {type: "enabled", budget_tokens: N}` form is the only
|
|
18
|
+
# accepted mode.
|
|
19
|
+
# nil — 3.5-and-earlier lines accept no thinking at all, and
|
|
20
|
+
# unrecognized names (custom aliases/proxies) get no auto thinking.
|
|
21
|
+
def self.thinking_mode_for(model_name)
|
|
22
|
+
s = model_name.to_s
|
|
23
|
+
return :legacy if s.include?("claude-3-7-sonnet")
|
|
24
|
+
return nil if s.match?(/\Aclaude-[23]/) # number-first 2.x/3.x lines (3-7 handled above)
|
|
25
|
+
|
|
26
|
+
# Single-digit captures: Anthropic versions are one digit per segment,
|
|
27
|
+
# so a snapshot date suffix (claude-sonnet-4-20250514) can never be
|
|
28
|
+
# swallowed as the minor version.
|
|
29
|
+
version = s.match(/\Aclaude-(?:sonnet|opus|haiku|fable)-(\d)(?:-(\d))?/)
|
|
30
|
+
return nil unless version
|
|
31
|
+
|
|
32
|
+
major = version[1].to_i
|
|
33
|
+
minor = version[2].to_i
|
|
34
|
+
major > 4 || (major == 4 && minor >= 6) ? :adaptive : :legacy
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def initialize(name: nil, model: "claude-sonnet-5-5", formatter: AnthropicFormatter.new, headers: {},
|
|
13
38
|
api_key: nil, api_key_env: nil)
|
|
14
39
|
super()
|
|
15
40
|
@name = name.to_s.downcase
|
|
@@ -170,14 +195,25 @@ module Rubik
|
|
|
170
195
|
thinking_opt = opts[:thinking]
|
|
171
196
|
thinking_disabled = [false, :disabled].include?(thinking_opt)
|
|
172
197
|
ratio = (opts[:thinking_budget_ratio] || 0.5).to_f
|
|
198
|
+
# Thinking is on for every model line that supports it unless
|
|
199
|
+
# explicitly disabled; thinking_budget_ratio only shapes the legacy
|
|
200
|
+
# budget form — adaptive models reason as they see fit.
|
|
201
|
+
mode = self.class.thinking_mode_for(target_model)
|
|
173
202
|
|
|
174
203
|
if thinking_opt.is_a?(Hash)
|
|
175
204
|
payload[:thinking] = thinking_opt
|
|
176
|
-
elsif !thinking_disabled && (thinking_opt == true ||
|
|
177
|
-
if
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
205
|
+
elsif !thinking_disabled && (thinking_opt == true || mode)
|
|
206
|
+
if mode == :legacy
|
|
207
|
+
if max_tokens > 2048
|
|
208
|
+
budget = [(max_tokens * ratio).to_i, 1024].max
|
|
209
|
+
budget = [budget, max_tokens - 1024].min
|
|
210
|
+
payload[:thinking] = { type: "enabled", budget_tokens: budget } if budget >= 1024
|
|
211
|
+
end
|
|
212
|
+
else
|
|
213
|
+
# Adaptive models reject the legacy form outright, and for
|
|
214
|
+
# unrecognized names adaptive is the modern-generation default.
|
|
215
|
+
payload[:thinking] = { type: "adaptive" }
|
|
216
|
+
payload[:output_config] = { effort: "high" }
|
|
181
217
|
end
|
|
182
218
|
end
|
|
183
219
|
|
data/lib/rubik/llm/registry.rb
CHANGED
|
@@ -38,7 +38,7 @@ module Rubik
|
|
|
38
38
|
@providers = {
|
|
39
39
|
"ollama" => OllamaProvider.new(model: "qwen2.5-coder:7b", headers: global_headers),
|
|
40
40
|
"openai" => OpenAIProvider.new(model: "gpt-4o", headers: global_headers),
|
|
41
|
-
"anthropic" => AnthropicProvider.new(model: "claude-
|
|
41
|
+
"anthropic" => AnthropicProvider.new(model: "claude-sonnet-5-5", headers: global_headers)
|
|
42
42
|
}
|
|
43
43
|
@models_catalog = {}
|
|
44
44
|
load_from_config(config) if config
|
|
@@ -82,7 +82,7 @@ module Rubik
|
|
|
82
82
|
end
|
|
83
83
|
if @providers["anthropic"].instance_of?(AnthropicProvider)
|
|
84
84
|
@providers["anthropic"] = AnthropicProvider.new(
|
|
85
|
-
model: @providers["anthropic"]&.model || "claude-
|
|
85
|
+
model: @providers["anthropic"]&.model || "claude-sonnet-5-5",
|
|
86
86
|
headers: global_headers
|
|
87
87
|
)
|
|
88
88
|
end
|
|
@@ -132,7 +132,7 @@ module Rubik
|
|
|
132
132
|
when "anthropic"
|
|
133
133
|
AnthropicProvider.new(
|
|
134
134
|
name: p_name,
|
|
135
|
-
model: default_model || "claude-
|
|
135
|
+
model: default_model || "claude-sonnet-5-5",
|
|
136
136
|
# Honor the same api_key / api_key_env that
|
|
137
137
|
# openai-compatible specs already forward;
|
|
138
138
|
# otherwise Anthropic auth fails silently.
|
|
@@ -257,7 +257,7 @@ module Rubik
|
|
|
257
257
|
when "anthropic"
|
|
258
258
|
anthropic_headers = (custom.respond_to?(:headers) ? custom.headers : nil) || global_headers
|
|
259
259
|
AnthropicProvider.new(
|
|
260
|
-
model: model_name || "claude-
|
|
260
|
+
model: model_name || "claude-sonnet-5-5",
|
|
261
261
|
headers: anthropic_headers,
|
|
262
262
|
api_key: custom.respond_to?(:api_key) ? custom.api_key : nil,
|
|
263
263
|
api_key_env: custom.respond_to?(:api_key_env) ? custom.api_key_env : nil
|
|
@@ -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