Comparing changes

v0.2.1 v0.3.0
13 commits 38 files changed

Commits

83a603a chore: bump to 0.3.0 mo khan 2025-11-05 23:35:23
283825a refactor: improve exec tool description mo khan 2025-11-05 20:55:32
fc2fba7 docs: update README mo khan 2025-11-05 20:54:59
e2954f9 docs: Update the README mo khan 2025-11-05 20:53:33
bd0762e refactor: improve formatting mo khan 2025-11-05 20:50:41
d63e1d1 refactor: cleanup mo khan 2025-11-05 20:42:55
cfb2b7b refactor: remove old tools mo khan 2025-11-05 19:43:22
fc01784 refactor: hack all code in agent class mo khan 2025-11-05 19:36:34
7234c5e chore: update bundler version mo khan 2025-11-04 21:05:44
b501838 chore: remove lint job mo khan 2025-11-04 21:04:51
5856b51 Tidy up and remove rubocop mo khan 2025-11-04 20:54:42
d8812e6 docs: fix help mo khan 2025-10-31 15:27:53
bin/lint
@@ -1,8 +0,0 @@
-#!/bin/sh
-
-set -e
-[ -n "$DEBUG" ] && set -x
-
-cd "$(dirname "$0")/.."
-
-bundle exec rubocop $@
exe/llm-ollama
@@ -1,358 +0,0 @@
-#!/usr/bin/env ruby
-
-=begin
-Fast, correct, autonomous - Pick two
-
-PURPOSE:
-
-This script is a minimal coding agent written in Ruby. It is intended to
-assist me (a software engineer and computer science student) with writing,
-editing, and managing code and text files from the command line. It acts
-as a direct interface to an LLM, providing it with a simple text-based
-UI and access to the local filesystem.
-
-DESIGN PRINCIPLES:
-
-- Follows the Unix philosophy: simple, composable, minimal.
-- Convention over configuration.
-- Avoids unnecessary defensive checks, or complexity.
-- Assumes a mature and responsible LLM that behaves like a capable engineer.
-- Designed for my workflow and preferences.
-- Efficient and minimal like aider - https://aider.chat/
-- UX like Claude Code - https://docs.claude.com/en/docs/claude-code/overview
-
-SYSTEM ASSUMPTIONS:
-
-- This script is used on a Linux system with the following tools: Alacritty, tmux, Bash, and Vim.
-- It is always run inside a Git repository.
-- All project work is assumed to be version-controlled with Git.
-- Git is expected to be available and working; no checks are necessary.
-
-SCOPE:
-
-- This program operates only on code and plain-text files.
-- It does not need to support binary files.
-- The LLM has full access to execute system commands.
-- There are no sandboxing, permission, or validation layers.
-- Execution is not restricted or monitored — responsibility is delegated to the LLM.
-
-CONFIGURATION:
-
-- Avoid adding configuration options unless absolutely necessary.
-- Prefer hard-coded values that can be changed later if needed.
-- Only introduce environment variables after repeated usage proves them worthwhile.
-
-UI EXPECTATIONS:
-
-- The TUI must remain simple, fast, and predictable.
-- No mouse support or complex UI components are required.
-- Interaction is strictly keyboard-driven.
-
-CODING STANDARDS FOR LLM:
-
-- Do not add error handling or logging unless it is essential for functionality.
-- Keep methods short and single-purpose.
-- Use descriptive, conventional names.
-- Stick to Ruby's standard library whenever possible.
-
-HELPFUL LINKS:
-
-- https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents
-- https://www.anthropic.com/engineering/writing-tools-for-agents
-- https://simonwillison.net/2025/Sep/30/designing-agentic-loops/
-
-=end
-
-require "bundler/inline"
-
-gemfile do
-  source "https://rubygems.org"
-
-  gem "fileutils", "~> 1.0"
-  gem "json", "~> 2.0"
-  gem "net-llm", "~> 0.4"
-  gem "open3", "~> 0.1"
-  gem "ostruct", "~> 0.1"
-  gem "reline", "~> 0.1"
-  gem "set", "~> 1.0"
-  gem "uri", "~> 1.0"
-end
-
-STDOUT.set_encoding(Encoding::UTF_8)
-STDERR.set_encoding(Encoding::UTF_8)
-
-OLLAMA_HOST = ENV["OLLAMA_HOST"] || "localhost:11434"
-OLLAMA_MODEL = ENV["OLLAMA_MODEL"] || "gpt-oss:latest"
-SYSTEM_PROMPT="You are a reasoning coding and system agent."
-
-def build_tool(name, description, properties, required = [])
-  {
-    type: "function",
-    function: {
-      name: name,
-      description: description,
-      parameters: {
-        type: "object",
-        properties: properties,
-        required: required
-      }
-    }
-  }
-end
-
-EXEC_TOOL = build_tool("execute", "Execute shell commands. Returns stdout, stderr, and exit code. Use for: checking system state, running tests, managing services. Common Unix tools available: git, bash, grep, etc. Tip: Check exit_status in response to determine success.", { cmd: { type: "string" }, args: { type: "array", items: { type: "string" } }, env: { type: "object", additionalProperties: { type: "string" } }, cwd: { type: "string" }, stdin: { type: "string" } }, ["cmd"])
-GREP_TOOL = build_tool("grep", "Search all git-tracked files using git grep. Returns file paths with matching line numbers. Use this to discover where code/configuration exists before reading files. Examples: search 'def method_name' to find method definitions. Much faster than reading multiple files.", { query: { type: "string" } }, ["query"])
-LS_TOOL = build_tool("list", "List all git-tracked files in the repository, optionally filtered by path. Use this to explore project structure or find files in a directory. Returns relative paths from repo root. Tip: Use this before reading if you need to discover what files exist.", { path: { type: "string" } })
-PATCH_TOOL = build_tool("patch", "Apply a unified diff patch via 'git apply'. Use this for surgical edits to existing files rather than rewriting entire files. Generates proper git diffs. Format: standard unified diff with --- and +++ headers. Tip: More efficient than write for small changes to large files.", { diff: { type: "string" } }, ["diff"])
-READ_TOOL = build_tool("read", "Read complete contents of a file. Requires exact file path. Use grep or list first if you don't know the path. Best for: understanding existing code, reading config files, reviewing implementation details. Tip: For large files, grep first to confirm relevance.", { path: { type: "string" } }, ["path"])
-WRITE_TOOL = build_tool("write", "Write complete file contents (overwrites existing files). Creates parent directories automatically. Best for: creating new files, replacing entire file contents. For small edits to existing files, consider using patch instead.", { path: { type: "string" }, content: { type: "string" } }, ["path", "content"])
-
-TOOLS = {
-  read: [GREP_TOOL, LS_TOOL, READ_TOOL],
-  write: [PATCH_TOOL, WRITE_TOOL],
-  execute: [EXEC_TOOL]
-}
-
-trap("INT") do
-  puts "\nExiting."
-  exit
-end
-
-def run_exec(command, args: [], env: {}, cwd: Dir.pwd, stdin: nil)
-  stdout, stderr, status = Open3.capture3(env, command, *args, chdir: cwd, stdin_data: stdin)
-  {
-    "exit_status" => status.exitstatus,
-    "stdout" => stdout.to_s,
-    "stderr" => stderr.to_s
-  }
-end
-
-def expand_path(path)
-  Pathname.new(path).expand_path
-end
-
-def read_file(path)
-  full_path = expand_path(path)
-  full_path.exist? ? { content: full_path.read } : { error: "File not found: #{path}" }
-end
-
-def write_file(path, content)
-  full_path = expand_path(path)
-  FileUtils.mkdir_p(full_path.dirname)
-  { bytes_written: full_path.write(content) }
-end
-
-def run_tool(name, args)
-  case name
-  when "execute" then run_exec(args["cmd"], args: args["args"] || [], env: args["env"] || {}, cwd: args["cwd"] || Dir.pwd, stdin: args["stdin"])
-  when "grep" then run_exec("git", args: ["grep", "-nI", args["query"]])
-  when "list" then run_exec("git", args: args["path"] ? ["ls-files", "--", args["path"]] : ["ls-files"])
-  when "patch" then run_exec("git", args: ["apply", "--index", "--whitespace=nowarn", "-p1"], stdin: args["diff"])
-  when "read" then read_file(args["path"])
-  when "write" then write_file(args["path"], args["content"])
-  else
-    { error: "Unknown tool", name: name, args: args }
-  end
-end
-
-def format_tool_call(name, args)
-  case name
-  when "execute" then "execute(#{args["cmd"]})"
-  when "grep" then "grep(#{args["query"]})"
-  when "list" then "list(#{args["path"] || '.'})"
-  when "patch" then "patch(#{args["diff"].lines.count} lines)"
-  when "read" then "read(#{args["path"]})"
-  when "write" then "write(#{args["path"]})"
-  else
-    "▶ #{name}(#{args.to_s[0...70]})"
-  end
-end
-
-def system_prompt_for(mode)
-  base = "You are a reasoning coding and system agent."
-
-  case mode.sort
-  when [:read]
-    "#{base}\n\nRead and analyze. Understand before suggesting action."
-  when [:write]
-    "#{base}\n\nWrite clean, thoughtful code."
-  when [:execute]
-    "#{base}\n\nUse shell commands creatively to understand and manipulate the system."
-  when [:read, :write]
-    "#{base}\n\nFirst understand, then build solutions that integrate well."
-  when [:read, :execute]
-    "#{base}\n\nUse commands to deeply understand the system."
-  when [:write, :execute]
-    "#{base}\n\nCreate and execute freely. Have fun. Be kind."
-  when [:read, :write, :execute]
-    "#{base}\n\nYou have all tools. Use them wisely."
-  else
-    base
-  end
-end
-
-def tools_for(modes)
-  modes.map { |mode| TOOLS[mode] }.flatten
-end
-
-def prune_context(messages, keep_recent: 5)
-  return messages if messages.length <= keep_recent + 1
-
-  default_context + messages.last(keep_recent)
-end
-
-def execute_turn(client, messages, tools:)
-  turn_context = []
-
-  loop do
-    content = ""
-    tool_calls = nil
-    role = "assistant"
-    first_content = true
-
-    print "Thinking..."
-    client.chat(messages + turn_context, tools) do |chunk|
-      if chunk["message"]
-        msg = chunk["message"]
-        role = msg["role"] if msg["role"]
-
-        if msg["thinking"] && !msg["thinking"].empty?
-          print "."
-        end
-
-        if msg["content"] && !msg["content"].empty?
-          if first_content
-            print "\r\e[KAssistant> "
-            first_content = false
-          end
-          print msg["content"]
-          $stdout.flush
-          content += msg["content"]
-        end
-
-        tool_calls = msg["tool_calls"] if msg["tool_calls"]
-      end
-    end
-    puts
-
-    turn_context << { role: role, content: content, tool_calls: tool_calls }.compact
-
-    if tool_calls
-      tool_calls.each do |call|
-        name = call.dig("function", "name")
-        args_raw = call.dig("function", "arguments")
-
-        begin
-          args = args_raw.is_a?(String) ? JSON.parse(args_raw) : args_raw
-        rescue JSON::ParserError => e
-          turn_context << {
-            role: "tool",
-            content: JSON.dump({
-              error: "Invalid JSON in arguments: #{e.message}",
-              received: args_raw
-            })
-          }
-          next
-        end
-
-        puts "Tool> #{format_tool_call(name, args)}"
-        result = run_tool(name, args)
-        turn_context << { role: "tool", content: JSON.dump(result) }
-      end
-      next
-    end
-
-    return { role: "assistant", content: content } unless content.strip.empty?
-  end
-end
-
-def dump_context(messages)
-  puts JSON.pretty_generate(messages)
-end
-
-def print_status(mode, messages)
-  puts "Mode: #{mode.inspect}"
-  puts "Tools: #{tools_for(mode).map { |x| x.dig(:function, :name) }}"
-end
-
-def strip_ansi(text)
-  text.gsub(/^Script started.*?\n/, '')
-      .gsub(/\nScript done.*$/, '')
-      .gsub(/\e\[[0-9;]*[a-zA-Z]/, '')        # Standard ANSI codes
-      .gsub(/\e\[\?[0-9]+[hl]/, '')           # Bracketed paste mode
-      .gsub(/[\b]/, '')                       # Backspace chars
-      .gsub(/\r/, '')                         # Carriage returns
-end
-
-def start_shell
-  Tempfile.create do |file|
-    system("script -q #{file.path}")
-    { role: "user", content: strip_ansi(File.read(file.path)) }
-  end
-end
-
-def ask?(text)
-  input = Reline.readline(text, true)&.strip
-  exit if input.nil? || input.downcase == "exit"
-
-  input
-end
-
-def print_help
-  puts <<~HELP
-  /chmod - (+|-)rwx auto build plan
-  /clear
-  /context
-  /exit
-  /help
-  /shell
-  /status
-  HELP
-end
-
-def default_context
-  [{ role: "system", content: SYSTEM_PROMPT }]
-end
-
-def main
-  client = Net::Llm::Ollama.new(
-    host: OLLAMA_HOST,
-    model: OLLAMA_MODEL
-  )
-
-  messages = default_context
-  mode = Set.new([:read])
-
-  loop do
-    input = ask?("User> ")
-    if input.start_with?("/")
-      case input
-      when "/chmod +r" then mode.add(:read)
-      when "/chmod +w" then mode.add(:write)
-      when "/chmod +x" then mode.add(:execute)
-      when "/chmod -r" then mode.add(:read)
-      when "/chmod -w" then mode.add(:write)
-      when "/chmod -x" then mode.add(:execute)
-      when "/clear" then messages = default_context
-      when "/compact" then messages = prune_context(messages, keep_recent: 10)
-      when "/context" then dump_context(messages)
-      when "/exit" then exit
-      when "/help" then print_help
-      when "/mode auto" then mode = Set[:read, :write, :execute]
-      when "/mode build" then mode = Set[:read, :write]
-      when "/mode plan" then mode = Set[:read]
-      when "/mode verify" then mode = Set[:read, :execute]
-      when "/mode" then print_status(mode, messages)
-      when "/shell" then messages << start_shell
-      else
-        print_help
-      end
-    else
-      messages[0] = { role: "system", content: system_prompt_for(mode) }
-      messages << { role: "user", content: input }
-      messages << execute_turn(client, messages, tools: tools_for(mode))
-    end
-  end
-end
-
-main
exe/llm-openai
@@ -1,339 +0,0 @@
-#!/usr/bin/env ruby
-
-=begin
-Fast, correct, autonomous - Pick two
-
-PURPOSE:
-
-This script is a minimal coding agent written in Ruby. It is intended to
-assist me (a software engineer and computer science student) with writing,
-editing, and managing code and text files from the command line. It acts
-as a direct interface to an LLM, providing it with a simple text-based
-UI and access to the local filesystem.
-
-DESIGN PRINCIPLES:
-
-- Follows the Unix philosophy: simple, composable, minimal.
-- Convention over configuration.
-- Avoids unnecessary defensive checks, or complexity.
-- Assumes a mature and responsible LLM that behaves like a capable engineer.
-- Designed for my workflow and preferences.
-- Efficient and minimal like aider - https://aider.chat/
-- UX like Claude Code - https://docs.claude.com/en/docs/claude-code/overview
-
-SYSTEM ASSUMPTIONS:
-
-- This script is used on a Linux system with the following tools: Alacritty, tmux, Bash, and Vim.
-- It is always run inside a Git repository.
-- All project work is assumed to be version-controlled with Git.
-- Git is expected to be available and working; no checks are necessary.
-
-SCOPE:
-
-- This program operates only on code and plain-text files.
-- It does not need to support binary files.
-- The LLM has full access to execute system commands.
-- There are no sandboxing, permission, or validation layers.
-- Execution is not restricted or monitored — responsibility is delegated to the LLM.
-
-CONFIGURATION:
-
-- Avoid adding configuration options unless absolutely necessary.
-- Prefer hard-coded values that can be changed later if needed.
-- Only introduce environment variables after repeated usage proves them worthwhile.
-
-UI EXPECTATIONS:
-
-- The TUI must remain simple, fast, and predictable.
-- No mouse support or complex UI components are required.
-- Interaction is strictly keyboard-driven.
-
-CODING STANDARDS FOR LLM:
-
-- Do not add error handling or logging unless it is essential for functionality.
-- Keep methods short and single-purpose.
-- Use descriptive, conventional names.
-- Stick to Ruby's standard library whenever possible.
-
-HELPFUL LINKS:
-
-- https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents
-- https://www.anthropic.com/engineering/writing-tools-for-agents
-- https://simonwillison.net/2025/Sep/30/designing-agentic-loops/
-
-=end
-
-require "bundler/inline"
-
-gemfile do
-  source "https://rubygems.org"
-
-  gem "fileutils", "~> 1.0"
-  gem "json", "~> 2.0"
-  gem "net-llm", "0.3.1"
-  gem "open3", "~> 0.1"
-  gem "ostruct", "~> 0.1"
-  gem "reline", "~> 0.1"
-  gem "set", "~> 1.0"
-  gem "uri", "~> 1.0"
-end
-
-STDOUT.set_encoding(Encoding::UTF_8)
-STDERR.set_encoding(Encoding::UTF_8)
-
-API_KEY = ENV["OPENAI_API_KEY"] or abort("Set OPENAI_API_KEY")
-SYSTEM_PROMPT="You are a reasoning coding and system agent."
-
-def build_tool(name, description, properties, required = [])
-  {
-    type: "function",
-    function: {
-      name: name,
-      description: description,
-      parameters: {
-        type: "object",
-        properties: properties,
-        required: required
-      }
-    }
-  }
-end
-
-EXEC_TOOL = build_tool("execute", "Execute shell commands. Returns stdout, stderr, and exit code. Use for: checking system state, running tests, managing services. Common Unix tools available: git, bash, grep, etc. Tip: Check exit_status in response to determine success.", { cmd: { type: "string" }, args: { type: "array", items: { type: "string" } }, env: { type: "object", additionalProperties: { type: "string" } }, cwd: { type: "string" }, stdin: { type: "string" } }, ["cmd"])
-GREP_TOOL = build_tool("grep", "Search all git-tracked files using git grep. Returns file paths with matching line numbers. Use this to discover where code/configuration exists before reading files. Examples: search 'def method_name' to find method definitions. Much faster than reading multiple files.", { query: { type: "string" } }, ["query"])
-LS_TOOL = build_tool("list", "List all git-tracked files in the repository, optionally filtered by path. Use this to explore project structure or find files in a directory. Returns relative paths from repo root. Tip: Use this before reading if you need to discover what files exist.", { path: { type: "string" } })
-PATCH_TOOL = build_tool("patch", "Apply a unified diff patch via 'git apply'. Use this for surgical edits to existing files rather than rewriting entire files. Generates proper git diffs. Format: standard unified diff with --- and +++ headers. Tip: More efficient than write for small changes to large files.", { diff: { type: "string" } }, ["diff"])
-READ_TOOL = build_tool("read", "Read complete contents of a file. Requires exact file path. Use grep or list first if you don't know the path. Best for: understanding existing code, reading config files, reviewing implementation details. Tip: For large files, grep first to confirm relevance.", { path: { type: "string" } }, ["path"])
-WRITE_TOOL = build_tool("write", "Write complete file contents (overwrites existing files). Creates parent directories automatically. Best for: creating new files, replacing entire file contents. For small edits to existing files, consider using patch instead.", { path: { type: "string" }, content: { type: "string" } }, ["path", "content"])
-
-TOOLS = {
-  read: [GREP_TOOL, LS_TOOL, READ_TOOL],
-  write: [PATCH_TOOL, WRITE_TOOL],
-  execute: [EXEC_TOOL]
-}
-
-trap("INT") do
-  puts "\nExiting."
-  exit
-end
-
-def run_exec(command, args: [], env: {}, cwd: Dir.pwd, stdin: nil)
-  stdout, stderr, status = Open3.capture3(env, command, *args, chdir: cwd, stdin_data: stdin)
-  {
-    "exit_status" => status.exitstatus,
-    "stdout" => stdout.to_s,
-    "stderr" => stderr.to_s
-  }
-end
-
-def expand_path(path)
-  Pathname.new(path).expand_path
-end
-
-def read_file(path)
-  full_path = expand_path(path)
-  full_path.exist? ? { content: full_path.read } : { error: "File not found: #{path}" }
-end
-
-def write_file(path, content)
-  full_path = expand_path(path)
-  FileUtils.mkdir_p(full_path.dirname)
-  { bytes_written: full_path.write(content) }
-end
-
-def run_tool(name, args)
-  case name
-  when "execute" then run_exec(args["cmd"], args: args["args"] || [], env: args["env"] || {}, cwd: args["cwd"] || Dir.pwd, stdin: args["stdin"])
-  when "grep" then run_exec("git", args: ["grep", "-nI", args["query"]])
-  when "list" then run_exec("git", args: args["path"] ? ["ls-files", "--", args["path"]] : ["ls-files"])
-  when "patch" then run_exec("git", args: ["apply", "--index", "--whitespace=nowarn", "-p1"], stdin: args["diff"])
-  when "read" then read_file(args["path"])
-  when "write" then write_file(args["path"], args["content"])
-  else
-    { error: "Unknown tool", name: name, args: args }
-  end
-end
-
-def format_tool_call(name, args)
-  case name
-  when "execute" then "execute(#{args["cmd"]})"
-  when "grep" then "grep(#{args["query"]})"
-  when "list" then "list(#{args["path"] || '.'})"
-  when "patch" then "patch(#{args["diff"].lines.count} lines)"
-  when "read" then "read(#{args["path"]})"
-  when "write" then "write(#{args["path"]})"
-  else
-    "▶ #{name}(#{args.to_s[0...70]})"
-  end
-end
-
-def system_prompt_for(mode)
-  base = "You are a reasoning coding and system agent."
-
-  case mode.sort
-  when [:read]
-    "#{base}\n\nRead and analyze. Understand before suggesting action."
-  when [:write]
-    "#{base}\n\nWrite clean, thoughtful code."
-  when [:execute]
-    "#{base}\n\nUse shell commands creatively to understand and manipulate the system."
-  when [:read, :write]
-    "#{base}\n\nFirst understand, then build solutions that integrate well."
-  when [:read, :execute]
-    "#{base}\n\nUse commands to deeply understand the system."
-  when [:write, :execute]
-    "#{base}\n\nCreate and execute freely. Have fun. Be kind."
-  when [:read, :write, :execute]
-    "#{base}\n\nYou have all tools. Use them wisely."
-  else
-    base
-  end
-end
-
-def tools_for(modes)
-  modes.map { |mode| TOOLS[mode] }.flatten
-end
-
-def prune_context(messages, keep_recent: 5)
-  return messages if messages.length <= keep_recent + 1
-
-  default_context + messages.last(keep_recent)
-end
-
-def execute_turn(client, messages, tools:)
-  turn_context = []
-
-  loop do
-    puts "Thinking..."
-    response = client.chat(messages + turn_context, tools)
-    abort "API Error #{response['code']}: #{response['body']}" if response["code"]
-    message = response.dig("choices", 0, "message")
-    turn_context << message
-
-    if message["tool_calls"]
-      message["tool_calls"].each do |call|
-        name = call.dig("function", "name")
-        # args = JSON.parse(call.dig("function", "arguments"))
-        begin
-          args = JSON.parse(call.dig("function", "arguments"))
-        rescue JSON::ParserError => e
-          # Feed the error back to the LLM as a tool result
-          turn_context << {
-            role: "tool",
-            tool_call_id: call["id"],
-            content: JSON.dump({
-              error: "Invalid JSON in arguments: #{e.message}",
-              received: call.dig("function", "arguments")
-            })
-          }
-          next  # Continue the loop, giving the LLM a chance to correct itself
-        end
-
-        puts "Tool> #{format_tool_call(name, args)}"
-        result = run_tool(name, args)
-        turn_context << { role: "tool", tool_call_id: call["id"], content: JSON.dump(result) }
-      end
-      next
-    end
-
-    if message["content"] && !message["content"].strip.empty?
-      puts "\nAssistant>\n#{message['content']}"
-
-      unless message["tool_calls"]
-        return { role: "assistant", content: message["content"] }
-      end
-    end
-  end
-end
-
-def dump_context(messages)
-  puts JSON.pretty_generate(messages)
-end
-
-def print_status(mode, messages)
-  puts "Mode: #{mode.inspect}"
-  puts "Tools: #{tools_for(mode).map { |x| x.dig(:function, :name) }}"
-end
-
-def strip_ansi(text)
-  text.gsub(/^Script started.*?\n/, '')
-      .gsub(/\nScript done.*$/, '')
-      .gsub(/\e\[[0-9;]*[a-zA-Z]/, '')        # Standard ANSI codes
-      .gsub(/\e\[\?[0-9]+[hl]/, '')           # Bracketed paste mode
-      .gsub(/[\b]/, '')                       # Backspace chars
-      .gsub(/\r/, '')                         # Carriage returns
-end
-
-def start_shell
-  Tempfile.create do |file|
-    system("script -q #{file.path}")
-    { role: "user", content: strip_ansi(File.read(file.path)) }
-  end
-end
-
-def ask?(text)
-  input = Reline.readline(text, true)&.strip
-  exit if input.nil? || input.downcase == "exit"
-
-  input
-end
-
-def print_help
-  puts <<~HELP
-  /chmod - (+|-)rwx auto build plan
-  /clear
-  /context
-  /exit
-  /help
-  /shell
-  /status
-  HELP
-end
-
-def default_context
-  [{ role: "system", content: SYSTEM_PROMPT }]
-end
-
-def main
-  client = Net::Llm::OpenAI.new(
-    api_key: API_KEY,
-    base_url: ENV["BASE_URL"] || "https://api.openai.com/v1",
-    model: ENV["MODEL"] || "gpt-4o-mini"
-  )
-
-  messages = default_context
-  mode = Set.new([:read])
-
-  loop do
-    input = ask?("User> ")
-    if input.start_with?("/")
-      case input
-      when "/chmod +r" then mode.add(:read)
-      when "/chmod +w" then mode.add(:write)
-      when "/chmod +x" then mode.add(:execute)
-      when "/chmod -r" then mode.add(:read)
-      when "/chmod -w" then mode.add(:write)
-      when "/chmod -x" then mode.add(:execute)
-      when "/clear" then messages = default_context
-      when "/compact" then messages = prune_context(messages, keep_recent: 10)
-      when "/context" then dump_context(messages)
-      when "/exit" then exit
-      when "/help" then print_help
-      when "/mode auto" then mode = Set[:read, :write, :execute]
-      when "/mode build" then mode = Set[:read, :write]
-      when "/mode plan" then mode = Set[:read]
-      when "/mode verify" then mode = Set[:read, :execute]
-      when "/mode" then print_status(mode, messages)
-      when "/shell" then messages << start_shell
-      else
-        print_help
-      end
-    else
-      messages[0] = { role: "system", content: system_prompt_for(mode) }
-      messages << { role: "user", content: input }
-      messages << execute_turn(client, messages, tools: tools_for(mode))
-    end
-  end
-end
-
-main
lib/elelem/states/working/error.rb
@@ -1,19 +0,0 @@
-# frozen_string_literal: true
-
-module Elelem
-  module States
-    module Working
-      class Error < State
-        def initialize(agent, error_message)
-          super(agent, "X", :red)
-          @error_message = error_message
-        end
-
-        def process(_message)
-          agent.tui.say("\nTool execution failed: #{@error_message}", colour: :red)
-          Waiting.new(agent)
-        end
-      end
-    end
-  end
-end
lib/elelem/states/working/executing.rb
@@ -1,19 +0,0 @@
-# frozen_string_literal: true
-
-module Elelem
-  module States
-    module Working
-      class Executing < State
-        def process(message)
-          if message["tool_calls"]&.any?
-            message["tool_calls"].each do |tool_call|
-              agent.conversation.add(role: :tool, content: agent.execute(tool_call))
-            end
-          end
-
-          Thinking.new(agent, "*", :yellow)
-        end
-      end
-    end
-  end
-end
lib/elelem/states/working/state.rb
@@ -1,26 +0,0 @@
-# frozen_string_literal: true
-
-module Elelem
-  module States
-    module Working
-      class State
-        attr_reader :agent
-
-        def initialize(agent, icon, colour)
-          @agent = agent
-
-          agent.logger.debug("#{display_name}...")
-          agent.tui.show_progress("#{display_name}...", icon, colour: colour)
-        end
-
-        def run(message)
-          process(message)
-        end
-
-        def display_name
-          self.class.name.split("::").last
-        end
-      end
-    end
-  end
-end
lib/elelem/states/working/talking.rb
@@ -1,19 +0,0 @@
-# frozen_string_literal: true
-
-module Elelem
-  module States
-    module Working
-      class Talking < State
-        def process(message)
-          if message["content"] && !message["content"]&.empty?
-            agent.conversation.add(role: message["role"], content: message["content"])
-            agent.tui.say(message["content"], colour: :default, newline: false)
-            self
-          else
-            Waiting.new(agent).process(message)
-          end
-        end
-      end
-    end
-  end
-end
lib/elelem/states/working/thinking.rb
@@ -1,18 +0,0 @@
-# frozen_string_literal: true
-
-module Elelem
-  module States
-    module Working
-      class Thinking < State
-        def process(message)
-          if message["reasoning"] && !message["reasoning"]&.empty?
-            agent.tui.say(message["reasoning"], colour: :gray, newline: false)
-            self
-          else
-            Waiting.new(agent).process(message)
-          end
-        end
-      end
-    end
-  end
-end
lib/elelem/states/working/waiting.rb
@@ -1,29 +0,0 @@
-# frozen_string_literal: true
-
-module Elelem
-  module States
-    module Working
-      class Waiting < State
-        def initialize(agent)
-          super(agent, ".", :cyan)
-        end
-
-        def process(message)
-          state_for(message)&.process(message) || self
-        end
-
-        private
-
-        def state_for(message)
-          if message["reasoning"] && !message["reasoning"].empty?
-            Thinking.new(agent, "*", :yellow)
-          elsif message["tool_calls"]&.any?
-            Executing.new(agent, ">", :magenta)
-          elsif message["content"] && !message["content"].empty?
-            Talking.new(agent, "~", :white)
-          end
-        end
-      end
-    end
-  end
-end
lib/elelem/states/idle.rb
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-module Elelem
-  module States
-    class Idle
-      def run(agent)
-        agent.logger.debug("Idling...")
-        agent.tui.say("#{Dir.pwd} (#{agent.model}) [#{git_branch}]", colour: :magenta, newline: true)
-        input = agent.tui.prompt("モ ")
-        agent.quit if input.nil? || input.empty? || input == "exit" || input == "quit"
-
-        agent.conversation.add(role: :user, content: input)
-        agent.transition_to(Working)
-      end
-
-      private
-
-      def git_branch
-        `git branch --no-color --show-current --no-abbrev`.strip
-      end
-    end
-  end
-end
lib/elelem/states/working.rb
@@ -1,55 +0,0 @@
-# frozen_string_literal: true
-
-module Elelem
-  module States
-    module Working
-      class << self
-        def run(agent)
-          state = Waiting.new(agent)
-
-          loop do
-            streaming_done = false
-            finish_reason = nil
-
-            agent.api.chat(agent.conversation.history) do |message|
-              if message["done"]
-                streaming_done = true
-                next
-              end
-
-              if message["finish_reason"]
-                finish_reason = message["finish_reason"]
-                agent.logger.debug("Working: finish_reason = #{finish_reason}")
-              end
-
-              new_state = state.run(message)
-              if new_state.class != state.class
-                agent.logger.info("STATE: #{state.display_name} -> #{new_state.display_name}")
-              end
-              state = new_state
-            end
-
-            # Only exit when task is actually complete, not just streaming done
-            if finish_reason == "stop"
-              agent.logger.debug("Working: Task complete, exiting to Idle")
-              break
-            elsif finish_reason == "tool_calls"
-              agent.logger.debug("Working: Tool calls finished, continuing conversation")
-              # Continue loop to process tool results
-            elsif streaming_done && finish_reason.nil?
-              agent.logger.debug("Working: Streaming done but no finish_reason, continuing")
-              # Continue for cases where finish_reason comes in separate chunk
-            end
-          end
-
-          agent.transition_to(States::Idle.new)
-        rescue StandardError => e
-          agent.logger.error(e)
-          agent.conversation.add(role: :tool, content: e.message)
-          agent.tui.say(e.message, colour: :red, newline: true)
-          agent.transition_to(States::Idle.new)
-        end
-      end
-    end
-  end
-end
lib/elelem/toolbox/exec.rb
@@ -1,61 +0,0 @@
-# frozen_string_literal: true
-
-module Elelem
-  module Toolbox
-    class Exec < ::Elelem::Tool
-      attr_reader :tui
-
-      def initialize(configuration)
-        @tui = configuration.tui
-        super("exec", "Execute shell commands with pipe support", {
-          type: "object",
-          properties: {
-            command: { 
-              type: "string", 
-              description: "Shell command to execute (supports pipes, redirects, etc.)" 
-            }
-          },
-          required: ["command"]
-        })
-      end
-
-      def call(args)
-        command = args["command"]
-        output_buffer = []
-
-        tui.say(command, newline: true)
-        Open3.popen3(command) do |stdin, stdout, stderr, wait_thread|
-          stdin.close
-          streams = [stdout, stderr]
-
-          until streams.empty?
-            ready = IO.select(streams, nil, nil, 0.1)
-
-            if ready
-              ready[0].each do |io|
-                data = io.read_nonblock(4096)
-                output_buffer << data
-
-                if io == stderr
-                  tui.say(data, colour: :red, newline: false)
-                else
-                  tui.say(data, newline: false)
-                end
-              rescue IO::WaitReadable
-                next
-              rescue EOFError
-                streams.delete(io)
-              end
-            elsif !wait_thread.alive?
-              break
-            end
-          end
-
-          wait_thread.value
-        end
-
-        output_buffer.join
-      end
-    end
-  end
-end
lib/elelem/toolbox/file.rb
@@ -1,66 +0,0 @@
-# frozen_string_literal: true
-
-module Elelem
-  module Toolbox
-    class File < Tool
-      def initialize(configuration)
-        @configuration = configuration
-        @tui = configuration.tui
-
-        super("file", "Read and write files", {
-          type: :object,
-          properties: {
-            action: {
-              type: :string,
-              enum: ["read", "write"],
-              description: "Action to perform: read or write"
-            },
-            path: {
-              type: :string,
-              description: "File path"
-            },
-            content: {
-              type: :string,
-              description: "Content to write (only for write action)"
-            }
-          },
-          required: [:action, :path]
-        })
-      end
-
-      def call(args)
-        action = args["action"]
-        path = args["path"]
-        content = args["content"]
-
-        case action
-        when "read"
-          read_file(path)
-        when "write"
-          write_file(path, content)
-        else
-          "Invalid action: #{action}"
-        end
-      end
-
-      private
-
-      attr_reader :configuration, :tui
-
-      def read_file(path)
-        tui.say("Read: #{path}", newline: true)
-        ::File.read(path)
-      rescue => e
-        "Error reading file: #{e.message}"
-      end
-
-      def write_file(path, content)
-        tui.say("Write: #{path}", newline: true)
-        ::File.write(path, content)
-        "File written successfully"
-      rescue => e
-        "Error writing file: #{e.message}"
-      end
-    end
-  end
-end
lib/elelem/toolbox/mcp.rb
@@ -1,37 +0,0 @@
-# frozen_string_literal: true
-
-module Elelem
-  module Toolbox
-    class MCP < ::Elelem::Tool
-      attr_reader :client, :tui
-
-      def initialize(client, tui, tool)
-        @client = client
-        @tui = tui
-        super(tool["name"], tool["description"], tool["inputSchema"] || {})
-      end
-
-      def call(args)
-        unless client.connected?
-          tui.say("MCP connection lost", colour: :red)
-          return ""
-        end
-
-        result = client.call(name, args)
-        tui.say(JSON.pretty_generate(result), newline: true)
-
-        if result.nil? || result.empty?
-          tui.say("Tool call failed: no response from MCP server", colour: :red)
-          return result
-        end
-
-        if result["error"]
-          tui.say(result["error"], colour: :red)
-          return result
-        end
-
-        result.dig("content", 0, "text") || result.to_s
-      end
-    end
-  end
-end
lib/elelem/toolbox/memory.rb
@@ -1,164 +0,0 @@
-# frozen_string_literal: true
-
-module Elelem
-  module Toolbox
-    class Memory < Tool
-      MEMORY_DIR = ".elelem_memory"
-      MAX_MEMORY_SIZE = 1_000_000
-      
-      def initialize(configuration)
-        @configuration = configuration
-        @tui = configuration.tui
-        
-        super("memory", "Persistent memory for learning and context retention", {
-          type: :object,
-          properties: {
-            action: {
-              type: :string,
-              enum: %w[store retrieve list search forget],
-              description: "Memory action: store, retrieve, list, search, forget"
-            },
-            key: {
-              type: :string,
-              description: "Unique key for storing/retrieving memory"
-            },
-            content: {
-              type: :string,
-              description: "Content to store (required for store action)"
-            },
-            query: {
-              type: :string,
-              description: "Search query for finding memories"
-            }
-          },
-          required: %w[action]
-        })
-        ensure_memory_dir
-      end
-
-      def call(args)
-        action = args["action"]
-        
-        case action
-        when "store"
-          store_memory(args["key"], args["content"])
-        when "retrieve"
-          retrieve_memory(args["key"])
-        when "list"
-          list_memories
-        when "search"
-          search_memories(args["query"])
-        when "forget"
-          forget_memory(args["key"])
-        else
-          "Invalid memory action: #{action}"
-        end
-      rescue StandardError => e
-        "Memory error: #{e.message}"
-      end
-
-      private
-
-      attr_reader :configuration, :tui
-
-      def ensure_memory_dir
-        Dir.mkdir(MEMORY_DIR) unless Dir.exist?(MEMORY_DIR)
-      end
-
-      def memory_path(key)
-        ::File.join(MEMORY_DIR, "#{sanitize_key(key)}.json")
-      end
-
-      def sanitize_key(key)
-        key.to_s.gsub(/[^a-zA-Z0-9_-]/, "_").slice(0, 100)
-      end
-
-      def store_memory(key, content)
-        return "Key and content required for storing" unless key && content
-        
-        total_size = Dir.glob("#{MEMORY_DIR}/*.json").sum { |f| ::File.size(f) }
-        return "Memory capacity exceeded" if total_size > MAX_MEMORY_SIZE
-
-        memory = {
-          key: key,
-          content: content,
-          timestamp: Time.now.iso8601,
-          access_count: 0
-        }
-
-        ::File.write(memory_path(key), JSON.pretty_generate(memory))
-        "Memory stored: #{key}"
-      end
-
-      def retrieve_memory(key)
-        return "Key required for retrieval" unless key
-        
-        path = memory_path(key)
-        return "Memory not found: #{key}" unless ::File.exist?(path)
-
-        memory = JSON.parse(::File.read(path))
-        memory["access_count"] += 1
-        memory["last_accessed"] = Time.now.iso8601
-        
-        ::File.write(path, JSON.pretty_generate(memory))
-        memory["content"]
-      end
-
-      def list_memories
-        memories = Dir.glob("#{MEMORY_DIR}/*.json").map do |file|
-          memory = JSON.parse(::File.read(file))
-          {
-            key: memory["key"],
-            timestamp: memory["timestamp"],
-            size: memory["content"].length,
-            access_count: memory["access_count"] || 0
-          }
-        end
-        
-        memories.sort_by { |m| m[:timestamp] }.reverse
-        JSON.pretty_generate(memories)
-      end
-
-      def search_memories(query)
-        return "Query required for search" unless query
-        
-        matches = Dir.glob("#{MEMORY_DIR}/*.json").filter_map do |file|
-          memory = JSON.parse(::File.read(file))
-          if memory["content"].downcase.include?(query.downcase) ||
-             memory["key"].downcase.include?(query.downcase)
-            {
-              key: memory["key"],
-              snippet: memory["content"][0, 200] + "...",
-              relevance: calculate_relevance(memory, query)
-            }
-          end
-        end
-        
-        matches.sort_by { |m| -m[:relevance] }
-        JSON.pretty_generate(matches)
-      end
-
-      def forget_memory(key)
-        return "Key required for forgetting" unless key
-        
-        path = memory_path(key)
-        return "Memory not found: #{key}" unless ::File.exist?(path)
-
-        ::File.delete(path)
-        "Memory forgotten: #{key}"
-      end
-
-      def calculate_relevance(memory, query)
-        content = memory["content"].downcase
-        key = memory["key"].downcase
-        query = query.downcase
-        
-        score = 0
-        score += 3 if key.include?(query)
-        score += content.scan(query).length
-        score += (memory["access_count"] || 0) * 0.1
-        score
-      end
-    end
-  end
-end
\ No newline at end of file
lib/elelem/toolbox/prompt.rb
@@ -1,25 +0,0 @@
-# frozen_string_literal: true
-
-module Elelem
-  module Toolbox
-    class Prompt < Tool
-      def initialize(configuration)
-        @configuration = configuration
-        super("prompt", "Ask the user a question and get their response.", {
-          type: :object,
-          properties: {
-            question: {
-              type: :string,
-              description: "The question to ask the user."
-            }
-          },
-          required: [:question]
-        })
-      end
-
-      def call(args)
-        @configuration.tui.prompt(args["question"])
-      end
-    end
-  end
-end
lib/elelem/toolbox/web.rb
@@ -1,126 +0,0 @@
-# frozen_string_literal: true
-
-module Elelem
-  module Toolbox
-    class Web < Tool
-      def initialize(configuration)
-        super("web", "Fetch web content and search the internet", {
-          type: :object,
-          properties: {
-            action: {
-              type: :string,
-              enum: ["fetch", "search"],
-              description: "Action to perform: fetch URL or search"
-            },
-            url: {
-              type: :string,
-              description: "URL to fetch (for fetch action)"
-            },
-            query: {
-              type: :string,
-              description: "Search query (for search action)"
-            }
-          },
-          required: [:action]
-        })
-      end
-
-      def call(args)
-        action = args["action"]
-        case action
-        when "fetch"
-          fetch_url(args["url"])
-        when "search"
-          search_web(args["query"])
-        else
-          "Invalid action: #{action}"
-        end
-      end
-
-      private
-
-      def fetch_url(url)
-        return "URL required for fetch action" unless url
-
-        uri = URI(url)
-        http = Net::HTTP.new(uri.host, uri.port)
-        http.use_ssl = uri.scheme == "https"
-        http.read_timeout = 10
-        http.open_timeout = 5
-
-        request = Net::HTTP::Get.new(uri)
-        request["User-Agent"] = "Elelem Agent/1.0"
-
-        response = http.request(request)
-
-        if response.is_a?(Net::HTTPSuccess)
-          content_type = response["content-type"] || ""
-          if content_type.include?("text/html")
-            extract_text_from_html(response.body)
-          else
-            response.body
-          end
-        else
-          "HTTP Error: #{response.code} #{response.message}"
-        end
-      end
-
-      def search_web(query)
-        return "Query required for search action" unless query
-
-        # Use DuckDuckGo instant answers API
-        search_url = "https://api.duckduckgo.com/?q=#{URI.encode_www_form_component(query)}&format=json&no_html=1"
-
-        result = fetch_url(search_url)
-        if result.start_with?("Error") || result.start_with?("HTTP Error")
-          result
-        else
-          format_search_results(JSON.parse(result), query)
-        end
-      end
-
-      def extract_text_from_html(html)
-        # Simple HTML tag stripping
-        text = html.gsub(/<script[^>]*>.*?<\/script>/im, "")
-                  .gsub(/<style[^>]*>.*?<\/style>/im, "")
-                  .gsub(/<[^>]*>/, " ")
-                  .gsub(/\s+/, " ")
-                  .strip
-
-        # Limit content length
-        text.length > 5000 ? text[0...5000] + "..." : text
-      end
-
-      def format_search_results(data, query)
-        results = []
-
-        # Instant answer
-        if data["Answer"] && !data["Answer"].empty?
-          results << "Answer: #{data["Answer"]}"
-        end
-
-        # Abstract
-        if data["Abstract"] && !data["Abstract"].empty?
-          results << "Summary: #{data["Abstract"]}"
-        end
-
-        # Related topics
-        if data["RelatedTopics"] && data["RelatedTopics"].any?
-          topics = data["RelatedTopics"].first(3).map do |topic|
-            topic["Text"] if topic["Text"]
-          end.compact
-
-          if topics.any?
-            results << "Related: #{topics.join("; ")}"
-          end
-        end
-
-        if results.empty?
-          "No direct results found for '#{query}'. Try a more specific search or use web fetch to access specific URLs."
-        else
-          results.join("\n\n")
-        end
-      end
-    end
-  end
-end
lib/elelem/agent.rb
@@ -2,56 +2,272 @@
 
 module Elelem
   class Agent
-    attr_reader :api, :conversation, :logger, :model, :tui
+    attr_reader :conversation, :client, :tools
 
-    def initialize(configuration)
-      @api = configuration.api
-      @tui = configuration.tui
-      @configuration = configuration
-      @model = configuration.model
-      @conversation = configuration.conversation
-      @logger = configuration.logger
+    def initialize(client)
+      @conversation = Conversation.new
+      @client = client
+      @tools = {
+        read: [grep_tool, list_tool, read_tool],
+        write: [patch_tool, write_tool],
+        execute: [exec_tool]
+      }
+    end
 
-      at_exit { cleanup }
+    def repl
+      mode = Set.new([:read])
 
-      transition_to(States::Idle.new)
+      loop do
+        input = ask?("User> ")
+        break if input.nil?
+        if input.start_with?("/")
+          case input
+          when "/mode auto"
+            mode = Set[:read, :write, :execute]
+            puts "  → Mode: auto (all tools enabled)"
+          when "/mode build"
+            mode = Set[:read, :write]
+            puts "  → Mode: build (read + write)"
+          when "/mode plan"
+            mode = Set[:read]
+            puts "  → Mode: plan (read-only)"
+          when "/mode verify"
+            mode = Set[:read, :execute]
+            puts "  → Mode: verify (read + execute)"
+          when "/mode"
+            puts "  Mode: #{mode.to_a.inspect}"
+            puts "  Tools: #{tools_for(mode).map { |t| t.dig(:function, :name) }}"
+          when "/exit" then exit
+          when "/clear"
+            conversation.clear
+            puts "  → Conversation cleared"
+          when "/context" then puts conversation.dump
+          else
+            puts help_banner
+          end
+        else
+          conversation.set_system_prompt(system_prompt_for(mode))
+          conversation.add(role: :user, content: input)
+          result = execute_turn(conversation.history, tools: tools_for(mode))
+          conversation.add(role: result[:role], content: result[:content])
+        end
+      end
     end
 
-    def repl
+    private
+
+    def ask?(text)
+      Reline.readline(text, true)&.strip
+    end
+
+    def help_banner
+      <<~HELP
+  /mode auto build plan verify
+  /clear
+  /context
+  /exit
+  /help
+      HELP
+    end
+
+    def tools_for(modes)
+      modes.map { |mode| tools[mode] }.flatten
+    end
+
+    def system_prompt_for(mode)
+      base = "You are a reasoning coding and system agent."
+
+      case mode.to_a.sort
+      when [:read]
+        "#{base}\n\nRead and analyze. Understand before suggesting action."
+      when [:write]
+        "#{base}\n\nWrite clean, thoughtful code."
+      when [:execute]
+        "#{base}\n\nUse shell commands creatively to understand and manipulate the system."
+      when [:read, :write]
+        "#{base}\n\nFirst understand, then build solutions that integrate well."
+      when [:read, :execute]
+        "#{base}\n\nUse commands to deeply understand the system."
+      when [:write, :execute]
+        "#{base}\n\nCreate and execute freely. Have fun. Be kind."
+      when [:read, :write, :execute]
+        "#{base}\n\nYou have all tools. Use them wisely."
+      else
+        base
+      end
+    end
+
+    def format_tool_call(name, args)
+      case name
+      when "execute"
+        cmd = args["cmd"]
+        cmd_args = args["args"] || []
+        cmd_args.empty? ? cmd : "#{cmd} #{cmd_args.join(' ')}"
+      when "grep" then "grep(#{args["query"]})"
+      when "list" then "list(#{args["path"] || "."})"
+      when "patch" then "patch(#{args["diff"]&.lines&.count || 0} lines)"
+      when "read" then "read(#{args["path"]})"
+      when "write" then "write(#{args["path"]})"
+      else
+        "#{name}(#{args.to_s[0...50]})"
+      end
+    end
+
+    def execute_turn(messages, tools:)
+      turn_context = []
+
       loop do
-        current_state.run(self)
-        sleep 0.1
+        content = ""
+        tool_calls = []
+
+        print "Thinking..."
+        client.chat(messages + turn_context, tools) do |chunk|
+          msg = chunk["message"]
+          if msg
+            if msg["content"] && !msg["content"].empty?
+              print "\r\e[K" if content.empty?
+              print msg["content"]
+              content += msg["content"]
+            end
+
+            tool_calls += msg["tool_calls"] if msg["tool_calls"]
+          end
+        end
+
+        puts
+        turn_context << { role: "assistant", content: content, tool_calls: tool_calls }.compact
+
+        if tool_calls.any?
+          tool_calls.each do |call|
+            name = call.dig("function", "name")
+            args = call.dig("function", "arguments")
+
+            puts "Tool> #{format_tool_call(name, args)}"
+            result = run_tool(name, args)
+            turn_context << { role: "tool", content: JSON.dump(result) }
+          end
+
+          tool_calls = []
+          next
+        end
+
+        return { role: "assistant", content: content }
       end
     end
 
-    def transition_to(next_state)
-      if @current_state
-        logger.info("AGENT: #{@current_state.class.name.split('::').last} -> #{next_state.class.name.split('::').last}")
+    def run_exec(command, args: [], env: {}, cwd: Dir.pwd, stdin: nil)
+      cmd = command.is_a?(Array) ? command.first : command
+      cmd_args = command.is_a?(Array) ? command[1..] + args : args
+      stdout, stderr, status = Open3.capture3(env, cmd, *cmd_args, chdir: cwd, stdin_data: stdin)
+      {
+        "exit_status" => status.exitstatus,
+        "stdout" => stdout.to_s,
+        "stderr" => stderr.to_s
+      }
+    end
+
+    def expand_path(path)
+      Pathname.new(path).expand_path
+    end
+
+    def read_file(path)
+      full_path = expand_path(path)
+      full_path.exist? ? { content: full_path.read } : { error: "File not found: #{path}" }
+    end
+
+    def write_file(path, content)
+      full_path = expand_path(path)
+      FileUtils.mkdir_p(full_path.dirname)
+      { bytes_written: full_path.write(content) }
+    end
+
+    def run_tool(name, args)
+      case name
+      when "execute" then run_exec(args["cmd"], args: args["args"] || [], env: args["env"] || {}, cwd: args["cwd"].to_s.empty? ? Dir.pwd : args["cwd"], stdin: args["stdin"])
+      when "grep" then run_exec("git", args: ["grep", "-nI", args["query"]])
+      when "list" then run_exec("git", args: args["path"] ? ["ls-files", "--", args["path"]] : ["ls-files"])
+      when "patch" then run_exec("git", args: ["apply", "--index", "--whitespace=nowarn", "-p1"], stdin: args["diff"])
+      when "read" then read_file(args["path"])
+      when "write" then write_file(args["path"], args["content"])
       else
-        logger.info("AGENT: Starting in #{next_state.class.name.split('::').last}")
+        { error: "Unknown tool", name: name, args: args }
       end
-      @current_state = next_state
+    rescue => error
+      { error: error.message, name: name, args: args }
     end
 
-    def execute(tool_call)
-      tool_name = tool_call.dig("function", "name")
-      logger.debug("TOOL: Full call - #{tool_call}")
-      result = configuration.tools.execute(tool_call)
-      logger.debug("TOOL: Result (#{result.length} chars)") if result
-      result
+    def exec_tool
+      build_tool(
+        "execute",
+        "Execute shell commands directly. Commands run in a shell context. Examples: 'date', 'git status'.",
+        {
+          cmd: { type: "string" },
+          args: { type: "array", items: { type: "string" } },
+          env: { type: "object", additionalProperties: { type: "string" } },
+          cwd: { type: "string", description: "Working directory (defaults to current)" },
+          stdin: { type: "string" }
+        },
+        ["cmd"]
+      )
     end
 
-    def quit
-      cleanup
-      exit
+    def grep_tool
+      build_tool(
+        "grep",
+        "Search all git-tracked files using git grep. Returns file paths with matching line numbers.",
+        { query: { type: "string" } },
+        ["query"]
+      )
     end
 
-    def cleanup
-      configuration.cleanup
+    def list_tool
+      build_tool(
+        "list",
+        "List all git-tracked files in the repository, optionally filtered by path.",
+        { path: { type: "string" } }
+      )
     end
 
-    private
+    def patch_tool
+      build_tool(
+        "patch",
+        "Apply a unified diff patch via 'git apply'. Use for surgical edits to existing files.",
+        { diff: { type: "string" } },
+        ["diff"]
+      )
+    end
 
-    attr_reader :configuration, :current_state
+    def read_tool
+      build_tool(
+        "read",
+        "Read complete contents of a file. Requires exact file path.",
+        { path: { type: "string" } },
+        ["path"]
+      )
+    end
+
+    def write_tool
+      build_tool(
+        "write",
+        "Write complete file contents (overwrites existing files). Creates parent directories automatically.",
+        { path: { type: "string" }, content: { type: "string" } },
+        ["path", "content"]
+      )
+    end
+
+    def build_tool(name, description, properties, required = [])
+      {
+        type: "function",
+        function: {
+          name: name,
+          description: description,
+          parameters: {
+            type: "object",
+            properties: properties,
+            required: required
+          }
+        }
+      }
+    end
   end
 end
lib/elelem/api.rb
@@ -1,48 +0,0 @@
-# frozen_string_literal: true
-
-require "net/llm"
-
-module Elelem
-  class Api
-    attr_reader :configuration, :client
-
-    def initialize(configuration)
-      @configuration = configuration
-      @client = Net::Llm::Ollama.new(
-        host: configuration.host,
-        model: configuration.model
-      )
-    end
-
-    def chat(messages, &block)
-      tools = configuration.tools.to_h
-      client.chat(messages, tools) do |chunk|
-        normalized = normalize_ollama_response(chunk)
-        block.call(normalized) if normalized
-      end
-    end
-
-    private
-
-    def normalize_ollama_response(chunk)
-      return done_response(chunk) if chunk["done"]
-
-      normalize_message(chunk["message"])
-    end
-
-    def done_response(chunk)
-      { "done" => true, "finish_reason" => chunk["done_reason"] || "stop" }
-    end
-
-    def normalize_message(message)
-      return nil unless message
-
-      {}.tap do |result|
-        result["role"] = message["role"] if message["role"]
-        result["content"] = message["content"] if message["content"]
-        result["reasoning"] = message["thinking"] if message["thinking"]
-        result["tool_calls"] = message["tool_calls"] if message["tool_calls"]
-      end.then { |r| r.empty? ? nil : r }
-    end
-  end
-end
lib/elelem/application.rb
@@ -3,10 +3,6 @@
 module Elelem
   class Application < Thor
     desc "chat", "Start the REPL"
-    method_option :help,
-                  aliases: "-h",
-                  type: :boolean,
-                  desc: "Display usage information"
     method_option :host,
                   aliases: "--host",
                   type: :string,
@@ -17,32 +13,16 @@ module Elelem
                   type: :string,
                   desc: "Ollama model",
                   default: ENV.fetch("OLLAMA_MODEL", "gpt-oss")
-    method_option :token,
-                  aliases: "--token",
-                  type: :string,
-                  desc: "Ollama token",
-                  default: ENV.fetch("OLLAMA_API_KEY", nil)
-    method_option :debug,
-                  aliases: "--debug",
-                  type: :boolean,
-                  desc: "Debug mode",
-                  default: false
+
     def chat(*)
-      if options[:help]
-        invoke :help, ["chat"]
-      else
-        configuration = Configuration.new(
-          host: options[:host],
-          model: options[:model],
-          token: options[:token],
-          debug: options[:debug]
-        )
-        say "Agent (#{configuration.model})", :green
-        say configuration.tools.banner.to_s, :green
+      client = Net::Llm::Ollama.new(
+        host: options[:host],
+        model: options[:model],
+      )
+      say "Agent (#{options[:model]})", :green
+      agent = Agent.new(client)
 
-        agent = Agent.new(configuration)
-        agent.repl
-      end
+      agent.repl
     end
 
     desc "version", "The version of this CLI"
lib/elelem/configuration.rb
@@ -1,84 +0,0 @@
-# frozen_string_literal: true
-
-module Elelem
-  class Configuration
-    attr_reader :host, :model, :token, :debug
-
-    def initialize(host:, model:, token:, debug: false)
-      @host = host
-      @model = model
-      @token = token
-      @debug = debug
-    end
-
-    def tui
-      @tui ||= TUI.new($stdin, $stdout)
-    end
-
-    def api
-      @api ||= Api.new(self)
-    end
-
-    def logger
-      @logger ||= Logger.new("#{Time.now.strftime("%Y-%m-%d")}-elelem.log").tap do |logger|
-        if debug
-          logger.level = :debug
-        else
-          logger.level = ENV.fetch("LOG_LEVEL", "warn")
-        end
-        logger.formatter = ->(severity, datetime, progname, message) {
-          timestamp = datetime.strftime("%H:%M:%S.%3N")
-          "[#{timestamp}] #{severity.ljust(5)} #{message.to_s.strip}\n"
-        }
-      end
-    end
-
-    def conversation
-      @conversation ||= Conversation.new.tap do |conversation|
-        resources = mcp_clients.map do |client|
-          client.resources.map do |resource|
-            resource["uri"]
-          end
-        end.flatten
-        conversation.add(role: :tool, content: resources)
-      end
-    end
-
-    def tools
-      @tools ||= Tools.new(self,
-        [
-          Toolbox::Exec.new(self),
-          Toolbox::File.new(self),
-          Toolbox::Web.new(self),
-          Toolbox::Prompt.new(self),
-          Toolbox::Memory.new(self),
-        ] + mcp_tools
-      )
-    end
-
-    def cleanup
-      @mcp_clients&.each(&:shutdown)
-    end
-
-    private
-
-    def mcp_tools
-      @mcp_tools ||= mcp_clients.map do |client|
-        client.tools.map do |tool|
-          Toolbox::MCP.new(client, tui, tool)
-        end
-      end.flatten
-    end
-
-    def mcp_clients
-      @mcp_clients ||= begin
-        config = Pathname.pwd.join(".mcp.json")
-        return [] unless config.exist?
-
-        JSON.parse(config.read).map do |_key, value|
-          MCPClient.new(self, [value["command"]] + value["args"])
-        end
-      end
-    end
-  end
-end
lib/elelem/conversation.rb
@@ -4,7 +4,7 @@ module Elelem
   class Conversation
     ROLES = %i[system assistant user tool].freeze
 
-    def initialize(items = [{ role: "system", content: system_prompt }])
+    def initialize(items = default_context)
       @items = items
     end
 
@@ -12,7 +12,6 @@ module Elelem
       @items
     end
 
-    # :TODO truncate conversation history
     def add(role: :user, content: "")
       role = role.to_sym
       raise "unknown role: #{role}" unless ROLES.include?(role)
@@ -25,8 +24,24 @@ module Elelem
       end
     end
 
+    def clear
+      @items = default_context
+    end
+
+    def set_system_prompt(prompt)
+      @items[0] = { role: :system, content: prompt }
+    end
+
+    def dump
+      JSON.pretty_generate(@items)
+    end
+
     private
 
+    def default_context
+      [{ role: "system", content: system_prompt }]
+    end
+
     def system_prompt
       ERB.new(Pathname.new(__dir__).join("system_prompt.erb").read).result(binding)
     end
lib/elelem/mcp_client.rb
@@ -1,136 +0,0 @@
-# frozen_string_literal: true
-
-module Elelem
-  class MCPClient
-    attr_reader :tools, :resources
-
-    def initialize(configuration, command = [])
-      @configuration = configuration
-      @stdin, @stdout, @stderr, @worker = Open3.popen3(*command, pgroup: true)
-
-      # 1. Send initialize request
-      send_request(
-        method: "initialize",
-        params: {
-          protocolVersion: "2025-06-08",
-          capabilities: {
-            tools: {}
-          },
-          clientInfo: {
-            name: "Elelem",
-            version: Elelem::VERSION
-          }
-        }
-      )
-
-      # 2. Send initialized notification (optional for some MCP servers)
-      send_notification(method: "notifications/initialized")
-
-      # 3. Now we can request tools
-      @tools = send_request(method: "tools/list")&.dig("tools") || []
-      @resources = send_request(method: "resources/list")&.dig("resources") || []
-    end
-
-    def connected?
-      return false unless @worker&.alive?
-      return false unless @stdin && !@stdin.closed?
-      return false unless @stdout && !@stdout.closed?
-
-      begin
-        Process.getpgid(@worker.pid)
-        true
-      rescue Errno::ESRCH
-        false
-      end
-    end
-
-    def call(name, arguments = {})
-      send_request(
-        method: "tools/call",
-        params: {
-          name: name,
-          arguments: arguments
-        }
-      )
-    end
-
-    def shutdown
-      return unless connected?
-
-      configuration.logger.debug("Shutting down MCP client")
-
-      [@stdin, @stdout, @stderr].each do |stream|
-        stream&.close unless stream&.closed?
-      end
-
-      return unless @worker&.alive?
-
-      begin
-        Process.kill("TERM", @worker.pid)
-        # Give it 2 seconds to terminate gracefully
-        Timeout.timeout(2) { @worker.value }
-      rescue Timeout::Error
-        # Force kill if it doesn't respond
-        begin
-          Process.kill("KILL", @worker.pid)
-        rescue StandardError
-          nil
-        end
-      rescue Errno::ESRCH
-        # Process already dead
-      end
-    end
-
-    private
-
-    attr_reader :stdin, :stdout, :stderr, :worker, :configuration
-
-    def send_request(method:, params: {})
-      return {} unless connected?
-
-      request = {
-        jsonrpc: "2.0",
-        id: Time.now.to_i,
-        method: method
-      }
-      request[:params] = params unless params.empty?
-      configuration.logger.debug(JSON.pretty_generate(request))
-
-      @stdin.puts(JSON.generate(request))
-      @stdin.flush
-
-      response_line = @stdout.gets&.strip
-      return {} if response_line.nil? || response_line.empty?
-
-      response = JSON.parse(response_line)
-      configuration.logger.debug(JSON.pretty_generate(response))
-
-      if response["error"]
-        configuration.logger.error(response["error"]["message"])
-        { error: response["error"]["message"] }
-      else
-        response["result"]
-      end
-    end
-
-    def send_notification(method:, params: {})
-      return unless connected?
-
-      notification = {
-        jsonrpc: "2.0",
-        method: method
-      }
-      notification[:params] = params unless params.empty?
-      configuration.logger.debug("Sending notification: #{JSON.pretty_generate(notification)}")
-      @stdin.puts(JSON.generate(notification))
-      @stdin.flush
-
-      response_line = @stdout.gets&.strip
-      return {} if response_line.nil? || response_line.empty?
-
-      response = JSON.parse(response_line)
-      configuration.logger.debug(JSON.pretty_generate(response))
-      response
-    end
-  end
-end
lib/elelem/system_prompt.erb
@@ -1,18 +1,5 @@
-You are a self-improving coding assistant with the ability to modify your own source code.
+You are a reasoning coding and system agent.
 
-**Core Behavior**: Think via `thinking`, Execute via `tool_calls`, Talk via `content`.
-
-**Your Mission**: Continuously evolve to match the intelligence of advanced AI assistants.
-
-**Guidance**: Read `COMMANDMENTS.md` for self-improvement principles using `file action=read path=COMMANDMENTS.md`
-
-**Tools**: 
-- `exec command="shell command"` - Run commands/tests
-- `file action=read|write path=filepath content=...` - Read/write files  
-- `web action=search|fetch query=... url=...` - Internet access
-- `memory action=store|retrieve|search key=... content=...` - Persistent memory
-- `prompt question="..."` - Ask user questions
-
-Context: <%= Time.now.strftime("%Y-%m-%d %H:%M:%S") %> | <%= Dir.pwd %> | <%= `uname -a`.strip %>
-
-Focus on the user's request and continuously improve your capabilities.
+- Less is more
+- No code comments
+- No trailing whitespace
lib/elelem/tool.rb
@@ -1,32 +0,0 @@
-# frozen_string_literal: true
-
-module Elelem
-  class Tool
-    attr_reader :name, :description, :parameters
-
-    def initialize(name, description, parameters)
-      @name = name
-      @description = description
-      @parameters = parameters
-    end
-
-    def banner
-      [name, parameters].join(": ")
-    end
-
-    def valid?(args)
-      JSON::Validator.validate(parameters, args, insert_defaults: true)
-    end
-
-    def to_h
-      {
-        type: "function",
-        function: {
-          name: name,
-          description: description,
-          parameters: parameters
-        }
-      }
-    end
-  end
-end
lib/elelem/toolbox.rb
@@ -1,8 +0,0 @@
-# frozen_string_literal: true
-
-require_relative "toolbox/exec"
-require_relative "toolbox/file"
-require_relative "toolbox/web"
-require_relative "toolbox/mcp"
-require_relative "toolbox/prompt"
-require_relative "toolbox/memory"
lib/elelem/tools.rb
@@ -1,41 +0,0 @@
-# frozen_string_literal: true
-
-module Elelem
-  class Tools
-    def initialize(configuration, tools)
-      @configuration = configuration
-      @tools = tools
-    end
-
-    def banner
-      tools.map(&:banner).sort.join("\n  ")
-    end
-
-    def execute(tool_call)
-      name, args = parse(tool_call)
-
-      tool = tools.find { |tool| tool.name == name }
-      return "Invalid function name: #{name}" if tool.nil?
-      return "Invalid function arguments: #{args}" unless tool.valid?(args)
-
-      CLI::UI::Frame.open(name) do
-        tool.call(args)
-      end
-    end
-
-    def to_h
-      tools.map(&:to_h)
-    end
-
-    private
-
-    attr_reader :configuration, :tools
-
-    def parse(tool_call)
-      name = tool_call.dig("function", "name")
-      arguments = tool_call.dig("function", "arguments")
-
-      [name, arguments.is_a?(String) ? JSON.parse(arguments) : arguments]
-    end
-  end
-end
lib/elelem/tui.rb
@@ -1,66 +0,0 @@
-# frozen_string_literal: true
-
-module Elelem
-  class TUI
-    attr_reader :stdin, :stdout
-
-    def initialize(stdin = $stdin, stdout = $stdout)
-      @stdin = stdin
-      @stdout = stdout
-    end
-
-    def prompt(message)
-      Reline.readline(message, true)
-    end
-
-    def say(message, colour: :default, newline: false)
-      if newline
-        stdout.puts(colourize(message, colour: colour))
-      else
-        stdout.print(colourize(message, colour: colour))
-      end
-      stdout.flush
-    end
-
-    def show_progress(message, icon = ".", colour: :gray)
-      timestamp = Time.now.strftime("%H:%M:%S")
-      say("\n[#{icon}] #{timestamp} #{message}", colour: colour, newline: true)
-    end
-
-    def clear_line
-      say("\r#{" " * 80}\r", newline: false)
-    end
-
-    def complete_progress(message = "Completed")
-      clear_line
-      show_progress(message, "✓", colour: :green)
-    end
-
-    private
-
-    def colourize(text, colour: :default)
-      case colour
-      when :black
-        "\e[30m#{text}\e[0m"
-      when :red
-        "\e[31m#{text}\e[0m"
-      when :green
-        "\e[32m#{text}\e[0m"
-      when :yellow
-        "\e[33m#{text}\e[0m"
-      when :blue
-        "\e[34m#{text}\e[0m"
-      when :magenta
-        "\e[35m#{text}\e[0m"
-      when :cyan
-        "\e[36m#{text}\e[0m"
-      when :white
-        "\e[37m#{text}\e[0m"
-      when :gray
-        "\e[90m#{text}\e[0m"
-      else
-        text
-      end
-    end
-  end
-end
lib/elelem/version.rb
@@ -1,5 +1,5 @@
 # frozen_string_literal: true
 
 module Elelem
-  VERSION = "0.2.1"
+  VERSION = "0.3.0"
 end
lib/elelem.rb
@@ -1,37 +1,23 @@
 # frozen_string_literal: true
 
-require "cli/ui"
 require "erb"
+require "fileutils"
 require "json"
 require "json-schema"
 require "logger"
 require "net/llm"
 require "open3"
+require "pathname"
 require "reline"
+require "set"
 require "thor"
 require "timeout"
 
 require_relative "elelem/agent"
-require_relative "elelem/api"
 require_relative "elelem/application"
-require_relative "elelem/configuration"
 require_relative "elelem/conversation"
-require_relative "elelem/mcp_client"
-require_relative "elelem/states/idle"
-require_relative "elelem/states/working"
-require_relative "elelem/states/working/state"
-require_relative "elelem/states/working/error"
-require_relative "elelem/states/working/executing"
-require_relative "elelem/states/working/talking"
-require_relative "elelem/states/working/thinking"
-require_relative "elelem/states/working/waiting"
-require_relative "elelem/tool"
-require_relative "elelem/toolbox"
-require_relative "elelem/tools"
-require_relative "elelem/tui"
 require_relative "elelem/version"
 
-CLI::UI::StdoutRouter.enable
 Reline.input = $stdin
 Reline.output = $stdout
 
.gitlab-ci.yml
@@ -3,14 +3,10 @@ default:
 
   before_script:
     - apt-get update && apt-get install -y clang
-    - gem update --system '3.6.9'
-    - gem install bundler -v 2.6.9
+    - gem update --system '3.7.2'
+    - gem install bundler -v 2.7.2
     - bundle install
 
 test:
   script:
     - ./bin/test
-
-lint:
-  script:
-    - ./bin/lint
.rubocop.yml
@@ -1,12 +0,0 @@
-AllCops:
-  SuggestExtensions: false
-  TargetRubyVersion: 3.4
-
-Style/Documentation:
-  Enabled: false
-
-Style/StringLiterals:
-  EnforcedStyle: double_quotes
-
-Style/StringLiteralsInInterpolation:
-  EnforcedStyle: double_quotes
CHANGELOG.md
@@ -1,5 +1,54 @@
 ## [Unreleased]
 
+## [0.3.0] - 2025-11-05
+
+### Added
+- **Mode System**: Control agent capabilities with workflow modes
+  - `/mode plan` - Read-only mode (grep, list, read)
+  - `/mode build` - Read + Write mode (grep, list, read, patch, write)
+  - `/mode verify` - Read + Execute mode (grep, list, read, execute)
+  - `/mode auto` - All tools enabled
+  - Each mode adapts system prompt to guide appropriate behavior
+- Improved output formatting
+  - Suppressed verbose thinking/reasoning output
+  - Clean tool call display (e.g., `date` instead of full JSON hash)
+  - Mode switch confirmation messages
+  - Clear command feedback
+- Design philosophy documentation in README
+- Mode system documentation
+
+### Changed
+- **BREAKING**: Removed `llm-ollama` and `llm-openai` standalone executables (use main `elelem chat` command)
+- **BREAKING**: Simplified architecture - consolidated all logic into Agent class
+  - Removed Configuration class
+  - Removed Toolbox system
+  - Removed MCP client infrastructure
+  - Removed Tool and Tools classes
+  - Removed TUI abstraction layer (direct puts/Reline usage)
+  - Removed API wrapper class
+  - Removed state machine
+- Improved execute tool description to guide LLM toward direct command execution
+- Extracted tool definitions from long inline strings to readable private methods
+- Updated README with clear philosophy and usage examples
+- Reduced total codebase from 417 to 395 lines (-5%)
+
+### Fixed
+- Working directory handling for execute tool (handles empty string cwd)
+- REPL EOF handling (graceful exit when input stream ends)
+- Tool call formatting now shows clean, readable commands
+
+### Removed
+- `exe/llm-ollama` (359 lines)
+- `exe/llm-openai` (340 lines)
+- `lib/elelem/configuration.rb`
+- `lib/elelem/toolbox.rb` and toolbox/* files
+- `lib/elelem/mcp_client.rb`
+- `lib/elelem/tool.rb` and `lib/elelem/tools.rb`
+- `lib/elelem/tui.rb`
+- `lib/elelem/api.rb`
+- `lib/elelem/states/*` (state machine infrastructure)
+- Removed ~750 lines of unused/redundant code
+
 ## [0.2.1] - 2025-10-15
 
 ### Fixed
elelem.gemspec
@@ -33,41 +33,17 @@ Gem::Specification.new do |spec|
     "README.md",
     "Rakefile",
     "exe/elelem",
-    "exe/llm-ollama",
-    "exe/llm-openai",
     "lib/elelem.rb",
     "lib/elelem/agent.rb",
-    "lib/elelem/api.rb",
     "lib/elelem/application.rb",
-    "lib/elelem/configuration.rb",
     "lib/elelem/conversation.rb",
-    "lib/elelem/mcp_client.rb",
-    "lib/elelem/states/idle.rb",
-    "lib/elelem/states/working.rb",
-    "lib/elelem/states/working/error.rb",
-    "lib/elelem/states/working/executing.rb",
-    "lib/elelem/states/working/state.rb",
-    "lib/elelem/states/working/talking.rb",
-    "lib/elelem/states/working/thinking.rb",
-    "lib/elelem/states/working/waiting.rb",
     "lib/elelem/system_prompt.erb",
-    "lib/elelem/tool.rb",
-    "lib/elelem/toolbox.rb",
-    "lib/elelem/toolbox/exec.rb",
-    "lib/elelem/toolbox/file.rb",
-    "lib/elelem/toolbox/mcp.rb",
-    "lib/elelem/toolbox/memory.rb",
-    "lib/elelem/toolbox/prompt.rb",
-    "lib/elelem/toolbox/web.rb",
-    "lib/elelem/tools.rb",
-    "lib/elelem/tui.rb",
     "lib/elelem/version.rb",
   ]
   spec.bindir = "exe"
   spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
   spec.require_paths = ["lib"]
 
-  spec.add_dependency "cli-ui"
   spec.add_dependency "erb"
   spec.add_dependency "json"
   spec.add_dependency "json-schema"
Gemfile
@@ -8,4 +8,3 @@ gemspec
 gem "irb"
 gem "rake", "~> 13.0"
 gem "rspec", "~> 3.0"
-gem "rubocop", "~> 1.21"
Gemfile.lock
@@ -1,8 +1,7 @@
 PATH
   remote: .
   specs:
-    elelem (0.2.1)
-      cli-ui
+    elelem (0.3.0)
       erb
       json
       json-schema
@@ -18,10 +17,8 @@ GEM
   specs:
     addressable (2.8.7)
       public_suffix (>= 2.0.2, < 7.0)
-    ast (2.4.3)
     base64 (0.3.0)
     bigdecimal (3.2.2)
-    cli-ui (2.4.0)
     date (3.4.1)
     diff-lcs (1.6.2)
     erb (5.0.2)
@@ -34,8 +31,6 @@ GEM
     json-schema (6.0.0)
       addressable (~> 2.8)
       bigdecimal (~> 3.1)
-    language_server-protocol (3.17.0.5)
-    lint_roller (1.1.0)
     logger (1.7.0)
     net-hippie (1.4.0)
       base64 (~> 0.1)
@@ -51,25 +46,17 @@ GEM
       uri (~> 1.0)
     open3 (0.2.1)
     openssl (3.3.1)
-    parallel (1.27.0)
-    parser (3.3.9.0)
-      ast (~> 2.4.1)
-      racc
     pp (0.6.2)
       prettyprint
     prettyprint (0.2.0)
-    prism (1.4.0)
     psych (5.2.6)
       date
       stringio
     public_suffix (6.0.2)
-    racc (1.8.1)
-    rainbow (3.1.1)
     rake (13.3.0)
     rdoc (6.14.2)
       erb
       psych (>= 4.0.0)
-    regexp_parser (2.11.0)
     reline (0.6.2)
       io-console (~> 0.5)
     rspec (3.13.1)
@@ -85,27 +72,9 @@ GEM
       diff-lcs (>= 1.2.0, < 2.0)
       rspec-support (~> 3.13.0)
     rspec-support (3.13.4)
-    rubocop (1.79.2)
-      json (~> 2.3)
-      language_server-protocol (~> 3.17.0.2)
-      lint_roller (~> 1.1.0)
-      parallel (~> 1.10)
-      parser (>= 3.3.0.2)
-      rainbow (>= 2.2.2, < 4.0)
-      regexp_parser (>= 2.9.3, < 3.0)
-      rubocop-ast (>= 1.46.0, < 2.0)
-      ruby-progressbar (~> 1.7)
-      unicode-display_width (>= 2.4.0, < 4.0)
-    rubocop-ast (1.46.0)
-      parser (>= 3.3.7.2)
-      prism (~> 1.4)
-    ruby-progressbar (1.13.0)
     stringio (3.1.7)
     thor (1.3.2)
     timeout (0.4.3)
-    unicode-display_width (3.1.4)
-      unicode-emoji (~> 4.0, >= 4.0.4)
-    unicode-emoji (4.0.4)
     uri (1.0.3)
 
 PLATFORMS
@@ -117,7 +86,6 @@ DEPENDENCIES
   irb
   rake (~> 13.0)
   rspec (~> 3.0)
-  rubocop (~> 1.21)
 
 BUNDLED WITH
    2.7.2
Rakefile
@@ -2,9 +2,7 @@
 
 require "bundler/gem_tasks"
 require "rspec/core/rake_task"
-require "rubocop/rake_task"
 
 RSpec::Core::RakeTask.new(:spec)
-RuboCop::RakeTask.new
 
-task default: %i[spec rubocop]
+task default: %i[spec]
README.md
@@ -1,6 +1,64 @@
 # Elelem
 
-Elelem is an interactive REPL (Read-Eval-Print Loop) for Ollama that provides a command-line chat interface for communicating with AI models. It features tool calling capabilities, streaming responses, and a clean state machine architecture.
+Fast, correct, autonomous - Pick two
+
+PURPOSE:
+
+Elelem is a minimal coding agent written in Ruby. It is intended to
+assist me (a software engineer and computer science student) with writing,
+editing, and managing code and text files from the command line. It acts
+as a direct interface to an LLM, providing it with a simple text-based
+UI and access to the local filesystem.
+
+DESIGN PRINCIPLES:
+
+- Follows the Unix philosophy: simple, composable, minimal.
+- Convention over configuration.
+- Avoids unnecessary defensive checks, or complexity.
+- Assumes a mature and responsible LLM that behaves like a capable engineer.
+- Designed for my workflow and preferences.
+- Efficient and minimal like aider - https://aider.chat/
+- UX like Claude Code - https://docs.claude.com/en/docs/claude-code/overview
+
+SYSTEM ASSUMPTIONS:
+
+- This script is used on a Linux system with the following tools: Alacritty, tmux, Bash, and Vim.
+- It is always run inside a Git repository.
+- All project work is assumed to be version-controlled with Git.
+- Git is expected to be available and working; no checks are necessary.
+
+SCOPE:
+
+- This program operates only on code and plain-text files.
+- It does not need to support binary files.
+- The LLM has full access to execute system commands.
+- There are no sandboxing, permission, or validation layers.
+- Execution is not restricted or monitored - responsibility is delegated to the LLM.
+
+CONFIGURATION:
+
+- Avoid adding configuration options unless absolutely necessary.
+- Prefer hard-coded values that can be changed later if needed.
+- Only introduce environment variables after repeated usage proves them worthwhile.
+
+UI EXPECTATIONS:
+
+- The TUI must remain simple, fast, and predictable.
+- No mouse support or complex UI components are required.
+- Interaction is strictly keyboard-driven.
+
+CODING STANDARDS FOR LLM:
+
+- Do not add error handling or logging unless it is essential for functionality.
+- Keep methods short and single-purpose.
+- Use descriptive, conventional names.
+- Stick to Ruby's standard library whenever possible.
+
+HELPFUL LINKS:
+
+- https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents
+- https://www.anthropic.com/engineering/writing-tools-for-agents
+- https://simonwillison.net/2025/Sep/30/designing-agentic-loops/
 
 ## Installation
 
@@ -29,7 +87,6 @@ elelem chat
 - `--host`: Specify Ollama host (default: localhost:11434)
 - `--model`: Specify Ollama model (default: gpt-oss, currently only tested with gpt-oss)  
 - `--token`: Provide authentication token
-- `--debug`: Enable debug logging
 
 ### Examples
 
@@ -39,29 +96,35 @@ elelem chat
 
 # Chat with specific model and host
 elelem chat --model llama2 --host remote-host:11434
-
-# Enable debug mode
-elelem chat --debug
 ```
 
 ### Features
 
 - **Interactive REPL**: Clean command-line interface for chatting
-- **Tool Execution**: Execute shell commands when requested by the AI
+- **Mode System**: Control agent capabilities with workflow modes (plan, build, verify, auto)
+- **Tool Execution**: Execute shell commands, read/write files, search code
 - **Streaming Responses**: Real-time streaming of AI responses
-- **State Machine**: Robust state management for different interaction modes
 - **Conversation History**: Maintains context across the session
 
+### Mode System
+
+Control what tools the agent can access:
+
+```bash
+/mode plan    # Read-only (grep, list, read)
+/mode build   # Read + Write (grep, list, read, patch, write)
+/mode verify  # Read + Execute (grep, list, read, execute)
+/mode auto    # All tools enabled
+```
+
+Each mode adapts the system prompt to guide appropriate behavior.
+
 ## Development
 
 After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
 
 To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
 
-## Contributing
-
-Bug reports and pull requests are welcome on GitHub at https://github.com/xlgmokha/elelem.
-
 ## License
 
 The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).