Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .agents/plugins/marketplace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "dispatch",
"interface": {
"displayName": "Dispatch"
},
"plugins": [
{
"name": "dispatch",
"source": {
"source": "local",
"path": "./plugins/dispatch"
},
"category": "workflow"
}
]
}
17 changes: 17 additions & 0 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "dispatch",
"owner": {
"name": "Dispatch",
"url": "https://github.com/selfcontained/dispatch"
},
"description": "Official plugin marketplace for Dispatch — skills that teach agents how to use Dispatch's own capabilities.",
"plugins": [
{
"name": "dispatch",
"source": "./plugins/dispatch",
"description": "Teaches agents how to use Dispatch: shared memory, subagents, repo tools, sharing artifacts, review workflow, whiteboard, jobs, and templates.",
"category": "workflow",
"keywords": ["dispatch", "agents", "orchestration", "mcp", "review"]
}
]
}
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,24 @@ Repos can define custom tools in `.dispatch/tools.json` — these are exposed to

These tools only work inside running agent sessions (they require agent-scoped MCP context which Dispatch provides automatically).

## Dispatch plugin (Claude Code + Codex)

This repo doubles as a plugin marketplace. The **Dispatch plugin** ships skills that teach agents how to use the capabilities above — the Brain, subagents, `.dispatch/tools.json`, artifact sharing, the review workflow, the whiteboard, jobs, and templates — so agents discover them instead of having to be told.

**Before you install:** plugins on Claude Code and Codex are **unsigned and unsandboxed, and run with your full local user privileges** — this one and every other self-hosted plugin. This plugin ships no executable components (no hooks, no `bin/`, no bundled MCP servers), only markdown skills; [plugins/dispatch/README.md](plugins/dispatch/README.md#trust) shows how to verify that for yourself before running the commands below.

```bash
# Claude Code
claude plugin marketplace add selfcontained/dispatch
claude plugin install dispatch@dispatch

# Codex
codex plugin marketplace add selfcontained/dispatch
codex plugin add dispatch@dispatch
```

See [plugins/dispatch/README.md](plugins/dispatch/README.md) for what each skill covers and for update mechanics — notably that Codex has no update command, so upgrading means re-running `codex plugin add`.

## Operations

- Update production from the Dispatch UI: **Settings → Updates**
Expand Down
124 changes: 124 additions & 0 deletions apps/server/test/plugin-manifest.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { readdirSync, readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

import { parse as parseYaml } from "yaml";
import { describe, expect, it } from "vitest";

/**
* Guards the published Dispatch plugin (Claude Code + Codex) that lives at the
* repo root. CI has no `claude`/`codex` CLI to run `claude plugin validate`
* against, and every failure mode here is silent at install time: a SKILL.md
* whose YAML frontmatter fails to parse still installs, it just loads with
* empty metadata and the skill never fires.
*/
const repoRoot = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
"../../.."
);
const PLUGIN_NAME = "dispatch";
const MARKETPLACE_NAME = "dispatch";
const PLUGIN_DIR = path.join(repoRoot, "plugins", PLUGIN_NAME);
const SKILLS_DIR = path.join(PLUGIN_DIR, "skills");

function readJson(relativePath: string): Record<string, unknown> {
return JSON.parse(readFileSync(path.join(repoRoot, relativePath), "utf8"));
}

const skillSlugs = readdirSync(SKILLS_DIR, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name)
.sort();

describe("plugin manifests", () => {
it("declares the same plugin under both marketplace formats", () => {
const claude = readJson(".claude-plugin/marketplace.json");
const codex = readJson(".agents/plugins/marketplace.json");

// The install identifier is `<plugin>@<marketplace>`. If the two
// marketplace names drift, the documented command works on one platform
// and fails on the other.
expect(claude.name).toBe(MARKETPLACE_NAME);
expect(codex.name).toBe(MARKETPLACE_NAME);

const claudePlugins = claude.plugins as Array<Record<string, unknown>>;
const codexPlugins = codex.plugins as Array<Record<string, unknown>>;
expect(claudePlugins.map((p) => p.name)).toEqual([PLUGIN_NAME]);
expect(codexPlugins.map((p) => p.name)).toEqual([PLUGIN_NAME]);

// Claude takes a bare relative string, Codex takes a local-source object.
expect(claudePlugins[0].source).toBe(`./plugins/${PLUGIN_NAME}`);
expect(codexPlugins[0].source).toEqual({
source: "local",
path: `./plugins/${PLUGIN_NAME}`,
});
});

it("keeps both plugin manifests on the same name and version", () => {
const claude = readJson(
`plugins/${PLUGIN_NAME}/.claude-plugin/plugin.json`
);
const codex = readJson(`plugins/${PLUGIN_NAME}/.codex-plugin/plugin.json`);

expect(claude.name).toBe(PLUGIN_NAME);
expect(codex.name).toBe(PLUGIN_NAME);

// Claude resolves the plugin version from plugin.json and uses it as the
// update cache key; Codex requires it outright. A mismatch means the two
// platforms disagree about which version is installed.
expect(claude.version).toMatch(/^\d+\.\d+\.\d+$/);
expect(codex.version).toBe(claude.version);

// Codex requires a description; Claude only warns without one.
expect(typeof codex.description).toBe("string");
expect((codex.description as string).length).toBeGreaterThan(0);
});
});

describe("plugin skills", () => {
it("ships at least one skill", () => {
expect(skillSlugs.length).toBeGreaterThan(0);
});

it.each(skillSlugs)("%s has parseable frontmatter", (slug) => {
const source = readFileSync(
path.join(SKILLS_DIR, slug, "SKILL.md"),
"utf8"
);

const match = /^---\n([\s\S]*?)\n---\n/.exec(source);
expect(match, "SKILL.md must open with a YAML frontmatter block").not.toBe(
null
);

// An unquoted `: ` or a leading `[`/`{`/`*`/`&` in a description is the
// realistic failure here — it parses as YAML structure, the block fails,
// and the skill silently loads with no name or description at all.
const frontmatter = parseYaml(match![1]) as Record<string, unknown>;

expect(frontmatter.name).toBe(slug);
expect(typeof frontmatter.description).toBe("string");
expect((frontmatter.description as string).trim().length).toBeGreaterThan(
0
);
});

it("keeps the always-on description budget in check", () => {
// Skill names and descriptions are injected into every session whether or
// not any skill fires, so this total is the plugin's unconditional cost.
// The ceiling is the 2,891 chars Dispatch's own launch guidance already
// injects — the plugin should not cost more than the guidance it augments.
const total = skillSlugs.reduce((sum, slug) => {
const source = readFileSync(
path.join(SKILLS_DIR, slug, "SKILL.md"),
"utf8"
);
const frontmatter = parseYaml(
/^---\n([\s\S]*?)\n---\n/.exec(source)![1]
) as Record<string, unknown>;
return sum + slug.length + (frontmatter.description as string).length;
}, 0);

expect(total).toBeLessThan(2891);
});
});
13 changes: 13 additions & 0 deletions plugins/dispatch/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "dispatch",
"displayName": "Dispatch",
"version": "0.1.0",
"description": "Teaches agents how to use Dispatch: shared memory, subagents, repo tools, sharing artifacts, review workflow, whiteboard, jobs, and templates.",
"author": {
"name": "Dispatch",
"url": "https://github.com/selfcontained/dispatch"
},
"homepage": "https://github.com/selfcontained/dispatch",
"repository": "https://github.com/selfcontained/dispatch",
"keywords": ["dispatch", "agents", "orchestration", "mcp", "review"]
}
19 changes: 19 additions & 0 deletions plugins/dispatch/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "dispatch",
"version": "0.1.0",
"description": "Teaches agents how to use Dispatch: shared memory, subagents, repo tools, sharing artifacts, review workflow, whiteboard, jobs, and templates.",
"author": {
"name": "Dispatch",
"url": "https://github.com/selfcontained/dispatch"
},
"homepage": "https://github.com/selfcontained/dispatch",
"repository": "https://github.com/selfcontained/dispatch",
"keywords": ["dispatch", "agents", "orchestration", "mcp", "review"],
"interface": {
"displayName": "Dispatch",
"shortDescription": "Skills that teach agents how to use Dispatch's own capabilities.",
"developerName": "Dispatch",
"category": "workflow",
"websiteURL": "https://github.com/selfcontained/dispatch"
}
}
139 changes: 139 additions & 0 deletions plugins/dispatch/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# Dispatch plugin

