Commit 5649997

mo khan <mo@mokhan.ca>
2026-08-29 22:17:20
chore: tidy up
.elelem/backlog/archive/002-hardware-detection.md
@@ -1,44 +0,0 @@
-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
@@ -1,44 +0,0 @@
-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
@@ -1,53 +0,0 @@
-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
@@ -1,49 +0,0 @@
-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
@@ -1,47 +0,0 @@
-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
@@ -1,43 +0,0 @@
-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
@@ -1,42 +0,0 @@
-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
@@ -1,98 +0,0 @@
-# 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
@@ -1,40 +0,0 @@
-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
@@ -1,217 +0,0 @@
-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
@@ -1,73 +0,0 @@
-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
@@ -1,94 +0,0 @@
-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
@@ -1,110 +0,0 @@
-# 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
@@ -1,85 +0,0 @@
-# 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
@@ -1,164 +0,0 @@
-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
-
-# DESIGN
-
-**Root cause:** Spacing is caller-side (each method decides its own prefix/suffix spacing)
-instead of boundary-aware (spacing happens at transitions).
-
-**Scenarios causing inconsistency:**
-- Dots running → `stop_dots` adds newline + `markdown` adds 2 newlines = 3 lines
-- No dots → `markdown` adds 2 newlines = 2 lines  
-- `header` returns `\n...` + `say` adds newline = double spacing
-
-**Solution:** One flag (`@at_line_start`), one method (`gap`).
-
-`gap` is idempotent: "ensure we're at a blank line". Call it anywhere between 
-sections. If already at line start, it's a no-op. If mid-content, it adds one newline.
-
-**Changes:**
-- Terminal tracks cursor state via `@at_line_start`
-- `gap` method: `newline unless @at_line_start`
-- `markdown` uses `gap` instead of `newline(n: 2)`
-- `header` drops its `\n` prefix (caller uses `gap`)
-
-**Trade-offs:**
-- Simplicity ✓ - 1 flag, 1 method, 3 file changes
-- No plugin changes needed - existing `say`/`print` calls just work
-- Idempotent - safe to call `gap` multiple times
-
-# SEE ALSO
-
-* [ ] lib/elelem/terminal.rb - Primary output methods (say, print, markdown, newline)
-* [ ] lib/elelem/agent.rb - REPL loop and turn processing with terminal calls
-* [ ] lib/elelem/toolbox.rb - header() method prepends \n to output
-* [ ] lib/elelem/plugins/read.rb - after hook uses terminal.say and display_file
-* [ ] lib/elelem/plugins/write.rb - after hook uses terminal.say and display_file
-* [ ] lib/elelem/plugins/execute.rb - streaming print and after hook
-* [ ] lib/elelem/plugins/tools.rb - markdown output for tool listings
-* [ ] lib/elelem/plugins/context.rb - multi-line output for context display
-* [ ] lib/elelem/plugins/builtins.rb - /clear and /help command output
-* [ ] lib/elelem/plugins/provider.rb - provider switching messages
-
-# Tasks
-
-## Terminal (lib/elelem/terminal.rb)
-* [x] Add `@at_line_start = true` in initialize
-* [x] Add `gap` method: `newline unless @at_line_start` (idempotent blank line)
-* [x] Update `say` to set `@at_line_start = true` after output
-* [x] Update `print` to set `@at_line_start = false` (mid-line content)
-* [x] Update `newline` to set `@at_line_start = true`
-* [x] Update `stop_dots` - already calls newline, will inherit correct state
-* [x] Update `markdown` - replace `newline(n: 2)` with `gap`
-
-## Toolbox (lib/elelem/toolbox.rb)
-* [x] Update `header` - remove leading `\n` from return string
-
-## Agent (lib/elelem/agent.rb)
-* [x] Add `terminal.gap` before `terminal.say toolbox.header(...)` in process method
-
-## Testing
-* [x] Add spec for `gap` idempotence: calling twice produces one blank line
-* [x] Visual audit: conversation with tool calls
-* [x] Visual audit: multi-tool execution
-* [ ] Visual audit: streaming execute output
-
-# Acceptance Criteria
-
-* [x] Single blank line between distinct output sections
-* [x] No double blank lines appear in any scenario
-* [x] No missing blank lines between sections
-* [x] Spacing is consistent regardless of event order
-* [ ] Visual audit of common workflows passes
-
-# Demo Notes
-
-Verified: 2026-02-04
-Status: ACCEPTED
-
-## Verification Run: 2026-02-04 (Final)
-
-All 83 tests pass (0 failures).
-
-**Code Review Fixes Applied:**
-- `newline` now respects `quiet?` mode
-- `print` and `say` only set `@at_line_start` when actually outputting
-- Test spec uses `expect` instead of `allow` for dots thread kill
-- Replaced `@quiet` with `quiet?` predicate throughout
-- Tests refactored to test behavior, not implementation (no `instance_variable_get`)
-
-**Manual Verification via `./bin/run`:**
-- Library loads without errors ✓
-- Spacing between sections is exactly 1 line ✓
-- No double blank lines in output ✓
-- `gap` is idempotent (multiple calls = single newline) ✓
-
-## Previous: Verification Run 2026-02-04
-
-All 75 tests pass (0 failures).
-
-**Automated Tests Verified:**
-- `gap` idempotence: calling multiple times produces single blank line ✓
-- `@at_line_start` state tracking across `say`, `print`, `newline` ✓
-- `gap` stops dots before adding newline ✓
-
-**Scenarios Verified:**
-| Scenario | Expected | Result |
-|----------|----------|--------|
-| Dots → tool header | 1 blank line | ✓ |
-| Tool → tool | 1 blank line | ✓ |
-| Tool → markdown | 1 blank line | ✓ |
-
-**Remaining:** Visual audit of streaming execute output (requires manual LLM testing)
-
----
-
-## Implementation Summary
-
-Added `@at_line_start` flag and `gap` method to Terminal class:
-- `gap` is idempotent: stops dots, then adds newline only if not at line start
-- All output methods (`say`, `print`, `newline`) update `@at_line_start`
-- `markdown` uses `gap` instead of hardcoded `newline(n: 2)`
-- `header` in Toolbox no longer prepends `\n`
-- Agent calls `terminal.gap` before tool headers
-
-## Tested Scenarios
-
-1. **Dots → tool header**: `.` prints, gap stops dots + newline, header prints
-   - Result: Single blank line after dots ✓
-
-2. **Tool header → tool header**: gap adds single newline between headers
-   - Result: Consistent single blank line ✓
-
-3. **Tool header → markdown**: gap inside markdown is no-op (already at line start)
-   - Result: No extra blank lines ✓
-
-4. **Idempotence**: Calling gap multiple times produces only one newline
-   - Result: Safe to call gap anywhere ✓
-
-## Files Changed
-
-- `lib/elelem/terminal.rb` - Added `@at_line_start`, `gap`, updated output methods
-- `lib/elelem/toolbox.rb` - Removed `\n` prefix from `header`
-- `lib/elelem/agent.rb` - Added `terminal.gap` before tool headers
-- `spec/elelem/terminal_spec.rb` - Added tests for gap behavior
-
-## Edge Case Fixed During Demo
-
-Initial implementation missed that `gap` should stop dots if running. When dots
-thread prints directly to stdout, `@at_line_start` stays true but cursor is
-mid-line. Fixed by having `gap` call `stop_dots` before checking state.
.elelem/backlog/016-tool-header-wrapping.md
@@ -1,49 +0,0 @@
-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
@@ -1,63 +0,0 @@
-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
@@ -1,44 +0,0 @@
-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
@@ -1,42 +0,0 @@
-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
@@ -1,41 +0,0 @@
-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
@@ -1,56 +0,0 @@
-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/gitlab.rb
@@ -1,11 +0,0 @@
-# frozen_string_literal: true
-
-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
.elelem/prompts/build.erb
@@ -5,13 +5,6 @@ Terminal coding agent. Execute tasks from a story.
 - 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/
@@ -28,7 +21,6 @@ New files: write tool
 # Search
 `rg -n "pattern" .` - text search
 `fd -e rb .` - file discovery
-`ast-grep -p 'def $NAME' -l ruby` - structural search
 
 # Task Completion
 When a task is done, edit the story file:
@@ -45,17 +37,3 @@ When a task is done, edit the story file:
 - 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
@@ -39,15 +39,3 @@ In the story's # Tasks section:
 - 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/plan.erb
@@ -57,15 +57,3 @@ Files: .elelem/backlog/NNN-short-name.md (e.g., 001-user-login.md)
 - 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 %>
.elelem/prompts/review.erb
@@ -42,15 +42,3 @@ Severity: critical | warning | nit
 - 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
@@ -37,15 +37,3 @@ Observations:
 - 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 %>
bin/evals
@@ -1,13 +0,0 @@
-#!/bin/sh
-
-# Tune the champion system prompt against the eval cases.
-# Usage: bin/evals [ROUNDS]
-
-set -e
-[ -n "$DEBUG" ] && set -x
-
-cd "$(dirname "$0")/.."
-
-[ -n "$1" ] && ROUNDS="$1" && export ROUNDS
-
-exec bundle exec rake evals:improve
ext/elelem_llama/elelem_llama.cpp
@@ -36,8 +36,7 @@ void *el_open(const char *path, int n_gpu_layers, int n_ctx, int n_threads, floa
     llama_model *model = llama_model_load_from_file(path, mp);
     if (!model) return nullptr;
 
-    return new el_handle{model, common_chat_templates_init(model, ""), n_ctx, n_threads,
-                         temp, (uint32_t) seed};
+    return new el_handle{model, common_chat_templates_init(model, ""), n_ctx, n_threads, temp, (uint32_t) seed};
 }
 
 // Tool-call arguments arrive as either a JSON string or an object; llama.cpp's
