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.
- checksums.yaml +4 -4
- data/ATTRIBUTION.md +4 -0
- data/CHANGELOG.md +81 -0
- data/README.md +137 -268
- data/bin/rubik +9 -0
- data/lib/rubik/cli/workspace_trust.rb +78 -0
- data/lib/rubik/kernel/init.rb +15 -0
- data/lib/rubik/orchestrator/auto_apply.rb +1 -1
- data/lib/rubik/orchestrator/subagent/activation.rb +29 -4
- data/lib/rubik/orchestrator/subagent/builtin_types.rb +117 -0
- data/lib/rubik/orchestrator/subagent/dynamic_store.rb +119 -0
- data/lib/rubik/orchestrator/subagent/instance.rb +8 -36
- data/lib/rubik/orchestrator/subagent/journal.rb +77 -4
- data/lib/rubik/orchestrator/subagent/profile_contract.rb +187 -0
- data/lib/rubik/orchestrator/subagent/profile_snapshot.rb +72 -0
- data/lib/rubik/orchestrator/subagent/reuse.rb +2 -1
- data/lib/rubik/orchestrator/subagent/scoped_registry.rb +2 -0
- data/lib/rubik/orchestrator/subagent/spawn_transaction.rb +170 -0
- data/lib/rubik/orchestrator/subagent/start_gate.rb +98 -0
- data/lib/rubik/orchestrator/subagent/supervisor.rb +4 -67
- data/lib/rubik/orchestrator/subagent/type_definition.rb +84 -0
- data/lib/rubik/orchestrator/subagent/type_registry.rb +139 -159
- data/lib/rubik/orchestrator/turn_executor/subagent_channel.rb +4 -2
- data/lib/rubik/platform/process.rb +78 -1
- data/lib/rubik/prompt/context_builder.rb +54 -20
- data/lib/rubik/prompt/doctrine.rb +158 -22
- data/lib/rubik/prompt/token_estimator.rb +5 -3
- data/lib/rubik/prompt/tool_teachings.rb +19 -12
- data/lib/rubik/sandbox/driver/bubblewrap.rb +13 -59
- data/lib/rubik/sandbox/driver/docker.rb +14 -60
- data/lib/rubik/sandbox/driver/podman.rb +13 -56
- data/lib/rubik/sandbox/driver/process.rb +12 -64
- data/lib/rubik/security/guard.rb +1 -1
- data/lib/rubik/storage/store.rb +52 -11
- data/lib/rubik/tools/builtins/define_subagent.rb +69 -0
- data/lib/rubik/tools/builtins/storage.rb +8 -5
- data/lib/rubik/tools/builtins/subagent.rb +20 -53
- data/lib/rubik/tools/builtins/synthesis.rb +6 -1
- data/lib/rubik/tools/builtins.rb +1 -0
- data/lib/rubik/tools/diagnostic_reporter.rb +2 -1
- data/lib/rubik/version.rb +1 -1
- 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 =
|
|
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
|
-
|
|
303
|
-
|
|
322
|
+
unless plan_mode
|
|
323
|
+
commands_xml = build_commands_xml(svc.commands)
|
|
324
|
+
sections << commands_xml unless commands_xml.empty?
|
|
304
325
|
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
309
|
-
|
|
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:
|
|
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
|
-
|
|
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: #{
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
9. Central Storage & Clean Worktree: #{
|
|
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.
|
|
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
|
|
71
|
-
7.
|
|
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
|
-
|
|
18
|
-
return 0 if
|
|
19
|
+
len = text.is_a?(String) ? text.length : text.to_s.length
|
|
20
|
+
return 0 if len.zero?
|
|
19
21
|
|
|
20
|
-
(
|
|
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
|
-
"
|
|
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: '...',
|
|
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,
|
|
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
|
|
39
|
-
"
|
|
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
|
|
59
|
-
"digest query with its own vocabulary: include a `Keywords:` line listing 5-10
|
|
60
|
-
"languages (e.g. TR + EN) — a Turkish query cannot otherwise find an English page.
|
|
61
|
-
"shallow, queue `vakanuvis(action: 'digest', query: '...')`: it returns a deterministic
|
|
62
|
-
"compiles the wiki page asynchronously. Make the session close a standard digest point: review
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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:
|
|
100
|
-
exit_code: exit_code,
|
|
101
|
-
stdout:
|
|
102
|
-
stderr:
|
|
103
|
-
output: [
|
|
104
|
-
duration: duration
|
|
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
|