Commit ca54124

mo khan <mo@mokhan.ca>
2026-01-15 02:17:39
refactor: remove modes and permissions
1 parent 078138b
lib/elelem/agent.rb
@@ -5,11 +5,10 @@ module Elelem
     PROVIDERS = %w[ollama anthropic openai vertex-ai].freeze
     ANTHROPIC_MODELS = %w[claude-sonnet-4-20250514 claude-opus-4-20250514 claude-haiku-3-5-20241022].freeze
     VERTEX_MODELS = %w[claude-sonnet-4@20250514 claude-opus-4-5@20251101].freeze
-    COMMANDS = %w[/env /mode /provider /model /shell /clear /context /exit /help].freeze
-    MODES = %w[auto build plan verify].freeze
+    COMMANDS = %w[/env /provider /model /shell /clear /context /exit /help].freeze
     ENV_VARS = %w[ANTHROPIC_API_KEY OPENAI_API_KEY OPENAI_BASE_URL OLLAMA_HOST GOOGLE_CLOUD_PROJECT GOOGLE_CLOUD_REGION].freeze
 
-    attr_reader :conversation, :client, :toolbox, :provider, :terminal, :permissions
+    attr_reader :conversation, :client, :toolbox, :provider, :terminal
 
     def initialize(provider, model, toolbox, terminal: nil)
       @conversation = Conversation.new
@@ -17,18 +16,17 @@ module Elelem
       @toolbox = toolbox
       @client = build_client(provider, model)
       @terminal = terminal || default_terminal
-      @permissions = Set.new([:read])
     end
 
     def repl
       loop do
-        input = terminal.ask("User> ")
+        input = terminal.ask("> ")
         break if input.nil?
         if input.start_with?("/")
           handle_slash_command(input)
         else
           conversation.add(role: :user, content: input)
-          result = execute_turn(conversation.history_for(permissions))
+          result = execute_turn(conversation.history)
           conversation.add(role: result[:role], content: result[:content])
         end
       end
@@ -40,37 +38,18 @@ module Elelem
       Terminal.new(
         commands: COMMANDS,
         env_vars: ENV_VARS,
-        modes: MODES,
         providers: PROVIDERS
       )
     end
 
     def handle_slash_command(input)
       case input
-      when "/mode auto"
-        permissions.replace([:read, :write, :execute])
-        terminal.say "  → Mode: auto (all tools enabled)"
-      when "/mode build"
-        permissions.replace([:read, :write])
-        terminal.say "  → Mode: build (read + write)"
-      when "/mode plan"
-        permissions.replace([:read])
-        terminal.say "  → Mode: plan (read-only)"
-      when "/mode verify"
-        permissions.replace([:read, :execute])
-        terminal.say "  → Mode: verify (read + execute)"
-      when "/mode"
-        terminal.say "  Usage: /mode [auto|build|plan|verify]"
-        terminal.say ""
-        terminal.say "  Provider: #{provider}/#{client.model}"
-        terminal.say "  Permissions: #{permissions.to_a.inspect}"
-        terminal.say "  Tools: #{toolbox.tools_for(permissions).map { |t| t.dig(:function, :name) }}"
       when "/exit" then exit
       when "/clear"
         conversation.clear
         terminal.say "  → Conversation cleared"
       when "/context"
-        terminal.say conversation.dump(permissions), markdown: true
+        terminal.say conversation.dump, markdown: true
       when "/shell"
         transcript = start_shell
         conversation.add(role: :user, content: transcript) unless transcript.strip.empty?
@@ -136,7 +115,6 @@ module Elelem
     def help_banner
       <<~HELP
   /env VAR cmd...
-  /mode auto build plan verify
   /provider
   /model
   /shell
@@ -230,7 +208,7 @@ module Elelem
     end
 
     def execute_turn(messages)
-      tools = toolbox.tools_for(permissions)
+      tools = toolbox.tools
       turn_context = []
       errors = 0
 
