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,223 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "monitor"
|
|
4
|
+
require_relative "frontmatter"
|
|
5
|
+
require_relative "storage"
|
|
6
|
+
require_relative "registry"
|
|
7
|
+
|
|
8
|
+
module Rubik
|
|
9
|
+
module Skills
|
|
10
|
+
# = Rubik::Skills::Manager
|
|
11
|
+
#
|
|
12
|
+
# Lifecycle facade of the Agent Skills subsystem (agentskills.io),
|
|
13
|
+
# provided as the +skill_manager+ coeffect (Composition.build →
|
|
14
|
+
# wire_runtime_context) and read per turn by the prompt builder.
|
|
15
|
+
#
|
|
16
|
+
# == AGENTS.md treatment
|
|
17
|
+
# A skill is DATA (frontmatter + markdown): never evaluated, no threads,
|
|
18
|
+
# no listeners — so the lifecycle-bearing unit is the Manager itself and
|
|
19
|
+
# the skills need no fibers. The Manager's single revertible effect is
|
|
20
|
+
# registering the +skill+ tool into the shared tool registry; that effect
|
|
21
|
+
# is opened on the Manager's OWN sub-context epoch (MCP::Manager
|
|
22
|
+
# pattern): enable/disable cycles extend/dispose the epoch, so inverses
|
|
23
|
+
# never stack across reloads (zero-restart rule, §1.A/§6) and disposal is
|
|
24
|
+
# local to the code that produced the effect (no detached teardown, §5).
|
|
25
|
+
class Manager
|
|
26
|
+
# Head-only body cap for the `skill` tool output. StepExecutor
|
|
27
|
+
# re-truncates every tool output on EITHER axis (tools.max_output_chars
|
|
28
|
+
# AND tools.max_output_lines) with head_tail, which would cut the middle
|
|
29
|
+
# out of ordered instructions — so Manager#body pre-truncates head-only
|
|
30
|
+
# below BOTH limits (see #body) and the read_file pointer is the
|
|
31
|
+
# recovery path for the omitted tail.
|
|
32
|
+
DEFAULT_MAX_BODY_CHARS = 8_000
|
|
33
|
+
# Lines the tool envelope adds around the body (3 header + truncation
|
|
34
|
+
# notice + recovery pointer) — headroom under the StepExecutor line
|
|
35
|
+
# budget (tools.max_output_lines).
|
|
36
|
+
BODY_LINE_ALLOWANCE = 8
|
|
37
|
+
|
|
38
|
+
attr_reader :config, :registry
|
|
39
|
+
|
|
40
|
+
def initialize(config:, root_dir:, home_dir:, runtime_context: nil)
|
|
41
|
+
@config = config
|
|
42
|
+
@root_dir = File.expand_path(root_dir.to_s)
|
|
43
|
+
@home_dir = home_dir ? File.expand_path(home_dir) : nil
|
|
44
|
+
@runtime_context = runtime_context
|
|
45
|
+
@registry = Registry.new
|
|
46
|
+
@epoch_context = nil
|
|
47
|
+
@monitor = Monitor.new
|
|
48
|
+
reload!
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Capability gate (MCP::Manager#enabled? precedent): disabling keeps
|
|
52
|
+
# the coeffect provided — retracting a fiber-gated coeffect would need
|
|
53
|
+
# a reactive reconcile — but empties the prompt index and closes the
|
|
54
|
+
# tool epoch.
|
|
55
|
+
def enabled?
|
|
56
|
+
fetch("skills.enabled", true) != false
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def max_body_chars
|
|
60
|
+
(fetch("skills.max_body_chars", DEFAULT_MAX_BODY_CHARS) || DEFAULT_MAX_BODY_CHARS).to_i
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Residual line budget for the body, under the StepExecutor envelope.
|
|
64
|
+
def max_body_lines
|
|
65
|
+
raw = fetch("tools.max_output_lines", Rubik::Tools::Truncator::DEFAULT_MAX_LINES) ||
|
|
66
|
+
Rubik::Tools::Truncator::DEFAULT_MAX_LINES
|
|
67
|
+
[raw.to_i - BODY_LINE_ALLOWANCE, 1].max
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def disabled_names
|
|
71
|
+
Array(fetch("skills.disabled", [])).map(&:to_s)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def all
|
|
75
|
+
return [] unless enabled?
|
|
76
|
+
|
|
77
|
+
@registry.all
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def find(name)
|
|
81
|
+
return nil unless enabled?
|
|
82
|
+
|
|
83
|
+
@registry.find(name)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Level-1 progressive-disclosure payload for the prompt layer (pure
|
|
87
|
+
# data, I/O-free downstream): name + description + scope only.
|
|
88
|
+
def prompt_index
|
|
89
|
+
all.map { |skill| { name: skill.name, description: skill.description, scope: skill.scope } }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Level-2 read: the SKILL.md body, head-truncated below BOTH
|
|
93
|
+
# StepExecutor budgets (chars and lines) so the downstream head_tail
|
|
94
|
+
# pass can never amputate the middle; a read_file pointer covers the
|
|
95
|
+
# omitted tail — progressive disclosure doubles as the recovery path.
|
|
96
|
+
def body(name)
|
|
97
|
+
skill = find(name)
|
|
98
|
+
return nil unless skill
|
|
99
|
+
|
|
100
|
+
parsed = Frontmatter.parse(File.read(skill.path))
|
|
101
|
+
text = parsed[:body]
|
|
102
|
+
return safe(text) if text.bytesize <= max_body_chars && text.lines.size <= max_body_lines
|
|
103
|
+
|
|
104
|
+
trunc = Rubik::Tools::Truncator.truncate(text, max_chars: max_body_chars, max_lines: max_body_lines, strategy: :head_only)
|
|
105
|
+
"#{safe(trunc[:text])}Load the omitted tail with read_file: #{skill.path}\n"
|
|
106
|
+
rescue StandardError => e
|
|
107
|
+
Rubik::Platform.debug_warn("Failed to read skill '#{name}': #{e.class}: #{e.message}")
|
|
108
|
+
nil
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Re-discover from disk and atomically swap the snapshot. Per-skill
|
|
112
|
+
# validation problems are collected warnings (spec: skip invalid
|
|
113
|
+
# skills); a hard storage failure keeps the previous snapshot alive.
|
|
114
|
+
def reload!
|
|
115
|
+
result = storage.discover(disabled: disabled_names)
|
|
116
|
+
@registry.install(result)
|
|
117
|
+
result.issues.each { |issue| Rubik::Platform.debug_warn("skill #{issue.path}: #{issue.field} #{issue.message}") }
|
|
118
|
+
true
|
|
119
|
+
rescue StandardError => e
|
|
120
|
+
warn "[rubik] skills reload failed (previous snapshot kept): #{e.class}: #{e.message}"
|
|
121
|
+
false
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Validation issues from the last discovery (fatal rejections +
|
|
125
|
+
# warnings), for !skill output. Severity split via ValidationIssue#level.
|
|
126
|
+
def issues
|
|
127
|
+
@registry.issues
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def status_summary
|
|
131
|
+
skills = all
|
|
132
|
+
{
|
|
133
|
+
enabled: enabled?,
|
|
134
|
+
total: skills.size,
|
|
135
|
+
by_scope: skills.group_by(&:scope).transform_values(&:size),
|
|
136
|
+
disabled: disabled_names,
|
|
137
|
+
skipped: @registry.issues.count(&:error?),
|
|
138
|
+
notes: @registry.issues.count(&:warning?),
|
|
139
|
+
shadowed: @registry.shadowed
|
|
140
|
+
}
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# --- Epoch management: the revertible tool registration ---
|
|
144
|
+
|
|
145
|
+
# Registers the `skill` tool together with its unregister inverse on a
|
|
146
|
+
# dedicated sub-context. No-op when the epoch is already open or the
|
|
147
|
+
# subsystem is disabled. An activation failure disposes the partial
|
|
148
|
+
# sub-context (atomic rollback) and re-raises.
|
|
149
|
+
def open_epoch!(tool_registry)
|
|
150
|
+
@monitor.synchronize do
|
|
151
|
+
return if @epoch_context || !enabled? || tool_registry.nil?
|
|
152
|
+
|
|
153
|
+
context = @runtime_context&.extend || Rubik::Runtime::Context.new
|
|
154
|
+
begin
|
|
155
|
+
tool = Rubik::Tools::SkillTool.new(self, runtime_context: @runtime_context)
|
|
156
|
+
tool_registry.register(tool, origin: :builtin)
|
|
157
|
+
context.register_disposable { tool_registry.unregister(Rubik::Tools::SkillTool::NAME) }
|
|
158
|
+
@epoch_context = context
|
|
159
|
+
rescue StandardError
|
|
160
|
+
context.dispose
|
|
161
|
+
raise
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Disposes the epoch sub-context; the LIFO inverse unregisters the
|
|
167
|
+
# `skill` tool. Safe to call repeatedly.
|
|
168
|
+
def close_epoch!
|
|
169
|
+
@monitor.synchronize do
|
|
170
|
+
@epoch_context&.dispose
|
|
171
|
+
@epoch_context = nil
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# ConfigWiring handler for skills.* knobs: re-read config, refresh the
|
|
176
|
+
# snapshot, and open/close the epoch only on an enabled flip.
|
|
177
|
+
def apply_config!(tool_registry: nil)
|
|
178
|
+
if enabled?
|
|
179
|
+
reload!
|
|
180
|
+
if tool_registry
|
|
181
|
+
open_epoch!(tool_registry)
|
|
182
|
+
else
|
|
183
|
+
Rubik::Platform.debug_warn("skills are enabled but no tool registry is available; the skill tool stays unregistered")
|
|
184
|
+
end
|
|
185
|
+
else
|
|
186
|
+
close_epoch!
|
|
187
|
+
reload!
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def epoch_open?
|
|
192
|
+
@monitor.synchronize { !@epoch_context.nil? }
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
private
|
|
196
|
+
|
|
197
|
+
# Built per discovery (not memoized): skills.paths must be re-read from
|
|
198
|
+
# live config on every reload! so `!config set skills.paths [...]`
|
|
199
|
+
# takes effect without a restart.
|
|
200
|
+
def storage
|
|
201
|
+
Storage.new(
|
|
202
|
+
root_dir: @root_dir,
|
|
203
|
+
home_dir: @home_dir,
|
|
204
|
+
extra_paths: Array(fetch("skills.paths", []))
|
|
205
|
+
)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# fetch-with-default without the `|| default` idiom: an explicit
|
|
209
|
+
# config value of false/nil must survive (see Loop#safe_config hazard).
|
|
210
|
+
def fetch(key, default)
|
|
211
|
+
return default unless @config.respond_to?(:fetch)
|
|
212
|
+
|
|
213
|
+
@config.fetch(key, default)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Encoding hygiene for File.read output reaching the model/history
|
|
217
|
+
# (byteslice truncation can split a multibyte character).
|
|
218
|
+
def safe(text)
|
|
219
|
+
Rubik::Platform.safe_utf8(text)
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "monitor"
|
|
4
|
+
|
|
5
|
+
module Rubik
|
|
6
|
+
module Skills
|
|
7
|
+
# In-memory immutable snapshot of discovered skills (name => Skill),
|
|
8
|
+
# Monitor-guarded. Registry#install swaps the whole snapshot atomically:
|
|
9
|
+
# readers observe either the complete old or the complete new index —
|
|
10
|
+
# that is the zero-downtime mechanism behind Manager#reload! (the system
|
|
11
|
+
# prompt is rebuilt per turn, so the next turn renders the fresh index).
|
|
12
|
+
class Registry
|
|
13
|
+
def initialize
|
|
14
|
+
@skills = {}.freeze
|
|
15
|
+
@shadowed = {}.freeze
|
|
16
|
+
@issues = [].freeze
|
|
17
|
+
@monitor = Monitor.new
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Precedence merge: the first occurrence of a name wins (Storage scans
|
|
21
|
+
# project > user > extra in that order); shadowed duplicates are
|
|
22
|
+
# recorded with their scopes and surfaced by `!skill list`.
|
|
23
|
+
def install(discovery)
|
|
24
|
+
merged = {}
|
|
25
|
+
shadowed = {}
|
|
26
|
+
discovery.skills.each do |skill|
|
|
27
|
+
if (winner = merged[skill.name])
|
|
28
|
+
entry = (shadowed[skill.name] ||= { winner_scope: winner.scope, loser_scopes: [] })
|
|
29
|
+
entry[:loser_scopes] << skill.scope
|
|
30
|
+
else
|
|
31
|
+
merged[skill.name] = skill
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
@monitor.synchronize do
|
|
35
|
+
@skills = merged.freeze
|
|
36
|
+
@shadowed = shadowed.transform_values(&:freeze).freeze
|
|
37
|
+
@issues = discovery.issues.freeze
|
|
38
|
+
end
|
|
39
|
+
self
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def all
|
|
43
|
+
@monitor.synchronize { @skills.values.sort_by(&:name) }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def find(name)
|
|
47
|
+
@monitor.synchronize { @skills[name.to_s] }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def size
|
|
51
|
+
@monitor.synchronize { @skills.size }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def shadowed
|
|
55
|
+
@monitor.synchronize { @shadowed.dup }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Validation issues from the last install (fatal rejections + warnings).
|
|
59
|
+
def issues
|
|
60
|
+
@monitor.synchronize { @issues.dup }
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubik
|
|
4
|
+
module Skills
|
|
5
|
+
# Immutable metadata record for one discovered skill (agentskills.io).
|
|
6
|
+
# Data only: it never holds the SKILL.md body (read on demand via
|
|
7
|
+
# Manager#body) and is never evaluated. One Struct instance per valid
|
|
8
|
+
# skill directory; +warnings+ carries non-fatal frontmatter deviations.
|
|
9
|
+
Skill = Struct.new(
|
|
10
|
+
:name, :description, :scope, :root,
|
|
11
|
+
:license, :compatibility, :metadata, :allowed_tools, :warnings,
|
|
12
|
+
keyword_init: true
|
|
13
|
+
) do
|
|
14
|
+
# Absolute path of the skill manifest.
|
|
15
|
+
def path
|
|
16
|
+
File.join(root, "SKILL.md")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Files bundled under the conventional auxiliary directories
|
|
20
|
+
# (level-3 progressive disclosure: the agent reads them on demand
|
|
21
|
+
# with read_file / run_command relative to the skill directory).
|
|
22
|
+
def auxiliary_files
|
|
23
|
+
AUXILIARY_DIRS.filter_map do |sub|
|
|
24
|
+
target = File.join(root, sub)
|
|
25
|
+
[sub, Dir.children(target).sort] if Dir.exist?(target)
|
|
26
|
+
end.to_h
|
|
27
|
+
rescue StandardError => e
|
|
28
|
+
Rubik::Platform.debug_warn("Failed to list auxiliary files for '#{name}': #{e.message}")
|
|
29
|
+
{}
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "frontmatter"
|
|
4
|
+
require_relative "skill"
|
|
5
|
+
require_relative "validator"
|
|
6
|
+
|
|
7
|
+
module Rubik
|
|
8
|
+
module Skills
|
|
9
|
+
DiscoveryResult = Struct.new(:skills, :issues, keyword_init: true)
|
|
10
|
+
|
|
11
|
+
# The only discovery/scan layer of the skills subsystem (body reads live
|
|
12
|
+
# in Manager#body, auxiliary listings in Skill#auxiliary_files). Pure
|
|
13
|
+
# scanner: no lifecycle, no caching, no writers — it returns every
|
|
14
|
+
# syntactically loadable skill (duplicates across scopes included);
|
|
15
|
+
# precedence resolution lives in Registry#install. Per-directory failures
|
|
16
|
+
# are collected as ValidationIssues and never abort the scan.
|
|
17
|
+
class Storage
|
|
18
|
+
attr_reader :project_root, :user_root, :extra_paths
|
|
19
|
+
|
|
20
|
+
def initialize(root_dir:, home_dir:, extra_paths: [])
|
|
21
|
+
@project_root = File.join(File.expand_path(root_dir.to_s), ".rubik", "skills")
|
|
22
|
+
@user_root = home_dir ? File.join(File.expand_path(home_dir), ".rubik", "skills") : nil
|
|
23
|
+
@extra_paths = Array(extra_paths).map { |p| File.expand_path(p.to_s) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Ordered discovery roots; earlier scopes win precedence in the
|
|
27
|
+
# Registry: project > user > extra paths (declaration order).
|
|
28
|
+
def scoped_roots
|
|
29
|
+
roots = [{ root: @project_root, scope: SCOPE_PROJECT }]
|
|
30
|
+
roots << { root: @user_root, scope: SCOPE_USER } if @user_root
|
|
31
|
+
@extra_paths.each { |path| roots << { root: path, scope: SCOPE_EXTRA } }
|
|
32
|
+
roots
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# => DiscoveryResult(skills:, issues:)
|
|
36
|
+
def discover(disabled: [])
|
|
37
|
+
skills = []
|
|
38
|
+
issues = []
|
|
39
|
+
disabled_names = Array(disabled).map(&:to_s)
|
|
40
|
+
|
|
41
|
+
scoped_roots.each do |target|
|
|
42
|
+
next unless target[:root] && Dir.exist?(target[:root])
|
|
43
|
+
|
|
44
|
+
Dir.children(target[:root]).sort.each do |dir_name|
|
|
45
|
+
next if dir_name.start_with?(".")
|
|
46
|
+
|
|
47
|
+
dir = File.join(target[:root], dir_name)
|
|
48
|
+
next unless File.directory?(dir)
|
|
49
|
+
|
|
50
|
+
path = File.join(dir, "SKILL.md")
|
|
51
|
+
if disabled_names.include?(dir_name)
|
|
52
|
+
issues << ValidationIssue.new(path: path, field: :disabled, message: "skipped (listed in skills.disabled)", level: :warning)
|
|
53
|
+
next
|
|
54
|
+
end
|
|
55
|
+
unless File.file?(path)
|
|
56
|
+
issues << ValidationIssue.new(path: path, field: :skill_md, message: "missing SKILL.md", level: :error)
|
|
57
|
+
next
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
parsed = Frontmatter.parse(File.read(path))
|
|
61
|
+
skill, skill_issues = Validator.validate(
|
|
62
|
+
dir_name: dir_name, root: dir, scope: target[:scope], path: path, frontmatter: parsed[:frontmatter]
|
|
63
|
+
)
|
|
64
|
+
skills << skill if skill
|
|
65
|
+
issues.concat(skill_issues)
|
|
66
|
+
rescue Frontmatter::Error => e
|
|
67
|
+
issues << ValidationIssue.new(path: path, field: :frontmatter, message: e.message, level: :error)
|
|
68
|
+
rescue StandardError => e
|
|
69
|
+
issues << ValidationIssue.new(path: path, field: :io, message: "#{e.class}: #{e.message}", level: :error)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
DiscoveryResult.new(skills: skills.freeze, issues: issues.freeze)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubik
|
|
4
|
+
module Skills
|
|
5
|
+
# One collected validation problem. +level+ separates fatal rejections
|
|
6
|
+
# (:error — the skill is skipped) from non-fatal deviations (:warning —
|
|
7
|
+
# the skill is accepted and the note is surfaced by !skill).
|
|
8
|
+
ValidationIssue = Struct.new(:path, :field, :message, :level, keyword_init: true) do
|
|
9
|
+
def error? = level == :error
|
|
10
|
+
def warning? = level == :warning
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# agentskills.io frontmatter validation, spec-strict: fatal violations of
|
|
14
|
+
# the required fields (name, description) reject the skill; deviations in
|
|
15
|
+
# optional/experimental fields (metadata, allowed-tools, unknown keys)
|
|
16
|
+
# degrade to warnings. One bad skill never aborts a discovery scan.
|
|
17
|
+
class Validator
|
|
18
|
+
NAME_RE = /\A[a-z0-9]+(?:-[a-z0-9]+)*\z/ # no lead/trail/double hyphen
|
|
19
|
+
MAX_NAME = 64
|
|
20
|
+
MAX_DESCRIPTION = 1024
|
|
21
|
+
MAX_COMPATIBILITY = 500
|
|
22
|
+
KNOWN_KEYS = ["name", "description", "license", "compatibility", "metadata", "allowed-tools"].freeze
|
|
23
|
+
|
|
24
|
+
class << self
|
|
25
|
+
# => [Skill, Array<ValidationIssue>] — the Skill is nil when a fatal
|
|
26
|
+
# issue exists; the issue array carries fatal issues + warnings.
|
|
27
|
+
def validate(dir_name:, root:, scope:, path:, frontmatter:)
|
|
28
|
+
fatal = []
|
|
29
|
+
warnings = []
|
|
30
|
+
|
|
31
|
+
name = frontmatter["name"].to_s
|
|
32
|
+
description = frontmatter["description"].to_s.strip
|
|
33
|
+
|
|
34
|
+
fatal << build_issue(path, :name, "is required") if name.empty?
|
|
35
|
+
unless name.empty? || (name.length <= MAX_NAME && NAME_RE.match?(name))
|
|
36
|
+
fatal << build_issue(path, :name,
|
|
37
|
+
"'#{name}' must be 1-#{MAX_NAME} characters of [a-z0-9-], " \
|
|
38
|
+
"no leading/trailing/double hyphen")
|
|
39
|
+
end
|
|
40
|
+
fatal << build_issue(path, :name, "'#{name}' must equal its directory name '#{dir_name}'") if !name.empty? && name != dir_name
|
|
41
|
+
|
|
42
|
+
fatal << build_issue(path, :description, "is required (1-#{MAX_DESCRIPTION} characters)") if description.empty?
|
|
43
|
+
fatal << build_issue(path, :description, "exceeds #{MAX_DESCRIPTION} characters") if !description.empty? && description.length > MAX_DESCRIPTION
|
|
44
|
+
|
|
45
|
+
license = optional_string(frontmatter["license"], "license", path, warnings)
|
|
46
|
+
compatibility = optional_string(frontmatter["compatibility"], "compatibility", path, warnings)
|
|
47
|
+
fatal << build_issue(path, :compatibility, "exceeds #{MAX_COMPATIBILITY} characters") if compatibility.to_s.length > MAX_COMPATIBILITY
|
|
48
|
+
|
|
49
|
+
metadata = parse_metadata(frontmatter["metadata"], path, fatal)
|
|
50
|
+
allowed_tools = parse_allowed_tools(frontmatter["allowed-tools"], path, warnings)
|
|
51
|
+
|
|
52
|
+
(frontmatter.keys - KNOWN_KEYS).each do |key|
|
|
53
|
+
warnings << build_issue(path, key.to_sym, "unknown frontmatter key ignored", :warning)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
issues = fatal + warnings
|
|
57
|
+
return [nil, issues] if fatal.any?
|
|
58
|
+
|
|
59
|
+
[Skill.new(
|
|
60
|
+
name: name, description: description, scope: scope, root: root,
|
|
61
|
+
license: license, compatibility: compatibility,
|
|
62
|
+
metadata: metadata.freeze, allowed_tools: allowed_tools.freeze,
|
|
63
|
+
warnings: warnings.freeze
|
|
64
|
+
), issues]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
# Fatal rejections default to :error; callers collecting warnings pass
|
|
70
|
+
# :warning explicitly.
|
|
71
|
+
def build_issue(path, field, message, level = :error)
|
|
72
|
+
ValidationIssue.new(path: path, field: field, message: message, level: level)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def optional_string(raw, key, path, warnings)
|
|
76
|
+
return nil if raw.nil?
|
|
77
|
+
return raw if raw.is_a?(String)
|
|
78
|
+
|
|
79
|
+
warnings << build_issue(path, key.to_sym, "ignored: must be a string", :warning)
|
|
80
|
+
nil
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Spec: "a map from string keys to string values" — any deviation is
|
|
84
|
+
# fatal (matches the skills-ref reference validator's strictness).
|
|
85
|
+
def parse_metadata(raw, path, fatal)
|
|
86
|
+
return {} if raw.nil?
|
|
87
|
+
|
|
88
|
+
unless raw.is_a?(Hash) && raw.keys.all?(String) && raw.values.all?(String)
|
|
89
|
+
fatal << build_issue(path, :metadata, "must be a map from string keys to string values")
|
|
90
|
+
return {}
|
|
91
|
+
end
|
|
92
|
+
raw
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Spec (experimental): "a space-separated string of pre-approved
|
|
96
|
+
# tools". Parsed and displayed only — never enforced. An Array form
|
|
97
|
+
# degrades to a warning (accepted for interoperability).
|
|
98
|
+
def parse_allowed_tools(raw, path, warnings)
|
|
99
|
+
case raw
|
|
100
|
+
when nil
|
|
101
|
+
[]
|
|
102
|
+
when String
|
|
103
|
+
raw.split(/\s+/).reject(&:empty?)
|
|
104
|
+
when Array
|
|
105
|
+
warnings << build_issue(path, :"allowed-tools",
|
|
106
|
+
"given as a list; the spec declares a space-separated string", :warning)
|
|
107
|
+
raw.map(&:to_s)
|
|
108
|
+
else
|
|
109
|
+
warnings << build_issue(path, :"allowed-tools", "ignored: must be a space-separated string", :warning)
|
|
110
|
+
[]
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
data/lib/rubik/skills.rb
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubik
|
|
4
|
+
module Skills
|
|
5
|
+
# Discovery scopes (precedence order: project > user > extra paths).
|
|
6
|
+
SCOPE_PROJECT = :project
|
|
7
|
+
SCOPE_USER = :user
|
|
8
|
+
SCOPE_EXTRA = :extra
|
|
9
|
+
|
|
10
|
+
# Conventional auxiliary directories of a skill (agentskills.io).
|
|
11
|
+
AUXILIARY_DIRS = ["scripts", "references", "assets"].freeze
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
require_relative "skills/skill"
|
|
16
|
+
require_relative "skills/frontmatter"
|
|
17
|
+
require_relative "skills/validator"
|
|
18
|
+
require_relative "skills/storage"
|
|
19
|
+
require_relative "skills/registry"
|
|
20
|
+
require_relative "skills/manager"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubik
|
|
4
|
+
module Storage
|
|
5
|
+
# Arama katlamasının TEK evi (issue #99 bulgu 7): Unicode default
|
|
6
|
+
# downcase Türkçe I-sınıfını kapatamaz — "I" → "i" olur ama "ı"
|
|
7
|
+
# kendisi kalır ve "İ" iki karaktere ("i" + U+0307) açılır; böylece
|
|
8
|
+
# "ı"-lı sorgu ASCII "I" içeren gövdeyi asla bulamaz. Katlama sorguya
|
|
9
|
+
# ve samanlığa AYNI şekilde uygulanır: determinist, LLM'siz ve İngilizce
|
|
10
|
+
# metinde birebir nötr (ı/İ içermez).
|
|
11
|
+
module Folding
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# "İ".downcase sonucu: "i" + U+0307 (birleşen nokta) — tek harfe indir.
|
|
15
|
+
COMBINING_DOT_ABOVE = "i\u0307"
|
|
16
|
+
DOTLESS_I = "\u0131"
|
|
17
|
+
|
|
18
|
+
def fold(text)
|
|
19
|
+
text.to_s.downcase.gsub(COMBINING_DOT_ABOVE, "i").gsub(DOTLESS_I, "i")
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|