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,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubik
|
|
4
|
+
module Orchestrator
|
|
5
|
+
# The Loop-side mailbox for subagent notices (fresh-scan card 4;
|
|
6
|
+
# ContinuationManager mold: plain constructor, own state, eager init in
|
|
7
|
+
# Loop#initialize plus a lazy reader for Loop.allocate test doubles).
|
|
8
|
+
#
|
|
9
|
+
# Boundary: QUEUE-ONLY. This class owns the thread-safe queue, the
|
|
10
|
+
# notice-shape validation at entry, and the run/continuation lifecycle
|
|
11
|
+
# of that queue. It deliberately does NOT render notices
|
|
12
|
+
# (Subagent::Notices owns the vocabulary) and does NOT merge into
|
|
13
|
+
# history (Loop owns @history): drain_all hands back validated notice
|
|
14
|
+
# hashes and the caller renders.
|
|
15
|
+
#
|
|
16
|
+
# Threading contract (inherited verbatim from the pre-extraction Loop
|
|
17
|
+
# code; see also repl/lifecycle.rb handle_subagent_event):
|
|
18
|
+
# - enqueue runs on subagent worker threads (the bus emit is
|
|
19
|
+
# synchronous): validation plus a bare Queue#push, never a lock a
|
|
20
|
+
# REPL thread might hold while routing an event.
|
|
21
|
+
# - drain_all / reset_for_new_run! run on the turn thread between
|
|
22
|
+
# steps and may race a concurrent enqueue safely: pop(true) raises
|
|
23
|
+
# ThreadError on empty, the race-safe terminator for the
|
|
24
|
+
# check-then-act window a conditional loop would miss.
|
|
25
|
+
# - reset_for_new_run! REASSIGNS the queue (faithful to Loop's old
|
|
26
|
+
# `@subagent_notices = Queue.new`), it does not clear it. A notice
|
|
27
|
+
# pushed to the old Queue object in the microscopic window between
|
|
28
|
+
# the previous run's last drain and the reassign is stranded on the
|
|
29
|
+
# dead queue: accepted by design — the user channel already rendered
|
|
30
|
+
# its line at enqueue time, and manage_subagents wait remains the
|
|
31
|
+
# model's recovery path.
|
|
32
|
+
class SubagentNoticeInbox
|
|
33
|
+
def initialize
|
|
34
|
+
@queue = Queue.new
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Entry point and the ONLY mutation path: validate, then push. The
|
|
38
|
+
# REPL routing seam reaches this via Loop#enqueue_subagent_notice
|
|
39
|
+
# with Notices.normalize output; this raise is the safety net against
|
|
40
|
+
# the legacy string dialect. Message pinned verbatim by tests.
|
|
41
|
+
def enqueue(notice)
|
|
42
|
+
raise ArgumentError, "subagent notice must be a Hash with :kind, got #{notice.inspect}" unless notice.is_a?(Hash) && notice[:kind]
|
|
43
|
+
|
|
44
|
+
@queue.push(notice)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Pops everything pending (FIFO) and returns the notice hashes.
|
|
48
|
+
# Empty-after-drain by construction; a second call returns [].
|
|
49
|
+
def drain_all
|
|
50
|
+
notices = []
|
|
51
|
+
loop { notices << @queue.pop(true) }
|
|
52
|
+
rescue ThreadError
|
|
53
|
+
notices
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Non-blocking emptiness probe (GoalWait's wake check). Advisory
|
|
57
|
+
# only: an enqueue racing this check is caught on the next poll or
|
|
58
|
+
# drain — Queue#pop remains the authoritative consumer.
|
|
59
|
+
def empty?
|
|
60
|
+
@queue.empty?
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Per-run lifecycle (Loop#run, aligned with the @history reset):
|
|
64
|
+
# stale notices from a previous run on a cached Loop instance must
|
|
65
|
+
# never drain into the fresh history. Reassigns — see the class
|
|
66
|
+
# comment for the stranding window this preserves on purpose.
|
|
67
|
+
def reset_for_new_run!
|
|
68
|
+
@queue = Queue.new
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Continuation lifecycle (Loop#continue_loop): an explicit,
|
|
72
|
+
# contract-carrying no-op. Continuation resumes the same run, so
|
|
73
|
+
# undelivered notices must survive into the next execute_steps pass.
|
|
74
|
+
# Retention is the decision — encoding it as a named call at the
|
|
75
|
+
# continuation seam keeps the run/continue asymmetry greppable in
|
|
76
|
+
# code instead of living only in a comment inside #run.
|
|
77
|
+
def keep_for_continuation; end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubik
|
|
4
|
+
module Orchestrator
|
|
5
|
+
# THE single vocabulary home for the orchestrator family's tool-call
|
|
6
|
+
# id spellings (previously three independent interpolations: Loop's
|
|
7
|
+
# response normalizer, Subagent::StepRunner, StepExecutor's defensive
|
|
8
|
+
# fallback). One prefix and joining convention; TWO deliberate
|
|
9
|
+
# uniqueness policies — the sites' collision realities differ:
|
|
10
|
+
#
|
|
11
|
+
# - Loop (unique: true): production builds a fresh Loop per turn while
|
|
12
|
+
# the session history persists, so step_count restarts every turn
|
|
13
|
+
# and a deterministic "call_3_1" WILL recur inside one history —
|
|
14
|
+
# duplicate tool_use ids break strict providers' tool_result
|
|
15
|
+
# pairing. The 4-digit random suffix is load-bearing, not decor.
|
|
16
|
+
# - Subagent StepRunner (deterministic): a subagent's history lives
|
|
17
|
+
# and dies with its own run — never restored, never continued — so
|
|
18
|
+
# (subagent id, step, index) is unique by construction, and
|
|
19
|
+
# determinism keeps ids greppable across subagents in transcripts.
|
|
20
|
+
# - StepExecutor (unique: true): dead on the production path (the
|
|
21
|
+
# TurnExecutor normalizes ids before dispatch) but live on the
|
|
22
|
+
# executor's own seam — test doubles call it with bare, id-less tool
|
|
23
|
+
# calls, and no turn-local index exists there.
|
|
24
|
+
#
|
|
25
|
+
# mint is spelling-only by design: the id PRECEDENCE chain
|
|
26
|
+
# (tc["id"] || tc[:id] || mint) lives in one place now — the shared
|
|
27
|
+
# TurnExecutor normalizer — while the mint policy per channel is
|
|
28
|
+
# supplied through Channel#mint_call_id. Both channels receive fresh
|
|
29
|
+
# normalized {"id","tool","args"} hashes; the llm/ layer's dumb
|
|
30
|
+
# call_#{rand} fallbacks stay there: llm must never require
|
|
31
|
+
# orchestrator (layering).
|
|
32
|
+
module ToolCallId
|
|
33
|
+
module_function
|
|
34
|
+
|
|
35
|
+
# mint(3, 2) -> "call_3_2"
|
|
36
|
+
# mint("subagent-2", 5, 3) -> "call_subagent-2_5_3"
|
|
37
|
+
# mint(4, unique: true) -> "call_4_4821"
|
|
38
|
+
# mint(3, 2, unique: true) -> "call_3_2_4821"
|
|
39
|
+
# Byte-compatible with every pre-extraction spelling.
|
|
40
|
+
def mint(*parts, unique: false)
|
|
41
|
+
id = "call_#{parts.join('_')}"
|
|
42
|
+
id = "#{id}_#{rand(1000..9999)}" if unique
|
|
43
|
+
id
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "monitor"
|
|
4
|
+
|
|
5
|
+
# Opt-in failure isolation for tool execution (G19): after `threshold`
|
|
6
|
+
# consecutive failures of the same tool, short-circuit further calls for a
|
|
7
|
+
# cooldown instead of burning agent steps on a wedged backend. Disabled by
|
|
8
|
+
# default — synchronous execution is the agent loop's design; this only
|
|
9
|
+
# stops repeated futile calls once the operator turns it on.
|
|
10
|
+
module Rubik
|
|
11
|
+
module Orchestrator
|
|
12
|
+
class ToolCircuitBreaker
|
|
13
|
+
# Raised by the StepExecutor when a call is refused; the executor's
|
|
14
|
+
# normal rescue converts it into a failed tool result so the loop
|
|
15
|
+
# plumbing (events, history) is reused untouched.
|
|
16
|
+
class CircuitOpenError < StandardError; end
|
|
17
|
+
|
|
18
|
+
STATES = [:closed, :open, :half_open].freeze
|
|
19
|
+
|
|
20
|
+
def initialize(threshold: 5, cooldown_seconds: 60.0)
|
|
21
|
+
@threshold = threshold
|
|
22
|
+
@cooldown_seconds = cooldown_seconds.to_f
|
|
23
|
+
@monitor = Monitor.new
|
|
24
|
+
@tools = {} # name -> { failures:, opened_at:, probing: }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def state(tool_name)
|
|
28
|
+
@monitor.synchronize { circuit(tool_name)[:state] }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def allow?(tool_name)
|
|
32
|
+
@monitor.synchronize do
|
|
33
|
+
circuit = circuit(tool_name)
|
|
34
|
+
case circuit[:state]
|
|
35
|
+
when :open
|
|
36
|
+
return false unless cooldown_elapsed?(circuit)
|
|
37
|
+
|
|
38
|
+
# one probe through the half-open gate
|
|
39
|
+
circuit[:state] = :half_open
|
|
40
|
+
circuit[:probing] = true
|
|
41
|
+
true
|
|
42
|
+
when :half_open
|
|
43
|
+
if circuit[:probing]
|
|
44
|
+
true
|
|
45
|
+
else
|
|
46
|
+
cooldown_elapsed?
|
|
47
|
+
end
|
|
48
|
+
else
|
|
49
|
+
true
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def record_success(tool_name)
|
|
55
|
+
@monitor.synchronize do
|
|
56
|
+
@tools.delete(tool_name)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def record_failure(tool_name)
|
|
61
|
+
@monitor.synchronize do
|
|
62
|
+
circuit = circuit(tool_name)
|
|
63
|
+
if circuit[:state] == :half_open
|
|
64
|
+
# probe failed: reopen for another full cooldown
|
|
65
|
+
circuit[:opened_at] = now
|
|
66
|
+
circuit[:probing] = false
|
|
67
|
+
circuit[:state] = :open
|
|
68
|
+
else
|
|
69
|
+
circuit[:failures] += 1
|
|
70
|
+
circuit[:state] = :open if circuit[:failures] >= @threshold
|
|
71
|
+
circuit[:opened_at] = now if circuit[:state] == :open && circuit[:opened_at].nil?
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def circuit(tool_name)
|
|
79
|
+
@tools[tool_name] ||= { failures: 0, opened_at: nil, probing: false, state: :closed }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def cooldown_elapsed?(circuit)
|
|
83
|
+
circuit[:opened_at] && (now - circuit[:opened_at]) >= @cooldown_seconds
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def now
|
|
87
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubik
|
|
4
|
+
module Orchestrator
|
|
5
|
+
# THE canonical tool-result history row — the dictionary single source
|
|
6
|
+
# of truth (agent-turn-executor plan, Phase 1). Every channel's writer
|
|
7
|
+
# (StepExecutor registered/invalid, MaestroGate refusal, subagent
|
|
8
|
+
# dispatch) constructs its row through ToolHistory.write, so key drift
|
|
9
|
+
# between channels is structurally impossible.
|
|
10
|
+
#
|
|
11
|
+
# Provider-consumption evidence (formatter.rb et al.):
|
|
12
|
+
# - tool_call_id — required by OpenAI native function calling
|
|
13
|
+
# (formatter.rb, extract_call_id) and Anthropic tool_use pairing;
|
|
14
|
+
# also read by TokenEstimator.
|
|
15
|
+
# - tool_name — OpenAI non-native and Ollama prefixed-user-message
|
|
16
|
+
# paths, TokenEstimator, ContextCompactor skill-tool filter,
|
|
17
|
+
# TranscriptFormatter.
|
|
18
|
+
# - status / duration_ms — read by NO wire-side consumer; observability
|
|
19
|
+
# only (tests, REPL events, stored transcripts). Formatters build
|
|
20
|
+
# fresh hashes and ignore unknown keys, so carrying them everywhere is
|
|
21
|
+
# wire-safe for every provider path.
|
|
22
|
+
# - content — an opaque String per channel (main path: Result#to_xml;
|
|
23
|
+
# subagent: truncated plain output). The dictionary unifies the row
|
|
24
|
+
# SHAPE, not the content encoding.
|
|
25
|
+
module ToolHistory
|
|
26
|
+
ROW_KEYS = [:role, :tool_call_id, :tool_name, :status, :duration_ms, :content].freeze
|
|
27
|
+
|
|
28
|
+
module_function
|
|
29
|
+
|
|
30
|
+
# Appends exactly one canonical row to +history+ and returns it.
|
|
31
|
+
def write(history, tool_call_id:, tool_name:, status:, duration_ms:, content:)
|
|
32
|
+
row = { role: "tool", tool_call_id: tool_call_id, tool_name: tool_name,
|
|
33
|
+
status: status, duration_ms: duration_ms, content: content }
|
|
34
|
+
history << row
|
|
35
|
+
row
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../tool_call_id"
|
|
4
|
+
|
|
5
|
+
module Rubik
|
|
6
|
+
module Orchestrator
|
|
7
|
+
class TurnExecutor
|
|
8
|
+
# The subagent-channel adapter: wraps the Subagent::Instance. Its
|
|
9
|
+
# methods run on the worker thread only, between monitor sections —
|
|
10
|
+
# the channel never holds @monitor and never emits on the bus under
|
|
11
|
+
# it (the same discipline as the StepRunner methods it replaces).
|
|
12
|
+
class SubagentChannel < Channel
|
|
13
|
+
def initialize(instance)
|
|
14
|
+
super()
|
|
15
|
+
@inst = instance
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
attr_reader :inst
|
|
19
|
+
|
|
20
|
+
# ── required state ──
|
|
21
|
+
def history = @inst.history
|
|
22
|
+
|
|
23
|
+
def tool_registry = @inst.tool_registry
|
|
24
|
+
|
|
25
|
+
def limits
|
|
26
|
+
{ context_limit: @inst.context_limit, max_output_tokens: @inst.max_output_tokens, config: @inst.config }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Subagent policy (ToolCallId doctrine): deterministic
|
|
30
|
+
# (subagent id, step, index) — the history lives and dies with the
|
|
31
|
+
# run, and greppable ids beat collision-proof ones here.
|
|
32
|
+
def mint_call_id(idx)
|
|
33
|
+
ToolCallId.mint(@inst.id, @inst.current_step, idx + 1)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# ── response shaping ──
|
|
37
|
+
# Moved verbatim from StepRunner#extract_response_content: Struct
|
|
38
|
+
# mock providers expose #content, real responses expose #text.
|
|
39
|
+
def response_text(res)
|
|
40
|
+
return res.text.to_s if res.respond_to?(:text)
|
|
41
|
+
return res.content.to_s if res.respond_to?(:content)
|
|
42
|
+
|
|
43
|
+
res.to_s
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Struct/duck mock providers expose the array attribute without the
|
|
47
|
+
# Response predicate — read the attribute directly.
|
|
48
|
+
def raw_tool_calls(res)
|
|
49
|
+
res.respond_to?(:tool_calls) ? res.tool_calls : []
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Deferred: Instance carries no total_tokens state.
|
|
53
|
+
def track_usage(_res) = nil
|
|
54
|
+
|
|
55
|
+
# Deferred: subagent continuation is future work.
|
|
56
|
+
def handle_truncation(_res, _text, _tool_calls, _finish_reason) = nil
|
|
57
|
+
|
|
58
|
+
# ── announce ──
|
|
59
|
+
def announce_compaction(reason, layer)
|
|
60
|
+
@inst.emit_compacted_event(reason, layer)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def announce_llm_thought(_text) = nil
|
|
64
|
+
|
|
65
|
+
# ── terminals & dispatch ──
|
|
66
|
+
# The shared two-strike nudge policy (Channel#empty_response_turn)
|
|
67
|
+
# now governs empty, tool-less responses: one standard nudge row,
|
|
68
|
+
# then the terminal below on the second consecutive strike. The
|
|
69
|
+
# subagent can no longer "complete" with an empty result — the
|
|
70
|
+
# declared Phase 4 delta.
|
|
71
|
+
def empty_response_failure
|
|
72
|
+
@inst.fail_turn!("Empty response from model (2 consecutive)")
|
|
73
|
+
{ ok: false, completed: false }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def final_text_result(final_text)
|
|
77
|
+
@inst.history << { role: "assistant", content: final_text.empty? ? nil : final_text, tool_calls: nil }
|
|
78
|
+
@inst.complete_turn!(final_text)
|
|
79
|
+
{ ok: true, completed: true }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def append_tool_call_row(final_text, tool_calls)
|
|
83
|
+
@inst.history << { role: "assistant", content: final_text.empty? ? nil : final_text, tool_calls: tool_calls }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def on_finish(args, fallback_text)
|
|
87
|
+
message = args["message"] || args[:message] || fallback_text || "Completed"
|
|
88
|
+
@inst.auto_apply_buffer!(:finish) unless @inst.workspace == "branch"
|
|
89
|
+
@inst.complete_turn!(message)
|
|
90
|
+
{ ok: true, completed: true }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def execute_tool_call(tool_call, text)
|
|
94
|
+
return on_finish(tool_call["args"] || {}, text) if TurnExecutor.finish_call?(@inst.tool_registry, tool_call)
|
|
95
|
+
|
|
96
|
+
@inst.execute_subagent_tool_call(tool_call)
|
|
97
|
+
nil
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../shell_deps"
|
|
4
|
+
require_relative "../tool_call_id"
|
|
5
|
+
require_relative "../tool_history"
|
|
6
|
+
|
|
7
|
+
module Rubik
|
|
8
|
+
module Orchestrator
|
|
9
|
+
class TurnExecutor
|
|
10
|
+
# The user-channel adapter: wraps the Loop through the ShellDeps
|
|
11
|
+
# façade, so bare Class#allocate-built test loops keep working
|
|
12
|
+
# (the StepExecutor/BudgetManager/StreamManager precedent). Owns the
|
|
13
|
+
# user channel's response shaping: continuation stitching, the
|
|
14
|
+
# truncation/auto-continue flow, thought-aware nudges, step events,
|
|
15
|
+
# and the turn_result contract.
|
|
16
|
+
class UserChannel < Channel
|
|
17
|
+
def initialize(loop_or_deps)
|
|
18
|
+
super()
|
|
19
|
+
@deps = ShellDeps.wrap(loop_or_deps)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
attr_reader :deps
|
|
23
|
+
|
|
24
|
+
# ── required state ──
|
|
25
|
+
def history = @deps.history
|
|
26
|
+
|
|
27
|
+
def tool_registry = @deps.tool_registry
|
|
28
|
+
|
|
29
|
+
# Observation projection directive (plan D): yalnız goal-mode user
|
|
30
|
+
# kanalında çözülür; directive_for önkoşul matrisi (knob'lar,
|
|
31
|
+
# coeffect, rev ≥ 1) sağlanmazsa nil döner → compaction byte-özdeş.
|
|
32
|
+
def absorb_directive
|
|
33
|
+
return nil unless @deps.goal_mode?
|
|
34
|
+
|
|
35
|
+
Rubik::Orchestrator::GoalLedger::ObservationProjection.directive_for(deps: @deps)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def limits
|
|
39
|
+
{ context_limit: @deps.context_limit, max_output_tokens: @deps.max_output_tokens, config: @deps.config }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Loop policy (ToolCallId doctrine): unique-by-suffix ids — the
|
|
43
|
+
# session history survives across turns while step_count restarts.
|
|
44
|
+
def mint_call_id(idx)
|
|
45
|
+
ToolCallId.mint(@deps.step_count, idx + 1, unique: true)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# ── response shaping ──
|
|
49
|
+
# Moved stitch_response: a buffered continuation turn prepends the
|
|
50
|
+
# accumulated partial text and resets the buffer.
|
|
51
|
+
def final_text(res, text)
|
|
52
|
+
cm = @deps.loop_instance.continuation_manager
|
|
53
|
+
return text if cm.continuation_buffer.empty?
|
|
54
|
+
|
|
55
|
+
cm.stitch_and_reset(res.text.to_s)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def track_usage(res) = @deps.budget_manager.track_token_usage!(res)
|
|
59
|
+
|
|
60
|
+
# Moved truncation/auto-continue flow (Plan 90): classify the
|
|
61
|
+
# truncation, append the partial thought/text rows plus the
|
|
62
|
+
# targeted nudge, or return the diagnostic terminal when
|
|
63
|
+
# auto-continue is disabled or the ceiling is exhausted.
|
|
64
|
+
def handle_truncation(res, _text_resp, tool_calls, finish_reason)
|
|
65
|
+
cm = @deps.loop_instance.continuation_manager
|
|
66
|
+
return nil unless cm.truncation?(finish_reason)
|
|
67
|
+
|
|
68
|
+
trunc_action = cm.handle_truncation(
|
|
69
|
+
finish_reason: finish_reason,
|
|
70
|
+
text_resp: res.text.to_s,
|
|
71
|
+
tool_calls: tool_calls,
|
|
72
|
+
res: res,
|
|
73
|
+
max_tokens: @deps.max_output_tokens || @deps.safe_config("llm.default_max_output_tokens", 4096)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
if trunc_action[:continue]
|
|
77
|
+
nudge_policy.reset!
|
|
78
|
+
if trunc_action[:thought]
|
|
79
|
+
@deps.history << { role: "assistant", content: nil, thought: trunc_action[:thought] }
|
|
80
|
+
elsif trunc_action[:partial_text]
|
|
81
|
+
@deps.history << { role: "assistant", content: trunc_action[:partial_text] }
|
|
82
|
+
end
|
|
83
|
+
@deps.history << { role: "user", content: trunc_action[:nudge] }
|
|
84
|
+
@deps.on_step&.call(trunc_action[:step_event].merge(step: @deps.step_count, max_steps: @deps.max_steps)) if trunc_action[:step_event]
|
|
85
|
+
return :consumed
|
|
86
|
+
end
|
|
87
|
+
return nil unless trunc_action[:error]
|
|
88
|
+
|
|
89
|
+
@deps.on_step&.call(type: :error, content: trunc_action[:error], step: @deps.step_count, max_steps: @deps.max_steps)
|
|
90
|
+
@deps.loop_instance.turn_result(success: false, error: trunc_action[:error])
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# ── nudge texts ──
|
|
94
|
+
# Plan 61 D-11 thought-aware variant: reasoning spent the budget
|
|
95
|
+
# without an action deserves a different sentence than a blank reply.
|
|
96
|
+
def empty_nudge_message(res)
|
|
97
|
+
if res.respond_to?(:thought) && res.thought && !res.thought.to_s.strip.empty?
|
|
98
|
+
"Your reasoning was completed, but no action or final response was provided. Please call a tool or provide your final output."
|
|
99
|
+
else
|
|
100
|
+
super
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# ── announce ──
|
|
105
|
+
def announce_compaction(reason, layer)
|
|
106
|
+
@deps.on_step&.call(type: :emergency_compact, reason: reason, step: @deps.step_count,
|
|
107
|
+
max_steps: @deps.max_steps, layer: layer)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def announce_llm_thought(text)
|
|
111
|
+
@deps.on_step&.call(type: :llm_thought, content: text, step: @deps.step_count, max_steps: @deps.max_steps)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# ── terminals & dispatch ──
|
|
115
|
+
def empty_response_failure
|
|
116
|
+
@deps.loop_instance.turn_result(success: false, error: "Empty response from model")
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def final_text_result(final_text)
|
|
120
|
+
@deps.history << { role: "assistant", content: final_text }
|
|
121
|
+
@deps.on_step&.call(type: :message, content: final_text, step: @deps.step_count, max_steps: @deps.max_steps)
|
|
122
|
+
return goal_wait_yield(via: "text") if goal_guard_pending?
|
|
123
|
+
|
|
124
|
+
@deps.loop_instance.turn_result(success: true, output: final_text)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def append_tool_call_row(final_text, tool_calls)
|
|
128
|
+
@deps.history << { role: "assistant", content: final_text.empty? ? nil : final_text, tool_calls: tool_calls }
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def on_finish(args, fallback_text)
|
|
132
|
+
@deps.loop_instance.step_executor.handle_finish_tool(args, fallback_text)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def execute_tool_call(tool_call, text)
|
|
136
|
+
# Guard BEFORE dispatch: a finish over live subagents must not
|
|
137
|
+
# reach handle_finish_tool (its auto-apply and :finish event
|
|
138
|
+
# would celebrate a completion the pipeline has not reached).
|
|
139
|
+
# The deferred call still gets its canonical result row — the
|
|
140
|
+
# loop continues, so the next provider request must see the
|
|
141
|
+
# tool_use paired (strict providers 400 on orphaned ids).
|
|
142
|
+
if TurnExecutor.finish_call?(@deps.tool_registry, tool_call) && goal_guard_pending?
|
|
143
|
+
ToolHistory.write(@deps.history,
|
|
144
|
+
tool_call_id: tool_call["id"],
|
|
145
|
+
tool_name: "finish",
|
|
146
|
+
status: :ok,
|
|
147
|
+
duration_ms: 0.0,
|
|
148
|
+
content: "finish deferred: dispatched subagents still active — the goal stays open")
|
|
149
|
+
return goal_wait_yield(via: "finish")
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
@deps.loop_instance.step_executor.execute_single_tool_call(tool_call, text)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
private
|
|
156
|
+
|
|
157
|
+
# Goal-mode premature-completion guard (GoalWait): in goal mode a
|
|
158
|
+
# success terminal while dispatched subagents are still live is a
|
|
159
|
+
# false "Goal Achieved" (field incident 2026-10-05) — route it
|
|
160
|
+
# through the implicit yield-and-wait, which returns nil so the
|
|
161
|
+
# step loop continues. Interactive/default mode never reaches it.
|
|
162
|
+
def goal_guard_pending?
|
|
163
|
+
@deps.goal_mode? && @deps.loop_instance.goal_wait.pending?
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def goal_wait_yield(via:)
|
|
167
|
+
@deps.loop_instance.goal_wait.premature_yield!(via: via)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|