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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b8a3ecbfb98ba136f13800cc4a2d9ab68703b30c0086a3297c005abe6991d060
|
|
4
|
+
data.tar.gz: 72e1fff12c37f318f5e1b817d0ef88601110a83099f05edbd10c3d294564027c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7a17907eee5524e8053a9f73eb946ebe8766001a64ca17db048240837c1f154f7d2ba3650ce765e8ab830dc6da8840a1ea15e147490a03af1f5203d33b678294
|
|
7
|
+
data.tar.gz: 54d72614c2c6fd31a0ff482144c7953148d780516ea748f248027a6943d0b91c40fdfd64d1867036109f8b666bc3ce9ecb58516f0418f3c775335f249fead2b5
|
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,850+ 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
|
@@ -10,6 +10,108 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
10
10
|
|
|
11
11
|
Nothing yet.
|
|
12
12
|
|
|
13
|
+
## [0.3.5] - 2026-10-24
|
|
14
|
+
|
|
15
|
+
A vakanuvis-durability patch: the chronicler's digest pipeline stops
|
|
16
|
+
losing in-flight work and serving stale reads, LSP diagnostics and
|
|
17
|
+
document sync get hardened across the board, and AST call
|
|
18
|
+
extraction runs measurably cheaper.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- **vakanuvis recall and digest are durable (#266, #267, #268,
|
|
23
|
+
#270, #271, #272):** in-place memory and wiki edits now register
|
|
24
|
+
as durable content updates for digest dirty tracking; superseded
|
|
25
|
+
records no longer surface as exact recall hits or pass smoke
|
|
26
|
+
checks; annal write failures and harness shutdown no longer
|
|
27
|
+
strand targeted digest jobs or events enqueued after the STOP
|
|
28
|
+
marker; the pending digest queue is bounded when the distiller is
|
|
29
|
+
unavailable; and the rogue-memory sweep preserves canonical
|
|
30
|
+
`mem.*` aliases, superseding raw duplicates in the canonical
|
|
31
|
+
line's favor.
|
|
32
|
+
- **LSP diagnostics and sync hardened (#252, closes #245–#251):**
|
|
33
|
+
diagnostic freshness is tracked per canonical document — stale
|
|
34
|
+
versioned publications are rejected and pending or unversioned
|
|
35
|
+
analysis is reported accurately; document state and LSP frame
|
|
36
|
+
writes are serialized, and transport failures propagate to wake
|
|
37
|
+
pending requests; text-document synchronization capabilities are
|
|
38
|
+
honored (unsupported dynamic registrations rejected, save
|
|
39
|
+
notifications sent when supported); server selection is separated
|
|
40
|
+
from document language IDs, with crash history retained across
|
|
41
|
+
short-lived restarts; and structured executable/argv server
|
|
42
|
+
configuration is supported through discovery, repair and client
|
|
43
|
+
startup without invoking a shell.
|
|
44
|
+
- **Anthropic API key resolution hardened (#261):** the reported
|
|
45
|
+
hardcoded-secret finding is confirmed a false positive (the
|
|
46
|
+
literal env-var name in error messages); `resolve_api_key` now
|
|
47
|
+
falls back to `ANTHROPIC_API_KEY` defensively when the configured
|
|
48
|
+
environment-variable name is blank or empty.
|
|
49
|
+
|
|
50
|
+
### Performance
|
|
51
|
+
|
|
52
|
+
- **AST call extraction is cheaper (#277):** the Go, Rust and
|
|
53
|
+
TypeScript extractors use zero-capture lookahead regexes and
|
|
54
|
+
frozen keyword-exclusion Sets — 35–80% fewer allocations and
|
|
55
|
+
~25–27% faster in hot call-extraction paths, verified
|
|
56
|
+
output-identical.
|
|
57
|
+
|
|
58
|
+
### Changed
|
|
59
|
+
|
|
60
|
+
- **No-behavior-change decompositions (#254–#265):** oversized
|
|
61
|
+
methods across the OpenAI-compatible provider, goal-turn driving,
|
|
62
|
+
the MCP/workspace/session/model/context command registrations,
|
|
63
|
+
buffer registration and apply, subagent spawn and adapter process
|
|
64
|
+
management were split into helpers — same behavior, reviewable
|
|
65
|
+
units.
|
|
66
|
+
|
|
67
|
+
## [0.3.4] - 2026-10-23
|
|
68
|
+
|
|
69
|
+
A security-hardening patch: three injection/traversal findings in
|
|
70
|
+
child-process enumeration, the docker sandbox driver and overlay
|
|
71
|
+
storage are closed, scan-heavy paths got faster, and a wave of
|
|
72
|
+
oversized REPL/LLM methods was decomposed with no behavior change.
|
|
73
|
+
|
|
74
|
+
### Fixed
|
|
75
|
+
|
|
76
|
+
- **Command injection in child-process enumeration (#238):**
|
|
77
|
+
`direct_child_pids` interpolated the target pid into backtick
|
|
78
|
+
shells (`pgrep -P #{pid}` / `ps --ppid #{pid}`); both lookups now
|
|
79
|
+
run through array-parameterized `Open3.capture2` calls, so pid
|
|
80
|
+
values never reach a shell for re-parsing.
|
|
81
|
+
- **Command injection in the docker sandbox driver (#237):**
|
|
82
|
+
`DockerDriver#available?` and container cleanup built command
|
|
83
|
+
strings with interpolation (`docker rm -f #{container_name}`);
|
|
84
|
+
both now use array-form `system`, keeping shell metacharacters
|
|
85
|
+
inert.
|
|
86
|
+
- **Path traversal in OverlayStorage (#232):** overlay directories
|
|
87
|
+
are validated against an allowlist of the four known overlay dirs
|
|
88
|
+
resolved under their trusted roots, manifests and every
|
|
89
|
+
command/tool write path are containment-checked, and traversal
|
|
90
|
+
attempts are rejected loudly with a `[SECURITY]` warning instead
|
|
91
|
+
of escaping the overlay root.
|
|
92
|
+
|
|
93
|
+
### Performance
|
|
94
|
+
|
|
95
|
+
- **Faster scanning internals (#220, #225, #227, #228, #244):**
|
|
96
|
+
storage indexing parses JSON lines with less string churn;
|
|
97
|
+
`Session::Replayer` streams with `File.foreach`;
|
|
98
|
+
`Buffer::Manager#all_files` prunes `.git` traversal;
|
|
99
|
+
`FuzzyMatcher#find_closest_subsequence` tightens its inner loop;
|
|
100
|
+
and `clean_utf8_deep` gains an allocation-free fast path
|
|
101
|
+
(Symbol#name validation, Hash-based cycle tracking, ~3.3x on
|
|
102
|
+
clean input). `Buffer::Manager#diff_all` also reads through
|
|
103
|
+
`Platform.read_text`, so on-disk content is UTF-8-tagged
|
|
104
|
+
consistently (#226).
|
|
105
|
+
|
|
106
|
+
### Changed
|
|
107
|
+
|
|
108
|
+
- **No-behavior-change decompositions (#231, #233, #234, #235,
|
|
109
|
+
#239, #240, #242, #243):** oversized REPL command registrations
|
|
110
|
+
(workspace allow-list, goal, agent providers, allow-cmd, LSP),
|
|
111
|
+
the LLM client's `post_json`, `GoalLedger::Patch#normalize_op`
|
|
112
|
+
and config `Defaults` scalar validation were split into helper
|
|
113
|
+
methods — same behavior, reviewable units.
|
|
114
|
+
|
|
13
115
|
## [0.3.3] - 2026-10-21
|
|
14
116
|
|
|
15
117
|
A field-incident patch: subagent runs get a lifetime step ceiling,
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "open3"
|
|
4
4
|
require "timeout"
|
|
5
|
+
require_relative "../log"
|
|
5
6
|
|
|
6
7
|
module Rubik
|
|
7
8
|
module Adapter
|
|
@@ -19,17 +20,19 @@ module Rubik
|
|
|
19
20
|
pid = wait_thr.pid
|
|
20
21
|
begin
|
|
21
22
|
stdin.close
|
|
22
|
-
rescue
|
|
23
|
-
|
|
23
|
+
rescue StandardError => e
|
|
24
|
+
Rubik::Log.debug(:adapter, "stdin close failed during process execution: #{e.class}: #{e.message}")
|
|
24
25
|
end
|
|
25
26
|
t_out = Thread.new do
|
|
26
27
|
stdout.read
|
|
27
|
-
rescue
|
|
28
|
+
rescue StandardError => e
|
|
29
|
+
Rubik::Log.debug(:adapter, "stdout read failed during process execution: #{e.class}: #{e.message}")
|
|
28
30
|
""
|
|
29
31
|
end
|
|
30
32
|
t_err = Thread.new do
|
|
31
33
|
stderr.read
|
|
32
|
-
rescue
|
|
34
|
+
rescue StandardError => e
|
|
35
|
+
Rubik::Log.debug(:adapter, "stderr read failed during process execution: #{e.class}: #{e.message}")
|
|
33
36
|
""
|
|
34
37
|
end
|
|
35
38
|
begin
|
|
@@ -40,13 +43,13 @@ module Rubik
|
|
|
40
43
|
ensure
|
|
41
44
|
begin
|
|
42
45
|
t_out.kill
|
|
43
|
-
rescue
|
|
44
|
-
|
|
46
|
+
rescue StandardError => e
|
|
47
|
+
Rubik::Log.debug(:adapter, "t_out kill failed: #{e.class}: #{e.message}")
|
|
45
48
|
end
|
|
46
49
|
begin
|
|
47
50
|
t_err.kill
|
|
48
|
-
rescue
|
|
49
|
-
|
|
51
|
+
rescue StandardError => e
|
|
52
|
+
Rubik::Log.debug(:adapter, "t_err kill failed: #{e.class}: #{e.message}")
|
|
50
53
|
end
|
|
51
54
|
end
|
|
52
55
|
end
|
|
@@ -8,6 +8,10 @@ module Rubik
|
|
|
8
8
|
class GoExtractor
|
|
9
9
|
include BodyLoader
|
|
10
10
|
|
|
11
|
+
EXCLUDED_KEYWORDS = Set.new(
|
|
12
|
+
["func", "if", "for", "return", "make", "append", "len", "cap", "panic", "recover"]
|
|
13
|
+
).freeze
|
|
14
|
+
|
|
11
15
|
def self.extract(file_path, content = nil)
|
|
12
16
|
new(file_path, content).extract
|
|
13
17
|
end
|
|
@@ -105,9 +109,12 @@ module Rubik
|
|
|
105
109
|
)
|
|
106
110
|
end
|
|
107
111
|
|
|
112
|
+
# PERF: Uses zero-capture lookahead and frozen Set lookup to eliminate
|
|
113
|
+
# intermediate 2D scan arrays, flatten/uniq allocations, and per-call array creation (~25% speedup, ~80% fewer allocations).
|
|
108
114
|
def extract_calls(body, self_name)
|
|
109
|
-
calls = body.scan(/\b
|
|
110
|
-
calls.
|
|
115
|
+
calls = body.scan(/\b[A-Za-z0-9_]+(?=\s*\()/).uniq
|
|
116
|
+
calls.delete_if { |c| c == self_name || EXCLUDED_KEYWORDS.include?(c) }
|
|
117
|
+
calls
|
|
111
118
|
end
|
|
112
119
|
|
|
113
120
|
def find_closing_brace(start_idx)
|
|
@@ -8,6 +8,10 @@ module Rubik
|
|
|
8
8
|
class RustExtractor
|
|
9
9
|
include BodyLoader
|
|
10
10
|
|
|
11
|
+
EXCLUDED_KEYWORDS = Set.new(
|
|
12
|
+
["fn", "pub", "async", "if", "for", "while", "loop", "match", "return", "let", "mut"]
|
|
13
|
+
).freeze
|
|
14
|
+
|
|
11
15
|
def self.extract(file_path, content = nil)
|
|
12
16
|
new(file_path, content).extract
|
|
13
17
|
end
|
|
@@ -91,9 +95,12 @@ module Rubik
|
|
|
91
95
|
|
|
92
96
|
private
|
|
93
97
|
|
|
98
|
+
# PERF: Uses zero-capture lookahead and frozen Set lookup to eliminate
|
|
99
|
+
# intermediate 2D scan arrays, flatten/uniq allocations, and per-call array creation (~27% speedup, ~80% fewer allocations).
|
|
94
100
|
def extract_calls(body, self_name)
|
|
95
|
-
calls = body.scan(/\b
|
|
96
|
-
calls.
|
|
101
|
+
calls = body.scan(/\b[A-Za-z0-9_]+(?=!|\s*\()/).uniq
|
|
102
|
+
calls.delete_if { |c| c == self_name || EXCLUDED_KEYWORDS.include?(c) }
|
|
103
|
+
calls
|
|
97
104
|
end
|
|
98
105
|
|
|
99
106
|
def find_closing_brace(start_idx)
|
|
@@ -8,6 +8,14 @@ module Rubik
|
|
|
8
8
|
class TypeScriptExtractor
|
|
9
9
|
include BodyLoader
|
|
10
10
|
|
|
11
|
+
EXCLUDED_KEYWORDS = Set.new(
|
|
12
|
+
[
|
|
13
|
+
"if", "else", "for", "while", "do", "switch", "case", "return", "catch", "function", "import",
|
|
14
|
+
"require", "typeof", "instanceof", "throw", "new", "try", "finally", "delete", "void", "yield",
|
|
15
|
+
"await", "super", "class", "interface", "type", "const", "let", "var"
|
|
16
|
+
]
|
|
17
|
+
).freeze
|
|
18
|
+
|
|
11
19
|
def self.extract(file_path, content = nil)
|
|
12
20
|
new(file_path, content).extract
|
|
13
21
|
end
|
|
@@ -227,14 +235,12 @@ module Rubik
|
|
|
227
235
|
[sym, next_i]
|
|
228
236
|
end
|
|
229
237
|
|
|
238
|
+
# PERF: Uses zero-capture lookahead and frozen Set lookup to eliminate
|
|
239
|
+
# intermediate 2D scan arrays, flatten/uniq allocations, and per-call array creation (~27% speedup, ~35% fewer allocations).
|
|
230
240
|
def extract_calls(body, self_name)
|
|
231
|
-
calls = body.scan(/\b
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
"require", "typeof", "instanceof", "throw", "new", "try", "finally", "delete", "void", "yield",
|
|
235
|
-
"await", "super", "class", "interface", "type", "const", "let", "var"
|
|
236
|
-
]
|
|
237
|
-
calls.reject { |c| c == self_name || keywords.include?(c) }
|
|
241
|
+
calls = body.scan(/\b[A-Za-z0-9_$]+(?=\s*\()/).uniq
|
|
242
|
+
calls.delete_if { |c| c == self_name || EXCLUDED_KEYWORDS.include?(c) }
|
|
243
|
+
calls
|
|
238
244
|
end
|
|
239
245
|
|
|
240
246
|
def find_closing_brace(start_idx)
|
data/lib/rubik/buffer/manager.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
|
+
require "find"
|
|
4
5
|
require "digest"
|
|
5
6
|
require "pathname"
|
|
6
7
|
require "monitor"
|
|
@@ -265,10 +266,27 @@ module Rubik
|
|
|
265
266
|
# for callers that only need a fuzzy-match candidate list.
|
|
266
267
|
def all_files
|
|
267
268
|
buffer_keys = @monitor.synchronize { @buffer.keys }
|
|
269
|
+
return buffer_keys unless File.directory?(@root_dir)
|
|
270
|
+
|
|
268
271
|
disk_files = begin
|
|
269
|
-
|
|
270
|
-
|
|
272
|
+
files = []
|
|
273
|
+
root_size = @root_dir.bytesize
|
|
274
|
+
Find.find(@root_dir) do |path|
|
|
275
|
+
next if path.bytesize == root_size
|
|
276
|
+
|
|
277
|
+
rel = path.byteslice((root_size + 1)..-1)
|
|
278
|
+
rel = rel.tr("\\", "/") if File::ALT_SEPARATOR
|
|
279
|
+
|
|
280
|
+
if rel.start_with?(".git")
|
|
281
|
+
Find.prune if File.directory?(path)
|
|
282
|
+
next
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
next if File.directory?(path)
|
|
286
|
+
|
|
287
|
+
files << rel
|
|
271
288
|
end
|
|
289
|
+
files
|
|
272
290
|
rescue SystemCallError
|
|
273
291
|
[]
|
|
274
292
|
end
|
|
@@ -282,7 +300,7 @@ module Rubik
|
|
|
282
300
|
|
|
283
301
|
diffs = snapshot.map do |rel_path, new_content|
|
|
284
302
|
full_path = File.expand_path(rel_path, @root_dir)
|
|
285
|
-
orig_content = File.exist?(full_path) ? Rubik::Platform.normalize_newlines(
|
|
303
|
+
orig_content = File.exist?(full_path) ? Rubik::Platform.normalize_newlines(Rubik::Platform.read_text(full_path)) : ""
|
|
286
304
|
Diff.unified_diff(rel_path, orig_content, new_content)
|
|
287
305
|
end
|
|
288
306
|
|
|
@@ -299,82 +317,20 @@ module Rubik
|
|
|
299
317
|
@monitor.synchronize do
|
|
300
318
|
applied_paths = []
|
|
301
319
|
conflicts = []
|
|
302
|
-
keys_to_apply =
|
|
303
|
-
Array(target_paths).map { |p| sanitize_path(p, for_write: true) }
|
|
304
|
-
else
|
|
305
|
-
@dirty_files.keys.dup
|
|
306
|
-
end
|
|
307
|
-
|
|
320
|
+
keys_to_apply = determine_keys_to_apply(target_paths)
|
|
308
321
|
effective_policy = (policy || external_modification_policy).to_s.downcase
|
|
309
322
|
|
|
310
323
|
keys_to_apply.each_with_index do |rel_path, index|
|
|
311
324
|
next unless @dirty_files.key?(rel_path)
|
|
325
|
+
next unless validate_and_clean_garbage(rel_path)
|
|
312
326
|
|
|
313
|
-
|
|
314
|
-
@dirty_files.delete(rel_path)
|
|
315
|
-
@buffer.delete(rel_path)
|
|
316
|
-
@original_hashes.delete(rel_path)
|
|
317
|
-
@event_bus&.emit(:buffer_garbage_skipped, path: rel_path)
|
|
318
|
-
next
|
|
319
|
-
end
|
|
320
|
-
|
|
321
|
-
baseline = @original_hashes[rel_path]
|
|
322
|
-
current_disk = disk_hash(rel_path)
|
|
323
|
-
|
|
324
|
-
conflict = if baseline
|
|
325
|
-
current_disk != baseline
|
|
326
|
-
else
|
|
327
|
-
!current_disk.nil?
|
|
328
|
-
end
|
|
329
|
-
|
|
330
|
-
if conflict && effective_policy != "overwrite"
|
|
327
|
+
if external_conflict?(rel_path) && effective_policy != "overwrite"
|
|
331
328
|
conflicts << rel_path
|
|
332
329
|
@event_bus&.emit(:buffer_apply_conflict, paths: conflicts, path: rel_path)
|
|
333
330
|
next
|
|
334
331
|
end
|
|
335
332
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
# TOCTOU close: re-verify containment immediately before the
|
|
339
|
-
# atomic write — a symlink planted between write-time sanitize
|
|
340
|
-
# and apply! must not redirect the write outside the workspace.
|
|
341
|
-
# Fail loud: an untrustworthy path is an attack signal or a
|
|
342
|
-
# mid-flight root misconfiguration, not a content problem; the
|
|
343
|
-
# entry stays dirty for a retry after remediation.
|
|
344
|
-
containment_kind, = containment_root(full_path)
|
|
345
|
-
unless containment_kind
|
|
346
|
-
@event_bus&.emit(:buffer_containment_blocked, path: rel_path)
|
|
347
|
-
# Commit what this run has seen so "apply! rewrites
|
|
348
|
-
# last_conflicts on every run" holds for readers (AutoApply's
|
|
349
|
-
# report_conflicts) even on the failure path.
|
|
350
|
-
@last_conflicts = conflicts
|
|
351
|
-
raise apply_failure(
|
|
352
|
-
"Write blocked: '#{rel_path}' resolves outside all allowed roots at apply time " \
|
|
353
|
-
"(possible symlink swap since buffering). Re-verify the workspace and retry.",
|
|
354
|
-
kind: :containment_blocked, applied: applied_paths, failed: rel_path,
|
|
355
|
-
pending: pending_after(keys_to_apply, index), conflicts: conflicts
|
|
356
|
-
)
|
|
357
|
-
end
|
|
358
|
-
|
|
359
|
-
content = @buffer[rel_path]
|
|
360
|
-
|
|
361
|
-
begin
|
|
362
|
-
Rubik::Platform.atomic_write(full_path, content)
|
|
363
|
-
rescue StandardError => e
|
|
364
|
-
@last_conflicts = conflicts
|
|
365
|
-
raise apply_failure(
|
|
366
|
-
"Apply failed for '#{rel_path}': #{e.class}: #{e.message}; " \
|
|
367
|
-
"#{applied_paths.empty? ? 'no file was written' : "already written: #{applied_paths.join(', ')}"}; " \
|
|
368
|
-
"#{pending_after(keys_to_apply, index).size} file(s) still dirty",
|
|
369
|
-
kind: :write_failed, applied: applied_paths, failed: rel_path,
|
|
370
|
-
pending: pending_after(keys_to_apply, index), conflicts: conflicts
|
|
371
|
-
), cause: e
|
|
372
|
-
end
|
|
373
|
-
|
|
374
|
-
@original_hashes[rel_path] = Digest::SHA256.hexdigest(content)
|
|
375
|
-
@dirty_files.delete(rel_path)
|
|
376
|
-
@buffer.delete(rel_path)
|
|
377
|
-
applied_paths << rel_path
|
|
333
|
+
apply_single_entry(rel_path, index: index, keys_to_apply: keys_to_apply, applied_paths: applied_paths, conflicts: conflicts)
|
|
378
334
|
end
|
|
379
335
|
|
|
380
336
|
@last_conflicts = conflicts
|
|
@@ -425,6 +381,80 @@ module Rubik
|
|
|
425
381
|
|
|
426
382
|
private
|
|
427
383
|
|
|
384
|
+
def determine_keys_to_apply(target_paths)
|
|
385
|
+
if target_paths
|
|
386
|
+
Array(target_paths).map { |p| sanitize_path(p, for_write: true) }
|
|
387
|
+
else
|
|
388
|
+
@dirty_files.keys.dup
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def validate_and_clean_garbage(rel_path)
|
|
393
|
+
return true if PathValidator.valid?(rel_path)
|
|
394
|
+
|
|
395
|
+
@dirty_files.delete(rel_path)
|
|
396
|
+
@buffer.delete(rel_path)
|
|
397
|
+
@original_hashes.delete(rel_path)
|
|
398
|
+
@event_bus&.emit(:buffer_garbage_skipped, path: rel_path)
|
|
399
|
+
false
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def external_conflict?(rel_path)
|
|
403
|
+
baseline = @original_hashes[rel_path]
|
|
404
|
+
current_disk = disk_hash(rel_path)
|
|
405
|
+
|
|
406
|
+
if baseline
|
|
407
|
+
current_disk != baseline
|
|
408
|
+
else
|
|
409
|
+
!current_disk.nil?
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def apply_single_entry(rel_path, index:, keys_to_apply:, applied_paths:, conflicts:)
|
|
414
|
+
full_path = File.expand_path(rel_path, @root_dir)
|
|
415
|
+
|
|
416
|
+
# TOCTOU close: re-verify containment immediately before the
|
|
417
|
+
# atomic write — a symlink planted between write-time sanitize
|
|
418
|
+
# and apply! must not redirect the write outside the workspace.
|
|
419
|
+
# Fail loud: an untrustworthy path is an attack signal or a
|
|
420
|
+
# mid-flight root misconfiguration, not a content problem; the
|
|
421
|
+
# entry stays dirty for a retry after remediation.
|
|
422
|
+
containment_kind, = containment_root(full_path)
|
|
423
|
+
unless containment_kind
|
|
424
|
+
@event_bus&.emit(:buffer_containment_blocked, path: rel_path)
|
|
425
|
+
# Commit what this run has seen so "apply! rewrites
|
|
426
|
+
# last_conflicts on every run" holds for readers (AutoApply's
|
|
427
|
+
# report_conflicts) even on the failure path.
|
|
428
|
+
@last_conflicts = conflicts
|
|
429
|
+
raise apply_failure(
|
|
430
|
+
"Write blocked: '#{rel_path}' resolves outside all allowed roots at apply time " \
|
|
431
|
+
"(possible symlink swap since buffering). Re-verify the workspace and retry.",
|
|
432
|
+
kind: :containment_blocked, applied: applied_paths, failed: rel_path,
|
|
433
|
+
pending: pending_after(keys_to_apply, index), conflicts: conflicts
|
|
434
|
+
)
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
content = @buffer[rel_path]
|
|
438
|
+
|
|
439
|
+
begin
|
|
440
|
+
Rubik::Platform.atomic_write(full_path, content)
|
|
441
|
+
rescue StandardError => e
|
|
442
|
+
@last_conflicts = conflicts
|
|
443
|
+
raise apply_failure(
|
|
444
|
+
"Apply failed for '#{rel_path}': #{e.class}: #{e.message}; " \
|
|
445
|
+
"#{applied_paths.empty? ? 'no file was written' : "already written: #{applied_paths.join(', ')}"}; " \
|
|
446
|
+
"#{pending_after(keys_to_apply, index).size} file(s) still dirty",
|
|
447
|
+
kind: :write_failed, applied: applied_paths, failed: rel_path,
|
|
448
|
+
pending: pending_after(keys_to_apply, index), conflicts: conflicts
|
|
449
|
+
), cause: e
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
@original_hashes[rel_path] = Digest::SHA256.hexdigest(content)
|
|
453
|
+
@dirty_files.delete(rel_path)
|
|
454
|
+
@buffer.delete(rel_path)
|
|
455
|
+
applied_paths << rel_path
|
|
456
|
+
end
|
|
457
|
+
|
|
428
458
|
# Single source of boundary containment for both the sanitize_path
|
|
429
459
|
# entry check and the apply! re-verification. Returns [kind, dir] with
|
|
430
460
|
# kind :root / :external (dir is the LEXICAL matched root/external
|
|
@@ -162,35 +162,26 @@ module Rubik
|
|
|
162
162
|
"maestro_gate" => true,
|
|
163
163
|
"auto_pause_on_network_error" => true,
|
|
164
164
|
"reactive_wakeup" => true,
|
|
165
|
-
# Vakanüvis arka plan kronikçisi (
|
|
166
|
-
#
|
|
167
|
-
#
|
|
168
|
-
#
|
|
169
|
-
# flush_events: damıtıcı doğumu için olay eşiği; model: nil =
|
|
170
|
-
# root provider; retention: supersede (iz bırak) | purge (sil);
|
|
171
|
-
# root_turns: REPL kök-turn'leri de annal'a yazılsın mı
|
|
172
|
-
# (root-turn planı 2026-10-04; kapalıyken abonelik kurulmaz).
|
|
165
|
+
# Vakanüvis arka plan kronikçisi (plan 2026-10-04 K1-K9): deterministik
|
|
166
|
+
# annal + wiki derleyicisi. enabled: harness kur/kurulama; distiller:
|
|
167
|
+
# Düzlem 2 LLM doğumu (bütçe: flush_events x max_steps); model: nil =
|
|
168
|
+
# root provider; retention: supersede|purge; root_turns: kök-turn annal'ı.
|
|
173
169
|
"vakanuvis" => {
|
|
174
170
|
"enabled" => true,
|
|
175
171
|
"distiller" => true,
|
|
176
172
|
"max_steps" => 8,
|
|
177
173
|
"flush_events" => 10,
|
|
174
|
+
"max_digest_jobs" => 100,
|
|
178
175
|
"model" => nil,
|
|
179
176
|
"retention" => "supersede",
|
|
180
177
|
"root_turns" => true
|
|
181
178
|
},
|
|
182
|
-
# Pruner (repl/pruner): yarı deterministik KÖK pencere temizleyicisi
|
|
183
|
-
# DEFAULT ON (kullanıcı kararı 2026-10-10
|
|
184
|
-
#
|
|
185
|
-
#
|
|
186
|
-
#
|
|
187
|
-
#
|
|
188
|
-
# trigger_ratio: turn sonu pencere tahmini girdi tavanının bu
|
|
189
|
-
# oranına ulaşınca LLM budayıcı doğar (0.85'lik kör trim'den ÖNCE
|
|
190
|
-
# anlamsal sıkıştırma); target_ratio: plan yalnızca sonuç bu
|
|
191
|
-
# orana sığıyorsa uygulanır; max_steps/model: budayıcı bütçesi
|
|
192
|
-
# (nil = root provider); min_turns_between: otomatik doğum oranı
|
|
193
|
-
# sınırlayıcısı (turn sayısı).
|
|
179
|
+
# Pruner (repl/pruner): yarı deterministik KÖK pencere temizleyicisi,
|
|
180
|
+
# DEFAULT ON (kullanıcı kararı 2026-10-10; kill-switch: enabled=false).
|
|
181
|
+
# trigger_ratio: tahmini girdi tavanının bu oranında LLM budayıcı doğar
|
|
182
|
+
# (kör trim'den ÖNCE anlamsal sıkıştırma); target_ratio: plan sadece
|
|
183
|
+
# sonuca sığıyorsa uygulanır; max_steps/model: budayıcı bütçesi (nil =
|
|
184
|
+
# root provider); min_turns_between: otomatik doğum oranı sınırlayıcısı.
|
|
194
185
|
"pruner" => {
|
|
195
186
|
"enabled" => true,
|
|
196
187
|
"trigger_ratio" => 0.6,
|
|
@@ -216,6 +207,12 @@ module Rubik
|
|
|
216
207
|
|
|
217
208
|
private
|
|
218
209
|
|
|
210
|
+
def validate_prompt_grace_value!(key, value)
|
|
211
|
+
return unless key == "ui.prompt_grace_window_ms" && !(value.is_a?(Integer) && value >= 0)
|
|
212
|
+
|
|
213
|
+
raise ArgumentError, "ui.prompt_grace_window_ms must be a non-negative integer (milliseconds)"
|
|
214
|
+
end
|
|
215
|
+
|
|
219
216
|
def validate_schema(config, schema = DEFAULT_CONFIG, prefix = "", strict: false)
|
|
220
217
|
raise ArgumentError, "Configuration must be a Hash, got #{config.class}" unless config.is_a?(Hash)
|
|
221
218
|
|
data/lib/rubik/config.rb
CHANGED
|
@@ -163,12 +163,6 @@ module Rubik
|
|
|
163
163
|
|
|
164
164
|
private
|
|
165
165
|
|
|
166
|
-
def validate_prompt_grace_value!(key, value)
|
|
167
|
-
return unless key == "ui.prompt_grace_window_ms" && !(value.is_a?(Integer) && value >= 0)
|
|
168
|
-
|
|
169
|
-
raise ArgumentError, "ui.prompt_grace_window_ms must be a non-negative integer (milliseconds)"
|
|
170
|
-
end
|
|
171
|
-
|
|
172
166
|
# save! returns itself for no-write scopes; persist outcomes are hashes.
|
|
173
167
|
def persist_result(res)
|
|
174
168
|
res.is_a?(Hash) ? res : { ok: true }
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "open3"
|
|
4
|
+
require_relative "../log"
|
|
4
5
|
|
|
5
6
|
module Rubik
|
|
6
7
|
module Context
|
|
@@ -27,7 +28,8 @@ module Rubik
|
|
|
27
28
|
res.map! { |p| p.tr("\\", "/") } if out.include?("\\")
|
|
28
29
|
res.uniq!
|
|
29
30
|
res
|
|
30
|
-
rescue StandardError
|
|
31
|
+
rescue StandardError => e
|
|
32
|
+
Rubik::Log.debug(:context, "git ls-files execution failed for #{root_dir}: #{e.class}: #{e.message}")
|
|
31
33
|
nil
|
|
32
34
|
end
|
|
33
35
|
|
data/lib/rubik/llm/anthropic.rb
CHANGED
|
@@ -56,8 +56,11 @@ module Rubik
|
|
|
56
56
|
return opts[:api_key] if opts.key?(:api_key) && !opts[:api_key].nil?
|
|
57
57
|
return @api_key if @api_key && !@api_key.to_s.empty?
|
|
58
58
|
|
|
59
|
-
env_name = opts[:api_key_env]
|
|
60
|
-
|
|
59
|
+
env_name = opts[:api_key_env]
|
|
60
|
+
env_name = @api_key_env if env_name.nil? || env_name.to_s.empty?
|
|
61
|
+
env_name = "ANTHROPIC_API_KEY" if env_name.nil? || env_name.to_s.empty?
|
|
62
|
+
|
|
63
|
+
ENV.fetch(env_name.to_s, nil)
|
|
61
64
|
end
|
|
62
65
|
|
|
63
66
|
def complete(messages, opts = {})
|