@@ -243,7 +221,6 @@ module Elelem
           client.fetch(messages + turn_context, tools) do |chunk|
             case chunk[:type]
             when :delta
-              terminal.write chunk[:thinking] if chunk[:thinking]
               content += chunk[:content] if chunk[:content]
             when :complete
               content = chunk[:content] if chunk[:content]
@@ -255,7 +232,7 @@ module Elelem
           return { role: "assistant", content: "[Error: #{e.message}]" }
         end
 
-        terminal.say("\nAssistant> #{content}", markdown: true) unless content.to_s.empty?
+        terminal.say("\n#{content}", markdown: true) unless content.to_s.empty?
         api_tool_calls = tool_calls.any? ? format_tool_calls_for_api(tool_calls) : nil
         turn_context << { role: "assistant", content: content, tool_calls: api_tool_calls }.compact
 
@@ -263,7 +240,7 @@ module Elelem
           tool_calls.each do |call|
             name, args = call[:name], call[:arguments]
             terminal.say "\nTool> #{name}(#{args})"
-            result = toolbox.run_tool(name, args, permissions: permissions)
+            result = toolbox.run_tool(name, args)
             terminal.say truncate_output(format_tool_call_result(result))
             turn_context << { role: "tool", tool_call_id: call[:id], content: JSON.dump(result) }
             errors += 1 if result[:error]
lib/elelem/conversation.rb
@@ -8,10 +8,8 @@ module Elelem
       @items = items
     end
 
-    def history_for(permissions)
-      history = @items.dup
-      history[0] = { role: "system", content: system_prompt_for(permissions) }
-      history
+    def history
+      @items.dup
     end
 
     def add(role: :user, content: "")
@@ -30,39 +28,16 @@ module Elelem
       @items = default_context
     end
 
-    def dump(permissions)
-      history_for(permissions).map do |item|
+    def dump
+      history.map do |item|
         "## #{item[:role].to_s.capitalize}\n\n#{item[:content]}"
       end.join("\n\n---\n\n")
     end
 
     private
 
-    def default_context(prompt = system_prompt_for([]))
-      [{ role: "system", content: prompt }]
-    end
-
-    def system_prompt_for(permissions)
-      base = system_prompt
-
-      case permissions.sort
-      when [:read]
-        "#{base}\n\nYou may read files on the system."
-      when [:write]
-        "#{base}\n\nYou may write files on the system."
-      when [:execute]
-        "#{base}\n\nYou may execute shell commands on the system."
-      when [:read, :write]
-        "#{base}\n\nYou may read and write files on the system."
-      when [:execute, :read]
-        "#{base}\n\nYou may execute shell commands and read files on the system."
-      when [:execute, :write]
-        "#{base}\n\nYou may execute shell commands and write files on the system."
-      when [:execute, :read, :write]
-        "#{base}\n\nYou may read files, write files and execute shell commands on the system."
-      else
-        base
-      end
+    def default_context
+      [{ role: "system", content: system_prompt }]
     end
 
     def system_prompt
