Commit 411aac8

mo khan <mo@mokhan.ca>
2026-01-22 05:36:08
refactor: extract class to generate a system prompt using ERB
1 parent 65ed0ec
lib/elelem/plugins/verify.rb
@@ -4,6 +4,7 @@ module Elelem
   module Verifiers
     SYNTAX = {
       ".rb" => "ruby -c %{path}",
+      ".erb" => "erb -x %{path} | ruby -c",
       ".py" => "python -m py_compile %{path}",
       ".go" => "go vet %{path}",
       ".rs" => "cargo check --quiet",
lib/elelem/templates/system_prompt.erb
@@ -0,0 +1,52 @@
+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 %>
+<%= git_branch %>
+
+# Codebase
+<%= repo_map %>
+<% if agents_md %>
+
+# Project Instructions
+<%= agents_md %>
+<% end %>
+<% if memory %>
+
+# Earlier Context
+<%= memory %>
+<% end %>
lib/elelem/agent.rb
@@ -52,7 +52,9 @@ module Elelem
       case input
       when "/exit" then exit(0)
       when "/init" then init_agents_md
-      when "/shell" then history << { role: "user", content: start_shell }
+      when "/shell"
+        transcript = start_shell
+        history << { role: "user", content: transcript } unless transcript.strip.empty?
       when "/clear"
         @history = []
         @memory = nil
@@ -141,13 +143,11 @@ module Elelem
     end
 
     def combined_history
-      [{ role: "system", content: system_prompt_with_memory }] + history
+      [{ role: "system", content: system_prompt }] + history
     end
 
-    def system_prompt_with_memory
-      prompt = @system_prompt || default_system_prompt
-      prompt += "\n\n# Earlier Context\n#{@memory}" if @memory
-      prompt
+    def system_prompt
+      @system_prompt || SystemPrompt.new(memory: @memory).render
     end
 
     def compact_if_needed
@@ -171,79 +171,5 @@ module Elelem
         end
       end
     end
-
-    def default_system_prompt
-      prompt = <<~PROMPT
-        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` to when you need to understand how to execute a program
-
-        # Environment
-        pwd: #{Dir.pwd}
-        platform: #{RUBY_PLATFORM.split("-").last}
-        date: #{Date.today}
-        #{git_branch}
-
-        # Codebase
-        #{repo_map}
-      PROMPT
-      prompt += "\n\n# Project Instructions\n#{agents_md}" if agents_md
-      prompt
-    end
-
-    def agents_md
-      Pathname.pwd.ascend.each do |dir|
-        file = dir / "AGENTS.md"
-        return file.read if file.exist?
-      end
-      nil
-    end
-
-    def git_branch
-      return unless File.exist?(".git")
-      "branch: #{`git branch --show-current`.strip}"
-    rescue
-      nil
-    end
-
-    def repo_map
-      `ctags -x --sort=no --languages=Ruby,Python,JavaScript,TypeScript,Go,Rust -R . 2>/dev/null`
-        .lines
-        .reject { |l| l.include?("vendor/") || l.include?("node_modules/") || l.include?("spec/") }
-        .first(100)
-        .join
-    rescue
-      ""
-    end
   end
 end
lib/elelem/system_prompt.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+module Elelem
+  class SystemPrompt
+    TEMPLATE_PATH = File.expand_path("templates/system_prompt.erb", __dir__)
+
+    attr_reader :memory
+
+    def initialize(memory: nil)
+      @memory = memory
+    end
+
+    def render
+      ERB.new(template, trim_mode: "-").result(binding)
+    end
+
+    private
+
+    def template
+      File.read(TEMPLATE_PATH)
+    end
+
+    def pwd
+      Dir.pwd
+    end
+
+    def platform
+      RUBY_PLATFORM.split("-").last
+    end
+
+    def date
+      Date.today
+    end
+
+    def git_branch
+      return unless File.exist?(".git")
+
+      "branch: #{`git branch --show-current`.strip}"
+    rescue
+      nil
+    end
+
+    def repo_map
+      `ctags -x --sort=no --languages=Ruby,Python,JavaScript,TypeScript,Go,Rust -R . 2>/dev/null`
+        .lines
+        .reject { |l| l.include?("vendor/") || l.include?("node_modules/") || l.include?("spec/") }
+        .first(100)
+        .join
+    rescue
+      ""
+    end
+
+    def agents_md
+      Pathname.pwd.ascend.each do |dir|
+        file = dir / "AGENTS.md"
+        return file.read if file.exist?
+      end
+      nil
+    end
+  end
+end
lib/elelem.rb
@@ -1,6 +1,7 @@
 # frozen_string_literal: true
 
 require "date"
+require "erb"
 require "fileutils"
 require "json"
 require "json_schemer"
@@ -14,6 +15,7 @@ require_relative "elelem/agent"
 require_relative "elelem/mcp"
 require_relative "elelem/net"
 require_relative "elelem/plugins"
+require_relative "elelem/system_prompt"
 require_relative "elelem/terminal"
 require_relative "elelem/tool"
 require_relative "elelem/toolbox"
elelem.gemspec
@@ -41,6 +41,8 @@ Gem::Specification.new do |spec|
     "lib/elelem/plugins/read.rb",
     "lib/elelem/plugins/verify.rb",
     "lib/elelem/plugins/write.rb",
+    "lib/elelem/system_prompt.rb",
+    "lib/elelem/templates/system_prompt.erb",
     "lib/elelem/terminal.rb",
     "lib/elelem/tool.rb",
     "lib/elelem/toolbox.rb",
@@ -51,6 +53,7 @@ Gem::Specification.new do |spec|
   spec.require_paths = ["lib"]
 
   spec.add_dependency "date", "~> 3.0"
+  spec.add_dependency "erb", "~> 6.0"
   spec.add_dependency "fileutils", "~> 1.0"
   spec.add_dependency "json", "~> 2.0"
   spec.add_dependency "json_schemer", "~> 2.0"