rubik 0.1.6 → 0.2.0
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 +88 -0
- data/README.md +30 -17
- data/lib/rubik/ast/extraction_result.rb +37 -0
- data/lib/rubik/ast/extractor.rb +35 -9
- data/lib/rubik/ast/graph.rb +27 -1
- data/lib/rubik/ast/treesitter/c_walker.rb +136 -28
- data/lib/rubik/ast/treesitter_extractor.rb +45 -17
- data/lib/rubik/buffer/apply_contract.rb +78 -0
- data/lib/rubik/buffer/manager.rb +61 -5
- data/lib/rubik/concurrency/event_loop.rb +56 -44
- data/lib/rubik/concurrency/scheduler.rb +7 -24
- data/lib/rubik/context/git_lister.rb +6 -1
- data/lib/rubik/context/scanner.rb +15 -3
- data/lib/rubik/mcp/transport/stdio.rb +14 -11
- data/lib/rubik/platform/process.rb +74 -2
- data/lib/rubik/plugin/manager.rb +148 -71
- data/lib/rubik/plugin/package_inventory.rb +84 -0
- data/lib/rubik/plugin/requires_dag.rb +40 -0
- data/lib/rubik/plugin/storage.rb +100 -14
- data/lib/rubik/repl/commands/buffer.rb +28 -18
- data/lib/rubik/runtime/repair_probe.rb +98 -0
- data/lib/rubik/security/blocked_guidance.rb +19 -0
- data/lib/rubik/security/guard.rb +115 -6
- data/lib/rubik/tools/builtins/lifecycle.rb +129 -136
- data/lib/rubik/version.rb +1 -1
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b9f5ba8b65658f3899e0ade863faf5f64a11c80a4c43d18f5230e3248bd215ae
|
|
4
|
+
data.tar.gz: 2132bc4a2fb1927a6a94fa8869a384bb2ca5c1d93f5d45e630fef394a42299b4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6de6f5477ca7fbee7099efec31dce72e44f3fb95a2a69b891f252e1c88d794be5e3a4139c3077197527c05c2fe2de3d5971229dad2da7e91c8a0802503243a71
|
|
7
|
+
data.tar.gz: 42c0c6074a7e8902e98bae63f0fe0b45b04a0a7f83cc68b3e63f12a2a88075ba768debb551b1837269c0baf69ac8435b86534bb7c612da91e9f91073388eda28
|
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,700+ 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,94 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
10
10
|
|
|
11
11
|
Nothing yet.
|
|
12
12
|
|
|
13
|
+
## [0.2.0] - 2026-10-16
|
|
14
|
+
|
|
15
|
+
The first minor of the 0.2 era is a hardening minor: a reproducible
|
|
16
|
+
runtime validation harness with capacity proofs, a fail-closed
|
|
17
|
+
security posture across the command guard, transactional plugin
|
|
18
|
+
reload, and failure reporting that names what actually happened.
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- **Reproducible runtime validation harness (#160, closes #151):**
|
|
23
|
+
`bundle exec ruby dev/validate_runtime_capacity.rb` runs without
|
|
24
|
+
provider credentials and writes a dated report — environment, seed,
|
|
25
|
+
fixture sizes, raw metrics, resource checkpoints, thresholds and
|
|
26
|
+
verdicts. Nine lifecycle families soak for the configured 1,000
|
|
27
|
+
epochs (real subprocess shutdown, failed JSON-RPC handshake,
|
|
28
|
+
crash/recovery); two single-run probes cover in-flight
|
|
29
|
+
disable/re-enable and known-descendant cleanup. Missing
|
|
30
|
+
capabilities report INCONCLUSIVE (exit 2), failed thresholds FAIL
|
|
31
|
+
(exit 1). The soak also surfaced production fixes that ship here:
|
|
32
|
+
nonblocking IO hooks keep their return codes and minimum lengths,
|
|
33
|
+
select wakes only fibers interested in each readiness direction,
|
|
34
|
+
queued wakeups carry their park generation so stale events cannot
|
|
35
|
+
resume a later operation, and a crashed MCP pipe closes with the
|
|
36
|
+
process tree terminated before stdin (recursive descendant
|
|
37
|
+
discovery on Unix).
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- **Command classifier fails closed (#143):** a classifier crash or a
|
|
42
|
+
partial tree-sitter parse no longer downgrades to the catastrophic
|
|
43
|
+
regex gate — both raise structured blocks (`:classifier_error` /
|
|
44
|
+
`:parse_ambiguous`) with the crash cause audited; the catastrophic
|
|
45
|
+
regex keeps first refusal for precise destructive-shape reasons and
|
|
46
|
+
the operator allowlist stays the only valve. SECURITY.md's
|
|
47
|
+
fallback residual is updated to the fail-closed policy.
|
|
48
|
+
- **Plugins verify whole packages and reload transactionally
|
|
49
|
+
(#144):** directory-plugin manifests hash every regular file under
|
|
50
|
+
the package root (additions, deletions and modifications audited;
|
|
51
|
+
symlinks and special files are containment violations); legacy
|
|
52
|
+
entry-file-only manifests require explicit re-consent; and
|
|
53
|
+
`reload_plugin` discovers, verifies and prepares the replacement
|
|
54
|
+
before touching the running version — a failed reload leaves the
|
|
55
|
+
old version running instead of claiming success.
|
|
56
|
+
- **Runtime repair verifies before promoting (#145):** LSP candidates
|
|
57
|
+
must complete an isolated initialize handshake and other targets
|
|
58
|
+
run bounded argv probes that preserve the proposed arguments; live
|
|
59
|
+
promotion records inverses and unwinds the transaction on failure,
|
|
60
|
+
rolling back to the previously configured client (with an honest
|
|
61
|
+
report if restoration itself fails).
|
|
62
|
+
- **Buffer apply names its authority and its partial truth (#146):**
|
|
63
|
+
`buffer.model_can_apply` is honored in every registry rebuild
|
|
64
|
+
(branch workspaces included); a multi-file `apply!` failure raises
|
|
65
|
+
a structured `ApplyFailedError` carrying what reached disk, what
|
|
66
|
+
failed and what a retry still writes; retry is incremental and the
|
|
67
|
+
tool and `!apply` render one shared failure vocabulary — a partial
|
|
68
|
+
apply is never reported as "Successfully applied".
|
|
69
|
+
- **C++ extraction sees classes and reports parse status (#147):**
|
|
70
|
+
the shared C/C++ walker no longer crashes on unknown declarator
|
|
71
|
+
shapes — class/struct/union symbols, namespaces, in-class members
|
|
72
|
+
(definitions, declarations, ctors, operators) and templates are
|
|
73
|
+
recorded; the extractor returns a structured status
|
|
74
|
+
(`:ok`/`:partial`/`:failed`/`:unavailable`) and a failed reindex
|
|
75
|
+
keeps the file's previously indexed symbols instead of silently
|
|
76
|
+
deleting them.
|
|
77
|
+
- **Plugin reinstallation survives symlinked ancestors (#158):**
|
|
78
|
+
resolved-source × lexical-destination comparison could delete the
|
|
79
|
+
source before copying it; file identity now detects an
|
|
80
|
+
already-installed directory.
|
|
81
|
+
- **Unit tests are isolated from external network and terminal env
|
|
82
|
+
(#148):** handshake-deadline assertions no longer depend on cleanup
|
|
83
|
+
timing.
|
|
84
|
+
|
|
85
|
+
### Changed
|
|
86
|
+
|
|
87
|
+
- **CI measures the platform matrix (#158):** four jobs — Linux Ruby
|
|
88
|
+
3.4/4.0, macOS Ruby 4.0, Windows Ruby 4.0 — each install the gem
|
|
89
|
+
into an isolated GEM_HOME, run CLI/require smoke and all three
|
|
90
|
+
suite loaders, and publish evidence artifacts; README and TESTING
|
|
91
|
+
replace the universal green-run guarantee with commit-specific
|
|
92
|
+
evidence.
|
|
93
|
+
- **Docs calibrate absolute guarantees (#159):** README, SECURITY,
|
|
94
|
+
CONFIG, PLUGINS and SELF_EVOLUTION swap "zero-risk"-style claims
|
|
95
|
+
for concrete preconditions and bounded behavior (the full buffer
|
|
96
|
+
review gate is `auto_apply: manual` + `model_can_apply: false`).
|
|
97
|
+
- **Cold scan gets cheaper (#161):** integer-micros mtime math, a
|
|
98
|
+
backslash translation that runs only when backslashes exist, and
|
|
99
|
+
in-place dedup; the fast scan keeps bracket-class ignores intact.
|
|
100
|
+
|
|
13
101
|
## [0.1.6] - 2026-10-15
|
|
14
102
|
|
|
15
103
|
The execution-contract release: subagent runs become first-class,
|
data/README.md
CHANGED
|
@@ -7,17 +7,18 @@ English | [Türkçe](README.tr.md)
|
|
|
7
7
|
[](https://rubygems.org/gems/rubik)
|
|
8
8
|
[](https://rubygems.org/gems/rubik)
|
|
9
9
|
[](https://www.ruby-lang.org)
|
|
10
|
-
[]()
|
|
10
|
+
[](https://github.com/ozgurulukir/rubik/actions/workflows/ci.yml)
|
|
11
11
|
[](AGENTS.md)
|
|
12
12
|
[](COMMANDS.md)
|
|
13
13
|
[](CONFIG.md)
|
|
14
|
-
[](https://github.com/ozgurulukir/rubik/actions/workflows/ci.yml)
|
|
15
|
+
[](https://github.com/ozgurulukir/rubik/actions/workflows/ci.yml)
|
|
15
16
|
|
|
16
17
|
---
|
|
17
18
|
|
|
18
19
|
## 👋 What is Rubik?
|
|
19
20
|
|
|
20
|
-
Rubik is a terminal-native autonomous coding agent and interactive REPL in pure Ruby. A **maestro** agent orchestrates specialized subagents (research / coder / self) over a 34-tool belt
|
|
21
|
+
Rubik is a terminal-native autonomous coding agent and interactive REPL in pure Ruby. A **maestro** agent orchestrates specialized subagents (research / coder / self) over a 34-tool belt: file edits stage in a virtual in-memory buffer (`!diff` → `!apply`, with configurable model authority and auto-apply policies), managed runtime side effects register LIFO inverses to support dynamic reloads and teardowns without process restarts, and failing services can be diagnosed and repaired live. It supports Linux, macOS, and Windows across a bounded CI matrix, connects to OpenAI-compatible endpoints, Anthropic, or local Ollama, and the runtime is embeddable from Ruby with `require "rubik"`.
|
|
21
22
|
|
|
22
23
|
---
|
|
23
24
|
|
|
@@ -27,6 +28,15 @@ Rubik is a terminal-native autonomous coding agent and interactive REPL in pure
|
|
|
27
28
|
|
|
28
29
|
Requires **Ruby 3.4+**:
|
|
29
30
|
|
|
31
|
+
CI tests Linux on Ruby 3.4 (minimum) and 4.0, and macOS and Windows on
|
|
32
|
+
Ruby 4.0. Each job installs dependencies, runs the full unit/integration suite,
|
|
33
|
+
and builds, installs and smokes the gem (`rubik --version` and `require "rubik"`).
|
|
34
|
+
See [CI runs](https://github.com/ozgurulukir/rubik/actions/workflows/ci.yml)
|
|
35
|
+
for results on a specific commit; platform artifacts contain environment details,
|
|
36
|
+
installation/smoke logs, test counts and skip reasons. Other Ruby/OS combinations
|
|
37
|
+
are not covered by this bounded matrix.
|
|
38
|
+
|
|
39
|
+
|
|
30
40
|
```bash
|
|
31
41
|
gem install rubik
|
|
32
42
|
```
|
|
@@ -152,33 +162,36 @@ Applied 1 file(s) before test. All 12 tests passed!
|
|
|
152
162
|
```
|
|
153
163
|
|
|
154
164
|
> [!TIP]
|
|
155
|
-
> **Safe Execution (Tree-Sitter AST) &
|
|
156
|
-
>
|
|
165
|
+
> **Safe Execution (Tree-Sitter AST) & Pluggable OS Isolation (Bubblewrap / Container):**
|
|
166
|
+
>
|
|
167
|
+
> Before running any command, Rubik keeps the `Security::Guard` shield active (Safe Execution), which detects dangerous commands, pipeline poisoning, and directory escapes ahead of time via Tree-Sitter syntax-tree (AST) analysis for Bash and PowerShell plus a database of 172 CLI YAML rules. The command gate fails closed on unrecognized syntax or classifier errors. Hardware-enforced OS isolation is optional and disabled by default (`sandbox.enabled: false`); enabling it on Linux requires Bubblewrap (`bwrap`) or OCI containers (`docker`/`podman`):
|
|
157
168
|
> ```bash
|
|
158
|
-
> !sandbox on #
|
|
159
|
-
> !config set sandbox.allow_network true -p #
|
|
169
|
+
> !sandbox on # Enables hardware/container OS isolation on Linux
|
|
170
|
+
> !config set sandbox.allow_network true -p # Grants outbound network access within the sandbox
|
|
160
171
|
> ```
|
|
161
172
|
|
|
162
173
|
---
|
|
163
174
|
|
|
164
175
|
## ⚡ Key Features
|
|
165
176
|
|
|
166
|
-
- 🌐 **
|
|
167
|
-
- 🛡️ **
|
|
177
|
+
- 🌐 **Cross-Platform Support (Linux, macOS, Windows):** Supports Windows 11 / Windows Server environments (PowerShell, Command Prompt, Windows Terminal) alongside POSIX systems (Linux, macOS). Uses pure-Ruby binary resolution, NTFS case-insensitivity handling, RFC 8089 LSP URIs, and native Windows CLI (`.cmd`, `.bat`, `.ps1`) wrappers. Verified against CI's bounded matrix (Windows gem installation compiles `tree_sitter_language_pack` from source using the Rust GNU toolchain; see [.github/workflows/ci.yml](.github/workflows/ci.yml)).
|
|
178
|
+
- 🛡️ **In-Memory Virtual Buffer & Staged Writes:** When the model writes code, file changes stage in an in-memory virtual buffer before touching disk. You can inspect changes with `!diff`, apply them with `!apply`, or discard unapplied buffer edits with `!undo`.
|
|
179
|
+
- **Authority & Review Gates:** Staging alone does not enforce manual human review. By default, `buffer.auto_apply: manual` stages changes, but `buffer.model_can_apply: true` allows the model to flush its own edits via the `apply_buffer` tool. To guarantee human operator review before any disk modification, set `buffer.model_can_apply: false` alongside `buffer.auto_apply: manual`. When `buffer.auto_apply` is set to `on_test`, `on_finish`, or `always`, the runtime automatically flushes dirty buffer edits to disk at trigger boundaries.
|
|
180
|
+
- **Atomic File Writes vs Multi-File Transactions:** Each individual file write is atomic (written to a temporary file and atomically renamed/replaced), but applying changes across multiple files is not an all-or-nothing multi-file transaction. If an error occurs midway (e.g. permission error or containment block), previously written files remain on disk while failed and pending files stay dirty in the buffer with a structured partial report (`applied`, `failed`, `pending`, `conflicts`).
|
|
168
181
|
- 🏢 **Multi-Workspace & Extra Root Directories:** In monorepo or sibling-package layouts (`../shared-kernel`, `../proto`), you can safely attach external root directories (`!workspace add`) and include them in the buffer and the AST index in read-only or writable (`--rw`) mode.
|
|
169
|
-
- ⚡ **Live LSP Compiler Diagnostics:**
|
|
170
|
-
- 🌳 **Multilingual Tree-Sitter & Prism Hybrid AST Engine:**
|
|
182
|
+
- ⚡ **Live LSP Compiler Diagnostics:** When configured language server binaries (`rust-analyzer`, `gopls`, `ruff`, `typescript-language-server`, `typeprof`, etc.) are installed on the host `PATH`, Rubik connects over stdio JSON-RPC to feed compiler diagnostics back to the agent after edits. Language servers are external dependencies; if a server binary is missing, unconfigured, or fails the handshake, Rubik falls back to syntax and AST analysis without blocking the session (see [LSP.md](LSP.md)).
|
|
183
|
+
- 🌳 **Multilingual Tree-Sitter & Prism Hybrid AST Engine:** Employs built-in **Prism** for Ruby and **Tree-Sitter** grammars for Python, Go, Rust, TypeScript, TSX, JavaScript, C, C++, and Zig to avoid string/comment lexical confusion during symbol parsing. If Tree-Sitter grammars or C extensions are unavailable or encounter parse errors, the engine falls back to legacy regex extractors (which have known structural limits with complex or nested code). The symbol and call graph (`!symbols`, `!outline`, `!refs`) scans asynchronously in the background and leverages an on-disk cache (`.rubik/cache/ast_index.json`) to minimize cold-start indexing overhead across restarts; cold and warm timings for a 150-file synthetic Ruby fixture are recorded in [_report/runtime-capacity-validation-2026-10-16.md](_report/runtime-capacity-validation-2026-10-16.md).
|
|
171
184
|
- 🤖 **Dynamic LLM Providers & Multi-Role Routing:** Light/heavy task routing with support for all OpenAI-compatible endpoints (DeepSeek, Groq, OpenRouter, Mistral, vLLM), Anthropic Claude, OpenAI, and local Ollama.
|
|
172
|
-
- 🧬 **Spatiotemporal Composability & Self-Evolving Runtime:** The model can synthesize new REPL commands (`synthesize_command`) or built-in tools (`synthesize_tool`) at runtime;
|
|
173
|
-
- 🩹 **Self-Healing Runtime (Service Repair Plane):** Runtime components never require a process restart. `inspect_runtime` exposes the live fiber/coeffect/service state; `repair_runtime_service` diagnoses a failing component (LSP server, test runner, linter, or shell command), probes replacement commands with pre-flight verification in an isolated sub-context, and re-provides the repaired service — session state, caches, and conversations stay intact. `provide_service` registers new services live;
|
|
185
|
+
- 🧬 **Spatiotemporal Composability & Self-Evolving Runtime:** The model can synthesize new REPL commands (`synthesize_command`) or built-in tools (`synthesize_tool`) at runtime; managed runtime effects registered through the context are unwound with LIFO inverses ($\Omega$).
|
|
186
|
+
- 🩹 **Self-Healing Runtime (Service Repair Plane):** Runtime components never require a process restart. `inspect_runtime` exposes the live fiber/coeffect/service state; `repair_runtime_service` diagnoses a failing component (LSP server, test runner, linter, or shell command), probes replacement commands with pre-flight verification in an isolated sub-context, and re-provides the repaired service — session state, caches, and conversations stay intact. `provide_service` registers new services live. Repair probes are bounded and disposable; failed promotion or persistence restores session state. Successfully saved operator configuration survives lifecycle cleanup. See [runtime repair](RUNTIME_REPAIR.md) for probe and rollback contracts.
|
|
174
187
|
- 📋 **Structured Goal Ledger (SKILL.state adaptation):** the `!goal` loop keeps a rev-guarded ΔΣ ledger (objective, tasks, findings, decisions, open questions, environment notes) instead of trusting lossy conversation memory. The maestro patches it with the root-only `update_goal_ledger` tool (classified syntax/schema/conflict feedback, bounded retries), the current Σ is pinned through every compaction tier and re-injected each step, every revision is appended to storage (`goal-ledger` ref), declarative `verify` specs are re-checked at step boundaries (a failed check flips the task to `stale` — the world-drift signal), and banked tool results compact into per-row `<absorbed_into_ledger>` tombstones. Default ON (`goal.ledger.*`, operator-only; opt-out per key).
|
|
175
188
|
- 🧹 **Context Pruner (Semi-Deterministic Window Cleanup):** Long sessions accumulate stale exploration in the maestro's conversation window. A Vakanüvis-style two-plane pruner cleans it: a silent LLM subagent proposes a keep/drop/summarize plan, and the harness validates it deterministically (goal pinning, tool-use pairing, head/tail preservation bands, token budget) and applies it atomically at the next turn boundary — never mid-turn. It auto-fires at 60% of the input ceiling (before the blind drop-oldest trimmer would) or on demand via the `prune_context` tool and `!prune`. Default ON since 2026-10-10 (`subagents.pruner.*`, operator-only; opt-out `enabled: false`); the deterministic compaction stack always remains the safety net.
|
|
176
|
-
- 🔌 **Drop-in Plugin System (User Plugins):** `.rb`
|
|
177
|
-
- 🔌 **Model Context Protocol (MCP) & Spatiotemporal Tool Isolation:** Dynamically attaches external MCP servers (`filesystem`, `fetch`, databases, GitHub, etc.) over the stdio transport (`!mcp connect`, `!mcp add`). Discovered tools join the registry as `Rubik::Tools::BaseTool` proxies;
|
|
189
|
+
- 🔌 **Drop-in Plugin System (User Plugins):** Drop-in `.rb` files or package directories under `~/.rubik/plugins` (global) or `.rubik/plugins` (project) load dynamically (`!plugin add`, `!plugin reload`). Integrity is gated by SHA-256 digests in `.manifest.json` (full package tree for directory plugins). Plugins execute arbitrary Ruby in the host process; manifests protect against untracked or modified files, but do not sandbox plugin runtime behavior. Foreign repositories without a manifest require explicit review or operator consent (`RUBIK_BOOTSTRAP_OVERLAYS=1` / `RUBIK_TRUST_OVERLAYS=1`). See [PLUGINS.md](PLUGINS.md).
|
|
190
|
+
- 🔌 **Model Context Protocol (MCP) & Spatiotemporal Tool Isolation:** Dynamically attaches external MCP servers (`filesystem`, `fetch`, databases, GitHub, etc.) over the stdio transport (`!mcp connect`, `!mcp add`). Discovered tools join the registry as `Rubik::Tools::BaseTool` proxies; server termination triggers registered LIFO cleanup inverses ($\Omega$) for child processes and proxy tools; these inverses help prevent orphan processes and stale tool calls when cleanup completes successfully. The cleanup path was exercised across 1,000 normal, handshake-failure, and crash/recovery cycles with explicit resource tolerances; see [_report/runtime-capacity-validation-2026-10-16.md](_report/runtime-capacity-validation-2026-10-16.md).
|
|
178
191
|
- 📺 **Tiered Live Agent Monitoring (Step Formatter & Verbosity):** With the `quiet`, `normal`, `verbose`, and `debug` levels, step progress, tool calls, execution times, and intermediate thoughts are watched in real time (`!verbosity`, `!verbose`).
|
|
179
192
|
- 🎨 **Hybrid TUI Display Layer:** The `⏺/⎿` agent flow language, a live spinner cycling through Turkish verbs (`✻ Kavuruyor… 4sn`), an above-prompt statusline (model · context percentage · dirty buffer), live markdown rendering, a colored diff viewer, one-key quick approvals, and the interactive `!agents` fiber board. Under `NO_COLOR`/`--no-color` and in pipe mode the output degrades to plain text; session logs never contain ANSI codes (live settings via `!config set ui.*`).
|
|
180
193
|
- ⚡ **Native Tool Acceleration:** When available on the system, `grep_search` is delegated to ripgrep and context scanning to `git ls-files`; AST symbols warm up with lazy body loading. Without rg/git, or with `tools.native: off` or `RUBIK_NATIVE=off`, every path falls back to pure Ruby with identical output — the output format never changes.
|
|
181
|
-
- 🧵 **Quiet Concurrency (Fiber Scheduler Event Loop):** The pure-stdlib `Rubik::Concurrency` layer provides cooperative single-threaded concurrency WITHOUT altering any existing blocking I/O call sites (Net::HTTP, sleep, Open3 pipes, Mutex/CV/Thread#join):
|
|
194
|
+
- 🧵 **Quiet Concurrency (Fiber Scheduler Event Loop):** The pure-stdlib `Rubik::Concurrency` layer provides cooperative single-threaded concurrency WITHOUT altering any existing blocking I/O call sites (Net::HTTP, sleep, Open3 pipes, Mutex/CV/Thread#join): tested at 100 and 1,000 concurrent local network requests, including latency, timeout, cancellation, and sequential comparisons; see [_report/runtime-capacity-validation-2026-10-16.md](_report/runtime-capacity-validation-2026-10-16.md)), the MCP boot fan-out, and the subagent provider pool all run through the `Concurrency.silently`/`.map` boundary (not CPU parallelism, due to the GVL). Kill switch: `RUBIK_FIBER_SCHEDULER=off`.
|
|
182
195
|
- 📜 **Vakanüvis Chronicler (Session Annals):** The deterministic harness records REPL root turns and subagent lifecycle events into the `rubik://storage` annals; a budget-capped LLM distiller mines memory and a wiki skeleton from those annals. All planes are governed by `subagents.vakanuvis.*`; chronicles can be recalled through the Vakanüvis tool (`recall`/`record`/`forget`/`digest`).
|
|
183
196
|
|
|
184
197
|
> 🛡️ **Security note — when cloning unfamiliar repositories:** The `.rubik/commands` and `.rubik/tools` directories contain overlay files that can be interpreted at runtime. SHA-256 manifest verification blocks later modifications; files carrying a sentinel header but no manifest are accepted on the first scan only with the explicit consent of `RUBIK_BOOTSTRAP_OVERLAYS=1`. If you have cloned a repository you do not know, inspect the `.rubik/` directory before the first run.
|
|
@@ -235,7 +248,7 @@ Environmental test skips:
|
|
|
235
248
|
- PowerShell shell execution tests are skipped when the `pwsh` (PowerShell Core) binary is not present on the system.
|
|
236
249
|
- Alternative AST parser tests are skipped when the `prism` gem or its C extension is not present on the system.
|
|
237
250
|
- In non-interactive / headless CI environments, direct TUI/reline terminal tests automatically run in non-blocking mode or are skipped in a controlled way.
|
|
238
|
-
-
|
|
251
|
+
- Capability skips report their reasons; unexpected failures still fail CI. A passing run describes that runner and commit, not a universal platform guarantee.
|
|
239
252
|
|
|
240
253
|
---
|
|
241
254
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubik
|
|
4
|
+
module AST
|
|
5
|
+
# Structured outcome of one file extraction (issue #147): `symbols` is
|
|
6
|
+
# always the best available list (possibly empty), while `status` says HOW
|
|
7
|
+
# much it can be trusted — so "no symbols here" and "extraction failed"
|
|
8
|
+
# are never confused:
|
|
9
|
+
# :ok — authoritative result (clean parse, or the primary
|
|
10
|
+
# extractor for this file/environment)
|
|
11
|
+
# :partial — the language parser reported syntax errors; extracted
|
|
12
|
+
# symbols are usable but may be incomplete
|
|
13
|
+
# :failed — no trustworthy result (parser crashed, parse errors
|
|
14
|
+
# with nothing extracted, fallback failed too);
|
|
15
|
+
# `symbols` may carry best-effort output
|
|
16
|
+
# :unavailable — no language extractor claims this extension; the
|
|
17
|
+
# legacy graceful fallback produced whatever it could
|
|
18
|
+
# `source` names who produced the symbols (:ruby, :tree_sitter,
|
|
19
|
+
# :fallback, :none) and `detail` carries the failure reason when there is
|
|
20
|
+
# one.
|
|
21
|
+
ExtractionResult = Struct.new(:symbols, :status, :source, :detail, keyword_init: true) do
|
|
22
|
+
def ok?
|
|
23
|
+
status == :ok
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def failed?
|
|
27
|
+
status == :failed
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# A result a caller may rely on for symbol lookup (possibly incomplete
|
|
31
|
+
# but produced by the language parser itself).
|
|
32
|
+
def trustworthy?
|
|
33
|
+
[:ok, :partial].include?(status)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
data/lib/rubik/ast/extractor.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "extraction_result"
|
|
3
4
|
require_relative "ruby_extractor"
|
|
4
5
|
require_relative "go_extractor"
|
|
5
6
|
require_relative "rust_extractor"
|
|
@@ -31,24 +32,49 @@ module Rubik
|
|
|
31
32
|
EXTENSIONS = FALLBACK_EXTRACTORS
|
|
32
33
|
|
|
33
34
|
def self.extract(file_path, content = nil)
|
|
35
|
+
extract_result(file_path, content).symbols
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Structured outcome (issue #147): symbols plus status/source/detail, so
|
|
39
|
+
# a failed extraction is distinguishable from a legitimately empty file
|
|
40
|
+
# and from a degraded regex fallback. Status vocabulary lives in
|
|
41
|
+
# AST::ExtractionResult.
|
|
42
|
+
def self.extract_result(file_path, content = nil)
|
|
34
43
|
ext = File.extname(file_path).downcase
|
|
35
44
|
|
|
36
45
|
# 1. Ruby files: always use Prism-powered RubyExtractor first
|
|
37
|
-
|
|
46
|
+
if ext == ".rb"
|
|
47
|
+
return ExtractionResult.new(symbols: RubyExtractor.extract(file_path, content),
|
|
48
|
+
status: :ok, source: :ruby)
|
|
49
|
+
end
|
|
38
50
|
|
|
39
|
-
# 2. Polyglot files: use Tree-Sitter AST extractor if available
|
|
51
|
+
# 2. Polyglot files: use the Tree-Sitter AST extractor if available
|
|
40
52
|
if TreeSitterExtractor.available? && TreeSitterExtractor.supports?(ext)
|
|
41
|
-
|
|
42
|
-
|
|
53
|
+
ts = TreeSitterExtractor.extract_result(file_path, content)
|
|
54
|
+
if ts.trustworthy?
|
|
55
|
+
return ExtractionResult.new(symbols: ts.symbols, status: ts.status,
|
|
56
|
+
source: :tree_sitter, detail: ts.detail)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# The language parser failed (crash / unparsable): the regex
|
|
60
|
+
# fallback is best-effort only, and the status says so.
|
|
61
|
+
return fallback_result(file_path, content, ext, status: :failed, detail: ts.detail) if ts.failed?
|
|
43
62
|
end
|
|
44
63
|
|
|
45
|
-
# 3. Graceful
|
|
46
|
-
|
|
47
|
-
|
|
64
|
+
# 3. Graceful fallback: the legacy regex extractor — the primary path
|
|
65
|
+
# in environments without the tree-sitter pack or for extensions no
|
|
66
|
+
# language extractor claims (:unavailable, legacy Ruby parse).
|
|
67
|
+
fallback_result(file_path, content, ext)
|
|
48
68
|
rescue StandardError, ScriptError => e
|
|
49
69
|
warn "[WARN] Extraction failed for #{file_path}: #{e.class}: #{e.message}" if ENV["RUBIK_DEBUG"]
|
|
50
|
-
|
|
51
|
-
|
|
70
|
+
ExtractionResult.new(symbols: [], status: :failed, source: :none, detail: "#{e.class}: #{e.message}")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def self.fallback_result(file_path, content, ext, status: nil, detail: nil)
|
|
74
|
+
extractor = FALLBACK_EXTRACTORS[ext]
|
|
75
|
+
status ||= extractor ? :ok : :unavailable
|
|
76
|
+
symbols = (extractor || RubyExtractor).extract(file_path, content)
|
|
77
|
+
ExtractionResult.new(symbols: symbols, status: status, source: :fallback, detail: detail)
|
|
52
78
|
end
|
|
53
79
|
|
|
54
80
|
def self.supported_extensions
|
data/lib/rubik/ast/graph.rb
CHANGED
|
@@ -36,9 +36,17 @@ module Rubik
|
|
|
36
36
|
@symbols = {}
|
|
37
37
|
@qualified_symbols = {}
|
|
38
38
|
@file_symbols = Hash.new { |h, k| h[k] = [] }
|
|
39
|
+
@extraction_status = {}
|
|
39
40
|
@mutex = Mutex.new
|
|
40
41
|
end
|
|
41
42
|
|
|
43
|
+
# Last extraction status recorded for a file (issue #147): :ok,
|
|
44
|
+
# :partial, :failed, or :unavailable. Lets callers tell "no symbols"
|
|
45
|
+
# apart from "extraction failed" without re-running the extractor.
|
|
46
|
+
def extraction_status(file_path)
|
|
47
|
+
@mutex.synchronize { @extraction_status[file_path] }
|
|
48
|
+
end
|
|
49
|
+
|
|
42
50
|
def symbols
|
|
43
51
|
@mutex.synchronize do
|
|
44
52
|
SymbolMap.new(@symbols.dup, @qualified_symbols.dup)
|
|
@@ -54,6 +62,7 @@ module Rubik
|
|
|
54
62
|
@symbols.clear
|
|
55
63
|
@qualified_symbols.clear
|
|
56
64
|
@file_symbols.clear
|
|
65
|
+
@extraction_status.clear
|
|
57
66
|
end
|
|
58
67
|
end
|
|
59
68
|
|
|
@@ -66,6 +75,7 @@ module Rubik
|
|
|
66
75
|
def remove_file(file_path)
|
|
67
76
|
@mutex.synchronize do
|
|
68
77
|
_remove_file_unlocked(file_path)
|
|
78
|
+
@extraction_status.delete(file_path)
|
|
69
79
|
end
|
|
70
80
|
end
|
|
71
81
|
|
|
@@ -218,12 +228,28 @@ module Rubik
|
|
|
218
228
|
end
|
|
219
229
|
|
|
220
230
|
def _index_file_unlocked(file_path, content = nil)
|
|
231
|
+
result = Extractor.extract_result(file_path, content)
|
|
232
|
+
@extraction_status[file_path] = result.status
|
|
233
|
+
|
|
234
|
+
# A failed extraction must not silently DELETE the file's previously
|
|
235
|
+
# indexed symbols (issue #147): stale-but-known beats unknown, and
|
|
236
|
+
# the log line makes the downgrade visible. The best-effort symbols
|
|
237
|
+
# are only indexed when there is nothing to protect.
|
|
238
|
+
if result.status == :failed
|
|
239
|
+
existing = _get_file_symbols_unlocked(file_path)
|
|
240
|
+
unless existing.empty?
|
|
241
|
+
Rubik::Log.warn(:ast, "extraction failed for #{file_path} (#{result.detail}); keeping previously indexed symbols")
|
|
242
|
+
return existing
|
|
243
|
+
end
|
|
244
|
+
Rubik::Log.warn(:ast, "extraction failed for #{file_path} (#{result.detail}); no symbols indexed")
|
|
245
|
+
end
|
|
246
|
+
|
|
221
247
|
_remove_file_unlocked(file_path)
|
|
222
248
|
|
|
223
|
-
extracted = Extractor.extract(file_path, content)
|
|
224
249
|
ext = File.extname(file_path).downcase
|
|
225
250
|
lang = CacheStore::LANGUAGE_MAP[ext] || "code"
|
|
226
251
|
|
|
252
|
+
extracted = result.symbols
|
|
227
253
|
extracted.each do |sym|
|
|
228
254
|
@symbols[sym.name] = sym
|
|
229
255
|
q_key = "#{lang}:#{file_path}##{sym.name}"
|