agent-dispatch 0.6.0__tar.gz → 0.9.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. agent_dispatch-0.9.0/AGENTS.md +58 -0
  2. {agent_dispatch-0.6.0 → agent_dispatch-0.9.0}/CHANGELOG.md +91 -0
  3. {agent_dispatch-0.6.0 → agent_dispatch-0.9.0}/PKG-INFO +145 -27
  4. {agent_dispatch-0.6.0 → agent_dispatch-0.9.0}/README.md +143 -25
  5. {agent_dispatch-0.6.0 → agent_dispatch-0.9.0}/agents.example.yaml +33 -1
  6. {agent_dispatch-0.6.0 → agent_dispatch-0.9.0}/pyproject.toml +15 -2
  7. {agent_dispatch-0.6.0 → agent_dispatch-0.9.0}/src/agent_dispatch/__init__.py +1 -1
  8. {agent_dispatch-0.6.0 → agent_dispatch-0.9.0}/src/agent_dispatch/cli.py +427 -41
  9. {agent_dispatch-0.6.0 → agent_dispatch-0.9.0}/src/agent_dispatch/config.py +17 -3
  10. {agent_dispatch-0.6.0 → agent_dispatch-0.9.0}/src/agent_dispatch/jobs.py +50 -16
  11. {agent_dispatch-0.6.0 → agent_dispatch-0.9.0}/src/agent_dispatch/models.py +70 -5
  12. {agent_dispatch-0.6.0 → agent_dispatch-0.9.0}/src/agent_dispatch/runner.py +169 -74
  13. {agent_dispatch-0.6.0 → agent_dispatch-0.9.0}/src/agent_dispatch/server.py +366 -92
  14. {agent_dispatch-0.6.0 → agent_dispatch-0.9.0}/tests/test_cli.py +540 -84
  15. {agent_dispatch-0.6.0 → agent_dispatch-0.9.0}/tests/test_config.py +85 -1
  16. {agent_dispatch-0.6.0 → agent_dispatch-0.9.0}/tests/test_jobs.py +81 -0
  17. {agent_dispatch-0.6.0 → agent_dispatch-0.9.0}/tests/test_models.py +77 -1
  18. {agent_dispatch-0.6.0 → agent_dispatch-0.9.0}/tests/test_runner.py +160 -0
  19. {agent_dispatch-0.6.0 → agent_dispatch-0.9.0}/tests/test_server.py +665 -144
  20. {agent_dispatch-0.6.0 → agent_dispatch-0.9.0}/.github/dependabot.yml +0 -0
  21. {agent_dispatch-0.6.0 → agent_dispatch-0.9.0}/.github/workflows/ci.yml +0 -0
  22. {agent_dispatch-0.6.0 → agent_dispatch-0.9.0}/.github/workflows/publish.yml +0 -0
  23. {agent_dispatch-0.6.0 → agent_dispatch-0.9.0}/.gitignore +0 -0
  24. {agent_dispatch-0.6.0 → agent_dispatch-0.9.0}/LICENSE +0 -0
  25. {agent_dispatch-0.6.0 → agent_dispatch-0.9.0}/SECURITY.md +0 -0
  26. {agent_dispatch-0.6.0 → agent_dispatch-0.9.0}/assets/mascot.png +0 -0
  27. {agent_dispatch-0.6.0 → agent_dispatch-0.9.0}/src/agent_dispatch/cache.py +0 -0
  28. {agent_dispatch-0.6.0 → agent_dispatch-0.9.0}/tests/__init__.py +0 -0
  29. {agent_dispatch-0.6.0 → agent_dispatch-0.9.0}/tests/conftest.py +0 -0
  30. {agent_dispatch-0.6.0 → agent_dispatch-0.9.0}/tests/test_cache.py +0 -0
