diff --git a/server/src/__tests__/agent-conversations.test.ts b/server/src/__tests__/agent-conversations.test.ts index b926c65c561..3a09d586ada 100644 --- a/server/src/__tests__/agent-conversations.test.ts +++ b/server/src/__tests__/agent-conversations.test.ts @@ -994,6 +994,16 @@ describe("chat prompt policy", () => { expect(prompt).toContain("//agents/"); }); + 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 --company-id "); + 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 //agents/new"); expect(AGENT_CHAT_DIRECTIVE).toContain("to your own /permissions page, where they can turn on hiring for you"); diff --git a/server/src/routes/openapi.ts b/server/src/routes/openapi.ts index 896ce60a514..651f90bb5d5 100644 --- a/server/src/routes/openapi.ts +++ b/server/src/routes/openapi.ts @@ -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 = { + "/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 --company-id . 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 }, diff --git a/server/src/services/agent-conversations.ts b/server/src/services/agent-conversations.ts index 04d35092920..e6dbf4c51b5 100644 --- a/server/src/services/agent-conversations.ts +++ b/server/src/services/agent-conversations.ts @@ -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 //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 --company-id . 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 //issues/, 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. diff --git a/server/src/services/native-runtime/runner-api-catalog.ts b/server/src/services/native-runtime/runner-api-catalog.ts index 61957bb9282..d52681af90d 100644 --- a/server/src/services/native-runtime/runner-api-catalog.ts +++ b/server/src/services/native-runtime/runner-api-catalog.ts @@ -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 } : {}), }); diff --git a/server/src/services/native-runtime/runner-api.test.ts b/server/src/services/native-runtime/runner-api.test.ts index 32b19d21090..a3401d5c953 100644 --- a/server/src/services/native-runtime/runner-api.test.ts +++ b/server/src/services/native-runtime/runner-api.test.ts @@ -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}");