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