fff3832a01
Fixes #7551 ## Thinking Path > - Paperclip is the control plane for AI-agent companies, and reusable company/team setup is part of making those companies faster to launch. > - The teams catalog work introduces app-shipped team templates that can be browsed, previewed, and installed into a company. > - Catalog installation crosses several contracts: bundled package contents, shared API types, server import/install behavior, CLI workflows, and the board UI. > - Agents also need a safe path through catalog installs: scoped company selection, explicit source policy, approval fallback for agent creation, and preserved catalog provenance. > - This pull request extracts the completed teams catalog branch into one reviewable PR on top of `public-gh/master`. > - The benefit is a reusable teams catalog foundation with server, CLI, package, docs, and hidden UI surfaces kept in sync. ## What Changed - Added the `@paperclipai/teams-catalog` package with bundled/optional team definitions, generated manifest, validators, catalog builder tests, and migration notes. - Added shared teams catalog types/validators plus server routes and services for listing, previewing, and installing catalog teams. - Integrated catalog install with company portability, skill/source policy checks, provenance metadata, origin hashes, target-manager reparenting, and installed/out-of-date detection. - Added CLI `teams` commands and agent-safe company selection behavior, including `company current` and approval fallback for forbidden agent-run installs. - Added hidden Team Catalog UI/API/query surfaces, Storybook fixtures, and targeted UI tests while keeping the UI route out of primary navigation. - Added docs for CLI/company/teams catalog behavior and removed generated screenshot artifacts from the PR diff. ## Verification - `pnpm exec vitest run cli/src/__tests__/company.test.ts cli/src/__tests__/teams.test.ts packages/teams-catalog/src/catalog-builder.test.ts packages/teams-catalog/src/shipped-catalog.test.ts server/src/__tests__/agent-permissions-service.test.ts server/src/__tests__/company-portability.test.ts server/src/__tests__/company-skills-service.test.ts server/src/__tests__/teams-catalog-routes.test.ts server/src/__tests__/teams-catalog-service.test.ts server/src/__tests__/teams-catalog-install-no-overrides.test.ts ui/src/lib/company-routes.test.ts ui/src/pages/TeamCard.test.tsx ui/src/pages/TeamCatalog.test.tsx ui/src/pages/useInstallTeamCatalogEntry.test.tsx` - `pnpm --filter @paperclipai/shared typecheck && pnpm --filter @paperclipai/teams-catalog typecheck && pnpm --filter paperclipai typecheck && pnpm --filter @paperclipai/server typecheck && pnpm --filter @paperclipai/ui typecheck` - Confirmed branch is rebased onto `public-gh/master` (`78dc3625a`) and `public-gh/master` is an ancestor of `HEAD`. - Confirmed PR diff excludes `pnpm-lock.yaml`, `.github/workflows/*`, generated screenshot images, and screenshot helper scripts. ## Risks - Medium review surface: this crosses package generation, shared contracts, server install behavior, CLI, docs, and hidden UI code. - Catalog install behavior creates agents/projects/tasks/skills and must keep company scoping, permissions, source policy, and provenance checks strict. - `pnpm-lock.yaml` is intentionally excluded per repo policy; CI/default-branch automation owns lockfile refresh. - The Team Catalog UI is included but hidden from primary navigation, so future enablement should re-check visual QA before exposure. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. > > ROADMAP checked: this aligns with reusable companies/templates and plugin-adjacent onboarding work. This PR packages work already developed on the Paperclip task branch for review. ## Model Used - OpenAI Codex, GPT-5 series coding agent in this Paperclip session; exact runtime context window was not exposed. Used shell, git, `gh`, and local test/typecheck tooling. ## 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 run tests locally and they pass - [x] I have added or updated tests where applicable - [x] If this change affects the UI, I have included before/after screenshots, or documented why screenshots are intentionally omitted - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
121 lines
4.9 KiB
TypeScript
121 lines
4.9 KiB
TypeScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { describe, expect, it } from "vitest";
|
|
import { catalogManifest, catalogTeams, resolveCatalogTeamRef } from "./index.js";
|
|
import { asBoolean, asString, parseFrontmatterMarkdown } from "./frontmatter.js";
|
|
import type { CatalogTeam } from "./types.js";
|
|
|
|
const EXPECTED_BUNDLED_KEYS = [
|
|
"paperclipai/bundled/company-defaults/core-exec-team",
|
|
"paperclipai/bundled/product/product-design",
|
|
"paperclipai/bundled/software-development/product-engineering",
|
|
];
|
|
|
|
const EXPECTED_OPTIONAL_KEYS = [
|
|
"paperclipai/optional/content/content-machine",
|
|
];
|
|
|
|
const PACKAGE_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
|
|
describe("shipped teams catalog", () => {
|
|
it("ships the expected bundled and optional team fixtures", () => {
|
|
const bundledKeys = catalogTeams
|
|
.filter((team) => team.kind === "bundled")
|
|
.map((team) => team.key)
|
|
.sort();
|
|
const optionalKeys = catalogTeams
|
|
.filter((team) => team.kind === "optional")
|
|
.map((team) => team.key)
|
|
.sort();
|
|
|
|
expect(bundledKeys).toEqual(EXPECTED_BUNDLED_KEYS);
|
|
expect(optionalKeys).toEqual(EXPECTED_OPTIONAL_KEYS);
|
|
});
|
|
|
|
it("keeps every shipped team free of executable scripts and external sources in Phase B", () => {
|
|
const risky = catalogTeams.filter(
|
|
(team) => team.trustLevel === "scripts_executables" || team.trustLevel === "external_sources",
|
|
);
|
|
expect(risky, formatViolations("script-bearing or external-source teams require later security review", risky)).toEqual([]);
|
|
});
|
|
|
|
it("populates browse/search-relevant fields for every shipped team", () => {
|
|
const issues: string[] = [];
|
|
for (const team of catalogTeams) {
|
|
if (team.compatibility !== "compatible") {
|
|
issues.push(`${team.key} compatibility=${team.compatibility}`);
|
|
}
|
|
if (!team.description || team.description.length < 40) {
|
|
issues.push(`${team.key} description must be at least 40 characters for catalog browse/search`);
|
|
}
|
|
if (team.recommendedForCompanyTypes.length === 0) {
|
|
issues.push(`${team.key} must list recommendedForCompanyTypes`);
|
|
}
|
|
if (team.tags.length === 0) {
|
|
issues.push(`${team.key} must list tags`);
|
|
}
|
|
if (team.rootAgentSlugs.length === 0) {
|
|
issues.push(`${team.key} must list a root agent slug`);
|
|
}
|
|
}
|
|
expect(issues).toEqual([]);
|
|
});
|
|
|
|
it("uses canonical paperclipai keys derived from kind/category/slug", () => {
|
|
const violations: string[] = [];
|
|
for (const team of catalogTeams) {
|
|
const expectedKey = `paperclipai/${team.kind}/${team.category}/${team.slug}`;
|
|
const expectedId = `paperclipai:${team.kind}:${team.category}:${team.slug}`;
|
|
if (team.key !== expectedKey) violations.push(`${team.key} should be ${expectedKey}`);
|
|
if (team.id !== expectedId) violations.push(`${team.id} should be ${expectedId}`);
|
|
}
|
|
expect(violations).toEqual([]);
|
|
});
|
|
|
|
it("exposes a stable manifest header for downstream consumers", () => {
|
|
expect(catalogManifest.schemaVersion).toBe(1);
|
|
expect(catalogManifest.packageName).toBe("@paperclipai/teams-catalog");
|
|
expect(catalogTeams.length).toBe(EXPECTED_BUNDLED_KEYS.length + EXPECTED_OPTIONAL_KEYS.length);
|
|
});
|
|
|
|
it("resolves shipped teams by id, key, and unique slug", () => {
|
|
const sample = catalogTeams.find((team) => team.key === "paperclipai/bundled/company-defaults/core-exec-team");
|
|
expect(sample, "expected core-exec-team to ship in the bundled catalog").toBeDefined();
|
|
if (!sample) return;
|
|
|
|
expect(resolveCatalogTeamRef(sample.id)).toMatchObject({ key: sample.key });
|
|
expect(resolveCatalogTeamRef(sample.key)).toMatchObject({ key: sample.key });
|
|
expect(resolveCatalogTeamRef(sample.slug)).toMatchObject({ key: sample.key });
|
|
});
|
|
|
|
it("declares a valid project for every shipped recurring task", () => {
|
|
const issues: string[] = [];
|
|
|
|
for (const team of catalogTeams) {
|
|
for (const file of team.files.filter((entry) => entry.kind === "task")) {
|
|
const absolutePath = path.join(PACKAGE_DIR, team.path, file.path);
|
|
const parsed = parseFrontmatterMarkdown(fs.readFileSync(absolutePath, "utf8"));
|
|
if (!asBoolean(parsed.frontmatter.recurring)) continue;
|
|
|
|
const project = asString(parsed.frontmatter.project);
|
|
if (!project) {
|
|
issues.push(`${team.key}/${file.path} recurring task must declare a project`);
|
|
continue;
|
|
}
|
|
if (!team.projectSlugs.includes(project)) {
|
|
issues.push(`${team.key}/${file.path} project=${project} must match a team project`);
|
|
}
|
|
}
|
|
}
|
|
|
|
expect(issues).toEqual([]);
|
|
});
|
|
});
|
|
|
|
function formatViolations(label: string, teams: CatalogTeam[]) {
|
|
if (teams.length === 0) return label;
|
|
const detail = teams.map((team) => `${team.key} (${team.trustLevel})`).join(", ");
|
|
return `${label}: ${detail}`;
|
|
}
|