rubik 0.1.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 +7 -0
- data/ATTRIBUTION.md +139 -0
- data/CHANGELOG.md +236 -0
- data/LICENSE +21 -0
- data/README.md +269 -0
- data/bin/rubik +82 -0
- data/lib/rubik/adapter/base.rb +195 -0
- data/lib/rubik/adapter/c.rb +36 -0
- data/lib/rubik/adapter/crystal.rb +78 -0
- data/lib/rubik/adapter/generic.rb +86 -0
- data/lib/rubik/adapter/go.rb +36 -0
- data/lib/rubik/adapter/node.rb +146 -0
- data/lib/rubik/adapter/process.rb +78 -0
- data/lib/rubik/adapter/python.rb +30 -0
- data/lib/rubik/adapter/registry.rb +81 -0
- data/lib/rubik/adapter/ruby.rb +30 -0
- data/lib/rubik/adapter/rust.rb +36 -0
- data/lib/rubik/adapter/zig.rb +36 -0
- data/lib/rubik/adapter.rb +18 -0
- data/lib/rubik/ast/body_loader.rb +19 -0
- data/lib/rubik/ast/c_extractor.rb +114 -0
- data/lib/rubik/ast/cache_store.rb +191 -0
- data/lib/rubik/ast/extractor.rb +59 -0
- data/lib/rubik/ast/fallback_cleaner.rb +145 -0
- data/lib/rubik/ast/go_extractor.rb +148 -0
- data/lib/rubik/ast/graph.rb +272 -0
- data/lib/rubik/ast/python_extractor.rb +120 -0
- data/lib/rubik/ast/ruby_extractor.rb +260 -0
- data/lib/rubik/ast/rust_extractor.rb +127 -0
- data/lib/rubik/ast/symbol.rb +103 -0
- data/lib/rubik/ast/treesitter/base_walker.rb +109 -0
- data/lib/rubik/ast/treesitter/c_walker.rb +122 -0
- data/lib/rubik/ast/treesitter/go_walker.rb +120 -0
- data/lib/rubik/ast/treesitter/python_walker.rb +107 -0
- data/lib/rubik/ast/treesitter/rust_walker.rb +130 -0
- data/lib/rubik/ast/treesitter/typescript_walker.rb +182 -0
- data/lib/rubik/ast/treesitter/zig_walker.rb +109 -0
- data/lib/rubik/ast/treesitter_extractor.rb +93 -0
- data/lib/rubik/ast/typescript_extractor.rb +283 -0
- data/lib/rubik/ast/zig_extractor.rb +116 -0
- data/lib/rubik/ast.rb +13 -0
- data/lib/rubik/benchmark/runner.rb +224 -0
- data/lib/rubik/benchmark/synth_repo.rb +59 -0
- data/lib/rubik/benchmark.rb +9 -0
- data/lib/rubik/buffer/diff.rb +43 -0
- data/lib/rubik/buffer/manager.rb +427 -0
- data/lib/rubik/buffer/path_validator.rb +60 -0
- data/lib/rubik/buffer/snapshot.rb +99 -0
- data/lib/rubik/buffer.rb +10 -0
- data/lib/rubik/coeffects.rb +129 -0
- data/lib/rubik/concurrency/boundary.rb +74 -0
- data/lib/rubik/concurrency/dns_pool.rb +68 -0
- data/lib/rubik/concurrency/errors.rb +17 -0
- data/lib/rubik/concurrency/event_loop.rb +441 -0
- data/lib/rubik/concurrency/join.rb +55 -0
- data/lib/rubik/concurrency/process_waiter.rb +55 -0
- data/lib/rubik/concurrency/scheduler.rb +84 -0
- data/lib/rubik/concurrency/timers.rb +109 -0
- data/lib/rubik/concurrency.rb +132 -0
- data/lib/rubik/config/deep_ops.rb +80 -0
- data/lib/rubik/config/defaults.rb +248 -0
- data/lib/rubik/config/model_catalog.rb +93 -0
- data/lib/rubik/config/store.rb +155 -0
- data/lib/rubik/config.rb +216 -0
- data/lib/rubik/context/git_info.rb +84 -0
- data/lib/rubik/context/git_lister.rb +37 -0
- data/lib/rubik/context/index.rb +134 -0
- data/lib/rubik/context/repo_context_reader.rb +121 -0
- data/lib/rubik/context/scanner.rb +439 -0
- data/lib/rubik/context.rb +11 -0
- data/lib/rubik/diagnostics/collector.rb +200 -0
- data/lib/rubik/diagnostics/entry.rb +60 -0
- data/lib/rubik/diagnostics/parser.rb +107 -0
- data/lib/rubik/diagnostics.rb +10 -0
- data/lib/rubik/event_bus.rb +56 -0
- data/lib/rubik/kernel/dispatcher.rb +171 -0
- data/lib/rubik/kernel/dmesg.rb +116 -0
- data/lib/rubik/kernel/init.rb +177 -0
- data/lib/rubik/kernel/record.rb +85 -0
- data/lib/rubik/kernel/unit.rb +160 -0
- data/lib/rubik/llm/anthropic.rb +202 -0
- data/lib/rubik/llm/chunk_assembler.rb +285 -0
- data/lib/rubik/llm/client.rb +526 -0
- data/lib/rubik/llm/formatter.rb +450 -0
- data/lib/rubik/llm/message_sanitizer.rb +87 -0
- data/lib/rubik/llm/ollama.rb +167 -0
- data/lib/rubik/llm/openai.rb +21 -0
- data/lib/rubik/llm/openai_compatible.rb +216 -0
- data/lib/rubik/llm/provider.rb +125 -0
- data/lib/rubik/llm/registry.rb +336 -0
- data/lib/rubik/llm/router.rb +100 -0
- data/lib/rubik/llm.rb +18 -0
- data/lib/rubik/log.rb +38 -0
- data/lib/rubik/lsp/client.rb +475 -0
- data/lib/rubik/lsp/manager.rb +366 -0
- data/lib/rubik/lsp.rb +9 -0
- data/lib/rubik/mcp/boot_lifecycle.rb +114 -0
- data/lib/rubik/mcp/client.rb +169 -0
- data/lib/rubik/mcp/errors.rb +11 -0
- data/lib/rubik/mcp/manager.rb +619 -0
- data/lib/rubik/mcp/proxy_tool.rb +104 -0
- data/lib/rubik/mcp/transport/stdio.rb +255 -0
- data/lib/rubik/mcp.rb +12 -0
- data/lib/rubik/orchestrator/activity_log.rb +75 -0
- data/lib/rubik/orchestrator/auto_apply.rb +166 -0
- data/lib/rubik/orchestrator/budget_manager.rb +97 -0
- data/lib/rubik/orchestrator/continuation_manager.rb +161 -0
- data/lib/rubik/orchestrator/finish_message_selector.rb +55 -0
- data/lib/rubik/orchestrator/goal_ledger/drift.rb +71 -0
- data/lib/rubik/orchestrator/goal_ledger/ledger.rb +399 -0
- data/lib/rubik/orchestrator/goal_ledger/observation_projection.rb +179 -0
- data/lib/rubik/orchestrator/goal_ledger/patch.rb +268 -0
- data/lib/rubik/orchestrator/goal_ledger/schema.rb +123 -0
- data/lib/rubik/orchestrator/goal_ledger/validator.rb +361 -0
- data/lib/rubik/orchestrator/goal_ledger/verifier.rb +129 -0
- data/lib/rubik/orchestrator/goal_ledger/verify_spec.rb +50 -0
- data/lib/rubik/orchestrator/goal_ledger.rb +65 -0
- data/lib/rubik/orchestrator/goal_state.rb +184 -0
- data/lib/rubik/orchestrator/goal_wait.rb +125 -0
- data/lib/rubik/orchestrator/loop.rb +388 -0
- data/lib/rubik/orchestrator/lsp_feedback.rb +55 -0
- data/lib/rubik/orchestrator/maestro_gate.rb +65 -0
- data/lib/rubik/orchestrator/provider_recovery.rb +90 -0
- data/lib/rubik/orchestrator/provider_resolver.rb +69 -0
- data/lib/rubik/orchestrator/shell_deps.rb +85 -0
- data/lib/rubik/orchestrator/step_executor.rb +328 -0
- data/lib/rubik/orchestrator/stream_manager.rb +118 -0
- data/lib/rubik/orchestrator/subagent/activation.rb +65 -0
- data/lib/rubik/orchestrator/subagent/budget.rb +108 -0
- data/lib/rubik/orchestrator/subagent/completion.rb +61 -0
- data/lib/rubik/orchestrator/subagent/concurrency_gate.rb +37 -0
- data/lib/rubik/orchestrator/subagent/conversation_id.rb +67 -0
- data/lib/rubik/orchestrator/subagent/dependency.rb +398 -0
- data/lib/rubik/orchestrator/subagent/instance.rb +384 -0
- data/lib/rubik/orchestrator/subagent/lifecycle.rb +118 -0
- data/lib/rubik/orchestrator/subagent/messaging.rb +61 -0
- data/lib/rubik/orchestrator/subagent/narration.rb +125 -0
- data/lib/rubik/orchestrator/subagent/notices.rb +209 -0
- data/lib/rubik/orchestrator/subagent/questioner.rb +80 -0
- data/lib/rubik/orchestrator/subagent/reaper.rb +98 -0
- data/lib/rubik/orchestrator/subagent/resilience.rb +70 -0
- data/lib/rubik/orchestrator/subagent/reuse.rb +127 -0
- data/lib/rubik/orchestrator/subagent/scoped_registry.rb +79 -0
- data/lib/rubik/orchestrator/subagent/step_runner.rb +240 -0
- data/lib/rubik/orchestrator/subagent/storage_id.rb +26 -0
- data/lib/rubik/orchestrator/subagent/supervisor.rb +327 -0
- data/lib/rubik/orchestrator/subagent/transcript_formatter.rb +141 -0
- data/lib/rubik/orchestrator/subagent/type_registry.rb +199 -0
- data/lib/rubik/orchestrator/subagent_notice_inbox.rb +80 -0
- data/lib/rubik/orchestrator/tool_call_id.rb +47 -0
- data/lib/rubik/orchestrator/tool_circuit_breaker.rb +91 -0
- data/lib/rubik/orchestrator/tool_history.rb +39 -0
- data/lib/rubik/orchestrator/turn_executor/subagent_channel.rb +102 -0
- data/lib/rubik/orchestrator/turn_executor/user_channel.rb +172 -0
- data/lib/rubik/orchestrator/turn_executor.rb +376 -0
- data/lib/rubik/orchestrator.rb +11 -0
- data/lib/rubik/platform/filesystem.rb +64 -0
- data/lib/rubik/platform/io_drain.rb +53 -0
- data/lib/rubik/platform/path.rb +210 -0
- data/lib/rubik/platform/process.rb +38 -0
- data/lib/rubik/platform/sanitize.rb +157 -0
- data/lib/rubik/platform.rb +113 -0
- data/lib/rubik/plugin/dsl.rb +51 -0
- data/lib/rubik/plugin/manager.rb +247 -0
- data/lib/rubik/plugin/storage.rb +195 -0
- data/lib/rubik/plugin.rb +10 -0
- data/lib/rubik/prompt/context_builder.rb +377 -0
- data/lib/rubik/prompt/context_compactor.rb +326 -0
- data/lib/rubik/prompt/conversation_rows.rb +74 -0
- data/lib/rubik/prompt/doctrine.rb +160 -0
- data/lib/rubik/prompt/emergency_compactor.rb +81 -0
- data/lib/rubik/prompt/services.rb +60 -0
- data/lib/rubik/prompt/token_estimator.rb +63 -0
- data/lib/rubik/prompt/tool_teachings.rb +78 -0
- data/lib/rubik/prompt.rb +13 -0
- data/lib/rubik/repair/loop.rb +95 -0
- data/lib/rubik/repair.rb +8 -0
- data/lib/rubik/repl/ast_warmup.rb +129 -0
- data/lib/rubik/repl/command_context.rb +214 -0
- data/lib/rubik/repl/command_parser.rb +48 -0
- data/lib/rubik/repl/commands/agent.rb +30 -0
- data/lib/rubik/repl/commands/agent_context.rb +289 -0
- data/lib/rubik/repl/commands/agent_execution.rb +342 -0
- data/lib/rubik/repl/commands/agent_models.rb +212 -0
- data/lib/rubik/repl/commands/agent_providers.rb +232 -0
- data/lib/rubik/repl/commands/base.rb +55 -0
- data/lib/rubik/repl/commands/buffer.rb +124 -0
- data/lib/rubik/repl/commands/dmesg.rb +87 -0
- data/lib/rubik/repl/commands/lsp.rb +235 -0
- data/lib/rubik/repl/commands/mcp.rb +194 -0
- data/lib/rubik/repl/commands/plugins.rb +208 -0
- data/lib/rubik/repl/commands/pruner.rb +71 -0
- data/lib/rubik/repl/commands/session.rb +183 -0
- data/lib/rubik/repl/commands/skills.rb +111 -0
- data/lib/rubik/repl/commands/storage.rb +85 -0
- data/lib/rubik/repl/commands/subagents.rb +195 -0
- data/lib/rubik/repl/commands/system.rb +85 -0
- data/lib/rubik/repl/commands/tasks.rb +120 -0
- data/lib/rubik/repl/commands/workspace.rb +443 -0
- data/lib/rubik/repl/commands.rb +44 -0
- data/lib/rubik/repl/composition.rb +32 -0
- data/lib/rubik/repl/config_wiring.rb +142 -0
- data/lib/rubik/repl/confirmations.rb +56 -0
- data/lib/rubik/repl/hooks.rb +47 -0
- data/lib/rubik/repl/input_execution.rb +323 -0
- data/lib/rubik/repl/input_loop.rb +213 -0
- data/lib/rubik/repl/interrupt_handler.rb +217 -0
- data/lib/rubik/repl/lifecycle.rb +410 -0
- data/lib/rubik/repl/paste_support.rb +88 -0
- data/lib/rubik/repl/pending_notice_queue.rb +60 -0
- data/lib/rubik/repl/presentation.rb +132 -0
- data/lib/rubik/repl/pruner/brief.rb +101 -0
- data/lib/rubik/repl/pruner/gate.rb +279 -0
- data/lib/rubik/repl/pruner/harness.rb +236 -0
- data/lib/rubik/repl/pruner/rules.rb +316 -0
- data/lib/rubik/repl/pruner/service.rb +82 -0
- data/lib/rubik/repl/pruner.rb +39 -0
- data/lib/rubik/repl/service_catalog.rb +374 -0
- data/lib/rubik/repl/setup_factories.rb +66 -0
- data/lib/rubik/repl/shell.rb +231 -0
- data/lib/rubik/repl/step_formatter.rb +324 -0
- data/lib/rubik/repl/tee_io.rb +105 -0
- data/lib/rubik/repl/terminal_guard.rb +157 -0
- data/lib/rubik/repl/turn_recorder.rb +234 -0
- data/lib/rubik/repl.rb +14 -0
- data/lib/rubik/runtime/ast_service.rb +78 -0
- data/lib/rubik/runtime/context.rb +335 -0
- data/lib/rubik/runtime/diagnostic_engine.rb +188 -0
- data/lib/rubik/runtime/discovery.rb +342 -0
- data/lib/rubik/runtime/fiber.rb +142 -0
- data/lib/rubik/runtime/fiber_ordering.rb +52 -0
- data/lib/rubik/runtime/fiber_retry_gate.rb +84 -0
- data/lib/rubik/runtime/index_service.rb +58 -0
- data/lib/rubik/runtime/loop_service.rb +73 -0
- data/lib/rubik/runtime/manager.rb +241 -0
- data/lib/rubik/runtime/overlay_storage.rb +481 -0
- data/lib/rubik/runtime/service_definition.rb +54 -0
- data/lib/rubik/runtime/service_registration.rb +33 -0
- data/lib/rubik/runtime/task_manager.rb +302 -0
- data/lib/rubik/runtime.rb +19 -0
- data/lib/rubik/sandbox/driver/base.rb +30 -0
- data/lib/rubik/sandbox/driver/bubblewrap.rb +111 -0
- data/lib/rubik/sandbox/driver/docker.rb +113 -0
- data/lib/rubik/sandbox/driver/podman.rb +98 -0
- data/lib/rubik/sandbox/driver/process.rb +190 -0
- data/lib/rubik/sandbox/driver.rb +91 -0
- data/lib/rubik/sandbox/runner.rb +218 -0
- data/lib/rubik/sandbox/shell_resolver.rb +178 -0
- data/lib/rubik/sandbox.rb +8 -0
- data/lib/rubik/security/blocked_guidance.rb +187 -0
- data/lib/rubik/security/classifier/classifier.rb +440 -0
- data/lib/rubik/security/classifier/commands/agent-browser.yaml +9 -0
- data/lib/rubik/security/classifier/commands/apt.yaml +25 -0
- data/lib/rubik/security/classifier/commands/awk.yaml +10 -0
- data/lib/rubik/security/classifier/commands/basename.yaml +7 -0
- data/lib/rubik/security/classifier/commands/bash-classify.yaml +8 -0
- data/lib/rubik/security/classifier/commands/bash.yaml +20 -0
- data/lib/rubik/security/classifier/commands/black.yaml +12 -0
- data/lib/rubik/security/classifier/commands/brew.yaml +31 -0
- data/lib/rubik/security/classifier/commands/builtin.yaml +8 -0
- data/lib/rubik/security/classifier/commands/cargo.yaml +114 -0
- data/lib/rubik/security/classifier/commands/cat.yaml +7 -0
- data/lib/rubik/security/classifier/commands/chgrp.yaml +10 -0
- data/lib/rubik/security/classifier/commands/chmod.yaml +10 -0
- data/lib/rubik/security/classifier/commands/chown.yaml +10 -0
- data/lib/rubik/security/classifier/commands/column.yaml +7 -0
- data/lib/rubik/security/classifier/commands/comm.yaml +7 -0
- data/lib/rubik/security/classifier/commands/command.yaml +12 -0
- data/lib/rubik/security/classifier/commands/cp.yaml +7 -0
- data/lib/rubik/security/classifier/commands/crystal.yaml +63 -0
- data/lib/rubik/security/classifier/commands/curl.yaml +30 -0
- data/lib/rubik/security/classifier/commands/cut.yaml +7 -0
- data/lib/rubik/security/classifier/commands/date.yaml +10 -0
- data/lib/rubik/security/classifier/commands/df.yaml +7 -0
- data/lib/rubik/security/classifier/commands/diff.yaml +7 -0
- data/lib/rubik/security/classifier/commands/dig.yaml +7 -0
- data/lib/rubik/security/classifier/commands/dirname.yaml +7 -0
- data/lib/rubik/security/classifier/commands/docker.yaml +177 -0
- data/lib/rubik/security/classifier/commands/du.yaml +7 -0
- data/lib/rubik/security/classifier/commands/echo.yaml +7 -0
- data/lib/rubik/security/classifier/commands/env.yaml +15 -0
- data/lib/rubik/security/classifier/commands/eslint.yaml +12 -0
- data/lib/rubik/security/classifier/commands/eval.yaml +8 -0
- data/lib/rubik/security/classifier/commands/exec.yaml +12 -0
- data/lib/rubik/security/classifier/commands/expand.yaml +7 -0
- data/lib/rubik/security/classifier/commands/false.yaml +7 -0
- data/lib/rubik/security/classifier/commands/file.yaml +7 -0
- data/lib/rubik/security/classifier/commands/find.yaml +25 -0
- data/lib/rubik/security/classifier/commands/fmt.yaml +7 -0
- data/lib/rubik/security/classifier/commands/fold.yaml +7 -0
- data/lib/rubik/security/classifier/commands/free.yaml +7 -0
- data/lib/rubik/security/classifier/commands/gawk.yaml +10 -0
- data/lib/rubik/security/classifier/commands/gcloud.yaml +979 -0
- data/lib/rubik/security/classifier/commands/gh.yaml +615 -0
- data/lib/rubik/security/classifier/commands/git.yaml +524 -0
- data/lib/rubik/security/classifier/commands/glab-discussion.yaml +21 -0
- data/lib/rubik/security/classifier/commands/glab.yaml +807 -0
- data/lib/rubik/security/classifier/commands/go.yaml +44 -0
- data/lib/rubik/security/classifier/commands/gog.yaml +983 -0
- data/lib/rubik/security/classifier/commands/gradle.yaml +92 -0
- data/lib/rubik/security/classifier/commands/gradlew.yaml +92 -0
- data/lib/rubik/security/classifier/commands/grep.yaml +7 -0
- data/lib/rubik/security/classifier/commands/groups.yaml +7 -0
- data/lib/rubik/security/classifier/commands/gunzip.yaml +8 -0
- data/lib/rubik/security/classifier/commands/gws.yaml +211 -0
- data/lib/rubik/security/classifier/commands/gzip.yaml +7 -0
- data/lib/rubik/security/classifier/commands/hadolint.yaml +9 -0
- data/lib/rubik/security/classifier/commands/halt.yaml +8 -0
- data/lib/rubik/security/classifier/commands/head.yaml +7 -0
- data/lib/rubik/security/classifier/commands/helm.yaml +129 -0
- data/lib/rubik/security/classifier/commands/hostname.yaml +7 -0
- data/lib/rubik/security/classifier/commands/id.yaml +7 -0
- data/lib/rubik/security/classifier/commands/install.yaml +7 -0
- data/lib/rubik/security/classifier/commands/isort.yaml +14 -0
- data/lib/rubik/security/classifier/commands/java.yaml +35 -0
- data/lib/rubik/security/classifier/commands/javac.yaml +39 -0
- data/lib/rubik/security/classifier/commands/join.yaml +7 -0
- data/lib/rubik/security/classifier/commands/journalctl.yaml +14 -0
- data/lib/rubik/security/classifier/commands/jq.yaml +7 -0
- data/lib/rubik/security/classifier/commands/kill.yaml +7 -0
- data/lib/rubik/security/classifier/commands/killall.yaml +7 -0
- data/lib/rubik/security/classifier/commands/kubectl.yaml +211 -0
- data/lib/rubik/security/classifier/commands/less.yaml +7 -0
- data/lib/rubik/security/classifier/commands/ln.yaml +7 -0
- data/lib/rubik/security/classifier/commands/ls.yaml +7 -0
- data/lib/rubik/security/classifier/commands/make.yaml +21 -0
- data/lib/rubik/security/classifier/commands/md5sum.yaml +7 -0
- data/lib/rubik/security/classifier/commands/mise.yaml +579 -0
- data/lib/rubik/security/classifier/commands/mkdir.yaml +8 -0
- data/lib/rubik/security/classifier/commands/more.yaml +7 -0
- data/lib/rubik/security/classifier/commands/mv.yaml +7 -0
- data/lib/rubik/security/classifier/commands/mvn.yaml +83 -0
- data/lib/rubik/security/classifier/commands/mvnw.yaml +83 -0
- data/lib/rubik/security/classifier/commands/mypy.yaml +9 -0
- data/lib/rubik/security/classifier/commands/netstat.yaml +7 -0
- data/lib/rubik/security/classifier/commands/nice.yaml +11 -0
- data/lib/rubik/security/classifier/commands/nl.yaml +7 -0
- data/lib/rubik/security/classifier/commands/node.yaml +20 -0
- data/lib/rubik/security/classifier/commands/nohup.yaml +9 -0
- data/lib/rubik/security/classifier/commands/npm.yaml +71 -0
- data/lib/rubik/security/classifier/commands/npx.yaml +42 -0
- data/lib/rubik/security/classifier/commands/nslookup.yaml +7 -0
- data/lib/rubik/security/classifier/commands/paste.yaml +7 -0
- data/lib/rubik/security/classifier/commands/ping.yaml +7 -0
- data/lib/rubik/security/classifier/commands/pip.yaml +33 -0
- data/lib/rubik/security/classifier/commands/pip3.yaml +33 -0
- data/lib/rubik/security/classifier/commands/pipx.yaml +33 -0
- data/lib/rubik/security/classifier/commands/pkill.yaml +7 -0
- data/lib/rubik/security/classifier/commands/pnpm.yaml +346 -0
- data/lib/rubik/security/classifier/commands/poetry.yaml +97 -0
- data/lib/rubik/security/classifier/commands/poweroff.yaml +8 -0
- data/lib/rubik/security/classifier/commands/prettier.yaml +13 -0
- data/lib/rubik/security/classifier/commands/printenv.yaml +7 -0
- data/lib/rubik/security/classifier/commands/printf.yaml +7 -0
- data/lib/rubik/security/classifier/commands/ps.yaml +7 -0
- data/lib/rubik/security/classifier/commands/pup.yaml +882 -0
- data/lib/rubik/security/classifier/commands/pwd.yaml +7 -0
- data/lib/rubik/security/classifier/commands/pytest.yaml +7 -0
- data/lib/rubik/security/classifier/commands/python.yaml +9 -0
- data/lib/rubik/security/classifier/commands/python3.yaml +9 -0
- data/lib/rubik/security/classifier/commands/read.yaml +7 -0
- data/lib/rubik/security/classifier/commands/readlink.yaml +7 -0
- data/lib/rubik/security/classifier/commands/realpath.yaml +7 -0
- data/lib/rubik/security/classifier/commands/reboot.yaml +8 -0
- data/lib/rubik/security/classifier/commands/rev.yaml +7 -0
- data/lib/rubik/security/classifier/commands/rg.yaml +7 -0
- data/lib/rubik/security/classifier/commands/rm.yaml +8 -0
- data/lib/rubik/security/classifier/commands/rsync.yaml +12 -0
- data/lib/rubik/security/classifier/commands/rtk.yaml +140 -0
- data/lib/rubik/security/classifier/commands/ruff.yaml +29 -0
- data/lib/rubik/security/classifier/commands/rustfmt.yaml +14 -0
- data/lib/rubik/security/classifier/commands/scp.yaml +7 -0
- data/lib/rubik/security/classifier/commands/sed.yaml +14 -0
- data/lib/rubik/security/classifier/commands/seq.yaml +7 -0
- data/lib/rubik/security/classifier/commands/sh.yaml +14 -0
- data/lib/rubik/security/classifier/commands/sha1sum.yaml +7 -0
- data/lib/rubik/security/classifier/commands/sha256sum.yaml +7 -0
- data/lib/rubik/security/classifier/commands/shellcheck.yaml +10 -0
- data/lib/rubik/security/classifier/commands/shutdown.yaml +8 -0
- data/lib/rubik/security/classifier/commands/slack.yaml +113 -0
- data/lib/rubik/security/classifier/commands/sleep.yaml +8 -0
- data/lib/rubik/security/classifier/commands/snip.yaml +79 -0
- data/lib/rubik/security/classifier/commands/sort.yaml +10 -0
- data/lib/rubik/security/classifier/commands/ss.yaml +7 -0
- data/lib/rubik/security/classifier/commands/ssh.yaml +41 -0
- data/lib/rubik/security/classifier/commands/stat.yaml +7 -0
- data/lib/rubik/security/classifier/commands/stdbuf.yaml +15 -0
- data/lib/rubik/security/classifier/commands/strace.yaml +20 -0
- data/lib/rubik/security/classifier/commands/strings.yaml +11 -0
- data/lib/rubik/security/classifier/commands/sudo.yaml +24 -0
- data/lib/rubik/security/classifier/commands/systemctl.yaml +58 -0
- data/lib/rubik/security/classifier/commands/systemd-run.yaml +30 -0
- data/lib/rubik/security/classifier/commands/tac.yaml +7 -0
- data/lib/rubik/security/classifier/commands/tail.yaml +7 -0
- data/lib/rubik/security/classifier/commands/tar.yaml +25 -0
- data/lib/rubik/security/classifier/commands/tee.yaml +8 -0
- data/lib/rubik/security/classifier/commands/terraform.yaml +70 -0
- data/lib/rubik/security/classifier/commands/test.yaml +7 -0
- data/lib/rubik/security/classifier/commands/time.yaml +8 -0
- data/lib/rubik/security/classifier/commands/timeout.yaml +18 -0
- data/lib/rubik/security/classifier/commands/top.yaml +7 -0
- data/lib/rubik/security/classifier/commands/touch.yaml +8 -0
- data/lib/rubik/security/classifier/commands/tr.yaml +7 -0
- data/lib/rubik/security/classifier/commands/traceroute.yaml +7 -0
- data/lib/rubik/security/classifier/commands/true.yaml +7 -0
- data/lib/rubik/security/classifier/commands/tsc.yaml +16 -0
- data/lib/rubik/security/classifier/commands/type.yaml +7 -0
- data/lib/rubik/security/classifier/commands/uname.yaml +7 -0
- data/lib/rubik/security/classifier/commands/unexpand.yaml +7 -0
- data/lib/rubik/security/classifier/commands/uniq.yaml +7 -0
- data/lib/rubik/security/classifier/commands/unzip.yaml +10 -0
- data/lib/rubik/security/classifier/commands/uptime.yaml +7 -0
- data/lib/rubik/security/classifier/commands/uv.yaml +156 -0
- data/lib/rubik/security/classifier/commands/wait.yaml +11 -0
- data/lib/rubik/security/classifier/commands/wc.yaml +7 -0
- data/lib/rubik/security/classifier/commands/wget.yaml +7 -0
- data/lib/rubik/security/classifier/commands/whereis.yaml +7 -0
- data/lib/rubik/security/classifier/commands/which.yaml +7 -0
- data/lib/rubik/security/classifier/commands/whoami.yaml +7 -0
- data/lib/rubik/security/classifier/commands/xargs.yaml +26 -0
- data/lib/rubik/security/classifier/commands/xmllint.yaml +7 -0
- data/lib/rubik/security/classifier/commands/yarn.yaml +72 -0
- data/lib/rubik/security/classifier/commands/yes.yaml +7 -0
- data/lib/rubik/security/classifier/commands/yq.yaml +10 -0
- data/lib/rubik/security/classifier/commands/zip.yaml +8 -0
- data/lib/rubik/security/classifier/commands/zsh.yaml +14 -0
- data/lib/rubik/security/classifier/database.rb +216 -0
- data/lib/rubik/security/classifier/matcher/delegation.rb +190 -0
- data/lib/rubik/security/classifier/matcher/options.rb +245 -0
- data/lib/rubik/security/classifier/matcher/subcommands.rb +103 -0
- data/lib/rubik/security/classifier/matcher.rb +249 -0
- data/lib/rubik/security/classifier/models.rb +170 -0
- data/lib/rubik/security/classifier/parser.rb +23 -0
- data/lib/rubik/security/classifier/parsers/bash.rb +280 -0
- data/lib/rubik/security/classifier/parsers/bash_extractors.rb +221 -0
- data/lib/rubik/security/classifier/parsers/powershell.rb +185 -0
- data/lib/rubik/security/classifier/pwsh_rules.rb +224 -0
- data/lib/rubik/security/classifier.rb +25 -0
- data/lib/rubik/security/errors.rb +39 -0
- data/lib/rubik/security/guard.rb +954 -0
- data/lib/rubik/security/redactor.rb +89 -0
- data/lib/rubik/security.rb +11 -0
- data/lib/rubik/session/manager.rb +331 -0
- data/lib/rubik/session/recorder.rb +149 -0
- data/lib/rubik/session/replayer.rb +65 -0
- data/lib/rubik/session.rb +10 -0
- data/lib/rubik/skills/frontmatter.rb +41 -0
- data/lib/rubik/skills/manager.rb +223 -0
- data/lib/rubik/skills/registry.rb +64 -0
- data/lib/rubik/skills/skill.rb +33 -0
- data/lib/rubik/skills/storage.rb +77 -0
- data/lib/rubik/skills/validator.rb +116 -0
- data/lib/rubik/skills.rb +20 -0
- data/lib/rubik/storage/folding.rb +23 -0
- data/lib/rubik/storage/layout.rb +145 -0
- data/lib/rubik/storage/render.rb +97 -0
- data/lib/rubik/storage/service.rb +42 -0
- data/lib/rubik/storage/store.rb +514 -0
- data/lib/rubik/storage/transcript_indexer.rb +116 -0
- data/lib/rubik/storage/uri_reader.rb +168 -0
- data/lib/rubik/storage/vakanuvis/annal_writer.rb +92 -0
- data/lib/rubik/storage/vakanuvis/distill_gate.rb +323 -0
- data/lib/rubik/storage/vakanuvis/gate.rb +250 -0
- data/lib/rubik/storage/vakanuvis/harness.rb +421 -0
- data/lib/rubik/storage/vakanuvis/memory_steward.rb +163 -0
- data/lib/rubik/storage/vakanuvis/service.rb +69 -0
- data/lib/rubik/storage/vakanuvis/vocabulary.rb +99 -0
- data/lib/rubik/storage/vakanuvis/wiki_skeleton.rb +67 -0
- data/lib/rubik/storage/vakanuvis.rb +77 -0
- data/lib/rubik/storage.rb +29 -0
- data/lib/rubik/tools/base.rb +107 -0
- data/lib/rubik/tools/builtins/dmesg.rb +74 -0
- data/lib/rubik/tools/builtins/exec.rb +293 -0
- data/lib/rubik/tools/builtins/files.rb +354 -0
- data/lib/rubik/tools/builtins/goal_ledger.rb +89 -0
- data/lib/rubik/tools/builtins/grep.rb +281 -0
- data/lib/rubik/tools/builtins/interactive.rb +175 -0
- data/lib/rubik/tools/builtins/lifecycle.rb +359 -0
- data/lib/rubik/tools/builtins/lsp.rb +283 -0
- data/lib/rubik/tools/builtins/pruner.rb +88 -0
- data/lib/rubik/tools/builtins/runtime.rb +120 -0
- data/lib/rubik/tools/builtins/send_message.rb +109 -0
- data/lib/rubik/tools/builtins/show_file.rb +84 -0
- data/lib/rubik/tools/builtins/skill.rb +72 -0
- data/lib/rubik/tools/builtins/storage.rb +232 -0
- data/lib/rubik/tools/builtins/subagent.rb +356 -0
- data/lib/rubik/tools/builtins/subagent_lifecycle.rb +72 -0
- data/lib/rubik/tools/builtins/symbols.rb +209 -0
- data/lib/rubik/tools/builtins/synthesis.rb +314 -0
- data/lib/rubik/tools/builtins/task.rb +168 -0
- data/lib/rubik/tools/builtins/unsynthesize.rb +111 -0
- data/lib/rubik/tools/builtins/vakanuvis.rb +161 -0
- data/lib/rubik/tools/builtins.rb +30 -0
- data/lib/rubik/tools/catalog.rb +70 -0
- data/lib/rubik/tools/declaration.rb +90 -0
- data/lib/rubik/tools/diagnostic_reporter.rb +283 -0
- data/lib/rubik/tools/fuzzy_matcher.rb +172 -0
- data/lib/rubik/tools/native_search.rb +180 -0
- data/lib/rubik/tools/output.rb +54 -0
- data/lib/rubik/tools/preflight_runner.rb +209 -0
- data/lib/rubik/tools/registry.rb +247 -0
- data/lib/rubik/tools/result.rb +71 -0
- data/lib/rubik/tools/services.rb +96 -0
- data/lib/rubik/tools/truncator.rb +78 -0
- data/lib/rubik/tools/uri_resolver.rb +195 -0
- data/lib/rubik/tools.rb +14 -0
- data/lib/rubik/ui/approvals.rb +52 -0
- data/lib/rubik/ui/banner.rb +43 -0
- data/lib/rubik/ui/capability.rb +122 -0
- data/lib/rubik/ui/dialog.rb +106 -0
- data/lib/rubik/ui/diff_viewer.rb +78 -0
- data/lib/rubik/ui/frame.rb +95 -0
- data/lib/rubik/ui/markdown_stream.rb +107 -0
- data/lib/rubik/ui/panel.rb +98 -0
- data/lib/rubik/ui/renderer.rb +206 -0
- data/lib/rubik/ui/spinner.rb +120 -0
- data/lib/rubik/ui/statusline.rb +134 -0
- data/lib/rubik/ui/theme.rb +118 -0
- data/lib/rubik/ui.rb +57 -0
- data/lib/rubik/uris.rb +101 -0
- data/lib/rubik/version.rb +5 -0
- data/lib/rubik.rb +65 -0
- metadata +770 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "yaml/store"
|
|
5
|
+
require "fileutils"
|
|
6
|
+
|
|
7
|
+
module Rubik
|
|
8
|
+
class Config
|
|
9
|
+
module Store
|
|
10
|
+
def global_config_path
|
|
11
|
+
File.join(@home_dir, ".rubik", "config.yml")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def project_config_path
|
|
15
|
+
dot_rubik_config = File.join(@root_dir, ".rubik", "config.yml")
|
|
16
|
+
return dot_rubik_config if File.exist?(dot_rubik_config)
|
|
17
|
+
|
|
18
|
+
root_rubik_yml = File.join(@root_dir, "rubik.yml")
|
|
19
|
+
return root_rubik_yml if File.exist?(root_rubik_yml)
|
|
20
|
+
|
|
21
|
+
dot_rubik_config
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def store_for(scope = :project)
|
|
25
|
+
scope_sym = scope.to_s.downcase.to_sym
|
|
26
|
+
path = case scope_sym
|
|
27
|
+
when :project
|
|
28
|
+
project_config_path
|
|
29
|
+
when :global
|
|
30
|
+
global_config_path
|
|
31
|
+
else
|
|
32
|
+
scope.to_s
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
@stores ||= {}
|
|
36
|
+
@stores[path] ||= begin
|
|
37
|
+
begin
|
|
38
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
39
|
+
rescue SystemCallError => e
|
|
40
|
+
warn "[WARN] Failed to create config directory #{File.dirname(path)}: #{e.message}" if ENV["RUBIK_DEBUG"]
|
|
41
|
+
end
|
|
42
|
+
YAML::Store.new(path, true)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def transaction(scope = :project, read_only: false, &)
|
|
47
|
+
store = store_for(scope)
|
|
48
|
+
store.transaction(read_only, &)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Persists configuration to the targeted scope (:global or :project) on disk.
|
|
52
|
+
# Semantics of deep_merge: in-memory @data is deep_merged over disk YAML to keep
|
|
53
|
+
# unmanaged keys, while provider sections ("models"/"model") are overwritten to
|
|
54
|
+
# clear stale specs. Both source and merged data undergo schema validation.
|
|
55
|
+
# `deletions` are dot-paths removed from the merged snapshot after the merge —
|
|
56
|
+
# deep_merge deliberately keeps disk-only keys, so a nested delete must be
|
|
57
|
+
# replayed here or the disk copy would resurrect it.
|
|
58
|
+
def save!(scope = :global, deletions: [])
|
|
59
|
+
return self if scope.nil? || scope == false || scope.to_s.downcase == "session" || scope.to_s.downcase == "none"
|
|
60
|
+
|
|
61
|
+
scope_sym = scope.to_s.downcase.to_sym == :project ? :project : :global
|
|
62
|
+
target_path = scope_sym == :project ? project_config_path : global_config_path
|
|
63
|
+
begin
|
|
64
|
+
FileUtils.mkdir_p(File.dirname(target_path))
|
|
65
|
+
rescue SystemCallError
|
|
66
|
+
nil
|
|
67
|
+
end
|
|
68
|
+
store = store_for(scope_sym)
|
|
69
|
+
|
|
70
|
+
validation_error = nil
|
|
71
|
+
store.transaction do
|
|
72
|
+
# Read existing data on disk
|
|
73
|
+
existing = store.roots.to_h { |k| [k, store[k]] }
|
|
74
|
+
merged = deep_merge(existing, deep_stringify_keys(@data))
|
|
75
|
+
|
|
76
|
+
# Explicitly overwrite model and role specs so stale keys from prior providers are not retained
|
|
77
|
+
if @data["models"].is_a?(Hash)
|
|
78
|
+
merged["models"] ||= {}
|
|
79
|
+
@data["models"].each do |r_name, r_spec|
|
|
80
|
+
merged["models"][r_name.to_s] = deep_dup(r_spec)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
merged["model"] = deep_dup(@data["model"]) if @data["model"].is_a?(Hash)
|
|
84
|
+
|
|
85
|
+
# Replay nested deletions so validate_schema sees what will be written.
|
|
86
|
+
# A blocked path means the deletion cannot reach disk and would
|
|
87
|
+
# silently resurrect on the next load — fail the save like a
|
|
88
|
+
# schema violation.
|
|
89
|
+
unapplied = Array(deletions).select { |path| deep_delete(merged, path) == :blocked }
|
|
90
|
+
validation_error = "could not apply deletion(s): #{unapplied.join(', ')}" if unapplied.any?
|
|
91
|
+
|
|
92
|
+
# Schema validation
|
|
93
|
+
unless validation_error
|
|
94
|
+
begin
|
|
95
|
+
validate_schema(@data)
|
|
96
|
+
validate_schema(merged)
|
|
97
|
+
rescue ArgumentError => e
|
|
98
|
+
validation_error = e.message
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
store.abort if validation_error
|
|
102
|
+
|
|
103
|
+
# Remove deleted roots
|
|
104
|
+
(store.roots - merged.keys).each { |r| store.delete(r) }
|
|
105
|
+
|
|
106
|
+
# Save merged entries
|
|
107
|
+
merged.each { |k, v| store[k] = v }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
if validation_error
|
|
111
|
+
{ ok: false, error: validation_error }
|
|
112
|
+
else
|
|
113
|
+
if File.exist?(target_path)
|
|
114
|
+
begin
|
|
115
|
+
File.chmod(0o600, target_path)
|
|
116
|
+
rescue
|
|
117
|
+
nil
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
{ ok: true, path: target_path, scope: scope_sym }
|
|
121
|
+
end
|
|
122
|
+
rescue StandardError => e
|
|
123
|
+
{ ok: false, error: e.message }
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
private
|
|
127
|
+
|
|
128
|
+
def ensure_rubik_directories
|
|
129
|
+
rubik_home = File.join(@home_dir, ".rubik")
|
|
130
|
+
["snapshots", "sessions", "commands", "tools"].each do |sub|
|
|
131
|
+
FileUtils.mkdir_p(File.join(rubik_home, sub))
|
|
132
|
+
end
|
|
133
|
+
rescue SystemCallError => e
|
|
134
|
+
@warnings << "Could not create ~/.rubik directory: #{e.message}"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def load_yaml(path)
|
|
138
|
+
return {} unless File.exist?(path)
|
|
139
|
+
|
|
140
|
+
store = store_for(path)
|
|
141
|
+
data = store.transaction(true) do
|
|
142
|
+
store.roots.to_h { |root| [root, store[root]] }
|
|
143
|
+
end
|
|
144
|
+
data.is_a?(Hash) ? deep_stringify_keys(data) : {}
|
|
145
|
+
rescue Psych::Exception, SystemCallError => e
|
|
146
|
+
# Psych::Exception, not just SyntaxError: YAML::Store#load safe_loads
|
|
147
|
+
# without aliases or Date, so a hand-edited "x: 2026-01-01" raises
|
|
148
|
+
# Psych::DisallowedClass and must still boot with defaults + warning.
|
|
149
|
+
warn_msg = "Cannot load config because #{e.message}. To fix, verify valid YAML syntax in #{path}."
|
|
150
|
+
@warnings << warn_msg
|
|
151
|
+
{}
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
data/lib/rubik/config.rb
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "config/defaults"
|
|
4
|
+
require_relative "config/deep_ops"
|
|
5
|
+
require_relative "config/store"
|
|
6
|
+
require_relative "config/model_catalog"
|
|
7
|
+
|
|
8
|
+
module Rubik
|
|
9
|
+
class Config
|
|
10
|
+
include Defaults
|
|
11
|
+
include DeepOps
|
|
12
|
+
include Store
|
|
13
|
+
include ModelCatalog
|
|
14
|
+
|
|
15
|
+
DEFAULT_CONFIG = Defaults::DEFAULT_CONFIG
|
|
16
|
+
|
|
17
|
+
attr_reader :data, :warnings, :root_dir, :home_dir
|
|
18
|
+
attr_accessor :event_bus
|
|
19
|
+
|
|
20
|
+
def initialize(root_dir: Dir.pwd, home_dir: nil)
|
|
21
|
+
@root_dir = File.expand_path(root_dir)
|
|
22
|
+
effective_home = home_dir || (if Rubik.const_defined?(:Platform)
|
|
23
|
+
Rubik::Platform.home_dir
|
|
24
|
+
else
|
|
25
|
+
begin
|
|
26
|
+
Dir.home
|
|
27
|
+
rescue ArgumentError
|
|
28
|
+
(ENV["USERPROFILE"] || ENV["HOME"] || ".")
|
|
29
|
+
end
|
|
30
|
+
end)
|
|
31
|
+
@home_dir = File.expand_path(effective_home)
|
|
32
|
+
@warnings = []
|
|
33
|
+
@stores = {}
|
|
34
|
+
@listeners = []
|
|
35
|
+
@event_bus = nil
|
|
36
|
+
ensure_rubik_directories
|
|
37
|
+
@data = load_merged_config
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def on_change(prefix = nil, &block)
|
|
41
|
+
return proc {} unless block_given?
|
|
42
|
+
|
|
43
|
+
norm_prefix = prefix&.to_s&.sub(/\.\z/, "")
|
|
44
|
+
entry = { prefix: norm_prefix, callback: block }
|
|
45
|
+
@listeners << entry
|
|
46
|
+
proc { @listeners.delete(entry) }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Retrieves a configuration value by dot-separated key path.
|
|
50
|
+
def fetch(key, default = nil)
|
|
51
|
+
keys = key.to_s.split(".")
|
|
52
|
+
keys.reduce(@data) do |acc, k|
|
|
53
|
+
return default unless acc.is_a?(Hash) && acc.key?(k)
|
|
54
|
+
|
|
55
|
+
acc[k]
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def [](key)
|
|
60
|
+
fetch(key)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Sets a configuration value at a dot-separated key path, optionally
|
|
64
|
+
# persisting to disk. Returns the persist outcome — {ok: true, path:,
|
|
65
|
+
# scope:} after a disk write, {ok: true} for session-only changes,
|
|
66
|
+
# {ok: false, error:, reason: :persist_failed} when the write failed —
|
|
67
|
+
# so callers report honestly instead of assuming success.
|
|
68
|
+
def set(key, value, persist: nil)
|
|
69
|
+
old_val = fetch(key)
|
|
70
|
+
typed_val = parse_scalar_value(value)
|
|
71
|
+
keys = key.to_s.split(".")
|
|
72
|
+
last_key = keys.pop
|
|
73
|
+
target = keys.reduce(@data) do |acc, k|
|
|
74
|
+
acc[k] ||= {}
|
|
75
|
+
acc[k]
|
|
76
|
+
end
|
|
77
|
+
target[last_key] = typed_val
|
|
78
|
+
|
|
79
|
+
res = persisting?(persist) ? persist_result(save!(persist)) : { ok: true }
|
|
80
|
+
res[:reason] = :persist_failed unless res[:ok]
|
|
81
|
+
notify_change(key, typed_val, old_val) if old_val != typed_val
|
|
82
|
+
res
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Removes the dot-path key from configuration, persisting like set.
|
|
86
|
+
# Deleting an absent key is a successful no-op. Returns the persist
|
|
87
|
+
# outcome (see set); notifies no listener — deletion callers refresh
|
|
88
|
+
# the runtime explicitly.
|
|
89
|
+
def delete(key, persist: nil)
|
|
90
|
+
blocked = deep_delete(@data, key) == :blocked
|
|
91
|
+
return { ok: false, error: "cannot delete '#{key}': path traverses a non-hash value" } if blocked
|
|
92
|
+
|
|
93
|
+
res = persisting?(persist) ? persist_result(save!(persist, deletions: [key])) : { ok: true }
|
|
94
|
+
res[:reason] = :persist_failed unless res[:ok]
|
|
95
|
+
res
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# True only for scopes save! writes to disk; nil/false/:session/"session"/
|
|
99
|
+
# "none" (downcased — save! itself is case-sensitive, this is not) never
|
|
100
|
+
# touch disk. Public so consumers wiring persist guards agree with Config.
|
|
101
|
+
def persisting?(scope)
|
|
102
|
+
return false if scope.nil? || scope == false
|
|
103
|
+
|
|
104
|
+
!["session", "none"].include?(scope.to_s.downcase)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def parse_scalar_value(val)
|
|
108
|
+
return val unless val.is_a?(String)
|
|
109
|
+
|
|
110
|
+
trimmed = val.strip
|
|
111
|
+
case trimmed.downcase
|
|
112
|
+
when "true" then true
|
|
113
|
+
when "false" then false
|
|
114
|
+
when "nil", "null" then nil
|
|
115
|
+
else
|
|
116
|
+
if (trimmed.start_with?("[") && trimmed.end_with?("]")) || (trimmed.start_with?("{") && trimmed.end_with?("}"))
|
|
117
|
+
begin
|
|
118
|
+
parsed = YAML.safe_load(trimmed, permitted_classes: [Symbol], aliases: true)
|
|
119
|
+
return parsed if parsed.is_a?(Array) || parsed.is_a?(Hash)
|
|
120
|
+
rescue Psych::Exception
|
|
121
|
+
# The whole safe_load failure family: SyntaxError for malformed
|
|
122
|
+
# input, DisallowedClass for e.g. "[2026-01-01]" (Date unpermitted),
|
|
123
|
+
# Alias errors — all mean "not a typed collection" → raw string.
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
if trimmed =~ /^-?\d+$/
|
|
128
|
+
trimmed.to_i
|
|
129
|
+
elsif trimmed =~ /^-?\d+\.\d+$/
|
|
130
|
+
trimmed.to_f
|
|
131
|
+
else
|
|
132
|
+
val
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def model_spec(role = :default)
|
|
138
|
+
role_str = role.to_s.downcase
|
|
139
|
+
spec = fetch("models.#{role_str}")
|
|
140
|
+
return spec if spec.is_a?(Hash) && spec["provider"] && spec["name"]
|
|
141
|
+
|
|
142
|
+
{
|
|
143
|
+
"provider" => fetch("model.provider", "ollama"),
|
|
144
|
+
"name" => fetch("model.name", "qwen2.5-coder:7b"),
|
|
145
|
+
"endpoint" => fetch("model.endpoint") || fetch("model.host", "http://localhost:11434")
|
|
146
|
+
}
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def verbosity
|
|
150
|
+
fetch("ui.verbosity") || fetch("agent.verbosity", "normal")
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def verbosity_level
|
|
154
|
+
case verbosity.to_s.downcase.strip
|
|
155
|
+
when "quiet", "0" then 0
|
|
156
|
+
when "verbose", "2" then 2
|
|
157
|
+
when "debug", "trace", "3" then 3
|
|
158
|
+
else 1
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
private
|
|
163
|
+
|
|
164
|
+
# save! returns itself for no-write scopes; persist outcomes are hashes.
|
|
165
|
+
def persist_result(res)
|
|
166
|
+
res.is_a?(Hash) ? res : { ok: true }
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def notify_change(key, new_val, old_val)
|
|
170
|
+
key_str = key.to_s
|
|
171
|
+
@listeners.dup.each do |entry|
|
|
172
|
+
prefix = entry[:prefix]
|
|
173
|
+
matches = prefix.nil? || prefix.empty? || key_str == prefix || key_str.start_with?("#{prefix}.")
|
|
174
|
+
next unless matches
|
|
175
|
+
|
|
176
|
+
begin
|
|
177
|
+
entry[:callback].call(key_str, new_val, old_val)
|
|
178
|
+
rescue StandardError => e
|
|
179
|
+
warn "[WARN] Config listener failed for '#{key_str}': #{e.message}"
|
|
180
|
+
@event_bus&.emit(:config_listener_error, key: key_str, error: e.message)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
@event_bus&.emit(:config_changed, key: key_str, value: new_val, old_val: old_val)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def load_merged_config
|
|
187
|
+
global_file = global_config_path
|
|
188
|
+
project_file = project_config_path
|
|
189
|
+
|
|
190
|
+
global_config = load_yaml(global_file)
|
|
191
|
+
project_config = load_yaml(project_file)
|
|
192
|
+
|
|
193
|
+
merged = deep_merge(deep_dup(DEFAULT_CONFIG), global_config)
|
|
194
|
+
merged = deep_merge(merged, project_config)
|
|
195
|
+
|
|
196
|
+
# If global or project config explicitly defines model roles, ensure stale default hosts are not retained
|
|
197
|
+
[global_config, project_config].each do |cfg|
|
|
198
|
+
next unless cfg.is_a?(Hash)
|
|
199
|
+
|
|
200
|
+
if cfg["models"].is_a?(Hash)
|
|
201
|
+
cfg["models"].each do |role, spec|
|
|
202
|
+
if spec.is_a?(Hash) && spec["provider"] && spec["name"]
|
|
203
|
+
merged["models"] ||= {}
|
|
204
|
+
merged["models"][role.to_s] = deep_dup(spec)
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
merged["model"] = deep_dup(cfg["model"]) if cfg["model"].is_a?(Hash) && cfg["model"]["provider"] && cfg["model"]["name"]
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
validate_schema(merged)
|
|
213
|
+
merged
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "shellwords"
|
|
5
|
+
|
|
6
|
+
module Rubik
|
|
7
|
+
module Context
|
|
8
|
+
class GitInfo
|
|
9
|
+
attr_reader :root_dir
|
|
10
|
+
|
|
11
|
+
def initialize(root_dir: Dir.pwd, process_runner: nil)
|
|
12
|
+
@root_dir = File.expand_path(root_dir)
|
|
13
|
+
@process_runner = process_runner || method(:default_runner)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def git_repo?
|
|
17
|
+
!git_dir.nil?
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def active_branch
|
|
21
|
+
dir = git_dir
|
|
22
|
+
return nil unless dir
|
|
23
|
+
|
|
24
|
+
head_file = File.join(dir, "HEAD")
|
|
25
|
+
return nil unless File.exist?(head_file)
|
|
26
|
+
|
|
27
|
+
content = Rubik::Platform.read_text(head_file).strip
|
|
28
|
+
return nil if content.empty?
|
|
29
|
+
|
|
30
|
+
if content.start_with?("ref: refs/heads/")
|
|
31
|
+
content.sub("ref: refs/heads/", "")
|
|
32
|
+
else
|
|
33
|
+
content[0..6]
|
|
34
|
+
end
|
|
35
|
+
rescue SystemCallError, ArgumentError
|
|
36
|
+
# ArgumentError: expand_path over hostile .git-file content (NUL, bad tilde)
|
|
37
|
+
nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def status
|
|
41
|
+
return { git: false } unless git_repo?
|
|
42
|
+
|
|
43
|
+
out, code = run_git("status --porcelain")
|
|
44
|
+
return { git: false } unless code.zero?
|
|
45
|
+
|
|
46
|
+
dirty = !out.strip.empty?
|
|
47
|
+
{
|
|
48
|
+
git: true,
|
|
49
|
+
branch: active_branch,
|
|
50
|
+
dirty: dirty
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
# The .git entry can be a directory (normal clone) or a FILE holding
|
|
57
|
+
# "gitdir: <path>" (linked worktree / submodule). Returns the directory
|
|
58
|
+
# that owns HEAD either way, or nil when the root is not a checkout.
|
|
59
|
+
def git_dir
|
|
60
|
+
dot_git = File.join(@root_dir, ".git")
|
|
61
|
+
if File.directory?(dot_git)
|
|
62
|
+
dot_git
|
|
63
|
+
elsif File.file?(dot_git)
|
|
64
|
+
target = Rubik::Platform.read_text(dot_git).strip.delete_prefix("gitdir:").strip
|
|
65
|
+
File.expand_path(target, @root_dir) unless target.empty?
|
|
66
|
+
end
|
|
67
|
+
rescue SystemCallError, ArgumentError
|
|
68
|
+
nil
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def default_runner(cmd)
|
|
72
|
+
args = cmd.is_a?(Array) ? cmd : Shellwords.split(cmd.to_s)
|
|
73
|
+
out, status = Open3.capture2e("git", "-C", @root_dir, *args)
|
|
74
|
+
[out, status.exitstatus || 1]
|
|
75
|
+
rescue StandardError
|
|
76
|
+
["", 1]
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def run_git(cmd)
|
|
80
|
+
@process_runner.call(cmd)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
module Rubik
|
|
6
|
+
module Context
|
|
7
|
+
# Fast file listing via `git ls-files` (C-speed walk + gitignore handling).
|
|
8
|
+
# Returns nil whenever git cannot answer (not a repo, spawn failure) so
|
|
9
|
+
# the caller falls back to its own walker — a missing git must degrade,
|
|
10
|
+
# never raise. Spawn is call-scoped: no persistent process, thread, or
|
|
11
|
+
# listener is kept alive.
|
|
12
|
+
module GitLister
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
# @return [Array<String>, nil] POSIX-style paths relative to root_dir,
|
|
16
|
+
# or nil when git is unusable.
|
|
17
|
+
def files(root_dir)
|
|
18
|
+
out, _err, status = Open3.capture3(
|
|
19
|
+
"git", "-C", root_dir.to_s,
|
|
20
|
+
"ls-files", "-z", "--cached", "--others", "--exclude-standard"
|
|
21
|
+
)
|
|
22
|
+
return nil unless status.success?
|
|
23
|
+
|
|
24
|
+
out.split("\x00", -1).reject(&:empty?).map { |p| p.tr("\\", "/") }.uniq
|
|
25
|
+
rescue StandardError
|
|
26
|
+
nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Switch resolution shared with the scanner: ENV beats config, anything
|
|
30
|
+
# other than "off" (auto/on/unknown) attempts the fast path first.
|
|
31
|
+
def enabled?(config_value, env: ENV)
|
|
32
|
+
mode = env["RUBIK_NATIVE"] || config_value || "auto"
|
|
33
|
+
mode.to_s.strip.downcase != "off"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
require_relative "scanner"
|
|
5
|
+
|
|
6
|
+
module Rubik
|
|
7
|
+
module Context
|
|
8
|
+
class Index
|
|
9
|
+
attr_reader :root_dir, :files, :languages, :last_scan_time, :scanner
|
|
10
|
+
|
|
11
|
+
def initialize(root_dir: Dir.pwd, scanner: nil, additional_roots: [], includes: [], excludes: [], native: nil)
|
|
12
|
+
@root_dir = File.expand_path(root_dir)
|
|
13
|
+
# Serialises mutations to @files / @languages. The index is driven both
|
|
14
|
+
# by synchronous REPL turns and by an async background AST indexer, so
|
|
15
|
+
# without a shared lock concurrent incremental_scan / update_file /
|
|
16
|
+
# full_scan / detect_languages calls race on the same hash (ruby/ruby#
|
|
17
|
+
# 17006) and can corrupt it. Scanning itself is not changed — only the
|
|
18
|
+
# shared-state mutation is made thread-safe.
|
|
19
|
+
@mutex = Mutex.new
|
|
20
|
+
@scanner = scanner || Scanner.new(
|
|
21
|
+
root_dir: @root_dir,
|
|
22
|
+
additional_roots: additional_roots,
|
|
23
|
+
includes: includes,
|
|
24
|
+
excludes: excludes,
|
|
25
|
+
native: native
|
|
26
|
+
)
|
|
27
|
+
@files = {}
|
|
28
|
+
@languages = []
|
|
29
|
+
@last_scan_time = nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def update_file_filters(includes: nil, excludes: nil)
|
|
33
|
+
@scanner.custom_includes = Array(includes).compact unless includes.nil?
|
|
34
|
+
@scanner.custom_excludes = Array(excludes).compact unless excludes.nil?
|
|
35
|
+
full_scan
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def additional_roots
|
|
39
|
+
@scanner.additional_roots
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def additional_roots=(roots)
|
|
43
|
+
@scanner.additional_roots = roots
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def full_scan
|
|
47
|
+
start_time = Time.now
|
|
48
|
+
files = @scanner.scan
|
|
49
|
+
languages = @scanner.detect_languages(files)
|
|
50
|
+
@mutex.synchronize do
|
|
51
|
+
@files = files
|
|
52
|
+
@languages = languages
|
|
53
|
+
@last_scan_time = Time.now - start_time
|
|
54
|
+
end
|
|
55
|
+
@files
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def incremental_scan
|
|
59
|
+
start_time = Time.now
|
|
60
|
+
snapshot = @mutex.synchronize { @files }
|
|
61
|
+
new_scan = @scanner.scan(snapshot)
|
|
62
|
+
old_files = snapshot
|
|
63
|
+
|
|
64
|
+
added = []
|
|
65
|
+
modified = []
|
|
66
|
+
deleted = []
|
|
67
|
+
unchanged = []
|
|
68
|
+
|
|
69
|
+
new_scan.each do |rel_path, info|
|
|
70
|
+
if old_files.key?(rel_path)
|
|
71
|
+
old_info = old_files[rel_path]
|
|
72
|
+
if old_info[:mtime] != info[:mtime] || old_info[:size] != info[:size] || old_info[:hash] != info[:hash]
|
|
73
|
+
modified << rel_path
|
|
74
|
+
else
|
|
75
|
+
unchanged << rel_path
|
|
76
|
+
end
|
|
77
|
+
else
|
|
78
|
+
added << rel_path
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
old_files.each_key do |rel_path|
|
|
83
|
+
deleted << rel_path unless new_scan.key?(rel_path)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
languages = @scanner.detect_languages(new_scan)
|
|
87
|
+
@mutex.synchronize do
|
|
88
|
+
@files = new_scan
|
|
89
|
+
@languages = languages
|
|
90
|
+
@last_scan_time = Time.now - start_time
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
{
|
|
94
|
+
added: added,
|
|
95
|
+
modified: modified,
|
|
96
|
+
deleted: deleted,
|
|
97
|
+
unchanged: unchanged
|
|
98
|
+
}
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def update_file(rel_path)
|
|
102
|
+
full_path = File.expand_path(rel_path, @root_dir)
|
|
103
|
+
unless File.file?(full_path)
|
|
104
|
+
return @mutex.synchronize do
|
|
105
|
+
@files.delete(rel_path)
|
|
106
|
+
false
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
info = @scanner.inspect_file(rel_path, full_path)
|
|
111
|
+
@mutex.synchronize do
|
|
112
|
+
old_info = @files[rel_path]
|
|
113
|
+
if old_info.nil? || old_info[:hash] != info[:hash]
|
|
114
|
+
@files[rel_path] = info
|
|
115
|
+
@languages = @scanner.detect_languages(@files)
|
|
116
|
+
true
|
|
117
|
+
else
|
|
118
|
+
false
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def status_summary
|
|
124
|
+
@mutex.synchronize do
|
|
125
|
+
{
|
|
126
|
+
file_count: @files.size,
|
|
127
|
+
languages: @languages,
|
|
128
|
+
last_scan_time_sec: @last_scan_time&.round(4)
|
|
129
|
+
}
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|