rubik 0.2.0 → 0.3.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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/ATTRIBUTION.md +4 -0
  3. data/CHANGELOG.md +81 -0
  4. data/README.md +137 -268
  5. data/bin/rubik +9 -0
  6. data/lib/rubik/cli/workspace_trust.rb +78 -0
  7. data/lib/rubik/kernel/init.rb +15 -0
  8. data/lib/rubik/orchestrator/auto_apply.rb +1 -1
  9. data/lib/rubik/orchestrator/subagent/activation.rb +29 -4
  10. data/lib/rubik/orchestrator/subagent/builtin_types.rb +117 -0
  11. data/lib/rubik/orchestrator/subagent/dynamic_store.rb +119 -0
  12. data/lib/rubik/orchestrator/subagent/instance.rb +8 -36
  13. data/lib/rubik/orchestrator/subagent/journal.rb +77 -4
  14. data/lib/rubik/orchestrator/subagent/profile_contract.rb +187 -0
  15. data/lib/rubik/orchestrator/subagent/profile_snapshot.rb +72 -0
  16. data/lib/rubik/orchestrator/subagent/reuse.rb +2 -1
  17. data/lib/rubik/orchestrator/subagent/scoped_registry.rb +2 -0
  18. data/lib/rubik/orchestrator/subagent/spawn_transaction.rb +170 -0
  19. data/lib/rubik/orchestrator/subagent/start_gate.rb +98 -0
  20. data/lib/rubik/orchestrator/subagent/supervisor.rb +4 -67
  21. data/lib/rubik/orchestrator/subagent/type_definition.rb +84 -0
  22. data/lib/rubik/orchestrator/subagent/type_registry.rb +139 -159
  23. data/lib/rubik/orchestrator/turn_executor/subagent_channel.rb +4 -2
  24. data/lib/rubik/platform/process.rb +78 -1
  25. data/lib/rubik/prompt/context_builder.rb +54 -20
  26. data/lib/rubik/prompt/doctrine.rb +158 -22
  27. data/lib/rubik/prompt/token_estimator.rb +5 -3
  28. data/lib/rubik/prompt/tool_teachings.rb +19 -12
  29. data/lib/rubik/sandbox/driver/bubblewrap.rb +13 -59
  30. data/lib/rubik/sandbox/driver/docker.rb +14 -60
  31. data/lib/rubik/sandbox/driver/podman.rb +13 -56
  32. data/lib/rubik/sandbox/driver/process.rb +12 -64
  33. data/lib/rubik/security/guard.rb +1 -1
  34. data/lib/rubik/storage/store.rb +52 -11
  35. data/lib/rubik/tools/builtins/define_subagent.rb +69 -0
  36. data/lib/rubik/tools/builtins/storage.rb +8 -5
  37. data/lib/rubik/tools/builtins/subagent.rb +20 -53
  38. data/lib/rubik/tools/builtins/synthesis.rb +6 -1
  39. data/lib/rubik/tools/builtins.rb +1 -0
  40. data/lib/rubik/tools/diagnostic_reporter.rb +2 -1
  41. data/lib/rubik/version.rb +1 -1
  42. metadata +10 -1
@@ -11,6 +11,11 @@ require_relative "services"
11
11
  module Rubik
12
12
  module Prompt
13
13
  class ContextBuilder
14
+ # Root prompts show every built-in plus only the newest eight active
15
+ # session-defined profiles. Registry ownership and direct lookup are
16
+ # unaffected by this display-only bound.
17
+ MAX_CUSTOM_SUBAGENTS_IN_PROMPT = 8
18
+
14
19
  def self.find_protocol_files(root_dir = Dir.pwd)
15
20
  Rubik::Context::RepoContextReader.new(root_dir: root_dir).find_protocol_files
16
21
  end
@@ -184,16 +189,10 @@ module Rubik
184
189
  XML
185
190
  end
186
191
 
187
- def self.build_subagents_xml(type_registry = nil)
192
+ def self.build_subagents_xml(type_registry = nil, active_tools: nil)
188
193
  return "" if type_registry.nil?
189
194
 
190
- types = if type_registry.respond_to?(:list)
191
- type_registry.list
192
- elsif type_registry.is_a?(Array)
193
- type_registry
194
- else
195
- []
196
- end
195
+ types = prompt_visible_subagent_types(type_registry)
197
196
  return "" if types.empty?
198
197
 
199
198
  esc = Rubik::Platform.method(:xml_escape)
@@ -211,7 +210,7 @@ module Rubik
211
210
  " <subagent name=\"#{name}\" tools=\"#{tools_mode}\" default_workspace=\"#{workspace}\">#{desc}</subagent>"