lib/elelem/git_context.rb
@@ -1,79 +0,0 @@
-# frozen_string_literal: true
-
-module Elelem
-  class GitContext
-    MAX_DIFF_LINES = 100
-
-    def initialize(shell = Elelem.shell)
-      @shell = shell
-    end
-
-    def to_s
-      return "" unless git_repo?
-
-      parts = []
-      parts << "Branch: #{branch}" if branch
-      parts << status_section if status.any?
-      parts << diff_section if staged_diff.any? || unstaged_diff.any?
-      parts << recent_commits_section if recent_commits.any?
-      parts.join("\n\n")
-    end
-
-    private
-
-    def git_repo?
-      @shell.execute("git", args: ["rev-parse", "--git-dir"])["exit_status"].zero?
-    end
-
-    def branch
-      @branch ||= @shell.execute("git", args: ["branch", "--show-current"])["stdout"].strip.then { |b| b.empty? ? nil : b }
-    end
-
-    def status
-      @status ||= @shell.execute("git", args: ["status", "--porcelain"])["stdout"].lines.map(&:chomp)
-    end
-
-    def staged_diff
-      @staged_diff ||= @shell.execute("git", args: ["diff", "--cached", "--stat"])["stdout"].lines
-    end
-
-    def unstaged_diff
-      @unstaged_diff ||= @shell.execute("git", args: ["diff", "--stat"])["stdout"].lines
-    end
-
-    def recent_commits
-      @recent_commits ||= @shell.execute("git", args: ["log", "--oneline", "-5"])["stdout"].lines.map(&:strip)
-    end
-
-    def status_section
-      modified = status.select { |l| l[0] == "M" || l[1] == "M" }.map { |l| l[3..] }
-      added = status.select { |l| l[0] == "A" || l.start_with?("??") }.map { |l| l[3..] }
-      deleted = status.select { |l| l[0] == "D" || l[1] == "D" }.map { |l| l[3..] }
-
-      lines = []
-      lines << "Modified: #{modified.join(', ')}" if modified.any?
-      lines << "Added: #{added.join(', ')}" if added.any?
-      lines << "Deleted: #{deleted.join(', ')}" if deleted.any?
-      lines.any? ? "Working tree:\n#{lines.join("\n")}" : nil
-    end
-
-    def diff_section
-      lines = []
-      lines << "Staged:\n#{truncate(staged_diff)}" if staged_diff.any?
-      lines << "Unstaged:\n#{truncate(unstaged_diff)}" if unstaged_diff.any?
-      lines.join("\n\n")
-    end
-
-    def recent_commits_section
-      "Recent commits:\n#{recent_commits.join("\n")}"
-    end
-
-    def truncate(lines)
-      if lines.size > MAX_DIFF_LINES
-        lines.first(MAX_DIFF_LINES).join + "\n... (#{lines.size - MAX_DIFF_LINES} more lines)"
-      else
-        lines.join
-      end
-    end
-  end
-end
lib/elelem/system_prompt.erb
@@ -10,7 +10,3 @@ You are a trusted terminal agent. You act on behalf of the user - executing task
 ## System
 
 <%= `uname -s`.strip %> · <%= ENV['PWD'] %>
-
-## Git State
-
-<%= Elelem::GitContext.new.to_s %>
lib/elelem/terminal.rb
@@ -2,9 +2,8 @@
 
 module Elelem
   class Terminal
-    def initialize(commands: [], modes: [], providers: [], env_vars: [])
+    def initialize(commands: [], providers: [], env_vars: [])
       @commands = commands
-      @modes = modes
       @providers = providers
       @env_vars = env_vars
       @spinner_thread = nil
@@ -73,8 +72,6 @@ module Elelem
       end
 
       case preposing.strip
-      when '/mode'
-        @modes.select { |m| m.start_with?(target) }
       when '/provider'
         @providers.select { |p| p.start_with?(target) }
       when '/env'
lib/elelem/toolbox.rb
@@ -26,7 +26,7 @@ module Elelem
       Elelem.shell.execute("git", args: args["path"] ? ["ls-files", "--", args["path"]] : ["ls-files"])
     end
 
-    PATCH_TOOL = Tool.build( "patch", "Apply a unified diff patch via 'git apply'. Use for surgical edits to existing files.", { diff: { type: "string" } }, ["diff"]) do |args|
+    PATCH_TOOL = Tool.build("patch", "Apply a unified diff patch via 'git apply'. Use for surgical edits to existing files.", { diff: { type: "string" } }, ["diff"]) do |args|
       Elelem.shell.execute("git", args: ["apply", "--index", "--whitespace=nowarn", "-p1"], stdin: args["diff"])
     end
 
@@ -63,47 +63,36 @@ module Elelem
       "web" => "fetch",
     }
 
-    attr_reader :tools
-
     def initialize
       @tools_by_name = {}
