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,441 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "errors"
|
|
4
|
+
require_relative "timers"
|
|
5
|
+
require_relative "dns_pool"
|
|
6
|
+
|
|
7
|
+
module Rubik
|
|
8
|
+
module Concurrency
|
|
9
|
+
# The reactor backing a Scheduler: one OS thread, N non-blocking fibers.
|
|
10
|
+
#
|
|
11
|
+
# Thread discipline (load-bearing — verified against Ruby 4.0.6 probes,
|
|
12
|
+
# see _plan/fiber-scheduler-silent-concurrency-2026-10-05.md):
|
|
13
|
+
# * VM hooks (io_wait, kernel_sleep, block, fiber creation) always run
|
|
14
|
+
# on the loop thread, inside a resume. Timers, the io-interest
|
|
15
|
+
# registry, park generations and spawn bookkeeping are therefore
|
|
16
|
+
# touched only by the loop thread — no locks.
|
|
17
|
+
# * Foreign threads (dying join targets, process waiters, DNS workers)
|
|
18
|
+
# wake fibers through #push / #unpark, which guard @runnable/@parked
|
|
19
|
+
# with @mutex and write a self-pipe byte so a blocked IO.select on
|
|
20
|
+
# the loop thread wakes immediately.
|
|
21
|
+
# * Wake staleness is handled by park generations: every registration
|
|
22
|
+
# stamps @gen[fiber]; timer fires and io dispatch only push when the
|
|
23
|
+
# generation still matches, so a fiber that timed out and re-parked
|
|
24
|
+
# elsewhere can never be resumed by a stale source.
|
|
25
|
+
#
|
|
26
|
+
# Drain model (waiters, not liveness — Fiber#alive? does not exist on
|
|
27
|
+
# Ruby 4.0): the loop runs while anything is runnable, parked (Mutex/CV/
|
|
28
|
+
# Thread#join/block), io-waiting, or timer-pending. Finished fibers leave
|
|
29
|
+
# no registration, so "is anything waiting?" answers "are we done?".
|
|
30
|
+
class EventLoop
|
|
31
|
+
READABLE = 1
|
|
32
|
+
WRITABLE = 2
|
|
33
|
+
|
|
34
|
+
attr_reader :spawn_errors
|
|
35
|
+
|
|
36
|
+
def initialize
|
|
37
|
+
@mutex = ::Mutex.new
|
|
38
|
+
@runnable = [] # [[fiber, resume_value]] — guarded by @mutex
|
|
39
|
+
@parked = {} # fiber => true (block-hook parks) — guarded
|
|
40
|
+
@waiter_threads = [] # process-wait threads — guarded
|
|
41
|
+
# Stale timers (fiber re-parked or resumed since registration) must
|
|
42
|
+
# not gate draining — @gen is bumped on every resume, so any of this
|
|
43
|
+
# fiber's outstanding registrations becomes inert at that moment.
|
|
44
|
+
@timers = Timers.new { |fiber, gen| @gen[fiber] != gen }
|
|
45
|
+
@io_waiters = {} # io => { read: {fiber=>gen}, write: {fiber=>gen} }
|
|
46
|
+
@gen = ::Hash.new(0) # fiber => park generation (loop thread only)
|
|
47
|
+
@live = {} # fiber => true while a spawned fiber runs
|
|
48
|
+
@next_ordinal = 0
|
|
49
|
+
@spawn_errors = [] # [[ordinal, exception]] — body/spawn lifecycle
|
|
50
|
+
@loop_thread = nil
|
|
51
|
+
@closed = false
|
|
52
|
+
@wake_read, @wake_write = ::IO.pipe
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def closed?
|
|
56
|
+
@closed
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# ---- Any-thread API ---------------------------------------------------
|
|
60
|
+
|
|
61
|
+
# Queues a fiber to be resumed with value. Safe from foreign threads:
|
|
62
|
+
# pushes write a self-pipe byte so the loop's IO.select wakes at once.
|
|
63
|
+
def push(fiber, value: true)
|
|
64
|
+
@mutex.synchronize do
|
|
65
|
+
return if @closed
|
|
66
|
+
|
|
67
|
+
@runnable << [fiber, value]
|
|
68
|
+
end
|
|
69
|
+
kick unless ::Thread.current.equal?(@loop_thread)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# unblock-hook counterpart for programmatically parked fibers (DNS
|
|
73
|
+
# jobs, fan-out latches): removes the park and delivers value. Park
|
|
74
|
+
# removal and resume append happen under ONE lock section — a split
|
|
75
|
+
# update could let the drain's idle? check observe both sets empty in
|
|
76
|
+
# the gap and exit, stranding the resume (and the fiber's ensures).
|
|
77
|
+
def unpark(_blocker, fiber, value: true)
|
|
78
|
+
@mutex.synchronize do
|
|
79
|
+
return if @closed
|
|
80
|
+
|
|
81
|
+
@parked.delete(fiber)
|
|
82
|
+
@runnable << [fiber, value]
|
|
83
|
+
end
|
|
84
|
+
kick unless ::Thread.current.equal?(@loop_thread)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def interrupt(fiber, exception = Interrupted)
|
|
88
|
+
exc =
|
|
89
|
+
if exception.is_a?(::Exception)
|
|
90
|
+
exception
|
|
91
|
+
elsif exception.is_a?(::Class)
|
|
92
|
+
exception.new("fiber interrupted")
|
|
93
|
+
else
|
|
94
|
+
Interrupted.new("fiber interrupted")
|
|
95
|
+
end
|
|
96
|
+
push(fiber, value: exc)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def track_waiter(thread)
|
|
100
|
+
@mutex.synchronize { @waiter_threads << thread unless @closed }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# ---- Loop-thread API (VM hooks & body) ---------------------------------
|
|
104
|
+
|
|
105
|
+
# Scheduler.fiber hook (Ruby 4.0): we create the fiber. Errors from the
|
|
106
|
+
# block never escape into the reactor — they are captured per spawn
|
|
107
|
+
# ordinal and raised by the boundary after the drain (K7), including
|
|
108
|
+
# Interrupt-level deaths so teardown can finish cleanly first.
|
|
109
|
+
def spawn_fiber(&block)
|
|
110
|
+
@next_ordinal += 1
|
|
111
|
+
ordinal = @next_ordinal
|
|
112
|
+
fiber = nil
|
|
113
|
+
fiber = ::Fiber.new(blocking: false) do
|
|
114
|
+
block.call
|
|
115
|
+
rescue ::Exception => e # rubocop:disable Lint/RescueException -- boundary re-raises after drain
|
|
116
|
+
@spawn_errors << [ordinal, e]
|
|
117
|
+
ensure
|
|
118
|
+
@live.delete(fiber)
|
|
119
|
+
end
|
|
120
|
+
@live[fiber] = true
|
|
121
|
+
push(fiber, value: nil)
|
|
122
|
+
fiber
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def nap(duration)
|
|
126
|
+
park_fiber { |fiber, gen| @timers.add(monotonic + duration, fiber, true, gen) }
|
|
127
|
+
true
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# block-hook: park the current fiber on a blocker (Mutex, CV, Thread).
|
|
131
|
+
# Returns truthy when unparked, false when the timeout elapsed. A
|
|
132
|
+
# timeout of 0 still registers a timer (fires immediately) — truthiness
|
|
133
|
+
# guards would park join(0)/wait(0) polls forever.
|
|
134
|
+
def park(_blocker, timeout = nil)
|
|
135
|
+
@mutex.synchronize { @parked[::Fiber.current] = true }
|
|
136
|
+
value = park_fiber do |fiber, gen|
|
|
137
|
+
@timers.add(monotonic + timeout, fiber, :timeout, gen) unless timeout.nil?
|
|
138
|
+
end
|
|
139
|
+
value == :timeout ? false : value
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# io_wait hook: interests is a READABLE/WRITABLE mask. The resume value
|
|
143
|
+
# is the ready-event mask (0 when the timeout elapsed — falsy, which is
|
|
144
|
+
# what IO#wait_readable reports to callers like Net::HTTP).
|
|
145
|
+
def wait_io(io, interests, timeout)
|
|
146
|
+
fiber = ::Fiber.current
|
|
147
|
+
gen = (@gen[fiber] += 1)
|
|
148
|
+
slot = (@io_waiters[io] ||= { read: {}, write: {} })
|
|
149
|
+
slot[:read][fiber] = gen if interests.anybits?(READABLE)
|
|
150
|
+
slot[:write][fiber] = gen if interests.anybits?(WRITABLE)
|
|
151
|
+
@timers.add(monotonic + timeout, fiber, 0, gen) unless timeout.nil?
|
|
152
|
+
value = ::Fiber.yield
|
|
153
|
+
raise value if value.is_a?(::Exception)
|
|
154
|
+
|
|
155
|
+
value
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# io_read body (one nonblocking attempt per call — the VM re-invokes
|
|
159
|
+
# the hook in a loop, mirroring IO::Buffer#read's one-syscall shape).
|
|
160
|
+
# Fills the caller's buffer and returns bytes read (0 = EOF).
|
|
161
|
+
#
|
|
162
|
+
# Two 4.0.6 facts shape this (verified by probe): the integer hook
|
|
163
|
+
# args are protocol noise (3.3 passes length; 4.0 passes 0,0 — both
|
|
164
|
+
# size the buffer to the requested read, hence buffer.size), and
|
|
165
|
+
# EVERY blocking-family read primitive — read_nonblock included — is
|
|
166
|
+
# itself routed back through scheduler.io_read once the hook exists.
|
|
167
|
+
# The Fiber.blocking wrap is the VM-sanctioned escape (ruby's own test
|
|
168
|
+
# scheduler wraps its primitives the same way): it runs the primitive
|
|
169
|
+
# immediately, bypassing interception, and read_nonblock(exception:
|
|
170
|
+
# false) never blocks the OS thread.
|
|
171
|
+
def read_io(io, buffer)
|
|
172
|
+
length = buffer.size
|
|
173
|
+
return 0 if length.zero?
|
|
174
|
+
|
|
175
|
+
loop do
|
|
176
|
+
chunk = ::Fiber.blocking { io.read_nonblock(length, exception: false) }
|
|
177
|
+
case chunk
|
|
178
|
+
when :wait_readable then wait_io(io, READABLE, nil)
|
|
179
|
+
when :wait_writable then wait_io(io, WRITABLE, nil)
|
|
180
|
+
when nil then return 0
|
|
181
|
+
else
|
|
182
|
+
buffer.copy(::IO::Buffer.for(chunk), 0)
|
|
183
|
+
return chunk.bytesize
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# io_write body: writes the whole caller-supplied buffer with one
|
|
189
|
+
# nonblocking attempt per call, parking on WRITABLE between attempts.
|
|
190
|
+
# (4.0.6 passes (0,0) as integers and expects the full buffer out.)
|
|
191
|
+
def write_io(io, buffer)
|
|
192
|
+
data = buffer.get_string(0, buffer.size)
|
|
193
|
+
return 0 if data.empty?
|
|
194
|
+
|
|
195
|
+
loop do
|
|
196
|
+
written = ::Fiber.blocking { io.write_nonblock(data, exception: false) }
|
|
197
|
+
return written if written.is_a?(::Integer)
|
|
198
|
+
|
|
199
|
+
wait_io(io, WRITABLE, nil)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# DNS resolution parks the fiber; the DnsPool worker delivers the list
|
|
204
|
+
# of IP strings (or an exception instance to raise) via unpark.
|
|
205
|
+
def resolve_address(hostname)
|
|
206
|
+
pool = (@dns_pool ||= DnsPool.new(self))
|
|
207
|
+
pool.resolve(hostname)
|
|
208
|
+
rescue StandardError => e
|
|
209
|
+
# Pool unusable (thread creation blocked, shutdown racing): resolve
|
|
210
|
+
# inline — correct, just not interleaved.
|
|
211
|
+
Rubik::Log.warn(:concurrency, "dns pool unavailable, resolving inline: #{e.class}: #{e.message}")
|
|
212
|
+
::Addrinfo.getaddrinfo(hostname, nil).map(&:ip_address).uniq
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# ---- Reactor -------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
# Runs the boundary body as a fiber and drains everything it spawned.
|
|
218
|
+
# Returns [body_result, body_error]; spawned-fiber errors live in
|
|
219
|
+
# #spawn_errors for the boundary to raise after the fact.
|
|
220
|
+
def run(&body)
|
|
221
|
+
@loop_thread = ::Thread.current
|
|
222
|
+
result = nil
|
|
223
|
+
body_error = nil
|
|
224
|
+
runner = nil
|
|
225
|
+
runner = ::Fiber.new(blocking: false) do
|
|
226
|
+
result = body.call
|
|
227
|
+
rescue ::Exception => e # rubocop:disable Lint/RescueException -- re-raised by Boundary after drain
|
|
228
|
+
body_error = e
|
|
229
|
+
end
|
|
230
|
+
@live[runner] = true
|
|
231
|
+
push(runner, value: nil)
|
|
232
|
+
run_until_drained
|
|
233
|
+
[result, body_error]
|
|
234
|
+
ensure
|
|
235
|
+
@live.delete(runner) if runner
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def run_until_drained
|
|
239
|
+
@loop_thread = ::Thread.current
|
|
240
|
+
until idle?
|
|
241
|
+
run_batch
|
|
242
|
+
break if idle?
|
|
243
|
+
|
|
244
|
+
select_or_wait
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def idle?
|
|
249
|
+
@mutex.synchronize { @runnable.empty? && @parked.empty? } &&
|
|
250
|
+
@timers.empty? && @io_waiters.empty?
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Idempotent teardown. Never raises (mirrors Platform::IoDrain):
|
|
254
|
+
# stops the DNS pool, joins process-wait threads with a bounded
|
|
255
|
+
# timeout, kills stragglers, closes the self-pipe, and reports any
|
|
256
|
+
# pending work left behind by an abandoned drain instead of hiding it.
|
|
257
|
+
def shutdown
|
|
258
|
+
waiters = nil
|
|
259
|
+
@mutex.synchronize do
|
|
260
|
+
return if @closed
|
|
261
|
+
|
|
262
|
+
@closed = true
|
|
263
|
+
waiters = @waiter_threads.dup
|
|
264
|
+
@waiter_threads.clear
|
|
265
|
+
end
|
|
266
|
+
@dns_pool&.shutdown
|
|
267
|
+
waiters.each do |thread|
|
|
268
|
+
thread.join(0.1)
|
|
269
|
+
thread.kill if thread.alive?
|
|
270
|
+
rescue StandardError
|
|
271
|
+
nil
|
|
272
|
+
end
|
|
273
|
+
leak = nil
|
|
274
|
+
@mutex.synchronize do
|
|
275
|
+
leak = { runnable: @runnable.size, parked: @parked.size, timers: @timers.size,
|
|
276
|
+
io_waiters: @io_waiters.size, live: @live.size }
|
|
277
|
+
end
|
|
278
|
+
Rubik::Log.warn(:concurrency, "event loop closed with pending work: #{leak.inspect}") if leak.values.sum.positive?
|
|
279
|
+
[@wake_read, @wake_write].each do |io|
|
|
280
|
+
io.close unless io.closed?
|
|
281
|
+
rescue StandardError
|
|
282
|
+
nil
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
private
|
|
287
|
+
|
|
288
|
+
# Shared park sequence: bump the generation (invalidating older stale
|
|
289
|
+
# wakeups), register the caller's timers, yield, and either re-raise a
|
|
290
|
+
# delivered exception or return the resume value.
|
|
291
|
+
def park_fiber
|
|
292
|
+
fiber = ::Fiber.current
|
|
293
|
+
gen = (@gen[fiber] += 1)
|
|
294
|
+
yield fiber, gen
|
|
295
|
+
value = ::Fiber.yield
|
|
296
|
+
raise value if value.is_a?(::Exception)
|
|
297
|
+
|
|
298
|
+
value
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def run_batch
|
|
302
|
+
while (item = @mutex.synchronize { @runnable.shift })
|
|
303
|
+
fiber, value = item
|
|
304
|
+
# A resumed fiber is definitionally no longer parked; dropping the
|
|
305
|
+
# registration here keeps interrupt-style wakes from leaking parks.
|
|
306
|
+
# Bumping the park generation invalidates every OTHER outstanding
|
|
307
|
+
# registration the fiber still holds (io_wait timeout timer, io
|
|
308
|
+
# interests) so dead entries cannot gate draining.
|
|
309
|
+
@mutex.synchronize { @parked.delete(fiber) }
|
|
310
|
+
@gen[fiber] += 1
|
|
311
|
+
drop_io_interest(fiber)
|
|
312
|
+
begin
|
|
313
|
+
fiber.resume(value)
|
|
314
|
+
rescue ::FiberError
|
|
315
|
+
nil # stale wakeup for a finished fiber
|
|
316
|
+
rescue StandardError => e
|
|
317
|
+
# A failure of the resume machinery itself (not the fiber's block
|
|
318
|
+
# — those are captured in spawn_fiber): record it so the
|
|
319
|
+
# boundary's raise policy surfaces it instead of only a log line.
|
|
320
|
+
Rubik::Log.warn(:concurrency, "fiber resume failed: #{e.class}: #{e.message} @ #{(e.backtrace || []).first}")
|
|
321
|
+
@spawn_errors << [@next_ordinal += 1, e]
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def select_or_wait
|
|
327
|
+
read_set = @io_waiters.keys
|
|
328
|
+
write_set = @io_waiters.each_with_object([]) do |(io, slot), acc|
|
|
329
|
+
acc << io unless slot[:write].empty?
|
|
330
|
+
end
|
|
331
|
+
read_set << @wake_read
|
|
332
|
+
timeout = @timers.next_gap(monotonic)
|
|
333
|
+
|
|
334
|
+
ready = safe_select(read_set, write_set, timeout)
|
|
335
|
+
return if ready == :retry
|
|
336
|
+
|
|
337
|
+
drain_wake_pipe
|
|
338
|
+
if ready
|
|
339
|
+
ready_read, ready_write = ready
|
|
340
|
+
dispatch_ready(ready_read, ready_write)
|
|
341
|
+
end
|
|
342
|
+
fire_due_timers
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
# A closed io among the watched sets makes IO.select raise; prune the
|
|
346
|
+
# offender and let the caller loop again (sentinel :retry). When
|
|
347
|
+
# nothing is prunable the loop would spin hot forever — fail loud and
|
|
348
|
+
# let the boundary's raise policy surface it instead.
|
|
349
|
+
def safe_select(read_set, write_set, timeout)
|
|
350
|
+
::IO.select(read_set, write_set, nil, timeout)
|
|
351
|
+
rescue IOError, Errno::EBADF, Errno::EINVAL => e
|
|
352
|
+
pruned = prune_closed_ios((read_set + write_set).uniq)
|
|
353
|
+
return :retry if pruned.positive?
|
|
354
|
+
|
|
355
|
+
Rubik::Log.warn(:concurrency, "io_select failed with nothing prunable (#{e.class}: #{e.message}; " \
|
|
356
|
+
"watching #{read_set.size + write_set.size} ios)")
|
|
357
|
+
raise Error, "event loop io_select failed: #{e.class}: #{e.message}"
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def fire_due_timers
|
|
361
|
+
@timers.fire_due!(monotonic) do |fiber, value, gen|
|
|
362
|
+
next if @gen[fiber] != gen # stale: fiber moved on after registering
|
|
363
|
+
|
|
364
|
+
drop_io_interest(fiber)
|
|
365
|
+
push(fiber, value: value)
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def dispatch_ready(ready_read, ready_write)
|
|
370
|
+
ready_read.each { |io| wake_io(io, READABLE) }
|
|
371
|
+
ready_write.each { |io| wake_io(io, WRITABLE) }
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def wake_io(io, events)
|
|
375
|
+
slot = @io_waiters[io]
|
|
376
|
+
return unless slot
|
|
377
|
+
|
|
378
|
+
waiters = slot[:read].merge(slot[:write])
|
|
379
|
+
waiters.each do |fiber, gen|
|
|
380
|
+
if @gen[fiber] != gen
|
|
381
|
+
# Stale interest (the fiber timed out or moved on): remove it so
|
|
382
|
+
# a never-ready io cannot keep the loop from draining.
|
|
383
|
+
slot[:read].delete(fiber)
|
|
384
|
+
slot[:write].delete(fiber)
|
|
385
|
+
next
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# Drop both interest directions for this fiber; if it waits again
|
|
389
|
+
# it re-registers with a fresh generation.
|
|
390
|
+
slot[:read].delete(fiber)
|
|
391
|
+
slot[:write].delete(fiber)
|
|
392
|
+
push(fiber, value: events)
|
|
393
|
+
end
|
|
394
|
+
@io_waiters.delete(io) if slot[:read].empty? && slot[:write].empty?
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def drop_io_interest(fiber)
|
|
398
|
+
@io_waiters.each do |io, slot|
|
|
399
|
+
next unless slot[:read].key?(fiber) || slot[:write].key?(fiber)
|
|
400
|
+
|
|
401
|
+
slot[:read].delete(fiber)
|
|
402
|
+
slot[:write].delete(fiber)
|
|
403
|
+
@io_waiters.delete(io) if slot[:read].empty? && slot[:write].empty?
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# Removes closed ios from the interest registry, waking their waiting
|
|
408
|
+
# fibers with a 0 mask (their next nonblocking attempt sees the truth).
|
|
409
|
+
# Returns the number of ios pruned.
|
|
410
|
+
def prune_closed_ios(ios)
|
|
411
|
+
pruned = 0
|
|
412
|
+
ios.each do |io|
|
|
413
|
+
next unless io.closed?
|
|
414
|
+
|
|
415
|
+
slot = @io_waiters.delete(io)
|
|
416
|
+
next unless slot
|
|
417
|
+
|
|
418
|
+
(slot[:read].keys + slot[:write].keys).each { |fiber| push(fiber, value: 0) }
|
|
419
|
+
pruned += 1
|
|
420
|
+
end
|
|
421
|
+
pruned
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def drain_wake_pipe
|
|
425
|
+
nil while @wake_read.read_nonblock(4096, exception: false).is_a?(String)
|
|
426
|
+
rescue IOError, Errno::EBADF
|
|
427
|
+
nil
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def kick
|
|
431
|
+
@wake_write.write_nonblock(".", exception: false)
|
|
432
|
+
rescue StandardError
|
|
433
|
+
nil # pipe full (a wake byte is already pending) or closing
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def monotonic
|
|
437
|
+
::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubik
|
|
4
|
+
module Concurrency
|
|
5
|
+
# Bounded thread-join mechanics (arch-deepening 2026-10-08 #12): the
|
|
6
|
+
# monotonic-deadline polling join previously hand-mirrored in
|
|
7
|
+
# mcp/manager.rb#join_boot_safely and repl/ast_warmup.rb#join_indexer_safely.
|
|
8
|
+
# Mechanics only — the caller passes the thread handle and keeps its own
|
|
9
|
+
# log channel and timeout-exhaustion policy; this module never logs and
|
|
10
|
+
# never raises (a Thread#join failure is RETURNED, the error-as-data
|
|
11
|
+
# precedent from ProcessWaiter).
|
|
12
|
+
module Join
|
|
13
|
+
# Poll cadence: short enough that teardown feels immediate, long
|
|
14
|
+
# enough to never spin the GVL.
|
|
15
|
+
POLL_INTERVAL = 0.05
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
# Joins +handle+ and reports the outcome:
|
|
20
|
+
# :no_thread nil handle (caller-level "nothing to join")
|
|
21
|
+
# :joined thread finished inside the bound
|
|
22
|
+
# :timeout Numeric timeout: deadline lapsed, thread still alive
|
|
23
|
+
# StandardError the thread died raising — RETURNED, never raised
|
|
24
|
+
# (caller owns the log). A crash BEFORE the first
|
|
25
|
+
# poll is surfaced too: the final non-blocking join
|
|
26
|
+
# observes a dead-with-exception thread that no
|
|
27
|
+
# loop iteration ever joined.
|
|
28
|
+
# timeout: nil joins unboundedly, polling at POLL_INTERVAL. The handle
|
|
29
|
+
# is captured ONCE at call time — callers that must not hold a lock
|
|
30
|
+
# while joining read the handle under their lock first (G5) and pass
|
|
31
|
+
# it in.
|
|
32
|
+
def thread(handle, timeout: nil)
|
|
33
|
+
return :no_thread unless handle
|
|
34
|
+
|
|
35
|
+
if timeout
|
|
36
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
|
37
|
+
while handle.alive?
|
|
38
|
+
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
39
|
+
break if remaining <= 0
|
|
40
|
+
|
|
41
|
+
handle.join([remaining, POLL_INTERVAL].min)
|
|
42
|
+
end
|
|
43
|
+
return :timeout if handle.alive?
|
|
44
|
+
else
|
|
45
|
+
handle.join(POLL_INTERVAL) while handle.alive?
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
handle.join(0)
|
|
49
|
+
:joined
|
|
50
|
+
rescue StandardError => e
|
|
51
|
+
e
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "English"
|
|
4
|
+
require_relative "errors"
|
|
5
|
+
|
|
6
|
+
module Rubik
|
|
7
|
+
module Concurrency
|
|
8
|
+
# process_wait hook body. Fast path: a WNOHANG poll satisfies callers
|
|
9
|
+
# that only probe (Process.detach, waitpid(WNOHANG)) without a thread.
|
|
10
|
+
# Blocking path: one short-lived waiter thread per waited pid doing a
|
|
11
|
+
# blocking Process.wait — the fiber's Thread#join on it parks through the
|
|
12
|
+
# block/unblock hooks, so no fiber-visible machinery is needed. Never
|
|
13
|
+
# waits on -1 (that would race Open3's per-child wait_thr reapers).
|
|
14
|
+
#
|
|
15
|
+
# Errno::ECHILD means someone else (usually the Open3 wait thread that
|
|
16
|
+
# spawned the child) reaped the pid first; returning nil forever would
|
|
17
|
+
# make the VM spin on the dead pid, so a truthy StatusLike stands in.
|
|
18
|
+
module ProcessWaiter
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
def wait(pid, flags, loop)
|
|
22
|
+
# Process.wait returns the PID (Integer); the Process::Status the VM
|
|
23
|
+
# requires as the hook's return lives in $? after the call. ECHILD
|
|
24
|
+
# means another reaper (usually Open3's wait_thr) got the status
|
|
25
|
+
# first; a StatusLike with a NIL exitstatus stands in — truthy for
|
|
26
|
+
# the VM's done-check without fabricating a success/failure code.
|
|
27
|
+
if flags.anybits?(::Process::WNOHANG)
|
|
28
|
+
begin
|
|
29
|
+
reaped = ::Process.wait(pid, flags)
|
|
30
|
+
return reaped.nil? ? nil : $CHILD_STATUS
|
|
31
|
+
rescue Errno::ECHILD
|
|
32
|
+
return Errors::StatusLike.new(pid: pid, exitstatus: nil)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
waiter = ::Thread.new do
|
|
37
|
+
::Process.wait(pid)
|
|
38
|
+
$CHILD_STATUS
|
|
39
|
+
rescue Errno::ECHILD
|
|
40
|
+
Errors::StatusLike.new(pid: pid, exitstatus: nil)
|
|
41
|
+
rescue StandardError => e
|
|
42
|
+
e
|
|
43
|
+
end
|
|
44
|
+
loop.track_waiter(waiter)
|
|
45
|
+
value = waiter.value
|
|
46
|
+
# Waiter failure arrives as the thread value; the VM expects a
|
|
47
|
+
# Process::Status-shaped return, so RAISE the original error here —
|
|
48
|
+
# returning it as data would surface as an opaque TypeError.
|
|
49
|
+
raise value if value.is_a?(::Exception)
|
|
50
|
+
|
|
51
|
+
value
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "event_loop"
|
|
4
|
+
require_relative "process_waiter"
|
|
5
|
+
|
|
6
|
+
module Rubik
|
|
7
|
+
module Concurrency
|
|
8
|
+
# Adapter for the VM's Fiber::Scheduler hook protocol — stateless; all
|
|
9
|
+
# state lives on the EventLoop. Ruby 3.3 ↔ 4.0 protocol drift is
|
|
10
|
+
# concentrated here (probes against 4.0.6, plan §2):
|
|
11
|
+
#
|
|
12
|
+
# fiber 4.0: fiber(&blk) — scheduler creates and returns the fiber.
|
|
13
|
+
# 3.3: fiber(fiber, block) — VM pre-created it.
|
|
14
|
+
# io_read integer args differ (3.3 passes length; 4.0 passes 0, 0)
|
|
15
|
+
# io_write but the buffer is always sized to the operation, so both
|
|
16
|
+
# hooks drive off buffer.size — see EventLoop#read_io.
|
|
17
|
+
#
|
|
18
|
+
# io_read/io_write are required on 4.0: without them a plain IO#read
|
|
19
|
+
# inside a scheduled fiber falls back to a whole-thread BLOCKING read
|
|
20
|
+
# (measured: 20 × 0.2 s child pipes serialized into 13 s). The buffer is
|
|
21
|
+
# filled/drained with read_nonblock/write_nonblock + IO::Buffer views —
|
|
22
|
+
# IO#nonblock= and IO::Buffer#read(io,...) are NOT usable on Windows
|
|
23
|
+
# builds (NoMethodError / constant-zero reads, verified), while the
|
|
24
|
+
# nonblock(…, exception: false) family works for pipes and sockets alike.
|
|
25
|
+
class Scheduler
|
|
26
|
+
attr_reader :loop
|
|
27
|
+
|
|
28
|
+
def initialize(loop)
|
|
29
|
+
@loop = loop
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def fiber(*args, &blk)
|
|
33
|
+
blk ? @loop.spawn_fiber(&blk) : args[0]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def fiber_interrupt(target, exception = nil)
|
|
37
|
+
@loop.interrupt(target, exception)
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def io_wait(io, events, timeout)
|
|
42
|
+
@loop.wait_io(io, events, timeout)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# The integer args differ per protocol (3.3: length; 4.0. offset/length
|
|
46
|
+
# both zero) but the buffer is always sized to the operation, so the
|
|
47
|
+
# EventLoop drives both hooks off buffer.size — see read_io/write_io.
|
|
48
|
+
def io_read(io, buffer, *_protocol_args)
|
|
49
|
+
@loop.read_io(io, buffer)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def io_write(io, buffer, *_protocol_args)
|
|
53
|
+
@loop.write_io(io, buffer)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def kernel_sleep(duration = nil)
|
|
57
|
+
duration ? @loop.nap(duration) : @loop.park(:sleep)
|
|
58
|
+
true
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def block(blocker, timeout = nil)
|
|
62
|
+
@loop.park(blocker, timeout)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def unblock(blocker, fiber)
|
|
66
|
+
@loop.unpark(blocker, fiber)
|
|
67
|
+
nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def process_wait(pid, flags)
|
|
71
|
+
ProcessWaiter.wait(pid, flags, @loop)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def address_resolve(hostname)
|
|
75
|
+
@loop.resolve_address(hostname)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def scheduler_close
|
|
79
|
+
@loop.shutdown
|
|
80
|
+
nil
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|