Skip to content

Instantly share code, notes, and snippets.

@mikehostetler
Created February 18, 2026 12:30
Show Gist options
  • Select an option

  • Save mikehostetler/466530610afa2467b6638d22c3fcf3e7 to your computer and use it in GitHub Desktop.

Select an option

Save mikehostetler/466530610afa2467b6638d22c3fcf3e7 to your computer and use it in GitHub Desktop.
Jido Issue Triage Bot — Pure ReAct Approach (V1)

Jido Issue Triage Bot — Pure ReAct Approach

Philosophy

No Runic DAG. No structured pipeline. No orchestrator. Just a Jido.AI.Agent with tools and a good prompt. The LLM decides what to do, in what order, and how to recover from errors — exactly like a human developer would.

mix jido.triage https://github.com/agentjido/jido/issues/42

┌──────────────────────────────────────────────────────┐
│  ReAct Agent                                         │
│                                                      │
│  Tools:                                              │
│    sprite_create    — spin up a Sprite               │
│    sprite_exec      — run a shell command in Sprite  │
│    sprite_destroy   — tear down a Sprite             │
│    claude_run       — run Claude Code in a Sprite    │
│                                                      │
│  Skills:                                             │
│    github-issue-triage  — how to investigate issues  │
│    sprite-workflow      — how to use Sprites          │
│    claude-in-sprite     — how to run Claude remotely │
│                                                      │
│  The LLM figures out the rest.                       │
└──────────────────────────────────────────────────────┘

1. The Agent — That's It

One module. Four tools. Three skills. A system prompt.

defmodule Jido.IssueTriage.Agent do
  use Jido.AI.Agent,
    name: "issue_triage_bot",
    description: "Investigates GitHub issues using Claude Code in ephemeral Sprites",
    tools: [
      Jido.IssueTriage.Tools.SpriteCreate,
      Jido.IssueTriage.Tools.SpriteExec,
      Jido.IssueTriage.Tools.SpriteDestroy,
      Jido.IssueTriage.Tools.ClaudeRun
    ],
    system_prompt: """
    You are a GitHub issue triage bot. You investigate issues by reading
    the actual codebase and running analysis using Claude Code inside
    an ephemeral development environment (Sprite).

    You have access to skills that explain your workflow in detail.
    Follow them carefully.
    """,
    max_iterations: 30,
    tool_timeout_ms: 300_000

  @skills [
    Jido.IssueTriage.Skills.GithubIssueTriage,
    Jido.IssueTriage.Skills.SpriteWorkflow,
    Jido.IssueTriage.Skills.ClaudeInSprite
  ]

  @impl true
  def on_before_cmd(agent, {:ai_react_start, %{query: query} = params} = _action) do
    # Inject secrets into tool_context — never in LLM prompts
    tool_context = %{
      fly_token: System.get_env("FLY_API_TOKEN"),
      gh_token: System.get_env("GH_TOKEN"),
      anthropic_key: System.get_env("ANTHROPIC_API_KEY")
    }

    # Render skill instructions into the system prompt
    skill_prompt = Jido.AI.Skill.Prompt.render(@skills)
    existing_context = Map.get(params, :tool_context, %{})
    updated_context = Map.merge(existing_context, tool_context)
    updated_params = Map.put(params, :tool_context, updated_context)

    agent = %{agent | state: agent.state
      |> Map.put(:last_query, query)
      |> Map.put(:completed, false)
      |> Map.put(:last_answer, "")
    }

    {:ok, agent, {:ai_react_start, updated_params}}
  end

  @impl true
  def on_before_cmd(agent, action), do: {:ok, agent, action}
end

That's the entire agent. The LLM reads the skills, uses the tools, and figures out the workflow dynamically.


2. Tools — Four Primitives

These are the only moving parts. Each is a Jido.Action that the ReAct loop can call. They're intentionally low-level — the LLM composes them.

Tool 1: sprite_create

Spins up a Fly.io Sprite and returns a session handle.

