Comparing changes

v0.1.1 v0.2.0
11 commits 26 files changed
lib/elelem/tools/compact.rb
@@ -1,10 +1,12 @@
 # frozen_string_literal: true
 
-Elelem::Plugins.register(:compact) do |agent|
-  agent.commands.register("compact", description: "Compress context") do
-    response = agent.turn("Summarize: accomplishments, state, next steps. Brief.")
-    agent.conversation.clear!
-    agent.conversation.add(role: "user", content: "Context: #{response}")
-    agent.terminal.say "  → compacted"
+Elelem.configure do |config|
+  config.setup(:compact) do |agent|
+    agent.commands.register("compact", description: "Compress context") do
+      response = agent.turn("Summarize: accomplishments, state, next steps. Brief.")
+      agent.conversation.clear!
+      agent.conversation.add(role: "user", content: "Context: #{response}")
+      agent.output.say "  → compacted"
+    end
   end
 end
lib/elelem/tools/confirm.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+Elelem.configure do |config|
+  config.setup(:confirm) do |agent|
+    permissions = Elelem::Tools::Permissions.new
+
+    agent.toolbox.before do |args, tool_name:|
+      permissions.check(tool_name, args, input: agent.input)
+    end
+  end
+end
lib/elelem/tools/edit.rb
@@ -1,15 +1,17 @@
 # frozen_string_literal: true
 
-Elelem::Plugins.register(:edit) do |agent|
-  agent.toolbox.add("edit",
-    description: "Replace first occurrence of text in file",
-    params: { path: { type: "string" }, old: { type: "string" }, new: { type: "string" } },
-    required: ["path", "old", "new"]
-  ) do |args|
-    path = Pathname.new(args["path"]).expand_path
-    content = path.read
-    agent.toolbox
-      .run("write", { "path" => args["path"], "content" => content.sub(args["old"], args["new"]) })
-      .merge(replaced: args["old"], with: args["new"])
+Elelem.configure do |config|
+  config.setup(:edit) do |agent|
+    agent.toolbox.add("edit",
+      description: "Replace first occurrence of text in file",
+      params: { path: { type: "string" }, old: { type: "string" }, new: { type: "string" } },
+      required: ["path", "old", "new"]
+    ) do |args|
+      path = Pathname.new(args["path"]).expand_path
+      content = path.read
+      agent.toolbox
+        .run("write", { "path" => args["path"], "content" => content.sub(args["old"], args["new"]) })
+        .merge(replaced: args["old"], with: args["new"])
+    end
   end
 end
lib/elelem/tools/eval.rb
@@ -1,20 +1,22 @@
 # frozen_string_literal: true
 
-Elelem::Plugins.register(:eval) do |agent|
-  description = <<~'DESC'
-    Evaluate Ruby code. Available API:
+Elelem.configure do |config|
+  config.setup(:eval) do |agent|
+    description = <<~'DESC'
+      Evaluate Ruby code. Available API:
 
-    name = "search"
-    agent.toolbox.add(name, description: "Search using rg", params: { query: { type: "string" } }, required: ["query"], aliases: []) do |args|
-      agent.toolbox.run("execute", { "command" => "rg --json -nI -F #{args["query"]}" })
-    end
-  DESC
+      name = "search"
+      agent.toolbox.add(name, description: "Search using rg", params: { query: { type: "string" } }, required: ["query"], aliases: []) do |args|
+        agent.toolbox.run("execute", { "command" => "rg --json -nI -F #{args["query"]}" })
+      end
+    DESC
 
-  agent.toolbox.add("eval",
-    description: description,
-    params: { ruby: { type: "string" } },
-    required: ["ruby"]
-  ) do |args|
-    { result: binding.eval(args["ruby"]) }
+    agent.toolbox.add("eval",
+      description: description,
+      params: { ruby: { type: "string" } },
+      required: ["ruby"]
+    ) do |args|
+      { result: binding.eval(args["ruby"]) }
+    end
   end
 end
