rubyn-code 0.1.0 → 0.2.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 (159) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +269 -467
  3. data/db/migrations/009_create_teams.sql +6 -6
  4. data/db/migrations/011_fix_mailbox_messages_columns.rb +35 -0
  5. data/db/migrations/012_expand_mailbox_message_types.rb +37 -0
  6. data/exe/rubyn-code +1 -1
  7. data/lib/rubyn_code/agent/RUBYN.md +17 -0
  8. data/lib/rubyn_code/agent/conversation.rb +68 -19
  9. data/lib/rubyn_code/agent/loop.rb +312 -54
  10. data/lib/rubyn_code/agent/loop_detector.rb +6 -6
  11. data/lib/rubyn_code/auth/RUBYN.md +19 -0
  12. data/lib/rubyn_code/auth/oauth.rb +40 -35
  13. data/lib/rubyn_code/auth/server.rb +16 -12
  14. data/lib/rubyn_code/auth/token_store.rb +22 -22
  15. data/lib/rubyn_code/autonomous/RUBYN.md +14 -0
  16. data/lib/rubyn_code/autonomous/daemon.rb +115 -79
  17. data/lib/rubyn_code/autonomous/idle_poller.rb +4 -8
  18. data/lib/rubyn_code/autonomous/task_claimer.rb +11 -11
  19. data/lib/rubyn_code/background/RUBYN.md +13 -0
  20. data/lib/rubyn_code/background/notifier.rb +0 -2
  21. data/lib/rubyn_code/background/worker.rb +60 -15
  22. data/lib/rubyn_code/cli/RUBYN.md +30 -0
  23. data/lib/rubyn_code/cli/app.rb +85 -9
  24. data/lib/rubyn_code/cli/commands/RUBYN.md +133 -0
  25. data/lib/rubyn_code/cli/commands/base.rb +53 -0
  26. data/lib/rubyn_code/cli/commands/budget.rb +24 -0
  27. data/lib/rubyn_code/cli/commands/clear.rb +16 -0
  28. data/lib/rubyn_code/cli/commands/compact.rb +21 -0
  29. data/lib/rubyn_code/cli/commands/context.rb +44 -0
  30. data/lib/rubyn_code/cli/commands/context_info.rb +56 -0
  31. data/lib/rubyn_code/cli/commands/cost.rb +23 -0
  32. data/lib/rubyn_code/cli/commands/diff.rb +30 -0
  33. data/lib/rubyn_code/cli/commands/doctor.rb +112 -0
  34. data/lib/rubyn_code/cli/commands/help.rb +41 -0
  35. data/lib/rubyn_code/cli/commands/model.rb +37 -0
  36. data/lib/rubyn_code/cli/commands/plan.rb +22 -0
  37. data/lib/rubyn_code/cli/commands/quit.rb +17 -0
  38. data/lib/rubyn_code/cli/commands/registry.rb +64 -0
  39. data/lib/rubyn_code/cli/commands/resume.rb +51 -0
  40. data/lib/rubyn_code/cli/commands/review.rb +26 -0
  41. data/lib/rubyn_code/cli/commands/skill.rb +32 -0
  42. data/lib/rubyn_code/cli/commands/spawn.rb +24 -0
  43. data/lib/rubyn_code/cli/commands/tasks.rb +32 -0
  44. data/lib/rubyn_code/cli/commands/tokens.rb +76 -0
  45. data/lib/rubyn_code/cli/commands/undo.rb +17 -0
  46. data/lib/rubyn_code/cli/commands/version.rb +16 -0
  47. data/lib/rubyn_code/cli/daemon_runner.rb +129 -0
  48. data/lib/rubyn_code/cli/input_handler.rb +20 -23
  49. data/lib/rubyn_code/cli/renderer.rb +25 -27
  50. data/lib/rubyn_code/cli/repl.rb +161 -194
  51. data/lib/rubyn_code/cli/setup.rb +117 -0
  52. data/lib/rubyn_code/cli/spinner.rb +40 -40
  53. data/lib/rubyn_code/cli/stream_formatter.rb +29 -28
  54. data/lib/rubyn_code/cli/version_check.rb +94 -0
  55. data/lib/rubyn_code/config/RUBYN.md +14 -0
  56. data/lib/rubyn_code/config/defaults.rb +28 -19
  57. data/lib/rubyn_code/config/project_config.rb +7 -9
  58. data/lib/rubyn_code/config/settings.rb +3 -3
  59. data/lib/rubyn_code/context/RUBYN.md +20 -0
  60. data/lib/rubyn_code/context/auto_compact.rb +7 -7
  61. data/lib/rubyn_code/context/compactor.rb +2 -2
  62. data/lib/rubyn_code/context/context_collapse.rb +45 -0
  63. data/lib/rubyn_code/context/manager.rb +20 -3
  64. data/lib/rubyn_code/context/manual_compact.rb +7 -7
  65. data/lib/rubyn_code/context/micro_compact.rb +12 -12
  66. data/lib/rubyn_code/db/RUBYN.md +40 -0
  67. data/lib/rubyn_code/db/connection.rb +13 -13
  68. data/lib/rubyn_code/db/migrator.rb +67 -27
  69. data/lib/rubyn_code/db/schema.rb +6 -6
  70. data/lib/rubyn_code/debug.rb +74 -0
  71. data/lib/rubyn_code/hooks/RUBYN.md +17 -0
  72. data/lib/rubyn_code/hooks/built_in.rb +9 -9
  73. data/lib/rubyn_code/hooks/registry.rb +5 -5
  74. data/lib/rubyn_code/hooks/runner.rb +1 -1
  75. data/lib/rubyn_code/hooks/user_hooks.rb +16 -16
  76. data/lib/rubyn_code/learning/RUBYN.md +16 -0
  77. data/lib/rubyn_code/learning/extractor.rb +22 -22
  78. data/lib/rubyn_code/learning/injector.rb +17 -18
  79. data/lib/rubyn_code/learning/instinct.rb +18 -14
  80. data/lib/rubyn_code/llm/RUBYN.md +15 -0
  81. data/lib/rubyn_code/llm/client.rb +121 -55
  82. data/lib/rubyn_code/llm/message_builder.rb +19 -15
  83. data/lib/rubyn_code/llm/streaming.rb +80 -50
  84. data/lib/rubyn_code/mcp/RUBYN.md +21 -0
  85. data/lib/rubyn_code/mcp/client.rb +25 -24
  86. data/lib/rubyn_code/mcp/config.rb +7 -7
  87. data/lib/rubyn_code/mcp/sse_transport.rb +27 -26
  88. data/lib/rubyn_code/mcp/stdio_transport.rb +22 -19
  89. data/lib/rubyn_code/mcp/tool_bridge.rb +32 -32
  90. data/lib/rubyn_code/memory/RUBYN.md +17 -0
  91. data/lib/rubyn_code/memory/models.rb +3 -3
  92. data/lib/rubyn_code/memory/search.rb +17 -17
  93. data/lib/rubyn_code/memory/session_persistence.rb +49 -34
  94. data/lib/rubyn_code/memory/store.rb +17 -17
  95. data/lib/rubyn_code/observability/RUBYN.md +19 -0
  96. data/lib/rubyn_code/observability/budget_enforcer.rb +16 -15
  97. data/lib/rubyn_code/observability/cost_calculator.rb +3 -3
  98. data/lib/rubyn_code/observability/token_counter.rb +1 -1
  99. data/lib/rubyn_code/observability/usage_reporter.rb +35 -35
  100. data/lib/rubyn_code/output/RUBYN.md +11 -0
  101. data/lib/rubyn_code/output/diff_renderer.rb +6 -6
  102. data/lib/rubyn_code/output/formatter.rb +4 -4
  103. data/lib/rubyn_code/permissions/RUBYN.md +17 -0
  104. data/lib/rubyn_code/permissions/prompter.rb +8 -8
  105. data/lib/rubyn_code/protocols/RUBYN.md +14 -0
  106. data/lib/rubyn_code/protocols/interrupt_handler.rb +1 -1
  107. data/lib/rubyn_code/protocols/plan_approval.rb +9 -9
  108. data/lib/rubyn_code/protocols/shutdown_handshake.rb +9 -11
  109. data/lib/rubyn_code/skills/RUBYN.md +19 -0
  110. data/lib/rubyn_code/skills/catalog.rb +7 -7
  111. data/lib/rubyn_code/skills/document.rb +15 -15
  112. data/lib/rubyn_code/skills/loader.rb +6 -8
  113. data/lib/rubyn_code/sub_agents/RUBYN.md +12 -0
  114. data/lib/rubyn_code/sub_agents/runner.rb +15 -15
  115. data/lib/rubyn_code/sub_agents/summarizer.rb +1 -1
  116. data/lib/rubyn_code/tasks/RUBYN.md +13 -0
  117. data/lib/rubyn_code/tasks/dag.rb +12 -16
  118. data/lib/rubyn_code/tasks/manager.rb +24 -24
  119. data/lib/rubyn_code/tasks/models.rb +4 -4
  120. data/lib/rubyn_code/teams/RUBYN.md +14 -0
  121. data/lib/rubyn_code/teams/mailbox.rb +38 -18
  122. data/lib/rubyn_code/teams/manager.rb +19 -19
  123. data/lib/rubyn_code/teams/teammate.rb +3 -4
  124. data/lib/rubyn_code/tools/RUBYN.md +38 -0
  125. data/lib/rubyn_code/tools/background_run.rb +9 -11
  126. data/lib/rubyn_code/tools/base.rb +54 -3
  127. data/lib/rubyn_code/tools/bash.rb +16 -34
  128. data/lib/rubyn_code/tools/bundle_add.rb +10 -12
  129. data/lib/rubyn_code/tools/bundle_install.rb +9 -11
  130. data/lib/rubyn_code/tools/compact.rb +10 -9
  131. data/lib/rubyn_code/tools/db_migrate.rb +17 -15
  132. data/lib/rubyn_code/tools/edit_file.rb +12 -12
  133. data/lib/rubyn_code/tools/executor.rb +9 -4
  134. data/lib/rubyn_code/tools/git_commit.rb +29 -34
  135. data/lib/rubyn_code/tools/git_diff.rb +17 -18
  136. data/lib/rubyn_code/tools/git_log.rb +17 -19
  137. data/lib/rubyn_code/tools/git_status.rb +18 -20
  138. data/lib/rubyn_code/tools/glob.rb +7 -9
  139. data/lib/rubyn_code/tools/grep.rb +11 -9
  140. data/lib/rubyn_code/tools/load_skill.rb +7 -7
  141. data/lib/rubyn_code/tools/memory_search.rb +13 -12
  142. data/lib/rubyn_code/tools/memory_write.rb +14 -12
  143. data/lib/rubyn_code/tools/rails_generate.rb +16 -16
  144. data/lib/rubyn_code/tools/read_file.rb +8 -7
  145. data/lib/rubyn_code/tools/read_inbox.rb +5 -5
  146. data/lib/rubyn_code/tools/registry.rb +2 -2
  147. data/lib/rubyn_code/tools/review_pr.rb +55 -55
  148. data/lib/rubyn_code/tools/run_specs.rb +20 -19
  149. data/lib/rubyn_code/tools/schema.rb +9 -11
  150. data/lib/rubyn_code/tools/send_message.rb +10 -10
  151. data/lib/rubyn_code/tools/spawn_agent.rb +51 -23
  152. data/lib/rubyn_code/tools/spawn_teammate.rb +21 -21
  153. data/lib/rubyn_code/tools/task.rb +28 -28
  154. data/lib/rubyn_code/tools/web_fetch.rb +46 -31
  155. data/lib/rubyn_code/tools/web_search.rb +64 -66
  156. data/lib/rubyn_code/tools/write_file.rb +7 -6
  157. data/lib/rubyn_code/version.rb +1 -1
  158. data/lib/rubyn_code.rb +136 -105
  159. metadata +94 -21
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "tty-prompt"
4
- require "pastel"
5
- require "json"
3
+ require 'tty-prompt'
4
+ require 'pastel'
5
+ require 'json'
6
6
 