defmodule Jido.IssueTriage.Tools.SpriteCreate do
  @moduledoc "Create an ephemeral Sprite VM and return a session ID."

  use Jido.Action,
    name: "sprite_create",
    description: """
    Creates an ephemeral Fly.io Sprite (a lightweight VM) and starts a shell session.
    Returns a session_id you can use with sprite_exec to run commands.
    Environment variables (GH_TOKEN, ANTHROPIC_API_KEY) are pre-configured.
    """,
    schema: [
      name: [type: :string, required: true,
             doc: "A short name for the sprite (e.g. 'triage-jido-42'). Lowercase, hyphens ok."]
    ]

  @impl true
  def run(params, context) do
    fly_token = context[:fly_token] || raise "Missing FLY_API_TOKEN"
    gh_token = context[:gh_token]
    anthropic_key = context[:anthropic_key]

    workspace_id = "triage-#{params.name}"

    env = %{"GH_PROMPT_DISABLED" => "1"}
    env = if gh_token, do: Map.put(env, "GH_TOKEN", gh_token), else: env
    env = if anthropic_key, do: Map.put(env, "ANTHROPIC_API_KEY", anthropic_key), else: env

    session_opts = [
      backend: {Jido.Shell.Backend.Sprite, %{
        sprite_name: params.name,
        token: fly_token,
        create: true
      }},
      cwd: "/",
      env: env
    ]

    case Jido.Shell.ShellSession.start_with_vfs(workspace_id, session_opts) do
      {:ok, session_id} ->
        {:ok, %{
          session_id: session_id,
          sprite_name: params.name,
          message: "Sprite '#{params.name}' is running. Use sprite_exec to run commands."
        }}

      {:error, reason} ->
        {:error, "Failed to create sprite: #{inspect(reason)}"}
    end
  end
end

Tool 2: sprite_exec

Runs a shell command inside an existing Sprite. This is the workhorse — the LLM uses it for everything: gh, git, ls, cat, mix, whatever it needs.

defmodule Jido.IssueTriage.Tools.SpriteExec do
  @moduledoc "Execute a shell command inside a running Sprite."

  use Jido.Action,
    name: "sprite_exec",
    description: """
    Runs a shell command inside a Sprite and returns the output.
    Use this for git clone, gh commands, file inspection, running tests, etc.
    The command runs as a real shell — you can pipe, redirect, use && and ||.
    """,
    schema: [
      session_id: [type: :string, required: true,
                   doc: "The session_id returned by sprite_create"],
      command: [type: :string, required: true,
                doc: "The shell command to execute (e.g. 'git clone ... /repo')"],
      timeout: [type: :integer, default: 60_000,
                doc: "Timeout in milliseconds (default: 60000)"]
    ]

  @impl true
  def run(params, _context) do
    case Jido.Shell.Agent.run(params.session_id, params.command, timeout: params.timeout) do
      {:ok, output} ->
        # Truncate very long output to avoid blowing up LLM context
        truncated = truncate(output, 8_000)
        {:ok, %{output: truncated, exit: "success"}}

      {:error, error} ->
        {:ok, %{output: inspect(error), exit: "error"}}
    end
  end

  defp truncate(text, max) when byte_size(text) <= max, do: text
  defp truncate(text, max) do
    head = binary_part(text, 0, div(max, 2))
    tail = binary_part(text, byte_size(text) - div(max, 2), div(max, 2))
    head <> "\n\n... [truncated #{byte_size(text) - max} bytes] ...\n\n" <> tail
  end
end

Tool 3: sprite_destroy

Tears down a Sprite. The LLM calls this when it's done.

defmodule Jido.IssueTriage.Tools.SpriteDestroy do
  @moduledoc "Destroy a Sprite and clean up resources."

  use Jido.Action,
    name: "sprite_destroy",
    description: """
    Stops the shell session and destroys the Sprite VM.
    Call this when you're done with the environment.
    If you want to keep the Sprite for later, don't call this.
    """,
    schema: [
      session_id: [type: :string, required: true,
                   doc: "The session_id to destroy"]
    ]

  @impl true
  def run(params, _context) do
    case Jido.Shell.ShellSession.stop(params.session_id) do
      :ok -> {:ok, %{message: "Sprite destroyed."}}
      {:error, reason} -> {:ok, %{message: "Cleanup attempted: #{inspect(reason)}"}}
    end
  end
end

Tool 4: claude_run

Runs Claude Code inside a Sprite. This is the only "complex" tool — it uses jido_claude's Shell executor to run Claude on the same Sprite without owning it.