lib/elelem/tools/exec.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module Elelem
+  module Tools
+    def self.exec(toolbox, *args)
+      command = args.flatten.map { |a| Shellwords.escape(a.to_s) }.join(" ")
+      toolbox.run("execute", { "command" => command })
+    end
+  end
+end
lib/elelem/tools/fork.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Elelem
+  module Tools
+    def self.fork(agent, system_prompt:)
+      Elelem::Agent.new(agent.provider, toolbox: agent.toolbox, output: agent.output, input: agent.input, system_prompt: system_prompt)
+    end
+  end
+end
lib/elelem/tools/git.rb
@@ -1,15 +1,17 @@
 # frozen_string_literal: true
 
-Elelem::Plugins.register(:git) do |agent|
-  agent.toolbox.add("git",
-    description: "Run git command",
-    params: { command: { type: "string" }, args: { type: "array", items: { type: "string" } } },
-    required: ["command"]
-  ) do |args|
-    agent.toolbox.exec("git", args["command"], *(args["args"] || []))
-  end
+Elelem.configure do |config|
+  config.setup(:git) do |agent|
+    agent.toolbox.add("git",
+      description: "Run git command",
+      params: { command: { type: "string" }, args: { type: "array", items: { type: "string" } } },
+      required: ["command"]
+    ) do |args|
+      Elelem::Tools.exec(agent.toolbox, "git", args["command"], *(args["args"] || []))
+    end
 
-  agent.toolbox.after("git") do |_, result|
-    agent.terminal.say "  ! #{result[:error]}" if result[:error]
+    agent.toolbox.after("git") do |_, result|
+      agent.output.say "  ! #{result[:error]}" if result[:error]
+    end
   end
 end
lib/elelem/tools/glob.rb
@@ -1,13 +1,15 @@
 # frozen_string_literal: true
 
-Elelem::Plugins.register(:glob) do |agent|
-  agent.toolbox.add("glob",
-    description: "Find files matching pattern",
-    params: { pattern: { type: "string" }, path: { type: "string" } },
-    required: ["pattern"]
-  ) do |args|
-    path = args["path"].to_s.empty? ? "." : args["path"]
-    result = agent.toolbox.exec("fd", "--glob", args["pattern"], path)
-    result[:ok] ? result : agent.toolbox.exec("find", path, "-name", args["pattern"])
+Elelem.configure do |config|
+  config.setup(:glob) do |agent|
+    agent.toolbox.add("glob",
+      description: "Find files matching pattern",
+      params: { pattern: { type: "string" }, path: { type: "string" } },
+      required: ["pattern"]
+    ) do |args|
+      path = args["path"].to_s.empty? ? "." : args["path"]
+      result = Elelem::Tools.exec(agent.toolbox, "fd", "--glob", args["pattern"], path)
+      result[:ok] ? result : Elelem::Tools.exec(agent.toolbox, "find", path, "-name", args["pattern"])
+    end
   end
 end
lib/elelem/tools/grep.rb
@@ -1,22 +1,24 @@
 # frozen_string_literal: true
 
-Elelem::Plugins.register(:grep) do |agent|
-  agent.toolbox.add("grep",
-    description: "Search file contents",
-    params: { pattern: { type: "string" }, path: { type: "string" }, glob: { type: "string" } },
-    required: ["pattern"]
-  ) do |args|
-    path = args["path"].to_s.empty? ? "." : args["path"]
-    glob = args["glob"]
+Elelem.configure do |config|
+  config.setup(:grep) do |agent|
+    agent.toolbox.add("grep",
+      description: "Search file contents",
+      params: { pattern: { type: "string" }, path: { type: "string" }, glob: { type: "string" } },
+      required: ["pattern"]
+    ) do |args|
+      path = args["path"].to_s.empty? ? "." : args["path"]
+      glob = args["glob"]
 
-    rg_args = ["rg", "-n", args["pattern"], path]
-    rg_args += ["-g", glob] if glob
-    result = agent.toolbox.exec(*rg_args)
-    next result if result[:ok]
+      rg_args = ["rg", "-n", args["pattern"], path]
+      rg_args += ["-g", glob] if glob
+      result = Elelem::Tools.exec(agent.toolbox, *rg_args)
+      next result if result[:ok]
 
