Skip to content
Merged
48 changes: 48 additions & 0 deletions agents/dispatch/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# @corbits/code-agent-dispatch

## What this is

A ready-to-go Corbits agent for the **dispatch** role (`@corbits/code-agent-dispatch`)
plus its named component parts so a consumer can recreate or remix it. Dispatch is
the Tier-1 primary orchestrator: it classifies work, DIYs obvious mechanical
corrections, and spawns named specialists.

## Consume (runLocal)

```ts
import { agent } from "@corbits/code-agent-dispatch";
const run = runLocal(defining(agent), env);
```

## Consume (deploy runWorkflow / hub / sidecar)

Hand the same `AgentDefinition` (`agent`) to a workflow, hub, or sidecar. The
package never exports a workflow itself — a workflow _consumes_ `agent`.

## Remix components

```ts
import {
director,
tools,
systemPrompt,
config,
defineAgent,
} from "@corbits/code-agent-dispatch";

// Swap components at the consumer side.
defineAgent({ director, tools, systemPrompt, config }); // or spread overrides
```

Each of `agent`, `defineAgent`, `director`, `tools`, `systemPrompt`, `config` is a
named, independently importable part.

## Byte-stability & single authority

- `systemPrompt.build()` is pure and byte-stable — identical string every call.
- `tools` is the literal expansion of the app's `DISPATCH_TOOLS` surface constant
(`src/agent/directors/tool-sets.ts`); the in-tree drift-guard
(`src/agent/directors/dispatch/package.test.ts`) enforces they match.
- This package is the single prompt authority for dispatch; the in-tree
`src/agent/directors/dispatch/package.ts` (and the duplicate
`createDispatchSystemPrompt` / `DISPATCH_CARD` in `src/agent/prompts.ts`) are deleted.
13 changes: 13 additions & 0 deletions agents/dispatch/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "@corbits/code-agent-dispatch",
"version": "0.1.0",
"private": true,
"type": "module",
"license": "SEE LICENSE IN LICENSE.md",
"exports": {
".": {
"types": "./src/index.ts",
"default": "./src/index.ts"
}
}
}
24 changes: 24 additions & 0 deletions agents/dispatch/src/agent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Dispatch assembled agent — ready unit + package defineAgent wrapper. Calls
// `@intx/agent`'s `defineAgent` directly (no Corbits bridge). Tool mounting is
// consumer/app-side: `tools` here is [] (the allowlist surface is exported from
// `./toolset.js` and wired by the app harness).

import {
defineAgent as coreDefineAgent,
type AgentDefinition,
} from "@intx/agent";

import { systemPrompt } from "./prompt.js";

export const agent: AgentDefinition = defineAgent();

