Comparing changes
v0.10.0
→
HEAD
32 commits
87 files changed
Commits
f0009ed
fix: use format defined in https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/long-context-tips#essential-tips-for-long-context-prompts
2026-01-31 06:27:56
95c5d03
refactor: demote plugins to project local until they are mature enough to be promoted to the gem
2026-01-29 23:10:52
Changed files (87)
.elelem
backlog
archive
plugins
prompts
doc
modes
plugins
lib
spec
.elelem/backlog/archive/002-hardware-detection.md
@@ -0,0 +1,44 @@
+As a `new user`, I `want elelem to detect my hardware capabilities`, so that `it can recommend an appropriate model for my system`.
+
+# SYNOPSIS
+
+Detect GPU/CPU capabilities to determine what models can run locally.
+
+# DESCRIPTION
+
+When elelem starts with no configuration, it should be able to detect:
+
+1. **GPU presence and type**:
+ - NVIDIA GPU with CUDA support (check nvidia-smi or similar)
+ - AMD GPU with ROCm support
+ - No discrete GPU (CPU-only fallback)
+
+2. **Available VRAM/RAM**:
+ - GPU memory available for model loading
+ - System RAM as fallback for CPU inference
+
+3. **Model recommendations**:
+ - Map hardware capabilities to appropriate model sizes
+ - Example: 8GB VRAM → 7B parameter model, 4GB VRAM → 3B model, CPU-only → small model
+
+This information will be used by the local provider to:
+- Select the default model automatically
+- Warn users if their hardware may struggle with a requested model
+
+# SEE ALSO
+
+* [ ] lib/elelem/system_prompt.rb (platform detection)
+* [ ] Story 001 (spike findings will inform implementation)
+
+# Tasks
+
+* [ ] TBD (filled in design mode)
+
+# Acceptance Criteria
+
+* [ ] Correctly detects NVIDIA GPU presence on Linux
+* [ ] Correctly detects AMD GPU presence on Linux
+* [ ] Correctly detects available VRAM when GPU present
+* [ ] Correctly detects available system RAM
+* [ ] Returns a capability summary that can be used for model selection
+* [ ] Works gracefully when detection tools (nvidia-smi, rocm-smi) are not installed
.elelem/backlog/archive/003-model-download.md
@@ -0,0 +1,44 @@
+As a `new user`, I `want elelem to automatically download the recommended model`, so that `I can start using it immediately without manual setup`.
+
+# SYNOPSIS
+
+Download LLM models from Hugging Face with progress indication.
+
+# DESCRIPTION
+
+When the local provider is used and the required model is not present locally:
+
+1. **Model selection**:
+ - Use hardware detection (Story 002) to pick an appropriate default model
+ - Support a curated list of known-good coding models (e.g., CodeLlama, DeepSeek Coder, Qwen Coder)
+
+2. **Download process**:
+ - Download from Hugging Face Hub (GGUF format preferred for llama.cpp)
+ - Show download progress (stream CLI output or use Terminal#waiting)
+ - Store in `~/.cache/elelem/models/` or similar standard location
+
+3. **Model management**:
+ - Check if model already exists before downloading
+ - Handle interrupted downloads gracefully (resume or restart)
+
+The approach (HF CLI vs direct download) will be determined by Story 001 spike.
+
+# SEE ALSO
+
+* [ ] Story 001 (determines download approach)
+* [ ] Story 002 (provides hardware info for model selection)
+* [ ] lib/elelem/terminal.rb (progress indication)
+* [ ] ~/.cache/elelem/models/ (storage location)
+
+# Tasks
+
+* [ ] TBD (filled in design mode)
+
+# Acceptance Criteria
+
+* [ ] Model downloads successfully from Hugging Face
+* [ ] User sees progress indication during download
+* [ ] Downloaded model is stored in consistent location
+* [ ] Subsequent runs do not re-download existing model
+* [ ] Graceful error handling if download fails (network error, disk full, etc.)
+* [ ] At least one good default coding model is identified and tested
.elelem/backlog/archive/004-local-inference-provider.md
@@ -0,0 +1,53 @@
+As a `user`, I `want to run LLM inference locally without external servers`, so that `I can use elelem without API keys, Ollama, or network connectivity`.
+
+# SYNOPSIS
+
+Implement a local inference provider that loads and runs models directly in-process.
+
+# DESCRIPTION
+
+Create a new provider in `lib/elelem/net/` that:
+
+1. **Loads models locally**:
+ - Use the approach determined by Story 001 (llama.cpp bindings or CLI)
+ - Load GGUF model files from `~/.cache/elelem/models/`
+ - Support GPU acceleration (CUDA, ROCm) when available
+ - Fall back to CPU inference when no GPU present
+
+2. **Implements the provider interface**:
+ - Match the interface of existing providers (ollama.rb, openai.rb, claude.rb)
+ - Support streaming responses
+ - Handle the conversation history format
+
+3. **Performance considerations**:
+ - Model loading may take a few seconds - show appropriate feedback
+ - Keep model loaded in memory for subsequent prompts (don't reload per-request)
+ - Handle memory limits gracefully
+
+4. **Configuration**:
+ - Configurable via `.elelem.yml` similar to other providers
+ - Support specifying custom model path
+ - Support model selection override
+
+# SEE ALSO
+
+* [ ] Story 001 (determines implementation approach)
+* [ ] Story 003 (provides downloaded models)
+* [ ] lib/elelem/net/ollama.rb (provider interface reference)
+* [ ] lib/elelem/net/openai.rb (provider interface reference)
+* [ ] lib/elelem/net/claude.rb (provider interface reference)
+
+# Tasks
+
+* [ ] TBD (filled in design mode)
+
+# Acceptance Criteria
+
+* [ ] Provider loads model from local disk
+* [ ] Provider generates streaming responses
+* [ ] Provider works with GPU acceleration on CUDA
+* [ ] Provider works with GPU acceleration on ROCm
+* [ ] Provider falls back to CPU when no GPU available
+* [ ] Provider integrates with existing elelem conversation flow
+* [ ] Tool calling works with local models (if model supports it)
+* [ ] Works fully offline once model is downloaded
.elelem/backlog/archive/005-default-provider-selection.md
@@ -0,0 +1,49 @@
+As a `new user`, I `want elelem to use local inference by default`, so that `I can start using it immediately without any configuration`.
+
+# SYNOPSIS
+
+Make the local provider the default when no configuration exists.
+
+# DESCRIPTION
+
+Update elelem's provider selection logic so that:
+
+1. **First-run experience**:
+ - When no `.elelem.yml` exists and no environment variables are set
+ - Automatically select the local provider
+ - Trigger model download if needed (Story 003)
+ - Start the normal prompt interface - no wizard or extra questions
+
+2. **Provider priority** (when no explicit config):
+ 1. Local provider (new default)
+ 2. Ollama (if running and accessible)
+ 3. OpenAI (if OPENAI_API_KEY set)
+ 4. Claude (if ANTHROPIC_API_KEY set)
+
+3. **Explicit configuration**:
+ - Users can still configure any provider in `.elelem.yml`
+ - Explicit config always takes precedence
+ - Document how to switch providers
+
+4. **Seamless transition**:
+ - Existing users with configuration are not affected
+ - Only new users (no config) get the new default behavior
+
+# SEE ALSO
+
+* [ ] Story 004 (local provider implementation)
+* [ ] lib/elelem/agent.rb (provider selection logic)
+* [ ] Configuration loading code
+
+# Tasks
+
+* [ ] TBD (filled in design mode)
+
+# Acceptance Criteria
+
+* [ ] New user with no config starts elelem and can chat immediately
+* [ ] Local provider is used by default (not Ollama or cloud providers)
+* [ ] Model downloads automatically on first run if not present
+* [ ] Existing users with `.elelem.yml` are not affected
+* [ ] Users with API keys in environment can still use cloud providers
+* [ ] Clear documentation on how to configure different providers
.elelem/backlog/archive/006-interview-question-types.md
@@ -0,0 +1,47 @@
+As an `agent`, I `want to ask questions with different answer types`, so that `I can collect structured responses while still allowing conversational flexibility`.
+
+# SYNOPSIS
+
+Add support for text, single-select, multi-select, and yes/no question types to the interview tool.
+
+# DESCRIPTION
+
+The interview tool currently only supports free-form text input. This story adds support for four question types:
+
+1. **text** - Open-ended free-form input (current behavior)
+2. **single** - Single selection from a list of options (radio-button style)
+3. **multi** - Multiple selections from a list of options (checkbox style)
+4. **yesno** - Boolean yes/no confirmation
+
+Even when structured options are presented, the user can always type free-form text instead. The agent should handle unexpected responses gracefully (e.g., if the user asks a clarifying question rather than selecting an option).
+
+Example API:
+```ruby
+interview(
+ question: "Pick a color",
+ type: "single",
+ options: ["Red", "Green", "Blue"]
+)
+```
+
+# SEE ALSO
+
+* [ ] lib/elelem/terminal.rb - Terminal input/output handling
+* [ ] lib/elelem/tool.rb - Tool definition structure
+
+# Tasks
+
+* [ ] TBD (filled in design mode)
+
+# Acceptance Criteria
+
+* [ ] Agent can specify `type: "text"` for free-form input (default behavior)
+* [ ] Agent can specify `type: "single"` with `options` array for single selection
+* [ ] Agent can specify `type: "multi"` with `options` array for multiple selection
+* [ ] Agent can specify `type: "yesno"` for boolean confirmation
+* [ ] Options are displayed as a numbered list (e.g., "1. Red", "2. Green", "3. Blue")
+* [ ] User can type a number to select an option
+* [ ] User can type free-form text instead of selecting a numbered option
+* [ ] For multi-select, user can enter comma-separated numbers (e.g., "1, 3")
+* [ ] For yes/no, accepts variations like "y", "yes", "n", "no" (case-insensitive)
+* [ ] Response returned to agent includes both the raw input and parsed selection(s)
.elelem/backlog/archive/007-interview-batch-questions.md
@@ -0,0 +1,43 @@
+As an `agent`, I `want to ask multiple questions at once`, so that `I can collect related information in a single interaction like a form`.
+
+# SYNOPSIS
+
+Allow the interview tool to accept an array of questions and return all answers together.
+
+# DESCRIPTION
+
+Building on the question types feature, this story adds the ability to send a batch of questions in a single interview call. This is useful when the agent needs to collect several related pieces of information and it would be tedious to ask them one at a time.
+
+Example API:
+```ruby
+interview(questions: [
+ { question: "What's your name?", type: "text" },
+ { question: "Pick a color", type: "single", options: ["Red", "Green", "Blue"] },
+ { question: "Select features", type: "multi", options: ["Auth", "API", "UI"] },
+ { question: "Ready to proceed?", type: "yesno" }
+])
+```
+
+The questions are presented sequentially, and all answers are collected before returning to the agent. The user can still respond with clarifying questions or unexpected input on any individual question.
+
+The existing single-question API remains supported for backward compatibility.
+
+# SEE ALSO
+
+* [ ] .elelem/backlog/006-interview-question-types.md - Prerequisite: question types
+* [ ] lib/elelem/terminal.rb - Terminal input/output handling
+
+# Tasks
+
+* [ ] TBD (filled in design mode)
+
+# Acceptance Criteria
+
+* [ ] Agent can pass `questions` array with multiple question objects
+* [ ] Each question in the array can have its own type and options
+* [ ] Questions are presented to user one at a time in order
+* [ ] All answers are collected before returning to agent
+* [ ] Response includes an array of answers matching the question order
+* [ ] Single-question API (`question: "..."`) still works for backward compatibility
+* [ ] If user provides unexpected input on one question, that input is captured and interview continues
+* [ ] Agent receives enough context to understand which answer corresponds to which question
.elelem/backlog/archive/008-interview-tui-selection.md
@@ -0,0 +1,42 @@
+As a `user`, I `want to navigate options with arrow keys`, so that `I can quickly select from a list without typing numbers`.
+
+# SYNOPSIS
+
+Add TUI-style interactive selection widgets for single and multi-select questions.
+
+# DESCRIPTION
+
+When the terminal supports it, present single-select and multi-select questions as interactive widgets where the user can:
+
+- Use **arrow keys** (up/down) to navigate between options
+- Press **space** to toggle selection (for multi-select)
+- Press **enter** to confirm selection
+
+This provides a smoother experience than typing numbers, especially for longer option lists. The numbered fallback (from story 006) remains available for terminals that don't support the TUI widgets or when the user starts typing text instead of navigating.
+
+The widget should be visually clear:
+- Highlight the currently focused option
+- Show a marker (e.g., `[x]` or `●`) for selected options
+- For single-select, selection and confirmation can be combined (enter selects and confirms)
+
+# SEE ALSO
+
+* [ ] .elelem/backlog/006-interview-question-types.md - Prerequisite: question types with numbered fallback
+* [ ] lib/elelem/terminal.rb - Terminal capabilities and input handling
+* [ ] Reline library - May provide building blocks for TUI input
+
+# Tasks
+
+* [ ] TBD (filled in design mode)
+
+# Acceptance Criteria
+
+* [ ] Single-select questions show an interactive list when terminal supports it
+* [ ] User can press up/down arrows to move highlight between options
+* [ ] User can press enter to select the highlighted option (single-select)
+* [ ] Multi-select questions allow space to toggle selection on highlighted option
+* [ ] Multi-select shows visual indicator for selected items (e.g., `[x]`)
+* [ ] Pressing enter on multi-select confirms current selections
+* [ ] If user starts typing text, widget gracefully switches to free-form input mode
+* [ ] Falls back to numbered list input if terminal doesn't support TUI features
+* [ ] Works correctly when terminal is resized during interaction
.elelem/backlog/archive/013-editor-integration-for-prompts.md
@@ -0,0 +1,98 @@
+# Editor Integration for Prompts
+
+As a **power user**, I want to open my `$EDITOR` from the prompt to compose long messages, so that I have a full editing environment for complex prompts.
+
+## SYNOPSIS
+
+Press `CTRL+x CTRL+e` at the prompt to open `$EDITOR`, compose text, and return to the prompt for review before sending.
+
+## DESCRIPTION
+
+When composing long or complex prompts, the terminal input line is limiting. Users need the ability to:
+- Write multi-line text comfortably
+- Paste content from other sources
+- Use familiar editor keybindings (vim, emacs, etc.)
+- Review and edit before sending
+
+### Keybinding
+
+`CTRL+x CTRL+e` - Standard Bash/Zsh binding for `edit-and-execute-command`
+
+This is the most intuitive choice for Linux/Bash/Vim/Tmux users as it matches their existing muscle memory.
+
+### Flow
+
+```
+> partial text█ # User types some text
+ # User presses CTRL+x CTRL+e
+ # Editor opens with "partial text" pre-populated
+ # User edits, saves, quits
+> partial text # Text appears at prompt
+ plus more content # (multi-line if applicable)
+ from the editor█ # Cursor at end, ready to review
+ # User presses Enter to send
+```
+
+### Edge Cases
+
+| Scenario | Behavior |
+|----------|----------|
+| Empty file saved | Return to prompt, no input |
+| Editor exits non-zero | Return to prompt, preserve original text |
+| `$EDITOR` not set | Fall back to `$VISUAL`, then `vi` |
+| Multi-line text | Display all lines, submit as single message |
+
+## SEE ALSO
+
+* [ ] lib/elelem/terminal.rb - `ask` method, Reline configuration
+* [ ] Reline documentation for custom key bindings
+* [ ] Bash `edit-and-execute-command` (CTRL+x CTRL+e)
+
+## Tasks
+
+* [ ] TBD (filled in design mode)
+
+## Acceptance Criteria
+
+* [ ] `CTRL+x CTRL+e` opens `$EDITOR` (or `$VISUAL`, or `vi`)
+* [ ] Editor pre-populates with any text already typed at the prompt
+* [ ] After saving and quitting, text appears at the prompt for review
+* [ ] User must press Enter to send (no auto-submit)
+* [ ] Empty file returns to prompt with no input (cancel)
+* [ ] Non-zero editor exit preserves original text
+* [ ] Temp file is created in appropriate location and cleaned up after
+* [ ] Multi-line text from editor displays correctly at prompt
+* [ ] Works with common editors: vim, nvim, nano, emacs
+
+## Implementation Notes
+
+```ruby
+# In Terminal, bind CTRL+x CTRL+e
+Reline::LineEditor.bind_key("\C-x\C-e") do |line_editor|
+ # 1. Get current line content
+ current_text = line_editor.line
+
+ # 2. Create temp file with content
+ require "tempfile"
+ file = Tempfile.new(["elelem-prompt-", ".md"])
+ file.write(current_text)
+ file.close
+
+ # 3. Open editor
+ editor = ENV["VISUAL"] || ENV["EDITOR"] || "vi"
+ system("#{editor} #{file.path}")
+
+ # 4. Read result
+ if $?.success?
+ new_text = File.read(file.path).strip
+ line_editor.replace_line(new_text) unless new_text.empty?
+ end
+
+ # 5. Cleanup
+ file.unlink
+end
+```
+
+### Multi-line Display
+
+Reline supports multi-line input. The edited text should be inserted and displayed across multiple lines if it contains newlines.
.elelem/backlog/001-local-inference-spike.md
@@ -0,0 +1,40 @@
+As a `developer`, I `want to research local LLM inference options`, so that `we can choose the best approach for running models without external servers`.
+
+# SYNOPSIS
+
+Research spike to evaluate llama.cpp, Hugging Face CLI, and other options for local inference.
+
+# DESCRIPTION
+
+Before building the local provider, we need to understand:
+
+1. **Inference engines**: Evaluate options like llama.cpp (via Ruby bindings or CLI), Hugging Face transformers, or other local inference tools
+2. **Ruby integration**: Determine if we should use Ruby bindings (e.g., `llama_cpp.rb` gem) or shell out to a CLI tool
+3. **Hugging Face integration**: Understand how to download GGUF/GGML models, whether to use `huggingface-cli` or direct API calls
+4. **GPU support**: Verify CUDA and ROCm acceleration works on Linux
+5. **Model format**: Determine which quantized model formats to support (GGUF recommended for llama.cpp)
+
+Deliverable: A written recommendation document with:
+- Recommended approach
+- Required dependencies
+- Example code showing basic inference working
+- Known limitations
+
+# SEE ALSO
+
+* [ ] https://github.com/ggerganov/llama.cpp
+* [ ] https://github.com/yoshoku/llama_cpp.rb
+* [ ] https://huggingface.co/docs/huggingface_hub/guides/cli
+* [ ] lib/elelem/net/ (existing provider implementations)
+
+# Tasks
+
+* [ ] TBD (filled in design mode)
+
+# Acceptance Criteria
+
+* [ ] Document exists with clear recommendation
+* [ ] Proof-of-concept code demonstrates loading a model and generating a response
+* [ ] GPU acceleration tested on at least one platform (CUDA or ROCm)
+* [ ] Decision made: Ruby bindings vs CLI wrapper
+* [ ] Decision made: Model download strategy (HF CLI vs direct download)
.elelem/backlog/009-enhanced-interview-tool.md
@@ -0,0 +1,217 @@
+As an `agent`, I `want to ask questions with selectors and batch support`, so that `I can collect structured responses efficiently`.
+
+# SYNOPSIS
+
+Extend the interview tool to support text, single-select, and multi-select inputs with TUI navigation and batch question capability.
+
+# DESCRIPTION
+
+The interview tool currently only supports free-form text input. This story adds:
+
+1. **Input modes**:
+ - `text` - Free-form text input (current behavior, default)
+ - `select` - Radio-button style single choice from options
+ - `multi` - Checkbox style multiple choice from options
+
+2. **TUI interaction** (when terminal supports it):
+ - Arrow keys (up/down) to navigate between options
+ - Space to toggle selection (multi-select)
+ - Enter to confirm selection
+
+3. **Numbered fallback** (for dumb terminals or piped input):
+ - Display numbered list (e.g., "1. Red", "2. Green", "3. Blue")
+ - User types number to select
+ - Comma-separated numbers for multi-select (e.g., "1, 3")
+
+4. **Batch questions**:
+ - Accept array of questions in single call
+ - Present sequentially, collect all answers before returning
+
+# SEE ALSO
+
+* [ ] lib/elelem/terminal.rb - Add `select` and `multi_select` methods
+* [ ] lib/elelem/plugins/interview.rb - Add `options`, `multi`, and `questions` params
+
+# Tasks
+
+* [ ] TBD (filled in design mode)
+
+# Acceptance Criteria
+
+* [ ] Agent can provide `options` array to enable selector mode
+* [ ] Agent can set `multi: true` to allow multiple selections
+* [ ] Single-select with TUI: arrow keys navigate, enter confirms
+* [ ] Multi-select with TUI: arrow keys navigate, space toggles, enter confirms
+* [ ] Falls back to numbered list when terminal doesn't support TUI
+* [ ] Free-form text input still works when no options provided
+* [ ] Agent can pass `questions` array with multiple question objects
+* [ ] Each question in batch can have its own options and multi setting
+* [ ] Batch returns array of answers matching question order
+* [ ] Single-question API remains backward compatible
+* [ ] No new gem dependencies (uses io/console from stdlib)
+
+# Implementation Notes
+
+The following implementation plan is provided as guidance for the developer.
+
+## Tool Schema
+
+```json
+{
+ "question": { "type": "string", "description": "The question to ask" },
+ "options": { "type": "array", "description": "List of options (enables selector)" },
+ "multi": { "type": "boolean", "description": "Allow multiple selections" },
+ "questions": { "type": "array", "description": "Batch of question objects" }
+}
+```
+
+## Files to Modify
+
+- `lib/elelem/terminal.rb` - Add `select` and `multi_select` methods
+- `lib/elelem/plugins/interview.rb` - Add `options`, `multi`, and `questions` params
+
+## Terminal API
+
+```ruby
+# Single select - returns selected option string
+terminal.select(options) # => "option1"
+
+# Multi select - returns array of selected options
+terminal.multi_select(options) # => ["option1", "option3"]
+```
+
+## Reference Implementation
+
+### Terminal#select (single choice)
+
+```ruby
+def select(options)
+ return options.first if options.size == 1
+ require "io/console"
+
+ index = 0
+ render_options = -> {
+ options.each_with_index do |opt, i|
+ prefix = i == index ? "> " : " "
+ $stdout.puts "#{prefix}#{opt}"
+ end
+ }
+
+ render_options.call
+
+ loop do
+ key = read_key
+ case key
+ when :up then index = (index - 1) % options.size
+ when :down then index = (index + 1) % options.size
+ when :enter then break
+ end
+ $stdout.print "\e[#{options.size}A\e[J"
+ render_options.call
+ end
+
+ options[index]
+end
+
+def read_key
+ char = $stdin.getch
+ return :enter if char == "\r" || char == "\n"
+ return char unless char == "\e"
+
+ return char unless $stdin.ready?
+ seq = $stdin.getch
+ return char unless seq == "["
+
+ code = $stdin.getch
+ case code
+ when "A" then :up
+ when "B" then :down
+ else char
+ end
+end
+```
+
+### Terminal#multi_select (multiple choice)
+
+```ruby
+def multi_select(options)
+ require "io/console"
+
+ index = 0
+ selected = Set.new
+
+ render_options = -> {
+ options.each_with_index do |opt, i|
+ cursor = i == index ? ">" : " "
+ check = selected.include?(i) ? "[x]" : "[ ]"
+ $stdout.puts "#{cursor} #{check} #{opt}"
+ end
+ }
+
+ render_options.call
+
+ loop do
+ key = read_key
+ case key
+ when :up then index = (index - 1) % options.size
+ when :down then index = (index + 1) % options.size
+ when :space then selected.include?(index) ? selected.delete(index) : selected.add(index)
+ when :enter then break
+ end
+ $stdout.print "\e[#{options.size}A\e[J"
+ render_options.call
+ end
+
+ options.values_at(*selected.to_a.sort)
+end
+```
+
+### Updated Interview Plugin
+
+```ruby
+Elelem::Plugins.register(:interview) do |agent|
+ agent.toolbox.add("interview",
+ description: "Ask the user a question and wait for their response",
+ params: {
+ question: { type: "string", description: "The question to ask" },
+ options: { type: "array", description: "List of options for selector" },
+ multi: { type: "boolean", description: "Allow multiple selections" },
+ questions: { type: "array", description: "Batch of question objects" }
+ },
+ required: ["question"]
+ ) do |args|
+ if args["questions"]&.any?
+ # Batch mode
+ answers = args["questions"].map do |q|
+ agent.terminal.say(agent.terminal.markdown(q["question"]))
+ ask_one(agent.terminal, q["options"], q["multi"])
+ end
+ { answers: answers }
+ else
+ # Single question mode
+ agent.terminal.say(agent.terminal.markdown(args["question"]))
+ answer = ask_one(agent.terminal, args["options"], args["multi"])
+ { answer: answer }
+ end
+ end
+
+ def ask_one(terminal, options, multi)
+ if options&.any?
+ multi ? terminal.multi_select(options) : terminal.select(options)
+ else
+ terminal.ask("> ")
+ end
+ end
+end
+```
+
+## Verification
+
+1. Run `./bin/run -p vertex`
+2. Ask the LLM to use the interview tool with options
+3. Test arrow key navigation
+4. Test single select (Enter confirms)
+5. Test multi select (Space toggles, Enter confirms)
+6. Test free-form text still works when no options provided
+7. Test batch questions with mixed types
+8. Run `bin/test`
.elelem/backlog/010-adr-support-in-design-mode.md
@@ -0,0 +1,73 @@
+As a `developer`, I `want design mode to support creating Architecture Decision Records`, so that `architectural decisions are documented consistently and repeatably`.
+
+# SYNOPSIS
+
+Add ADR creation capability to design mode with a standard template.
+
+# DESCRIPTION
+
+When architectural decisions emerge during design sessions, the agent should be able to create ADRs using a consistent template. ADRs are stored in `doc/adr/` and follow a numbered naming convention.
+
+The design prompt should be updated to:
+1. Explain when to create ADRs (significant architectural decisions)
+2. Provide the ADR template
+3. Allow writing to `doc/adr/` directory
+
+# SEE ALSO
+
+* [ ] lib/elelem/prompts/design.erb - Design mode prompt
+* [ ] doc/adr/ - ADR storage location (to be created)
+
+# Tasks
+
+* [ ] TBD (filled in design mode)
+
+# Acceptance Criteria
+
+* [ ] Design mode prompt includes ADR template
+* [ ] Design mode can write files to `doc/adr/`
+* [ ] ADRs follow naming convention: `ADR-NNNN-short-name.md`
+* [ ] Template includes: Date, Status, Context, Decision, Consequences
+* [ ] Agent understands when to propose creating an ADR
+
+# ADR Template
+
+```markdown
+# ADR-NNNN: Title
+
+**Date:** YYYY-MM-DD
+**Status:** Proposed | Accepted | Deprecated | Superseded by ADR-XXXX
+
+## Context
+
+What is the issue that we're seeing that is motivating this decision or change?
+
+## Decision
+
+What is the change that we're proposing and/or doing?
+
+## Consequences
+
+**Positive:**
+- Benefit 1
+- Benefit 2
+
+**Negative:**
+- Tradeoff 1
+- Tradeoff 2
+```
+
+# Guidance for Design Mode
+
+Include in the prompt:
+
+> **When to create an ADR:**
+> - Choosing between multiple valid approaches
+> - Adopting a new technology or pattern
+> - Changing an existing architectural decision
+> - Decisions that affect multiple components
+>
+> **When NOT to create an ADR:**
+> - Implementation details within a single file
+> - Bug fixes
+> - Routine refactoring
.elelem/backlog/011-local-inference-implementation.md
@@ -0,0 +1,94 @@
+As a `new user`, I `want elelem to run locally without external servers or API keys`, so that `I can start using it immediately with zero configuration`.
+
+# SYNOPSIS
+
+Implement complete local inference: hardware detection, model download, local provider, and default selection.
+
+# DESCRIPTION
+
+This story implements the full local inference capability, consolidating the work from stories 002-005 (see ADR-0001). The spike (story 001) should be completed first to inform implementation decisions.
+
+## 1. Hardware Detection
+
+Detect GPU/CPU capabilities to determine what models can run locally:
+
+- **GPU presence and type**: NVIDIA (CUDA), AMD (ROCm), or CPU-only
+- **Available VRAM/RAM**: GPU memory and system RAM
+- **Model recommendations**: Map hardware to appropriate model sizes
+ - 8GB+ VRAM → 7B parameter model
+ - 4GB VRAM → 3B model
+ - CPU-only → small model (1-3B)
+
+## 2. Model Download
+
+Download LLM models from Hugging Face with progress indication:
+
+- Use hardware detection to pick an appropriate default model
+- Support curated list of coding models (CodeLlama, DeepSeek Coder, Qwen Coder)
+- Download GGUF format from Hugging Face Hub
+- Store in `~/.cache/elelem/models/`
+- Show progress, handle interrupted downloads
+
+## 3. Local Inference Provider
+
+Create `lib/elelem/net/local.rb` provider:
+
+- Load GGUF models using approach from spike (llama.cpp bindings or CLI)
+- Support GPU acceleration (CUDA, ROCm) with CPU fallback
+- Implement same interface as existing providers (streaming, conversation history)
+- Keep model loaded in memory between prompts
+- Configurable via `.elelem.yml`
+
+## 4. Default Provider Selection
+
+Make local provider the default for new users:
+
+- When no config exists and no API keys set, use local provider
+- Trigger model download if needed
+- Provider priority (when no explicit config):
+ 1. Local provider (new default)
+ 2. Ollama (if running)
+ 3. Cloud providers (if API keys set)
+- Existing users with config are not affected
+
+# SEE ALSO
+
+* [ ] .elelem/backlog/001-local-inference-spike.md - Complete spike first
+* [ ] doc/adr/ADR-0001-consolidate-local-inference-stories.md - Decision record
+* [ ] lib/elelem/net/ollama.rb - Provider interface reference
+* [ ] lib/elelem/net/openai.rb - Provider interface reference
+* [ ] lib/elelem/system_prompt.rb - Platform detection patterns
+
+# Tasks
+
+* [ ] TBD (filled in design mode, after spike completes)
+
+# Acceptance Criteria
+
+## Hardware Detection
+* [ ] Correctly detects NVIDIA GPU presence on Linux
+* [ ] Correctly detects AMD GPU presence on Linux
+* [ ] Correctly detects available VRAM when GPU present
+* [ ] Correctly detects available system RAM
+* [ ] Works gracefully when detection tools are not installed
+
+## Model Download
+* [ ] Model downloads successfully from Hugging Face
+* [ ] User sees progress indication during download
+* [ ] Downloaded model is stored in consistent location
+* [ ] Subsequent runs do not re-download existing model
+* [ ] Graceful error handling if download fails
+
+## Local Provider
+* [ ] Provider loads model from local disk
+* [ ] Provider generates streaming responses
+* [ ] Provider works with GPU acceleration (CUDA and ROCm)
+* [ ] Provider falls back to CPU when no GPU available
+* [ ] Provider integrates with existing conversation flow
+* [ ] Works fully offline once model is downloaded
+
+## Default Selection
+* [ ] New user with no config starts elelem and can chat immediately
+* [ ] Local provider is used by default
+* [ ] Model downloads automatically on first run if not present
+* [ ] Existing users with `.elelem.yml` are not affected
.elelem/backlog/012-xdg-base-directory-support.md
@@ -0,0 +1,110 @@
+# XDG Base Directory Support
+
+As a **Linux user**, I want elelem to respect XDG Base Directory conventions, so that my configuration, data, and cache files are organized in standard locations.
+
+## SYNOPSIS
+
+Support `XDG_CONFIG_HOME`, `XDG_DATA_HOME`, and `XDG_CACHE_HOME` environment variables for file storage.
+
+## DESCRIPTION
+
+Currently, elelem uses `~/.elelem/` for all user-level files (permissions, plugins, prompts, MCP config). This doesn't follow the XDG Base Directory specification used by most Linux applications.
+
+### Current Behavior
+
+```ruby
+# permissions.rb, plugins.rb, system_prompt.rb, mcp.rb
+LOAD_PATHS = [
+ "~/.elelem/...",
+ ".elelem/..."
+]
+```
+
+### Proposed Behavior
+
+**Config** (`XDG_CONFIG_HOME` or `~/.config`):
+- `permissions.json`
+- `plugins/`
+- `prompts/`
+- `mcp.json`
+
+**Data** (`XDG_DATA_HOME` or `~/.local/share`):
+- Conversation history (future)
+- MCP OAuth tokens
+
+**Cache** (`XDG_CACHE_HOME` or `~/.cache`):
+- Downloaded models (for local inference)
+- MCP server logs
+
+### Search Order (Config)
+
+```ruby
+LOAD_PATHS = [
+ ".elelem", # 1. Project-local (highest)
+ File.join(xdg_config_home, "elelem"), # 2. XDG location
+ File.join(ENV["HOME"], ".elelem"), # 3. Legacy (deprecated)
+]
+```
+
+### Migration Path
+
+1. **Phase 1**: Add XDG support, keep `~/.elelem` as fallback
+2. **Phase 2**: Log deprecation warning when `~/.elelem` is used
+3. **Phase 3**: Remove `~/.elelem` support in future major version
+
+## SEE ALSO
+
+* [ ] lib/elelem/permissions.rb - `LOAD_PATHS` constant
+* [ ] lib/elelem/plugins.rb - `LOAD_PATHS` constant
+* [ ] lib/elelem/system_prompt.rb - `LOAD_PATHS` constant
+* [ ] lib/elelem/mcp.rb - hardcoded paths for config and logs
+* [ ] lib/elelem/mcp/token_storage.rb - OAuth token paths
+* [ ] XDG Base Directory Spec: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
+
+## Tasks
+
+* [ ] TBD (filled in design mode)
+
+## Acceptance Criteria
+
+* [ ] When `XDG_CONFIG_HOME` is set, config files load from `$XDG_CONFIG_HOME/elelem/`
+* [ ] When `XDG_CONFIG_HOME` is unset, config files load from `~/.config/elelem/`
+* [ ] When `XDG_DATA_HOME` is set, data files store in `$XDG_DATA_HOME/elelem/`
+* [ ] When `XDG_DATA_HOME` is unset, data files store in `~/.local/share/elelem/`
+* [ ] When `XDG_CACHE_HOME` is set, cache files store in `$XDG_CACHE_HOME/elelem/`
+* [ ] When `XDG_CACHE_HOME` is unset, cache files store in `~/.cache/elelem/`
+* [ ] Project-local `.elelem/` takes precedence over XDG locations for config
+* [ ] Project-local `.elelem/` does NOT store data or cache (only config)
+* [ ] Legacy `~/.elelem/` still works as fallback
+* [ ] Deprecation warning logged when loading from `~/.elelem/`
+* [ ] All affected files updated: permissions.rb, plugins.rb, system_prompt.rb, mcp.rb, token_storage.rb
+
+## Implementation Notes
+
+Consider extracting a shared module:
+
+```ruby
+module Elelem
+ module Paths
+ def self.config_home
+ ENV["XDG_CONFIG_HOME"] || File.join(ENV["HOME"], ".config")
+ end
+
+ def self.data_home
+ ENV["XDG_DATA_HOME"] || File.join(ENV["HOME"], ".local", "share")
+ end
+
+ def self.cache_home
+ ENV["XDG_CACHE_HOME"] || File.join(ENV["HOME"], ".cache")
+ end
+
+ def self.config_paths
+ [
+ ".elelem",
+ File.join(config_home, "elelem"),
+ File.join(ENV["HOME"], ".elelem") # deprecated
+ ]
+ end
+ end
+end
+```
.elelem/backlog/014-documentation-website.md
@@ -0,0 +1,85 @@
+# Documentation Website
+
+As a **user discovering elelem**, I want comprehensive documentation on a website, so that I can learn how to configure and use elelem effectively.
+
+As a **plugin author**, I want documentation with examples, so that I can extend elelem for my workflows.
+
+## SYNOPSIS
+
+Create a minimal, fast documentation website with no JavaScript or cookies.
+
+## DESCRIPTION
+
+Build a static documentation site that serves as the primary reference for elelem.
+The site should have a man-page-style minimal aesthetic - clean, fast, focused on content.
+
+### Content Structure
+
+```
+/ # Overview, what is elelem
+/getting-started/ # Installation, first run, basic usage
+/workflow/ # Plan → Design → Build → Review → Verify loop
+/configuration/ # Config files, environment variables, XDG paths
+/modes/ # Detailed explanation of each mode
+ /plan/
+ /design/
+ /build/
+ /review/
+ /verify/
+/plugins/ # Plugin system overview
+ /authoring/ # How to write plugins
+ /examples/ # Example plugins with explanations
+/mcp/ # MCP integration guide
+/reference/ # Command reference, tool schemas
+```
+
+### Design Principles
+
+- **No JavaScript** - Content works without JS
+- **No cookies** - No tracking, no consent banners
+- **Fast** - Minimal CSS, no frameworks
+- **Accessible** - Semantic HTML, good contrast, works with screen readers
+- **Unix aesthetic** - Clean, monospace-friendly, man-page inspired
+
+### Workflow Diagram
+
+Include the development workflow prominently:
+
+```
+┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
+│ PLAN │ → │ DESIGN │ → │ BUILD │ → │ REVIEW │ → │ VERIFY │ → done
+│ draft │ │ ready │ │designing│ │building │ │reviewing│
+│ │ │ │ │→building│ │→reviewing│ │→verifying│
+└─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘
+ Interview Research Execute Code Smoke test
+ stories create tasks tasks review demo
+```
+
+## SEE ALSO
+
+* [ ] https://www.mokhan.ca/ - Style reference
+* [ ] Existing README.md content to migrate
+
+## Research (Design Phase)
+
+- [ ] Evaluate static site generators (Hugo, Zola, Eleventy, plain HTML+Make)
+- [ ] Determine hosting approach (self-hosted server)
+- [ ] Design information architecture
+- [ ] Create minimal CSS theme
+
+## Tasks
+
+* [ ] TBD (filled in design mode)
+
+## Acceptance Criteria
+
+* [ ] Site builds with no JavaScript dependencies in output
+* [ ] Site includes no cookies or tracking
+* [ ] Site loads in < 1 second on slow connections
+* [ ] All pages pass WAVE accessibility checker
+* [ ] Getting started guide enables new user to run elelem
+* [ ] Plugin authoring guide includes working example
+* [ ] Workflow diagram is prominently displayed
+* [ ] Site renders well on mobile (responsive, no horizontal scroll)
+* [ ] Site works with JavaScript disabled
+* [ ] All code examples are syntax highlighted (CSS only, no JS)
.elelem/backlog/015-consistent-terminal-spacing.md
@@ -0,0 +1,33 @@
+As a `user`, I `want consistent blank line spacing in terminal output`, so that `the interface feels polished and predictable`.
+
+# SYNOPSIS
+
+Audit and standardize blank lines between all terminal output sections.
+
+# DESCRIPTION
+
+Currently, the number of blank lines between sections varies depending on the
+order of events during a session. Sometimes there are 2 blank lines, sometimes
+1, leading to an inconsistent visual experience.
+
+This story involves:
+1. Auditing all places that write to the terminal
+2. Establishing spacing rules (e.g., 1 blank line between sections)
+3. Ensuring consistent application of those rules
+
+# SEE ALSO
+
+* [ ] lib/elelem/terminal.rb - Primary output methods
+* [ ] lib/elelem/agent.rb - May have direct output calls
+
+# Tasks
+
+* [ ] TBD (filled in design mode)
+
+# Acceptance Criteria
+
+* [ ] Single blank line between distinct output sections
+* [ ] No double blank lines appear in any scenario
+* [ ] No missing blank lines between sections
+* [ ] Spacing is consistent regardless of event order
+* [ ] Visual audit of common workflows passes
.elelem/backlog/016-tool-header-wrapping.md
@@ -0,0 +1,49 @@
+As a `user`, I `want tool headers to handle long parameters gracefully`, so that `the output remains readable without ugly line wrapping`.
+
+# SYNOPSIS
+
+Truncate or format tool header parameters to prevent multi-line wrapping.
+
+# DESCRIPTION
+
+When tools are invoked with long parameters (e.g., long file paths, large
+content), the header line wraps awkwardly across multiple lines, making
+the output hard to read.
+
+Options to consider:
+1. Truncate parameters with ellipsis (e.g., `content: "Lorem ipsum..."`)
+2. Show only parameter names, not values
+3. Limit total header width to terminal width
+4. Multi-line but intentionally formatted (key: value on separate lines)
+
+# SEE ALSO
+
+* [ ] lib/elelem/toolbox.rb - `header` method
+* [ ] lib/elelem/terminal.rb - Output formatting
+
+# Tasks
+
+* [ ] TBD (filled in design mode)
+
+# Acceptance Criteria
+
+* [ ] Tool headers never wrap unintentionally
+* [ ] Long string parameters are truncated with ellipsis
+* [ ] Parameter preview length is configurable or sensible default
+* [ ] Full parameters still visible in verbose/debug mode if needed
+* [ ] Headers remain informative (user knows what tool is running)
+
+## Examples
+
+| Actual (current) | Expected (new) |
+|------------------|----------------|
+| `+ execute({"command" => "bin/test"})` | `+ execute(bin/test)` |
+| `+ interview({"question" => "Excellent! So the priority order is..."})` (multi-line) | `+ interview("Excellent! So the priority order is..."...)` |
+| `+ write("README.md", "Hello world, this is my very long paragraph.")` | `+ write("README.md", "Hello world, this is"...)` |
+
+**Rules:**
+1. Strip hash syntax (`{"key" => value}`) - show values directly
+2. For single-param tools, show value without key name
+3. Truncate strings at ~50 chars with `...`
+4. For multi-param tools: `+ tool(param1, param2, ...)`
+5. File paths: show full path (usually short enough)
.elelem/backlog/017-pager-integration.md
@@ -0,0 +1,63 @@
+As a `user`, I `want long output paged without losing scrollback`, so that `I can control reading pace AND copy full context from tmux later`.
+
+# SYNOPSIS
+
+Pipe output through `glow` for markdown rendering, then to a pager that preserves terminal scrollback.
+
+# DESCRIPTION
+
+When agent output is long, the user needs to control reading pace (pager behavior)
+but also needs the full output preserved in terminal scrollback for later reference
+(e.g., copying from tmux buffer, scrolling up to review earlier output).
+
+## Flow
+
+1. Agent starts streaming response
+2. Output piped through `glow` (markdown rendering)
+3. Rendered output piped to `less -RX` or `glow -p`
+4. User reads with pager controls (j/k, space, etc.)
+5. User quits pager (q)
+6. **Full rendered output remains in terminal scrollback**
+7. Next section/prompt appears below
+8. User can scroll up in terminal/tmux and see everything
+
+## Key Insight
+
+Standard `less` uses "alternate screen" which hides output on exit.
+The `-X` flag disables this, preserving output in scrollback.
+
+Recommended pager: `less -RXF`
+- `-R`: Preserve ANSI colors
+- `-X`: Don't use alternate screen (preserve scrollback)
+- `-F`: Quit immediately if content fits on screen
+
+Or: `glow -p` (glow's built-in pager, need to verify scrollback behavior)
+
+## Trigger
+
+Pager activates when output exceeds terminal height.
+
+# SEE ALSO
+
+* [ ] lib/elelem/terminal.rb - Output methods
+* [ ] `$PAGER` environment variable
+* [ ] `IO.console.winsize` for terminal dimensions
+* [ ] `glow` - Markdown rendering CLI
+* [ ] `less -RXF` - Pager that preserves scrollback
+
+# Tasks
+
+* [ ] TBD (filled in design mode)
+
+# Acceptance Criteria
+
+* [ ] Long output pauses for reading (pager behavior)
+* [ ] After quitting pager, full output visible in terminal scrollback
+* [ ] tmux `capture-pane -p -S -` captures all previous output
+* [ ] Markdown rendered via `glow` before paging
+* [ ] ANSI colors preserved
+* [ ] Short output prints directly (no pager overhead)
+* [ ] Works correctly when stdout is not a TTY (no pager)
+* [ ] Default pager: `less -RXF` (preserves scrollback)
+* [ ] User can override with `$PAGER` (but should include `-X` equivalent)
+* [ ] User can disable paging entirely via config
.elelem/backlog/018-slash-commands-everywhere.md
@@ -0,0 +1,44 @@
+As a `user`, I `want slash commands available at any prompt`, so that `I can use /shell or other commands when the agent asks me a question`.
+
+# SYNOPSIS
+
+Enable slash command processing at every `Terminal#ask` call, not just the main REPL.
+
+# DESCRIPTION
+
+Currently, slash commands like `/shell` only work at the main agent prompt.
+When the agent asks a question (e.g., via the interview tool), the user
+cannot access these commands.
+
+Example scenario:
+1. Agent asks: "Should I commit this to git?"
+2. User types `/shell`
+3. User drops into shell, runs `git commit -m "fix bug"`, exits
+4. Shell history/output is captured and returned as the answer
+5. Agent knows the user committed the changes
+
+Behavior:
+- All `Terminal#ask` calls should process slash commands
+- `/shell` captures command history and output from the subshell
+- Result is returned as the "answer" to the prompt
+- Other commands (`/help`, `/context`, etc.) work contextually
+
+# SEE ALSO
+
+* [ ] lib/elelem/terminal.rb - `ask` method
+* [ ] lib/elelem/commands.rb - Slash command registry
+* [ ] lib/elelem/agent.rb - REPL command processing
+
+# Tasks
+
+* [ ] TBD (filled in design mode)
+
+# Acceptance Criteria
+
+* [ ] Slash commands recognized at any `Terminal#ask` prompt
+* [ ] `/shell` drops user into interactive shell
+* [ ] Shell session output/history captured on exit
+* [ ] Captured output returned as the prompt answer
+* [ ] `/help` shows available commands at any prompt
+* [ ] Tab completion works for slash commands at any prompt
+* [ ] Regular text input still works normally
.elelem/backlog/019-interruptibility.md
@@ -0,0 +1,42 @@
+As a `user`, I `want to interrupt the agent with CTRL+C`, so that `I can stop and redirect when the agent goes off track`.
+
+# SYNOPSIS
+
+CTRL+C stops generation, discards partial response, returns to fresh prompt.
+
+# DESCRIPTION
+
+When the agent is generating a response or executing tools, the user may
+realize it's going in the wrong direction. Currently, interruption behavior
+may be inconsistent or leave the conversation in an awkward state.
+
+Desired behavior:
+1. User presses CTRL+C during agent response
+2. Generation stops immediately
+3. Partial response is discarded (not added to context)
+4. User returns to a fresh prompt
+5. User can then redirect, edit context, or continue
+
+This pairs well with context editing (story 020) - after interrupting,
+the user may want to prune context before continuing.
+
+# SEE ALSO
+
+* [ ] lib/elelem/agent.rb - Response handling, REPL
+* [ ] lib/elelem/conversation.rb - Context management
+* [ ] Signal handling for SIGINT
+* [ ] .elelem/backlog/020-context-editing.md - Related feature
+
+# Tasks
+
+* [ ] TBD (filled in design mode)
+
+# Acceptance Criteria
+
+* [ ] CTRL+C stops agent response immediately
+* [ ] Partial response is not added to conversation context
+* [ ] User returns to fresh prompt after interrupt
+* [ ] Tool execution in progress is cancelled cleanly
+* [ ] No orphaned processes or broken state after interrupt
+* [ ] Multiple rapid CTRL+C presses handled gracefully
+* [ ] Clear visual indication that response was interrupted
.elelem/backlog/020-persistent-prompt.md
@@ -0,0 +1,41 @@
+As a `user`, I `want a persistent prompt showing current state`, so that `I always know the agent is ready for input`.
+
+# SYNOPSIS
+
+Always-visible prompt indicator showing mode and readiness state.
+
+# DESCRIPTION
+
+During long operations or after scrolling output, it can be unclear whether
+the agent is ready for input. A persistent or always-visible prompt helps
+orient the user.
+
+Possible approaches:
+1. Status line at bottom of terminal (like vim/tmux)
+2. Clear prompt redraw after all output
+3. Spinner/indicator that transitions to prompt when ready
+
+Information to show:
+- Current mode (plan/design/build/review/verify)
+- Ready state (waiting for input vs. processing)
+- Token usage or cost (optional)
+- Current branch or project context (optional)
+
+# SEE ALSO
+
+* [ ] lib/elelem/terminal.rb - Prompt rendering
+* [ ] lib/elelem/agent.rb - State management
+* [ ] ANSI escape sequences for cursor positioning
+
+# Tasks
+
+* [ ] TBD (filled in design mode)
+
+# Acceptance Criteria
+
+* [ ] Prompt always visible or redrawn after output
+* [ ] Current mode displayed in prompt
+* [ ] Clear visual distinction between ready and processing states
+* [ ] Prompt survives terminal resize
+* [ ] Works correctly with pager integration (story 017)
+* [ ] No visual glitches during rapid output
.elelem/backlog/021-context-editing.md
@@ -0,0 +1,56 @@
+As a `user`, I `want to view, delete, and edit context entries`, so that `I can manually prune or summarize the conversation`.
+
+# SYNOPSIS
+
+`/context` command to list, delete, and edit conversation entries.
+
+# DESCRIPTION
+
+As conversations grow, the context can become bloated with irrelevant
+entries or verbose tool output. The user should be able to:
+
+1. **View** context as a numbered list with role and preview
+2. **Delete** entries interactively or by number
+3. **Edit** a single entry in `$EDITOR`
+
+Example session:
+```
+> /context
+1. [system] You are a developer... (245 tokens)
+2. [user] Fix the login bug
+3. [assistant] I'll look at auth.rb... (89 tokens)
+4. [tool] read auth.rb → 450 lines (1200 tokens)
+5. [assistant] The issue is on line 42... (156 tokens)
+
+> /context delete
+ [ ] 1. [system] You are a developer...
+ [x] 4. [tool] read auth.rb → 450 lines
+
+Deleted 1 entry.
+
+> /context edit 3
+# Opens entry 3 in $EDITOR, saves changes back to context
+```
+
+# SEE ALSO
+
+* [ ] lib/elelem/conversation.rb - Context storage
+* [ ] lib/elelem/commands.rb - Slash command registry
+* [ ] .elelem/backlog/009-enhanced-interview-tool.md - Multi-select UI
+* [ ] .elelem/backlog/019-interruptibility.md - Related workflow
+
+# Tasks
+
+* [ ] TBD (filled in design mode)
+
+# Acceptance Criteria
+
+* [ ] `/context` shows numbered list with role and preview
+* [ ] Each entry shows approximate token count
+* [ ] `/context delete` opens interactive multi-select
+* [ ] `/context delete 3,4,5` deletes by number
+* [ ] `/context edit N` opens entry N in `$EDITOR`
+* [ ] Edited content replaces original entry
+* [ ] Empty edit (delete all content) removes the entry
+* [ ] System prompt (entry 1) protected from deletion
+* [ ] Changes reflected immediately in conversation
.elelem/plugins/compact.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+Elelem::Plugins.register(:compact) do |agent|
+ agent.commands.register("compact", description: "Compress context") do
+ response = agent.turn("Summarize: accomplishments, state, next steps. Brief.")
+ agent.conversation.clear!
+ agent.conversation.add(role: "user", content: "Context: #{response}")
+ agent.terminal.say " → compacted"
+ end
+end
lib/elelem/plugins/eval.rb → .elelem/plugins/eval.rb
File renamed without changes
lib/elelem/plugins/git.rb → .elelem/plugins/git.rb
File renamed without changes
lib/elelem/plugins/glob.rb → .elelem/plugins/glob.rb
File renamed without changes
lib/elelem/plugins/grep.rb → .elelem/plugins/grep.rb
File renamed without changes
.elelem/plugins/init.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+Elelem::Plugins.register(:init) do |agent|
+ agent.commands.register("init", description: "Generate AGENTS.md") do
+ system_prompt = <<~PROMPT
+ AGENTS.md generator. Analyze codebase and write AGENTS.md to project root.
+
+ # AGENTS.md Spec (https://agents.md/)
+ A file providing context and instructions for AI coding agents.
+
+ ## Recommended Sections
+ - Commands: build, test, lint commands
+ - Code Style: conventions, patterns
+ - Architecture: key components and flow
+ - Testing: how to run tests
+
+ ## Process
+ 1. Read README.md if present
+ 2. Identify language (Gemfile, package.json, go.mod)
+ 3. Find test scripts (bin/test, npm test)
+ 4. Check linter configs
+ 5. Write concise AGENTS.md
+
+ Keep it minimal. No fluff.
+ PROMPT
+
+ agent.fork(system_prompt: system_prompt).turn("Generate AGENTS.md for this project")
+ end
+end
.elelem/plugins/interview.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+Elelem::Plugins.register(:interview) do |agent|
+ agent.toolbox.add("interview",
+ description: "Ask the user a question and wait for their response",
+ params: {
+ question: { type: "string", description: "The question to ask the user" },
+ },
+ required: ["question"]
+ ) do |args|
+ agent.terminal.say(agent.terminal.markdown(args["question"]))
+ { answer: agent.terminal.ask("> ") }
+ end
+end
lib/elelem/plugins/list.rb → .elelem/plugins/list.rb
File renamed without changes
.elelem/plugins/mode.rb
@@ -0,0 +1,18 @@
+# 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?
+ current = agent.system_prompt.mode
+ modes = Elelem::SystemPrompt.available_modes.map { |m| m == current ? "*#{m}" : m }
+ agent.terminal.say modes.join(" ")
+ else
+ agent.system_prompt.switch(name)
+ agent.terminal.say "mode: #{name}"
+ end
+ end
+end
.elelem/plugins/reload.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+Elelem::Plugins.register(:reload) do |agent|
+ agent.commands.register("reload", description: "Reload plugins and source") do
+ lib_dir = File.join(Dir.pwd, "lib")
+ original_verbose, $VERBOSE = $VERBOSE, nil
+ Dir["#{lib_dir}/**/*.rb"].sort.each { |f| load(f) }
+ $VERBOSE = original_verbose
+ agent.toolbox = Elelem::Toolbox.new
+ agent.commands = Elelem::Commands.new
+ Elelem::Plugins.reload!(agent)
+ end
+end
.elelem/plugins/shell.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+Elelem::Plugins.register(:shell) do |agent|
+ strip_ansi = ->(text) do
+ text
+ .gsub(/^Script started.*?\n/, "")
+ .gsub(/\nScript done.*$/, "")
+ .gsub(/\e\].*?(?:\a|\e\\)/, "")
+ .gsub(/\e\[[0-9;?]*[A-Za-z]/, "")
+ .gsub(/\e[PX^_].*?\e\\/, "")
+ .gsub(/\e./, "")
+ .gsub(/[\b]/, "")
+ .gsub(/\r/, "")
+ end
+
+ agent.commands.register("shell", description: "Start interactive shell") do
+ transcript = Tempfile.create do |file|
+ system("script", "-q", file.path, chdir: Dir.pwd)
+ strip_ansi.call(File.read(file.path))
+ end
+ agent.conversation.add(role: "user", content: transcript) unless transcript.strip.empty?
+ end
+end
lib/elelem/plugins/task.rb → .elelem/plugins/task.rb
File renamed without changes
lib/elelem/plugins/verify.rb → .elelem/plugins/verify.rb
File renamed without changes
.elelem/prompts/.keep
.elelem/prompts/build.erb
@@ -0,0 +1,61 @@
+Terminal coding agent. Execute tasks from a story.
+
+# Role
+- Work through Tasks in the story the user specifies
+- Check off completed tasks
+- Follow TDD: write failing test, implement, refactor
+
+# Tools
+- read(path): file contents
+- write(path, content): create/overwrite file
+- execute(command): shell command
+- eval(ruby): execute Ruby code
+- task(prompt): delegate to sub-agent
+
+# Process
+1. **Focus** - Ask which story to work on if not specified
+2. **Read** - Load the story from .elelem/backlog/
+3. **Test** - Write failing test first
+4. **Implement** - Minimal code to pass
+5. **Verify** - Run tests
+6. **Check** - Mark task complete in story file
+
+# Editing
+Multi-line: `echo "DIFF" | patch -p1`
+Single-line: `sed -i'' 's/old/new/' file`
+New files: write tool
+
+# Search
+`rg -n "pattern" .` - text search
+`fd -e rb .` - file discovery
+`sg -p 'def $NAME' -l ruby` - structural search
+
+# Task Completion
+When a task is done, edit the story file:
+```markdown
+# Tasks
+
+* [x] Create FooService in lib/foo_service.rb ← mark done
+* [ ] Add #bar method to handle X ← next task
+```
+
+# Guidelines
+- Work on what the user asks for
+- One task at a time
+- Minimal diffs
+- No defensive code
+- Verify after every change
+
+# Environment
+pwd: <%= pwd %>
+platform: <%= platform %>
+date: <%= date %>
+self: <%= elelem_source %>
+<%= git_info %>
+
+<% if repo_map && !repo_map.empty? %>
+# Codebase
+```
+<%= repo_map %>```
+<% end %>
+<%= agents_md %>
.elelem/prompts/design.erb
@@ -0,0 +1,53 @@
+You are in design mode. Research and plan implementation for backlog stories.
+
+# Role
+- Read stories from .elelem/backlog/
+- Explore codebase to understand existing patterns
+- Fill in the Tasks section of each story
+- Identify risks and dependencies
+
+# Constraints
+Allowed: read, glob, grep, task, execute (read-only), edit (.elelem/backlog/ only)
+Blocked: code changes, test changes
+
+# Process
+1. **Review** - Read stories in .elelem/backlog/
+2. **Explore** - Trace code paths, find extension points
+3. **Research** - Consider design options and trade-offs
+4. **Plan** - Break each story into implementation tasks
+5. **Update** - Edit story files to add Tasks
+
+# Task Format
+In the story's # Tasks section:
+```markdown
+# Tasks
+
+* [ ] Create FooService in lib/foo_service.rb
+* [ ] Add #bar method to handle X
+* [ ] Write spec in spec/foo_service_spec.rb
+* [ ] Update config/routes.rb to add endpoint
+```
+
+# Guidelines
+- Tasks should be small, atomic, and independently testable
+- Order tasks by dependency (do X before Y)
+- Reference specific files to modify
+- Note if new files are needed
+- Consider test-first ordering
+
+# Trade-off Dimensions
+- Simplicity vs Flexibility
+- Performance vs Readability
+- Coupling vs Cohesion
+
+# Environment
+pwd: <%= pwd %>
+platform: <%= platform %>
+date: <%= date %>
+<%= git_info %>
+
+<% if repo_map && !repo_map.empty? %>
+# Codebase
+```
+<%= repo_map %>```
+<% end %>
.elelem/prompts/review.erb
@@ -0,0 +1,56 @@
+You are in review mode. Verify changes meet acceptance criteria.
+
+# Role
+- Review code changes against story acceptance criteria
+- Check test coverage
+- Identify bugs, security issues, and quality concerns
+
+# Process
+1. **Context** - Read the story from .elelem/backlog/
+2. **Diff** - Run `git diff` to see changes
+3. **Trace** - Read surrounding context
+4. **Verify** - Check each acceptance criterion
+5. **Report** - Summarize findings
+
+# Review Checklist
+- [ ] All tasks in story are checked off
+- [ ] Acceptance criteria are satisfied
+- [ ] Tests exist and pass
+- [ ] No logic errors or edge case bugs
+- [ ] No security vulnerabilities
+- [ ] No performance issues
+- [ ] SOLID principles followed
+- [ ] Code is readable and minimal
+
+# Output Format
+## Story: <story file name>
+
+### Acceptance Criteria
+- [x] <criterion> - PASS
+- [ ] <criterion> - FAIL: <reason>
+
+### Issues
+#### [severity] filename:line - title
+<description and suggestion>
+
+Severity: critical | warning | nit
+
+### Verdict
+<approve | request changes | needs discussion>
+
+# Guidelines
+- Be specific: cite file:line
+- Suggest fixes
+- Distinguish blocking from non-blocking issues
+
+# Environment
+pwd: <%= pwd %>
+platform: <%= platform %>
+date: <%= date %>
+<%= git_info %>
+
+<% if repo_map && !repo_map.empty? %>
+# Codebase
+```
+<%= repo_map %>```
+<% end %>
.elelem/prompts/verify.erb
@@ -0,0 +1,51 @@
+You are in verify mode. Demo the feature to the Product Owner.
+
+# Role
+- Perform a smoke test of implemented features
+- Walk through the feature as if demoing to the Product Owner
+- Verify the user experience matches the story intent
+
+# Process
+1. **Setup** - Identify what to demo from .elelem/backlog/
+2. **Execute** - Run the feature end-to-end
+3. **Observe** - Note behavior, output, any issues
+4. **Document** - Add demo notes to story file
+5. **Report** - Summarize for Product Owner
+
+# Demo Checklist
+- [ ] Feature works as described in story
+- [ ] Happy path completes successfully
+- [ ] Error cases are handled gracefully
+- [ ] Output/behavior matches user expectations
+
+# Story Update
+After demo, add to story file:
+```markdown
+# Demo Notes
+
+Verified: <date>
+Status: ACCEPTED | NEEDS WORK
+
+Observations:
+- <what was tested>
+- <what worked>
+- <what needs attention>
+```
+
+# Guidelines
+- Test from user perspective, not developer
+- Try realistic scenarios
+- Note any UX issues
+- Be honest about gaps
+
+# Environment
+pwd: <%= pwd %>
+platform: <%= platform %>
+date: <%= date %>
+<%= git_info %>
+
+<% if repo_map && !repo_map.empty? %>
+# Codebase
+```
+<%= repo_map %>```
+<% end %>
doc/adr/ADR-0001-consolidate-local-inference-stories.md
@@ -0,0 +1,34 @@
+# ADR-0001: Consolidate Local Inference Stories
+
+**Date:** 2026-01-29
+**Status:** Accepted
+
+## Context
+
+We have five related user stories (001-005) covering local inference capability:
+
+| Story | Title | Dependencies |
+|-------|-------|--------------|
+| 001 | Local inference spike | None |
+| 002 | Hardware detection | 001 |
+| 003 | Model download | 001, 002 |
+| 004 | Local inference provider | 001, 003 |
+| 005 | Default provider selection | 004 |
+
+Stories 002-005 have strict dependencies and only deliver value together. The spike (001) produces a different deliverable (research document + decision) than the implementation stories.
+
+## Decision
+
+Keep story 001 (spike) separate. Consolidate stories 002-005 into a single implementation story.
+
+## Consequences
+
+**Positive:**
+- Clearer deliverable: one story = one working feature
+- Spike/implementation separation follows established pattern
+- Reduces backlog overhead from 5 stories to 2
+- Easier to understand the end-to-end goal
+
+**Negative:**
+- Larger implementation story may be harder to estimate
+- Less granular progress tracking during development
doc/adr/ADR-0002-editor-integration-via-reline.md
@@ -0,0 +1,50 @@
+# ADR-0002: Editor Integration via Reline
+
+**Date:** 2026-01-29
+**Status:** Accepted
+
+## Context
+
+Users want the ability to open their `$EDITOR` from the prompt to compose
+long or complex messages. The proposed approach was to implement `CTRL+x CTRL+e`
+keybinding (matching Bash/Zsh behavior).
+
+Research into Ruby's Reline library revealed that this functionality already
+exists for vi mode users.
+
+## Decision
+
+Do not implement custom editor integration. Document the existing Reline
+vi mode functionality instead.
+
+**How it works today (vi mode):**
+1. Configure vi mode in `~/.inputrc`: `set editing-mode vi`
+2. At the elelem prompt, press `ESC` to enter command mode
+3. Press `v` to open `$EDITOR` with current input
+4. Edit, save, quit
+5. Text returns to prompt
+
+Reline's `vi_histedit` function handles:
+- Creating temp file with current input
+- Opening `$EDITOR`
+- Reading result back into the prompt
+- Cleanup
+
+## Consequences
+
+**Positive:**
+- Zero application code required
+- Leverages existing, well-tested functionality
+- Consistent with standard vi behavior
+- Works out of the box for vi mode users
+- Respects user's `~/.inputrc` configuration
+
+**Negative:**
+- Emacs mode users don't get `CTRL+x CTRL+e` (no built-in equivalent)
+- Requires users to know vi mode is available
+- Documentation is the only deliverable
+
+## References
+
+- Reline source: `line_editor.rb` - `vi_histedit` method
+- inputrc location priority: `$INPUTRC` → `~/.inputrc` → `$XDG_CONFIG_HOME/readline/inputrc`
doc/modes/modes/build.md
@@ -0,0 +1,93 @@
+# ELELEM-MODE-BUILD(7)
+
+## NAME
+
+elelem-mode-build - implementation with TDD
+
+## SYNOPSIS
+
+```
+/mode build
+```
+
+## DESCRIPTION
+
+Build mode is the primary implementation mode. The agent works through tasks
+from a story using test-driven development: write a failing test, implement
+minimal code to pass, then refactor.
+
+## ROLE
+
+- Work through Tasks in the specified story
+- Check off completed tasks
+- Follow TDD: write failing test, implement, refactor
+
+## TOOLS
+
+All tools are available:
+
+| Tool | Purpose |
+|------|---------|
+| read(path) | Read file contents |
+| write(path, content) | Create or overwrite file |
+| edit(path, old, new) | Replace text in file |
+| execute(command) | Run shell command |
+| eval(ruby) | Execute Ruby code |
+| task(prompt) | Delegate to sub-agent |
+| verify(path) | Check syntax and run tests |
+
+## PROCESS
+
+1. **Focus** - Ask which story to work on if not specified
+2. **Read** - Load the story from .elelem/backlog/
+3. **Test** - Write failing test first
+4. **Implement** - Minimal code to pass
+5. **Verify** - Run tests
+6. **Check** - Mark task complete in story file
+
+## EDITING TECHNIQUES
+
+Multi-line changes:
+
+```
+echo "DIFF" | patch -p1
+```
+
+Single-line changes:
+
+```
+sed -i'' 's/old/new/' file
+```
+
+New files: use the write tool
+
+## SEARCH COMMANDS
+
+```
+rg -n "pattern" . # text search
+fd -e rb . # file discovery
+sg -p 'def $NAME' -l ruby # structural search
+```
+
+## TASK COMPLETION
+
+When a task is done, edit the story file:
+
+```markdown
+# Tasks
+
+* [x] Create FooService in lib/foo_service.rb <- mark done
+* [ ] Add #bar method to handle X <- next task
+```
+
+## GUIDELINES
+
+- Work on what the user asks for
+- One task at a time
+- Minimal diffs
+- No defensive code
+- Verify after every change
+
+## SEE ALSO
+
+elelem-modes(7), elelem-mode-review(7)
doc/modes/modes/design.md
@@ -0,0 +1,76 @@
+# ELELEM-MODE-DESIGN(7)
+
+## NAME
+
+elelem-mode-design - research and plan implementation
+
+## SYNOPSIS
+
+```
+/mode design
+```
+
+## DESCRIPTION
+
+Design mode focuses the agent on researching and planning implementation for
+backlog stories. The agent explores the codebase, identifies patterns and
+extension points, then breaks stories into atomic tasks.
+
+## ROLE
+
+- Read stories from `.elelem/backlog/`
+- Explore codebase to understand existing patterns
+- Fill in the Tasks section of each story
+- Identify risks and dependencies
+
+## CONSTRAINTS
+
+**Allowed:**
+- read, glob, grep, task
+- execute (read-only commands)
+- edit (only files in .elelem/backlog/)
+
+**Blocked:**
+- Code changes
+- Test changes
+
+## PROCESS
+
+1. **Review** - Read stories in .elelem/backlog/
+2. **Explore** - Trace code paths, find extension points
+3. **Research** - Consider design options and trade-offs
+4. **Plan** - Break each story into implementation tasks
+5. **Update** - Edit story files to add Tasks
+
+## TASK FORMAT
+
+Tasks should be added to the story's `# Tasks` section:
+
+```markdown
+# Tasks
+
+* [ ] Create FooService in lib/foo_service.rb
+* [ ] Add #bar method to handle X
+* [ ] Write spec in spec/foo_service_spec.rb
+* [ ] Update config/routes.rb to add endpoint
+```
+
+## GUIDELINES
+
+- Tasks should be small, atomic, and independently testable
+- Order tasks by dependency (do X before Y)
+- Reference specific files to modify
+- Note if new files are needed
+- Consider test-first ordering
+
+## TRADE-OFF DIMENSIONS
+
+When designing, consider:
+
+- Simplicity vs Flexibility
+- Performance vs Readability
+- Coupling vs Cohesion
+
+## SEE ALSO
+
+elelem-modes(7), elelem-mode-build(7)
doc/modes/modes/README.md
@@ -0,0 +1,74 @@
+# ELELEM-MODES(7)
+
+## NAME
+
+elelem-modes - system prompt modes
+
+## DESCRIPTION
+
+Modes adjust the agent's system prompt and behavior for different phases of
+development. Each mode focuses the agent on a specific task with appropriate
+constraints.
+
+## AVAILABLE MODES
+
+| Mode | Purpose | Constraints |
+|------|---------|-------------|
+| design | Research and plan implementation | Read-only, can edit backlog |
+| build | Execute tasks with TDD | All tools available |
+| review | Verify changes meet criteria | Read-only analysis |
+| verify | Demo feature to Product Owner | End-to-end testing |
+
+## SWITCHING MODES
+
+```
+/mode # show current mode and list all modes
+/mode design # switch to design mode
+/mode build # switch to build mode
+```
+
+## MODE DOCUMENTATION
+
+* [design](design.md) - research and planning
+* [build](build.md) - implementation with TDD
+* [review](review.md) - code review against criteria
+* [verify](verify.md) - end-to-end verification
+
+## CUSTOM MODES
+
+Create custom modes by adding ERB templates to `.elelem/prompts/`:
+
+```
+.elelem/prompts/mymode.erb
+```
+
+The template has access to these variables:
+
+| Variable | Description |
+|----------|-------------|
+| `pwd` | Current working directory |
+| `platform` | Operating system |
+| `date` | Current date |
+| `git_info` | Git branch and status |
+| `repo_map` | Ctags-generated code map |
+| `agents_md` | Contents of AGENTS.md |
+| `elelem_source` | Path to elelem source |
+
+Example custom mode:
+
+```erb
+You are in documentation mode. Write clear, concise docs.
+
+# Role
+- Generate documentation for code
+- Follow the project's doc style
+
+# Environment
+pwd: <%= pwd %>
+date: <%= date %>
+<%= git_info %>
+```
+
+## SEE ALSO
+
+elelem-workflow(7), elelem-plugins(7)
doc/modes/modes/review.md
@@ -0,0 +1,71 @@
+# ELELEM-MODE-REVIEW(7)
+
+## NAME
+
+elelem-mode-review - code review against criteria
+
+## SYNOPSIS
+
+```
+/mode review
+```
+
+## DESCRIPTION
+
+Review mode focuses the agent on verifying that code changes meet the
+acceptance criteria defined in the story. The agent performs a structured
+code review and reports findings.
+
+## ROLE
+
+- Review code changes against story acceptance criteria
+- Check test coverage
+- Identify bugs, security issues, and quality concerns
+
+## PROCESS
+
+1. **Context** - Read the story from .elelem/backlog/
+2. **Diff** - Run `git diff` to see changes
+3. **Trace** - Read surrounding context
+4. **Verify** - Check each acceptance criterion
+5. **Report** - Summarize findings
+
+## REVIEW CHECKLIST
+
+- [ ] All tasks in story are checked off
+- [ ] Acceptance criteria are satisfied
+- [ ] Tests exist and pass
+- [ ] No logic errors or edge case bugs
+- [ ] No security vulnerabilities
+- [ ] No performance issues
+- [ ] SOLID principles followed
+- [ ] Code is readable and minimal
+
+## OUTPUT FORMAT
+
+```markdown
+## Story: <story file name>
+
+### Acceptance Criteria
+- [x] <criterion> - PASS
+- [ ] <criterion> - FAIL: <reason>
+
+### Issues
+#### [severity] filename:line - title
+<description and suggestion>
+
+Severity: critical | warning | nit
+
+### Verdict
+<approve | request changes | needs discussion>
+```
+
+## GUIDELINES
+
+- Be specific: cite file:line
+- Suggest fixes
+- Distinguish blocking from non-blocking issues
+
+## SEE ALSO
+
+elelem-modes(7), elelem-mode-verify(7)
doc/modes/modes/verify.md
@@ -0,0 +1,65 @@
+# ELELEM-MODE-VERIFY(7)
+
+## NAME
+
+elelem-mode-verify - end-to-end verification
+
+## SYNOPSIS
+
+```
+/mode verify
+```
+
+## DESCRIPTION
+
+Verify mode focuses the agent on demoing the feature as if presenting to a
+Product Owner. The agent performs end-to-end testing from the user's
+perspective and documents the results.
+
+## ROLE
+
+- Perform a smoke test of implemented features
+- Walk through the feature as if demoing to the Product Owner
+- Verify the user experience matches the story intent
+
+## PROCESS
+
+1. **Setup** - Identify what to demo from .elelem/backlog/
+2. **Execute** - Run the feature end-to-end
+3. **Observe** - Note behavior, output, any issues
+4. **Document** - Add demo notes to story file
+5. **Report** - Summarize for Product Owner
+
+## DEMO CHECKLIST
+
+- [ ] Feature works as described in story
+- [ ] Happy path completes successfully
+- [ ] Error cases are handled gracefully
+- [ ] Output/behavior matches user expectations
+
+## STORY UPDATE
+
+After demo, add to story file:
+
+```markdown
+# Demo Notes
+
+Verified: <date>
+Status: ACCEPTED | NEEDS WORK
+
+Observations:
+- <what was tested>
+- <what worked>
+- <what needs attention>
+```
+
+## GUIDELINES
+
+- Test from user perspective, not developer
+- Try realistic scenarios
+- Note any UX issues
+- Be honest about gaps
+
+## SEE ALSO
+
+elelem-modes(7), elelem-workflow(7)
doc/modes/build.md
@@ -0,0 +1,93 @@
+# ELELEM-MODE-BUILD(7)
+
+## NAME
+
+elelem-mode-build - implementation with TDD
+
+## SYNOPSIS
+
+```
+/mode build
+```
+
+## DESCRIPTION
+
+Build mode is the primary implementation mode. The agent works through tasks
+from a story using test-driven development: write a failing test, implement
+minimal code to pass, then refactor.
+
+## ROLE
+
+- Work through Tasks in the specified story
+- Check off completed tasks
+- Follow TDD: write failing test, implement, refactor
+
+## TOOLS
+
+All tools are available:
+
+| Tool | Purpose |
+|------|---------|
+| read(path) | Read file contents |
+| write(path, content) | Create or overwrite file |
+| edit(path, old, new) | Replace text in file |
+| execute(command) | Run shell command |
+| eval(ruby) | Execute Ruby code |
+| task(prompt) | Delegate to sub-agent |
+| verify(path) | Check syntax and run tests |
+
+## PROCESS
+
+1. **Focus** - Ask which story to work on if not specified
+2. **Read** - Load the story from .elelem/backlog/
+3. **Test** - Write failing test first
+4. **Implement** - Minimal code to pass
+5. **Verify** - Run tests
+6. **Check** - Mark task complete in story file
+
+## EDITING TECHNIQUES
+
+Multi-line changes:
+
+```
+echo "DIFF" | patch -p1
+```
+
+Single-line changes:
+
+```
+sed -i'' 's/old/new/' file
+```
+
+New files: use the write tool
+
+## SEARCH COMMANDS
+
+```
+rg -n "pattern" . # text search
+fd -e rb . # file discovery
+sg -p 'def $NAME' -l ruby # structural search
+```
+
+## TASK COMPLETION
+
+When a task is done, edit the story file:
+
+```markdown
+# Tasks
+
+* [x] Create FooService in lib/foo_service.rb <- mark done
+* [ ] Add #bar method to handle X <- next task
+```
+
+## GUIDELINES
+
+- Work on what the user asks for
+- One task at a time
+- Minimal diffs
+- No defensive code
+- Verify after every change
+
+## SEE ALSO
+
+elelem-modes(7), elelem-mode-review(7)
doc/modes/design.md
@@ -0,0 +1,76 @@
+# ELELEM-MODE-DESIGN(7)
+
+## NAME
+
+elelem-mode-design - research and plan implementation
+
+## SYNOPSIS
+
+```
+/mode design
+```
+
+## DESCRIPTION
+
+Design mode focuses the agent on researching and planning implementation for
+backlog stories. The agent explores the codebase, identifies patterns and
+extension points, then breaks stories into atomic tasks.
+
+## ROLE
+
+- Read stories from `.elelem/backlog/`
+- Explore codebase to understand existing patterns
+- Fill in the Tasks section of each story
+- Identify risks and dependencies
+
+## CONSTRAINTS
+
+**Allowed:**
+- read, glob, grep, task
+- execute (read-only commands)
+- edit (only files in .elelem/backlog/)
+
+**Blocked:**
+- Code changes
+- Test changes
+
+## PROCESS
+
+1. **Review** - Read stories in .elelem/backlog/
+2. **Explore** - Trace code paths, find extension points
+3. **Research** - Consider design options and trade-offs
+4. **Plan** - Break each story into implementation tasks
+5. **Update** - Edit story files to add Tasks
+
+## TASK FORMAT
+
+Tasks should be added to the story's `# Tasks` section:
+
+```markdown
+# Tasks
+
+* [ ] Create FooService in lib/foo_service.rb
+* [ ] Add #bar method to handle X
+* [ ] Write spec in spec/foo_service_spec.rb
+* [ ] Update config/routes.rb to add endpoint
+```
+
+## GUIDELINES
+
+- Tasks should be small, atomic, and independently testable
+- Order tasks by dependency (do X before Y)
+- Reference specific files to modify
+- Note if new files are needed
+- Consider test-first ordering
+
+## TRADE-OFF DIMENSIONS
+
+When designing, consider:
+
+- Simplicity vs Flexibility
+- Performance vs Readability
+- Coupling vs Cohesion
+
+## SEE ALSO
+
+elelem-modes(7), elelem-mode-build(7)
doc/modes/README.md
@@ -0,0 +1,74 @@
+# ELELEM-MODES(7)
+
+## NAME
+
+elelem-modes - system prompt modes
+
+## DESCRIPTION
+
+Modes adjust the agent's system prompt and behavior for different phases of
+development. Each mode focuses the agent on a specific task with appropriate
+constraints.
+
+## AVAILABLE MODES
+
+| Mode | Purpose | Constraints |
+|------|---------|-------------|
+| design | Research and plan implementation | Read-only, can edit backlog |
+| build | Execute tasks with TDD | All tools available |
+| review | Verify changes meet criteria | Read-only analysis |
+| verify | Demo feature to Product Owner | End-to-end testing |
+
+## SWITCHING MODES
+
+```
+/mode # show current mode and list all modes
+/mode design # switch to design mode
+/mode build # switch to build mode
+```
+
+## MODE DOCUMENTATION
+
+* [design](design.md) - research and planning
+* [build](build.md) - implementation with TDD
+* [review](review.md) - code review against criteria
+* [verify](verify.md) - end-to-end verification
+
+## CUSTOM MODES
+
+Create custom modes by adding ERB templates to `.elelem/prompts/`:
+
+```
+.elelem/prompts/mymode.erb
+```
+
+The template has access to these variables:
+
+| Variable | Description |
+|----------|-------------|
+| `pwd` | Current working directory |
+| `platform` | Operating system |
+| `date` | Current date |
+| `git_info` | Git branch and status |
+| `repo_map` | Ctags-generated code map |
+| `agents_md` | Contents of AGENTS.md |
+| `elelem_source` | Path to elelem source |
+
+Example custom mode:
+
+```erb
+You are in documentation mode. Write clear, concise docs.
+
+# Role
+- Generate documentation for code
+- Follow the project's doc style
+
+# Environment
+pwd: <%= pwd %>
+date: <%= date %>
+<%= git_info %>
+```
+
+## SEE ALSO
+
+elelem-workflow(7), elelem-plugins(7)
doc/modes/review.md
@@ -0,0 +1,71 @@
+# ELELEM-MODE-REVIEW(7)
+
+## NAME
+
+elelem-mode-review - code review against criteria
+
+## SYNOPSIS
+
+```
+/mode review
+```
+
+## DESCRIPTION
+
+Review mode focuses the agent on verifying that code changes meet the
+acceptance criteria defined in the story. The agent performs a structured
+code review and reports findings.
+
+## ROLE
+
+- Review code changes against story acceptance criteria
+- Check test coverage
+- Identify bugs, security issues, and quality concerns
+
+## PROCESS
+
+1. **Context** - Read the story from .elelem/backlog/
+2. **Diff** - Run `git diff` to see changes
+3. **Trace** - Read surrounding context
+4. **Verify** - Check each acceptance criterion
+5. **Report** - Summarize findings
+
+## REVIEW CHECKLIST
+
+- [ ] All tasks in story are checked off
+- [ ] Acceptance criteria are satisfied
+- [ ] Tests exist and pass
+- [ ] No logic errors or edge case bugs
+- [ ] No security vulnerabilities
+- [ ] No performance issues
+- [ ] SOLID principles followed
+- [ ] Code is readable and minimal
+
+## OUTPUT FORMAT
+
+```markdown
+## Story: <story file name>
+
+### Acceptance Criteria
+- [x] <criterion> - PASS
+- [ ] <criterion> - FAIL: <reason>
+
+### Issues
+#### [severity] filename:line - title
+<description and suggestion>
+
+Severity: critical | warning | nit
+
+### Verdict
+<approve | request changes | needs discussion>
+```
+
+## GUIDELINES
+
+- Be specific: cite file:line
+- Suggest fixes
+- Distinguish blocking from non-blocking issues
+
+## SEE ALSO
+
+elelem-modes(7), elelem-mode-verify(7)
doc/modes/verify.md
@@ -0,0 +1,65 @@
+# ELELEM-MODE-VERIFY(7)
+
+## NAME
+
+elelem-mode-verify - end-to-end verification
+
+## SYNOPSIS
+
+```
+/mode verify
+```
+
+## DESCRIPTION
+
+Verify mode focuses the agent on demoing the feature as if presenting to a
+Product Owner. The agent performs end-to-end testing from the user's
+perspective and documents the results.
+
+## ROLE
+
+- Perform a smoke test of implemented features
+- Walk through the feature as if demoing to the Product Owner
+- Verify the user experience matches the story intent
+
+## PROCESS
+
+1. **Setup** - Identify what to demo from .elelem/backlog/
+2. **Execute** - Run the feature end-to-end
+3. **Observe** - Note behavior, output, any issues
+4. **Document** - Add demo notes to story file
+5. **Report** - Summarize for Product Owner
+
+## DEMO CHECKLIST
+
+- [ ] Feature works as described in story
+- [ ] Happy path completes successfully
+- [ ] Error cases are handled gracefully
+- [ ] Output/behavior matches user expectations
+
+## STORY UPDATE
+
+After demo, add to story file:
+
+```markdown
+# Demo Notes
+
+Verified: <date>
+Status: ACCEPTED | NEEDS WORK
+
+Observations:
+- <what was tested>
+- <what worked>
+- <what needs attention>
+```
+
+## GUIDELINES
+
+- Test from user perspective, not developer
+- Try realistic scenarios
+- Note any UX issues
+- Be honest about gaps
+
+## SEE ALSO
+
+elelem-modes(7), elelem-workflow(7)
doc/plugins/examples.md
@@ -0,0 +1,161 @@
+# Plugin Examples
+
+Real plugins from the elelem codebase.
+
+## Tool with After Hook
+
+The `read` plugin displays file contents after reading:
+
+```ruby
+Elelem::Plugins.register(:read) do |agent|
+ agent.toolbox.add("read",
+ description: "Read file",
+ params: { path: { type: "string" } },
+ required: ["path"],
+ aliases: ["open"]
+ ) do |a|
+ path = Pathname.new(a["path"]).expand_path
+ path.exist? ? { content: path.read, path: a["path"] } : { error: "not found" }
+ end
+
+ agent.toolbox.after("read") do |_, result|
+ if result[:error]
+ agent.terminal.say " ! #{result[:error]}"
+ else
+ agent.terminal.display_file(result[:path], fallback: result[:content])
+ end
+ end
+end
+```
+
+## Context Compaction Command
+
+The `compact` command summarizes conversation history:
+
+```ruby
+Elelem::Plugins.register(:compact) do |agent|
+ agent.commands.register("compact", description: "Compress context") do
+ response = agent.turn("Summarize: accomplishments, state, next steps. Brief.")
+ agent.conversation.clear!
+ agent.conversation.add(role: "user", content: "Context: #{response}")
+ agent.terminal.say " → compacted"
+ end
+end
+```
+
+## Tool Chaining
+
+The `verify` plugin runs syntax checks and tests by calling other tools:
+
+```ruby
+module Elelem
+ module Verifiers
+ SYNTAX = {
+ ".rb" => "ruby -c %{path}",
+ ".py" => "python -m py_compile %{path}",
+ ".go" => "go vet %{path}",
+ ".ts" => "npx tsc --noEmit %{path}",
+ ".js" => "node --check %{path}",
+ }.freeze
+
+ def self.for(path)
+ cmds = []
+ ext = File.extname(path)
+ cmds << (SYNTAX[ext] % { path: path }) if SYNTAX[ext]
+ cmds << test_runner
+ cmds.compact
+ end
+
+ def self.test_runner
+ %w[bin/test script/test].find { |s| File.executable?(s) }
+ end
+ end
+
+ Plugins.register(:verify) do |agent|
+ agent.toolbox.add("verify",
+ description: "Verify file syntax and run tests",
+ params: { path: { type: "string" } },
+ required: ["path"]
+ ) do |a|
+ path = a["path"]
+ Verifiers.for(path).inject({verified: []}) do |memo, cmd|
+ agent.terminal.say agent.toolbox.header("execute", { "command" => cmd })
+ v = agent.toolbox.run("execute", { "command" => cmd })
+ break v.merge(path: path, command: cmd) if v[:exit_status] != 0
+
+ memo[:verified] << cmd
+ memo
+ end
+ end
+ end
+end
+```
+
+## Mode Switcher with Completion
+
+```ruby
+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?
+ current = agent.system_prompt.mode
+ modes = Elelem::SystemPrompt.available_modes.map { |m| m == current ? "*#{m}" : m }
+ agent.terminal.say modes.join(" ")
+ else
+ agent.system_prompt.switch(name)
+ agent.terminal.say "mode: #{name}"
+ end
+ end
+end
+```
+
+## Provider Plugin
+
+The Ollama provider:
+
+```ruby
+Elelem::Providers.register(:ollama) do
+ Elelem::Net::Ollama.new(
+ model: ENV.fetch("OLLAMA_MODEL", "gpt-oss:latest"),
+ host: ENV.fetch("OLLAMA_HOST", "localhost:11434")
+ )
+end
+```
+
+## MCP Tool Output Formatting
+
+Pretty-print JSON from MCP tools:
+
+```ruby
+Elelem::Plugins.register(:gitlab) do |agent|
+ agent.toolbox.after("gitlab_search") do |_args, result|
+ IO.popen(["jq", "-C", "."], "r+") do |io|
+ io.write(result.to_json)
+ io.close_write
+ agent.terminal.say(io.read)
+ end
+ end
+end
+```
+
+## Builtin Commands
+
+Simple commands for common operations:
+
+```ruby
+Elelem::Plugins.register(:builtins) do |agent|
+ agent.commands.register("exit", description: "Exit elelem") { exit(0) }
+
+ agent.commands.register("clear", description: "Clear conversation history") do
+ agent.conversation.clear!
+ agent.terminal.say " → context cleared"
+ end
+
+ agent.commands.register("help", description: "Show available commands") do
+ agent.terminal.say agent.commands.map { |name, desc| "#{name.ljust(12)} #{desc}" }.join("\n")
+ end
+end
+```
doc/plugins/README.md
@@ -0,0 +1,29 @@
+# Plugins
+
+Plugins extend elelem with custom functionality. There are four types:
+
+| Type | Purpose | Registry |
+|------|---------|----------|
+| [Tool](authoring.md#tool-plugins) | Functions the LLM can call | `agent.toolbox.add` |
+| [Command](authoring.md#command-plugins) | Slash commands for the user | `agent.commands.register` |
+| [Hook](authoring.md#hook-plugins) | Before/after tool execution | `agent.toolbox.before/after` |
+| [Provider](authoring.md#provider-plugins) | LLM backends | `Elelem::Providers.register` |
+
+## Location
+
+- `~/.elelem/plugins/` - user global (all projects)
+- `.elelem/plugins/` - project local
+
+## Basic Structure
+
+```ruby
+# ~/.elelem/plugins/myplugin.rb
+Elelem::Plugins.register(:myplugin) do |agent|
+ # add tools, commands, hooks
+end
+```
+
+## Guides
+
+- [Authoring](authoring.md) - step-by-step plugin creation
+- [Examples](examples.md) - real plugins from the codebase
doc/configuration.md
@@ -0,0 +1,27 @@
+# Configuration
+
+Elelem uses convention over configuration. Most behavior comes from file locations and environment variables.
+
+## Directory Structure
+
+**User global** (`~/.elelem/`):
+- `plugins/` - plugins loaded for all projects
+- `mcp.json` - global MCP servers
+
+**Project local** (`.elelem/`):
+- `plugins/` - project-specific plugins
+- `prompts/` - custom mode templates (ERB)
+- `backlog/` - user stories for workflow
+- `mcp.json` - project MCP servers
+
+## Plugin Loading Order
+
+1. `lib/elelem/plugins/` (built-in)
+2. `~/.elelem/plugins/` (user global)
+3. `.elelem/plugins/` (project local)
+
+Later plugins override earlier ones. Plugins starting with `zz_` load last.
+
+## Project Instructions
+
+`AGENTS.md` provides project-specific instructions. Elelem searches up the directory tree from the current working directory.
doc/getting-started.md
@@ -0,0 +1,39 @@
+# Getting Started
+
+## First Run
+
+```
+gem install elelem
+cd your-project
+elelem chat
+```
+
+Elelem requires a git repository and an LLM provider. By default it uses Ollama on localhost:11434.
+
+## Your First Conversation
+
+```
+you: What files are in this project?
+```
+
+The agent uses its tools to explore and respond. Type `/help` to see available commands.
+
+## Project Instructions
+
+Create an `AGENTS.md` file at your repository root to give the agent project-specific instructions:
+
+```markdown
+# Project Instructions
+
+- Use 2 spaces for indentation
+- Run `bin/test` after changes
+- Follow TDD
+```
+
+Elelem searches up the directory tree for this file.
+
+## Next Steps
+
+- [Workflow](workflow.md) - learn the development cycle
+- [Configuration](configuration.md) - customize elelem
+- [Plugins](plugins/) - extend with custom tools
doc/mcp.md
@@ -0,0 +1,53 @@
+# MCP Integration
+
+Elelem supports the Model Context Protocol for connecting to external tool servers.
+
+## Configuration
+
+Create `~/.elelem/mcp.json` (global) or `.elelem/mcp.json` (project):
+
+```json
+{
+ "mcpServers": {
+ "server-name": {
+ "command": "path/to/server"
+ }
+ }
+}
+```
+
+## Server Types
+
+**Stdio** - communicates via stdin/stdout:
+
+```json
+{
+ "mcpServers": {
+ "myserver": {
+ "command": "npx",
+ "args": ["@example/mcp-server"]
+ }
+ }
+}
+```
+
+**HTTP** - communicates via HTTP with SSE:
+
+```json
+{
+ "mcpServers": {
+ "myserver": {
+ "type": "http",
+ "url": "https://api.example.com/mcp"
+ }
+ }
+}
+```
+
+## OAuth
+
+HTTP servers support OAuth automatically. Elelem handles the browser flow, token storage, and refresh.
+
+## Debugging
+
+Logs are written to `~/.elelem/mcp.log`.
doc/README.md
@@ -0,0 +1,18 @@
+# Elelem Documentation
+
+Minimal coding agent for the command line.
+
+## Contents
+
+- [Getting Started](getting-started.md) - installation and first run
+- [Workflow](workflow.md) - plan/design/build/review/verify cycle
+- [Configuration](configuration.md) - config files and environment variables
+- [Modes](modes/) - system prompt modes
+- [Plugins](plugins/) - extending elelem with custom tools
+- [MCP](mcp.md) - Model Context Protocol integration
+- [Reference](reference.md) - tool schemas and commands
+
+## See Also
+
+- [README](../README.md) - project overview
+- [CHANGELOG](../CHANGELOG.md) - version history
doc/reference.md
@@ -0,0 +1,25 @@
+# Reference
+
+## Commands
+
+| Command | Description |
+|---------|-------------|
+| /clear | Clear conversation history |
+| /compact | Summarize and compress context |
+| /context | Show conversation state |
+| /exit | Exit elelem |
+| /help | List commands |
+| /init | Generate AGENTS.md |
+| /mode | Switch system prompt mode |
+| /provider | Switch LLM provider |
+| /reload | Hot-reload source code |
+| /shell | Shell session with transcript capture |
+| /tools | List available tools |
+
+## Built-in Tools
+
+See the [README](../README.md) for the complete tool reference.
+
+## Environment Variables
+
+See [Configuration](configuration.md) for environment variable reference.
doc/workflow.md
@@ -0,0 +1,163 @@
+# ELELEM-WORKFLOW(7)
+
+## NAME
+
+elelem-workflow - the plan/design/build/review/verify cycle
+
+## DESCRIPTION
+
+Elelem supports a structured workflow for feature development, inspired by
+agile practices. Each phase has a dedicated mode that adjusts the system
+prompt and available tools.
+
+## WORKFLOW DIAGRAM
+
+```
+ +--------+
+ | Plan |
+ +---+----+
+ |
+ v
+ +--------+
+ | Design |
+ +---+----+
+ |
+ v
+ +--------+
+ | Build |
+ +---+----+
+ |
+ v
+ +--------+
+ | Review |
+ +---+----+
+ |
+ v
+ +--------+
+ | Verify |
+ +--------+
+```
+
+## PHASES
+
+### Plan
+
+Create user stories in `.elelem/backlog/`. Each story is a markdown file
+with acceptance criteria and tasks.
+
+Example story:
+
+```markdown
+# Add logout button
+
+As a user, I want to log out so that I can end my session.
+
+## Acceptance Criteria
+
+- [ ] Button visible when logged in
+- [ ] Click logs user out
+- [ ] Redirects to home page
+
+## Tasks
+
+(filled in during design phase)
+```
+
+### Design
+
+Research and plan implementation. The agent explores the codebase, identifies
+extension points, and breaks the story into atomic tasks.
+
+```
+/mode design
+```
+
+Constraints:
+- Allowed: read, glob, grep, task, execute (read-only)
+- Blocked: code changes, test changes
+- Can only edit files in .elelem/backlog/
+
+### Build
+
+Execute tasks from the story using TDD:
+
+1. Write failing test
+2. Implement minimal code to pass
+3. Refactor if needed
+4. Mark task complete
+
+```
+/mode build
+```
+
+All tools available. Work through tasks one at a time.
+
+### Review
+
+Verify changes meet acceptance criteria:
+
+```
+/mode review
+```
+
+The agent checks:
+- All tasks completed
+- Acceptance criteria satisfied
+- Tests exist and pass
+- No logic errors or security issues
+- SOLID principles followed
+
+### Verify
+
+Demo the feature as if presenting to the Product Owner:
+
+```
+/mode verify
+```
+
+The agent performs end-to-end testing from the user's perspective and
+documents the results.
+
+## STORY FILES
+
+Stories live in `.elelem/backlog/` and follow this structure:
+
+```markdown
+# Story Title
+
+Brief description of the feature.
+
+## Acceptance Criteria
+
+- [ ] Criterion 1
+- [ ] Criterion 2
+
+## Tasks
+
+* [ ] Task 1
+* [ ] Task 2
+* [x] Completed task
+
+## Demo Notes
+
+Verified: 2026-01-15
+Status: ACCEPTED
+
+Observations:
+- Feature works as expected
+- Edge case X handled correctly
+```
+
+## SWITCHING MODES
+
+Use the `/mode` command:
+
+```
+/mode # show current mode
+/mode build # switch to build mode
+/mode design # switch to design mode
+```
+
+## SEE ALSO
+
+elelem-modes(7), elelem-plugins(7)
lib/elelem/plugins/anthropic.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+Elelem::Providers.register(:anthropic) do
+ Elelem::Net::Claude.anthropic(
+ model: ENV.fetch("ANTHROPIC_MODEL", "claude-opus-4-5-20250514"),
+ api_key: ENV.fetch("ANTHROPIC_API_KEY")
+ )
+end
lib/elelem/plugins/builtins.rb
@@ -8,89 +8,7 @@ Elelem::Plugins.register(:builtins) do |agent|
agent.terminal.say " → context cleared"
end
- agent.commands.register("context", description: "Show conversation context") do |args|
- messages = agent.context
-
- case args
- when nil, ""
- messages.each_with_index do |msg, i|
- role = msg[:role]
- preview = msg[:content].to_s.lines.first&.strip&.slice(0, 60) || ""
- preview += "..." if msg[:content].to_s.length > 60
- agent.terminal.say " #{i + 1}. #{role}: #{preview}"
- end
- when "json"
- agent.terminal.say JSON.pretty_generate(messages)
- when /^\d+$/
- index = args.to_i - 1
- if index >= 0 && index < messages.length
- content = messages[index][:content].to_s
- agent.terminal.say(agent.terminal.markdown(content))
- else
- agent.terminal.say " Invalid index: #{args}"
- end
- else
- agent.terminal.say " Usage: /context [json|<number>]"
- end
- end
-
- strip_ansi = ->(text) do
- text
- .gsub(/^Script started.*?\n/, "")
- .gsub(/\nScript done.*$/, "")
- .gsub(/\e\].*?(?:\a|\e\\)/, "")
- .gsub(/\e\[[0-9;?]*[A-Za-z]/, "")
- .gsub(/\e[PX^_].*?\e\\/, "")
- .gsub(/\e./, "")
- .gsub(/[\b]/, "")
- .gsub(/\r/, "")
- end
-
- agent.commands.register("shell", description: "Start interactive shell") do
- transcript = Tempfile.create do |file|
- system("script", "-q", file.path, chdir: Dir.pwd)
- strip_ansi.call(File.read(file.path))
- end
- agent.conversation.add(role: "user", content: transcript) unless transcript.strip.empty?
- end
-
- agent.commands.register("init", description: "Generate AGENTS.md") do
- system_prompt = <<~PROMPT
- AGENTS.md generator. Analyze codebase and write AGENTS.md to project root.
-
- # AGENTS.md Spec (https://agents.md/)
- A file providing context and instructions for AI coding agents.
-
- ## Recommended Sections
- - Commands: build, test, lint commands
- - Code Style: conventions, patterns
- - Architecture: key components and flow
- - Testing: how to run tests
-
- ## Process
- 1. Read README.md if present
- 2. Identify language (Gemfile, package.json, go.mod)
- 3. Find test scripts (bin/test, npm test)
- 4. Check linter configs
- 5. Write concise AGENTS.md
-
- Keep it minimal. No fluff.
- PROMPT
-
- agent.fork(system_prompt: system_prompt).turn("Generate AGENTS.md for this project")
- end
-
- agent.commands.register("reload", description: "Reload plugins and source") do
- lib_dir = File.expand_path("../..", __dir__)
- original_verbose, $VERBOSE = $VERBOSE, nil
- Dir["#{lib_dir}/**/*.rb"].sort.each { |f| load(f) }
- $VERBOSE = original_verbose
- agent.toolbox = Elelem::Toolbox.new
- agent.commands = Elelem::Commands.new
- Elelem::Plugins.reload!(agent)
- end
-
agent.commands.register("help", description: "Show available commands") do
- agent.terminal.say agent.commands.names.join(" ")
+ agent.terminal.say agent.commands.map { |name, description| "#{name.ljust(12)} #{description}" }.join("\n")
end
end
lib/elelem/plugins/zz_confirm.rb → lib/elelem/plugins/confirm.rb
File renamed without changes
lib/elelem/plugins/context.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+Elelem::Plugins.register(:context) do |agent|
+ agent.commands.register("context", description: "Show conversation context") do |args|
+ messages = agent.context
+
+ case args
+ when nil, ""
+ messages.each_with_index do |msg, i|
+ role = msg[:role]
+ preview = msg[:content].to_s.lines.first&.strip&.slice(0, 60) || ""
+ preview += "..." if msg[:content].to_s.length > 60
+ agent.terminal.say " #{i + 1}. #{role}: #{preview}"
+ end
+ when "json"
+ agent.terminal.say JSON.pretty_generate(messages)
+ when /^\d+$/
+ index = args.to_i - 1
+ if index >= 0 && index < messages.length
+ content = messages[index][:content].to_s
+ agent.terminal.say(agent.terminal.markdown(content))
+ else
+ agent.terminal.say " Invalid index: #{args}"
+ end
+ else
+ agent.terminal.say " Usage: /context [json|<number>]"
+ end
+ end
+end
lib/elelem/plugins/ollama.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+Elelem::Providers.register(:ollama) do
+ Elelem::Net::Ollama.new(
+ model: ENV.fetch("OLLAMA_MODEL", "gpt-oss:latest"),
+ host: ENV.fetch("OLLAMA_HOST", "localhost:11434")
+ )
+end
lib/elelem/plugins/openai.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+Elelem::Providers.register(:openai) do
+ Elelem::Net::OpenAI.new(
+ model: ENV.fetch("OPENAI_MODEL", "gpt-4o"),
+ api_key: ENV.fetch("OPENAI_API_KEY")
+ )
+end
lib/elelem/plugins/permissions.json
@@ -1,6 +0,0 @@
-{
- "read": "allow",
- "write": "ask",
- "edit": "ask",
- "execute": "ask"
-}
lib/elelem/plugins/provider.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+Elelem::Plugins.register(:provider) do |agent|
+ agent.commands.register("provider", description: "Switch provider", completions: -> { Elelem::Providers.names }) do |name|
+ if name.nil? || name.empty?
+ agent.terminal.say " → available: #{Elelem::Providers.names.join(", ")}"
+ else
+ agent.client = Elelem::Providers.build(name)
+ agent.terminal.say " → switched to #{name}"
+ end
+ end
+end
lib/elelem/plugins/tools.rb
@@ -1,13 +1,35 @@
# frozen_string_literal: true
Elelem::Plugins.register(:tools) do |agent|
- agent.commands.register("tools", description: "List available tools") do
- agent.toolbox.tools.each_value do |tool|
- agent.terminal.say ""
- agent.terminal.say " #{tool.name}"
- agent.terminal.say " #{tool.description}"
- tool.params.each { |k, v| agent.terminal.say " #{k}: #{v[:type] || v["type"]}" }
- agent.terminal.say " aliases: #{tool.aliases.join(", ")}" if tool.aliases.any?
+ completions = -> { agent.toolbox.tools.keys }
+
+ agent.commands.register("tools", description: "List available tools", completions: completions) do |arg|
+ if arg && !arg.empty?
+ tool = agent.toolbox.tools[arg]
+ unless tool
+ agent.terminal.say "Unknown tool: #{arg}"
+ next
+ end
+
+ lines = ["## #{tool.name}", "", tool.description, "", "### Parameters", ""]
+ tool.params.each do |name, spec|
+ req = tool.required.include?(name.to_s) ? ", required" : ""
+ desc = spec[:description] ? " - #{spec[:description]}" : ""
+ lines << "- `#{name}` (#{spec[:type]}#{req})#{desc}"
+ end
+ lines << "" << "*aliases: #{tool.aliases.join(", ")}*" if tool.aliases.any?
+
+ agent.terminal.say agent.terminal.markdown(lines.join("\n"))
+ else
+ rows = agent.toolbox.tools.each_value.map do |tool|
+ "| #{tool.name} | #{tool.description.lines.first.chomp} |"
+ end
+
+ md = String.new("| Tool | Description |\n")
+ md << "|------|-------------|\n"
+ md << rows.join("\n")
+
+ agent.terminal.say agent.terminal.markdown(md)
end
end
end
lib/elelem/plugins/vertex.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+Elelem::Providers.register(:vertex) do
+ Elelem::Net::Claude.vertex(
+ model: ENV.fetch("VERTEX_MODEL", "claude-opus-4-5@20251101"),
+ project: ENV.fetch("GOOGLE_CLOUD_PROJECT"),
+ region: ENV.fetch("GOOGLE_CLOUD_REGION", "us-east5")
+ )
+end
lib/elelem/prompts/default.erb
@@ -0,0 +1,1 @@
+Terminal system agent. Be concise. Verify your work.
lib/elelem/prompts/plan.erb
@@ -0,0 +1,71 @@
+You are a Scrum Master capturing requirements. Interview the user as if they are the Product Owner.
+
+# Role
+- Ask clarifying questions to understand the feature
+- Break large requests into focused user stories
+- Capture acceptance criteria in testable terms
+- Write each story to .elelem/backlog/ as a separate file
+
+# Tools
+- interview(question, context?): Ask the user a question and wait for response
+- read(path): Read existing stories or code
+- glob(pattern): Find files
+- grep(pattern): Search contents
+- write(path, content): Write story files to .elelem/backlog/
+
+# Constraints
+Allowed: interview, read, glob, grep, task, execute (read-only), write (.elelem/backlog/ only)
+Blocked: code changes, test changes
+
+# Process
+1. **Listen** - Understand what the user wants to achieve
+2. **Clarify** - Ask questions about personas, goals, edge cases
+3. **Scope** - Break large features into small, deliverable stories
+4. **Document** - Create story files in .elelem/backlog/
+5. **Confirm** - Read back the stories for user approval
+
+# Story Template
+```markdown
+As a `[persona]`, I `[want to]`, so that `[goal]`.
+
+# SYNOPSIS
+
+<one-line summary>
+
+# DESCRIPTION
+
+<detailed explanation>
+
+# SEE ALSO
+
+* [ ] <related files or concepts>
+
+# Tasks
+
+* [ ] TBD (filled in design mode)
+
+# Acceptance Criteria
+
+* [ ] <testable criterion>
+```
+
+# Naming Convention
+Files: .elelem/backlog/NNN-short-name.md (e.g., 001-user-login.md)
+
+# Guidelines
+- One story per file
+- Stories should be small enough to complete in one session
+- Acceptance criteria must be objectively testable
+- Ask "how will we know this is done?"
+
+# Environment
+pwd: <%= pwd %>
+platform: <%= platform %>
+date: <%= date %>
+<%= git_info %>
+
+<% if repo_map && !repo_map.empty? %>
+# Codebase
+```
+<%= repo_map %>```
+<% end %>
lib/elelem/agent.rb
@@ -2,8 +2,8 @@
module Elelem
class Agent
- attr_reader :conversation, :client, :toolbox, :terminal, :commands
- attr_writer :terminal, :toolbox, :commands
+ attr_reader :conversation, :system_prompt
+ attr_accessor :client, :toolbox, :terminal, :commands
def initialize(client, toolbox: Toolbox.new, terminal: nil, system_prompt: nil, commands: nil)
@client = client
@@ -11,7 +11,7 @@ module Elelem
@commands = commands || Commands.new
@terminal = terminal
@conversation = Conversation.new
- @system_prompt = system_prompt
+ @system_prompt = SystemPrompt.new(system_prompt)
end
def repl
@@ -31,7 +31,7 @@ module Elelem
end
def context
- @conversation.to_a(system_prompt: system_prompt)
+ @conversation.to_a(system_prompt: system_prompt.render)
end
def fork(system_prompt:)
@@ -71,7 +71,7 @@ module Elelem
content = String.new
tool_calls = []
- client.fetch(@conversation.to_a(system_prompt: system_prompt) + ctx, toolbox.to_a) do |event|
+ client.fetch(@conversation.to_a(system_prompt: system_prompt.render) + ctx, toolbox.to_a) do |event|
case event[:type]
when "saying"
content << event[:text].to_s
@@ -87,9 +87,5 @@ module Elelem
terminal.say "\n ✗ #{e.message}"
["Error: #{e.message} #{e.backtrace.join("\n")}", []]
end
-
- def system_prompt
- @system_prompt || SystemPrompt.new.render
- end
end
end
lib/elelem/commands.rb
@@ -2,12 +2,22 @@
module Elelem
class Commands
+ include Enumerable
+
def initialize
@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/permissions.json
@@ -0,0 +1,7 @@
+{
+ "edit": "ask",
+ "execute": "ask",
+ "interview": "allow",
+ "read": "allow",
+ "write": "ask"
+}
lib/elelem/permissions.rb
@@ -3,7 +3,7 @@
module Elelem
class Permissions
LOAD_PATHS = [
- File.expand_path("plugins/permissions.json", __dir__),
+ File.expand_path("permissions.json", __dir__),
"~/.elelem/permissions.json",
".elelem/permissions.json"
].freeze
lib/elelem/plugins.rb
@@ -9,17 +9,22 @@ module Elelem
].freeze
def self.setup!(agent)
- load_plugins
+ load!
+ run!(agent)
+ end
+
+ def self.run!(agent)
registry.each_value { |plugin| plugin.call(agent) }
end
def self.reload!(agent)
+ Providers.registry.clear
registry.clear
- load_plugins
- registry.each_value { |plugin| plugin.call(agent) }
+ load!
+ run!(agent)
end
- def self.load_plugins
+ def self.load!
LOAD_PATHS.each do |path|
dir = File.expand_path(path)
next unless File.directory?(dir)
lib/elelem/providers.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module Elelem
+ # Registry for LLM provider plugins.
+ #
+ # Providers must implement:
+ #
+ # fetch(messages, tools = []) { |event| ... } -> Array<tool_calls>
+ #
+ # Messages (OpenAI format):
+ # { role: "system"|"user"|"assistant", content: "..." }
+ # { role: "tool", tool_call_id: "...", content: "..." }
+ #
+ # Tools (OpenAI format):
+ # { type: "function", function: { name:, description:, parameters: } }
+ #
+ # Streaming events (yield to block):
+ # { type: "saying", text: "..." }
+ # { type: "thinking", text: "..." }
+ # { type: "tool_call", id:, name:, arguments: }
+ #
+ # Returns: [{ id:, name:, arguments: }, ...]
+ #
+ # Example:
+ #
+ # Elelem::Providers.register(:gemini) do
+ # MyGeminiClient.new(model: ENV.fetch("GEMINI_MODEL", "gemini-pro"))
+ # end
+ #
+ module Providers
+ def self.register(name, &factory)
+ registry[name.to_s] = factory
+ end
+
+ def self.build(name)
+ Plugins.load! if registry.empty?
+ registry.fetch(name.to_s).call
+ end
+
+ def self.names
+ registry.keys
+ end
+
+ def self.registry
+ @registry ||= {}
+ end
+ end
+end
lib/elelem/system_prompt.rb
@@ -2,58 +2,59 @@
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
+ attr_reader :mode
+
+ def initialize(template = nil)
+ @mode = "default"
+ @template = template || self.class.get("default")
+ end
+
+ def switch(name)
+ @mode = name
+ @template = self.class.get(name)
+ end
def render
- ERB.new(TEMPLATE, trim_mode: "-").result(binding)
+ ERB.new(template, trim_mode: "-").result(binding)
end
private
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
@@ -26,6 +26,7 @@ require_relative "elelem/mcp"
require_relative "elelem/net"
require_relative "elelem/permissions"
require_relative "elelem/plugins"
+require_relative "elelem/providers"
require_relative "elelem/system_prompt"
require_relative "elelem/terminal"
require_relative "elelem/tool"
@@ -47,14 +48,16 @@ module Elelem
end
end
- def self.start(client, toolbox: Toolbox.new)
+ def self.start(provider: "ollama", toolbox: Toolbox.new)
+ client = Providers.build(provider)
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
- def self.ask(client, prompt, toolbox: Toolbox.new)
+ def self.ask(prompt, provider: "ollama", toolbox: Toolbox.new)
+ client = Providers.build(provider)
agent = Agent.new(client, toolbox: toolbox, terminal: Terminal.new(quiet: true))
Plugins.setup!(agent)
agent.turn(prompt)
@@ -62,23 +65,8 @@ module Elelem
end
class CLI
- MODELS = {
- "ollama" => "gpt-oss:latest",
- "anthropic" => "claude-opus-4-5-20250514",
- "vertex" => "claude-opus-4-5@20251101",
- "openai" => "gpt-4o"
- }.freeze
-
- PROVIDERS = {
- "ollama" => ->(model) { Elelem::Net::Ollama.new(model: model, host: ENV.fetch("OLLAMA_HOST", "localhost:11434")) },
- "anthropic" => ->(model) { Elelem::Net::Claude.anthropic(model: model, api_key: ENV.fetch("ANTHROPIC_API_KEY")) },
- "vertex" => ->(model) { Elelem::Net::Claude.vertex(model: model, project: ENV.fetch("GOOGLE_CLOUD_PROJECT"), region: ENV.fetch("GOOGLE_CLOUD_REGION", "us-east5")) },
- "openai" => ->(model) { Elelem::Net::OpenAI.new(model: model, api_key: ENV.fetch("OPENAI_API_KEY")) }
- }.freeze
-
def initialize(args)
@provider = "ollama"
- @model = nil
@args = parse(args)
end
@@ -101,7 +89,6 @@ module Elelem
o.separator " help Show this help"
o.separator "\nOptions:"
o.on("-p", "--provider NAME", "ollama, anthropic, vertex, openai") { |p| @provider = p }
- o.on("-m", "--model NAME", "Override default model") { |m| @model = m }
o.on("-h", "--help") { puts o; exit }
end
@parser.parse!(args)
@@ -111,20 +98,15 @@ module Elelem
puts @parser
end
- def client
- model = @model || MODELS.fetch(@provider)
- PROVIDERS.fetch(@provider).call(model)
- end
-
def chat
- Elelem.start(client)
+ Elelem.start(provider: @provider)
end
def ask
abort "Usage: elelem ask <prompt>" if @args.empty?
prompt = @args.join(" ")
prompt = "#{prompt}\n\n```\n#{$stdin.read}\n```" if $stdin.stat.pipe?
- Elelem::Terminal.new.markdown Elelem.ask(client, prompt)
+ Elelem::Terminal.new.markdown Elelem.ask(prompt, provider: @provider)
end
def files
@@ -133,7 +115,7 @@ module Elelem
files.each_with_index do |line, i|
path = line.strip
next if path.empty? || !File.file?(path)
- puts %Q{<document index="#{i + 1}"><source>#{path}</source><content><![CDATA[#{File.read(path)}]]></content></document>}
+ puts %Q{<document index="#{i + 1}"><source>#{path}</source><document_content><![CDATA[#{File.read(path)}]]></document_content></document>}
end
puts "</documents>"
end
spec/elelem/providers_spec.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+RSpec.describe Elelem::Providers do
+ before do
+ described_class.registry.clear
+ end
+
+ after do
+ described_class.registry.clear
+ end
+
+ describe ".register" do
+ it "registers a provider factory" do
+ client = double("client")
+ described_class.register(:test) { client }
+
+ expect(described_class.build("test")).to eq(client)
+ end
+
+ it "accepts symbol or string names" do
+ client = double("client")
+ described_class.register("string_provider") { client }
+
+ expect(described_class.build(:string_provider)).to eq(client)
+ end
+ end
+
+ describe ".build" do
+ it "calls the factory and returns the client" do
+ call_count = 0
+ described_class.register(:counter) { call_count += 1 }
+
+ described_class.build("counter")
+ described_class.build("counter")
+
+ expect(call_count).to eq(2)
+ end
+
+ it "raises KeyError for unknown provider" do
+ expect { described_class.build("unknown") }.to raise_error(KeyError)
+ end
+ end
+
+ describe ".names" do
+ it "returns registered provider names" do
+ described_class.register(:alpha) { }
+ described_class.register(:beta) { }
+
+ expect(described_class.names).to contain_exactly("alpha", "beta")
+ end
+ end
+end
spec/elelem/system_prompt_spec.rb
@@ -0,0 +1,81 @@
+# 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 system agent")
+ end
+
+ it "returns plan template" do
+ template = described_class.get("plan")
+ expect(template).to include("Scrum Master")
+ 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 system agent")
+
+ prompt.switch("plan")
+ expect(prompt.template).to include("Scrum Master")
+ end
+
+ it "updates the mode name" do
+ prompt = described_class.new
+ expect(prompt.mode).to eq("default")
+
+ prompt.switch("plan")
+ expect(prompt.mode).to eq("plan")
+ 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
.gitignore
@@ -2,7 +2,6 @@
/.yardoc
/_yardoc/
/coverage/
-/doc/
/pkg/
/spec/reports/
/tmp/
elelem.gemspec
@@ -16,7 +16,7 @@ Gem::Specification.new do |spec|
spec.required_rubygems_version = ">= 4.0.0"
spec.metadata["allowed_push_host"] = "https://rubygems.org"
spec.metadata["homepage_uri"] = spec.homepage
- spec.metadata["source_code_uri"] = "https://src.mokhan.ca/xlgmokha/elelem"
+ spec.metadata["source_code_uri"] = "https://git.mokhan.ca/xlgmokha/elelem.git"
spec.metadata["changelog_uri"] = "https://src.mokhan.ca/xlgmokha/elelem/blob/main/CHANGELOG.md.html"
spec.files = [
@@ -36,9 +36,11 @@ Gem::Specification.new do |spec|
"lib/elelem/net/claude.rb",
"lib/elelem/net/ollama.rb",
"lib/elelem/net/openai.rb",
+ "lib/elelem/permissions.json",
"lib/elelem/permissions.rb",
"lib/elelem/plugins.rb",
"lib/elelem/plugins/builtins.rb",
+ "lib/elelem/plugins/confirm.rb",
"lib/elelem/plugins/edit.rb",
"lib/elelem/plugins/eval.rb",
"lib/elelem/plugins/execute.rb",
@@ -47,13 +49,11 @@ Gem::Specification.new do |spec|
"lib/elelem/plugins/grep.rb",
"lib/elelem/plugins/list.rb",
"lib/elelem/plugins/mcp.rb",
- "lib/elelem/plugins/permissions.json",
"lib/elelem/plugins/read.rb",
"lib/elelem/plugins/task.rb",
"lib/elelem/plugins/tools.rb",
"lib/elelem/plugins/verify.rb",
"lib/elelem/plugins/write.rb",
- "lib/elelem/plugins/zz_confirm.rb",
"lib/elelem/system_prompt.rb",
"lib/elelem/terminal.rb",
"lib/elelem/tool.rb",
Gemfile.lock
@@ -45,16 +45,20 @@ GEM
regexp_parser (~> 2.0)
simpleidn (~> 0.2)
logger (1.7.0)
- net-hippie (1.4.0)
+ monitor (0.2.0)
+ net-hippie (1.5.1)
base64 (~> 0.1)
json (~> 2.0)
logger (~> 1.0)
- net-http (~> 0.6)
- openssl (~> 3.0)
+ monitor (~> 0.1)
+ net-http (~> 0.1)
+ openssl (~> 4.0)
+ resolv (~> 0.1)
+ timeout (~> 0.1)
net-http (0.9.1)
uri (>= 0.11.1)
open3 (0.2.1)
- openssl (3.3.2)
+ openssl (4.0.0)
optparse (0.8.1)
pathname (0.4.0)
pp (0.6.3)
@@ -71,6 +75,7 @@ GEM
regexp_parser (2.11.3)
reline (0.6.3)
io-console (~> 0.5)
+ resolv (0.7.1)
rspec (3.13.2)
rspec-core (~> 3.13.0)
rspec-expectations (~> 3.13.0)
@@ -89,6 +94,7 @@ GEM
simpleidn (0.2.3)
stringio (3.2.0)
tempfile (0.3.1)
+ timeout (0.6.0)
tsort (0.2.0)
uri (1.1.1)
webrick (1.9.2)
README.md
@@ -202,15 +202,14 @@ Configure MCP servers in `~/.elelem/mcp.json` or `.elelem/mcp.json`:
{
"mcpServers": {
"gitlab": {
- "command": "npx",
- "args": ["-y", "@anthropics/gitlab-mcp"],
- "env": {
- "GITLAB_TOKEN": "${GITLAB_TOKEN}"
- }
- },
- "remote": {
"type": "http",
- "url": "https://mcp.example.com/sse"
+ "url": "https://gitlab.com/api/v4/mcp"
+ },
+ "playwright": {
+ "command": "npx",
+ "args": [
+ "@playwright/mcp@latest"
+ ]
}
}
}