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,281 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../base"
|
|
4
|
+
require_relative "../../coeffects"
|
|
5
|
+
require_relative "../output"
|
|
6
|
+
require_relative "../native_search"
|
|
7
|
+
require_relative "../diagnostic_reporter"
|
|
8
|
+
|
|
9
|
+
module Rubik
|
|
10
|
+
module Tools
|
|
11
|
+
class GrepSearchTool < BaseTool
|
|
12
|
+
MAX_LINE_CHARS = 500
|
|
13
|
+
MAX_OUTPUT_CHARS = 30_000
|
|
14
|
+
|
|
15
|
+
DECLARATION = Declaration.new(
|
|
16
|
+
name: "grep_search", aliases: ["search_code", "grep", "search_files"],
|
|
17
|
+
description: "Search for exact string or regular expression across project files with line numbers and matched content",
|
|
18
|
+
risk: :read, archetypes: [:research, :coder],
|
|
19
|
+
params_schema: {
|
|
20
|
+
type: "object",
|
|
21
|
+
properties: {
|
|
22
|
+
query: { type: "string", description: "Text or regex pattern to search for" },
|
|
23
|
+
path: { type: "string",
|
|
24
|
+
description: "Subdirectory or file path relative to the project root (e.g. 'lib', 'src/main.go'). " \
|
|
25
|
+
"Absolute paths are converted automatically. (optional)" },
|
|
26
|
+
extension: { type: "string", description: "File extension filter, e.g. .rb, .go (optional)" },
|
|
27
|
+
case_sensitive: { type: "boolean", description: "Whether search is case-sensitive (optional, default: false)" },
|
|
28
|
+
max_results: { type: "integer", description: "Maximum number of matched lines to return (optional, default: 50)" }
|
|
29
|
+
},
|
|
30
|
+
required: ["query"]
|
|
31
|
+
}
|
|
32
|
+
)
|
|
33
|
+
def initialize(buffer_manager = nil, context_index = nil, root_dir: Dir.pwd, runtime_context: nil, context: nil, config: nil)
|
|
34
|
+
super(DECLARATION)
|
|
35
|
+
@buffer_manager = buffer_manager
|
|
36
|
+
@context_index = context_index
|
|
37
|
+
@runtime_context = runtime_context || context
|
|
38
|
+
@config = config
|
|
39
|
+
@root_dir = File.expand_path(root_dir)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def context_index
|
|
43
|
+
Rubik::Coeffects.resolve(@runtime_context, Rubik::Coeffects::CONTEXT_INDEX, @context_index)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def buffer_manager
|
|
47
|
+
Rubik::Coeffects.resolve(@runtime_context, Rubik::Coeffects::BUFFER_MANAGER, @buffer_manager)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def execute(args = {})
|
|
51
|
+
# Scrub before strip-driven string ops and result interpolation: an
|
|
52
|
+
# invalid-UTF-8 query (model transcription artifact) must not raise
|
|
53
|
+
# or leak broken bytes into the rendered tool output.
|
|
54
|
+
query = Rubik::Platform.safe_utf8((args["query"] || args[:query]).to_s).strip
|
|
55
|
+
return ToolOutput.error("Query parameter is required.") if query.empty?
|
|
56
|
+
|
|
57
|
+
path_filter = (args["path"] || args[:path]).to_s.strip
|
|
58
|
+
ext_filter = (args["extension"] || args[:extension]).to_s.strip
|
|
59
|
+
|
|
60
|
+
path_filter, outside_error = BaseTool.normalize_path_filter(path_filter, @root_dir)
|
|
61
|
+
if outside_error
|
|
62
|
+
allowed = [{ path: @root_dir, primary: true }]
|
|
63
|
+
idx = context_index
|
|
64
|
+
idx.additional_roots.each { |r| allowed << { path: r, primary: false } } if idx.respond_to?(:additional_roots)
|
|
65
|
+
return ToolOutput.error(DiagnosticReporter.render_traversal_blocked(args["path"] || args[:path], allowed_roots: allowed))
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
case_sensitive = args["case_sensitive"] || args[:case_sensitive] || false
|
|
69
|
+
max_results = (args["max_results"] || args[:max_results] || 50).to_i
|
|
70
|
+
max_results = 50 if max_results <= 0
|
|
71
|
+
|
|
72
|
+
regex = regex_for(query, case_sensitive)
|
|
73
|
+
|
|
74
|
+
matches = try_native_search(query, path_filter, ext_filter, case_sensitive, max_results, regex)
|
|
75
|
+
matches ||= ruby_search(path_filter, ext_filter, regex, max_results)
|
|
76
|
+
|
|
77
|
+
if matches.empty?
|
|
78
|
+
"No matching lines found for '#{query}'.#{" (path filter: '#{path_filter}')" unless path_filter.empty?}"
|
|
79
|
+
else
|
|
80
|
+
lines = ["Found #{matches.size} match(es) for '#{query}':"]
|
|
81
|
+
total_chars = 0
|
|
82
|
+
capped = false
|
|
83
|
+
matches.each do |m|
|
|
84
|
+
content = sanitize_line_content(m[:content].to_s)
|
|
85
|
+
entry = "#{m[:file]}:#{m[:line]}: #{content}"
|
|
86
|
+
if (total_chars + entry.length) > MAX_OUTPUT_CHARS
|
|
87
|
+
capped = true
|
|
88
|
+
break
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
lines << entry
|
|
92
|
+
total_chars += entry.length
|
|
93
|
+
end
|
|
94
|
+
lines << "... [Grep output capped at #{MAX_OUTPUT_CHARS / 1024}KB to protect context window. Narrow search via 'path' or 'extension']" if capped
|
|
95
|
+
lines.join("\n")
|
|
96
|
+
end
|
|
97
|
+
rescue Errno::EACCES => e
|
|
98
|
+
ToolOutput.error(DiagnosticReporter.render_permission_denied(path_filter.empty? ? @root_dir : path_filter, operation: "read", reason: e.message))
|
|
99
|
+
rescue Rubik::Buffer::InvalidPathError
|
|
100
|
+
ToolOutput.error(DiagnosticReporter.render_traversal_blocked(path_filter.empty? ? @root_dir : path_filter))
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
# ripgrep fast path (Plan 52 D2/D7/D8). Gated so divergence risk stays
|
|
106
|
+
# near zero: an unusable rg, a switched-off config, additional roots
|
|
107
|
+
# rg cannot see, or a failed/untrusted run all return nil and the
|
|
108
|
+
# pure-Ruby path renders the result instead. Dirty buffer entries are
|
|
109
|
+
# re-scanned from memory so the model never sees stale disk content.
|
|
110
|
+
def try_native_search(query, path_filter, ext_filter, case_sensitive, max_results, regex)
|
|
111
|
+
return nil unless NativeSearch.enabled?(@config&.fetch("tools.native"))
|
|
112
|
+
return nil unless NativeSearch.available?
|
|
113
|
+
|
|
114
|
+
idx = context_index
|
|
115
|
+
return nil if idx && !idx.additional_roots.empty?
|
|
116
|
+
|
|
117
|
+
glob = nil
|
|
118
|
+
unless ext_filter.empty?
|
|
119
|
+
ext = ext_filter.start_with?(".") ? ext_filter[1..] : ext_filter
|
|
120
|
+
glob = "*.#{ext}"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
target = path_filter.empty? ? "." : path_filter.sub(%r{^\./}, "")
|
|
124
|
+
|
|
125
|
+
native = NativeSearch.search(
|
|
126
|
+
query,
|
|
127
|
+
root_dir: @root_dir,
|
|
128
|
+
path: target,
|
|
129
|
+
glob: glob,
|
|
130
|
+
case_sensitive: case_sensitive,
|
|
131
|
+
max_results: max_results
|
|
132
|
+
)
|
|
133
|
+
return nil if native.nil?
|
|
134
|
+
|
|
135
|
+
buf = buffer_manager
|
|
136
|
+
dirty = buf&.dirty_files ? buf.dirty_files.keys : []
|
|
137
|
+
native = native.reject { |m| dirty.include?(m[:file]) }
|
|
138
|
+
scoped_dirty = dirty.select { |f| pass_path?(f, path_filter) && pass_extension?(f, ext_filter) }
|
|
139
|
+
|
|
140
|
+
merge_native_results(native, scoped_dirty, regex, max_results)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Memory-scans the dirty buffer files and merges them with the disk
|
|
144
|
+
# (rg) results in the exact (file, line) order the pure-Ruby path has
|
|
145
|
+
# always produced.
|
|
146
|
+
def merge_native_results(native, scoped_dirty, regex, max_results)
|
|
147
|
+
return native.sort_by { |m| [m[:file], m[:line]] } if scoped_dirty.empty?
|
|
148
|
+
|
|
149
|
+
memory = collect_matches(scoped_dirty, regex, max_results, from_buffer: true)
|
|
150
|
+
(native + memory).sort_by { |m| [m[:file], m[:line]] }.first(max_results)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Pure-Ruby search over the context index (or a fresh scan). Reads
|
|
154
|
+
# stay on the historical data path — buffer.read for every file when a
|
|
155
|
+
# buffer manager exists (it warms the diff baseline), whole-content
|
|
156
|
+
# strings otherwise — while the per-file line iteration avoids
|
|
157
|
+
# materializing a line array.
|
|
158
|
+
def ruby_search(path_filter, ext_filter, regex, max_results)
|
|
159
|
+
idx = context_index
|
|
160
|
+
files = if idx
|
|
161
|
+
idx.files.empty? ? idx.full_scan : idx.files
|
|
162
|
+
else
|
|
163
|
+
Rubik::Context::Scanner.new(root_dir: @root_dir, native: @config&.fetch("tools.native")).scan
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
all_file_keys = files.keys
|
|
167
|
+
all_file_keys = (all_file_keys + buffer_manager.dirty_files.keys).uniq if buffer_manager&.dirty_files
|
|
168
|
+
all_file_keys = all_file_keys.select { |f| pass_path?(f, path_filter) }
|
|
169
|
+
all_file_keys = all_file_keys.select { |f| pass_extension?(f, ext_filter) }
|
|
170
|
+
|
|
171
|
+
collect_matches(all_file_keys.sort, regex, max_results, from_buffer: !buffer_manager.nil?)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Iterates files in order, collecting matches until max_results.
|
|
175
|
+
# from_buffer: content comes from buffer.read (memory or disk read into
|
|
176
|
+
# a string — historical semantics, incl. the whole-content NUL check);
|
|
177
|
+
# otherwise lines are streamed from disk with File.foreach. A NUL byte
|
|
178
|
+
# in a streamed file revokes that file's earlier matches so the result
|
|
179
|
+
# stays identical to the whole-content binary check.
|
|
180
|
+
def collect_matches(file_keys, regex, max_results, from_buffer:)
|
|
181
|
+
matches = []
|
|
182
|
+
file_keys.each do |f|
|
|
183
|
+
if from_buffer
|
|
184
|
+
content = begin
|
|
185
|
+
buffer_manager.read(f)
|
|
186
|
+
rescue SystemCallError, IOError, Rubik::Buffer::InvalidPathError
|
|
187
|
+
# InvalidPathError: an outbound symlink stays a per-file skip,
|
|
188
|
+
# never an abort of the whole grep (the index lists symlinked
|
|
189
|
+
# files on purpose).
|
|
190
|
+
nil
|
|
191
|
+
end
|
|
192
|
+
next unless content.is_a?(String)
|
|
193
|
+
next if content.include?("\x00")
|
|
194
|
+
|
|
195
|
+
append_string_lines(matches, f, content, regex, max_results)
|
|
196
|
+
else
|
|
197
|
+
collect_disk_matches(matches, f, regex, max_results)
|
|
198
|
+
end
|
|
199
|
+
break if matches.size >= max_results
|
|
200
|
+
end
|
|
201
|
+
matches
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def append_string_lines(matches, file_key, content, regex, max_results)
|
|
205
|
+
line_no = 0
|
|
206
|
+
content.each_line do |line_content|
|
|
207
|
+
line_no += 1
|
|
208
|
+
line_content = searchable_line(line_content)
|
|
209
|
+
if regex.match?(line_content)
|
|
210
|
+
matches << { file: file_key, line: line_no, content: sanitize_line_content(line_content.chomp) }
|
|
211
|
+
break if matches.size >= max_results
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Buffer content and dirty-file edits may carry invalid UTF-8 (tool
|
|
217
|
+
# output captured mid-stream, binary-ish edits). regex.match? raises
|
|
218
|
+
# ArgumentError on such lines — scrub only the broken ones so the
|
|
219
|
+
# common valid path pays only the cheap valid_encoding? check.
|
|
220
|
+
def searchable_line(line)
|
|
221
|
+
line.valid_encoding? ? line : line.scrub("")
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def sanitize_line_content(content)
|
|
225
|
+
return content if content.length <= MAX_LINE_CHARS
|
|
226
|
+
|
|
227
|
+
"#{content[0..MAX_LINE_CHARS]}... [line truncated, #{content.length} chars total]"
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def collect_disk_matches(matches, file_key, regex, max_results)
|
|
231
|
+
full_p = File.expand_path(file_key, @root_dir)
|
|
232
|
+
return unless File.exist?(full_p)
|
|
233
|
+
|
|
234
|
+
line_no = 0
|
|
235
|
+
# broken-encoding guard: a file saved in a non-UTF-8 codepage must not
|
|
236
|
+
# raise out of regex matching; its valid bytes still get searched.
|
|
237
|
+
File.foreach(full_p, encoding: "UTF-8") do |line_content|
|
|
238
|
+
line_no += 1
|
|
239
|
+
if line_content.include?("\x00")
|
|
240
|
+
matches.delete_if { |m| m[:file] == file_key }
|
|
241
|
+
break
|
|
242
|
+
end
|
|
243
|
+
line_content = searchable_line(line_content)
|
|
244
|
+
next unless regex.match?(line_content)
|
|
245
|
+
|
|
246
|
+
matches << { file: file_key, line: line_no, content: sanitize_line_content(line_content.chomp) }
|
|
247
|
+
break if matches.size >= max_results
|
|
248
|
+
end
|
|
249
|
+
rescue SystemCallError, IOError, ArgumentError
|
|
250
|
+
nil
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def pass_path?(file_key, path_filter)
|
|
254
|
+
return true if path_filter.empty?
|
|
255
|
+
|
|
256
|
+
file_key.start_with?(path_filter.sub(%r{^\./}, ""))
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def pass_extension?(file_key, ext_filter)
|
|
260
|
+
return true if ext_filter.empty?
|
|
261
|
+
|
|
262
|
+
ext = ext_filter.start_with?(".") ? ext_filter : ".#{ext_filter}"
|
|
263
|
+
file_key.end_with?(ext)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def regex_for(query, case_sensitive)
|
|
267
|
+
flags = case_sensitive ? nil : Regexp::IGNORECASE
|
|
268
|
+
# Encoding hygiene: a query with invalid UTF-8 (model-transcription
|
|
269
|
+
# artifact) raises RegexpError from Regexp.new and again from the
|
|
270
|
+
# escape fallback. Scrub first so the fallback stays a true fallback,
|
|
271
|
+
# and cover the escape branch with the same rescue.
|
|
272
|
+
query = Rubik::Platform.safe_utf8(query)
|
|
273
|
+
begin
|
|
274
|
+
Regexp.new(query, flags)
|
|
275
|
+
rescue RegexpError
|
|
276
|
+
Regexp.new(Regexp.escape(query), flags)
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../base"
|
|
4
|
+
require_relative "../output"
|
|
5
|
+
require_relative "../../ui/dialog"
|
|
6
|
+
require_relative "../../coeffects"
|
|
7
|
+
|
|
8
|
+
module Rubik
|
|
9
|
+
module Tools
|
|
10
|
+
# Prompts the human operator with structured questions to resolve ambiguity,
|
|
11
|
+
# clarify design decisions, or choose between implementation paths (Plan 92).
|
|
12
|
+
# Subagent plane: when the SUBAGENT_QUESTION_ASKER coeffect is satisfied on
|
|
13
|
+
# the runtime context (supervisor#spawn provides the instance into its child
|
|
14
|
+
# context), the question routes to the maestro instead of a human dialog — the
|
|
15
|
+
# caller blocks until the maestro answers via send_message or the question times
|
|
16
|
+
# out. No REQUIRES constant on purpose: the root agent must keep the tool.
|
|
17
|
+
class AskQuestionTool < BaseTool
|
|
18
|
+
attr_reader :shell, :dialog
|
|
19
|
+
|
|
20
|
+
DECLARATION = Declaration.new(
|
|
21
|
+
name: "ask_question", aliases: ["ask", "question"],
|
|
22
|
+
description: "Ask a structured question with selectable options — either to the human operator, " \
|
|
23
|
+
"or, from a subagent, to the maestro supervisor (blocks until answered).",
|
|
24
|
+
risk: :read, archetypes: [:research, :coder],
|
|
25
|
+
params_schema: {
|
|
26
|
+
type: "object",
|
|
27
|
+
properties: {
|
|
28
|
+
"questions" => {
|
|
29
|
+
type: "array",
|
|
30
|
+
description: "List of questions to ask the operator.",
|
|
31
|
+
items: {
|
|
32
|
+
type: "object",
|
|
33
|
+
properties: {
|
|
34
|
+
"question" => { type: "string", description: "The clear question text to display." },
|
|
35
|
+
"options" => {
|
|
36
|
+
type: "array",
|
|
37
|
+
items: { type: "string" },
|
|
38
|
+
minItems: 2,
|
|
39
|
+
description: "List of selectable answer options (at least 2). Recommended option should be prefixed with '(Recommended)'."
|
|
40
|
+
},
|
|
41
|
+
"is_multi_select" => { type: "boolean", description: "True if multiple options can be chosen simultaneously." }
|
|
42
|
+
},
|
|
43
|
+
required: ["question", "options"]
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
required: ["questions"]
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
def initialize(shell: nil, dialog: nil, runtime_context: nil)
|
|
51
|
+
super(DECLARATION)
|
|
52
|
+
@shell = shell
|
|
53
|
+
@dialog = dialog || Rubik::UI::Dialog.new(shell: shell)
|
|
54
|
+
@runtime_context = runtime_context
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def execute(args = {})
|
|
58
|
+
raw_questions = args["questions"] || args[:questions]
|
|
59
|
+
raw_questions = [args] if raw_questions.nil? && (args["question"] || args[:question])
|
|
60
|
+
return ToolOutput.error("Missing required parameter: questions (must be a non-empty array of question objects).") unless raw_questions.is_a?(Array) && raw_questions.any?
|
|
61
|
+
|
|
62
|
+
asker = init0_asker
|
|
63
|
+
return ask_init0(asker, raw_questions) if asker
|
|
64
|
+
|
|
65
|
+
results = []
|
|
66
|
+
raw_questions.each_with_index do |q_data, idx|
|
|
67
|
+
res = process_single_question(q_data, index: idx + 1)
|
|
68
|
+
return res if res.is_a?(ToolOutput) && res.error?
|
|
69
|
+
|
|
70
|
+
results << res[:text]
|
|
71
|
+
break if res[:cancelled]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
ToolOutput.ok(results.join("\n\n"))
|
|
75
|
+
rescue StandardError => e
|
|
76
|
+
ToolOutput.error("ask_question failed: #{e.message}")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
# The subagent plane: resolves ONLY when this tool instance was bound to
|
|
82
|
+
# a subagent child context (ScopedRegistry rebind); the root-context
|
|
83
|
+
# instance stays nil here and keeps the human-dialog behavior.
|
|
84
|
+
def init0_asker
|
|
85
|
+
return nil unless @runtime_context.respond_to?(:get)
|
|
86
|
+
|
|
87
|
+
Rubik::Coeffects.get(@runtime_context, Rubik::Coeffects::SUBAGENT_QUESTION_ASKER)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# One blocking round-trip for the whole batch: the maestro answers once and
|
|
91
|
+
# the reply covers every numbered question — N sequential blocks would
|
|
92
|
+
# burn the subagent's wall-clock budget for one interaction.
|
|
93
|
+
def ask_init0(asker, raw_questions)
|
|
94
|
+
validated = raw_questions.each_with_index.map do |q_data, idx|
|
|
95
|
+
res = validate_question(q_data, index: idx + 1)
|
|
96
|
+
return res if res.is_a?(ToolOutput)
|
|
97
|
+
|
|
98
|
+
res
|
|
99
|
+
end
|
|
100
|
+
combined = validated.each_with_index.map do |res, idx|
|
|
101
|
+
validated.size == 1 ? res[:question] : "#{idx + 1}. #{res[:question]}"
|
|
102
|
+
end.join("\n")
|
|
103
|
+
|
|
104
|
+
answer = asker.ask_init0(combined, options: validated.flat_map { |res| res[:options] }.uniq)
|
|
105
|
+
if answer == :unavailable
|
|
106
|
+
ToolOutput.ok("[Question routing unavailable (no supervisor channel) — proceed autonomously. Question: #{combined}]")
|
|
107
|
+
elsif answer.nil?
|
|
108
|
+
ToolOutput.ok("[No answer from the maestro within the question timeout — proceed with your best judgment. Question: #{combined}]")
|
|
109
|
+
else
|
|
110
|
+
ToolOutput.ok("Question: #{combined}\nMaestro answered: #{answer}")
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def process_single_question(q_data, index:)
|
|
115
|
+
parsed = validate_question(q_data, index: index)
|
|
116
|
+
return parsed if parsed.is_a?(ToolOutput)
|
|
117
|
+
|
|
118
|
+
if @dialog.interactive?
|
|
119
|
+
ask_interactively(parsed[:question], parsed[:options], parsed[:is_multi])
|
|
120
|
+
else
|
|
121
|
+
{ text: auto_select_fallback(parsed[:question], parsed[:options], parsed[:is_multi]), cancelled: false }
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Shared validation for both planes; returns a question Hash or a
|
|
126
|
+
# ToolOutput error.
|
|
127
|
+
def validate_question(q_data, index:)
|
|
128
|
+
return ToolOutput.error("Invalid question #{index}: item must be an object with 'question' and 'options' properties.") unless q_data.is_a?(Hash)
|
|
129
|
+
|
|
130
|
+
q_text = (q_data["question"] || q_data[:question]).to_s.strip
|
|
131
|
+
raw_options = q_data["options"] || q_data[:options]
|
|
132
|
+
is_multi = q_data["is_multi_select"] || q_data[:is_multi_select] || false
|
|
133
|
+
|
|
134
|
+
return ToolOutput.error("Invalid question #{index}: question text cannot be blank.") if q_text.empty?
|
|
135
|
+
return ToolOutput.error("Invalid question #{index}: must provide an array of selectable options.") unless raw_options.is_a?(Array)
|
|
136
|
+
|
|
137
|
+
options = raw_options.map(&:to_s).map(&:strip).reject(&:empty?).uniq
|
|
138
|
+
return ToolOutput.error("Invalid question #{index}: must provide at least 2 distinct non-empty selectable options.") if options.size < 2
|
|
139
|
+
|
|
140
|
+
{ question: q_text, options: options, is_multi: is_multi }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def ask_interactively(q_text, options, is_multi)
|
|
144
|
+
if is_multi
|
|
145
|
+
ans = @dialog.ask_multi(q_text, options)
|
|
146
|
+
if ans == :cancelled
|
|
147
|
+
{ text: "User cancelled question prompt without selection: \"#{q_text}\"", cancelled: true }
|
|
148
|
+
else
|
|
149
|
+
{ text: "Question: \"#{q_text}\"\nUser response: #{Array(ans).inspect}", cancelled: false }
|
|
150
|
+
end
|
|
151
|
+
else
|
|
152
|
+
ans = @dialog.ask_single(q_text, options)
|
|
153
|
+
if ans == :cancelled
|
|
154
|
+
{ text: "User cancelled question prompt without selection: \"#{q_text}\"", cancelled: true }
|
|
155
|
+
elsif ans.to_s.start_with?("Other (write-in): ")
|
|
156
|
+
clean_ans = ans.to_s.sub(/\AOther \(write-in\):\s*/, "")
|
|
157
|
+
{ text: "Question: \"#{q_text}\"\nUser response (custom write-in): #{clean_ans}", cancelled: false }
|
|
158
|
+
elsif ans.to_s.start_with?("Other (")
|
|
159
|
+
{ text: "Question: \"#{q_text}\"\nUser response (custom write-in): #{ans}", cancelled: false }
|
|
160
|
+
else
|
|
161
|
+
{ text: "Question: \"#{q_text}\"\nUser response: #{ans}", cancelled: false }
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def auto_select_fallback(q_text, options, is_multi)
|
|
167
|
+
recommended = options.find { |opt| opt =~ /\A\s*\(recommended\)/i } || options.first
|
|
168
|
+
selection = is_multi ? [recommended] : recommended
|
|
169
|
+
selection_label = is_multi ? selection.inspect : selection
|
|
170
|
+
|
|
171
|
+
"[Non-interactive session: automatically selected '#{selection_label}' for question: \"#{q_text}\"]"
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|