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,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "notices"
|
|
4
|
+
|
|
5
|
+
module Rubik
|
|
6
|
+
module Orchestrator
|
|
7
|
+
module Subagent
|
|
8
|
+
# THE render seam: (lifecycle state, facts) -> text, one direction,
|
|
9
|
+
# no reverse writes. Pure — no locks, no IO, no instance state; the
|
|
10
|
+
# only require is the notices vocabulary leaf. Every state->text
|
|
11
|
+
# spelling for an Instance's own lifecycle (state_note, the
|
|
12
|
+
# manage_subagents status/wait guidance) and the manage_subagents
|
|
13
|
+
# action vocabulary (schema enum, dispatch keys, help list, error
|
|
14
|
+
# text) projects from here, so the drift between them is
|
|
15
|
+
# unexpressable: the session-review bug class (B3 stale placeholder,
|
|
16
|
+
# B4 grants-blind halt wording) existed because narration was
|
|
17
|
+
# hand-STORED beside state instead of derived from it.
|
|
18
|
+
module Narration
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
# --- instance-state narration (the only home of state->text) ---
|
|
22
|
+
|
|
23
|
+
# The pause/halt story for a lifecycle state, computed on demand.
|
|
24
|
+
# state is an Instance::TRANSITIONS key; silent states (live or
|
|
25
|
+
# settled runs) yield nil — :completed narrates via its real
|
|
26
|
+
# :result, plain :failed via :last_error, so nothing is invented.
|
|
27
|
+
# paused_after_s is the recorded wait fact (Instance#pause_facts)
|
|
28
|
+
# feeding the halt wording's "within Ns" clause.
|
|
29
|
+
def state_note(state, grants_received: 0, max_steps: nil, last_error: nil, paused_after_s: nil)
|
|
30
|
+
case state.to_sym
|
|
31
|
+
when :waiting_budget
|
|
32
|
+
"Paused awaiting budget expansion from the maestro."
|
|
33
|
+
when :budget_exhausted
|
|
34
|
+
if grants_received.positive?
|
|
35
|
+
# B4: after a consumed grant the halt must say no FURTHER
|
|
36
|
+
# expansion arrived — "not received" read as a lost grant.
|
|
37
|
+
"#{Notices.budget_exhausted_line(max_steps)} No further budget expansion arrived " \
|
|
38
|
+
"within #{paused_after_s ? "#{paused_after_s}s" : 'the wait timeout'} " \
|
|
39
|
+
"(#{grants_received} grant(s) were received and consumed earlier)."
|
|
40
|
+
else
|
|
41
|
+
"#{Notices.budget_exhausted_line(max_steps)} Budget expansion was not received from the maestro."
|
|
42
|
+
end
|
|
43
|
+
when :paused_error
|
|
44
|
+
"Paused awaiting error recovery: #{last_error}"
|
|
45
|
+
when :waiting_input
|
|
46
|
+
"Asked the maestro a clarifying question; blocked awaiting the answer."
|
|
47
|
+
when :failed
|
|
48
|
+
"Subagent halted after #{paused_after_s}s in paused_error state: #{last_error}" if paused_after_s
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# manage_subagents status/wait channel: blocked states get the exact
|
|
53
|
+
# recovery action, a mid-flight worker gets the poll discipline. All
|
|
54
|
+
# other states yield nil (no story).
|
|
55
|
+
def guidance(state, id:, max_steps:, step: nil, uptime: nil)
|
|
56
|
+
case state.to_sym
|
|
57
|
+
when :waiting_input
|
|
58
|
+
"Subagent asked a question and is BLOCKED awaiting your answer. Reply via send_message(recipient: '#{id}', message: '<your answer>')."
|
|
59
|
+
when :waiting_budget
|
|
60
|
+
Notices.budget_paused_line(id, max_steps)
|
|
61
|
+
when :budget_exhausted
|
|
62
|
+
Notices.budget_exhausted_line(max_steps)
|
|
63
|
+
when :paused_error
|
|
64
|
+
"Subagent paused on provider/network error. Call manage_subagents(action: 'resume', conversation_id: '#{id}') to retry."
|
|
65
|
+
when :running
|
|
66
|
+
# The poll-discipline story needs the live facts; callers without
|
|
67
|
+
# them (wait rows, plain state dispatch) get nil = no story —
|
|
68
|
+
# this guidance channel was budget-states-only before the seam.
|
|
69
|
+
if step && uptime
|
|
70
|
+
"Subagent is actively working (step #{step}/#{max_steps}, #{uptime.round(1)}s uptime). " \
|
|
71
|
+
"DO NOT poll 'status' in a loop! Call #{Notices.wait_any_hint} to block until completion, " \
|
|
72
|
+
"or simply stop calling tools to receive reactive wakeup."
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# The nil-provider fast path's terminal stub (a legitimate outcome:
|
|
78
|
+
# no provider, no story beyond the echo of the task itself).
|
|
79
|
+
def completed_stub_line(id, role, prompt)
|
|
80
|
+
"Subagent [#{id}] (#{role}) completed task successfully: #{prompt.slice(0, 80)}"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# --- manage_subagents action vocabulary (single declaration) ---
|
|
84
|
+
|
|
85
|
+
# One canonical row per action; model-facing aliases ride along.
|
|
86
|
+
# The schema enum, the dispatch canonicalization, the description's
|
|
87
|
+
# action list, and the unknown-action error are ALL projections of
|
|
88
|
+
# this table — adding an action means adding a row, nothing else.
|
|
89
|
+
Action = Struct.new(:canonical, :aliases, :param, keyword_init: true)
|
|
90
|
+
|
|
91
|
+
ACTION_SPECS = [
|
|
92
|
+
Action.new(canonical: "list", aliases: []),
|
|
93
|
+
Action.new(canonical: "status", aliases: [], param: "conversation_id"),
|
|
94
|
+
Action.new(canonical: "wait", aliases: [], param: "conversation_ids"),
|
|
95
|
+
Action.new(canonical: "transcript", aliases: [], param: "conversation_id"),
|
|
96
|
+
Action.new(canonical: "extend_budget", aliases: ["grant_steps", "extend"], param: "conversation_id"),
|
|
97
|
+
Action.new(canonical: "resume", aliases: ["retry"], param: "conversation_id"),
|
|
98
|
+
Action.new(canonical: "redeploy", aliases: [], param: "conversation_id"),
|
|
99
|
+
Action.new(canonical: "reap", aliases: ["clean"]),
|
|
100
|
+
Action.new(canonical: "kill", aliases: [], param: "conversation_ids"),
|
|
101
|
+
Action.new(canonical: "kill_all", aliases: ["killall"])
|
|
102
|
+
].freeze
|
|
103
|
+
|
|
104
|
+
CANONICAL_NAMES = ACTION_SPECS.map(&:canonical).freeze
|
|
105
|
+
|
|
106
|
+
# Model-facing vocabulary: canonical names plus their aliases (the
|
|
107
|
+
# schema enum previously drifted from dispatch by missing "killall").
|
|
108
|
+
SCHEMA_ACTIONS = ACTION_SPECS.flat_map { |spec| [spec.canonical, *spec.aliases] }.freeze
|
|
109
|
+
|
|
110
|
+
CANONICAL_LOOKUP = ACTION_SPECS.flat_map { |spec| [spec.canonical, *spec.aliases].map { |token| [token, spec.canonical] } }.to_h.freeze
|
|
111
|
+
|
|
112
|
+
# Alias-resolving dispatch key; nil = unknown action.
|
|
113
|
+
def canonical_action(token)
|
|
114
|
+
token && CANONICAL_LOOKUP[token.to_s.downcase.strip]
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Rendered canonical list for the tool description (order-stable, so
|
|
118
|
+
# TOOLS.md generation stays byte-identical).
|
|
119
|
+
def quoted_canonical_list
|
|
120
|
+
CANONICAL_NAMES.map { |name| "'#{name}'" }.join(", ")
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../platform"
|
|
4
|
+
require_relative "budget"
|
|
5
|
+
|
|
6
|
+
module Rubik
|
|
7
|
+
module Orchestrator
|
|
8
|
+
module Subagent
|
|
9
|
+
# THE single vocabulary home for subagent notices: event names and
|
|
10
|
+
# payloads enter, two channel renderings leave (user channel: REPL
|
|
11
|
+
# hints allowed; model channel: tool-oriented).
|
|
12
|
+
module Notices
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
# Bus event names
|
|
16
|
+
COMPLETED_EVENT = :subagent_completed
|
|
17
|
+
BUDGET_EXHAUSTED_EVENT = :subagent_budget_exhausted
|
|
18
|
+
COMPACTED_EVENT = :subagent_context_compacted
|
|
19
|
+
AUTO_APPLY_FAILED_EVENT = :subagent_auto_apply_failed
|
|
20
|
+
ERROR_PAUSED_EVENT = :subagent_error_paused
|
|
21
|
+
QUESTION_EVENT = :subagent_question
|
|
22
|
+
MESSAGE_EVENT = :subagent_message_to_parent
|
|
23
|
+
|
|
24
|
+
# Bus event name -> normalized notice kind.
|
|
25
|
+
EVENTS = {
|
|
26
|
+
COMPLETED_EVENT => :completed,
|
|
27
|
+
BUDGET_EXHAUSTED_EVENT => :budget_exhausted,
|
|
28
|
+
COMPACTED_EVENT => :context_compacted,
|
|
29
|
+
AUTO_APPLY_FAILED_EVENT => :auto_apply_failed,
|
|
30
|
+
ERROR_PAUSED_EVENT => :error_paused,
|
|
31
|
+
QUESTION_EVENT => :question
|
|
32
|
+
}.freeze
|
|
33
|
+
|
|
34
|
+
# Sessizlik (§4.3): bu roller notice ÜRETMEZ, yalnız dmesg'e düşer (Distiller + Context Pruner).
|
|
35
|
+
SILENT_ROLES = ["Vakanüvis Distiller", "Context Pruner"].freeze
|
|
36
|
+
|
|
37
|
+
# Dmesg-plane emission metadata (bug-llm-timeout-observability): every
|
|
38
|
+
# subagent-plane event lands in the ring under subsystem :subagent with
|
|
39
|
+
# an operational-weight severity, so dmesg(level:/subsystem:) surfaces
|
|
40
|
+
# a stalled subagent. MESSAGE_EVENT routes here too (a :message notice
|
|
41
|
+
# via handle_parent_message, not normalize).
|
|
42
|
+
SUBSYSTEM = :subagent
|
|
43
|
+
|
|
44
|
+
SEVERITIES = {
|
|
45
|
+
COMPLETED_EVENT => :info,
|
|
46
|
+
BUDGET_EXHAUSTED_EVENT => :warn,
|
|
47
|
+
COMPACTED_EVENT => :info,
|
|
48
|
+
AUTO_APPLY_FAILED_EVENT => :warn,
|
|
49
|
+
ERROR_PAUSED_EVENT => :err,
|
|
50
|
+
QUESTION_EVENT => :notice,
|
|
51
|
+
MESSAGE_EVENT => :info
|
|
52
|
+
}.freeze
|
|
53
|
+
|
|
54
|
+
# The one emission door: severity and subsystem are decided HERE,
|
|
55
|
+
# never re-derived at call sites. Nil bus = the old `&.emit` skip;
|
|
56
|
+
# a bus lacking #emit still raises loud; a legacy 2-arity emit gets
|
|
57
|
+
# a plain 2-arg call (severity metadata best-effort, delivery
|
|
58
|
+
# load-bearing). `fallback:` flips a raise into a stderr warn (the
|
|
59
|
+
# AutoApply.report_failure pattern) for load-bearing event sites.
|
|
60
|
+
def emit(bus, event_name, payload, fallback: nil)
|
|
61
|
+
return if bus.nil?
|
|
62
|
+
|
|
63
|
+
emit_it = lambda {
|
|
64
|
+
m = bus.method(:emit)
|
|
65
|
+
kw = { subsystem: SUBSYSTEM, level: SEVERITIES.fetch(event_name, :info) }
|
|
66
|
+
if m.parameters.any? { |p| [:key, :keyrest].include?(p.first) }
|
|
67
|
+
m.call(event_name, payload, **kw)
|
|
68
|
+
else
|
|
69
|
+
m.call(event_name, payload)
|
|
70
|
+
end
|
|
71
|
+
}
|
|
72
|
+
emit_it.call
|
|
73
|
+
rescue StandardError => e
|
|
74
|
+
raise if fallback.nil?
|
|
75
|
+
|
|
76
|
+
warn("#{fallback} (#{e.class}: #{e.message})")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def normalize(event_name, payload)
|
|
80
|
+
return nil if SILENT_ROLES.include?(payload[:role].to_s)
|
|
81
|
+
|
|
82
|
+
kind = EVENTS[event_name.to_sym]
|
|
83
|
+
return nil unless kind
|
|
84
|
+
|
|
85
|
+
notice = { kind: kind, id: payload[:id].to_s, role: payload[:role].to_s,
|
|
86
|
+
state: payload[:state].to_s, max_steps: payload[:max_steps] }
|
|
87
|
+
raw_err = payload[:error] || payload[:reason]
|
|
88
|
+
# Q-timeout marker rides AFTER the cap (#97): truncation can never eat it.
|
|
89
|
+
if kind == :question && notice[:state] == "question_timed_out"
|
|
90
|
+
notice[:reason] = "#{raw_err.to_s.slice(0, 230)} [timed out unanswered after #{payload[:timeout]}s — proceeding on its own judgment]"
|
|
91
|
+
elsif [:context_compacted, :auto_apply_failed, :error_paused, :question].include?(kind)
|
|
92
|
+
notice[:reason] = raw_err.to_s.slice(0, kind == :question ? 300 : 120)
|
|
93
|
+
end
|
|
94
|
+
notice[:timeout] = payload[:timeout] if kind == :question
|
|
95
|
+
# B11: grant/step markers make repeated pauses distinguishable.
|
|
96
|
+
if kind == :budget_exhausted
|
|
97
|
+
notice[:grants] = payload[:grants].to_i if payload[:grants].to_i.positive?
|
|
98
|
+
notice[:step] = payload[:step].to_i if payload[:step].to_i.positive?
|
|
99
|
+
end
|
|
100
|
+
notice
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def user_line(notice)
|
|
104
|
+
case notice[:kind]
|
|
105
|
+
when :context_compacted
|
|
106
|
+
detail = notice[:reason].to_s.empty? ? "" : " (#{notice[:reason]})"
|
|
107
|
+
"🧹 [Subagent Compacted] [#{notice[:id]}] '#{notice[:role]}' recovered from a provider context-limit error by compacting its own history; work continues#{detail}."
|
|
108
|
+
when :auto_apply_failed
|
|
109
|
+
"⚠️ [Subagent Auto-Apply] [#{notice[:id]}] '#{notice[:role]}' could not auto-apply its buffer (#{notice[:reason]}). Changes remain unapplied."
|
|
110
|
+
when :budget_exhausted
|
|
111
|
+
grants = notice[:grants].to_i
|
|
112
|
+
step = notice[:step]
|
|
113
|
+
cyc = " again (after #{grants} grant(s) consumed#{", at step #{step}" if step})" if grants.positive?
|
|
114
|
+
"⚠️ [Subagent Paused] [#{notice[:id]}] '#{notice[:role]}' reached step budget#{cyc} " \
|
|
115
|
+
"(#{notice[:max_steps]} steps). Run '!subagents extend #{notice[:id]}' to resume."
|
|
116
|
+
when :error_paused
|
|
117
|
+
"⚠️ [Subagent Paused] [#{notice[:id]}] '#{notice[:role]}' paused due to provider/network error (#{notice[:reason]}). Run '!subagents resume #{notice[:id]}' to retry."
|
|
118
|
+
when :message
|
|
119
|
+
"💬 [Message from #{notice[:id]}] #{notice[:reason]}"
|
|
120
|
+
when :question
|
|
121
|
+
badge = notice[:state] == "question_timed_out" ? "⌛ [Question timed out] [#{notice[:id]}]" : "❓ [Question from #{notice[:id]}]"
|
|
122
|
+
"#{badge} '#{notice[:role]}': #{notice[:reason]}"
|
|
123
|
+
when :task
|
|
124
|
+
badge = notice[:state] == "completed" ? "⚡ [Task Done]" : "❌ [Task Failed]"
|
|
125
|
+
"#{badge} [#{notice[:id]}] - #{notice[:state].to_s.upcase}"
|
|
126
|
+
else
|
|
127
|
+
case notice[:state]
|
|
128
|
+
when "budget_exhausted"
|
|
129
|
+
"🛑 [Subagent Budget Exhausted] [#{notice[:id]}] '#{notice[:role]}' - BUDGET_EXHAUSTED (task incomplete)"
|
|
130
|
+
when "failed"
|
|
131
|
+
"❌ [Subagent Failed] [#{notice[:id]}] '#{notice[:role]}' - FAILED. Call manage_subagents(action: 'status', conversation_id: '#{notice[:id]}') for the error."
|
|
132
|
+
when "completed"
|
|
133
|
+
"⚡ [Subagent Completed] [#{notice[:id]}] '#{notice[:role]}' - COMPLETED"
|
|
134
|
+
else
|
|
135
|
+
"🏁 [Subagent Finished] [#{notice[:id]}] '#{notice[:role]}' - #{notice[:state].to_s.upcase}"
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def model_element(notice)
|
|
141
|
+
attrs = { kind: notice[:kind], id: notice[:id], role: notice[:role], state: notice[:state] }
|
|
142
|
+
attrs[:max_steps] = notice[:max_steps] if notice[:kind] == :budget_exhausted
|
|
143
|
+
reason_kinds = [:context_compacted, :auto_apply_failed, :error_paused, :message, :task, :question]
|
|
144
|
+
attrs[:reason] = notice[:reason] if reason_kinds.include?(notice[:kind]) && !notice[:reason].to_s.empty?
|
|
145
|
+
attrs[:timeout] = notice[:timeout] if notice[:kind] == :question && notice[:timeout]
|
|
146
|
+
attrs[:guidance] = guidance_for(notice)
|
|
147
|
+
rendered = attrs.map { |k, v| %(#{k}="#{Rubik::Platform.xml_escape(v)}") }.join(" ")
|
|
148
|
+
"<notification #{rendered} />"
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def history_block(notices)
|
|
152
|
+
elements = notices.map { |n| " #{model_element(n)}" }.join("\n")
|
|
153
|
+
"<subagent_notifications>\n#{elements}\n</subagent_notifications>"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Event-channel guidance (notice-kind keyed); instance-state narration lives in Narration.
|
|
157
|
+
def guidance_for(notice)
|
|
158
|
+
case notice[:kind]
|
|
159
|
+
when :task
|
|
160
|
+
"Inspect task output via manage_task(action: 'logs', task_id: '#{notice[:id]}')."
|
|
161
|
+
when :context_compacted
|
|
162
|
+
"No action needed: the subagent recovered automatically; its history was compacted, so early details may be summarized."
|
|
163
|
+
when :auto_apply_failed
|
|
164
|
+
"Auto-apply failed; changes remain unapplied. Apply via the apply_buffer tool."
|
|
165
|
+
when :budget_exhausted
|
|
166
|
+
"Resume via #{extend_budget_hint(notice[:id])} or kill it with action: 'kill'."
|
|
167
|
+
when :error_paused
|
|
168
|
+
"Subagent paused on network error. Resume via #{resume_hint(notice[:id])} or kill with action: 'kill'."
|
|
169
|
+
when :message
|
|
170
|
+
"Message from subagent. Respond or grant steps via send_message(recipient: '#{notice[:id]}', message: '...')."
|
|
171
|
+
when :question
|
|
172
|
+
answered = "No answer is needed anymore: it timed out and the subagent proceeded on its own judgment. " \
|
|
173
|
+
"A late send_message(recipient: '#{notice[:id]}') lands as a regular message."
|
|
174
|
+
blocking = "Answer NOW via send_message(recipient: '#{notice[:id]}', message: '<your answer>') — " \
|
|
175
|
+
"the subagent is blocked waiting (#{notice[:timeout] || 300}s timeout)."
|
|
176
|
+
notice[:state] == "question_timed_out" ? answered : blocking
|
|
177
|
+
else
|
|
178
|
+
if notice[:state] == "budget_exhausted"
|
|
179
|
+
budget_exhausted_line(notice[:max_steps])
|
|
180
|
+
else
|
|
181
|
+
"Retrieve the full result with #{wait_hint(notice[:id])}."
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def budget_paused_line(id, max_steps) = "Subagent reached step budget (#{max_steps} steps) and paused. Call #{extend_budget_hint(id)} to resume."
|
|
187
|
+
|
|
188
|
+
def budget_exhausted_line(max_steps = nil)
|
|
189
|
+
count = max_steps ? " (#{max_steps} steps)" : ""
|
|
190
|
+
"Subagent reached step budget#{count} and halted: task was NOT completed. Inspect transcript or spawn a new subagent."
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
DEFAULT_WAIT_TIMEOUT = 30
|
|
194
|
+
|
|
195
|
+
def render_ids(ids) = "[#{Array(ids).map { |id| "'#{id}'" }.join(', ')}]"
|
|
196
|
+
|
|
197
|
+
def wait_hint(ids, timeout: DEFAULT_WAIT_TIMEOUT) = "manage_subagents(action: 'wait', conversation_ids: #{render_ids(ids)}, timeout: #{timeout})"
|
|
198
|
+
|
|
199
|
+
def wait_any_hint(timeout: DEFAULT_WAIT_TIMEOUT) = "manage_subagents(action: 'wait', timeout: #{timeout})"
|
|
200
|
+
|
|
201
|
+
def wait_hint_template = "manage_subagents(action: 'wait', conversation_ids: [...], timeout: #{DEFAULT_WAIT_TIMEOUT})"
|
|
202
|
+
|
|
203
|
+
def extend_budget_hint(id, steps: Budget::DEFAULT_GRANT_STEPS) = "manage_subagents(action: 'extend_budget', conversation_id: '#{id}', steps: #{steps})"
|
|
204
|
+
|
|
205
|
+
def resume_hint(id) = "manage_subagents(action: 'resume', conversation_id: '#{id}')"
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubik
|
|
4
|
+
module Orchestrator
|
|
5
|
+
module Subagent
|
|
6
|
+
# The ask-maestro bridge for the ask_question tool's subagent plane.
|
|
7
|
+
# Flips the instance to :waiting_input, announces the question on the
|
|
8
|
+
# event bus (REASON carries the full display text incl. options —
|
|
9
|
+
# Notices needs no question-specific plumbing), then polls the shared
|
|
10
|
+
# inbox for the maestro's send_message reply. The worker thread is the ONLY
|
|
11
|
+
# inbox drainer (drain_inbox_into_history runs between steps) and it is
|
|
12
|
+
# blocked inside the tool here, so the pop cannot race a drain.
|
|
13
|
+
# Polling (not a cond var) follows the Supervisor#wait_for precedent:
|
|
14
|
+
# kill hard-kills the thread, so perfect signaling is not required.
|
|
15
|
+
module Questioner
|
|
16
|
+
DEFAULT_QUESTION_TIMEOUT = 300
|
|
17
|
+
|
|
18
|
+
# Returns the answer String, :unavailable (no bus / wrong state —
|
|
19
|
+
# nobody can ever answer, so blocking would be wrong), or nil when
|
|
20
|
+
# unanswered (timeout or kill).
|
|
21
|
+
def ask_init0(question_text, options: [], timeout: nil)
|
|
22
|
+
return :unavailable unless @event_bus
|
|
23
|
+
|
|
24
|
+
# Drain pending steers into history FIRST (same discipline as the
|
|
25
|
+
# step top): a pre-question inbox message was NOT sent to a
|
|
26
|
+
# question-waiting subagent and must not pose as its answer.
|
|
27
|
+
drain_inbox_into_history
|
|
28
|
+
reason = Array(options).empty? ? question_text.to_s : "#{question_text} (options: #{Array(options).join(' / ')})"
|
|
29
|
+
payload = { id: @id, role: @role, state: :waiting_input,
|
|
30
|
+
reason: reason, timeout: question_timeout(timeout) }
|
|
31
|
+
@monitor.synchronize do
|
|
32
|
+
return :unavailable unless executing?
|
|
33
|
+
|
|
34
|
+
transition!(:waiting_input)
|
|
35
|
+
@unit&.sync_state!
|
|
36
|
+
end
|
|
37
|
+
answer = nil
|
|
38
|
+
begin
|
|
39
|
+
# Emit OUTSIDE the monitor (budget-pause discipline): synchronous
|
|
40
|
+
# bus dispatch finishes before the poll starts, so the answer can
|
|
41
|
+
# never land before we begin waiting for it.
|
|
42
|
+
Notices.emit(@event_bus, Notices::QUESTION_EVENT, payload)
|
|
43
|
+
deadline = Time.now + payload[:timeout]
|
|
44
|
+
until answer
|
|
45
|
+
@monitor.synchronize { answer = @inbox.pop(true)&.text unless @inbox.empty? }
|
|
46
|
+
break if Time.now >= deadline || !(awaiting_human? || executing?)
|
|
47
|
+
|
|
48
|
+
sleep 0.1
|
|
49
|
+
end
|
|
50
|
+
ensure
|
|
51
|
+
# Guarded restore (also covers a raise or Thread#kill mid-wait):
|
|
52
|
+
# grant_budget! or kill may have legitimately moved the state on.
|
|
53
|
+
@monitor.synchronize do
|
|
54
|
+
transition!(:running) if awaiting_human?
|
|
55
|
+
@unit&.sync_state!
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
if answer.nil? && Time.now >= deadline
|
|
59
|
+
# The subagent-side timeout escalation ("proceed with your best
|
|
60
|
+
# judgment") is invisible to the maestro — it woke nobody. The
|
|
61
|
+
# timed-out notice re-surfaces the dead-end on the next wake so a
|
|
62
|
+
# late answer becomes a conscious choice, not a shot into a void
|
|
63
|
+
# the asker already left (issue #97 finding 4). The human-readable
|
|
64
|
+
# marker is appended by Notices.normalize AFTER its reason cap, so
|
|
65
|
+
# it can never be truncated away; here the state alone carries it.
|
|
66
|
+
Notices.emit(@event_bus, Notices::QUESTION_EVENT, payload.merge(state: :question_timed_out))
|
|
67
|
+
end
|
|
68
|
+
answer
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def question_timeout(override = nil)
|
|
72
|
+
return override.to_f if override&.to_f&.positive?
|
|
73
|
+
|
|
74
|
+
(@config.respond_to?(:fetch) ? @config.fetch("subagents.question_timeout", DEFAULT_QUESTION_TIMEOUT) : DEFAULT_QUESTION_TIMEOUT) ||
|
|
75
|
+
DEFAULT_QUESTION_TIMEOUT
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../platform"
|
|
4
|
+
|
|
5
|
+
module Rubik
|
|
6
|
+
module Orchestrator
|
|
7
|
+
module Subagent
|
|
8
|
+
# Özenli tasfiye (diligent reaping): işi biten subagent'lar grace
|
|
9
|
+
# süresi sonunda mezar taşına (tombstone) taşınır — bellekteki
|
|
10
|
+
# history serbest bırakılır, dayanıklı transcript ve storage indeksi
|
|
11
|
+
# kalır, status sorgulanabilir. :budget_exhausted / :paused_error
|
|
12
|
+
# ASLA reap edilmez: thread canlıdır (condvar park), resume hakları
|
|
13
|
+
# bozulmaz.
|
|
14
|
+
module Reaper
|
|
15
|
+
def archived? = !!@archived
|
|
16
|
+
|
|
17
|
+
# History yalnız transcript diske DURABLY yazıldıysa silinir —
|
|
18
|
+
# dosya yoksa reap sonraki süpürmeye kalır (yarım tasfiye yok).
|
|
19
|
+
def archive!
|
|
20
|
+
@monitor.synchronize do
|
|
21
|
+
return false unless File.exist?(@transcript_uri.to_s)
|
|
22
|
+
|
|
23
|
+
@history.replace([])
|
|
24
|
+
@archived = true
|
|
25
|
+
true
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Supervisor düzlemi: tombstone haritası + otomatik/force süpürme.
|
|
31
|
+
# Tombstone'lar get/list üzerinden sorgulanabilir kalır; wait_for
|
|
32
|
+
# bilinçli olarak yalnız @instances'e bakar (reap edilen zaten
|
|
33
|
+
# notice ile duyurulmuş, sonucu transcriptte).
|
|
34
|
+
module ReaperSupervisor
|
|
35
|
+
def reap_finished!(force: false)
|
|
36
|
+
dropped_steering = []
|
|
37
|
+
reaped = @monitor.synchronize do
|
|
38
|
+
grace = reap_grace_seconds
|
|
39
|
+
reaped = []
|
|
40
|
+
@instances.each do |id, inst|
|
|
41
|
+
next unless inst.respond_to?(:terminal_and_settled?) && inst.terminal_and_settled?
|
|
42
|
+
next unless force || (grace && inst.ended_at && Time.now - inst.ended_at >= grace)
|
|
43
|
+
next unless inst.archive!
|
|
44
|
+
|
|
45
|
+
dropped_steering << id if inst.respond_to?(:pending_redeploy_messages?) && inst.pending_redeploy_messages?
|
|
46
|
+
(@finished ||= {})[id] = inst
|
|
47
|
+
reaped << id
|
|
48
|
+
end
|
|
49
|
+
reaped.each { |id| @instances.delete(id) }
|
|
50
|
+
evict_overflow!
|
|
51
|
+
reaped
|
|
52
|
+
end
|
|
53
|
+
# OUTSIDE the supervisor monitor (emit/logging discipline): queued
|
|
54
|
+
# steering that died with the instance instead of a redeploy — the
|
|
55
|
+
# send_message :queued reply discloses this, but the loss must be
|
|
56
|
+
# observable (issue #55 3.2).
|
|
57
|
+
Platform.debug_warn("[Reaper] reaped with undelivered queued steering: #{dropped_steering.join(', ')}") unless dropped_steering.empty?
|
|
58
|
+
reaped
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
# reap_orphans gövdesinden çağrılır (handle_subagent_completion'ın
|
|
64
|
+
# hazır kancası) — disposed kabukları at, sonra grace dolanları
|
|
65
|
+
# tombstone'la. Tamamen sessiz bir oturumda SON biten subagent
|
|
66
|
+
# hiçbir tamamlanma tetikleyicisi bulamaz: sınırlı kalıntı, force
|
|
67
|
+
# ile elle reap edilebilir.
|
|
68
|
+
def sweep_disposed_and_finished
|
|
69
|
+
@instances.delete_if { |_k, v| v.disposed? }
|
|
70
|
+
reap_finished!
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def reap_grace_seconds
|
|
74
|
+
raw = @config.respond_to?(:fetch) ? @config.fetch("subagents.reap_finished_after", 300) : 300
|
|
75
|
+
raw = 300 if raw.nil?
|
|
76
|
+
raw.to_f <= 0 ? nil : raw.to_f
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def tombstone_limit
|
|
80
|
+
raw = @config.respond_to?(:fetch) ? @config.fetch("subagents.tombstone_limit", 25) : 25
|
|
81
|
+
raw = 25 if raw.nil?
|
|
82
|
+
raw.to_i
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# FIFO taşma: en eski mezar taşı evict edilir ve cleanup ile
|
|
86
|
+
# :disposed'a çekilir (ctx zaten dispose'lu; cleanup no-op güvenli).
|
|
87
|
+
def evict_overflow!
|
|
88
|
+
return unless @finished
|
|
89
|
+
|
|
90
|
+
while @finished.size > tombstone_limit
|
|
91
|
+
_id, inst = @finished.shift
|
|
92
|
+
inst&.cleanup
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubik
|
|
4
|
+
module Orchestrator
|
|
5
|
+
module Subagent
|
|
6
|
+
# Transient network/provider error detection, backoff retries, and
|
|
7
|
+
# pause-on-error coordination for autonomous subagents (Plan 101).
|
|
8
|
+
module Resilience
|
|
9
|
+
DEFAULT_NETWORK_RETRIES = 3
|
|
10
|
+
DEFAULT_BACKOFF_BASE = 0.5
|
|
11
|
+
DEFAULT_ERROR_WAIT_TIMEOUT = 300
|
|
12
|
+
|
|
13
|
+
TRANSIENT_PATTERN = Regexp.union(
|
|
14
|
+
/5\d{2}|server_error|engine[-_]core error|rate[-_]limit|too many requests/i,
|
|
15
|
+
/read timeout|timeout|closed stream|closed socket/i,
|
|
16
|
+
/connection reset|connection refused|broken pipe/i
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
def transient_error?(err)
|
|
22
|
+
return false unless err.is_a?(StandardError)
|
|
23
|
+
|
|
24
|
+
msg = err.message.to_s
|
|
25
|
+
return true if msg.match?(TRANSIENT_PATTERN)
|
|
26
|
+
return true if defined?(Rubik::LLM::RateLimitError) && err.is_a?(Rubik::LLM::RateLimitError)
|
|
27
|
+
return true if defined?(Rubik::LLM::ClientError) && err.is_a?(Rubik::LLM::ClientError) && msg.match?(TRANSIENT_PATTERN)
|
|
28
|
+
|
|
29
|
+
transient_exception_class?(err)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def transient_exception_class?(err)
|
|
33
|
+
name = err.class.name.to_s
|
|
34
|
+
name.include?("Timeout") ||
|
|
35
|
+
name.include?("Socket") ||
|
|
36
|
+
name.include?("ECONNRESET") ||
|
|
37
|
+
name.include?("ECONNREFUSED") ||
|
|
38
|
+
name.include?("EPIPE") ||
|
|
39
|
+
name.include?("EOFError")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def retry_transient(config: nil, gate: nil, max_retries: nil, backoff_base: nil, on_retry: nil, &block)
|
|
43
|
+
retries_limit = max_retries ||
|
|
44
|
+
(config.respond_to?(:fetch) && config.fetch("subagents.network_retry_count", nil)) ||
|
|
45
|
+
DEFAULT_NETWORK_RETRIES
|
|
46
|
+
base_delay = backoff_base ||
|
|
47
|
+
(config.respond_to?(:fetch) && config.fetch("subagents.network_backoff_base", nil)) ||
|
|
48
|
+
DEFAULT_BACKOFF_BASE
|
|
49
|
+
|
|
50
|
+
attempt = 0
|
|
51
|
+
begin
|
|
52
|
+
attempt += 1
|
|
53
|
+
if gate
|
|
54
|
+
gate.synchronize(&block)
|
|
55
|
+
else
|
|
56
|
+
block.call
|
|
57
|
+
end
|
|
58
|
+
rescue StandardError => e
|
|
59
|
+
raise e unless attempt <= retries_limit.to_i && transient_error?(e)
|
|
60
|
+
|
|
61
|
+
delay = base_delay.to_f * (2**(attempt - 1)) * (0.8 + (0.4 * rand))
|
|
62
|
+
on_retry&.call(attempt, retries_limit.to_i, delay, e)
|
|
63
|
+
sleep(delay)
|
|
64
|
+
retry
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|