From fa23971341744dd414b4c9212c909a84f6b36c2a Mon Sep 17 00:00:00 2001
From: Brad Harris
Date: Thu, 13 Aug 2026 22:06:11 -0600
Subject: [PATCH] Document the repo tool execution contract (docs audit)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Repo Tools deep-dive against repo-tools.ts, shared/mcp/server.ts,
lifecycle-hooks.ts and run-command.ts. The built-in tool list checked out
in both directions; the gaps were all in how `.dispatch/tools.json`
actually behaves:
- Commands are spawned directly, not through a shell, so pipes/globs/`&&`
silently don't work.
- `name`, `description` and a non-empty `command` are required, and one
malformed entry throws during the manifest load — every `repo_` tool
disappears, with nothing that reads like a parse error.
- Repo tools allow every exit code (allowedExitCodes 0-255), so stdout
comes back as the text result with exit code/stdout/stderr alongside;
a non-zero exit is something the agent reads, not an error. Dispatch
sets no timeout on them, unlike the 15s lifecycle hooks.
- The manifest is re-read per tool listing (only hooks are mtime-cached),
but the agent's CLI holds the list it fetched at session start, so a
newly added tool needs a new session while an edited command does not.
Also adds an ambient tip for `.dispatch/tools.json` — the feature has no
UI surface at all and had no tip — and fixes the plugin skill's claim
that a colliding tool name is rejected at load: BUILTIN_TOOL_NAMES holds
unprefixed names and the check tests the `repo_`-prefixed one, so it can
never fire. The prefix is what prevents collisions.
CLAUDE.md's project tree gains plugins/ and release-notes/.
Co-Authored-By: Claude Opus 5 (1M context)
---
CLAUDE.md | 3 ++
.../components/app/docs-sections/tools.tsx | 43 ++++++++++++++++++-
apps/web/src/lib/tips/tips.ts | 8 ++++
plugins/dispatch/skills/repo-tools/SKILL.md | 16 ++++---
4 files changed, 63 insertions(+), 7 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 96733e9d..103a1ec4 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -33,6 +33,9 @@ dispatch/
├── e2e/ # Playwright E2E tests
├── bin/ # dispatch-dev, dispatch-server, install-dispatch, etc.
├── scripts/ # e2e-isolated.sh, generate-icon-colors.ts
+├── plugins/
+│ └── dispatch/ # official Dispatch plugin (skills for Claude Code + Codex)
+├── release-notes/ # release notes + assisted-update metadata
├── .dispatch/ # repo-level Dispatch config
│ ├── config.json # repo-level settings (e.g. Linear integration)
│ ├── job-state/ # persistent state files for recurring jobs
diff --git a/apps/web/src/components/app/docs-sections/tools.tsx b/apps/web/src/components/app/docs-sections/tools.tsx
index 8286df94..62d4c1ae 100644
--- a/apps/web/src/components/app/docs-sections/tools.tsx
+++ b/apps/web/src/components/app/docs-sections/tools.tsx
@@ -44,6 +44,15 @@ export function ToolsContent() {
The tools above would be available to agents as repo_lint
, repo_test, and repo_db_reset.
+
+ Every entry needs a name, a description, and
+ a non-empty command array. The command is executed
+ directly rather than through a shell, so pipes, globs, and{" "}
+ && don't work — put anything shell-shaped in a
+ script and point command at that. A malformed entry
+ aborts the whole manifest, so a typo in one tool makes every{" "}
+ repo_ tool disappear.
+
@@ -78,7 +87,39 @@ export function ToolsContent() {
When an agent calls repo_dev_up with{" "}
{'{ cwd: "/path", live: true }'}, Dispatch runs{" "}
./bin/dev up --cwd /path --live. Parameters that are
- omitted or false are skipped.
+ omitted, false, or an empty string are skipped. Every parameter is
+ optional to the agent, and name, type, and{" "}
+ flag are required in the definition — the{" "}
+ description is what the agent reads to decide what to
+ pass, so it's worth writing.
+
+
+
+
+ What agents get back
+
+ A repo tool call doesn't fail on a non-zero exit. The command's stdout
+ comes back as the tool's text result, with the exit code, stdout, and
+ stderr in the structured payload, so the agent can read a failure and
+ react to it instead of just seeing an error. Write scripts that fail
+ loudly on stderr.
+
+
+ Dispatch puts no time limit on a repo tool command (lifecycle hooks
+ below do get one), though the agent's own MCP client may give up on a
+ very long call.
+
+
+
+
+ Picking up changes
+
+ .dispatch/tools.json is re-read from disk on every tool
+ listing — no server restart needed. The limit is on the agent's side:
+ a CLI fetches its tool list once when the session starts and holds it.
+ So an edited command runs the new version on the next call, but a
+ newly added tool usually isn't callable until the agent reconnects or
+ a new session starts.
diff --git a/apps/web/src/lib/tips/tips.ts b/apps/web/src/lib/tips/tips.ts
index 0d2acc2b..19d386d8 100644
--- a/apps/web/src/lib/tips/tips.ts
+++ b/apps/web/src/lib/tips/tips.ts
@@ -260,6 +260,14 @@ export const tips: Tip[] = [
since: "0.33.6",
surfaces: ["ambient"],
},
+ {
+ id: "repo-tools",
+ title: "Give Agents Your Repo's Commands",
+ body: "Add a .dispatch/tools.json to a repo and every entry becomes a real tool in each agent's tool list — so nobody has to paste the same build, reset, or dev-stack command into a prompt again.",
+ docsSection: "tools",
+ since: "0.11.12",
+ surfaces: ["ambient"],
+ },
{
id: "shortcut-pins",
title: "One-Click Agent Shortcuts",
diff --git a/plugins/dispatch/skills/repo-tools/SKILL.md b/plugins/dispatch/skills/repo-tools/SKILL.md
index 2455f9d8..31b4ec4a 100644
--- a/plugins/dispatch/skills/repo-tools/SKILL.md
+++ b/plugins/dispatch/skills/repo-tools/SKILL.md
@@ -54,13 +54,13 @@ it at the moment of use.
| ------------- | -------- | --------------------------------------------------------------------------- |
| `name` | yes | Exposed as `repo_`. Dots are stripped — MCP names cannot contain them |
| `description` | yes | This is what makes the tool get used. See below |
-| `command` | yes | Argv array, run from the repo root |
+| `command` | yes | Argv array, run from the agent's checkout root |
| `params` | no | Turned into CLI flags appended to `command` |
| `scope` | no | Any of `agent`, `reviewer`, `job`. Omit to expose everywhere |
-`repo_` prefixing is automatic, and a name that would collide with a built-in
-Dispatch tool (`create_pr`, `get_pr_status`, `dispatch_event`, `dispatch_share`)
-is rejected at load.
+`repo_` prefixing is automatic and keeps repo tools in their own namespace, so a
+repo tool can never shadow a built-in Dispatch tool like `create_pr` or
+`dispatch_event`.
**Write the description for an agent that has never seen this repo.** It is the
only thing standing between the tool existing and the tool being used. Say what
@@ -79,7 +79,10 @@ load. Give every param a `description` too — the agent picks values from it.
## Execution model
-Commands run from the repo root with `DISPATCH_AGENT_ID` set in the environment.
+Commands run from the root of the agent's checkout — the worktree root for
+worktree agents, otherwise the repo root — with `DISPATCH_AGENT_ID` set in the
+environment. There is no shell: the argv array is spawned directly, so pipes,
+globs, and `&&` need a wrapper script.
**Every exit code is returned to the agent** rather than throwing — stdout,
stderr, and the exit code all come back, so the agent can read a failure instead
of just seeing an error. Write scripts that fail loudly on stderr.
@@ -89,7 +92,8 @@ of just seeing an error. Write scripts that fail loudly on stderr.
`hooks.stop.command` runs when the agent stops. Use it for teardown that would
otherwise leak: stopping a dev stack, removing a container, releasing a port.
Keep it fast and idempotent — it may run when the thing it cleans up was never
-started.
+started, and it is killed after 15 seconds. A failing or timed-out hook is
+logged and never blocks the stop.
## Working on it