@@ -0,0 +1,58 @@
1
+ # AGENTS.md
2
+
3
+ Guidance for AI coding agents working on this repository.
4
+
5
+ > **Using agent-dispatch** (not developing it)? Read [README.md](README.md) — it has the full setup path with verify steps and the complete MCP tool reference. This file is for contributing to the codebase.
6
+
7
+ ## What this project is
8
+
9
+ MCP server + CLI that lets Claude Code agents delegate tasks to agents in other project directories. One sync core, two surfaces:
10
+
11
+ | File | Role |
12
+ |------|------|
13
+ | `src/agent_dispatch/runner.py` | Sync subprocess wrapper around `claude -p` — the actual work |
14
+ | `src/agent_dispatch/server.py` | Async FastMCP interface (21 MCP tools), wraps runner in `asyncio.to_thread` + semaphore |
15
+ | `src/agent_dispatch/cli.py` | Click CLI: `init`, `add`, `update`, `remove`, `list`, `describe`, `test`, `doctor`, `jobs`, `job`, `cancel`, `gc`, `group` (add/list/inspect/update/remove), `serve` |
16
+ | `src/agent_dispatch/models.py` | Pydantic v2 models (`AgentConfig`, `DispatchGroup`/`GroupMember`, `Settings`, `DispatchResult`) |
17
+ | `src/agent_dispatch/config.py` | YAML config load/save + project auto-description |
18
+ | `src/agent_dispatch/cache.py` | Thread-safe in-memory TTL cache |
19
+ | `src/agent_dispatch/jobs.py` | Persistent per-job JSON files for async dispatch |
20
+
21
+ ## Dev setup
22
+
23
+ ```bash
24
+ pip install -e ".[dev]"
25
+ ```
26
+
27
+ ## Gates — both must pass before a change is done (CI rejects otherwise)
28
+
29
+ ```bash
30
+ ruff check src/ tests/
31
+ python3 -m pytest tests/ -v # 466 tests, ~2s — all subprocess calls are mocked
32
+ ```
33
+
34
+ Tests must **never** invoke the real `claude` CLI. Runner tests mock `shutil.which` + `subprocess.run`/`Popen`; server tests mock `_get_config` + `runner.dispatch`.
35
+
36
+ ## Non-obvious invariants (violating these breaks real behavior)
37
+
38
+ - `allowed_tools` / `disallowed_tools` are **tri-state**: `None` = inherit settings defaults, `[]` = explicitly no tools, `[...]` = exactly these. Check with `is not None`, never `or` — `[]` is falsy but semantically distinct.
39
+ - `denied_tools` non-empty + `is_error` ⇒ `error_type="permission"`, regardless of what the error text matches.
40
+ - **Groups**: a group's `shared_context` is folded into the `context` *string* before the cache/runner calls (`_merge_group_context` in server.py) — runner.py and cache.py are untouched, the cache key disambiguates groups for free, and `group=""` is byte-identical to a plain dispatch. Membership is validated up front (`_validate_group_member`, separate from the pure merge so `dispatch_parallel`'s all-or-nothing pre-check holds). `DispatchConfig` validates only group *keys*, never member existence — a hard cross-ref check would brick config load when a shared gateway agent is removed; dangling refs are flagged (`unknown:true`) at read time instead.
41
+ - On failure, callers read `DispatchResult.error` + `error_type` — `result` holds the raw agent output even on errors.
42
+ - `--session-id` and `--resume` conflict — never pass both to `claude`.
43
+ - Valid permission modes: `default`, `plan`, `bypassPermissions` (`models.py: KNOWN_PERMISSION_MODES`).
44
+ - `JobStore.finish`/`fail` refuse already-terminal jobs (returns `None`) — this closes the race with force-cancel; never "fix" it by overwriting.
45
+ - Cancelling a *running* job requires the in-memory `_running_procs` registry (server.py) — the job is marked `cancelled` **before** the subprocess is killed. Don't persist PIDs to disk (PID reuse after restart could kill an unrelated process).
46
+ - `max_budget_usd` is **post-hoc**: `_apply_budget` (runner.py) sets `budget_exceeded` + `hint` after the cost is known; it never fails the dispatch.
47
+
48
+ ## Conventions
49
+
50
+ Python ≥ 3.10 · `from __future__ import annotations` everywhere · Pydantic v2 · Click (CLI) + FastMCP (server) · ruff, line length 100 · all MCP tools return JSON strings, errors as `{"error": "..."}`.
51
+
52
+ ## When adding a feature, check every layer
53
+
54
+ `models.py` (data shape) → `runner.py` (dispatch mechanics) → `server.py` (MCP tool) → `cli.py` (CLI flag) → tests for each → `README.md` + `agents.example.yaml` (user docs).
55
+
56
+ ## More detail
57
+
58
+ [README.md](README.md) documents every MCP tool with parameter tables, response shapes, and the error-recovery map — it doubles as the behavioral spec. The test suite (`tests/`, 466 tests) encodes the exact expected behavior of every layer: when in doubt, read the tests for the module you're touching (`test_runner.py`, `test_server.py`, `test_cli.py`, ...).
@@ -7,6 +7,97 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.9.0] - 2026-06-30
11
+
12
+ Coordinate a group of related projects from one session.
13
+
14
+ ### Added
15
+ - **Project groups.** A new `groups` mapping in the config bundles related
16
+ agents — code repos plus capability gateways like infra (Portainer) or
17
+ analytics (browser / Yandex Metrica) — into a cross-project working set.
18
+ Each group has an orchestrator-facing `description` (how to coordinate, never
19
+ sent to members) and a member-facing `shared_context` of facts (stack names,
20
+ ids, conventions). Members reference agents by name; membership is
21
+ many-to-many (a shared gateway can belong to several groups). A group is a
22
+ *descriptive layer*, not an execution engine — there is no router; the
23
+ orchestrating LLM coordinates with the existing dispatch tools.
24
+ - **`list_groups()` / `inspect_group(name)` MCP tools** — cheap, no-subprocess
25
+ readouts of groups, their briefs, and members (dangling member refs are
26
+ flagged, never crash). For a deep dive on a member, use `inspect_agent`.
27
+ - **`group=` on `dispatch` and per-item in `dispatch_parallel`** — when set,
28
+ the agent must be a member of the group and the group's `shared_context` is
29
+ auto-prepended to the call's `context`. Folded into the context string, so
30
+ the cache key disambiguates groups automatically and `group=""` is byte-for-
31
+ byte identical to a plain dispatch. Parallel validates membership up front
32
+ (one bad item rejects the whole call before any subprocess runs).
33
+ - **`agent-dispatch group` CLI** — `add` / `list` / `inspect` / `update` /
34
+ `remove` for managing groups, mirroring the agent commands.
35
+
36
+ ### Changed
37
+ - `save_config` prunes an empty `groups` block and empty member lists so
38
+ group-less configs stay clean in YAML (same idiom as capabilities).
39
+
40
+ ## [0.8.0] - 2026-06-17
41
+
42
+ Let agents declare what they are good at, so callers can pick the right one.
43
+
44
+ ### Added
45
+ - **Declared capabilities.** `AgentConfig` gains `capabilities` and
46
+ `risky_capabilities` — short snake_case labels describing what an agent is
47
+ for (e.g. `docker_logs`, `restart_services`). They are descriptive metadata
48
+ only (never passed to the `claude` CLI): settable via `add_agent` /
49
+ `update_agent` (MCP) and `add` / `update` (CLI, `--capabilities` /
50
+ `--risky-capabilities`, `none` clears), and surfaced in `list_agents` /
51
+ `inspect_agent` so the calling agent can choose a target at a glance.
52
+ `risky_capabilities` flags higher-risk abilities for extra scrutiny.
53
+
54
+ ### Changed
55
+ - `save_config` no longer writes empty `capabilities` / `risky_capabilities`
56
+ keys for agents that don't declare them, keeping `agents.yaml` clean.
57
+
58
+ ### Note
59
+ - A keyword-scoring router (`recommend_agent` / `dispatch_auto` MCP tools and
60
+ `recommend` / `auto` CLI commands) was prototyped during this cycle and then
61
+ removed before release: a deterministic keyword scorer adds little over the
62
+ calling LLM's own judgment when there are only a handful of agents, and the
63
+ capability labels above cover the "what is this agent for" need without the
64
+ extra surface area or the risk of auto-dispatching to a wrong guess.
65
+
66
+ ## [0.7.0] - 2026-06-10
67
+
68
+ Job control release: running jobs become cancellable, the budget field stops
69
+ being decorative, and async jobs get a CLI.
70
+
71
+ ### Added
72
+ - **Cancel running jobs.** `dispatch_cancel(job_id)` now kills a *running*
73
+ job's `claude` subprocess when the job was started by the same server
74
+ instance (in-memory process registry — no PID files, no risk of killing an
75
+ unrelated process after a restart). The job is marked `cancelled` *before*
76
+ the kill, and `JobStore.finish`/`fail` now refuse already-terminal jobs, so
77
+ the worker's trailing write can't resurrect it. New outcome:
78
+ `cancelled_running`. Jobs from a previous server run still report
79
+ `running` (cannot be killed safely).
80
+ - **Budget visibility (post-hoc).** `max_budget_usd` was stored and displayed
81
+ but never checked. A dispatch whose `cost_usd` exceeds the agent's
82
+ `max_budget_usd` (or `settings.default_max_budget_usd`) now returns
83
+ `budget_exceeded: true` plus a `hint`. The dispatch is *not* failed — the
84
+ `claude` CLI has no spend cap, so by the time the cost is known the money is
85
+ spent; the flag makes runaway agents visible instead of silent.
86
+ - **CLI for async jobs.** New commands: `agent-dispatch jobs [--status
87
+ --limit]` (list), `agent-dispatch job <id>` (detail with progress tail and
88
+ result preview), `agent-dispatch cancel <id>` (pending jobs; running jobs
89
+ belong to the MCP server process), `agent-dispatch gc [--days]` (purge old
90
+ terminal jobs).
91
+ - **PyPI discoverability:** expanded package keywords (5 → 12).
92
+
93
+ ### Changed
94
+ - `runner.dispatch_stream` accepts an `on_proc` callback (receives the Popen
95
+ handle right after spawn) — used by the async worker to register the
96
+ process for cancellation.
97
+ - `JobStore.cancel` accepts `force=True` to cancel running jobs (callers must
98
+ kill the subprocess themselves); `finish`/`fail` return `None` for terminal
99
+ jobs instead of overwriting them.
100
+
10
101
  ## [0.6.0] - 2026-06-04