-      @tool_permissions = {}
-      @tools = { read: [], write: [], execute: [] }
-      add_tool(eval_tool(binding), :execute)
-      add_tool(WEB_SEARCH_TOOL, :read)
-      add_tool(EXEC_TOOL, :execute)
-      add_tool(FETCH_TOOL, :read)
-      add_tool(GREP_TOOL, :read)
-      add_tool(LIST_TOOL, :read)
-      add_tool(PATCH_TOOL, :write)
-      add_tool(READ_TOOL, :read)
-      add_tool(WRITE_TOOL, :write)
+      add_tool(eval_tool(binding))
+      add_tool(EXEC_TOOL)
+      add_tool(FETCH_TOOL)
+      add_tool(GREP_TOOL)
+      add_tool(LIST_TOOL)
+      add_tool(PATCH_TOOL)
+      add_tool(READ_TOOL)
+      add_tool(WEB_SEARCH_TOOL)
+      add_tool(WRITE_TOOL)
     end
 
-    def add_tool(tool, permission)
-      @tools[permission] << tool
+    def add_tool(tool)
       @tools_by_name[tool.name] = tool
-      @tool_permissions[tool.name] = permission
     end
 
-    def register_tool(name, description, properties = {}, required = [], mode: :execute, &block)
-      add_tool(Tool.build(name, description, properties, required, &block), mode)
+    def register_tool(name, description, properties = {}, required = [], &block)
+      add_tool(Tool.build(name, description, properties, required, &block))
     end
 
-    def tools_for(permissions)
-      Array(permissions).map { |permission| tools[permission].map(&:to_h) }.flatten
+    def tools
+      @tools_by_name.values.map(&:to_h)
     end
 
-    def run_tool(name, args, permissions: [])
+    def run_tool(name, args)
       resolved_name = TOOL_ALIASES.fetch(name, name)
       tool = @tools_by_name[resolved_name]
       return { error: "Unknown tool", name: name, args: args } unless tool
 
-      tool_permission = @tool_permissions[resolved_name]
-      unless Array(permissions).include?(tool_permission)
-        return { error: "Tool '#{resolved_name}' not available in current mode", name: name }
-      end
-
       tool.call(args)
     rescue => error
       { error: error.message, name: name, args: args, backtrace: error.backtrace.first(5) }
@@ -116,7 +105,7 @@ module Elelem
     private
 
     def eval_tool(target_binding)
-      Tool.build("eval", "Evaluates Ruby code with full access to register new tools via the `register_tool(name, desc, properties, required, mode: :execute) { |args| ... }` method.", { ruby: { type: "string" } }, ["ruby"]) do |args|
+      Tool.build("eval", "Evaluates Ruby code with full access to register new tools via the `register_tool(name, desc, properties, required) { |args| ... }` method.", { ruby: { type: "string" } }, ["ruby"]) do |args|
         { result: target_binding.eval(args["ruby"]) }
       end
     end
lib/elelem.rb
@@ -20,7 +20,6 @@ require "tty/markdown"
 require_relative "elelem/agent"
 require_relative "elelem/application"
 require_relative "elelem/conversation"
-require_relative "elelem/git_context"
 require_relative "elelem/terminal"
 require_relative "elelem/tool"
 require_relative "elelem/toolbox"
spec/elelem/agent_e2e_spec.rb
@@ -9,53 +9,6 @@ RSpec.describe Elelem::Agent do
   end
 
   describe "slash commands" do
