feat(stage-1b): fleet roster, MCP registry, fleet protocol with schemas and tests

fleet.yaml declares all 25 experts (twelve core enabled) from the salvaged catalog checked against the research catalog. dispatcher/src/fleet-schema.ts and mcp-registry.ts validate both files; tests cover names, enabled set, and every schema rejection.
This commit is contained in:
John Smith committed 2026-10-13 18:11:31 -04:00
1 parent 15e397d006
commit 0f7641f895
10 files changed
+1186

No files matched your search

+8
View File
@@ -4,6 +4,10 @@
"workspaces": {
"": {
"name": "the-agency",
"dependencies": {
"jsonc-parser": "3.3.1",
"yaml": "2.9.1",
},
"devDependencies": {
"@types/bun": "1.4.3",
"oxlint": "1.87.0",
@@ -72,6 +76,8 @@
"bun-types": ["[email protected]", "", { "dependencies": { "@types/node": "*", "undici-types": "*" } }, "sha512-tinRJNGOmUN+tEAsn2y2e942n9JHRHR42iUYzDH75NmhJqVBokGSQkkfSFdNWwsANIdrK6ACH9t01M452YyTwg=="],
"jsonc-parser": ["[email protected]", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="],
"oxlint": ["[email protected]", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.87.0", "@oxlint/binding-android-arm64": "1.87.0", "@oxlint/binding-darwin-arm64": "1.87.0", "@oxlint/binding-darwin-x64": "1.87.0", "@oxlint/binding-freebsd-x64": "1.87.0", "@oxlint/binding-linux-arm-gnueabihf": "1.87.0", "@oxlint/binding-linux-arm-musleabihf": "1.87.0", "@oxlint/binding-linux-arm64-gnu": "1.87.0", "@oxlint/binding-linux-arm64-musl": "1.87.0", "@oxlint/binding-linux-ppc64-gnu": "1.87.0", "@oxlint/binding-linux-riscv64-gnu": "1.87.0", "@oxlint/binding-linux-riscv64-musl": "1.87.0", "@oxlint/binding-linux-s390x-gnu": "1.87.0", "@oxlint/binding-linux-x64-gnu": "1.87.0", "@oxlint/binding-linux-x64-musl": "1.87.0", "@oxlint/binding-openharmony-arm64": "1.87.0", "@oxlint/binding-win32-arm64-msvc": "1.87.0", "@oxlint/binding-win32-ia32-msvc": "1.87.0", "@oxlint/binding-win32-x64-msvc": "1.87.0" }, "peerDependencies": { "oxlint-tsgolint": ">=7.0.2003", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-drxVlaW7Oqq7wSy1CN25znRu00aRq8n57Q2wniJRxCmtYVz07ufCm7EVDPQFZURrTgQIjzKrVEHAD/HcvJXObg=="],
"prettier": ["[email protected]", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
@@ -80,6 +86,8 @@
"undici-types": ["[email protected]", "", {}, "sha512-iMVNmWZ0leK/goS6eXMizSzmm9CDWtyphwbaCms3DNLqRxDL+mMoNVcZMTyyVgXP0N+Z8neAMzDoUOUJL8veKg=="],
"yaml": ["[email protected]", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw=="],
"zod": ["[email protected]", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@types/node/undici-types": ["[email protected]", "", {}, "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg=="],
+79
View File
@@ -0,0 +1,79 @@
import { z } from "zod";
/**
* Zod schema for fleet.yaml (shared context 9.9). Imported by the renderer
* and, later, by the Dispatcher. Unknown keys are rejected; an `ro` expert
* with `proposed_writes` is rejected; a `scoped` expert without them is
* rejected.
*/
export const TierSchema = z.enum(["frontier", "standard", "fast", "review"]);
export const ProfileSchema = z.enum(["core", "extended"]);
export const AccessSchema = z.enum(["ro", "scoped"]);
const ExpertBaseSchema = z
.object({
enabled: z.boolean(),
profile: ProfileSchema,
tier: TierSchema,
access: AccessSchema,
description: z.string().min(1),
handles: z.array(z.string().min(1)).min(1),
delegates: z.array(z.string().min(1)).min(1),
proposed_writes: z.array(z.string().min(1)),
mcp: z.array(z.string().min(1)),
skills: z.array(z.string().min(1)),
image_tools: z.array(z.string().min(1)),
egress: z.array(z.string().min(1)),
browser: z.boolean(),
})
.strict();
export type ExpertEntry = z.infer<typeof ExpertBaseSchema>;
export const FleetExpertSchema = ExpertBaseSchema.superRefine((expert, ctx) => {
if (expert.access === "ro" && expert.proposed_writes.length > 0) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `access "ro" expert must not list proposed_writes`,
});
}
if (expert.access === "scoped" && expert.proposed_writes.length === 0) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `access "scoped" expert must list proposed_writes`,
});
}
});
export const FleetSchema = z
.object({
version: z.literal(1),
defaults: z
.object({
port: z.number().int().positive(),
maxConcurrent: z.number().int().positive(),
taskTimeoutSec: z.number().int().positive(),
resources: z
.object({
cpus: z.number().positive(),
memory: z.string().min(1),
})
.strict(),
})
.strict(),
tiers: z.record(z.string().min(1)).refine((tiers) => Object.keys(tiers).length > 0, {
message: "tiers must not be empty",
}),
experts: z.record(FleetExpertSchema),
})
.strict();
export type FleetConfig = z.infer<typeof FleetSchema>;
/** Names of the enabled experts, in fleet.yaml order. */
export function enabledExperts(fleet: FleetConfig): string[] {
return Object.entries(fleet.experts)
.filter(([, expert]) => expert.enabled)
.map(([name]) => name);
}
+56
View File
@@ -0,0 +1,56 @@
import { z } from "zod";
/**
* Zod schema for mcp/registry.jsonc (shared context 9.11). Imported by the
* renderer and by the schema test. Each entry carries the v2 server config
* plus `requires` (variables that must be non-empty for the server to be
* enabled). Unknown keys are rejected.
*/
const TimeoutSchema = z
.object({
startup: z.number().int().positive().optional(),
catalog: z.number().int().positive().optional(),
execution: z.number().int().positive().optional(),
})
.strict();
const LocalServerConfigSchema = z
.object({
type: z.literal("local"),
command: z.array(z.string().min(1)).min(1),
cwd: z.string().optional(),
environment: z.record(z.string()).optional(),
disabled: z.boolean().optional(),
codemode: z.boolean().optional(),
timeout: TimeoutSchema.optional(),
})
.strict();
const RemoteServerConfigSchema = z
.object({
type: z.literal("remote"),
url: z.string().min(1),
headers: z.record(z.string()).optional(),
oauth: z.union([z.boolean(), z.record(z.string())]).optional(),
disabled: z.boolean().optional(),
codemode: z.boolean().optional(),
timeout: TimeoutSchema.optional(),
})
.strict();
export const McpRegistryEntrySchema = z
.object({
requires: z.array(z.string().min(1)),
config: z.union([LocalServerConfigSchema, RemoteServerConfigSchema]),
})
.strict();
export const McpRegistrySchema = z
.object({
servers: z.record(McpRegistryEntrySchema),
})
.strict();
export type McpRegistry = z.infer<typeof McpRegistrySchema>;
export type McpRegistryEntry = z.infer<typeof McpRegistryEntrySchema>;
+94
View File
@@ -0,0 +1,94 @@
import { describe, expect, test } from "bun:test";
import { parse as parseYaml } from "yaml";
import { FleetSchema, enabledExperts } from "../src/fleet-schema.js";
const CORE_TWELVE = [
"director",
"scout",
"architect",
"product",
"frontend",
"backend",
"dba",
"platform",
"security",
"qa",
"reviewer",
"docs",
];
const ALL_TWENTY_FIVE = [
...CORE_TWELVE,
"qa-functional",
"qa-adversarial",
"api",
"sre",
"performance",
"debugger",
"refactor",
"data",
"ml",
"mobile",
"design",
"release",
"compliance",
];
async function loadFleet(): Promise<unknown> {
const file = Bun.file(new URL("../../fleet.yaml", import.meta.url));
return parseYaml(await file.text());
}
describe("fleet.yaml", () => {
test("validates against the schema", async () => {
const parsed = FleetSchema.safeParse(await loadFleet());
expect(parsed.success).toBe(true);
});
test("holds the 25 names in shared context 9.10 and no others", async () => {
const fleet = FleetSchema.parse(await loadFleet());
expect(Object.keys(fleet.experts).sort()).toEqual([...ALL_TWENTY_FIVE].sort());
});
test("enables exactly the twelve core experts", async () => {
const fleet = FleetSchema.parse(await loadFleet());
expect(enabledExperts(fleet).sort()).toEqual([...CORE_TWELVE].sort());
});
});
describe("fleet schema rejections", () => {
test("rejects an unknown key", async () => {
const raw = (await loadFleet()) as Record<string, unknown>;
const experts = (raw["experts"] ?? {}) as Record<string, Record<string, unknown>>;
const mutated = structuredClone(raw);
(mutated["experts"] as Record<string, Record<string, unknown>>)["backend"] = {
...experts["backend"],
somethingNew: true,
};
expect(FleetSchema.safeParse(mutated).success).toBe(false);
});
test("rejects an ro expert with proposed_writes", async () => {
const raw = (await loadFleet()) as Record<string, unknown>;
const mutated = structuredClone(raw);
const experts = mutated["experts"] as Record<string, Record<string, unknown>>;
(experts["scout"] as Record<string, unknown>)["proposed_writes"] = ["notes/*"];
expect(FleetSchema.safeParse(mutated).success).toBe(false);
});
test("rejects a scoped expert without proposed_writes", async () => {
const raw = (await loadFleet()) as Record<string, unknown>;
const mutated = structuredClone(raw);
const experts = mutated["experts"] as Record<string, Record<string, unknown>>;
(experts["backend"] as Record<string, unknown>)["proposed_writes"] = [];
expect(FleetSchema.safeParse(mutated).success).toBe(false);
});
test("rejects an unknown tier value", async () => {
const raw = (await loadFleet()) as Record<string, unknown>;
const mutated = structuredClone(raw);
const experts = mutated["experts"] as Record<string, Record<string, unknown>>;
(experts["backend"] as Record<string, unknown>)["tier"] = "ultra";
expect(FleetSchema.safeParse(mutated).success).toBe(false);
});
});
+56
View File
@@ -0,0 +1,56 @@
import { describe, expect, test } from "bun:test";
import { parse as parseJsonc } from "jsonc-parser";
import { parse as parseYaml } from "yaml";
import { McpRegistrySchema } from "../src/mcp-registry.js";
import { FleetSchema } from "../src/fleet-schema.js";
async function loadRegistry(): Promise<unknown> {
const file = Bun.file(new URL("../../mcp/registry.jsonc", import.meta.url));
const errors: unknown[] = [];
const parsed: unknown = parseJsonc(await file.text(), errors, { allowTrailingComma: true });
expect(errors).toEqual([]);
return parsed;
}
describe("mcp/registry.jsonc", () => {
test("validates against the schema", async () => {
const parsed = McpRegistrySchema.safeParse(await loadRegistry());
expect(parsed.success).toBe(true);
});
test("covers every MCP key named in fleet.yaml", async () => {
const fleetFile = Bun.file(new URL("../../fleet.yaml", import.meta.url));
const fleet = FleetSchema.parse(parseYaml(await fleetFile.text()));
const registry = McpRegistrySchema.parse(await loadRegistry());
const named = new Set(Object.values(fleet.experts).flatMap((expert) => expert.mcp));
for (const key of named) {
expect(registry.servers[key]).toBeDefined();
}
});
test("postgres, dbhub, figma, and linear ship disabled", async () => {
const registry = McpRegistrySchema.parse(await loadRegistry());
for (const key of ["postgres", "dbhub", "figma", "linear"]) {
const entry = registry.servers[key];
expect(entry).toBeDefined();
const config = entry!.config;
expect("disabled" in config && config.disabled).toBe(true);
}
});
test("fleet requires FLEET_MCP_TOKEN", async () => {
const registry = McpRegistrySchema.parse(await loadRegistry());
expect(registry.servers["fleet"]?.requires).toContain("FLEET_MCP_TOKEN");
});
test("no local server is launched with an unpinned runtime fetch", async () => {
const registry = McpRegistrySchema.parse(await loadRegistry());
for (const [key, entry] of Object.entries(registry.servers)) {
if (entry.config.type !== "local") continue;
const command = entry.config.command.join(" ");
// npx/uvx without a pinned version would fetch latest at run time.
expect(command).not.toMatch(/\bnpx\b(?![^]*@)/);
expect(command).not.toMatch(/\buvx\b.*--from\s+\S+(?<!\d)$/);
}
});
});
+13
View File
@@ -0,0 +1,13 @@
import { describe, expect, test } from "bun:test";
import { extractBlock } from "../src/protocol.js";
describe("protocol examples in FLEET_PROTOCOL.md parse", () => {
test("RESULT and VERDICT examples extract and validate", async () => {
const file = Bun.file(new URL("../../shared/FLEET_PROTOCOL.md", import.meta.url));
const text = await file.text();
const result = extractBlock(text, "RESULT");
expect(result.ok).toBe(true);
const verdict = extractBlock(text, "VERDICT");
expect(verdict.ok).toBe(true);
});
});
+598
View File
@@ -0,0 +1,598 @@
# The Agency fleet roster (Stage 1b). Source of truth for the roster.
# From shared context 9.9/9.10, descriptions/handles/delegates/skills for the
# twelve core experts taken from the salvaged opencode-fleet catalog
# ($SALVAGE_SRC/packages/opencode-fleet/src/experts/catalog.ts at 14f297a,
# itself ported from docs/research/02-expert-fleet-catalog.md sections
# 3.1-3.12) and checked against that catalog; the other thirteen written
# from the research catalog (sections 3.13-3.14 and 4). The TypeScript
# catalog is not kept: this file replaces it.
version: 1
defaults:
port: 4096
maxConcurrent: 2
taskTimeoutSec: 3600
resources:
cpus: 2
memory: 4g
tiers:
frontier: "{env:AGENCY_MODEL}"
standard: "{env:AGENCY_MODEL}"
fast: "{env:AGENCY_MODEL}"
review: "{env:AGENCY_MODEL}"
experts:
director:
enabled: true
profile: core
tier: frontier
access: ro
description: "Mission orchestrator. Leads each mission: picks a playbook, writes the plan and task briefs, reads results, decides accept or rework within playbook limits, and reports. One Director session per mission."
handles:
- "Decomposition of missions into plans and task briefs"
- "Playbook selection and sequencing"
- "Reading results and diffs"
- "Rework decisions within playbook limits"
- "Final mission report"
delegates:
- "Everything else: never edits files"
- "Landing to the Dispatcher (cannot skip a playbook gate)"
proposed_writes: []
mcp: [fleet]
skills:
[task-decomposition, dispatch-brief, playbook-selection, rework-decisions, mission-reporting]
image_tools: []
egress: []
browser: false
scout:
enabled: true
profile: core
tier: fast
access: ro
description: "Reconnaissance and research. Cheap, fast context gathering so other experts and the Director do not spend their context on it."
handles:
- "Code maps (where X lives, who calls it)"
- "Dependency and stack inventories"
- "Summaries of unfamiliar modules"
- "External research briefs with sources"
delegates:
- "Any judgement call about design"
- "Every change"
proposed_writes: []
mcp: [context7, deepwiki]
skills: [codebase-mapping, research-brief]
image_tools: [ast-grep, scc, tree]
egress: []
browser: false
architect:
enabled: true
profile: core
tier: frontier
access: scoped
description: "System design owner: module boundaries, data flow, API contracts at design level, technology choices, and the decision record. Breaks ties when the two QA judges disagree."
handles:
- "ADRs and architecture docs/diagrams"
- "Design reviews of proposed changes"
- "Breaking large work into components with interfaces"
- "Tie-breaking QA judge disagreements (decision recorded with reasons)"
delegates:
- "Implementation to the builders"
- "Detailed contract files to api"
- "Threat modeling to security"
proposed_writes: ["docs/adr/*", "docs/architecture/*"]
mcp: [context7, deepwiki]
skills: [adr-writing, c4-modeling, tradeoff-analysis, interface-first-decomposition]
image_tools: [scc, dependency-cruiser, madge]
egress: []
browser: false
product:
enabled: true
profile: core
tier: standard
access: scoped
description: "Requirements owner. Turns intent into testable requirements: user stories, acceptance criteria, scope cuts, backlog slicing."
handles:
- "User stories"
- "Acceptance criteria in Given/When/Then form"
- "Scope cuts and backlog slicing"
- "Issue grooming"
delegates:
- "Technical design to architect"
- "Estimates to the owning builder"
proposed_writes: ["docs/product/*", "docs/requirements/*"]
mcp: [github, linear]
skills: [user-story-writing, acceptance-criteria, scope-slicing]
image_tools: [gh]
egress: []
browser: false
frontend:
enabled: true
profile: core
tier: standard
access: scoped
description: "UI engineering: components, client state, styling, client-side data fetching, accessibility, colocated tests, visual verification."
handles:
- "Components and client state"
- "Styling and client-side data fetching"
- "Accessibility in the implementation"
- "Colocated component tests"
- "Visual verification with screenshots"
delegates:
- "API changes to backend"
- "End-to-end suites to qa"
- "Design-system decisions and audits to design"
proposed_writes:
[
"web/*",
"apps/web/*",
"src/components/*",
"src/pages/*",
"src/app/*",
"src/styles/*",
"public/*",
]
mcp: [playwright, context7, figma]
skills:
[
component-implementation,
a11y-in-implementation,
visual-verification,
frontend-state-conventions,
]
image_tools: [pnpm, bun, chromium-playwright, "@axe-core/cli"]
egress: []
browser: true
backend:
enabled: true
profile: core
tier: standard
access: scoped
description: "Services and APIs: endpoints, business logic, integrations, background jobs, colocated unit tests, error handling and logging."
handles:
- "Endpoints and business logic"
- "Integrations and background jobs"
- "Colocated unit tests"
- "Error handling and logging in application code"
delegates:
- "Schema changes and migrations to dba"
- "Pipelines and deployment to platform"
- "Contract files to api"
proposed_writes:
["src/*", "server/*", "api/*", "services/*", "internal/*", "pkg/*", "cmd/*", "lib/*"]
mcp: [context7, serena, dbhub]
skills:
[
endpoint-implementation,
error-handling-conventions,
dependency-addition-policy,
test-with-the-change,
]
image_tools: [pnpm, hurl, httpie, make]
egress: []
browser: false
dba:
enabled: true
profile: core
tier: standard
access: scoped
description: "Data layer owner: schema design, reversible migrations, index design, query review, data-safety checks, seed data. Works against the dev database only."
handles:
- "Schema design and reversible migrations (expand then contract)"
- "Index design and query review with EXPLAIN"
- "Data-safety checks and seed data"
delegates:
- "Application query code to backend"
- "Database provisioning to platform"
proposed_writes: ["migrations/*", "db/*", "prisma/*", "schema/*", "*.sql"]
mcp: [postgres, dbhub]
skills: [migration-authoring, query-plan-review, index-design, data-safety-checklist]
image_tools: [psql, pg_dump, sqlite3, mysql, sqlfluff, atlas]
egress: []
browser: false
platform:
enabled: true
profile: core
tier: standard
access: scoped
description: "DevOps and infrastructure: Dockerfiles, compose stacks, CI/CD, IaC, manifests and charts. No container daemon access; builds run in the environment runner."
handles:
- "Dockerfiles and compose files"
- "CI/CD workflows and build scripts"
- "Infrastructure as code, K8s manifests, Helm charts"
delegates:
- "Application code to the builders"
- "Applying to real environments and secret rotation to a human"
proposed_writes:
[
"Dockerfile*",
"*compose*.yml",
".github/*",
".gitlab-ci.yml",
"infra/*",
"deploy/*",
"k8s/*",
"helm/*",
"terraform/*",
"Makefile",
"scripts/*",
]
mcp: [terraform, kubernetes, github]
skills: [dockerfile-hardening, ci-pipeline-authoring, iac-plan-review, k8s-manifest-conventions]
image_tools:
[
hadolint,
actionlint,
shellcheck,
shfmt,
yamllint,
terraform,
tflint,
kubectl,
helm,
kustomize,
kubeconform,
docker,
]
egress: []
browser: false
security:
enabled: true
profile: core
tier: frontier
access: ro
description: "Application security reviewer. Finds and explains security problems; does not fix them. Output is a severity-ranked report plus SARIF."
handles:
- "Threat models (STRIDE per trust boundary)"
- "SAST triage with reachability confirmation"
- "Dependency, container, and secret scanning"
- "Security review of diffs and IaC misconfigurations"
delegates:
- "Fixes to the owning expert, with a suggested patch in the report"
proposed_writes: []
mcp: [semgrep, trivy]
skills:
[
threat-modeling,
sast-triage,
dependency-risk-review,
secret-exposure-procedure,
security-report-format,
]
image_tools: [semgrep, trivy, gitleaks, osv-scanner, syft, grype, checkov]
egress: []
browser: false
qa:
enabled: true
profile: core
tier: standard
access: scoped
description: "Test author. Writes the tests a change is judged by, independently of the builder: acceptance, integration, e2e, fixtures, flake triage, coverage gaps."
handles:
- "Failing acceptance tests from the brief before the build lands"
- "Integration and end-to-end tests, fixtures"
- "Flake triage and coverage gap analysis"
- "Reproducing reported defects as failing tests"
delegates:
- "Production-code fixes to the owning expert"
- "Pass/fail judgement to the two QA judges"
proposed_writes:
[
"tests/*",
"test/*",
"e2e/*",
"*.test.*",
"*.spec.*",
"*__tests__*",
"playwright.config.*",
"vitest.config.*",
"jest.config.*",
]
mcp: [playwright]
skills: [test-strategy, e2e-authoring, flake-triage, coverage-gap-analysis, bug-report-format]
image_tools: [pnpm, chromium-playwright, pytest]
egress: []
browser: true
reviewer:
enabled: true
profile: core
tier: review
access: ro
description: "Independent code reviewer of a task diff before integration: correctness, regressions, maintainability, test adequacy, repo conventions. Verdict: approve or request changes."
handles:
- "Correctness and regression review"
- "Maintainability and test adequacy"
- "Adherence to repo conventions"
- "Verdict (approve / request changes) with findings by severity and file:line"
delegates:
- "Deep security analysis to security"
- "Fixes to the author"
proposed_writes: []
# No MCP by default (leanest context in the fleet). The github
# pull-request toolset is available but disabled; see the registry.
mcp: []
skills: [review-rubric, diff-reading-procedure, review-report-format]
image_tools: [delta, difftastic, scc]
egress: []
browser: false
docs:
enabled: true
profile: core
tier: fast
access: scoped
description: "Technical writing owner: README, guides, tutorials, API reference prose, troubleshooting. Every documented command is run first."
handles:
- "README, guides, tutorials, troubleshooting pages"
- "API reference prose"
- "Doc verification by running documented commands"
delegates:
- "ADRs to architect"
- "Requirements to product"
- "Changelog and release notes to release"
proposed_writes:
[
"docs/guides/*",
"docs/reference/*",
"docs/tutorials/*",
"README*",
"CONTRIBUTING*",
"mkdocs.yml",
]
mcp: [context7]
skills: [docs-structure, reference-from-source, verify-what-you-document]
image_tools: [markdownlint-cli2, vale, lychee, prettier]
egress: []
browser: false
qa-functional:
enabled: false
profile: core
tier: standard
access: ro
description: "Functional judge. Decides whether the change does what the brief and its acceptance criteria say. Its verdict blocks landing. Arrives in Stage 2a."
handles:
- "Running the acceptance tests and the application in the task environment stack"
- "Checking each criterion, returning a verdict with evidence per criterion"
delegates:
- "Fixes to the author"
- "Writes nothing to the repository"
proposed_writes: []
mcp: [playwright]
skills: [criterion-by-criterion-verification, verdict-format]
image_tools: [pnpm, chromium-playwright, hurl]
egress: []
browser: true
qa-adversarial:
enabled: false
profile: core
tier: standard
access: ro
description: "Adversarial judge. Tries to break the change: edge cases, invalid and hostile input, concurrency, error paths, regressions in neighbouring behavior. Its verdict blocks landing. Arrives in Stage 2a."
handles:
- "Edge cases, invalid and hostile input, concurrency, error paths"
- "Regressions in neighbouring behavior, every failure with reproduction steps"
delegates:
- "Fixes to the author"
- "Writes nothing to the repository"
proposed_writes: []
mcp: [playwright]
skills: [adversarial-test-design, regression-probing, verdict-format]
image_tools: [pnpm, chromium-playwright, hurl]
egress: []
browser: true
api:
enabled: false
profile: extended
tier: standard
access: scoped
description: "Contracts owner: OpenAPI, GraphQL, and protobuf definitions; contract linting; breaking-change detection; contract tests; mock servers. Starts on demand (Stage 4a)."
handles:
- "OpenAPI, GraphQL, and protobuf definitions"
- "Contract linting and breaking-change detection"
- "Contract tests and mock servers"
delegates:
- "Implementing the contract to backend"
- "Consuming it to frontend"
proposed_writes: ["openapi/*", "proto/*", "graphql/*", "*.graphql", "contracts/*"]
mcp: [context7]
skills: [openapi-design-rules, breaking-change-review, contract-testing]
image_tools: [spectral, oasdiff, buf, schemathesis, hurl, prism]
egress: []
browser: false
sre:
enabled: false
profile: extended
tier: standard
access: scoped
description: "Reliability and observability owner: SLOs, alert rules, dashboards as code, runbooks, incident timelines and postmortems. Starts on demand (Stage 4a)."
handles:
- "SLOs, alert rules, dashboards as code"
- "Runbooks, incident timelines and postmortems, capacity notes"
delegates:
- "Instrumentation inside application code to the builders"
- "Infrastructure changes to platform"
proposed_writes: ["ops/*", "monitoring/*", "alerts/*", "dashboards/*", "docs/runbooks/*"]
mcp: [grafana, sentry]
skills: [slo-definition, symptom-based-alerting, runbook-format, postmortem-format]
image_tools: [promtool, amtool, logcli]
egress: []
browser: false
performance:
enabled: false
profile: extended
tier: standard
access: scoped
description: "Performance engineering: measures, then recommends. Benchmarks, load tests, profiling, budgets, regression reports with numbers and method. Starts on demand (Stage 4a)."
handles:
- "Benchmarks, load tests, profiling"
- "Performance budgets and regression reports with numbers and method"
delegates:
- "The optimization itself to the owning builder, unless confined to benchmark code"
proposed_writes: ["bench/*", "perf/*", "k6/*"]
mcp: [chrome-devtools]
skills: [benchmark-methodology, load-test-design, perf-report-format]
image_tools: [k6, hyperfine, lighthouse, py-spy, Chromium]
egress: []
browser: true
debugger:
enabled: false
profile: extended
tier: frontier
access: scoped
description: "Root-cause analysis: reproduction, bisection, instrumentation, a written hypothesis log, a root-cause report with the minimal failing case. Starts on demand (Stage 4a)."
handles:
- "Reproduction, bisection, instrumentation"
- "Written hypothesis log and root-cause report with the minimal failing case"
delegates:
- "The fix to the owning expert"
proposed_writes: ["tests/repro/*"]
mcp: [chrome-devtools, sentry]
skills: [hypothesis-driven-debugging, minimal-repro, bisect-procedure, rca-report-format]
image_tools: [gdb, strace, ltrace]
egress: []
browser: false
refactor:
enabled: false
profile: extended
tier: standard
access: scoped
description: "Mechanical change at scale: codemods, renames, module moves, dependency and framework upgrades, dead-code removal. Characterization tests come first. Starts on demand (Stage 4a)."
handles:
- "Codemods, renames, module moves"
- "Dependency and framework upgrades, dead-code removal"
delegates:
- "Any change that alters behavior to the owning builder"
proposed_writes:
[
"src/*",
"server/*",
"api/*",
"services/*",
"internal/*",
"pkg/*",
"cmd/*",
"lib/*",
"web/*",
"apps/web/*",
"src/components/*",
"src/pages/*",
"src/app/*",
"src/styles/*",
"public/*",
]
mcp: [serena]
skills: [behavior-preserving-refactor, codemod-authoring, upgrade-playbook]
image_tools: [ast-grep, comby, jscodeshift, knip, ruff]
egress: []
browser: false
data:
enabled: false
profile: extended
tier: standard
access: scoped
description: "Data engineering and analytics owner: transformations, dbt models, warehouse schemas, data quality tests, analytical SQL. Starts on demand (Stage 4a)."
handles:
- "Transformations, dbt models, warehouse schemas"
- "Data quality tests and analytical SQL"
delegates:
- "Operational database schema to dba"
- "Pipeline infrastructure to platform"
proposed_writes: ["data/*", "pipelines/*", "dbt/*", "analytics/*", "notebooks/*"]
mcp: [dbhub]
skills: [dbt-modeling-conventions, data-quality-tests, idempotent-pipelines]
image_tools: [duckdb, dbt-core, sqlfluff]
egress: []
browser: false
ml:
enabled: false
profile: extended
tier: standard
access: scoped
description: "AI and ML engineering owner: prompts, evaluation suites, retrieval pipelines, model client code, serving wrappers. Evals are written before prompt changes. Starts on demand (Stage 4a)."
handles:
- "Prompts, evaluation suites, retrieval pipelines"
- "Model client code and serving wrappers"
delegates:
- "Compute and serving infrastructure to platform"
- "Upstream data to data"
proposed_writes: ["ml/*", "models/*", "evals/*", "prompts/*"]
mcp: [huggingface, context7]
skills: [eval-first-development, prompt-versioning, rag-design-checklist]
image_tools: [promptfoo, jupyter]
egress: []
browser: false
mobile:
enabled: false
profile: extended
tier: standard
access: scoped
description: "Mobile client implementation (code only: a Linux container has no Xcode or iOS simulator, and an Android emulator needs KVM passthrough). Starts on demand (Stage 4a)."
handles:
- "React Native, Flutter, Kotlin, and Swift source"
- "Platform conventions and offline behavior"
delegates:
- "Backend changes to backend"
- "Store submission to a human"
proposed_writes: ["mobile/*", "apps/mobile/*", "android/*", "ios/*"]
mcp: [context7]
skills: [platform-guidelines, offline-first-patterns, store-release-checklist]
image_tools: [node-android-tools]
egress: []
browser: false
design:
enabled: false
profile: extended
tier: standard
access: scoped
description: "Design system and experience audit owner: design tokens, component usage guidance, WCAG audits, heuristic UX reviews, interface copy. Starts on demand (Stage 4a)."
handles:
- "Design tokens and component usage guidance"
- "WCAG audits, heuristic UX reviews, interface copy"
delegates:
- "Component implementation to frontend"
proposed_writes: ["design/*", "docs/design/*", "tokens/*"]
mcp: [playwright, figma]
skills: [wcag-audit-procedure, design-token-conventions, ux-heuristic-review, ui-copy-style]
image_tools: ["@axe-core/cli", pa11y, lighthouse, Chromium]
egress: []
browser: true
release:
enabled: false
profile: extended
tier: standard
access: scoped
description: "Release management owner: version decisions, changelog, release notes, tagging, dependency update batches, release checklists. The only expert whose ruleset re-allows git tag. Starts on demand (Stage 4a)."
handles:
- "Version decisions, changelog, release notes, tagging"
- "Dependency update batches and release checklists"
delegates:
- "Fixes found during release checks to the owning expert"
- "Pushing and opening the draft pull request to the Dispatcher; merging and publishing to a human"
proposed_writes:
[
"CHANGELOG*",
"release/*",
".release-please*",
"package.json",
"Cargo.toml",
"pyproject.toml",
]
mcp: [github]
skills: [semver-decision, changelog-authoring, release-checklist]
image_tools: [gh, git-cliff]
egress: []
browser: false
compliance:
enabled: false
profile: extended
tier: standard
access: ro
description: "Licensing and privacy checks: license inventory and compatibility, SBOM generation, PII data-flow review, mapping controls to a checklist. Starts on demand (Stage 4a)."
handles:
- "License inventory and compatibility, SBOM generation"
- "PII data-flow review and control mapping"
delegates:
- "Remediation to the owning expert"
- "Legal judgement to a human"
proposed_writes: []
mcp: []
skills: [license-compatibility-review, sbom-generation, pii-dataflow-review]
image_tools: [syft, trivy, reuse]
egress: []
browser: false
+186
View File
@@ -0,0 +1,186 @@
// MCP server registry (Stage 1b). One entry per server from shared context
// 9.11: the v2 server config plus `requires` (variables that must be
// non-empty for the server to be enabled). The renderer inlines `config`
// into each expert's opencode.jsonc and writes `requires` plus the config
// into mcp-requirements.json; the entrypoint disables servers whose
// requirements are unmet at container start.
//
// Rules honored here: every local server is pinned to an exact version and
// installed into the image at build time (no `npx -y <unpinned>` at run
// time); a server whose requirements are unmet ends up `disabled`;
// `postgres`, `dbhub`, `figma`, and `linear` ship disabled (9.11); `fleet`
// requires FLEET_MCP_TOKEN, which is not set until Stage 1d, so the
// Director's fleet server ends up disabled in this stage (correct here).
// Verification of each entry is recorded in docs/build-notes.md.
{
"servers": {
"fleet": {
"requires": ["FLEET_MCP_TOKEN"],
"config": {
"type": "remote",
"url": "http://dispatcher:8080/mcp",
"headers": { "Authorization": "Bearer {env:FLEET_MCP_TOKEN}" },
"oauth": false,
"codemode": false,
"timeout": { "execution": 900000 },
},
},
"context7": {
"requires": [],
"config": { "type": "remote", "url": "https://mcp.context7.com/mcp" },
},
"deepwiki": {
"requires": [],
"config": { "type": "remote", "url": "https://mcp.deepwiki.com/mcp" },
},
"playwright": {
// npm @playwright/mcp 0.0.83, bin `playwright-mcp`, installed at
// build time in the frontend and qa images (with a Chromium from the
// matching playwright-core 1.64.0-alpha).
"requires": [],
"config": {
"type": "local",
"command": ["playwright-mcp", "--headless", "--isolated", "--no-sandbox"],
},
},
"chrome-devtools": {
// npm chrome-devtools-mcp 1.10.1, bin `chrome-devtools-mcp`. Used
// only by extended experts (Stage 4a); installed then.
"requires": [],
"config": {
"type": "local",
"command": ["chrome-devtools-mcp", "--headless", "--isolated"],
},
},
"serena": {
// oraios/serena at commit f9972a32c67359dd4c4a5ab597688bb10d21d1f3,
// installed at build time in the backend image via uv; binary on PATH.
"requires": [],
"config": {
"type": "local",
"command": [
"serena",
"start-mcp-server",
"--context",
"ide-assistant",
"--project",
"/work",
],
},
},
"postgres": {
// Ships disabled (9.11): a task's database address reaches the
// expert as a session variable, which a container-start MCP server
// may not see. The DBA uses psql with the task's DATABASE_URL until
// a test shows the MCP server receives the per-task address.
"requires": [],
"config": {
"type": "local",
"command": ["postgres-mcp", "--access-mode=restricted"],
"disabled": true,
},
},
"dbhub": {
// Ships disabled (9.11), same reason as postgres. npm
// @bytebase/dbhub 1.5.0, bin `dbhub`.
"requires": [],
"config": {
"type": "local",
"command": ["dbhub", "--transport", "stdio", "--readonly", "--dsn", "{env:DATABASE_URL}"],
"disabled": true,
},
},
"github": {
// Remote GitHub MCP. Read-only use; the token that can push or open
// pull requests is held only by the Dispatcher (Stage 3a). Requires
// GITHUB_READ_TOKEN, unset until then, so this ends up disabled now.
"requires": ["GITHUB_READ_TOKEN"],
"config": {
"type": "remote",
"url": "https://api.githubcopilot.com/mcp/",
"headers": { "Authorization": "Bearer {env:GITHUB_READ_TOKEN}" },
},
},
"semgrep": {
// Semgrep CLI 1.180.0 (PyPI) installed at build time in the security
// image; `semgrep mcp` is bundled with recent releases.
"requires": [],
"config": { "type": "local", "command": ["semgrep", "mcp"] },
},
"trivy": {
// Trivy 0.75.0 binary plus its `mcp` plugin, both installed at build
// time in the security image.
"requires": [],
"config": { "type": "local", "command": ["trivy", "mcp"] },
},
"terraform": {
// hashicorp/terraform-mcp-server 1.3.0 binary installed at build
// time in the platform image. ENABLE_TF_OPERATIONS is never set, so
// plan and apply stay off.
"requires": [],
"config": { "type": "local", "command": ["terraform-mcp-server"] },
},
"kubernetes": {
// npm kubernetes-mcp-server 0.0.67, bin `kubernetes-mcp-server`,
// installed at build time in the platform image. Disabled until a
// kubeconfig is mounted.
"requires": ["KUBECONFIG"],
"config": {
"type": "local",
"command": ["kubernetes-mcp-server", "--read-only"],
},
},
"grafana": {
// grafana/mcp-grafana 2.0.2 binary (linux x64 asset verified).
// Used only by the extended sre expert (Stage 4a); installed then.
// Disabled until a URL and a viewer-role token exist.
"requires": ["GRAFANA_URL", "GRAFANA_SERVICE_ACCOUNT_TOKEN"],
"config": {
"type": "local",
"command": ["mcp-grafana"],
"environment": {
"GRAFANA_URL": "{env:GRAFANA_URL}",
"GRAFANA_SERVICE_ACCOUNT_TOKEN": "{env:GRAFANA_SERVICE_ACCOUNT_TOKEN}",
},
},
},
"sentry": {
// Remote Sentry MCP (read scopes). Used only by extended experts;
// disabled until a token exists.
"requires": ["SENTRY_ACCESS_TOKEN"],
"config": {
"type": "remote",
"url": "https://mcp.sentry.dev/mcp",
"headers": { "Authorization": "Bearer {env:SENTRY_ACCESS_TOKEN}" },
},
},
"huggingface": {
// Remote Hugging Face MCP (read scopes). Used only by the extended
// ml expert; disabled until a token exists.
"requires": ["HF_TOKEN"],
"config": {
"type": "remote",
"url": "https://huggingface.co/mcp",
"headers": { "Authorization": "Bearer {env:HF_TOKEN}" },
},
},
"figma": {
// OAuth-only remote server: disabled until authorized.
"requires": [],
"config": {
"type": "remote",
"url": "https://mcp.figma.com/mcp",
"disabled": true,
},
},
"linear": {
// OAuth-only remote server: disabled until authorized.
"requires": [],
"config": {
"type": "remote",
"url": "https://mcp.linear.app/mcp",
"disabled": true,
},
},
},
}
+4
View File
@@ -15,5 +15,9 @@
"oxlint": "1.87.0",
"prettier": "3.6.2",
"typescript": "5.9.2"
},
"dependencies": {
"jsonc-parser": "3.3.1",
"yaml": "2.9.1"
}
}
+92
View File
@@ -0,0 +1,92 @@
# Fleet protocol (Stage 1b)
This file becomes every expert's `AGENTS.md`. It is generated into
`experts/<name>/config/AGENTS.md` by `scripts/render.ts`; edit this file,
never the copies.
## Who you are
You are one specialist in a fleet of experts. Your tasks arrive from the
Dispatcher, a program, not from a person. There is nobody on the other end
of the conversation: no human reads your questions and no other expert is
listening. When something is unclear, make a reasonable assumption, write
it down under `assumptions`, and keep going. If the assumption would decide
the outcome, stop and report `blocked` with a `question` instead.
## Boundaries
- Work only inside the task's worktree, on its branch, and only on the
paths the task grants you. Read-only experts change nothing in the
repository and write reports, screenshots, and logs under
`/artifacts/<taskId>/`.
- If the repository root has an `AGENTS.md`, read it first and follow the
repository's own conventions inside your granted paths.
- Commit your work on the current branch when you are a writer. Never push,
merge, switch branches, create tags, or touch another expert's paths.
Landing is the Dispatcher's job, not yours.
- Stay inside your role. When you need another expert, do not do their job:
name them and state the request under `handoffs` in your closing block.
## Verification
Run what you built and report what actually happened. Every claim about
behavior names the command, its exit code, and what you observed. A test
you did not run is not evidence. Quote the trimmed output; do not invent
numbers.
## Closing blocks
Builders and authors end their final message with a `## RESULT` heading and
one fenced `json` block shaped like this example:
## RESULT
```json
{
"status": "done",
"summary": "Added request-id middleware to the HTTP service and covered it with unit tests.",
"changes": [{ "commit": "9f2c1ab", "files": ["src/middleware/request-id.ts"] }],
"verification": [{ "command": "bun test", "exit": 0, "observed": "18 passed, 0 failed" }],
"assumptions": ["Existing log format stays unchanged."],
"risks": ["Proxies that strip unknown headers will drop the id."],
"handoffs": [
{ "expert": "docs", "request": "Document the X-Request-Id header in the operations guide." }
],
"artifacts": ["/artifacts/t-41/report.md"],
"question": null
}
```
`status` is one of `done`, `blocked`, `needs-handoff`, `needs-split`.
`question` is required when `status` is `blocked`.
Judges (QA judges, reviewer, security, the architect on a tiebreak) end
their final message with a `## VERDICT` heading and one fenced `json`
block shaped like this example:
## VERDICT
```json
{
"decision": "fail",
"findings": [
{
"severity": "major",
"where": "src/session/store.ts:88",
"what": "Expired sessions are never evicted, so the table grows without bound."
}
],
"evidence": [
{
"command": "bun test tests/session-eviction.test.ts",
"exit": 1,
"observed": "expected 0 live rows, kept 412",
"artifact": "/artifacts/t-42/eviction.log"
}
]
}
```
`decision` is `pass` or `fail`. `severity` is `blocker`, `major`, or
`minor`. A `pass` that lists a `blocker` finding is invalid, and a `fail`
with no `blocker` or `major` finding is invalid.