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,324 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "../platform"
|
|
5
|
+
|
|
6
|
+
module Rubik
|
|
7
|
+
module REPL
|
|
8
|
+
class StepFormatter
|
|
9
|
+
LEVEL_QUIET = 0
|
|
10
|
+
LEVEL_NORMAL = 1
|
|
11
|
+
LEVEL_VERBOSE = 2
|
|
12
|
+
LEVEL_DEBUG = 3
|
|
13
|
+
|
|
14
|
+
# Canonical argument keys considered the "primary" operand of a tool
|
|
15
|
+
# call, shared with UI::Renderer (Plan 51, Decision D2).
|
|
16
|
+
PRIMARY_ARG_KEYS = ["path", "file", "target_file", "filename", "command", "cmd", "query", "pattern", "message", "code", "url", "name", "role",
|
|
17
|
+
"input"].freeze
|
|
18
|
+
|
|
19
|
+
attr_reader :level, :max_steps
|
|
20
|
+
|
|
21
|
+
def self.normalize_level(val)
|
|
22
|
+
case val.to_s.downcase.strip
|
|
23
|
+
when "quiet", "0" then LEVEL_QUIET
|
|
24
|
+
when "verbose", "2" then LEVEL_VERBOSE
|
|
25
|
+
when "debug", "trace", "3" then LEVEL_DEBUG
|
|
26
|
+
else LEVEL_NORMAL
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def initialize(config: nil, verbosity: nil, max_steps: nil)
|
|
31
|
+
@config = config
|
|
32
|
+
@max_steps = max_steps
|
|
33
|
+
raw_verb = verbosity || config&.verbosity || "normal"
|
|
34
|
+
@level = self.class.normalize_level(raw_verb)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def verbosity
|
|
38
|
+
case @level
|
|
39
|
+
when LEVEL_QUIET then "quiet"
|
|
40
|
+
when LEVEL_VERBOSE then "verbose"
|
|
41
|
+
when LEVEL_DEBUG then "debug"
|
|
42
|
+
else "normal"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def format(step_info)
|
|
47
|
+
return nil unless step_info.is_a?(Hash)
|
|
48
|
+
|
|
49
|
+
type = step_info[:type]&.to_sym
|
|
50
|
+
case type
|
|
51
|
+
when :thinking
|
|
52
|
+
format_thinking(step_info)
|
|
53
|
+
when :tool_call
|
|
54
|
+
format_tool_call(step_info)
|
|
55
|
+
when :tool_result
|
|
56
|
+
format_tool_result(step_info)
|
|
57
|
+
when :llm_thought
|
|
58
|
+
format_llm_thought(step_info)
|
|
59
|
+
when :step_warning
|
|
60
|
+
format_step_warning(step_info)
|
|
61
|
+
when :llm_retry
|
|
62
|
+
format_llm_retry(step_info)
|
|
63
|
+
when :auto_apply
|
|
64
|
+
format_auto_apply(step_info)
|
|
65
|
+
when :emergency_compact
|
|
66
|
+
format_emergency_compact(step_info)
|
|
67
|
+
when :goal_wait
|
|
68
|
+
format_goal_wait(step_info)
|
|
69
|
+
when :finish
|
|
70
|
+
format_finish(step_info)
|
|
71
|
+
when :message
|
|
72
|
+
format_message(step_info)
|
|
73
|
+
when :chunk, :chunk_end
|
|
74
|
+
nil
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def format_thinking(step_info)
|
|
79
|
+
return nil if @level == LEVEL_QUIET
|
|
80
|
+
|
|
81
|
+
parts = thinking_parts(step_info)
|
|
82
|
+
target_str = parts[:target].empty? ? "" : " (#{parts[:target]})"
|
|
83
|
+
|
|
84
|
+
case @level
|
|
85
|
+
when LEVEL_NORMAL
|
|
86
|
+
" ⏳ [Step #{parts[:step]}/#{parts[:max]}] Thinking...#{target_str}"
|
|
87
|
+
when LEVEL_VERBOSE
|
|
88
|
+
" ⏳ [Step #{parts[:step]}/#{parts[:max]}] Querying LLM#{target_str}..."
|
|
89
|
+
when LEVEL_DEBUG
|
|
90
|
+
"[DEBUG:thinking] step=#{parts[:step]}/#{parts[:max]} provider=#{step_info[:provider]} model=#{step_info[:model]}"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Semantic decomposition shared with UI::Renderer: step budget parts
|
|
95
|
+
# and the "provider/model" target label of a :thinking event.
|
|
96
|
+
def thinking_parts(step_info)
|
|
97
|
+
step = step_info[:step] || step_info[:current] || 1
|
|
98
|
+
max = step_info[:max_steps] || step_info[:max] || @max_steps || "?"
|
|
99
|
+
target = [step_info[:provider], step_info[:model]].compact.reject { |s| s.to_s.strip.empty? }.join("/")
|
|
100
|
+
{ step: step, max: max, target: target }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def summarize_primary_arg(args)
|
|
104
|
+
return "" unless args.is_a?(Hash) && !args.empty?
|
|
105
|
+
|
|
106
|
+
matched_key = PRIMARY_ARG_KEYS.find { |k| args.key?(k) || args.key?(k.to_sym) }
|
|
107
|
+
|
|
108
|
+
if matched_key
|
|
109
|
+
"#{matched_key}: #{format_arg_value(primary_arg_value(args))}"
|
|
110
|
+
else
|
|
111
|
+
k, v = args.first
|
|
112
|
+
"#{k}: #{format_arg_value(v)}"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Raw value of the primary operand (no key prefix, no quoting) — the
|
|
117
|
+
# semantic twin of #summarize_primary_arg consumed by UI::Renderer.
|
|
118
|
+
def primary_arg_value(args)
|
|
119
|
+
return "" unless args.is_a?(Hash) && !args.empty?
|
|
120
|
+
|
|
121
|
+
matched_key = PRIMARY_ARG_KEYS.find { |k| args.key?(k) || args.key?(k.to_sym) }
|
|
122
|
+
key = matched_key || args.first[0]
|
|
123
|
+
args.key?(key) ? args[key] : args[key.to_sym]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def format_arg_value(val)
|
|
127
|
+
val.is_a?(String) ? "\"#{truncate_str(val, 50)}\"" : val.to_s
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def summarize_result(output, step_info = {})
|
|
131
|
+
return step_info[:summary] if step_info[:summary] && !step_info[:summary].to_s.empty?
|
|
132
|
+
|
|
133
|
+
lines = output.to_s.lines
|
|
134
|
+
if lines.empty?
|
|
135
|
+
"ok"
|
|
136
|
+
elsif lines.size > 1
|
|
137
|
+
"#{lines.size} lines"
|
|
138
|
+
else
|
|
139
|
+
line = lines.first.strip
|
|
140
|
+
line.length > 60 ? "#{line[0..57]}..." : line
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# First printable line of an assistant thought with fenced JSON tool
|
|
145
|
+
# blocks stripped; nil when nothing human-readable remains. Shared
|
|
146
|
+
# with UI::Renderer (Plan 51, Decision D2).
|
|
147
|
+
def thought_first_line(content)
|
|
148
|
+
clean_content = content.to_s.sub(/```(?:json)?\s*\{.*?\}\s*```/m, "").strip
|
|
149
|
+
return nil if clean_content.empty?
|
|
150
|
+
|
|
151
|
+
clean_content.lines.first.to_s.strip
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
private
|
|
155
|
+
|
|
156
|
+
def truncate_str(str, max_len = 50)
|
|
157
|
+
return str if str.length <= max_len
|
|
158
|
+
|
|
159
|
+
"#{str[0...(max_len - 3)]}..."
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def format_tool_call(step_info)
|
|
163
|
+
return nil if @level == LEVEL_QUIET
|
|
164
|
+
|
|
165
|
+
step = step_info[:step] || step_info[:current] || 1
|
|
166
|
+
max = step_info[:max_steps] || step_info[:max] || @max_steps || "?"
|
|
167
|
+
tool = step_info[:tool]
|
|
168
|
+
args = step_info[:args] || {}
|
|
169
|
+
call_id = step_info[:call_id]
|
|
170
|
+
|
|
171
|
+
case @level
|
|
172
|
+
when LEVEL_NORMAL
|
|
173
|
+
summary = summarize_primary_arg(args)
|
|
174
|
+
if summary.empty?
|
|
175
|
+
" ▶ [Step #{step}/#{max}] #{tool}"
|
|
176
|
+
else
|
|
177
|
+
" ▶ [Step #{step}/#{max}] #{tool} (#{summary})"
|
|
178
|
+
end
|
|
179
|
+
when LEVEL_VERBOSE
|
|
180
|
+
" ▶ [Step #{step}/#{max}] Calling #{tool} with args: #{Rubik::Platform.safe_json_generate(args)}"
|
|
181
|
+
when LEVEL_DEBUG
|
|
182
|
+
"[DEBUG:tool_call] step=#{step}/#{max} tool=#{tool} call_id=#{call_id} args=#{Rubik::Platform.safe_json_generate(args)}"
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def format_tool_result(step_info)
|
|
187
|
+
return nil if @level == LEVEL_QUIET
|
|
188
|
+
|
|
189
|
+
tool = step_info[:tool]
|
|
190
|
+
status = (step_info[:status] || :ok).to_sym
|
|
191
|
+
duration_ms = step_info[:duration_ms] || 0.0
|
|
192
|
+
output = step_info[:output].to_s
|
|
193
|
+
|
|
194
|
+
case @level
|
|
195
|
+
when LEVEL_NORMAL
|
|
196
|
+
if status == :ok
|
|
197
|
+
summary_line = summarize_result(output, step_info)
|
|
198
|
+
" ✔ [Obs] #{summary_line} (#{duration_ms}ms)"
|
|
199
|
+
else
|
|
200
|
+
err = step_info[:error]
|
|
201
|
+
err = output.lines.first.to_s.strip if err.nil? || err.empty?
|
|
202
|
+
err = "Command failed" if err.empty?
|
|
203
|
+
if err.start_with?("Security blocked:") || output.start_with?("Security blocked:")
|
|
204
|
+
sec_line = output.lines.first.to_s.strip
|
|
205
|
+
hint = output[/!(?:allow-cmd|allow-dir)\s+[^\n]+/]&.strip
|
|
206
|
+
msg = hint ? "#{sec_line} [#{hint}]" : sec_line
|
|
207
|
+
" ⚠ [Security] #{msg}"
|
|
208
|
+
else
|
|
209
|
+
" ✖ [Error] #{err} (#{duration_ms}ms)"
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
when LEVEL_VERBOSE
|
|
213
|
+
lines_size = step_info[:line_count] || output.lines.size
|
|
214
|
+
byte_size = step_info[:byte_size] || output.bytesize
|
|
215
|
+
if status == :ok
|
|
216
|
+
header = " ✔ [Obs] #{lines_size} lines (#{byte_size} B, #{duration_ms}ms)"
|
|
217
|
+
else
|
|
218
|
+
err = step_info[:error]
|
|
219
|
+
err = output.lines.first.to_s.strip if err.nil? || err.empty?
|
|
220
|
+
err = "Command failed" if err.empty?
|
|
221
|
+
if err.start_with?("Security blocked:") || output.start_with?("Security blocked:")
|
|
222
|
+
sec_line = output.lines.first.to_s.strip
|
|
223
|
+
hint = output[/!(?:allow-cmd|allow-dir)\s+[^\n]+/]&.strip
|
|
224
|
+
msg = hint ? "#{sec_line} [#{hint}]" : sec_line
|
|
225
|
+
return " ⚠ [Security] #{msg}"
|
|
226
|
+
end
|
|
227
|
+
header = " ✖ [Error] #{err} (#{duration_ms}ms)"
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
if output.empty?
|
|
231
|
+
header
|
|
232
|
+
else
|
|
233
|
+
preview = output.lines.first(5).map { |l| " #{l.chomp}" }.join("\n")
|
|
234
|
+
"#{header}\n ↳ Preview:\n#{preview}"
|
|
235
|
+
end
|
|
236
|
+
when LEVEL_DEBUG
|
|
237
|
+
"[DEBUG:tool_result] tool=#{tool} status=#{status} duration=#{duration_ms}ms\n#{output}"
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# First printable line of an assistant thought with fenced JSON tool
|
|
242
|
+
# blocks stripped; nil when nothing human-readable remains. Shared
|
|
243
|
+
# with UI::Renderer (Plan 51, Decision D2).
|
|
244
|
+
def format_llm_thought(step_info)
|
|
245
|
+
return nil if @level == LEVEL_QUIET || @level == LEVEL_NORMAL
|
|
246
|
+
|
|
247
|
+
first_line = thought_first_line(step_info[:content])
|
|
248
|
+
return nil if first_line.nil? || first_line.empty?
|
|
249
|
+
|
|
250
|
+
" 💭 [Thought] #{first_line}"
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def format_step_warning(step_info)
|
|
254
|
+
current = step_info[:current] || step_info[:step]
|
|
255
|
+
max = step_info[:max] || step_info[:max_steps] || @max_steps
|
|
256
|
+
remaining = step_info[:remaining] || (max && current ? (max.to_i - current.to_i) : 0)
|
|
257
|
+
" ⚠️ [Rubik::Agent] Step budget warning: #{current}/#{max} steps used (#{remaining} remaining)."
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def format_llm_retry(step_info)
|
|
261
|
+
return nil if @level == LEVEL_QUIET
|
|
262
|
+
|
|
263
|
+
attempt = step_info[:attempt] || "?"
|
|
264
|
+
max = step_info[:max] || "?"
|
|
265
|
+
reason = step_info[:reason].to_s
|
|
266
|
+
if @level == LEVEL_DEBUG
|
|
267
|
+
"[DEBUG:llm_retry] step=#{step_info[:step] || '?'}/#{step_info[:max_steps] || '?'} retry=#{attempt}/#{max} reason=#{reason}"
|
|
268
|
+
else
|
|
269
|
+
" ↻ [Rubik::LLM] retry #{attempt}/#{max} (#{reason})"
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def format_auto_apply(step_info)
|
|
274
|
+
return nil if @level == LEVEL_QUIET
|
|
275
|
+
|
|
276
|
+
files = step_info[:files] || []
|
|
277
|
+
" ⚡ [Rubik::Buffer] Auto-applied #{files.size} modified file(s) to disk: #{files.join(', ')}"
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def format_emergency_compact(_step_info)
|
|
281
|
+
" ⚠️ [Rubik::Context] Context overflow auto-recovery: applied emergency compaction to history."
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# Goal-mode implicit wait (GoalWait): the run is holding itself open
|
|
285
|
+
# on live subagents instead of accepting a premature completion.
|
|
286
|
+
def format_goal_wait(step_info)
|
|
287
|
+
return nil if @level == LEVEL_QUIET
|
|
288
|
+
|
|
289
|
+
timeout = step_info[:timeout]
|
|
290
|
+
wait_note = timeout.to_i.positive? ? "waiting up to #{timeout}s" : "no blocking wait (agent.goal_wait_timeout=0)"
|
|
291
|
+
" ⏳ [Rubik::Goal] #{step_info[:active]} subagent(s) still active — goal held open (#{wait_note})."
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# The full answer is rendered exactly once elsewhere (live chunks or
|
|
295
|
+
# the post-loop markdown render); this header stays to ONE line so the
|
|
296
|
+
# whole text is never printed twice. The suffix must tell the truth
|
|
297
|
+
# about WHERE the content went: streamed turns show "(+N more lines)"
|
|
298
|
+
# — the text is already above; batch turns say "(full answer below)"
|
|
299
|
+
# because the post-loop render follows immediately. A missing :streamed
|
|
300
|
+
# key defaults to streamed (the dominant interactive path).
|
|
301
|
+
def format_finish(step_info)
|
|
302
|
+
return nil if @level == LEVEL_QUIET
|
|
303
|
+
|
|
304
|
+
message = (step_info[:message] || "Completed").to_s.strip
|
|
305
|
+
return " 🏁 [Rubik::Agent] Completed" if message.empty?
|
|
306
|
+
|
|
307
|
+
streamed = step_info.key?(:streamed) ? step_info[:streamed] == true : true
|
|
308
|
+
return " 🏁 [Rubik::Agent] Completed (full answer below)" unless streamed
|
|
309
|
+
|
|
310
|
+
lines = message.lines
|
|
311
|
+
first = lines.first.to_s.strip
|
|
312
|
+
suffix = lines.count > 1 ? " (+#{lines.count - 1} more lines)" : ""
|
|
313
|
+
" 🏁 [Rubik::Agent] Completed: #{first}#{suffix}"
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def format_message(step_info)
|
|
317
|
+
return nil if @level == LEVEL_QUIET || @level == LEVEL_NORMAL || @level == LEVEL_VERBOSE
|
|
318
|
+
|
|
319
|
+
content = step_info[:content]
|
|
320
|
+
"[DEBUG:message] #{content}"
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../ui/capability"
|
|
4
|
+
|
|
5
|
+
module Rubik
|
|
6
|
+
module REPL
|
|
7
|
+
class TeeIO
|
|
8
|
+
attr_reader :targets
|
|
9
|
+
|
|
10
|
+
def initialize(*targets)
|
|
11
|
+
@targets = targets.compact
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def string_io?
|
|
15
|
+
@targets.any?(StringIO)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Public predicate for test capture detection. Returns true when
|
|
19
|
+
# the TeeIO is being used to capture output into a StringIO
|
|
20
|
+
# buffer (typically in tests). The Shell and agent commands
|
|
21
|
+
# use this to short-circuit interactive yes/no prompts to
|
|
22
|
+
# "no" without inspecting the implementation details.
|
|
23
|
+
def test_capture?
|
|
24
|
+
string_io? || @targets.empty?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def puts(*args)
|
|
28
|
+
dispatch(:puts, args)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def print(*args)
|
|
32
|
+
dispatch(:print, args)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def printf(*args)
|
|
36
|
+
dispatch(:printf, args)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Writes data to all underlying IO targets simultaneously.
|
|
40
|
+
# StringIO targets (session capture, conversation recording) receive
|
|
41
|
+
# ANSI-stripped content so recordings and LLM context stay clean.
|
|
42
|
+
#
|
|
43
|
+
# @param args [Array<String>] one or more strings to write
|
|
44
|
+
# @return [void]
|
|
45
|
+
def write(*args)
|
|
46
|
+
dispatch(:write, args)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def flush
|
|
50
|
+
ordered_targets.each do |target|
|
|
51
|
+
target.flush if target.respond_to?(:flush)
|
|
52
|
+
rescue StandardError => e
|
|
53
|
+
drop_failed_target(target, :flush, e)
|
|
54
|
+
end
|
|
55
|
+
nil
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Returns "" by design when every recorder has been detached by the
|
|
59
|
+
# failure policy — a degraded transcript, never a raise.
|
|
60
|
+
def string
|
|
61
|
+
target = @targets.find { |t| t.respond_to?(:string) }
|
|
62
|
+
target&.string || ""
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def reopen(*)
|
|
66
|
+
@targets.each { |t| t.reopen(*) if t.respond_to?(:reopen) }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
# User-facing streams first, recorders (StringIO captures) last: the
|
|
72
|
+
# recorder must survive a dead user stream and keep its copy of the
|
|
73
|
+
# transcript. Ordering only affects failure isolation — tee semantics
|
|
74
|
+
# deliver identical bytes to every target either way.
|
|
75
|
+
def ordered_targets
|
|
76
|
+
recorders, users = @targets.partition { |t| t.is_a?(StringIO) }
|
|
77
|
+
users + recorders
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def dispatch(method, args)
|
|
81
|
+
ordered_targets.each do |target|
|
|
82
|
+
if target.is_a?(StringIO)
|
|
83
|
+
target.public_send(method, *strip_args(args))
|
|
84
|
+
else
|
|
85
|
+
target.public_send(method, *args)
|
|
86
|
+
end
|
|
87
|
+
rescue StandardError => e
|
|
88
|
+
# One broken target (closed pipe, EPIPE) must never kill the tee:
|
|
89
|
+
# warn once, detach it, keep the remaining targets — the recorder
|
|
90
|
+
# keeps its copy even when the user stream dies.
|
|
91
|
+
drop_failed_target(target, method, e)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def drop_failed_target(target, method, exception)
|
|
96
|
+
@targets.delete(target)
|
|
97
|
+
warn "[rubik] tee target failed during ##{method} (#{exception.class}: #{exception.message}); detached from output"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def strip_args(args)
|
|
101
|
+
args.map { |a| a.is_a?(String) ? Rubik::UI::Capability.strip_ansi(a) : a }
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "monitor"
|
|
4
|
+
|
|
5
|
+
module Rubik
|
|
6
|
+
module REPL
|
|
7
|
+
# Guard protecting Reline input buffer and screen from asynchronous background notices (Plan 105).
|
|
8
|
+
# When Reline is actively reading, notices clear the current line (\r\e[2K), print cleanly,
|
|
9
|
+
# and call Reline.redisplay to restore the prompt and in-progress user input buffer.
|
|
10
|
+
# When idle, notices are output directly. Supports quiet inbox buffering.
|
|
11
|
+
class TerminalGuard
|
|
12
|
+
include MonitorMixin
|
|
13
|
+
|
|
14
|
+
attr_accessor :quiet, :output, :reline
|
|
15
|
+
|
|
16
|
+
def initialize(output: $stdout, reline: Reline, quiet: false)
|
|
17
|
+
super()
|
|
18
|
+
@output = output
|
|
19
|
+
@reline = reline
|
|
20
|
+
@quiet = quiet
|
|
21
|
+
@reading = false
|
|
22
|
+
@collecting = false
|
|
23
|
+
@inbox = []
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def reading?
|
|
27
|
+
synchronize { @reading }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def with_reading
|
|
31
|
+
synchronize { @reading = true }
|
|
32
|
+
yield
|
|
33
|
+
ensure
|
|
34
|
+
synchronize { @reading = false }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def print_safe_notice(text, force: false)
|
|
38
|
+
return false if text.nil? || text.to_s.empty?
|
|
39
|
+
|
|
40
|
+
clean_text = text.to_s.sub(/\A\n+/, "")
|
|
41
|
+
|
|
42
|
+
synchronize do
|
|
43
|
+
if @quiet && !force
|
|
44
|
+
@inbox << clean_text
|
|
45
|
+
return false
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
if @reading
|
|
49
|
+
@output.print "\r\e[2K"
|
|
50
|
+
@output.puts clean_text
|
|
51
|
+
redisplay_cleanly
|
|
52
|
+
else
|
|
53
|
+
@output.puts clean_text
|
|
54
|
+
end
|
|
55
|
+
@output.flush if @output.respond_to?(:flush)
|
|
56
|
+
true
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def quiet?
|
|
61
|
+
synchronize { @quiet }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def inbox
|
|
65
|
+
synchronize { @inbox.dup }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def flush_inbox!
|
|
69
|
+
synchronize do
|
|
70
|
+
items = @inbox.dup
|
|
71
|
+
@inbox.clear
|
|
72
|
+
items
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def flush_notices!
|
|
77
|
+
items = flush_inbox!
|
|
78
|
+
return false if items.empty?
|
|
79
|
+
|
|
80
|
+
synchronize do
|
|
81
|
+
if @reading
|
|
82
|
+
@output.print "\r\e[2K"
|
|
83
|
+
items.each { |item| @output.puts item }
|
|
84
|
+
redisplay_cleanly
|
|
85
|
+
else
|
|
86
|
+
items.each { |item| @output.puts item }
|
|
87
|
+
end
|
|
88
|
+
@output.flush if @output.respond_to?(:flush)
|
|
89
|
+
end
|
|
90
|
+
true
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Plan 109: true when Reline is blocked reading AND the in-progress
|
|
94
|
+
# line is empty — the only state where an injected Enter submits an
|
|
95
|
+
# EMPTY line and cannot truncate user typing. respond_to? guards
|
|
96
|
+
# follow the redisplay_cleanly pattern: a double without
|
|
97
|
+
# line_buffer cannot prove emptiness, so it reads as "not idle"
|
|
98
|
+
# (never wake).
|
|
99
|
+
def idle_and_empty?
|
|
100
|
+
synchronize do
|
|
101
|
+
return false unless @reading
|
|
102
|
+
return false unless @reline.respond_to?(:line_buffer)
|
|
103
|
+
|
|
104
|
+
@reline.line_buffer.to_s.strip.empty?
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Plan 109: wake a blocked empty readline by injecting an Enter.
|
|
109
|
+
# Reline 0.7.0's IO gates (ANSI/Windows/Dumb) all poll and re-check
|
|
110
|
+
# their ungetc buffer (10ms/100ms/100ms), so the injection reaches
|
|
111
|
+
# the blocked reader on every gate. idle_and_empty? is re-evaluated
|
|
112
|
+
# inside this critical section (Monitor reentrancy makes the nested
|
|
113
|
+
# synchronize free); the residual window between check and gate
|
|
114
|
+
# consumption is inherent to Reline's thread model and accepted. A
|
|
115
|
+
# failing ungetc degrades to false — never raises into the kworker.
|
|
116
|
+
# with_reading never holds this monitor during readline, so this
|
|
117
|
+
# cannot deadlock with the reader.
|
|
118
|
+
def safe_wakeup!
|
|
119
|
+
synchronize do
|
|
120
|
+
return false if @collecting
|
|
121
|
+
return false unless idle_and_empty?
|
|
122
|
+
return false unless @reline.respond_to?(:ungetc)
|
|
123
|
+
|
|
124
|
+
begin
|
|
125
|
+
@reline.ungetc("\n".ord)
|
|
126
|
+
rescue StandardError
|
|
127
|
+
return false
|
|
128
|
+
end
|
|
129
|
+
true
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Plan 109: paste collectors (explicit !paste block, paste-burst
|
|
134
|
+
# join) set this so a reactive wake can never inject a blank Enter
|
|
135
|
+
# into the user's pasted text — the wake defers to the next regular
|
|
136
|
+
# prompt instead.
|
|
137
|
+
def with_collecting
|
|
138
|
+
synchronize { @collecting = true }
|
|
139
|
+
yield
|
|
140
|
+
ensure
|
|
141
|
+
synchronize { @collecting = false }
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
private
|
|
145
|
+
|
|
146
|
+
def redisplay_cleanly
|
|
147
|
+
if @reline.respond_to?(:line_editor) && @reline.line_editor
|
|
148
|
+
le = @reline.line_editor
|
|
149
|
+
le.send(:clear_rendered_screen_cache) if le.respond_to?(:clear_rendered_screen_cache, true)
|
|
150
|
+
end
|
|
151
|
+
@reline.redisplay if @reline.respond_to?(:redisplay)
|
|
152
|
+
rescue StandardError
|
|
153
|
+
nil
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|