212
211
  end.join("\n")
213
212
 
214
- guidance = Doctrine.orchestration_guidance_bullets.map { |bullet| " • #{bullet}" }.join("\n")
213
+ guidance = Doctrine.orchestration_guidance_bullets(active_tools: active_tools).map { |bullet| " • #{bullet}" }.join("\n")
215
214
 
216
215
  <<~XML.strip
217
216
  <subagents count="#{types.size}">
@@ -224,6 +223,26 @@ module Rubik
224
223
  XML
225
224
  end
226
225
 
226
+ def self.prompt_visible_subagent_types(type_registry)
227
+ available = if type_registry.respond_to?(:builtins) && type_registry.respond_to?(:dynamics)
228
+ type_registry.builtins + type_registry.dynamics
229
+ elsif type_registry.respond_to?(:list)
230
+ type_registry.list
231
+ elsif type_registry.is_a?(Array)
232
+ type_registry
233
+ else
234
+ []
235
+ end
236
+ builtins, custom = available.partition { |type| !type.respond_to?(:custom_prompt) || !type.custom_prompt }
237
+ newest_custom = custom.sort_by do |type|
238
+ revision = type.respond_to?(:revision) ? type.revision : nil
239
+ [revision.respond_to?(:to_i) ? revision.to_i : -1, type.name.to_s]
240
+ end.last(MAX_CUSTOM_SUBAGENTS_IN_PROMPT)
241
+
242
+ builtins + newest_custom
243
+ end
244
+ private_class_method :prompt_visible_subagent_types
245
+
227
246
  def self.build_lsp_xml(lsp_manager = nil)
228
247
  return "" unless lsp_manager&.enabled?
229
248
 
@@ -272,11 +291,12 @@ module Rubik
272
291
  # at the composition boundary (Shell integration happens once, in
273
292
  # Services.from_shell — see orchestrator/loop.rb). Defaults live in
274
293
  # Prompt::Services; unsatisfied slots degrade to omitted sections.
275
- def self.build_system_context(services:)
294
+ def self.build_system_context(services:, active_tools: nil, mode: :default)
276
295
  svc = services
296
+ plan_mode = mode&.to_sym == :plan
277
297
  sections = []
278
298
 
279
- rules_xml = Doctrine.operational_rules_xml
299
+ rules_xml = Doctrine.operational_rules_xml(active_tools: active_tools, mode: mode)
280
300
  sections << rules_xml unless rules_xml.empty?
281
301
 
282
302
  # Machine-readable shell facts (Plan 55 D-6), generated from the same
@@ -299,14 +319,20 @@ module Rubik
299
319
  sections << "<context_budget limit=\"#{svc.context_limit.to_i}\" unit=\"tokens\" />" if svc.context_limit&.to_i&.positive?
300
320
  sections << "<output_budget limit=\"#{svc.max_output_tokens.to_i}\" unit=\"tokens\" />" if svc.max_output_tokens&.to_i&.positive?
301
321
 
302
- commands_xml = build_commands_xml(svc.commands)
303
- sections << commands_xml unless commands_xml.empty?
322
+ unless plan_mode
323
+ commands_xml = build_commands_xml(svc.commands)
324
+ sections << commands_xml unless commands_xml.empty?
304
325
 
305
- skills_xml = build_skills_xml(svc.skills_index)
306
- sections << skills_xml unless skills_xml.empty?
326
+ if Doctrine.tool_active?("skill", active_tools)
327
+ skills_xml = build_skills_xml(svc.skills_index)
328
+ sections << skills_xml unless skills_xml.empty?
329
+ end
307
330
 
308
- subagents_xml = build_subagents_xml(svc.type_registry)
309
- sections << subagents_xml unless subagents_xml.empty?
331
+ if Doctrine.tool_active?("invoke_subagent", active_tools)
332
+ subagents_xml = build_subagents_xml(svc.type_registry, active_tools: active_tools)
333
+ sections << subagents_xml unless subagents_xml.empty?
334
+ end
335
+ end
310
336
 
311
337
  sections << "<extra_context>\n#{Rubik::Platform.xml_escape(svc.extra_context.to_s.strip)}\n</extra_context>" unless svc.extra_context.to_s.strip.empty?
312
338
 
@@ -322,14 +348,15 @@ module Rubik
322
348
  # carries only a compact name listing (the full JSON dump would double
323
349
  # the token cost every step); the JSON-block convention is documented
324
350
  # as a fallback only.