-    describe "/mode" do
-      it "shows help when called without arguments" do
-        terminal = Elelem::FakeTerminal.new(inputs: ["/mode", nil])
-        agent = described_class.new("ollama", nil, toolbox, terminal: terminal)
-
-        agent.repl
-
-        expect(terminal.output).to include("  Usage: /mode [auto|build|plan|verify]")
-      end
-
-      it "switches to auto mode" do
-        terminal = Elelem::FakeTerminal.new(inputs: ["/mode auto", nil])
-        agent = described_class.new("ollama", nil, toolbox, terminal: terminal)
-
-        agent.repl
-
-        expect(terminal.output).to include("  → Mode: auto (all tools enabled)")
-      end
-
-      it "switches to build mode" do
-        terminal = Elelem::FakeTerminal.new(inputs: ["/mode build", nil])
-        agent = described_class.new("ollama", nil, toolbox, terminal: terminal)
-
-        agent.repl
-
-        expect(terminal.output).to include("  → Mode: build (read + write)")
-      end
-
-      it "switches to plan mode" do
-        terminal = Elelem::FakeTerminal.new(inputs: ["/mode plan", nil])
-        agent = described_class.new("ollama", nil, toolbox, terminal: terminal)
-
-        agent.repl
-
-        expect(terminal.output).to include("  → Mode: plan (read-only)")
-      end
-
-      it "switches to verify mode" do
-        terminal = Elelem::FakeTerminal.new(inputs: ["/mode verify", nil])
-        agent = described_class.new("ollama", nil, toolbox, terminal: terminal)
-
-        agent.repl
-
-        expect(terminal.output).to include("  → Mode: verify (read + execute)")
-      end
-    end
-
     describe "/clear" do
       it "clears the conversation" do
         terminal = Elelem::FakeTerminal.new(inputs: ["/clear", nil])
@@ -98,7 +51,8 @@ RSpec.describe Elelem::Agent do
         agent.repl
 
         expect(terminal.output.join).to include("/env VAR cmd...")
-        expect(terminal.output.join).to include("/mode auto build plan verify")
+        expect(terminal.output.join).to include("/provider")
+        expect(terminal.output.join).to include("/clear")
       end
     end
   end
spec/elelem/agent_spec.rb
@@ -20,24 +20,9 @@ RSpec.describe Elelem::Agent do
       expect(agent.client).to eq(mock_client)
     end
 
-    it "initializes tools for all modes" do
-      expect(agent.toolbox.tools[:read]).to be_an(Array)
-      expect(agent.toolbox.tools[:write]).to be_an(Array)
-      expect(agent.toolbox.tools[:execute]).to be_an(Array)
-    end
-  end
-
-  describe "integration with conversation" do
-    it "conversation uses mode-aware prompts" do
-      conversation = agent.conversation
-      conversation.add(role: :user, content: "test message")
-
-      read_history = conversation.history_for([:read])
-      write_history = conversation.history_for([:write])
-
-      expect(read_history[0][:content]).to include("You may read files on the system")
-      expect(write_history[0][:content]).to include("You may write files on the system")
-      expect(read_history[0][:content]).not_to eq(write_history[0][:content])
+    it "initializes toolbox with all tools" do
+      tool_names = agent.toolbox.tools.map { |t| t.dig(:function, :name) }
+      expect(tool_names).to include("read", "write", "exec", "grep", "list")
     end
   end
 end
spec/elelem/conversation_spec.rb
@@ -3,90 +3,13 @@
 RSpec.describe Elelem::Conversation do
   let(:conversation) { described_class.new }
 
-  describe "#history_for" do
-    context "with empty conversation" do
-      it "returns history with mode-specific system prompt for read mode" do
-        history = conversation.history_for([:read])
-
-        expect(history.length).to eq(1)
-        expect(history[0][:role]).to eq("system")
-        expect(history[0][:content]).to include("You may read files on the system")
-      end
-
-      it "returns history with mode-specific system prompt for write mode" do
-        history = conversation.history_for([:write])
-
-        expect(history[0][:content]).to include("You may write files on the system")
-      end
-
-      it "returns history with mode-specific system prompt for execute mode" do
-        history = conversation.history_for([:execute])
-
-        expect(history[0][:content]).to include("You may execute shell commands on the system")
-      end
-
-      it "returns history with mode-specific system prompt for read+write mode" do
-        history = conversation.history_for([:read, :write])
-
-        expect(history[0][:content]).to include("You may read and write files on the system")
-      end
-
-      it "returns history with mode-specific system prompt for read+execute mode" do
-        history = conversation.history_for([:read, :execute])
+  describe "#history" do
+    it "returns history with system prompt" do
+      history = conversation.history
 