7
7
  module RubynCode
8
8
  module Permissions
@@ -19,7 +19,7 @@ module RubynCode
19
19
  display_tool_summary(pastel, tool_name, tool_input)
20
20
 
21
21
  prompt.yes?(
22
- pastel.yellow("Allow this tool call?"),
22
+ pastel.yellow('Allow this tool call?'),
23
23
  default: true
24
24
  )
25
25
  rescue TTY::Prompt::Reader::InputInterrupt
@@ -36,16 +36,16 @@ module RubynCode
36
36
  prompt = build_prompt
37
37
  pastel = Pastel.new
38
38
 
39
- $stdout.puts pastel.red.bold("WARNING: Destructive operation requested")
40
- $stdout.puts pastel.red("=" * 50)
39
+ $stdout.puts pastel.red.bold('WARNING: Destructive operation requested')
40
+ $stdout.puts pastel.red('=' * 50)
41
41
  display_tool_summary(pastel, tool_name, tool_input)
42
- $stdout.puts pastel.red("=" * 50)
42
+ $stdout.puts pastel.red('=' * 50)
43
43
 
44
44
  answer = prompt.ask(
45
45
  pastel.red.bold('Type "yes" to confirm this destructive action:')
46
46
  )
47
47
 
48
- answer&.strip&.downcase == "yes"
48
+ answer&.strip&.downcase == 'yes'
49
49
  rescue TTY::Prompt::Reader::InputInterrupt