325
- def self.build_system_prompt(services:, mode: :default, tool_schemas: [], native_tools: false)
351
+ def self.build_system_prompt(services:, mode: :default, tool_schemas: nil, native_tools: false)
326
352
  # Plan mode is text-only: never publish the toolbelt, regardless of
327
353
  # what callers pass. Execution-side gating lives in Orchestrator::Loop.
328
354
  tool_schemas = [] if mode.to_sym == :plan
355
+ active_tools = extract_active_tool_names(tool_schemas, mode: mode)
329
356
 
330
- xml_context = build_system_context(services: services)
357
+ xml_context = build_system_context(services: services, active_tools: active_tools, mode: mode)
331
358
 
332
- role_header = Doctrine.role_header(mode, goal_ledger: ledger_enabled?(services))
359
+ role_header = Doctrine.role_header(mode, goal_ledger: ledger_enabled?(services), active_tools: active_tools)
333
360
 
334
361
  tool_section = if tool_schemas.nil? || tool_schemas.empty?
335
362
  ""
@@ -348,6 +375,13 @@ module Rubik
348
375
  PROMPT
349
376
  end
350
377
 
378
+ def self.extract_active_tool_names(tool_schemas, mode: :default)
379
+ return [] if mode&.to_sym == :plan
380
+ return nil if tool_schemas.nil? || tool_schemas.empty?
381
+
382
+ tool_schemas.filter_map { |t| (t[:name] || t["name"])&.to_s }
383
+ end
384
+
351
385
  def self.format_user_task(prompt)
352
386
  <<~XML.strip
353
387
  <user_task>
@@ -21,22 +21,23 @@ module Rubik
21
21
  module Doctrine
22
22
  module_function
23
23
 
24
- def operational_rules_xml
24
+ def operational_rules_xml(active_tools: nil, mode: :default)
25
25
  # Maestro doktrini (kullanıcı kararı 2026-10-01): maestro HER modda
26
26
  # orkestratör. Devir caveat'i kural 2-3'ün BAŞINA yazılır — sondaki
27
27
  # ek caveat zayıf modellerde atlanıyordu (opencode-personal bulgusu);
28
28
  # doktrin kendi promptunun içinde çelişmemeli.
29
+ plan_mode = mode&.to_sym == :plan
29
30
  <<~XML.strip
30
31
  <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
+ #{rule_one_exploration(plan_mode: plan_mode)}
32
33
  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
34
  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
35
  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
+ 5. Autonomous Tool Synthesis: #{rule_five_synthesis(plan_mode: plan_mode)}
36
37
  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. For non-trivial modifications, allocate sufficient step budget (e.g. `max_steps: 100-140`) to prevent avoidable mid-flight pauses. When chaining stages with `depends_on`, hand the successor the predecessor's canonical transcript reference (`read_file('rubik://subagent/<storage-id>/transcript')`, reported by status/completion notices) — never a handwritten storage path; if the successor launches before the storage id is known, paste the predecessor's findings into its brief. Consume subagents' structured reports (Outcome, Changes/Locations, Verification/Findings, Assumptions) directly. 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>`) and are readable live or persisted via the canonical `rubik://subagent/<storage-id>/transcript` reference — indexed entities and live history have different availability, so address the exact execution by its unique storage id (from status/completion notices), never by a display id or a disk path. #{Rubik::Prompt::ToolTeachings.teaching_for('prune_context')}
38
+ #{rule_seven_delegation(plan_mode: plan_mode)}
39
+ #{rule_eight_introspection(plan_mode: plan_mode)}
40
+ 9. Central Storage & Clean Worktree: #{rule_nine_body(active_tools: active_tools, plan_mode: plan_mode)}
40
41
  </operational_rules>
41
42
  XML
42
43
  end
@@ -47,34 +48,35 @@ module Rubik
47
48
  # koşulludur (goal-ledger planı A) — OFF'ta bayt-özdeş; öğreti
48
49
  # TEACHINGS'e giremez (weld testi her öğretiyi operational_rules
49
50
  # içinde zorunlu kılar), goal header'ın 6. maddesinde yaşar.
50
- def role_header(mode, goal_ledger: false)
51
+ def role_header(mode, goal_ledger: false, active_tools: nil)
51
52
  case mode.to_sym
52
53
  when :plan
53
54
  <<~HDR
54
55
  You are Rubik (maestro), the orchestrator in PLAN-ONLY mode: you analyze, plan, and orchestrate; execution belongs to subagents.
55
56
  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
+ No tools are exposed in PLAN-ONLY mode — do NOT emit tool calls or JSON tool blocks. 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
58
  HDR
