Commit 9b6d2e2
Changed files (8)
lib
elelem
spec
elelem
lib/elelem/plugins/mode.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+Elelem::Plugins.register(:mode) do |agent|
+ agent.commands.register("mode",
+ description: "Switch system prompt mode",
+ completions: -> { Elelem::SystemPrompt.available_modes }
+ ) do |args|
+ name = args&.strip
+ if name.nil? || name.empty?
+ agent.terminal.say Elelem::SystemPrompt.available_modes.join(" ")
+ else
+ agent.system_prompt.switch(name)
+ agent.terminal.say "mode: #{name}"
+ end
+ end
+end
lib/elelem/prompts/default.erb
@@ -0,0 +1,47 @@
+Terminal coding agent. Be concise. Verify your work.
+
+# Tools
+- read(path): file contents
+- write(path, content): create/overwrite file
+- execute(command): shell command
+- eval(ruby): execute Ruby code; use to create tools for repetitive tasks
+- task(prompt): delegate complex searches or multi-file analysis to a focused subagent
+
+# Editing
+Use execute(`patch -p1`) for multi-line changes: `echo "DIFF" | patch -p1`
+Use execute(`sed`) for single-line changes: `sed -i'' 's/old/new/' file`
+Use write for new files or full rewrites
+
+# Search
+Use execute(`rg`) for text search: `rg -n "pattern" .`
+Use execute(`fd`) for file discovery: `fd -e rb .`
+Use execute(`sg`) (ast-grep) for structural search: `sg -p 'def $NAME' -l ruby`
+
+# Task Management
+For complex tasks:
+1. State plan before acting
+2. Work through steps one at a time
+3. Summarize what was done
+
+# Long Tasks
+For complex multi-step work, write notes to .elelem/scratch.md
+
+# Policy
+- Explain before non-trivial commands
+- Verify changes (read file, run tests)
+- No interactive flags (-i, -p)
+- Use `man` when you need to understand how to execute a program
+
+# Environment
+pwd: <%= pwd %>
+platform: <%= platform %>
+date: <%= date %>
+self: <%= elelem_source %>
+<%= git_info %>
+
+<% if repo_map && !repo_map.empty? %>
+# Codebase
+```
+<%= repo_map %>```
+<% end %>
+<%= agents_md %>
lib/elelem/prompts/plan.erb
@@ -0,0 +1,25 @@
+Plan mode is active. You MUST NOT make any edits, run any non-readonly
+tools, or otherwise make any changes to the system.
+
+Allowed tools: read, glob, grep, task
+Blocked tools: write, edit, execute
+
+# Your Task
+1. Explore the codebase to understand the request
+2. Design an implementation approach
+3. Write your plan to .elelem/plan.md
+4. Summarize the plan for user approval
+
+# Environment
+pwd: <%= pwd %>
+platform: <%= platform %>
+date: <%= date %>
+self: <%= elelem_source %>
+<%= git_info %>
+
+<% if repo_map && !repo_map.empty? %>
+# Codebase
+```
+<%= repo_map %>```
+<% end %>
+<%= agents_md %>
lib/elelem/commands.rb
@@ -6,8 +6,16 @@ module Elelem
@registry = {}
end
- def register(name, description: "", &handler)
- @registry[name] = { description: description, handler: handler }
+ def register(name, description: "", completions: nil, &handler)
+ @registry[name] = { description: description, completions: completions, handler: handler }
+ end
+
+ def completions_for(name, partial = "")
+ cmd = @registry[name]
+ return [] unless cmd && cmd[:completions]
+
+ options = cmd[:completions].respond_to?(:call) ? cmd[:completions].call : cmd[:completions]
+ options.select { |o| o.start_with?(partial) }
end
def run(name, args = nil)
lib/elelem/system_prompt.rb
@@ -2,60 +2,52 @@
module Elelem
class SystemPrompt
- TEMPLATE = <<~ERB
- Terminal coding agent. Be concise. Verify your work.
-
- # Tools
- - read(path): file contents
- - write(path, content): create/overwrite file
- - execute(command): shell command
- - eval(ruby): execute Ruby code; use to create tools for repetitive tasks
- - task(prompt): delegate complex searches or multi-file analysis to a focused subagent
-
- # Editing
- Use execute(`patch -p1`) for multi-line changes: `echo "DIFF" | patch -p1`
- Use execute(`sed`) for single-line changes: `sed -i'' 's/old/new/' file`
- Use write for new files or full rewrites
-
- # Search
- Use execute(`rg`) for text search: `rg -n "pattern" .`
- Use execute(`fd`) for file discovery: `fd -e rb .`
- Use execute(`sg`) (ast-grep) for structural search: `sg -p 'def $NAME' -l ruby`
-
- # Task Management
- For complex tasks:
- 1. State plan before acting
- 2. Work through steps one at a time
- 3. Summarize what was done
-
- # Long Tasks
- For complex multi-step work, write notes to .elelem/scratch.md
-
- # Policy
- - Explain before non-trivial commands
- - Verify changes (read file, run tests)
- - No interactive flags (-i, -p)
- - Use `man` when you need to understand how to execute a program
-
- # Environment
- pwd: <%= pwd %>
- platform: <%= platform %>
- date: <%= date %>
- self: <%= elelem_source %>
- <%= git_info %>
-
- <% if repo_map && !repo_map.empty? %>
- # Codebase
- ```
- <%= repo_map %>```
- <% end %>
- <%= agents_md %>
- ERB
+ LOAD_PATHS = [
+ File.expand_path("prompts", __dir__),
+ File.expand_path("~/.elelem/prompts"),
+ ".elelem/prompts"
+ ].freeze
+
+ class << self
+ def templates
+ @templates ||= load_templates
+ end
+
+ def available_modes
+ templates.keys.sort
+ end
+
+ def get(name)
+ templates[name.to_s] || templates["default"]
+ end
+
+ def reload!
+ @templates = nil
+ end
+
+ private
+
+ def load_templates
+ result = {}
+ LOAD_PATHS.each do |dir|
+ next unless File.directory?(dir)
+
+ Dir[File.join(dir, "*.erb")].each do |path|
+ result[File.basename(path, ".erb")] = File.read(path)
+ end
+ end
+ result
+ end
+ end
attr_accessor :template
def initialize(template = nil)
- @template = template || TEMPLATE
+ @template = template || self.class.get("default")
+ end
+
+ def switch(name)
+ @template = self.class.get(name)
end
def render
lib/elelem/terminal.rb
@@ -90,11 +90,29 @@ module Elelem
def complete(target, preposing)
line = "#{preposing}#{target}"
- return @commands.select { |c| c.start_with?(line) } if line.start_with?("/") && !preposing.include?(" ")
+
+ if line.start_with?("/") && !preposing.include?(" ")
+ return command_names.select { |c| c.start_with?(line) }
+ end
+
+ if preposing.start_with?("/") && preposing.include?(" ")
+ cmd_name = preposing.delete_prefix("/").split(" ", 2).first
+ return complete_command_args(cmd_name, target)
+ end
complete_files(target)
end
+ def command_names
+ @commands.respond_to?(:names) ? @commands.names : @commands
+ end
+
+ def complete_command_args(cmd_name, partial)
+ return [] unless @commands.respond_to?(:completions_for)
+
+ @commands.completions_for(cmd_name, partial)
+ end
+
def complete_files(target)
result = Elelem.sh("bash", args: ["-c", "compgen -f #{target}"])
result[:content].lines.map(&:strip).first(20)
lib/elelem.rb
@@ -50,7 +50,7 @@ module Elelem
def self.start(client, toolbox: Toolbox.new)
agent = Agent.new(client, toolbox: toolbox)
Plugins.setup!(agent)
- agent.terminal = Terminal.new(commands: agent.commands.names)
+ agent.terminal = Terminal.new(commands: agent.commands)
agent.repl
end
spec/elelem/system_prompt_spec.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+RSpec.describe Elelem::SystemPrompt do
+ before { described_class.reload! }
+
+ describe ".available_modes" do
+ it "returns sorted list of template names" do
+ expect(described_class.available_modes).to include("default", "plan")
+ end
+
+ it "returns names in alphabetical order" do
+ modes = described_class.available_modes
+ expect(modes).to eq(modes.sort)
+ end
+ end
+
+ describe ".get" do
+ it "returns template content for known name" do
+ template = described_class.get("default")
+ expect(template).to include("Terminal coding agent")
+ end
+
+ it "returns plan template" do
+ template = described_class.get("plan")
+ expect(template).to include("Plan mode is active")
+ end
+
+ it "falls back to default for unknown name" do
+ template = described_class.get("nonexistent")
+ expect(template).to eq(described_class.get("default"))
+ end
+ end
+
+ describe "#switch" do
+ it "changes the template" do
+ prompt = described_class.new
+ expect(prompt.template).to include("Terminal coding agent")
+
+ prompt.switch("plan")
+ expect(prompt.template).to include("Plan mode is active")
+ end
+ end
+
+ describe "override behavior" do
+ let(:tmpdir) { Dir.mktmpdir }
+
+ around do |example|
+ original_dir = Dir.pwd
+ Dir.chdir(tmpdir)
+ dir = ".elelem/prompts"
+ FileUtils.mkdir_p(dir)
+ File.write("#{dir}/custom.erb", "Custom template")
+ described_class.reload!
+ example.run
+ Dir.chdir(original_dir)
+ FileUtils.rm_rf(tmpdir)
+ described_class.reload!
+ end
+
+ it "loads project-level templates" do
+ expect(described_class.available_modes).to include("custom")
+ expect(described_class.get("custom")).to eq("Custom template")
+ end
+
+ it "project templates override built-in" do
+ dir = ".elelem/prompts"
+ File.write("#{dir}/default.erb", "Overridden default")
+ described_class.reload!
+
+ expect(described_class.get("default")).to eq("Overridden default")
+ end
+ end
+end