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. │
└──────────────────────────────────────────────────────┘
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}
endThat's the entire agent. The LLM reads the skills, uses the tools, and figures out the workflow dynamically.
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.
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
endRuns 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
endTears 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
endRuns 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
endSkills 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.
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
"""
enddefmodule 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.
"""
enddefmodule 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
"""
enddefmodule 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
endThe 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/42The 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 /repofirst to detect the project type - Run
mix testto see if existing tests pass - Retry a failed
git clonewithout--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
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
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]}
]
endNo 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).
| 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 |
| 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. |
- 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
SpriteCreate,SpriteExec,SpriteDestroy— straightforward wrappers aroundJido.Shell.AgentClaudeRun— wrapper aroundJidoClaude.Actions.StartSessionwithcreate: false- Unit tests with mocked shell backend
- Write the three skill modules (pure text, no logic)
- Wire up the agent with tools + skills
on_before_cmdfor secret injection
- Mix task with option parsing
- Telemetry trace handler
- Integration test with real Sprite + GitHub (
@tag :integration)
Total: ~1.5 days to a working bot.
-
claude_runresult collection —JidoClaude.Actions.StartSessionis async. Need to either: (a) build a synchronous wrapper that blocks until Claude's result message arrives, or (b) havesprite_execread a file that Claude wrote. Option (b) is simpler — tell Claude to write its report to/tmp/investigation.md, thensprite_exec: cat /tmp/investigation.md. -
Sprite base image — Needs
gh,git,claudeCLI, and common toolchains. Does this image exist? -
Output length — Claude's investigation might be very long. The truncation in
sprite_exechelps, butclaude_runoutput also needs truncation or the LLM context will overflow. -
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.