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,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "monitor"
4
+
5
+ module Rubik
6
+ module Orchestrator
7
+ module Subagent
8
+ # Step-budget state machine for a subagent Instance (architecture-review
9
+ # Candidate 04): owns max_steps/current_step and their entire invariant
10
+ # set — initial normalization, consumption, exhaustion, the per-step
11
+ # visibility marker, and grant (normalize -> floor -> +=).
12
+ #
13
+ # Kinship: the main loop's Orchestrator::BudgetManager implements the
14
+ # 75% warning arithmetic as a STATELESS collaborator over Loop-held
15
+ # state, emitting XML into the main history. This class is the OWNING
16
+ # variant for the subagent path (different owner, different channel:
17
+ # a subagent-local user message). The two stay separate by the
18
+ # stateless-over-owner doctrine; do not merge them. Plan 109 removed
19
+ # the subagent-side 75% latch: transparency ([Step X/Y] every step)
20
+ # replaced it — the subagent paces itself, agent-0 steers.
21
+ #
22
+ # Concurrency contract: every method synchronizes on this object's
23
+ # Monitor. Lock order is ALWAYS instance -> budget: callers may hold
24
+ # the Instance monitor while calling in (grant_budget!, to_h, pause);
25
+ # Budget never calls back into Instance, so no inverse order exists.
26
+ # Grants are monotone and the worker thread is the sole consumer, so
27
+ # separately-locked operations compose without a global transaction.
28
+ class Budget
29
+ DEFAULT_MAX_STEPS = 88
30
+ DEFAULT_GRANT_STEPS = 20
31
+
32
+ def initialize(max_steps: nil)
33
+ @max_steps = (max_steps || DEFAULT_MAX_STEPS).to_i
34
+ @current_step = 0
35
+ @monitor = Monitor.new
36
+ end
37
+
38
+ def max_steps
39
+ @monitor.synchronize { @max_steps }
40
+ end
41
+
42
+ def current_step
43
+ @monitor.synchronize { @current_step }
44
+ end
45
+
46
+ def exhausted?
47
+ @monitor.synchronize { @current_step >= @max_steps }
48
+ end
49
+
50
+ # Enter the next step; returns the new current_step.
51
+ def consume
52
+ @monitor.synchronize { @current_step += 1 }
53
+ end
54
+
55
+ # Per-step visibility marker (Plan 109 Faz A, replacing the 75%
56
+ # latch): computed under the Monitor so X/Y can never straddle a
57
+ # concurrent grant — grant! raises Y and the next marker shows the
58
+ # new total automatically. Stateless by design: only the newest
59
+ # marker carries information, so emergency compaction may
60
+ # summarize older markers away freely.
61
+ def step_marker
62
+ @monitor.synchronize { "[Step #{@current_step}/#{@max_steps}]" }
63
+ end
64
+
65
+ # Normalize -> floor -> +=, atomically. Returns the
66
+ # [extra, total] pair Instance's note lambda needs. Terminal-state
67
+ # refusal, the :running transition, the history note, and the condvar
68
+ # broadcast stay on Instance — lifecycle facts, not budget facts.
69
+ # Called with the Instance monitor held (lock order: instance -> budget).
70
+ def grant!(steps)
71
+ @monitor.synchronize do
72
+ extra = Budget.normalize_grant(steps)
73
+ @max_steps += extra
74
+ [extra, @max_steps]
75
+ end
76
+ end
77
+
78
+ # One-lock snapshot: the two counters can never straddle a grant.
79
+ def to_h
80
+ @monitor.synchronize { { current_step: @current_step, max_steps: @max_steps } }
81
+ end
82
+
83
+ # Pure half of the grant invariant, shared by grant! (mutation edge)
84
+ # and the reporting edges (manage_subagents steps arg, REPL
85
+ # !subagents extend arg): nil/non-positive -> DEFAULT_GRANT_STEPS, so
86
+ # the number a caller echoes can never disagree with the number
87
+ # grant! applies. Idempotent.
88
+ def self.normalize_grant(steps)
89
+ extra = (steps || DEFAULT_GRANT_STEPS).to_i
90
+ extra = DEFAULT_GRANT_STEPS if extra <= 0
91
+ extra
92
+ end
93
+
94
+ # Grant history notes (same audience as the step marker above: the
95
+ # subagent's own loop, not the main agent — that is why they live
96
+ # here rather than in Notices). Rendered by Instance's grant_budget!
97
+ # note lambdas.
98
+ def self.grant_note(extra, total)
99
+ "[Supervisor maestro]: Granted #{extra} additional step(s). Total budget: #{total}. Continue execution."
100
+ end
101
+
102
+ def self.message_grant_note(extra, total)
103
+ "[Supervisor maestro]: Granted #{extra} additional step(s) with message. Total budget: #{total}. Continue execution."
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "notices"
4
+ require_relative "../auto_apply"
5
+
6
+ module Rubik
7
+ module Orchestrator
8
+ module Subagent
9
+ # Supervisor'ın tamamlanma koreografisi — supervisor.rb'den çıkarıldı
10
+ # (satır kapısı) ve bağımlılık motorunun bırakma kancası buraya
11
+ # bağlandı: reap'ten SONRA release_ready! çalışır — kalkan slot
12
+ # görünür, mezar taşlaşmış öncül get ile çözülebilir ve iptal
13
+ # bildirimleri öncülün kendi tamamlanma bildiriminden önce gelir
14
+ # (zararsız sıralama).
15
+ module CompletionSupervisor
16
+ def handle_subagent_completion(inst)
17
+ merge_branch_buffer(inst) if inst.workspace == "branch" && inst.completed?
18
+ auto_apply_subagent_buffer(inst) if inst.completed?
19
+ Rubik::Storage::TranscriptIndexer.index(storage_store, inst)
20
+ reap_orphans
21
+ release_ready!
22
+ # Result is deliberately absent: notices announce, the model retrieves
23
+ # via manage_subagents wait — the wait protocol stays authoritative.
24
+ # max_steps rides along so the budget-exhausted guidance can count.
25
+ # storage_id: Vakanüvis kronikçisinin annal/transcript ref anahtarı
26
+ # (plan 2026-10-04) — mevcut aboneler bu anahtarı yok sayar.
27
+ Notices.emit(@event_bus, Notices::COMPLETED_EVENT, { id: inst.id, role: inst.role, state: inst.state, max_steps: inst.max_steps, storage_id: inst.storage_id })
28
+ end
29
+
30
+ private
31
+
32
+ # Completion-time spelling of the shared auto-apply policy: the
33
+ # subagents flag OR the buffer.auto_apply mode decides (previously
34
+ # this site read the flag alone and ignored the mode), and a failed
35
+ # apply surfaces as a notice instead of a silent rescue. The child
36
+ # context is already disposed here, so :shell resolves from the root.
37
+ def auto_apply_subagent_buffer(inst)
38
+ return unless AutoApply.apply?(@config, trigger: :completion, subagent: true)
39
+
40
+ begin
41
+ buffer = @buffer_manager || inst.buffer_manager
42
+ AutoApply.apply_and_sync(buffer, Rubik::Coeffects.get(@root_context, Rubik::Coeffects::SHELL))
43
+ AutoApply.report_conflicts(@event_bus, buffer,
44
+ id: inst.id, role: inst.role, state: inst.state, trigger: :completion)
45
+ rescue StandardError => e
46
+ AutoApply.report_failure(@event_bus, id: inst.id, role: inst.role, state: inst.state,
47
+ trigger: :completion, reason: "#{e.class}: #{e.message}")
48
+ end
49
+ end
50
+
51
+ def merge_branch_buffer(inst)
52
+ return unless inst.buffer_manager && @buffer_manager && inst.buffer_manager != @buffer_manager
53
+
54
+ inst.buffer_manager.dirty_files.each_key do |path|
55
+ @buffer_manager.write(path, inst.buffer_manager.read(path))
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubik
4
+ module Orchestrator
5
+ module Subagent
6
+ # Semaphore gate limiting concurrent outbound LLM requests across subagents (Plan 97).
7
+ # Prevents network queue exhaustion and 90s provider read timeouts under load.
8
+ class ConcurrencyGate
9
+ attr_reader :capacity
10
+
11
+ def initialize(capacity = 2)
12
+ @capacity = [capacity.to_i, 1].max
13
+ @queue = Thread::SizedQueue.new(@capacity)
14
+ @capacity.times { @queue.push(:token) }
15
+ end
16
+
17
+ def synchronize
18
+ token = @queue.pop
19
+ acquired = true
20
+ begin
21
+ yield
22
+ ensure
23
+ @queue.push(token) if acquired
24
+ end
25
+ end
26
+
27
+ def active_count
28
+ @capacity - @queue.size
29
+ end
30
+
31
+ def available_count
32
+ @queue.size
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Rubik
6
+ module Orchestrator
7
+ module Subagent
8
+ # THE single vocabulary home for subagent conversation-id spellings:
9
+ # the machine root id, the root recipient aliases, and the parse-side
10
+ # normalization of model-supplied id input. Previously the B5 dialect
11
+ # grammar (JSON-array-string / comma list / quoted) was class-private
12
+ # to ManageSubagentsTool while send_message, the supervisor plane and
13
+ # the storage gate each spelled "agent-0" (and scrubbed ids) on their
14
+ # own — the exact door B5's bug class walks through next.
15
+ #
16
+ # Naming doctrine (plan 2026-10-02): machine id "agent-0" (parent_id/
17
+ # sender; persisted specs round-trip it), persona "maestro" (model-
18
+ # visible text; renamed from "init-0" 2026-10-03 — "init-0" survives
19
+ # only as a legacy recipient alias). "root"/"parent" are aliases; all
20
+ # four route to the root plane.
21
+ module ConversationId
22
+ ROOT_ID = "agent-0"
23
+ ROOT_ALIASES = ["agent-0", "init-0", "root", "parent"].map(&:downcase).freeze
24
+
25
+ module_function
26
+
27
+ # Single-id scrub: surrounding whitespace + wrapping single/double quotes.
28
+ def scrub(id)
29
+ id.to_s.strip.gsub(/\A['"]|['"]\z/, "")
30
+ end
31
+
32
+ def root_alias?(id)
33
+ ROOT_ALIASES.include?(scrub(id).downcase)
34
+ end
35
+
36
+ # Model-supplied id lists arrive in three dialects (session review
37
+ # B5, observed live): a real JSON array, a JSON-encoded array STRING
38
+ # ('["subagent-5", "subagent-6"]' — previously ONE nonexistent id),
39
+ # or a comma list. Normalize all three (plus Array/single/non-id
40
+ # raw), strip quotes, drop empties. Byte-identical to the grammar
41
+ # extracted from ManageSubagentsTool#resolve_ids.
42
+ def normalize_list(raw)
43
+ list = case raw
44
+ when Array then raw
45
+ when String then parse_id_string(raw)
46
+ else Array(raw)
47
+ end
48
+ list.flatten.compact.map { |id| scrub(id) }.reject(&:empty?)
49
+ end
50
+
51
+ def parse_id_string(str)
52
+ stripped = str.strip
53
+ parsed = safe_json_array(stripped) if stripped.start_with?("[")
54
+ parsed || (stripped.include?(",") ? stripped.split(",") : [stripped])
55
+ end
56
+
57
+ def safe_json_array(str)
58
+ parsed = JSON.parse(str)
59
+ parsed if parsed.is_a?(Array)
60
+ rescue JSON::ParserError
61
+ nil
62
+ end
63
+ private_class_method :parse_id_string, :safe_json_array
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,398 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "budget"
4
+ require_relative "notices"
5
+ require_relative "conversation_id"
6
+
7
+ module Rubik
8
+ module Orchestrator
9
+ module Subagent
10
+ # PM tarzı dispatch kısıtları (kullanıcı kararı 2026-10-01): bağlı
11
+ # işler aynı anda ateşlenmez — FS (finish-to-start, varsayılan) öncül
12
+ # tamamlanınca, SS (start-to-start) öncül İLK kez running'a geçince
13
+ # bırakılır. Bekleyen spec bir Instance DEĞİL: thread yok, slot yok,
14
+ # maliyet yok — bırakılınca gerçek spawn koreografisi oynar.
15
+ # FF/SF v1'de bilinçli olarak desteklenmez (normalize_dep reddeder).
16
+ class DeferredSpec
17
+ attr_reader :id, :parent_id, :role, :type_name, :prompt, :system_prompt,
18
+ :workspace, :model, :max_steps, :deps, :provider
19
+ attr_accessor :state, :cancel_reason
20
+
21
+ def initialize(id:, role:, prompt:, type_name:, deps:, parent_id: ConversationId::ROOT_ID, system_prompt: nil,
22
+ workspace: "inherit", model: "inherit", max_steps: nil, provider: nil)
23
+ @id = id
24
+ @parent_id = parent_id
25
+ @role = role
26
+ @prompt = prompt
27
+ @type_name = type_name
28
+ @deps = deps
29
+ @system_prompt = system_prompt
30
+ @workspace = workspace
31
+ @model = model
32
+ @max_steps = max_steps
33
+ @provider = provider
34
+ @state = :queued
35
+ @cancel_reason = nil
36
+ end
37
+
38
+ def queued? = @state == :queued
39
+ def spawning? = @state == :spawning
40
+ def cancelled? = @state == :cancelled
41
+
42
+ # Duck-type yüzeyi: render'lar (list/status/wait) Instance ile
43
+ # DeferredSpec'i AYIRT ETMEDEN kullanır — dallanma yok.
44
+ def running? = false
45
+ def active? = false
46
+ def uptime = 0.0
47
+ def started_at = nil
48
+ def current_step = 0
49
+ def result = nil
50
+ def transcript_uri = nil
51
+ def last_error = @cancel_reason
52
+ def terminal_and_settled? = false
53
+ def redeployable? = false
54
+ # send_message'in deliverable? kapısı spec'e de değebilir (get,
55
+ # kuyruk fallback'i döndürür): bir spec henüz doğmadı — teslim
56
+ # edilemez.
57
+ def deliverable? = false
58
+
59
+ def depends_on = @deps.map { |d| "#{d[:id]}:#{d[:type]}" }
60
+
61
+ def to_h
62
+ { id: @id, parent_id: @parent_id, role: @role, type: @type_name, state: @state.to_s,
63
+ depends_on: depends_on, current_step: 0, max_steps: @max_steps, uptime_seconds: 0.0,
64
+ started_at: nil, ended_at: nil, transcript: nil, last_error: @cancel_reason,
65
+ result_summary: nil, result: nil }
66
+ end
67
+ end
68
+
69
+ # Saf graf fonksiyonları — kilit yok, IO yok, izole test edilebilir.
70
+ module DependencyGraph
71
+ module_function
72
+
73
+ # "subagent-1" => FS varsayılan; {id:, type:} => FS|SS zorunlu.
74
+ def normalize_dep(raw)
75
+ return { id: raw.to_s.strip, type: "FS" } if raw.is_a?(String)
76
+
77
+ id = raw.is_a?(Hash) ? (raw[:id] || raw["id"]) : nil
78
+ raise ArgumentError, "Invalid dependency #{raw.inspect}: expected 'subagent-N' or {id:, type: 'FS'|'SS'}" if id.nil? || id.to_s.strip.empty?
79
+
80
+ type = (raw[:type] || raw["type"] || "FS").to_s.upcase.strip
81
+ unless ["FS", "SS"].include?(type)
82
+ raise ArgumentError, "Unsupported dependency type '#{type}' (only FS and SS; " \
83
+ "express finish-to-finish as FS on the successor and wait on both)"
84
+ end
85
+
86
+ { id: (raw[:id] || raw["id"]).to_s.strip, type: type }
87
+ end
88
+
89
+ def normalize_deps(list)
90
+ Array(list).compact.map { |raw| normalize_dep(raw) }
91
+ end
92
+
93
+ # Üç renkli DFS; graf dışı komşular terminaldir (Instance'lar kenar
94
+ # taşımaz). Döngü => yol ismiyle ArgumentError (hiçbir şey rezerve
95
+ # edilmeden önce çağrılır — fail fast).
96
+ def assert_acyclic!(graph)
97
+ color = Hash.new(:unvisited)
98
+ path = []
99
+ graph.each_key do |start|
100
+ next unless color[start] == :unvisited
101
+
102
+ cycle = dfs(start, graph, color, path)
103
+ raise ArgumentError, "Dependency cycle detected: #{cycle.join(' -> ')}" if cycle
104
+ end
105
+ end
106
+
107
+ def dfs(node, graph, color, path)
108
+ color[node] = :open
109
+ path << node
110
+ graph[node].each do |dep|
111
+ next unless graph.key?(dep)
112
+
113
+ return path.dup << dep if color[dep] == :open
114
+
115
+ if color[dep] == :unvisited && (cycle = dfs(dep, graph, color, path))
116
+ return cycle
117
+ end
118
+ end
119
+ path.pop
120
+ color[node] = :done
121
+ nil
122
+ end
123
+ end
124
+
125
+ # Supervisor düzlemi: rezervasyon, DAG doğrulama, bırakma motoru ve
126
+ # iptal zinciri. Kilit sözleşmesi: @deferred mutasyonları @monitor
127
+ # altında; emit ASLA kilit altında değil (topla-sonra-yay).
128
+ module DependencySupervisor
129
+ # Tool girişi: spec'leri normalize et, geçici id'ler ata, DAG'ı
130
+ # doğrula (döngü/ölü öncül/bilinmeyen referans => ArgumentError,
131
+ # HİÇBİR ŞEY rezerve edilmeden), sonra rezerve et ve hazır olanları
132
+ # talep et. Dönen çiftler: [:launched, Instance] | [:queued, DeferredSpec].
133
+ def spawn_batch(specs)
134
+ reserved = []
135
+ claimed = []
136
+ @monitor.synchronize do
137
+ base = @counter
138
+ normalized = specs.each_with_index.map { |s, i| normalize_batch_spec(s, base + i + 1) }
139
+ refute_dead_dependencies!(normalized)
140
+ refute_unknown_dependencies!(normalized)
141
+ DependencyGraph.assert_acyclic!(dependency_graph(normalized))
142
+ normalized.each do |spec|
143
+ @deferred[spec.id] = spec
144
+ reserved << spec
145
+ end
146
+ @counter += normalized.size
147
+ reserved.each do |spec|
148
+ next unless dependencies_evaluate(spec) == :ready
149
+
150
+ spec.state = :spawning
151
+ claimed << spec
152
+ end
153
+ end
154
+
155
+ reserved.map do |spec|
156
+ claimed.include?(spec) ? launch_claimed!(spec) : resolve_outcome(spec)
157
+ end
158
+ end
159
+
160
+ # Bırakma süpürmesi: rezervasyon sırasıyla, kuyruktakileri değerlendir.
161
+ # :ready => talep et (atomik), :dead => iptal (öncül failed/kayboldu),
162
+ # :blocked => bekle. İptal dalgaları SABİT NOKTAYA kadar kademelenir —
163
+ # iptal edilmiş bir öncülün kendi bağımlıları da ölür (yoksa sonsuza
164
+ # dek :queued sıkışır). Emit'ler kilidin DIŞINDA.
165
+ def release_ready!
166
+ claimed = []
167
+ cancelled = []
168
+ @monitor.synchronize do
169
+ loop do
170
+ wave = []
171
+ @deferred.each_value do |spec|
172
+ next unless spec.queued?
173
+
174
+ case dependencies_evaluate(spec)
175
+ when :ready
176
+ spec.state = :spawning
177
+ claimed << spec
178
+ when :dead
179
+ cancel_spec!(spec, "predecessor failed or is no longer tracked")
180
+ wave << spec
181
+ end
182
+ end
183
+ cancelled.concat(wave)
184
+ break if wave.empty?
185
+ end
186
+ end
187
+ announce_cancellations(cancelled)
188
+ claimed.each { |spec| launch_claimed!(spec) }
189
+ end
190
+
191
+ # wait_for'un döngü koşulu için: hedef id'ler arasında hâlâ kuyrukta
192
+ # bekleyen var mı? (Kuyruğa takılı zincir wait'i canlı tutar.)
193
+ def deferred_pending?(ids)
194
+ @monitor.synchronize do
195
+ ids.any? do |id|
196
+ spec = @deferred[id.to_s]
197
+ spec && (spec.queued? || spec.spawning?)
198
+ end
199
+ end
200
+ end
201
+
202
+ # spawn kuyruğunun kuyruk-sweep'i (spawn tail) spec'i bu arada
203
+ # bırakmış olabilir — etiketi GERÇEK duruma göre çöz (SS batch'inde
204
+ # "QUEUED" diyip arka planda koşan spec yanıltır).
205
+ def resolve_outcome(spec)
206
+ @monitor.synchronize do
207
+ inst = @instances[spec.id]
208
+ return [:launched, inst] if inst
209
+ return [:cancelled, spec] if spec.cancelled?
210
+
211
+ [:queued, spec]
212
+ end
213
+ end
214
+
215
+ private
216
+
217
+ # Bir spesifikasyonun bağımlılık durumu: :ready / :blocked / :dead.
218
+ # FS: öncül completed => hazır, failed => ölü, yoksa => bloklu.
219
+ # SS: öncül hiç start olmadıysa (pending) => bloklu, aksi halde hazır.
220
+ # Kuyrukta İPTAL edilmiş bir öncül her tip için ölüdür — yoksa
221
+ # bağımlıları sonsuza dek :queued sıkışır (liveness).
222
+ def dependencies_evaluate(spec)
223
+ verdicts = spec.deps.map do |d|
224
+ deferred_pred = @deferred[d[:id]]
225
+ return :dead if deferred_pred&.cancelled?
226
+ return :dead if deferred_pred.nil? && @instances[d[:id]].nil? && (@finished || {})[d[:id]].nil?
227
+
228
+ inst = @instances[d[:id]] || (@finished || {})[d[:id]]
229
+
230
+ if d[:type] == "FS"
231
+ return :dead if inst&.failed?
232
+ return :blocked if inst.nil? || !inst.completed?
233
+ elsif inst.nil? || inst.pending?
234
+ return :blocked
235
+ end
236
+ :ok
237
+ end
238
+ verdicts.all? { |v| v == :ok } ? :ready : :blocked
239
+ end
240
+
241
+ # Gerçek spawn koreografisi (child ctx, scoped registry, asker
242
+ # provide, start_async + fresh unit). REZERVE ID KORUNUR — modelin
243
+ # depends_on/wait ile referans verdiği kimlik bırakılan instance'ın
244
+ # kimliğidir. Başarı => spec silinir; ConcurrencyExceeded =>
245
+ # :queued'a geri (slot açılınca sıradaki süpürme alır); başka hata
246
+ # => iptal (sessiz yeniden deneme döngüsü yok — fail loud). Spawn
247
+ # ÖNCESİ/SONRASI kill yarışına karşı yeniden doğrulama: kill anlık
248
+ # :spawning spec'i işaretler, burada uyumlu hale getirilir.
249
+ def launch_claimed!(spec)
250
+ @monitor.synchronize do
251
+ return [:cancelled, spec] if spec.cancelled? || !@deferred.key?(spec.id)
252
+ end
253
+
254
+ inst = spawn(role: spec.role, prompt: spec.prompt, type_name: spec.type_name,
255
+ system_prompt: spec.system_prompt, parent_id: spec.parent_id,
256
+ workspace: spec.workspace, model: spec.model, max_steps: spec.max_steps,
257
+ provider: spec.provider, reserved_id: spec.id)
258
+
259
+ killed_midflight = false
260
+ @monitor.synchronize do
261
+ killed_midflight = spec.cancelled?
262
+ @deferred.delete(spec.id)
263
+ end
264
+ inst&.kill if killed_midflight
265
+ killed_midflight ? [:cancelled, spec] : [:launched, inst]
266
+ rescue ConcurrencyExceededError
267
+ @monitor.synchronize { spec.state = :queued unless spec.cancelled? }
268
+ [:queued, spec]
269
+ rescue StandardError => e
270
+ cancelled = nil
271
+ @monitor.synchronize do
272
+ cancel_spec!(spec, "launch failed: #{e.class}: #{e.message}")
273
+ cancelled = spec
274
+ end
275
+ announce_cancellations([cancelled])
276
+ [:cancelled, spec]
277
+ end
278
+
279
+ # Ölü öncülün kendi bağımlılarını da sabit noktaya kadar iptal et —
280
+ # iptal edilmiş spec için gelecekte süpürme tetiklenmeyecek.
281
+ def cancel_dependents_of(dead_ids)
282
+ cancelled = []
283
+ @monitor.synchronize do
284
+ until dead_ids.empty?
285
+ wave = @deferred.values.select do |spec|
286
+ spec.queued? && spec.deps.any? { |d| dead_ids.include?(d[:id]) }
287
+ end
288
+ break if wave.empty?
289
+
290
+ wave.each do |spec|
291
+ cancel_spec!(spec, "predecessor failed or is no longer tracked")
292
+ cancelled << spec
293
+ end
294
+ dead_ids = wave.map(&:id)
295
+ end
296
+ end
297
+ cancelled
298
+ end
299
+
300
+ def cancel_spec!(spec, reason)
301
+ spec.cancel_reason = reason
302
+ spec.state = :cancelled
303
+ end
304
+
305
+ def announce_cancellations(specs)
306
+ specs.each do |spec|
307
+ Notices.emit(@event_bus, Notices::COMPLETED_EVENT,
308
+ { id: spec.id, role: spec.role, state: :cancelled, max_steps: spec.max_steps })
309
+ end
310
+ end
311
+
312
+ def normalize_batch_spec(raw, provisional_id)
313
+ role = raw[:role].to_s.strip
314
+ prompt = raw[:prompt].to_s.strip
315
+ raise ArgumentError, "Subagent role cannot be empty" if role.empty?
316
+ raise ArgumentError, "Subagent prompt cannot be empty" if prompt.empty?
317
+
318
+ DeferredSpec.new(
319
+ id: "subagent-#{provisional_id}", role: role, prompt: prompt,
320
+ type_name: (raw[:type_name] || "research").to_s.strip, deps: DependencyGraph.normalize_deps(raw[:depends_on]),
321
+ system_prompt: raw[:system_prompt], workspace: (raw[:workspace] || "inherit").to_s,
322
+ model: (raw[:model] || "inherit").to_s, max_steps: (raw[:max_steps] || @default_max_steps).to_i,
323
+ provider: raw[:provider], parent_id: (raw[:parent_id] || ConversationId::ROOT_ID).to_s
324
+ )
325
+ end
326
+
327
+ def refute_dead_dependencies!(specs)
328
+ specs.each do |spec|
329
+ spec.deps.each do |d|
330
+ inst = @instances[d[:id]] || (@finished || {})[d[:id]]
331
+ if inst
332
+ # Tek tüketici: atomlar yeterli — yeni bir Instance
333
+ # predicate'ine gerek yok (settled? disposed'u dışlar,
334
+ # buradaki küme onu da içerir).
335
+ if d[:type] == "FS" && (inst.failed? || inst.disposed? || inst.canceling?)
336
+ raise ArgumentError, "FS dependency '#{d[:id]}' already #{inst.state} — a dependent on a failed predecessor is refused"
337
+ end
338
+ elsif @deferred[d[:id]]&.cancelled?
339
+ raise ArgumentError, "Dependency '#{d[:id]}' is cancelled — re-dispatch without it"
340
+ end
341
+ end
342
+ end
343
+ end
344
+
345
+ def refute_unknown_dependencies!(specs)
346
+ known = @instances.keys + (@finished || {}).keys + @deferred.keys + specs.map(&:id)
347
+ specs.each do |spec|
348
+ spec.deps.each do |d|
349
+ next if known.include?(d[:id])
350
+
351
+ raise ArgumentError, "Unknown dependency '#{d[:id]}' for #{spec.id}"
352
+ end
353
+ end
354
+ end
355
+
356
+ # Kenarlar: spec id => bağımlılık id'leri (yalnız graf içi id'ler;
357
+ # Instance'lar terminal). Döngü tespiti saf DependencyGraph'ta.
358
+ def dependency_graph(specs)
359
+ graph = {}
360
+ (@deferred.values + specs).each { |spec| graph[spec.id] = spec.deps.map { |d| d[:id] } }
361
+ graph
362
+ end
363
+
364
+ # Kilit-altı kill çekirdeği: ASLA emit etmez, ASLA süpürme yapmaz —
365
+ # mutasyon + iptal listesi toplama (announce/sweep arayanın işi,
366
+ # kilidin dışında). :spawning spec SİLİNMEZ (launch_claimed! ile
367
+ # uzlaştırılır): yalnız işaretlenir, bırakma yarışı burada ölür.
368
+ def kill_locked(id, cascade, cancelled)
369
+ if (spec = @deferred[id])
370
+ if spec.spawning?
371
+ spec.cancel_reason = "killed while launching"
372
+ spec.state = :cancelled
373
+ else
374
+ @deferred.delete(id)
375
+ end
376
+ cancelled.concat(cancel_dependents_of([spec.id]))
377
+ true
378
+ elsif (inst = @instances[id])
379
+ if cascade
380
+ @instances.values.select { |c| c.parent_id == inst.id }.each do |child|
381
+ kill_locked(child.id, cascade, cancelled)
382
+ end
383
+ end
384
+ cancelled.concat(cancel_dependents_of([inst.id]))
385
+ inst.kill
386
+ true
387
+ else
388
+ false
389
+ end
390
+ end
391
+
392
+ def queued_spec(id)
393
+ @deferred[id.to_s]
394
+ end
395
+ end
396
+ end
397
+ end
398
+ end