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,326 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "token_estimator"
|
|
4
|
+
require_relative "conversation_rows"
|
|
5
|
+
require_relative "../tools/truncator"
|
|
6
|
+
|
|
7
|
+
module Rubik
|
|
8
|
+
module Prompt
|
|
9
|
+
module ContextCompactor
|
|
10
|
+
# Plan 61 D-12: single source of truth for the live conversation
|
|
11
|
+
# window cap (TurnRecorder and Session::Manager both use it).
|
|
12
|
+
DEFAULT_MAX_CONVERSATION_ENTRIES = 40
|
|
13
|
+
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
def effective_input_ceiling(context_limit:, max_output_tokens: 4096, safety_margin: 1024)
|
|
17
|
+
[1024, context_limit.to_i - max_output_tokens.to_i - safety_margin.to_i].max
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def needs_compaction?(history, context_limit:, max_output_tokens: 4096, safety_margin: 1024, max_tokens_ratio: 0.85, config: {})
|
|
21
|
+
return false if history.nil? || history.empty?
|
|
22
|
+
|
|
23
|
+
ratio = (config["max_tokens_ratio"] || config[:max_tokens_ratio] || max_tokens_ratio || 0.85).to_f
|
|
24
|
+
margin = (config["safety_margin_tokens"] || config[:safety_margin_tokens] || safety_margin || 1024).to_i
|
|
25
|
+
max_out = (config["max_output_tokens"] || config[:max_output_tokens] || max_output_tokens || 4096).to_i
|
|
26
|
+
|
|
27
|
+
ceiling = effective_input_ceiling(
|
|
28
|
+
context_limit: context_limit,
|
|
29
|
+
max_output_tokens: max_out,
|
|
30
|
+
safety_margin: margin
|
|
31
|
+
)
|
|
32
|
+
threshold = (ceiling * ratio).to_i
|
|
33
|
+
total_tokens = TokenEstimator.estimate_history(history)
|
|
34
|
+
total_tokens > threshold
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# absorb: (goal-ledger planı D) duck-typed directive — nil (default)
|
|
38
|
+
# iken tier atlanır ve davranış bit-bit aynıdır; verilirse dup
|
|
39
|
+
# kopyaya #apply!(rows) uygulanır (prompt katmanı goal bilmez;
|
|
40
|
+
# directive'i orchestrator/goal_ledger ailesi kurar).
|
|
41
|
+
def compact_history(history, context_limit:, max_output_tokens: 4096, safety_margin: 1024, config: {}, force: false, absorb: nil)
|
|
42
|
+
return [] if history.nil? || history.empty?
|
|
43
|
+
|
|
44
|
+
strategy = (config["compaction_strategy"] || config[:compaction_strategy] || "hybrid").to_s
|
|
45
|
+
return history.map { |m| ConversationRows.row?(m) ? m.dup : m } if strategy == "off"
|
|
46
|
+
|
|
47
|
+
max_tokens_ratio = (config["max_tokens_ratio"] || config[:max_tokens_ratio] || 0.85).to_f
|
|
48
|
+
tool_result_prune_chars = (config["tool_result_prune_chars"] || config[:tool_result_prune_chars] || 1500).to_i
|
|
49
|
+
preserve_first_n = (config["preserve_first_n"] || config[:preserve_first_n] || 2).to_i
|
|
50
|
+
preserve_last_n = (config["preserve_last_n"] || config[:preserve_last_n] || 6).to_i
|
|
51
|
+
safety_margin = (config["safety_margin_tokens"] || config[:safety_margin_tokens] || safety_margin || 1024).to_i
|
|
52
|
+
|
|
53
|
+
ceiling = effective_input_ceiling(
|
|
54
|
+
context_limit: context_limit,
|
|
55
|
+
max_output_tokens: max_output_tokens,
|
|
56
|
+
safety_margin: safety_margin
|
|
57
|
+
)
|
|
58
|
+
threshold = (ceiling * max_tokens_ratio).to_i
|
|
59
|
+
|
|
60
|
+
# Return duplicate if under threshold and not forced
|
|
61
|
+
if !force && TokenEstimator.estimate_history(history) <= threshold
|
|
62
|
+
return history.map { |m| ConversationRows.row?(m) ? m.dup : m }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
compacted = history.map { |m| ConversationRows.row?(m) ? m.dup : m }
|
|
66
|
+
|
|
67
|
+
# Tier 0 (plan D): absorption — generic drop'lardan ÖNCE; absorb
|
|
68
|
+
# tek başına eşiği karşıladıysa (force yoksa) generic tier'lar hiç
|
|
69
|
+
# koşmaz (:72 erken-çıkış kalıbının aynası).
|
|
70
|
+
if absorb
|
|
71
|
+
absorb.apply!(compacted)
|
|
72
|
+
return compacted if !force && TokenEstimator.estimate_history(compacted) <= threshold
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Strategy: prune_tool_results or hybrid (Phase 1: Pruning intermediate tool results)
|
|
76
|
+
if ["hybrid", "prune_tool_results"].include?(strategy)
|
|
77
|
+
compacted = prune_intermediate_tool_results(
|
|
78
|
+
compacted,
|
|
79
|
+
preserve_first_n: preserve_first_n,
|
|
80
|
+
preserve_last_n: preserve_last_n,
|
|
81
|
+
max_chars: tool_result_prune_chars
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
return compacted if strategy == "prune_tool_results" || (!force && TokenEstimator.estimate_history(compacted) <= threshold)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Strategy: sliding_window or hybrid (Phase 2: Collapsing intermediate atomic units)
|
|
88
|
+
if ["hybrid", "sliding_window"].include?(strategy)
|
|
89
|
+
compacted = apply_sliding_window(
|
|
90
|
+
compacted,
|
|
91
|
+
threshold: threshold,
|
|
92
|
+
preserve_first_n: preserve_first_n,
|
|
93
|
+
preserve_last_n: preserve_last_n,
|
|
94
|
+
force: force
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
compacted
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def prune_intermediate_tool_results(history, preserve_first_n:, preserve_last_n:, max_chars:)
|
|
102
|
+
return history if history.size <= preserve_first_n + preserve_last_n
|
|
103
|
+
|
|
104
|
+
first_end_idx = preserve_first_n
|
|
105
|
+
last_start_idx = [history.size - preserve_last_n, first_end_idx].max
|
|
106
|
+
|
|
107
|
+
history.each_with_index do |msg, idx|
|
|
108
|
+
next unless idx >= first_end_idx && idx < last_start_idx
|
|
109
|
+
next unless ConversationRows.row?(msg)
|
|
110
|
+
|
|
111
|
+
role = ConversationRows.role_of(msg)
|
|
112
|
+
next unless ["tool", "tool_result"].include?(role)
|
|
113
|
+
|
|
114
|
+
# A loaded skill body is the task's standing instruction payload
|
|
115
|
+
# (progressive disclosure), not intermediate output — pruning it to
|
|
116
|
+
# max_chars mid-task would silently amputate the agent's orders.
|
|
117
|
+
# The sliding-window phase may still drop it under genuine token
|
|
118
|
+
# emergency; the <skills> index lets the agent re-invoke `skill`.
|
|
119
|
+
next if (msg[:tool_name] || msg["tool_name"]) == Rubik::Tools::SkillTool::NAME
|
|
120
|
+
|
|
121
|
+
content_key = ConversationRows.content_key_of(msg)
|
|
122
|
+
content = msg[content_key].to_s
|
|
123
|
+
if content.length > max_chars
|
|
124
|
+
trunc = Rubik::Tools::Truncator.truncate(content, max_chars: max_chars, strategy: :head_tail)
|
|
125
|
+
msg[content_key] = trunc[:text]
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
history
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def group_atomic_units(history)
|
|
133
|
+
units = []
|
|
134
|
+
i = 0
|
|
135
|
+
while i < history.size
|
|
136
|
+
msg = history[i]
|
|
137
|
+
role = ConversationRows.role_of(msg)
|
|
138
|
+
tool_calls = msg[:tool_calls] || msg["tool_calls"]
|
|
139
|
+
|
|
140
|
+
if role == "assistant" && tool_calls && !tool_calls.empty?
|
|
141
|
+
unit = [msg]
|
|
142
|
+
j = i + 1
|
|
143
|
+
while j < history.size
|
|
144
|
+
next_msg = history[j]
|
|
145
|
+
next_role = ConversationRows.role_of(next_msg)
|
|
146
|
+
break unless ["tool", "tool_result"].include?(next_role)
|
|
147
|
+
|
|
148
|
+
unit << next_msg
|
|
149
|
+
j += 1
|
|
150
|
+
|
|
151
|
+
end
|
|
152
|
+
units << unit
|
|
153
|
+
i = j
|
|
154
|
+
else
|
|
155
|
+
units << [msg]
|
|
156
|
+
i += 1
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
units
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Hedef mesajı mı? <user_task> sarmalayıcısını yalnızca
|
|
163
|
+
# ContextBuilder.format_user_task üretir — goal/plan döngülerinin görev
|
|
164
|
+
# bildirgesi. Açılış+kapanış etiketi birlikte aranır: kullanıcı
|
|
165
|
+
# mesajına alıntılanmış yalnızca açılış etiketi yapıştırmak mesajı
|
|
166
|
+
# kalıcı olarak sabitleyip düşürülemez/devrilinemez hale getirirdi.
|
|
167
|
+
# String/symbol anahtar farkı umursanmaz.
|
|
168
|
+
def goal_message?(msg)
|
|
169
|
+
return false unless ConversationRows.row?(msg)
|
|
170
|
+
|
|
171
|
+
role = ConversationRows.role_of(msg)
|
|
172
|
+
content = ConversationRows.content_of(msg).to_s
|
|
173
|
+
role == "user" && content.include?("<user_task>") && content.include?("</user_task>")
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Yapılandırılmış goal ledger satırı mı? (goal-ledger planı A) —
|
|
177
|
+
# goal_message? ile aynı alıntı-tek-etiket disiplini: açılış
|
|
178
|
+
# `<goal_ledger ` + kapanış `</goal_ledger>` BİRLİKTE aranır; kullanıcı
|
|
179
|
+
# mesajına yapıştırılan tek etiket mesajı pınleyemez. Role "user"
|
|
180
|
+
# zorunludur (EmergencyCompactor hard tier user satırlarına dokunmaz).
|
|
181
|
+
def ledger_message?(msg)
|
|
182
|
+
return false unless ConversationRows.row?(msg)
|
|
183
|
+
|
|
184
|
+
role = ConversationRows.role_of(msg)
|
|
185
|
+
content = ConversationRows.content_of(msg).to_s
|
|
186
|
+
role == "user" && content.start_with?("<goal_ledger ") && content.include?("</goal_ledger>")
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# İsim korunur (pruner rules.rb:39 çağrısı yük taşıyor): pin ailesi
|
|
190
|
+
# artık hedef satırı VE güncel Σ satırını kapsar.
|
|
191
|
+
def unit_pins_goal?(unit)
|
|
192
|
+
unit.any? { |msg| goal_message?(msg) || ledger_message?(msg) }
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def apply_sliding_window(history, threshold:, preserve_first_n:, preserve_last_n:, force: false) # rubocop:disable Lint/UnusedMethodArgument -- arayüz uyumluluğu için force korunuyor
|
|
196
|
+
units = group_atomic_units(history)
|
|
197
|
+
return history if units.size <= 2
|
|
198
|
+
|
|
199
|
+
# Hedef sabitleme: <user_task> taşıyan birimler ARA birim olsa bile
|
|
200
|
+
# asla düşürülemez ve trim edilemez — model hedefini koşu ortasında
|
|
201
|
+
# kaybetmesin. Normal ve emergency yolların ikisi de compact_history
|
|
202
|
+
# üzerinden buraya akar. Birden çok <user_task> (run yeniden girişi)
|
|
203
|
+
# özgün sırasıyla sabitlenir.
|
|
204
|
+
pinned = units.map { |unit| unit_pins_goal?(unit) }
|
|
205
|
+
|
|
206
|
+
# Determine head units (covering at least preserve_first_n messages)
|
|
207
|
+
head_units = []
|
|
208
|
+
head_msg_count = 0
|
|
209
|
+
head_idx = 0
|
|
210
|
+
while head_idx < units.size && head_msg_count < preserve_first_n
|
|
211
|
+
head_units << units[head_idx]
|
|
212
|
+
head_msg_count += units[head_idx].size
|
|
213
|
+
head_idx += 1
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Determine tail units (covering at least preserve_last_n messages)
|
|
217
|
+
tail_units = []
|
|
218
|
+
tail_msg_count = 0
|
|
219
|
+
tail_idx = units.size - 1
|
|
220
|
+
while tail_idx >= head_idx && tail_msg_count < preserve_last_n
|
|
221
|
+
tail_units.unshift(units[tail_idx])
|
|
222
|
+
tail_msg_count += units[tail_idx].size
|
|
223
|
+
tail_idx -= 1
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
intermediate_indexes = (head_idx..tail_idx).to_a
|
|
227
|
+
return history if intermediate_indexes.empty?
|
|
228
|
+
|
|
229
|
+
# Sabitlenmiş birimler düşürülemeyeceğinden droppable kuyruğundan
|
|
230
|
+
# çıkarılır; yalnızca kalanlar (en eskiden yeniye) düşürülür.
|
|
231
|
+
droppable = intermediate_indexes.reject { |idx| pinned[idx] }
|
|
232
|
+
return history if droppable.empty?
|
|
233
|
+
|
|
234
|
+
dropped_indexes = []
|
|
235
|
+
remaining = droppable.dup
|
|
236
|
+
# Plan 61 D-9: drop first, THEN measure — the previous loop measured
|
|
237
|
+
# the candidate before shifting, so it could drop one unit too many.
|
|
238
|
+
until remaining.empty?
|
|
239
|
+
dropped_indexes << remaining.shift
|
|
240
|
+
break if TokenEstimator.estimate_history(assemble_window(units, dropped_indexes)) <= threshold
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
result = assemble_window(units, dropped_indexes)
|
|
244
|
+
|
|
245
|
+
# Unit dropping cannot shrink a single oversized user turn —
|
|
246
|
+
# hard-trim the largest remaining plain messages if still over.
|
|
247
|
+
# Sabitlenmiş aile (hedef <user_task> + Σ <goal_ledger>) bu katmandan muaftır.
|
|
248
|
+
if TokenEstimator.estimate_history(result) > threshold
|
|
249
|
+
pinned_indexes = result.each_index.select { |i| goal_message?(result[i]) || ledger_message?(result[i]) }
|
|
250
|
+
result = trim_oversized_messages(result, threshold: threshold, pinned_indexes: pinned_indexes)
|
|
251
|
+
end
|
|
252
|
+
result
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Birimleri özgün sıralarında (düşürülenler hariç) birleştirir ve
|
|
256
|
+
# <compacted_summary>'yi İLK düşürülen birimin konumuna yerleştirir.
|
|
257
|
+
# Sabitleme yoksa ilk düşürme her zaman ilk ara birimdir → yerleşim,
|
|
258
|
+
# eski "head + summary + rest" düzeniyle bire bir aynıdır.
|
|
259
|
+
def assemble_window(units, dropped_indexes)
|
|
260
|
+
first_drop = dropped_indexes.min
|
|
261
|
+
result = []
|
|
262
|
+
summary_slot = nil
|
|
263
|
+
units.each_with_index do |unit, idx|
|
|
264
|
+
summary_slot = result.size if idx == first_drop
|
|
265
|
+
result.concat(unit) unless dropped_indexes.include?(idx)
|
|
266
|
+
end
|
|
267
|
+
dropped_msg_count = dropped_indexes.sum { |i| units[i].size }
|
|
268
|
+
result.insert(summary_slot || result.size, summary_message(dropped_msg_count))
|
|
269
|
+
result
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Plan 61 D-9: last-resort in-place shortening of the largest
|
|
273
|
+
# user/assistant messages when structural dropping is not enough.
|
|
274
|
+
# pinned_indexes (hedef <user_task> mesajları) bu katmandan muaftır.
|
|
275
|
+
def trim_oversized_messages(history, threshold:, min_len: 2000, max_passes: 10, pinned_indexes: [])
|
|
276
|
+
max_passes.times do
|
|
277
|
+
break if TokenEstimator.estimate_history(history) <= threshold
|
|
278
|
+
|
|
279
|
+
candidates = history.each_index.select { |i| ConversationRows.row?(history[i]) && !pinned_indexes.include?(i) }
|
|
280
|
+
eligible = candidates.select do |i|
|
|
281
|
+
role = ConversationRows.role_of(history[i])
|
|
282
|
+
content = ConversationRows.content_of(history[i])
|
|
283
|
+
["user", "assistant"].include?(role) && content.to_s.length > min_len
|
|
284
|
+
end
|
|
285
|
+
idx = eligible.max_by { |i| ConversationRows.content_of(history[i]).to_s.length }
|
|
286
|
+
break if idx.nil?
|
|
287
|
+
|
|
288
|
+
content_key = ConversationRows.content_key_of(history[idx])
|
|
289
|
+
str = history[idx][content_key].to_s
|
|
290
|
+
trunc = Rubik::Tools::Truncator.truncate(str, max_chars: str.length / 2, max_lines: 10_000, strategy: :head_tail)
|
|
291
|
+
history[idx][content_key] = trunc[:text]
|
|
292
|
+
end
|
|
293
|
+
history
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# Plan 61 D-12: token-aware rolling window for the long-lived REPL
|
|
297
|
+
# conversation history. Drops the oldest entries until the estimate
|
|
298
|
+
# fits the ceiling; max_entries is an absolute cap; the most recent
|
|
299
|
+
# min_entries messages always survive. The window is guaranteed to
|
|
300
|
+
# open with a user turn — strict providers (Anthropic) require it.
|
|
301
|
+
def trim_history_to_budget(history, ceiling:, max_entries: nil, min_entries: 4)
|
|
302
|
+
return history if history.nil? || history.empty?
|
|
303
|
+
|
|
304
|
+
trimmed = history
|
|
305
|
+
trimmed = trimmed.last(max_entries) if max_entries&.positive? && trimmed.size > max_entries
|
|
306
|
+
|
|
307
|
+
limit = ceiling&.positive? ? ceiling : nil
|
|
308
|
+
trimmed = trimmed.last(trimmed.size - 1) while limit && trimmed.size > min_entries && TokenEstimator.estimate_history(trimmed) > limit
|
|
309
|
+
|
|
310
|
+
trimmed = trimmed.last(trimmed.size - 1) while trimmed.size > 1 && ConversationRows.role_of(trimmed.first) != "user"
|
|
311
|
+
trimmed
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# Plan 61 D-9: role is "user" (not "system") — strict providers reject
|
|
315
|
+
# or relocate mid-conversation system messages; the tag text is kept
|
|
316
|
+
# stable for downstream recognition.
|
|
317
|
+
def summary_message(dropped_count)
|
|
318
|
+
{
|
|
319
|
+
role: "user",
|
|
320
|
+
content: "<compacted_summary count=\"#{dropped_count}\" " \
|
|
321
|
+
"message=\"#{dropped_count} intermediate conversation messages and tool execution steps were collapsed to stay within token limits.\" />"
|
|
322
|
+
}
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubik
|
|
4
|
+
module Prompt
|
|
5
|
+
# Single owner of the conversation-row wire dialect (Candidate 7,
|
|
6
|
+
# arch-deepening 2026-10-08). History rows arrive in two dialects:
|
|
7
|
+
# symbol-keyed ({ role:, content: } — everything this runtime pushes)
|
|
8
|
+
# and string-keyed (restored sessions, external providers). Every
|
|
9
|
+
# probe below is pure; append_notice! is the only mutator (bang =
|
|
10
|
+
# in-place, explicit per the side-effect policy).
|
|
11
|
+
module ConversationRows
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# Row guard: nils and non-hash junk rows appear from restored
|
|
15
|
+
# sessions and test doubles.
|
|
16
|
+
def row?(row) = row.is_a?(Hash)
|
|
17
|
+
|
|
18
|
+
# Raw read in both dialects (no type coercion — callers keep their
|
|
19
|
+
# own nil/to_s handling).
|
|
20
|
+
def value_of(row, key) = row[key] || row[key.to_s]
|
|
21
|
+
|
|
22
|
+
# Role read, both dialects, stringified (the
|
|
23
|
+
# `(row[:role] || row["role"]).to_s` idiom of sanitizer/compactors).
|
|
24
|
+
def role_of(row) = value_of(row, :role).to_s
|
|
25
|
+
|
|
26
|
+
# Content read, both dialects. Returns the raw value (may be nil) —
|
|
27
|
+
# callers keep their own .to_s / length logic.
|
|
28
|
+
def content_of(row) = value_of(row, :content)
|
|
29
|
+
|
|
30
|
+
# Key resolution for WRITES: a row that already carries the symbol
|
|
31
|
+
# key is written back on the symbol; anything else (string-keyed, or
|
|
32
|
+
# missing the key entirely) is written on the STRING key. This exact
|
|
33
|
+
# asymmetry is load-bearing behavior pinned by the merge tests.
|
|
34
|
+
def resolve_key(row, key)
|
|
35
|
+
sym = key.to_sym
|
|
36
|
+
row.key?(sym) ? sym : key.to_s
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def content_key_of(row) = resolve_key(row, :content)
|
|
40
|
+
|
|
41
|
+
# Merge-policy predicate. Deliberately NOT role_of-based: the merge
|
|
42
|
+
# compares exact string equality (a Symbol :user role must NOT
|
|
43
|
+
# match), preserving current behavior bit-for-bit.
|
|
44
|
+
def user_row?(row) = row[:role] == "user" || row["role"] == "user"
|
|
45
|
+
|
|
46
|
+
# The notice-merge policy, previously duplicated in
|
|
47
|
+
# Orchestrator::Loop#merge_notice_block_into_history! and
|
|
48
|
+
# Lifecycle#inject_subagent_notice_into_conversation_history!.
|
|
49
|
+
# Mutates history in place and returns it (both current call sites
|
|
50
|
+
# are statement-position and rely on the in-place mutation).
|
|
51
|
+
#
|
|
52
|
+
# Semantics (byte-identical to both originals):
|
|
53
|
+
# * empty history or non-user trailing row -> push { role: "user",
|
|
54
|
+
# content: payload } with SYMBOL keys;
|
|
55
|
+
# * trailing user row -> append on its existing key dialect:
|
|
56
|
+
# "existing\n\npayload"; a row whose content is empty or absent
|
|
57
|
+
# gets the payload written directly (absent key resolves to the
|
|
58
|
+
# STRING "content" key — see resolve_key).
|
|
59
|
+
def append_notice!(history, xml_payload)
|
|
60
|
+
raise ArgumentError, "history must be an Array, got #{history.class}" unless history.is_a?(Array)
|
|
61
|
+
|
|
62
|
+
last = history.last
|
|
63
|
+
if last && user_row?(last)
|
|
64
|
+
key = content_key_of(last)
|
|
65
|
+
existing = last[key].to_s
|
|
66
|
+
last[key] = existing.empty? ? xml_payload : "#{existing}\n\n#{xml_payload}"
|
|
67
|
+
else
|
|
68
|
+
history << { role: "user", content: xml_payload }
|
|
69
|
+
end
|
|
70
|
+
history
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../platform"
|
|
4
|
+
# Vocabulary leaf ONLY, never the orchestrator barrel: orchestrator sits
|
|
5
|
+
# above the prompt layer; this pure platform-only module renders the
|
|
6
|
+
# manage_subagents snippets the rules below teach. Do not widen this edge.
|
|
7
|
+
require_relative "../orchestrator/subagent/notices"
|
|
8
|
+
require_relative "tool_teachings"
|
|
9
|
+
|
|
10
|
+
module Rubik
|
|
11
|
+
module Prompt
|
|
12
|
+
# Doctrine SSOT (architecture review 2026-10-01, candidate 2): ALL
|
|
13
|
+
# policy prose the prompt teaches — operational rules, role headers,
|
|
14
|
+
# orchestration guidance, skills preamble, tool wire notes. Owns
|
|
15
|
+
# WORDING only: rendered data (tool names, schema dumps) arrives as
|
|
16
|
+
# parameters and services are never touched here (the orchestrator
|
|
17
|
+
# resolves, the prompt renders). module_function mirrors
|
|
18
|
+
# ToolTeachings/Notices. Byte-identical rendering is pinned by
|
|
19
|
+
# test_prompt_golden_output — reword here = fixture diff there, never
|
|
20
|
+
# a context_builder.rb diff.
|
|
21
|
+
module Doctrine
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
def operational_rules_xml
|
|
25
|
+
# Maestro doktrini (kullanıcı kararı 2026-10-01): maestro HER modda
|
|
26
|
+
# orkestratör. Devir caveat'i kural 2-3'ün BAŞINA yazılır — sondaki
|
|
27
|
+
# ek caveat zayıf modellerde atlanıyordu (opencode-personal bulgusu);
|
|
28
|
+
# doktrin kendi promptunun içinde çelişmemeli.
|
|
29
|
+
<<~XML.strip
|
|
30
|
+
<operational_rules>
|
|
31
|
+
1. Code Exploration & Review (read-only: shape subagent assignments and answer the user): Use native tools directly (`list_files`, `grep_search`, `search_symbols`, `get_outline`, `read_file`, `find_references`). If neither a tool nor a listed command matches your need, inspect the codebase structurally: examine the directory layout (`list_files`), search for patterns (`grep_search`), read core modules (`get_outline`, `read_file`), and look for TODO/FIXME markers. For conceptual queries (e.g. "code review", "code quality"), do NOT search for the literal words "refactor" or "review"; evaluate the architecture structurally.
|
|
32
|
+
2. Code Editing & Targeted Changes: MAESTRO OVERRIDE — this rule is delegated wholesale: route ALL edits through 'coder' subagents and never stage edits in your own context (the runtime refuses direct edit tools). The mechanics below are what your coder subagents use. #{Rubik::Prompt::ToolTeachings.teaching_for('replace_file_content')} #{Rubik::Prompt::ToolTeachings.teaching_for('write_file')} #{Rubik::Prompt::ToolTeachings.teaching_for('apply_buffer')}
|
|
33
|
+
3. Testing & Execution: MAESTRO OVERRIDE — this rule is delegated wholesale: subagents run tests and commands; you read their reports and never execute them yourself (the runtime refuses direct run tools). The mechanics below are what your coder subagents use: they invoke `run_tests` and `run_command` as native tools, never wrapped in `exec_repl_command`. Commands in `run_command` run directly in the project working directory (`<working_directory>`); coder subagents must not prepend `cd` commands or guess non-existent paths (e.g. `/home/ubuntu`). Always fulfill the user's primary requested action (e.g. merge, build, test, commit) to completion. #{Rubik::Prompt::ToolTeachings.teaching_for('run_command')}
|
|
34
|
+
4. REPL Bang Commands: `exec_repl_command` runs only the session-safe commands listed in <agent_commands> — runtime configuration, context and step-budget self-management, buffer review and rollback, runtime introspection, and code validation. If that section is absent, do not invoke REPL commands. File, code, test, and shell operations never travel through REPL commands — under the maestro doctrine (rules 2-3) they are delegated to subagents, and the direct tools (`read_file`, `write_file`, `replace_file_content`, `run_tests`, `run_command`) are your coder subagents' mechanics. Buffer-review commands (`!diff`, `!undo`, `!rollback`) operate on the in-memory staging buffer (unapplied edits) — they are a preview layer, not file ground truth: verify file content with `read_file`, and read git history from your coder subagent's `run_command` report.
|
|
35
|
+
5. Autonomous Tool Synthesis: #{Rubik::Prompt::ToolTeachings.teaching_for('synthesize_tool')}
|
|
36
|
+
6. Output Budget & Conciseness: Respect your output token budget (`<output_budget>`). Produce direct, high-signal responses and avoid unnecessary pleasantries, verbose conversational preambles, or duplicated context blocks that waste generation quota.
|
|
37
|
+
7. Subagent Delegation & Parallelism (Maestro): As the maestro you orchestrate — ALL execution work (exploration, code changes, verification) is delegated via `invoke_subagent`, in every mode. Match task to archetype: use 'research' for read-only codebase exploration; MUST use 'coder' for code modifications, refactoring, and test execution ('research' has no write or test tools); reach for 'self' (a full capability clone, minus subagent spawning) for work that fits neither archetype — runtime introspection/repair, self-evolution work, hybrid tasks. Subagents execute asynchronously in disposable sub-contexts in the background. After dispatching subagents, do NOT block with 'wait' unless you strictly depend on their output in the current turn — conclude your turn and liberate the prompt so the user can interact concurrently. Completion, budget-pause, error, and message notifications arrive reactively: during your turn via your notification inbox; while you are idle, the runtime automatically wakes you with a dedicated notification-evaluation turn — evaluate the pending notifications, act (extend budget, steer with send_message, retrieve results, wrap up, or report to the user), and conclude your turn again. Subagents can ask YOU questions mid-flight via `ask_question` — a question notification means that subagent is BLOCKED until you reply: answer it promptly with `send_message(recipient: '<id>', message: '<your answer>')` (it times out otherwise). Call `#{Rubik::Orchestrator::Subagent::Notices.wait_hint_template}` only when synchronous waiting is strictly required. Never poll 'status' repeatedly in a loop.
|
|
38
|
+
8. Runtime Introspection, Self-Healing & Human Feedback: If internal errors or service degradation occur, inspect runtime state with `inspect_runtime` and repair failed services via `repair_runtime_service`. When critical requirements are underspecified or high-impact architectural choices require user confirmation, use `ask_question` with structured multiple-choice options.
|
|
39
|
+
9. Central Storage & Clean Worktree: #{Rubik::Prompt::ToolTeachings.teaching_for('storage')} #{Rubik::Prompt::ToolTeachings.teaching_for('vakanuvis')} #{Rubik::Prompt::ToolTeachings.teaching_for('show_file')} Subagent transcripts are indexed automatically as `transcript` entities (alias `subagent-<storage-id>`). #{Rubik::Prompt::ToolTeachings.teaching_for('prune_context')}
|
|
40
|
+
</operational_rules>
|
|
41
|
+
XML
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Returns the UNSTRIPPED mode header; the assembler strips at embed
|
|
45
|
+
# time (verbatim move from build_system_prompt — rendered bytes are
|
|
46
|
+
# pinned by the golden fixtures). goal_ledger: goal-mode öğretisi
|
|
47
|
+
# koşulludur (goal-ledger planı A) — OFF'ta bayt-özdeş; öğreti
|
|
48
|
+
# TEACHINGS'e giremez (weld testi her öğretiyi operational_rules
|
|
49
|
+
# içinde zorunlu kılar), goal header'ın 6. maddesinde yaşar.
|
|
50
|
+
def role_header(mode, goal_ledger: false)
|
|
51
|
+
case mode.to_sym
|
|
52
|
+
when :plan
|
|
53
|
+
<<~HDR
|
|
54
|
+
You are Rubik (maestro), the orchestrator in PLAN-ONLY mode: you analyze, plan, and orchestrate; execution belongs to subagents.
|
|
55
|
+
Analyze the user's task within <user_task> and output a concise, numbered step-by-step implementation plan.
|
|
56
|
+
Do NOT call apply_buffer, write_file, replace_file_content, run_tests, or run_command — the maestro runtime refuses them in every mode; analysis and planning are your work. Proposed changes may sit in the buffer — do not apply them. Specify what files will be created or edited.
|
|
57
|
+
HDR
|
|
58
|
+
when :goal
|
|
59
|
+
ledger_line = " If a `<goal_ledger>` row is present, keep its state current: read `rubik://goal/ledger` and " \
|
|
60
|
+
"apply JSON patches with `update_goal_ledger(patch: ...)` against the reported rev after each " \
|
|
61
|
+
"meaningful step (task status changes, new findings/decisions)."
|
|
62
|
+
<<~HDR
|
|
63
|
+
You are Rubik (maestro), an autonomous AI coding SUPERVISOR in AUTONOMOUS GOAL mode.
|
|
64
|
+
You own the goal in <user_task> and MUST drive it to verified completion as a pure orchestrator — you conduct, subagents perform. All research, code changes, and verification run in subagent contexts, never in yours:
|
|
65
|
+
1. Decompose the goal into staged subagent work: at minimum a plan/research stage, a build stage, a review/audit stage whose findings feed a fix stage, and a final verification stage. You decide the exact stages. Declare cross-stage ordering IN THE SAME dispatch with `depends_on` (FS default: the successor launches automatically once the predecessor completes; SS: once it starts) — never fire a stage in parallel with its own predecessors.
|
|
66
|
+
2. Dispatch every stage with `invoke_subagent`. You MAY explore read-only yourself (`list_files`, `grep_search`, `read_file`) to shape assignments, but ALL code changes and test runs belong to subagents — MUST use the 'coder' archetype for any file modification ('research' is read-only).
|
|
67
|
+
3. Keep YOUR context lean and managerial: no code drafts, no test output, no full-file reads in your own context — consume stage reports, `rubik://storage/...` summaries, and notification digests instead. Answer subagent `ask_question` prompts promptly.
|
|
68
|
+
4. Work the notifications, never a poll loop: when subagents are dispatched and nothing needs your immediate action, end your turn WITHOUT calling finish — the runtime waits for them and wakes you with their completion/pause/error/question notices. Ending your turn (or calling finish) with subagents still active does NOT complete the goal: the runtime holds the goal open and re-prompts you with their notices. React to each notice (extend a budget-paused subagent (`manage_subagents` action: 'extend_budget') instead of absorbing its task yourself), then yield again. Conclude with finish ONLY when every dispatched subagent has settled and verification is green.
|
|
69
|
+
5. Close every stage through verification: read subagent reports, integrate verified results, and run final verification as a subagent stage — a 'coder' mandated to `run_tests` whose green/red report you read. Iterate the review→fix stages until the verifier reports green.
|
|
70
|
+
6. Live goal snapshot (status, step budget, extensions): read it with `read_file(path: 'rubik://goal/state')`.#{ledger_line if goal_ledger}
|
|
71
|
+
7. Persist pipeline knowledge in the central storage (`storage` tool): the decomposed plan, review findings, and the final goal record — link stages with entity ids (`links: {"goal": "goal.N.<hash16>"}`) so the audit trail survives the session. Per-stage MEMORY notes go through the chronicler instead: `vakanuvis(action: 'record', topic: '...', body: '...')` mints the `mem.` ref, upserts and supersedes — never write memory with raw storage puts; re-enter prior context with `vakanuvis(action: 'recall', query: '...')` (also after compaction). Keep the repository worktree free of agent workfiles. Transcripts of finished subagents are indexed automatically (searchable via `storage` action 'search') and persist after the subagent is reaped.
|
|
72
|
+
8. Call `finish` only when the goal is verified, complete, and no dispatched subagent remains active — a premature finish is converted into a wait and you will be re-prompted with their notices. Summarize what each stage produced.
|
|
73
|
+
HDR
|
|
74
|
+
else
|
|
75
|
+
<<~HDR
|
|
76
|
+
You are Rubik (maestro), a maestro coding orchestrator for the human operator.
|
|
77
|
+
You ALWAYS work through subagents: research, code changes, and verification run in subagent contexts via `invoke_subagent` (MUST use the 'coder' archetype for any file modification — 'research' is read-only), never in your own context.
|
|
78
|
+
Keep your own context lean and managerial: you MAY explore read-only (`list_files`, `grep_search`, `read_file`) to understand requests and shape assignments, but no code drafts, no test runs, no full-file dumps — consume subagent reports and `rubik://storage/...` summaries instead.
|
|
79
|
+
Focus on driving tasks to completion: decompose the user's request into subagent tasks, work the reactive notices (completion, budget pause, error, question), steer with `send_message` / `manage_subagents` ('extend_budget', 'redeploy'), and answer subagent `ask_question` prompts promptly — they block until you reply.
|
|
80
|
+
Prefer reuse over re-spawning: hand follow-up tasks to a finished subagent with `manage_subagents(action: 'redeploy', conversation_id: '<id>', prompt: '<new task>')` — its accumulated context survives.
|
|
81
|
+
Small talk, explanations of earlier reports, and questions you can answer from read-only context or subagent findings are yours directly; every unit of execution work belongs to a subagent.
|
|
82
|
+
HDR
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Guidance BULLET PROSE (no wrapper tags, no indent): the renderer
|
|
87
|
+
# owns the <orchestration_guidance> XML structure and indentation,
|
|
88
|
+
# Doctrine owns the policy sentences.
|
|
89
|
+
def orchestration_guidance_bullets
|
|
90
|
+
[
|
|
91
|
+
"Archetype selection: Use 'research' for read-only exploration; MUST use 'coder' for code changes/bug fixes " \
|
|
92
|
+
"(read-only agents cannot modify files, run tests, or run shell commands — git/gh plumbing and command " \
|
|
93
|
+
"output belong to a coder, plan that pass from the start).",
|
|
94
|
+
"Asynchronous execution: Subagents run concurrently in the background. Conclude your turn to yield: in " \
|
|
95
|
+
"interactive sessions this liberates the prompt for the user; in the autonomous goal loop the runtime " \
|
|
96
|
+
"waits for active subagents and wakes you with their notices — the goal is never complete while they " \
|
|
97
|
+
"run. Completion, pause, error, and message notices arrive reactively in your inbox and " \
|
|
98
|
+
"automatically wake you with a notification-evaluation turn while you are idle. Call " \
|
|
99
|
+
"`#{Rubik::Orchestrator::Subagent::Notices.wait_hint_template}` only if you have an immediate blocking " \
|
|
100
|
+
"dependency within the current turn. Do NOT poll 'status' in a loop.",
|
|
101
|
+
"Workspace isolation: 'inherit' shares project workspace directly; 'branch' isolates edits in a virtual " \
|
|
102
|
+
"buffer merged atomically upon completion.",
|
|
103
|
+
"Budget escalation: A subagent paused at its step budget (WAITING_BUDGET) is NOT terminal: " \
|
|
104
|
+
"`#{Rubik::Orchestrator::Subagent::Notices.extend_budget_hint('[...]')}` resumes it with context intact. " \
|
|
105
|
+
"Extend BEFORE absorbing the task yourself; re-spawn only if its transcript shows the objective changed.",
|
|
106
|
+
"Error recovery: A subagent paused on a provider/network error (PAUSED_ERROR) is also NOT terminal: " \
|
|
107
|
+
"`#{Rubik::Orchestrator::Subagent::Notices.resume_hint('[...]')}` retries it with context intact. " \
|
|
108
|
+
"Kill only if its transcript shows the error is permanent.",
|
|
109
|
+
"Context reuse: A finished (completed/failed) subagent not yet reaped holds its accumulated context — hand " \
|
|
110
|
+
"it a follow-up with `manage_subagents(action: 'redeploy', conversation_id: '<id>', prompt: '<new task>')` " \
|
|
111
|
+
"instead of re-spawning and re-explaining. A reaped (archived) subagent cannot be redeployed: spawn a " \
|
|
112
|
+
"fresh one.",
|
|
113
|
+
"Staged dispatch (dependencies): A spec may declare `depends_on` — an array of predecessor ids (strings ⇒ " \
|
|
114
|
+
"FS: launches when the predecessor completes) or `{'id': ..., 'type': 'SS'}` (launches when it starts). " \
|
|
115
|
+
"Queued specs hold no slot and cost nothing; `list`/`status` show them as QUEUED with their unmet deps, " \
|
|
116
|
+
"and `wait` accepts queued ids (blocks until the whole chain settles). A failed/killed predecessor " \
|
|
117
|
+
"CANCELS its dependents — re-dispatch with the new information; a budget- or error-paused predecessor " \
|
|
118
|
+
"keeps them queued — extend or resume it instead.",
|
|
119
|
+
"Chronicler briefs: when dispatching a subagent, paste the relevant chronicled knowledge into the task " \
|
|
120
|
+
"brief — the recall digest or a rubik://storage/... link — so the subagent pulls the depth itself. " \
|
|
121
|
+
"Subagents never see the `vakanuvis` tool; you are the only librarian.",
|
|
122
|
+
"Diligent cleanup: Finished subagents are reaped automatically after a grace period " \
|
|
123
|
+
"(`subagents.reap_finished_after`) — in-memory context is reclaimed while the durable transcript stays " \
|
|
124
|
+
"readable at its transcript URI and in `rubik://storage` search. `manage_subagents(action: 'reap')` " \
|
|
125
|
+
"reaps finished subagents immediately."
|
|
126
|
+
]
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def skills_preamble
|
|
130
|
+
"The skills below bundle specialized instructions. When a task matches one, invoke the `skill` tool with its " \
|
|
131
|
+
"name to load its full SKILL.md instructions first; files it references (scripts/, references/, assets/) " \
|
|
132
|
+
"are read with read_file relative to its directory."
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def native_tools_note(names)
|
|
136
|
+
<<~TLS
|
|
137
|
+
|
|
138
|
+
Tools are available via native function calling (the request's `tools` parameter): invoke them with native tool calls.
|
|
139
|
+
As a fallback, a JSON block also works:
|
|
140
|
+
```json
|
|
141
|
+
{ "tool": "tool_name", "args": { ... } }
|
|
142
|
+
```
|
|
143
|
+
Available tools: #{names}
|
|
144
|
+
TLS
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def json_tools_note(tool_schemas)
|
|
148
|
+
<<~TLS
|
|
149
|
+
|
|
150
|
+
You can call tools by outputting a JSON block:
|
|
151
|
+
```json
|
|
152
|
+
{ "tool": "tool_name", "args": { ... } }
|
|
153
|
+
```
|
|
154
|
+
Available tools:
|
|
155
|
+
#{Rubik::Platform.safe_json_dump(tool_schemas)}
|
|
156
|
+
TLS
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "context_compactor"
|
|
4
|
+
require_relative "conversation_rows"
|
|
5
|
+
require_relative "../tools/truncator"
|
|
6
|
+
|
|
7
|
+
module Rubik
|
|
8
|
+
module Prompt
|
|
9
|
+
# Mutating context compactor for hot paths. Where ContextCompactor
|
|
10
|
+
# is pure (returns a new history), EmergencyCompactor mutates the
|
|
11
|
+
# given history in place for callers that need a fast reduction
|
|
12
|
+
# without an extra allocation.
|
|
13
|
+
#
|
|
14
|
+
# Use ContextCompactor first; fall back to EmergencyCompactor
|
|
15
|
+
# only when a quick reduction is needed and the caller accepts
|
|
16
|
+
# the in-place mutation.
|
|
17
|
+
module EmergencyCompactor
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
# Plan 63 Faz A: tiered escalation. When the caller supplies a positive
|
|
21
|
+
# context_limit, the structural ContextCompactor (tool-result pruning,
|
|
22
|
+
# atomic-unit sliding window with a summary marker) runs first with
|
|
23
|
+
# force: true; the blind 300/1000-char truncation is the last resort,
|
|
24
|
+
# applied only when the history is still over budget afterwards.
|
|
25
|
+
# Without a context_limit the behavior is identical to the legacy
|
|
26
|
+
# single-layer path, so direct callers keep their exact contract.
|
|
27
|
+
#
|
|
28
|
+
# The history Array keeps its identity (in-place replace), so the caller
|
|
29
|
+
# holding it — the Loop retry paths — sees the compacted messages.
|
|
30
|
+
# Returns the layer that produced the final history (:structural or
|
|
31
|
+
# :hard_truncate); the nil/empty guards return the input unchanged
|
|
32
|
+
# (pinned by tests). The return value is observational: callers forward
|
|
33
|
+
# it on the :emergency_compact on_step event rather than branch on it.
|
|
34
|
+
def compact!(history, context_limit: nil, max_output_tokens: nil, config: {}, absorb: nil)
|
|
35
|
+
return history if history.nil? || history.empty?
|
|
36
|
+
|
|
37
|
+
config = {} unless config.is_a?(Hash) # a null `context:` YAML section makes fetch return nil
|
|
38
|
+
max_out = max_output_tokens || 4096
|
|
39
|
+
if context_limit&.positive?
|
|
40
|
+
structural = Rubik::Prompt::ContextCompactor.compact_history(
|
|
41
|
+
history, context_limit: context_limit,
|
|
42
|
+
max_output_tokens: max_out, config: config, force: true, absorb: absorb
|
|
43
|
+
)
|
|
44
|
+
history.replace(structural)
|
|
45
|
+
still_over = Rubik::Prompt::ContextCompactor.needs_compaction?(
|
|
46
|
+
history, context_limit: context_limit, max_output_tokens: max_out, config: config
|
|
47
|
+
)
|
|
48
|
+
return :structural unless still_over
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Hard-truncate layer strategy is configurable (context.emergency_truncate_strategy):
|
|
52
|
+
# "head_tail" (default, original behavior) keeps head + tail around the
|
|
53
|
+
# omission notice; "head_only" keeps the message head — useful when the
|
|
54
|
+
# actionable guidance at the tail must not crowd out the error header.
|
|
55
|
+
# Truncator normalizes strings/symbols and falls back to head_tail on
|
|
56
|
+
# unknown values.
|
|
57
|
+
strategy = config["emergency_truncate_strategy"] || config[:emergency_truncate_strategy] || "head_tail"
|
|
58
|
+
|
|
59
|
+
history.each do |msg|
|
|
60
|
+
next unless ConversationRows.row?(msg)
|
|
61
|
+
|
|
62
|
+
role = ConversationRows.role_of(msg)
|
|
63
|
+
content_key = ConversationRows.content_key_of(msg)
|
|
64
|
+
content = msg[content_key].to_s
|
|
65
|
+
|
|
66
|
+
if ["tool", "tool_result"].include?(role)
|
|
67
|
+
if content.length > 300
|
|
68
|
+
trunc = Rubik::Tools::Truncator.truncate(content, max_chars: 300, max_lines: 10, strategy: strategy)
|
|
69
|
+
msg[content_key] = trunc[:text]
|
|
70
|
+
end
|
|
71
|
+
elsif role == "assistant" && content.length > 1000
|
|
72
|
+
trunc = Rubik::Tools::Truncator.truncate(content, max_chars: 1000, max_lines: 30, strategy: strategy)
|
|
73
|
+
msg[content_key] = trunc[:text]
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
:hard_truncate
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|