> ## Documentation Index
> Fetch the complete documentation index at: https://lab.pollack.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Extending Loopy

> Custom skills, subagents, tool profiles, listeners, and the programmatic API

Loopy has five extension points, from zero-code Markdown files to full Java SPIs.

<CardGroup cols={4}>
  <Card title="Skills" icon="book">
    Domain knowledge the agent reads on demand
  </Card>

  <Card title="Slash Commands" icon="terminal">
    Markdown-defined commands from files
  </Card>

  <Card title="Subagents" icon="users">
    Specialist agents for delegation
  </Card>

  <Card title="Tool Profiles" icon="wrench">
    New tools via Java SPI
  </Card>
</CardGroup>

***

## Skills

Skills are the fastest way to make Loopy smarter. A skill is a Markdown file with YAML frontmatter. The agent sees skill names and descriptions upfront but only loads full content when relevant — no tokens wasted.

### Create a skill

Place a `SKILL.md` in your project:

```
.claude/skills/my-conventions/SKILL.md
```

```markdown theme={null}
---
name: my-conventions
description: Team coding conventions for our Spring Boot services
---

# Instructions

When working on this codebase:
- Use constructor injection, never field injection
- All REST endpoints return ProblemDetail for errors (RFC 9457)
- Tests use @WebMvcTest with MockMvc, not @SpringBootTest
- Entity IDs are UUIDs, never auto-increment
```

Next time you run Loopy in that directory, the agent discovers the skill automatically.

### Where skills live

| Location  | Path                                 | Use case                                |
| --------- | ------------------------------------ | --------------------------------------- |
| Project   | `.claude/skills/*/SKILL.md`          | Team conventions, checked into the repo |
| Global    | `~/.claude/skills/*/SKILL.md`        | Personal skills across all projects     |
| Classpath | `META-INF/skills/*/SKILL.md` in JARs | Published skill packages (Maven dep)    |

### Install from the catalog

Loopy ships with 23+ curated skills from 8 publishers:

```bash theme={null}
/skills search testing
/skills info systematic-debugging
/skills add systematic-debugging
```

### Publish skills as a JAR (SkillsJars)

Package skills as a Maven dependency so teams get them automatically:

```
my-skills.jar
└── META-INF/skills/
    └── my-org/my-repo/api-design/
        └── SKILL.md
```

