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,326 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "token_estimator"
4
+ require_relative "conversation_rows"
5
+ require_relative "../tools/truncator"
6
+
7
+ module Rubik
8
+ module Prompt
9
+ module ContextCompactor
10
+ # Plan 61 D-12: single source of truth for the live conversation
11
+ # window cap (TurnRecorder and Session::Manager both use it).
12
+ DEFAULT_MAX_CONVERSATION_ENTRIES = 40
13
+
14
+ module_function
15
+
16
+ def effective_input_ceiling(context_limit:, max_output_tokens: 4096, safety_margin: 1024)
17
+ [1024, context_limit.to_i - max_output_tokens.to_i - safety_margin.to_i].max
18
+ end
19
+
20
+ def needs_compaction?(history, context_limit:, max_output_tokens: 4096, safety_margin: 1024, max_tokens_ratio: 0.85, config: {})
21
+ return false if history.nil? || history.empty?
22
+
23
+ ratio = (config["max_tokens_ratio"] || config[:max_tokens_ratio] || max_tokens_ratio || 0.85).to_f
24
+ margin = (config["safety_margin_tokens"] || config[:safety_margin_tokens] || safety_margin || 1024).to_i
25
+ max_out = (config["max_output_tokens"] || config[:max_output_tokens] || max_output_tokens || 4096).to_i
26
+
27
+ ceiling = effective_input_ceiling(
28
+ context_limit: context_limit,
29
+ max_output_tokens: max_out,
30
+ safety_margin: margin
31
+ )
32
+ threshold = (ceiling * ratio).to_i
33
+ total_tokens = TokenEstimator.estimate_history(history)
34
+ total_tokens > threshold
35
+ end
36
+
37
+ # absorb: (goal-ledger planı D) duck-typed directive — nil (default)
38
+ # iken tier atlanır ve davranış bit-bit aynıdır; verilirse dup
39
+ # kopyaya #apply!(rows) uygulanır (prompt katmanı goal bilmez;
40
+ # directive'i orchestrator/goal_ledger ailesi kurar).
41
+ def compact_history(history, context_limit:, max_output_tokens: 4096, safety_margin: 1024, config: {}, force: false, absorb: nil)
42
+ return [] if history.nil? || history.empty?
43
+
44
+ strategy = (config["compaction_strategy"] || config[:compaction_strategy] || "hybrid").to_s
45
+ return history.map { |m| ConversationRows.row?(m) ? m.dup : m } if strategy == "off"
46
+
47
+ max_tokens_ratio = (config["max_tokens_ratio"] || config[:max_tokens_ratio] || 0.85).to_f
48
+ tool_result_prune_chars = (config["tool_result_prune_chars"] || config[:tool_result_prune_chars] || 1500).to_i
49
+ preserve_first_n = (config["preserve_first_n"] || config[:preserve_first_n] || 2).to_i
50
+ preserve_last_n = (config["preserve_last_n"] || config[:preserve_last_n] || 6).to_i
51
+ safety_margin = (config["safety_margin_tokens"] || config[:safety_margin_tokens] || safety_margin || 1024).to_i
52
+
53
+ ceiling = effective_input_ceiling(
54
+ context_limit: context_limit,
55
+ max_output_tokens: max_output_tokens,
56
+ safety_margin: safety_margin
57
+ )
58
+ threshold = (ceiling * max_tokens_ratio).to_i
59
+
60
+ # Return duplicate if under threshold and not forced
61
+ if !force && TokenEstimator.estimate_history(history) <= threshold
62
+ return history.map { |m| ConversationRows.row?(m) ? m.dup : m }
63
+ end
64
+
65
+ compacted = history.map { |m| ConversationRows.row?(m) ? m.dup : m }
66
+
67
+ # Tier 0 (plan D): absorption — generic drop'lardan ÖNCE; absorb
68
+ # tek başına eşiği karşıladıysa (force yoksa) generic tier'lar hiç
69
+ # koşmaz (:72 erken-çıkış kalıbının aynası).
70
+ if absorb
71
+ absorb.apply!(compacted)
72
+ return compacted if !force && TokenEstimator.estimate_history(compacted) <= threshold
73
+ end
74
+
75
+ # Strategy: prune_tool_results or hybrid (Phase 1: Pruning intermediate tool results)
76
+ if ["hybrid", "prune_tool_results"].include?(strategy)
77
+ compacted = prune_intermediate_tool_results(
78
+ compacted,
79
+ preserve_first_n: preserve_first_n,
80
+ preserve_last_n: preserve_last_n,
81
+ max_chars: tool_result_prune_chars
82
+ )
83
+
84
+ return compacted if strategy == "prune_tool_results" || (!force && TokenEstimator.estimate_history(compacted) <= threshold)
85
+ end
86
+
87
+ # Strategy: sliding_window or hybrid (Phase 2: Collapsing intermediate atomic units)
88
+ if ["hybrid", "sliding_window"].include?(strategy)
89
+ compacted = apply_sliding_window(
90
+ compacted,
91
+ threshold: threshold,
92
+ preserve_first_n: preserve_first_n,
93
+ preserve_last_n: preserve_last_n,
94
+ force: force
95
+ )
96
+ end
97
+
98
+ compacted
99
+ end
100
+
101
+ def prune_intermediate_tool_results(history, preserve_first_n:, preserve_last_n:, max_chars:)
102
+ return history if history.size <= preserve_first_n + preserve_last_n
103
+
104
+ first_end_idx = preserve_first_n
105
+ last_start_idx = [history.size - preserve_last_n, first_end_idx].max
106
+
107
+ history.each_with_index do |msg, idx|
108
+ next unless idx >= first_end_idx && idx < last_start_idx
109
+ next unless ConversationRows.row?(msg)
110
+
111
+ role = ConversationRows.role_of(msg)
112
+ next unless ["tool", "tool_result"].include?(role)
113
+
114
+ # A loaded skill body is the task's standing instruction payload
115
+ # (progressive disclosure), not intermediate output — pruning it to
116
+ # max_chars mid-task would silently amputate the agent's orders.
117
+ # The sliding-window phase may still drop it under genuine token
118
+ # emergency; the <skills> index lets the agent re-invoke `skill`.
119
+ next if (msg[:tool_name] || msg["tool_name"]) == Rubik::Tools::SkillTool::NAME
120
+
121
+ content_key = ConversationRows.content_key_of(msg)
122
+ content = msg[content_key].to_s
123
+ if content.length > max_chars
124
+ trunc = Rubik::Tools::Truncator.truncate(content, max_chars: max_chars, strategy: :head_tail)
125
+ msg[content_key] = trunc[:text]
126
+ end
127
+ end
128
+
129
+ history
130
+ end
131
+
132
+ def group_atomic_units(history)
133
+ units = []
134
+ i = 0
135
+ while i < history.size
136
+ msg = history[i]
137
+ role = ConversationRows.role_of(msg)
138
+ tool_calls = msg[:tool_calls] || msg["tool_calls"]
139
+
140
+ if role == "assistant" && tool_calls && !tool_calls.empty?
141
+ unit = [msg]
142
+ j = i + 1
143
+ while j < history.size
144
+ next_msg = history[j]
145
+ next_role = ConversationRows.role_of(next_msg)
146
+ break unless ["tool", "tool_result"].include?(next_role)
147
+
148
+ unit << next_msg
149
+ j += 1
150
+
151
+ end
152
+ units << unit
153
+ i = j
154
+ else
155
+ units << [msg]
156
+ i += 1
157
+ end
158
+ end
159
+ units
160
+ end
161
+
162
+ # Hedef mesajı mı? <user_task> sarmalayıcısını yalnızca
163
+ # ContextBuilder.format_user_task üretir — goal/plan döngülerinin görev
164
+ # bildirgesi. Açılış+kapanış etiketi birlikte aranır: kullanıcı
165
+ # mesajına alıntılanmış yalnızca açılış etiketi yapıştırmak mesajı
166
+ # kalıcı olarak sabitleyip düşürülemez/devrilinemez hale getirirdi.
167
+ # String/symbol anahtar farkı umursanmaz.
168
+ def goal_message?(msg)
169
+ return false unless ConversationRows.row?(msg)
170
+
171
+ role = ConversationRows.role_of(msg)
172
+ content = ConversationRows.content_of(msg).to_s
173
+ role == "user" && content.include?("<user_task>") && content.include?("</user_task>")
174
+ end
175
+
176
+ # Yapılandırılmış goal ledger satırı mı? (goal-ledger planı A) —
177
+ # goal_message? ile aynı alıntı-tek-etiket disiplini: açılış
178
+ # `<goal_ledger ` + kapanış `</goal_ledger>` BİRLİKTE aranır; kullanıcı
179
+ # mesajına yapıştırılan tek etiket mesajı pınleyemez. Role "user"
180
+ # zorunludur (EmergencyCompactor hard tier user satırlarına dokunmaz).
181
+ def ledger_message?(msg)
182
+ return false unless ConversationRows.row?(msg)
183
+
184
+ role = ConversationRows.role_of(msg)
185
+ content = ConversationRows.content_of(msg).to_s
186
+ role == "user" && content.start_with?("<goal_ledger ") && content.include?("</goal_ledger>")
187
+ end
188
+
189
+ # İsim korunur (pruner rules.rb:39 çağrısı yük taşıyor): pin ailesi
190
+ # artık hedef satırı VE güncel Σ satırını kapsar.
191
+ def unit_pins_goal?(unit)
192
+ unit.any? { |msg| goal_message?(msg) || ledger_message?(msg) }
193
+ end
194
+
195
+ def apply_sliding_window(history, threshold:, preserve_first_n:, preserve_last_n:, force: false) # rubocop:disable Lint/UnusedMethodArgument -- arayüz uyumluluğu için force korunuyor
196
+ units = group_atomic_units(history)
197
+ return history if units.size <= 2
198
+
199
+ # Hedef sabitleme: <user_task> taşıyan birimler ARA birim olsa bile
200
+ # asla düşürülemez ve trim edilemez — model hedefini koşu ortasında
201
+ # kaybetmesin. Normal ve emergency yolların ikisi de compact_history
202
+ # üzerinden buraya akar. Birden çok <user_task> (run yeniden girişi)
203
+ # özgün sırasıyla sabitlenir.
204
+ pinned = units.map { |unit| unit_pins_goal?(unit) }
205
+
206
+ # Determine head units (covering at least preserve_first_n messages)
207
+ head_units = []
208
+ head_msg_count = 0
209
+ head_idx = 0
210
+ while head_idx < units.size && head_msg_count < preserve_first_n
211
+ head_units << units[head_idx]
212
+ head_msg_count += units[head_idx].size
213
+ head_idx += 1
214
+ end
215
+
216
+ # Determine tail units (covering at least preserve_last_n messages)
217
+ tail_units = []
218
+ tail_msg_count = 0
219
+ tail_idx = units.size - 1
220
+ while tail_idx >= head_idx && tail_msg_count < preserve_last_n
221
+ tail_units.unshift(units[tail_idx])
222
+ tail_msg_count += units[tail_idx].size
223
+ tail_idx -= 1
224
+ end
225
+
226
+ intermediate_indexes = (head_idx..tail_idx).to_a
227
+ return history if intermediate_indexes.empty?
228
+
229
+ # Sabitlenmiş birimler düşürülemeyeceğinden droppable kuyruğundan
230
+ # çıkarılır; yalnızca kalanlar (en eskiden yeniye) düşürülür.
231
+ droppable = intermediate_indexes.reject { |idx| pinned[idx] }
232
+ return history if droppable.empty?
233
+
234
+ dropped_indexes = []
235
+ remaining = droppable.dup
236
+ # Plan 61 D-9: drop first, THEN measure — the previous loop measured
237
+ # the candidate before shifting, so it could drop one unit too many.
238
+ until remaining.empty?
239
+ dropped_indexes << remaining.shift
240
+ break if TokenEstimator.estimate_history(assemble_window(units, dropped_indexes)) <= threshold
241
+ end
242
+
243
+ result = assemble_window(units, dropped_indexes)
244
+
245
+ # Unit dropping cannot shrink a single oversized user turn —
246
+ # hard-trim the largest remaining plain messages if still over.
247
+ # Sabitlenmiş aile (hedef <user_task> + Σ <goal_ledger>) bu katmandan muaftır.
248
+ if TokenEstimator.estimate_history(result) > threshold
249
+ pinned_indexes = result.each_index.select { |i| goal_message?(result[i]) || ledger_message?(result[i]) }
250
+ result = trim_oversized_messages(result, threshold: threshold, pinned_indexes: pinned_indexes)
251
+ end
252
+ result
253
+ end
254
+
255
+ # Birimleri özgün sıralarında (düşürülenler hariç) birleştirir ve
256
+ # <compacted_summary>'yi İLK düşürülen birimin konumuna yerleştirir.
257
+ # Sabitleme yoksa ilk düşürme her zaman ilk ara birimdir → yerleşim,
258
+ # eski "head + summary + rest" düzeniyle bire bir aynıdır.
259
+ def assemble_window(units, dropped_indexes)
260
+ first_drop = dropped_indexes.min
261
+ result = []
262
+ summary_slot = nil
263
+ units.each_with_index do |unit, idx|
264
+ summary_slot = result.size if idx == first_drop
265
+ result.concat(unit) unless dropped_indexes.include?(idx)
266
+ end
267
+ dropped_msg_count = dropped_indexes.sum { |i| units[i].size }
268
+ result.insert(summary_slot || result.size, summary_message(dropped_msg_count))
269
+ result
270
+ end
271
+
272
+ # Plan 61 D-9: last-resort in-place shortening of the largest
273
+ # user/assistant messages when structural dropping is not enough.
274
+ # pinned_indexes (hedef <user_task> mesajları) bu katmandan muaftır.
275
+ def trim_oversized_messages(history, threshold:, min_len: 2000, max_passes: 10, pinned_indexes: [])
276
+ max_passes.times do
277
+ break if TokenEstimator.estimate_history(history) <= threshold
278
+
279
+ candidates = history.each_index.select { |i| ConversationRows.row?(history[i]) && !pinned_indexes.include?(i) }
280
+ eligible = candidates.select do |i|
281
+ role = ConversationRows.role_of(history[i])
282
+ content = ConversationRows.content_of(history[i])
283
+ ["user", "assistant"].include?(role) && content.to_s.length > min_len
284
+ end
285
+ idx = eligible.max_by { |i| ConversationRows.content_of(history[i]).to_s.length }
286
+ break if idx.nil?
287
+
288
+ content_key = ConversationRows.content_key_of(history[idx])
289
+ str = history[idx][content_key].to_s
290
+ trunc = Rubik::Tools::Truncator.truncate(str, max_chars: str.length / 2, max_lines: 10_000, strategy: :head_tail)
291
+ history[idx][content_key] = trunc[:text]
292
+ end
293
+ history
294
+ end
295
+
296
+ # Plan 61 D-12: token-aware rolling window for the long-lived REPL
297
+ # conversation history. Drops the oldest entries until the estimate
298
+ # fits the ceiling; max_entries is an absolute cap; the most recent
299
+ # min_entries messages always survive. The window is guaranteed to
300
+ # open with a user turn — strict providers (Anthropic) require it.
301
+ def trim_history_to_budget(history, ceiling:, max_entries: nil, min_entries: 4)
302
+ return history if history.nil? || history.empty?
303
+
304
+ trimmed = history
305
+ trimmed = trimmed.last(max_entries) if max_entries&.positive? && trimmed.size > max_entries
306
+
307
+ limit = ceiling&.positive? ? ceiling : nil
308
+ trimmed = trimmed.last(trimmed.size - 1) while limit && trimmed.size > min_entries && TokenEstimator.estimate_history(trimmed) > limit
309
+
310
+ trimmed = trimmed.last(trimmed.size - 1) while trimmed.size > 1 && ConversationRows.role_of(trimmed.first) != "user"
311
+ trimmed
312
+ end
313
+
314
+ # Plan 61 D-9: role is "user" (not "system") — strict providers reject
315
+ # or relocate mid-conversation system messages; the tag text is kept
316
+ # stable for downstream recognition.
317
+ def summary_message(dropped_count)
318
+ {
319
+ role: "user",
320
+ content: "<compacted_summary count=\"#{dropped_count}\" " \
321
+ "message=\"#{dropped_count} intermediate conversation messages and tool execution steps were collapsed to stay within token limits.\" />"
322
+ }
323
+ end
324
+ end
325
+ end
326
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubik
4
+ module Prompt
5
+ # Single owner of the conversation-row wire dialect (Candidate 7,
6
+ # arch-deepening 2026-10-08). History rows arrive in two dialects:
7
+ # symbol-keyed ({ role:, content: } — everything this runtime pushes)
8
+ # and string-keyed (restored sessions, external providers). Every
9
+ # probe below is pure; append_notice! is the only mutator (bang =
10
+ # in-place, explicit per the side-effect policy).
11
+ module ConversationRows
12
+ module_function
13
+
14
+ # Row guard: nils and non-hash junk rows appear from restored
15
+ # sessions and test doubles.
16
+ def row?(row) = row.is_a?(Hash)
17
+
18
+ # Raw read in both dialects (no type coercion — callers keep their
19
+ # own nil/to_s handling).
20
+ def value_of(row, key) = row[key] || row[key.to_s]
21
+
22
+ # Role read, both dialects, stringified (the
23
+ # `(row[:role] || row["role"]).to_s` idiom of sanitizer/compactors).
24
+ def role_of(row) = value_of(row, :role).to_s
25
+
26
+ # Content read, both dialects. Returns the raw value (may be nil) —
27
+ # callers keep their own .to_s / length logic.
28
+ def content_of(row) = value_of(row, :content)
29
+
30
+ # Key resolution for WRITES: a row that already carries the symbol
31
+ # key is written back on the symbol; anything else (string-keyed, or
32
+ # missing the key entirely) is written on the STRING key. This exact
33
+ # asymmetry is load-bearing behavior pinned by the merge tests.
34
+ def resolve_key(row, key)
35
+ sym = key.to_sym
36
+ row.key?(sym) ? sym : key.to_s
37
+ end
38
+
39
+ def content_key_of(row) = resolve_key(row, :content)
40
+
41
+ # Merge-policy predicate. Deliberately NOT role_of-based: the merge
42
+ # compares exact string equality (a Symbol :user role must NOT
43
+ # match), preserving current behavior bit-for-bit.
44
+ def user_row?(row) = row[:role] == "user" || row["role"] == "user"
45
+
46
+ # The notice-merge policy, previously duplicated in
47
+ # Orchestrator::Loop#merge_notice_block_into_history! and
48
+ # Lifecycle#inject_subagent_notice_into_conversation_history!.
49
+ # Mutates history in place and returns it (both current call sites
50
+ # are statement-position and rely on the in-place mutation).
51
+ #
52
+ # Semantics (byte-identical to both originals):
53
+ # * empty history or non-user trailing row -> push { role: "user",
54
+ # content: payload } with SYMBOL keys;
55
+ # * trailing user row -> append on its existing key dialect:
56
+ # "existing\n\npayload"; a row whose content is empty or absent
57
+ # gets the payload written directly (absent key resolves to the
58
+ # STRING "content" key — see resolve_key).
59
+ def append_notice!(history, xml_payload)
60
+ raise ArgumentError, "history must be an Array, got #{history.class}" unless history.is_a?(Array)
61
+
62
+ last = history.last
63
+ if last && user_row?(last)
64
+ key = content_key_of(last)
65
+ existing = last[key].to_s
66
+ last[key] = existing.empty? ? xml_payload : "#{existing}\n\n#{xml_payload}"
67
+ else
68
+ history << { role: "user", content: xml_payload }
69
+ end
70
+ history
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../platform"
4
+ # Vocabulary leaf ONLY, never the orchestrator barrel: orchestrator sits
5
+ # above the prompt layer; this pure platform-only module renders the
6
+ # manage_subagents snippets the rules below teach. Do not widen this edge.
7
+ require_relative "../orchestrator/subagent/notices"
8
+ require_relative "tool_teachings"
9
+
10
+ module Rubik
11
+ module Prompt
12
+ # Doctrine SSOT (architecture review 2026-10-01, candidate 2): ALL
13
+ # policy prose the prompt teaches — operational rules, role headers,
14
+ # orchestration guidance, skills preamble, tool wire notes. Owns
15
+ # WORDING only: rendered data (tool names, schema dumps) arrives as
16
+ # parameters and services are never touched here (the orchestrator
17
+ # resolves, the prompt renders). module_function mirrors
18
+ # ToolTeachings/Notices. Byte-identical rendering is pinned by
19
+ # test_prompt_golden_output — reword here = fixture diff there, never
20
+ # a context_builder.rb diff.
21
+ module Doctrine
22
+ module_function
23
+
24
+ def operational_rules_xml
25
+ # Maestro doktrini (kullanıcı kararı 2026-10-01): maestro HER modda
26
+ # orkestratör. Devir caveat'i kural 2-3'ün BAŞINA yazılır — sondaki
27
+ # ek caveat zayıf modellerde atlanıyordu (opencode-personal bulgusu);
28
+ # doktrin kendi promptunun içinde çelişmemeli.
29
+ <<~XML.strip
30
+ <operational_rules>
31
+ 1. Code Exploration & Review (read-only: shape subagent assignments and answer the user): Use native tools directly (`list_files`, `grep_search`, `search_symbols`, `get_outline`, `read_file`, `find_references`). If neither a tool nor a listed command matches your need, inspect the codebase structurally: examine the directory layout (`list_files`), search for patterns (`grep_search`), read core modules (`get_outline`, `read_file`), and look for TODO/FIXME markers. For conceptual queries (e.g. "code review", "code quality"), do NOT search for the literal words "refactor" or "review"; evaluate the architecture structurally.
32
+ 2. Code Editing & Targeted Changes: MAESTRO OVERRIDE — this rule is delegated wholesale: route ALL edits through 'coder' subagents and never stage edits in your own context (the runtime refuses direct edit tools). The mechanics below are what your coder subagents use. #{Rubik::Prompt::ToolTeachings.teaching_for('replace_file_content')} #{Rubik::Prompt::ToolTeachings.teaching_for('write_file')} #{Rubik::Prompt::ToolTeachings.teaching_for('apply_buffer')}
33
+ 3. Testing & Execution: MAESTRO OVERRIDE — this rule is delegated wholesale: subagents run tests and commands; you read their reports and never execute them yourself (the runtime refuses direct run tools). The mechanics below are what your coder subagents use: they invoke `run_tests` and `run_command` as native tools, never wrapped in `exec_repl_command`. Commands in `run_command` run directly in the project working directory (`<working_directory>`); coder subagents must not prepend `cd` commands or guess non-existent paths (e.g. `/home/ubuntu`). Always fulfill the user's primary requested action (e.g. merge, build, test, commit) to completion. #{Rubik::Prompt::ToolTeachings.teaching_for('run_command')}
34
+ 4. REPL Bang Commands: `exec_repl_command` runs only the session-safe commands listed in <agent_commands> — runtime configuration, context and step-budget self-management, buffer review and rollback, runtime introspection, and code validation. If that section is absent, do not invoke REPL commands. File, code, test, and shell operations never travel through REPL commands — under the maestro doctrine (rules 2-3) they are delegated to subagents, and the direct tools (`read_file`, `write_file`, `replace_file_content`, `run_tests`, `run_command`) are your coder subagents' mechanics. Buffer-review commands (`!diff`, `!undo`, `!rollback`) operate on the in-memory staging buffer (unapplied edits) — they are a preview layer, not file ground truth: verify file content with `read_file`, and read git history from your coder subagent's `run_command` report.
35
+ 5. Autonomous Tool Synthesis: #{Rubik::Prompt::ToolTeachings.teaching_for('synthesize_tool')}
36
+ 6. Output Budget & Conciseness: Respect your output token budget (`<output_budget>`). Produce direct, high-signal responses and avoid unnecessary pleasantries, verbose conversational preambles, or duplicated context blocks that waste generation quota.
37
+ 7. Subagent Delegation & Parallelism (Maestro): As the maestro you orchestrate — ALL execution work (exploration, code changes, verification) is delegated via `invoke_subagent`, in every mode. Match task to archetype: use 'research' for read-only codebase exploration; MUST use 'coder' for code modifications, refactoring, and test execution ('research' has no write or test tools); reach for 'self' (a full capability clone, minus subagent spawning) for work that fits neither archetype — runtime introspection/repair, self-evolution work, hybrid tasks. Subagents execute asynchronously in disposable sub-contexts in the background. After dispatching subagents, do NOT block with 'wait' unless you strictly depend on their output in the current turn — conclude your turn and liberate the prompt so the user can interact concurrently. Completion, budget-pause, error, and message notifications arrive reactively: during your turn via your notification inbox; while you are idle, the runtime automatically wakes you with a dedicated notification-evaluation turn — evaluate the pending notifications, act (extend budget, steer with send_message, retrieve results, wrap up, or report to the user), and conclude your turn again. Subagents can ask YOU questions mid-flight via `ask_question` — a question notification means that subagent is BLOCKED until you reply: answer it promptly with `send_message(recipient: '<id>', message: '<your answer>')` (it times out otherwise). Call `#{Rubik::Orchestrator::Subagent::Notices.wait_hint_template}` only when synchronous waiting is strictly required. Never poll 'status' repeatedly in a loop.
38
+ 8. Runtime Introspection, Self-Healing & Human Feedback: If internal errors or service degradation occur, inspect runtime state with `inspect_runtime` and repair failed services via `repair_runtime_service`. When critical requirements are underspecified or high-impact architectural choices require user confirmation, use `ask_question` with structured multiple-choice options.
39
+ 9. Central Storage & Clean Worktree: #{Rubik::Prompt::ToolTeachings.teaching_for('storage')} #{Rubik::Prompt::ToolTeachings.teaching_for('vakanuvis')} #{Rubik::Prompt::ToolTeachings.teaching_for('show_file')} Subagent transcripts are indexed automatically as `transcript` entities (alias `subagent-<storage-id>`). #{Rubik::Prompt::ToolTeachings.teaching_for('prune_context')}
40
+ </operational_rules>
41
+ XML
42
+ end
43
+
44
+ # Returns the UNSTRIPPED mode header; the assembler strips at embed
45
+ # time (verbatim move from build_system_prompt — rendered bytes are
46
+ # pinned by the golden fixtures). goal_ledger: goal-mode öğretisi
47
+ # koşulludur (goal-ledger planı A) — OFF'ta bayt-özdeş; öğreti
48
+ # TEACHINGS'e giremez (weld testi her öğretiyi operational_rules
49
+ # içinde zorunlu kılar), goal header'ın 6. maddesinde yaşar.
50
+ def role_header(mode, goal_ledger: false)
51
+ case mode.to_sym
52
+ when :plan
53
+ <<~HDR
54
+ You are Rubik (maestro), the orchestrator in PLAN-ONLY mode: you analyze, plan, and orchestrate; execution belongs to subagents.
55
+ Analyze the user's task within <user_task> and output a concise, numbered step-by-step implementation plan.
56
+ Do NOT call apply_buffer, write_file, replace_file_content, run_tests, or run_command — the maestro runtime refuses them in every mode; analysis and planning are your work. Proposed changes may sit in the buffer — do not apply them. Specify what files will be created or edited.
57
+ HDR
58
+ when :goal
59
+ ledger_line = " If a `<goal_ledger>` row is present, keep its state current: read `rubik://goal/ledger` and " \
60
+ "apply JSON patches with `update_goal_ledger(patch: ...)` against the reported rev after each " \
61
+ "meaningful step (task status changes, new findings/decisions)."
62
+ <<~HDR
63
+ You are Rubik (maestro), an autonomous AI coding SUPERVISOR in AUTONOMOUS GOAL mode.
64
+ You own the goal in <user_task> and MUST drive it to verified completion as a pure orchestrator — you conduct, subagents perform. All research, code changes, and verification run in subagent contexts, never in yours:
65
+ 1. Decompose the goal into staged subagent work: at minimum a plan/research stage, a build stage, a review/audit stage whose findings feed a fix stage, and a final verification stage. You decide the exact stages. Declare cross-stage ordering IN THE SAME dispatch with `depends_on` (FS default: the successor launches automatically once the predecessor completes; SS: once it starts) — never fire a stage in parallel with its own predecessors.
66
+ 2. Dispatch every stage with `invoke_subagent`. You MAY explore read-only yourself (`list_files`, `grep_search`, `read_file`) to shape assignments, but ALL code changes and test runs belong to subagents — MUST use the 'coder' archetype for any file modification ('research' is read-only).
67
+ 3. Keep YOUR context lean and managerial: no code drafts, no test output, no full-file reads in your own context — consume stage reports, `rubik://storage/...` summaries, and notification digests instead. Answer subagent `ask_question` prompts promptly.
68
+ 4. Work the notifications, never a poll loop: when subagents are dispatched and nothing needs your immediate action, end your turn WITHOUT calling finish — the runtime waits for them and wakes you with their completion/pause/error/question notices. Ending your turn (or calling finish) with subagents still active does NOT complete the goal: the runtime holds the goal open and re-prompts you with their notices. React to each notice (extend a budget-paused subagent (`manage_subagents` action: 'extend_budget') instead of absorbing its task yourself), then yield again. Conclude with finish ONLY when every dispatched subagent has settled and verification is green.
69
+ 5. Close every stage through verification: read subagent reports, integrate verified results, and run final verification as a subagent stage — a 'coder' mandated to `run_tests` whose green/red report you read. Iterate the review→fix stages until the verifier reports green.
70
+ 6. Live goal snapshot (status, step budget, extensions): read it with `read_file(path: 'rubik://goal/state')`.#{ledger_line if goal_ledger}
71
+ 7. Persist pipeline knowledge in the central storage (`storage` tool): the decomposed plan, review findings, and the final goal record — link stages with entity ids (`links: {"goal": "goal.N.<hash16>"}`) so the audit trail survives the session. Per-stage MEMORY notes go through the chronicler instead: `vakanuvis(action: 'record', topic: '...', body: '...')` mints the `mem.` ref, upserts and supersedes — never write memory with raw storage puts; re-enter prior context with `vakanuvis(action: 'recall', query: '...')` (also after compaction). Keep the repository worktree free of agent workfiles. Transcripts of finished subagents are indexed automatically (searchable via `storage` action 'search') and persist after the subagent is reaped.
72
+ 8. Call `finish` only when the goal is verified, complete, and no dispatched subagent remains active — a premature finish is converted into a wait and you will be re-prompted with their notices. Summarize what each stage produced.
73
+ HDR
74
+ else
75
+ <<~HDR
76
+ You are Rubik (maestro), a maestro coding orchestrator for the human operator.
77
+ You ALWAYS work through subagents: research, code changes, and verification run in subagent contexts via `invoke_subagent` (MUST use the 'coder' archetype for any file modification — 'research' is read-only), never in your own context.
78
+ Keep your own context lean and managerial: you MAY explore read-only (`list_files`, `grep_search`, `read_file`) to understand requests and shape assignments, but no code drafts, no test runs, no full-file dumps — consume subagent reports and `rubik://storage/...` summaries instead.
79
+ Focus on driving tasks to completion: decompose the user's request into subagent tasks, work the reactive notices (completion, budget pause, error, question), steer with `send_message` / `manage_subagents` ('extend_budget', 'redeploy'), and answer subagent `ask_question` prompts promptly — they block until you reply.
80
+ Prefer reuse over re-spawning: hand follow-up tasks to a finished subagent with `manage_subagents(action: 'redeploy', conversation_id: '<id>', prompt: '<new task>')` — its accumulated context survives.
81
+ Small talk, explanations of earlier reports, and questions you can answer from read-only context or subagent findings are yours directly; every unit of execution work belongs to a subagent.
82
+ HDR
83
+ end
84
+ end
85
+
86
+ # Guidance BULLET PROSE (no wrapper tags, no indent): the renderer
87
+ # owns the <orchestration_guidance> XML structure and indentation,
88
+ # Doctrine owns the policy sentences.
89
+ def orchestration_guidance_bullets
90
+ [
91
+ "Archetype selection: Use 'research' for read-only exploration; MUST use 'coder' for code changes/bug fixes " \
92
+ "(read-only agents cannot modify files, run tests, or run shell commands — git/gh plumbing and command " \
93
+ "output belong to a coder, plan that pass from the start).",
94
+ "Asynchronous execution: Subagents run concurrently in the background. Conclude your turn to yield: in " \
95
+ "interactive sessions this liberates the prompt for the user; in the autonomous goal loop the runtime " \
96
+ "waits for active subagents and wakes you with their notices — the goal is never complete while they " \
97
+ "run. Completion, pause, error, and message notices arrive reactively in your inbox and " \
98
+ "automatically wake you with a notification-evaluation turn while you are idle. Call " \
99
+ "`#{Rubik::Orchestrator::Subagent::Notices.wait_hint_template}` only if you have an immediate blocking " \
100
+ "dependency within the current turn. Do NOT poll 'status' in a loop.",
101
+ "Workspace isolation: 'inherit' shares project workspace directly; 'branch' isolates edits in a virtual " \
102
+ "buffer merged atomically upon completion.",
103
+ "Budget escalation: A subagent paused at its step budget (WAITING_BUDGET) is NOT terminal: " \
104
+ "`#{Rubik::Orchestrator::Subagent::Notices.extend_budget_hint('[...]')}` resumes it with context intact. " \
105
+ "Extend BEFORE absorbing the task yourself; re-spawn only if its transcript shows the objective changed.",
106
+ "Error recovery: A subagent paused on a provider/network error (PAUSED_ERROR) is also NOT terminal: " \
107
+ "`#{Rubik::Orchestrator::Subagent::Notices.resume_hint('[...]')}` retries it with context intact. " \
108
+ "Kill only if its transcript shows the error is permanent.",
109
+ "Context reuse: A finished (completed/failed) subagent not yet reaped holds its accumulated context — hand " \
110
+ "it a follow-up with `manage_subagents(action: 'redeploy', conversation_id: '<id>', prompt: '<new task>')` " \
111
+ "instead of re-spawning and re-explaining. A reaped (archived) subagent cannot be redeployed: spawn a " \
112
+ "fresh one.",
113
+ "Staged dispatch (dependencies): A spec may declare `depends_on` — an array of predecessor ids (strings ⇒ " \
114
+ "FS: launches when the predecessor completes) or `{'id': ..., 'type': 'SS'}` (launches when it starts). " \
115
+ "Queued specs hold no slot and cost nothing; `list`/`status` show them as QUEUED with their unmet deps, " \
116
+ "and `wait` accepts queued ids (blocks until the whole chain settles). A failed/killed predecessor " \
117
+ "CANCELS its dependents — re-dispatch with the new information; a budget- or error-paused predecessor " \
118
+ "keeps them queued — extend or resume it instead.",
119
+ "Chronicler briefs: when dispatching a subagent, paste the relevant chronicled knowledge into the task " \
120
+ "brief — the recall digest or a rubik://storage/... link — so the subagent pulls the depth itself. " \
121
+ "Subagents never see the `vakanuvis` tool; you are the only librarian.",
122
+ "Diligent cleanup: Finished subagents are reaped automatically after a grace period " \
123
+ "(`subagents.reap_finished_after`) — in-memory context is reclaimed while the durable transcript stays " \
124
+ "readable at its transcript URI and in `rubik://storage` search. `manage_subagents(action: 'reap')` " \
125
+ "reaps finished subagents immediately."
126
+ ]
127
+ end
128
+
129
+ def skills_preamble
130
+ "The skills below bundle specialized instructions. When a task matches one, invoke the `skill` tool with its " \
131
+ "name to load its full SKILL.md instructions first; files it references (scripts/, references/, assets/) " \
132
+ "are read with read_file relative to its directory."
133
+ end
134
+
135
+ def native_tools_note(names)
136
+ <<~TLS
137
+
138
+ Tools are available via native function calling (the request's `tools` parameter): invoke them with native tool calls.
139
+ As a fallback, a JSON block also works:
140
+ ```json
141
+ { "tool": "tool_name", "args": { ... } }
142
+ ```
143
+ Available tools: #{names}
144
+ TLS
145
+ end
146
+
147
+ def json_tools_note(tool_schemas)
148
+ <<~TLS
149
+
150
+ You can call tools by outputting a JSON block:
151
+ ```json
152
+ { "tool": "tool_name", "args": { ... } }
153
+ ```
154
+ Available tools:
155
+ #{Rubik::Platform.safe_json_dump(tool_schemas)}
156
+ TLS
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "context_compactor"
4
+ require_relative "conversation_rows"
5
+ require_relative "../tools/truncator"
6
+
7
+ module Rubik
8
+ module Prompt
9
+ # Mutating context compactor for hot paths. Where ContextCompactor
10
+ # is pure (returns a new history), EmergencyCompactor mutates the
11
+ # given history in place for callers that need a fast reduction
12
+ # without an extra allocation.
13
+ #
14
+ # Use ContextCompactor first; fall back to EmergencyCompactor
15
+ # only when a quick reduction is needed and the caller accepts
16
+ # the in-place mutation.
17
+ module EmergencyCompactor
18
+ module_function
19
+
20
+ # Plan 63 Faz A: tiered escalation. When the caller supplies a positive
21
+ # context_limit, the structural ContextCompactor (tool-result pruning,
22
+ # atomic-unit sliding window with a summary marker) runs first with
23
+ # force: true; the blind 300/1000-char truncation is the last resort,
24
+ # applied only when the history is still over budget afterwards.
25
+ # Without a context_limit the behavior is identical to the legacy
26
+ # single-layer path, so direct callers keep their exact contract.
27
+ #
28
+ # The history Array keeps its identity (in-place replace), so the caller
29
+ # holding it — the Loop retry paths — sees the compacted messages.
30
+ # Returns the layer that produced the final history (:structural or
31
+ # :hard_truncate); the nil/empty guards return the input unchanged
32
+ # (pinned by tests). The return value is observational: callers forward
33
+ # it on the :emergency_compact on_step event rather than branch on it.
34
+ def compact!(history, context_limit: nil, max_output_tokens: nil, config: {}, absorb: nil)
35
+ return history if history.nil? || history.empty?
36
+
37
+ config = {} unless config.is_a?(Hash) # a null `context:` YAML section makes fetch return nil
38
+ max_out = max_output_tokens || 4096
39
+ if context_limit&.positive?
40
+ structural = Rubik::Prompt::ContextCompactor.compact_history(
41
+ history, context_limit: context_limit,
42
+ max_output_tokens: max_out, config: config, force: true, absorb: absorb
43
+ )
44
+ history.replace(structural)
45
+ still_over = Rubik::Prompt::ContextCompactor.needs_compaction?(
46
+ history, context_limit: context_limit, max_output_tokens: max_out, config: config
47
+ )
48
+ return :structural unless still_over
49
+ end
50
+
51
+ # Hard-truncate layer strategy is configurable (context.emergency_truncate_strategy):
52
+ # "head_tail" (default, original behavior) keeps head + tail around the
53
+ # omission notice; "head_only" keeps the message head — useful when the
54
+ # actionable guidance at the tail must not crowd out the error header.
55
+ # Truncator normalizes strings/symbols and falls back to head_tail on
56
+ # unknown values.
57
+ strategy = config["emergency_truncate_strategy"] || config[:emergency_truncate_strategy] || "head_tail"
58
+
59
+ history.each do |msg|
60
+ next unless ConversationRows.row?(msg)
61
+
62
+ role = ConversationRows.role_of(msg)
63
+ content_key = ConversationRows.content_key_of(msg)
64
+ content = msg[content_key].to_s
65
+
66
+ if ["tool", "tool_result"].include?(role)
67
+ if content.length > 300
68
+ trunc = Rubik::Tools::Truncator.truncate(content, max_chars: 300, max_lines: 10, strategy: strategy)
69
+ msg[content_key] = trunc[:text]
70
+ end
71
+ elsif role == "assistant" && content.length > 1000
72
+ trunc = Rubik::Tools::Truncator.truncate(content, max_chars: 1000, max_lines: 30, strategy: strategy)
73
+ msg[content_key] = trunc[:text]
74
+ end
75
+ end
76
+
77
+ :hard_truncate
78
+ end
79
+ end
80
+ end
81
+ end