50
50
  false
51
51
  end
@@ -0,0 +1,14 @@
1
+ # Layer 10: Protocols
2
+
3
+ Safety and coordination protocols for agent lifecycle.
4
+
5
+ ## Classes
6
+
7
+ - **`ShutdownHandshake`** — Graceful shutdown. Waits for the current tool call to complete,
8
+ saves conversation state, and cleans up resources.
9
+
10
+ - **`PlanApproval`** — When the agent proposes a multi-step plan, this prompts the user
11
+ for approval before execution. Shows the plan, waits for yes/no/edit.
12
+
13
+ - **`InterruptHandler`** — Traps SIGINT (Ctrl+C). First interrupt cancels the current
14
+ operation. Second interrupt within 2 seconds triggers shutdown.
@@ -23,7 +23,7 @@ module RubynCode
23
23
  @last_interrupt_at = nil
24
24
  end
25
25
 
26
- trap("INT") do
26
+ trap('INT') do
27
27
  handle_interrupt
28
28
  end
29
29
  end
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "tty-prompt"
4
- require "tty-reader"
5
- require "pastel"
3
+ require 'tty-prompt'
4
+ require 'tty-reader'
5
+ require 'pastel'
6
6
 
7
7
  module RubynCode
8
8
  module Protocols
@@ -25,10 +25,10 @@ module RubynCode
25
25
  tty = build_prompt
