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,376 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "../platform"
|
|
5
|
+
require_relative "tool_call_id"
|
|
6
|
+
require_relative "tool_history"
|
|
7
|
+
require_relative "provider_recovery"
|
|
8
|
+
|
|
9
|
+
module Rubik
|
|
10
|
+
module Orchestrator
|
|
11
|
+
# THE shared response-processing spine for both agent channels (the
|
|
12
|
+
# agent-turn-executor plan): Loop (user channel) and Subagent::StepRunner
|
|
13
|
+
# (subagent channel) become thin parameterizations of this executor —
|
|
14
|
+
# neither carries its own copy of "one provider response → history
|
|
15
|
+
# mutations → (nudge | final text | tool dispatch) → turn decision".
|
|
16
|
+
#
|
|
17
|
+
# Stateless-per-call: one instance per channel, built lazily by its
|
|
18
|
+
# owner (the provider_resolver precedent keeps Class#allocate-built
|
|
19
|
+
# test doubles working). Channel policy lives in the adapters (Channel
|
|
20
|
+
# base + UserChannel + SubagentChannel); request assembly stays
|
|
21
|
+
# channel-native on purpose — stream vs batch and their recovery stacks
|
|
22
|
+
# genuinely differ, so the channel hands the executor the raw response
|
|
23
|
+
# and ProviderRecovery keeps wrapping the provider CALL at its three
|
|
24
|
+
# sites. The compaction actuator it drives (emergency_compact!) has
|
|
25
|
+
# exactly one home: here.
|
|
26
|
+
class TurnExecutor
|
|
27
|
+
class << self
|
|
28
|
+
# JSON-text tool-call fallback (Plan 61): a non-tool-native model's
|
|
29
|
+
# JSON reply parses to one tool call. Degenerate model text — a
|
|
30
|
+
# bare "```json" fence, an empty fence, or a valid-JSON non-object
|
|
31
|
+
# — degrades to nil (no tool call; the text is treated as output)
|
|
32
|
+
# instead of raising into the step loop: fallback text is
|
|
33
|
+
# model-controlled, so the same bounded policy as parse_json_args
|
|
34
|
+
# applies.
|
|
35
|
+
def parse_tool_call(text)
|
|
36
|
+
cleaned = text.to_s.gsub("\\n", "\n")
|
|
37
|
+
json_str = if cleaned.include?("```json")
|
|
38
|
+
cleaned.split("```json").last.to_s.split("```").first.to_s.strip
|
|
39
|
+
elsif cleaned.start_with?("{") && cleaned.end_with?("}")
|
|
40
|
+
cleaned
|
|
41
|
+
end
|
|
42
|
+
return nil if json_str.nil? || json_str.empty?
|
|
43
|
+
|
|
44
|
+
parsed = JSON.parse(json_str)
|
|
45
|
+
parsed.is_a?(Hash) ? parsed : nil
|
|
46
|
+
rescue JSON::ParserError
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def extract_finish_reason(res)
|
|
51
|
+
return nil unless res.respond_to?(:raw) && res.raw.is_a?(Hash)
|
|
52
|
+
|
|
53
|
+
raw = res.raw
|
|
54
|
+
raw["finish_reason"] || raw.dig("choices", 0, "finish_reason") ||
|
|
55
|
+
raw["stop_reason"] || raw.dig("delta", "stop_reason") || raw["done_reason"]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Registry-canonical finish detection: a call is the finish call when
|
|
59
|
+
# the registry resolves it to the finish tool — an unregistered case
|
|
60
|
+
# variant ("Finish") downcases to finish instead of dispatching an
|
|
61
|
+
# unknown tool. A nil registry degrades to the downcase compare.
|
|
62
|
+
def finish_call?(tool_registry, tool_call)
|
|
63
|
+
t_name = tool_call["tool"] || tool_call[:tool]
|
|
64
|
+
resolved = tool_registry.respond_to?(:get) ? tool_registry.get(t_name) : nil
|
|
65
|
+
(resolved ? resolved.name : t_name.to_s.downcase) == "finish"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def initialize(channel:)
|
|
70
|
+
@channel = channel
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
attr_reader :channel
|
|
74
|
+
|
|
75
|
+
# Channel-owned counter (Loop's public empty_response_count reader
|
|
76
|
+
# delegates here).
|
|
77
|
+
def nudge_policy = channel.nudge_policy
|
|
78
|
+
|
|
79
|
+
# Per-run reset of channel-owned counters (empty-response strikes).
|
|
80
|
+
def reset!
|
|
81
|
+
channel.nudge_policy.reset!
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# THE seam. Processes one provider response: normalize tool calls
|
|
85
|
+
# (native + JSON fallback, channel-minted ids), truncation hook,
|
|
86
|
+
# plan-mode branch, empty-nudge, assistant row, finish detection,
|
|
87
|
+
# tool-dispatch loop.
|
|
88
|
+
#
|
|
89
|
+
# Returns nil to continue the step loop, or the terminal Hash (the
|
|
90
|
+
# channel's own result contract) to stop it.
|
|
91
|
+
def call(res, plan_only: false)
|
|
92
|
+
channel.track_usage(res)
|
|
93
|
+
|
|
94
|
+
# safe_utf8 before strip: degraded providers can echo invalid byte
|
|
95
|
+
# sequences (inside JSON string values JSON.parse would even accept
|
|
96
|
+
# them) — strip regexes the text and would kill the turn on the
|
|
97
|
+
# spot. Model-controlled text is sanitized at this one boundary.
|
|
98
|
+
text_resp = Platform.safe_utf8(channel.response_text(res)).to_s.strip
|
|
99
|
+
tool_calls = normalize_tool_calls(res, text_resp)
|
|
100
|
+
|
|
101
|
+
handled = channel.handle_truncation(res, text_resp, tool_calls, self.class.extract_finish_reason(res).to_s)
|
|
102
|
+
return nil if handled == :consumed
|
|
103
|
+
return handled if handled.is_a?(Hash)
|
|
104
|
+
|
|
105
|
+
return plan_turn(res, text_resp) if plan_only
|
|
106
|
+
return text_turn(res, text_resp) if tool_calls.empty?
|
|
107
|
+
|
|
108
|
+
tool_turn(res, text_resp, tool_calls)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Emergency compaction, ONE home for both channels: the compaction
|
|
112
|
+
# math stays in Prompt::EmergencyCompactor.compact!, the announce is
|
|
113
|
+
# channel-parameterized. Returns the layer that produced the final
|
|
114
|
+
# history (the Loop seam's documented contract).
|
|
115
|
+
def emergency_compact!(reason)
|
|
116
|
+
limits = channel.limits
|
|
117
|
+
layer = Rubik::Prompt::EmergencyCompactor.compact!(
|
|
118
|
+
channel.history, context_limit: limits[:context_limit], max_output_tokens: limits[:max_output_tokens],
|
|
119
|
+
config: ProviderRecovery.compaction_config(limits[:config]),
|
|
120
|
+
absorb: channel.absorb_directive
|
|
121
|
+
)
|
|
122
|
+
channel.announce_compaction(reason, layer)
|
|
123
|
+
layer
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Capped at the same 200 chars the streaming diagnostics path
|
|
127
|
+
# (:malformed_tool_args) shows the UI.
|
|
128
|
+
MALFORMED_ARGS_FRAGMENT_CAP = 200
|
|
129
|
+
|
|
130
|
+
private
|
|
131
|
+
|
|
132
|
+
# One shared normalizer for both channels (wire-identical: every
|
|
133
|
+
# provider path already emits string-keyed {"id","tool","args"} —
|
|
134
|
+
# ChunkAssembler and the three batch providers). Builds fresh
|
|
135
|
+
# normalized hashes with a channel-minted id per call; the JSON-text
|
|
136
|
+
# fallback (Plan 61) parses a non-native model's JSON reply into one
|
|
137
|
+
# call.
|
|
138
|
+
def normalize_tool_calls(res, text_resp)
|
|
139
|
+
raw_calls = channel.raw_tool_calls(res).to_a
|
|
140
|
+
if raw_calls.empty?
|
|
141
|
+
parsed = self.class.parse_tool_call(text_resp)
|
|
142
|
+
raw_calls = parsed ? [parsed] : []
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
raw_calls.map.with_index do |tc, idx|
|
|
146
|
+
id = tc["id"] || tc[:id] || channel.mint_call_id(idx)
|
|
147
|
+
name = tc["tool"] || tc[:tool] || tc["name"] || tc[:name]
|
|
148
|
+
normalize_one_tool_call(tc, id, name)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Undecodable args (unparsable JSON string, valid non-object, native
|
|
153
|
+
# non-Hash payload) are NOT dispatched as {}: the row carries
|
|
154
|
+
# "malformed_args" and tool_turn writes a canonical error row quoting
|
|
155
|
+
# the rejected payload (issue #55 3.4) instead of producing a
|
|
156
|
+
# downstream missing-parameter or guard error the model cannot
|
|
157
|
+
# connect to the real cause. The marker may also arrive pre-attached
|
|
158
|
+
# from a provider-layer parse (chunk_assembler, openai_compatible,
|
|
159
|
+
# ollama degrade sites). Absent/nil args stay a legal {} dispatch
|
|
160
|
+
# (tools without parameters).
|
|
161
|
+
def normalize_one_tool_call(call, id, name)
|
|
162
|
+
raw_args = call["args"] || call[:args] || call["arguments"] || call[:arguments]
|
|
163
|
+
provider_fragment = call["malformed_args"] || call[:malformed_args]
|
|
164
|
+
row = { "id" => id, "tool" => name }
|
|
165
|
+
case raw_args
|
|
166
|
+
when String
|
|
167
|
+
parsed = parse_json_args(raw_args)
|
|
168
|
+
if parsed.is_a?(Hash)
|
|
169
|
+
row["args"] = parsed
|
|
170
|
+
else
|
|
171
|
+
row["args"] = {}
|
|
172
|
+
row["malformed_args"] = malformed_args_fragment(raw_args)
|
|
173
|
+
end
|
|
174
|
+
when Hash
|
|
175
|
+
row["args"] = raw_args
|
|
176
|
+
when nil
|
|
177
|
+
row["args"] = {}
|
|
178
|
+
else
|
|
179
|
+
row["args"] = {}
|
|
180
|
+
row["malformed_args"] = malformed_args_fragment(raw_args.to_s)
|
|
181
|
+
end
|
|
182
|
+
row["malformed_args"] = malformed_args_fragment(provider_fragment) if provider_fragment && !row["malformed_args"]
|
|
183
|
+
row
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def malformed_args_fragment(raw)
|
|
187
|
+
# safe_utf8 first: raw model-controlled args can carry invalid byte
|
|
188
|
+
# sequences (JSON.parse tolerates them inside string values), and
|
|
189
|
+
# gsub with a regex source raises ArgumentError on those — the one
|
|
190
|
+
# raise this bounded-error path must never produce.
|
|
191
|
+
fragment = Platform.safe_utf8(raw).to_s.gsub(/\s+/, " ").strip
|
|
192
|
+
fragment.length > MALFORMED_ARGS_FRAGMENT_CAP ? "#{fragment[0, MALFORMED_ARGS_FRAGMENT_CAP]}..." : fragment
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def parse_json_args(raw)
|
|
196
|
+
JSON.parse(raw)
|
|
197
|
+
rescue JSON::ParserError
|
|
198
|
+
nil
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def plan_turn(res, text_resp)
|
|
202
|
+
return channel.empty_response_turn(res, channel.plan_nudge_message) if text_resp.empty?
|
|
203
|
+
|
|
204
|
+
final_text_turn(res, text_resp)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def text_turn(res, text_resp)
|
|
208
|
+
return channel.empty_response_turn(res) if text_resp.empty?
|
|
209
|
+
|
|
210
|
+
final_text_turn(res, text_resp)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def final_text_turn(res, text_resp)
|
|
214
|
+
channel.nudge_policy.reset!
|
|
215
|
+
channel.final_text_result(channel.final_text(res, text_resp))
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def tool_turn(res, text_resp, tool_calls)
|
|
219
|
+
channel.nudge_policy.reset!
|
|
220
|
+
channel.append_tool_call_row(channel.final_text(res, text_resp), tool_calls)
|
|
221
|
+
channel.announce_llm_thought(text_resp) if !text_resp.empty? && tool_calls.none? { |tc| self.class.finish_call?(channel.tool_registry, tc) }
|
|
222
|
+
|
|
223
|
+
tool_calls.each do |tc|
|
|
224
|
+
# finish is exempt (pinned by test_orchestrator_stream): its args
|
|
225
|
+
# are optional and the fallback text carries the message, so a
|
|
226
|
+
# malformed finish must still complete the turn gracefully instead
|
|
227
|
+
# of error-rowing the completion signal into a retry cycle.
|
|
228
|
+
if tc["malformed_args"] && !self.class.finish_call?(channel.tool_registry, tc)
|
|
229
|
+
channel.malformed_args_row(tc)
|
|
230
|
+
next
|
|
231
|
+
end
|
|
232
|
+
result = channel.execute_tool_call(tc, text_resp)
|
|
233
|
+
return result if result
|
|
234
|
+
end
|
|
235
|
+
nil
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# The per-channel policy adapter. Base class carries the shared
|
|
239
|
+
# mechanics with no-op defaults where a channel legitimately has
|
|
240
|
+
# nothing to do; channel-specific terminals raise
|
|
241
|
+
# NotImplementedError — a missing terminal is a construction bug,
|
|
242
|
+
# not a runtime condition (fail loud at the boundary).
|
|
243
|
+
class Channel
|
|
244
|
+
# ── nudge policy (channel-owned so the owner's accessor delegates) ──
|
|
245
|
+
def nudge_policy
|
|
246
|
+
@nudge_policy ||= NudgePolicy.new
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Default empty-response policy: the shared two-strike nudge. One
|
|
250
|
+
# nudge row per empty, tool-less response; terminal failure on the
|
|
251
|
+
# second consecutive strike.
|
|
252
|
+
def empty_response_turn(res, nudge_message = empty_nudge_message(res))
|
|
253
|
+
count = nudge_policy.register_empty!
|
|
254
|
+
return empty_response_failure if nudge_policy.strike?(count)
|
|
255
|
+
|
|
256
|
+
history << { role: "user", content: nudge_message }
|
|
257
|
+
nil
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# ── required state ──
|
|
261
|
+
def history = raise NotImplementedError, "channel must expose the mutable conversation history"
|
|
262
|
+
|
|
263
|
+
def tool_registry = nil
|
|
264
|
+
|
|
265
|
+
def limits
|
|
266
|
+
raise NotImplementedError, "channel must expose { context_limit:, max_output_tokens:, config: }"
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def mint_call_id(_idx)
|
|
270
|
+
raise NotImplementedError,
|
|
271
|
+
"channel must mint ToolCallId ids (unique vs deterministic is per-channel doctrine)"
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# ── response shaping ──
|
|
275
|
+
# The channel's native tool-call source from a raw provider
|
|
276
|
+
# response. Main: the Response#tool_calls? predicate gates access;
|
|
277
|
+
# the subagent's Struct/duck mock providers expose the array
|
|
278
|
+
# attribute directly, so that channel overrides this hook.
|
|
279
|
+
def raw_tool_calls(res)
|
|
280
|
+
res.respond_to?(:tool_calls?) && res.tool_calls? ? res.tool_calls : []
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def response_text(res) = res.text.to_s
|
|
284
|
+
|
|
285
|
+
def final_text(_res, text) = text
|
|
286
|
+
|
|
287
|
+
def track_usage(_res) = nil
|
|
288
|
+
|
|
289
|
+
# -> nil (not applicable) | :consumed (rows+nudge appended; executor
|
|
290
|
+
# continues) | Hash (terminal error).
|
|
291
|
+
def handle_truncation(_res, _text, _tool_calls, _finish_reason) = nil
|
|
292
|
+
|
|
293
|
+
# ── nudge texts ──
|
|
294
|
+
def empty_nudge_message(_res) = "Your last response was empty. Continue the task or call the finish tool."
|
|
295
|
+
|
|
296
|
+
def plan_nudge_message
|
|
297
|
+
"Plan mode: tool calls are disabled. Respond with the numbered implementation plan as plain text."
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# ── announce ──
|
|
301
|
+
def announce_compaction(_reason, _layer) = nil
|
|
302
|
+
|
|
303
|
+
def announce_llm_thought(_text) = nil
|
|
304
|
+
|
|
305
|
+
# Goal-ledger observation projection (plan D): related-optional
|
|
306
|
+
# capability — base channel is structurally inert (subagent planes
|
|
307
|
+
# never absorb); UserChannel overrides.
|
|
308
|
+
def absorb_directive = nil
|
|
309
|
+
|
|
310
|
+
# ── terminals & dispatch ──
|
|
311
|
+
def empty_response_failure
|
|
312
|
+
raise NotImplementedError, "channel must build its terminal empty-response failure Hash"
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def final_text_result(_final_text)
|
|
316
|
+
raise NotImplementedError, "channel must build its text-completion terminal Hash"
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def append_tool_call_row(_final_text, _tool_calls)
|
|
320
|
+
raise NotImplementedError, "channel must append the assistant tool-call row"
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def on_finish(_args, _fallback_text)
|
|
324
|
+
raise NotImplementedError, "channel must implement its finish semantics"
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def execute_tool_call(_tool_call, _text)
|
|
328
|
+
raise NotImplementedError, "channel must dispatch tool calls"
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Canonical error row for a tool call whose String args failed to
|
|
332
|
+
# parse (issue #55 3.4): the model sees its own rejected payload,
|
|
333
|
+
# the tool_call_id stays paired (strict providers 400 on orphaned
|
|
334
|
+
# ids), and the tool never dispatches with degraded {} args.
|
|
335
|
+
def malformed_args_row(tool_call)
|
|
336
|
+
ToolHistory.write(history,
|
|
337
|
+
tool_call_id: tool_call["id"],
|
|
338
|
+
tool_name: tool_call["tool"].to_s,
|
|
339
|
+
status: :error,
|
|
340
|
+
duration_ms: 0.0,
|
|
341
|
+
content: "Malformed tool call: arguments were not parsable as a JSON object, so the " \
|
|
342
|
+
"tool was NOT executed. Rejected payload (whitespace-collapsed, first " \
|
|
343
|
+
"#{MALFORMED_ARGS_FRAGMENT_CAP} chars): #{tool_call['malformed_args']}. " \
|
|
344
|
+
"Re-issue the call with complete, valid JSON arguments.")
|
|
345
|
+
nil
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# Two-strike empty-response counter, channel-owned so the owner
|
|
350
|
+
# object (Loop) can delegate its public reader.
|
|
351
|
+
class NudgePolicy
|
|
352
|
+
DEFAULT_STRIKE_LIMIT = 2
|
|
353
|
+
|
|
354
|
+
attr_reader :strike_limit, :count
|
|
355
|
+
|
|
356
|
+
def initialize(strike_limit = DEFAULT_STRIKE_LIMIT)
|
|
357
|
+
@strike_limit = strike_limit
|
|
358
|
+
@count = 0
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# Registers one empty response and returns the running count.
|
|
362
|
+
def register_empty!
|
|
363
|
+
@count += 1
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def reset!
|
|
367
|
+
@count = 0
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def strike?(count = @count)
|
|
371
|
+
count >= @strike_limit
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "orchestrator/loop"
|
|
4
|
+
require_relative "orchestrator/goal_state"
|
|
5
|
+
require_relative "orchestrator/goal_ledger"
|
|
6
|
+
require_relative "orchestrator/subagent/supervisor"
|
|
7
|
+
|
|
8
|
+
module Rubik
|
|
9
|
+
module Orchestrator
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubik
|
|
4
|
+
module Platform
|
|
5
|
+
# File system operations (atomic writes, newline normalization).
|
|
6
|
+
# Extracted from lib/rubik/platform.rb (Plan 86 D-4).
|
|
7
|
+
module FileSystem
|
|
8
|
+
def self.included(base)
|
|
9
|
+
base.extend self
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def atomic_write(target_path, content)
|
|
13
|
+
dir = File.dirname(target_path)
|
|
14
|
+
FileUtils.mkdir_p(dir) unless File.directory?(dir)
|
|
15
|
+
tmp_file = "#{target_path}.tmp.#{::Process.pid}.#{rand(1_000_000)}"
|
|
16
|
+
|
|
17
|
+
File.open(tmp_file, "wb") do |f|
|
|
18
|
+
f.write(content)
|
|
19
|
+
f.flush
|
|
20
|
+
begin
|
|
21
|
+
f.fsync
|
|
22
|
+
rescue StandardError, ScriptError
|
|
23
|
+
nil
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Atomic replace: rename tmp_file -> target_path without unlinking first
|
|
28
|
+
# (closing TOCTOU). Retry for Windows transient locks.
|
|
29
|
+
retries = 3
|
|
30
|
+
begin
|
|
31
|
+
File.rename(tmp_file, target_path)
|
|
32
|
+
rescue SystemCallError => e
|
|
33
|
+
if retries.positive?
|
|
34
|
+
retries -= 1
|
|
35
|
+
sleep 0.02
|
|
36
|
+
retry
|
|
37
|
+
end
|
|
38
|
+
debug_warn("atomic_write fallback for #{target_path}: #{e.class}: #{e.message}")
|
|
39
|
+
File.binwrite(target_path, content)
|
|
40
|
+
ensure
|
|
41
|
+
begin
|
|
42
|
+
FileUtils.rm_f(tmp_file)
|
|
43
|
+
rescue StandardError, ScriptError
|
|
44
|
+
nil
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def normalize_newlines(content)
|
|
50
|
+
content.to_s.gsub("\r\n", "\n")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# UTF-8 read path for project files: tags the bytes as UTF-8 regardless
|
|
54
|
+
# of the OS default external encoding (Windows codepages must not leak
|
|
55
|
+
# into Rubik's pipeline) and scrubs invalid sequences so the model,
|
|
56
|
+
# history, and regex consumers never receive broken encoding. All
|
|
57
|
+
# buffer hash computations share this path, so a broken-encoding file
|
|
58
|
+
# shifts its digest once and then stays stable.
|
|
59
|
+
def read_text(path)
|
|
60
|
+
Rubik::Platform.safe_utf8(File.read(path, encoding: "UTF-8"))
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubik
|
|
4
|
+
module Platform
|
|
5
|
+
# Continuously drains a child-process IO (stderr) so a chatty child
|
|
6
|
+
# never fills the OS pipe buffer (~64KB) and deadlocks writing
|
|
7
|
+
# diagnostics while the parent waits on stdout. Discards content; the
|
|
8
|
+
# optional on_line observer is a future debug-forwarding hook.
|
|
9
|
+
# Lifecycle contract mirrors the existing reader threads (MCP stdio,
|
|
10
|
+
# LSP stdout): kill + bounded join, never raises from #stop.
|
|
11
|
+
class IoDrain
|
|
12
|
+
JOIN_TIMEOUT = 0.5
|
|
13
|
+
|
|
14
|
+
def self.start(io, on_line: nil)
|
|
15
|
+
new(io, on_line: on_line).tap(&:start)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def initialize(io, on_line: nil)
|
|
19
|
+
@io = io
|
|
20
|
+
@on_line = on_line
|
|
21
|
+
@thread = nil
|
|
22
|
+
@stopped = false
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def start
|
|
26
|
+
@stopped = false
|
|
27
|
+
@thread = Thread.new do
|
|
28
|
+
while (line = @io.gets)
|
|
29
|
+
@on_line&.call(line)
|
|
30
|
+
end
|
|
31
|
+
rescue IOError, Errno::EBADF, Errno::EPIPE => e
|
|
32
|
+
# Expected while #stop closes the pipes underneath us. Any other
|
|
33
|
+
# time it is the watchdog dying while the child may still flood
|
|
34
|
+
# the pipe — the exact deadlock this class exists to prevent —
|
|
35
|
+
# so it must leave a trace.
|
|
36
|
+
warn "[rubik] stderr drain died unexpectedly (#{e.class}: #{e.message})" unless @stopped
|
|
37
|
+
end
|
|
38
|
+
self
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def stop
|
|
42
|
+
@stopped = true
|
|
43
|
+
thread = @thread
|
|
44
|
+
@thread = nil
|
|
45
|
+
return unless thread
|
|
46
|
+
|
|
47
|
+
thread.kill
|
|
48
|
+
thread.join(JOIN_TIMEOUT)
|
|
49
|
+
nil
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|