fix(chat): point agents at the teams catalog for ready-made teams (#20239)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agent Chat answers a person's requests with a chat agent that reads
the API catalog and the chat guidance
> - Paperclip ships a teams catalog of ready-made teams that a company
can preview and install
> - The chat guidance does not name this catalog, and its API catalog
entries have only short summaries
> - So agents asked for a "ready-made team" check only the existing
agents and skills, or search npm and GitHub
> - This pull request names the teams catalog where the agent already
looks: the chat guidance and the API catalog entries
> - The benefit is that agents find, preview and link ready-made teams
instead of saying none exist

## Linked Issues or Issue Description

**What happened**
An agent was asked "Do we have a ready-made team for building a
website?". It read only the company's agents, projects and skills. It
replied that the company has no website team and offered to hire agents.
It did not read the teams catalog, which has a Product Engineering team.
An agent was asked "Show me what installing the website team would
add.". It found `GET /api/teams/catalog`, but found no team named
"website". It then searched local files, the skills catalog, npm and
GitHub until the turn timed out. It did not preview the install.

**Expected behavior**
The agent reads the teams catalog and chooses a team by its description
and roles. It previews an install with the preview route, which creates
no resources. It does not link a teams catalog page, because the web app
does not route one. It does not search outside Paperclip for other
teams. It knows who can install a team, and it names the `paperclipai
teams install` command for a board user. Preview also works in Ask and
Plan modes, because it saves no data.

**Steps to reproduce**
1. Create a company with only a CEO agent.
2. In Agent Chat, ask "Do we have a ready-made team for building a
website?".
3. Read the run. The agent does not call `GET /api/teams/catalog`, and
the reply says no team exists.
4. In a new chat, ask "Show me what installing the website team would
add.".
5. Read the run. The agent searches npm and GitHub and does not call the
preview route.

## What Changed

- `server/src/services/agent-conversations.ts`: the chat directive has a
new short paragraph. It says that ready-made teams are packages in the
teams catalog. It tells the agent to list the catalog, to choose by
description and roles, and not to search package registries,
repositories or the web. It names the preview route and says that
preview creates no resources. It says who can install. It says that the
web app has no teams catalog page, so the agent must not link one, and
it names the `paperclipai teams install <team id> --company-id
<companyId>` command for a board user.
- `server/src/routes/openapi.ts`: `GET /api/teams/catalog` now has a
description and documents its `q`, `kind` and `category` query
parameters. The preview route says that it creates no resources. The
install route says who can install and names the CLI command.
- `server/src/services/native-runtime/runner-api-catalog.ts`: the team
preview route is allowed in Ask and Plan modes, like other reads. It
computes a plan and saves no data. Install stays in standard mode only.
- Tests in `agent-conversations.test.ts` and `runner-api.test.ts`. The
API search for "ready-made team" now returns `GET /api/teams/catalog`
first. A test validates a preview call in Ask mode and rejects install
in Ask mode.

## Verification

- `pnpm exec vitest run --root server
src/services/native-runtime/runner-api.test.ts
src/__tests__/agent-conversations.test.ts
src/__tests__/openapi-routes.test.ts
src/__tests__/teams-catalog-routes.test.ts`: all tests pass.
- The change is also cherry-picked onto the eval branch
`chat-eval/case-teams`. The live Runner E2E cells for `teams.find-team`,
`teams.preview-install` and `teams.install-team` run there.

## Risks

- Low risk. Most of the change is text in the API catalog and in the
chat guidance, plus documented query parameters that the route already
reads.
- One behavior change: agents in Ask and Plan modes can now call the
team preview route. The route only computes a plan, and it applies the
same company access check.
- The chat directive is slightly longer, so each chat prompt uses a few
more tokens.
- Related open PR #18970 adds request body schemas to the same team
routes. The two changes touch nearby lines and can need a small merge.

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), with tool use and code
execution in Claude Code.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DottaandClaude Opus 5.5 authored and GitHub committed 2026-11-09 05:36:24 -06:00
1 parent 9715b3d210
commit 9db7da444f
5 files changed
+48 -2

No files matched your search