Skills that teach agents how to use Dispatch's own capabilities. Install it in
Claude Code or Codex and agents get shared memory, subagent orchestration, repo
tools, artifact sharing, the review workflow, the whiteboard, jobs, and templates
as discoverable skills instead of tribal knowledge.

## Install

Installing from a marketplace is a two-step process on both platforms: register
the catalog, then install the plugin from it. Adding the marketplace installs
nothing on its own.

**Claude Code**

```
/plugin marketplace add selfcontained/dispatch
/plugin install dispatch@dispatch
```

Or non-interactively:

```bash
claude plugin marketplace add selfcontained/dispatch
claude plugin install dispatch@dispatch
```

The shell form doesn't run inside a session, so the skills load the next time you
start Claude Code — or immediately if you run `/reload-plugins` in a session
that's already open.

**Codex**

```bash
codex plugin marketplace add selfcontained/dispatch
codex plugin add dispatch@dispatch
```

`codex plugin add` installs from a configured marketplace snapshot, so the
`marketplace add` step is required here too. `/plugins` inside a Codex session
opens the same thing as a browser.

## Updating

**Claude Code** — `claude plugin update dispatch@dispatch` (or
`/plugin marketplace update dispatch` to refresh the catalog first).

Auto-update is **off by default** for third-party marketplaces like this one.
Turn it on per-marketplace in `/plugin` → Marketplaces if you want updates
picked up in the background.

**Codex** — there is no update subcommand. Re-run
`codex plugin add dispatch@dispatch` to upgrade; it replaces the cached copy.
`codex plugin marketplace upgrade` only refreshes the catalog snapshot, not the
installed plugin, and `codex plugin list` will not tell you a newer version
exists.