-    grep_args = ["grep", "-rn"]
-    grep_args += ["--include", glob] if glob
-    grep_args += [args["pattern"], path]
-    agent.toolbox.exec(*grep_args)
+      grep_args = ["grep", "-rn"]
+      grep_args += ["--include", glob] if glob
+      grep_args += [args["pattern"], path]
+      Elelem::Tools.exec(agent.toolbox, *grep_args)
+    end
   end
 end
lib/elelem/tools/init.rb
@@ -1,29 +1,31 @@
 # frozen_string_literal: true
 
-Elelem::Plugins.register(:init) do |agent|
-  agent.commands.register("init", description: "Generate AGENTS.md") do
-    system_prompt = <<~PROMPT
-      AGENTS.md generator. Analyze codebase and write AGENTS.md to project root.
+Elelem.configure do |config|
+  config.setup(:init) do |agent|
+    agent.commands.register("init", description: "Generate AGENTS.md") do
+      system_prompt = <<~PROMPT
+        AGENTS.md generator. Analyze codebase and write AGENTS.md to project root.
 
-      # AGENTS.md Spec (https://agents.md/)
-      A file providing context and instructions for AI coding agents.
+        # AGENTS.md Spec (https://agents.md/)
+        A file providing context and instructions for AI coding agents.
 
-      ## Recommended Sections
-      - Commands: build, test, lint commands
-      - Code Style: conventions, patterns
-      - Architecture: key components and flow
-      - Testing: how to run tests
+        ## Recommended Sections
+        - Commands: build, test, lint commands
+        - Code Style: conventions, patterns
+        - Architecture: key components and flow
+        - Testing: how to run tests
 
-      ## Process
-      1. Read README.md if present
-      2. Identify language (Gemfile, package.json, go.mod)
-      3. Find test scripts (bin/test, npm test)
-      4. Check linter configs
-      5. Write concise AGENTS.md
+        ## Process
+        1. Read README.md if present
+        2. Identify language (Gemfile, package.json, go.mod)
+        3. Find test scripts (bin/test, npm test)
+        4. Check linter configs
+        5. Write concise AGENTS.md
 
-      Keep it minimal. No fluff.
-    PROMPT
+        Keep it minimal. No fluff.
+      PROMPT
 
-    agent.fork(system_prompt: system_prompt).turn("Generate AGENTS.md for this project")
+      Elelem::Tools.fork(agent, system_prompt: system_prompt).turn("Generate AGENTS.md for this project")
+    end
   end
 end
lib/elelem/tools/interview.rb
@@ -1,14 +1,16 @@
 # frozen_string_literal: true
 
-Elelem::Plugins.register(:interview) do |agent|
-  agent.toolbox.add("interview",
-    description: "Ask the user a question and wait for their response",
-    params: {
-      question: { type: "string", description: "The question to ask the user" },
-    },
-    required: ["question"]
-  ) do |args|
-    agent.terminal.say(agent.terminal.markdown(args["question"]))
-    { answer: agent.terminal.ask("> ") }
+Elelem.configure do |config|
+  config.setup(:interview) do |agent|
+    agent.toolbox.add("interview",
+      description: "Ask the user a question and wait for their response",
+      params: {
+        question: { type: "string", description: "The question to ask the user" },
+      },
+      required: ["question"]
+    ) do |args|
+      agent.output.say(args["question"], as: :markdown)
+      { answer: agent.input.ask("> ") }
+    end
   end
 end
lib/elelem/tools/list.rb
@@ -1,14 +1,16 @@
 # frozen_string_literal: true
 
-Elelem::Plugins.register(:list) do |agent|
-  agent.toolbox.add("list",
-    description: "List directory contents",
-    params: { path: { type: "string" }, recursive: { type: "boolean" } },
-    required: [],
-    aliases: ["ls"]
-  ) do |args|
-    path = args["path"] && !args["path"].empty? ? args["path"] : "."
-    flags = args["recursive"] ? "-laR" : "-la"
-    agent.toolbox.exec("ls", flags, path)
+Elelem.configure do |config|
+  config.setup(:list) do |agent|
+    agent.toolbox.add("list",
+      description: "List directory contents",
+      params: { path: { type: "string" }, recursive: { type: "boolean" } },
+      required: [],
+      aliases: ["ls"]
+    ) do |args|
+      path = args["path"] && !args["path"].empty? ? args["path"] : "."
+      flags = args["recursive"] ? "-laR" : "-la"
+      Elelem::Tools.exec(agent.toolbox, "ls", flags, path)
+    end
   end
 end
