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,232 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../base"
|
|
4
|
+
require_relative "../output"
|
|
5
|
+
require_relative "../../coeffects"
|
|
6
|
+
require_relative "../../uris"
|
|
7
|
+
require_relative "../../storage"
|
|
8
|
+
|
|
9
|
+
module Rubik
|
|
10
|
+
module Tools
|
|
11
|
+
# rubik://storage sanal FS-DB'nin yazma düzlemi (okuma düzlemi read_file
|
|
12
|
+
# URI rotalarıdır — research archetype'ı storage aracına gerek olmadan
|
|
13
|
+
# okur). maestro ve coder subagent'ları hedef/plan/bellek/wiki/transcript/
|
|
14
|
+
# inceleme/not varlıklarını buradan yazar; worktree'ye iş dosyası
|
|
15
|
+
# saçtırmadan merkezi depo tutar.
|
|
16
|
+
class StorageTool < BaseTool
|
|
17
|
+
# Registry#schemas, REQUIRES memnuniyetsizliğinde aracı gizler (storage
|
|
18
|
+
# kapalı/bağlanmamışken reklam edilmez); plain (ctx'siz) registry'ler
|
|
19
|
+
# her şeyi reklam etmeye devam eder.
|
|
20
|
+
REQUIRES = [Rubik::Coeffects::STORAGE_STORE].freeze
|
|
21
|
+
|
|
22
|
+
attr_accessor :store, :runtime_context
|
|
23
|
+
|
|
24
|
+
DECLARATION = Declaration.new(
|
|
25
|
+
name: "storage", aliases: ["rubik_storage"],
|
|
26
|
+
description: "Central rubik://storage database for goals, plans, memory, wiki, transcripts, reviews, " \
|
|
27
|
+
"and notes. Actions: 'put' (create/update; returns id rubik://storage URI), 'get', 'list', " \
|
|
28
|
+
"'search', 'set_ref' (alias), 'delete'. Keep agent workfiles HERE — not in the worktree. " \
|
|
29
|
+
"Read back any entity with read_file path 'rubik://storage/<type>/<id>'.",
|
|
30
|
+
risk: :write, archetypes: [:coder], write_grant: true,
|
|
31
|
+
maestro_blocked: false, # storage plane stays direct at root (TOOLS.md maestro policy)
|
|
32
|
+
params_schema: {
|
|
33
|
+
type: "object",
|
|
34
|
+
properties: {
|
|
35
|
+
action: {
|
|
36
|
+
type: "string",
|
|
37
|
+
enum: ["put", "get", "list", "search", "set_ref", "delete"],
|
|
38
|
+
description: "Operation (default: 'list')."
|
|
39
|
+
},
|
|
40
|
+
type: {
|
|
41
|
+
type: "string",
|
|
42
|
+
enum: Rubik::Storage::Store::TYPES,
|
|
43
|
+
description: "Entity type: goal, plan, memory, wiki, transcript, review, or note."
|
|
44
|
+
},
|
|
45
|
+
title: { type: "string", description: "Entity title (required for put)." },
|
|
46
|
+
body: { type: "string", description: "Entity body (markdown text)." },
|
|
47
|
+
tags: {
|
|
48
|
+
type: "array", items: { type: "string" },
|
|
49
|
+
description: "Tags (array or comma-separated string)."
|
|
50
|
+
},
|
|
51
|
+
links: {
|
|
52
|
+
type: "object", additionalProperties: { type: "string" },
|
|
53
|
+
description: "Cross-references, e.g. {\"goal\": \"goal.1.<hash16>\"}."
|
|
54
|
+
},
|
|
55
|
+
status: { type: "string", description: "Lifecycle status (default 'active')." },
|
|
56
|
+
id: { type: "string", description: "Entity id to update/get/delete: full 'plan.3.<hash16>', 'plan.3', row '3', hash prefix, or ref alias." },
|
|
57
|
+
ref: { type: "string", description: "Alias name bound to the entity (upsert semantics)." },
|
|
58
|
+
query: { type: "string", description: "Search query; every space-separated term must appear (case-insensitive over title/body/tags)." },
|
|
59
|
+
format: { type: "string", enum: ["markdown", "json"], description: "Output format (default markdown)." }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
def initialize(store = nil, runtime_context: nil, config: nil, root_dir: Dir.pwd)
|
|
64
|
+
super(DECLARATION)
|
|
65
|
+
@store = store
|
|
66
|
+
@runtime_context = runtime_context
|
|
67
|
+
@config = config
|
|
68
|
+
@root_dir = root_dir
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def execute(args = {})
|
|
72
|
+
store = resolve_store
|
|
73
|
+
unless store
|
|
74
|
+
return ToolOutput.error("Storage is not available: disabled by config (storage.enabled=false) or no store wired. " \
|
|
75
|
+
"Read-only rubik://storage URIs may still resolve if a store exists.")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
action = (args["action"] || args[:action] || "list").to_s
|
|
79
|
+
json = (args["format"] || args[:format]).to_s == "json"
|
|
80
|
+
|
|
81
|
+
case action
|
|
82
|
+
when "put" then handle_put(store, args, json)
|
|
83
|
+
when "get" then handle_get(store, args, json)
|
|
84
|
+
when "list" then handle_list(store, args, json)
|
|
85
|
+
when "search" then handle_search(store, args, json)
|
|
86
|
+
when "set_ref" then handle_set_ref(store, args)
|
|
87
|
+
when "delete" then handle_delete(store, args)
|
|
88
|
+
else ToolOutput.error("Unknown storage action '#{action}' (valid: put, get, list, search, set_ref, delete).")
|
|
89
|
+
end
|
|
90
|
+
rescue ArgumentError => e
|
|
91
|
+
ToolOutput.error("storage #{action}: #{e.message}")
|
|
92
|
+
rescue StandardError => e
|
|
93
|
+
ToolOutput.error("storage error: #{e.class}: #{e.message}")
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
# Üçlü zincir (Tools::Services ev hükümleri): ctor enjeksiyonu →
|
|
99
|
+
# STORAGE_STORE coeffect'i (parent chain — subagent'lar buradan çözer)
|
|
100
|
+
# → config'e dayalı tembel fresh default (storage kapalıysa nil).
|
|
101
|
+
def resolve_store
|
|
102
|
+
resolved = Rubik::Coeffects.resolve(@runtime_context, Rubik::Coeffects::STORAGE_STORE, @store)
|
|
103
|
+
return resolved if resolved
|
|
104
|
+
|
|
105
|
+
@resolve_store ||= Rubik::Storage::Service.build(config: @config, root_dir: @root_dir)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def handle_put(store, args, json)
|
|
109
|
+
record = store.put(
|
|
110
|
+
type: arg(args, "type"), title: arg(args, "title").to_s,
|
|
111
|
+
body: arg(args, "body", ""), tags: normalize_tags(args),
|
|
112
|
+
links: arg(args, "links", {}) || {}, status: arg(args, "status", "active"),
|
|
113
|
+
id: arg(args, "id"), ref: arg(args, "ref")
|
|
114
|
+
)
|
|
115
|
+
return JSON.pretty_generate(record) if json
|
|
116
|
+
|
|
117
|
+
uri = store.uri_for(record["type"], record["id"])
|
|
118
|
+
<<~MD
|
|
119
|
+
Stored: #{record['id']} (version #{record['version']})
|
|
120
|
+
URI: #{uri}
|
|
121
|
+
Read back: read_file(path: '#{uri}')
|
|
122
|
+
MD
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def handle_get(store, args, json)
|
|
126
|
+
type = arg(args, "type").to_s
|
|
127
|
+
id = arg(args, "id").to_s
|
|
128
|
+
record = store.get(type: type, id: id)
|
|
129
|
+
unless record
|
|
130
|
+
return ToolOutput.error("Entity not found: #{type}/#{id} in project '#{store.project}'. " \
|
|
131
|
+
"Use action 'list' to see existing ids.")
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
json ? JSON.pretty_generate(record) : Rubik::Storage::Render.entity(record, project: store.project)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def handle_list(store, args, json)
|
|
138
|
+
type = arg(args, "type")
|
|
139
|
+
summaries = store.list(type: type)
|
|
140
|
+
return JSON.pretty_generate(summaries.map(&:to_h)) if json
|
|
141
|
+
|
|
142
|
+
type_label = type || "all types"
|
|
143
|
+
uri_hint = "read_file #{Rubik::Uris.storage_listing(project: store.project, type: type_label)}/<id>"
|
|
144
|
+
Rubik::Storage::Render.listing(project: store.project, type: type_label, summaries: summaries.map(&:to_h),
|
|
145
|
+
uri_hint: uri_hint)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def handle_search(store, args, json)
|
|
149
|
+
query = arg(args, "query").to_s
|
|
150
|
+
return ToolOutput.error "storage search: 'query' is required." if query.strip.empty?
|
|
151
|
+
|
|
152
|
+
hits = store.search(query: query, type: arg(args, "type"))
|
|
153
|
+
return "No storage entities match '#{query}'." if hits.empty?
|
|
154
|
+
|
|
155
|
+
return JSON.pretty_generate(hits) if json
|
|
156
|
+
|
|
157
|
+
hits.map { |r| "#{r['id']} — #{r['title']} (#{r['status']})" }.join("\n")
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def handle_set_ref(store, args)
|
|
161
|
+
result = store.set_ref(arg(args, "ref").to_s, arg(args, "id").to_s)
|
|
162
|
+
"Ref set: #{result['ref']} → #{result['id']}\nURI: #{result['uri']}"
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def handle_delete(store, args)
|
|
166
|
+
type = arg(args, "type").to_s
|
|
167
|
+
home_id = home_row_id(store, type)
|
|
168
|
+
deleted = store.delete(type: type, id: arg(args, "id").to_s)
|
|
169
|
+
return ToolOutput.error("Entity not found: #{arg(args, 'type')}/#{arg(args, 'id')}") unless deleted
|
|
170
|
+
|
|
171
|
+
rebuild_home_quietly(store, type) unless home_id && deleted["id"] == home_id
|
|
172
|
+
"Deleted: #{deleted['id']} (row #{deleted['row']} is never reused)."
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Manuel wiki silmesi kronikçinin flush ritmini beklemez — wiki.home
|
|
176
|
+
# bayat girdiyle kalırdı (issue #100 P2 alan bulgusu). Silme sonrası
|
|
177
|
+
# best-effort yeniden hesap: bir hata silme sonucunu ASLA maskelemez.
|
|
178
|
+
# wiki.home'un KENDİSİNİN silmesi meşrudur — rebuild onu diriltmez
|
|
179
|
+
# (review: "silindi" diyordu, entity bir sonraki get'te geri gelirdi).
|
|
180
|
+
def home_row_id(store, type)
|
|
181
|
+
return unless type == "wiki"
|
|
182
|
+
|
|
183
|
+
store.get(type: "wiki", id: Rubik::Storage::Vakanuvis::Vocabulary::HOME_REF)&.[]("id")
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def rebuild_home_quietly(store, type)
|
|
187
|
+
return unless type == "wiki"
|
|
188
|
+
|
|
189
|
+
Rubik::Storage::Vakanuvis::WikiSkeleton.rebuild_home(store)
|
|
190
|
+
rescue StandardError => e
|
|
191
|
+
Rubik::Storage::Vakanuvis.log_via(
|
|
192
|
+
nil, :warn,
|
|
193
|
+
"wiki row deleted but home rebuild failed (#{e.class}: #{e.message}) — the chronicler's next " \
|
|
194
|
+
"flush retries it when the chronicler is enabled; otherwise rewrite wiki.home manually"
|
|
195
|
+
)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def arg(args, key, default = nil)
|
|
199
|
+
args[key] || args[key.to_sym] || default
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def normalize_tags(args)
|
|
203
|
+
# Anahtar-varlığıyla okunur: tags: false sessizce [] düşmesin —
|
|
204
|
+
# üst-seviye boolean/sayı RET edilir (review H1: eskiden to_s
|
|
205
|
+
# üzerinden "true"/"42" etiket olarak saklanıyordu).
|
|
206
|
+
raw = if args.key?("tags") || args.key?(:tags)
|
|
207
|
+
args["tags"] || args[:tags]
|
|
208
|
+
else
|
|
209
|
+
[]
|
|
210
|
+
end
|
|
211
|
+
unless raw.is_a?(Array) || raw.is_a?(String)
|
|
212
|
+
raise ArgumentError,
|
|
213
|
+
"tags must be an array of strings or a comma-separated string — " \
|
|
214
|
+
"got #{raw.class} (#{raw.inspect})."
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Dize olarak gelen etiketler virgülle ayrılır (LLM klavye alışkanlığı).
|
|
218
|
+
# Dizi öğeleri arasındaki boolean/sayı da ret edilir (issue #100 P1) —
|
|
219
|
+
# execute'ın ArgumentError köprüsü modele temiz araç hatası döner.
|
|
220
|
+
list = raw.is_a?(Array) ? raw : raw.to_s.split(",").map(&:strip)
|
|
221
|
+
list.each do |tag|
|
|
222
|
+
next if tag.is_a?(String)
|
|
223
|
+
|
|
224
|
+
raise ArgumentError,
|
|
225
|
+
"tags must be strings — got #{tag.class} (#{tag.inspect}). " \
|
|
226
|
+
"Pass an array of strings like [\"infra\"] or a comma-separated string."
|
|
227
|
+
end
|
|
228
|
+
list
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "../base"
|
|
5
|
+
require_relative "../output"
|
|
6
|
+
require_relative "subagent_lifecycle"
|
|
7
|
+
require_relative "../../coeffects"
|
|
8
|
+
require_relative "../../orchestrator/subagent/budget"
|
|
9
|
+
require_relative "../../orchestrator/subagent/notices"
|
|
10
|
+
require_relative "../../orchestrator/subagent/narration"
|
|
11
|
+
require_relative "../../orchestrator/subagent/conversation_id"
|
|
12
|
+
|
|
13
|
+
module Rubik
|
|
14
|
+
module Tools
|
|
15
|
+
# Helper extracting SubagentSupervisor instance from constructor or runtime context.
|
|
16
|
+
module SubagentToolHelpers
|
|
17
|
+
def resolve_supervisor(supervisor, runtime_context)
|
|
18
|
+
return supervisor if supervisor
|
|
19
|
+
return nil unless runtime_context.respond_to?(:get)
|
|
20
|
+
|
|
21
|
+
Rubik::Coeffects.get(runtime_context, Rubik::Coeffects::SUBAGENT_SUPERVISOR)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Tool for asynchronously spawning one or more isolated subagents (Plan 94).
|
|
26
|
+
class InvokeSubagentTool < BaseTool
|
|
27
|
+
include SubagentToolHelpers
|
|
28
|
+
|
|
29
|
+
# Schema truth: document the EFFECTIVE spawn default — the bound
|
|
30
|
+
# supervisor's config-derived value, the Budget invariant otherwise.
|
|
31
|
+
# The literal lives once, here; the static DECLARATION carries the
|
|
32
|
+
# invariant and construction swaps in the supervisor value only when
|
|
33
|
+
# it differs (construction-time snapshot from the kwarg only —
|
|
34
|
+
# deterministic; a runtime-context peek would depend on registration
|
|
35
|
+
# order).
|
|
36
|
+
def self.params_schema_for(steps_default)
|
|
37
|
+
{
|
|
38
|
+
type: "object",
|
|
39
|
+
properties: {
|
|
40
|
+
subagents: {
|
|
41
|
+
type: "array", description: "Array of subagents to launch concurrently",
|
|
42
|
+
items: {
|
|
43
|
+
type: "object",
|
|
44
|
+
properties: {
|
|
45
|
+
type_name: { type: "string", description: "Archetype: 'research' (default), 'coder', 'self', or custom" },
|
|
46
|
+
role: { type: "string", description: "2-5 word job title" },
|
|
47
|
+
prompt: { type: "string", description: "Actionable task description" },
|
|
48
|
+
system_prompt: { type: "string", description: "Optional custom system prompt" },
|
|
49
|
+
workspace: { type: "string", enum: ["inherit", "branch", "share"], description: "Isolation mode" },
|
|
50
|
+
model: { type: "string", enum: ["inherit", "flash_lite", "flash", "pro"], description: "Model tier" },
|
|
51
|
+
max_steps: { type: "integer", description: "Step budget for subagent execution (default: #{steps_default})" },
|
|
52
|
+
depends_on: { type: "array", description: "Dispatch constraints: ['subagent-1'] = FS (launch after it completes); " \
|
|
53
|
+
"{'id': 'subagent-1', 'type': 'SS'} = launch when it starts. Queued specs hold no slot until released." }
|
|
54
|
+
},
|
|
55
|
+
required: ["role", "prompt"]
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
role: { type: "string", description: "Role for single subagent invocation" },
|
|
59
|
+
prompt: { type: "string", description: "Prompt for single subagent invocation" },
|
|
60
|
+
type_name: { type: "string", description: "Archetype: 'research', 'coder', or 'self'" },
|
|
61
|
+
max_steps: { type: "integer", description: "Step budget for single subagent invocation (default: #{steps_default})" }
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
DECLARATION = Declaration.new(
|
|
67
|
+
name: "invoke_subagent", aliases: ["spawn_subagent", "start_subagent"],
|
|
68
|
+
description: "Invokes one or more subagents asynchronously in disposable sub-contexts.",
|
|
69
|
+
risk: :execute, subagent_spawn: true, maestro_blocked: false, # the delegation verb itself — the gate's whole point
|
|
70
|
+
params_schema: params_schema_for(Rubik::Orchestrator::Subagent::Budget::DEFAULT_MAX_STEPS)
|
|
71
|
+
)
|
|
72
|
+
def initialize(supervisor: nil, runtime_context: nil)
|
|
73
|
+
sup_default = supervisor.respond_to?(:default_max_steps) ? supervisor.default_max_steps : nil
|
|
74
|
+
if sup_default && sup_default != Rubik::Orchestrator::Subagent::Budget::DEFAULT_MAX_STEPS
|
|
75
|
+
super(DECLARATION.with(params_schema: self.class.params_schema_for(sup_default)))
|
|
76
|
+
else
|
|
77
|
+
super(DECLARATION)
|
|
78
|
+
end
|
|
79
|
+
@supervisor = supervisor
|
|
80
|
+
@runtime_context = runtime_context
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def execute(args = {})
|
|
84
|
+
sup = resolve_supervisor(@supervisor, @runtime_context)
|
|
85
|
+
return ToolOutput.error("SubagentSupervisor is not available in runtime context.") unless sup
|
|
86
|
+
|
|
87
|
+
specs = extract_subagent_specs(args)
|
|
88
|
+
return ToolOutput.error("No valid subagents specified to invoke.") if specs.empty?
|
|
89
|
+
|
|
90
|
+
begin
|
|
91
|
+
results = sup.spawn_batch(specs)
|
|
92
|
+
rescue StandardError => e
|
|
93
|
+
return ToolOutput.error("Failed to invoke subagent(s): #{e.message}")
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
launched, queued = results.partition { |outcome, _obj| outcome == :launched }
|
|
97
|
+
lines = ["Launched #{launched.size} background subagent(s):"] + launched.map { |_o, i| " • [#{i.id}] '#{i.role}' (#{i.type_name}) - #{i.state.to_s.upcase}" }
|
|
98
|
+
unless queued.empty?
|
|
99
|
+
rows = queued.map { |_o, s| " • [#{s.id}] '#{s.role}' - QUEUED (waiting on: #{s.depends_on.join(', ')})" }
|
|
100
|
+
lines << ("\nQueued #{queued.size} subagent(s) on unmet dependencies (no slot, no cost until released): " + rows.join("\n"))
|
|
101
|
+
end
|
|
102
|
+
all_ids = (launched + queued).map { |_o, obj| obj.id }
|
|
103
|
+
|
|
104
|
+
lines << "\nSubagents run asynchronously in the background. Conclude your turn to yield the prompt to the user, or call " \
|
|
105
|
+
"#{Rubik::Orchestrator::Subagent::Notices.wait_hint(all_ids)} only if you strictly need results in this turn (queued ids block until the chain settles)."
|
|
106
|
+
lines.join("\n")
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
def extract_subagent_specs(args)
|
|
112
|
+
list = args["subagents"] || args[:subagents]
|
|
113
|
+
return list.map { |item| normalize_spec(symbolize_keys(item)) } if list.is_a?(Array) && !list.empty?
|
|
114
|
+
|
|
115
|
+
role = args["role"] || args[:role]
|
|
116
|
+
prompt = args["prompt"] || args[:prompt]
|
|
117
|
+
return [] unless role && prompt
|
|
118
|
+
|
|
119
|
+
[normalize_spec(
|
|
120
|
+
type_name: args["type_name"] || args[:type_name], role: role, prompt: prompt,
|
|
121
|
+
system_prompt: args["system_prompt"] || args[:system_prompt], workspace: args["workspace"] || args[:workspace] || "inherit",
|
|
122
|
+
model: args["model"] || args[:model] || "inherit", max_steps: args["max_steps"] || args[:max_steps],
|
|
123
|
+
depends_on: args["depends_on"] || args[:depends_on]
|
|
124
|
+
)]
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def normalize_spec(spec)
|
|
128
|
+
spec[:type_name] = spec[:type_name].to_s.strip
|
|
129
|
+
spec[:type_name] = code_modification_prompt?(spec[:role], spec[:prompt]) ? "coder" : "research" if spec[:type_name].empty?
|
|
130
|
+
spec[:max_steps] = spec[:max_steps].to_i if spec[:max_steps] && !spec[:max_steps].to_s.empty?
|
|
131
|
+
spec
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def code_modification_prompt?(role, prompt)
|
|
135
|
+
"#{role} #{prompt}".match?(/\b(fix|patch|implement|refactor|write|create|modify|edit|debug)\b/i)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def symbolize_keys(hash)
|
|
139
|
+
hash.is_a?(Hash) ? hash.transform_keys(&:to_sym) : {}
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Tool for listing, inspecting, or canceling running subagents (Plan 94).
|
|
144
|
+
class ManageSubagentsTool < BaseTool
|
|
145
|
+
include SubagentToolHelpers
|
|
146
|
+
include SubagentLifecycle
|
|
147
|
+
|
|
148
|
+
DECLARATION = Declaration.new(
|
|
149
|
+
name: "manage_subagents", aliases: ["subagents_status", "subagent_list"],
|
|
150
|
+
description: "Manage existing subagents (actions: #{Rubik::Orchestrator::Subagent::Narration.quoted_canonical_list}). " \
|
|
151
|
+
"Subagents run in background; avoid 'wait' unless strictly blocking.",
|
|
152
|
+
risk: :read,
|
|
153
|
+
params_schema: {
|
|
154
|
+
type: "object",
|
|
155
|
+
properties: {
|
|
156
|
+
action: { type: "string", description: "Action to perform",
|
|
157
|
+
enum: Rubik::Orchestrator::Subagent::Narration::SCHEMA_ACTIONS },
|
|
158
|
+
timeout: { type: "integer", description: "Max seconds to wait for actions 'wait' or 'status' if running " \
|
|
159
|
+
"(default: #{Rubik::Orchestrator::Subagent::Notices::DEFAULT_WAIT_TIMEOUT} for wait)" },
|
|
160
|
+
conversation_ids: { type: "array", items: { type: "string" }, description: "IDs of subagents to manage/wait" },
|
|
161
|
+
conversation_id: { type: "string", description: "Single subagent ID" },
|
|
162
|
+
prompt: { type: "string", description: "New task text for action 'redeploy'" },
|
|
163
|
+
steps: { type: "integer", description: "Additional steps (extend_budget) or redeploy budget" }
|
|
164
|
+
},
|
|
165
|
+
required: ["action"]
|
|
166
|
+
}
|
|
167
|
+
)
|
|
168
|
+
def initialize(supervisor: nil, runtime_context: nil)
|
|
169
|
+
super(DECLARATION)
|
|
170
|
+
@supervisor = supervisor
|
|
171
|
+
@runtime_context = runtime_context
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def execute(args = {})
|
|
175
|
+
sup = resolve_supervisor(@supervisor, @runtime_context)
|
|
176
|
+
return ToolOutput.error("SubagentSupervisor is not available in runtime context.") unless sup
|
|
177
|
+
|
|
178
|
+
action = (args["action"] || args[:action] || "list").to_s.downcase.strip
|
|
179
|
+
# Dispatch keys on the canonical form projected from Narration::ACTION_SPECS,
|
|
180
|
+
# so schema enum, help text, and these arms can never drift apart.
|
|
181
|
+
case Rubik::Orchestrator::Subagent::Narration.canonical_action(action)
|
|
182
|
+
when "list"
|
|
183
|
+
list_action(sup)
|
|
184
|
+
when "status"
|
|
185
|
+
id = resolve_id(args)
|
|
186
|
+
return ToolOutput.error("Missing required parameter 'conversation_id' for action 'status'.") if id.empty?
|
|
187
|
+
|
|
188
|
+
inst = sup.get(id)
|
|
189
|
+
return ToolOutput.error("Subagent '#{id}' not found.") unless inst
|
|
190
|
+
|
|
191
|
+
timeout = (args["timeout"] || args[:timeout]).to_i
|
|
192
|
+
if timeout.positive? && inst.running?
|
|
193
|
+
sup.wait_for([id], timeout: timeout)
|
|
194
|
+
inst = sup.get(id) || inst
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
data = inst.to_h
|
|
198
|
+
guidance = Rubik::Orchestrator::Subagent::Narration.guidance(inst.state, id: inst.id, max_steps: inst.max_steps)
|
|
199
|
+
if guidance
|
|
200
|
+
data[:guidance] = guidance
|
|
201
|
+
elsif inst.running?
|
|
202
|
+
data[:guidance] = Rubik::Orchestrator::Subagent::Narration.guidance(
|
|
203
|
+
:running, id: inst.id, max_steps: inst.max_steps, step: inst.current_step, uptime: inst.uptime
|
|
204
|
+
)
|
|
205
|
+
end
|
|
206
|
+
JSON.pretty_generate(data)
|
|
207
|
+
when "wait"
|
|
208
|
+
timeout = [(args["timeout"] || args[:timeout] || Rubik::Orchestrator::Subagent::Notices::DEFAULT_WAIT_TIMEOUT).to_i, 1].max
|
|
209
|
+
ids = resolve_ids(args) # empty = wait for ALL (wait_any_hint contract)
|
|
210
|
+
results = sup.wait_for(ids, timeout: timeout)
|
|
211
|
+
return "No active subagents to wait for.#{unknown_ids_hint(sup, ids)}" if results.empty?
|
|
212
|
+
|
|
213
|
+
lines = ["Subagents wait finished (#{results.size} tracked):"]
|
|
214
|
+
results.each do |r|
|
|
215
|
+
lines << " • [#{r[:id]}] '#{r[:role]}' (#{r[:type]}) - #{r[:state].to_s.upcase}"
|
|
216
|
+
res = (r[:result] || r[:result_summary]).to_s.strip
|
|
217
|
+
lines << " #{truncated_result_line(res, r)}" unless res.empty?
|
|
218
|
+
lines << " Guidance: #{guidance}" if (guidance = Rubik::Orchestrator::Subagent::Narration.guidance(r[:state], id: r[:id], max_steps: r[:max_steps]))
|
|
219
|
+
end
|
|
220
|
+
lines.join("\n")
|
|
221
|
+
when "transcript"
|
|
222
|
+
id = resolve_id(args)
|
|
223
|
+
return ToolOutput.error("Missing required parameter 'conversation_id' for action 'transcript'.") if id.empty?
|
|
224
|
+
|
|
225
|
+
inst = sup.get(id)
|
|
226
|
+
return ToolOutput.error("Subagent '#{id}' not found.") unless inst
|
|
227
|
+
return ToolOutput.error("Subagent '#{id}' is #{inst.state} — no transcript yet.") unless inst.respond_to?(:format_transcript)
|
|
228
|
+
|
|
229
|
+
inst.format_transcript
|
|
230
|
+
when "extend_budget"
|
|
231
|
+
id = resolve_id(args)
|
|
232
|
+
return ToolOutput.error("Missing required parameter 'conversation_id' for action 'extend_budget'.") if id.empty?
|
|
233
|
+
|
|
234
|
+
steps = Rubik::Orchestrator::Subagent::Budget.normalize_grant(args["steps"] || args[:steps])
|
|
235
|
+
if sup.extend_budget(id, steps)
|
|
236
|
+
"Step budget for subagent '#{id}' successfully extended by #{steps} steps (total budget: #{sup.get(id).max_steps}, state: #{sup.get(id).state.to_s.upcase})."
|
|
237
|
+
else
|
|
238
|
+
ToolOutput.error("Subagent '#{id}' not found or cannot extend budget (current state: #{sup.get(id)&.state || 'not found'}).")
|
|
239
|
+
end
|
|
240
|
+
when "resume"
|
|
241
|
+
id = resolve_id(args)
|
|
242
|
+
if id.empty?
|
|
243
|
+
resumed = sup.resume_all
|
|
244
|
+
return resumed.empty? ? "No paused subagents to resume." : "Resumed subagent(s): #{resumed.join(', ')}."
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
if sup.resume(id, additional_steps: args["steps"] || args[:steps])
|
|
248
|
+
"Subagent '#{id}' successfully resumed (state: #{sup.get(id).state.to_s.upcase})."
|
|
249
|
+
else
|
|
250
|
+
ToolOutput.error("Subagent '#{id}' cannot be resumed (current state: #{sup.get(id)&.state || 'not found'}).")
|
|
251
|
+
end
|
|
252
|
+
when "redeploy"
|
|
253
|
+
redeploy_action(sup, args)
|
|
254
|
+
when "reap"
|
|
255
|
+
reap_action(sup)
|
|
256
|
+
when "kill"
|
|
257
|
+
ids = resolve_ids(args)
|
|
258
|
+
return ToolOutput.error("Missing required parameter 'conversation_ids' for action 'kill'.#{id_format_hint}") if ids.empty?
|
|
259
|
+
|
|
260
|
+
"Killed subagent(s): #{ids.filter_map { |cid| sup.kill(cid, cascade: true) ? cid : nil }.join(', ')}."
|
|
261
|
+
when "kill_all"
|
|
262
|
+
sup.kill_all && "All active subagents have been terminated."
|
|
263
|
+
else
|
|
264
|
+
ToolOutput.error("Unknown manage_subagents action: '#{action}'. Valid: #{Rubik::Orchestrator::Subagent::Narration::CANONICAL_NAMES.join(', ')}.")
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
ID_FORMAT_EXAMPLE = "Pass ids as a JSON array (conversation_ids: [\"subagent-5\", \"subagent-6\"]) " \
|
|
269
|
+
"or a single id (conversation_id: 'subagent-5')."
|
|
270
|
+
|
|
271
|
+
private
|
|
272
|
+
|
|
273
|
+
def resolve_id(args)
|
|
274
|
+
resolve_ids(args).first.to_s
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# Model-supplied id dialects (JSON array / array-string / comma list /
|
|
278
|
+
# quoted single id) normalize in ConversationId — the parse-side
|
|
279
|
+
# vocabulary shared with send_message.
|
|
280
|
+
def resolve_ids(args)
|
|
281
|
+
raw = args["conversation_ids"] || args[:conversation_ids] ||
|
|
282
|
+
args["conversation_id"] || args[:conversation_id] ||
|
|
283
|
+
args["id"] || args[:id]
|
|
284
|
+
Rubik::Orchestrator::Subagent::ConversationId.normalize_list(raw)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def id_format_hint
|
|
288
|
+
" #{ID_FORMAT_EXAMPLE}"
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Wait returning nothing: finished-and-reaped or nonexistent — the
|
|
292
|
+
# supervisor roster is the tiebreaker, so the model gets a correction.
|
|
293
|
+
def unknown_ids_hint(sup, ids)
|
|
294
|
+
known = (sup.respond_to?(:list) ? sup.list : []).map(&:id)
|
|
295
|
+
missing = ids - known
|
|
296
|
+
missing.empty? ? "" : " Unknown id(s): #{missing.join(', ')}.#{id_format_hint}"
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Truncation hands the model the DIRECT read path (transcript URI) —
|
|
300
|
+
# session review B10: retrieving the full report cost an extra call.
|
|
301
|
+
# Returns the line WITHOUT indentation; the caller adds the prefix.
|
|
302
|
+
def truncated_result_line(res, row)
|
|
303
|
+
return "Result: #{res}" if res.length <= 1500
|
|
304
|
+
|
|
305
|
+
uri = row[:transcript].to_s
|
|
306
|
+
more = uri.empty? ? "manage_subagents(action: 'transcript', conversation_id: '#{row[:id]}')" : "read_file(path: '#{uri}')"
|
|
307
|
+
"Result: #{res[0, 1500]}... [truncated — full result: #{more}]"
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# Tool for dynamically defining a new subagent archetype at runtime (Plan 94).
|
|
312
|
+
class DefineSubagentTool < BaseTool
|
|
313
|
+
include SubagentToolHelpers
|
|
314
|
+
|
|
315
|
+
DECLARATION = Declaration.new(
|
|
316
|
+
name: "define_subagent", aliases: ["register_subagent_type", "create_subagent_type"],
|
|
317
|
+
description: "Defines a new archetype of subagent that can be invoked via invoke_subagent.",
|
|
318
|
+
risk: :execute, subagent_spawn: true, maestro_blocked: false, # self-evolution plane (TOOLS.md maestro policy)
|
|
319
|
+
params_schema: {
|
|
320
|
+
type: "object",
|
|
321
|
+
properties: {
|
|
322
|
+
name: { type: "string", description: "Unique name for archetype" },
|
|
323
|
+
description: { type: "string", description: "Human-readable description" },
|
|
324
|
+
system_prompt: { type: "string", description: "Detailed system prompt" },
|
|
325
|
+
enable_write_tools: { type: "boolean", description: "Equip subagent with write tools (default: false)" },
|
|
326
|
+
enable_subagent_tools: { type: "boolean", description: "Allow subagent to spawn subagents (default: false)" }
|
|
327
|
+
},
|
|
328
|
+
required: ["name", "description", "system_prompt"]
|
|
329
|
+
}
|
|
330
|
+
)
|
|
331
|
+
def initialize(supervisor: nil, runtime_context: nil)
|
|
332
|
+
super(DECLARATION)
|
|
333
|
+
@supervisor = supervisor
|
|
334
|
+
@runtime_context = runtime_context
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def execute(args = {})
|
|
338
|
+
sup = resolve_supervisor(@supervisor, @runtime_context)
|
|
339
|
+
return ToolOutput.error("SubagentSupervisor is not available in runtime context.") unless sup
|
|
340
|
+
|
|
341
|
+
name = (args["name"] || args[:name]).to_s.strip
|
|
342
|
+
desc = (args["description"] || args[:description]).to_s.strip
|
|
343
|
+
prompt = (args["system_prompt"] || args[:system_prompt]).to_s.strip
|
|
344
|
+
return ToolOutput.error("Parameters 'name', 'description', and 'system_prompt' are required.") if name.empty? || desc.empty? || prompt.empty?
|
|
345
|
+
|
|
346
|
+
type_def = sup.type_registry.define_custom(
|
|
347
|
+
name: name, description: desc, system_prompt: prompt,
|
|
348
|
+
enable_write_tools: args["enable_write_tools"] || args[:enable_write_tools] || false,
|
|
349
|
+
enable_subagent_tools: args["enable_subagent_tools"] || args[:enable_subagent_tools] || false
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
"Subagent archetype '#{type_def.name}' successfully defined and available for invoke_subagent."
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../output"
|
|
4
|
+
require_relative "../../orchestrator/subagent/budget"
|
|
5
|
+
require_relative "../../orchestrator/subagent/notices"
|
|
6
|
+
require_relative "../../orchestrator/subagent/narration"
|
|
7
|
+
|
|
8
|
+
module Rubik
|
|
9
|
+
module Tools
|
|
10
|
+
# Lifecycle actions + guidance renderings for ManageSubagentsTool.
|
|
11
|
+
# Instance-method module on purpose (never a tool class): the builtin
|
|
12
|
+
# registry stays at 31 — redeploy/reap are ACTIONS of manage_subagents.
|
|
13
|
+
module SubagentLifecycle
|
|
14
|
+
# Context-preserving reuse: hand a NEW task to a finished subagent
|
|
15
|
+
# instead of re-spawning and re-explaining (maestro persona workflow).
|
|
16
|
+
def redeploy_action(sup, args)
|
|
17
|
+
id = resolve_id(args)
|
|
18
|
+
return ToolOutput.error("Missing required parameter 'conversation_id' for action 'redeploy'.") if id.empty?
|
|
19
|
+
|
|
20
|
+
prompt = (args["prompt"] || args[:prompt]).to_s.strip
|
|
21
|
+
return ToolOutput.error("Missing required parameter 'prompt' (the new task) for action 'redeploy'.") if prompt.empty?
|
|
22
|
+
|
|
23
|
+
inst = sup.redeploy(id, prompt: prompt, max_steps: args["steps"] || args[:steps])
|
|
24
|
+
"Redeployed subagent '#{id}' with a new task — context preserved (#{inst.history.size} messages), " \
|
|
25
|
+
"fresh budget: #{inst.max_steps} steps, state: #{inst.state.to_s.upcase}. It runs in the background."
|
|
26
|
+
rescue StandardError => e
|
|
27
|
+
ToolOutput.error(e.message)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Diligent cleanup: tombstone every finished subagent NOW (in-memory
|
|
31
|
+
# history reclaimed; durable transcripts and storage index survive).
|
|
32
|
+
def reap_action(sup)
|
|
33
|
+
reaped = sup.reap_finished!(force: true)
|
|
34
|
+
return "No finished subagents to reap." if reaped.empty?
|
|
35
|
+
|
|
36
|
+
"Reaped #{reaped.size} finished subagent(s): #{reaped.join(', ')} — in-memory context reclaimed; " \
|
|
37
|
+
"transcripts remain readable via their transcript URIs and rubik://storage search."
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Blocked states that need maestro action before their chain proceeds
|
|
41
|
+
# (bug-llm-timeout-observability ④): a bare JSON list made a stalled
|
|
42
|
+
# subagent indistinguishable from a healthy one at a glance.
|
|
43
|
+
ATTENTION_STATES = [:paused_error, :waiting_budget, :budget_exhausted, :waiting_input].freeze
|
|
44
|
+
|
|
45
|
+
# List with a prominent attention block: any blocked subagent is called
|
|
46
|
+
# out ABOVE the JSON with its state, last error, and the exact recovery
|
|
47
|
+
# action, so a silent stall can't hide inside the pretty-printed rows.
|
|
48
|
+
def list_action(sup)
|
|
49
|
+
instances = sup.list
|
|
50
|
+
return "No active or tracked subagents." if instances.empty?
|
|
51
|
+
|
|
52
|
+
lines = []
|
|
53
|
+
attention = instances.select { |i| ATTENTION_STATES.include?(i.state.to_sym) }
|
|
54
|
+
if attention.any?
|
|
55
|
+
lines << "⚠ #{attention.size} subagent(s) need attention:"
|
|
56
|
+
attention.each do |i|
|
|
57
|
+
err = i.last_error.to_s
|
|
58
|
+
# Tail-preserving slice (AutoApply.capped_reason doctrine): OS and
|
|
59
|
+
# provider errors carry the actionable part at the end.
|
|
60
|
+
detail = err.empty? ? "" : " — #{err[-120, 120]}"
|
|
61
|
+
guidance = Rubik::Orchestrator::Subagent::Narration.guidance(i.state, id: i.id, max_steps: i.max_steps) ||
|
|
62
|
+
"Run manage_subagents(action: 'status', conversation_id: '#{i.id}') for details."
|
|
63
|
+
lines << " • [#{i.id}] '#{i.role}' — #{i.state.to_s.upcase}#{detail}: #{guidance}"
|
|
64
|
+
end
|
|
65
|
+
lines << ""
|
|
66
|
+
end
|
|
67
|
+
lines << JSON.pretty_generate(instances.map(&:to_h))
|
|
68
|
+
lines.join("\n")
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|