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,384 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "monitor"
|
|
4
|
+
require_relative "transcript_formatter"
|
|
5
|
+
require_relative "storage_id"
|
|
6
|
+
require_relative "lifecycle"
|
|
7
|
+
require_relative "step_runner"
|
|
8
|
+
require_relative "budget"
|
|
9
|
+
require_relative "questioner"
|
|
10
|
+
require_relative "reuse"
|
|
11
|
+
require_relative "notices"
|
|
12
|
+
require_relative "narration"
|
|
13
|
+
require_relative "resilience"
|
|
14
|
+
require_relative "../../prompt"
|
|
15
|
+
|
|
16
|
+
module Rubik
|
|
17
|
+
module Orchestrator
|
|
18
|
+
module Subagent
|
|
19
|
+
# Autonomous background subagent execution instance running in an isolated
|
|
20
|
+
# disposable sub-context with its own message inbox, tool slice, and lifecycle (Plan 94, Plan 95).
|
|
21
|
+
class Instance
|
|
22
|
+
# Lifecycle (first include) owns the state vocabulary: TRANSITIONS,
|
|
23
|
+
# transition!, and every predicate.
|
|
24
|
+
include Lifecycle
|
|
25
|
+
include StepRunner
|
|
26
|
+
include Questioner
|
|
27
|
+
include Redeploy
|
|
28
|
+
include Reaper
|
|
29
|
+
|
|
30
|
+
attr_accessor :parent_id, :unit
|
|
31
|
+
# `state`: the sanctioned raw reader — display/serialization/kernel
|
|
32
|
+
# bridge only; control flow asks the Lifecycle predicates. `result`
|
|
33
|
+
# is TERMINAL-ONLY (real report / nil-provider stub); pauses narrate
|
|
34
|
+
# through #state_note — derived, nothing stored to go stale (B3).
|
|
35
|
+
attr_reader :id, :role, :type_name, :prompt, :system_prompt,
|
|
36
|
+
:workspace, :model, :context, :tool_registry, :buffer_manager,
|
|
37
|
+
:state, :started_at, :ended_at, :result,
|
|
38
|
+
:last_error, :storage_id, :transcript_uri, :history,
|
|
39
|
+
:context_limit, :max_output_tokens, :config, :pause_facts, :prompt_reconstructible
|
|
40
|
+
|
|
41
|
+
# Budget delegation: step counters live in Subagent::Budget (reader contract preserved).
|
|
42
|
+
def current_step = @budget.current_step
|
|
43
|
+
def max_steps = @budget.max_steps
|
|
44
|
+
|
|
45
|
+
def initialize(id:, parent_id:, role:, type_name:, prompt:, context:, tool_registry:,
|
|
46
|
+
system_prompt: nil, workspace: "inherit", model: "inherit",
|
|
47
|
+
provider: nil, buffer_manager: nil, max_steps: Budget::DEFAULT_MAX_STEPS, root_dir: Dir.pwd,
|
|
48
|
+
context_limit: nil, config: nil, event_bus: nil, provider_gate: nil, transcript_storage: nil,
|
|
49
|
+
prompt_reconstructible: false)
|
|
50
|
+
@id = id.to_s
|
|
51
|
+
@parent_id = parent_id.to_s
|
|
52
|
+
@role = role.to_s.strip
|
|
53
|
+
@type_name = type_name.to_s.downcase.strip
|
|
54
|
+
@prompt = prompt.to_s.strip
|
|
55
|
+
@system_prompt = system_prompt
|
|
56
|
+
@prompt_reconstructible = prompt_reconstructible # spawn kararı: kurulabilir prompt → kalıcı kopya stub'a iner
|
|
57
|
+
@workspace = workspace.to_s.downcase.strip
|
|
58
|
+
@model = model
|
|
59
|
+
@context = context
|
|
60
|
+
@tool_registry = tool_registry
|
|
61
|
+
@provider = provider
|
|
62
|
+
@buffer_manager = buffer_manager
|
|
63
|
+
@budget = Budget.new(max_steps: max_steps)
|
|
64
|
+
@root_dir = root_dir
|
|
65
|
+
@config = config
|
|
66
|
+
@event_bus = event_bus
|
|
67
|
+
@provider_gate = provider_gate
|
|
68
|
+
@context_limit = (context_limit || (@config.respond_to?(:fetch) ? @config.fetch("context.max_tokens", 32_768) : 32_768) || 32_768).to_i
|
|
69
|
+
@max_output_tokens = (@config.respond_to?(:fetch) ? @config.fetch("llm.default_max_output_tokens", 4_096) : 4_096).to_i
|
|
70
|
+
|
|
71
|
+
@state = :pending
|
|
72
|
+
@history = []
|
|
73
|
+
@inbox = Queue.new
|
|
74
|
+
@started_at = @ended_at = @result = @last_error = @transcript_path = @pause_facts = nil
|
|
75
|
+
# Grant telemetry (session review B4/B11): grants consumed this run —
|
|
76
|
+
# the halt message says "no FURTHER expansion" only when one landed.
|
|
77
|
+
@grants_received = 0
|
|
78
|
+
# SSOT (Plan 110): minted once; absorb (transcript_storage) kökü değiştirir.
|
|
79
|
+
@storage_id = StorageId.mint(@id)
|
|
80
|
+
@transcript_uri = TranscriptFormatter.mint_path(@root_dir, @storage_id, storage: transcript_storage)
|
|
81
|
+
@monitor = Monitor.new
|
|
82
|
+
@budget_cond = @monitor.new_cond
|
|
83
|
+
@error_cond = @monitor.new_cond
|
|
84
|
+
@context_cleaned_up = false
|
|
85
|
+
@thread = nil
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def uptime = @started_at ? ((@ended_at || Time.now) - @started_at).to_f : 0.0
|
|
89
|
+
|
|
90
|
+
# Inbox girdisi: mesaj metni + GERÇEK gönderen id'si. nil veya makine
|
|
91
|
+
# adı "agent-0" gönderenler render'da maestro persona'sına çevrilir
|
|
92
|
+
# (iki-düzlem adlandırma sözleşmesi) — subagent gönderenler kendi
|
|
93
|
+
# id'leriyle görünür (drain: StepRunner#sender_label).
|
|
94
|
+
InboxMessage = Struct.new(:text, :from)
|
|
95
|
+
|
|
96
|
+
def post_message(message, sender: nil, additional_steps: nil)
|
|
97
|
+
@monitor.synchronize do
|
|
98
|
+
@inbox.push(InboxMessage.new(message, sender))
|
|
99
|
+
if error_paused?
|
|
100
|
+
resume!(additional_steps: additional_steps)
|
|
101
|
+
elsif awaiting_budget? || additional_steps
|
|
102
|
+
grant_budget!(additional_steps, note: ->(extra, total) { Budget.message_grant_note(extra, total) })
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def resume!(additional_steps: nil)
|
|
108
|
+
@monitor.synchronize do
|
|
109
|
+
return false if terminal?
|
|
110
|
+
|
|
111
|
+
if error_paused?
|
|
112
|
+
transition!(:running)
|
|
113
|
+
@last_error = nil
|
|
114
|
+
@pause_facts = nil
|
|
115
|
+
# Error-resume grants ride the same accounting as budget grants
|
|
116
|
+
# (review B4): counted, noted to the subagent's own loop — the
|
|
117
|
+
# halt message must not later claim no grant ever arrived.
|
|
118
|
+
grant_budget!(additional_steps, note: ->(extra, total) { Budget.grant_note(extra, total) }) if additional_steps&.to_i&.positive?
|
|
119
|
+
@error_cond.broadcast
|
|
120
|
+
@unit&.sync_state!
|
|
121
|
+
true
|
|
122
|
+
elsif awaiting_budget?
|
|
123
|
+
grant_budget!(additional_steps || Budget::DEFAULT_GRANT_STEPS,
|
|
124
|
+
note: ->(extra, total) { Budget.grant_note(extra, total) })
|
|
125
|
+
else
|
|
126
|
+
false
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def extend_budget(additional_steps = Budget::DEFAULT_GRANT_STEPS)
|
|
132
|
+
grant_budget!(additional_steps, note: ->(extra, total) { Budget.grant_note(extra, total) })
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def start_async(&on_complete)
|
|
136
|
+
@monitor.synchronize do
|
|
137
|
+
return false if @thread&.alive?
|
|
138
|
+
|
|
139
|
+
@started_at = Time.now
|
|
140
|
+
transition!(:running)
|
|
141
|
+
@unit&.sync_state!
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
@thread = Thread.new do
|
|
145
|
+
run_execution_cycle
|
|
146
|
+
ensure
|
|
147
|
+
@ended_at ||= Time.now
|
|
148
|
+
save_transcript! if @transcript_path.nil?
|
|
149
|
+
cleanup_context
|
|
150
|
+
on_complete&.call(self)
|
|
151
|
+
end
|
|
152
|
+
true
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def kill
|
|
156
|
+
@monitor.synchronize do
|
|
157
|
+
return true if disposed?
|
|
158
|
+
|
|
159
|
+
transition!(:canceling)
|
|
160
|
+
@ended_at = Time.now
|
|
161
|
+
@budget_cond.broadcast
|
|
162
|
+
@error_cond.broadcast
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
begin; @thread&.kill if @thread&.alive?; rescue StandardError; nil; end
|
|
166
|
+
|
|
167
|
+
cleanup
|
|
168
|
+
true
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def cleanup
|
|
172
|
+
@monitor.synchronize do
|
|
173
|
+
return if disposed?
|
|
174
|
+
|
|
175
|
+
@budget_cond.broadcast
|
|
176
|
+
@error_cond.broadcast
|
|
177
|
+
cleanup_context
|
|
178
|
+
transition!(:disposed)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Idempotent under monitor: check-and-set of @context_cleaned_up (ensure block vs kill cleanup race).
|
|
183
|
+
def cleanup_context
|
|
184
|
+
@monitor.synchronize do
|
|
185
|
+
return if @context_cleaned_up
|
|
186
|
+
|
|
187
|
+
@context_cleaned_up = true
|
|
188
|
+
begin
|
|
189
|
+
@context&.dispose unless @context&.disposed?
|
|
190
|
+
rescue StandardError => e
|
|
191
|
+
@last_error ||= "#{e.class}: #{e.message}"
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def to_h
|
|
197
|
+
@monitor.synchronize do
|
|
198
|
+
{ id: @id, parent_id: @parent_id, role: @role, type: @type_name, state: @state.to_s,
|
|
199
|
+
**@budget.to_h, grants_received: @grants_received, uptime_seconds: uptime.round(2),
|
|
200
|
+
started_at: @started_at&.iso8601, ended_at: @ended_at&.iso8601,
|
|
201
|
+
transcript: @transcript_uri, last_error: @last_error,
|
|
202
|
+
result_summary: @result&.to_s&.slice(0, 100), result: @result, state_note: state_note }
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Derived narration (Narration seam): computed on demand, never stored.
|
|
207
|
+
def state_note
|
|
208
|
+
@monitor.synchronize do
|
|
209
|
+
Narration.state_note(@state, grants_received: @grants_received, max_steps: @budget.max_steps,
|
|
210
|
+
last_error: @last_error, paused_after_s: @pause_facts&.dig(:timeout))
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def format_transcript
|
|
215
|
+
TranscriptFormatter.format(self, @history, @monitor)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
private
|
|
219
|
+
|
|
220
|
+
# The run's single failure drain: record why, land on :failed, sync
|
|
221
|
+
# the unit, persist — the loud prefix keeps an FSM violation
|
|
222
|
+
# distinguishable from a provider failure.
|
|
223
|
+
def fail_run(reason)
|
|
224
|
+
@monitor.synchronize do
|
|
225
|
+
@last_error = reason
|
|
226
|
+
transition!(:failed)
|
|
227
|
+
@unit&.sync_state!
|
|
228
|
+
end
|
|
229
|
+
save_transcript!
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Private by design: a grant must pair with the terminal guard, the
|
|
233
|
+
# history note, and the broadcast — all lifecycle facts owned here.
|
|
234
|
+
# Tests drive budget states through supervisor + provider, never by
|
|
235
|
+
# reaching into this reader.
|
|
236
|
+
attr_reader :budget
|
|
237
|
+
|
|
238
|
+
def save_transcript!
|
|
239
|
+
saved = TranscriptFormatter.save!(@transcript_uri, @history, @monitor,
|
|
240
|
+
prompt_reconstructible: @prompt_reconstructible)
|
|
241
|
+
@transcript_path = saved
|
|
242
|
+
# A failed save must not be silent: the advertised path would 404 silently.
|
|
243
|
+
@last_error ||= "Transcript save failed for #{@transcript_uri}: see stderr" unless saved
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def run_execution_cycle
|
|
247
|
+
# Fresh runs seed; a redeployed run continues its preserved history.
|
|
248
|
+
@history.push({ role: "system", content: build_effective_system_prompt }, { role: "user", content: @prompt }) if @history.empty?
|
|
249
|
+
|
|
250
|
+
unless @provider
|
|
251
|
+
@budget.consume
|
|
252
|
+
@result = Narration.completed_stub_line(@id, @role, @prompt)
|
|
253
|
+
transition!(:completed)
|
|
254
|
+
@unit&.sync_state!
|
|
255
|
+
save_transcript!
|
|
256
|
+
return
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
execute_steps_with_provider
|
|
260
|
+
@unit&.sync_state!
|
|
261
|
+
save_transcript!
|
|
262
|
+
rescue StateError => e
|
|
263
|
+
fail_run("LIFECYCLE INVARIANT VIOLATED: #{e.class}: #{e.message}")
|
|
264
|
+
rescue StandardError => e
|
|
265
|
+
fail_run("#{e.class}: #{e.message}")
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# The single budget-grant core (Candidate 03): terminal guard ->
|
|
269
|
+
# grant -> resume -> note -> broadcast. Note wording lives in
|
|
270
|
+
# Budget.grant_note/message_grant_note (audience boundary: the
|
|
271
|
+
# subagent's own loop); call sites pass a lambda receiving extra +
|
|
272
|
+
# new total. The budget mechanics run inside @budget.grant! under
|
|
273
|
+
# the Budget lock (lock order: instance -> budget). The broadcast
|
|
274
|
+
# is a harmless no-op when nobody waits. Terminal and canceling
|
|
275
|
+
# states refuse the grant — a dying or dead instance must not
|
|
276
|
+
# resurrect (kill's race window included).
|
|
277
|
+
def grant_budget!(steps, note:)
|
|
278
|
+
@monitor.synchronize do
|
|
279
|
+
return false if terminal?
|
|
280
|
+
|
|
281
|
+
extra, total = @budget.grant!(steps)
|
|
282
|
+
transition!(:running)
|
|
283
|
+
@grants_received += 1
|
|
284
|
+
@pause_facts = nil # the wake retires the wait fact with the pause (B3's clearer, now fact-only)
|
|
285
|
+
@history << { role: "user", content: note.call(extra, total) }
|
|
286
|
+
@budget_cond.broadcast
|
|
287
|
+
@error_cond.broadcast
|
|
288
|
+
@unit&.sync_state!
|
|
289
|
+
true
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def pause_and_await_budget_expansion!(wait_timeout = nil)
|
|
294
|
+
timeout = wait_timeout || (@config.respond_to?(:fetch) ? @config.fetch("subagents.budget_wait_timeout", 180) : 180) || 180
|
|
295
|
+
payload = nil
|
|
296
|
+
@monitor.synchronize do
|
|
297
|
+
return false unless executing?
|
|
298
|
+
|
|
299
|
+
transition!(:waiting_budget)
|
|
300
|
+
@pause_facts = { timeout: timeout }.freeze
|
|
301
|
+
payload = { id: @id, role: @role, state: @state, max_steps: @budget.max_steps,
|
|
302
|
+
grants: @grants_received, step: @budget.current_step }
|
|
303
|
+
@unit&.sync_state!
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# The object that owns the fact emits it (outside the monitor, so
|
|
307
|
+
# subscribers never run under the instance lock).
|
|
308
|
+
Notices.emit(@event_bus, Notices::BUDGET_EXHAUSTED_EVENT, payload)
|
|
309
|
+
|
|
310
|
+
exhausted_payload = nil
|
|
311
|
+
@monitor.synchronize do
|
|
312
|
+
@budget_cond.wait(timeout) if @state == :waiting_budget
|
|
313
|
+
if @state == :waiting_budget
|
|
314
|
+
transition!(:budget_exhausted)
|
|
315
|
+
exhausted_payload = { id: @id, role: @role, state: @state, max_steps: @budget.max_steps,
|
|
316
|
+
grants: @grants_received, step: @budget.current_step }
|
|
317
|
+
@unit&.sync_state!
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
Notices.emit(@event_bus, Notices::COMPLETED_EVENT, exhausted_payload) if exhausted_payload
|
|
322
|
+
|
|
323
|
+
@monitor.synchronize do
|
|
324
|
+
@budget_cond.wait while @state == :budget_exhausted
|
|
325
|
+
return false if terminal?
|
|
326
|
+
|
|
327
|
+
transition!(:running)
|
|
328
|
+
@unit&.sync_state!
|
|
329
|
+
true
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def pause_on_error!(error, wait_timeout = nil)
|
|
334
|
+
timeout = wait_timeout ||
|
|
335
|
+
(@config.respond_to?(:fetch) && @config.fetch("subagents.error_wait_timeout", nil)) ||
|
|
336
|
+
Resilience::DEFAULT_ERROR_WAIT_TIMEOUT
|
|
337
|
+
payload = nil
|
|
338
|
+
@monitor.synchronize do
|
|
339
|
+
return false unless executing?
|
|
340
|
+
|
|
341
|
+
transition!(:paused_error)
|
|
342
|
+
@last_error = "#{error.class}: #{error.message}"
|
|
343
|
+
@pause_facts = { timeout: timeout }.freeze
|
|
344
|
+
payload = { id: @id, role: @role, state: @state, error: @last_error }
|
|
345
|
+
@unit&.sync_state!
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# The pause IS the recovery: a raising emit degrades to warn (door fallback), never to a terminal failure.
|
|
349
|
+
Notices.emit(@event_bus, Notices::ERROR_PAUSED_EVENT, payload,
|
|
350
|
+
fallback: "[Subagent] '#{@id}' error-paused notice emit failed (#{@last_error})")
|
|
351
|
+
|
|
352
|
+
@monitor.synchronize do
|
|
353
|
+
@error_cond.wait(timeout) if error_paused?
|
|
354
|
+
if error_paused?
|
|
355
|
+
transition!(:failed)
|
|
356
|
+
@unit&.sync_state!
|
|
357
|
+
false
|
|
358
|
+
else
|
|
359
|
+
# Symmetric with the budget wake: a kill that flipped us to
|
|
360
|
+
# :canceling while parked wins — the worker must not resurrect.
|
|
361
|
+
return false if terminal?
|
|
362
|
+
|
|
363
|
+
transition!(:running)
|
|
364
|
+
@unit&.sync_state!
|
|
365
|
+
true
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def build_effective_system_prompt
|
|
371
|
+
base = "You are a specialized subagent operating under autonomous supervision.\n\n" \
|
|
372
|
+
"Subagent ID: #{@id} | Role: #{@role} | Type: #{@type_name}\n" \
|
|
373
|
+
"Allocated Step Budget: #{@budget.max_steps} steps.\n\n" \
|
|
374
|
+
"A [Step X/Y] status line precedes each of your steps (X = current step, Y = total budget). Pace yourself: " \
|
|
375
|
+
"when remaining steps (Y - X) run low, conclude the task or prepare a progress summary for the maestro " \
|
|
376
|
+
"supervisor — it may extend your budget or steer you. If a requirement is genuinely ambiguous, ask the maestro directly with the " \
|
|
377
|
+
"ask_question tool — it blocks until the maestro answers or times out; ask only when the answer truly changes your work.\n\n" \
|
|
378
|
+
"Perform your assigned task with high precision and provide a structured synthesis report."
|
|
379
|
+
@system_prompt && !@system_prompt.strip.empty? ? "#{base}\n\nCustom Instructions:\n#{@system_prompt}" : base
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubik
|
|
4
|
+
module Orchestrator
|
|
5
|
+
module Subagent
|
|
6
|
+
# The Instance lifecycle vocabulary, extracted from instance.rb to
|
|
7
|
+
# satisfy module hygiene (same vertical-cohesion extraction as
|
|
8
|
+
# StepRunner/Questioner, Plan 95): TRANSITIONS is the single home of
|
|
9
|
+
# the state machine, transition! its only mutation path, and these
|
|
10
|
+
# predicates the only control-flow surface. Instance includes this
|
|
11
|
+
# module, so Instance::TRANSITIONS / Instance::StateError remain the
|
|
12
|
+
# public constants — adding a state means editing THIS file (plus the
|
|
13
|
+
# kernel STATE_MAP, held by contract test).
|
|
14
|
+
module Lifecycle
|
|
15
|
+
class StateError < StandardError; end
|
|
16
|
+
|
|
17
|
+
# Every legal from->to edge. A transition not listed here is a bug
|
|
18
|
+
# — fail loud with both endpoints in the message. :disposed is
|
|
19
|
+
# absorbing (reaper/evict own the shell). completed/failed ->
|
|
20
|
+
# running is the redeploy window (redeploy_for! only resets;
|
|
21
|
+
# start_async performs the flip). canceling -> completed/failed is
|
|
22
|
+
# the kill race: kill announces under the monitor but Thread#kill
|
|
23
|
+
# lands after, so the worker may still record its outcome;
|
|
24
|
+
# completed -> failed is the total-catch rescue firing after a
|
|
25
|
+
# post-outcome raise (transcript save, sync). Same-state writes are
|
|
26
|
+
# idempotent no-ops — the waker flips before the parked thread
|
|
27
|
+
# re-flips.
|
|
28
|
+
TRANSITIONS = {
|
|
29
|
+
pending: [:running, :canceling, :disposed],
|
|
30
|
+
running: [:waiting_input, :waiting_budget, :paused_error, :completed, :failed, :canceling, :disposed],
|
|
31
|
+
waiting_input: [:running, :canceling, :disposed],
|
|
32
|
+
waiting_budget: [:budget_exhausted, :running, :failed, :canceling, :disposed],
|
|
33
|
+
budget_exhausted: [:running, :failed, :canceling, :disposed],
|
|
34
|
+
paused_error: [:running, :failed, :canceling, :disposed],
|
|
35
|
+
canceling: [:completed, :failed, :disposed],
|
|
36
|
+
completed: [:running, :failed, :canceling, :disposed],
|
|
37
|
+
failed: [:running, :canceling, :disposed],
|
|
38
|
+
disposed: []
|
|
39
|
+
}.transform_values(&:freeze).freeze
|
|
40
|
+
|
|
41
|
+
# --- Predicates: control-flow questions answer here, not at
|
|
42
|
+
# consumers. All take the re-entrant monitor; callers composing
|
|
43
|
+
# them under a held lock pay nothing extra, and external readers
|
|
44
|
+
# see consistent snapshots.
|
|
45
|
+
|
|
46
|
+
# Worker thread mid-flight (possibly parked on a steering, budget,
|
|
47
|
+
# or error wait).
|
|
48
|
+
def running?
|
|
49
|
+
@monitor.synchronize { @state == :running || @state == :waiting_input || @state == :waiting_budget || @state == :paused_error }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Occupies a concurrency slot / tracked in Supervisor#instances.
|
|
53
|
+
# Composed from the vocabulary's own words: everything live except
|
|
54
|
+
# :budget_exhausted (deliberately excluded, pinned by
|
|
55
|
+
# test_pause_and_await_budget_expansion_timeout; supervisor's
|
|
56
|
+
# send_message compensates via deliverable?).
|
|
57
|
+
def active?
|
|
58
|
+
@monitor.synchronize { pending? || running? || canceling? }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def pending? = @monitor.synchronize { @state == :pending }
|
|
62
|
+
def executing? = @monitor.synchronize { @state == :running }
|
|
63
|
+
def awaiting_human? = @monitor.synchronize { @state == :waiting_input }
|
|
64
|
+
def error_paused? = @monitor.synchronize { @state == :paused_error }
|
|
65
|
+
def canceling? = @monitor.synchronize { @state == :canceling }
|
|
66
|
+
def completed? = @monitor.synchronize { @state == :completed }
|
|
67
|
+
def failed? = @monitor.synchronize { @state == :failed }
|
|
68
|
+
def disposed? = @monitor.synchronize { @state == :disposed }
|
|
69
|
+
|
|
70
|
+
# A budget grant is the only wake-up from either of these.
|
|
71
|
+
def awaiting_budget?
|
|
72
|
+
@monitor.synchronize { @state == :waiting_budget || @state == :budget_exhausted }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Condvar-parked, reap-immune; the resume_all targets.
|
|
76
|
+
def parked?
|
|
77
|
+
@monitor.synchronize { error_paused? || awaiting_budget? }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# The no-resurrection set (kill's race window included).
|
|
81
|
+
def terminal?
|
|
82
|
+
@monitor.synchronize { settled? || disposed? || canceling? }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Final outcome recorded; the thread may still be in its ensure
|
|
86
|
+
# block — terminal_and_settled? (Redeploy) is the strict form.
|
|
87
|
+
def settled?
|
|
88
|
+
@monitor.synchronize { @state == :completed || @state == :failed }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Inbox accepts a message; exactly active? plus :budget_exhausted.
|
|
92
|
+
# :canceling stays deliverable (a message to a dying instance is
|
|
93
|
+
# harmless but pointless) — preserved behavior, revisitable.
|
|
94
|
+
def deliverable?
|
|
95
|
+
@monitor.synchronize { !settled? && !disposed? }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Guarded low-level seam (mirrors Unit#transition_to!): business
|
|
99
|
+
# callers use the named drivers (resume!, grant_budget!, kill...);
|
|
100
|
+
# tests use this for legal-but-rare windows (e.g. the :canceling
|
|
101
|
+
# kill race, which is not externally holdable). Idempotent on same
|
|
102
|
+
# state; raises StateError on an undeclared edge. Does NOT sync the
|
|
103
|
+
# unit or emit — call sites own that timing (the kernel reconciles
|
|
104
|
+
# lazily through its own sync_state! polls).
|
|
105
|
+
def transition!(to)
|
|
106
|
+
@monitor.synchronize do
|
|
107
|
+
target = to.to_sym
|
|
108
|
+
return @state if @state == target
|
|
109
|
+
|
|
110
|
+
raise StateError, "illegal subagent transition #{@id}: #{@state} -> #{target}" unless TRANSITIONS.fetch(@state, []).include?(target)
|
|
111
|
+
|
|
112
|
+
@state = target
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../platform"
|
|
4
|
+
require_relative "notices"
|
|
5
|
+
require_relative "conversation_id"
|
|
6
|
+
|
|
7
|
+
module Rubik
|
|
8
|
+
module Orchestrator
|
|
9
|
+
module Subagent
|
|
10
|
+
# Inter-agent messaging (Plan 94 + issue #55 3.2): supervisor-plane
|
|
11
|
+
# delivery for SendMessageTool. Extracted from supervisor.rb to keep
|
|
12
|
+
# the file under its module-hygiene cap; the supervisor delegates
|
|
13
|
+
# send_message here. Root recipient aliases live in conversation_id.rb
|
|
14
|
+
# (root plane alias resolution is referenced by tool reply text too).
|
|
15
|
+
module MessagingSupervisor
|
|
16
|
+
# Returns true (delivered to an active instance), :queued (buffered
|
|
17
|
+
# on a finished-but-unreaped instance — drain_inbox_into_history
|
|
18
|
+
# delivers it when that id is redeployed), or false (unknown /
|
|
19
|
+
# disposed / reaped-mid-flight recipient). The completion→reap
|
|
20
|
+
# window used to reject steering outright while redeploy on the same
|
|
21
|
+
# id succeeded (issue #55 3.2); buffering closes it. The buffer is
|
|
22
|
+
# monitor-atomic against the reaper (buffer_for_redeploy), and
|
|
23
|
+
# posting to a settled instance cannot resurrect it (pinned by
|
|
24
|
+
# test_dead_instance_must_not_resurrect_via_post_message).
|
|
25
|
+
def send_message(recipient_id, message, sender_id: ConversationId::ROOT_ID, additional_steps: nil, **kwargs)
|
|
26
|
+
rec = ConversationId.scrub(recipient_id)
|
|
27
|
+
eff_sender = kwargs[:_sender_id] || sender_id
|
|
28
|
+
if ConversationId.root_alias?(rec)
|
|
29
|
+
Notices.emit(@event_bus, Notices::MESSAGE_EVENT, { sender_id: eff_sender, recipient: rec, message: message })
|
|
30
|
+
return true
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
inst = get(rec)
|
|
34
|
+
return false unless inst
|
|
35
|
+
|
|
36
|
+
if inst.deliverable?
|
|
37
|
+
inst.post_message(message, sender: eff_sender, additional_steps: additional_steps)
|
|
38
|
+
return true
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# get's queue fallback can also surface a DeferredSpec (a not-yet-
|
|
42
|
+
# born dependency-queued spec): nothing to buffer onto, nothing to
|
|
43
|
+
# redeploy — undeliverable.
|
|
44
|
+
return false unless inst.respond_to?(:buffer_for_redeploy)
|
|
45
|
+
|
|
46
|
+
# Finished-but-unreaped recipient: buffer into the inbox —
|
|
47
|
+
# redeploy's drain_inbox_into_history delivers it. Steps grants
|
|
48
|
+
# are meaningless here (redeploy mints a fresh budget). A race
|
|
49
|
+
# loss (reaper archived the instance between the checks) reports
|
|
50
|
+
# false instead of a stranded :queued.
|
|
51
|
+
if inst.buffer_for_redeploy(message, sender: eff_sender)
|
|
52
|
+
:queued
|
|
53
|
+
else
|
|
54
|
+
Platform.debug_warn("[Supervisor] steering message for '#{rec}' lost: reaped between liveness checks")
|
|
55
|
+
false
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|