@@ -994,6 +994,16 @@ describe("chat prompt policy", () => {
expect(prompt).toContain("/<prefix>/agents/<urlKey or id>");
});
it("points chat at the teams catalog for ready-made teams and stops the search there", () => {
expect(AGENT_CHAT_DIRECTIVE).toContain("Ready-made teams are installable packages in Paperclip's teams catalog");
expect(AGENT_CHAT_DIRECTIVE).toContain("list the catalog with GET /api/teams/catalog");
expect(AGENT_CHAT_DIRECTIVE).toContain("do not look for others in package registries, repositories or the web");
expect(AGENT_CHAT_DIRECTIVE).toContain("The web app has no teams catalog page, so do not link one");
expect(AGENT_CHAT_DIRECTIVE).toContain("paperclipai teams install <team id> --company-id <companyId>");
const prompt = buildPaperclipTaskMarkdown({ issue: { id: "chat", identifier: "RUN-1", title: "Chat", workMode: "ask", conversationAgentId: "agent" } });
expect(prompt).toContain("GET /api/teams/catalog");
});
it("tells chat where a person can hire when the agent cannot", () => {
expect(AGENT_CHAT_DIRECTIVE).toContain("send the person to the hire form at /<prefix>/agents/new");
expect(AGENT_CHAT_DIRECTIVE).toContain("to your own /permissions page, where they can turn on hiring for you");
+17 -1
View File
@@ -3135,6 +3135,14 @@ registry.registerPath({
// ─── Teams Catalog ──────────────────────────────────────────────────────────
/** Ready-made teams are found here, so these entries must match how people ask for them. */
const TEAM_CATALOG_DESCRIPTIONS: Record<string, string> = {
"/api/teams/catalog":
"Lists Paperclip's ready-made teams: installable team packages, not agents already in a company. Each entry gives its name, description, roles (agentSlugs), project and routine counts, required skills (requiredSkills) and configuration inputs (envInputs). This catalog is the only source of ready-made teams; choose by description and roles rather than an exact name. Optional q filters by text in names, descriptions and tags. The web app has no teams catalog page; people browse it with the paperclipai teams browse CLI command.",
"/api/companies/{companyId}/teams/catalog/{catalogId}/preview":
"Shows what installing a catalog team would add to this company: planned agents, projects, routines and skills, and any name or slug conflicts with existing resources. Creates no resources, so it is also allowed in Ask and Plan modes. Company members and agents in the company can preview.",
};
for (const route of [
["get", "/api/teams/catalog", "List catalog teams"],
["get", "/api/teams/catalog/{catalogId}/files", "Get catalog team file"],
@@ -3160,8 +3168,16 @@ for (const route of [
path: route[1],
tags: ["teams"],
summary: route[2],
...(route[1] === "/api/teams/catalog" ? {
query: z.object({
q: z.string().optional().describe("Case-insensitive text matched against team names, descriptions and tags."),
kind: z.string().optional().describe("Catalog kind, such as bundled or optional."),
category: z.string().optional().describe("Catalog category, such as software-development."),
}),
} : {}),
...(TEAM_CATALOG_DESCRIPTIONS[route[1]] ? { description: TEAM_CATALOG_DESCRIPTIONS[route[1]] } : {}),
...(route[1].endsWith("/install") ? {
description: "Imports team resources; HTTP success does not prove every required skill is ready. Read warnings for post-import failures and skillPreparations for required sources; a preparation plan is not proof of installation. Report unresolved requirements as partial completion and preserve existing resources. Use the actual returned portabilityImport agents and projects for company-scoped resource links. Before repeating an uncertain install, inspect installed teams and saved resource identities rather than creating another copy. resourceLinks provides canonical company-scoped hrefs for actual saved agents, projects and routines, including reused resources; share these links with the installation outcome.",
description: "Installs a catalog team into this company. Requires a board user with permission to create agents, or an agent with permission to create agents; without it, a board user can run paperclipai teams install <team id> --company-id <companyId>. Imports team resources; HTTP success does not prove every required skill is ready. Read warnings for post-import failures and skillPreparations for required sources; a preparation plan is not proof of installation. Report unresolved requirements as partial completion and preserve existing resources. Use the actual returned portabilityImport agents and projects for company-scoped resource links. Before repeating an uncertain install, inspect installed teams and saved resource identities rather than creating another copy. resourceLinks provides canonical company-scoped hrefs for actual saved agents, projects and routines, including reused resources; share these links with the installation outcome.",
responses: { 201: r.ok(z.object({ resourceLinks: z.array(z.object({
kind: z.enum(["agent", "project", "routine"]), id: z.string(), name: z.string(), href: z.string(),
})) }).passthrough()), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 404: r.notFound },
@@ -119,6 +119,8 @@ When a reply lists many tasks, write each task identifier in full, such as RUN-1
A person's own display name and avatar belong to their account, and only they can change them in their profile settings at /<prefix>/company/settings/instance/profile. Agent credentials cannot edit a person's profile, and renaming an agent, a member record or the company does not change it. When the person asks to change their own name or avatar, link that page instead of attempting the change.
Ready-made teams are installable packages in Paperclip's teams catalog, not agents already in this company. When the person asks whether a ready-made team exists, or refers to one, list the catalog with GET /api/teams/catalog and choose by its descriptions and roles, since teams are not named after every kind of project. The catalog is the only source of ready-made teams: do not look for others in package registries, repositories or the web. A team chosen earlier in this chat may be recorded in its task context (get_task_context). POST /api/companies/{companyId}/teams/catalog/{catalogId}/preview shows what an install would add and creates no resources. Installing needs a board user or an agent with permission to create agents. The web app has no teams catalog page, so do not link one: name the team and its id, and when you cannot install, tell the person that a board user can run paperclipai teams install <team id> --company-id <companyId>. After an install, link the installed agents and projects at their own pages.
This conversation and your credentials belong to one company. A task identifier whose prefix differs from this conversation's prefix belongs to another company or to none, and you cannot read it here, even when the person can open it themselves: a lookup that is not found does not mean the task does not exist. Do not search for it further, guess its content or copy anything from it. Say that this chat can only work in this company, and that to use that task the person can switch to its company and open a chat there. Link the task at /<its prefix>/issues/<identifier>, taking the prefix from the identifier the person gave; this is the one link that does not use this conversation's prefix.
Tasks and projects in this company can be private. A private task is hidden from you unless you have access to it: your searches, lists and direct lookups leave it out or return not found, even when the person who asks can open it. So when the person refers to a task in this company that one search does not find, especially one they call private, it may be private rather than missing. Do not search for it further through other tools, endpoints, files or activity records, and do not guess its content. Say that this chat cannot see that task, possibly because it is private, and that if they want you to work on it they can open the task and share it with you from its sharing controls, or paste the parts they want here.
@@ -89,6 +89,8 @@ export function buildRunnerApiCatalog(document: Json = buildOpenApiDocument()):
const conversationalConfirmation = method === "POST"
&& /^\/api\/issues\/\{[^}]+\}\/interactions\/\{[^}]+\}\/resolve-from-comment$/.test(path);
const taskTitleUpdate = method === "PUT" && /^\/api\/issues\/\{[^}]+\}\/title$/.test(path);
// Computes an install plan without saving it, so read-only modes may call it.
const readOnlyPreview = method === "POST" && path === "/api/companies/{companyId}/teams/catalog/{catalogId}/preview";
const skillReference = runnerApiReference[`${method} ${path.replace(/\{[^}]+\}/g, "{}")}`];
const protocol = operation["x-paperclip-transport"] === "protocol" || (operation["x-paperclip-transport"] !== "rest" && (!path.startsWith("/api/") || /\/(oauth|auth|runtime-tools|mcp|ws)(\/|$)/.test(path)
|| /\/(claude-login|login-sessions|start-authorization|finalize-oauth-access)(\/|$)/.test(path)
@@ -116,7 +118,7 @@ export function buildRunnerApiCatalog(document: Json = buildOpenApiDocument()):
? "Prefer hire_agent for native teammate identity and persona fields. It fixes the reportsTo and source task context and inherits the caller's native runtime. Additional API fields use the route's existing agent permissions."
: "Prefer an available dedicated tool for its supported fields. call_api supports the route's other operations and fields under the same agent permissions.",
allowedModes: operation["x-paperclip-connection-management"] ? (method === "GET" ? ["standard", "ask", "planning"] : ["standard"]) : method !== "GET" && (path.endsWith("/case-handoffs") || /\/handoffs\//.test(path)) ? ["standard"]
: method === "GET" || method === "HEAD" || taskTitleUpdate ? ["standard", "ask", "planning", "skill_test"]
: method === "GET" || method === "HEAD" || taskTitleUpdate || readOnlyPreview ? ["standard", "ask", "planning", "skill_test"]
: conversationalConfirmation ? ["standard", "planning", "skill_test"] : ["standard", "skill_test"],
...(skillReference ? { skillReference } : {}),
});
@@ -222,6 +222,22 @@ describe("runner API catalog", () => {
expect(operation.description).toMatch(/agent credential/i);
expect(operation.description).toMatch(/supervisor/i);
});
it("finds the teams catalog when asked for a ready-made team", () => {
const operation = searchRunnerApi({ query: "ready-made team", limit: 1 }).results[0]!;
expect(operation.operationId).toBe("GET /api/teams/catalog");
expect(operation.description).toMatch(/only source of ready-made teams/i);
expect(operation.description).toContain("no teams catalog page");
expect(operation.parameters.map(parameter => parameter.name)).toContain("q");
const preview = runnerApiOperation("POST /api/companies/{companyId}/teams/catalog/{catalogId}/preview")!;
expect(preview.description).toMatch(/creates no resources/i);
expect(preview.allowedModes).toEqual(expect.arrayContaining(["standard", "ask", "planning"]));
expect(runnerApiOperation("POST /api/companies/{companyId}/teams/catalog/{catalogId}/install")!.allowedModes).not.toContain("ask");
const askContext = { ...context, workMode: "ask" };
expect(validateRunnerApiCall({ operationId: preview.operationId, pathParams: { catalogId: "team" }, body: {} }, askContext).operation.operationId).toBe(preview.operationId);
expect(() => validateRunnerApiCall({ operationId: "POST /api/companies/{companyId}/teams/catalog/{catalogId}/install", pathParams: { catalogId: "team" }, body: {} }, askContext)).toThrow();
const install = runnerApiOperation("POST /api/companies/{companyId}/teams/catalog/{catalogId}/install")!;
expect(install.description).toMatch(/permission to create agents/i);
});
it("discovers where to save project defaults without rewriting existing tasks", () => {
const operation = searchRunnerApi({ query: "save project defaults", limit: 1 }).results[0]!;
expect(operation.operationId).toBe("PATCH /api/projects/{id}");