rubyn-code 0.7.0 → 0.9.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/README.md +45 -17
- data/lib/rubyn_code/agent/conversation.rb +11 -1
- data/lib/rubyn_code/agent/dynamic_tool_schema.rb +1 -1
- data/lib/rubyn_code/agent/llm_caller.rb +5 -1
- data/lib/rubyn_code/agent/loop.rb +57 -3
- data/lib/rubyn_code/agent/response_parser.rb +8 -0
- data/lib/rubyn_code/agent/system_prompt_builder.rb +3 -0
- data/lib/rubyn_code/agent/tool_processor.rb +10 -0
- data/lib/rubyn_code/autonomous/daemon.rb +1 -1
- data/lib/rubyn_code/cli/commands/context.rb +27 -0
- data/lib/rubyn_code/cli/commands/custom_command.rb +44 -2
- data/lib/rubyn_code/cli/commands/custom_loader.rb +36 -5
- data/lib/rubyn_code/cli/commands/effort.rb +47 -0
- data/lib/rubyn_code/cli/commands/export.rb +174 -0
- data/lib/rubyn_code/cli/commands/mcp.rb +32 -8
- data/lib/rubyn_code/cli/commands/resume.rb +97 -26
- data/lib/rubyn_code/cli/commands/think.rb +47 -0
- data/lib/rubyn_code/cli/first_run.rb +1 -1
- data/lib/rubyn_code/cli/mention_expander.rb +19 -0
- data/lib/rubyn_code/cli/repl.rb +31 -1
- data/lib/rubyn_code/cli/repl_commands.rb +1 -1
- data/lib/rubyn_code/cli/repl_setup.rb +8 -6
- data/lib/rubyn_code/config/defaults.rb +2 -1
- data/lib/rubyn_code/config/schema.json +5 -0
- data/lib/rubyn_code/config/settings.rb +4 -2
- data/lib/rubyn_code/context/auto_compact.rb +1 -1
- data/lib/rubyn_code/context/manual_compact.rb +1 -1
- data/lib/rubyn_code/index/codebase_index.rb +64 -3
- data/lib/rubyn_code/index/prism_extractor.rb +82 -0
- data/lib/rubyn_code/learning/injector.rb +1 -2
- data/lib/rubyn_code/llm/adapters/anthropic.rb +107 -17
- data/lib/rubyn_code/llm/adapters/anthropic_streaming.rb +13 -0
- data/lib/rubyn_code/llm/adapters/base.rb +2 -1
- data/lib/rubyn_code/llm/adapters/openai.rb +1 -1
- data/lib/rubyn_code/llm/adapters/openai_message_translator.rb +21 -0
- data/lib/rubyn_code/llm/client.rb +16 -3
- data/lib/rubyn_code/llm/image_reader.rb +60 -0
- data/lib/rubyn_code/llm/message_builder.rb +21 -1
- data/lib/rubyn_code/llm/model_router.rb +4 -4
- data/lib/rubyn_code/mcp/discovery.rb +93 -0
- data/lib/rubyn_code/memory/session_persistence.rb +1 -1
- data/lib/rubyn_code/observability/cost_calculator.rb +6 -3
- data/lib/rubyn_code/protocols/RUBYN.md +0 -3
- data/lib/rubyn_code/tasks/models.rb +0 -16
- data/lib/rubyn_code/teams/teammate.rb +0 -15
- data/lib/rubyn_code/tools/RUBYN.md +3 -3
- data/lib/rubyn_code/tools/bash.rb +3 -3
- data/lib/rubyn_code/tools/code_graph.rb +134 -0
- data/lib/rubyn_code/tools/executor.rb +6 -1
- data/lib/rubyn_code/tools/phone_a_friend.rb +135 -0
- data/lib/rubyn_code/tools/todo_store.rb +55 -0
- data/lib/rubyn_code/tools/todo_write.rb +88 -0
- data/lib/rubyn_code/version.rb +1 -1
- data/lib/rubyn_code.rb +14 -8
- data/skills/rubyn_self_test.md +140 -0
- metadata +11 -7
- data/lib/rubyn_code/context/context_budget.rb +0 -183
- data/lib/rubyn_code/context/schema_filter.rb +0 -64
- data/lib/rubyn_code/learning/shortcut.rb +0 -95
- data/lib/rubyn_code/llm/adapters/token_caching.rb +0 -54
- data/lib/rubyn_code/llm/streaming.rb +0 -10
- data/lib/rubyn_code/protocols/plan_approval.rb +0 -72
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require_relative 'registry'
|
|
5
|
+
|
|
6
|
+
module RubynCode
|
|
7
|
+
module Tools
|
|
8
|
+
# Update the in-turn task checklist. The model uses this to keep the user
|
|
9
|
+
# informed of progress while it works. The store is shared between the
|
|
10
|
+
# Agent::Loop (which exposes it for the renderer) and this tool.
|
|
11
|
+
class TodoWrite < Base
|
|
12
|
+
TOOL_NAME = 'TodoWrite'
|
|
13
|
+
DESCRIPTION = 'Update the in-turn task checklist. ' \
|
|
14
|
+
'Use this to keep the user informed of what you plan to do, ' \
|
|
15
|
+
'what you are currently working on, and what you have finished.'
|
|
16
|
+
PARAMETERS = {
|
|
17
|
+
todos: { type: :array, required: true,
|
|
18
|
+
description: 'The full set of tasks currently on the checklist. ' \
|
|
19
|
+
'Replace the existing list with this. Each task is ' \
|
|
20
|
+
'{ "content": "...", "status": "pending|in_progress|completed", ' \
|
|
21
|
+
'"active_form": "..." }.' }
|
|
22
|
+
}.freeze
|
|
23
|
+
RISK_LEVEL = :read
|
|
24
|
+
REQUIRES_CONFIRMATION = false
|
|
25
|
+
|
|
26
|
+
VALID_STATUS = %w[pending in_progress completed].freeze
|
|
27
|
+
|
|
28
|
+
def initialize(project_root:, store: nil)
|
|
29
|
+
super(project_root: project_root)
|
|
30
|
+
@store = store
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def execute(todos:)
|
|
34
|
+
items = Array(todos)
|
|
35
|
+
validated = []
|
|
36
|
+
|
|
37
|
+
items.each_with_index do |item, i|
|
|
38
|
+
return "TodoWrite: item #{i} is not a hash — got #{item.class}" unless item.is_a?(Hash)
|
|
39
|
+
|
|
40
|
+
content = item[:content] || item['content']
|
|
41
|
+
status = (item[:status] || item['status']).to_s
|
|
42
|
+
active_form = item[:active_form] || item['active_form']
|
|
43
|
+
|
|
44
|
+
return "TodoWrite: item #{i} missing 'content'" if content.to_s.empty?
|
|
45
|
+
unless VALID_STATUS.include?(status)
|
|
46
|
+
return "TodoWrite: item #{i} status must be one of #{VALID_STATUS.join('/')} (got #{status.inspect})"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
validated << {
|
|
50
|
+
'content' => content.to_s,
|
|
51
|
+
'status' => status,
|
|
52
|
+
'active_form' => active_form.to_s.empty? ? content.to_s : active_form.to_s
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
@store&.replace(validated)
|
|
57
|
+
format(validated)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.summarize(_output, args)
|
|
61
|
+
count = Array(args[:todos] || args['todos']).size
|
|
62
|
+
if count.zero?
|
|
63
|
+
'cleared checklist'
|
|
64
|
+
else
|
|
65
|
+
"checklist: #{count} item#{'s' unless count == 1}"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def format(items)
|
|
72
|
+
return 'Checklist cleared.' if items.empty?
|
|
73
|
+
|
|
74
|
+
items.map do |item|
|
|
75
|
+
mark =
|
|
76
|
+
case item['status']
|
|
77
|
+
when 'completed' then '[x]'
|
|
78
|
+
when 'in_progress' then '[~]'
|
|
79
|
+
else '[ ]'
|
|
80
|
+
end
|
|
81
|
+
"#{mark} #{item['content']}"
|
|
82
|
+
end.join("\n")
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
Registry.register(TodoWrite)
|
|
87
|
+
end
|
|
88
|
+
end
|
data/lib/rubyn_code/version.rb
CHANGED
data/lib/rubyn_code.rb
CHANGED
|
@@ -47,6 +47,11 @@ module RubynCode
|
|
|
47
47
|
module LLM
|
|
48
48
|
autoload :Client, 'rubyn_code/llm/client'
|
|
49
49
|
autoload :MessageBuilder, 'rubyn_code/llm/message_builder'
|
|
50
|
+
autoload :TextBlock, 'rubyn_code/llm/message_builder'
|
|
51
|
+
autoload :ThinkingBlock, 'rubyn_code/llm/message_builder'
|
|
52
|
+
autoload :ImageBlock, 'rubyn_code/llm/message_builder'
|
|
53
|
+
autoload :ToolUseBlock, 'rubyn_code/llm/message_builder'
|
|
54
|
+
autoload :ImageReader, 'rubyn_code/llm/image_reader'
|
|
50
55
|
autoload :ModelRouter, 'rubyn_code/llm/model_router'
|
|
51
56
|
|
|
52
57
|
# Adapters (provider-specific implementations)
|
|
@@ -54,7 +59,6 @@ module RubynCode
|
|
|
54
59
|
autoload :Base, 'rubyn_code/llm/adapters/base'
|
|
55
60
|
autoload :JsonParsing, 'rubyn_code/llm/adapters/json_parsing'
|
|
56
61
|
autoload :PromptCaching, 'rubyn_code/llm/adapters/prompt_caching'
|
|
57
|
-
autoload :TokenCaching, 'rubyn_code/llm/adapters/token_caching'
|
|
58
62
|
autoload :Anthropic, 'rubyn_code/llm/adapters/anthropic'
|
|
59
63
|
autoload :AnthropicCompatible, 'rubyn_code/llm/adapters/anthropic_compatible'
|
|
60
64
|
autoload :AnthropicStreaming, 'rubyn_code/llm/adapters/anthropic_streaming'
|
|
@@ -63,9 +67,6 @@ module RubynCode
|
|
|
63
67
|
autoload :OpenAICompatible, 'rubyn_code/llm/adapters/openai_compatible'
|
|
64
68
|
autoload :OpenAIMessageTranslator, 'rubyn_code/llm/adapters/openai_message_translator'
|
|
65
69
|
end
|
|
66
|
-
|
|
67
|
-
# Backward-compat: LLM::Streaming → Adapters::AnthropicStreaming
|
|
68
|
-
autoload :Streaming, 'rubyn_code/llm/streaming'
|
|
69
70
|
end
|
|
70
71
|
|
|
71
72
|
# Layer 1: Agent Loop
|
|
@@ -81,6 +82,7 @@ module RubynCode
|
|
|
81
82
|
module Tools
|
|
82
83
|
autoload :Base, 'rubyn_code/tools/base'
|
|
83
84
|
autoload :Registry, 'rubyn_code/tools/registry'
|
|
85
|
+
autoload :TodoStore, 'rubyn_code/tools/todo_store'
|
|
84
86
|
autoload :Schema, 'rubyn_code/tools/schema'
|
|
85
87
|
autoload :Executor, 'rubyn_code/tools/executor'
|
|
86
88
|
autoload :ReadFile, 'rubyn_code/tools/read_file'
|
|
@@ -88,6 +90,7 @@ module RubynCode
|
|
|
88
90
|
autoload :EditFile, 'rubyn_code/tools/edit_file'
|
|
89
91
|
autoload :Glob, 'rubyn_code/tools/glob'
|
|
90
92
|
autoload :Grep, 'rubyn_code/tools/grep'
|
|
93
|
+
autoload :CodeGraph, 'rubyn_code/tools/code_graph'
|
|
91
94
|
autoload :Bash, 'rubyn_code/tools/bash'
|
|
92
95
|
autoload :RailsGenerate, 'rubyn_code/tools/rails_generate'
|
|
93
96
|
autoload :DbMigrate, 'rubyn_code/tools/db_migrate'
|
|
@@ -95,6 +98,7 @@ module RubynCode
|
|
|
95
98
|
autoload :BundleInstall, 'rubyn_code/tools/bundle_install'
|
|
96
99
|
autoload :BundleAdd, 'rubyn_code/tools/bundle_add'
|
|
97
100
|
autoload :Compact, 'rubyn_code/tools/compact'
|
|
101
|
+
autoload :TodoWrite, 'rubyn_code/tools/todo_write'
|
|
98
102
|
autoload :LoadSkill, 'rubyn_code/tools/load_skill'
|
|
99
103
|
autoload :Task, 'rubyn_code/tools/task'
|
|
100
104
|
autoload :MemorySearch, 'rubyn_code/tools/memory_search'
|
|
@@ -103,6 +107,7 @@ module RubynCode
|
|
|
103
107
|
autoload :ReadInbox, 'rubyn_code/tools/read_inbox'
|
|
104
108
|
autoload :ReviewPr, 'rubyn_code/tools/review_pr'
|
|
105
109
|
autoload :SpawnAgent, 'rubyn_code/tools/spawn_agent'
|
|
110
|
+
autoload :PhoneAFriend, 'rubyn_code/tools/phone_a_friend'
|
|
106
111
|
autoload :BackgroundRun, 'rubyn_code/tools/background_run'
|
|
107
112
|
autoload :WebSearch, 'rubyn_code/tools/web_search'
|
|
108
113
|
autoload :WebFetch, 'rubyn_code/tools/web_fetch'
|
|
@@ -133,8 +138,6 @@ module RubynCode
|
|
|
133
138
|
autoload :AutoCompact, 'rubyn_code/context/auto_compact'
|
|
134
139
|
autoload :ManualCompact, 'rubyn_code/context/manual_compact'
|
|
135
140
|
autoload :ContextCollapse, 'rubyn_code/context/context_collapse'
|
|
136
|
-
autoload :ContextBudget, 'rubyn_code/context/context_budget'
|
|
137
|
-
autoload :SchemaFilter, 'rubyn_code/context/schema_filter'
|
|
138
141
|
autoload :DecisionCompactor, 'rubyn_code/context/decision_compactor'
|
|
139
142
|
end
|
|
140
143
|
|
|
@@ -201,7 +204,6 @@ module RubynCode
|
|
|
201
204
|
# Layer 10: Protocols
|
|
202
205
|
module Protocols
|
|
203
206
|
autoload :ShutdownHandshake, 'rubyn_code/protocols/shutdown_handshake'
|
|
204
|
-
autoload :PlanApproval, 'rubyn_code/protocols/plan_approval'
|
|
205
207
|
autoload :InterruptHandler, 'rubyn_code/protocols/interrupt_handler'
|
|
206
208
|
end
|
|
207
209
|
|
|
@@ -258,6 +260,7 @@ module RubynCode
|
|
|
258
260
|
autoload :ToolBridge, 'rubyn_code/mcp/tool_bridge'
|
|
259
261
|
autoload :ServerExtrasBridge, 'rubyn_code/mcp/server_extras_bridge'
|
|
260
262
|
autoload :Config, 'rubyn_code/mcp/config'
|
|
263
|
+
autoload :Discovery, 'rubyn_code/mcp/discovery'
|
|
261
264
|
end
|
|
262
265
|
|
|
263
266
|
# Layer 16: Learning
|
|
@@ -266,7 +269,6 @@ module RubynCode
|
|
|
266
269
|
autoload :Instinct, 'rubyn_code/learning/instinct'
|
|
267
270
|
autoload :InstinctMethods, 'rubyn_code/learning/instinct'
|
|
268
271
|
autoload :Injector, 'rubyn_code/learning/injector'
|
|
269
|
-
autoload :Shortcut, 'rubyn_code/learning/shortcut'
|
|
270
272
|
autoload :Porter, 'rubyn_code/learning/porter'
|
|
271
273
|
end
|
|
272
274
|
|
|
@@ -286,6 +288,7 @@ module RubynCode
|
|
|
286
288
|
# Codebase Index
|
|
287
289
|
module Index
|
|
288
290
|
autoload :CodebaseIndex, 'rubyn_code/index/codebase_index'
|
|
291
|
+
autoload :PrismExtractor, 'rubyn_code/index/prism_extractor'
|
|
289
292
|
end
|
|
290
293
|
|
|
291
294
|
# CLI
|
|
@@ -347,6 +350,9 @@ module RubynCode
|
|
|
347
350
|
autoload :ChiselAudit, 'rubyn_code/cli/commands/chisel_audit'
|
|
348
351
|
autoload :ChiselDebt, 'rubyn_code/cli/commands/chisel_debt'
|
|
349
352
|
autoload :ChiselGain, 'rubyn_code/cli/commands/chisel_gain'
|
|
353
|
+
autoload :Think, 'rubyn_code/cli/commands/think'
|
|
354
|
+
autoload :Effort, 'rubyn_code/cli/commands/effort'
|
|
355
|
+
autoload :Export, 'rubyn_code/cli/commands/export'
|
|
350
356
|
end
|
|
351
357
|
end
|
|
352
358
|
|
data/skills/rubyn_self_test.md
CHANGED
|
@@ -449,6 +449,146 @@ fast and need no API calls.
|
|
|
449
449
|
'
|
|
450
450
|
```
|
|
451
451
|
|
|
452
|
+
#### 17k. Extended thinking + `/think` toggle (PRs #132, #138)
|
|
453
|
+
- **bash**: confirms `LLM::ThinkingBlock` is defined, the Anthropic adapter
|
|
454
|
+
translates `thinking: {budget_tokens}` into the request body, and `/think`
|
|
455
|
+
toggles state on `LLM::Client`. PASS if the final line is `THINKING: PASS`.
|
|
456
|
+
|
|
457
|
+
```bash
|
|
458
|
+
bundle exec ruby -Ilib -rrubyn_code -rrubyn_code/llm/message_builder -e '
|
|
459
|
+
tb = RubynCode::LLM::ThinkingBlock.new(text: "plan")
|
|
460
|
+
adapter = RubynCode::LLM::Adapters::Anthropic.allocate
|
|
461
|
+
body = {}
|
|
462
|
+
adapter.send(:apply_thinking, body, budget_tokens: 4096)
|
|
463
|
+
client = RubynCode::LLM::Client.allocate
|
|
464
|
+
client.thinking_budget_tokens = 4096
|
|
465
|
+
ok = (tb.type == "thinking") &&
|
|
466
|
+
(body[:thinking] == { type: "enabled", budget_tokens: 4096 }) &&
|
|
467
|
+
(client.thinking_budget_tokens == 4096)
|
|
468
|
+
puts(ok ? "THINKING: PASS" : "THINKING: FAIL")
|
|
469
|
+
'
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
#### 17l. Image / vision input (`@image.png`) (PRs #133, #140, #141)
|
|
473
|
+
- **bash**: confirms `LLM::ImageBlock`, `LLM::ImageReader`, and
|
|
474
|
+
`MentionExpander#expand_images` are wired and base64-encode PNGs.
|
|
475
|
+
PASS if the final line is `IMAGES: PASS`.
|
|
476
|
+
|
|
477
|
+
```bash
|
|
478
|
+
bundle exec ruby -Ilib -rrubyn_code -rrubyn_code/llm/message_builder -e '
|
|
479
|
+
require "tmpdir"
|
|
480
|
+
Dir.mktmpdir do |dir|
|
|
481
|
+
path = File.join(dir, "p.png")
|
|
482
|
+
File.binwrite(path, "\x89PNG\r\n\x1a\n".dup)
|
|
483
|
+
block = RubynCode::LLM::ImageReader.for_path(path)
|
|
484
|
+
ex = RubynCode::CLI::MentionExpander.new(project_root: dir).expand_images("look @p.png")
|
|
485
|
+
ok = (block.is_a?(RubynCode::LLM::ImageBlock)) &&
|
|
486
|
+
(block.media_type == "image/png") &&
|
|
487
|
+
(ex.size == 1)
|
|
488
|
+
puts(ok ? "IMAGES: PASS" : "IMAGES: FAIL")
|
|
489
|
+
end
|
|
490
|
+
'
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
#### 17m. TodoWrite live checklist (PR #134)
|
|
494
|
+
- **bash**: confirms `Tools::TodoWrite` is registered and mutates a shared
|
|
495
|
+
`Tools::TodoStore`. PASS if the final line is `TODOWRITE: PASS`.
|
|
496
|
+
|
|
497
|
+
```bash
|
|
498
|
+
bundle exec ruby -Ilib -rrubyn_code -rrubyn_code/tools/todo_write -rrubyn_code/tools/todo_store -e '
|
|
499
|
+
store = RubynCode::Tools::TodoStore.new
|
|
500
|
+
tool = RubynCode::Tools::TodoWrite.new(project_root: Dir.pwd, store: store)
|
|
501
|
+
out = tool.execute(todos: [{ "content" => "a", "status" => "in_progress", "active_form" => "A" }])
|
|
502
|
+
ok = (out.include?("[~] a")) && (store.current.first[:status] == "in_progress") &&
|
|
503
|
+
(RubynCode::Tools::Registry.get("TodoWrite") == RubynCode::Tools::TodoWrite)
|
|
504
|
+
puts(ok ? "TODOWRITE: PASS" : "TODOWRITE: FAIL")
|
|
505
|
+
'
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
#### 17n. Custom-command frontmatter (argument-hint, allowed-tools, model:) (PRs #135, #138)
|
|
509
|
+
- **bash**: confirms `CustomCommand` exposes all three frontmatter keys and
|
|
510
|
+
`Agent::Loop` honors the per-prompt override setters.
|
|
511
|
+
PASS if the final line is `FRONTMATTER: PASS`.
|
|
512
|
+
|
|
513
|
+
```bash
|
|
514
|
+
bundle exec ruby -Ilib -rrubyn_code -rrubyn_code/agent/loop -e '
|
|
515
|
+
loop = RubynCode::Agent::Loop.allocate
|
|
516
|
+
loop.allowed_tools_override = %w[bash read]
|
|
517
|
+
loop.model_override = "claude-opus-4-8"
|
|
518
|
+
at = loop.instance_variable_get(:@allowed_tools_override)
|
|
519
|
+
mo = loop.instance_variable_get(:@model_override)
|
|
520
|
+
cmd = RubynCode::CLI::Commands::CustomCommand.new(
|
|
521
|
+
name: "d", description: "d", body: "b",
|
|
522
|
+
argument_hint: "[env]", allowed_tools: %w[bash read], model: "claude-opus-4-8"
|
|
523
|
+
)
|
|
524
|
+
ok = (at == %w[bash read]) && (mo == "claude-opus-4-8") &&
|
|
525
|
+
(cmd.argument_hint == "[env]") && (cmd.allowed_tools == %w[bash read]) &&
|
|
526
|
+
(cmd.model == "claude-opus-4-8")
|
|
527
|
+
puts(ok ? "FRONTMATTER: PASS" : "FRONTMATTER: FAIL")
|
|
528
|
+
'
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
#### 17o. `.mcp.json` auto-discovery (PRs #136, #138, #139)
|
|
532
|
+
- **bash**: writes a project-root `.mcp.json`, calls `MCP::Discovery.discover`,
|
|
533
|
+
and asserts the entry is tagged `:project`.
|
|
534
|
+
PASS if the final line is `MCP-DISCOVERY: PASS`.
|
|
535
|
+
|
|
536
|
+
```bash
|
|
537
|
+
bundle exec ruby -Ilib -rrubyn_code -rrubyn_code/mcp/discovery -e '
|
|
538
|
+
require "tmpdir"; require "json"
|
|
539
|
+
Dir.mktmpdir do |dir|
|
|
540
|
+
File.write(File.join(dir, ".mcp.json"),
|
|
541
|
+
JSON.generate(mcpServers: { "p" => { command: "p" } }))
|
|
542
|
+
entries = RubynCode::MCP::Discovery.discover(dir)
|
|
543
|
+
ok = (entries.any? { |e| e.name == "p" && e.source == :project })
|
|
544
|
+
puts(ok ? "MCP-DISCOVERY: PASS" : "MCP-DISCOVERY: FAIL")
|
|
545
|
+
end
|
|
546
|
+
'
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
#### 17p. `/export` transcript (markdown / jsonl) (PR #137)
|
|
550
|
+
- **bash**: drives `Commands::Export` against a synthetic conversation with
|
|
551
|
+
a `thinking` block and a `tool_use`. Writes a markdown file and asserts
|
|
552
|
+
the file contains role sections, the thinking `<details>`, and the
|
|
553
|
+
fenced-JSON tool_use. PASS if the final line is `EXPORT: PASS`.
|
|
554
|
+
|
|
555
|
+
```bash
|
|
556
|
+
bundle exec ruby -Ilib -rrubyn_code -rrubyn_code/cli/commands/export -e '
|
|
557
|
+
require "tmpdir"
|
|
558
|
+
Dir.mktmpdir do |dir|
|
|
559
|
+
msgs = [
|
|
560
|
+
{ role: "user", content: "hi" },
|
|
561
|
+
{ role: "assistant", content: [
|
|
562
|
+
{ type: "thinking", text: "plan" },
|
|
563
|
+
{ type: "text", text: "ok" },
|
|
564
|
+
{ type: "tool_use", name: "bash", input: { cmd: "ls" } }
|
|
565
|
+
] }
|
|
566
|
+
]
|
|
567
|
+
conv = Object.new
|
|
568
|
+
conv.define_singleton_method(:to_a) { msgs }
|
|
569
|
+
renderer = Object.new
|
|
570
|
+
renderer.define_singleton_method(:info) { |*_| nil }
|
|
571
|
+
renderer.define_singleton_method(:warning) { |*_| nil }
|
|
572
|
+
renderer.define_singleton_method(:ask) { |*_| true }
|
|
573
|
+
ctx = RubynCode::CLI::Commands::Context.new(
|
|
574
|
+
renderer: renderer, conversation: conv, agent_loop: nil,
|
|
575
|
+
context_manager: nil, budget_enforcer: nil, llm_client: nil,
|
|
576
|
+
db: nil, session_id: nil, project_root: nil, skill_loader: nil,
|
|
577
|
+
session_persistence: nil, background_worker: nil, permission_tier: nil,
|
|
578
|
+
plan_mode: false, message_handler: nil, hook_registry: nil,
|
|
579
|
+
checkpoint_manager: nil
|
|
580
|
+
)
|
|
581
|
+
path = File.join(dir, "x.md")
|
|
582
|
+
RubynCode::CLI::Commands::Export.new.execute([path], ctx)
|
|
583
|
+
body = File.read(path)
|
|
584
|
+
ok = body.include?("## User") &&
|
|
585
|
+
body.include?("<details><summary>thinking</summary>") &&
|
|
586
|
+
body.include?("[tool: bash]")
|
|
587
|
+
puts(ok ? "EXPORT: PASS" : "EXPORT: FAIL")
|
|
588
|
+
end
|
|
589
|
+
'
|
|
590
|
+
```
|
|
591
|
+
|
|
452
592
|
### 18. Chisel — Minimal-Code Enforcement (opt-in)
|
|
453
593
|
|
|
454
594
|
Chisel is rubyn-code's "write the minimum that works" layer. It is **off by
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rubyn-code
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- fadedmaturity
|
|
@@ -240,6 +240,8 @@ files:
|
|
|
240
240
|
- lib/rubyn_code/cli/commands/custom_loader.rb
|
|
241
241
|
- lib/rubyn_code/cli/commands/diff.rb
|
|
242
242
|
- lib/rubyn_code/cli/commands/doctor.rb
|
|
243
|
+
- lib/rubyn_code/cli/commands/effort.rb
|
|
244
|
+
- lib/rubyn_code/cli/commands/export.rb
|
|
243
245
|
- lib/rubyn_code/cli/commands/goal.rb
|
|
244
246
|
- lib/rubyn_code/cli/commands/help.rb
|
|
245
247
|
- lib/rubyn_code/cli/commands/install_skills.rb
|
|
@@ -262,6 +264,7 @@ files:
|
|
|
262
264
|
- lib/rubyn_code/cli/commands/skills.rb
|
|
263
265
|
- lib/rubyn_code/cli/commands/spawn.rb
|
|
264
266
|
- lib/rubyn_code/cli/commands/tasks.rb
|
|
267
|
+
- lib/rubyn_code/cli/commands/think.rb
|
|
265
268
|
- lib/rubyn_code/cli/commands/tokens.rb
|
|
266
269
|
- lib/rubyn_code/cli/commands/undo.rb
|
|
267
270
|
- lib/rubyn_code/cli/commands/version.rb
|
|
@@ -289,13 +292,11 @@ files:
|
|
|
289
292
|
- lib/rubyn_code/context/RUBYN.md
|
|
290
293
|
- lib/rubyn_code/context/auto_compact.rb
|
|
291
294
|
- lib/rubyn_code/context/compactor.rb
|
|
292
|
-
- lib/rubyn_code/context/context_budget.rb
|
|
293
295
|
- lib/rubyn_code/context/context_collapse.rb
|
|
294
296
|
- lib/rubyn_code/context/decision_compactor.rb
|
|
295
297
|
- lib/rubyn_code/context/manager.rb
|
|
296
298
|
- lib/rubyn_code/context/manual_compact.rb
|
|
297
299
|
- lib/rubyn_code/context/micro_compact.rb
|
|
298
|
-
- lib/rubyn_code/context/schema_filter.rb
|
|
299
300
|
- lib/rubyn_code/db/RUBYN.md
|
|
300
301
|
- lib/rubyn_code/db/connection.rb
|
|
301
302
|
- lib/rubyn_code/db/migrator.rb
|
|
@@ -338,12 +339,12 @@ files:
|
|
|
338
339
|
- lib/rubyn_code/ide/protocol.rb
|
|
339
340
|
- lib/rubyn_code/ide/server.rb
|
|
340
341
|
- lib/rubyn_code/index/codebase_index.rb
|
|
342
|
+
- lib/rubyn_code/index/prism_extractor.rb
|
|
341
343
|
- lib/rubyn_code/learning/RUBYN.md
|
|
342
344
|
- lib/rubyn_code/learning/extractor.rb
|
|
343
345
|
- lib/rubyn_code/learning/injector.rb
|
|
344
346
|
- lib/rubyn_code/learning/instinct.rb
|
|
345
347
|
- lib/rubyn_code/learning/porter.rb
|
|
346
|
-
- lib/rubyn_code/learning/shortcut.rb
|
|
347
348
|
- lib/rubyn_code/llm/RUBYN.md
|
|
348
349
|
- lib/rubyn_code/llm/adapters/anthropic.rb
|
|
349
350
|
- lib/rubyn_code/llm/adapters/anthropic_compatible.rb
|
|
@@ -355,14 +356,14 @@ files:
|
|
|
355
356
|
- lib/rubyn_code/llm/adapters/openai_message_translator.rb
|
|
356
357
|
- lib/rubyn_code/llm/adapters/openai_streaming.rb
|
|
357
358
|
- lib/rubyn_code/llm/adapters/prompt_caching.rb
|
|
358
|
-
- lib/rubyn_code/llm/adapters/token_caching.rb
|
|
359
359
|
- lib/rubyn_code/llm/client.rb
|
|
360
|
+
- lib/rubyn_code/llm/image_reader.rb
|
|
360
361
|
- lib/rubyn_code/llm/message_builder.rb
|
|
361
362
|
- lib/rubyn_code/llm/model_router.rb
|
|
362
|
-
- lib/rubyn_code/llm/streaming.rb
|
|
363
363
|
- lib/rubyn_code/mcp/RUBYN.md
|
|
364
364
|
- lib/rubyn_code/mcp/client.rb
|
|
365
365
|
- lib/rubyn_code/mcp/config.rb
|
|
366
|
+
- lib/rubyn_code/mcp/discovery.rb
|
|
366
367
|
- lib/rubyn_code/mcp/server_extras_bridge.rb
|
|
367
368
|
- lib/rubyn_code/mcp/sse_transport.rb
|
|
368
369
|
- lib/rubyn_code/mcp/stdio_transport.rb
|
|
@@ -393,7 +394,6 @@ files:
|
|
|
393
394
|
- lib/rubyn_code/permissions/tier.rb
|
|
394
395
|
- lib/rubyn_code/protocols/RUBYN.md
|
|
395
396
|
- lib/rubyn_code/protocols/interrupt_handler.rb
|
|
396
|
-
- lib/rubyn_code/protocols/plan_approval.rb
|
|
397
397
|
- lib/rubyn_code/protocols/shutdown_handshake.rb
|
|
398
398
|
- lib/rubyn_code/self_test.rb
|
|
399
399
|
- lib/rubyn_code/skills/RUBYN.md
|
|
@@ -430,6 +430,7 @@ files:
|
|
|
430
430
|
- lib/rubyn_code/tools/bash.rb
|
|
431
431
|
- lib/rubyn_code/tools/bundle_add.rb
|
|
432
432
|
- lib/rubyn_code/tools/bundle_install.rb
|
|
433
|
+
- lib/rubyn_code/tools/code_graph.rb
|
|
433
434
|
- lib/rubyn_code/tools/compact.rb
|
|
434
435
|
- lib/rubyn_code/tools/db_migrate.rb
|
|
435
436
|
- lib/rubyn_code/tools/edit_file.rb
|
|
@@ -447,6 +448,7 @@ files:
|
|
|
447
448
|
- lib/rubyn_code/tools/memory_search.rb
|
|
448
449
|
- lib/rubyn_code/tools/memory_write.rb
|
|
449
450
|
- lib/rubyn_code/tools/output_compressor.rb
|
|
451
|
+
- lib/rubyn_code/tools/phone_a_friend.rb
|
|
450
452
|
- lib/rubyn_code/tools/rails_generate.rb
|
|
451
453
|
- lib/rubyn_code/tools/read_file.rb
|
|
452
454
|
- lib/rubyn_code/tools/read_inbox.rb
|
|
@@ -459,6 +461,8 @@ files:
|
|
|
459
461
|
- lib/rubyn_code/tools/spawn_teammate.rb
|
|
460
462
|
- lib/rubyn_code/tools/spec_output_parser.rb
|
|
461
463
|
- lib/rubyn_code/tools/task.rb
|
|
464
|
+
- lib/rubyn_code/tools/todo_store.rb
|
|
465
|
+
- lib/rubyn_code/tools/todo_write.rb
|
|
462
466
|
- lib/rubyn_code/tools/web_fetch.rb
|
|
463
467
|
- lib/rubyn_code/tools/web_search.rb
|
|
464
468
|
- lib/rubyn_code/tools/write_file.rb
|
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RubynCode
|
|
4
|
-
module Context
|
|
5
|
-
# Budget-aware context loader that prioritizes which related files
|
|
6
|
-
# to load fully vs. as signatures-only. Prevents context bloat by
|
|
7
|
-
# capping auto-loaded context at a configurable token budget.
|
|
8
|
-
class ContextBudget
|
|
9
|
-
CHARS_PER_TOKEN = 4
|
|
10
|
-
DEFAULT_BUDGET = 4000 # tokens
|
|
11
|
-
|
|
12
|
-
# Rails convention-based priority for related files.
|
|
13
|
-
# Lower number = higher priority = loaded first.
|
|
14
|
-
PRIORITY_MAP = {
|
|
15
|
-
'spec' => 1, # tests for the file
|
|
16
|
-
'factory' => 2, # FactoryBot factories
|
|
17
|
-
'service' => 3, # service objects
|
|
18
|
-
'model' => 4, # related models
|
|
19
|
-
'controller' => 5, # controllers
|
|
20
|
-
'serializer' => 6, # serializers
|
|
21
|
-
'concern' => 7, # concerns/mixins
|
|
22
|
-
'helper' => 8, # helpers
|
|
23
|
-
'migration' => 9 # migrations
|
|
24
|
-
}.freeze
|
|
25
|
-
|
|
26
|
-
attr_reader :loaded_files, :signature_files, :tokens_used
|
|
27
|
-
|
|
28
|
-
def initialize(budget: DEFAULT_BUDGET, codebase_index: nil)
|
|
29
|
-
@budget = budget
|
|
30
|
-
@codebase_index = codebase_index
|
|
31
|
-
@loaded_files = []
|
|
32
|
-
@signature_files = []
|
|
33
|
-
@tokens_used = 0
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
# Load context for a primary file, filling budget with related files.
|
|
37
|
-
# Returns array of { file:, content:, mode: :full|:signatures }
|
|
38
|
-
#
|
|
39
|
-
# When a codebase_index is available and no related_files are supplied,
|
|
40
|
-
# uses impact_analysis to auto-discover related files (specs,
|
|
41
|
-
# associated models, controllers, etc.).
|
|
42
|
-
def load_for(file_path, related_files: [])
|
|
43
|
-
results = []
|
|
44
|
-
|
|
45
|
-
# Primary file always loads fully
|
|
46
|
-
primary_content = safe_read(file_path)
|
|
47
|
-
return results unless primary_content
|
|
48
|
-
|
|
49
|
-
primary_tokens = estimate_tokens(primary_content)
|
|
50
|
-
@tokens_used = primary_tokens
|
|
51
|
-
@loaded_files << file_path
|
|
52
|
-
results << { file: file_path, content: primary_content, mode: :full }
|
|
53
|
-
|
|
54
|
-
# Auto-discover related files from the index when none supplied
|
|
55
|
-
related_files = discover_related_files(file_path) if related_files.empty? && @codebase_index
|
|
56
|
-
|
|
57
|
-
# Sort related files by priority and fill remaining budget
|
|
58
|
-
sorted = prioritize(related_files)
|
|
59
|
-
remaining = @budget - @tokens_used
|
|
60
|
-
remaining = load_full_files(sorted, results, remaining)
|
|
61
|
-
load_signature_files(sorted, results, remaining)
|
|
62
|
-
|
|
63
|
-
results
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
# Extract method signatures and class structure without method bodies.
|
|
67
|
-
# Much more compact than full source — typically 10-20% of original size.
|
|
68
|
-
def extract_signatures(content)
|
|
69
|
-
signatures = []
|
|
70
|
-
indent_stack = []
|
|
71
|
-
|
|
72
|
-
content.lines.each do |line|
|
|
73
|
-
process_signature_line(line, signatures, indent_stack)
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
signatures.join
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
# Returns budget utilization stats.
|
|
80
|
-
def stats
|
|
81
|
-
{
|
|
82
|
-
budget: @budget,
|
|
83
|
-
tokens_used: @tokens_used,
|
|
84
|
-
utilization: @budget.positive? ? (@tokens_used.to_f / @budget).round(3) : 0.0,
|
|
85
|
-
full_files: @loaded_files.size,
|
|
86
|
-
signature_files: @signature_files.size
|
|
87
|
-
}
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
private
|
|
91
|
-
|
|
92
|
-
def discover_related_files(file_path)
|
|
93
|
-
analysis = @codebase_index.impact_analysis(file_path)
|
|
94
|
-
analysis[:affected_files].reject { |f| f == file_path }
|
|
95
|
-
rescue StandardError
|
|
96
|
-
[]
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def load_full_files(sorted, results, remaining)
|
|
100
|
-
sorted.each do |rel_path|
|
|
101
|
-
content = safe_read(rel_path)
|
|
102
|
-
next unless content
|
|
103
|
-
|
|
104
|
-
size = estimate_tokens(content)
|
|
105
|
-
next unless size <= remaining
|
|
106
|
-
|
|
107
|
-
results << { file: rel_path, content: content, mode: :full }
|
|
108
|
-
@loaded_files << rel_path
|
|
109
|
-
@tokens_used += size
|
|
110
|
-
remaining -= size
|
|
111
|
-
end
|
|
112
|
-
remaining
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
def load_signature_files(sorted, results, remaining)
|
|
116
|
-
sorted.each do |rel_path|
|
|
117
|
-
next if @loaded_files.include?(rel_path)
|
|
118
|
-
|
|
119
|
-
content = safe_read(rel_path)
|
|
120
|
-
next unless content
|
|
121
|
-
|
|
122
|
-
sigs = extract_signatures(content)
|
|
123
|
-
sig_size = estimate_tokens(sigs)
|
|
124
|
-
next unless sig_size <= remaining
|
|
125
|
-
|
|
126
|
-
results << { file: rel_path, content: sigs, mode: :signatures }
|
|
127
|
-
@signature_files << rel_path
|
|
128
|
-
@tokens_used += sig_size
|
|
129
|
-
remaining -= sig_size
|
|
130
|
-
end
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
# -- signature extraction dispatch
|
|
134
|
-
def process_signature_line(line, signatures, indent_stack)
|
|
135
|
-
stripped = line.strip
|
|
136
|
-
if signature_line?(stripped)
|
|
137
|
-
signatures << line
|
|
138
|
-
indent_stack << current_indent(line) if block_opener?(stripped)
|
|
139
|
-
elsif stripped == 'end' && indent_stack.any? && current_indent(line) <= indent_stack.last
|
|
140
|
-
signatures << line
|
|
141
|
-
indent_stack.pop
|
|
142
|
-
elsif class_or_module_line?(stripped)
|
|
143
|
-
signatures << line
|
|
144
|
-
indent_stack << current_indent(line)
|
|
145
|
-
end
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
def prioritize(files)
|
|
149
|
-
files.sort_by do |path|
|
|
150
|
-
basename = File.basename(path, '.*').downcase
|
|
151
|
-
priority = PRIORITY_MAP.find { |key, _| basename.include?(key) }&.last || 10
|
|
152
|
-
priority
|
|
153
|
-
end
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
def signature_line?(stripped)
|
|
157
|
-
stripped.match?(/\A\s*(def\s|attr_|include\s|extend\s|has_|belongs_|validates|scope\s|delegate\s)/)
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
def class_or_module_line?(stripped)
|
|
161
|
-
stripped.match?(/\A\s*(class|module)\s/)
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
def block_opener?(stripped)
|
|
165
|
-
stripped.match?(/\Adef\s/)
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
def current_indent(line)
|
|
169
|
-
line.match(/\A(\s*)/)[1].length
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
def safe_read(path)
|
|
173
|
-
File.read(path)
|
|
174
|
-
rescue StandardError
|
|
175
|
-
nil
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
def estimate_tokens(text)
|
|
179
|
-
(text.bytesize.to_f / CHARS_PER_TOKEN).ceil
|
|
180
|
-
end
|
|
181
|
-
end
|
|
182
|
-
end
|
|
183
|
-
end
|