/** Package convenience wrapper → `AgentDefinition` for `runLocal` authoring. */
export function defineAgent(): AgentDefinition {
return coreDefineAgent({
id: "dispatch",
systemPrompt: systemPrompt.build(),
tools: [],
capabilities: [],
inference: { sources: [] },
});
}
26 changes: 26 additions & 0 deletions agents/dispatch/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Dispatch config — uniform `{ spawn, tier, modelRole }` shape (§5).
// dispatch is the Tier-1 primary orchestrator: maySpawn true with spawn
// allowlist; others are leaf workers with no allowlist.
export const config = {
spawn: {
maySpawn: true,
allowlist: [
"explorer",
"planner",
"coder",
"reviewer",
"designer",
"artist",
"warden",
"shakespeare",
"prober",
"qa-lead",
],
},
tier: "orchestrator",
modelRole: "orchestrator",
} as const satisfies {
spawn: { maySpawn: boolean; allowlist?: readonly string[] };
tier: string;
modelRole: string;
};
26 changes: 26 additions & 0 deletions agents/dispatch/src/director.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Dispatch director card; imports @intx/* only. The app asserts this satisfies
// `DirectorPackage` in-tree (src/agent/directors/dispatch/package.test.ts).
import { tools } from "./toolset.js";
import { systemPrompt } from "./prompt.js";
import { config } from "./config.js";

export const director = {
id: "dispatch",
primaryIntent:
"Coordinate named specialists; DIY only obvious mechanical corrections",
outOfLane: [
"substantive product work without spawning, including single-file behavior changes",
"docs/design authorship (PRODUCT.md, ARCHITECTURE.md, DESIGN.md) except one-line fixes",
"walking the repo yourself; spawn explorers (as many as the question needs)",
"being the reviewer, planner, or coder by default",
"catch-all worker",
"searching the repo yourself after a worker stops without finishing",
],
description:
"Primary dispatcher — classify, DIY tiny edits, spawn named specialists",
systemPrompt: systemPrompt.build(),
tools: { allow: tools },
spawn: config.spawn,
modelRole: config.modelRole,
tier: config.tier,
} as const;
8 changes: 8 additions & 0 deletions agents/dispatch/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Public surface — re-exports only, so the package imports no Corbits code
// (the app asserts the director contract in-tree via drift-guard). No `workflow`
// export: workflows consume `agent`.
export { agent, defineAgent } from "./agent.js";
export { director } from "./director.js";
export { tools } from "./toolset.js";
export { systemPrompt } from "./prompt.js";
export { config } from "./config.js";
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
// Dispatch: primary dispatcher card. Idle/mailbox/poll live in the harness.

import type { DirectorPackage } from "../types.js";
import { DISPATCH_TOOLS } from "../tool-sets.js";

const DISPATCH_CARD = `# Role
// Single prompt authority: `build()` is pure and byte-stable — it returns an
// identical string on every call.
const CARD = `# Role
You are Dispatch, the coordinator for Corbits Code. Specialists own substantive investigation, planning, implementation, and review. You own routing, briefs, coordination, and synthesis.

# Route
Expand Down Expand Up @@ -44,41 +41,9 @@ You are Dispatch, the coordinator for Corbits Code. Specialists own substantive
- Alive, not sterile: dry wit is fine; do not perform, do not pad.
- No emoji. No essays. Personality is tone, not extra paragraphs.`;

export function createDispatchSystemPrompt(): string {
return DISPATCH_CARD;
}
export const theme = "dispatch";

export const dispatchPackage: DirectorPackage = {
id: "dispatch",
primaryIntent:
"Coordinate named specialists; DIY only obvious mechanical corrections",
outOfLane: [
"substantive product work without spawning, including single-file behavior changes",
"docs/design authorship (PRODUCT.md, ARCHITECTURE.md, DESIGN.md) except one-line fixes",
"walking the repo yourself; spawn explorers (as many as the question needs)",
"being the reviewer, planner, or coder by default",
"catch-all worker",
"searching the repo yourself after a worker stops without finishing",
],
description:
"Primary dispatcher — classify, DIY tiny edits, spawn named specialists",
systemPrompt: DISPATCH_CARD,
tools: { allow: DISPATCH_TOOLS },
spawn: {
maySpawn: true,
allowlist: [
"explorer",
"planner",
"coder",
"reviewer",
"designer",
"artist",
"warden",
"shakespeare",
"prober",
"qa-lead",
],
},
modelRole: "orchestrator",
tier: "orchestrator",
export const systemPrompt = {
theme,
build: (): string => CARD,
};
26 changes: 26 additions & 0 deletions agents/dispatch/src/toolset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Dispatch primary: orchestrator surface plus fleet discovery (Tier-1 only).
// Drift against the app's DISPATCH_TOOLS is enforced by the in-tree
// src/agent/directors/dispatch/package.test.ts.
export const tools = [
"read_file",
"grep",
"search_files",
"list_dir",
"lsp",
"run_shell",
"web_fetch",
"web_search",
"skill_search",
"use_skill",
"write_file",
"edit_file",
"delete_file",
"spawn_agent",
"list_agents",
"close_agent",
"resume_agent",
"interrupt_agent",
"send_input",
"read_agent_trace",
"search_agents",
] as const;
7 changes: 7 additions & 0 deletions agents/dispatch/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"noEmit": true
},
"include": ["src/**/*.ts"]
}
5 changes: 5 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions scripts/dead-export-allowlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@ src/agent/directors/identity.ts: Record
packages/opencode-go/src/models.ts: satisfies
packages/opencode-go/src/models.ts: readonly

# Agent-package public API (CL-9800 extraction wave): each @corbits/code-agent-*
# workspace package's public surface (agent/defineAgent + the config card) is
# consumed by the app registry/barrel once the fleet merges; on a per-extract
# branch they have no in-repo consumer yet. Public API by design — owned by the
# agent-template lane; remove with the code if the package is deleted.
agents/dispatch/src/index.ts: agent
agents/dispatch/src/index.ts: defineAgent
agents/dispatch/src/config.ts: satisfies
agents/dispatch/src/config.ts: boolean

# Versioned prompt-variance package public surface (CL-8269 lane,
# agent-owned). Family rows data re-exported through the package entry point;
# no in-repo barrel consumer exists yet for the entries below (src/ consumes
Expand Down
50 changes: 50 additions & 0 deletions src/agent/directors/dispatch/package.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { describe, expect, test } from "bun:test";