-        expect(history[0][:content]).to include("You may execute shell commands and read files on the system")
-      end
-
-      it "returns history with mode-specific system prompt for write+execute mode" do
-        history = conversation.history_for([:write, :execute])
-
-        expect(history[0][:content]).to include("You may execute shell commands and write files on the system")
-      end
-
-      it "returns history with mode-specific system prompt for all tools mode" do
-        history = conversation.history_for([:read, :write, :execute])
-
-        expect(history[0][:content]).to include("You may read files, write files and execute shell commands on the system")
-      end
-
-      it "returns base system prompt for unknown mode" do
-        history = conversation.history_for([:unknown])
-
-        expect(history[0][:content]).not_to include("Read and analyze")
-        expect(history[0][:content]).not_to include("Write clean")
-      end
-
-      it "returns base system prompt for empty mode" do
-        history = conversation.history_for([])
-
-        expect(history[0][:role]).to eq("system")
-        expect(history[0][:content]).to be_a(String)
-      end
-    end
-
-    context "with mode order independence" do
-      it "returns same prompt for [:read, :write] and [:write, :read]" do
-        history1 = conversation.history_for([:read, :write])
-        history2 = conversation.history_for([:write, :read])
-
-        expect(history1[0][:content]).to eq(history2[0][:content])
-      end
-
-      it "returns same prompt for [:read, :execute] and [:execute, :read]" do
-        history1 = conversation.history_for([:read, :execute])
-        history2 = conversation.history_for([:execute, :read])
-
-        expect(history1[0][:content]).to eq(history2[0][:content])
-      end
-
-      it "returns same prompt for all permutations of [:read, :write, :execute]" do
-        history1 = conversation.history_for([:read, :write, :execute])
-        history2 = conversation.history_for([:execute, :read, :write])
-        history3 = conversation.history_for([:write, :execute, :read])
-
-        expect(history1[0][:content]).to eq(history2[0][:content])
-        expect(history2[0][:content]).to eq(history3[0][:content])
-      end
+      expect(history.length).to eq(1)
+      expect(history[0][:role]).to eq("system")
+      expect(history[0][:content]).to be_a(String)
     end
 
     context "with populated conversation" do
@@ -96,7 +19,7 @@ RSpec.describe Elelem::Conversation do
       end
 
       it "preserves all conversation items" do
-        history = conversation.history_for([:read])
+        history = conversation.history
 
         expect(history.length).to eq(3)
         expect(history[1][:role]).to eq(:user)
@@ -105,18 +28,8 @@ RSpec.describe Elelem::Conversation do
         expect(history[2][:content]).to eq("Hi there")
       end
 
-      it "updates system prompt without mutating original" do
-        original_items = conversation.instance_variable_get(:@items)
-        original_system_content = original_items[0][:content]
-
-        history = conversation.history_for([:read])
-
-        expect(history[0][:content]).not_to eq(original_system_content)
-        expect(original_items[0][:content]).to eq(original_system_content)
-      end
-
       it "returns a copy, not the original array" do
-        history = conversation.history_for([:read])
+        history = conversation.history
         original_items = conversation.instance_variable_get(:@items)
 
         expect(history).not_to be(original_items)
@@ -127,7 +40,7 @@ RSpec.describe Elelem::Conversation do
   describe "#add" do
     it "adds user message to conversation" do
       conversation.add(role: :user, content: "test message")
-      history = conversation.history_for([])
+      history = conversation.history
 
       expect(history.length).to eq(2)
       expect(history[1][:content]).to eq("test message")
@@ -136,7 +49,7 @@ RSpec.describe Elelem::Conversation do
     it "merges consecutive messages with same role" do
       conversation.add(role: :user, content: "part 1")
       conversation.add(role: :user, content: "part 2")
-      history = conversation.history_for([])
+      history = conversation.history
 
       expect(history.length).to eq(2)
       expect(history[1][:content]).to eq("part 1part 2")
