diff --git a/agents/dispatch/README.md b/agents/dispatch/README.md new file mode 100644 index 000000000..53c4048da --- /dev/null +++ b/agents/dispatch/README.md @@ -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. diff --git a/agents/dispatch/package.json b/agents/dispatch/package.json new file mode 100644 index 000000000..9521e89db --- /dev/null +++ b/agents/dispatch/package.json @@ -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" + } + } +} diff --git a/agents/dispatch/src/agent.ts b/agents/dispatch/src/agent.ts new file mode 100644 index 000000000..5abd16364 --- /dev/null +++ b/agents/dispatch/src/agent.ts @@ -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: [] }, + }); +} diff --git a/agents/dispatch/src/config.ts b/agents/dispatch/src/config.ts new file mode 100644 index 000000000..d232bdedf --- /dev/null +++ b/agents/dispatch/src/config.ts @@ -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; +}; diff --git a/agents/dispatch/src/director.ts b/agents/dispatch/src/director.ts new file mode 100644 index 000000000..4d82f7ff0 --- /dev/null +++ b/agents/dispatch/src/director.ts @@ -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; diff --git a/agents/dispatch/src/index.ts b/agents/dispatch/src/index.ts new file mode 100644 index 000000000..4badbc49d --- /dev/null +++ b/agents/dispatch/src/index.ts @@ -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"; diff --git a/src/agent/directors/dispatch/package.ts b/agents/dispatch/src/prompt.ts similarity index 73% rename from src/agent/directors/dispatch/package.ts rename to agents/dispatch/src/prompt.ts index 0313df46d..8bd278704 100644 --- a/src/agent/directors/dispatch/package.ts +++ b/agents/dispatch/src/prompt.ts @@ -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 @@ -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, }; diff --git a/agents/dispatch/src/toolset.ts b/agents/dispatch/src/toolset.ts new file mode 100644 index 000000000..f273a330e --- /dev/null +++ b/agents/dispatch/src/toolset.ts @@ -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; diff --git a/agents/dispatch/tsconfig.json b/agents/dispatch/tsconfig.json new file mode 100644 index 000000000..0d7100cbc --- /dev/null +++ b/agents/dispatch/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "noEmit": true + }, + "include": ["src/**/*.ts"] +} diff --git a/bun.lock b/bun.lock index 84d0c2a99..2d4d50072 100644 --- a/bun.lock +++ b/bun.lock @@ -60,6 +60,10 @@ "name": "@corbits/code-agent-prober", "version": "0.1.0", }, + "agents/dispatch": { + "name": "@corbits/code-agent-dispatch", + "version": "0.1.0", + }, "packages/first-class-providers": { "name": "@corbits/first-class-providers", "version": "0.1.0", @@ -254,6 +258,7 @@ "@corbits/code-agent-artist": ["@corbits/code-agent-artist@workspace:agents/artist"], "@corbits/code-agent-shakespeare": ["@corbits/code-agent-shakespeare@workspace:agents/shakespeare"], "@corbits/code-agent-prober": ["@corbits/code-agent-prober@workspace:agents/prober"], + "@corbits/code-agent-dispatch": ["@corbits/code-agent-dispatch@workspace:agents/dispatch"], "@corbits/codex-provider": ["@corbits/codex-provider@0.1.1", "", { "dependencies": { "arktype": "^2.2.3" }, "peerDependencies": { "@corbits/oauth-core": "^0.2.0", "@corbits/openai-responses": "^0.2.0", "@intx/inference": "^0.4.0", "@intx/types": "^0.4.0" } }, "sha512-MiGA5djQNtRpHqLvJAGdFgHEbUl808AbveMPwt7krLjOH0i20DyElpitHPZF3ltCXkFMhcKIoyTAEpRDqeB/Rw=="], diff --git a/scripts/dead-export-allowlist.txt b/scripts/dead-export-allowlist.txt index 0003b259a..03b4b8571 100644 --- a/scripts/dead-export-allowlist.txt +++ b/scripts/dead-export-allowlist.txt @@ -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 diff --git a/src/agent/directors/dispatch/package.test.ts b/src/agent/directors/dispatch/package.test.ts new file mode 100644 index 000000000..ccf700fe5 --- /dev/null +++ b/src/agent/directors/dispatch/package.test.ts @@ -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); + }); +}); diff --git a/src/agent/directors/registry.ts b/src/agent/directors/registry.ts index 688db63e4..8741a0318 100644 --- a/src/agent/directors/registry.ts +++ b/src/agent/directors/registry.ts @@ -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"; @@ -37,7 +37,7 @@ export const INTENT_DEFAULT_DIRECTOR: Readonly< */ export const DIRECTOR_REGISTRY: Readonly> = { - dispatch: dispatchPackage, + dispatch: dispatchDirector, explorer: explorerPackage, planner: plannerPackage, coder: coderPackage, diff --git a/src/agent/prompts.ts b/src/agent/prompts.ts index 6fb7f45a9..39cc73a13 100644 --- a/src/agent/prompts.ts +++ b/src/agent/prompts.ts @@ -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, @@ -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 diff --git a/tsconfig.json b/tsconfig.json index c7e3946b1..1cb912592 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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"