defmodule Jido.IssueTriage.Tools.ClaudeRun do
  @moduledoc "Run Claude Code inside a Sprite to investigate code."

  use Jido.Action,
    name: "claude_run",
    description: """
    Runs Claude Code (the CLI agent) inside a running Sprite.
    Claude will read files, run commands, and reason about the codebase.
    Returns Claude's final output text.

    The Sprite must already have the repo cloned and set up.
    Claude's ANTHROPIC_API_KEY is pre-configured in the Sprite environment.

    This is a long-running operation — it may take several minutes.
    """,
    schema: [
      sprite_name: [type: :string, required: true,
                    doc: "The sprite_name (not session_id) to run Claude in"],
      prompt: [type: :string, required: true,
               doc: "The prompt/instructions for Claude Code"],
      cwd: [type: :string, default: "/repo",
            doc: "Working directory inside the Sprite (default: /repo)"],
      model: [type: :string, default: "sonnet",
              doc: "Claude model to use (default: sonnet)"],
      max_turns: [type: :integer, default: 25,
                  doc: "Maximum agentic turns for Claude (default: 25)"]
    ]

  @impl true
  def run(params, context) do
    fly_token = context[:fly_token] || raise "Missing FLY_API_TOKEN"

    start_args = %{
      prompt: params.prompt,
      model: params.model,
      max_turns: params.max_turns,
      target: :sprite,
      cwd: params.cwd,
      shell: %{
        sprite: %{
          sprite_name: params.sprite_name,
          token: fly_token,
          create: false        # attach to existing Sprite
        },
        cwd: params.cwd
      }
    }

    agent_pid = context[:agent_pid] || self()

    case JidoClaude.Actions.StartSession.run(start_args, %{agent_pid: agent_pid}) do
      {:ok, result, _} ->
        {:ok, %{
          status: result.status,
          result: result[:result] || "Session started. Awaiting result...",
          session_id: result[:session_id]
        }}

      {:error, reason} ->
        {:error, "Claude failed to start: #{inspect(reason)}"}
    end
  end
end

3. Skills — LLM Instructions (Not Code)

Skills are just prompt text that teaches the LLM how to use the tools. No code logic — pure natural language workflow instructions. The agent renders them via Jido.AI.Skill.Prompt.render/1 and they become part of the system prompt.

Skill 1: github-issue-triage

defmodule Jido.IssueTriage.Skills.GithubIssueTriage do
  use Jido.AI.Skill,
    name: "github-issue-triage",
    description: "How to investigate and triage a GitHub issue end-to-end.",
    allowed_tools: ~w(sprite_create sprite_exec sprite_destroy claude_run),
    actions: [
      Jido.IssueTriage.Tools.SpriteCreate,
      Jido.IssueTriage.Tools.SpriteExec,
      Jido.IssueTriage.Tools.SpriteDestroy,
      Jido.IssueTriage.Tools.ClaudeRun
    ],
    body: """
    # GitHub Issue Triage Workflow

    You are triaging a GitHub issue. Your job is to:
    1. Understand what the issue is about
    2. Set up a dev environment (Sprite) with the repo
    3. Run Claude Code to deeply investigate the codebase
    4. Post your findings as a comment on the issue
    5. Clean up the environment

    ## Step-by-step

    ### 1. Parse the issue URL
    Extract the owner, repo, and issue number from the URL.
    Example: `https://github.com/agentjido/jido/issues/42` → owner=agentjido, repo=jido, issue=42

    ### 2. Create a Sprite
    Use `sprite_create` with a descriptive name like `triage-{repo}-{issue_number}`.

    ### 3. Fetch the issue
    Use `sprite_exec` to run:
    ```
    gh issue view {number} --repo {owner}/{repo} --json title,body,labels,author
    ```
    Parse the JSON output to understand the issue.

    ### 4. Clone the repository
    Use `sprite_exec` to run:
    ```
    git clone --depth 1 https://github.com/{owner}/{repo}.git /repo
    ```

    ### 5. Run setup (if needed)
    Based on the project type, run appropriate setup:
    - Elixir: `cd /repo && mix deps.get && mix compile`
    - Node: `cd /repo && npm install`
    - Python: `cd /repo && pip install -r requirements.txt`
    - Rust: `cd /repo && cargo build`

    Look at the repo contents first (`ls /repo`) to determine the project type.

    ### 6. Investigate with Claude Code
    Use `claude_run` with a detailed prompt that includes the issue title and body.
    Ask Claude to:
    - Find relevant code files
    - Analyze the root cause
    - Suggest a fix
    - Check test coverage

    ### 7. Post the results
    Use `sprite_exec` to post a comment:
    ```
    cat > /tmp/comment.md << 'EOF'
    ## 🔍 Automated Issue Investigation

    {investigation results here}

    ---
    _Generated by Jido Issue Triage Bot_
    EOF
    gh issue comment {number} --repo {owner}/{repo} --body-file /tmp/comment.md
    ```

    ### 8. Clean up
    Use `sprite_destroy` to tear down the Sprite.
    Unless the user asked to keep it alive.

    ## Error handling
    - If `gh` commands fail, check that GH_TOKEN is set
    - If `git clone` fails, try without `--depth 1`
    - If setup fails, that's ok — Claude can still investigate the raw source
    - If Claude fails, retry once with a simpler prompt
    - ALWAYS clean up the Sprite, even if earlier steps failed
    """