26
26
 
27
27
  $stdout.puts
28
- $stdout.puts pastel.cyan.bold("Proposed Plan")
29
- $stdout.puts pastel.cyan("=" * 60)
28
+ $stdout.puts pastel.cyan.bold('Proposed Plan')
29
+ $stdout.puts pastel.cyan('=' * 60)
30
30
  $stdout.puts plan_text
31
- $stdout.puts pastel.cyan("=" * 60)
31
+ $stdout.puts pastel.cyan('=' * 60)
32
32
  $stdout.puts
33
33
 
34
34
  if prompt
@@ -37,15 +37,15 @@ module RubynCode
37
37
  end
38
38
 
39
39
  approved = tty.yes?(
40
- pastel.yellow.bold("Do you approve this plan?"),
40
+ pastel.yellow.bold('Do you approve this plan?'),
41
41
  default: false
42
42
  )
43
43
 
44
44
  if approved
45
- $stdout.puts pastel.green("Plan approved.")
45
+ $stdout.puts pastel.green('Plan approved.')
46
46
  APPROVED
47
47
  else
48
- $stdout.puts pastel.red("Plan rejected.")
48
+ $stdout.puts pastel.red('Plan rejected.')
49
49
  REJECTED
50
50
  end
51
51
  rescue TTY::Reader::InputInterrupt
@@ -22,8 +22,8 @@ module RubynCode
22
22
  mailbox.send(
23
23
  from: from,
24
24
  to: to,
25
- content: "shutdown_request",
26
- message_type: "shutdown_request"
25
+ content: 'shutdown_request',
26
+ message_type: 'shutdown_request'
27
27
  )
28
28
 
29
29
  deadline = Time.now + timeout
@@ -31,7 +31,7 @@ module RubynCode
31
31
  loop do
32
32
  messages = mailbox.read_inbox(from)
33
33
  response = messages.find do |msg|
34
- msg[:from] == to && msg[:message_type] == "shutdown_response"
34
+ msg[:from] == to && msg[:message_type] == 'shutdown_response'
35
35
  end
36
36
 
37
37
  return :acknowledged if response
@@ -50,13 +50,13 @@ module RubynCode
50
50
  # @param approve [Boolean] whether to approve the shutdown (default: true)
51
51
  # @return [String] the message id
52
52
  def respond(mailbox:, from:, to:, approve: true)
53
- content = approve ? "shutdown_approved" : "shutdown_denied"
53
+ content = approve ? 'shutdown_approved' : 'shutdown_denied'
54
54
 