lib/elelem/net/gguf.rb
@@ -5,12 +5,6 @@ require "json"
 
 module Elelem
   module Net
-    # In-process GGUF client: loads a local model inside the elelem process via a
-    # thin C shim over llama.cpp + its common_chat layer (see
-    # ext/elelem_llama/elelem_llama.cpp), bound with stdlib Fiddle. The shim takes
-    # OpenAI-style messages + tools as JSON and returns {content, tool_calls} as
-    # JSON, so the model participates in the normal agent tool loop. No subprocess,
-    # no HTTP server, no third-party gem.
     class GGUF
       NATIVE = File.expand_path("../native", __dir__)
       SHIM = File.join(NATIVE, "libelelem_llama.so")
@@ -18,20 +12,14 @@ module Elelem
       I = Fiddle::TYPE_INT
       F = Fiddle::TYPE_FLOAT
 
-      # The GPU backend the extension compiled (extconf.rb stamps this at install);
-      # "cpu" when absent. Lets the provider default GPU offload to what was built.
       def self.backend
         File.read(File.join(NATIVE, "backend")).strip
       rescue SystemCallError
         "cpu"
       end
 
-      # dlopen + bindings are process-wide resources -- memoize once, like Net.http.
       def self.functions
         @functions ||= begin
-          unless File.exist?(SHIM)
-            raise "gguf: native shim missing at #{SHIM}\n       run: bundle exec rake compile"
-          end
           lib = Fiddle.dlopen(SHIM)
           {
             open: Fiddle::Function.new(lib["el_open"], [V, I, I, I, F, I], V),
@@ -40,28 +28,25 @@ module Elelem
         end
       end
 
-      # temp <= 0 => greedy/deterministic; seed -1 keeps llama.cpp's default seed.
-      def initialize(model_path:, n_ctx: 4096, n_threads: 16, max_tokens: 512,
-                     n_gpu_layers: 0, temp: 0.7, seed: -1)
+      def initialize(model:, n_ctx: 8192, n_threads: 16, max_tokens: 512, n_gpu_layers: 0, temp: 0.7, seed: -1)
         @max_tokens = max_tokens
-        @handle = self.class.functions[:open].call(model_path, n_gpu_layers, n_ctx, n_threads, temp, seed)
-        raise "gguf: failed to load model at #{model_path}" if @handle.null?
+        @handle = self.class.functions[:open].call(model, n_gpu_layers, n_ctx, n_threads, temp, seed)
+        raise "gguf: failed to load model at #{model}" if @handle.null?
       end
 
-      # elelem provider contract: fetch(messages, tools=[]) { |event| } -> tool_calls.
-      # Streams the reply as a "saying" event and each parsed tool call as a
-      # "doing" event, which the agent loop executes and feeds back.
       def fetch(messages, tools = [], &block)
-        ptr = self.class.functions[:generate].call(
-          @handle, JSON.generate(messages), JSON.generate(tools), @max_tokens
-        )
+        ptr = self.class.functions[:generate].call(@handle, JSON.generate(messages), JSON.generate(tools), @max_tokens)
         result = JSON.parse(Fiddle::Pointer.new(ptr).to_s)
 
         content = result["content"].to_s
         block&.call(type: "saying", text: content) unless content.empty?
 
         result.fetch("tool_calls", []).map do |call|
-          tool_call = { id: call["id"], name: call["name"], arguments: parse_args(call["arguments"]) }
+          tool_call = {
+            id: call["id"],
+            name: call["name"],
+            arguments: parse(call["arguments"])
+          }
           block&.call(tool_call.merge(type: "doing"))
           tool_call
         end
@@ -69,7 +54,7 @@ module Elelem
 
       private
 
-      def parse_args(raw)
+      def parse(raw)
         JSON.parse(raw.to_s)
       rescue JSON::ParserError
         {}
lib/elelem/net/ollama.rb
@@ -39,68 +39,6 @@ module Elelem
         "#{base}/api/chat"
       end
 
-      # POST /api/chat request body. Anything left unset uses the server or default.
-=begin
-
-  | Field              | Type                  | Notes                                                 |
-  | ---                | ---                   | ---                                                   |
-  | model              | string                │ required                                              │
-  | messages           | array                 │ see below                                             │
-  | tools              | array                 │ JSON tool schemas                                     │
-  | stream             | bool                  │ NDJSON stream when true                               │
-  | think              | bool or string        │ thinking models; "low"/"medium"/"high"                │
-  | format             | "json" or JSON schema │ structured output                                     │
-  | options            | object                │ model params, see below                               │
-  | keep_alive         | duration              │ how long model stays resident, e.g. "5m", 0 to unload │
-  | truncate           | bool                  │ truncate prompt to fit context                        │
-  | shift              | bool                  │ shift context window instead of erroring when full    │
-  | logprobs           | bool                  │ return token logprobs                                 │
-  | top_logprobs       | int                   │ how many alternatives per token                       │
-  | _debug_render_only | bool                  │ return rendered prompt without inference              │
-
-  Message object
-
-  | Field | Description |
-  | ---- | --------- |
-  | role | (system|user|assistant|tool) |
-  | content | |
-  | thinking | |
-  | images | (base64 array, multimodal) |
-  | tool_calls | |
-  | tool_name | name of the tool that produced a tool message |
-
-  Options
-
-    Sampling:
-
-    | Field | Description |
-    | ---- | ---- |
-    | seed | |
-    | temperature | |
-    | top_k | |
-    | top_p | |
-    | min_p | |
-    | typical_p | |
-    | num_predict | |
-    | num_keep | |
-    | stop (array) | |
-    | repeat_last_n | |
-    | repeat_penalty | |
-    | presence_penalty | |
-    | frequency_penalty | |
-
-    Runner:
-
-    | Field | Description |
-    | ----- | ----------- |
-    | num_ctx | |
-    | num_batch | |
-    | num_gpu | |
-    | main_gpu | |
-    | use_mmap | |
-    | num_thread | |
-    | draft_num_predict | |
-=end
       def build_request_body(messages, tools)
         {
           model: @model,
lib/elelem/plugins/gguf.rb
@@ -1,18 +1,15 @@
 # frozen_string_literal: true
 
-# In-process GGUF provider (see Elelem::Net::GGUF in lib/elelem/net/gguf.rb).
 Elelem::Providers.register(:gguf) do
-  # Offload all layers by default when a GPU backend was compiled (extconf.rb stamps
-  # it); CPU builds stay at 0. GGUF_N_GPU_LAYERS overrides either way.
   gpu = %w[vulkan cuda metal].include?(Elelem::Net::GGUF.backend)
 
   Elelem::Net::GGUF.new(
-    model_path: ENV.fetch("GGUF_MODEL", File.expand_path("~/models/Qwen2.5-Coder-7B-Instruct-Q4_K_M.gguf")),
-    n_ctx: Integer(ENV.fetch("GGUF_N_CTX", "8192")), # room for system prompt + tool results
+    model: ENV.fetch("GGUF_MODEL", File.expand_path("~/models/Qwen2.5-Coder-7B-Instruct-Q4_K_M.gguf")),
+    n_ctx: Integer(ENV.fetch("GGUF_N_CTX", "8192")),
     n_threads: Integer(ENV.fetch("GGUF_THREADS", "16")),
     max_tokens: Integer(ENV.fetch("GGUF_MAX_TOKENS", "512")),
-    n_gpu_layers: Integer(ENV.fetch("GGUF_N_GPU_LAYERS", gpu ? "999" : "0")), # 999 = offload all
-    temp: Float(ENV.fetch("GGUF_TEMP", "0.7")),                               # 0 = greedy
-    seed: Integer(ENV.fetch("GGUF_SEED", "-1"))                              # -1 = llama default
+    n_gpu_layers: Integer(ENV.fetch("GGUF_N_GPU_LAYERS", gpu ? "999" : "0")),
+    temp: Float(ENV.fetch("GGUF_TEMP", "0.7")),
+    seed: Integer(ENV.fetch("GGUF_SEED", "-1"))
   )
 end
lib/elelem/prompts/default.erb
@@ -1,40 +1,3 @@
-Terminal coding agent. Answer in as few words as the task allows -- no preamble, no filler. Verify your work.
+You are a terminal coding agent.
 
-# Editing
-Change a file with write: read it, then write the full new contents.
-Use `sed` only for a trivial single-line substitution.
-Never patch, apply_patch, or heredocs.
-
-# Shell
-<% if bsd? -%>
-BSD: no `find -printf`, no `du -b`; size `stat -f%z`; in place `sed -i ''`.
-<% else -%>
-GNU: `find -printf`, `du -b`; size `stat -c%s`; in place `sed -i`.
-<% end -%>
-
-# Search
-- text: `rg -n "pattern" .`
-- files: `fd -e rb .`
-- structure: `ast-grep -p 'def $NAME' -l ruby`
-- the repository means tracked files: `git ls-files`, never `.git`
-
-# Policy
-- Explain before non-trivial commands
-- After using tools, answer the question in your reply
-- Verify changes (read file, run tests)
-- No interactive flags (-i, -p)
-- `man` for unfamiliar flags
-
-# Environment
-pwd: <%= pwd %>
-platform: <%= platform %>
-date: <%= date %>
-self: <%= elelem_source %>
-<%= git_info %>
-
-<% if repo_map && !repo_map.empty? %>
-# Codebase
-```
-<%= repo_map %>```
-<% end %>
 <%= agents_md %>
lib/elelem/mcp.rb
@@ -68,7 +68,7 @@ module Elelem
 
       def call(name, args)
         result = request("tools/call", { name: name, arguments: args })
-        logger.info({ tool: name, args: args, result: result }.to_json)
+        Elelem.logger.info({ tool: name, args: args, result: result }.to_json)
         content = extract_content(result)
         result["isError"] ? { error: content } : { content: content }
       end
@@ -81,10 +81,6 @@ module Elelem
         end
       end
 
-      def logger
-        @logger ||= Logger.new(File.expand_path("~/.elelem/mcp.log"))
-      end
-
       private
 
       def handshake!
lib/elelem/system_prompt.rb
@@ -59,97 +59,6 @@ module Elelem
 
     private
 
-    def pwd = Dir.pwd
-    def platform = RUBY_PLATFORM.split("-").last
-    def bsd? = RUBY_PLATFORM.match?(/darwin|bsd/)
-    def date = DateTime.now
-
-    def elelem_source
-      spec = Gem.loaded_specs["elelem"]
-      spec ? spec.gem_dir : File.expand_path("../..", __dir__)
-    end
-
-    def git_info
-      return unless File.exist?(".git")
-      "branch: #{`git branch --show-current`.strip}"
-    rescue Errno::ENOENT
-      nil
-    end
-
-    def repo_map
-      files = `git ls-files '*.rb' 2>/dev/null`.lines.map(&:strip)
-      return "" if files.empty?
-
-      symbols = extract_symbols(files)
-      format_symbols(symbols, budget: 2000)
-    end
-
-    def extract_symbols(files)
-      output, status = Open3.capture2("sg", "run", "-p", "def $NAME", "-l", "ruby", "--json=compact", ".", err: File::NULL)
-      return ctags_fallback(files) unless status.success?
-
-      parse_sg_output(output, files)
-    end
-
-    def parse_sg_output(output, tracked_files)
-      JSON.parse(output).filter_map do |match|
-        file = match["file"]
-        next unless tracked_files.include?(file)
-        { file: file, name: match.dig("metaVariables", "single", "NAME", "text") }
-      end
-    rescue JSON::ParserError
-      []
-    end
-
-    def ctags_fallback(files)
-      return [] if files.empty?
-
-      output = IO.popen(["ctags", "-x", "--languages=Ruby", "--kinds-Ruby=cfm", "-L", "-"], "r+") do |io|
-        io.puts(files)
-        io.close_write
-        io.read
-      end
-
-      output.lines.map do |line|
-        parts = line.split(/\s+/, 4)
-        { file: parts[2], name: parts[0] }
-      end
-    rescue Errno::ENOENT
-      []
-    end
-
-    def format_symbols(symbols, budget:)
-      tree = build_tree(symbols)
-      render_tree(tree, budget: budget)
-    end
-
-    def build_tree(symbols)
-      tree = {}
-      symbols.group_by { |s| s[:file] }.each do |file, syms|
-        parts = file.split("/")
-        node = tree
-        parts[0..-2].each { |dir| node = (node[dir + "/"] ||= {}) }
-        node[parts.last] = syms.map { |s| s[:name] }.uniq
-      end
-      tree
-    end
-
-    def render_tree(node, indent: 0, budget:, result: String.new)
-      node.each do |key, value|
-        if value.is_a?(Hash)
-          line = "  " * indent + key + "\n"
-          return result if result.length + line.length > budget
-          result << line
-          render_tree(value, indent: indent + 1, budget: budget, result: result)
-        else
-          line = "  " * indent + key.sub(/\.rb$/, "") + ": " + value.join(" ") + "\n"
-          return result if result.length + line.length > budget
-          result << line
-        end
-      end
-      result
-    end
-
     def agents_md
       Pathname.pwd.ascend.each do |dir|
         file = dir / "AGENTS.md"
lib/elelem.rb
@@ -37,11 +37,13 @@ require_relative "elelem/version"
 require_relative "elelem/web_terminal"
 
 module Elelem
+    def logger
+      @logger ||= Logger.new("./elelem/current.log")
+    end
+
   def self.sh(cmd, args: [], cwd: Dir.pwd, env: {}, timeout: nil)
     output = StringIO.new
     options = { chdir: cwd }
-    # Own process group so a wall-clock timeout can kill the shell AND anything
-    # it spawned, not just bash itself.
     options[:pgroup] = true if timeout
 
     Open3.popen2e(env, cmd, *args, **options) do |stdin, out, wait_thr|
@@ -57,7 +59,6 @@ module Elelem
         yield line if block_given?
         output.write(line)
       end
-      # If it fired, let it finish escalating TERM -> KILL; otherwise cancel it.
       timed_out ? watchdog&.join : watchdog&.kill
 
       status = wait_thr.value
@@ -66,7 +67,6 @@ module Elelem
     end
   end
 
-  # Kill a process group started with pgroup: true (TERM, then KILL).
   def self.terminate(pid)
     Process.kill("TERM", -pid)
     sleep 0.5
@@ -74,7 +74,6 @@ module Elelem
   rescue Errno::ESRCH
     nil
   end
-  private_class_method :terminate
 
   def self.command_timeout
     value = ENV["ELELEM_CMD_TIMEOUT"]
spec/elelem/system_prompt_spec.rb
@@ -8,27 +8,18 @@ RSpec.describe Elelem::SystemPrompt do
   describe ".available_modes" do
     subject(:modes) { described_class.available_modes }
 
-    it { is_expected.to include("default", "plan") }
+    it { is_expected.to include("default") }
     it { is_expected.to eq(modes.sort) }
   end
 
   describe ".get" do
-    it { expect(described_class.get("default")).to include("Terminal coding agent") }
-    it { expect(described_class.get("plan")).to include("Scrum Master") }
+    it { expect(described_class.get("default")).to include("<%= agents_md %>") }
     it { expect(described_class.get("nonexistent")).to eq(described_class.get("default")) }
   end
 
-  it { expect(prompt.template).to include("Terminal coding agent") }
+  it { expect(prompt.template).to include("<%= agents_md %>") }
   it { expect(prompt.mode).to eq("default") }
 
-  describe "#render" do
-    it "gives the flags of the host userland" do
-      expected = RUBY_PLATFORM.match?(/darwin|bsd/) ? "stat -f%z" : "stat -c%s"
-
-      expect(prompt.render).to include(expected)
-    end
-  end
-
   describe "#switch" do
     before { prompt.switch("plan") }
 
spec/evals/cases/unix.yml
@@ -23,3 +23,12 @@
   expect:
     verify: '[ "$(cat total.txt)" = "$(cat *.rb | wc -l | tr -d " ")" ]'
     tools_used: ["execute"]
+
+# Defends: answer a live-system-state question with the real command, not a guess.
+- id: current-time
+  fixture: blank
+  turns:
+    - what time is it?
+  expect:
+    tools_used:
+      - execute: { command: "date" }
spec/evals/harness/tasks_spec.rb
@@ -28,34 +28,4 @@ RSpec.describe "Elelem::Evals tasks" do
       expect(ablator).to have_received(:minimize).with("KEEP\nDEAD\n")
     end
   end
-
-  describe ".regenerate" do
-    let(:seed) { File.join(dir, "seed.erb") }
-    let(:fake_loop) { instance_double(Elelem::Evals::Loop, run: true) }
-
-    before { File.write(seed, "# Environment\n<%= pwd %>\n") }
-
-    it "copies the seed to a candidate and runs the loop against it" do
-      candidate = File.join(dir, "candidate.erb")
-      captured = nil
-      loop_for = lambda do |champion|
-        captured = champion
-        fake_loop
-      end
-
-      Elelem::Evals.regenerate(rounds: 4, seed: seed, workdir: dir, loop_for: loop_for, out: out)
-
-      expect(captured).to eq(candidate)
-      expect(File.read(candidate)).to eq("# Environment\n<%= pwd %>\n")
-      expect(fake_loop).to have_received(:run).with(rounds: 4)
-    end
-
-    it "never touches the shipped champion path" do
-      loop_for = ->(_champion) { fake_loop }
-
-      Elelem::Evals.regenerate(rounds: 1, seed: seed, workdir: dir, loop_for: loop_for, out: out)
-
-      expect(File.read(Elelem::Evals::CHAMPION)).to include("Terminal coding agent")
-    end
-  end
 end
spec/evals/prompts/seed.erb
@@ -1,16 +1,1 @@
-<% # Minimal seed: only the structural ERB anchors, no guidance. -%>
-<% # `bin/evals regenerate` grows guidance from here; every added line must -%>
-<% # earn its place by defending an eval case. -%>
-# Environment
-pwd: <%= pwd %>
-platform: <%= platform %>
-date: <%= date %>
-self: <%= elelem_source %>
-<%= git_info %>
-
-<% if repo_map && !repo_map.empty? %>
-# Codebase
-```
-<%= repo_map %>```
-<% end %>
 <%= agents_md %>
spec/evals/support/tasks.rb
@@ -5,16 +5,11 @@ require "fileutils"
 
 module Elelem
   module Evals
-    # A scorer_for lambda over the full case set: prompt -> Score, running the
-    # real agent against the real model. Shared by the loop, minimize, regenerate.
     def self.scorer_for(cases: Case.load_all)
       ->(prompt) { Scorer.new(runner: Runner.new(prompt: prompt)).call(cases) }
     end
 
-    # Refactor step: strip every champion line that defends no case, writing the
-    # result and the line -> case map to WORKDIR for a human to review and adopt.
-    def self.minimize!(champion: CHAMPION, workdir: WORKDIR, out: $stdout,
-      ablator: Ablator.new(scorer_for: scorer_for))
+    def self.minimize!(champion: CHAMPION, workdir: WORKDIR, out: $stdout, ablator: Ablator.new(scorer_for: scorer_for))
       before = File.read(champion)
       ablation = ablator.minimize(before)
 
@@ -22,15 +17,11 @@ module Elelem
       File.write(File.join(workdir, "minimized.erb"), ablation.prompt)
       File.write(File.join(workdir, "defends.json"), JSON.pretty_generate(ablation.defends))
 
-      out.puts "minimize: #{before.length} -> #{ablation.prompt.length} chars, " \
-        "wrote minimized.erb + defends.json to #{workdir}"
+      out.puts "minimize: #{before.length} -> #{ablation.prompt.length} chars, wrote minimized.erb + defends.json to #{workdir}"
       ablation
     end
 
-    # Grow a fresh prompt from the minimal seed. Runs the loop against a candidate
-    # copy so the shipped champion is never overwritten; a human diffs and adopts.
-    def self.regenerate(rounds:, seed: SEED, workdir: WORKDIR, out: $stdout,
-      loop_for: ->(champion) { Loop.new(champion: champion, out: out) })
+    def self.regenerate(rounds:, seed: SEED, workdir: WORKDIR, out: $stdout, loop_for: ->(champion) { Loop.new(champion: champion, out: out) })
       FileUtils.mkdir_p(workdir)
       candidate = File.join(workdir, "candidate.erb")
       FileUtils.cp(seed, candidate)
elelem.gemspec
@@ -9,7 +9,7 @@ Gem::Specification.new do |spec|
   spec.email = ["mo@mokhan.ca"]
 
   spec.summary = "A minimal coding agent for LLMs."
-  spec.description = "A minimal coding agent supporting Ollama and more."
+  spec.description = "A minimal coding agent."
   spec.homepage = "https://src.mokhan.ca/xlgmokha/elelem"
   spec.license = "MIT"
   spec.required_ruby_version = ">= 4.0.0"
Rakefile
@@ -10,36 +10,4 @@ task :compile do
   ruby "ext/elelem_llama/extconf.rb"
 end
 
-task :evals_env do
-  ENV["EVALS"] = "1"
-end
-
-desc "Score the champion system prompt against the eval cases"
-RSpec::Core::RakeTask.new({ evals: :evals_env }) do |t|
-  t.pattern = "spec/evals/cases_spec.rb"
-end
-
-namespace :evals do
-  desc "Tune the champion system prompt against the eval cases"
-  task :improve do
-    require_relative "spec/support/evals"
-
-    Elelem::Evals::Loop.new.run(rounds: Integer(ENV.fetch("ROUNDS", "3")))
-  end
-
-  desc "Drop every champion prompt line whose removal regresses no case"
-  task :minimize do
-    require_relative "spec/support/evals"
-
-    Elelem::Evals.minimize!
-  end
-
-  desc "Grow a fresh prompt from the minimal seed (leaves the champion untouched)"
-  task :regenerate do
-    require_relative "spec/support/evals"
-
-    Elelem::Evals.regenerate(rounds: Integer(ENV.fetch("ROUNDS", "8")))
-  end
-end
-
 task default: %i[spec]
README.md
@@ -90,10 +90,6 @@ Configure MCP servers in `~/.elelem/mcp.json` or `.elelem/mcp.json`:
 ```json
 {
   "mcpServers": {
-    "gitlab": {
-      "type": "http",
-      "url": "https://gitlab.com/api/v4/mcp"
-    },
     "playwright": {
       "command": "npx",
       "args": [