58
59
  when :goal
60
+ ledger_active = goal_ledger && tool_active?("update_goal_ledger", active_tools)
59
61
  ledger_line = " If a `<goal_ledger>` row is present, keep its state current: read `rubik://goal/ledger` and " \
60
62
  "apply JSON patches with `update_goal_ledger(patch: ...)` against the reported rev after each " \
61
63
  "meaningful step (task status changes, new findings/decisions)."
62
64
  <<~HDR
63
65
  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:
66
+ You own the goal in <user_task> and MUST drive it to verified completion as a pure orchestrator — you conduct, subagents perform. You may perform lightweight read-only inspection (`list_files`, `grep_search`, `read_file`) to orient and shape assignments, while all broader research, code changes, and verification run in subagent contexts, never in yours:
65
67
  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
68
  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
69
  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
70
  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
71
  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
+ 6. Live goal snapshot (status, step budget, extensions): read it with `read_file(path: 'rubik://goal/state')`.#{ledger_line if ledger_active}
73
+ 7. #{goal_storage_rule(active_tools)}
72
74
  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
75
  HDR
74
76
  else
75
77
  <<~HDR
76
78
  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.
79
+ You ALWAYS work through subagents for execution: while you MAY explore read-only (`list_files`, `grep_search`, `read_file`) for quick orientation, broader codebase research, all code changes, and all 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
80
  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
81
  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
82
  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.
@@ -86,8 +88,8 @@ module Rubik
86
88
  # Guidance BULLET PROSE (no wrapper tags, no indent): the renderer
87
89
  # owns the <orchestration_guidance> XML structure and indentation,
88
90
  # Doctrine owns the policy sentences.