55
55
  mailbox.send(
56
56
  from: from,
57
57
  to: to,
58
58
  content: content,
59
- message_type: "shutdown_response"
59
+ message_type: 'shutdown_response'
60
60
  )
61
61
  end
62
62
 
@@ -74,16 +74,14 @@ module RubynCode
74
74
  save_state(agent_name, session_persistence, conversation)
75
75
 
76
76
  # Step 2: Send shutdown acknowledgement if there is a requester
77
- if requester
78
- respond(mailbox: mailbox, from: agent_name, to: requester, approve: true)
79
- end
77
+ respond(mailbox: mailbox, from: agent_name, to: requester, approve: true) if requester
80
78
 
81
79
  # Step 3: Broadcast offline status to all listeners
82
80
  mailbox.send(
83
81
  from: agent_name,
84
- to: "_system",
82
+ to: '_system',
85
83
  content: "#{agent_name} is now offline",
86
- message_type: "status_change"
84
+ message_type: 'status_change'
87
85
  )
88
86
  end
89
87
 
@@ -101,7 +99,7 @@ module RubynCode
101
99
  messages: conversation.messages
102
100
  )
103
101
  rescue StandardError => e
104
- $stderr.puts "[ShutdownHandshake] Warning: failed to save state for '#{agent_name}': #{e.message}"
102
+ warn "[ShutdownHandshake] Warning: failed to save state for '#{agent_name}': #{e.message}"
105
103
  end
106
104
  end
107
105
  end
@@ -0,0 +1,19 @@
1
+ # Layer 5: Skills
2
+
3
+ 112 curated markdown skill documents loaded on demand.
4
+
5
+ ## Classes
6
+
7
+ - **`Catalog`** — Discovers all skill files under `skills/` and builds a searchable index.
8
+ Maps slash-names (`/factory-bot`) to file paths. No registration needed — drop a `.md`
9
+ file in the right category directory and it's discoverable.
10
+
11
+ - **`Loader`** — Loads a skill document by name, returns its content. Caches loaded skills
12
+ in the `skills_cache` SQLite table to avoid repeated file reads.
13
+
14
+ - **`Document`** — Value object representing a loaded skill: name, category, content, path.
15
+
16
+ ## Adding a Skill
17
+
18
+ 1. Create `skills/<category>/my-skill.md`
19
+ 2. It auto-discovers. That's it.
@@ -3,7 +3,7 @@
3
3
  module RubynCode
4
4
  module Skills
5
5
  class Catalog
6
- SKILL_GLOB = "**/*.md"
6
+ SKILL_GLOB = '**/*.md'
7
7
 
8
8
  attr_reader :skills_dirs
9
9
 
@@ -14,7 +14,7 @@ module RubynCode
14
14
 
15
15
  def descriptions
16
16
  entries = available
17
- return "" if entries.empty?
17
+ return '' if entries.empty?
18
18
 
19
19
  entries.map { |entry| "- /#{entry[:name]}: #{entry[:description]}" }.join("\n")
20
20
  end
@@ -37,7 +37,7 @@ module RubynCode
37
37
  skills_dirs.each do |dir|
38
38
  next unless File.directory?(dir)
39
39
 
40
- Dir.glob(File.join(dir, SKILL_GLOB)).sort.each do |path|
40
+ Dir.glob(File.join(dir, SKILL_GLOB)).each do |path|
41
41
  entry = extract_metadata(path)
42
42
  @index << entry if entry
43
43
  end
@@ -47,12 +47,12 @@ module RubynCode
47
47
  end
48
48
 
49
49
  def extract_metadata(path)
50
- header = File.read(path, 1024, encoding: "UTF-8")
51
- .encode("UTF-8", invalid: :replace, undef: :replace, replace: "")
50
+ header = File.read(path, 1024, encoding: 'UTF-8')
51
+ .encode('UTF-8', invalid: :replace, undef: :replace, replace: '')
52
52
  doc = Document.parse(header, filename: path)
53
53
 
54
- name = if doc.name.empty? || doc.name == "unknown"
55
- File.basename(path, ".md")
54
+ name = if doc.name.empty? || doc.name == 'unknown'
55
+ File.basename(path, '.md')
56
56
  else
57
57
  doc.name
58
58
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "yaml"
3
+ require 'yaml'
4
4
 
5
5
  module RubynCode
6
6
  module Skills
@@ -25,15 +25,15 @@ module RubynCode
25
25
  body = match[2].to_s.strip
26
26
 