11
102
 
12
103
  Reliability release: timeouts stop being fatal, permission-blocked "successes"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-dispatch
3
- Version: 0.6.0
3
+ Version: 0.9.0
4
4
  Summary: MCP server that lets Claude Code agents delegate tasks to agents in other project directories
5
5
  Project-URL: Homepage, https://github.com/ginkida/agent-dispatch
6
6
  Project-URL: Repository, https://github.com/ginkida/agent-dispatch
@@ -8,7 +8,7 @@ Project-URL: Issues, https://github.com/ginkida/agent-dispatch/issues
8
8
  Author: ginkida
9
9
  License-Expression: MIT
10
10
  License-File: LICENSE
11
- Keywords: agent,claude,dispatch,mcp,multi-agent
11
+ Keywords: agent,agent-orchestration,ai-agents,anthropic,claude,claude-code,delegation,dispatch,mcp,mcp-server,multi-agent,subagents
12
12
  Classifier: Development Status :: 3 - Alpha
13
13
  Classifier: Intended Audience :: Developers
14
14
  Classifier: License :: OSI Approved :: MIT License
@@ -45,29 +45,61 @@ Each agent runs as a separate `claude -p` session in its own project directory
45
45
 
46
46
  Works with OAuth, API key, and Claude subscription authentication.
