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.
Files changed (523) hide show
  1. checksums.yaml +7 -0
  2. data/ATTRIBUTION.md +139 -0
  3. data/CHANGELOG.md +236 -0
  4. data/LICENSE +21 -0
  5. data/README.md +269 -0
  6. data/bin/rubik +82 -0
  7. data/lib/rubik/adapter/base.rb +195 -0
  8. data/lib/rubik/adapter/c.rb +36 -0
  9. data/lib/rubik/adapter/crystal.rb +78 -0
  10. data/lib/rubik/adapter/generic.rb +86 -0
  11. data/lib/rubik/adapter/go.rb +36 -0
  12. data/lib/rubik/adapter/node.rb +146 -0
  13. data/lib/rubik/adapter/process.rb +78 -0
  14. data/lib/rubik/adapter/python.rb +30 -0
  15. data/lib/rubik/adapter/registry.rb +81 -0
  16. data/lib/rubik/adapter/ruby.rb +30 -0
  17. data/lib/rubik/adapter/rust.rb +36 -0
  18. data/lib/rubik/adapter/zig.rb +36 -0
  19. data/lib/rubik/adapter.rb +18 -0
  20. data/lib/rubik/ast/body_loader.rb +19 -0
  21. data/lib/rubik/ast/c_extractor.rb +114 -0
  22. data/lib/rubik/ast/cache_store.rb +191 -0
  23. data/lib/rubik/ast/extractor.rb +59 -0
  24. data/lib/rubik/ast/fallback_cleaner.rb +145 -0
  25. data/lib/rubik/ast/go_extractor.rb +148 -0
  26. data/lib/rubik/ast/graph.rb +272 -0
  27. data/lib/rubik/ast/python_extractor.rb +120 -0
  28. data/lib/rubik/ast/ruby_extractor.rb +260 -0
  29. data/lib/rubik/ast/rust_extractor.rb +127 -0
  30. data/lib/rubik/ast/symbol.rb +103 -0
  31. data/lib/rubik/ast/treesitter/base_walker.rb +109 -0
  32. data/lib/rubik/ast/treesitter/c_walker.rb +122 -0
  33. data/lib/rubik/ast/treesitter/go_walker.rb +120 -0
  34. data/lib/rubik/ast/treesitter/python_walker.rb +107 -0
  35. data/lib/rubik/ast/treesitter/rust_walker.rb +130 -0
  36. data/lib/rubik/ast/treesitter/typescript_walker.rb +182 -0
  37. data/lib/rubik/ast/treesitter/zig_walker.rb +109 -0
  38. data/lib/rubik/ast/treesitter_extractor.rb +93 -0
  39. data/lib/rubik/ast/typescript_extractor.rb +283 -0
  40. data/lib/rubik/ast/zig_extractor.rb +116 -0
  41. data/lib/rubik/ast.rb +13 -0
  42. data/lib/rubik/benchmark/runner.rb +224 -0
  43. data/lib/rubik/benchmark/synth_repo.rb +59 -0
  44. data/lib/rubik/benchmark.rb +9 -0
  45. data/lib/rubik/buffer/diff.rb +43 -0
  46. data/lib/rubik/buffer/manager.rb +427 -0
  47. data/lib/rubik/buffer/path_validator.rb +60 -0
  48. data/lib/rubik/buffer/snapshot.rb +99 -0
  49. data/lib/rubik/buffer.rb +10 -0
  50. data/lib/rubik/coeffects.rb +129 -0
  51. data/lib/rubik/concurrency/boundary.rb +74 -0
  52. data/lib/rubik/concurrency/dns_pool.rb +68 -0
  53. data/lib/rubik/concurrency/errors.rb +17 -0
  54. data/lib/rubik/concurrency/event_loop.rb +441 -0
  55. data/lib/rubik/concurrency/join.rb +55 -0
  56. data/lib/rubik/concurrency/process_waiter.rb +55 -0
  57. data/lib/rubik/concurrency/scheduler.rb +84 -0
  58. data/lib/rubik/concurrency/timers.rb +109 -0
  59. data/lib/rubik/concurrency.rb +132 -0
  60. data/lib/rubik/config/deep_ops.rb +80 -0
  61. data/lib/rubik/config/defaults.rb +248 -0
  62. data/lib/rubik/config/model_catalog.rb +93 -0
  63. data/lib/rubik/config/store.rb +155 -0
  64. data/lib/rubik/config.rb +216 -0
  65. data/lib/rubik/context/git_info.rb +84 -0
  66. data/lib/rubik/context/git_lister.rb +37 -0
  67. data/lib/rubik/context/index.rb +134 -0
  68. data/lib/rubik/context/repo_context_reader.rb +121 -0
  69. data/lib/rubik/context/scanner.rb +439 -0
  70. data/lib/rubik/context.rb +11 -0
  71. data/lib/rubik/diagnostics/collector.rb +200 -0
  72. data/lib/rubik/diagnostics/entry.rb +60 -0
  73. data/lib/rubik/diagnostics/parser.rb +107 -0
  74. data/lib/rubik/diagnostics.rb +10 -0
  75. data/lib/rubik/event_bus.rb +56 -0
  76. data/lib/rubik/kernel/dispatcher.rb +171 -0
  77. data/lib/rubik/kernel/dmesg.rb +116 -0
  78. data/lib/rubik/kernel/init.rb +177 -0
  79. data/lib/rubik/kernel/record.rb +85 -0
  80. data/lib/rubik/kernel/unit.rb +160 -0
  81. data/lib/rubik/llm/anthropic.rb +202 -0
  82. data/lib/rubik/llm/chunk_assembler.rb +285 -0
  83. data/lib/rubik/llm/client.rb +526 -0
  84. data/lib/rubik/llm/formatter.rb +450 -0
  85. data/lib/rubik/llm/message_sanitizer.rb +87 -0
  86. data/lib/rubik/llm/ollama.rb +167 -0
  87. data/lib/rubik/llm/openai.rb +21 -0
  88. data/lib/rubik/llm/openai_compatible.rb +216 -0
  89. data/lib/rubik/llm/provider.rb +125 -0
  90. data/lib/rubik/llm/registry.rb +336 -0
  91. data/lib/rubik/llm/router.rb +100 -0
  92. data/lib/rubik/llm.rb +18 -0
  93. data/lib/rubik/log.rb +38 -0
  94. data/lib/rubik/lsp/client.rb +475 -0
  95. data/lib/rubik/lsp/manager.rb +366 -0
  96. data/lib/rubik/lsp.rb +9 -0
  97. data/lib/rubik/mcp/boot_lifecycle.rb +114 -0
  98. data/lib/rubik/mcp/client.rb +169 -0
  99. data/lib/rubik/mcp/errors.rb +11 -0
  100. data/lib/rubik/mcp/manager.rb +619 -0
  101. data/lib/rubik/mcp/proxy_tool.rb +104 -0
  102. data/lib/rubik/mcp/transport/stdio.rb +255 -0
  103. data/lib/rubik/mcp.rb +12 -0
  104. data/lib/rubik/orchestrator/activity_log.rb +75 -0
  105. data/lib/rubik/orchestrator/auto_apply.rb +166 -0
  106. data/lib/rubik/orchestrator/budget_manager.rb +97 -0
  107. data/lib/rubik/orchestrator/continuation_manager.rb +161 -0
  108. data/lib/rubik/orchestrator/finish_message_selector.rb +55 -0
  109. data/lib/rubik/orchestrator/goal_ledger/drift.rb +71 -0
  110. data/lib/rubik/orchestrator/goal_ledger/ledger.rb +399 -0
  111. data/lib/rubik/orchestrator/goal_ledger/observation_projection.rb +179 -0
  112. data/lib/rubik/orchestrator/goal_ledger/patch.rb +268 -0
  113. data/lib/rubik/orchestrator/goal_ledger/schema.rb +123 -0
  114. data/lib/rubik/orchestrator/goal_ledger/validator.rb +361 -0
  115. data/lib/rubik/orchestrator/goal_ledger/verifier.rb +129 -0
  116. data/lib/rubik/orchestrator/goal_ledger/verify_spec.rb +50 -0
  117. data/lib/rubik/orchestrator/goal_ledger.rb +65 -0
  118. data/lib/rubik/orchestrator/goal_state.rb +184 -0
  119. data/lib/rubik/orchestrator/goal_wait.rb +125 -0
  120. data/lib/rubik/orchestrator/loop.rb +388 -0
  121. data/lib/rubik/orchestrator/lsp_feedback.rb +55 -0
  122. data/lib/rubik/orchestrator/maestro_gate.rb +65 -0
  123. data/lib/rubik/orchestrator/provider_recovery.rb +90 -0
  124. data/lib/rubik/orchestrator/provider_resolver.rb +69 -0
  125. data/lib/rubik/orchestrator/shell_deps.rb +85 -0
  126. data/lib/rubik/orchestrator/step_executor.rb +328 -0
  127. data/lib/rubik/orchestrator/stream_manager.rb +118 -0
  128. data/lib/rubik/orchestrator/subagent/activation.rb +65 -0
  129. data/lib/rubik/orchestrator/subagent/budget.rb +108 -0
  130. data/lib/rubik/orchestrator/subagent/completion.rb +61 -0
  131. data/lib/rubik/orchestrator/subagent/concurrency_gate.rb +37 -0
  132. data/lib/rubik/orchestrator/subagent/conversation_id.rb +67 -0
  133. data/lib/rubik/orchestrator/subagent/dependency.rb +398 -0
  134. data/lib/rubik/orchestrator/subagent/instance.rb +384 -0
  135. data/lib/rubik/orchestrator/subagent/lifecycle.rb +118 -0
  136. data/lib/rubik/orchestrator/subagent/messaging.rb +61 -0
  137. data/lib/rubik/orchestrator/subagent/narration.rb +125 -0
  138. data/lib/rubik/orchestrator/subagent/notices.rb +209 -0
  139. data/lib/rubik/orchestrator/subagent/questioner.rb +80 -0
  140. data/lib/rubik/orchestrator/subagent/reaper.rb +98 -0
  141. data/lib/rubik/orchestrator/subagent/resilience.rb +70 -0
  142. data/lib/rubik/orchestrator/subagent/reuse.rb +127 -0
  143. data/lib/rubik/orchestrator/subagent/scoped_registry.rb +79 -0
  144. data/lib/rubik/orchestrator/subagent/step_runner.rb +240 -0
  145. data/lib/rubik/orchestrator/subagent/storage_id.rb +26 -0
  146. data/lib/rubik/orchestrator/subagent/supervisor.rb +327 -0
  147. data/lib/rubik/orchestrator/subagent/transcript_formatter.rb +141 -0
  148. data/lib/rubik/orchestrator/subagent/type_registry.rb +199 -0
  149. data/lib/rubik/orchestrator/subagent_notice_inbox.rb +80 -0
  150. data/lib/rubik/orchestrator/tool_call_id.rb +47 -0
  151. data/lib/rubik/orchestrator/tool_circuit_breaker.rb +91 -0
  152. data/lib/rubik/orchestrator/tool_history.rb +39 -0
  153. data/lib/rubik/orchestrator/turn_executor/subagent_channel.rb +102 -0
  154. data/lib/rubik/orchestrator/turn_executor/user_channel.rb +172 -0
  155. data/lib/rubik/orchestrator/turn_executor.rb +376 -0
  156. data/lib/rubik/orchestrator.rb +11 -0
  157. data/lib/rubik/platform/filesystem.rb +64 -0
  158. data/lib/rubik/platform/io_drain.rb +53 -0
  159. data/lib/rubik/platform/path.rb +210 -0
  160. data/lib/rubik/platform/process.rb +38 -0
  161. data/lib/rubik/platform/sanitize.rb +157 -0
  162. data/lib/rubik/platform.rb +113 -0
  163. data/lib/rubik/plugin/dsl.rb +51 -0
  164. data/lib/rubik/plugin/manager.rb +247 -0
  165. data/lib/rubik/plugin/storage.rb +195 -0
  166. data/lib/rubik/plugin.rb +10 -0
  167. data/lib/rubik/prompt/context_builder.rb +377 -0
  168. data/lib/rubik/prompt/context_compactor.rb +326 -0
  169. data/lib/rubik/prompt/conversation_rows.rb +74 -0
  170. data/lib/rubik/prompt/doctrine.rb +160 -0
  171. data/lib/rubik/prompt/emergency_compactor.rb +81 -0
  172. data/lib/rubik/prompt/services.rb +60 -0
  173. data/lib/rubik/prompt/token_estimator.rb +63 -0
  174. data/lib/rubik/prompt/tool_teachings.rb +78 -0
  175. data/lib/rubik/prompt.rb +13 -0
  176. data/lib/rubik/repair/loop.rb +95 -0
  177. data/lib/rubik/repair.rb +8 -0
  178. data/lib/rubik/repl/ast_warmup.rb +129 -0
  179. data/lib/rubik/repl/command_context.rb +214 -0
  180. data/lib/rubik/repl/command_parser.rb +48 -0
  181. data/lib/rubik/repl/commands/agent.rb +30 -0
  182. data/lib/rubik/repl/commands/agent_context.rb +289 -0
  183. data/lib/rubik/repl/commands/agent_execution.rb +342 -0
  184. data/lib/rubik/repl/commands/agent_models.rb +212 -0
  185. data/lib/rubik/repl/commands/agent_providers.rb +232 -0
  186. data/lib/rubik/repl/commands/base.rb +55 -0
  187. data/lib/rubik/repl/commands/buffer.rb +124 -0
  188. data/lib/rubik/repl/commands/dmesg.rb +87 -0
  189. data/lib/rubik/repl/commands/lsp.rb +235 -0
  190. data/lib/rubik/repl/commands/mcp.rb +194 -0
  191. data/lib/rubik/repl/commands/plugins.rb +208 -0
  192. data/lib/rubik/repl/commands/pruner.rb +71 -0
  193. data/lib/rubik/repl/commands/session.rb +183 -0
  194. data/lib/rubik/repl/commands/skills.rb +111 -0
  195. data/lib/rubik/repl/commands/storage.rb +85 -0
  196. data/lib/rubik/repl/commands/subagents.rb +195 -0
  197. data/lib/rubik/repl/commands/system.rb +85 -0
  198. data/lib/rubik/repl/commands/tasks.rb +120 -0
  199. data/lib/rubik/repl/commands/workspace.rb +443 -0
  200. data/lib/rubik/repl/commands.rb +44 -0
  201. data/lib/rubik/repl/composition.rb +32 -0
  202. data/lib/rubik/repl/config_wiring.rb +142 -0
  203. data/lib/rubik/repl/confirmations.rb +56 -0
  204. data/lib/rubik/repl/hooks.rb +47 -0
  205. data/lib/rubik/repl/input_execution.rb +323 -0
  206. data/lib/rubik/repl/input_loop.rb +213 -0
  207. data/lib/rubik/repl/interrupt_handler.rb +217 -0
  208. data/lib/rubik/repl/lifecycle.rb +410 -0
  209. data/lib/rubik/repl/paste_support.rb +88 -0
  210. data/lib/rubik/repl/pending_notice_queue.rb +60 -0
  211. data/lib/rubik/repl/presentation.rb +132 -0
  212. data/lib/rubik/repl/pruner/brief.rb +101 -0
  213. data/lib/rubik/repl/pruner/gate.rb +279 -0
  214. data/lib/rubik/repl/pruner/harness.rb +236 -0
  215. data/lib/rubik/repl/pruner/rules.rb +316 -0
  216. data/lib/rubik/repl/pruner/service.rb +82 -0
  217. data/lib/rubik/repl/pruner.rb +39 -0
  218. data/lib/rubik/repl/service_catalog.rb +374 -0
  219. data/lib/rubik/repl/setup_factories.rb +66 -0
  220. data/lib/rubik/repl/shell.rb +231 -0
  221. data/lib/rubik/repl/step_formatter.rb +324 -0
  222. data/lib/rubik/repl/tee_io.rb +105 -0
  223. data/lib/rubik/repl/terminal_guard.rb +157 -0
  224. data/lib/rubik/repl/turn_recorder.rb +234 -0
  225. data/lib/rubik/repl.rb +14 -0
  226. data/lib/rubik/runtime/ast_service.rb +78 -0
  227. data/lib/rubik/runtime/context.rb +335 -0
  228. data/lib/rubik/runtime/diagnostic_engine.rb +188 -0
  229. data/lib/rubik/runtime/discovery.rb +342 -0
  230. data/lib/rubik/runtime/fiber.rb +142 -0
  231. data/lib/rubik/runtime/fiber_ordering.rb +52 -0
  232. data/lib/rubik/runtime/fiber_retry_gate.rb +84 -0
  233. data/lib/rubik/runtime/index_service.rb +58 -0
  234. data/lib/rubik/runtime/loop_service.rb +73 -0
  235. data/lib/rubik/runtime/manager.rb +241 -0
  236. data/lib/rubik/runtime/overlay_storage.rb +481 -0
  237. data/lib/rubik/runtime/service_definition.rb +54 -0
  238. data/lib/rubik/runtime/service_registration.rb +33 -0
  239. data/lib/rubik/runtime/task_manager.rb +302 -0
  240. data/lib/rubik/runtime.rb +19 -0
  241. data/lib/rubik/sandbox/driver/base.rb +30 -0
  242. data/lib/rubik/sandbox/driver/bubblewrap.rb +111 -0
  243. data/lib/rubik/sandbox/driver/docker.rb +113 -0
  244. data/lib/rubik/sandbox/driver/podman.rb +98 -0
  245. data/lib/rubik/sandbox/driver/process.rb +190 -0
  246. data/lib/rubik/sandbox/driver.rb +91 -0
  247. data/lib/rubik/sandbox/runner.rb +218 -0
  248. data/lib/rubik/sandbox/shell_resolver.rb +178 -0
  249. data/lib/rubik/sandbox.rb +8 -0
  250. data/lib/rubik/security/blocked_guidance.rb +187 -0
  251. data/lib/rubik/security/classifier/classifier.rb +440 -0
  252. data/lib/rubik/security/classifier/commands/agent-browser.yaml +9 -0
  253. data/lib/rubik/security/classifier/commands/apt.yaml +25 -0
  254. data/lib/rubik/security/classifier/commands/awk.yaml +10 -0
  255. data/lib/rubik/security/classifier/commands/basename.yaml +7 -0
  256. data/lib/rubik/security/classifier/commands/bash-classify.yaml +8 -0
  257. data/lib/rubik/security/classifier/commands/bash.yaml +20 -0
  258. data/lib/rubik/security/classifier/commands/black.yaml +12 -0
  259. data/lib/rubik/security/classifier/commands/brew.yaml +31 -0
  260. data/lib/rubik/security/classifier/commands/builtin.yaml +8 -0
  261. data/lib/rubik/security/classifier/commands/cargo.yaml +114 -0
  262. data/lib/rubik/security/classifier/commands/cat.yaml +7 -0
  263. data/lib/rubik/security/classifier/commands/chgrp.yaml +10 -0
  264. data/lib/rubik/security/classifier/commands/chmod.yaml +10 -0
  265. data/lib/rubik/security/classifier/commands/chown.yaml +10 -0
  266. data/lib/rubik/security/classifier/commands/column.yaml +7 -0
  267. data/lib/rubik/security/classifier/commands/comm.yaml +7 -0
  268. data/lib/rubik/security/classifier/commands/command.yaml +12 -0
  269. data/lib/rubik/security/classifier/commands/cp.yaml +7 -0
  270. data/lib/rubik/security/classifier/commands/crystal.yaml +63 -0
  271. data/lib/rubik/security/classifier/commands/curl.yaml +30 -0
  272. data/lib/rubik/security/classifier/commands/cut.yaml +7 -0
  273. data/lib/rubik/security/classifier/commands/date.yaml +10 -0
  274. data/lib/rubik/security/classifier/commands/df.yaml +7 -0
  275. data/lib/rubik/security/classifier/commands/diff.yaml +7 -0
  276. data/lib/rubik/security/classifier/commands/dig.yaml +7 -0
  277. data/lib/rubik/security/classifier/commands/dirname.yaml +7 -0
  278. data/lib/rubik/security/classifier/commands/docker.yaml +177 -0
  279. data/lib/rubik/security/classifier/commands/du.yaml +7 -0
  280. data/lib/rubik/security/classifier/commands/echo.yaml +7 -0
  281. data/lib/rubik/security/classifier/commands/env.yaml +15 -0
  282. data/lib/rubik/security/classifier/commands/eslint.yaml +12 -0
  283. data/lib/rubik/security/classifier/commands/eval.yaml +8 -0
  284. data/lib/rubik/security/classifier/commands/exec.yaml +12 -0
  285. data/lib/rubik/security/classifier/commands/expand.yaml +7 -0
  286. data/lib/rubik/security/classifier/commands/false.yaml +7 -0
  287. data/lib/rubik/security/classifier/commands/file.yaml +7 -0
  288. data/lib/rubik/security/classifier/commands/find.yaml +25 -0
  289. data/lib/rubik/security/classifier/commands/fmt.yaml +7 -0
  290. data/lib/rubik/security/classifier/commands/fold.yaml +7 -0
  291. data/lib/rubik/security/classifier/commands/free.yaml +7 -0
  292. data/lib/rubik/security/classifier/commands/gawk.yaml +10 -0
  293. data/lib/rubik/security/classifier/commands/gcloud.yaml +979 -0
  294. data/lib/rubik/security/classifier/commands/gh.yaml +615 -0
  295. data/lib/rubik/security/classifier/commands/git.yaml +524 -0
  296. data/lib/rubik/security/classifier/commands/glab-discussion.yaml +21 -0
  297. data/lib/rubik/security/classifier/commands/glab.yaml +807 -0
  298. data/lib/rubik/security/classifier/commands/go.yaml +44 -0
  299. data/lib/rubik/security/classifier/commands/gog.yaml +983 -0
  300. data/lib/rubik/security/classifier/commands/gradle.yaml +92 -0
  301. data/lib/rubik/security/classifier/commands/gradlew.yaml +92 -0
  302. data/lib/rubik/security/classifier/commands/grep.yaml +7 -0
  303. data/lib/rubik/security/classifier/commands/groups.yaml +7 -0
  304. data/lib/rubik/security/classifier/commands/gunzip.yaml +8 -0
  305. data/lib/rubik/security/classifier/commands/gws.yaml +211 -0
  306. data/lib/rubik/security/classifier/commands/gzip.yaml +7 -0
  307. data/lib/rubik/security/classifier/commands/hadolint.yaml +9 -0
  308. data/lib/rubik/security/classifier/commands/halt.yaml +8 -0
  309. data/lib/rubik/security/classifier/commands/head.yaml +7 -0
  310. data/lib/rubik/security/classifier/commands/helm.yaml +129 -0
  311. data/lib/rubik/security/classifier/commands/hostname.yaml +7 -0
  312. data/lib/rubik/security/classifier/commands/id.yaml +7 -0
  313. data/lib/rubik/security/classifier/commands/install.yaml +7 -0
  314. data/lib/rubik/security/classifier/commands/isort.yaml +14 -0
  315. data/lib/rubik/security/classifier/commands/java.yaml +35 -0
  316. data/lib/rubik/security/classifier/commands/javac.yaml +39 -0
  317. data/lib/rubik/security/classifier/commands/join.yaml +7 -0
  318. data/lib/rubik/security/classifier/commands/journalctl.yaml +14 -0
  319. data/lib/rubik/security/classifier/commands/jq.yaml +7 -0
  320. data/lib/rubik/security/classifier/commands/kill.yaml +7 -0
  321. data/lib/rubik/security/classifier/commands/killall.yaml +7 -0
  322. data/lib/rubik/security/classifier/commands/kubectl.yaml +211 -0
  323. data/lib/rubik/security/classifier/commands/less.yaml +7 -0
  324. data/lib/rubik/security/classifier/commands/ln.yaml +7 -0
  325. data/lib/rubik/security/classifier/commands/ls.yaml +7 -0
  326. data/lib/rubik/security/classifier/commands/make.yaml +21 -0
  327. data/lib/rubik/security/classifier/commands/md5sum.yaml +7 -0
  328. data/lib/rubik/security/classifier/commands/mise.yaml +579 -0
  329. data/lib/rubik/security/classifier/commands/mkdir.yaml +8 -0
  330. data/lib/rubik/security/classifier/commands/more.yaml +7 -0
  331. data/lib/rubik/security/classifier/commands/mv.yaml +7 -0
  332. data/lib/rubik/security/classifier/commands/mvn.yaml +83 -0
  333. data/lib/rubik/security/classifier/commands/mvnw.yaml +83 -0
  334. data/lib/rubik/security/classifier/commands/mypy.yaml +9 -0
  335. data/lib/rubik/security/classifier/commands/netstat.yaml +7 -0
  336. data/lib/rubik/security/classifier/commands/nice.yaml +11 -0
  337. data/lib/rubik/security/classifier/commands/nl.yaml +7 -0
  338. data/lib/rubik/security/classifier/commands/node.yaml +20 -0
  339. data/lib/rubik/security/classifier/commands/nohup.yaml +9 -0
  340. data/lib/rubik/security/classifier/commands/npm.yaml +71 -0
  341. data/lib/rubik/security/classifier/commands/npx.yaml +42 -0
  342. data/lib/rubik/security/classifier/commands/nslookup.yaml +7 -0
  343. data/lib/rubik/security/classifier/commands/paste.yaml +7 -0
  344. data/lib/rubik/security/classifier/commands/ping.yaml +7 -0
  345. data/lib/rubik/security/classifier/commands/pip.yaml +33 -0
  346. data/lib/rubik/security/classifier/commands/pip3.yaml +33 -0
  347. data/lib/rubik/security/classifier/commands/pipx.yaml +33 -0
  348. data/lib/rubik/security/classifier/commands/pkill.yaml +7 -0
  349. data/lib/rubik/security/classifier/commands/pnpm.yaml +346 -0
  350. data/lib/rubik/security/classifier/commands/poetry.yaml +97 -0
  351. data/lib/rubik/security/classifier/commands/poweroff.yaml +8 -0
  352. data/lib/rubik/security/classifier/commands/prettier.yaml +13 -0
  353. data/lib/rubik/security/classifier/commands/printenv.yaml +7 -0
  354. data/lib/rubik/security/classifier/commands/printf.yaml +7 -0
  355. data/lib/rubik/security/classifier/commands/ps.yaml +7 -0
  356. data/lib/rubik/security/classifier/commands/pup.yaml +882 -0
  357. data/lib/rubik/security/classifier/commands/pwd.yaml +7 -0
  358. data/lib/rubik/security/classifier/commands/pytest.yaml +7 -0
  359. data/lib/rubik/security/classifier/commands/python.yaml +9 -0
  360. data/lib/rubik/security/classifier/commands/python3.yaml +9 -0
  361. data/lib/rubik/security/classifier/commands/read.yaml +7 -0
  362. data/lib/rubik/security/classifier/commands/readlink.yaml +7 -0
  363. data/lib/rubik/security/classifier/commands/realpath.yaml +7 -0
  364. data/lib/rubik/security/classifier/commands/reboot.yaml +8 -0
  365. data/lib/rubik/security/classifier/commands/rev.yaml +7 -0
  366. data/lib/rubik/security/classifier/commands/rg.yaml +7 -0
  367. data/lib/rubik/security/classifier/commands/rm.yaml +8 -0
  368. data/lib/rubik/security/classifier/commands/rsync.yaml +12 -0
  369. data/lib/rubik/security/classifier/commands/rtk.yaml +140 -0
  370. data/lib/rubik/security/classifier/commands/ruff.yaml +29 -0
  371. data/lib/rubik/security/classifier/commands/rustfmt.yaml +14 -0
  372. data/lib/rubik/security/classifier/commands/scp.yaml +7 -0
  373. data/lib/rubik/security/classifier/commands/sed.yaml +14 -0
  374. data/lib/rubik/security/classifier/commands/seq.yaml +7 -0
  375. data/lib/rubik/security/classifier/commands/sh.yaml +14 -0
  376. data/lib/rubik/security/classifier/commands/sha1sum.yaml +7 -0
  377. data/lib/rubik/security/classifier/commands/sha256sum.yaml +7 -0
  378. data/lib/rubik/security/classifier/commands/shellcheck.yaml +10 -0
  379. data/lib/rubik/security/classifier/commands/shutdown.yaml +8 -0
  380. data/lib/rubik/security/classifier/commands/slack.yaml +113 -0
  381. data/lib/rubik/security/classifier/commands/sleep.yaml +8 -0
  382. data/lib/rubik/security/classifier/commands/snip.yaml +79 -0
  383. data/lib/rubik/security/classifier/commands/sort.yaml +10 -0
  384. data/lib/rubik/security/classifier/commands/ss.yaml +7 -0
  385. data/lib/rubik/security/classifier/commands/ssh.yaml +41 -0
  386. data/lib/rubik/security/classifier/commands/stat.yaml +7 -0
  387. data/lib/rubik/security/classifier/commands/stdbuf.yaml +15 -0
  388. data/lib/rubik/security/classifier/commands/strace.yaml +20 -0
  389. data/lib/rubik/security/classifier/commands/strings.yaml +11 -0
  390. data/lib/rubik/security/classifier/commands/sudo.yaml +24 -0
  391. data/lib/rubik/security/classifier/commands/systemctl.yaml +58 -0
  392. data/lib/rubik/security/classifier/commands/systemd-run.yaml +30 -0
  393. data/lib/rubik/security/classifier/commands/tac.yaml +7 -0
  394. data/lib/rubik/security/classifier/commands/tail.yaml +7 -0
  395. data/lib/rubik/security/classifier/commands/tar.yaml +25 -0
  396. data/lib/rubik/security/classifier/commands/tee.yaml +8 -0
  397. data/lib/rubik/security/classifier/commands/terraform.yaml +70 -0
  398. data/lib/rubik/security/classifier/commands/test.yaml +7 -0
  399. data/lib/rubik/security/classifier/commands/time.yaml +8 -0
  400. data/lib/rubik/security/classifier/commands/timeout.yaml +18 -0
  401. data/lib/rubik/security/classifier/commands/top.yaml +7 -0
  402. data/lib/rubik/security/classifier/commands/touch.yaml +8 -0
  403. data/lib/rubik/security/classifier/commands/tr.yaml +7 -0
  404. data/lib/rubik/security/classifier/commands/traceroute.yaml +7 -0
  405. data/lib/rubik/security/classifier/commands/true.yaml +7 -0
  406. data/lib/rubik/security/classifier/commands/tsc.yaml +16 -0
  407. data/lib/rubik/security/classifier/commands/type.yaml +7 -0
  408. data/lib/rubik/security/classifier/commands/uname.yaml +7 -0
  409. data/lib/rubik/security/classifier/commands/unexpand.yaml +7 -0
  410. data/lib/rubik/security/classifier/commands/uniq.yaml +7 -0
  411. data/lib/rubik/security/classifier/commands/unzip.yaml +10 -0
  412. data/lib/rubik/security/classifier/commands/uptime.yaml +7 -0
  413. data/lib/rubik/security/classifier/commands/uv.yaml +156 -0
  414. data/lib/rubik/security/classifier/commands/wait.yaml +11 -0
  415. data/lib/rubik/security/classifier/commands/wc.yaml +7 -0
  416. data/lib/rubik/security/classifier/commands/wget.yaml +7 -0
  417. data/lib/rubik/security/classifier/commands/whereis.yaml +7 -0
  418. data/lib/rubik/security/classifier/commands/which.yaml +7 -0
  419. data/lib/rubik/security/classifier/commands/whoami.yaml +7 -0
  420. data/lib/rubik/security/classifier/commands/xargs.yaml +26 -0
  421. data/lib/rubik/security/classifier/commands/xmllint.yaml +7 -0
  422. data/lib/rubik/security/classifier/commands/yarn.yaml +72 -0
  423. data/lib/rubik/security/classifier/commands/yes.yaml +7 -0
  424. data/lib/rubik/security/classifier/commands/yq.yaml +10 -0
  425. data/lib/rubik/security/classifier/commands/zip.yaml +8 -0
  426. data/lib/rubik/security/classifier/commands/zsh.yaml +14 -0
  427. data/lib/rubik/security/classifier/database.rb +216 -0
  428. data/lib/rubik/security/classifier/matcher/delegation.rb +190 -0
  429. data/lib/rubik/security/classifier/matcher/options.rb +245 -0
  430. data/lib/rubik/security/classifier/matcher/subcommands.rb +103 -0
  431. data/lib/rubik/security/classifier/matcher.rb +249 -0
  432. data/lib/rubik/security/classifier/models.rb +170 -0
  433. data/lib/rubik/security/classifier/parser.rb +23 -0
  434. data/lib/rubik/security/classifier/parsers/bash.rb +280 -0
  435. data/lib/rubik/security/classifier/parsers/bash_extractors.rb +221 -0
  436. data/lib/rubik/security/classifier/parsers/powershell.rb +185 -0
  437. data/lib/rubik/security/classifier/pwsh_rules.rb +224 -0
  438. data/lib/rubik/security/classifier.rb +25 -0
  439. data/lib/rubik/security/errors.rb +39 -0
  440. data/lib/rubik/security/guard.rb +954 -0
  441. data/lib/rubik/security/redactor.rb +89 -0
  442. data/lib/rubik/security.rb +11 -0
  443. data/lib/rubik/session/manager.rb +331 -0
  444. data/lib/rubik/session/recorder.rb +149 -0
  445. data/lib/rubik/session/replayer.rb +65 -0
  446. data/lib/rubik/session.rb +10 -0
  447. data/lib/rubik/skills/frontmatter.rb +41 -0
  448. data/lib/rubik/skills/manager.rb +223 -0
  449. data/lib/rubik/skills/registry.rb +64 -0
  450. data/lib/rubik/skills/skill.rb +33 -0
  451. data/lib/rubik/skills/storage.rb +77 -0
  452. data/lib/rubik/skills/validator.rb +116 -0
  453. data/lib/rubik/skills.rb +20 -0
  454. data/lib/rubik/storage/folding.rb +23 -0
  455. data/lib/rubik/storage/layout.rb +145 -0
  456. data/lib/rubik/storage/render.rb +97 -0
  457. data/lib/rubik/storage/service.rb +42 -0
  458. data/lib/rubik/storage/store.rb +514 -0
  459. data/lib/rubik/storage/transcript_indexer.rb +116 -0
  460. data/lib/rubik/storage/uri_reader.rb +168 -0
  461. data/lib/rubik/storage/vakanuvis/annal_writer.rb +92 -0
  462. data/lib/rubik/storage/vakanuvis/distill_gate.rb +323 -0
  463. data/lib/rubik/storage/vakanuvis/gate.rb +250 -0
  464. data/lib/rubik/storage/vakanuvis/harness.rb +421 -0
  465. data/lib/rubik/storage/vakanuvis/memory_steward.rb +163 -0
  466. data/lib/rubik/storage/vakanuvis/service.rb +69 -0
  467. data/lib/rubik/storage/vakanuvis/vocabulary.rb +99 -0
  468. data/lib/rubik/storage/vakanuvis/wiki_skeleton.rb +67 -0
  469. data/lib/rubik/storage/vakanuvis.rb +77 -0
  470. data/lib/rubik/storage.rb +29 -0
  471. data/lib/rubik/tools/base.rb +107 -0
  472. data/lib/rubik/tools/builtins/dmesg.rb +74 -0
  473. data/lib/rubik/tools/builtins/exec.rb +293 -0
  474. data/lib/rubik/tools/builtins/files.rb +354 -0
  475. data/lib/rubik/tools/builtins/goal_ledger.rb +89 -0
  476. data/lib/rubik/tools/builtins/grep.rb +281 -0
  477. data/lib/rubik/tools/builtins/interactive.rb +175 -0
  478. data/lib/rubik/tools/builtins/lifecycle.rb +359 -0
  479. data/lib/rubik/tools/builtins/lsp.rb +283 -0
  480. data/lib/rubik/tools/builtins/pruner.rb +88 -0
  481. data/lib/rubik/tools/builtins/runtime.rb +120 -0
  482. data/lib/rubik/tools/builtins/send_message.rb +109 -0
  483. data/lib/rubik/tools/builtins/show_file.rb +84 -0
  484. data/lib/rubik/tools/builtins/skill.rb +72 -0
  485. data/lib/rubik/tools/builtins/storage.rb +232 -0
  486. data/lib/rubik/tools/builtins/subagent.rb +356 -0
  487. data/lib/rubik/tools/builtins/subagent_lifecycle.rb +72 -0
  488. data/lib/rubik/tools/builtins/symbols.rb +209 -0
  489. data/lib/rubik/tools/builtins/synthesis.rb +314 -0
  490. data/lib/rubik/tools/builtins/task.rb +168 -0
  491. data/lib/rubik/tools/builtins/unsynthesize.rb +111 -0
  492. data/lib/rubik/tools/builtins/vakanuvis.rb +161 -0
  493. data/lib/rubik/tools/builtins.rb +30 -0
  494. data/lib/rubik/tools/catalog.rb +70 -0
  495. data/lib/rubik/tools/declaration.rb +90 -0
  496. data/lib/rubik/tools/diagnostic_reporter.rb +283 -0
  497. data/lib/rubik/tools/fuzzy_matcher.rb +172 -0
  498. data/lib/rubik/tools/native_search.rb +180 -0
  499. data/lib/rubik/tools/output.rb +54 -0
  500. data/lib/rubik/tools/preflight_runner.rb +209 -0
  501. data/lib/rubik/tools/registry.rb +247 -0
  502. data/lib/rubik/tools/result.rb +71 -0
  503. data/lib/rubik/tools/services.rb +96 -0
  504. data/lib/rubik/tools/truncator.rb +78 -0
  505. data/lib/rubik/tools/uri_resolver.rb +195 -0
  506. data/lib/rubik/tools.rb +14 -0
  507. data/lib/rubik/ui/approvals.rb +52 -0
  508. data/lib/rubik/ui/banner.rb +43 -0
  509. data/lib/rubik/ui/capability.rb +122 -0
  510. data/lib/rubik/ui/dialog.rb +106 -0
  511. data/lib/rubik/ui/diff_viewer.rb +78 -0
  512. data/lib/rubik/ui/frame.rb +95 -0
  513. data/lib/rubik/ui/markdown_stream.rb +107 -0
  514. data/lib/rubik/ui/panel.rb +98 -0
  515. data/lib/rubik/ui/renderer.rb +206 -0
  516. data/lib/rubik/ui/spinner.rb +120 -0
  517. data/lib/rubik/ui/statusline.rb +134 -0
  518. data/lib/rubik/ui/theme.rb +118 -0
  519. data/lib/rubik/ui.rb +57 -0
  520. data/lib/rubik/uris.rb +101 -0
  521. data/lib/rubik/version.rb +5 -0
  522. data/lib/rubik.rb +65 -0
  523. 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
@@ -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