Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

346 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

agnz

A Claude Code plugin that exposes a locally-hosted LLM (LM Studio, Ollama, any OpenAI-compatible endpoint) as a sandboxed sub-agent.

Parent Claude drives it through the agnz CLI (from Bash). The sub-agent does the heavy file work — reading, grepping, mechanical edits — and Parent Claude only sees the distilled outcome. Same value model as the built-in Agent tool, but the model is one you control and host.

Why

  • Save tokens. Use a free local model for grunt work instead of burning Anthropic credits on dozens of intermediate file reads.
  • Keep Parent Claude's context small. Only the sub-agent's final answer ever lands in the parent transcript.
  • Stay in control. The sub-agent runs inside a sandbox: locked to a single working directory, tiered permissions defined by the agent definition.
  • Real concurrency, no daemon. Each run is its own short-lived OS process (a detached runner) — nothing stays resident between runs, and multiple agents run in genuine parallel.

The CLI

There is no MCP server. The parent calls the CLI via Bash; every verb prints a JSON object/array to stdout so the outcome is parseable. When the plugin is enabled, Claude Code puts its bin/ on the parent shell's PATH, so the parent runs agnz <verb> from any working directory — no $CLAUDE_PLUGIN_ROOT needed. (Developing in this repo, node bin/agnz.mjs <verb> works too.)

