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,283 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../storage/layout"
|
|
4
|
+
|
|
5
|
+
module Rubik
|
|
6
|
+
module Tools
|
|
7
|
+
# Generates empathetic, actionable Elm-compiler-style error diagnostics (Plan 56 D-2).
|
|
8
|
+
# Combines clear problem statements, context inspection, "Did you mean?" suggestions,
|
|
9
|
+
# and numbered next steps to optimize LLM self-healing on first attempt.
|
|
10
|
+
class DiagnosticReporter
|
|
11
|
+
HEADER_WIDTH = 70
|
|
12
|
+
|
|
13
|
+
def self.box_header(title)
|
|
14
|
+
dashes_len = [HEADER_WIDTH - title.length - 4, 4].max
|
|
15
|
+
"-- #{title.upcase} #{'-' * dashes_len}"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Elm-style diagnostic when a file path does not exist in workspace.
|
|
19
|
+
def self.render_file_not_found(path, candidates: [], existing_dir_files: [])
|
|
20
|
+
lines = [box_header("File Not Found")]
|
|
21
|
+
lines << "I cannot find the file `#{path}` in the project workspace."
|
|
22
|
+
lines << ""
|
|
23
|
+
|
|
24
|
+
if candidates && !candidates.empty?
|
|
25
|
+
lines << "Did you mean this?"
|
|
26
|
+
candidates.each { |c| lines << " • #{c}" }
|
|
27
|
+
lines << ""
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
if existing_dir_files && !existing_dir_files.empty?
|
|
31
|
+
dir_name = File.dirname(path)
|
|
32
|
+
display_dir = dir_name == "." ? "project root" : "`#{dir_name}/`"
|
|
33
|
+
lines << "Existing files in #{display_dir}:"
|
|
34
|
+
existing_dir_files.first(8).each { |f| lines << " • #{f}" }
|
|
35
|
+
lines << ""
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
lines << "Actionable Next Steps:"
|
|
39
|
+
lines << if candidates && !candidates.empty?
|
|
40
|
+
" 1. If you meant an existing file, call `read_file(path: '#{candidates.first}')`."
|
|
41
|
+
else
|
|
42
|
+
" 1. Verify the exact path or explore directory structure using `list_files(path: '#{File.dirname(path)}')`."
|
|
43
|
+
end
|
|
44
|
+
lines << " 2. If you want to create this new file, call `write_file(path: '#{path}', content: '...')`."
|
|
45
|
+
lines << " 3. To search for code containing this filename, call `grep_search(query: '#{File.basename(path)}')`."
|
|
46
|
+
|
|
47
|
+
lines.join("\n")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Elm-style diagnostic when target_content is not found in the file or range.
|
|
51
|
+
def self.render_target_not_found(path, target_content, closest_snippet: nil, closest_range: nil, similarity: nil, start_line: nil, end_line: nil)
|
|
52
|
+
range_str = start_line || end_line ? " within lines #{start_line || 1}..#{end_line || 'end'}" : ""
|
|
53
|
+
lines = [box_header("Target Content Not Found")]
|
|
54
|
+
lines << "I could not find the exact `target_content` in `#{path}`#{range_str}."
|
|
55
|
+
lines << ""
|
|
56
|
+
lines << "Target content requested:"
|
|
57
|
+
lines << format_snippet_box(target_content)
|
|
58
|
+
lines << ""
|
|
59
|
+
|
|
60
|
+
if closest_snippet && !closest_snippet.empty?
|
|
61
|
+
sim_str = similarity ? " (#{similarity}% similarity)" : ""
|
|
62
|
+
range_label = closest_range ? "lines #{closest_range.begin}..#{closest_range.end}" : "nearby lines"
|
|
63
|
+
lines << "Closest matching snippet found at #{range_label}#{sim_str}:"
|
|
64
|
+
lines << format_snippet_box(closest_snippet)
|
|
65
|
+
lines << ""
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
suggested_offset = closest_range ? [closest_range.begin - 2, 1].max : [start_line.to_i - 5, 1].max
|
|
69
|
+
lines << "Actionable Next Steps:"
|
|
70
|
+
lines << " 1. Call `read_file(path: '#{path}', offset: #{suggested_offset}, limit: 25)` to inspect exact indentation, whitespace, and line content."
|
|
71
|
+
lines << " 2. Re-run `replace_file_content` with the exact verbatim lines from the file."
|
|
72
|
+
lines << " 3. Or omit `start_line`/`end_line` if the target content moved elsewhere in the file." if start_line || end_line
|
|
73
|
+
|
|
74
|
+
lines.join("\n")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Elm-style diagnostic when target_content matches multiple locations.
|
|
78
|
+
def self.render_target_not_unique(path, target_content, match_count, line_occurrences = [])
|
|
79
|
+
lines = [box_header("Target Content Not Unique")]
|
|
80
|
+
occ_str = line_occurrences.empty? ? "" : " at lines: #{line_occurrences.join(', ')}"
|
|
81
|
+
lines << "The specified `target_content` was found #{match_count} times in `#{path}`#{occ_str}."
|
|
82
|
+
lines << ""
|
|
83
|
+
lines << "Target content:"
|
|
84
|
+
lines << format_snippet_box(target_content)
|
|
85
|
+
lines << ""
|
|
86
|
+
lines << "Actionable Next Steps:"
|
|
87
|
+
lines << " 1. Provide more surrounding context lines in `target_content` to make the match unique."
|
|
88
|
+
if line_occurrences && line_occurrences.size >= 2
|
|
89
|
+
first_occ = line_occurrences.first
|
|
90
|
+
start_hint = [first_occ - 2, 1].max
|
|
91
|
+
end_hint = first_occ + target_content.to_s.lines.size + 2
|
|
92
|
+
lines << " 2. Or restrict the search range with `start_line` and `end_line` (e.g. `start_line: #{start_hint}, end_line: #{end_hint}`)."
|
|
93
|
+
else
|
|
94
|
+
lines << " 2. Or specify `start_line` and `end_line` to target the exact occurrence."
|
|
95
|
+
end
|
|
96
|
+
lines << " 3. If you intentionally want to replace ALL #{match_count} occurrences across the entire file, pass `allow_multiple: true`."
|
|
97
|
+
|
|
98
|
+
lines.join("\n")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Elm-style diagnostic when a tool name is unrecognized.
|
|
102
|
+
def self.render_unknown_tool(tool_name, candidate_tools: [], all_tools: [])
|
|
103
|
+
lines = [box_header("Unknown Tool")]
|
|
104
|
+
lines << "I cannot find an active tool named `#{tool_name}`."
|
|
105
|
+
lines << ""
|
|
106
|
+
|
|
107
|
+
if candidate_tools && !candidate_tools.empty?
|
|
108
|
+
lines << "Did you mean one of these?"
|
|
109
|
+
candidate_tools.each { |c| lines << " • #{c}" }
|
|
110
|
+
lines << ""
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
if all_tools && !all_tools.empty?
|
|
114
|
+
formatted_tools = all_tools.map { |t| "`#{t}`" }.join(", ")
|
|
115
|
+
lines << "Available direct tools in toolbelt:"
|
|
116
|
+
lines << " #{formatted_tools}"
|
|
117
|
+
lines << ""
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
lines << "💡 Need a specialized capability?"
|
|
121
|
+
lines << " You can dynamically create and register your own custom tool at runtime using:"
|
|
122
|
+
lines << " `synthesize_tool(name: '#{tool_name.to_s.downcase}', description: '...', code: '->(args) { ... }')`"
|
|
123
|
+
|
|
124
|
+
lines.join("\n")
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Elm-style diagnostic when a symbol definition is not found in AST.
|
|
128
|
+
def self.render_symbol_not_found(sym_name, candidate_symbols: [])
|
|
129
|
+
lines = [box_header("Symbol Not Found")]
|
|
130
|
+
lines << "I could not find the symbol `#{sym_name}` in the AST semantic graph."
|
|
131
|
+
lines << ""
|
|
132
|
+
|
|
133
|
+
if candidate_symbols && !candidate_symbols.empty?
|
|
134
|
+
lines << "Did you mean one of these?"
|
|
135
|
+
candidate_symbols.each { |s| lines << " • #{s}" }
|
|
136
|
+
lines << ""
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
lines << "Actionable Next Steps:"
|
|
140
|
+
lines << " 1. Search for partial matches using `search_symbols(query: '#{sym_name.to_s[0..4]}')`."
|
|
141
|
+
lines << " 2. Search for code occurrences across the workspace with `grep_search(query: '#{sym_name}')`."
|
|
142
|
+
lines << " 3. Inspect a specific file outline with `get_outline(file_path: '...')`."
|
|
143
|
+
|
|
144
|
+
lines.join("\n")
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Elm-style diagnostic when a shell/sandbox command fails.
|
|
148
|
+
def self.render_command_failed(cmd, exit_code, stderr, suggestions: [])
|
|
149
|
+
lines = [box_header("Command Execution Failed")]
|
|
150
|
+
lines << "Command `#{cmd}` failed with exit code #{exit_code}."
|
|
151
|
+
lines << ""
|
|
152
|
+
if stderr && !stderr.to_s.strip.empty?
|
|
153
|
+
lines << "Output / Error:"
|
|
154
|
+
lines << format_snippet_box(stderr.to_s.strip)
|
|
155
|
+
lines << ""
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
if suggestions && !suggestions.empty?
|
|
159
|
+
lines << "Actionable Next Steps:"
|
|
160
|
+
suggestions.each_with_index { |s, idx| lines << " #{idx + 1}. #{s}" }
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
lines.join("\n")
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Elm-style diagnostic when operating system permissions prevent file/directory access (Errno::EACCES).
|
|
167
|
+
def self.render_permission_denied(path, operation: "read", reason: nil)
|
|
168
|
+
lines = [box_header("Permission Denied")]
|
|
169
|
+
reason_str = reason ? " (#{reason})" : " (Errno::EACCES)"
|
|
170
|
+
op_text = operation.to_s == "read" ? "access" : operation.to_s
|
|
171
|
+
lines << "I cannot #{op_text} `#{path}` due to operating system permission restrictions#{reason_str}."
|
|
172
|
+
lines << ""
|
|
173
|
+
lines << "Actionable Next Steps:"
|
|
174
|
+
lines << " 1. If this is a scratch or temporary directory, allow it via REPL: `!allow #{path}`"
|
|
175
|
+
lines << " 2. Ensure the process has proper OS #{operation} permissions for this directory."
|
|
176
|
+
lines << " 3. Target a specific accessible subfolder using `list_files(path: 'accessible_dir')`."
|
|
177
|
+
|
|
178
|
+
lines.join("\n")
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Elm-style diagnostic when path traversal is blocked outside allowed workspace roots.
|
|
182
|
+
def self.render_traversal_blocked(path, allowed_roots: [], allowed_exts: []) # rubocop:disable Lint/UnusedMethodArgument -- arayüz kwarg'i korunuyor
|
|
183
|
+
lines = [box_header("Path Access Restricted (Sandbox Gate)")]
|
|
184
|
+
lines << "Access to `#{path}` was blocked because it is outside the project workspace."
|
|
185
|
+
lines << ""
|
|
186
|
+
|
|
187
|
+
roots = Array(allowed_roots)
|
|
188
|
+
unless roots.empty?
|
|
189
|
+
lines << "Configured Allowed Roots:"
|
|
190
|
+
roots.each do |root_info|
|
|
191
|
+
if root_info.is_a?(Hash)
|
|
192
|
+
tag = root_info[:primary] ? "[PRIMARY]" : "[ALLOWED]"
|
|
193
|
+
note = root_info[:zero_ast] ? " (Zero-AST)" : ""
|
|
194
|
+
lines << " • #{tag} #{root_info[:path]}#{note}"
|
|
195
|
+
else
|
|
196
|
+
lines << " • #{root_info}"
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
lines << ""
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
lines << "Actionable Next Steps:"
|
|
203
|
+
lines << " 1. Use a path relative to the project root (e.g. 'lib', 'src') or explore with `list_files`."
|
|
204
|
+
lines << " 2. To allow transient I/O and sandbox access to this folder without AST bloat, run:"
|
|
205
|
+
lines << " `!allow #{path}`"
|
|
206
|
+
lines << " 3. To add it as a full workspace root with AST indexing, run:"
|
|
207
|
+
lines << " `!workspace add #{path} [--rw]`"
|
|
208
|
+
|
|
209
|
+
# Canonical-route hint (incident #1): a blocked path inside the
|
|
210
|
+
# central Rubik storage can be read through its rubik:// route —
|
|
211
|
+
# no sandbox widening needed. Best effort; empty outside storage.
|
|
212
|
+
hints = Rubik::Storage::Layout.storage_route_hints(path)
|
|
213
|
+
unless hints.empty?
|
|
214
|
+
lines << " 4. This path is inside the Rubik central storage; read the same content through its canonical route (no sandbox change needed):"
|
|
215
|
+
hints.each { |hint| lines << " `read_file #{hint}`" }
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
lines.join("\n")
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Elm-style diagnostic when malformed or invalid UTF-8 byte sequences are detected.
|
|
222
|
+
def self.render_encoding_warning(source, sample_snippet: nil)
|
|
223
|
+
lines = [box_header("Encoding Warning (Malformed UTF-8)")]
|
|
224
|
+
lines << "Malformed or invalid UTF-8 byte sequences detected in `#{source}`."
|
|
225
|
+
lines << "The input has been automatically sanitized with lossless replacement characters."
|
|
226
|
+
lines << ""
|
|
227
|
+
|
|
228
|
+
if sample_snippet && !sample_snippet.to_s.empty?
|
|
229
|
+
lines << "Sanitized Sample Preview:"
|
|
230
|
+
lines << format_snippet_box(sample_snippet)
|
|
231
|
+
lines << ""
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
lines << "Actionable Next Steps:"
|
|
235
|
+
lines << " 1. Ensure source files and command inputs are encoded in standard UTF-8."
|
|
236
|
+
lines << " 2. For binary or non-text files, use dedicated binary inspection or base64 decoding."
|
|
237
|
+
|
|
238
|
+
lines.join("\n")
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Elm-style diagnostic guidance for LLM context window utilization and token management.
|
|
242
|
+
def self.render_context_status_guidance(stats, token_breakdown: {})
|
|
243
|
+
lines = [box_header("Context Window & Token Guidance")]
|
|
244
|
+
stats_hash = stats.is_a?(Hash) ? stats : {}
|
|
245
|
+
model_name = stats_hash[:model] || stats_hash["model"] || "active model"
|
|
246
|
+
used_tokens = stats_hash[:consumed_tokens] || stats_hash[:estimated_tokens] || stats_hash["consumed_tokens"] || 0
|
|
247
|
+
limit_tokens = stats_hash[:context_limit] || stats_hash[:usable_capacity] || stats_hash["context_limit"] || 32_768
|
|
248
|
+
pct = limit_tokens.positive? ? ((used_tokens.to_f / limit_tokens) * 100).round(1) : 0.0
|
|
249
|
+
|
|
250
|
+
lines << "Current Context Utilization: #{used_tokens} / #{limit_tokens} tokens (#{pct}%) for `#{model_name}`"
|
|
251
|
+
lines << ""
|
|
252
|
+
|
|
253
|
+
breakdown_hash = token_breakdown.is_a?(Hash) ? token_breakdown : {}
|
|
254
|
+
unless breakdown_hash.empty?
|
|
255
|
+
lines << "Token Breakdown:"
|
|
256
|
+
breakdown_hash.each do |k, v|
|
|
257
|
+
lines << " • #{k}: #{v} tokens"
|
|
258
|
+
end
|
|
259
|
+
lines << ""
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
lines << "Actionable Next Steps:"
|
|
263
|
+
if pct >= 80.0
|
|
264
|
+
lines << " 1. Context window is nearly full (>=80%). Run `!context compact` to prune older conversation turns."
|
|
265
|
+
lines << " 2. Switch to a higher-capacity model using `!model default <provider>/<model>`."
|
|
266
|
+
lines << " 3. Drop unnecessary indexed files using `!context drop <path>`."
|
|
267
|
+
elsif pct >= 50.0
|
|
268
|
+
lines << " 1. Context utilization is moderate (>=50%). Use `!context stats` to monitor token growth."
|
|
269
|
+
lines << " 2. Consider running `!context compact` before lengthy multi-step goal loops."
|
|
270
|
+
else
|
|
271
|
+
lines << " 1. Context window is healthy with ample headroom (<50%)."
|
|
272
|
+
lines << " 2. Continue normal interactions or add context with `!context add <path>`."
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
lines.join("\n")
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def self.format_snippet_box(text)
|
|
279
|
+
text.to_s.lines.map { |l| " #{l.chomp}" }.join("\n")
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "did_you_mean"
|
|
4
|
+
begin
|
|
5
|
+
require "diff/lcs"
|
|
6
|
+
rescue LoadError
|
|
7
|
+
# fallback if not yet installed in some minimal environments
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
module Rubik
|
|
11
|
+
module Tools
|
|
12
|
+
# Provides fast, stdlib-powered spelling suggestions and closest-subsequence
|
|
13
|
+
# matching for Elm-style actionable diagnostics (Plan 56 D-1).
|
|
14
|
+
class FuzzyMatcher
|
|
15
|
+
# Returns the top candidates closest to `query` using DidYouMean::SpellChecker
|
|
16
|
+
# and Levenshtein similarity ranking.
|
|
17
|
+
#
|
|
18
|
+
# @param query [String] Search query or misspelled token
|
|
19
|
+
# @param candidates [Array<String>] Dictionary of valid identifiers
|
|
20
|
+
# @param limit [Integer] Maximum number of suggestions to return
|
|
21
|
+
# @return [Array<String>] Sorted list of closest matches
|
|
22
|
+
# Single source of truth for the candidate-score cutoff in best_matches
|
|
23
|
+
# and the length-bound prune inside similarity_ratio - keep them tied.
|
|
24
|
+
MATCH_THRESHOLD = 0.35
|
|
25
|
+
|
|
26
|
+
def self.best_matches(query, candidates, limit: 3)
|
|
27
|
+
return [] if query.to_s.strip.empty? || candidates.nil? || candidates.empty?
|
|
28
|
+
|
|
29
|
+
str_query = query.to_s.downcase.strip
|
|
30
|
+
str_candidates = candidates.map { |c| c.to_s.downcase.strip }.reject(&:empty?).uniq
|
|
31
|
+
|
|
32
|
+
# Direct spell check via stdlib if available
|
|
33
|
+
dym_matches = []
|
|
34
|
+
if defined?(DidYouMean::SpellChecker)
|
|
35
|
+
checker = DidYouMean::SpellChecker.new(dictionary: str_candidates)
|
|
36
|
+
dym_matches = checker.correct(str_query)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Substring inclusions get prioritized
|
|
40
|
+
substring_matches = str_candidates.select { |c| c.include?(str_query) || str_query.include?(c) }
|
|
41
|
+
|
|
42
|
+
# Rank all candidates by Levenshtein similarity ratio
|
|
43
|
+
# PERF: pass min_threshold to prune candidates whose length difference
|
|
44
|
+
# prevents them from ever reaching MATCH_THRESHOLD
|
|
45
|
+
scored_pairs = str_candidates.map do |cand|
|
|
46
|
+
score = similarity_ratio(str_query, cand, min_threshold: MATCH_THRESHOLD)
|
|
47
|
+
[cand, score]
|
|
48
|
+
end
|
|
49
|
+
scored = scored_pairs.select { |_c, score| score >= MATCH_THRESHOLD }
|
|
50
|
+
.sort_by { |_c, score| -score }
|
|
51
|
+
.map(&:first)
|
|
52
|
+
|
|
53
|
+
(dym_matches + substring_matches + scored).uniq.first(limit)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Computes normalized string similarity ratio in [0.0, 1.0].
|
|
57
|
+
#
|
|
58
|
+
# @param str1 [String] First string
|
|
59
|
+
# @param str2 [String] Second string
|
|
60
|
+
# @param min_threshold [Float, nil] Optional minimum acceptable score for early upper-bound pruning
|
|
61
|
+
# @return [Float] Similarity score (1.0 = exact match)
|
|
62
|
+
def self.similarity_ratio(str1, str2, min_threshold: nil)
|
|
63
|
+
s1 = str1.to_s.downcase
|
|
64
|
+
s2 = str2.to_s.downcase
|
|
65
|
+
return 1.0 if s1 == s2
|
|
66
|
+
return 0.0 if s1.empty? || s2.empty?
|
|
67
|
+
|
|
68
|
+
l1 = s1.length
|
|
69
|
+
l2 = s2.length
|
|
70
|
+
max_len = l1 > l2 ? l1.to_f : l2.to_f
|
|
71
|
+
|
|
72
|
+
if min_threshold
|
|
73
|
+
len_diff = (l1 - l2).abs
|
|
74
|
+
# PERF: Upper-bound check on string length difference; if max achievable ratio < min_threshold, prune edit distance calculation
|
|
75
|
+
return 0.0 if (1.0 - (len_diff / max_len)) < min_threshold
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
dist = levenshtein_distance(s1, s2)
|
|
79
|
+
[1.0 - (dist / max_len), 0.0].max
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Computes Levenshtein edit distance between two strings using standard dynamic programming.
|
|
83
|
+
def self.levenshtein_distance(str1, str2)
|
|
84
|
+
return DidYouMean::Levenshtein.distance(str1.to_s, str2.to_s) if defined?(DidYouMean::Levenshtein) && DidYouMean::Levenshtein.respond_to?(:distance)
|
|
85
|
+
|
|
86
|
+
s1 = str1.to_s.chars
|
|
87
|
+
s2 = str2.to_s.chars
|
|
88
|
+
return s2.length if s1.empty?
|
|
89
|
+
return s1.length if s2.empty?
|
|
90
|
+
|
|
91
|
+
d = Array.new(s1.length + 1) { |i| i }
|
|
92
|
+
|
|
93
|
+
s2.each_with_index do |char2, j|
|
|
94
|
+
new_d = [j + 1]
|
|
95
|
+
s1.each_with_index do |char1, i|
|
|
96
|
+
cost = char1 == char2 ? 0 : 1
|
|
97
|
+
new_d << [
|
|
98
|
+
d[i + 1] + 1, # deletion
|
|
99
|
+
new_d[i] + 1, # insertion
|
|
100
|
+
d[i] + cost # substitution
|
|
101
|
+
].min
|
|
102
|
+
end
|
|
103
|
+
d = new_d
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
d.last
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Finds the closest matching line window in `file_content` for `target_content`
|
|
110
|
+
# using Longest Common Subsequence (LCS) analysis (Plan 56 D-1/D-3).
|
|
111
|
+
#
|
|
112
|
+
# @param target_content [String] The snippet being searched
|
|
113
|
+
# @param file_content [String] Full file content
|
|
114
|
+
# @return [Hash, nil] { snippet:, range:, similarity: } or nil if no good match
|
|
115
|
+
def self.find_closest_subsequence(target_content, file_content)
|
|
116
|
+
t_raw = target_content.to_s.gsub("\r\n", "\n")
|
|
117
|
+
f_raw = file_content.to_s.gsub("\r\n", "\n")
|
|
118
|
+
t_lines = t_raw.lines.map(&:rstrip)
|
|
119
|
+
f_lines = f_raw.lines.map(&:rstrip)
|
|
120
|
+
return nil if t_lines.empty? || f_lines.empty?
|
|
121
|
+
|
|
122
|
+
k = t_lines.size
|
|
123
|
+
best_score = 0.0
|
|
124
|
+
best_range = 1..1
|
|
125
|
+
best_i = nil
|
|
126
|
+
best_window_len = nil
|
|
127
|
+
|
|
128
|
+
# PERF: memoize raw file lines once before loop to avoid re-splitting inside loop on score updates
|
|
129
|
+
f_raw_lines = f_raw.lines
|
|
130
|
+
|
|
131
|
+
(0...(f_lines.size)).each do |i|
|
|
132
|
+
window_len = [k, f_lines.size - i].min
|
|
133
|
+
# PERF: Array#[start, len] slice avoids Range object allocation inside hot loop
|
|
134
|
+
window = f_lines[i, window_len]
|
|
135
|
+
|
|
136
|
+
matches, total =
|
|
137
|
+
if defined?(Diff::LCS) && Diff::LCS.respond_to?(:LCS)
|
|
138
|
+
[Diff::LCS.LCS(t_lines, window).size, [t_lines.size, window.size].max]
|
|
139
|
+
else
|
|
140
|
+
# Fallback line matching
|
|
141
|
+
[(0...[t_lines.size, window.size].min).count { |idx| t_lines[idx] == window[idx] },
|
|
142
|
+
[t_lines.size, window.size].max]
|
|
143
|
+
end
|
|
144
|
+
score = total.positive? ? (matches.to_f / total) : 0.0
|
|
145
|
+
|
|
146
|
+
if k == 1 && window_len == 1
|
|
147
|
+
char_score = similarity_ratio(t_lines.first, window.first)
|
|
148
|
+
score = [score, char_score].max
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
next unless score > best_score
|
|
152
|
+
|
|
153
|
+
best_score = score
|
|
154
|
+
best_range = (i + 1)..(i + window_len)
|
|
155
|
+
best_i = i
|
|
156
|
+
best_window_len = window_len
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
return nil if best_score < 0.25 || best_i.nil?
|
|
160
|
+
|
|
161
|
+
# PERF: defer snippet string construction until after best match selection
|
|
162
|
+
best_snippet = f_raw_lines[best_i, best_window_len].join
|
|
163
|
+
|
|
164
|
+
{
|
|
165
|
+
snippet: best_snippet.strip,
|
|
166
|
+
range: best_range,
|
|
167
|
+
similarity: (best_score * 100).round
|
|
168
|
+
}
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
begin
|
|
6
|
+
require "tty/which"
|
|
7
|
+
rescue LoadError
|
|
8
|
+
nil
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
module Rubik
|
|
12
|
+
module Tools
|
|
13
|
+
# ripgrep shellout for GrepSearchTool's native fast path. The module is
|
|
14
|
+
# side-effect free: every spawn is call-scoped (stdin closed immediately,
|
|
15
|
+
# streams closed, process reaped — killed first when the result cap cuts
|
|
16
|
+
# the stream short, else rg would deadlock on a full stdout pipe) — no
|
|
17
|
+
# persistent process, thread, or listener outlives a search, so no
|
|
18
|
+
# dispose inverse is owed.
|
|
19
|
+
#
|
|
20
|
+
# Windows stdin trap (measured, Plan 52 §2.4): rg spawned with a pipe
|
|
21
|
+
# stdin decides from a heuristic to search STDIN instead of "./" and
|
|
22
|
+
# exits with zero results. The path argument is therefore ALWAYS explicit
|
|
23
|
+
# and stdin is closed right after spawn; both are regression-tested.
|
|
24
|
+
#
|
|
25
|
+
# rg runs parallel: measured on a 5.000-file tree, `-j 1` triples the
|
|
26
|
+
# scan time (247 → 864 ms) while pruning globs are free. Consequence:
|
|
27
|
+
# when the result cap cuts the stream, WHICH subset of matches survives
|
|
28
|
+
# follows rg's arrival order — the merged result is still sorted
|
|
29
|
+
# (file, line) and correctly capped, and byte-identical reproducibility
|
|
30
|
+
# remains available via tools.native: off.
|
|
31
|
+
module NativeSearch
|
|
32
|
+
BASE_FLAGS = ["--no-messages", "--no-heading", "-n", "--hidden"].freeze
|
|
33
|
+
|
|
34
|
+
@mutex = Mutex.new
|
|
35
|
+
@rg_path = nil
|
|
36
|
+
|
|
37
|
+
class << self
|
|
38
|
+
# Rubik prunes these subtrees itself; the target repo's .gitignore
|
|
39
|
+
# may not, so the rg-side and Ruby-side file universes stay
|
|
40
|
+
# identical. Lazily built: Rubik::Context::Scanner may not be loaded
|
|
41
|
+
# at require time (tools load before context in some entry points).
|
|
42
|
+
def prune_globs
|
|
43
|
+
@prune_globs ||= Rubik::Context::Scanner::DEFAULT_IGNORES.flat_map { |p| ["-g", "!#{p}"] }.freeze
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Resolved rg executable, or nil. RUBIK_RG_PATH beats PATH lookup
|
|
47
|
+
# (blank treated as unset); memoized (reset_memoization! for tests).
|
|
48
|
+
def rg_path(env: ENV)
|
|
49
|
+
@mutex.synchronize do
|
|
50
|
+
@rg_path ||= begin
|
|
51
|
+
override = env["RUBIK_RG_PATH"]
|
|
52
|
+
override.nil? || override.strip.empty? ? resolve_via_which : override
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def available?(env: ENV)
|
|
58
|
+
!rg_path(env: env).nil?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# ENV beats config; anything other than "off" (auto/on/unknown)
|
|
62
|
+
# attempts the native path first.
|
|
63
|
+
def enabled?(config_value, env: ENV)
|
|
64
|
+
mode = env["RUBIK_NATIVE"] || config_value || "auto"
|
|
65
|
+
mode.to_s.strip.downcase != "off"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Runs rg and parses "path:line:content" rows into GrepSearchTool's
|
|
69
|
+
# match hashes. Returns nil when rg could not be trusted (missing
|
|
70
|
+
# binary, spawn failure, non-0/1 exit such as a regex-usage error) —
|
|
71
|
+
# the caller must fall back to its Ruby path rather than render a
|
|
72
|
+
# possibly-wrong result.
|
|
73
|
+
#
|
|
74
|
+
# The argument count is inherent to a search request: one pattern,
|
|
75
|
+
# three scope knobs, a cap, and two injection hooks (env/runner for
|
|
76
|
+
# test isolation). Grouping them into a bag-hash would hide the
|
|
77
|
+
# contract without reducing real complexity.
|
|
78
|
+
def search(pattern, root_dir:, path: ".", glob: nil, case_sensitive: false, max_results: 50, env: ENV, runner: nil)
|
|
79
|
+
rg = rg_path(env: env)
|
|
80
|
+
return nil if rg.nil? && runner.nil?
|
|
81
|
+
|
|
82
|
+
argv = [rg, *BASE_FLAGS]
|
|
83
|
+
argv << "-i" unless case_sensitive
|
|
84
|
+
argv += ["-g", glob] if glob && !glob.empty?
|
|
85
|
+
argv += prune_globs
|
|
86
|
+
argv += ["-e", pattern.to_s, path]
|
|
87
|
+
|
|
88
|
+
matches = []
|
|
89
|
+
trusted = run_spawn(argv, root_dir: root_dir, runner: runner) do |raw|
|
|
90
|
+
m = parse_line(raw, root_dir: root_dir)
|
|
91
|
+
next false unless m
|
|
92
|
+
|
|
93
|
+
matches << m
|
|
94
|
+
next true if matches.size >= max_results
|
|
95
|
+
|
|
96
|
+
false
|
|
97
|
+
end
|
|
98
|
+
trusted == true ? matches : nil
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def reset_memoization!
|
|
102
|
+
@mutex.synchronize { @rg_path = nil }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# "path:12:content". The SPLIT anchors on the FIRST :digits: boundary
|
|
106
|
+
# and the candidate path is existence-checked against root_dir: a
|
|
107
|
+
# matched content line like `ts = 12:34:56` must not hijack the line
|
|
108
|
+
# number (a greedy split would attribute 34 as the line). If no
|
|
109
|
+
# boundary yields a real file — a path that itself contains :digits:
|
|
110
|
+
# — fall back to the greedy split. Both the path and the content are
|
|
111
|
+
# scrubbed to valid UTF-8: rg emits raw bytes, and a broken-encoding
|
|
112
|
+
# file must not poison JSON payloads downstream.
|
|
113
|
+
def parse_line(raw, root_dir: nil)
|
|
114
|
+
match = raw.match(/\A(.+?):(\d+):(.*)\z/m)
|
|
115
|
+
return nil unless match
|
|
116
|
+
|
|
117
|
+
file = scrub_utf8(match[1])
|
|
118
|
+
if root_dir && !File.exist?(File.join(root_dir, file.tr("\\", "/")))
|
|
119
|
+
match = raw.match(/\A(.+):(\d+):(.*)\z/m)
|
|
120
|
+
return nil unless match
|
|
121
|
+
|
|
122
|
+
file = scrub_utf8(match[1])
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
content = scrub_utf8(match[3])
|
|
126
|
+
content = "#{content[0..500]}... [line truncated, #{content.length} chars total]" if content.length > 500
|
|
127
|
+
|
|
128
|
+
{
|
|
129
|
+
file: file.tr("\\", "/").sub(%r{\A\./}, ""),
|
|
130
|
+
line: match[2].to_i,
|
|
131
|
+
content: content
|
|
132
|
+
}
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
private
|
|
136
|
+
|
|
137
|
+
def resolve_via_which
|
|
138
|
+
defined?(TTY::Which) ? TTY::Which.which("rg") : nil
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Yields each stdout line; the block's truthy return marks an early
|
|
142
|
+
# cap. On cap the rg process is killed BEFORE waiter.value, else a
|
|
143
|
+
# capped rg keeps filling its stdout pipe with no reader and the wait
|
|
144
|
+
# would deadlock. Without a cap, rg exits 0 (matches) / 1 (no
|
|
145
|
+
# matches) — trusted. Exit 2 (usage/regex error) and spawn failures
|
|
146
|
+
# are untrusted → nil → caller falls back.
|
|
147
|
+
def run_spawn(argv, root_dir:, runner:, &)
|
|
148
|
+
if runner
|
|
149
|
+
runner.call(argv, root_dir).each(&)
|
|
150
|
+
return true
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
cap_reached = false
|
|
154
|
+
Open3.popen3(*argv, chdir: root_dir, err: File::NULL) do |stdin, stdout, _stderr, waiter|
|
|
155
|
+
stdin.close
|
|
156
|
+
stdout.each_line do |raw|
|
|
157
|
+
cap_reached = yield raw.chomp
|
|
158
|
+
break if cap_reached
|
|
159
|
+
end
|
|
160
|
+
begin
|
|
161
|
+
Process.kill("KILL", waiter.pid) if cap_reached
|
|
162
|
+
rescue StandardError
|
|
163
|
+
nil
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
status = waiter.value
|
|
167
|
+
cap_reached || (status.exited? && [0, 1].include?(status.exitstatus))
|
|
168
|
+
end
|
|
169
|
+
rescue StandardError
|
|
170
|
+
false
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def scrub_utf8(str)
|
|
174
|
+
str = str.dup.force_encoding(Encoding::UTF_8) if str.encoding == Encoding::ASCII_8BIT
|
|
175
|
+
str.valid_encoding? ? str : str.scrub
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubik
|
|
4
|
+
module Tools
|
|
5
|
+
# A String tool output that carries an explicit error classification.
|
|
6
|
+
#
|
|
7
|
+
# Why a String subclass: the historical builtin-tool contract returns
|
|
8
|
+
# plain Strings, and StepExecutor inferred success/failure by sniffing
|
|
9
|
+
# ERROR_PREFIXES — real failures ("AST graph is not available", "LSP
|
|
10
|
+
# manager is not connected", argument-validation messages) were silently
|
|
11
|
+
# labeled status="ok" in the tool_result envelope. ToolOutput keeps every
|
|
12
|
+
# existing string consumer (tests, Truncator, formatters) working while
|
|
13
|
+
# letting StepExecutor's existing `respond_to?(:error?)` branch classify
|
|
14
|
+
# the result correctly.
|
|
15
|
+
class ToolOutput < String
|
|
16
|
+
def self.ok(text)
|
|
17
|
+
new(text.to_s, is_error: false)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.error(text)
|
|
21
|
+
new(text.to_s, is_error: true)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Display-output contract: the full text is meant for the USER'S
|
|
25
|
+
# terminal, not for the model's context. StepExecutor emits it as a
|
|
26
|
+
# :user_output event and feeds only the short stub back into history.
|
|
27
|
+
def self.display(text, stub:)
|
|
28
|
+
new(text.to_s, is_error: false, display_stub: stub)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def initialize(text = "", is_error: false, display_stub: nil)
|
|
32
|
+
super(text)
|
|
33
|
+
@is_error = is_error ? true : false
|
|
34
|
+
@display_stub = display_stub
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def error?
|
|
38
|
+
@is_error
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def ok?
|
|
42
|
+
!@is_error
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def user_display?
|
|
46
|
+
!@display_stub.nil?
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def display_stub
|
|
50
|
+
@display_stub.to_s
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|