47
47
 
48
+ > **AI agents:** this README is the canonical doc for *using* the tool — setup: [Quick Start](#quick-start) (every step has a deterministic verify), first call: [`dispatch`](#dispatch), tool selection: [Which Tool to Use](#which-tool-to-use), failure handling: [Error Recovery](#error-recovery). Working *on* this repo instead? See [AGENTS.md](AGENTS.md).
49
+
48
50
  ## Quick Start
49
51
 
52
+ **Prerequisite:** the [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) must be installed and authenticated. Check first:
53
+
50
54
  ```bash
51
- pip install agent-dispatch
55
+ claude --version # must print a version — if it fails, install Claude Code before continuing
56
+ ```
57
+
58
+ Then:
52
59
 
53
- # Initialize: creates config + registers MCP server with Claude Code
60
+ ```bash
61
+ pip install agent-dispatch # or: pipx install agent-dispatch
62
+
63
+ # 1. Create config + register the MCP server with Claude Code (user scope)
54
64
  agent-dispatch init
55
65
 
56
- # Add agents (description auto-generated from project files)
66
+ # 2. Register project directories as agents REPLACE the example paths with
67
+ # real directories on your machine; they must exist (~ is expanded, relative
68
+ # paths are resolved). Descriptions are auto-generated from project files.
69
+ # No second project handy? Use the zero-setup block below instead.
57
70
  agent-dispatch add infra ~/projects/infra
58
71
  agent-dispatch add backend ~/projects/backend
59
72
 
60
- # Test it works
73
+ # 3. Smoke test — dispatches a real task to the agent added in step 2 and prints
74
+ # the answer; exit 0 on success. Default task when none given:
75
+ # "What project is this? Describe in one sentence."
61
76
  agent-dispatch test infra
62
77
 
63
- # If agents hit permission errors, grant tool access:
64
- agent-dispatch update infra --permission-mode bypassPermissions
65
-
66
- # If something doesn't work, run the diagnostic:
78
+ # 4. Verify the whole install prints "All checks passed." and exits 0 on success
67
79
  agent-dispatch doctor
68
80
  ```
69
81
 
70
- Done. Every Claude Code session now has access to all dispatch tools.
82
+ **Zero-setup alternative** for steps 2–3 (no second project needed registers the current directory):
83
+
84
+ ```bash
85
+ agent-dispatch add self . && agent-dispatch test self "Say hello"
86
+ ```
87
+
88
+ Every Claude Code session now has the dispatch tools. Independent check: `claude mcp list` must print a line starting with `agent-dispatch:`. From inside a Claude Code session, the first MCP calls are `list_agents()`, then [`dispatch(...)`](#dispatch).
89
+
90
+ **If `init` fails to register the MCP server** (prints a warning instead of `Registered MCP server`), register manually:
91
+
92
+ ```bash
93
+ claude mcp add-json agent-dispatch "{\"type\":\"stdio\",\"command\":\"$(which agent-dispatch)\",\"args\":[\"serve\"]}" --scope user
94
+ ```
95
+
96
+ **If `test` fails with a permission error** (`error_type: "permission"`), grant tool access and re-test:
97
+
98
+ ```bash
99
+ agent-dispatch update infra --allowed-tools "Bash,Read,Grep" # least privilege
100
+ # or, if the agent needs everything (see SECURITY.md for the trade-off):
101
+ agent-dispatch update infra --permission-mode bypassPermissions
102
+ ```
71
103
 
72
104
  ## When to Dispatch
73
105
 
@@ -97,6 +129,8 @@ Lists all configured agents. **Call this first** to see what's available.
97
129
  "mcp_servers": ["portainer", "postgres"],
98
130
  "stacks": ["Python", "Docker"],
99
131
  "dbs": ["Alembic"],
132
+ "capabilities": ["docker_logs", "deploy_debug"],
133
+ "risky_capabilities": ["restart_services"],
100
134
  "permission_mode": "bypassPermissions",
101
135
  "allowed_tools": ["Bash", "Read", "Grep"]
102
136
  }
@@ -116,6 +150,37 @@ Cheap detailed lookup — reads the agent's files without spawning a `claude` se
116
150
 
117
151
  Use this **before** `dispatch_async`/`dispatch` to confirm an agent has the tools and context for your task — much cheaper than a probe dispatch.
118
152
 
153
+ ### Groups
154
+
155
+ A **group** bundles related agents into a cross-project working set — typically a few code repos plus capability gateways (an `infra` agent with a Portainer MCP, an `analytics` agent with a browser + Yandex Metrica). It lets one orchestrating session coordinate work that spans code, deploy, and verification.
156
+
157
+ A group is a **descriptive layer, not an execution engine** — there is no router and no state machine. You pick members by reading their hints and coordinate with the normal dispatch tools. Two text fields target two audiences:
158
+
159
+ - `description` — **orchestrator-facing**: how to coordinate the group (the order of steps, who to call for what). Surfaced by `list_groups`/`inspect_group`, **never** injected into a member's prompt.
160
+ - `shared_context` — **member-facing facts** (stack names, counter ids, conventions) that hold regardless of which member reads them. Auto-prepended to a member's `context` when you pass `group=`.
161
+
162
+ Members reference agents by name; membership is many-to-many (a shared gateway can belong to several groups). Manage groups with the `agent-dispatch group` CLI (`add`/`list`/`inspect`/`update`/`remove`) or by editing `agents.yaml`.
163
+
164
+ **`list_groups()`** — cheap, no-subprocess readout of every group: description, member count, and each member's `use_for` hint + health. A member whose agent was removed is flagged `"unknown": true` rather than crashing.
165
+
166
+ **`inspect_group(name)`** — one group's full brief: `description`, the complete `shared_context`, and the member list. For a deep dive on a specific member, call `inspect_agent(member)` — `inspect_group` deliberately stays a cheap membership readout.
167
+
168
+ **Using a group** — pass `group=` to `dispatch` (or per-item in `dispatch_parallel`). The agent must be a member; its group's `shared_context` rides along automatically:
169
+
170
+ ```python
171
+ # From the shop-web codebase, hand the deploy to the infra gateway.
172
+ # The "shop" group's facts (stack name, counter id) are auto-attached.
173
+ dispatch(
174
+ agent="infra",
175
+ task="Redeploy the shop-web container",
176
+ caller="shop-web",
177
+ goal="ship the checkout fix",
178
+ group="shop",
179
+ )
180
+ ```
181
+
182
+ `group=""` (the default) is byte-for-byte identical to a plain dispatch — the shared facts are folded into the `context` string, so the result cache disambiguates groups automatically and group-less calls are unaffected.
183
+
119
184
  ### `dispatch`
120
185
 
121
186
  One-shot task delegation. Results are cached — identical requests within TTL return instantly.
@@ -131,6 +196,18 @@ One-shot task delegation. Results are cached — identical requests within TTL r
131
196
  | `return_ref` | bool | no | When `true`, returns just a `ref` + summary preview instead of the full result text. Use `fetch_result(ref)` to load the full text on demand. |
132
197
  | `summary_chars` | int | no | Max chars of result text to include in the ref response (default 500). |
133
198
  | `timeout_seconds` | int | no | One-off timeout override for this call (0 = agent's configured timeout; clamped to 10–7200). No config edit needed for known-long tasks. |
199
+ | `group` | string | no | Group name (from `list_groups`). The agent must be a member; the group's `shared_context` (member-facing facts) is auto-prepended to `context`. Empty = plain dispatch. See [Groups](#groups). |
200
+
201
+ ```python
202
+ # Call — recommended form (always include caller and goal)
203
+ dispatch(
204
+ agent="infra", # must exist in list_agents()
205
+ task="Check container logs for errors related to the scheduler service",
206
+ context="Error: TypeError at scheduler.py:42",
207
+ caller="backend", # your project/role
208
+ goal="debug production crash" # the broader objective
209
+ )
210
+ ```
134
211
 
135
212
  ```json
136
213
  // Response (success)
@@ -229,7 +306,7 @@ Run multiple tasks concurrently. Much faster than sequential `dispatch` calls.
229
306
 
230
307
  | Parameter | Type | Required | Description |
231
308
  |-----------|------|----------|-------------|
232
- | `dispatches` | string (JSON) | yes | JSON array of `{"agent", "task", "context?", "caller?", "goal?", "response_format?", "return_ref?", "summary_chars?", "timeout_seconds?"}` |
309
+ | `dispatches` | string (JSON) | yes | JSON array of `{"agent", "task", "context?", "caller?", "goal?", "response_format?", "return_ref?", "summary_chars?", "timeout_seconds?", "group?"}` (a per-item `group` validates membership up front and auto-injects its `shared_context`) |
233
310
  | `aggregate` | string | no | Agent name to synthesize all results into one answer |
234
311
 
235
312
  **Important:** `dispatches` is a JSON string, not a list.
@@ -307,9 +384,10 @@ Register a new project directory as an agent. Description is auto-generated from
307
384
  | Parameter | Type | Required | Description |
308
385
  |-----------|------|----------|-------------|
309
386
  | `name` | string | yes | Agent name (letters, digits, hyphens, underscores) |
310
- | `directory` | string | yes | Absolute path to project directory |
387
+ | `directory` | string | yes | Path to an existing project directory (`~` is expanded, relative paths resolved) |
311
388
  | `description` | string | no | What this agent can do — auto-generated if empty |
312
389
  | `timeout` | int | no | Timeout in seconds (0 = use global default) |
390
+ | `max_budget_usd` | float | no | Max cost in USD per dispatch (0 = no limit) |
313
391
  | `permission_mode` | string | no | Permission mode (e.g. `default`, `plan`, `bypassPermissions`) |
314
392
  | `allowed_tools` | string | no | Comma-separated allowed tools (e.g. `"Bash,Read,Edit"`) |
315
393
  | `disallowed_tools` | string | no | Comma-separated disallowed tools |
@@ -323,6 +401,7 @@ Update an existing agent's configuration. Only non-empty fields are changed. Pas
323
401
  | `name` | string | yes | Agent name to update |
324
402
  | `description` | string | no | New description |
325
403
  | `timeout` | int | no | New timeout (0 = don't change) |
404
+ | `max_budget_usd` | float | no | New budget limit (0 = don't change, negative = clear the limit) |
326
405
  | `model` | string | no | Model override. `"none"` to clear |
327
406
  | `permission_mode` | string | no | Permission mode. `"none"` to clear |
328
407
  | `allowed_tools` | string | no | Comma-separated. `"none"` to clear |
@@ -373,7 +452,7 @@ dispatch_status(job_id="8f3a...e1")
373
452
  -> {"id": "8f3a...e1", "status": "running", "started_at": 1730000123.4,
374
453
  "progress": ["Using tool: Bash", "Scanning container logs for OOM events..."], ...}
375
454
 
376
- // 3. or block until done (with a timeout cap)
455
+ // 3. or block until done (timeout_seconds default: 60, capped at 3600)
377
456
  dispatch_wait(job_id="8f3a...e1", timeout_seconds=120)
378
457
  -> {"id": "8f3a...e1", "status": "done", "result": {"agent": "infra", "success": true, ...}}
379
458
 
@@ -381,13 +460,13 @@ dispatch_wait(job_id="8f3a...e1", timeout_seconds=120)
381
460
  -> {"id": "...", "status": "running", "timed_out_waiting": true}
382
461
  ```
383
462
 
384
- `dispatch_cancel(job_id)` cancels a job that is still **pending** (before its subprocess starts) a running job is left to finish, since its `claude` subprocess can't be safely interrupted. The response carries an `outcome` of `cancelled`, `running`, `already_terminal`, or `not_found`.
463
+ `dispatch_cancel(job_id)` cancels a **pending** job, and also kills a **running** job's `claude` subprocess when the job was started by the same server instance (the job is marked `cancelled` first, so the worker's trailing write can't undo it; partial work is lost but the progress tail is preserved). A running job started by a *previous* server run can't be killed safely and is left to finish. The response carries an `outcome` of `cancelled`, `cancelled_running`, `running` (not owned by this server), `already_terminal`, or `not_found`.
385
464
 
386
465
  Async workers run with streaming under the hood: the job file keeps a rolling tail (last 20 lines, ~1 write/sec) of assistant text and tool-use events. `dispatch_status` shows it as `progress` while the job runs and keeps it afterwards as a post-mortem trace; `dispatch_jobs` shows `last_progress` for running jobs.
387
466
 
388
467
  `dispatch_jobs(status?)` lists recent jobs as summaries (filter by `pending` / `running` / `done` / `failed` / `cancelled`). `dispatch_gc(max_age_days=7)` purges terminal jobs older than the threshold — pending and running jobs are never deleted.
389
468
 
390
- Job state persists to disk at `~/.config/agent-dispatch/jobs/` (override with `AGENT_DISPATCH_JOBS_DIR`). One JSON file per job, written owner-only (`0o600`) with atomic writes — safe to read or `ls` while jobs are in flight. Caller-supplied `job_id`s are validated as 32-char hex before any file access (no path traversal). On startup the server marks jobs abandoned in `running` by a prior crashed instance as `failed`.
469
+ Job state persists to disk at `~/.config/agent-dispatch/jobs/` (override with `AGENT_DISPATCH_JOBS_DIR`). One JSON file per job, written owner-only (`0o600`) with atomic writes — safe to read or `ls` while jobs are in flight. Caller-supplied `job_id`s are validated as 32-char hex before any file access (no path traversal). On startup the server marks jobs left in `running` by a crashed instance as `failed` once they are stale (stuck for over an hour).
391
470
 
392
471
  | When to use async | When to use `dispatch` |
393
472
  |-------------------|------------------------|
@@ -395,14 +474,6 @@ Job state persists to disk at `~/.config/agent-dispatch/jobs/` (override with `A
395
474
  | Several long tasks you'll collect later | Several short tasks → `dispatch_parallel` |
396
475
  | Don't care about caching (each call is a fresh job) | Cached by default — identical requests are free |
397
476
 
398
- ### Error Responses
399
-
400
- All tools return errors as:
401
-
402
- ```json
403
- {"error": "Unknown agent: 'foo'. Available: infra, db, monitoring"}
404
- ```
405
-
406
477
  ## Which Tool to Use
407
478
 
408
479
  | Scenario | Tool |
@@ -417,6 +488,32 @@ All tools return errors as:
417
488
  | Check progress without blocking | `dispatch_status` |
418
489
  | Known-long task, one-off | any dispatch tool with `timeout_seconds=...` |
419
490
  | A dispatch timed out | `dispatch_session` with the `session_id` from the error |
491
+ | Coordinating a set of related projects | define a [group](#groups), then `dispatch(..., group=name)` |
492
+ | See which agents form a working set | `list_groups` / `inspect_group` |
493
+
494
+ ## Error Recovery
495
+
496
+ Failures are deterministic: check `success`, then branch on `error_type`.
497
+
498
+ | `error_type` | Meaning | Recovery |
499
+ |--------------|---------|----------|
500
+ | `permission` | A tool call was denied | `update_agent(name, allowed_tools="Bash,Read")` (least privilege) or `update_agent(name, permission_mode="bypassPermissions")`, then re-dispatch. The `error` text includes a hint with the exact fix. |
501
+ | `timeout` | Process killed at the timeout | Resume the partial work: `dispatch_session(agent, "Continue where you left off", session_id=<from the error text>)`. Or retry with a bigger `timeout_seconds=`, or use `dispatch_async`. |
502
+ | `not_found` | Agent directory or `claude` CLI missing | `list_agents()` → check `healthy`. Re-add the agent with an existing path, or run `agent-dispatch doctor` to find what's missing. |
503
+ | `recursion` | Dispatch nesting exceeded `max_dispatch_depth` (default 3) | Don't dispatch from dispatched agents; if the nesting is intentional, raise `max_dispatch_depth` in settings. |
504
+ | `cli_error` | Anything else from the `claude` subprocess | Read the `error` text; run `agent-dispatch doctor` for environment issues; retry once if transient. |
505
+
506
+ Three soft signals that arrive with `success: true`:
507
+
508
+ - **`denied_tools` + `hint`** — the agent finished but some tool calls were blocked; the result may be incomplete. Grant access (see the `permission` row) and re-dispatch.
509
+ - **`parsed_result: null` with `response_format="json"`** — the reply wasn't valid JSON; the raw text is still in `result`. Caveat: an agent that *can't* comply returns `{"error": "<reason>"}` — which parses successfully — so also check `parsed_result` for an `"error"` key.
510
+ - **`budget_exceeded: true`** — `cost_usd` exceeded the agent's `max_budget_usd` (or the settings default). The dispatch is not failed — the money is already spent — but a runaway agent is now visible. Tighten the task, pick a cheaper model, or raise the budget.
511
+
512
+ Tool-level errors (unknown agent, malformed input) return a plain envelope instead of a `DispatchResult`:
513
+
514
+ ```json
515
+ {"error": "Unknown agent: 'foo'. Available: infra, db, monitoring"}
516
+ ```
420
517
 
421
518
  ## Configuration
422
519
 
@@ -428,9 +525,14 @@ agents:
428
525
  directory: ~/projects/infra
429
526
  description: "Infrastructure agent. MCP: portainer."
430
527
  timeout: 300 # seconds, default: 300
528
+ capabilities: # capability labels, shown in list_agents
529
+ - docker_logs
530
+ - deploy_debug
531
+ risky_capabilities: # high-risk labels, surfaced for visibility
532
+ - restart_services
431
533
  # model: sonnet # optional model override
432
534
  # max_budget_usd: 1.0 # cost limit per dispatch
433
- # permission_mode: auto # permission mode for the agent
535
+ # permission_mode: bypassPermissions # one of: default | plan | bypassPermissions
434
536
  # allowed_tools: # restrict which tools the agent can use
435
537
  # - Read
436
538
  # - Grep
@@ -465,6 +567,18 @@ Config is reloaded on every tool call — add agents without restarting.
465
567
  - Stack indicators — Docker, Rust, Go, Python, Node.js
466
568
  - DB indicators — Prisma, Alembic, migrations
467
569
 
570
+ ### Explicit Capabilities
571
+
572
+ Auto-description is useful, but explicit `capabilities` make it clearer what each agent is for. Add short snake_case task labels to agents:
573
+
574
+ ```bash
575
+ agent-dispatch update infra \
576
+ --capabilities docker_logs,deploy_debug \
577
+ --risky-capabilities restart_services
578
+ ```
579
+
580
+ `list_agents` and `inspect_agent` surface `capabilities` and `risky_capabilities` so the caller can pick the right agent at a glance — `risky_capabilities` flags higher-risk abilities (e.g. restarting services) for extra scrutiny.
581
+
468
582
  ## How It Works
469
583
 
470
584
  ```
@@ -491,7 +605,7 @@ agent-dispatch MCP server
491
605
  - **Argument-injection guard** — structured CLI fields (`session_id`, `model`, `permission_mode`, tool names) that start with `-` are rejected so they can't smuggle extra `claude` flags.
492
606
  - **Path-traversal guard** — caller-supplied `job_id`/`ref` values are validated as 32-char hex before any filesystem access.
493
607
  - **Owner-only state** — job files (`0o600`) and `agents.yaml` (`0o600`) are written for the owner only; their directories are `0o700`.
494
- - **Cost control** — `max_budget_usd` per agent or globally.
608
+ - **Cost visibility** — `max_budget_usd` per agent or globally; a dispatch whose cost exceeds it returns `budget_exceeded: true` + a hint (post-hoc — the `claude` CLI has no spend cap, so the overage can be flagged but not prevented).
495
609
  - **Concurrency** — `max_concurrency` (default: 5) caps parallel `claude -p` processes. Note: the sync and async dispatch paths use separate semaphores, so the worst-case total is `2 × max_concurrency`.
496
610
  - **Timeout** — per-agent or global (default: 300s). Orphaned processes are cleaned up.
497
611
  - **Caching** — identical `(agent, task, context, caller, goal, response_format)` requests return cached results, bounded by `cache.max_size` (oldest entry evicted first). Only successes are cached. Sessions and dialogues are never cached.
@@ -510,12 +624,16 @@ See [SECURITY.md](SECURITY.md) for the full threat model (including the `bypassP
510
624
  | `agent-dispatch describe <name>` | Show full configuration for one agent (tri-state tools, project files) |
511
625
  | `agent-dispatch test <name> [task] [--stream]` | Test an agent with a dispatch (`--stream` for live progress) |
512
626
  | `agent-dispatch doctor` | Diagnose installation: claude CLI, MCP registration, agent health |
627
+ | `agent-dispatch jobs [--status --limit]` | List async dispatch jobs (most recent first) |
628
+ | `agent-dispatch job <id>` | Show one job: status, progress tail, result preview |
629
+ | `agent-dispatch cancel <id>` | Cancel a pending job (running jobs: use the `dispatch_cancel` MCP tool) |
630
+ | `agent-dispatch gc [--days]` | Purge terminal jobs older than N days (default 7) |
513
631
  | `agent-dispatch serve` | Start MCP server (stdio, used by Claude Code) |
514
632
 
515
633
  ## Requirements
516
634
 
517
635
  - Python >= 3.10
518
- - [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) installed and authenticated
636
+ - [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) installed, authenticated, and on `PATH` (verify: `claude --version`)
519
637
 
520
638
  ## License
521
639