89
- def orchestration_guidance_bullets
90
- [
91
+ def orchestration_guidance_bullets(active_tools: nil)
92
+ bullets = [
91
93
  "Archetype selection: Use 'research' for read-only exploration; MUST use 'coder' for code changes/bug fixes " \
92
94
  "(read-only agents cannot modify files, run tests, or run shell commands — git/gh plumbing and command " \
93
95
  "output belong to a coder, plan that pass from the start).",
@@ -119,15 +121,149 @@ module Rubik
119
121
  "keeps them queued — extend or resume it instead. When chaining dependent stages, direct the successor " \
120
122
  "to read predecessor findings via the canonical transcript reference " \
121
123
  "(`read_file('rubik://subagent/<storage-id>/transcript')`, from status/completion notices) — never a " \
122
- "handwritten storage path; the findings can also be pasted into the brief.",
123
- "Chronicler briefs: when dispatching a subagent, paste the relevant chronicled knowledge into the task " \
124
- "brief — the recall digest or a rubik://storage/... link — so the subagent pulls the depth itself. " \
125
- "Subagents never see the `vakanuvis` tool; you are the only librarian.",
126
- "Diligent cleanup: Finished subagents are reaped automatically after a grace period " \
127
- "(`subagents.reap_finished_after`) — in-memory context is reclaimed while the durable transcript stays " \
128
- "readable at its transcript URI and in `rubik://storage` search. `manage_subagents(action: 'reap')` " \
129
- "reaps finished subagents immediately."
124
+ "handwritten storage path; the findings can also be pasted into the brief."
130
125
  ]
126
+ if tool_active?("vakanuvis", active_tools)
127
+ bullets << "Chronicler briefs: when dispatching a subagent, paste the relevant chronicled knowledge into the task " \
128
+ "brief — the recall digest or a rubik://storage/... link — so the subagent pulls the depth itself. " \
129
+ "Subagents never see the `vakanuvis` tool; you are the only librarian."
130
+ end
131
+ bullets << "Diligent cleanup: Finished subagents are reaped automatically after a grace period " \
132
+ "(`subagents.reap_finished_after`) — in-memory context is reclaimed while the durable transcript stays " \
133
+ "readable at its transcript URI and in `rubik://storage` search. `manage_subagents(action: 'reap')` " \
134
+ "reaps finished subagents immediately."
135
+ bullets
136
+ end
137
+
138
+ def tool_active?(tool_name, active_tools)
139
+ return true if active_tools.nil?
140
+
141
+ active_tools.include?(tool_name.to_s)
142
+ end
143
+
144
+ def rule_one_exploration(plan_mode:)
145
+ if plan_mode
146
+ "1. Code Exploration & Review (PLAN-ONLY — no tool calls): Inspect the `<workspace_status>`, `<protocol>`, " \
147
+ "`<buffer_status>`, `<lsp_diagnostics>`, and `<extra_context>` sections already provided in `<system_context>` " \
148
+ "to understand the repository layout, active branch, dirty buffer files, and diagnostics without invoking tools. " \
149
+ "For conceptual queries (e.g. \"code review\", \"code quality\"), evaluate the architecture structurally rather " \
150
+ "than assuming literal keyword matches."
151
+ else
152
+ "1. Code Exploration & Review (read-only: shape subagent assignments and answer the user): Use native tools " \
153
+ "directly (`list_files`, `grep_search`, `search_symbols`, `get_outline`, `read_file`, `find_references`) when " \
154
+ "available in your toolbelt for lightweight orientation, or delegate broader exploration to a 'research' subagent. " \
155
+ "If neither a tool nor a listed command matches your need, inspect the codebase structurally: examine the " \
156
+ "directory layout (`list_files`), search for patterns (`grep_search`), read core modules (`get_outline`, " \
157
+ "`read_file`), and look for TODO/FIXME markers. For conceptual queries (e.g. \"code review\", \"code quality\"), " \
158
+ "do NOT search for the literal words \"refactor\" or \"review\"; evaluate the architecture structurally."
159
+ end
160
+ end
161
+
162
+ def rule_five_synthesis(plan_mode:)
163
+ if plan_mode
164
+ "In PLAN-ONLY mode, do not synthesize tools; if a custom runtime tool would help during execution, note it " \
165
+ "as a proposed step in your plan."
166
+ else
167
+ Rubik::Prompt::ToolTeachings.teaching_for("synthesize_tool")
168
+ end
169
+ end
170
+
171
+ def rule_seven_delegation(plan_mode:)
172
+ if plan_mode
173
+ "7. Subagent Delegation & Parallelism (Maestro — PLAN-ONLY): In PLAN-ONLY mode no tools are exposed, so you " \
174
+ "do NOT invoke `invoke_subagent` or any other tools during this turn; instead, design the plan around the " \
175
+ "maestro execution model (where 'research' handles read-only codebase exploration, 'coder' handles all file " \
176
+ "modifications, refactoring, and test/command execution, and 'self' handles runtime introspection or hybrid " \
177
+ "tasks) so the resulting steps can be executed cleanly once approved."
178
+ else
179
+ "7. Subagent Delegation & Parallelism (Maestro): As the maestro you orchestrate — while you may use read-only " \
180
+ "tools directly for quick orientation (rule 1), broader codebase exploration, code changes, and verification — " \
181
+ "all execution work — is delegated via `invoke_subagent`, in every mode. Match task to archetype: use 'research' " \
182
+ "for read-only codebase exploration; MUST use 'coder' for code modifications, refactoring, and test execution " \
183
+ "('research' has no write or test tools); reach for 'self' (a full capability clone, minus subagent spawning) " \
184
+ "for work that fits neither archetype — runtime introspection/repair, self-evolution work, hybrid tasks. " \
185
+ "Subagents execute asynchronously in disposable sub-contexts in the background. For non-trivial modifications, " \
186
+ "allocate sufficient step budget (e.g. `max_steps: 100-140`) to prevent avoidable mid-flight pauses. When " \
187
+ "chaining stages with `depends_on`, hand the successor the predecessor's canonical transcript reference " \
188
+ "(`read_file('rubik://subagent/<storage-id>/transcript')`, reported by status/completion notices) — never a " \
189
+ "handwritten storage path; if the successor launches before the storage id is known, paste the predecessor's " \
190
+ "findings into its brief. Consume subagents' structured reports (Outcome, Changes/Locations, " \
191
+ "Verification/Findings, Assumptions) directly. After dispatching subagents, do NOT block with 'wait' unless " \
192
+ "you strictly depend on their output in the current turn — conclude your turn and liberate the prompt so the " \
193
+ "user can interact concurrently. Completion, budget-pause, error, and message notifications arrive reactively: " \
194
+ "during your turn via your notification inbox; while you are idle, the runtime automatically wakes you with a " \
195
+ "dedicated notification-evaluation turn — evaluate the pending notifications, act (extend budget, steer with " \
196
+ "send_message, retrieve results, wrap up, or report to the user), and conclude your turn again. Subagents can " \
197
+ "ask YOU questions mid-flight via `ask_question` — a question notification means that subagent is BLOCKED until " \
198
+ "you reply: answer it promptly with `send_message(recipient: '<id>', message: '<your answer>')` (it times out " \
199
+ "otherwise). Call `#{Rubik::Orchestrator::Subagent::Notices.wait_hint_template}` only when synchronous waiting " \
200
+ "is strictly required. Never poll 'status' repeatedly in a loop."
201
+ end
202
+ end
203
+
204
+ def rule_eight_introspection(plan_mode:)
205
+ if plan_mode
206
+ "8. Runtime Introspection, Self-Healing & Human Feedback: In PLAN-ONLY mode, note any missing context, " \
207
+ "architectural trade-offs, or open questions directly in your plan output instead of invoking runtime or " \
208
+ "question tools."
209
+ else
210
+ "8. Runtime Introspection, Self-Healing & Human Feedback: If internal errors or service degradation occur, " \
211
+ "inspect runtime state with `inspect_runtime` and repair failed services via `repair_runtime_service`. When " \
212
+ "critical requirements are underspecified or high-impact architectural choices require user confirmation, use " \
213
+ "`ask_question` with structured multiple-choice options."
214
+ end
215
+ end
216
+
217
+ def rule_nine_body(active_tools: nil, plan_mode: false)
218
+ return "Keep the repository worktree free of agent workfiles, scratch notes, and temporary artifacts." if plan_mode
219
+
220
+ storage_part = if tool_active?("storage", active_tools)
221
+ Rubik::Prompt::ToolTeachings.teaching_for("storage")
222
+ else
223
+ "Keep the repository worktree free of scattered agent workfiles; read any existing " \
224
+ "`rubik://storage/<type>/<id>` entities via `read_file` when a store is wired."
225
+ end
226
+ parts = [storage_part]
227
+ parts << Rubik::Prompt::ToolTeachings.teaching_for("vakanuvis") if tool_active?("vakanuvis", active_tools)
228
+ parts << Rubik::Prompt::ToolTeachings.teaching_for("show_file") if tool_active?("show_file", active_tools)
229
+ parts << "Subagent transcripts are indexed automatically as `transcript` entities (alias `subagent-<storage-id>`) " \
230
+ "and are readable live or persisted via the canonical `rubik://subagent/<storage-id>/transcript` reference — " \
231
+ "indexed entities and live history have different availability, so address the exact execution by its unique " \
232
+ "storage id (from status/completion notices), never by a display id or a disk path."
233
+ parts << Rubik::Prompt::ToolTeachings.teaching_for("prune_context") if tool_active?("prune_context", active_tools)
234
+ parts.join(" ")
235
+ end
236
+
237
+ def goal_storage_rule(active_tools)
238
+ storage_on = tool_active?("storage", active_tools)
239
+ vakanuvis_on = tool_active?("vakanuvis", active_tools)
240
+ if storage_on && vakanuvis_on
241
+ "Persist pipeline knowledge in the central storage (`storage` tool): the decomposed plan, review findings, " \
242
+ "and the final goal record — link stages with entity ids (`links: {\"goal\": \"goal.N.<hash16>\"}`) so the " \
243
+ "audit trail survives the session. Per-stage MEMORY notes go through the chronicler instead: " \
244
+ "`vakanuvis(action: 'record', topic: '...', body: '...')` mints the `mem.` ref, upserts and supersedes — " \
245
+ "never write memory with raw storage puts when `vakanuvis` is available " \
246
+ "(fall back to `storage` `put` only when `vakanuvis` is unavailable); re-enter prior context with " \
247
+ "`vakanuvis(action: 'recall', query: '...')` (also after compaction). Keep the repository worktree free of " \
248
+ "agent workfiles. Transcripts of finished " \
249
+ "subagents are indexed automatically (searchable via `storage` action 'search') and persist after the " \
250
+ "subagent is reaped."
251
+ elsif storage_on
252
+ "Persist pipeline knowledge in the central storage (`storage` tool): the decomposed plan, review findings, " \
253
+ "and the final goal record — link stages with entity ids (`links: {\"goal\": \"goal.N.<hash16>\"}`) so the " \
254
+ "audit trail survives the session. Keep the repository worktree free of agent workfiles. Transcripts of " \
255
+ "finished subagents are indexed automatically (searchable via `storage` action 'search') and persist after " \
256
+ "the subagent is reaped."
257
+ elsif vakanuvis_on
258
+ "Persist durable per-stage MEMORY notes through the chronicler: `vakanuvis(action: 'record', topic: '...', " \
259
+ "body: '...')` mints the `mem.` ref, upserts and supersedes; re-enter prior context with " \
260
+ "`vakanuvis(action: 'recall', query: '...')` (also after compaction). Keep the repository worktree free of " \
261
+ "agent workfiles. Transcripts of finished subagents remain readable via `rubik://subagent/<storage-id>/transcript` " \
262
+ "after the subagent is reaped."
263
+ else
264
+ "Keep the repository worktree free of agent workfiles. Transcripts of finished subagents remain readable via " \
265
+ "`rubik://subagent/<storage-id>/transcript` after the subagent is reaped."
266
+ end
131
267
  end
132
268
 
133
269
  def skills_preamble
@@ -11,13 +11,15 @@ module Rubik
11
11
 
12
12
  module_function
13
13
 
14
+ # PERF: Integer math (len * 10 + 37) / 38 avoids Float allocation and division,
15
+ # bit-for-bit identical to (len / 3.8).ceil; direct length avoids .to_s on Strings.
14
16
  def estimate_text(text)
15
17
  return 0 if text.nil?
16
18
 
17
- str = text.to_s
18
- return 0 if str.empty?
19
+ len = text.is_a?(String) ? text.length : text.to_s.length
20
+ return 0 if len.zero?
19
21
 
20
- (str.length / CHARS_PER_TOKEN).ceil
22
+ ((len * 10) + 37) / 38
21
23
  end
22
24
 
23
25
  def estimate_message(msg)
@@ -19,8 +19,8 @@ module Rubik
19
19
  module ToolTeachings
20
20
  TEACHINGS = {
21
21
  "replace_file_content" =>
22
- "For modifying existing files, prefer `replace_file_content` with exact verbatim `target_content` " \
23
- "(or `old_content`) and `replacement_content` (or `new_content`).",
22
+ "For modifying existing files, prefer `replace_file_content` with `path`, exact verbatim `target_content`, " \
23
+ "and `replacement_content` (plus optional `start_line`, `end_line`, and `allow_multiple`).",
24
24
  "write_file" =>
25
25
  "For creating new files or completely rewriting files, use `write_file`.",
26
26
  "apply_buffer" =>
@@ -31,12 +31,19 @@ module Rubik
31
31
  "synthesize_tool" =>
32
32
  "When you need a specialized capability not available in the built-in toolbelt (such as custom AST " \
33
33
  "transformers, data converters, or domain-specific benchmarkers), dynamically create and register your " \
34
- "own tool at runtime using `synthesize_tool(name: '...', description: '...', code: '->(args) { ... }')`.",
34
+ "own tool at runtime using `synthesize_tool(name: '...', description: '...', " \
35
+ "params_schema: { type: 'object', properties: { ... }, required: [...] }, code: '->(args) { ... }')` " \
36
+ "(always provide `params_schema` when the handler reads arguments from `args`; omit it or pass an empty " \
37
+ "`{ type: 'object', properties: {} }` schema only for tools that take no arguments).",
35
38
  "storage" =>
36
- "Persist agent workfiles — goals, plans, memory notes, wiki knowledge, reviews — in the central " \
39
+ "Persist agent workfiles — goals, plans, reviews, and work notes — in the central " \
37
40
  "`storage` database (`storage` tool: put/get/list/search/set_ref/delete), NOT as scattered files in " \
38
- "the repository worktree. Read entities back with `read_file(path: 'rubik://storage/<type>/<id>')`; " \
39
- "browse with `rubik://storage/index` and `rubik://storage/<type>` listings.",
41
+ "the repository worktree. Read, list, and search all entity types (`goal`, `plan`, `memory`, `wiki`, " \
42
+ "`transcript`, `review`, `note`) via `storage` or `read_file(path: 'rubik://storage/<type>/<id>')` " \
43
+ "(browse with `rubik://storage/index` and `rubik://storage/<type>` listings); write durable `memory` " \
44
+ "cards and compiled `wiki` pages through `vakanuvis` (`record` / `digest`) whenever `vakanuvis` is in " \
45
+ "your toolbelt, reserving direct `storage` `put` on `memory`/`wiki` for sessions where `vakanuvis` is " \
46
+ "unavailable.",
40
47
  "run_command" =>
41
48
  "GitHub facts must be verified, not assumed: after `gh pr merge` (which can succeed with EMPTY output) " \
42
49
  "confirm the merge with `gh pr view <number> --json state`; `mergeable: UNKNOWN` right after a push means " \
@@ -55,12 +62,12 @@ module Rubik
55
62
  "dispatching a subagent, paste the relevant recall digest (or its rubik:// link) into the task brief. " \
56
63
  "At the end of a task, persist only the durable knowledge with `vakanuvis(action: 'record', topic: '...', " \
57
64
  "body: '...')` — memory writes go through record (ref minting, upsert, supersede come free), never " \
58
- "through raw storage puts. Recall is keyword-AND over card/page text, so END every recorded body and " \
59
- "digest query with its own vocabulary: include a `Keywords:` line listing 5-10 synonyms in BOTH working " \
60
- "languages (e.g. TR + EN) — a Turkish query cannot otherwise find an English page. If recall is too " \
61
- "shallow, queue `vakanuvis(action: 'digest', query: '...')`: it returns a deterministic receipt and " \
62
- "compiles the wiki page asynchronously. Make the session close a standard digest point: review the " \
63
- "session's new cards, queue one digest per open topic, and let the home index refresh.",
65
+ "through raw storage puts when `vakanuvis` is available. Recall is keyword-AND over card/page text, so END " \
66
+ "every recorded body and digest query with its own vocabulary: include a `Keywords:` line listing 5-10 " \
67
+ "synonyms in BOTH working languages (e.g. TR + EN) — a Turkish query cannot otherwise find an English page. " \
68
+ "If recall is too shallow, queue `vakanuvis(action: 'digest', query: '...')`: it returns a deterministic " \
69
+ "receipt and compiles the wiki page asynchronously. Make the session close a standard digest point: review " \
70
+ "the session's new cards, queue one digest per open topic, and let the home index refresh.",
64
71
  "prune_context" =>
65
72
  "When long sessions leave stale exploration and superseded tool output weighing on the ROOT window, " \
66
73
  "check `prune_context(action: 'status')` (window estimate vs ceiling, staged plan state) and force a " \
@@ -18,7 +18,6 @@ module Rubik
18
18
  end
19
19
 
20
20
  def run(cmd, timeout: 15, chdir: Dir.pwd, allow_network: false, memory_limit_mb: 512, cpus: "1.0") # rubocop:disable Lint/UnusedMethodArgument
21
- start_time = Time.now
22
21
  workspace = File.expand_path(chdir)
23
22
 
24
23
  bwrap_cmd = [
@@ -42,66 +41,21 @@ module Rubik
42
41
  bwrap_cmd.push("--unshare-pid", "--die-with-parent")
43
42
  bwrap_cmd.push("sh", "-c", cmd)
44
43
 
45
- stdout_str = ""
46
- stderr_str = ""
47
- exit_code = 127
48
- pid = nil
44
+ res = Rubik::Platform.popen_capture(
45
+ *bwrap_cmd,
46
+ timeout: timeout,
47
+ chdir: workspace,
48
+ timeout_message: "Bubblewrap execution timed out after #{timeout} seconds",
49
+ error_message: "Bubblewrap execution error"
50
+ )
49
51
 
50
- begin
51
- Timeout.timeout(timeout) do
52
- Open3.popen3(*bwrap_cmd) do |stdin, stdout, stderr, wait_thr|
53
- pid = wait_thr.pid
54
- begin
55
- stdin.close
56
- rescue
57
- nil
58
- end
59
- t_out = Thread.new do
60
- stdout.read
61
- rescue
62
- ""
63
- end
64
- t_err = Thread.new do
65
- stderr.read
66
- rescue
67
- ""
68
- end
69
- begin
70
- stdout_str = t_out.value.to_s
71
- stderr_str = t_err.value.to_s
72
- status = wait_thr.value
73
- exit_code = status.exitstatus || (status.signaled? ? (128 + (status.termsig || 1)) : 1)
74
- ensure
75
- begin
76
- t_out.kill
77
- rescue
78
- nil
79
- end
80
- begin
81
- t_err.kill
82
- rescue
83
- nil
84
- end
85
- end
86
- end
87
- end
88
- rescue Timeout::Error
89
- Rubik::Platform.kill_tree(pid) if pid
90
- stderr_str = "Bubblewrap execution timed out after #{timeout} seconds"
91
- exit_code = 124
92
- rescue StandardError => e
93
- stderr_str = "Bubblewrap execution error: #{e.message}"
94
- exit_code = 1
95
- end
96
-
97
- duration = Time.now - start_time
98
52
  {
99
- ok: exit_code.zero?,
100
- exit_code: exit_code,
101
- stdout: stdout_str,
102
- stderr: stderr_str,
103
- output: [stdout_str, stderr_str].reject(&:empty?).join("\n"),
104
- duration: duration.round(4),
53
+ ok: res[:ok],
54
+ exit_code: res[:exit_code],
55
+ stdout: res[:stdout],
56
+ stderr: res[:stderr],
57
+ output: res[:output],
58
+ duration: res[:duration],
105
59
  driver: "bwrap"
106
60
  }
107
61
  end