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,384 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "monitor"
4
+ require_relative "transcript_formatter"
5
+ require_relative "storage_id"
6
+ require_relative "lifecycle"
7
+ require_relative "step_runner"
8
+ require_relative "budget"
9
+ require_relative "questioner"
10
+ require_relative "reuse"
11
+ require_relative "notices"
12
+ require_relative "narration"
13
+ require_relative "resilience"
14
+ require_relative "../../prompt"
15
+
16
+ module Rubik
17
+ module Orchestrator
18
+ module Subagent
19
+ # Autonomous background subagent execution instance running in an isolated
20
+ # disposable sub-context with its own message inbox, tool slice, and lifecycle (Plan 94, Plan 95).
21
+ class Instance
22
+ # Lifecycle (first include) owns the state vocabulary: TRANSITIONS,
23
+ # transition!, and every predicate.
24
+ include Lifecycle
25
+ include StepRunner
26
+ include Questioner
27
+ include Redeploy
28
+ include Reaper
29
+
30
+ attr_accessor :parent_id, :unit
31
+ # `state`: the sanctioned raw reader — display/serialization/kernel
32
+ # bridge only; control flow asks the Lifecycle predicates. `result`
33
+ # is TERMINAL-ONLY (real report / nil-provider stub); pauses narrate
34
+ # through #state_note — derived, nothing stored to go stale (B3).
35
+ attr_reader :id, :role, :type_name, :prompt, :system_prompt,
36
+ :workspace, :model, :context, :tool_registry, :buffer_manager,
37
+ :state, :started_at, :ended_at, :result,
38
+ :last_error, :storage_id, :transcript_uri, :history,
39
+ :context_limit, :max_output_tokens, :config, :pause_facts, :prompt_reconstructible
40
+
41
+ # Budget delegation: step counters live in Subagent::Budget (reader contract preserved).
42
+ def current_step = @budget.current_step
43
+ def max_steps = @budget.max_steps
44
+
45
+ def initialize(id:, parent_id:, role:, type_name:, prompt:, context:, tool_registry:,
46
+ system_prompt: nil, workspace: "inherit", model: "inherit",
47
+ provider: nil, buffer_manager: nil, max_steps: Budget::DEFAULT_MAX_STEPS, root_dir: Dir.pwd,
48
+ context_limit: nil, config: nil, event_bus: nil, provider_gate: nil, transcript_storage: nil,
49
+ prompt_reconstructible: false)
50
+ @id = id.to_s
51
+ @parent_id = parent_id.to_s
52
+ @role = role.to_s.strip
53
+ @type_name = type_name.to_s.downcase.strip
54
+ @prompt = prompt.to_s.strip
55
+ @system_prompt = system_prompt
56
+ @prompt_reconstructible = prompt_reconstructible # spawn kararı: kurulabilir prompt → kalıcı kopya stub'a iner
57
+ @workspace = workspace.to_s.downcase.strip
58
+ @model = model
59
+ @context = context
60
+ @tool_registry = tool_registry
61
+ @provider = provider
62
+ @buffer_manager = buffer_manager
63
+ @budget = Budget.new(max_steps: max_steps)
64
+ @root_dir = root_dir
65
+ @config = config
66
+ @event_bus = event_bus
67
+ @provider_gate = provider_gate
68
+ @context_limit = (context_limit || (@config.respond_to?(:fetch) ? @config.fetch("context.max_tokens", 32_768) : 32_768) || 32_768).to_i
69
+ @max_output_tokens = (@config.respond_to?(:fetch) ? @config.fetch("llm.default_max_output_tokens", 4_096) : 4_096).to_i
70
+
71
+ @state = :pending
72
+ @history = []
73
+ @inbox = Queue.new
74
+ @started_at = @ended_at = @result = @last_error = @transcript_path = @pause_facts = nil
75
+ # Grant telemetry (session review B4/B11): grants consumed this run —
76
+ # the halt message says "no FURTHER expansion" only when one landed.
77
+ @grants_received = 0
78
+ # SSOT (Plan 110): minted once; absorb (transcript_storage) kökü değiştirir.
79
+ @storage_id = StorageId.mint(@id)
80
+ @transcript_uri = TranscriptFormatter.mint_path(@root_dir, @storage_id, storage: transcript_storage)
81
+ @monitor = Monitor.new
82
+ @budget_cond = @monitor.new_cond
83
+ @error_cond = @monitor.new_cond
84
+ @context_cleaned_up = false
85
+ @thread = nil
86
+ end
87
+
88
+ def uptime = @started_at ? ((@ended_at || Time.now) - @started_at).to_f : 0.0
89
+
90
+ # Inbox girdisi: mesaj metni + GERÇEK gönderen id'si. nil veya makine
91
+ # adı "agent-0" gönderenler render'da maestro persona'sına çevrilir
92
+ # (iki-düzlem adlandırma sözleşmesi) — subagent gönderenler kendi
93
+ # id'leriyle görünür (drain: StepRunner#sender_label).
94
+ InboxMessage = Struct.new(:text, :from)
95
+
96
+ def post_message(message, sender: nil, additional_steps: nil)
97
+ @monitor.synchronize do
98
+ @inbox.push(InboxMessage.new(message, sender))
99
+ if error_paused?
100
+ resume!(additional_steps: additional_steps)
101
+ elsif awaiting_budget? || additional_steps
102
+ grant_budget!(additional_steps, note: ->(extra, total) { Budget.message_grant_note(extra, total) })
103
+ end
104
+ end
105
+ end
106
+
107
+ def resume!(additional_steps: nil)
108
+ @monitor.synchronize do
109
+ return false if terminal?
110
+
111
+ if error_paused?
112
+ transition!(:running)
113
+ @last_error = nil
114
+ @pause_facts = nil
115
+ # Error-resume grants ride the same accounting as budget grants
116
+ # (review B4): counted, noted to the subagent's own loop — the
117
+ # halt message must not later claim no grant ever arrived.
118
+ grant_budget!(additional_steps, note: ->(extra, total) { Budget.grant_note(extra, total) }) if additional_steps&.to_i&.positive?
119
+ @error_cond.broadcast
120
+ @unit&.sync_state!
121
+ true
122
+ elsif awaiting_budget?
123
+ grant_budget!(additional_steps || Budget::DEFAULT_GRANT_STEPS,
124
+ note: ->(extra, total) { Budget.grant_note(extra, total) })
125
+ else
126
+ false
127
+ end
128
+ end
129
+ end
130
+
131
+ def extend_budget(additional_steps = Budget::DEFAULT_GRANT_STEPS)
132
+ grant_budget!(additional_steps, note: ->(extra, total) { Budget.grant_note(extra, total) })
133
+ end
134
+
135
+ def start_async(&on_complete)
136
+ @monitor.synchronize do
137
+ return false if @thread&.alive?
138
+
139
+ @started_at = Time.now
140
+ transition!(:running)
141
+ @unit&.sync_state!
142
+ end
143
+
144
+ @thread = Thread.new do
145
+ run_execution_cycle
146
+ ensure
147
+ @ended_at ||= Time.now
148
+ save_transcript! if @transcript_path.nil?
149
+ cleanup_context
150
+ on_complete&.call(self)
151
+ end
152
+ true
153
+ end
154
+
155
+ def kill
156
+ @monitor.synchronize do
157
+ return true if disposed?
158
+
159
+ transition!(:canceling)
160
+ @ended_at = Time.now
161
+ @budget_cond.broadcast
162
+ @error_cond.broadcast
163
+ end
164
+
165
+ begin; @thread&.kill if @thread&.alive?; rescue StandardError; nil; end
166
+
167
+ cleanup
168
+ true
169
+ end
170
+
171
+ def cleanup
172
+ @monitor.synchronize do
173
+ return if disposed?
174
+
175
+ @budget_cond.broadcast
176
+ @error_cond.broadcast
177
+ cleanup_context
178
+ transition!(:disposed)
179
+ end
180
+ end
181
+
182
+ # Idempotent under monitor: check-and-set of @context_cleaned_up (ensure block vs kill cleanup race).
183
+ def cleanup_context
184
+ @monitor.synchronize do
185
+ return if @context_cleaned_up
186
+
187
+ @context_cleaned_up = true
188
+ begin
189
+ @context&.dispose unless @context&.disposed?
190
+ rescue StandardError => e
191
+ @last_error ||= "#{e.class}: #{e.message}"
192
+ end
193
+ end
194
+ end
195
+
196
+ def to_h
197
+ @monitor.synchronize do
198
+ { id: @id, parent_id: @parent_id, role: @role, type: @type_name, state: @state.to_s,
199
+ **@budget.to_h, grants_received: @grants_received, uptime_seconds: uptime.round(2),
200
+ started_at: @started_at&.iso8601, ended_at: @ended_at&.iso8601,
201
+ transcript: @transcript_uri, last_error: @last_error,
202
+ result_summary: @result&.to_s&.slice(0, 100), result: @result, state_note: state_note }
203
+ end
204
+ end
205
+
206
+ # Derived narration (Narration seam): computed on demand, never stored.
207
+ def state_note
208
+ @monitor.synchronize do
209
+ Narration.state_note(@state, grants_received: @grants_received, max_steps: @budget.max_steps,
210
+ last_error: @last_error, paused_after_s: @pause_facts&.dig(:timeout))
211
+ end
212
+ end
213
+
214
+ def format_transcript
215
+ TranscriptFormatter.format(self, @history, @monitor)
216
+ end
217
+
218
+ private
219
+
220
+ # The run's single failure drain: record why, land on :failed, sync
221
+ # the unit, persist — the loud prefix keeps an FSM violation
222
+ # distinguishable from a provider failure.
223
+ def fail_run(reason)
224
+ @monitor.synchronize do
225
+ @last_error = reason
226
+ transition!(:failed)
227
+ @unit&.sync_state!
228
+ end
229
+ save_transcript!
230
+ end
231
+
232
+ # Private by design: a grant must pair with the terminal guard, the
233
+ # history note, and the broadcast — all lifecycle facts owned here.
234
+ # Tests drive budget states through supervisor + provider, never by
235
+ # reaching into this reader.
236
+ attr_reader :budget
237
+
238
+ def save_transcript!
239
+ saved = TranscriptFormatter.save!(@transcript_uri, @history, @monitor,
240
+ prompt_reconstructible: @prompt_reconstructible)
241
+ @transcript_path = saved
242
+ # A failed save must not be silent: the advertised path would 404 silently.
243
+ @last_error ||= "Transcript save failed for #{@transcript_uri}: see stderr" unless saved
244
+ end
245
+
246
+ def run_execution_cycle
247
+ # Fresh runs seed; a redeployed run continues its preserved history.
248
+ @history.push({ role: "system", content: build_effective_system_prompt }, { role: "user", content: @prompt }) if @history.empty?
249
+
250
+ unless @provider
251
+ @budget.consume
252
+ @result = Narration.completed_stub_line(@id, @role, @prompt)
253
+ transition!(:completed)
254
+ @unit&.sync_state!
255
+ save_transcript!
256
+ return
257
+ end
258
+
259
+ execute_steps_with_provider
260
+ @unit&.sync_state!
261
+ save_transcript!
262
+ rescue StateError => e
263
+ fail_run("LIFECYCLE INVARIANT VIOLATED: #{e.class}: #{e.message}")
264
+ rescue StandardError => e
265
+ fail_run("#{e.class}: #{e.message}")
266
+ end
267
+
268
+ # The single budget-grant core (Candidate 03): terminal guard ->
269
+ # grant -> resume -> note -> broadcast. Note wording lives in
270
+ # Budget.grant_note/message_grant_note (audience boundary: the
271
+ # subagent's own loop); call sites pass a lambda receiving extra +
272
+ # new total. The budget mechanics run inside @budget.grant! under
273
+ # the Budget lock (lock order: instance -> budget). The broadcast
274
+ # is a harmless no-op when nobody waits. Terminal and canceling
275
+ # states refuse the grant — a dying or dead instance must not
276
+ # resurrect (kill's race window included).
277
+ def grant_budget!(steps, note:)
278
+ @monitor.synchronize do
279
+ return false if terminal?
280
+
281
+ extra, total = @budget.grant!(steps)
282
+ transition!(:running)
283
+ @grants_received += 1
284
+ @pause_facts = nil # the wake retires the wait fact with the pause (B3's clearer, now fact-only)
285
+ @history << { role: "user", content: note.call(extra, total) }
286
+ @budget_cond.broadcast
287
+ @error_cond.broadcast
288
+ @unit&.sync_state!
289
+ true
290
+ end
291
+ end
292
+
293
+ def pause_and_await_budget_expansion!(wait_timeout = nil)
294
+ timeout = wait_timeout || (@config.respond_to?(:fetch) ? @config.fetch("subagents.budget_wait_timeout", 180) : 180) || 180
295
+ payload = nil
296
+ @monitor.synchronize do
297
+ return false unless executing?
298
+
299
+ transition!(:waiting_budget)
300
+ @pause_facts = { timeout: timeout }.freeze
301
+ payload = { id: @id, role: @role, state: @state, max_steps: @budget.max_steps,
302
+ grants: @grants_received, step: @budget.current_step }
303
+ @unit&.sync_state!
304
+ end
305
+
306
+ # The object that owns the fact emits it (outside the monitor, so
307
+ # subscribers never run under the instance lock).
308
+ Notices.emit(@event_bus, Notices::BUDGET_EXHAUSTED_EVENT, payload)
309
+
310
+ exhausted_payload = nil
311
+ @monitor.synchronize do
312
+ @budget_cond.wait(timeout) if @state == :waiting_budget
313
+ if @state == :waiting_budget
314
+ transition!(:budget_exhausted)
315
+ exhausted_payload = { id: @id, role: @role, state: @state, max_steps: @budget.max_steps,
316
+ grants: @grants_received, step: @budget.current_step }
317
+ @unit&.sync_state!
318
+ end
319
+ end
320
+
321
+ Notices.emit(@event_bus, Notices::COMPLETED_EVENT, exhausted_payload) if exhausted_payload
322
+
323
+ @monitor.synchronize do
324
+ @budget_cond.wait while @state == :budget_exhausted
325
+ return false if terminal?
326
+
327
+ transition!(:running)
328
+ @unit&.sync_state!
329
+ true
330
+ end
331
+ end
332
+
333
+ def pause_on_error!(error, wait_timeout = nil)
334
+ timeout = wait_timeout ||
335
+ (@config.respond_to?(:fetch) && @config.fetch("subagents.error_wait_timeout", nil)) ||
336
+ Resilience::DEFAULT_ERROR_WAIT_TIMEOUT
337
+ payload = nil
338
+ @monitor.synchronize do
339
+ return false unless executing?
340
+
341
+ transition!(:paused_error)
342
+ @last_error = "#{error.class}: #{error.message}"
343
+ @pause_facts = { timeout: timeout }.freeze
344
+ payload = { id: @id, role: @role, state: @state, error: @last_error }
345
+ @unit&.sync_state!
346
+ end
347
+
348
+ # The pause IS the recovery: a raising emit degrades to warn (door fallback), never to a terminal failure.
349
+ Notices.emit(@event_bus, Notices::ERROR_PAUSED_EVENT, payload,
350
+ fallback: "[Subagent] '#{@id}' error-paused notice emit failed (#{@last_error})")
351
+
352
+ @monitor.synchronize do
353
+ @error_cond.wait(timeout) if error_paused?
354
+ if error_paused?
355
+ transition!(:failed)
356
+ @unit&.sync_state!
357
+ false
358
+ else
359
+ # Symmetric with the budget wake: a kill that flipped us to
360
+ # :canceling while parked wins — the worker must not resurrect.
361
+ return false if terminal?
362
+
363
+ transition!(:running)
364
+ @unit&.sync_state!
365
+ true
366
+ end
367
+ end
368
+ end
369
+
370
+ def build_effective_system_prompt
371
+ base = "You are a specialized subagent operating under autonomous supervision.\n\n" \
372
+ "Subagent ID: #{@id} | Role: #{@role} | Type: #{@type_name}\n" \
373
+ "Allocated Step Budget: #{@budget.max_steps} steps.\n\n" \
374
+ "A [Step X/Y] status line precedes each of your steps (X = current step, Y = total budget). Pace yourself: " \
375
+ "when remaining steps (Y - X) run low, conclude the task or prepare a progress summary for the maestro " \
376
+ "supervisor — it may extend your budget or steer you. If a requirement is genuinely ambiguous, ask the maestro directly with the " \
377
+ "ask_question tool — it blocks until the maestro answers or times out; ask only when the answer truly changes your work.\n\n" \
378
+ "Perform your assigned task with high precision and provide a structured synthesis report."
379
+ @system_prompt && !@system_prompt.strip.empty? ? "#{base}\n\nCustom Instructions:\n#{@system_prompt}" : base
380
+ end
381
+ end
382
+ end
383
+ end
384
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubik
4
+ module Orchestrator
5
+ module Subagent
6
+ # The Instance lifecycle vocabulary, extracted from instance.rb to
7
+ # satisfy module hygiene (same vertical-cohesion extraction as
8
+ # StepRunner/Questioner, Plan 95): TRANSITIONS is the single home of
9
+ # the state machine, transition! its only mutation path, and these
10
+ # predicates the only control-flow surface. Instance includes this
11
+ # module, so Instance::TRANSITIONS / Instance::StateError remain the
12
+ # public constants — adding a state means editing THIS file (plus the
13
+ # kernel STATE_MAP, held by contract test).
14
+ module Lifecycle
15
+ class StateError < StandardError; end
16
+
17
+ # Every legal from->to edge. A transition not listed here is a bug
18
+ # — fail loud with both endpoints in the message. :disposed is
19
+ # absorbing (reaper/evict own the shell). completed/failed ->
20
+ # running is the redeploy window (redeploy_for! only resets;
21
+ # start_async performs the flip). canceling -> completed/failed is
22
+ # the kill race: kill announces under the monitor but Thread#kill
23
+ # lands after, so the worker may still record its outcome;
24
+ # completed -> failed is the total-catch rescue firing after a
25
+ # post-outcome raise (transcript save, sync). Same-state writes are
26
+ # idempotent no-ops — the waker flips before the parked thread
27
+ # re-flips.
28
+ TRANSITIONS = {
29
+ pending: [:running, :canceling, :disposed],
30
+ running: [:waiting_input, :waiting_budget, :paused_error, :completed, :failed, :canceling, :disposed],
31
+ waiting_input: [:running, :canceling, :disposed],
32
+ waiting_budget: [:budget_exhausted, :running, :failed, :canceling, :disposed],
33
+ budget_exhausted: [:running, :failed, :canceling, :disposed],
34
+ paused_error: [:running, :failed, :canceling, :disposed],
35
+ canceling: [:completed, :failed, :disposed],
36
+ completed: [:running, :failed, :canceling, :disposed],
37
+ failed: [:running, :canceling, :disposed],
38
+ disposed: []
39
+ }.transform_values(&:freeze).freeze
40
+
41
+ # --- Predicates: control-flow questions answer here, not at
42
+ # consumers. All take the re-entrant monitor; callers composing
43
+ # them under a held lock pay nothing extra, and external readers
44
+ # see consistent snapshots.
45
+
46
+ # Worker thread mid-flight (possibly parked on a steering, budget,
47
+ # or error wait).
48
+ def running?
49
+ @monitor.synchronize { @state == :running || @state == :waiting_input || @state == :waiting_budget || @state == :paused_error }
50
+ end
51
+
52
+ # Occupies a concurrency slot / tracked in Supervisor#instances.
53
+ # Composed from the vocabulary's own words: everything live except
54
+ # :budget_exhausted (deliberately excluded, pinned by
55
+ # test_pause_and_await_budget_expansion_timeout; supervisor's
56
+ # send_message compensates via deliverable?).
57
+ def active?
58
+ @monitor.synchronize { pending? || running? || canceling? }
59
+ end
60
+
61
+ def pending? = @monitor.synchronize { @state == :pending }
62
+ def executing? = @monitor.synchronize { @state == :running }
63
+ def awaiting_human? = @monitor.synchronize { @state == :waiting_input }
64
+ def error_paused? = @monitor.synchronize { @state == :paused_error }
65
+ def canceling? = @monitor.synchronize { @state == :canceling }
66
+ def completed? = @monitor.synchronize { @state == :completed }
67
+ def failed? = @monitor.synchronize { @state == :failed }
68
+ def disposed? = @monitor.synchronize { @state == :disposed }
69
+
70
+ # A budget grant is the only wake-up from either of these.
71
+ def awaiting_budget?
72
+ @monitor.synchronize { @state == :waiting_budget || @state == :budget_exhausted }
73
+ end
74
+
75
+ # Condvar-parked, reap-immune; the resume_all targets.
76
+ def parked?
77
+ @monitor.synchronize { error_paused? || awaiting_budget? }
78
+ end
79
+
80
+ # The no-resurrection set (kill's race window included).
81
+ def terminal?
82
+ @monitor.synchronize { settled? || disposed? || canceling? }
83
+ end
84
+
85
+ # Final outcome recorded; the thread may still be in its ensure
86
+ # block — terminal_and_settled? (Redeploy) is the strict form.
87
+ def settled?
88
+ @monitor.synchronize { @state == :completed || @state == :failed }
89
+ end
90
+
91
+ # Inbox accepts a message; exactly active? plus :budget_exhausted.
92
+ # :canceling stays deliverable (a message to a dying instance is
93
+ # harmless but pointless) — preserved behavior, revisitable.
94
+ def deliverable?
95
+ @monitor.synchronize { !settled? && !disposed? }
96
+ end
97
+
98
+ # Guarded low-level seam (mirrors Unit#transition_to!): business
99
+ # callers use the named drivers (resume!, grant_budget!, kill...);
100
+ # tests use this for legal-but-rare windows (e.g. the :canceling
101
+ # kill race, which is not externally holdable). Idempotent on same
102
+ # state; raises StateError on an undeclared edge. Does NOT sync the
103
+ # unit or emit — call sites own that timing (the kernel reconciles
104
+ # lazily through its own sync_state! polls).
105
+ def transition!(to)
106
+ @monitor.synchronize do
107
+ target = to.to_sym
108
+ return @state if @state == target
109
+
110
+ raise StateError, "illegal subagent transition #{@id}: #{@state} -> #{target}" unless TRANSITIONS.fetch(@state, []).include?(target)
111
+
112
+ @state = target
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../platform"
4
+ require_relative "notices"
5
+ require_relative "conversation_id"
6
+
7
+ module Rubik
8
+ module Orchestrator
9
+ module Subagent
10
+ # Inter-agent messaging (Plan 94 + issue #55 3.2): supervisor-plane
11
+ # delivery for SendMessageTool. Extracted from supervisor.rb to keep
12
+ # the file under its module-hygiene cap; the supervisor delegates
13
+ # send_message here. Root recipient aliases live in conversation_id.rb
14
+ # (root plane alias resolution is referenced by tool reply text too).
15
+ module MessagingSupervisor
16
+ # Returns true (delivered to an active instance), :queued (buffered
17
+ # on a finished-but-unreaped instance — drain_inbox_into_history
18
+ # delivers it when that id is redeployed), or false (unknown /
19
+ # disposed / reaped-mid-flight recipient). The completion→reap
20
+ # window used to reject steering outright while redeploy on the same
21
+ # id succeeded (issue #55 3.2); buffering closes it. The buffer is
22
+ # monitor-atomic against the reaper (buffer_for_redeploy), and
23
+ # posting to a settled instance cannot resurrect it (pinned by
24
+ # test_dead_instance_must_not_resurrect_via_post_message).
25
+ def send_message(recipient_id, message, sender_id: ConversationId::ROOT_ID, additional_steps: nil, **kwargs)
26
+ rec = ConversationId.scrub(recipient_id)
27
+ eff_sender = kwargs[:_sender_id] || sender_id
28
+ if ConversationId.root_alias?(rec)
29
+ Notices.emit(@event_bus, Notices::MESSAGE_EVENT, { sender_id: eff_sender, recipient: rec, message: message })
30
+ return true
31
+ end
32
+
33
+ inst = get(rec)
34
+ return false unless inst
35
+
36
+ if inst.deliverable?
37
+ inst.post_message(message, sender: eff_sender, additional_steps: additional_steps)
38
+ return true
39
+ end
40
+
41
+ # get's queue fallback can also surface a DeferredSpec (a not-yet-
42
+ # born dependency-queued spec): nothing to buffer onto, nothing to
43
+ # redeploy — undeliverable.
44
+ return false unless inst.respond_to?(:buffer_for_redeploy)
45
+
46
+ # Finished-but-unreaped recipient: buffer into the inbox —
47
+ # redeploy's drain_inbox_into_history delivers it. Steps grants
48
+ # are meaningless here (redeploy mints a fresh budget). A race
49
+ # loss (reaper archived the instance between the checks) reports
50
+ # false instead of a stranded :queued.
51
+ if inst.buffer_for_redeploy(message, sender: eff_sender)
52
+ :queued
53
+ else
54
+ Platform.debug_warn("[Supervisor] steering message for '#{rec}' lost: reaped between liveness checks")
55
+ false
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end