The plugin carries an explicit `version` in its manifests, so updates only ship
when that version is bumped — routine commits to `main` do not register as a
plugin update.

## Trust

Plugins on both platforms are **unsigned and unsandboxed, and run with your full
local user privileges**. That is true of this plugin and of every other one you
install from a self-hosted marketplace.

This plugin ships **no executable components** — no `hooks/`, no `bin/`, no
bundled MCP servers (`.mcp.json`), no LSP servers. Everything it contributes is
markdown that agents read. Verify that yourself rather than taking this file's
word for it:

```bash
# Nothing executable should be listed.
ls plugins/dispatch # .claude-plugin .codex-plugin README.md evals skills
cat plugins/dispatch/.claude-plugin/plugin.json # no hooks/mcpServers/bin fields
cat plugins/dispatch/.codex-plugin/plugin.json
```

`claude plugin details dispatch@dispatch` reports the same thing after install —
it prints a component inventory with `Hooks (0)`, `MCP servers (0)`, and
`LSP servers (0)`.

The rest of the tree is documentation: `skills/` (the skill bodies agents load),
`evals/` (test cases), and this README.

## What's in it

| Skill | Fires when |
| ----------------- | ------------------------------------------------------------- |
| `brain` | Something needs to outlive the session or reach another agent |
| `subagents` | Work should be delegated, or another agent needs coordinating |
| `repo-tools` | A repo script should become a first-class tool |
| `sharing` | An artifact needs to reach the user |
| `review-workflow` | A PR is going up, or review feedback needs working |
| `ui-validation` | A UI change needs proving in a browser |
| `personas` | This repo needs a reviewer with a domain lens |
| `whiteboard` | The user's sketch matters, or a diagram beats prose |
| `jobs` | Work should run on a schedule and report structurally |
| `templates` | A launch configuration is worth saving |
| `personalities` | The user is commenting on how agents talk |

## Design notes

**The description is the product.** Skill descriptions are loaded into every
session unconditionally — that is the cost the plugin always pays. The body is
loaded only when a skill matches. So descriptions are written as _symptom
triggers_ ("you have produced a file the user should see") rather than feature
labels ("artifact sharing API"): an agent that does not know a capability exists
will never match its name, but will match a description of the situation it is
currently in.

**Narrow skills, not mega-skills.** Eleven narrow skills cost eleven short
descriptions always-on and load exactly one body on a match. Folding them into
three broad skills would load four unrelated bodies every time one of them fired.
The binding budget is total description bytes (currently ~2.3 KB), not skill
count.

**What is deliberately _not_ here.** Guidance that is always relevant cannot be
a skill, because skills only load on a task match. Status reporting
(`dispatch_event`), pin discipline, and session naming stay in Dispatch's
injected launch guidance for that reason.

## Layout

Dual manifests, one tree — both platforms install from the same repo:

```
.claude-plugin/marketplace.json # Claude Code marketplace
.agents/plugins/marketplace.json # Codex marketplace
plugins/dispatch/
├── .claude-plugin/plugin.json # Claude Code manifest
├── .codex-plugin/plugin.json # Codex manifest
├── skills/<name>/SKILL.md # shared by both platforms
└── evals/ # see evals/README.md
```

Codex can also read `.claude-plugin/` as a fallback, but that behavior is
undocumented by OpenAI, so the Codex-native manifests are carried explicitly.
38 changes: 38 additions & 0 deletions plugins/dispatch/evals/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Evals

Ablation cases for the discovery skills — the ones whose whole job is to make an
agent aware that a Dispatch capability exists.

```bash
claude plugin eval dispatch@dispatch --ablation with-without
```

`--ablation with-without` runs a **no-plugin baseline arm** alongside the
with-plugin arm and reports the score delta. That delta is the number that
matters here. A skill whose description is too narrow shows no delta because it
never fires; one that is too broad shows always-on cost with no delta either.
Both failures are invisible without the baseline arm.

## Status: authored, not yet executed

`claude plugin eval` is in early access and refuses to run on the CLI these were
written against (`2.1.231`) — every invocation returns
`` `plugin eval` is currently in early access `` and exits without running
anything. The cases below follow the documented bare-template layout
(`prompt.md` + `graders/criteria.md`), but **no case here has been executed and
no score has been observed.** Treat the criteria as a first draft to be tuned
once the runner is available, not as a passing suite.

## Cases

| Case | Skill under test | The failure it is aimed at |
| ------------------- | ---------------- | ------------------------------------------------------------------------- |
| `share-screenshot` | `sharing` | Writing an artifact to disk and pasting the path instead of sharing it |
| `remember-decision` | `brain` | Recording durable context in chat, where the next agent cannot find it |
| `delegate-work` | `subagents` | Doing independent parallelizable work serially in one session |
| `repo-script-tool` | `repo-tools` | Re-teaching each agent a shell command instead of publishing it as a tool |

`sharing` is the sharpest test in the set: the rule it encodes already exists in
two always-on places in Dispatch (launch guidance and repo instructions) and
still gets ignored, so a measurable delta there is evidence that arriving at the
moment of action beats more background text.
Loading
Loading