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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +45 -17
  3. data/lib/rubyn_code/agent/conversation.rb +11 -1
  4. data/lib/rubyn_code/agent/dynamic_tool_schema.rb +1 -1
  5. data/lib/rubyn_code/agent/llm_caller.rb +5 -1
  6. data/lib/rubyn_code/agent/loop.rb +57 -3
  7. data/lib/rubyn_code/agent/response_parser.rb +8 -0
  8. data/lib/rubyn_code/agent/system_prompt_builder.rb +3 -0
  9. data/lib/rubyn_code/agent/tool_processor.rb +10 -0
  10. data/lib/rubyn_code/autonomous/daemon.rb +1 -1
  11. data/lib/rubyn_code/cli/commands/context.rb +27 -0
  12. data/lib/rubyn_code/cli/commands/custom_command.rb +44 -2
  13. data/lib/rubyn_code/cli/commands/custom_loader.rb +36 -5
  14. data/lib/rubyn_code/cli/commands/effort.rb +47 -0
  15. data/lib/rubyn_code/cli/commands/export.rb +174 -0
  16. data/lib/rubyn_code/cli/commands/mcp.rb +32 -8
  17. data/lib/rubyn_code/cli/commands/resume.rb +97 -26
  18. data/lib/rubyn_code/cli/commands/think.rb +47 -0
  19. data/lib/rubyn_code/cli/first_run.rb +1 -1
  20. data/lib/rubyn_code/cli/mention_expander.rb +19 -0
  21. data/lib/rubyn_code/cli/repl.rb +31 -1
  22. data/lib/rubyn_code/cli/repl_commands.rb +1 -1
  23. data/lib/rubyn_code/cli/repl_setup.rb +8 -6
  24. data/lib/rubyn_code/config/defaults.rb +2 -1
  25. data/lib/rubyn_code/config/schema.json +5 -0
  26. data/lib/rubyn_code/config/settings.rb +4 -2
  27. data/lib/rubyn_code/context/auto_compact.rb +1 -1
  28. data/lib/rubyn_code/context/manual_compact.rb +1 -1
  29. data/lib/rubyn_code/index/codebase_index.rb +64 -3
  30. data/lib/rubyn_code/index/prism_extractor.rb +82 -0
  31. data/lib/rubyn_code/learning/injector.rb +1 -2
  32. data/lib/rubyn_code/llm/adapters/anthropic.rb +107 -17
  33. data/lib/rubyn_code/llm/adapters/anthropic_streaming.rb +13 -0
  34. data/lib/rubyn_code/llm/adapters/base.rb +2 -1
  35. data/lib/rubyn_code/llm/adapters/openai.rb +1 -1
  36. data/lib/rubyn_code/llm/adapters/openai_message_translator.rb +21 -0
  37. data/lib/rubyn_code/llm/client.rb +16 -3
  38. data/lib/rubyn_code/llm/image_reader.rb +60 -0
  39. data/lib/rubyn_code/llm/message_builder.rb +21 -1
  40. data/lib/rubyn_code/llm/model_router.rb +4 -4
  41. data/lib/rubyn_code/mcp/discovery.rb +93 -0
  42. data/lib/rubyn_code/memory/session_persistence.rb +1 -1
  43. data/lib/rubyn_code/observability/cost_calculator.rb +6 -3
  44. data/lib/rubyn_code/protocols/RUBYN.md +0 -3
  45. data/lib/rubyn_code/tasks/models.rb +0 -16
  46. data/lib/rubyn_code/teams/teammate.rb +0 -15
  47. data/lib/rubyn_code/tools/RUBYN.md +3 -3
  48. data/lib/rubyn_code/tools/bash.rb +3 -3
  49. data/lib/rubyn_code/tools/code_graph.rb +134 -0
  50. data/lib/rubyn_code/tools/executor.rb +6 -1
  51. data/lib/rubyn_code/tools/phone_a_friend.rb +135 -0
  52. data/lib/rubyn_code/tools/todo_store.rb +55 -0
  53. data/lib/rubyn_code/tools/todo_write.rb +88 -0
  54. data/lib/rubyn_code/version.rb +1 -1
  55. data/lib/rubyn_code.rb +14 -8
  56. data/skills/rubyn_self_test.md +140 -0
  57. metadata +11 -7
  58. data/lib/rubyn_code/context/context_budget.rb +0 -183
  59. data/lib/rubyn_code/context/schema_filter.rb +0 -64
  60. data/lib/rubyn_code/learning/shortcut.rb +0 -95
  61. data/lib/rubyn_code/llm/adapters/token_caching.rb +0 -54
  62. data/lib/rubyn_code/llm/streaming.rb +0 -10
  63. 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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubynCode
4
- VERSION = '0.7.0'
4
+ VERSION = '0.9.0'
5
5
  end
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
 
@@ -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.7.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