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,234 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../prompt/context_compactor"
|
|
4
|
+
require_relative "../coeffects"
|
|
5
|
+
|
|
6
|
+
module Rubik
|
|
7
|
+
module REPL
|
|
8
|
+
# Plan 61: cross-turn conversational memory policy, included by Shell.
|
|
9
|
+
# Covers sticky route resolution (D-7), per-turn outcome recording for
|
|
10
|
+
# success/failure/cancel (D-2), and the token-budgeted conversation
|
|
11
|
+
# window (D-12). All state lives on Shell; this module only encodes
|
|
12
|
+
# the policy.
|
|
13
|
+
module TurnRecorder
|
|
14
|
+
MAX_CONVERSATION_ENTRIES = Rubik::Prompt::ContextCompactor::DEFAULT_MAX_CONVERSATION_ENTRIES
|
|
15
|
+
|
|
16
|
+
# Vakanüvis kök-turn olay adı — harness'in TURN_EVENT sabitiyle eş
|
|
17
|
+
# pinlenir (testler iki taraftan kilitler; isim çelişkisi koşuldan
|
|
18
|
+
# önce patlar).
|
|
19
|
+
TURN_EVENT_TARGET = :turn_finished
|
|
20
|
+
|
|
21
|
+
# Plan 61 D-7: sticky routing. Once a conversation has started,
|
|
22
|
+
# free-text turns keep the established provider/model instead of
|
|
23
|
+
# re-running keyword classification every turn (mid-conversation
|
|
24
|
+
# model/style/context-limit churn). `!model` resets the stamp via
|
|
25
|
+
# sync_default_provider; explicit task types (!plan/!goal) never
|
|
26
|
+
# touch it. Disable with `!config set router.sticky false`.
|
|
27
|
+
def resolve_turn_route(text)
|
|
28
|
+
sticky = @config.fetch("router.sticky", true) != false
|
|
29
|
+
return @conversation_route_stamp if sticky && @conversation_route_stamp && !@conversation_history.empty?
|
|
30
|
+
|
|
31
|
+
routed = @router.route(task_type: :auto, prompt: text)
|
|
32
|
+
@conversation_route_stamp = routed if routed && routed[:provider]
|
|
33
|
+
routed
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Plan 61 D-2: every free-text turn — success, failure, cancel —
|
|
37
|
+
# leaves a user/assistant pair in the conversation window so the
|
|
38
|
+
# model keeps the thread and !resume restores it. Failures record a
|
|
39
|
+
# tagged trace plus the attempted-work summary so the next turn knows
|
|
40
|
+
# what was already done.
|
|
41
|
+
def record_turn_outcome!(user_text, assistant_content, tokens: nil, streamed: nil)
|
|
42
|
+
return if user_text.to_s.strip.empty?
|
|
43
|
+
return if assistant_content.nil? || assistant_content.to_s.strip.empty?
|
|
44
|
+
|
|
45
|
+
@conversation_history << { role: "user", content: user_text }
|
|
46
|
+
@conversation_history << { role: "assistant", content: assistant_content.to_s }
|
|
47
|
+
trim_conversation_window!
|
|
48
|
+
@recorder&.record_assistant_response(assistant_content.to_s, tokens: tokens, streamed: streamed)
|
|
49
|
+
rescue StandardError => e
|
|
50
|
+
Rubik::Platform.debug_warn("turn outcome recording failed (#{e.class}: #{e.message}); turn lost from conversation memory")
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def record_failed_turn!(user_text, kind, reason, orchestrator)
|
|
55
|
+
return if @config.fetch("context.log_failed_turns", true) == false
|
|
56
|
+
|
|
57
|
+
label = case kind
|
|
58
|
+
when "cancelled" then "[Turn cancelled by user (^C)]"
|
|
59
|
+
when "incomplete" then "[Turn incomplete: #{reason}]"
|
|
60
|
+
else "[Turn failed: #{reason}]"
|
|
61
|
+
end
|
|
62
|
+
summary = if orchestrator.respond_to?(:activity)
|
|
63
|
+
orchestrator.activity.summary(orchestrator.buffer_manager).to_s.strip
|
|
64
|
+
else
|
|
65
|
+
""
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Plan 85 D-3: kronolojik tool listesi (D-2'deki chronological_summary).
|
|
69
|
+
chronicle_enabled = @config.fetch("context.tool_chronicle_in_history", true)
|
|
70
|
+
max_entries = @config.fetch("context.tool_chronicle_max_entries", 15).to_i
|
|
71
|
+
tool_chronicle = if chronicle_enabled &&
|
|
72
|
+
orchestrator.respond_to?(:activity) &&
|
|
73
|
+
orchestrator.activity.respond_to?(:chronological_summary)
|
|
74
|
+
chron = orchestrator.activity.chronological_summary(max_entries: max_entries).strip
|
|
75
|
+
chron.empty? ? "" : "\nTool calls made:\n#{chron}"
|
|
76
|
+
else
|
|
77
|
+
""
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
parts = []
|
|
81
|
+
parts << label
|
|
82
|
+
parts << "Partial progress before this outcome:\n#{summary}" unless summary.empty?
|
|
83
|
+
parts << tool_chronicle unless tool_chronicle.empty?
|
|
84
|
+
body = "#{parts.first}#{parts[1..].map { |p| "\n#{p}" }.join}"
|
|
85
|
+
|
|
86
|
+
# R9: log_failed_turns kapalıysa başarısız turn hiçbir yere
|
|
87
|
+
# kaydedilmez — kronik de dahil (least surprise).
|
|
88
|
+
emit_turn_finished(kind: kind, prompt: user_text, outcome: body, orchestrator: orchestrator)
|
|
89
|
+
record_turn_outcome!(user_text, body)
|
|
90
|
+
rescue StandardError => e
|
|
91
|
+
Rubik::Platform.debug_warn("failed-turn recording crashed (#{e.class}: #{e.message}); failure context lost")
|
|
92
|
+
nil
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Vakanüvis kök-turn kapısı (root-turn planı 2026-10-04): her REPL
|
|
96
|
+
# turn'ünün sonu deterministik tek satıra indirgenip bus'a düşer.
|
|
97
|
+
# Sessizlik sözleşmesi aynen — :turn_finished WAKE_KINDS dışındadır,
|
|
98
|
+
# tek abone vakanüvis harness'idir; notice yayılmaz. Emisyon asla
|
|
99
|
+
# turn'ü bozmaz: bus yoksa no-op, patlarsa debug_warn.
|
|
100
|
+
def emit_turn_finished(kind:, prompt:, outcome:, orchestrator: nil, tokens: nil)
|
|
101
|
+
return unless @event_bus.respond_to?(:post)
|
|
102
|
+
|
|
103
|
+
# Süs bozulursa olay düşmesin: tally degradasyonu kendi rescue'sunda
|
|
104
|
+
# ("") kalır, post için ayrılan dış rescue bozulmuş bus'u yakalar.
|
|
105
|
+
tools = begin
|
|
106
|
+
orchestrator.respond_to?(:activity) ? orchestrator.activity.tool_tally : ""
|
|
107
|
+
rescue StandardError => e
|
|
108
|
+
Rubik::Platform.debug_warn("tool tally failed (#{e.class}: #{e.message}); chronicling without tools")
|
|
109
|
+
""
|
|
110
|
+
end
|
|
111
|
+
# Loop'ün token sayacı Hash'tir ({input:, output:}) — kronik satırı
|
|
112
|
+
# tek skaler ister; Hash toplamı alınır (Numeric olmayan üye sessizce
|
|
113
|
+
# elenir — üretici asla böyle göndermez, savunma derinliği), diğer
|
|
114
|
+
# şekiller to_i'ye iner. Düzleşme burada (üretici) VE turn_line'da
|
|
115
|
+
# (derinlemesine savunma) yaşar: iki taraf da tek başına dayanıklı.
|
|
116
|
+
tokens_val = tokens.is_a?(Hash) ? tokens.values.filter_map { |v| v.to_i if v.respond_to?(:to_i) }.sum : tokens.to_i
|
|
117
|
+
@event_bus.post(TURN_EVENT_TARGET,
|
|
118
|
+
{ kind: kind.to_s, prompt: prompt.to_s, outcome: outcome.to_s,
|
|
119
|
+
tools: tools.to_s, tokens: tokens_val }, subsystem: :repl)
|
|
120
|
+
rescue StandardError => e
|
|
121
|
+
Rubik::Platform.debug_warn("turn-finished emission failed (#{e.class}: #{e.message}); turn not chronicled")
|
|
122
|
+
nil
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Plan 61 D-12: the long-lived REPL memory is trimmed by a token
|
|
126
|
+
# budget (plus an absolute entry cap) so one huge turn cannot evict
|
|
127
|
+
# the whole thread, and eviction order stays oldest-first.
|
|
128
|
+
def trim_conversation_window!
|
|
129
|
+
ceiling = begin
|
|
130
|
+
spec = current_model_spec
|
|
131
|
+
@llm_registry.effective_input_limit_for(spec["provider"], spec["name"])
|
|
132
|
+
rescue StandardError => e
|
|
133
|
+
Rubik::Platform.debug_warn("input ceiling lookup failed (#{e.class}: #{e.message}); using nil budget")
|
|
134
|
+
nil
|
|
135
|
+
end
|
|
136
|
+
@conversation_history = Rubik::Prompt::ContextCompactor.trim_history_to_budget(
|
|
137
|
+
@conversation_history, ceiling: ceiling, max_entries: MAX_CONVERSATION_ENTRIES
|
|
138
|
+
)
|
|
139
|
+
rescue StandardError => e
|
|
140
|
+
Rubik::Log.warn(:repl, "conversation trim failed (#{e.class}: #{e.message}); window may exceed token budget")
|
|
141
|
+
nil
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Plan 85 D-7 (kök neden): turn sonunda turn içi @history'deki tool ara
|
|
145
|
+
# adımlarını (role: "tool" mesajları) conversation_history'ye kopyalar;
|
|
146
|
+
# hata veya başarı fark etmez. Böylece continue'da model turn sırasındaki
|
|
147
|
+
# tüm bağlamı görür ve "sıfırdan keşfe" başlamaz.
|
|
148
|
+
# loop.rb'ye dokunmaz: caller res[:loop_instance].history üzerinden turn
|
|
149
|
+
# içi @history'yi okur (turn_result zaten loop_instance + history accessor
|
|
150
|
+
# döndürür).
|
|
151
|
+
def merge_turn_tool_history!(turn_history, max_entries: 20)
|
|
152
|
+
return false if turn_history.nil? || turn_history.empty?
|
|
153
|
+
return false unless @config.fetch("context.merge_turn_tool_history", true)
|
|
154
|
+
|
|
155
|
+
# Yalnızca tool sonucu taşıyan mesajları filtrele; en SON max_entries
|
|
156
|
+
# adımı al — bir sonraki turn'ün ihtiyacı olan, turn'in güncel arazi
|
|
157
|
+
# durumudur (hangi edit'ler tuttu, son hatalar), turn'in başı değil.
|
|
158
|
+
tool_msgs = turn_history.select do |m|
|
|
159
|
+
role = m[:role] || m["role"]
|
|
160
|
+
role == "tool" || m[:tool_calls] || m["tool_calls"]
|
|
161
|
+
end.last(max_entries)
|
|
162
|
+
return false if tool_msgs.empty?
|
|
163
|
+
|
|
164
|
+
@conversation_history.concat(tool_msgs)
|
|
165
|
+
|
|
166
|
+
# conversation_history artık tam tool sonuçları taşıdığından trim zorunludur.
|
|
167
|
+
trim_conversation_window!
|
|
168
|
+
true
|
|
169
|
+
rescue StandardError => e
|
|
170
|
+
Rubik::Platform.debug_warn("tool-history merge failed (#{e.class}: #{e.message}); next turn starts without tool context")
|
|
171
|
+
nil
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Plan 61 D-13: the model spec the CURRENT conversation is bound to
|
|
175
|
+
# (sticky stamp when present), falling back to the default role.
|
|
176
|
+
def current_model_spec
|
|
177
|
+
stamp = @conversation_route_stamp
|
|
178
|
+
return stamp[:spec] if stamp.is_a?(Hash) && stamp[:spec].is_a?(Hash) && stamp[:spec]["provider"]
|
|
179
|
+
|
|
180
|
+
@config.model_spec(:default)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Pruner apply seam (context-pruner 2026-10-09): turn thread'inde,
|
|
184
|
+
# orchestrator.run pencereyi tohumlamadan HEMEN ÖNCE çağrılır — hem
|
|
185
|
+
# execute_free_text (input_execution.rb) hem !goal (agent_execution.rb
|
|
186
|
+
# ctx.apply_staged_prune_plan! üzerinden). Kopyala-doğrula-takas:
|
|
187
|
+
# take_applicable_plan! yeni dizi döner; herhangi bir ihlalde nil ve
|
|
188
|
+
# pencere olduğu gibi kalır. Replay sırasında atlanır (yeniden
|
|
189
|
+
# oynatılan oturumu budamak kaydı çarpıtır); pruner kapalıysa (coeffect
|
|
190
|
+
# yok) no-op'tur.
|
|
191
|
+
def apply_staged_prune_plan!
|
|
192
|
+
return if @replaying
|
|
193
|
+
|
|
194
|
+
harness = Rubik::Coeffects.get(@runtime_manager&.context, Rubik::Coeffects::PRUNER)
|
|
195
|
+
return unless harness
|
|
196
|
+
|
|
197
|
+
new_window, receipt = harness.take_applicable_plan!(@conversation_history)
|
|
198
|
+
return unless new_window
|
|
199
|
+
|
|
200
|
+
@conversation_history = new_window
|
|
201
|
+
line = "[Pruner] Applied staged plan: dropped #{receipt[:dropped]} unit(s), " \
|
|
202
|
+
"summarized #{receipt[:summarized]} (#{receipt[:skipped_stale]} stale op(s) skipped) — " \
|
|
203
|
+
"window ~#{receipt[:before_tokens]} -> ~#{receipt[:after_tokens]} tokens."
|
|
204
|
+
if @terminal_guard
|
|
205
|
+
@terminal_guard.print_safe_notice(line)
|
|
206
|
+
else
|
|
207
|
+
@output.puts(line)
|
|
208
|
+
end
|
|
209
|
+
rescue StandardError => e
|
|
210
|
+
Rubik::Log.warn(:repl, "staged prune apply failed (#{e.class}: #{e.message}); window untouched")
|
|
211
|
+
nil
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Session continuity (Plan 34/61): hydrate a recorded session into
|
|
215
|
+
# the live conversation window.
|
|
216
|
+
def resume_session(identifier = :latest)
|
|
217
|
+
root_d = @buffer_manager&.root_dir || Dir.pwd
|
|
218
|
+
home_d = @config&.home_dir || ENV.fetch("HOME", nil)
|
|
219
|
+
|
|
220
|
+
target_file = if identifier == :latest || identifier.to_s.downcase == "latest"
|
|
221
|
+
Rubik::Session::Manager.find_latest_session(root_d, home_d, exclude_file: @recorder&.session_file, config: @config)
|
|
222
|
+
else
|
|
223
|
+
Rubik::Session::Manager.find_session(identifier, root_d, home_d, config: @config)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
return { ok: false, error: "No past session found matching '#{identifier}'" } unless target_file
|
|
227
|
+
|
|
228
|
+
res = Rubik::Session::Manager.restore_session(self, target_file)
|
|
229
|
+
@output.puts "[Rubik::Session] Resumed session: #{File.basename(target_file)} (#{res[:turns_hydrated]} dialogue turns hydrated, 0 API tokens used)" if res[:ok]
|
|
230
|
+
res
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
data/lib/rubik/repl.rb
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "repl/command_parser"
|
|
4
|
+
require_relative "repl/hooks"
|
|
5
|
+
require_relative "repl/step_formatter"
|
|
6
|
+
require_relative "repl/commands"
|
|
7
|
+
require_relative "repl/terminal_guard"
|
|
8
|
+
require_relative "repl/pruner"
|
|
9
|
+
require_relative "repl/shell"
|
|
10
|
+
|
|
11
|
+
module Rubik
|
|
12
|
+
module REPL
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../ast"
|
|
4
|
+
require_relative "../coeffects"
|
|
5
|
+
require_relative "service_registration"
|
|
6
|
+
|
|
7
|
+
require_relative "../platform"
|
|
8
|
+
|
|
9
|
+
module Rubik
|
|
10
|
+
module Runtime
|
|
11
|
+
module ASTService
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def register(context_or_manager, ast_graph, cache_store: nil)
|
|
15
|
+
ServiceRegistration.call(context_or_manager) do |ctx|
|
|
16
|
+
next ast_graph unless ctx
|
|
17
|
+
|
|
18
|
+
ctx.provide(Rubik::Coeffects::AST_GRAPH, ast_graph)
|
|
19
|
+
|
|
20
|
+
ctx.on_dispose do
|
|
21
|
+
begin
|
|
22
|
+
ast_graph.save_to_cache(cache_store) if cache_store && ast_graph.respond_to?(:save_to_cache)
|
|
23
|
+
rescue StandardError => e
|
|
24
|
+
# Report but keep draining: a failed cache save must not abort
|
|
25
|
+
# the LIFO unwind, and silence here would leak a stale symbol
|
|
26
|
+
# cache with no trace.
|
|
27
|
+
warn "[ASTService] cache save failed: #{e.class}: #{e.message}"
|
|
28
|
+
end
|
|
29
|
+
ast_graph.clear if ast_graph.respond_to?(:clear)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
ast_graph
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Drop graph entries for files inside the project root that the context
|
|
37
|
+
# scanner no longer reports — e.g. subtrees that became covered by an
|
|
38
|
+
# ignore rule (the scanner's whole-subtree pruning is stronger than the
|
|
39
|
+
# legacy one-level fnmatch filter). Without this, symbols loaded from a
|
|
40
|
+
# pre-upgrade cache would linger in the graph and be re-saved forever.
|
|
41
|
+
# Guarded on a non-empty scan so a failed scan can never wipe the graph.
|
|
42
|
+
# Files outside the primary root (additional roots, external dirs) are
|
|
43
|
+
# untouched: the context index may not cover them.
|
|
44
|
+
# Returns the number of purged entries.
|
|
45
|
+
def purge_stale_entries!(ast_graph, context_index, root_dir)
|
|
46
|
+
files = context_index.files
|
|
47
|
+
return 0 if files.empty?
|
|
48
|
+
|
|
49
|
+
is_win = Rubik::Platform.windows?
|
|
50
|
+
norm_root = Rubik::Platform.normalize_path_raw(root_dir).chomp("/")
|
|
51
|
+
root_prefix = "#{norm_root}/"
|
|
52
|
+
root_prefix_cmp = is_win ? root_prefix.downcase : root_prefix
|
|
53
|
+
|
|
54
|
+
known_keys = if is_win
|
|
55
|
+
files.keys.to_h { |k| [k.downcase, true] }
|
|
56
|
+
else
|
|
57
|
+
files.keys.to_h { |k| [k, true] }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
supported = Rubik::AST::Extractor.supported_extensions
|
|
61
|
+
stale = []
|
|
62
|
+
|
|
63
|
+
ast_graph.file_symbols.each_key do |path|
|
|
64
|
+
norm = Rubik::Platform.normalize_path_raw(path)
|
|
65
|
+
norm_cmp = is_win ? norm.downcase : norm
|
|
66
|
+
next unless norm_cmp.start_with?(root_prefix_cmp)
|
|
67
|
+
|
|
68
|
+
rel = norm[root_prefix.length..]
|
|
69
|
+
rel_key = is_win ? rel.downcase : rel
|
|
70
|
+
stale << path if supported.include?(File.extname(rel).downcase) && !known_keys.key?(rel_key)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
stale.each { |path| ast_graph.remove_file(path) }
|
|
74
|
+
stale.size
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "monitor"
|
|
4
|
+
require_relative "../coeffects"
|
|
5
|
+
|
|
6
|
+
module Rubik
|
|
7
|
+
module Runtime
|
|
8
|
+
class CoeffectMissingError < StandardError; end
|
|
9
|
+
|
|
10
|
+
# Raised when a disposed context is asked to create new scope. Unlike a
|
|
11
|
+
# late inverse (which can run now), there is no meaningful fallback for
|
|
12
|
+
# extend, so it fails fast (AGENTS.md Fail Fast template).
|
|
13
|
+
class ContextDisposedError < StandardError; end
|
|
14
|
+
|
|
15
|
+
class Context
|
|
16
|
+
attr_reader :parent
|
|
17
|
+
|
|
18
|
+
# Legacy alias -> canonical key, derived from the Rubik::Coeffects SSOT
|
|
19
|
+
# (AGENTS.md §1.B). Providers publish canonical keys only; consumers may
|
|
20
|
+
# still ask with documented legacy names (e.g. plugin DSL inject: [:tools]).
|
|
21
|
+
CANONICAL_OF = Rubik::Coeffects::ALIAS_TO_CANONICAL
|
|
22
|
+
# Canonical key -> legacy alias spellings (forward half of the SSOT).
|
|
23
|
+
LEGACY_ALIASES = Rubik::Coeffects::LEGACY_ALIASES
|
|
24
|
+
|
|
25
|
+
def initialize(parent: nil)
|
|
26
|
+
@parent = parent
|
|
27
|
+
@monitor = Monitor.new
|
|
28
|
+
@services = {}
|
|
29
|
+
# Delete-inverse handles keyed by service / child, so explicit
|
|
30
|
+
# removals (unprovide, child prune) can also retract the inverse
|
|
31
|
+
# from the Omega accumulator instead of leaving dead no-ops behind.
|
|
32
|
+
@service_inverses = {}
|
|
33
|
+
@child_inverses = {}
|
|
34
|
+
@disposables = []
|
|
35
|
+
@children = []
|
|
36
|
+
@disposed = false
|
|
37
|
+
@last_dispose_errors = []
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def register_disposable(cleanup_fn = nil, &block)
|
|
41
|
+
inverse = cleanup_fn || block
|
|
42
|
+
return unless inverse
|
|
43
|
+
|
|
44
|
+
@monitor.synchronize do
|
|
45
|
+
if @disposed
|
|
46
|
+
# Ω is sealed: the context is already torn down, so there is no
|
|
47
|
+
# later drain that would run this inverse. Execute it now instead
|
|
48
|
+
# of silently storing it in a dead accumulator (leaked cleanup).
|
|
49
|
+
run_inverse(inverse)
|
|
50
|
+
else
|
|
51
|
+
@disposables << inverse
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
inverse
|
|
55
|
+
end
|
|
56
|
+
alias on_dispose register_disposable
|
|
57
|
+
|
|
58
|
+
# Number of pending inverse operations in the Omega accumulator.
|
|
59
|
+
# Exposed for runtime visualization (!agents panel, !runtime).
|
|
60
|
+
def disposable_count
|
|
61
|
+
@monitor.synchronize { @disposables.size }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def services
|
|
65
|
+
@monitor.synchronize { @services.dup.freeze }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def service_names
|
|
69
|
+
@monitor.synchronize { @services.keys }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def last_dispose_errors
|
|
73
|
+
@monitor.synchronize { @last_dispose_errors.dup }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def provide(name, service)
|
|
77
|
+
key = name.to_s.downcase
|
|
78
|
+
@monitor.synchronize do
|
|
79
|
+
already_provided = @services.key?(key)
|
|
80
|
+
@services[key] = service
|
|
81
|
+
return if already_provided
|
|
82
|
+
|
|
83
|
+
# Exactly one delete-inverse per key for the context's lifetime:
|
|
84
|
+
# re-providing a service swaps the value but never stacks a second
|
|
85
|
+
# inverse into the Omega accumulator (AGENTS.md zero-restart rule).
|
|
86
|
+
# The handle is kept so unprovide can retract it symmetrically.
|
|
87
|
+
@service_inverses[key] = register_disposable { @services.delete(key) }
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Removes a service AND its delete-inverse from the accumulator, the
|
|
92
|
+
# symmetric counterpart of provide. Callers own the reactive surface:
|
|
93
|
+
# unlike Runtime::Manager#provide, removal triggers no reconciliation.
|
|
94
|
+
# Re-providing the key afterwards registers a fresh inverse, so
|
|
95
|
+
# provide/unprovide cycles return Ω to its baseline (zero-restart).
|
|
96
|
+
def unprovide(name)
|
|
97
|
+
key = name.to_s.downcase
|
|
98
|
+
@monitor.synchronize do
|
|
99
|
+
return unless @services.key?(key)
|
|
100
|
+
|
|
101
|
+
@services.delete(key)
|
|
102
|
+
inverse = @service_inverses.delete(key)
|
|
103
|
+
@disposables.delete(inverse) if inverse
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def require(name)
|
|
108
|
+
value = resolve_coeffect(name.to_s.downcase)
|
|
109
|
+
return value unless value.nil?
|
|
110
|
+
|
|
111
|
+
raise CoeffectMissingError, "Required coeffect '#{name}' is not satisfied in the context."
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def get(name, default = nil)
|
|
115
|
+
val = resolve_coeffect(name.to_s.downcase)
|
|
116
|
+
val.nil? ? default : val
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Walk the context chain for a literal key. Returns nil when the key is
|
|
120
|
+
# absent (services are always live objects, never nil, in this runtime).
|
|
121
|
+
# Per-hop locking: each context holds only its own monitor during the
|
|
122
|
+
# local lookup and releases it before recursing to the parent, so
|
|
123
|
+
# chained reads never nest child→parent locks (disposal nests only
|
|
124
|
+
# parent→child; the reverse order would risk deadlock).
|
|
125
|
+
def resolve(key)
|
|
126
|
+
found, local = @monitor.synchronize do
|
|
127
|
+
[@services.key?(key), @services[key]]
|
|
128
|
+
end
|
|
129
|
+
return local if found && !local.nil?
|
|
130
|
+
|
|
131
|
+
@parent&.resolve(key)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def satisfied?(dependencies)
|
|
135
|
+
Array(dependencies).all? { |dep| has?(dep) }
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def has?(name)
|
|
139
|
+
!resolve_coeffect(name.to_s.downcase).nil?
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Single-pass coeffect resolution: per hop, under one monitor hold, all
|
|
143
|
+
# three spellings of the requested key are tried atomically — the
|
|
144
|
+
# literal key (a third party may have provided the alias itself), its
|
|
145
|
+
# canonical form (the request used a legacy name), then its legacy
|
|
146
|
+
# aliases (the request used the canonical name but a provider
|
|
147
|
+
# registered the short form). Scoping wins over spelling: the nearest
|
|
148
|
+
# provider wins regardless of which spelling it registered under.
|
|
149
|
+
# Per-hop locking and the parent→child lock order: see `resolve`.
|
|
150
|
+
# Protected (not private): the parent-chain recursion needs an
|
|
151
|
+
# explicit receiver — the same boundary as prune_child, so external
|
|
152
|
+
# callers get NoMethodError (closes the accidentally-public
|
|
153
|
+
# resolve_with_alias surface).
|
|
154
|
+
def resolve_coeffect(key)
|
|
155
|
+
local = @monitor.synchronize do
|
|
156
|
+
if @services.key?(key)
|
|
157
|
+
@services[key]
|
|
158
|
+
elsif (canonical = CANONICAL_OF[key]) && @services.key?(canonical)
|
|
159
|
+
@services[canonical]
|
|
160
|
+
else
|
|
161
|
+
aliases = LEGACY_ALIASES[key]
|
|
162
|
+
found = nil
|
|
163
|
+
aliases&.each do |alias_key|
|
|
164
|
+
# key? probing, never truthiness (test_coeffects_falsy).
|
|
165
|
+
next unless @services.key?(alias_key)
|
|
166
|
+
|
|
167
|
+
found = @services[alias_key]
|
|
168
|
+
break
|
|
169
|
+
end
|
|
170
|
+
found
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
return local unless local.nil?
|
|
174
|
+
|
|
175
|
+
@parent&.resolve_coeffect(key)
|
|
176
|
+
end
|
|
177
|
+
protected :resolve_coeffect
|
|
178
|
+
|
|
179
|
+
def extend
|
|
180
|
+
child = nil
|
|
181
|
+
@monitor.synchronize do
|
|
182
|
+
if @disposed
|
|
183
|
+
raise ContextDisposedError,
|
|
184
|
+
"Cannot extend a disposed context. To fix, check `disposed?` before extending or create a fresh context."
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
child = Context.new(parent: self)
|
|
188
|
+
@children << child
|
|
189
|
+
# Keep the cascade-inverse handle so a child disposed before its
|
|
190
|
+
# parent (epoch close, per-turn loop context, MCP reconnect) can
|
|
191
|
+
# prune both itself and the dead inverse out of this context.
|
|
192
|
+
@child_inverses[child] = register_disposable { child.dispose unless child.disposed? }
|
|
193
|
+
end
|
|
194
|
+
child
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def disposed?
|
|
198
|
+
@disposed
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Internal counterpart of extend: a disposed child removes itself and
|
|
202
|
+
# its dead cascade inverse from this context. Extension contexts open
|
|
203
|
+
# and close constantly (per-turn loop contexts, MCP reconnects, tool
|
|
204
|
+
# epochs, fiber reactivations) — without the prune every cycle would
|
|
205
|
+
# leave a dead child and a no-op inverse in the accumulator forever
|
|
206
|
+
# (AGENTS.md zero-restart rule). Protected: only Context itself may
|
|
207
|
+
# strip a child's bookkeeping, so external code cannot unhook a live
|
|
208
|
+
# child (leak footgun). Must be called AFTER the child released its
|
|
209
|
+
# own monitor, so child disposal never nests child→parent locks
|
|
210
|
+
# against the parent→child order used by dispose. No-op on a disposed
|
|
211
|
+
# parent (its own drain owns the bookkeeping by then).
|
|
212
|
+
def prune_child(child)
|
|
213
|
+
@monitor.synchronize do
|
|
214
|
+
return if @disposed
|
|
215
|
+
|
|
216
|
+
@children.delete(child)
|
|
217
|
+
inverse = @child_inverses.delete(child)
|
|
218
|
+
@disposables.delete(inverse) if inverse
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
protected :prune_child
|
|
222
|
+
|
|
223
|
+
def dispose
|
|
224
|
+
parent = nil
|
|
225
|
+
deferred_interrupt = nil
|
|
226
|
+
@monitor.synchronize do
|
|
227
|
+
return if @disposed
|
|
228
|
+
|
|
229
|
+
@disposed = true
|
|
230
|
+
parent = @parent
|
|
231
|
+
# Reset BEFORE the child cascade so signals adopted from children
|
|
232
|
+
# (record_deferred_signal) survive into the final
|
|
233
|
+
# last_dispose_errors instead of being wiped by this reset.
|
|
234
|
+
@last_dispose_errors = []
|
|
235
|
+
dmesg = resolve_dmesg
|
|
236
|
+
# Counted before the cascade: disposing children prune their
|
|
237
|
+
# inverses from our accumulator (pre-cascade size is what the
|
|
238
|
+
# "beginning" log reports).
|
|
239
|
+
dmesg&.log(:context, :info, "Beginning context dispose with #{@disposables.size} inverse(s)")
|
|
240
|
+
|
|
241
|
+
deferred_interrupt = dispose_children
|
|
242
|
+
drained, drain_signal = drain_inverses(dmesg: dmesg)
|
|
243
|
+
deferred_interrupt ||= drain_signal
|
|
244
|
+
|
|
245
|
+
dmesg&.log(:context, :info, "Context dispose complete; #{drained} inverse(s) executed")
|
|
246
|
+
end
|
|
247
|
+
# Release the child's monitor before reaching the parent (lock
|
|
248
|
+
# order), then prune the parent's record of this child.
|
|
249
|
+
parent&.prune_child(self)
|
|
250
|
+
# Re-surface the FIRST deferred signal only after the unwind is
|
|
251
|
+
# fully complete — Ctrl+C during teardown still reaches the
|
|
252
|
+
# caller (repo convention: Interrupt is never swallowed), never
|
|
253
|
+
# at the cost of a half-drained accumulator.
|
|
254
|
+
raise deferred_interrupt if deferred_interrupt
|
|
255
|
+
end
|
|
256
|
+
alias dispose! dispose
|
|
257
|
+
|
|
258
|
+
private
|
|
259
|
+
|
|
260
|
+
# Phase 1: dispose child contexts first (parent→child lock order;
|
|
261
|
+
# children never hold their lock while reaching back to the parent).
|
|
262
|
+
# Each child's prune is a no-op here (@disposed sealed the bookkeeping),
|
|
263
|
+
# so the descending iteration never observes mutation. A child's
|
|
264
|
+
# deferred Interrupt/SystemExit is adopted as (possibly) the FIRST
|
|
265
|
+
# signal of this cascade; returns the first deferred child signal.
|
|
266
|
+
def dispose_children
|
|
267
|
+
deferred = nil
|
|
268
|
+
@children.reverse_each do |child|
|
|
269
|
+
child.dispose unless child.disposed?
|
|
270
|
+
rescue Interrupt, SystemExit => e
|
|
271
|
+
record_deferred_signal(e)
|
|
272
|
+
deferred ||= e
|
|
273
|
+
end
|
|
274
|
+
@children.clear
|
|
275
|
+
deferred
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# Phase 2: execute accumulated inverses in strict reverse order
|
|
279
|
+
# (LIFO — Omega accumulator). A failing inverse never aborts the unwind
|
|
280
|
+
# (AGENTS.md §5): record it, report, keep draining. Interrupt/SystemExit
|
|
281
|
+
# get the same complete-drain policy: the signal is recorded and
|
|
282
|
+
# deferred, the drain ALWAYS finishes, and the first signal is re-raised
|
|
283
|
+
# by dispose after bookkeeping and the parent prune. Late registration
|
|
284
|
+
# racing the drain is monitor-serialized (pop loop, or the sealed
|
|
285
|
+
# immediate-run path). Returns [drained, first_signal].
|
|
286
|
+
def drain_inverses(dmesg:)
|
|
287
|
+
drained = 0
|
|
288
|
+
deferred = nil
|
|
289
|
+
begin
|
|
290
|
+
while (inverse = @disposables.pop)
|
|
291
|
+
begin
|
|
292
|
+
run_inverse(inverse, dmesg: dmesg)
|
|
293
|
+
drained += 1
|
|
294
|
+
rescue Interrupt, SystemExit => e
|
|
295
|
+
record_deferred_signal(e)
|
|
296
|
+
deferred ||= e
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
ensure
|
|
300
|
+
@services.clear
|
|
301
|
+
@service_inverses.clear
|
|
302
|
+
@child_inverses.clear
|
|
303
|
+
end
|
|
304
|
+
[drained, deferred]
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def resolve_dmesg
|
|
308
|
+
d = @services["dmesg"] || resolve("dmesg")
|
|
309
|
+
d if d.respond_to?(:log)
|
|
310
|
+
rescue StandardError
|
|
311
|
+
nil
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def record_deferred_signal(exception)
|
|
315
|
+
@last_dispose_errors << exception
|
|
316
|
+
Rubik::Log.warn(:runtime, "dispose inverse raised #{exception.class} (deferred until drain completes): #{exception.message}")
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# Runs one inverse, never letting a failure abort the unwind (AGENTS.md §5):
|
|
320
|
+
# record it, report to stderr, keep going. Interrupt/SystemExit are NOT
|
|
321
|
+
# rescued here: the drain loop in dispose defers them so the unwind
|
|
322
|
+
# completes first (complete-drain-then-re-raise policy), while the
|
|
323
|
+
# sealed immediate-run path in register_disposable lets them propagate
|
|
324
|
+
# as-is — there is no accumulator left to protect there.
|
|
325
|
+
def run_inverse(inverse, dmesg: nil)
|
|
326
|
+
inverse.call
|
|
327
|
+
rescue StandardError => e
|
|
328
|
+
@last_dispose_errors << e
|
|
329
|
+
Rubik::Log.warn(:runtime, "dispose inverse failed: #{e.class}: #{e.message}")
|
|
330
|
+
dmesg ||= resolve_dmesg
|
|
331
|
+
dmesg&.log(:context, :err, "Dispose inverse failed: #{e.class}: #{e.message}")
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
end
|