Add the JAR to `pom.xml` and Loopy discovers it on the classpath. Skills follow the [agentskills.io](https://agentskills.io) spec — they work in 40+ agentic CLIs, not just Loopy.

***

## Slash Commands

Slash commands let you define reusable prompts as Markdown files, following Claude Code's `~/.claude/commands/` convention. Type `/command-name` in the chat and the agent executes the expanded prompt.

### Create a slash command

Create a `.md` file in your project or home directory:

```
.claude/commands/review.md
```

```markdown theme={null}
---
name: review
description: "Code review the current diff"
---

Review the staged changes (`git diff --cached`). For each file:

1. Check for bugs, security issues, and performance problems
2. Verify test coverage for new code paths
3. Flag any style violations

Summarize findings as a table. $ARGUMENTS
```

### How it works

1. **YAML front matter** — `name` and `description` fields. If no front matter, the filename becomes the name.
2. **`$ARGUMENTS` substitution** — replaced with whatever the user types after the command. `/review focus on error handling` substitutes "focus on error handling".
3. **Agent delegation** — the expanded prompt is sent to the agent as if the user typed it.

### Where commands are discovered

| Location | Path                      |
| -------- | ------------------------- |
| Project  | `.claude/commands/*.md`   |
| Global   | `~/.claude/commands/*.md` |

Markdown commands override built-in Java commands with the same name.

***

## Subagents

Subagents are specialist agents the main agent delegates to via the `Task` tool. Define them as Markdown files — no Java required.

### Create a subagent

Create `.claude/agents/test-runner.md`:

```markdown theme={null}
---
name: test-runner
description: Runs tests and reports pass/fail summary
tools: Bash, Read
---

You are a testing specialist. When invoked:

1. Run `./mvnw test` in the working directory
2. Parse output for pass/fail/skip counts
3. If tests fail, read the relevant test source files
4. Report a concise summary: what passed, what failed, and why
```

The main agent delegates testing tasks to this subagent automatically based on the `description` field.

### Frontmatter fields

| Field         | Required | Description                                              |
| ------------- | -------- | -------------------------------------------------------- |
| `name`        | Yes      | Unique identifier (lowercase, hyphens)                   |
| `description` | Yes      | When to use — the main agent reads this to decide        |
| `tools`       | No       | Allowed tools (comma-separated). Inherits all if omitted |
| `model`       | No       | `haiku`, `sonnet`, or `opus`                             |

### Tips

* Keep `description` specific — "Runs tests and reports results" routes better than "helps with testing"
* Restrict `tools` to what the subagent needs. A test runner doesn't need `Edit`
* Subagents run in isolated context windows — they can't see the main conversation
* Subagents cannot spawn other subagents (the `Task` tool is excluded automatically)

***

## Tool Profiles (Java SPI)

Add new tools the agent can call. Implement a Java interface, package as a JAR, and Loopy discovers it at startup via `ServiceLoader`.

### Implement ToolProfileContributor

```java theme={null}
public class DatabaseToolProfile implements ToolProfileContributor {

    @Override
    public String profileName() {
        return "database-tools";
    }

    @Override
    public List<ToolCallback> tools(ToolFactoryContext ctx) {
        return List.of(
            ToolCallbacks.from(new QueryTool(ctx.workingDirectory())),
            ToolCallbacks.from(new SchemaTool(ctx.workingDirectory()))
        );
    }
}
```

### Register via ServiceLoader

Create `META-INF/services/io.github.markpollack.loopy.tools.ToolProfileContributor`:

```
com.example.tools.DatabaseToolProfile
```

### What ToolFactoryContext provides

| Field              | Type        | Description                             |
| ------------------ | ----------- | --------------------------------------- |
| `workingDirectory` | `Path`      | Agent's working directory               |
| `chatModel`        | `ChatModel` | The active LLM (for tools that need AI) |
| `commandTimeout`   | `Duration`  | Tool execution timeout (default 120s)   |
| `interactive`      | `boolean`   | True in TUI mode, false in print/REPL   |

### Built-in profiles

| Profile    | Description                                                          |
| ---------- | -------------------------------------------------------------------- |
| `dev`      | Full interactive toolset (bash, file I/O, search, skills, subagents) |
| `boot`     | Spring Boot scaffolding tools                                        |
| `headless` | Same as `dev` minus `AskUserQuestion` (for CI/CD)                    |
| `readonly` | Read-only: file read, grep, glob, list directory                     |

Custom profiles load **alongside** built-in profiles, not replacing them.

***

## Listeners

Observe what the agent does without changing its behavior.

### ToolCallListener

Fires around every tool execution:

```java theme={null}
public class CostTracker implements ToolCallListener {

    @Override
    public void onToolExecutionCompleted(String runId, int turn,
            AssistantMessage.ToolCall toolCall, String result,
            Duration duration) {
        log.info("Tool {} took {}ms", toolCall.name(), duration.toMillis());
    }
}
```

### AgentLoopListener

Fires at loop lifecycle boundaries:

```java theme={null}
public class ProgressReporter implements AgentLoopListener {

    @Override
    public void onLoopCompleted(String runId, LoopState state,
            TerminationReason reason) {
        System.err.printf("Done: %s (%d turns)%n", reason, state.turns());
    }
}
```

Wire listeners through the `MiniAgent` builder:

```java theme={null}
var agent = MiniAgent.builder()
    .config(config)
    .model(chatModel)
    .toolCallListener(new CostTracker())
    .loopListener(new ProgressReporter())
    .build();
```

<Note>
  Listener methods should not throw exceptions — exceptions are logged and swallowed to avoid crashing the agent loop.
</Note>

***

## Programmatic API

Embed Loopy's agent in other Java applications:

```java theme={null}
LoopyAgent agent = LoopyAgent.builder()
    .workingDirectory(Path.of("/path/to/project"))
    .build();

LoopyResult result = agent.run("add input validation to UserController");
```

### Multi-step with session memory

Context is preserved across `run()` calls by default:

```java theme={null}
LoopyAgent agent = LoopyAgent.builder()
    .workingDirectory(workspace)
    .maxTurns(80)
    .build();

agent.run("plan a refactoring of the service layer");
agent.run("now execute the plan");  // sees the previous conversation
```

### Builder options

| Method                        | Description                           | Default                |
| ----------------------------- | ------------------------------------- | ---------------------- |
| `.model(String)`              | Model ID                              | `claude-sonnet-4-6`    |
| `.workingDirectory(Path)`     | Agent's working directory             | *required*             |
| `.systemPrompt(String)`       | Custom system prompt                  | Built-in coding prompt |
| `.maxTurns(int)`              | Max loop iterations                   | `80`                   |
| `.costLimit(double)`          | Max cost in dollars                   | `$5.00`                |
| `.sessionMemory(boolean)`     | Preserve context across `run()` calls | `true`                 |
| `.timeout(Duration)`          | Overall loop timeout                  | `10 min`               |
| `.disabledTools(Set<String>)` | Tools to exclude                      | none                   |

### Custom endpoints (vLLM, LM Studio)

```java theme={null}
LoopyAgent agent = LoopyAgent.builder()
    .baseUrl("http://localhost:1234/v1")
    .apiKey("lm-studio")
    .model("local-model")
    .workingDirectory(workspace)
    .build();
```