end

Skill 2: sprite-workflow

defmodule Jido.IssueTriage.Skills.SpriteWorkflow do
  use Jido.AI.Skill,
    name: "sprite-workflow",
    description: "How to work with Fly.io Sprites as ephemeral dev environments.",
    allowed_tools: ~w(sprite_create sprite_exec sprite_destroy),
    actions: [
      Jido.IssueTriage.Tools.SpriteCreate,
      Jido.IssueTriage.Tools.SpriteExec,
      Jido.IssueTriage.Tools.SpriteDestroy
    ],
    body: """
    # Working with Sprites

    Sprites are ephemeral Fly.io VMs. They're lightweight, fast to create,
    and automatically destroyed when you're done.

    ## Lifecycle
    1. `sprite_create` — creates the VM and returns a session_id
    2. `sprite_exec` — runs commands (as many as you want)
    3. `sprite_destroy` — tears it down

    ## Important rules
    - The session_id from `sprite_create` is needed for ALL `sprite_exec` calls
    - The sprite_name from `sprite_create` is needed for `claude_run`
    - Keep both values — you need them throughout the workflow
    - Environment variables (GH_TOKEN, ANTHROPIC_API_KEY) are automatically set
    - The Sprite has internet access — you can clone repos, fetch packages, etc.
    - Commands run as root in a Linux environment
    - Default working directory is `/`
    - Use absolute paths (e.g. `/repo`) to avoid confusion

    ## Common patterns

    ### Clone and explore
    ```
    sprite_exec: git clone --depth 1 https://github.com/owner/repo.git /repo
    sprite_exec: ls -la /repo
    sprite_exec: cat /repo/README.md
    ```

    ### Run project commands
    ```
    sprite_exec: cd /repo && mix deps.get && mix compile
    sprite_exec: cd /repo && mix test --max-failures 3
    ```

    ### Write multi-line files
    ```
    sprite_exec: cat > /tmp/myfile.md << 'EOF'
    content here
    multiple lines
    EOF
    ```

    ## Cleanup
    ALWAYS call `sprite_destroy` when done, even if something failed.
    Sprites cost money while running.
    """
end

Skill 3: claude-in-sprite

defmodule Jido.IssueTriage.Skills.ClaudeInSprite do
  use Jido.AI.Skill,
    name: "claude-in-sprite",
    description: "How to run Claude Code inside a Sprite for deep codebase analysis.",
    allowed_tools: ~w(claude_run sprite_exec),
    actions: [
      Jido.IssueTriage.Tools.ClaudeRun,
      Jido.IssueTriage.Tools.SpriteExec
    ],
    body: """
    # Running Claude Code in a Sprite

    Claude Code is a CLI coding agent that can read files, run commands,
    and reason about code. Running it inside a Sprite gives it access
    to the full cloned repository.

    ## Prerequisites
    Before running `claude_run`, make sure:
    1. The Sprite exists (you called `sprite_create`)
    2. The repo is cloned (you ran `git clone` via `sprite_exec`)
    3. Dependencies are installed (if applicable)

    ## Usage
    Call `claude_run` with:
    - `sprite_name` — the name you gave to `sprite_create` (NOT the session_id!)
    - `prompt` — a detailed investigation prompt
    - `cwd` — where the repo is (default: /repo)
    - `model` — "sonnet" (default) or "opus" for harder problems
    - `max_turns` — how many iterations Claude gets (default: 25)

    ## Writing good investigation prompts
    Be specific about what you want Claude to find:

    ```
    Investigate this GitHub issue:

    ## {issue title}

    {issue body}

    Instructions:
    1. Read the project structure and understand the codebase
    2. Find code files relevant to this issue
    3. Analyze the root cause
    4. Check if there are existing tests covering this area
    5. Write a detailed investigation report in markdown

    Your report must include:
    - Summary (one paragraph)
    - Relevant files with descriptions
    - Root cause analysis
    - Suggested fix (concrete code changes)
    - Test coverage assessment
    - Risk level (Low/Medium/High)
    ```

    ## Tips
    - Use a higher `max_turns` (30-50) for complex issues
    - Use "opus" model for subtle bugs or architectural issues
    - If Claude's output is too brief, run it again with "elaborate further on..."
    - The Sprite filesystem persists — Claude can create files that you read afterward
    """
