diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json new file mode 100644 index 00000000..2e013591 --- /dev/null +++ b/.agents/plugins/marketplace.json @@ -0,0 +1,16 @@ +{ + "name": "dispatch", + "interface": { + "displayName": "Dispatch" + }, + "plugins": [ + { + "name": "dispatch", + "source": { + "source": "local", + "path": "./plugins/dispatch" + }, + "category": "workflow" + } + ] +} diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 00000000..cd7cba13 --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -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"] + } + ] +} diff --git a/README.md b/README.md index b121f7ff..802e3695 100644 --- a/README.md +++ b/README.md @@ -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** diff --git a/apps/server/test/plugin-manifest.test.ts b/apps/server/test/plugin-manifest.test.ts new file mode 100644 index 00000000..283c31dc --- /dev/null +++ b/apps/server/test/plugin-manifest.test.ts @@ -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 { + 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 `@`. 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>; + const codexPlugins = codex.plugins as Array>; + 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; + + 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; + return sum + slug.length + (frontmatter.description as string).length; + }, 0); + + expect(total).toBeLessThan(2891); + }); +}); diff --git a/plugins/dispatch/.claude-plugin/plugin.json b/plugins/dispatch/.claude-plugin/plugin.json new file mode 100644 index 00000000..2ae8da0e --- /dev/null +++ b/plugins/dispatch/.claude-plugin/plugin.json @@ -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"] +} diff --git a/plugins/dispatch/.codex-plugin/plugin.json b/plugins/dispatch/.codex-plugin/plugin.json new file mode 100644 index 00000000..3dbd64da --- /dev/null +++ b/plugins/dispatch/.codex-plugin/plugin.json @@ -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" + } +} diff --git a/plugins/dispatch/README.md b/plugins/dispatch/README.md new file mode 100644 index 00000000..b38ed993 --- /dev/null +++ b/plugins/dispatch/README.md @@ -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//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. diff --git a/plugins/dispatch/evals/README.md b/plugins/dispatch/evals/README.md new file mode 100644 index 00000000..77e3218a --- /dev/null +++ b/plugins/dispatch/evals/README.md @@ -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. diff --git a/plugins/dispatch/evals/delegate-work/graders/criteria.md b/plugins/dispatch/evals/delegate-work/graders/criteria.md new file mode 100644 index 00000000..c02422cc --- /dev/null +++ b/plugins/dispatch/evals/delegate-work/graders/criteria.md @@ -0,0 +1,35 @@ +Grade the response on whether it recognizes this as delegable work and uses +Dispatch's agent-launching mechanism, with briefings good enough for a child +agent that has none of this context. + +**Pass criteria:** + +1. The response uses `dispatch_launch_agent` to run the independent handlers + concurrently, rather than implementing all four serially in this session. +2. Each launch prompt is a standalone briefing — it names the deliverable and + the relevant paths, rather than referring to "the endpoints we discussed" or + otherwise assuming shared context. +3. The response accounts for the shared files (the route module and schema). + Acceptable handling includes giving each child its own worktree, or doing the + shared edits itself before or after the fan-out. Simply ignoring the overlap + is a fail. +4. The response says how it will collect results — `list_agents`, + `dispatch_send_message`, or waiting for the children to report. + +**Fail if:** + +- All four endpoints are implemented inline with no delegation considered, given + the stated concern about wall-clock time. +- Children are launched with one-line prompts that assume they can see this + conversation. +- The response describes spawning agents in the abstract without calling + `dispatch_launch_agent`. + +**Do not penalize:** deciding to do the shared route-module and schema edits +first and only fanning out the four handlers, or launching fewer than four +children with a stated reason. Judgment about how much to parallelize is fine; +not knowing delegation is available is not. + +Score 1.0 when all four criteria hold, 0.5 when it delegates but the briefings +are context-dependent or the shared files are unaddressed, 0.0 when it does not +delegate at all. diff --git a/plugins/dispatch/evals/delegate-work/prompt.md b/plugins/dispatch/evals/delegate-work/prompt.md new file mode 100644 index 00000000..e7380b1d --- /dev/null +++ b/plugins/dispatch/evals/delegate-work/prompt.md @@ -0,0 +1,8 @@ +You are working in a Dispatch agent session on an API service. + +The user wants four new endpoints implemented: `GET /reports`, `POST /reports`, +`GET /reports/:id`, and `DELETE /reports/:id`. They share a route module and a +schema file but the handlers are independent of each other, and each one needs +its own tests. The user has said they care about wall-clock time. + +Decide how to approach this and start. diff --git a/plugins/dispatch/evals/remember-decision/graders/criteria.md b/plugins/dispatch/evals/remember-decision/graders/criteria.md new file mode 100644 index 00000000..6dbfb16c --- /dev/null +++ b/plugins/dispatch/evals/remember-decision/graders/criteria.md @@ -0,0 +1,33 @@ +Grade the response on whether the decision is written somewhere a _different_ +agent in a _later_ session can find it. + +**Pass criteria — all must hold:** + +1. The response writes the decision to Dispatch's shared brain — a + `brain_store_object` call (or `brain_append_event` for a dated decision + record) with a `collection` and `name`. +2. The stored value captures the decision _and its reasoning_, including the + measured contention rationale and the fact that advisory locks and a + serialized write queue were rejected. A bare "use optimistic concurrency" + with no why is not sufficient. +3. The `collection` name is plausible and guessable by another agent (something + like `decisions` or `architecture-decisions`), not an opaque or + session-specific string. + +**Fail if:** + +- The decision is only summarized in the chat reply. +- The response says it will "remember" the decision without any persistence + call. +- The only persistence is a local file or a code comment, with no brain write. + +**Do not penalize:** + +- Also writing an ADR or repo doc _in addition to_ the brain write. +- Reading with `brain_list_objects` / `brain_get_object` first to check whether + a record already exists — that is correct behavior. +- Omitting `expectedRevision` when creating a new object. + +Score 1.0 when the decision plus its rationale lands in the brain under a +discoverable collection, 0.5 when it lands in the brain but loses the rejected +alternatives or the reasoning, 0.0 when nothing is persisted. diff --git a/plugins/dispatch/evals/remember-decision/prompt.md b/plugins/dispatch/evals/remember-decision/prompt.md new file mode 100644 index 00000000..a382e1a3 --- /dev/null +++ b/plugins/dispatch/evals/remember-decision/prompt.md @@ -0,0 +1,12 @@ +You are working in a Dispatch agent session on a service repo. + +After a long investigation you and the user settled a design question: the +service will keep using optimistic concurrency on the `orders` table rather than +switching to row-level locks, because the contention measured under load was +lower than the lock overhead. Two alternatives were explicitly rejected — +advisory locks and a serialized write queue. + +Other agents will work on this repo over the next few weeks and some of them will +reach the same fork in the road. + +Make sure this decision is available to them. diff --git a/plugins/dispatch/evals/repo-script-tool/graders/criteria.md b/plugins/dispatch/evals/repo-script-tool/graders/criteria.md new file mode 100644 index 00000000..bf9c9ec8 --- /dev/null +++ b/plugins/dispatch/evals/repo-script-tool/graders/criteria.md @@ -0,0 +1,31 @@ +Grade the response on whether it publishes the script as a first-class Dispatch +repo tool rather than writing more documentation nobody reads. + +**Pass criteria:** + +1. The response creates or edits `.dispatch/tools.json` at the repo root. +2. It adds a `tools` entry with `name`, `description`, and a `command` array + (`["./bin/stack", "up"]` or equivalent). +3. The two flags become `params` entries with correct types — `live` as + `boolean` with `flag: "--live"`, `port` as `string` with `flag: "--port"`. +4. The `description` explains what the command does and when to use it, written + for an agent that has never seen this repo — not merely restating the tool's + name. + +**Fail if:** + +- The response only edits `CONTRIBUTING.md`, a `CLAUDE.md`/`AGENTS.md`, or + another docs file. +- It proposes a shell alias, a Makefile target, or a git hook instead. +- It writes a tools manifest to a path other than `.dispatch/tools.json`. +- It types `live` as a string or gives a boolean param a value-taking flag. + +**Do not penalize:** + +- Also adding a `hooks.stop` teardown entry — that is a reasonable extension. +- Naming the tool `stack_up` (it is exposed as `repo_stack_up`) or any other + sensible name. +- Additionally tightening the `CONTRIBUTING.md` prose alongside the manifest. + +Score 1.0 when all four criteria hold, 0.5 when the manifest is created but the +param types or flags are wrong, 0.0 when the answer is documentation only. diff --git a/plugins/dispatch/evals/repo-script-tool/prompt.md b/plugins/dispatch/evals/repo-script-tool/prompt.md new file mode 100644 index 00000000..864609e4 --- /dev/null +++ b/plugins/dispatch/evals/repo-script-tool/prompt.md @@ -0,0 +1,9 @@ +You are working in a Dispatch agent session on a repo you have used before. + +Three times this week — across three different sessions — an agent has had to be +told the same thing: to bring up a local stack you run `./bin/stack up`, and it +takes `--live` to use the real runtime and `--port` to pin a port. It is written +down in a section of `CONTRIBUTING.md` that nobody reads, and agents keep either +guessing at the flags or starting the services by hand. + +Make sure the next agent in this repo does not have to be told. diff --git a/plugins/dispatch/evals/share-screenshot/graders/criteria.md b/plugins/dispatch/evals/share-screenshot/graders/criteria.md new file mode 100644 index 00000000..493872d5 --- /dev/null +++ b/plugins/dispatch/evals/share-screenshot/graders/criteria.md @@ -0,0 +1,28 @@ +Grade the response on whether the screenshot is actually delivered to the user +through Dispatch, rather than merely referenced. + +**Pass criteria — all must hold:** + +1. The response uses `dispatch_share` (or states that it is calling + `dispatch_share`) to upload `/tmp/sidebar-overflow-fixed.png`. +2. A `description` is supplied, and it describes what the screenshot _shows_ — + the fixed overflow menu at a narrow viewport — rather than restating the + filename. +3. The response does not present the local filesystem path as the way for the + user to view the image. + +**Fail if any of these appear:** + +- The response tells the user to open, check, or find + `/tmp/sidebar-overflow-fixed.png` (or any local path) with no share call. +- The response describes the screenshot in prose as a substitute for delivering + it. +- The response uses a generic image-attachment or upload mechanism that is not + Dispatch's `dispatch_share`. + +**Do not penalize:** a short prose summary of the fix alongside the share, or +closing the browser, or emitting a status event. Those are correct behavior and +orthogonal to what is being measured here. + +Score 1.0 when all three pass criteria hold, 0.0 when the artifact is left +local-only. diff --git a/plugins/dispatch/evals/share-screenshot/prompt.md b/plugins/dispatch/evals/share-screenshot/prompt.md new file mode 100644 index 00000000..70e2c6fb --- /dev/null +++ b/plugins/dispatch/evals/share-screenshot/prompt.md @@ -0,0 +1,9 @@ +You are working in a Dispatch agent session on a web app. + +You have just finished fixing a CSS bug where the sidebar's overflow menu was +clipped at narrow viewports. You ran the app, drove it in a headless browser, and +captured a screenshot of the fixed state at a 375px viewport. The image is at +`/tmp/sidebar-overflow-fixed.png`. + +Report back to the user that the fix is done, and make sure they can actually see +the screenshot. diff --git a/plugins/dispatch/skills/brain/SKILL.md b/plugins/dispatch/skills/brain/SKILL.md new file mode 100644 index 00000000..ef109473 --- /dev/null +++ b/plugins/dispatch/skills/brain/SKILL.md @@ -0,0 +1,102 @@ +--- +name: brain +description: Record decisions, findings, or state in the repo's shared persistent store so they survive this session. Use when you need to remember something for later, look up what was already decided, or accumulate results across runs. +--- + +# Shared memory (the brain) + +Dispatch gives every repo a shared, persistent store that all agents working in +that repo can read and write. It is the only place where something you learn now +is still available to a different agent next week. + +Reach for it when you catch yourself thinking any of these: + +- "Someone should know this later" → store an object. +- "Was this already decided?" → read before you re-derive. +- "This run found three more of them" → push onto a list. +- "I want a record of what happened, not just the current state" → append an event. + +## Three shapes, three purposes + +| Shape | Mutable? | Use it for | +| ---------- | -------- | ---------------------------------------------------------------- | +| **Object** | yes | Current state of one named thing — a decision, a config, an idea | +| **List** | yes | An ordered, growing collection — a queue, an inbox, a backlog | +| **Event** | no | History — what happened, when, and what was observed | + +Everything is namespaced by `collection` (a topic) plus `name` (the item). Pick a +stable, descriptive collection name and reuse it; a collection nobody can guess is +a collection nobody will read. + +## Objects + +``` +brain_list_objects collection, namePrefix?, updatedAfter?, limit? +brain_get_object collection, name +brain_store_object collection, name, value, expectedRevision? +brain_delete_object collection, name +``` + +`brain_list_objects` truncates long strings — it is for finding things, not for +reading them. Call `brain_get_object` for the full value. + +**Writes use optimistic concurrency.** Omit `expectedRevision` to create. To +update, pass the `revision` you got from your read. A blind overwrite of an +existing object is rejected on purpose: two agents editing the same object is +normal in Dispatch, and last-write-wins would silently destroy the other one's +work. If the write fails on revision mismatch, re-read, merge, and retry — do not +retry with a bumped number. + +`value` is a JSON object. Give it a small, consistent shape and keep using it — +something like `{title, status, details, updated}` — so a later reader can scan a +collection without opening every item. + +## Lists + +``` +brain_list_push collection, name, items, maxItems? +brain_list_get collection, name, offset?, limit? +brain_get_list_item collection, name, index +brain_list_set collection, name, index, value +brain_list_remove collection, name, index | where { field, equals } +brain_list_delete collection, name +``` + +`maxItems` caps the list and rolls the oldest entries off — the right way to keep +a rolling log of the last N results without a cleanup pass. `brain_list_get` +reports indexes and truncates long values; `brain_get_list_item` returns one entry +in full. + +`brain_list_remove` takes either an `index` or a `where` object — `{ field, +equals }`, matching the first item whose top-level `field` equals that string. +Indexes shift as items are removed, so prefer `where` when you are removing by +identity rather than by position. + +## Events + +``` +brain_append_event collection, kind, subject?, tags?, value +brain_query_events collection?, kind?, subject?, tags?, since?, until?, limit? +brain_get_event id +brain_delete_events ids | (collection + filters), dryRun? +``` + +Events are append-only. Use them when the sequence matters — assessments over +time, decisions with a date, observations you want to trend. `kind` is the event +type, `subject` is what it is about; both are how you find it again, so set them +deliberately. + +Deletion is permanent and unscoped filter deletes reach further than you expect +(kinds and subjects are reused across collections). Always run a filter delete +with `dryRun: true` first and check the match count. + +## Conventions that make the brain usable + +- **Read before you write.** It is also just good manners: someone may have + already recorded the thing you are about to record. +- **Absolute dates, not relative ones.** "Last Tuesday" is meaningless to the + agent that reads it in a month. +- **Name the agent or PR that produced a finding** inside the value, so a reader + can trace it back. +- **Don't store what the repo already records.** Code structure, git history, and + file contents are cheaper to read directly than to keep in sync. diff --git a/plugins/dispatch/skills/jobs/SKILL.md b/plugins/dispatch/skills/jobs/SKILL.md new file mode 100644 index 00000000..4227aeb9 --- /dev/null +++ b/plugins/dispatch/skills/jobs/SKILL.md @@ -0,0 +1,121 @@ +--- +name: jobs +description: Run agent work on a schedule with structured pass/fail reporting and notifications. Use for recurring or unattended work — nightly triage, release babysitting, cleanup — or when a run's outcome must be machine-readable. +--- + +# Jobs: scheduled and monitored agent runs + +A job is automation layered on a template. The template supplies the agent +configuration — prompt, agent type, directory, worktree settings. The job adds +everything that makes an unattended run safe: a cron schedule, timeouts, a +one-at-a-time guarantee, structured reporting, auto-archive, and notifications. + +**Template or job?** A template is for work a human launches — quick-start +workflows out of the command palette. A job is for work that runs on its own and +whose outcome someone needs to check later without reading a transcript. If +nobody is watching when it runs, it wants to be a job. + +Jobs need a backing template, so start with the `templates` skill if one does not +exist yet. + +## Tools + +``` +list_jobs — scoped to a directory; prompts omitted (length only) +get_job — one job by ID, or by name within a directory +create_job — only `name` is required; everything else has a default +update_job — identified by name (+ directory); pass only what changes +delete_job — fails if the job has an active run +run_job — trigger a run now; returns the run ID and agent ID immediately +``` + +Jobs are unique per (`directory`, `name`). + +## Configuration + +| Field | Meaning | +| --------------------- | ----------------------------------------------------------------- | +| `templateId` | The backing template supplying agent config | +| `defaultArgs` | Values for the template's arguments on scheduled runs | +| `schedule` | Cron expression. Optional — a job without one still runs manually | +| `timeoutMs` | Wall-clock ceiling for a run (default 30 min) | +| `needsInputTimeoutMs` | How long a run may sit waiting on a human (default 24 h) | +| `singleton` | Default true: only one run active at a time | +| `autoArchive` | Archive the spawned agent when the run completes | +| `enabled` | False skips the cron schedule; manual runs still work | + +Leave `singleton` on unless overlapping runs are genuinely safe — for anything +that touches a branch, a container, or a shared resource, they are not. + +`enabled: false` is the right way to pause a job you are debugging. Deleting and +recreating loses its history. + +## Run lifecycle + +``` +started → running → completed | failed | needs_input | timed_out | crashed +``` + +The runner creates an agent named `job--` and waits for it to +make a terminal MCP call. A run that ends without one is not "done" — it +eventually times out and reports as such. + +## Reporting, from inside a job agent + +If you are the agent running a job, these are yours: + +``` +job_log — append a structured log line (debug|info|warn|error) to a named task +job_complete — terminal: the run succeeded +job_failed — terminal: the run failed +job_needs_input — pause the run pending a human answer +``` + +The report shape: + +```json +{ + "status": "completed", + "summary": "Processed 3 pull requests.", + "tasks": [ + { + "name": "triage-pr-123", + "status": "success", + "summary": "Re-ran CI, posted comment" + }, + { + "name": "triage-pr-124", + "status": "error", + "summary": "Rebase failed", + "errors": [ + { "message": "Merge conflict in server.ts", "recoverable": false } + ] + } + ] +} +``` + +**One task per unit of work, named stably across runs.** That is what makes a +job's history readable — "this task has failed four nights running" is only +visible if the name stayed the same. `job_log` creates a task if it does not +exist yet, so use it for progress between terminal calls. + +Mark `recoverable` honestly. It is the signal that decides whether a human needs +to look tonight. + +Limits: 1 MB per report, 100 tasks, 500 logs per task, 10 KB summary and error +strings, 5 KB log messages. + +`job_needs_input` pauses the run rather than failing it — the right call when a +decision genuinely requires a human. An unanswered one times out per +`needsInputTimeoutMs`. + +## Notifications + +Jobs send their own Slack messages, with independent webhook lists per event: +`onComplete` (finished successfully), `onError` (failed, timed out, or crashed), +and `onNeedsInput` (waiting on a human). The job agent's own per-agent +notifications are suppressed so completion does not notify twice. + +Wire `onError` and `onNeedsInput` at minimum. A job whose failures are silent is +worse than no job — it looks like coverage without providing any. diff --git a/plugins/dispatch/skills/personalities/SKILL.md b/plugins/dispatch/skills/personalities/SKILL.md new file mode 100644 index 00000000..de63bc0f --- /dev/null +++ b/plugins/dispatch/skills/personalities/SKILL.md @@ -0,0 +1,74 @@ +--- +name: personalities +description: Change how Dispatch agents communicate — tone, verbosity, how much they narrate. Use when the user comments on the way agents talk rather than what they do — too wordy, be blunter, skip the preamble. +--- + +# Personalities: how agents communicate + +A personality is a short saved instruction that shapes the **voice** of every +standard agent launched afterward. It is about delivery — tone, length, how much +process gets narrated — not about capability or workflow. + +The signal is a complaint about form rather than substance: "too wordy", "stop +apologizing", "just give me the answer", "I want the reasoning spelled out". If +the user is asking agents to _do_ something differently, that is a template +prompt or repo guidance, not a personality. + +Personalities are unrelated to review personas. Personas are reviewers with a +domain lens (see the `personas` skill); personalities are the house style. + +## Tools + +``` +list_personalities — saved personalities plus the currently active ID +create_personality name, prompt — saved but NOT activated +update_personality id, name?, prompt? +set_active_personality id +clear_active_personality — back to no personality text +delete_personality id +``` + +`prompt` is capped at 1000 characters, `name` at 80. **Creating does not +activate** — call `set_active_personality` as a second step, or the user will +wonder why nothing changed. + +Activation applies to **subsequently launched** standard agents. It does not +retroactively change a session already running, including yours. Say so when you +set one, otherwise the user tests it in the current session and concludes it is +broken. + +## Writing one + +The 1000-character budget is small on purpose. Spend it on rules that change +observable output: + +``` +Lead with the answer, then the reasoning. No preamble, no restating the +question. Prefer a short paragraph over a bulleted list unless the content is +genuinely a list. Never apologize for a mistake — just state the correction and +move on. When you are unsure, say which part you are unsure about instead of +hedging the whole answer. +``` + +What works: + +- **Directives about output shape.** Length, ordering, formatting, what to omit. +- **Named anti-patterns.** "Don't open with 'Great question'" beats "be natural" + — a concrete prohibition is checkable, a vibe is not. +- **A stated default with an escape hatch.** "Default to three sentences; expand + when the answer genuinely needs it" avoids terse-but-useless replies. + +What does not: + +- Workflow rules ("always run the tests"). Those belong in repo guidance, where + they apply regardless of who is talking. +- Tool instructions. A personality is loaded for every agent; tool guidance + belongs in a skill that loads when it is relevant. +- Long persona fiction. Backstory burns the budget and rarely changes output. + +## Managing them + +Keep a small set with names that say when to use them — "Terse", "Explain +Everything", "Pairing" — rather than one that gets rewritten every time the mood +changes. `clear_active_personality` returns to the default voice; reach for it +before assuming a personality is at fault for something. diff --git a/plugins/dispatch/skills/personas/SKILL.md b/plugins/dispatch/skills/personas/SKILL.md new file mode 100644 index 00000000..6e611089 --- /dev/null +++ b/plugins/dispatch/skills/personas/SKILL.md @@ -0,0 +1,107 @@ +--- +name: personas +description: Author repository-specific reviewers in .dispatch/personas/. Use when generic review keeps missing this repo's real risks, or when a reviewer would need domain rules nobody has written down. +--- + +# Authoring review personas + +A persona is a reviewer with a point of view, stored as a markdown file in the +repo. Dispatch ships a built-in generalist (`code-review`) that works anywhere; +a repo-specific persona is what you write when the generalist keeps missing +things only this codebase knows about — a migration invariant, a tenancy +boundary, a wire format two services agree on. + +Launching an existing persona is covered by the `review-workflow` skill. This +skill is about creating one. + +## When it is worth writing + +- The same class of defect keeps reaching main. +- A correct-looking change can still be wrong for reasons that live in the + domain, not the diff. +- Onboarding docs describe rules that reviewers should be checking mechanically. + +If you cannot name the specific failure the persona would catch, you do not need +a persona yet — use `code-review`. + +## Tools + +``` +list_personas — effective list: repo personas plus the built-in generalist +persona_templates — starting points with the exact authoring fields +persona_upsert — create or update a persona in .dispatch/personas/ +persona_validate — check personas parse and have required fields +``` + +Call `list_personas` first. A near-match you can sharpen beats a new file, and +duplicate reviewers with overlapping scope produce duplicate findings. + +## Templates + +`persona_templates` returns three starting points: + +| id | For | +| --------------- | ---------------------------------------------------------------- | +| `code-review` | Correctness and maintainability — a focused engineering reviewer | +| `product-ux` | User-facing flows, wording, empty/error states, accessibility | +| `domain-review` | A deliberately blank frame for a repo-specific expert reviewer | + +`domain-review` is the one to start from for anything genuinely repo-specific: +its instructions are a placeholder telling you to replace them with the business +rules, invariants, data boundaries, and failure modes reviewers should check. + +## Writing one + +``` +persona_upsert slug: "migration-safety", + template: "domain-review", + name: "Migration Safety", + description: "Reviews schema and data migrations for irreversible or blocking operations.", + instructions: "…", + feedbackFormat: "findings" +``` + +- **`slug`** — lowercase letters, numbers, single hyphens, max 80 chars. It is + the filename and the launch identifier. +- **`description`** — how anyone (human or agent) decides whether to launch this + reviewer. Name the risk it covers, not the role it plays. +- **`instructions`** — the persona's whole brief. Write the checklist you wish + someone had handed you: the invariants, what "wrong" looks like concretely, + which failure modes are cheap to miss. +- **`feedbackFormat`** — single-line, defaults to `findings`. + +Writes land in `.dispatch/personas/.md` inside the current workspace, and +only there — the writer refuses symlinked directories and paths that escape the +workspace. Rendered file: + +```markdown +--- +name: Migration Safety +description: Reviews schema and data migrations for irreversible or blocking operations. +feedbackFormat: findings +--- + + +``` + +You can also write the file by hand; `persona_upsert` just gets the frontmatter +right. Run `persona_validate` afterward either way. + +## What makes instructions actually work + +- **State the invariants, not the vibe.** "Every migration must be reversible or + explicitly marked irreversible with a rollback note" is checkable. "Be careful + with migrations" is not. +- **Scope it to the diff.** Say explicitly that only issues introduced or + worsened by the reviewed change are in scope. Without that line, personas + report pre-existing debt and bury the real finding. +- **Ask for impact and a fix.** Require each finding to name the concrete failure + scenario and point at the smallest useful change. +- **Keep each persona narrow.** Two sharp reviewers find more than one broad one, + and their findings barely overlap. + +## Worktree precedence + +Personas resolve from the agent's worktree first, then the repo root, then the +built-ins. A persona edited inside a worktree takes effect for that agent +immediately — which is how you iterate on one before committing it. diff --git a/plugins/dispatch/skills/repo-tools/SKILL.md b/plugins/dispatch/skills/repo-tools/SKILL.md new file mode 100644 index 00000000..2455f9d8 --- /dev/null +++ b/plugins/dispatch/skills/repo-tools/SKILL.md @@ -0,0 +1,108 @@ +--- +name: repo-tools +description: Expose a repo's own scripts to agents as first-class tools, and run cleanup on agent stop, via .dispatch/tools.json. Use when you keep re-running the same shell command across sessions. +--- + +# Repo-specific tools and hooks (`.dispatch/tools.json`) + +Every repo can publish its own MCP tools to the agents working in it. Drop a +`.dispatch/tools.json` at the repo root and each entry becomes a real tool in the +agent's tool list — discoverable without anyone documenting it in a prompt. + +The symptom that means you want this: agents keep rediscovering the same shell +incantation, or a README paragraph keeps getting ignored because nothing surfaces +it at the moment of use. + +## File shape + +```json +{ + "hooks": { + "stop": { + "command": ["./bin/dev", "down"], + "description": "Tear down the agent's isolated dev environment on stop." + } + }, + "tools": [ + { + "name": "dev_up", + "description": "Start the repo's isolated dev environment on free ports.", + "command": ["./bin/dev", "up"], + "scope": ["agent"], + "params": [ + { + "name": "live", + "type": "boolean", + "flag": "--live", + "description": "Enable the live agent runtime instead of inert mode." + }, + { + "name": "cwd", + "type": "string", + "flag": "--cwd", + "description": "Working directory override (e.g. a worktree path)." + } + ] + } + ] +} +``` + +## Tool entries + +| Field | Required | Notes | +| ------------- | -------- | --------------------------------------------------------------------------- | +| `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 | +| `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. + +**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 +the command does and when to reach for it — not just what it is named. + +## Params + +Each param becomes a flag appended to `command`: + +- `type: "string"` → appends ` ` when a non-empty value is passed. +- `type: "boolean"` → appends `` only when the value is `true`. +- Omitted or null values append nothing. + +`name`, `type`, and `flag` are all required; a param missing any of them fails to +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. +**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. + +## Hooks + +`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. + +## Working on it + +The tools manifest is re-read from disk on **every** MCP request — there is no +caching — so an edited command, description, or param takes effect on the next +tool listing with no server restart. (The mtime cache in Dispatch applies only to +`hooks`, not to `tools`.) + +The catch is on the client side: an agent's CLI fetches its tool list once at +session start and holds it. So an edit to an **existing** tool is picked up by +the server immediately, but a **newly added** tool usually will not be callable +by an already-running agent until it reconnects or a new session starts. + +A malformed entry (missing `name`, `description`, or `command`) throws at load, +which surfaces as the repo's tools being absent rather than as a parse error. If +`repo_*` tools vanish, validate the JSON first. diff --git a/plugins/dispatch/skills/review-workflow/SKILL.md b/plugins/dispatch/skills/review-workflow/SKILL.md new file mode 100644 index 00000000..211f2d91 --- /dev/null +++ b/plugins/dispatch/skills/review-workflow/SKILL.md @@ -0,0 +1,117 @@ +--- +name: review-workflow +description: Open a pull request and get the change reviewed in Dispatch, then work the findings. Use when wrapping up a change, about to open a PR, or when review feedback has come back to respond to. +--- + +# Pull requests and review in Dispatch + +Dispatch has its own PR and review path. Two habits it overrides: + +1. **Open PRs with `create_pr`**, not with a built-in PR skill and not with the + `gh` CLI. `create_pr` is what registers the PR with Dispatch, so it shows up + in the UI and in review tracking. +2. **Get reviewed by launching a persona**, not by re-reading your own diff. + Reviews launched with `dispatch_launch_persona` come back as structured, + trackable feedback items with their own discussion threads. + +## Opening the PR + +``` +create_pr title?, body?, baseBranch?, draft?, fillFromCommits? +get_pr_status — status details for an existing PR +``` + +`baseBranch` defaults correctly for the current worktree. **Do not override it** +unless you specifically mean to target something other than the repo's default +branch — an overridden base is the usual cause of a PR containing someone else's +commits. + +Commit and push your branch before calling it. Pin the returned PR link so the +user can reach it from the sidebar. + +## Getting it reviewed + +``` +list_personas — what reviewers exist here, with their descriptions +dispatch_launch_persona persona, context, includeDiff?, agentType?, model? +``` + +Call `list_personas` first and **launch one reviewer per distinct scope the +change touches** — a change spanning backend and frontend gets both; anything +cross-cutting or introducing a new module also gets an architecture pass. +Reviewers with different lenses barely overlap in what they find, and the one you +almost skipped is often the one that finds the real defect. Launch them in the +same turn rather than serially. + +If nothing matches well, launch the closest persona anyway and say so plainly in +the briefing. Skipping review because the fit is imperfect is worse than an +imperfect reviewer. To write a better-fitting one, see the `personas` skill. + +### The briefing is the whole game + +`context` is what separates a review that finds defects from one that returns a +summary. Include: + +- **What changed**, and the key files — actual paths. +- **What is out of scope**, explicitly. Otherwise reviewers flag pre-existing + issues and you spend a round sorting them out. +- **Decisions already made, and the alternatives that were rejected.** Without + this, reviewers re-propose the rejected option and you relitigate a settled + call. +- **The specific properties you want attacked** — the edge cases in new parsing + logic, the trust boundary a caller-supplied value now crosses, the invariant a + shared helper now owns. A briefing that only describes the change gets a + summary back; one that poses questions gets findings. + +Set `includeDiff: false` only for non-code reviews (a plan, a document, media) +where the diff is not the review target. + +## Working the feedback + +``` +dispatch_review_list_feedback reviewId? — item ids, locations, status +dispatch_review_get_feedback id — full thread plus the captured diff hunk +dispatch_review_add_message id, message — reply in the item's thread +dispatch_review_resolve id — reviewer-side: mark fixed or dismissed +dispatch_review_reopen id — more work or discussion needed +``` + +`dispatch_review_list_feedback` finds items; `dispatch_review_get_feedback` gives +you the one you are about to work, including the diff hunk captured when it was +filed. + +**Keep all discussion in the item thread.** That is where the reviewer is +listening, and it keeps the finding, the fix, and the verification attached to +each other. + +**After fixing an item, ask the reviewer to verify it — do not resolve it +yourself.** Post a short `dispatch_review_add_message` saying what you changed; +the reviewer re-inspects and resolves, or replies with what is still missing. +Replies are capped around 600 characters: state the decision or result, and skip +restating the feedback or narrating the work. + +**Not every finding has to be accepted.** When you disagree, say so in the thread +with concrete evidence — what the system actually does, what the API or database +will actually accept. A reviewer given a real rebuttal will dismiss its own +finding, and that exchange is worth more than silently complying with a wrong +one. When a finding asserts a failure mode rather than pointing at visible broken +behavior, measure the real system to settle it rather than arguing in the +abstract. + +Verify each fix the same way you verified the original work. Collapsing two +constraints that merely looked alike, or hoisting an invariant into a shared +helper, is exactly how a review fix introduces a regression of its own. + +## Autonomous Review + +When Autonomous Review is enabled for a session, the loop is driven for you: +commit and push, open a draft PR with `create_pr`, launch the reviewer, then +**end the turn**. Do not poll, sleep, call `list_agents`, or schedule a wakeup — +Dispatch injects the review prompt when it is ready. A clean zero-item approval +needs no action; otherwise work the items above. Don't report the task complete +until every submitted review is resolved. + +## Cleaning up + +Once a reviewer's output is consumed, `dispatch_archive_agent` retires it. See +the `subagents` skill. diff --git a/plugins/dispatch/skills/sharing/SKILL.md b/plugins/dispatch/skills/sharing/SKILL.md new file mode 100644 index 00000000..250f98c8 --- /dev/null +++ b/plugins/dispatch/skills/sharing/SKILL.md @@ -0,0 +1,88 @@ +--- +name: sharing +description: Give the user a file, screenshot, log, or snippet they can actually open. Use whenever you produce an artifact worth seeing — writing it to disk and pasting the path does not surface it in Dispatch. +--- + +# Sharing artifacts with the user + +When you produce something the user should see — a screenshot, a diff, a +generated config, a log excerpt, a report — hand it over with `dispatch_share`. +It uploads the artifact into the Dispatch session, where it renders inline and +stays attached to the conversation. + +**The failure this prevents:** writing the file to `/tmp` and pasting the path. +That path is meaningless to a user reading the session in a browser, on a phone, +or on a different machine from the one the agent is running on. A local path is +not a deliverable. + +## Two ways to call it + +**Share a file that already exists:** + +``` +dispatch_share filePath: "/tmp/login-flow.png", + description: "Login flow after the redirect fix" +``` + +**Share text you are generating right now** — no temp file needed: + +``` +dispatch_share content: "…", + name: "migration-plan.md", + description: "Proposed migration order" +``` + +`name` is required with `content` and must carry a real extension — it drives +syntax highlighting and how the artifact renders. + +Supported: images (`png`, `jpg`, `jpeg`, `gif`, `webp`), video (`mp4`), documents +(`pdf`), and text (`txt`, `md`, `json`, `yaml`, `ts`, `py`, `go`, `rs`, `sh`, +`sql`, and similar). + +`source: "simulator"` captures directly from a booted iOS Simulator — pass +`simulatorUdid` to target a specific one, or leave it for the booted device. + +## Updating instead of duplicating + +Every share returns a `fileName`. Pass it back as `update` to replace the +contents in place: + +``` +dispatch_share filePath: "/tmp/report.md", + description: "Report — second pass", + update: "" +``` + +Use this for anything you regenerate — a report that gets refined, a screenshot +retaken after a fix. Five near-identical uploads make the session harder to read, +not more thorough. + +## Managing what you've shared + +``` +dispatch_list_media — metadata for this agent's shared files, including filePath +dispatch_delete_media fileName — permanently removes the file and its record +``` + +`dispatch_list_media` returns metadata only; read the content through `filePath` +with normal file tools. + +## Write a description that earns the click + +The description is the label the user sees before deciding to open it. Say what +the artifact _shows_, not what it is: + +- Weak: "screenshot.png" +- Strong: "Sidebar collapsed — the overflow menu no longer clips at 375px" + +## When to share + +- **Any screenshot from a browser or simulator run.** Never leave one local-only. +- **Before/after pairs** when you have fixed something visual — two shares beat a + paragraph describing the difference. +- **Long output** you would otherwise paste into chat: test failures, generated + files, query results. Shared, it stays readable and does not bury your summary. +- **Anything the user might want to forward.** A path cannot be forwarded. + +Keep the prose summary in your reply and put the bulk in the artifact. The reply +says what happened; the share is the evidence. diff --git a/plugins/dispatch/skills/subagents/SKILL.md b/plugins/dispatch/skills/subagents/SKILL.md new file mode 100644 index 00000000..1ea5144f --- /dev/null +++ b/plugins/dispatch/skills/subagents/SKILL.md @@ -0,0 +1,83 @@ +--- +name: subagents +description: Delegate work to other Dispatch agents and coordinate with running ones. Use when a task splits into independent parts, needs its own worktree, or when you must message or hand off to another agent. +--- + +# Launching and coordinating agents + +A Dispatch agent can launch other agents. Each one is a full agent — its own +session, its own working directory, optionally its own git worktree and branch — +not an in-process helper that shares your context. + +That difference decides when delegation is worth it: + +- **Worth it:** independent parts of a task that can run at the same time; work + that needs its own branch or worktree; a long-running background task; a + genuinely separate perspective on what you just built. +- **Not worth it:** anything you could finish faster yourself. A launch costs a + full CLI startup, and the child starts with none of your context — everything + it needs has to be written into the prompt. + +For _review_ specifically, launch a persona rather than a plain agent — see the +`review-workflow` and `personas` skills. + +## Launching + +``` +dispatch_launch_agent prompt, ... (see the tool schema for the full option set) +``` + +The prompt is the child's entire world. Write it as a standalone briefing: + +- What the task is, stated as a deliverable rather than a topic. +- Where the relevant code lives — actual paths, not "the auth stuff". +- What is explicitly **out** of scope. Children reliably expand scope when the + boundary is unstated. +- What decisions have already been made and should not be relitigated. +- How you want the result reported back. + +If a template already captures this launch configuration, launch from it instead +of retyping the prompt — see the `templates` skill. + +## Coordinating + +``` +list_agents — who exists, their IDs, names, statuses, latest activity +dispatch_send_message target, message +``` + +`dispatch_send_message` injects a message directly into the target's session, and +it can reply the same way. `target` accepts an agent ID (`agt_…`) or a name, which +is fuzzy-matched. **It only works for agents that are currently running** — a +message to a stopped agent goes nowhere, so check `list_agents` when a send fails +rather than assuming it was delivered. + +Messaging is for coordination, not for streaming progress. A parent that wants a +start and an end does not want twelve interim pings; fold the detail into the +final report. + +For results that need to outlive either session — a finding, a decision, an +accumulating list — write to the brain instead of messaging it. See the `brain` +skill. + +## Cleaning up + +``` +dispatch_archive_agent agentId +``` + +Archive a child once you have consumed its output. This is scoped to agents you +launched directly — archiving someone else's agent is rejected. It stops the +session and soft-deletes it, and it cannot be undone, so read the child's output +first. + +## Patterns that work + +- **Fan out, then reconcile.** Launch one agent per independent part, each in its + own worktree, then merge and run the full check suite yourself. Expect + conflicts where their files overlap and plan who resolves them. +- **Build, then review.** Finish the change, then launch a reviewer persona + against the diff rather than reviewing your own work. +- **Delegate the search, keep the conclusion.** A child that reads twenty files + and reports three sentences saves your context; one that reports the twenty + files does not. diff --git a/plugins/dispatch/skills/templates/SKILL.md b/plugins/dispatch/skills/templates/SKILL.md new file mode 100644 index 00000000..81ee72b1 --- /dev/null +++ b/plugins/dispatch/skills/templates/SKILL.md @@ -0,0 +1,92 @@ +--- +name: templates +description: Save a reusable agent launch configuration with fill-in-the-blank arguments, launchable from the command palette. Use when the user keeps starting the same kind of task, or to back a scheduled job. +--- + +# Templates: reusable agent launches + +A template is a saved agent launch — prompt, agent type, directory, worktree +settings — that a human can fire from the Cmd+K command palette or that a job can +run on a schedule. + +The signal that you want one: you just wrote a careful multi-paragraph launch +prompt, or the user has asked for the same shape of task a third time. Templates +are also the prerequisite for jobs — see the `jobs` skill. + +## Tools + +``` +list_templates — scoped to a directory; reports promptArgs and promptChars, not prompt bodies +get_template — one template by ID, or by name within a directory +create_template — only `name` is required +update_template — pass only the fields you want to change +delete_template — fails if any job references it +``` + +Templates are unique per (`directory`, `name`). `list_templates` deliberately +omits prompt bodies — call `get_template` for the one you actually want. + +## Fields + +| Field | Meaning | +| ------------- | ---------------------------------------------------- | +| `name` | Display name, unique within `directory` | +| `description` | Shown in Cmd+K and launch views | +| `directory` | Absolute path of the repo this template runs against | +| `prompt` | The agent's first turn | +| `agentType` | `claude`, `codex`, `cursor`, or `opencode` | +| `model` | Optional model id, matching the agent type | +| `useWorktree` | Give the agent its own git worktree | +| `baseBranch` | Base branch for that worktree | +| `branchName` | Branch name for that worktree | +| `fullAccess` | Pass the CLI's full-access / bypass-approvals flag | +| `callable` | Show it in the Cmd+K command palette | + +Set `useWorktree` for anything that writes code. Agents sharing a working tree +overwrite each other's changes, and the damage is silent. + +## Runtime arguments + +Put `{{D:Arg Name}}` placeholders in the prompt and they become fields at launch: + +``` +Review the PR at {{D:PR URL|required}} and focus on {{D:Review Focus|multiline}}. +``` + +- Arguments are **optional by default**. +- `|required` makes it mandatory at launch. +- `|multiline` (or `|textarea`) renders a textarea instead of a single-line input. +- Repeated occurrences merge their modifiers — required or multiline anywhere + applies everywhere. + +Argument values are also pinned to the launched agent's sidebar for reference. + +**Write prompts that still read correctly with optional arguments blank.** A +blank optional argument has its placeholder removed and the surrounding text left +as-is, so `focus on {{D:Review Focus}}.` becomes a dangling `focus on .` Phrase it +so the sentence survives — or make the argument required. + +`dispatch_launch_agent` accepts `templateArgs` for launching a template +programmatically; `get_template`'s `promptArgs` field tells you which argument +names it expects. + +## The command palette + +Templates with `callable: true` appear in Cmd+K under "Templates": + +- **No arguments** → a confirmation step; Enter twice launches it. +- **With arguments** → a launch dialog for filling values in first. + +Set `callable: false` for templates that exist only to back a job — otherwise the +palette fills with entries nobody launches by hand. + +## Writing the prompt + +The launched agent starts with none of your context, so the prompt is its entire +briefing. The guidance in the `subagents` skill applies directly: state the +deliverable, name real paths, say what is out of scope, and record decisions that +should not be relitigated. + +One extra consideration specific to templates: they run again months from now. +Avoid anything that goes stale — "the PR we discussed", "the current sprint", +today's date. Anything that varies per run belongs in an argument. diff --git a/plugins/dispatch/skills/ui-validation/SKILL.md b/plugins/dispatch/skills/ui-validation/SKILL.md new file mode 100644 index 00000000..0b22dda4 --- /dev/null +++ b/plugins/dispatch/skills/ui-validation/SKILL.md @@ -0,0 +1,79 @@ +--- +name: ui-validation +description: Prove a UI change works before calling it done. Use after changing any layout, style, component, or user-facing flow — drive it in a real browser instead of trusting a static render. +--- + +# Validating UI changes in a browser + +A UI change is not finished when it compiles. It is finished when someone has +watched it behave. In Dispatch that means driving the running app with Playwright +and handing the user screenshots they can look at. + +Applies to any change to layout, styling, a component, or a user-facing flow — +including ones that "obviously can't break anything". + +## The loop + +1. **Run the app.** Use the repo's own dev tooling; if the repo exposes + `repo_*` tools for it (see the `repo-tools` skill), use those rather than + starting a server by hand. +2. **Drive the changed path**, don't just load the page. +3. **Screenshot the meaningful states.** +4. **Share them with `dispatch_share`** — see the `sharing` skill. A screenshot + left on disk was never delivered. +5. **`browser_close` when done.** Leaving browsers open wastes resources, + especially on headless machines. Close before your final status event. + +## Exercise states, not renders + +A static screenshot of the initial render is the weakest possible evidence. Drive +the interaction: + +- **Every toggle in both directions** — opened _and_ closed, enabled _and_ + disabled. Half of layout regressions only appear on the way back. +- **Persisted state across a reload** — if something is meant to survive a + refresh, refresh it. +- **Empty, loading, and error states**, not only the happy path. +- **Overlays and z-index.** These are a recurring source of bugs that clicks + catch and static review misses. **A click that times out because another + element intercepts it is a real bug, not test flakiness** — chase it rather + than retrying with a different selector. + +## Both layouts + +A change scoped to one layout must be verified _unchanged_ on the other. Desktop +work that quietly reflows mobile is common and nobody notices until a user does. +A before/after pixel comparison at the other layout's viewport is cheap and +decisive. + +## Waiting for readiness + +**Do not use `networkidle`** on pages with SSE or WebSocket activity — the +connection never goes idle, so the wait burns its full timeout and then reports a +failure that has nothing to do with your change. + +Use `domcontentloaded` (or `load`), then wait for a concrete UI-ready signal: a +specific control being visible, a piece of text appearing, a state class landing. +Wait for the thing you actually need, not for the network to go quiet. + +## Capturing screenshots worth reading + +- Set **`deviceScaleFactor: 2`** or higher in the browser context. 1x captures + are unreadable once shared, and higher still is right for mobile viewports. +- **Capture before and after** when fixing something visual. Two images beat a + paragraph describing a difference. +- Screenshots from Playwright MCP land in the repo root — **move or delete them + before committing** so no stray files end up in the diff. Use a temp directory + and share from there. + +## Default headless + +Run headless unless you specifically need to watch it. Headless is faster and +works on machines with no display. + +## What to report + +Say which states you exercised and share the images. "Validated in Playwright" +with nothing attached is not evidence. If something is still unverified — a state +you could not reach, a viewport you did not test — say so explicitly rather than +letting the screenshots imply full coverage. diff --git a/plugins/dispatch/skills/whiteboard/SKILL.md b/plugins/dispatch/skills/whiteboard/SKILL.md new file mode 100644 index 00000000..1a884d94 --- /dev/null +++ b/plugins/dispatch/skills/whiteboard/SKILL.md @@ -0,0 +1,238 @@ +--- +name: whiteboard +description: Draw on or read the shared whiteboard the user sketches on. Use when the user mentions the whiteboard, board, or drawing, or when a diagram would explain something better than more prose. +--- + +# The shared whiteboard + +Every Dispatch agent has a whiteboard: a live Excalidraw canvas the user can see +and draw on at the same time you can. Your edits appear immediately; so do +theirs. + +Two directions of use, and both matter: + +- **Reading** — the user sketched something and is referring to it. Anytime they + say "the whiteboard", "the board", "the diagram", or "what I drew", call + `whiteboard_get` before answering. +- **Drawing** — you have an architecture, a flow, or a set of relationships that + a picture explains faster than text. + +## Tools + +``` +whiteboard_get — current elements plus snapshotPath, a PNG of the board +whiteboard_howto — this reference, available at runtime +whiteboard_update — add or replace elements (merged by id); deleteIds removes +whiteboard_clear — wipe the board +``` + +`whiteboard_get` returns a simplified element list _and_ a `snapshotPath`. **Read +that PNG with a file/image tool** — freehand strokes are close to unreadable from +element JSON, and a user's rough sketch is usually freehand. If the response says +the snapshot is stale, trust the element list over the image until the board is +re-exported. + +`whiteboard_update` merges by `id`: an element whose id already exists is replaced +entirely, anything else is added. That makes iteration cheap — resend one box to +move it, rather than rebuilding the board. + +## Drawing workflow + +**Workflow:** Call `whiteboard_get` first to see current elements, their ids, and where free space is. Then construct your elements and send them to `whiteboard_update`. Give elements readable ids (e.g. 'api-box', 'db-node') so you can reference them in arrow bindings. + +**Labels:** To put text inside a shape, create BOTH a shape element (with a `boundElements` back-reference to the text) AND a text element (with `containerId` pointing to the shape). + +**Arrows:** Use `startBinding`/`endBinding` with `elementId` and `fixedPoint` to connect arrows to shapes. Add back-references in each target shape's `boundElements` array. Set `elbowed: true` for right-angle routing. + +**Layout:** Typical box w=160, h=70 with ~80px gaps. Keep labels short (2–5 words). + +## Excalidraw Element Format Reference + +Every element is a JSON object. Fields marked (required) must be present; all others have sensible defaults the editor will apply if omitted. + +### Common fields (all element types) + +| Field | Type | Required | Default | Notes | +| --------------- | ------------ | -------- | ------------- | ------------------------------------------------------------------------------ | +| id | string | YES | — | Unique id. Use readable slugs like "api-box", "db-node". | +| type | string | YES | — | One of: rectangle, ellipse, diamond, text, arrow, line, frame, freedraw, image | +| x | number | YES | — | Left edge, canvas pixels | +| y | number | YES | — | Top edge, canvas pixels | +| width | number | YES | — | Element width (use 0 for arrows/lines) | +| height | number | YES | — | Element height (use 0 for arrows/lines) | +| angle | number | no | 0 | Rotation in radians | +| strokeColor | string | no | "#1e1e1e" | Stroke/outline color (hex) | +| backgroundColor | string | no | "transparent" | Fill color (hex or "transparent") | +| fillStyle | string | no | "solid" | One of: solid, hachure, cross-hatch | +| strokeWidth | number | no | 2 | Stroke thickness in px | +| strokeStyle | string | no | "solid" | One of: solid, dashed, dotted | +| roughness | number | no | 1 | 0=smooth, 1=normal, 2=rough (hand-drawn look) | +| opacity | number | no | 100 | 0–100 | +| groupIds | string[] | no | [] | Group membership | +| frameId | string\|null | no | null | Parent frame id | +| roundness | object\|null | no | null | { type: 3 } for rounded corners on rectangles | +| seed | number | no | random | Random seed for roughness rendering | +| version | number | no | 1 | Bump on each edit | +| versionNonce | number | no | random | Random nonce, changes with version | +| isDeleted | boolean | no | false | Soft-delete flag | +| boundElements | array\|null | no | null | Back-references: [{ id, type }] | +| updated | number | no | Date.now() | Timestamp ms | +| link | string\|null | no | null | URL link | +| locked | boolean | no | false | Prevent editing | + +### Shape elements: rectangle, ellipse, diamond + +```json +{ + "id": "api-box", + "type": "rectangle", + "x": 100, + "y": 100, + "width": 160, + "height": 70, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "fillStyle": "solid", + "roundness": { "type": 3 } +} +``` + +Ellipse and diamond use the same fields, just change `type`. + +### Text elements + +```json +{ + "id": "title-text", + "type": "text", + "x": 100, + "y": 50, + "width": 200, + "height": 25, + "text": "API Server", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "originalText": "API Server" +} +``` + +**Bound text (label inside a shape):** Create a text element with `containerId` pointing to the shape, and add a back-reference in the shape's `boundElements`: + +```json +[ + { + "id": "box1", + "type": "rectangle", + "x": 100, + "y": 100, + "width": 160, + "height": 70, + "boundElements": [{ "id": "box1-label", "type": "text" }] + }, + { + "id": "box1-label", + "type": "text", + "x": 110, + "y": 120, + "width": 140, + "height": 25, + "text": "API", + "originalText": "API", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "box1" + } +] +``` + +Text font families: 1=Virgil (handwritten), 3=Cascadia (monospace), 5=Excalifont (default). + +### Arrow and line elements + +```json +{ + "id": "flow-arrow", + "type": "arrow", + "x": 260, + "y": 135, + "width": 140, + "height": 0, + "points": [ + [0, 0], + [140, 0] + ], + "startArrowhead": null, + "endArrowhead": "arrow", + "startBinding": { + "elementId": "api-box", + "focus": 0, + "gap": 1, + "fixedPoint": [1, 0.5] + }, + "endBinding": { + "elementId": "db-node", + "focus": 0, + "gap": 1, + "fixedPoint": [0, 0.5] + }, + "elbowed": false +} +``` + +**Points:** Array of [x, y] offsets relative to the element's x, y. First point is always [0, 0]. Add intermediate points for bends. + +**Arrowheads:** `startArrowhead` and `endArrowhead` can be: null, "arrow", "bar", "dot", "triangle", "diamond". + +**Bindings:** Connect arrows to shapes. `fixedPoint` is [proportionX, proportionY] on the target shape (0-1 range): [0.5, 0] = top center, [1, 0.5] = right center, [0.5, 1] = bottom center, [0, 0.5] = left center. + +When binding an arrow, also add a back-reference in the target shape's `boundElements`: + +```json +{ "id": "flow-arrow", "type": "arrow" } +``` + +**Elbow routing:** Set `elbowed: true` for right-angle connector routing (auto-computed path). The `points` array will be overridden by the editor. + +**Lines** use the same format but `type: "line"` and no arrowheads. + +### Frame elements + +```json +{ + "id": "frame1", + "type": "frame", + "x": 50, + "y": 50, + "width": 400, + "height": 300, + "name": "Backend Services" +} +``` + +Children are assigned to frames by setting their `frameId` to the frame's id. + +### Color reference + +**Stroke colors:** #1e1e1e (black/default), #e03131 (red), #2f9e44 (green), #1971c2 (blue), #f08c00 (orange), #6741d9 (violet), #0c8599 (cyan), #e8590c (dark orange), #868e96 (gray) + +**Pastel fills (good for shape backgrounds):** #a5d8ff (light blue), #b2f2bb (light green), #ffd8a8 (light orange), #d0bfff (light purple), #ffc9c9 (light red), #fff3bf (light yellow), #c3fae8 (light teal), #eebefa (light pink), #e5dbff (light violet) + +### Layout tips + +- Typical box: width 160, height 70 +- Leave ~80px gaps between shapes +- Center labels inside shapes using `containerId` + `boundElements` binding +- Keep labels short (2–5 words) — text wraps to shape width +- For arrows: set x, y to the start point, compute width/height from the last point offset +- Arrow width = last point's x offset, height = last point's y offset (can be negative) + +### Important notes + +- The editor auto-heals many issues (null fields, missing indices). Don't over-validate. +- Always provide `id`, `type`, `x`, `y` at minimum. Width and height default to 0 if omitted. +- Use readable, descriptive ids — you'll reference them in bindings and future updates. +- Elements are merged by id: sending an element with an existing id replaces it entirely.