lib/elelem/tools/permissions.json
@@ -0,0 +1,5 @@
+{
+  "execute": "ask",
+  "read": "allow",
+  "write": "allow"
+}
lib/elelem/tools/permissions.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+module Elelem
+  module Tools
+    class Permissions
+      LOAD_PATHS = [
+        File.expand_path("permissions.json", __dir__),
+        "~/.agents/permissions.json",
+        ".agents/permissions.json"
+      ].freeze
+
+      def initialize(rules: default_rules)
+        @rules = rules
+      end
+
+      def check(tool_name, args, input:)
+        policy = @rules[tool_name.to_sym] || :ask
+        case policy
+        when :allow then true
+        when :deny then raise "Permission denied: #{tool_name}"
+        when :ask then prompt(tool_name, args, input)
+        end
+      end
+
+      private
+
+      def load_config(path)
+        return {} unless File.exist?(path)
+
+        JSON.parse(File.read(path)).transform_keys(&:to_sym).transform_values(&:to_sym)
+      rescue JSON::ParserError
+        {}
+      end
+
+      def prompt(tool_name, args, input)
+        return true unless input.interactive?
+
+        answer = input.ask("  Allow? [Y/n] > ")&.downcase
+        raise "User denied permission: #{tool_name}" if answer == "n"
+
+        true
+      end
+
+      def default_rules
+        LOAD_PATHS.reduce({}) do |rules, path|
+          rules.merge(load_config(File.expand_path(path)))
+        end
+      end
+    end
+  end
+end
lib/elelem/tools/shell.rb
@@ -1,23 +1,25 @@
 # frozen_string_literal: true
 