Verb Purpose
start <name> ["task"] --agent <def> Create a thread (--inline "<frontmatter>" for an ad-hoc role). Without a task it starts idle.
send <name|id> "msg" Send a task. Reuses the existing live thread of that name (resume), else needs an id.
wait <id|name> [--timeout <s>] Watch a detached run until it settles and print the outcome (default 300 s). A poller, not a run mode — collects even a crashed thread by name.
approve <id|name> allow|deny [--persist] Resolve an approval pause (no tool_call_id needed — the thread's pending call is used).
answer <id|name> "text" Resolve an AskUser question pause.
interrupt <id|name> ["directive"] Hard-interrupt a runaway/working agent: aborts the current step, leaves it resumable, optionally queues a directive.
stop <id|name> End + archive a thread: kills a live runner, hides it from the list, keeps the transcript. Resume later with send.
remove <id|name> / remove --status stopped|error Delete a thread permanently — sweeps every threads/<id>.* file. Live threads must be stopped first.
list [--status <s>] List threads in this workspace (--cwd for another project).
show <id|name> Lean structural view: status, pending approval/question, spend, and folded trace stats — no raw transcript.

Every thread-addressing verb accepts a name or an id; a name resolves to its most recent live thread.

Runs are always detached: the CLI returns at once and the runner works in its own process. The result reaches the parent via the message hook — the runner appends to messages.jsonl, and the UserPromptSubmit hook injects unread parent mail plus a live status block for each active thread into your next prompt automatically (an OS notification fires for urgent mail), so you stay informed without polling. To collect a specific run sooner, agnz wait <id>; to inspect one, agnz show <id> (without a target, show lists all threads). agnz also protects your own Claude session's context: the sub-agents' raw transcripts stay out of it — show returns a compact structural view, and a hook keeps the lead from directly reading thread transcripts and traces.

Architecture at a glance

Claude Code (Parent)
    │  Bash
    ▼
bin/agnz.mjs            ← CLI: start/send/wait/approve/answer/stop/remove/interrupt/list/show
    │  spawns a detached runner per active run
    ▼
lib/runner.mjs → lib/loop.mjs   ← LLM ↔ tool loop, persists transcript, then exits
    │
    ├──▶ tools/         (Read, Edit, Write, Grep, LS, Bash, AskUser, SendMessage, Skill)
    ├──▶ sandbox.mjs    (cwd lock + tiered permission policy)
    ├──▶ agent-defs.mjs (named roles from .claude/agents/ and plugin agents/)
    ├──▶ workspace-store (<cwd>/.claude/agnz/ — threads, workspace.json)
    ├──▶ proc-lock.mjs  (cross-process mkdir locks on shared state files)
    ├──▶ config.mjs     (two-layer config: user defaults + project overrides)
    └──▶ llm/openai-compatible.mjs  (native fetch, no SDK)

Results flow back independently of the CLI process via messages.jsonl + the UserPromptSubmit hook. For the deep dive — module map, agent loop, sandbox semantics — see CLAUDE.md and ADR 0014.

What the agent sees

Each turn the agent receives a system prompt composed of:

  1. Sandbox framing — cwd, tool workflow rules (Grep before Read, Read before Write — ADR 0013), messaging instructions
  2. Project conventions<cwd>/CLAUDE.md at startup (or AGENTS.md if no CLAUDE.md exists — first hit wins, so symlinked pairs don't duplicate); subdirectory files are added as the agent accesses those directories
  3. Tool restrictions — which tools are allowed/denied per the agent def
  4. Skills catalog — names + descriptions of available skills; agent loads full content on demand via Skill({action:"load", name:"..."})
  5. Agent body — the role definition from the agent def frontmatter

The loop also carries harness-level robustness for small local models: malformed JSON tool arguments are repaired, and a tool call that leaks out as plain text in the model's native wire syntax (e.g. Mistral's Name[ARGS]{…} when the server's parser misses it) is deterministically recovered and executed instead of silently ending the run. Both paths are visible as repair events in the trace. LLM requests have no default timeout — local cold-loads and big-context turns are legitimately slow; set llmTimeoutMs per profile if you want a deadline, and use interrupt/stop for a hung run.

The sub-agent's own context is managed cache-consciously: the system prompt is a frozen prefix and the transcript is append-only, so the inference server's prefix cache stays warm turn over turn. The harness also skips re-sending what the agent already has (a full re-read of an unchanged file is answered with a one-liner) and blocks stale overwrites (a Write to a file that changed on disk since the agent's last read must re-read first). For long sessions, declare your model's contextWindow in the profile: near the limit (default 90%) the agent summarizes its own session once and continues with a fresh, small context — the full history stays on disk.

Install

This repo is a plain Claude Code plugin. The canonical marketplace is Superheld/claude-bauchladen:

/plugin marketplace add Superheld/claude-bauchladen
/plugin install agnz@claude-bauchladen
/reload-plugins

Installing wires the hooks (result delivery, spend summary, and the context guardrails that keep sub-agent transcripts out of your session), the bundled agents, and the skills. The parent invokes the CLI via Bash; verify with:

agnz show

After code changes, update in place: /plugin marketplace update agnz && /plugin install agnz@agnz && /reload-plugins.

Upgrading to 0.18+ from an older install: the config layout changed (breaking, ADR 0017). There is no migration — re-run /agnz:setup add once; the old ~/.claude/agnz/profiles.json, thread-index.json, and per-project cursors/ directories are ignored and can be deleted. Threads and transcripts are untouched.

Configuration (ADR 0017)

One schema, two layers, project wins per entry:

~/.claude/agnz/config.json        ← machine defaults: {profiles, mappings}
<cwd>/.claude/agnz/config.json    ← optional project overrides — committable

Run /agnz:setup add for interactive setup, or pass the fields directly. LM Studio (default endpoint http://localhost:1234/v1) and Ollama examples:

agnz config add \
  lmstudio http://localhost:1234/v1 mistralai/devstral-small-2-2512
agnz config add \
  ollama http://192.168.1.10:11434/v1 devstral-2:96k

The first profile added becomes the _default mapping automatically. Write commands take --project to target the project override file. Profile resolution at thread start: merged mappings[agentDef.model]mappings._default → the identifier as a profile name. /agnz:setup info prints the effective merged config with each value's origin.

Agent definitions

Any Claude Code agent .md file works as an agnz agent definition. Pass --agent <name> to agnz start; the def is resolved from <cwd>/.claude/agents/, ~/.claude/agents/, then the plugin-bundled agents/ directory (first match wins).

Plugin-bundled defaults: dev, researcher, reviewer, general.

The tools: frontmatter field lists tools that run without approval. Anything not listed requires agnz approve. disallowedTools: blocks tools entirely.

Data layout

Two independent roots:

User-wide — the config's default layer. Default root: ~/.claude/agnz/. Override with $AGNZ_DATA_DIR.

~/.claude/agnz/
└── config.json              ← user-layer config: {profiles, mappings}

Per-project — one workspace per cwd, co-located with other Claude Code state:

<cwd>/.claude/
├── agents/
│   └── <name>.md            ← agent definitions (shared with CC)
├── agnz/
│   ├── config.json          ← optional project config overrides (committable)
│   ├── workspace.json       ← pure workspace state (parent delivery cursor etc.)
│   ├── messages.jsonl       ← event bus for inter-agent communication
│   └── threads/
│       ├── <thread-id>.meta.json      ← status, pending, session state
│       ├── <thread-id>.jsonl          ← append-only transcript
│       ├── <thread-id>.trace.jsonl    ← runtime trace (observability)
│       └── <thread-id>.system.txt     ← frozen system-prompt prefix (write-once)
└── skills/
    └── <name>/
        └── SKILL.md         ← project-local skills

Bundled skills

Skill Slash command Purpose
agnz-setup /agnz:setup Manage LLM profiles (add, remove, use, test, mappings)
agnz Progressive-disclosure reference for agent definitions and the CLI lifecycle

Observability & evaluation

Every thread writes an append-only runtime trace next to its transcript (<thread-id>.trace.jsonl): per-turn LLM latency + token usage, tool outcomes, JSON-repair events, and a terminal thread_end. From it you get:

  • Statsagnz show <id> folds one thread's trace into turns, tokens, latency, tool-error and repair rates. Workspace-wide rollups (per-model comparisons) are deliberately not a CLI concern: the trace files are plain JSONL, built to be read by external tooling (lib/trace-stats.mjs remains available as an importable aggregator).
  • Live spend — the SessionStart/UserPromptSubmit hooks inject a per-thread status line (dev:1a2b3c4d — running · 5 turns · ctx ~2k, where ctx is the resume weight a send re-sends) into Claude's context, so the parent sees what's running without reading files.
  • Evalsnode evals/run.mjs runs fixtures against one or more profiles in throwaway workspaces and scores outcome + trace metrics, ranking profiles by pass rate then token cost. The answer to "which local model for which role?".

Tests: node --test tests/*.test.mjs (the loop runs against an injectable fake LLM, no model needed). Full guide: docs/observability.md.

Conventions

  • Native Node only. No npm dependencies.
  • Comments explain why, not what.
  • JSONL for streams, JSON for snapshots. Transcripts are append-only; thread meta is rewritten in place.
  • Two data roots, two lifetimes. User-wide under ~/.claude/agnz/ for cross-project state. Per-project under <cwd>/.claude/agnz/ for workspace state.

ADRs

Design decisions are captured as ADRs under docs/adr/ — authoritative descriptions of how the system works right now. The CLI architecture is ADR 0014; observability/evals, context management, and tool-workflow discipline are ADRs 0011–0013.

License

MIT

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages