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,354 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base"
4
+ require_relative "../../coeffects"
5
+ require_relative "../uri_resolver"
6
+ require_relative "../output"
7
+ require_relative "../diagnostic_reporter"
8
+ require_relative "../fuzzy_matcher"
9
+ require_relative "../../buffer/path_validator"
10
+
11
+ module Rubik
12
+ module Tools
13
+ class ReadFileTool < BaseTool
14
+ DECLARATION = Declaration.new(
15
+ name: "read_file", aliases: ["view_file", "read"],
16
+ description: "Read content of a file from buffer or disk with 1-based line numbers (optional 1-based offset and line limit for paging)",
17
+ risk: :read, archetypes: [:research, :coder],
18
+ params_schema: {
19
+ type: "object",
20
+ properties: {
21
+ path: { type: "string", description: "File path, or virtual rubik:// URI: 'rubik://storage/<project>/<type>/<id>' " \
22
+ "(browse from 'rubik://storage/index'), 'rubik://goal/state', 'rubik://subagent/<id>/transcript'" },
23
+ offset: { type: "integer", description: "1-based line number to start reading from (optional)" },
24
+ limit: { type: "integer", description: "Maximum number of lines to read (optional)" },
25
+ from_disk: { type: "boolean", description: "Read current disk content without modifying in-memory buffer" },
26
+ reload: { type: "boolean", description: "Discard dirty buffer and reload from disk (resets pending changes)" }
27
+ },
28
+ required: ["path"]
29
+ }
30
+ )
31
+ def initialize(buffer_manager, runtime_context: nil)
32
+ super(DECLARATION)
33
+ @buffer_manager = buffer_manager
34
+ @runtime_context = runtime_context
35
+ end
36
+
37
+ def execute(args = {})
38
+ path = (args["path"] || args[:path]).to_s.strip
39
+ # rubik:// sevk tek yerde: UriResolver (bilinmeyen şema/gramer reddi
40
+ # nil döndürür ve buffer düzlemine düşer — bugünkü düşüş sözleşmesi).
41
+ virt = UriResolver.resolve(path, ctx: @runtime_context, buffer_manager: @buffer_manager)
42
+ return virt unless virt.nil?
43
+
44
+ if path.start_with?("file://")
45
+ clean = path.sub(%r{\Afile://}, "")
46
+ path = clean.sub(%r{\A/([A-Za-z]:)}, '\1')
47
+ end
48
+
49
+ content = if args["reload"] == true || args[:reload] == true
50
+ @buffer_manager.read(path, reload: true)
51
+ elsif args["from_disk"] == true || args[:from_disk] == true
52
+ @buffer_manager.read(path, from_disk: true)
53
+ else
54
+ @buffer_manager.read(path)
55
+ end
56
+
57
+ unless content
58
+ known_files = @buffer_manager.respond_to?(:all_files) ? @buffer_manager.all_files : []
59
+ candidates = FuzzyMatcher.best_matches(File.basename(path), known_files.map { |f| File.basename(f) })
60
+ return ToolOutput.error(DiagnosticReporter.render_file_not_found(path, candidates: candidates))
61
+ end
62
+
63
+ offset = (args["offset"] || args[:offset]).to_i
64
+ limit = (args["limit"] || args[:limit]).to_i
65
+ lines = content.lines
66
+ return "[empty file (0 lines)]" if lines.empty?
67
+
68
+ start_idx = [offset, 1].max - 1
69
+ slice = limit.positive? ? (lines[start_idx, limit] || []) : (lines[start_idx..] || [])
70
+ return ToolOutput.error("No content at offset #{offset} (file has #{lines.length} lines).") if slice.empty?
71
+
72
+ numbered = slice.each_with_index.map { |line, idx| "#{start_idx + idx + 1}: #{line}" }.join
73
+ offset > 1 || limit.positive? ? "[lines #{start_idx + 1}-#{start_idx + slice.length} of #{lines.length} total]\n#{numbered}" : numbered
74
+ rescue Errno::EACCES => e
75
+ ToolOutput.error(DiagnosticReporter.render_permission_denied(path, operation: "read", reason: e.message))
76
+ rescue Rubik::Buffer::InvalidPathError
77
+ ToolOutput.error(DiagnosticReporter.render_traversal_blocked(path))
78
+ end
79
+ end
80
+
81
+ class WriteFileTool < BaseTool
82
+ DECLARATION = Declaration.new(
83
+ name: "write_file", aliases: ["create_file", "save_file", "overwrite_file"],
84
+ description: "Write full content to the in-memory buffer (does not write to disk until apply_buffer is called). " \
85
+ "For surgical edits to existing files, use replace_file_content instead. " \
86
+ "Use apply_buffer to persist the buffer to disk before running tests or tests will not see your changes.",
87
+ risk: :write, archetypes: [:coder], write_grant: true, maestro_blocked: true,
88
+ params_schema: {
89
+ type: "object",
90
+ properties: {
91
+ path: { type: "string", description: "File path relative to the project root (e.g. 'lib/app.rb', 'Gemfile')" },
92
+ content: { type: "string", description: "Full file content to write (overwrites entire file)" }
93
+ },
94
+ required: ["path", "content"]
95
+ }
96
+ )
97
+ def initialize(buffer_manager)
98
+ super(DECLARATION)
99
+ @buffer_manager = buffer_manager
100
+ end
101
+
102
+ def execute(args = {})
103
+ path = (args["path"] || args[:path]).to_s.strip
104
+ content = args["content"] || args[:content]
105
+
106
+ return ToolOutput.error("Error: `path` parameter is required.") if path.empty?
107
+ unless Rubik::Buffer::PathValidator.valid?(path)
108
+ return ToolOutput.error("Error: Invalid file path '#{path}'. Path must be a valid file path " \
109
+ "relative to the project root (e.g. 'lib/app.rb', 'src/main.go', 'Gemfile').")
110
+ end
111
+
112
+ begin
113
+ # NB: reset_baseline defaults to false on purpose. The buffer only
114
+ # replaces the tracked baseline for files that have no baseline yet
115
+ # (new files). Passing reset_baseline: true here would overwrite the
116
+ # original baseline of an ALREADY-loaded file, defeating undo and
117
+ # conflict restoration — the buffer could never restore the file's
118
+ # true original state after a full overwrite.
119
+ @buffer_manager.write(path, content)
120
+ rescue ArgumentError
121
+ @buffer_manager.write(path, content)
122
+ end
123
+ "Successfully wrote to in-memory buffer: #{path}"
124
+ rescue Errno::EACCES => e
125
+ ToolOutput.error(DiagnosticReporter.render_permission_denied(path, operation: "write", reason: e.message))
126
+ rescue Rubik::Buffer::InvalidPathError
127
+ ToolOutput.error(DiagnosticReporter.render_traversal_blocked(path))
128
+ end
129
+ end
130
+
131
+ class ListFilesTool < BaseTool
132
+ # Declared coeffects: hidden from schemas (PENDING) until satisfied (AGENTS.md §2).
133
+ REQUIRES = [Rubik::Coeffects::CONTEXT_INDEX].freeze
134
+
135
+ DECLARATION = Declaration.new(
136
+ name: "list_files", aliases: ["list_directory", "list_dir", "ls"],
137
+ description: "List project files matching an optional path prefix or extension filter. " \
138
+ "Returns at most 50 files; narrow the query with 'path' and 'extension' for larger trees.",
139
+ risk: :read, archetypes: [:research, :coder],
140
+ params_schema: {
141
+ type: "object",
142
+ properties: {
143
+ path: { type: "string",
144
+ description: "Subdirectory or path prefix relative to the project root (e.g. 'lib', 'src'). " \
145
+ "Absolute paths are converted automatically. (optional)" },
146
+ extension: { type: "string", description: "File extension filter, e.g. .rb, .go, .zig (optional)" }
147
+ }
148
+ }
149
+ )
150
+ def initialize(context_index = nil, root_dir: Dir.pwd, runtime_context: nil, context: nil)
151
+ super(DECLARATION)
152
+ @context_index = context_index
153
+ @runtime_context = runtime_context || context
154
+ @root_dir = File.expand_path(root_dir)
155
+ end
156
+
157
+ def context_index
158
+ Rubik::Coeffects.resolve(@runtime_context, Rubik::Coeffects::CONTEXT_INDEX, @context_index)
159
+ end
160
+
161
+ def execute(args = {})
162
+ path_filter = args["path"] || args[:path]
163
+ ext_filter = args["extension"] || args[:extension]
164
+ idx = context_index
165
+ return ToolOutput.error("Context index is not available. Run '!context scan' or restart the session to build it.") unless idx
166
+
167
+ filter, error = BaseTool.normalize_path_filter(path_filter, @root_dir)
168
+ if error
169
+ allowed = [{ path: @root_dir, primary: true }]
170
+ idx.additional_roots.each { |r| allowed << { path: r, primary: false } } if idx.respond_to?(:additional_roots)
171
+ return ToolOutput.error(DiagnosticReporter.render_traversal_blocked(path_filter, allowed_roots: allowed))
172
+ end
173
+
174
+ files = idx.files.empty? ? idx.full_scan : idx.files
175
+ # Path-filtered list drives the B9 explanation: an extension miss in
176
+ # an INDEXED directory must read "no such files here", not "ignored".
177
+ path_filtered = filter.empty? ? files.keys : files.keys.select { |f| f.start_with?(filter) }
178
+ file_list = ext_filter && !ext_filter.empty? ? path_filtered.select { |f| f.end_with?(ext_filter) } : path_filtered
179
+
180
+ return ignored_directory_message(filter, ext_filter) if path_filtered.empty? && existing_but_unindexed?(@root_dir, filter)
181
+
182
+ return BaseTool.no_match_message(filter) if file_list.empty?
183
+
184
+ "Found #{file_list.size} file(s):\n#{file_list.first(50).map { |f| " - #{f}" }.join("\n")}"
185
+ rescue Errno::EACCES => e
186
+ ToolOutput.error(DiagnosticReporter.render_permission_denied(path_filter || @root_dir, operation: "read", reason: e.message))
187
+ end
188
+
189
+ private
190
+
191
+ # Session review B9: `list_files(path: 'lib')` answered "No matching
192
+ # files" for a directory that exists — the context index prunes ignore
193
+ # rules (.gitignore, DEFAULT_IGNORES) silently. Say so and hand the
194
+ # escape hatch instead of letting the model doubt the directory.
195
+ def existing_but_unindexed?(root_dir, filter)
196
+ return false if filter.to_s.empty?
197
+
198
+ File.directory?(File.join(root_dir, filter.to_s))
199
+ end
200
+
201
+ def ignored_directory_message(filter, ext_filter)
202
+ ext_note = ext_filter && !ext_filter.to_s.empty? ? " matching '#{ext_filter}'" : ""
203
+ "No indexed files#{ext_note} under '#{filter}', but the directory EXISTS on disk — its contents are " \
204
+ "excluded by ignore rules (.gitignore / default excludes) and never entered the context index. " \
205
+ "Browse it with run_command (e.g. `ls -la #{filter}`) or read files directly by absolute path."
206
+ end
207
+ end
208
+
209
+ # Surgical, targeted chunk replacement tool for frontier coding models (Plan 56).
210
+ class ReplaceFileContentTool < BaseTool
211
+ DECLARATION = Declaration.new(
212
+ name: "replace_file_content", aliases: ["edit_file", "edit_chunk", "str_replace", "patch_file"],
213
+ description: "Replace an exact chunk of code within an existing file. Requires exact verbatim matching of target_content " \
214
+ "(including whitespace and indentation). Optional start_line and end_line narrow the search range.",
215
+ risk: :write, archetypes: [:coder], write_grant: true, maestro_blocked: true,
216
+ params_schema: {
217
+ type: "object",
218
+ properties: {
219
+ path: { type: "string", description: "Path to the file to edit" },
220
+ target_content: { type: "string", description: "The exact contiguous block of code/text to replace (must match verbatim)" },
221
+ replacement_content: { type: "string", description: "The new replacement text to insert in place of target_content" },
222
+ start_line: { type: "integer", description: "1-based starting line number to restrict search scope (optional)" },
223
+ end_line: { type: "integer", description: "1-based ending line number to restrict search scope (optional)" },
224
+ allow_multiple: { type: "boolean",
225
+ description: "If true, replaces all occurrences. " \
226
+ "If false, fails safely when multiple matches exist (optional, default: false)" }
227
+ },
228
+ required: ["path", "target_content", "replacement_content"]
229
+ }
230
+ )
231
+ def initialize(buffer_manager)
232
+ super(DECLARATION)
233
+ @buffer_manager = buffer_manager
234
+ end
235
+
236
+ def execute(args = {})
237
+ path = (args["path"] || args[:path]).to_s.strip
238
+ target = args["target_content"] || args[:target_content] ||
239
+ args["old_content"] || args[:old_content] ||
240
+ args["old_str"] || args[:old_str] ||
241
+ args["target"] || args[:target]
242
+ replacement = args["replacement_content"] || args[:replacement_content] ||
243
+ args["new_content"] || args[:new_content] ||
244
+ args["new_str"] || args[:new_str] ||
245
+ args["replacement"] || args[:replacement]
246
+ start_line = args["start_line"] || args[:start_line]
247
+ end_line = args["end_line"] || args[:end_line]
248
+ allow_multiple = args["allow_multiple"] == true || args[:allow_multiple] == true
249
+
250
+ return ToolOutput.error("Error: `path` parameter is required.") if path.empty?
251
+ return ToolOutput.error("Error: `target_content` parameter is required.") if target.nil?
252
+ return ToolOutput.error("Error: `target_content` cannot be empty.") if target.to_s.empty?
253
+ return ToolOutput.error("Error: `replacement_content` parameter is required.") if replacement.nil?
254
+
255
+ content = @buffer_manager.read(path)
256
+ unless content
257
+ known_files = (@buffer_manager.respond_to?(:all_files) ? @buffer_manager.all_files : [])
258
+ candidates = FuzzyMatcher.best_matches(File.basename(path), known_files.map { |f| File.basename(f) })
259
+ return ToolOutput.error(DiagnosticReporter.render_file_not_found(path, candidates: candidates))
260
+ end
261
+
262
+ has_crlf = content.to_s.include?("\r\n")
263
+ norm_file = content.to_s.gsub("\r\n", "\n")
264
+ norm_target = target.to_s.gsub("\r\n", "\n")
265
+ norm_replacement = replacement.to_s.gsub("\r\n", "\n")
266
+
267
+ if start_line || end_line
268
+ perform_scoped_replace(path, norm_file, norm_target, norm_replacement, start_line, end_line, allow_multiple, has_crlf)
269
+ else
270
+ perform_full_replace(path, norm_file, norm_target, norm_replacement, allow_multiple, has_crlf)
271
+ end
272
+ rescue Errno::EACCES => e
273
+ ToolOutput.error(DiagnosticReporter.render_permission_denied(path, operation: "write", reason: e.message))
274
+ rescue Rubik::Buffer::InvalidPathError
275
+ ToolOutput.error(DiagnosticReporter.render_traversal_blocked(path))
276
+ end
277
+
278
+ private
279
+
280
+ def perform_scoped_replace(path, norm_file, norm_target, norm_replacement, start_line, end_line, allow_multiple, has_crlf)
281
+ s_line = [start_line.to_i, 1].max
282
+ e_line = end_line ? end_line.to_i : norm_file.lines.size
283
+ s_line, e_line = e_line, s_line if s_line > e_line
284
+
285
+ lines = norm_file.lines
286
+ slice_start_idx = s_line - 1
287
+ slice_len = [e_line - s_line + 1, 0].max
288
+ slice_text = lines[slice_start_idx, slice_len]&.join || ""
289
+ match_count = slice_text.scan(norm_target).size
290
+
291
+ if match_count.zero?
292
+ closest = FuzzyMatcher.find_closest_subsequence(norm_target, slice_text)
293
+ adjusted_range = closest ? ((s_line + closest[:range].begin - 1)..(s_line + closest[:range].end - 1)) : nil
294
+ return ToolOutput.error(DiagnosticReporter.render_target_not_found(
295
+ path, norm_target, closest_snippet: closest&.fetch(:snippet, nil),
296
+ closest_range: adjusted_range, similarity: closest&.fetch(:similarity, nil),
297
+ start_line: s_line, end_line: e_line
298
+ ))
299
+ end
300
+
301
+ if match_count > 1 && !allow_multiple
302
+ occurrences = find_line_occurrences(slice_text, norm_target, line_offset: s_line)
303
+ return ToolOutput.error(DiagnosticReporter.render_target_not_unique(path, norm_target, match_count, occurrences))
304
+ end
305
+
306
+ new_slice = allow_multiple ? slice_text.gsub(norm_target) { norm_replacement } : slice_text.sub(norm_target) { norm_replacement }
307
+ pre_text = lines[0...slice_start_idx]&.join || ""
308
+ post_text = lines[(slice_start_idx + slice_len)..]&.join || ""
309
+ final_content = "#{pre_text}#{new_slice}#{post_text}"
310
+ final_content = final_content.gsub("\n", "\r\n") if has_crlf && !final_content.include?("\r\n")
311
+
312
+ @buffer_manager.write(path, final_content)
313
+ "Successfully replaced content in in-memory buffer: #{path} (scoped to lines #{s_line}..#{e_line})"
314
+ end
315
+
316
+ def perform_full_replace(path, norm_file, norm_target, norm_replacement, allow_multiple, has_crlf)
317
+ match_count = norm_file.scan(norm_target).size
318
+
319
+ if match_count.zero?
320
+ closest = FuzzyMatcher.find_closest_subsequence(norm_target, norm_file)
321
+ return ToolOutput.error(DiagnosticReporter.render_target_not_found(
322
+ path, norm_target, closest_snippet: closest&.fetch(:snippet, nil),
323
+ closest_range: closest&.fetch(:range, nil), similarity: closest&.fetch(:similarity, nil)
324
+ ))
325
+ end
326
+
327
+ if match_count > 1 && !allow_multiple
328
+ occurrences = find_line_occurrences(norm_file, norm_target, line_offset: 1)
329
+ return ToolOutput.error(DiagnosticReporter.render_target_not_unique(path, norm_target, match_count, occurrences))
330
+ end
331
+
332
+ final_content = allow_multiple ? norm_file.gsub(norm_target) { norm_replacement } : norm_file.sub(norm_target) { norm_replacement }
333
+ first_occ_idx = norm_file.index(norm_target)
334
+ first_occ_line = first_occ_idx ? (norm_file[0...first_occ_idx].count("\n") + 1) : 1
335
+ rep_lines_size = norm_replacement.lines.size
336
+ end_mod_line = [first_occ_line + rep_lines_size - 1, first_occ_line].max
337
+
338
+ final_content = final_content.gsub("\n", "\r\n") if has_crlf && !final_content.include?("\r\n")
339
+ @buffer_manager.write(path, final_content)
340
+ "Successfully replaced content in in-memory buffer: #{path} (modified lines #{first_occ_line}..#{end_mod_line})"
341
+ end
342
+
343
+ def find_line_occurrences(text, target, line_offset: 1)
344
+ occurrences = []
345
+ current_pos = 0
346
+ while (idx = text.index(target, current_pos))
347
+ occurrences << (text[0...idx].count("\n") + line_offset)
348
+ current_pos = idx + target.length
349
+ end
350
+ occurrences.uniq
351
+ end
352
+ end
353
+ end
354
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base"
4
+ require_relative "../output"
5
+ require_relative "../../coeffects"
6
+
7
+ module Rubik
8
+ module Tools
9
+ # Talep düzlemi (goal-ledger planı A): !goal döngüsünün yapılandırılmış
10
+ # ΔΣ defterinin masası. Pruner/vakanüvis emsali REQUIRES-görünürlük:
11
+ # araç HER ZAMAN kayıtlıdır, model yüzünde yalnız GOAL_LEDGER coeffect'i
12
+ # (çalışan !goal + knob ON) tatmininde görünür; Registry#get yolu ise
13
+ # her zaman açık olduğundan kapalıyken dürüst hata verir. Ret
14
+ # geri bildirimi C Validator'ın message+hint çiftinin birebir
15
+ # yankısıdır — bounded-retry protokolünün model-bakan ucu.
16
+ class UpdateGoalLedgerTool < BaseTool
17
+ REQUIRES = [Rubik::Coeffects::GOAL_LEDGER].freeze
18
+
19
+ DECLARATION = Declaration.new(
20
+ name: "update_goal_ledger", aliases: ["ledger"],
21
+ description: "Update the structured goal ledger of the RUNNING !goal loop with a " \
22
+ "JSON patch ({\"rev\": N, \"ops\": [{\"op\": ..., \"path\": ..., \"value\": ...}]}). " \
23
+ "Read the current ledger via read_file(path: 'rubik://goal/ledger') and issue the " \
24
+ "patch against the rev it reports. Rejections carry per-op fix hints; after the " \
25
+ "retry budget the ledger stays unchanged — read it and continue. A task status of " \
26
+ "`stale` means the harness re-checked that entry's verify predicate against the " \
27
+ "filesystem and it FAILED (the world drifted): re-inspect reality, then update the " \
28
+ "entry (including status) with a normal patch — a stale rev also means the ledger " \
29
+ "moved under you.",
30
+ risk: :write, archetypes: [], write_grant: false,
31
+ maestro_blocked: false, # vakanüvis/pruner emsali: kök düzlem aracı
32
+ params_schema: {
33
+ type: "object",
34
+ properties: {
35
+ patch: {
36
+ type: "string",
37
+ description: "JSON patch text ({\"rev\": N, \"ops\": [...]}) — rev must match the ledger's current rev."
38
+ }
39
+ },
40
+ required: ["patch"]
41
+ }
42
+ )
43
+
44
+ def initialize(runtime_context: nil)
45
+ super(DECLARATION)
46
+ @runtime_context = runtime_context
47
+ end
48
+
49
+ def execute(args = {})
50
+ patch_text = args["patch"] || args[:patch]
51
+ unless patch_text.is_a?(String) && !patch_text.strip.empty?
52
+ return ToolOutput.error("update_goal_ledger: the 'patch' parameter is required — a JSON string " \
53
+ "{\"rev\": N, \"ops\": [...]}.")
54
+ end
55
+
56
+ ledger = Rubik::Coeffects.resolve(@runtime_context, Rubik::Coeffects::GOAL_LEDGER, nil)
57
+ unless ledger
58
+ return ToolOutput.error("Goal ledger is not available: no running !goal loop (or goal.ledger.enabled=false). " \
59
+ "The ledger exists only while a goal loop runs; update it after each meaningful step.")
60
+ end
61
+
62
+ render_result(ledger.update(patch_text))
63
+ rescue StandardError => e
64
+ ToolOutput.error("update_goal_ledger failed: #{e.class}: #{e.message} — the ledger is unchanged; " \
65
+ "read rubik://goal/ledger and retry.")
66
+ end
67
+
68
+ private
69
+
70
+ def render_result(result)
71
+ case result.status
72
+ when :applied
73
+ row = %(<goal_ledger_applied rev="#{result.rev}" uri="#{result.uri}" />)
74
+ ToolOutput.ok(result.uri ? row : %(<goal_ledger_applied rev="#{result.rev}" storage="unavailable (memory-only)" />))
75
+ when :exhausted
76
+ ToolOutput.error("patch budget exhausted (consecutive rejections); the ledger is unchanged at rev " \
77
+ "#{result.rev}. Goal work may continue — read rubik://goal/ledger when you next " \
78
+ "update state, and issue a valid patch against that rev.")
79
+ else
80
+ lines = result.reasons.map do |reason|
81
+ at = reason[:op_index].nil? ? "" : " (op #{reason[:op_index]})"
82
+ "[#{reason[:class]}]#{at} #{reason[:path]}: #{reason[:message]} — #{reason[:hint]}"
83
+ end
84
+ ToolOutput.error("ledger patch rejected (rev still #{result.rev}):\n#{lines.join("\n")}")
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end