rubik 0.1.6 → 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 +5 -1
- data/CHANGELOG.md +169 -0
- data/README.md +137 -255
- data/bin/rubik +9 -0
- data/lib/rubik/ast/extraction_result.rb +37 -0
- data/lib/rubik/ast/extractor.rb +35 -9
- data/lib/rubik/ast/graph.rb +27 -1
- data/lib/rubik/ast/treesitter/c_walker.rb +136 -28
- data/lib/rubik/ast/treesitter_extractor.rb +45 -17
- data/lib/rubik/buffer/apply_contract.rb +78 -0
- data/lib/rubik/buffer/manager.rb +61 -5
- data/lib/rubik/cli/workspace_trust.rb +78 -0
- data/lib/rubik/concurrency/event_loop.rb +56 -44
- data/lib/rubik/concurrency/scheduler.rb +7 -24
- data/lib/rubik/context/git_lister.rb +6 -1
- data/lib/rubik/context/scanner.rb +15 -3
- data/lib/rubik/kernel/init.rb +15 -0
- data/lib/rubik/mcp/transport/stdio.rb +14 -11
- 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 +152 -3
- data/lib/rubik/plugin/manager.rb +148 -71
- data/lib/rubik/plugin/package_inventory.rb +84 -0
- data/lib/rubik/plugin/requires_dag.rb +40 -0
- data/lib/rubik/plugin/storage.rb +100 -14
- 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/repl/commands/buffer.rb +28 -18
- data/lib/rubik/runtime/repair_probe.rb +98 -0
- 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/blocked_guidance.rb +19 -0
- data/lib/rubik/security/guard.rb +116 -7
- data/lib/rubik/storage/store.rb +52 -11
- data/lib/rubik/tools/builtins/define_subagent.rb +69 -0
- data/lib/rubik/tools/builtins/lifecycle.rb +129 -136
- 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 +15 -1
data/lib/rubik/storage/store.rb
CHANGED
|
@@ -178,7 +178,8 @@ module Rubik
|
|
|
178
178
|
next if records.empty?
|
|
179
179
|
|
|
180
180
|
haystacks = haystacks_for(t, records)
|
|
181
|
-
|
|
181
|
+
folded_fields = folded_fields_for(t, records)
|
|
182
|
+
{ records: records, haystacks: haystacks, folded_fields: folded_fields,
|
|
182
183
|
has: tokens.map { |token| haystacks.map { |h| h.include?(token) } } }
|
|
183
184
|
end
|
|
184
185
|
return [] if corpus.empty?
|
|
@@ -194,14 +195,17 @@ module Rubik
|
|
|
194
195
|
df = dfs[ti]
|
|
195
196
|
next 0.0 if df.zero?
|
|
196
197
|
|
|
197
|
-
|
|
198
|
+
# PERF: count_occurrences using String#index avoids Array allocation of String#scan
|
|
199
|
+
tf = count_occurrences(c[:haystacks][i], token)
|
|
198
200
|
next 0.0 if tf.zero?
|
|
199
201
|
|
|
200
202
|
idf = Math.log(((total - df + 0.5) / (df + 0.5)) + 1)
|
|
201
203
|
dl = c[:haystacks][i].bytesize
|
|
202
204
|
idf * (tf * (BM25_K1 + 1)) / (tf + (BM25_K1 * (1 - BM25_B + (BM25_B * dl / avgdl))))
|
|
203
205
|
end
|
|
204
|
-
|
|
206
|
+
# PERF: pass cached folded title and tags to avoid String folding & array join allocations per score
|
|
207
|
+
f_title, f_tags = c[:folded_fields][i]
|
|
208
|
+
[score + field_tier_bonus(tokens, f_title, f_tags), c[:records][i]]
|
|
205
209
|
end
|
|
206
210
|
end
|
|
207
211
|
ordered = scored.sort do |(score_a, rec_a), (score_b, rec_b)|
|
|
@@ -224,22 +228,44 @@ module Rubik
|
|
|
224
228
|
end
|
|
225
229
|
end
|
|
226
230
|
|
|
231
|
+
# PERF: Zero-allocation string occurrence counting via String#index loop.
|
|
232
|
+
# Replaces String#scan(token).size to avoid array allocation per matched term.
|
|
233
|
+
def count_occurrences(haystack, token)
|
|
234
|
+
return 0 if token.empty?
|
|
235
|
+
|
|
236
|
+
count = 0
|
|
237
|
+
offset = 0
|
|
238
|
+
token_len = token.length
|
|
239
|
+
while (idx = haystack.index(token, offset))
|
|
240
|
+
count += 1
|
|
241
|
+
offset = idx + token_len
|
|
242
|
+
end
|
|
243
|
+
count
|
|
244
|
+
end
|
|
245
|
+
|
|
227
246
|
# Terim başına alan izi: katlanmış başlıkta geçen terim +1.0,
|
|
228
247
|
# yalnız katlanmış etiketlerde geçen +0.5, gövde izi 0 — bkz.
|
|
229
248
|
# TITLE_TIER_BONUS/TAGS_TIER_BONUS. Yalnız AND eşiğini geçen
|
|
230
249
|
# kayıtlar için hesaplanır (ek tarama yok).
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
tags
|
|
234
|
-
|
|
250
|
+
# PERF: accepts pre-folded title & tags strings or fallback record for backward compatibility.
|
|
251
|
+
def field_tier_bonus(tokens, title_or_record, tags = nil)
|
|
252
|
+
if tags.nil? && title_or_record.is_a?(Hash)
|
|
253
|
+
title = Folding.fold(title_or_record["title"])
|
|
254
|
+
tags = Folding.fold(Array(title_or_record["tags"]).join("\n"))
|
|
255
|
+
else
|
|
256
|
+
title = title_or_record.to_s
|
|
257
|
+
tags = tags.to_s
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
sum = 0.0
|
|
261
|
+
tokens.each do |token|
|
|
235
262
|
if title.include?(token)
|
|
236
|
-
TITLE_TIER_BONUS
|
|
263
|
+
sum += TITLE_TIER_BONUS
|
|
237
264
|
elsif tags.include?(token)
|
|
238
|
-
TAGS_TIER_BONUS
|
|
239
|
-
else
|
|
240
|
-
0.0
|
|
265
|
+
sum += TAGS_TIER_BONUS
|
|
241
266
|
end
|
|
242
267
|
end
|
|
268
|
+
sum
|
|
243
269
|
end
|
|
244
270
|
|
|
245
271
|
def set_ref(name, id)
|
|
@@ -430,6 +456,21 @@ module Rubik
|
|
|
430
456
|
entry[:haystacks] ||= records.map { |r| haystack_of(r) }.freeze
|
|
431
457
|
end
|
|
432
458
|
|
|
459
|
+
# PERF: Memoize folded title and tags per record in @records_cache
|
|
460
|
+
# to avoid re-folding and array joining on every search_ranked query.
|
|
461
|
+
def folded_fields_for(type, records)
|
|
462
|
+
entry = @records_cache[type]
|
|
463
|
+
unless entry
|
|
464
|
+
return records.map do |r|
|
|
465
|
+
[Folding.fold(r["title"]), Folding.fold(Array(r["tags"]).join("\n"))]
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
entry[:folded_fields] ||= records.map do |r|
|
|
470
|
+
[Folding.fold(r["title"]), Folding.fold(Array(r["tags"]).join("\n"))].freeze
|
|
471
|
+
end.freeze
|
|
472
|
+
end
|
|
473
|
+
|
|
433
474
|
def haystack_of(record)
|
|
434
475
|
Folding.fold("#{record['title']}\n#{record['body']}\n#{Array(record['tags']).join("\n")}")
|
|
435
476
|
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../base"
|
|
4
|
+
require_relative "../output"
|
|
5
|
+
require_relative "../../orchestrator/subagent/profile_contract"
|
|
6
|
+
require_relative "subagent"
|
|
7
|
+
|
|
8
|
+
module Rubik
|
|
9
|
+
module Tools
|
|
10
|
+
# Tool for dynamically defining a new subagent archetype at runtime (Plan 94).
|
|
11
|
+
class DefineSubagentTool < BaseTool
|
|
12
|
+
include SubagentToolHelpers
|
|
13
|
+
|
|
14
|
+
DECLARATION = Declaration.new(
|
|
15
|
+
name: "define_subagent", aliases: ["register_subagent_type", "create_subagent_type"],
|
|
16
|
+
description: "Defines a new archetype of subagent that can be invoked via invoke_subagent.",
|
|
17
|
+
risk: :execute, subagent_spawn: true, maestro_blocked: false, # self-evolution plane (TOOLS.md maestro policy)
|
|
18
|
+
params_schema: {
|
|
19
|
+
type: "object",
|
|
20
|
+
properties: {
|
|
21
|
+
name: { type: "string", maxLength: Rubik::Orchestrator::Subagent::ProfileContract::FIELD_LIMITS.fetch(:name),
|
|
22
|
+
description: "Unique name for archetype" },
|
|
23
|
+
description: { type: "string", maxLength: Rubik::Orchestrator::Subagent::ProfileContract::FIELD_LIMITS.fetch(:description),
|
|
24
|
+
description: "Human-readable description" },
|
|
25
|
+
system_prompt: { type: "string", maxLength: Rubik::Orchestrator::Subagent::ProfileContract::FIELD_LIMITS.fetch(:system_prompt),
|
|
26
|
+
description: "Detailed system prompt" },
|
|
27
|
+
requires: { type: "array", maxItems: Rubik::Orchestrator::Subagent::ProfileContract::MAX_COEFFECTS,
|
|
28
|
+
items: { type: "string", maxLength: Rubik::Orchestrator::Subagent::ProfileContract::MAX_COEFFECT_LENGTH },
|
|
29
|
+
description: "Runtime coeffect keys required by this profile" },
|
|
30
|
+
grants: { type: "array", maxItems: Rubik::Orchestrator::Subagent::ProfileContract::MAX_GRANTS,
|
|
31
|
+
items: { type: "string", enum: Rubik::Orchestrator::Subagent::ProfileContract::CAPABILITY_GRANTS },
|
|
32
|
+
description: "Optional capabilities: write and subagent_spawn (default: none)" },
|
|
33
|
+
replace: { type: "boolean", description: "Explicitly allow replacing an existing dynamic archetype (default: false)" }
|
|
34
|
+
},
|
|
35
|
+
required: ["name", "description", "system_prompt"]
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def initialize(supervisor: nil, runtime_context: nil)
|
|
40
|
+
super(DECLARATION)
|
|
41
|
+
@supervisor = supervisor
|
|
42
|
+
@runtime_context = runtime_context
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def execute(args = {})
|
|
46
|
+
sup = resolve_supervisor(@supervisor, @runtime_context)
|
|
47
|
+
return ToolOutput.error("SubagentSupervisor is not available in runtime context.") unless sup
|
|
48
|
+
|
|
49
|
+
argument = ->(key) { args.key?(key.to_s) ? args[key.to_s] : args[key.to_sym] }
|
|
50
|
+
provided = ->(key) { args.key?(key.to_s) || args.key?(key.to_sym) }
|
|
51
|
+
|
|
52
|
+
owner_ctx = @runtime_context || (sup.respond_to?(:root_context) ? sup.root_context : nil)
|
|
53
|
+
kwargs = {
|
|
54
|
+
name: argument.call(:name), description: argument.call(:description),
|
|
55
|
+
system_prompt: argument.call(:system_prompt), replace: argument.call(:replace) || false
|
|
56
|
+
}
|
|
57
|
+
[:requires, :grants, :enable_write_tools, :enable_subagent_tools].each do |key|
|
|
58
|
+
kwargs[key] = argument.call(key) if provided.call(key)
|
|
59
|
+
end
|
|
60
|
+
kwargs[:context] = owner_ctx if owner_ctx
|
|
61
|
+
|
|
62
|
+
type_def = sup.type_registry.define_custom(**kwargs)
|
|
63
|
+
"Subagent archetype '#{type_def.name}' successfully defined and available for invoke_subagent."
|
|
64
|
+
rescue ArgumentError => e
|
|
65
|
+
ToolOutput.error(e.message)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
require_relative "../base"
|
|
4
4
|
require_relative "../output"
|
|
5
5
|
require_relative "../../runtime"
|
|
6
|
+
require_relative "../../runtime/repair_probe"
|
|
7
|
+
require_relative "../../buffer/apply_contract"
|
|
6
8
|
|
|
7
9
|
module Rubik
|
|
8
10
|
module Tools
|
|
@@ -33,7 +35,9 @@ module Rubik
|
|
|
33
35
|
def execute(args = {})
|
|
34
36
|
return ToolOutput.error("Buffer manager is not connected.") unless @buffer_manager
|
|
35
37
|
|
|
36
|
-
|
|
38
|
+
shell_config = @shell.respond_to?(:config) ? @shell.config : nil
|
|
39
|
+
manager_config = @buffer_manager.respond_to?(:config) ? @buffer_manager.config : nil
|
|
40
|
+
unless Rubik::Buffer::ApplyContract.model_can_apply?(shell_config: shell_config, manager_config: manager_config)
|
|
37
41
|
return ToolOutput.error("Model buffer application is disabled by configuration " \
|
|
38
42
|
"(buffer.model_can_apply: false). The human operator will review and apply changes.")
|
|
39
43
|
end
|
|
@@ -43,7 +47,15 @@ module Rubik
|
|
|
43
47
|
policy = force ? "overwrite" : nil
|
|
44
48
|
|
|
45
49
|
if @buffer_manager.dirty?
|
|
46
|
-
applied =
|
|
50
|
+
applied = nil
|
|
51
|
+
begin
|
|
52
|
+
applied = @buffer_manager.apply!(policy: policy)
|
|
53
|
+
rescue Rubik::Buffer::ApplyFailedError => e
|
|
54
|
+
# Partial apply: files in e.applied ARE on disk — sync the model's
|
|
55
|
+
# view for them and report the remainder honestly (issue #146).
|
|
56
|
+
lines = Rubik::Buffer::ApplyContract.failure_lines(e, sync: ->(paths) { @shell&.sync_context_and_ast(paths) })
|
|
57
|
+
return ToolOutput.error(lines.join("\n"))
|
|
58
|
+
end
|
|
47
59
|
conflicts = @buffer_manager.last_conflicts || []
|
|
48
60
|
@shell&.sync_context_and_ast(applied) unless applied.empty?
|
|
49
61
|
|
|
@@ -68,30 +80,16 @@ module Rubik
|
|
|
68
80
|
messages.join("\n")
|
|
69
81
|
end
|
|
70
82
|
else
|
|
71
|
-
clean_buffer_message
|
|
83
|
+
Rubik::Buffer::ApplyContract.clean_buffer_message(@buffer_manager.last_applied, auto_apply_mode)
|
|
72
84
|
end
|
|
73
85
|
end
|
|
74
86
|
|
|
75
87
|
private
|
|
76
88
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
# (issue #97 finding 6). When the mode cannot be read, the origin stays
|
|
82
|
-
# deliberately two-sided rather than inventing a specific cause.
|
|
83
|
-
def clean_buffer_message
|
|
84
|
-
last = @buffer_manager.last_applied
|
|
85
|
-
return "No unapplied buffer changes to write (buffer is clean)." unless last.is_a?(Hash) && Array(last[:paths]).any?
|
|
86
|
-
|
|
87
|
-
mode = @shell.respond_to?(:config) && @shell.config.respond_to?(:fetch) ? @shell.config.fetch("buffer.auto_apply", "manual") : nil
|
|
88
|
-
origin = if !mode.nil? && !mode.to_s.strip.empty? && mode.to_s.downcase != "manual"
|
|
89
|
-
"the auto-apply policy (buffer.auto_apply: #{mode})"
|
|
90
|
-
else
|
|
91
|
-
"an earlier apply_buffer call or the auto-apply policy"
|
|
92
|
-
end
|
|
93
|
-
"No unapplied buffer changes to write (buffer is clean): #{last[:paths].size} file(s) were already written to disk " \
|
|
94
|
-
"at #{last[:at].utc.iso8601} by #{origin}: #{last[:paths].join(', ')}."
|
|
89
|
+
def auto_apply_mode
|
|
90
|
+
return nil unless @shell.respond_to?(:config) && @shell.config.respond_to?(:fetch)
|
|
91
|
+
|
|
92
|
+
@shell.config.fetch("buffer.auto_apply", "manual")
|
|
95
93
|
end
|
|
96
94
|
end
|
|
97
95
|
|
|
@@ -117,6 +115,10 @@ module Rubik
|
|
|
117
115
|
type: "string",
|
|
118
116
|
description: "Replacement or custom command string (optional, probed automatically if auto_probe: true)"
|
|
119
117
|
},
|
|
118
|
+
probe_args: {
|
|
119
|
+
type: "array", items: { type: "string" },
|
|
120
|
+
description: "Explicit non-mutating functional probe arguments appended to the exact proposed command; required for unknown executables"
|
|
121
|
+
},
|
|
120
122
|
auto_probe: {
|
|
121
123
|
type: "boolean",
|
|
122
124
|
description: "Whether to automatically probe system binaries and candidate commands (default: true)"
|
|
@@ -187,7 +189,7 @@ module Rubik
|
|
|
187
189
|
error_diagnostic = nil
|
|
188
190
|
|
|
189
191
|
begin
|
|
190
|
-
verified, error_diagnostic = verify_binary(target, lang, proposed_cmd, sub_ctx)
|
|
192
|
+
verified, error_diagnostic = verify_binary(target, lang, proposed_cmd, sub_ctx, probe_args: args["probe_args"] || args[:probe_args])
|
|
191
193
|
rescue StandardError => e
|
|
192
194
|
error_diagnostic = Rubik::Runtime::DiagnosticEngine.diagnose(
|
|
193
195
|
cmd: proposed_cmd,
|
|
@@ -200,66 +202,114 @@ module Rubik
|
|
|
200
202
|
|
|
201
203
|
return ToolOutput.error("Verification failed for #{target} (#{lang}):\n#{error_diagnostic&.format_for_llm}") unless verified
|
|
202
204
|
|
|
203
|
-
#
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
205
|
+
# Live changes belong to a disposable transaction. Successful project
|
|
206
|
+
# persistence is an operator decision and outlives lifecycle cleanup.
|
|
207
|
+
promotion_ctx = root_ctx.extend
|
|
208
|
+
released = false
|
|
209
|
+
@repair_scopes ||= {}
|
|
210
|
+
cfg_key = { "lsp" => "lsp.servers", "test_runner" => "test.runners",
|
|
211
|
+
"linter" => "linter.commands" }[target]
|
|
212
|
+
cfg_key = "#{cfg_key}.#{lang}" if cfg_key
|
|
213
|
+
prev_cmd = @config&.fetch(cfg_key) if cfg_key
|
|
214
|
+
service_key = "#{{ 'test_runner' => 'runner' }.fetch(target, target)}:#{lang}"
|
|
215
|
+
previous_services = root_ctx.services
|
|
216
|
+
phase = "Promotion"
|
|
217
|
+
begin
|
|
218
|
+
promotion_ctx.on_dispose do
|
|
219
|
+
next if released
|
|
220
|
+
|
|
221
|
+
@repair_scopes.delete([root_ctx, service_key]) if @repair_scopes[[root_ctx, service_key]].equal?(promotion_ctx)
|
|
222
|
+
unless root_ctx.disposed?
|
|
223
|
+
if previous_services.key?(service_key)
|
|
224
|
+
restored = target == "lsp" ? @lsp_manager&.clients&.[](lang) : nil
|
|
225
|
+
root_ctx.provide(service_key, restored || previous_services[service_key])
|
|
226
|
+
else
|
|
227
|
+
root_ctx.unprovide(service_key)
|
|
228
|
+
end
|
|
223
229
|
end
|
|
224
|
-
active_client = @lsp_manager.client_for(lang)
|
|
225
|
-
root_ctx.provide("lsp:#{lang}", active_client || proposed_cmd)
|
|
226
|
-
else
|
|
227
|
-
root_ctx.provide("lsp:#{lang}", proposed_cmd)
|
|
228
230
|
end
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
adapter = @adapter_registry.get(lang)
|
|
235
|
-
if adapter
|
|
236
|
-
adapter.config ||= @config
|
|
237
|
-
adapter.custom_test_cmd = proposed_cmd
|
|
238
|
-
else
|
|
239
|
-
require_relative "../../adapter/generic" unless defined?(Rubik::Adapter::GenericAdapter)
|
|
240
|
-
new_adapter = Rubik::Adapter::GenericAdapter.new(lang, test_cmd: proposed_cmd, guard: @guard)
|
|
241
|
-
new_adapter.config = @config
|
|
242
|
-
@adapter_registry.register(lang, new_adapter, ctx: root_ctx)
|
|
231
|
+
if cfg_key && @config
|
|
232
|
+
promotion_ctx.on_dispose do
|
|
233
|
+
next if released
|
|
234
|
+
|
|
235
|
+
prev_cmd.nil? ? @config.delete(cfg_key) : @config.set(cfg_key, prev_cmd)
|
|
243
236
|
end
|
|
237
|
+
@config.set(cfg_key, proposed_cmd)
|
|
244
238
|
end
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
@
|
|
239
|
+
|
|
240
|
+
case target
|
|
241
|
+
when "lsp"
|
|
242
|
+
raise "LSP manager and config are required for live activation" unless @lsp_manager && @config
|
|
243
|
+
|
|
244
|
+
had_client = !@lsp_manager.clients[lang].nil?
|
|
245
|
+
# The manager owns its client table. Restore the old configured
|
|
246
|
+
# client on rollback; probe clients never enter that table.
|
|
247
|
+
promotion_ctx.on_dispose do
|
|
248
|
+
next if released
|
|
249
|
+
|
|
250
|
+
@lsp_manager.stop_client(lang)
|
|
251
|
+
prev_cmd.nil? ? @config.delete(cfg_key) : @config.set(cfg_key, prev_cmd)
|
|
252
|
+
if had_client && !root_ctx.disposed?
|
|
253
|
+
restored = @lsp_manager.start_client(lang)
|
|
254
|
+
raise "Previous LSP client could not be restarted: #{restored}" unless restored[:ok]
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
@lsp_manager.stop_client(lang)
|
|
258
|
+
start_res = @lsp_manager.start_client(lang)
|
|
259
|
+
raise "Failed to start LSP server: #{start_res}" unless start_res.is_a?(Hash) && start_res[:ok]
|
|
260
|
+
|
|
261
|
+
service = @lsp_manager.clients[lang]
|
|
262
|
+
raise "LSP manager reported success without an active client" unless service
|
|
263
|
+
when "test_runner", "linter"
|
|
264
|
+
if @adapter_registry
|
|
265
|
+
adapter = @adapter_registry.get(lang)
|
|
266
|
+
if adapter
|
|
267
|
+
field = target == "test_runner" ? :custom_test_cmd : :custom_format_cmd
|
|
268
|
+
old_value = adapter.public_send(field)
|
|
269
|
+
old_config = adapter.config
|
|
270
|
+
promotion_ctx.on_dispose do
|
|
271
|
+
next if released
|
|
272
|
+
|
|
273
|
+
adapter.public_send("#{field}=", old_value)
|
|
274
|
+
adapter.config = old_config
|
|
275
|
+
end
|
|
276
|
+
adapter.config ||= @config
|
|
277
|
+
adapter.public_send("#{field}=", proposed_cmd)
|
|
278
|
+
else
|
|
279
|
+
require_relative "../../adapter/generic" unless defined?(Rubik::Adapter::GenericAdapter)
|
|
280
|
+
options = target == "test_runner" ? { test_cmd: proposed_cmd } : { format_cmd: proposed_cmd }
|
|
281
|
+
adapter = Rubik::Adapter::GenericAdapter.new(lang, **options, guard: @guard)
|
|
282
|
+
adapter.config = @config
|
|
283
|
+
promotion_ctx.on_dispose do
|
|
284
|
+
@adapter_registry.unregister(lang) if !released && @adapter_registry.get(lang).equal?(adapter)
|
|
285
|
+
end
|
|
286
|
+
@adapter_registry.register(lang, adapter)
|
|
287
|
+
end
|
|
258
288
|
end
|
|
289
|
+
service = proposed_cmd
|
|
290
|
+
when "command"
|
|
291
|
+
service = proposed_cmd
|
|
259
292
|
end
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
293
|
+
|
|
294
|
+
root_ctx.provide(service_key, service)
|
|
295
|
+
phase = "Persistence"
|
|
296
|
+
persist_res = @config.set(cfg_key, proposed_cmd, persist: :project) if cfg_key && @config
|
|
297
|
+
raise persist_res[:error].to_s if persist_res.is_a?(Hash) && !persist_res[:ok]
|
|
298
|
+
rescue StandardError => e
|
|
299
|
+
promotion_ctx.dispose
|
|
300
|
+
rollback_errors = promotion_ctx.last_dispose_errors
|
|
301
|
+
detail = rollback_errors.empty? ? "previous session state restored" : "rollback incomplete: #{rollback_errors}"
|
|
302
|
+
return ToolOutput.error("#{phase} failed for #{target} (#{lang}): #{e.message}; #{detail}")
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# Keep one lifetime inverse per repaired key, preserving the original
|
|
306
|
+
# baseline across repeated repairs without retaining every transaction.
|
|
307
|
+
scope_key = [root_ctx, service_key]
|
|
308
|
+
if @repair_scopes.key?(scope_key)
|
|
309
|
+
released = true
|
|
310
|
+
promotion_ctx.dispose
|
|
311
|
+
else
|
|
312
|
+
@repair_scopes[scope_key] = promotion_ctx
|
|
263
313
|
end
|
|
264
314
|
|
|
265
315
|
if @config.nil? && ["lsp", "test_runner", "linter"].include?(target)
|
|
@@ -267,71 +317,14 @@ module Rubik
|
|
|
267
317
|
"so the command is active for this session only and was not saved."
|
|
268
318
|
end
|
|
269
319
|
|
|
270
|
-
if persist_res.is_a?(Hash) && !persist_res[:ok]
|
|
271
|
-
return "⚠️ Promoted '#{proposed_cmd}' for #{target} (#{lang}) — active in this session, " \
|
|
272
|
-
"but saving to the project config failed: #{persist_res[:error]}"
|
|
273
|
-
end
|
|
274
|
-
|
|
275
320
|
"✨ Successfully verified and promoted runtime service for #{target} (#{lang}): '#{proposed_cmd}'. " \
|
|
276
|
-
"Service is now active
|
|
321
|
+
"Service is now active in the live context."
|
|
277
322
|
end
|
|
278
323
|
|
|
279
324
|
private
|
|
280
325
|
|
|
281
|
-
def verify_binary(target, lang, proposed_cmd, sub_ctx)
|
|
282
|
-
|
|
283
|
-
when "lsp"
|
|
284
|
-
client = Rubik::LSP::Client.new(lang, root_dir: @root_dir, server_cmd: proposed_cmd)
|
|
285
|
-
if client.available?
|
|
286
|
-
sub_ctx.provide("lsp_probe:#{lang}", client)
|
|
287
|
-
[true, nil]
|
|
288
|
-
else
|
|
289
|
-
[false, Rubik::Runtime::DiagnosticEngine.diagnose(
|
|
290
|
-
cmd: proposed_cmd,
|
|
291
|
-
exit_code: 127,
|
|
292
|
-
stderr: "Pre-flight check failed: LSP binary '#{client.binary_name}' not available on system"
|
|
293
|
-
)]
|
|
294
|
-
end
|
|
295
|
-
when "test_runner"
|
|
296
|
-
if Rubik::Runtime::Discovery.binary_available?(proposed_cmd)
|
|
297
|
-
sub_ctx.provide("runner_probe:#{lang}", proposed_cmd)
|
|
298
|
-
[true, nil]
|
|
299
|
-
else
|
|
300
|
-
[false, Rubik::Runtime::DiagnosticEngine.diagnose(
|
|
301
|
-
cmd: proposed_cmd,
|
|
302
|
-
exit_code: 127,
|
|
303
|
-
stderr: "Pre-flight check failed: Test runner executable not found in PATH"
|
|
304
|
-
)]
|
|
305
|
-
end
|
|
306
|
-
when "linter"
|
|
307
|
-
if Rubik::Runtime::Discovery.binary_available?(proposed_cmd)
|
|
308
|
-
sub_ctx.provide("linter_probe:#{lang}", proposed_cmd)
|
|
309
|
-
[true, nil]
|
|
310
|
-
else
|
|
311
|
-
[false, Rubik::Runtime::DiagnosticEngine.diagnose(
|
|
312
|
-
cmd: proposed_cmd,
|
|
313
|
-
exit_code: 127,
|
|
314
|
-
stderr: "Pre-flight check failed: Linter/formatter executable not found in PATH"
|
|
315
|
-
)]
|
|
316
|
-
end
|
|
317
|
-
when "command"
|
|
318
|
-
if Rubik::Runtime::Discovery.binary_available?(proposed_cmd)
|
|
319
|
-
sub_ctx.provide("command_probe:#{lang}", proposed_cmd)
|
|
320
|
-
[true, nil]
|
|
321
|
-
else
|
|
322
|
-
[false, Rubik::Runtime::DiagnosticEngine.diagnose(
|
|
323
|
-
cmd: proposed_cmd,
|
|
324
|
-
exit_code: 127,
|
|
325
|
-
stderr: "Pre-flight check failed: Command executable not found in PATH"
|
|
326
|
-
)]
|
|
327
|
-
end
|
|
328
|
-
else
|
|
329
|
-
[false, Rubik::Runtime::DiagnosticEngine.diagnose(
|
|
330
|
-
cmd: proposed_cmd,
|
|
331
|
-
exit_code: 1,
|
|
332
|
-
stderr: "Unknown target component type: #{target}"
|
|
333
|
-
)]
|
|
334
|
-
end
|
|
326
|
+
def verify_binary(target, lang, proposed_cmd, sub_ctx, probe_args: nil)
|
|
327
|
+
Rubik::Runtime::RepairProbe.new(root_dir: @root_dir).verify(target, lang, proposed_cmd, sub_ctx, probe_args: probe_args)
|
|
335
328
|
end
|
|
336
329
|
end
|
|
337
330
|
|
|
@@ -24,9 +24,12 @@ module Rubik
|
|
|
24
24
|
DECLARATION = Declaration.new(
|
|
25
25
|
name: "storage", aliases: ["rubik_storage"],
|
|
26
26
|
description: "Central rubik://storage database for goals, plans, memory, wiki, transcripts, reviews, " \
|
|
27
|
-
"and notes. Actions: 'put' (create/update
|
|
28
|
-
"'search', 'set_ref' (alias), 'delete'.
|
|
29
|
-
"
|
|
27
|
+
"and notes. Actions: 'put' (create/update work artifacts — goal, plan, review, note; " \
|
|
28
|
+
"returns id and rubik://storage URI), 'get', 'list', 'search', 'set_ref' (alias), 'delete'. " \
|
|
29
|
+
"When `vakanuvis` is in your toolbelt, write durable `memory` via `vakanuvis(action: 'record')` " \
|
|
30
|
+
"and compiled `wiki` via `vakanuvis(action: 'digest')`, reserving direct `put` on `memory`/`wiki` " \
|
|
31
|
+
"for roles or sessions where `vakanuvis` is unavailable. Keep agent workfiles HERE — not in " \
|
|
32
|
+
"the worktree. Read back any entity with read_file path 'rubik://storage/<type>/<id>'.",
|
|
30
33
|
risk: :write, archetypes: [:coder], write_grant: true,
|
|
31
34
|
maestro_blocked: false, # storage plane stays direct at root (TOOLS.md maestro policy)
|
|
32
35
|
params_schema: {
|
|
@@ -35,12 +38,12 @@ module Rubik
|
|
|
35
38
|
action: {
|
|
36
39
|
type: "string",
|
|
37
40
|
enum: ["put", "get", "list", "search", "set_ref", "delete"],
|
|
38
|
-
description: "Operation (default: 'list')."
|
|
41
|
+
description: "Operation (default: 'list'). Use 'put' for goal/plan/review/note workfiles (or fallback writes when vakanuvis is unavailable)."
|
|
39
42
|
},
|
|
40
43
|
type: {
|
|
41
44
|
type: "string",
|
|
42
45
|
enum: Rubik::Storage::Store::TYPES,
|
|
43
|
-
description: "Entity type: goal, plan, memory, wiki, transcript, review, or note."
|
|
46
|
+
description: "Entity type: goal, plan, memory, wiki, transcript, review, or note (prefer vakanuvis record/digest for memory/wiki writes when available)."
|
|
44
47
|
},
|
|
45
48
|
title: { type: "string", description: "Entity title (required for put)." },
|
|
46
49
|
body: { type: "string", description: "Entity body (markdown text)." },
|
|
@@ -101,20 +101,32 @@ module Rubik
|
|
|
101
101
|
return ToolOutput.error("Failed to invoke subagent(s): #{e.message}")
|
|
102
102
|
end
|
|
103
103
|
|
|
104
|
-
launched,
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
104
|
+
launched, remaining = results.partition { |outcome, _obj| outcome == :launched }
|
|
105
|
+
queued, failed = remaining.partition { |outcome, _obj| outcome == :queued }
|
|
106
|
+
lines = launched.empty? ? ["No subagents launched."] : ["Launched #{launched.size} background subagent(s):"]
|
|
107
|
+
lines.concat(launched.map do |_o, i|
|
|
108
|
+
" • [#{i.id}] '#{i.role}' (#{i.type_name}) - #{i.state.to_s.upcase} | transcript: #{i.transcript_ref}"
|
|
109
|
+
end)
|
|
109
110
|
unless queued.empty?
|
|
110
111
|
rows = queued.map { |_o, s| " • [#{s.id}] '#{s.role}' - QUEUED (waiting on: #{s.depends_on.join(', ')})" }
|
|
111
112
|
lines << ("\nQueued #{queued.size} subagent(s) on unmet dependencies (no slot, no cost until released): " + rows.join("\n"))
|
|
112
113
|
end
|
|
114
|
+
unless failed.empty?
|
|
115
|
+
rows = failed.map do |_outcome, spec|
|
|
116
|
+
reason = spec.respond_to?(:last_error) && spec.last_error
|
|
117
|
+
reason = "cancelled before launch" if reason.to_s.empty?
|
|
118
|
+
" • [#{spec.id}] '#{spec.role}' - FAILED TO START (#{reason})"
|
|
119
|
+
end
|
|
120
|
+
lines << ("\n#{failed.size} subagent(s) failed to start: " + rows.join("\n"))
|
|
121
|
+
end
|
|
113
122
|
all_ids = (launched + queued).map { |_o, obj| obj.id }
|
|
114
123
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
124
|
+
unless all_ids.empty?
|
|
125
|
+
lines << "\nSubagents run asynchronously in the background. Conclude your turn to yield the prompt to the user, or call " \
|
|
126
|
+
"#{Rubik::Orchestrator::Subagent::Notices.wait_hint(all_ids)} only if you strictly need results in this turn (queued ids block until the chain settles)."
|
|
127
|
+
end
|
|
128
|
+
result = lines.join("\n")
|
|
129
|
+
failed.empty? ? result : ToolOutput.error(result)
|
|
118
130
|
end
|
|
119
131
|
|
|
120
132
|
private
|
|
@@ -315,50 +327,5 @@ module Rubik
|
|
|
315
327
|
"Result: #{res[0, 1500]}... [truncated — full result: #{more}]"
|
|
316
328
|
end
|
|
317
329
|
end
|
|
318
|
-
|
|
319
|
-
# Tool for dynamically defining a new subagent archetype at runtime (Plan 94).
|
|
320
|
-
class DefineSubagentTool < BaseTool
|
|
321
|
-
include SubagentToolHelpers
|
|
322
|
-
|
|
323
|
-
DECLARATION = Declaration.new(
|
|
324
|
-
name: "define_subagent", aliases: ["register_subagent_type", "create_subagent_type"],
|
|
325
|
-
description: "Defines a new archetype of subagent that can be invoked via invoke_subagent.",
|
|
326
|
-
risk: :execute, subagent_spawn: true, maestro_blocked: false, # self-evolution plane (TOOLS.md maestro policy)
|
|
327
|
-
params_schema: {
|
|
328
|
-
type: "object",
|
|
329
|
-
properties: {
|
|
330
|
-
name: { type: "string", description: "Unique name for archetype" },
|
|
331
|
-
description: { type: "string", description: "Human-readable description" },
|
|
332
|
-
system_prompt: { type: "string", description: "Detailed system prompt" },
|
|
333
|
-
enable_write_tools: { type: "boolean", description: "Equip subagent with write tools (default: false)" },
|
|
334
|
-
enable_subagent_tools: { type: "boolean", description: "Allow subagent to spawn subagents (default: false)" }
|
|
335
|
-
},
|
|
336
|
-
required: ["name", "description", "system_prompt"]
|
|
337
|
-
}
|
|
338
|
-
)
|
|
339
|
-
def initialize(supervisor: nil, runtime_context: nil)
|
|
340
|
-
super(DECLARATION)
|
|
341
|
-
@supervisor = supervisor
|
|
342
|
-
@runtime_context = runtime_context
|
|
343
|
-
end
|
|
344
|
-
|
|
345
|
-
def execute(args = {})
|
|
346
|
-
sup = resolve_supervisor(@supervisor, @runtime_context)
|
|
347
|
-
return ToolOutput.error("SubagentSupervisor is not available in runtime context.") unless sup
|
|
348
|
-
|
|
349
|
-
name = (args["name"] || args[:name]).to_s.strip
|
|
350
|
-
desc = (args["description"] || args[:description]).to_s.strip
|
|
351
|
-
prompt = (args["system_prompt"] || args[:system_prompt]).to_s.strip
|
|
352
|
-
return ToolOutput.error("Parameters 'name', 'description', and 'system_prompt' are required.") if name.empty? || desc.empty? || prompt.empty?
|
|
353
|
-
|
|
354
|
-
type_def = sup.type_registry.define_custom(
|
|
355
|
-
name: name, description: desc, system_prompt: prompt,
|
|
356
|
-
enable_write_tools: args["enable_write_tools"] || args[:enable_write_tools] || false,
|
|
357
|
-
enable_subagent_tools: args["enable_subagent_tools"] || args[:enable_subagent_tools] || false
|
|
358
|
-
)
|
|
359
|
-
|
|
360
|
-
"Subagent archetype '#{type_def.name}' successfully defined and available for invoke_subagent."
|
|
361
|
-
end
|
|
362
|
-
end
|
|
363
330
|
end
|
|
364
331
|
end
|
|
@@ -176,7 +176,12 @@ module Rubik
|
|
|
176
176
|
description: "Storage scope: session (in-memory only), project (.rubik/tools), global (~/.rubik/tools)" },
|
|
177
177
|
risk: { type: "string", enum: ["read", "write", "execute"], default: "execute",
|
|
178
178
|
description: "Risk level of tool (optional; read/write/execute, default: execute)" },
|
|
179
|
-
params_schema: {
|
|
179
|
+
params_schema: {
|
|
180
|
+
type: "object",
|
|
181
|
+
description: "JSON Schema definition of the tool parameters (provide properties/required when the tool " \
|
|
182
|
+
"accepts arguments; defaults to an empty object schema `{ type: 'object', properties: {} }` " \
|
|
183
|
+
"for tools that take no arguments)"
|
|
184
|
+
},
|
|
180
185
|
code: { type: "string", description: "Ruby implementation of tool handler: ->(args) { ... } or class extending Rubik::Tools::BaseTool" },
|
|
181
186
|
test_args: { type: "object", description: "Optional test arguments to perform pre-flight verification in an isolated sandbox" }
|
|
182
187
|
},
|