-Elelem::Plugins.register(:shell) do |agent|
-  strip_ansi = ->(text) do
-    text
-      .gsub(/^Script started.*?\n/, "")
-      .gsub(/\nScript done.*$/, "")
-      .gsub(/\e\].*?(?:\a|\e\\)/, "")
-      .gsub(/\e\[[0-9;?]*[A-Za-z]/, "")
-      .gsub(/\e[PX^_].*?\e\\/, "")
-      .gsub(/\e./, "")
-      .gsub(/[\b]/, "")
-      .gsub(/\r/, "")
-  end
+Elelem.configure do |config|
+  config.setup(:shell) do |agent|
+    strip_ansi = ->(text) do
+      text
+        .gsub(/^Script started.*?\n/, "")
+        .gsub(/\nScript done.*$/, "")
+        .gsub(/\e\].*?(?:\a|\e\\)/, "")
+        .gsub(/\e\[[0-9;?]*[A-Za-z]/, "")
+        .gsub(/\e[PX^_].*?\e\\/, "")
+        .gsub(/\e./, "")
+        .gsub(/[\b]/, "")
+        .gsub(/\r/, "")
+    end
 
-  agent.commands.register("shell", description: "Start interactive shell") do
-    transcript = Tempfile.create do |file|
-      system("script", "-q", file.path, chdir: Dir.pwd)
-      strip_ansi.call(File.read(file.path))
+    agent.commands.register("shell", description: "Start interactive shell") do
+      transcript = Tempfile.create do |file|
+        system("script", "-q", file.path, chdir: Dir.pwd)
+        strip_ansi.call(File.read(file.path))
+      end
+      agent.conversation.add(role: "user", content: transcript) unless transcript.strip.empty?
     end
-    agent.conversation.add(role: "user", content: transcript) unless transcript.strip.empty?
   end
 end
lib/elelem/tools/task.rb
@@ -1,13 +1,15 @@
 # frozen_string_literal: true
 
-Elelem::Plugins.register(:task) do |agent|
-  agent.toolbox.add("task",
-    description: "Delegate subtask to focused agent (complex searches, multi-file analysis)",
-    params: { prompt: { type: "string" } },
-    required: ["prompt"]
-  ) do |args|
-    sub = agent.fork(system_prompt: "Research agent. Search, analyze, report. Be concise.")
-    sub.turn(args["prompt"])
-    { result: sub.conversation.last[:content] }
+Elelem.configure do |config|
+  config.setup(:task) do |agent|
+    agent.toolbox.add("task",
+      description: "Delegate subtask to focused agent (complex searches, multi-file analysis)",
+      params: { prompt: { type: "string" } },
+      required: ["prompt"]
+    ) do |args|
+      sub = Elelem::Tools.fork(agent, system_prompt: "Research agent. Search, analyze, report. Be concise.")
+      sub.turn(args["prompt"])
+      { result: sub.conversation.last[:content] }
+    end
   end
 end
lib/elelem/tools/verify.rb
@@ -1,45 +1,47 @@
 # frozen_string_literal: true
 
-Elelem::Plugins.register(:verify) do |agent|
-  class Verifiers
-    SYNTAX = {
-      ".rb" => "ruby -c %{path}",
-      ".erb" => "erb -x %{path} | ruby -c",
-      ".py" => "python -m py_compile %{path}",
-      ".go" => "go vet %{path}",
-      ".rs" => "cargo check --quiet",
-      ".ts" => "npx tsc --noEmit %{path}",
-      ".js" => "node --check %{path}",
-    }.freeze
+class Verifiers
+  SYNTAX = {
+    ".rb" => "ruby -c %{path}",
+    ".erb" => "erb -x %{path} | ruby -c",
+    ".py" => "python -m py_compile %{path}",
+    ".go" => "go vet %{path}",
+    ".rs" => "cargo check --quiet",
+    ".ts" => "npx tsc --noEmit %{path}",
+    ".js" => "node --check %{path}",
+  }.freeze
 
-    def self.for(path)
-      return [] unless path
+  def self.for(path)
+    return [] unless path
 
-      cmds = []
-      ext = File.extname(path)
-      cmds << (SYNTAX[ext] % { path: path }) if SYNTAX[ext]
-      cmds << test_runner
-      cmds.compact
-    end
+    cmds = []
+    ext = File.extname(path)
+    cmds << (SYNTAX[ext] % { path: path }) if SYNTAX[ext]
+    cmds << test_runner
+    cmds.compact
+  end
 
-    def self.test_runner
-      %w[bin/test script/test].find { |s| File.executable?(s) }
-    end
+  def self.test_runner
+    %w[bin/test script/test].find { |s| File.executable?(s) }
   end
+end
 
-  agent.toolbox.add("verify",
-    description: "Verify file syntax and run tests",
-    params: { path: { type: "string" } },
-    required: ["path"]
-  ) do |args|
-    path = args["path"]
-    Verifiers.for(path).inject({ verified: [] }) do |memo, cmd|
-      agent.terminal.say agent.toolbox.header("execute", { "command" => cmd })
-      v = agent.toolbox.run("execute", { "command" => cmd })
-      break v.merge(path: path, command: cmd) if v[:exit_status] != 0
+Elelem.configure do |config|
+  config.setup(:verify) do |agent|
+    agent.toolbox.add("verify",
+      description: "Verify file syntax and run tests",
+      params: { path: { type: "string" } },
+      required: ["path"]
+    ) do |args|
+      path = args["path"]
+      Verifiers.for(path).inject({ verified: [] }) do |memo, cmd|
+        agent.output.doing("execute", { "command" => cmd })
+        v = agent.toolbox.run("execute", { "command" => cmd })
+        break v.merge(path: path, command: cmd) if v[:exit_status] != 0
 
-      memo[:verified] << cmd
-      memo
+        memo[:verified] << cmd
+        memo
+      end
     end
   end
 end
lib/elelem/tools/version.rb
@@ -2,6 +2,6 @@
 
 module Elelem
   module Tools
-    VERSION = "0.1.1"
+    VERSION = "0.2.0"
   end
 end
lib/elelem/tools.rb
@@ -2,18 +2,23 @@
 
 require "elelem"
 require "pathname"
+require "shellwords"
 require "tempfile"
 
 require_relative "tools/version"
 require_relative "tools/compact"
+require_relative "tools/confirm"
 require_relative "tools/edit"
 require_relative "tools/eval"
+require_relative "tools/exec"
+require_relative "tools/fork"
 require_relative "tools/git"
 require_relative "tools/glob"
 require_relative "tools/grep"
 require_relative "tools/init"
 require_relative "tools/interview"
 require_relative "tools/list"
+require_relative "tools/permissions"
 require_relative "tools/shell"
 require_relative "tools/task"
 require_relative "tools/verify"
spec/elelem/tools/permissions_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+RSpec.describe Elelem::Tools::Permissions do
+  subject { described_class.new }
+
+  let(:input) { double(ask: nil, interactive?: false) }
+
+  describe "#check" do
+    context "with default allow policies" do
+      it "allows read without prompting" do
+        expect(subject.check("read", {}, input: input)).to be true
+        expect(input).not_to have_received(:ask)
+      end
+    end
+
+    context "with deny policy" do
+      subject { described_class.new(rules: { write: :deny }) }
+
+      it { expect { subject.check("write", {}, input: input) }.to raise_error(/Permission denied/) }
+    end
+
+    context "with ask policy on a non-interactive input" do
+      it "returns true without prompting" do
+        expect(subject.check("execute", {}, input: input)).to be true
+        expect(input).not_to have_received(:ask)
+      end
+    end
+
+    context "with ask policy on an interactive input" do
+      let(:input) { double(ask: answer, interactive?: true) }
+
+      context "when approved" do
+        let(:answer) { "y" }
+
+        it "prompts and returns true" do
+          expect(subject.check("execute", {}, input: input)).to be true
+          expect(input).to have_received(:ask)
+        end
+      end
+
+      context "when denied" do
+        let(:answer) { "n" }
+
+        it { expect { subject.check("execute", {}, input: input) }.to raise_error(/User denied permission/) }
+      end
+    end
+  end
+end
spec/spec_helper.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require_relative "../lib/elelem/tools"
+
+RSpec.configure do |config|
+  config.disable_monkey_patching!
+
+  config.expect_with :rspec do |c|
+    c.syntax = :expect
+  end
+end
.rspec
@@ -0,0 +1,1 @@
+--require spec_helper
elelem-tools.gemspec
@@ -25,7 +25,9 @@ Gem::Specification.new do |spec|
   end
   spec.require_paths = ["lib"]
 
-  spec.add_dependency "elelem", "~> 0.10"
+  spec.add_dependency "elelem", "~> 0.11"
+  spec.add_dependency "elelem-builtins", "~> 0.1"
+  spec.add_dependency "json", "~> 3.0"
   spec.add_dependency "pathname", "~> 0.5"
   spec.add_dependency "tempfile", "~> 0.3"
 end
Gemfile
@@ -6,3 +6,4 @@ gemspec name: "elelem-tools"
 
 gem "irb"
 gem "rake", "~> 13.0"
+gem "rspec", "~> 3.0"
Gemfile.lock
@@ -1,40 +1,41 @@
 PATH
   remote: .
   specs:
-    elelem-tools (0.1.1)
-      elelem (~> 0.10)
+    elelem-tools (0.2.0)
+      elelem (~> 0.11)
+      elelem-builtins (~> 0.1)
+      json (~> 3.0)
       pathname (~> 0.5)
       tempfile (~> 0.3)
 
 GEM
   remote: https://rubygems.org/
   specs:
-    base64 (0.3.0)
     bigdecimal (4.1.2)
-    date (3.5.1)
-    digest (3.2.1)
-    elelem (0.10.0)
-      base64 (~> 0.1)
-      date (~> 3.0)
-      digest (~> 3.0)
+    diff-lcs (1.6.2)
+    elelem (0.11.0)
       erb (~> 6.0)
-      fileutils (~> 1.0)
-      json (~> 2.0)
-      json_schemer (~> 2.0)
-      logger (~> 1.0)
-      net-hippie (~> 1.0)
-      open3 (~> 0.1)
-      optparse (~> 0.1)
-      pathname (~> 0.1)
-      reline (~> 0.6)
-      securerandom (~> 0.1)
+      forwardable (~> 1.4)
+      io-console (~> 0.9)
+      json (~> 3.0)
+      json_schemer (~> 2.5)
+      logger (~> 1.7)
+      open3 (~> 0.2)
+      optparse (~> 0.8)
+      pathname (~> 0.5)
+      reline (~> 0.7)
       shellwords (~> 0.2)
-      stringio (~> 3.0)
-      tempfile (~> 0.3)
       uri (~> 1.0)
-      webrick (~> 1.9)
+    elelem-builtins (0.3.1)
+      elelem (~> 0.11)
+      fileutils (~> 1.8)
+      json (~> 3.0)
+      open3 (~> 0.2)
+      stringio (~> 3.1)
+      tempfile (~> 0.3)
     erb (6.0.7)
     fileutils (1.8.0)
+    forwardable (1.4.0)
     hana (1.3.7)
     io-console (0.9.2)
     irb (1.18.0)
@@ -42,27 +43,14 @@ GEM
       prism (>= 1.3.0)
       rdoc (>= 4.0.0)
       reline (>= 0.4.2)
-    json (2.21.2)
+    json (3.0.0)
     json_schemer (2.5.0)
       bigdecimal
       hana (~> 1.3)
       regexp_parser (~> 2.0)
       simpleidn (~> 0.2)
     logger (1.7.0)
-    monitor (0.2.0)
-    net-hippie (1.5.1)
-      base64 (~> 0.1)
-      json (~> 2.0)
-      logger (~> 1.0)
-      monitor (~> 0.1)
-      net-http (~> 0.1)
-      openssl (~> 4.0)
-      resolv (~> 0.1)
-      timeout (~> 0.1)
-    net-http (0.9.1)
-      uri (>= 0.11.1)
     open3 (0.2.1)
-    openssl (4.0.2)
     optparse (0.8.1)
     pathname (0.5.0)
     pp (0.6.4)
@@ -82,16 +70,25 @@ GEM
     regexp_parser (2.12.0)
     reline (0.7.0)
       io-console (~> 0.5)
-    resolv (0.7.2)
-    securerandom (0.4.1)
+    rspec (3.13.2)
+      rspec-core (~> 3.13.0)
+      rspec-expectations (~> 3.13.0)
+      rspec-mocks (~> 3.13.0)
+    rspec-core (3.13.6)
+      rspec-support (~> 3.13.0)
+    rspec-expectations (3.13.5)
+      diff-lcs (>= 1.2.0, < 2.0)
+      rspec-support (~> 3.13.0)
+    rspec-mocks (3.13.8)
+      diff-lcs (>= 1.2.0, < 2.0)
+      rspec-support (~> 3.13.0)
+    rspec-support (3.13.7)
     shellwords (0.2.2)
     simpleidn (0.3.0)
     stringio (3.2.0)
     tempfile (0.3.1)
-    timeout (0.6.1)
     tsort (0.2.0)
     uri (1.1.1)
-    webrick (1.9.2)
 
 PLATFORMS
   ruby
@@ -101,28 +98,25 @@ DEPENDENCIES
   elelem-tools!
   irb
   rake (~> 13.0)
+  rspec (~> 3.0)
 
 CHECKSUMS
-  base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
   bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd
   bundler (4.0.20) sha256=7978a8ac648767f5e635bc522445b79e80a52b907a39a36c2d8085ed6bc762ae
-  date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0
-  digest (3.2.1) sha256=ab3312b4e272d7d5dc41c564c86a25861a1f34ac5153374199a0b74861395947
-  elelem (0.10.0) sha256=de67f3a28351640da471e6e80e7a1d79e83e293cbc502a9e98c3c609cad2e237
-  elelem-tools (0.1.1)
+  diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962
+  elelem (0.11.0) sha256=552cafb092320e3896b8e07572d322fe2b94f0f10d965bf7b993b74517a6f744
+  elelem-builtins (0.3.1) sha256=32d066baaa706896f5aa276d01b17aa38f4e822d0ca43636ed45e7cdc00f008c
+  elelem-tools (0.2.0)
   erb (6.0.7) sha256=c5ca6dc25b0ef974a44dc8f59fe847577122483b1968a38dec305c60bf91ee92
   fileutils (1.8.0) sha256=8c6b1df54e2540bdb2f39258f08af78853aa70bad52b4d394bbc6424593c6e02
+  forwardable (1.4.0) sha256=f1cd40cc9812937980e1c76f1aa053660990a7c9b6a98fc37d945468afcce838
   hana (1.3.7) sha256=5425db42d651fea08859811c29d20446f16af196308162894db208cac5ce9b0d
   io-console (0.9.2) sha256=efa74f891dd03c0939a931dfc6e74c2813d904763d456ea9762b0525e748db08
   irb (1.18.0) sha256=de9454a0703a54704b9811a5ef31a60c86949fbf4013fcf244fabc7c775248e3
-  json (2.21.2) sha256=1f1d3b7cf2b3ba1a69beca0bb6db13d5438b80bff3cd54cdaaa620b9b07c1c6a
+  json (3.0.0) sha256=1ff82a28c05c5cc7b646f3e3a3ac710e4ae2aa933690cba12f1f650b0ecdeb99
   json_schemer (2.5.0) sha256=2f01fb4cce721a4e08dd068fc2030cffd0702a7f333f1ea2be6e8991f00ae396
   logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
-  monitor (0.2.0) sha256=18698584f161ca6611d73130663990ca7dc439d9d88095226e683035d66f1f0f
-  net-hippie (1.5.1) sha256=76ecfc7d8df7866c3ed4f2e25fe963d87537451a1fb0d318b58eca161127fbcc
-  net-http (0.9.1) sha256=25ba0b67c63e89df626ed8fac771d0ad24ad151a858af2cc8e6a716ca4336996
   open3 (0.2.1) sha256=8e2d7d2113526351201438c1aa35c8139f0141c9e8913baa007c898973bf3952
-  openssl (4.0.2) sha256=1037ad2868ae58df9ad917891c0c0f9815a1172f6846d4bcdd508e4c2ee747c2
   optparse (0.8.1) sha256=42bea10d53907ccff4f080a69991441d611fbf8733b60ed1ce9ee365ce03bd1a
   pathname (0.5.0) sha256=d5a331784f6e1f2fefb31c2ff0b8855aabfb661d807284ead9fa47b883d81623
   pp (0.6.4) sha256=dfcb0fce700c41456265922884f9fe195d7fbb0674a3578e6c0f69588e82b570
@@ -133,16 +127,17 @@ CHECKSUMS
   rdoc (8.0.0) sha256=03bf8c08a9639658855a0cfd77c0abca8325c227693f7f33f82957811348c469
   regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb
   reline (0.7.0) sha256=5b012d8e55dbf9d450f12bde2cf7d15ff546ae80b3f8f3b30e570d431815583d
-  resolv (0.7.2) sha256=626d044d975ab2daac759bf898416f1b51e2cb8dcd6727c2b5b5b28b97ead2e1
-  securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1
+  rspec (3.13.2) sha256=206284a08ad798e61f86d7ca3e376718d52c0bc944626b2349266f239f820587
+  rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d
+  rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836
+  rspec-mocks (3.13.8) sha256=086ad3d3d17533f4237643de0b5c42f04b66348c28bf6b9c2d3f4a3b01af1d47
+  rspec-support (3.13.7) sha256=0640e5570872aafefd79867901deeeeb40b0c9875a36b983d85f54fb7381c47c
   shellwords (0.2.2) sha256=b8695a791de2f71472de5abdc3f4332f6535a4177f55d8f99e7e44266cd32f94
   simpleidn (0.3.0) sha256=12ca730bed2f3db04d11e9bfd1bca3e11fb37f55b21eb2e9793fb5814bf54d03
   stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1
   tempfile (0.3.1) sha256=0bb53ab646744e505eb3102147e22ae130a626a15563e882428c1ec973fed76a
-  timeout (0.6.1) sha256=78f57368a7e7bbadec56971f78a3f5ecbcfb59b7fcbb0a3ed6ddc08a5094accb
   tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f
   uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6
-  webrick (1.9.2) sha256=beb4a15fc474defed24a3bda4ffd88a490d517c9e4e6118c3edce59e45864131
 
 BUNDLED WITH
   4.0.20
Rakefile
@@ -1,4 +1,8 @@
 # frozen_string_literal: true
 
 require "bundler/gem_tasks"
-task default: %i[]
+require "rspec/core/rake_task"
+
+RSpec::Core::RakeTask.new(:spec)
+
+task default: %i[spec]