end

4. Mix Task — Entry Point

defmodule Mix.Tasks.Jido.Triage do
  @shortdoc "Investigate a GitHub issue using an AI agent with Claude Code"

  @moduledoc """
  Triage a GitHub issue by spinning up a Sprite, cloning the repo,
  and running Claude Code to investigate.

  ## Usage

      mix jido.triage https://github.com/owner/repo/issues/42

  ## Options

      --keep-sprite          Don't destroy the Sprite after completion
      --model MODEL          Outer agent model (default: anthropic:claude-sonnet-4-20250514)
      --claude-model MODEL   Inner Claude Code model (default: sonnet)
      --timeout MS           Overall timeout (default: 600000)
      --trace                Show reasoning trace
      --quiet                Suppress progress output

  ## Environment Variables (required)

      GH_TOKEN               GitHub personal access token
      FLY_API_TOKEN          Fly.io API token for Sprites
      ANTHROPIC_API_KEY      Anthropic API key for Claude Code

  ## Examples

      mix jido.triage https://github.com/agentjido/jido/issues/42
      mix jido.triage https://github.com/agentjido/jido/issues/42 --trace
      mix jido.triage https://github.com/agentjido/jido/issues/42 --keep-sprite
  """

  use Mix.Task

  @impl Mix.Task
  def run(argv) do
    Mix.Task.rerun("app.start")
    load_dotenv()

    {opts, args, _} = OptionParser.parse(argv,
      strict: [
        keep_sprite: :boolean,
        model: :string,
        claude_model: :string,
        timeout: :integer,
        trace: :boolean,
        quiet: :boolean
      ]
    )

    url = List.first(args) || fatal("Usage: mix jido.triage <github-issue-url>")

    for var <- ["GH_TOKEN", "FLY_API_TOKEN", "ANTHROPIC_API_KEY"] do
      System.get_env(var) || fatal("Missing required env var: #{var}")
    end

    quiet = opts[:quiet] || false
    timeout = opts[:timeout] || 600_000

    unless quiet do
      IO.puts("🔍 Investigating: #{url}")
      IO.puts("")
    end

    if opts[:trace], do: attach_trace()

    {:ok, _} = Jido.start_link(name: JidoTriage.Jido)
    {:ok, pid} = Jido.start_agent(JidoTriage.Jido, Jido.IssueTriage.Agent)

    keep_flag = if opts[:keep_sprite], do: " Keep the Sprite alive when done.", else: ""
    query = "Investigate this GitHub issue: #{url}#{keep_flag}"

    case Jido.IssueTriage.Agent.ask_sync(pid, query, timeout: timeout) do
      {:ok, answer} ->
        unless quiet, do: IO.puts("\n#{answer}")

      {:error, reason} ->
        IO.puts(:stderr, "❌ Failed: #{inspect(reason)}")
        System.halt(1)
    end

    Jido.stop(JidoTriage.Jido)
  end

  defp fatal(msg) do
    IO.puts(:stderr, "Fatal: #{msg}")
    System.halt(1)
  end

  defp load_dotenv do
    if Code.ensure_loaded?(Dotenvy) do
      env = Path.join(File.cwd!(), ".env")
      if File.exists?(env), do: Dotenvy.source!([env])
    end
  end

  defp attach_trace do
    # Reuse the telemetry trace handler pattern from Mix.Tasks.JidoAi
    events = [
      [:jido, :ai, :tool, :start],
      [:jido, :ai, :tool, :complete],
      [:jido, :ai, :tool, :error],
      [:jido, :ai, :llm, :start],
      [:jido, :ai, :llm, :complete]
    ]

    :telemetry.attach_many("triage-trace", events, fn
      [:jido, :ai, :tool, :start], _m, meta, _ ->
        IO.puts("  🔧 #{meta[:tool_name]}...")

      [:jido, :ai, :tool, :complete], m, meta, _ ->
        IO.puts("  ✅ #{meta[:tool_name]} (#{m[:duration_ms]}ms)")

      [:jido, :ai, :tool, :error], _m, meta, _ ->
        IO.puts("  ❌ #{meta[:tool_name]}: #{inspect(meta[:error])}")

      [:jido, :ai, :llm, :start], _m, meta, _ ->
        IO.puts("  🤔 thinking... (#{meta[:model]})")

      [:jido, :ai, :llm, :complete], m, _meta, _ ->
        IO.puts("  💭 done (#{m[:duration_ms]}ms)")

      _, _, _, _ -> :ok
    end, nil)
  end
