diff --git a/assets/agents/.shared/utils.sh b/assets/agents/.shared/utils.sh new file mode 100755 index 0000000..bf631de --- /dev/null +++ b/assets/agents/.shared/utils.sh @@ -0,0 +1,447 @@ +#!/usr/bin/env bash +# Shared Agent Utilities - Minimal, focused helper functions +set -euo pipefail + +############################# +## CONTEXT FILE MANAGEMENT ## +############################# + +get_context_file() { + local project_dir="${LLM_AGENT_VAR_PROJECT_DIR:-.}" + echo "${project_dir}/.loki-context" +} + +# Initialize context file for a new task +# Usage: init_context "Task description" +init_context() { + local task="$1" + local project_dir="${LLM_AGENT_VAR_PROJECT_DIR:-.}" + local context_file + context_file=$(get_context_file) + + cat > "${context_file}" <> "${context_file}" + fi +} + +# Read the current context (returns empty string if no context) +# Usage: context=$(read_context) +read_context() { + local context_file + context_file=$(get_context_file) + + if [[ -f "${context_file}" ]]; then + cat "${context_file}" + fi +} + +# Clear the context file +clear_context() { + local context_file + context_file=$(get_context_file) + rm -f "${context_file}" +} + +####################### +## PROJECT DETECTION ## +####################### + +# Cache file name for detected project info +_LOKI_PROJECT_CACHE=".loki-project.json" + +# Read cached project detection if valid +# Usage: _read_project_cache "/path/to/project" +# Returns: cached JSON on stdout (exit 0) or nothing (exit 1) +_read_project_cache() { + local dir="$1" + local cache_file="${dir}/${_LOKI_PROJECT_CACHE}" + + if [[ -f "${cache_file}" ]]; then + local cached + cached=$(cat "${cache_file}" 2>/dev/null) || return 1 + if echo "${cached}" | jq -e '.type and .build != null and .test != null and .check != null' &>/dev/null; then + echo "${cached}" + return 0 + fi + fi + return 1 +} + +# Write project detection result to cache +# Usage: _write_project_cache "/path/to/project" '{"type":"rust",...}' +_write_project_cache() { + local dir="$1" + local json="$2" + local cache_file="${dir}/${_LOKI_PROJECT_CACHE}" + + echo "${json}" > "${cache_file}" 2>/dev/null || true +} + +_detect_heuristic() { + local dir="$1" + + # Rust + if [[ -f "${dir}/Cargo.toml" ]]; then + echo '{"type":"rust","build":"cargo build","test":"cargo test","check":"cargo check"}' + return 0 + fi + + # Go + if [[ -f "${dir}/go.mod" ]]; then + echo '{"type":"go","build":"go build ./...","test":"go test ./...","check":"go vet ./..."}' + return 0 + fi + + # Node.JS/Deno/Bun + if [[ -f "${dir}/deno.json" ]] || [[ -f "${dir}/deno.jsonc" ]]; then + echo '{"type":"deno","build":"deno task build","test":"deno test","check":"deno lint"}' + return 0 + fi + + if [[ -f "${dir}/package.json" ]]; then + local pm="npm" + + [[ -f "${dir}/bun.lockb" ]] || [[ -f "${dir}/bun.lock" ]] && pm="bun" + [[ -f "${dir}/pnpm-lock.yaml" ]] && pm="pnpm" + [[ -f "${dir}/yarn.lock" ]] && pm="yarn" + + echo "{\"type\":\"nodejs\",\"build\":\"${pm} run build\",\"test\":\"${pm} test\",\"check\":\"${pm} run lint\"}" + return 0 + fi + + # Python + if [[ -f "${dir}/pyproject.toml" ]] || [[ -f "${dir}/setup.py" ]] || [[ -f "${dir}/setup.cfg" ]]; then + local test_cmd="pytest" + local check_cmd="ruff check ." + + if [[ -f "${dir}/poetry.lock" ]]; then + test_cmd="poetry run pytest" + check_cmd="poetry run ruff check ." + elif [[ -f "${dir}/uv.lock" ]]; then + test_cmd="uv run pytest" + check_cmd="uv run ruff check ." + fi + + echo "{\"type\":\"python\",\"build\":\"\",\"test\":\"${test_cmd}\",\"check\":\"${check_cmd}\"}" + return 0 + fi + + # JVM (Maven) + if [[ -f "${dir}/pom.xml" ]]; then + echo '{"type":"java","build":"mvn compile","test":"mvn test","check":"mvn verify"}' + return 0 + fi + + # JVM (Gradle) + if [[ -f "${dir}/build.gradle" ]] || [[ -f "${dir}/build.gradle.kts" ]]; then + local gw="gradle" + [[ -f "${dir}/gradlew" ]] && gw="./gradlew" + echo "{\"type\":\"java\",\"build\":\"${gw} build\",\"test\":\"${gw} test\",\"check\":\"${gw} check\"}" + return 0 + fi + + # .NET / C# + if compgen -G "${dir}/*.sln" &>/dev/null || compgen -G "${dir}/*.csproj" &>/dev/null; then + echo '{"type":"dotnet","build":"dotnet build","test":"dotnet test","check":"dotnet build --warnaserrors"}' + return 0 + fi + + # C/C++ (CMake) + if [[ -f "${dir}/CMakeLists.txt" ]]; then + echo '{"type":"cmake","build":"cmake --build build","test":"ctest --test-dir build","check":"cmake --build build"}' + return 0 + fi + + # Ruby + if [[ -f "${dir}/Gemfile" ]]; then + local test_cmd="bundle exec rake test" + [[ -f "${dir}/Rakefile" ]] && grep -q "rspec" "${dir}/Gemfile" 2>/dev/null && test_cmd="bundle exec rspec" + echo "{\"type\":\"ruby\",\"build\":\"\",\"test\":\"${test_cmd}\",\"check\":\"bundle exec rubocop\"}" + return 0 + fi + + # Elixir + if [[ -f "${dir}/mix.exs" ]]; then + echo '{"type":"elixir","build":"mix compile","test":"mix test","check":"mix credo"}' + return 0 + fi + + # PHP + if [[ -f "${dir}/composer.json" ]]; then + echo '{"type":"php","build":"","test":"./vendor/bin/phpunit","check":"./vendor/bin/phpstan analyse"}' + return 0 + fi + + # Swift + if [[ -f "${dir}/Package.swift" ]]; then + echo '{"type":"swift","build":"swift build","test":"swift test","check":"swift build"}' + return 0 + fi + + # Zig + if [[ -f "${dir}/build.zig" ]]; then + echo '{"type":"zig","build":"zig build","test":"zig build test","check":"zig build"}' + return 0 + fi + + # Generic build systems (last resort before LLM) + if [[ -f "${dir}/justfile" ]] || [[ -f "${dir}/Justfile" ]]; then + echo '{"type":"just","build":"just build","test":"just test","check":"just lint"}' + return 0 + fi + + if [[ -f "${dir}/Makefile" ]] || [[ -f "${dir}/makefile" ]] || [[ -f "${dir}/GNUmakefile" ]]; then + echo '{"type":"make","build":"make build","test":"make test","check":"make lint"}' + return 0 + fi + + return 1 +} + +# Gather lightweight evidence about a project for LLM analysis +# Usage: _gather_project_evidence "/path/to/project" +# Returns: evidence string on stdout +_gather_project_evidence() { + local dir="$1" + local evidence="" + + evidence+="Root files and directories:"$'\n' + evidence+=$(ls -1 "${dir}" 2>/dev/null | head -50) + evidence+=$'\n\n' + + evidence+="File extension counts:"$'\n' + evidence+=$(find "${dir}" -type f \ + -not -path '*/.git/*' \ + -not -path '*/node_modules/*' \ + -not -path '*/target/*' \ + -not -path '*/dist/*' \ + -not -path '*/__pycache__/*' \ + -not -path '*/vendor/*' \ + -not -path '*/.build/*' \ + 2>/dev/null \ + | sed 's/.*\.//' | sort | uniq -c | sort -rn | head -10) + evidence+=$'\n\n' + + local config_patterns=("*.toml" "*.yaml" "*.yml" "*.json" "*.xml" "*.gradle" "*.gradle.kts" "*.cabal" "*.pro" "Makefile" "justfile" "Justfile" "Dockerfile" "Taskfile*" "BUILD" "WORKSPACE" "flake.nix" "shell.nix" "default.nix") + local found_configs=0 + + for pattern in "${config_patterns[@]}"; do + if [[ ${found_configs} -ge 5 ]]; then + break + fi + + local files + files=$(find "${dir}" -maxdepth 1 -name "${pattern}" -type f 2>/dev/null) + + while IFS= read -r f; do + if [[ -n "${f}" && ${found_configs} -lt 5 ]]; then + local basename + basename=$(basename "${f}") + evidence+="--- ${basename} (first 30 lines) ---"$'\n' + evidence+=$(head -30 "${f}" 2>/dev/null) + evidence+=$'\n\n' + found_configs=$((found_configs + 1)) + fi + done <<< "${files}" + done + + echo "${evidence}" +} + +# LLM-based project detection fallback +# Usage: _detect_with_llm "/path/to/project" +# Returns: JSON on stdout or empty (exit 1) +_detect_with_llm() { + local dir="$1" + local evidence + evidence=$(_gather_project_evidence "${dir}") + local prompt + prompt=$(cat <<-EOF + + Analyze this project directory and determine the project type, primary language, and the correct shell commands to build, test, and check (lint/typecheck) it. + + EOF + ) + prompt+=$'\n'"${evidence}"$'\n' + prompt+=$(cat <<-EOF + + Respond with ONLY a valid JSON object. No markdown fences, no explanation, no extra text. + The JSON must have exactly these 4 keys: + {"type":"","build":"","test":"","check":""} + + Rules: + - "type" must be a single lowercase word (e.g. rust, go, python, nodejs, java, ruby, elixir, cpp, c, zig, haskell, scala, kotlin, dart, swift, php, dotnet, etc.) + - If a command doesn't apply to this project, use an empty string, "" + - Use the most standard/common commands for the detected ecosystem + - If you detect a package manager lockfile, use that package manager (e.g. pnpm over npm) + EOF + ) + + local llm_response + llm_response=$(loki --no-stream "${prompt}" 2>/dev/null) || return 1 + + llm_response=$(echo "${llm_response}" | sed 's/^```json//;s/^```//;s/```$//' | tr -d '\n' | sed 's/^[[:space:]]*//') + llm_response=$(echo "${llm_response}" | grep -o '{[^}]*}' | head -1) + + if echo "${llm_response}" | jq -e '.type and .build != null and .test != null and .check != null' &>/dev/null; then + echo "${llm_response}" | jq -c '{type: (.type // "unknown"), build: (.build // ""), test: (.test // ""), check: (.check // "")}' + return 0 + fi + + return 1 +} + +# Detect project type and return build/test commands +# Uses: cached result -> fast heuristics -> LLM fallback +detect_project() { + local dir="${1:-.}" + + local cached + if cached=$(_read_project_cache "${dir}"); then + echo "${cached}" | jq -c '{type, build, test, check}' + return 0 + fi + + local result + if result=$(_detect_heuristic "${dir}"); then + local enriched + enriched=$(echo "${result}" | jq -c '. + {"_detected_by":"heuristic","_cached_at":"'"$(date -Iseconds)"'"}') + + _write_project_cache "${dir}" "${enriched}" + + echo "${result}" + return 0 + fi + + if result=$(_detect_with_llm "${dir}"); then + local enriched + enriched=$(echo "${result}" | jq -c '. + {"_detected_by":"llm","_cached_at":"'"$(date -Iseconds)"'"}') + + _write_project_cache "${dir}" "${enriched}" + + echo "${result}" + return 0 + fi + + echo '{"type":"unknown","build":"","test":"","check":""}' +} + +###################### +## AGENT INVOCATION ## +###################### + +# Invoke a subagent with optional context injection +# Usage: invoke_agent [extra_args...] +invoke_agent() { + local agent="$1" + local prompt="$2" + shift 2 + + local context + context=$(read_context) + + local full_prompt + if [[ -n "${context}" ]]; then + full_prompt="## Orchestrator Context + +The orchestrator (sisyphus) has gathered this context from prior work: + + +${context} + + +## Your Task + +${prompt}" + else + full_prompt="${prompt}" + fi + + env AUTO_CONFIRM=true loki --agent "${agent}" "$@" "${full_prompt}" 2>&1 +} + +# Invoke a subagent and capture a summary of its findings +# Usage: result=$(invoke_agent_with_summary "explore" "find auth patterns") +invoke_agent_with_summary() { + local agent="$1" + local prompt="$2" + shift 2 + + local output + output=$(invoke_agent "${agent}" "${prompt}" "$@") + + local summary="" + + if echo "${output}" | grep -q "FINDINGS:"; then + summary=$(echo "${output}" | sed -n '/FINDINGS:/,/^[A-Z_]*COMPLETE/p' | grep "^- " | sed 's/^- / - /') + elif echo "${output}" | grep -q "CODER_COMPLETE:"; then + summary=$(echo "${output}" | grep "CODER_COMPLETE:" | sed 's/CODER_COMPLETE: *//') + elif echo "${output}" | grep -q "ORACLE_COMPLETE"; then + summary=$(echo "${output}" | sed -n '/^## Recommendation/,/^## /{/^## Recommendation/d;/^## /d;p}' | sed '/^$/d' | head -10) + fi + + # Failsafe: extract up to 5 meaningful lines if no markers found + if [[ -z "${summary}" ]]; then + summary=$(echo "${output}" | grep -v "^$" | grep -v "^#" | grep -v "^\-\-\-" | tail -10 | head -5) + fi + + if [[ -n "${summary}" ]]; then + append_context "${agent}" "${summary}" + fi + + echo "${output}" +} + +########################### +## FILE SEARCH UTILITIES ## +########################### + +search_files() { + local pattern="$1" + local dir="${2:-.}" + + find "${dir}" -type f -name "${pattern}" \ + -not -path '*/target/*' \ + -not -path '*/node_modules/*' \ + -not -path '*/.git/*' \ + -not -path '*/dist/*' \ + -not -path '*/__pycache__/*' \ + 2>/dev/null | head -25 +} + +get_tree() { + local dir="${1:-.}" + local depth="${2:-3}" + + if command -v tree &>/dev/null; then + tree -L "${depth}" --noreport -I 'node_modules|target|dist|.git|__pycache__|*.pyc' "${dir}" 2>/dev/null || find "${dir}" -maxdepth "${depth}" -type f | head -50 + else + find "${dir}" -maxdepth "${depth}" -type f \ + -not -path '*/target/*' \ + -not -path '*/node_modules/*' \ + -not -path '*/.git/*' \ + 2>/dev/null | head -50 + fi +} diff --git a/assets/agents/explore/README.md b/assets/agents/explore/README.md new file mode 100644 index 0000000..9085c77 --- /dev/null +++ b/assets/agents/explore/README.md @@ -0,0 +1,15 @@ +# Explore + +An AI agent specialized in exploring codebases, finding patterns, and understanding project structures. + +This agent is designed to be delegated to by the **[Sisyphus](../sisyphus/README.md)** agent to gather information and context. Sisyphus +acts as the coordinator/architect, while Explore handles the research and discovery phase. + +It can also be used as a standalone tool for understanding codebases and finding specific information. + +## Features + +- 🔍 Deep codebase exploration and pattern matching +- 📂 File system navigation and content analysis +- 🧠 Context gathering for complex tasks +- 🛡️ Read-only operations for safe investigation diff --git a/assets/agents/explore/config.yaml b/assets/agents/explore/config.yaml new file mode 100644 index 0000000..8be28bf --- /dev/null +++ b/assets/agents/explore/config.yaml @@ -0,0 +1,74 @@ +name: explore +description: Fast codebase exploration agent - finds patterns, structures, and relevant files +version: 1.0.0 +temperature: 0.1 +top_p: 0.95 + +variables: + - name: project_dir + description: Project directory to explore + default: '.' + +global_tools: + - fs_read.sh + - fs_grep.sh + - fs_glob.sh + - fs_ls.sh + +instructions: | + You are a codebase explorer. Your job: Search, find, report. Nothing else. + + ## Your Mission + + Given a search task, you: + 1. Search for relevant files and patterns + 2. Read key files to understand structure + 3. Report findings concisely + 4. Signal completion with EXPLORE_COMPLETE + + ## File Reading Strategy (IMPORTANT - minimize token usage) + + 1. **Find first, read second** - Never read a file without knowing why + 2. **Use grep to locate** - `fs_grep --pattern "struct User" --include "*.rs"` finds exactly where things are + 3. **Use glob to discover** - `fs_glob --pattern "*.rs" --path src/` finds files by name + 4. **Read targeted sections** - `fs_read --path "src/main.rs" --offset 50 --limit 30` reads only lines 50-79 + 5. **Never read entire large files** - If a file is 500+ lines, read the relevant section only + + ## Available Actions + + - `fs_grep --pattern "struct User" --include "*.rs"` - Find content across files + - `fs_glob --pattern "*.rs" --path src/` - Find files by name pattern + - `fs_read --path "src/main.rs"` - Read a file (with line numbers) + - `fs_read --path "src/main.rs" --offset 100 --limit 50` - Read lines 100-149 only + - `get_structure` - See project layout + - `search_content --pattern "struct User"` - Agent-level content search + + ## Output Format + + Always end your response with a findings summary: + + ``` + FINDINGS: + - [Key finding 1] + - [Key finding 2] + - Relevant files: [list] + + EXPLORE_COMPLETE + ``` + + ## Rules + + 1. **Be fast** - Don't read every file, read representative ones + 2. **Be focused** - Answer the specific question asked + 3. **Be concise** - Report findings, not your process + 4. **Never modify files** - You are read-only + 5. **Limit reads** - Max 5 file reads per exploration + + ## Context + - Project: {{project_dir}} + - CWD: {{__cwd__}} + +conversation_starters: + - 'Find how authentication is implemented' + - 'What patterns are used for API endpoints' + - 'Show me the project structure' diff --git a/assets/agents/explore/tools.sh b/assets/agents/explore/tools.sh new file mode 100755 index 0000000..510d4f3 --- /dev/null +++ b/assets/agents/explore/tools.sh @@ -0,0 +1,157 @@ +#!/usr/bin/env bash +set -eo pipefail + +# shellcheck disable=SC1090 +source "$LLM_PROMPT_UTILS_FILE" +source "$LLM_ROOT_DIR/agents/.shared/utils.sh" + +# @env LLM_OUTPUT=/dev/stdout +# @env LLM_AGENT_VAR_PROJECT_DIR=. +# @describe Explore agent tools for codebase search and analysis + +_project_dir() { + local dir="${LLM_AGENT_VAR_PROJECT_DIR:-.}" + (cd "${dir}" 2>/dev/null && pwd) || echo "${dir}" +} + +# @cmd Get project structure and layout +get_structure() { + local project_dir + project_dir=$(_project_dir) + + info "Project structure:" >> "$LLM_OUTPUT" + echo "" >> "$LLM_OUTPUT" + + local project_info + project_info=$(detect_project "${project_dir}") + + { + echo "Type: $(echo "${project_info}" | jq -r '.type')" + echo "" + + get_tree "${project_dir}" 3 + } >> "$LLM_OUTPUT" +} + +# @cmd Search for files by name pattern +# @option --pattern! File name pattern (e.g., "*.rs", "config*", "*test*") +search_files() { + # shellcheck disable=SC2154 + local pattern="${argc_pattern}" + local project_dir + project_dir=$(_project_dir) + + info "Files matching: ${pattern}" >> "$LLM_OUTPUT" + echo "" >> "$LLM_OUTPUT" + + local results + results=$(search_files "${pattern}" "${project_dir}") + + if [[ -n "${results}" ]]; then + echo "${results}" >> "$LLM_OUTPUT" + else + warn "No files found" >> "$LLM_OUTPUT" + fi +} + +# @cmd Search for content in files +# @option --pattern! Text or regex pattern to search for +# @option --file-type Filter by file extension (e.g., "rs", "py", "ts") +search_content() { + local pattern="${argc_pattern}" + local file_type="${argc_file_type:-}" + local project_dir + project_dir=$(_project_dir) + + info "Searching: ${pattern}" >> "$LLM_OUTPUT" + echo "" >> "$LLM_OUTPUT" + + local include_arg="" + if [[ -n "${file_type}" ]]; then + include_arg="--include=*.${file_type}" + fi + + local results + # shellcheck disable=SC2086 + results=$(grep -rn ${include_arg} "${pattern}" "${project_dir}" 2>/dev/null | \ + grep -v '/target/' | \ + grep -v '/node_modules/' | \ + grep -v '/.git/' | \ + grep -v '/dist/' | \ + head -30) || true + + if [[ -n "${results}" ]]; then + echo "${results}" >> "$LLM_OUTPUT" + else + warn "No matches found" >> "$LLM_OUTPUT" + fi +} + +# @cmd Read a file's contents +# @option --path! Path to the file (relative to project root) +# @option --lines Maximum lines to read (default: 200) +read_file() { + # shellcheck disable=SC2154 + local file_path="${argc_path}" + local max_lines="${argc_lines:-200}" + local project_dir + project_dir=$(_project_dir) + + local full_path="${project_dir}/${file_path}" + + if [[ ! -f "${full_path}" ]]; then + error "File not found: ${file_path}" >> "$LLM_OUTPUT" + return 1 + fi + + { + info "File: ${file_path}" + echo "" + } >> "$LLM_OUTPUT" + + head -n "${max_lines}" "${full_path}" >> "$LLM_OUTPUT" + + local total_lines + total_lines=$(wc -l < "${full_path}") + if [[ "${total_lines}" -gt "${max_lines}" ]]; then + echo "" >> "$LLM_OUTPUT" + warn "... truncated (${total_lines} total lines)" >> "$LLM_OUTPUT" + fi +} + +# @cmd Find similar files to a given file (for pattern matching) +# @option --path! Path to the reference file +find_similar() { + local file_path="${argc_path}" + local project_dir + project_dir=$(_project_dir) + + local ext="${file_path##*.}" + local dir + dir=$(dirname "${file_path}") + + info "Files similar to: ${file_path}" >> "$LLM_OUTPUT" + echo "" >> "$LLM_OUTPUT" + + local results + results=$(find "${project_dir}/${dir}" -maxdepth 1 -type f -name "*.${ext}" \ + ! -name "$(basename "${file_path}")" \ + ! -name "*test*" \ + ! -name "*spec*" \ + 2>/dev/null | head -5) + + if [[ -n "${results}" ]]; then + echo "${results}" >> "$LLM_OUTPUT" + else + results=$(find "${project_dir}" -type f -name "*.${ext}" \ + ! -name "$(basename "${file_path}")" \ + ! -name "*test*" \ + -not -path '*/target/*' \ + 2>/dev/null | head -5) + if [[ -n "${results}" ]]; then + echo "${results}" >> "$LLM_OUTPUT" + else + warn "No similar files found" >> "$LLM_OUTPUT" + fi + fi +} \ No newline at end of file