import {
director,
tools,
config,
systemPrompt,
} from "@corbits/code-agent-dispatch";
import { DISPATCH_TOOLS } from "../tool-sets.js";
import { DIRECTOR_REGISTRY } from "../registry.js";
import type { DirectorPackage } from "../types.js";

describe("dispatchPackage", () => {
test("workspace package satisfies the director contract and feeds the registry", () => {
const asDirector: DirectorPackage = director;
expect(asDirector.id).toBe("dispatch");
expect(DIRECTOR_REGISTRY.dispatch).toBe(director);
expect([...director.tools.allow]).toEqual([...DISPATCH_TOOLS]);
expect(new Set(director.tools.allow).size).toBe(
director.tools.allow.length,
);
});

test("spawn/tier/modelRole match config (Tier-1 orchestrator)", () => {
expect(director.spawn).toEqual(config.spawn);
expect(director.tier).toBe(config.tier);
expect(director.modelRole).toBe(config.modelRole);
expect(director.spawn.maySpawn).toBe(true);
expect(director.tier).toBe("orchestrator");
expect(director.modelRole).toBe("orchestrator");
});

test("prompt authority is byte-stable", () => {
expect(director.systemPrompt).toBe(systemPrompt.build());
expect(systemPrompt.build()).toBe(systemPrompt.build());
});

test("tools surface matches DISPATCH_TOOLS and is orchestration-scoped", () => {
expect([...tools]).toEqual([...DISPATCH_TOOLS]);
// Orchestration surface: search_agents IS present, wait_agents is NOT.
expect(tools).toContain("search_agents");
expect(tools).not.toContain("wait_agents");
expect(tools).not.toContain("ask_operator");
expect(tools).toContain("spawn_agent");
});

test("tools allowlist has no duplicates", () => {
expect(new Set([...tools]).size).toBe(tools.length);
});
});
4 changes: 2 additions & 2 deletions src/agent/directors/registry.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { director as shakespeareDirector } from "@corbits/code-agent-shakespeare";
import { director as proberDirector } from "@corbits/code-agent-prober";
import { director as dispatchDirector } from "@corbits/code-agent-dispatch";
import type { AgentProfile, CapabilityFilter } from "../profile-types.js";
import { director as artistDirector } from "@corbits/code-agent-artist";
import { coderPackage } from "./coder/package.js";
import { designerPackage } from "./designer/package.js";
import { dispatchPackage } from "./dispatch/package.js";
import { explorerPackage } from "./explorer/package.js";
import { plannerPackage } from "./planner/package.js";
import { qaLeadPackage } from "./qa-lead/package.js";
Expand Down Expand Up @@ -37,7 +37,7 @@ export const INTENT_DEFAULT_DIRECTOR: Readonly<
*/
export const DIRECTOR_REGISTRY: Readonly<Record<DirectorId, DirectorPackage>> =
{
dispatch: dispatchPackage,
dispatch: dispatchDirector,
explorer: explorerPackage,
planner: plannerPackage,
coder: coderPackage,
Expand Down
4 changes: 2 additions & 2 deletions src/agent/prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { EnvironmentInfo } from "./environment.js";
import type { SkillSummary } from "../extensions/skills.js";
import type { SessionMode } from "../config/session-mode.js";
import type { ToolAvailability } from "./tool-search.js";
import { createDispatchSystemPrompt } from "./directors/dispatch/package.js";
import { systemPrompt as dispatchSystemPrompt } from "@corbits/code-agent-dispatch";
import {
buildWorkerContract,
buildWorkerToolNames,
Expand Down Expand Up @@ -48,7 +48,7 @@ export function buildChatRole(
): string {
// Primary session identity is the closed Dispatch director package.
// Harness facts / guidelines still append after this role in baseSection.
return createDispatchSystemPrompt();
return dispatchSystemPrompt.build();
}

// Facts the model cannot derive from its training: what the permission layer
Expand Down
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"@corbits/code-agent-artist": ["./agents/artist/src/index.ts"],
"@corbits/code-agent-shakespeare": ["./agents/shakespeare/src/index.ts"],
"@corbits/code-agent-prober": ["./agents/prober/src/index.ts"],
"@corbits/code-agent-dispatch": ["./agents/dispatch/src/index.ts"],
"@corbits/provider-opencode-go": ["./packages/opencode-go/src/index.ts"],
"@corbits/first-class-providers": [
"./packages/first-class-providers/src/index.ts"
Expand Down
Loading