27
27
  new(
28
- name: frontmatter["name"].to_s,
29
- description: frontmatter["description"].to_s,
30
- tags: Array(frontmatter["tags"]),
28
+ name: frontmatter['name'].to_s,
29
+ description: frontmatter['description'].to_s,
30
+ tags: Array(frontmatter['tags']),
31
31
  body: body
32
32
  )
33
33
  else
34
34
  body = content.to_s.strip
35
35
  title = extract_title(body)
36
- derived_name = filename ? File.basename(filename, ".*").tr("_", "-") : title_to_name(title)
36
+ derived_name = filename ? File.basename(filename, '.*').tr('_', '-') : title_to_name(title)
37
37
  tags = derive_tags(derived_name, body)
38
38
 
39
39
  new(
@@ -49,29 +49,29 @@ module RubynCode
49
49
  raise Error, "Skill file not found: #{path}" unless File.exist?(path)
50
50
  raise Error, "Not a file: #{path}" unless File.file?(path)
51
51
 
52
- content = File.read(path, encoding: "UTF-8")
52
+ content = File.read(path, encoding: 'UTF-8')
53
53
  parse(content, filename: path)
54
54
  end
55
55
 
56
56
  private
57
57
 
58
58
  def extract_title(body)
59
- first_line = body.lines.first&.strip || ""
60
- first_line.start_with?("#") ? first_line.sub(/^#+\s*/, "") : first_line[0..80]
59
+ first_line = body.lines.first&.strip || ''
60
+ first_line.start_with?('#') ? first_line.sub(/^#+\s*/, '') : first_line[0..80]
61
61
  end
62
62
 
63
63
  def title_to_name(title)
64
- title.downcase.gsub(/[^a-z0-9]+/, "-").gsub(/^-|-$/, "")[0..40]
64
+ title.downcase.gsub(/[^a-z0-9]+/, '-').gsub(/^-|-$/, '')[0..40]
65
65
  end
66
66
 
67
67
  def derive_tags(name, body)
68
68
  tags = []
69
- tags << "ruby" if body.match?(/\bruby\b/i) || name.include?("ruby")
70
- tags << "rails" if body.match?(/\brails\b/i) || name.include?("rails")
71
- tags << "rspec" if body.match?(/\brspec\b/i) || name.include?("rspec")
72
- tags << "testing" if body.match?(/\b(test|spec|minitest)\b/i)
73
- tags << "patterns" if body.match?(/\b(pattern|design|solid)\b/i)
74
- tags << "refactoring" if body.match?(/\brefactor/i)
69
+ tags << 'ruby' if body.match?(/\bruby\b/i) || name.include?('ruby')
70
+ tags << 'rails' if body.match?(/\brails\b/i) || name.include?('rails')
71
+ tags << 'rspec' if body.match?(/\brspec\b/i) || name.include?('rspec')
72
+ tags << 'testing' if body.match?(/\b(test|spec|minitest)\b/i)
73
+ tags << 'patterns' if body.match?(/\b(pattern|design|solid)\b/i)
74
+ tags << 'refactoring' if body.match?(/\brefactor/i)
75
75
  tags.uniq
76
76
  end
77
77
  end
@@ -13,9 +13,7 @@ module RubynCode
13
13
  def load(name)
14
14
  name = name.to_s
15
15
 
16
- if @loaded.key?(name)
17
- return @loaded[name]
18
- end
16
+ return @loaded[name] if @loaded.key?(name)
19
17
 
20
18
  path = catalog.find(name)
21
19
  raise Error, "Skill not found: #{name}" unless path
@@ -41,16 +39,16 @@ module RubynCode
41
39
  parts = []
42
40
  parts << "<skill name=\"#{escape_xml(doc.name)}\">"
43
41
  parts << doc.body unless doc.body.empty?
44
- parts << "</skill>"
42
+ parts << '</skill>'
45
43
  parts.join("\n")
46
44
  end
47
45
 
48
46
  def escape_xml(text)
49
47
  text.to_s
50
- .gsub("&", "&amp;")
51
- .gsub("<", "&lt;")
52
- .gsub(">", "&gt;")
53
- .gsub("\"", "&quot;")
48
+ .gsub('&', '&amp;')
49
+ .gsub('<', '&lt;')
50
+ .gsub('>', '&gt;')
51
+ .gsub('"', '&quot;')
54
52
  end
55
53
  end
56
54
  end
@@ -0,0 +1,12 @@
1
+ # Layer 6: Sub-Agents
2
+
3
+ Isolated agents spawned for specific tasks, scoped to read-only or full access.
4
+
5
+ ## Classes
6
+
7
+ - **`Runner`** — Spawns a sub-agent with its own fresh conversation context. Two types:
8
+ `explore` (read-only tools) and `worker` (full write access). The sub-agent runs
9
+ its own `Agent::Loop`, completes its task, and returns only a summary.
10
+
11
+ - **`Summarizer`** — Compresses a sub-agent's full conversation into a concise summary
12
+ for the parent agent. Keeps the parent's context clean.
@@ -37,7 +37,7 @@ module RubynCode
37
37
  tool_defs = build_tool_definitions
38
38
 
39
39
  iteration = 0
40
- final_text = ""
40
+ final_text = ''
41
41
 
42
42
  loop do
43
43
  break if iteration >= @max_iterations
@@ -53,10 +53,10 @@ module RubynCode
53
53
  break
54
54
  end
55
55
 
56
- conversation << { role: "assistant", content: response }
56
+ conversation << { role: 'assistant', content: response }
57
57
 
58
58
  tool_results = execute_tools(executor, tool_calls)
59
- conversation << { role: "user", content: tool_results }
59
+ conversation << { role: 'user', content: tool_results }
60
60
 
61
61
  final_text = text_content unless text_content.empty?
62
62
  end
@@ -68,7 +68,7 @@ module RubynCode
68
68
 
69
69
  def build_conversation
70
70
  [
71
- { role: "user", content: @prompt }
71
+ { role: 'user', content: @prompt }
72
72
  ]
73
73
  end
74
74
 
@@ -108,7 +108,7 @@ module RubynCode
108
108
  when String
109
109
  response
110
110
  when Hash
111
- content = response[:content] || response["content"]
111
+ content = response[:content] || response['content']
112
112
  extract_text_from_content(content)
113
113
  when Array
114
114
  extract_text_from_content(response)
@@ -121,43 +121,43 @@ module RubynCode
121
121
  return content.to_s unless content.is_a?(Array)
122
122
 
123
123
  content
124
- .select { |block| block_type(block) == "text" }
125
- .map { |block| block[:text] || block["text"] }
124
+ .select { |block| block_type(block) == 'text' }
125
+ .map { |block| block[:text] || block['text'] }
126
126
  .compact
127
127
  .join("\n")
128
128
  end
129
129
 
130
130
  def extract_tool_calls(response)
131
131
  content = case response
132
- when Hash then response[:content] || response["content"]
132
+ when Hash then response[:content] || response['content']
133
133
  when Array then response
134
134
  else return []
135
135
  end
136
136
 
137
137
  return [] unless content.is_a?(Array)
138
138
 
139
- content.select { |block| block_type(block) == "tool_use" }
139
+ content.select { |block| block_type(block) == 'tool_use' }
140
140
  end
141
141
 
142
142
  def block_type(block)
143
- (block[:type] || block["type"]).to_s
143
+ (block[:type] || block['type']).to_s
144
144
  end
145
145
 
146
146
  def execute_tools(executor, tool_calls)
147
147
  tool_calls.map do |call|
148
- tool_name = call[:name] || call["name"]
149
- tool_input = call[:input] || call["input"] || {}
150
- tool_id = call[:id] || call["id"]
148
+ tool_name = call[:name] || call['name']
149
+ tool_input = call[:input] || call['input'] || {}
150
+ tool_id = call[:id] || call['id']
151
151
 
152
152
  # Prevent recursive sub-agent spawning
153
153
  result = if SUB_AGENT_TOOLS.include?(tool_name)
154
- "Error: Sub-agents cannot spawn other sub-agents."
154
+ 'Error: Sub-agents cannot spawn other sub-agents.'
155
155
  else
156
156
  executor.execute(tool_name, tool_input)
157
157
  end
158
158
 
159
159
  {
160
- type: "tool_result",
160
+ type: 'tool_result',
161
161
  tool_use_id: tool_id,
162
162
  content: result.to_s
163
163
  }
@@ -8,7 +8,7 @@ module RubynCode
8
8
 
9
9
  class << self
10
10
  def call(text, max_length: DEFAULT_MAX_LENGTH)
11
- return "" if text.nil? || text.empty?
11
+ return '' if text.nil? || text.empty?
12
12
 
13
13
  text = text.to_s.strip
14
14
 
@@ -0,0 +1,13 @@
1
+ # Layer 7: Tasks
2
+
3
+ Task tracking with DAG-based dependency management.
4
+
5
+ ## Classes
6
+
7
+ - **`Manager`** — CRUD operations for tasks. Persists to the `tasks` SQLite table.
8
+ Supports status tracking, priority, ownership, and dependency resolution.
9
+
10
+ - **`DAG`** — Directed acyclic graph for task dependencies. Determines which tasks are
11
+ ready to run (all dependencies met), detects cycles, and computes execution order.
12
+
13
+ - **`Models`** — Data objects for tasks and dependencies. Maps to/from SQLite rows.
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "set"
4
-
5
3
  module RubynCode
6
4
  module Tasks
7
5
  # Directed acyclic graph tracking task dependencies.
@@ -24,13 +22,13 @@ module RubynCode
24
22
  # @raise [ArgumentError] if this would create a cycle
25
23
  # @return [void]
26
24
  def add_dependency(task_id, depends_on_id)
27
- raise ArgumentError, "A task cannot depend on itself" if task_id == depends_on_id
28
- raise ArgumentError, "Cycle detected" if reachable?(depends_on_id, task_id)
25
+ raise ArgumentError, 'A task cannot depend on itself' if task_id == depends_on_id
26
+ raise ArgumentError, 'Cycle detected' if reachable?(depends_on_id, task_id)
29
27
 
30
28
  return if @forward[task_id].include?(depends_on_id)
31
29
 
32
30
  @db.execute(
33
- "INSERT OR IGNORE INTO task_dependencies (task_id, depends_on_id) VALUES (?, ?)",
31
+ 'INSERT OR IGNORE INTO task_dependencies (task_id, depends_on_id) VALUES (?, ?)',
34
32
  [task_id, depends_on_id]
35
33
  )
36
34
  @forward[task_id].add(depends_on_id)
@@ -44,7 +42,7 @@ module RubynCode
44
42
  # @return [void]
45
43
  def remove_dependency(task_id, depends_on_id)
46
44
  @db.execute(
47
- "DELETE FROM task_dependencies WHERE task_id = ? AND depends_on_id = ?",
45
+ 'DELETE FROM task_dependencies WHERE task_id = ? AND depends_on_id = ?',
48
46
  [task_id, depends_on_id]
49
47
  )
50
48
  @forward[task_id].delete(depends_on_id)
@@ -94,11 +92,11 @@ module RubynCode
94
92
  dependents_of(completed_task_id).each do |dep_id|
95
93
  next if blocked?(dep_id)
96
94
 
97
- rows = @db.query("SELECT status FROM tasks WHERE id = ?", [dep_id]).to_a
95
+ rows = @db.query('SELECT status FROM tasks WHERE id = ?', [dep_id]).to_a
98
96
  next if rows.empty?
99
97
 
100
- current_status = rows.first["status"]
101
- next unless current_status == "blocked"
98
+ current_status = rows.first['status']
99
+ next unless current_status == 'blocked'
102
100
 
103
101
  @db.execute(
104
102
  "UPDATE tasks SET status = 'pending', updated_at = datetime('now') WHERE id = ?",
@@ -151,9 +149,7 @@ module RubynCode
151
149
  end
152
150
  end
153
151
 
154
- if sorted.size != all_nodes.size
155
- raise "Cycle detected in task dependency graph"
156
- end
152
+ raise 'Cycle detected in task dependency graph' if sorted.size != all_nodes.size
157
153
 
158
154
  sorted
159
155
  end
@@ -173,10 +169,10 @@ module RubynCode
173
169
  end
174
170
 
175
171
  def load_from_db
176
- rows = @db.query("SELECT task_id, depends_on_id FROM task_dependencies").to_a
172
+ rows = @db.query('SELECT task_id, depends_on_id FROM task_dependencies').to_a
177
173
  rows.each do |row|
178
- tid = row["task_id"]
179
- did = row["depends_on_id"]
174
+ tid = row['task_id']
175
+ did = row['depends_on_id']
180
176
  @forward[tid].add(did)
181
177
  @reverse[did].add(tid)
182
178
  end
@@ -201,7 +197,7 @@ module RubynCode
201
197
  end
202
198
 
203
199
  def placeholders(count)
204
- (["?"] * count).join(", ")
200
+ (['?'] * count).join(', ')
205
201
  end
206
202
  end
207
203
  end