end

5. What Actually Happens at Runtime

The user runs:

GH_TOKEN=ghp_xxx FLY_API_TOKEN=fly_xxx ANTHROPIC_API_KEY=sk-ant-xxx \
  mix jido.triage https://github.com/agentjido/jido/issues/42

The ReAct loop iterates something like:

Thought: I need to investigate GitHub issue #42 on agentjido/jido.
         First, I'll create a Sprite environment.

Action:  sprite_create(name: "triage-jido-42")
Result:  {session_id: "sprite-session-a1b2", sprite_name: "triage-jido-42"}

Thought: Good, Sprite is up. Let me fetch the issue details.

Action:  sprite_exec(session_id: "...", command: "gh issue view 42 --repo agentjido/jido --json title,body,labels")
Result:  {"title": "Bug: Widget.call/1 crashes on nil", "body": "When I pass nil...", ...}

Thought: This is a bug in Widget.call/1. Let me clone the repo.

Action:  sprite_exec(session_id: "...", command: "git clone --depth 1 https://github.com/agentjido/jido.git /repo")
Result:  "Cloning into '/repo'..."

Thought: It's an Elixir project. Let me set it up.

Action:  sprite_exec(session_id: "...", command: "cd /repo && mix deps.get && mix compile")
Result:  "Resolving Hex dependencies..."

Thought: Environment is ready. Let me run Claude Code to investigate.

Action:  claude_run(sprite_name: "triage-jido-42", prompt: "Investigate this issue: Bug: Widget.call/1 crashes on nil...")
Result:  {result: "## Investigation Report\n\nThe crash occurs in lib/jido/widget.ex line 47..."}

Thought: Good investigation. Let me post this as a comment on the issue.

Action:  sprite_exec(session_id: "...", command: "cat > /tmp/comment.md << 'EOF'\n## 🔍 Automated Investigation\n...\nEOF")
Result:  ""

Action:  sprite_exec(session_id: "...", command: "gh issue comment 42 --repo agentjido/jido --body-file /tmp/comment.md")
Result:  "https://github.com/agentjido/jido/issues/42#issuecomment-123456"

Thought: Comment posted. Time to clean up.

Action:  sprite_destroy(session_id: "sprite-session-a1b2")
Result:  "Sprite destroyed."

Answer:  I investigated issue #42 and posted my findings as a comment.
         The bug is in lib/jido/widget.ex — Widget.call/1 doesn't handle
         nil input. See: https://github.com/...#issuecomment-123456

The agent might also:

  • Try ls /repo first to detect the project type
  • Run mix test to see if existing tests pass
  • Retry a failed git clone without --depth 1
  • Skip setup if it fails and let Claude work with raw source
  • Read Claude's output and decide to ask Claude to elaborate further
  • Handle errors and adapt — just like a human would

6. File Structure