@@ -144,14 +57,14 @@ RSpec.describe Elelem::Conversation do
 
     it "ignores nil content" do
       conversation.add(role: :user, content: nil)
-      history = conversation.history_for([])
+      history = conversation.history
 
       expect(history.length).to eq(1)
     end
 
     it "ignores empty content" do
       conversation.add(role: :user, content: "")
-      history = conversation.history_for([])
+      history = conversation.history
 
       expect(history.length).to eq(1)
     end
@@ -167,7 +80,7 @@ RSpec.describe Elelem::Conversation do
     it "resets conversation to default context" do
       conversation.add(role: :user, content: "test")
       conversation.clear
-      history = conversation.history_for([])
+      history = conversation.history
 
       expect(history.length).to eq(1)
       expect(history[0][:role]).to eq("system")
@@ -175,13 +88,12 @@ RSpec.describe Elelem::Conversation do
   end
 
   describe "#dump" do
-    it "returns markdown representation with mode-specific prompt" do
+    it "returns markdown representation" do
       conversation.add(role: :user, content: "test")
-      result = conversation.dump([:read])
+      result = conversation.dump
 
       expect(result).to include("## System")
       expect(result).to include("## User")
-      expect(result).to include("You may read files on the system")
     end
   end
 end
spec/elelem/toolbox_spec.rb
@@ -1,61 +1,16 @@
 # frozen_string_literal: true
-#
+
 RSpec.describe Elelem::Toolbox do
   subject { described_class.new }
 
-  describe "#tools_for" do
-    it "returns read tools for read mode" do
-      mode = Set[:read]
-      tools = subject.tools_for(mode)
-
-      tool_names = tools.map { |t| t.dig(:function, :name) }
-      expect(tool_names).to include("grep", "list", "read", "fetch", "web_search")
-      expect(tool_names).not_to include("write", "patch", "exec")
-    end
-
-    it "returns write tools for write mode" do
-      mode = Set[:write]
-      tools = subject.tools_for(mode)
-
-      tool_names = tools.map { |t| t.dig(:function, :name) }
-      expect(tool_names).to include("patch", "write")
-      expect(tool_names).not_to include("grep", "exec")
-    end
-
-    it "returns execute tools for execute mode" do
-      mode = Set[:execute]
-      tools = subject.tools_for(mode)
-
-      tool_names = tools.map { |t| t.dig(:function, :name) }
-      expect(tool_names).to include("exec")
-      expect(tool_names).not_to include("grep", "write")
-    end
-
-    it "returns all tools for auto mode" do
-      mode = Set[:read, :write, :execute]
-      tools = subject.tools_for(mode)
-
-      tool_names = tools.map { |t| t.dig(:function, :name) }
-      expect(tool_names).to include("grep", "list", "read", "patch", "write", "exec", "fetch", "web_search")
-    end
-
-    it "returns combined tools for build mode" do
-      mode = Set[:read, :write]
-      tools = subject.tools_for(mode)
-
-      tool_names = tools.map { |t| t.dig(:function, :name) }
-      expect(tool_names).to include("grep", "read", "write", "patch", "fetch", "web_search")
-      expect(tool_names).not_to include("exec")
+  describe "#tools" do
+    it "returns all tools" do
+      tool_names = subject.tools.map { |t| t.dig(:function, :name) }
+      expect(tool_names).to include("grep", "list", "read", "patch", "write", "exec", "fetch", "web_search", "eval")
     end
   end
 
-  describe "web tools" do
-    it "includes fetch and web_search in read permissions" do
-      tools = subject.tools_for([:read])
-      names = tools.map { |t| t.dig(:function, :name) }
-      expect(names).to include("fetch", "web_search")
-    end
-
+  describe "aliases" do
     it "resolves web and get aliases to fetch" do
       expect(Elelem::Toolbox::TOOL_ALIASES["web"]).to eq("fetch")
       expect(Elelem::Toolbox::TOOL_ALIASES["get"]).to eq("fetch")
@@ -64,26 +19,25 @@ RSpec.describe Elelem::Toolbox do
     it "resolves duckduckgo alias to web_search" do
       expect(Elelem::Toolbox::TOOL_ALIASES["duckduckgo"]).to eq("web_search")
     end
-  end
 
-  describe "#run_tool mode enforcement" do
-    it "allows tool execution when mode matches" do
-      result = subject.run_tool("read", { "path" => __FILE__ }, permissions: [:read])
-      expect(result[:content]).to include("RSpec.describe")
+    it "resolves bash alias to exec" do
+      expect(Elelem::Toolbox::TOOL_ALIASES["bash"]).to eq("exec")
     end
+  end
 
-    it "blocks tool execution when mode does not match" do
-      result = subject.run_tool("exec", { "cmd" => "echo hello" }, permissions: [:read])
-      expect(result[:error]).to include("not available in current mode")
+  describe "#run_tool" do
+    it "executes tools" do
+      result = subject.run_tool("read", { "path" => __FILE__ })
+      expect(result[:content]).to include("RSpec.describe")
     end
 
-    it "resolves aliases and enforces mode" do
-      result = subject.run_tool("bash", { "cmd" => "echo hello" }, permissions: [:read])
-      expect(result[:error]).to include("not available in current mode")
+    it "resolves aliases" do
+      result = subject.run_tool("open", { "path" => __FILE__ })
+      expect(result[:content]).to include("RSpec.describe")
     end
 
     it "returns unknown tool error for non-existent tools" do
-      result = subject.run_tool("nonexistent", {}, permissions: [:read])
+      result = subject.run_tool("nonexistent", {})
       expect(result[:error]).to include("Unknown tool")
     end
   end
@@ -96,9 +50,9 @@ RSpec.describe Elelem::Toolbox do
             { greeting: "Hello, " + args['name']+ "!" }
           end
         RUBY
-      }, permissions: [:execute])
+      })
 
-      expect(subject.tools_for(:execute)).to include(hash_including({
+      expect(subject.tools).to include(hash_including({
         type: "function",
         function: {
           name: "hello",
@@ -119,25 +73,25 @@ RSpec.describe Elelem::Toolbox do
             { sum: args["a"] + args["b"] }
           end
         RUBY
-      }, permissions: [:execute])
+      })
 
-      result = subject.run_tool("add", { "a" => 5, "b" => 3 }, permissions: [:execute])
+      result = subject.run_tool("add", { "a" => 5, "b" => 3 })
       expect(result[:sum]).to eq(8)
     end
 
     it "allows LLM to inspect tool schemas" do
-      result = subject.run_tool("eval", { "ruby" => "tool_schema('read')" }, permissions: [:execute])
+      result = subject.run_tool("eval", { "ruby" => "tool_schema('read')" })
       expect(result[:result]).to be_a(Hash)
       expect(result[:result].dig(:function, :name)).to eq("read")
     end
 
     it "executes arbitrary Ruby code" do
-      result = subject.run_tool("eval", { "ruby" => "2 + 2" }, permissions: [:execute])
+      result = subject.run_tool("eval", { "ruby" => "2 + 2" })
       expect(result[:result]).to eq(4)
     end
 
     it "handles errors gracefully" do
-      result = subject.run_tool("eval", { "ruby" => "undefined_variable" }, permissions: [:execute])
+      result = subject.run_tool("eval", { "ruby" => "undefined_variable" })
       expect(result[:error]).to include("undefined")
       expect(result[:backtrace]).to be_an(Array)
     end
elelem.gemspec
@@ -29,7 +29,6 @@ Gem::Specification.new do |spec|
     "lib/elelem/agent.rb",
     "lib/elelem/application.rb",
     "lib/elelem/conversation.rb",
-    "lib/elelem/git_context.rb",
     "lib/elelem/system_prompt.erb",
     "lib/elelem/terminal.rb",
     "lib/elelem/tool.rb",