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,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "notices"
4
+
5
+ module Rubik
6
+ module Orchestrator
7
+ module Subagent
8
+ # THE render seam: (lifecycle state, facts) -> text, one direction,
9
+ # no reverse writes. Pure — no locks, no IO, no instance state; the
10
+ # only require is the notices vocabulary leaf. Every state->text
11
+ # spelling for an Instance's own lifecycle (state_note, the
12
+ # manage_subagents status/wait guidance) and the manage_subagents
13
+ # action vocabulary (schema enum, dispatch keys, help list, error
14
+ # text) projects from here, so the drift between them is
15
+ # unexpressable: the session-review bug class (B3 stale placeholder,
16
+ # B4 grants-blind halt wording) existed because narration was
17
+ # hand-STORED beside state instead of derived from it.
18
+ module Narration
19
+ module_function
20
+
21
+ # --- instance-state narration (the only home of state->text) ---
22
+
23
+ # The pause/halt story for a lifecycle state, computed on demand.
24
+ # state is an Instance::TRANSITIONS key; silent states (live or
25
+ # settled runs) yield nil — :completed narrates via its real
26
+ # :result, plain :failed via :last_error, so nothing is invented.
27
+ # paused_after_s is the recorded wait fact (Instance#pause_facts)
28
+ # feeding the halt wording's "within Ns" clause.
29
+ def state_note(state, grants_received: 0, max_steps: nil, last_error: nil, paused_after_s: nil)
30
+ case state.to_sym
31
+ when :waiting_budget
32
+ "Paused awaiting budget expansion from the maestro."
33
+ when :budget_exhausted
34
+ if grants_received.positive?
35
+ # B4: after a consumed grant the halt must say no FURTHER
36
+ # expansion arrived — "not received" read as a lost grant.
37
+ "#{Notices.budget_exhausted_line(max_steps)} No further budget expansion arrived " \
38
+ "within #{paused_after_s ? "#{paused_after_s}s" : 'the wait timeout'} " \
39
+ "(#{grants_received} grant(s) were received and consumed earlier)."
40
+ else
41
+ "#{Notices.budget_exhausted_line(max_steps)} Budget expansion was not received from the maestro."
42
+ end
43
+ when :paused_error
44
+ "Paused awaiting error recovery: #{last_error}"
45
+ when :waiting_input
46
+ "Asked the maestro a clarifying question; blocked awaiting the answer."
47
+ when :failed
48
+ "Subagent halted after #{paused_after_s}s in paused_error state: #{last_error}" if paused_after_s
49
+ end
50
+ end
51
+
52
+ # manage_subagents status/wait channel: blocked states get the exact
53
+ # recovery action, a mid-flight worker gets the poll discipline. All
54
+ # other states yield nil (no story).
55
+ def guidance(state, id:, max_steps:, step: nil, uptime: nil)
56
+ case state.to_sym
57
+ when :waiting_input
58
+ "Subagent asked a question and is BLOCKED awaiting your answer. Reply via send_message(recipient: '#{id}', message: '<your answer>')."
59
+ when :waiting_budget
60
+ Notices.budget_paused_line(id, max_steps)
61
+ when :budget_exhausted
62
+ Notices.budget_exhausted_line(max_steps)
63
+ when :paused_error
64
+ "Subagent paused on provider/network error. Call manage_subagents(action: 'resume', conversation_id: '#{id}') to retry."
65
+ when :running
66
+ # The poll-discipline story needs the live facts; callers without
67
+ # them (wait rows, plain state dispatch) get nil = no story —
68
+ # this guidance channel was budget-states-only before the seam.
69
+ if step && uptime
70
+ "Subagent is actively working (step #{step}/#{max_steps}, #{uptime.round(1)}s uptime). " \
71
+ "DO NOT poll 'status' in a loop! Call #{Notices.wait_any_hint} to block until completion, " \
72
+ "or simply stop calling tools to receive reactive wakeup."
73
+ end
74
+ end
75
+ end
76
+
77
+ # The nil-provider fast path's terminal stub (a legitimate outcome:
78
+ # no provider, no story beyond the echo of the task itself).
79
+ def completed_stub_line(id, role, prompt)
80
+ "Subagent [#{id}] (#{role}) completed task successfully: #{prompt.slice(0, 80)}"
81
+ end
82
+
83
+ # --- manage_subagents action vocabulary (single declaration) ---
84
+
85
+ # One canonical row per action; model-facing aliases ride along.
86
+ # The schema enum, the dispatch canonicalization, the description's
87
+ # action list, and the unknown-action error are ALL projections of
88
+ # this table — adding an action means adding a row, nothing else.
89
+ Action = Struct.new(:canonical, :aliases, :param, keyword_init: true)
90
+
91
+ ACTION_SPECS = [
92
+ Action.new(canonical: "list", aliases: []),
93
+ Action.new(canonical: "status", aliases: [], param: "conversation_id"),
94
+ Action.new(canonical: "wait", aliases: [], param: "conversation_ids"),
95
+ Action.new(canonical: "transcript", aliases: [], param: "conversation_id"),
96
+ Action.new(canonical: "extend_budget", aliases: ["grant_steps", "extend"], param: "conversation_id"),
97
+ Action.new(canonical: "resume", aliases: ["retry"], param: "conversation_id"),
98
+ Action.new(canonical: "redeploy", aliases: [], param: "conversation_id"),
99
+ Action.new(canonical: "reap", aliases: ["clean"]),
100
+ Action.new(canonical: "kill", aliases: [], param: "conversation_ids"),
101
+ Action.new(canonical: "kill_all", aliases: ["killall"])
102
+ ].freeze
103
+
104
+ CANONICAL_NAMES = ACTION_SPECS.map(&:canonical).freeze
105
+
106
+ # Model-facing vocabulary: canonical names plus their aliases (the
107
+ # schema enum previously drifted from dispatch by missing "killall").
108
+ SCHEMA_ACTIONS = ACTION_SPECS.flat_map { |spec| [spec.canonical, *spec.aliases] }.freeze
109
+
110
+ CANONICAL_LOOKUP = ACTION_SPECS.flat_map { |spec| [spec.canonical, *spec.aliases].map { |token| [token, spec.canonical] } }.to_h.freeze
111
+
112
+ # Alias-resolving dispatch key; nil = unknown action.
113
+ def canonical_action(token)
114
+ token && CANONICAL_LOOKUP[token.to_s.downcase.strip]
115
+ end
116
+
117
+ # Rendered canonical list for the tool description (order-stable, so
118
+ # TOOLS.md generation stays byte-identical).
119
+ def quoted_canonical_list
120
+ CANONICAL_NAMES.map { |name| "'#{name}'" }.join(", ")
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,209 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../platform"
4
+ require_relative "budget"
5
+
6
+ module Rubik
7
+ module Orchestrator
8
+ module Subagent
9
+ # THE single vocabulary home for subagent notices: event names and
10
+ # payloads enter, two channel renderings leave (user channel: REPL
11
+ # hints allowed; model channel: tool-oriented).
12
+ module Notices
13
+ module_function
14
+
15
+ # Bus event names
16
+ COMPLETED_EVENT = :subagent_completed
17
+ BUDGET_EXHAUSTED_EVENT = :subagent_budget_exhausted
18
+ COMPACTED_EVENT = :subagent_context_compacted
19
+ AUTO_APPLY_FAILED_EVENT = :subagent_auto_apply_failed
20
+ ERROR_PAUSED_EVENT = :subagent_error_paused
21
+ QUESTION_EVENT = :subagent_question
22
+ MESSAGE_EVENT = :subagent_message_to_parent
23
+
24
+ # Bus event name -> normalized notice kind.
25
+ EVENTS = {
26
+ COMPLETED_EVENT => :completed,
27
+ BUDGET_EXHAUSTED_EVENT => :budget_exhausted,
28
+ COMPACTED_EVENT => :context_compacted,
29
+ AUTO_APPLY_FAILED_EVENT => :auto_apply_failed,
30
+ ERROR_PAUSED_EVENT => :error_paused,
31
+ QUESTION_EVENT => :question
32
+ }.freeze
33
+
34
+ # Sessizlik (§4.3): bu roller notice ÜRETMEZ, yalnız dmesg'e düşer (Distiller + Context Pruner).
35
+ SILENT_ROLES = ["Vakanüvis Distiller", "Context Pruner"].freeze
36
+
37
+ # Dmesg-plane emission metadata (bug-llm-timeout-observability): every
38
+ # subagent-plane event lands in the ring under subsystem :subagent with
39
+ # an operational-weight severity, so dmesg(level:/subsystem:) surfaces
40
+ # a stalled subagent. MESSAGE_EVENT routes here too (a :message notice
41
+ # via handle_parent_message, not normalize).
42
+ SUBSYSTEM = :subagent
43
+
44
+ SEVERITIES = {
45
+ COMPLETED_EVENT => :info,
46
+ BUDGET_EXHAUSTED_EVENT => :warn,
47
+ COMPACTED_EVENT => :info,
48
+ AUTO_APPLY_FAILED_EVENT => :warn,
49
+ ERROR_PAUSED_EVENT => :err,
50
+ QUESTION_EVENT => :notice,
51
+ MESSAGE_EVENT => :info
52
+ }.freeze
53
+
54
+ # The one emission door: severity and subsystem are decided HERE,
55
+ # never re-derived at call sites. Nil bus = the old `&.emit` skip;
56
+ # a bus lacking #emit still raises loud; a legacy 2-arity emit gets
57
+ # a plain 2-arg call (severity metadata best-effort, delivery
58
+ # load-bearing). `fallback:` flips a raise into a stderr warn (the
59
+ # AutoApply.report_failure pattern) for load-bearing event sites.
60
+ def emit(bus, event_name, payload, fallback: nil)
61
+ return if bus.nil?
62
+
63
+ emit_it = lambda {
64
+ m = bus.method(:emit)
65
+ kw = { subsystem: SUBSYSTEM, level: SEVERITIES.fetch(event_name, :info) }
66
+ if m.parameters.any? { |p| [:key, :keyrest].include?(p.first) }
67
+ m.call(event_name, payload, **kw)
68
+ else
69
+ m.call(event_name, payload)
70
+ end
71
+ }
72
+ emit_it.call
73
+ rescue StandardError => e
74
+ raise if fallback.nil?
75
+
76
+ warn("#{fallback} (#{e.class}: #{e.message})")
77
+ end
78
+
79
+ def normalize(event_name, payload)
80
+ return nil if SILENT_ROLES.include?(payload[:role].to_s)
81
+
82
+ kind = EVENTS[event_name.to_sym]
83
+ return nil unless kind
84
+
85
+ notice = { kind: kind, id: payload[:id].to_s, role: payload[:role].to_s,
86
+ state: payload[:state].to_s, max_steps: payload[:max_steps] }
87
+ raw_err = payload[:error] || payload[:reason]
88
+ # Q-timeout marker rides AFTER the cap (#97): truncation can never eat it.
89
+ if kind == :question && notice[:state] == "question_timed_out"
90
+ notice[:reason] = "#{raw_err.to_s.slice(0, 230)} [timed out unanswered after #{payload[:timeout]}s — proceeding on its own judgment]"
91
+ elsif [:context_compacted, :auto_apply_failed, :error_paused, :question].include?(kind)
92
+ notice[:reason] = raw_err.to_s.slice(0, kind == :question ? 300 : 120)
93
+ end
94
+ notice[:timeout] = payload[:timeout] if kind == :question
95
+ # B11: grant/step markers make repeated pauses distinguishable.
96
+ if kind == :budget_exhausted
97
+ notice[:grants] = payload[:grants].to_i if payload[:grants].to_i.positive?
98
+ notice[:step] = payload[:step].to_i if payload[:step].to_i.positive?
99
+ end
100
+ notice
101
+ end
102
+
103
+ def user_line(notice)
104
+ case notice[:kind]
105
+ when :context_compacted
106
+ detail = notice[:reason].to_s.empty? ? "" : " (#{notice[:reason]})"
107
+ "🧹 [Subagent Compacted] [#{notice[:id]}] '#{notice[:role]}' recovered from a provider context-limit error by compacting its own history; work continues#{detail}."
108
+ when :auto_apply_failed
109
+ "⚠️ [Subagent Auto-Apply] [#{notice[:id]}] '#{notice[:role]}' could not auto-apply its buffer (#{notice[:reason]}). Changes remain unapplied."
110
+ when :budget_exhausted
111
+ grants = notice[:grants].to_i
112
+ step = notice[:step]
113
+ cyc = " again (after #{grants} grant(s) consumed#{", at step #{step}" if step})" if grants.positive?
114
+ "⚠️ [Subagent Paused] [#{notice[:id]}] '#{notice[:role]}' reached step budget#{cyc} " \
115
+ "(#{notice[:max_steps]} steps). Run '!subagents extend #{notice[:id]}' to resume."
116
+ when :error_paused
117
+ "⚠️ [Subagent Paused] [#{notice[:id]}] '#{notice[:role]}' paused due to provider/network error (#{notice[:reason]}). Run '!subagents resume #{notice[:id]}' to retry."
118
+ when :message
119
+ "💬 [Message from #{notice[:id]}] #{notice[:reason]}"
120
+ when :question
121
+ badge = notice[:state] == "question_timed_out" ? "⌛ [Question timed out] [#{notice[:id]}]" : "❓ [Question from #{notice[:id]}]"
122
+ "#{badge} '#{notice[:role]}': #{notice[:reason]}"
123
+ when :task
124
+ badge = notice[:state] == "completed" ? "⚡ [Task Done]" : "❌ [Task Failed]"
125
+ "#{badge} [#{notice[:id]}] - #{notice[:state].to_s.upcase}"
126
+ else
127
+ case notice[:state]
128
+ when "budget_exhausted"
129
+ "🛑 [Subagent Budget Exhausted] [#{notice[:id]}] '#{notice[:role]}' - BUDGET_EXHAUSTED (task incomplete)"
130
+ when "failed"
131
+ "❌ [Subagent Failed] [#{notice[:id]}] '#{notice[:role]}' - FAILED. Call manage_subagents(action: 'status', conversation_id: '#{notice[:id]}') for the error."
132
+ when "completed"
133
+ "⚡ [Subagent Completed] [#{notice[:id]}] '#{notice[:role]}' - COMPLETED"
134
+ else
135
+ "🏁 [Subagent Finished] [#{notice[:id]}] '#{notice[:role]}' - #{notice[:state].to_s.upcase}"
136
+ end
137
+ end
138
+ end
139
+
140
+ def model_element(notice)
141
+ attrs = { kind: notice[:kind], id: notice[:id], role: notice[:role], state: notice[:state] }
142
+ attrs[:max_steps] = notice[:max_steps] if notice[:kind] == :budget_exhausted
143
+ reason_kinds = [:context_compacted, :auto_apply_failed, :error_paused, :message, :task, :question]
144
+ attrs[:reason] = notice[:reason] if reason_kinds.include?(notice[:kind]) && !notice[:reason].to_s.empty?
145
+ attrs[:timeout] = notice[:timeout] if notice[:kind] == :question && notice[:timeout]
146
+ attrs[:guidance] = guidance_for(notice)
147
+ rendered = attrs.map { |k, v| %(#{k}="#{Rubik::Platform.xml_escape(v)}") }.join(" ")
148
+ "<notification #{rendered} />"
149
+ end
150
+
151
+ def history_block(notices)
152
+ elements = notices.map { |n| " #{model_element(n)}" }.join("\n")
153
+ "<subagent_notifications>\n#{elements}\n</subagent_notifications>"
154
+ end
155
+
156
+ # Event-channel guidance (notice-kind keyed); instance-state narration lives in Narration.
157
+ def guidance_for(notice)
158
+ case notice[:kind]
159
+ when :task
160
+ "Inspect task output via manage_task(action: 'logs', task_id: '#{notice[:id]}')."
161
+ when :context_compacted
162
+ "No action needed: the subagent recovered automatically; its history was compacted, so early details may be summarized."
163
+ when :auto_apply_failed
164
+ "Auto-apply failed; changes remain unapplied. Apply via the apply_buffer tool."
165
+ when :budget_exhausted
166
+ "Resume via #{extend_budget_hint(notice[:id])} or kill it with action: 'kill'."
167
+ when :error_paused
168
+ "Subagent paused on network error. Resume via #{resume_hint(notice[:id])} or kill with action: 'kill'."
169
+ when :message
170
+ "Message from subagent. Respond or grant steps via send_message(recipient: '#{notice[:id]}', message: '...')."
171
+ when :question
172
+ answered = "No answer is needed anymore: it timed out and the subagent proceeded on its own judgment. " \
173
+ "A late send_message(recipient: '#{notice[:id]}') lands as a regular message."
174
+ blocking = "Answer NOW via send_message(recipient: '#{notice[:id]}', message: '<your answer>') — " \
175
+ "the subagent is blocked waiting (#{notice[:timeout] || 300}s timeout)."
176
+ notice[:state] == "question_timed_out" ? answered : blocking
177
+ else
178
+ if notice[:state] == "budget_exhausted"
179
+ budget_exhausted_line(notice[:max_steps])
180
+ else
181
+ "Retrieve the full result with #{wait_hint(notice[:id])}."
182
+ end
183
+ end
184
+ end
185
+
186
+ def budget_paused_line(id, max_steps) = "Subagent reached step budget (#{max_steps} steps) and paused. Call #{extend_budget_hint(id)} to resume."
187
+
188
+ def budget_exhausted_line(max_steps = nil)
189
+ count = max_steps ? " (#{max_steps} steps)" : ""
190
+ "Subagent reached step budget#{count} and halted: task was NOT completed. Inspect transcript or spawn a new subagent."
191
+ end
192
+
193
+ DEFAULT_WAIT_TIMEOUT = 30
194
+
195
+ def render_ids(ids) = "[#{Array(ids).map { |id| "'#{id}'" }.join(', ')}]"
196
+
197
+ def wait_hint(ids, timeout: DEFAULT_WAIT_TIMEOUT) = "manage_subagents(action: 'wait', conversation_ids: #{render_ids(ids)}, timeout: #{timeout})"
198
+
199
+ def wait_any_hint(timeout: DEFAULT_WAIT_TIMEOUT) = "manage_subagents(action: 'wait', timeout: #{timeout})"
200
+
201
+ def wait_hint_template = "manage_subagents(action: 'wait', conversation_ids: [...], timeout: #{DEFAULT_WAIT_TIMEOUT})"
202
+
203
+ def extend_budget_hint(id, steps: Budget::DEFAULT_GRANT_STEPS) = "manage_subagents(action: 'extend_budget', conversation_id: '#{id}', steps: #{steps})"
204
+
205
+ def resume_hint(id) = "manage_subagents(action: 'resume', conversation_id: '#{id}')"
206
+ end
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubik
4
+ module Orchestrator
5
+ module Subagent
6
+ # The ask-maestro bridge for the ask_question tool's subagent plane.
7
+ # Flips the instance to :waiting_input, announces the question on the
8
+ # event bus (REASON carries the full display text incl. options —
9
+ # Notices needs no question-specific plumbing), then polls the shared
10
+ # inbox for the maestro's send_message reply. The worker thread is the ONLY
11
+ # inbox drainer (drain_inbox_into_history runs between steps) and it is
12
+ # blocked inside the tool here, so the pop cannot race a drain.
13
+ # Polling (not a cond var) follows the Supervisor#wait_for precedent:
14
+ # kill hard-kills the thread, so perfect signaling is not required.
15
+ module Questioner
16
+ DEFAULT_QUESTION_TIMEOUT = 300
17
+
18
+ # Returns the answer String, :unavailable (no bus / wrong state —
19
+ # nobody can ever answer, so blocking would be wrong), or nil when
20
+ # unanswered (timeout or kill).
21
+ def ask_init0(question_text, options: [], timeout: nil)
22
+ return :unavailable unless @event_bus
23
+
24
+ # Drain pending steers into history FIRST (same discipline as the
25
+ # step top): a pre-question inbox message was NOT sent to a
26
+ # question-waiting subagent and must not pose as its answer.
27
+ drain_inbox_into_history
28
+ reason = Array(options).empty? ? question_text.to_s : "#{question_text} (options: #{Array(options).join(' / ')})"
29
+ payload = { id: @id, role: @role, state: :waiting_input,
30
+ reason: reason, timeout: question_timeout(timeout) }
31
+ @monitor.synchronize do
32
+ return :unavailable unless executing?
33
+
34
+ transition!(:waiting_input)
35
+ @unit&.sync_state!
36
+ end
37
+ answer = nil
38
+ begin
39
+ # Emit OUTSIDE the monitor (budget-pause discipline): synchronous
40
+ # bus dispatch finishes before the poll starts, so the answer can
41
+ # never land before we begin waiting for it.
42
+ Notices.emit(@event_bus, Notices::QUESTION_EVENT, payload)
43
+ deadline = Time.now + payload[:timeout]
44
+ until answer
45
+ @monitor.synchronize { answer = @inbox.pop(true)&.text unless @inbox.empty? }
46
+ break if Time.now >= deadline || !(awaiting_human? || executing?)
47
+
48
+ sleep 0.1
49
+ end
50
+ ensure
51
+ # Guarded restore (also covers a raise or Thread#kill mid-wait):
52
+ # grant_budget! or kill may have legitimately moved the state on.
53
+ @monitor.synchronize do
54
+ transition!(:running) if awaiting_human?
55
+ @unit&.sync_state!
56
+ end
57
+ end
58
+ if answer.nil? && Time.now >= deadline
59
+ # The subagent-side timeout escalation ("proceed with your best
60
+ # judgment") is invisible to the maestro — it woke nobody. The
61
+ # timed-out notice re-surfaces the dead-end on the next wake so a
62
+ # late answer becomes a conscious choice, not a shot into a void
63
+ # the asker already left (issue #97 finding 4). The human-readable
64
+ # marker is appended by Notices.normalize AFTER its reason cap, so
65
+ # it can never be truncated away; here the state alone carries it.
66
+ Notices.emit(@event_bus, Notices::QUESTION_EVENT, payload.merge(state: :question_timed_out))
67
+ end
68
+ answer
69
+ end
70
+
71
+ def question_timeout(override = nil)
72
+ return override.to_f if override&.to_f&.positive?
73
+
74
+ (@config.respond_to?(:fetch) ? @config.fetch("subagents.question_timeout", DEFAULT_QUESTION_TIMEOUT) : DEFAULT_QUESTION_TIMEOUT) ||
75
+ DEFAULT_QUESTION_TIMEOUT
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../platform"
4
+
5
+ module Rubik
6
+ module Orchestrator
7
+ module Subagent
8
+ # Özenli tasfiye (diligent reaping): işi biten subagent'lar grace
9
+ # süresi sonunda mezar taşına (tombstone) taşınır — bellekteki
10
+ # history serbest bırakılır, dayanıklı transcript ve storage indeksi
11
+ # kalır, status sorgulanabilir. :budget_exhausted / :paused_error
12
+ # ASLA reap edilmez: thread canlıdır (condvar park), resume hakları
13
+ # bozulmaz.
14
+ module Reaper
15
+ def archived? = !!@archived
16
+
17
+ # History yalnız transcript diske DURABLY yazıldıysa silinir —
18
+ # dosya yoksa reap sonraki süpürmeye kalır (yarım tasfiye yok).
19
+ def archive!
20
+ @monitor.synchronize do
21
+ return false unless File.exist?(@transcript_uri.to_s)
22
+
23
+ @history.replace([])
24
+ @archived = true
25
+ true
26
+ end
27
+ end
28
+ end
29
+
30
+ # Supervisor düzlemi: tombstone haritası + otomatik/force süpürme.
31
+ # Tombstone'lar get/list üzerinden sorgulanabilir kalır; wait_for
32
+ # bilinçli olarak yalnız @instances'e bakar (reap edilen zaten
33
+ # notice ile duyurulmuş, sonucu transcriptte).
34
+ module ReaperSupervisor
35
+ def reap_finished!(force: false)
36
+ dropped_steering = []
37
+ reaped = @monitor.synchronize do
38
+ grace = reap_grace_seconds
39
+ reaped = []
40
+ @instances.each do |id, inst|
41
+ next unless inst.respond_to?(:terminal_and_settled?) && inst.terminal_and_settled?
42
+ next unless force || (grace && inst.ended_at && Time.now - inst.ended_at >= grace)
43
+ next unless inst.archive!
44
+
45
+ dropped_steering << id if inst.respond_to?(:pending_redeploy_messages?) && inst.pending_redeploy_messages?
46
+ (@finished ||= {})[id] = inst
47
+ reaped << id
48
+ end
49
+ reaped.each { |id| @instances.delete(id) }
50
+ evict_overflow!
51
+ reaped
52
+ end
53
+ # OUTSIDE the supervisor monitor (emit/logging discipline): queued
54
+ # steering that died with the instance instead of a redeploy — the
55
+ # send_message :queued reply discloses this, but the loss must be
56
+ # observable (issue #55 3.2).
57
+ Platform.debug_warn("[Reaper] reaped with undelivered queued steering: #{dropped_steering.join(', ')}") unless dropped_steering.empty?
58
+ reaped
59
+ end
60
+
61
+ private
62
+
63
+ # reap_orphans gövdesinden çağrılır (handle_subagent_completion'ın
64
+ # hazır kancası) — disposed kabukları at, sonra grace dolanları
65
+ # tombstone'la. Tamamen sessiz bir oturumda SON biten subagent
66
+ # hiçbir tamamlanma tetikleyicisi bulamaz: sınırlı kalıntı, force
67
+ # ile elle reap edilebilir.
68
+ def sweep_disposed_and_finished
69
+ @instances.delete_if { |_k, v| v.disposed? }
70
+ reap_finished!
71
+ end
72
+
73
+ def reap_grace_seconds
74
+ raw = @config.respond_to?(:fetch) ? @config.fetch("subagents.reap_finished_after", 300) : 300
75
+ raw = 300 if raw.nil?
76
+ raw.to_f <= 0 ? nil : raw.to_f
77
+ end
78
+
79
+ def tombstone_limit
80
+ raw = @config.respond_to?(:fetch) ? @config.fetch("subagents.tombstone_limit", 25) : 25
81
+ raw = 25 if raw.nil?
82
+ raw.to_i
83
+ end
84
+
85
+ # FIFO taşma: en eski mezar taşı evict edilir ve cleanup ile
86
+ # :disposed'a çekilir (ctx zaten dispose'lu; cleanup no-op güvenli).
87
+ def evict_overflow!
88
+ return unless @finished
89
+
90
+ while @finished.size > tombstone_limit
91
+ _id, inst = @finished.shift
92
+ inst&.cleanup
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubik
4
+ module Orchestrator
5
+ module Subagent
6
+ # Transient network/provider error detection, backoff retries, and
7
+ # pause-on-error coordination for autonomous subagents (Plan 101).
8
+ module Resilience
9
+ DEFAULT_NETWORK_RETRIES = 3
10
+ DEFAULT_BACKOFF_BASE = 0.5
11
+ DEFAULT_ERROR_WAIT_TIMEOUT = 300
12
+
13
+ TRANSIENT_PATTERN = Regexp.union(
14
+ /5\d{2}|server_error|engine[-_]core error|rate[-_]limit|too many requests/i,
15
+ /read timeout|timeout|closed stream|closed socket/i,
16
+ /connection reset|connection refused|broken pipe/i
17
+ )
18
+
19
+ module_function
20
+
21
+ def transient_error?(err)
22
+ return false unless err.is_a?(StandardError)
23
+
24
+ msg = err.message.to_s
25
+ return true if msg.match?(TRANSIENT_PATTERN)
26
+ return true if defined?(Rubik::LLM::RateLimitError) && err.is_a?(Rubik::LLM::RateLimitError)
27
+ return true if defined?(Rubik::LLM::ClientError) && err.is_a?(Rubik::LLM::ClientError) && msg.match?(TRANSIENT_PATTERN)
28
+
29
+ transient_exception_class?(err)
30
+ end
31
+
32
+ def transient_exception_class?(err)
33
+ name = err.class.name.to_s
34
+ name.include?("Timeout") ||
35
+ name.include?("Socket") ||
36
+ name.include?("ECONNRESET") ||
37
+ name.include?("ECONNREFUSED") ||
38
+ name.include?("EPIPE") ||
39
+ name.include?("EOFError")
40
+ end
41
+
42
+ def retry_transient(config: nil, gate: nil, max_retries: nil, backoff_base: nil, on_retry: nil, &block)
43
+ retries_limit = max_retries ||
44
+ (config.respond_to?(:fetch) && config.fetch("subagents.network_retry_count", nil)) ||
45
+ DEFAULT_NETWORK_RETRIES
46
+ base_delay = backoff_base ||
47
+ (config.respond_to?(:fetch) && config.fetch("subagents.network_backoff_base", nil)) ||
48
+ DEFAULT_BACKOFF_BASE
49
+
50
+ attempt = 0
51
+ begin
52
+ attempt += 1
53
+ if gate
54
+ gate.synchronize(&block)
55
+ else
56
+ block.call
57
+ end
58
+ rescue StandardError => e
59
+ raise e unless attempt <= retries_limit.to_i && transient_error?(e)
60
+
61
+ delay = base_delay.to_f * (2**(attempt - 1)) * (0.8 + (0.4 * rand))
62
+ on_retry&.call(attempt, retries_limit.to_i, delay, e)
63
+ sleep(delay)
64
+ retry
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end