jido_issue_bot/
├── lib/
│   ├── jido/issue_triage/
│   │   ├── agent.ex                    # The ReAct agent (one module)
│   │   ├── tools/
│   │   │   ├── sprite_create.ex        # Jido.Action — create Sprite
│   │   │   ├── sprite_exec.ex          # Jido.Action — run command
│   │   │   ├── sprite_destroy.ex       # Jido.Action — tear down
│   │   │   └── claude_run.ex           # Jido.Action — run Claude Code
│   │   └── skills/
│   │       ├── github_issue_triage.ex  # Skill — triage workflow
│   │       ├── sprite_workflow.ex      # Skill — Sprite usage
│   │       └── claude_in_sprite.ex     # Skill — Claude in Sprite
│   └── mix/tasks/
│       └── jido.triage.ex              # Mix task entry point
├── test/
│   ├── jido/issue_triage/
│   │   ├── tools/
│   │   │   ├── sprite_create_test.exs
│   │   │   ├── sprite_exec_test.exs
│   │   │   ├── sprite_destroy_test.exs
│   │   │   └── claude_run_test.exs
│   │   └── agent_test.exs
│   └── integration/
│       └── triage_integration_test.exs   # @tag :integration
├── mix.exs
└── .env.example

7. Dependencies

defp deps do
  [
    {:jido, "~> 1.0"},
    {:jido_ai, path: "../jido_ai"},
    {:jido_shell, path: "../jido_shell"},
    {:jido_claude, path: "../jido_claude"},
    {:jason, "~> 1.4"},
    {:dotenvy, "~> 0.8", only: [:dev, :test]}
  ]
end

No jido_runic. No zoi (no custom schemas — the agent state is just the ReAct machine). No tentacat. No splode (tool errors are just strings the LLM reads).


8. Tradeoffs — Why This Works (and Where It Breaks)

Why this is better than the structured approach

Advantage Detail
Adaptable LLM can detect project type, skip failing steps, retry differently
Minimal code 4 tool modules, 3 skill modules, 1 agent, 1 mix task
Easy to extend Add a new tool, mention it in a skill, done
Self-healing If mix deps.get fails, the LLM can try mix deps.get --force or skip
No schema coupling No Zoi schemas threading data between nodes
Composable Same tools work for any task — not just issue triage

Where this breaks down

Risk Mitigation
LLM forgets to clean up Sprite Skill instructions say "ALWAYS clean up". Add a Sprite TTL as safety net.
LLM loops or gets stuck max_iterations: 30 caps the loop. tool_timeout_ms: 300_000 caps each tool.
Non-deterministic Same issue may produce different investigation quality. Skills reduce variance.
Token cost Each iteration costs LLM tokens. Long tool outputs eat context. truncate/2 helps.
Observability Less structured than Runic — use --trace flag. No node-level introspection.
Error attribution If it fails, you get an LLM reasoning trace, not a specific node failure.

When to move to the structured approach

  • You're running this in production at scale (>50 issues/day)
  • You need guaranteed cleanup (billing concern)
  • You need audit trails with per-step status
  • You need deterministic retry semantics
  • You want to swap Claude for another provider without changing the prompt

9. Implementation Roadmap

Phase 1: Tools (~half day)

  1. SpriteCreate, SpriteExec, SpriteDestroy — straightforward wrappers around Jido.Shell.Agent
  2. ClaudeRun — wrapper around JidoClaude.Actions.StartSession with create: false
  3. Unit tests with mocked shell backend

Phase 2: Skills + Agent (~half day)

  1. Write the three skill modules (pure text, no logic)
  2. Wire up the agent with tools + skills
  3. on_before_cmd for secret injection

Phase 3: Mix Task + Integration (~half day)

  1. Mix task with option parsing
  2. Telemetry trace handler
  3. Integration test with real Sprite + GitHub (@tag :integration)

Total: ~1.5 days to a working bot.


10. Open Questions

  1. claude_run result collectionJidoClaude.Actions.StartSession is async. Need to either: (a) build a synchronous wrapper that blocks until Claude's result message arrives, or (b) have sprite_exec read a file that Claude wrote. Option (b) is simpler — tell Claude to write its report to /tmp/investigation.md, then sprite_exec: cat /tmp/investigation.md.

  2. Sprite base image — Needs gh, git, claude CLI, and common toolchains. Does this image exist?

  3. Output length — Claude's investigation might be very long. The truncation in sprite_exec helps, but claude_run output also needs truncation or the LLM context will overflow.

  4. Nested agents — This is an LLM agent calling Claude Code (another LLM agent). Token cost will be significant. Consider using a cheaper model for the outer agent (e.g., Haiku) since it's mostly orchestrating, not reasoning deeply.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment