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.
- checksums.yaml +4 -4
- data/README.md +269 -467
- data/db/migrations/009_create_teams.sql +6 -6
- data/db/migrations/011_fix_mailbox_messages_columns.rb +35 -0
- data/db/migrations/012_expand_mailbox_message_types.rb +37 -0
- data/exe/rubyn-code +1 -1
- data/lib/rubyn_code/agent/RUBYN.md +17 -0
- data/lib/rubyn_code/agent/conversation.rb +68 -19
- data/lib/rubyn_code/agent/loop.rb +312 -54
- data/lib/rubyn_code/agent/loop_detector.rb +6 -6
- data/lib/rubyn_code/auth/RUBYN.md +19 -0
- data/lib/rubyn_code/auth/oauth.rb +40 -35
- data/lib/rubyn_code/auth/server.rb +16 -12
- data/lib/rubyn_code/auth/token_store.rb +22 -22
- data/lib/rubyn_code/autonomous/RUBYN.md +14 -0
- data/lib/rubyn_code/autonomous/daemon.rb +115 -79
- data/lib/rubyn_code/autonomous/idle_poller.rb +4 -8
- data/lib/rubyn_code/autonomous/task_claimer.rb +11 -11
- data/lib/rubyn_code/background/RUBYN.md +13 -0
- data/lib/rubyn_code/background/notifier.rb +0 -2
- data/lib/rubyn_code/background/worker.rb +60 -15
- data/lib/rubyn_code/cli/RUBYN.md +30 -0
- data/lib/rubyn_code/cli/app.rb +85 -9
- data/lib/rubyn_code/cli/commands/RUBYN.md +133 -0
- data/lib/rubyn_code/cli/commands/base.rb +53 -0
- data/lib/rubyn_code/cli/commands/budget.rb +24 -0
- data/lib/rubyn_code/cli/commands/clear.rb +16 -0
- data/lib/rubyn_code/cli/commands/compact.rb +21 -0
- data/lib/rubyn_code/cli/commands/context.rb +44 -0
- data/lib/rubyn_code/cli/commands/context_info.rb +56 -0
- data/lib/rubyn_code/cli/commands/cost.rb +23 -0
- data/lib/rubyn_code/cli/commands/diff.rb +30 -0
- data/lib/rubyn_code/cli/commands/doctor.rb +112 -0
- data/lib/rubyn_code/cli/commands/help.rb +41 -0
- data/lib/rubyn_code/cli/commands/model.rb +37 -0
- data/lib/rubyn_code/cli/commands/plan.rb +22 -0
- data/lib/rubyn_code/cli/commands/quit.rb +17 -0
- data/lib/rubyn_code/cli/commands/registry.rb +64 -0
- data/lib/rubyn_code/cli/commands/resume.rb +51 -0
- data/lib/rubyn_code/cli/commands/review.rb +26 -0
- data/lib/rubyn_code/cli/commands/skill.rb +32 -0
- data/lib/rubyn_code/cli/commands/spawn.rb +24 -0
- data/lib/rubyn_code/cli/commands/tasks.rb +32 -0
- data/lib/rubyn_code/cli/commands/tokens.rb +76 -0
- data/lib/rubyn_code/cli/commands/undo.rb +17 -0
- data/lib/rubyn_code/cli/commands/version.rb +16 -0
- data/lib/rubyn_code/cli/daemon_runner.rb +129 -0
- data/lib/rubyn_code/cli/input_handler.rb +20 -23
- data/lib/rubyn_code/cli/renderer.rb +25 -27
- data/lib/rubyn_code/cli/repl.rb +161 -194
- data/lib/rubyn_code/cli/setup.rb +117 -0
- data/lib/rubyn_code/cli/spinner.rb +40 -40
- data/lib/rubyn_code/cli/stream_formatter.rb +29 -28
- data/lib/rubyn_code/cli/version_check.rb +94 -0
- data/lib/rubyn_code/config/RUBYN.md +14 -0
- data/lib/rubyn_code/config/defaults.rb +28 -19
- data/lib/rubyn_code/config/project_config.rb +7 -9
- data/lib/rubyn_code/config/settings.rb +3 -3
- data/lib/rubyn_code/context/RUBYN.md +20 -0
- data/lib/rubyn_code/context/auto_compact.rb +7 -7
- data/lib/rubyn_code/context/compactor.rb +2 -2
- data/lib/rubyn_code/context/context_collapse.rb +45 -0
- data/lib/rubyn_code/context/manager.rb +20 -3
- data/lib/rubyn_code/context/manual_compact.rb +7 -7
- data/lib/rubyn_code/context/micro_compact.rb +12 -12
- data/lib/rubyn_code/db/RUBYN.md +40 -0
- data/lib/rubyn_code/db/connection.rb +13 -13
- data/lib/rubyn_code/db/migrator.rb +67 -27
- data/lib/rubyn_code/db/schema.rb +6 -6
- data/lib/rubyn_code/debug.rb +74 -0
- data/lib/rubyn_code/hooks/RUBYN.md +17 -0
- data/lib/rubyn_code/hooks/built_in.rb +9 -9
- data/lib/rubyn_code/hooks/registry.rb +5 -5
- data/lib/rubyn_code/hooks/runner.rb +1 -1
- data/lib/rubyn_code/hooks/user_hooks.rb +16 -16
- data/lib/rubyn_code/learning/RUBYN.md +16 -0
- data/lib/rubyn_code/learning/extractor.rb +22 -22
- data/lib/rubyn_code/learning/injector.rb +17 -18
- data/lib/rubyn_code/learning/instinct.rb +18 -14
- data/lib/rubyn_code/llm/RUBYN.md +15 -0
- data/lib/rubyn_code/llm/client.rb +121 -55
- data/lib/rubyn_code/llm/message_builder.rb +19 -15
- data/lib/rubyn_code/llm/streaming.rb +80 -50
- data/lib/rubyn_code/mcp/RUBYN.md +21 -0
- data/lib/rubyn_code/mcp/client.rb +25 -24
- data/lib/rubyn_code/mcp/config.rb +7 -7
- data/lib/rubyn_code/mcp/sse_transport.rb +27 -26
- data/lib/rubyn_code/mcp/stdio_transport.rb +22 -19
- data/lib/rubyn_code/mcp/tool_bridge.rb +32 -32
- data/lib/rubyn_code/memory/RUBYN.md +17 -0
- data/lib/rubyn_code/memory/models.rb +3 -3
- data/lib/rubyn_code/memory/search.rb +17 -17
- data/lib/rubyn_code/memory/session_persistence.rb +49 -34
- data/lib/rubyn_code/memory/store.rb +17 -17
- data/lib/rubyn_code/observability/RUBYN.md +19 -0
- data/lib/rubyn_code/observability/budget_enforcer.rb +16 -15
- data/lib/rubyn_code/observability/cost_calculator.rb +3 -3
- data/lib/rubyn_code/observability/token_counter.rb +1 -1
- data/lib/rubyn_code/observability/usage_reporter.rb +35 -35
- data/lib/rubyn_code/output/RUBYN.md +11 -0
- data/lib/rubyn_code/output/diff_renderer.rb +6 -6
- data/lib/rubyn_code/output/formatter.rb +4 -4
- data/lib/rubyn_code/permissions/RUBYN.md +17 -0
- data/lib/rubyn_code/permissions/prompter.rb +8 -8
- data/lib/rubyn_code/protocols/RUBYN.md +14 -0
- data/lib/rubyn_code/protocols/interrupt_handler.rb +1 -1
- data/lib/rubyn_code/protocols/plan_approval.rb +9 -9
- data/lib/rubyn_code/protocols/shutdown_handshake.rb +9 -11
- data/lib/rubyn_code/skills/RUBYN.md +19 -0
- data/lib/rubyn_code/skills/catalog.rb +7 -7
- data/lib/rubyn_code/skills/document.rb +15 -15
- data/lib/rubyn_code/skills/loader.rb +6 -8
- data/lib/rubyn_code/sub_agents/RUBYN.md +12 -0
- data/lib/rubyn_code/sub_agents/runner.rb +15 -15
- data/lib/rubyn_code/sub_agents/summarizer.rb +1 -1
- data/lib/rubyn_code/tasks/RUBYN.md +13 -0
- data/lib/rubyn_code/tasks/dag.rb +12 -16
- data/lib/rubyn_code/tasks/manager.rb +24 -24
- data/lib/rubyn_code/tasks/models.rb +4 -4
- data/lib/rubyn_code/teams/RUBYN.md +14 -0
- data/lib/rubyn_code/teams/mailbox.rb +38 -18
- data/lib/rubyn_code/teams/manager.rb +19 -19
- data/lib/rubyn_code/teams/teammate.rb +3 -4
- data/lib/rubyn_code/tools/RUBYN.md +38 -0
- data/lib/rubyn_code/tools/background_run.rb +9 -11
- data/lib/rubyn_code/tools/base.rb +54 -3
- data/lib/rubyn_code/tools/bash.rb +16 -34
- data/lib/rubyn_code/tools/bundle_add.rb +10 -12
- data/lib/rubyn_code/tools/bundle_install.rb +9 -11
- data/lib/rubyn_code/tools/compact.rb +10 -9
- data/lib/rubyn_code/tools/db_migrate.rb +17 -15
- data/lib/rubyn_code/tools/edit_file.rb +12 -12
- data/lib/rubyn_code/tools/executor.rb +9 -4
- data/lib/rubyn_code/tools/git_commit.rb +29 -34
- data/lib/rubyn_code/tools/git_diff.rb +17 -18
- data/lib/rubyn_code/tools/git_log.rb +17 -19
- data/lib/rubyn_code/tools/git_status.rb +18 -20
- data/lib/rubyn_code/tools/glob.rb +7 -9
- data/lib/rubyn_code/tools/grep.rb +11 -9
- data/lib/rubyn_code/tools/load_skill.rb +7 -7
- data/lib/rubyn_code/tools/memory_search.rb +13 -12
- data/lib/rubyn_code/tools/memory_write.rb +14 -12
- data/lib/rubyn_code/tools/rails_generate.rb +16 -16
- data/lib/rubyn_code/tools/read_file.rb +8 -7
- data/lib/rubyn_code/tools/read_inbox.rb +5 -5
- data/lib/rubyn_code/tools/registry.rb +2 -2
- data/lib/rubyn_code/tools/review_pr.rb +55 -55
- data/lib/rubyn_code/tools/run_specs.rb +20 -19
- data/lib/rubyn_code/tools/schema.rb +9 -11
- data/lib/rubyn_code/tools/send_message.rb +10 -10
- data/lib/rubyn_code/tools/spawn_agent.rb +51 -23
- data/lib/rubyn_code/tools/spawn_teammate.rb +21 -21
- data/lib/rubyn_code/tools/task.rb +28 -28
- data/lib/rubyn_code/tools/web_fetch.rb +46 -31
- data/lib/rubyn_code/tools/web_search.rb +64 -66
- data/lib/rubyn_code/tools/write_file.rb +7 -6
- data/lib/rubyn_code/version.rb +1 -1
- data/lib/rubyn_code.rb +136 -105
- metadata +94 -21
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
5
|
-
require
|
|
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(
|
|
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(
|
|
40
|
-
$stdout.puts pastel.red(
|
|
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(
|
|
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 ==
|
|
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.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
5
|
-
require
|
|
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(
|
|
29
|
-
$stdout.puts pastel.cyan(
|
|
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(
|
|
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(
|
|
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(
|
|
45
|
+
$stdout.puts pastel.green('Plan approved.')
|
|
46
46
|
APPROVED
|
|
47
47
|
else
|
|
48
|
-
$stdout.puts pastel.red(
|
|
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:
|
|
26
|
-
message_type:
|
|
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] ==
|
|
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 ?
|
|
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:
|
|
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:
|
|
82
|
+
to: '_system',
|
|
85
83
|
content: "#{agent_name} is now offline",
|
|
86
|
-
message_type:
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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)).
|
|
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:
|
|
51
|
-
.encode(
|
|
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 ==
|
|
55
|
-
File.basename(path,
|
|
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
|
|
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[
|
|
29
|
-
description: frontmatter[
|
|
30
|
-
tags: Array(frontmatter[
|
|
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,
|
|
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:
|
|
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?(
|
|
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]+/,
|
|
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 <<
|
|
70
|
-
tags <<
|
|
71
|
-
tags <<
|
|
72
|
-
tags <<
|
|
73
|
-
tags <<
|
|
74
|
-
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)
|
|
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 <<
|
|
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(
|
|
51
|
-
.gsub(
|
|
52
|
-
.gsub(
|
|
53
|
-
.gsub("
|
|
48
|
+
.gsub('&', '&')
|
|
49
|
+
.gsub('<', '<')
|
|
50
|
+
.gsub('>', '>')
|
|
51
|
+
.gsub('"', '"')
|
|
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:
|
|
56
|
+
conversation << { role: 'assistant', content: response }
|
|
57
57
|
|
|
58
58
|
tool_results = execute_tools(executor, tool_calls)
|
|
59
|
-
conversation << { role:
|
|
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:
|
|
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[
|
|
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) ==
|
|
125
|
-
.map { |block| block[:text] || block[
|
|
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[
|
|
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) ==
|
|
139
|
+
content.select { |block| block_type(block) == 'tool_use' }
|
|
140
140
|
end
|
|
141
141
|
|
|
142
142
|
def block_type(block)
|
|
143
|
-
(block[:type] || block[
|
|
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[
|
|
149
|
-
tool_input = call[:input] || call[
|
|
150
|
-
tool_id = call[:id] || call[
|
|
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
|
-
|
|
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:
|
|
160
|
+
type: 'tool_result',
|
|
161
161
|
tool_use_id: tool_id,
|
|
162
162
|
content: result.to_s
|
|
163
163
|
}
|
|
@@ -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.
|
data/lib/rubyn_code/tasks/dag.rb
CHANGED
|
@@ -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,
|
|
28
|
-
raise ArgumentError,
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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[
|
|
101
|
-
next unless current_status ==
|
|
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(
|
|
172
|
+
rows = @db.query('SELECT task_id, depends_on_id FROM task_dependencies').to_a
|
|
177
173
|
rows.each do |row|
|
|
178
|
-
tid = row[
|
|
179
|
-
did = row[
|
|
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
|
-
([
|
|
200
|
+
(['?'] * count).join(', ')
|
|
205
201
|
end
|
|
206
202
|
end
|
|
207
203
|
end
|