[codex] Add teams catalog extraction (#7550)

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>
This commit is contained in:
Dotta
2026-06-05 07:55:49 -10:00
committed by GitHub
parent 3657854e5e
commit fff3832a01
80 changed files with 12449 additions and 470 deletions
+154
View File
@@ -0,0 +1,154 @@
export interface MarkdownDoc {
frontmatter: Record<string, unknown>;
body: string;
hasFrontmatter: boolean;
}
export function isPlainRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
export function asString(value: unknown): string | null {
if (typeof value !== "string") return null;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
}
export function asBoolean(value: unknown): boolean | null {
return typeof value === "boolean" ? value : null;
}
export function asStringArray(value: unknown): string[] | null {
if (value === undefined) return [];
if (!Array.isArray(value)) return null;
const out: string[] = [];
for (const item of value) {
const text = asString(item);
if (!text) return null;
out.push(text);
}
return out;
}
export function parseFrontmatterMarkdown(raw: string): MarkdownDoc {
const normalized = raw.replace(/\r\n/g, "\n");
if (!normalized.startsWith("---\n")) {
return { frontmatter: {}, body: normalized.trim(), hasFrontmatter: false };
}
const closing = normalized.indexOf("\n---\n", 4);
if (closing < 0) {
return { frontmatter: {}, body: normalized.trim(), hasFrontmatter: false };
}
const frontmatterRaw = normalized.slice(4, closing).trim();
const body = normalized.slice(closing + 5).trim();
return {
frontmatter: parseYamlFrontmatter(frontmatterRaw),
body,
hasFrontmatter: true,
};
}
function parseYamlFrontmatter(raw: string): Record<string, unknown> {
const prepared = prepareYamlLines(raw);
if (prepared.length === 0) return {};
const parsed = parseYamlBlock(prepared, 0, prepared[0]!.indent);
return isPlainRecord(parsed.value) ? parsed.value : {};
}
function prepareYamlLines(raw: string) {
return raw
.split("\n")
.map((line) => ({
indent: line.match(/^ */)?.[0].length ?? 0,
content: line.trim(),
}))
.filter((line) => line.content.length > 0 && !line.content.startsWith("#"));
}
function parseYamlBlock(
lines: Array<{ indent: number; content: string }>,
startIndex: number,
indentLevel: number,
): { value: unknown; nextIndex: number } {
let index = startIndex;
if (index >= lines.length || lines[index]!.indent < indentLevel) {
return { value: {}, nextIndex: index };
}
const isArray = lines[index]!.indent === indentLevel && lines[index]!.content.startsWith("-");
if (isArray) {
const values: unknown[] = [];
while (index < lines.length) {
const line = lines[index]!;
if (line.indent < indentLevel) break;
if (line.indent !== indentLevel || !line.content.startsWith("-")) break;
const remainder = line.content.slice(1).trim();
index += 1;
if (!remainder) {
const nested = parseYamlBlock(lines, index, indentLevel + 2);
values.push(nested.value);
index = nested.nextIndex;
continue;
}
values.push(parseYamlScalar(remainder));
}
return { value: values, nextIndex: index };
}
const record: Record<string, unknown> = {};
while (index < lines.length) {
const line = lines[index]!;
if (line.indent < indentLevel) break;
if (line.indent !== indentLevel) {
index += 1;
continue;
}
const separatorIndex = line.content.indexOf(":");
if (separatorIndex <= 0) {
index += 1;
continue;
}
const key = line.content.slice(0, separatorIndex).trim();
const remainder = line.content.slice(separatorIndex + 1).trim();
index += 1;
if (!remainder) {
const nested = parseYamlBlock(lines, index, indentLevel + 2);
record[key] = nested.value;
index = nested.nextIndex;
continue;
}
record[key] = parseYamlScalar(remainder);
}
return { value: record, nextIndex: index };
}
function parseYamlScalar(rawValue: string): unknown {
const trimmed = rawValue.trim();
if (trimmed === "") return "";
if (trimmed === "null" || trimmed === "~") return null;
if (trimmed === "true") return true;
if (trimmed === "false") return false;
if (trimmed === "[]") return [];
if (trimmed === "{}") return {};
if (/^-?\d+(\.\d)?\d*$/.test(trimmed)) return Number(trimmed);
if (
trimmed.startsWith("\"") ||
trimmed.startsWith("[") ||
trimmed.startsWith("{")
) {
try {
return JSON.parse(trimmed);
} catch {
return trimmed;
}
}
return trimmed;
}
+45
View File
@@ -1,4 +1,12 @@
export { agentAdapterTypeSchema, optionalAgentAdapterTypeSchema } from "./adapter-type.js";
export {
asBoolean,
asString,
asStringArray,
isPlainRecord as isFrontmatterPlainRecord,
parseFrontmatterMarkdown,
type MarkdownDoc,
} from "./frontmatter.js";
export {
COMPANY_STATUSES,
DEFAULT_COMPANY_ATTACHMENT_MAX_BYTES,
@@ -322,6 +330,27 @@ export type {
CatalogSkillFileDetail,
CompanySkillInstallCatalogRequest,
CompanySkillInstallCatalogResult,
CatalogTeamKind,
CatalogTeamTrustLevel,
CatalogTeamCompatibility,
CatalogTeamFileKind,
CatalogTeamSkillRequirementType,
CatalogTeamSkillRequirement,
CatalogTeamEnvInputSummary,
CatalogTeamSourceRef,
CatalogTeamFile,
CatalogTeam,
CatalogManifest,
CatalogTeamListQuery,
CatalogTeamFileDetail,
CatalogTeamSourcePolicy,
CatalogTeamImportOptions,
CatalogTeamInstallOptions,
CatalogTeamSkillPreparationAction,
CatalogTeamSkillPreparation,
CatalogTeamImportPreviewResult,
CatalogTeamInstallResult,
InstalledCatalogTeam,
AgentSkillSyncMode,
AgentSkillState,
AgentSkillOrigin,
@@ -1110,6 +1139,22 @@ export {
companySkillInstallCatalogResultSchema,
companySkillInstallUpdateSchema,
companySkillResetSchema,
catalogTeamKindSchema,
catalogTeamTrustLevelSchema,
catalogTeamCompatibilitySchema,
catalogTeamFileKindSchema,
catalogTeamSkillRequirementTypeSchema,
catalogTeamSkillRequirementSchema,
catalogTeamEnvInputSummarySchema,
catalogTeamSourceRefSchema,
catalogTeamFileSchema,
catalogTeamSchema,
catalogTeamListQuerySchema,
catalogTeamFileDetailSchema,
catalogTeamSourcePolicySchema,
catalogTeamPreviewSchema,
catalogTeamInstallSchema,
catalogTeamSkillPreparationSchema,
portabilityIncludeSchema,
portabilityEnvInputSchema,
portabilityCompanyManifestEntrySchema,
@@ -138,6 +138,8 @@ export interface CompanyPortabilityAgentManifestEntry {
icon: string | null;
capabilities: string | null;
reportsToSlug: string | null;
reportsToExistingAgentId: string | null;
reportsToExistingAgentSlug: string | null;
adapterType: string;
adapterConfig: Record<string, unknown>;
runtimeConfig: Record<string, unknown>;
@@ -294,6 +296,7 @@ export interface CompanyPortabilityAdapterOverride {
export interface CompanyPortabilityImportRequest extends CompanyPortabilityPreviewRequest {
adapterOverrides?: Record<string, CompanyPortabilityAdapterOverride>;
secretValues?: Record<string, string>;
}
export interface CompanyPortabilityImportResult {
+23
View File
@@ -78,6 +78,29 @@ export type {
CompanySkillInstallCatalogRequest,
CompanySkillInstallCatalogResult,
} from "./company-skill.js";
export type {
CatalogTeamKind,
CatalogTeamTrustLevel,
CatalogTeamCompatibility,
CatalogTeamFileKind,
CatalogTeamSkillRequirementType,
CatalogTeamSkillRequirement,
CatalogTeamEnvInputSummary,
CatalogTeamSourceRef,
CatalogTeamFile,
CatalogTeam,
CatalogManifest,
CatalogTeamListQuery,
CatalogTeamFileDetail,
CatalogTeamSourcePolicy,
CatalogTeamImportOptions,
CatalogTeamInstallOptions,
CatalogTeamSkillPreparationAction,
CatalogTeamSkillPreparation,
CatalogTeamImportPreviewResult,
CatalogTeamInstallResult,
InstalledCatalogTeam,
} from "./teams-catalog.js";
export type {
AgentSkillSyncMode,
AgentSkillState,
+217
View File
@@ -0,0 +1,217 @@
import type {
CompanyPortabilityAdapterOverride,
CompanyPortabilityAgentSelection,
CompanyPortabilityCollisionStrategy,
CompanyPortabilityImportResult,
CompanyPortabilityInclude,
CompanyPortabilityPreviewResult,
} from "./company-portability.js";
export type CatalogTeamKind = "bundled" | "optional";
export type CatalogTeamTrustLevel =
| "markdown_only"
| "assets"
| "scripts_executables"
| "external_sources";
export type CatalogTeamCompatibility = "compatible" | "unknown" | "invalid";
export type CatalogTeamFileKind =
| "team"
| "agent"
| "project"
| "task"
| "skill"
| "extension"
| "readme"
| "reference"
| "script"
| "asset"
| "markdown"
| "other";
export type CatalogTeamSkillRequirementType =
| "catalog"
| "local"
| "skills_sh"
| "github"
| "url"
| "local_path"
| "agent_package";
export interface CatalogTeamSkillRequirement {
type: CatalogTeamSkillRequirementType;
ref: string;
agentSlugs: string[];
resolved: boolean;
catalogSkillId?: string;
catalogSkillKey?: string;
localPath?: string;
sourceLocator?: string;
sourceRef?: string;
}
export interface CatalogTeamEnvInputSummary {
key: string;
agentSlug: string | null;
projectSlug: string | null;
kind: "secret" | "plain";
requirement: "required" | "optional";
}
export interface CatalogTeamSourceRef {
type: Exclude<CatalogTeamSkillRequirementType, "catalog" | "local"> | "include";
ref: string;
pinned: boolean;
}
export interface CatalogTeamFile {
path: string;
kind: CatalogTeamFileKind;
sizeBytes: number;
sha256: string;
}
export interface CatalogTeam {
id: string;
key: string;
kind: CatalogTeamKind;
category: string;
slug: string;
name: string;
description: string;
path: string;
entrypoint: "TEAM.md";
schema: "agentcompanies/v1";
defaultInstall: boolean;
recommendedForCompanyTypes: string[];
tags: string[];
counts: {
agents: number;
projects: number;
tasks: number;
routines: number;
localSkills: number;
catalogSkills: number;
externalSkillSources: number;
};
rootAgentSlugs: string[];
agentSlugs: string[];
projectSlugs: string[];
requiredSkills: CatalogTeamSkillRequirement[];
envInputs: CatalogTeamEnvInputSummary[];
sourceRefs: CatalogTeamSourceRef[];
files: CatalogTeamFile[];
trustLevel: CatalogTeamTrustLevel;
compatibility: CatalogTeamCompatibility;
contentHash: string;
packageName?: string;
packageVersion?: string;
}
export interface CatalogManifest {
schemaVersion: 1;
packageName: "@paperclipai/teams-catalog";
packageVersion: string;
generatedAt: string;
teams: CatalogTeam[];
}
export interface CatalogTeamListQuery {
kind?: CatalogTeamKind;
category?: string;
q?: string;
}
export interface CatalogTeamFileDetail {
catalogTeamId: string;
path: string;
kind: CatalogTeamFileKind;
content: string;
language: string | null;
markdown: boolean;
}
export interface CatalogTeamSourcePolicy {
allowExternalSources?: boolean;
allowUnpinnedOptionalSources?: boolean;
allowLocalPathSources?: boolean;
}
export interface CatalogTeamImportOptions {
targetManagerAgentId?: string | null;
targetManagerSlug?: string | null;
include?: Partial<CompanyPortabilityInclude>;
agents?: CompanyPortabilityAgentSelection;
collisionStrategy?: CompanyPortabilityCollisionStrategy;
nameOverrides?: Record<string, string>;
selectedFiles?: string[];
sourcePolicy?: CatalogTeamSourcePolicy;
}
export interface CatalogTeamInstallOptions extends CatalogTeamImportOptions {
adapterOverrides?: Record<string, CompanyPortabilityAdapterOverride>;
secretValues?: Record<string, string>;
}
export type CatalogTeamSkillPreparationAction =
| "already_in_package"
| "catalog_install_required"
| "external_import_required"
| "blocked";
export interface CatalogTeamSkillPreparation {
type: CatalogTeamSkillRequirementType;
ref: string;
agentSlugs: string[];
action: CatalogTeamSkillPreparationAction;
catalogSkillId: string | null;
catalogSkillKey: string | null;
sourceLocator: string | null;
sourceRef: string | null;
reason: string | null;
}
export interface CatalogTeamImportPreviewResult {
team: CatalogTeam;
portabilityPreview: CompanyPortabilityPreviewResult;
skillPreparations: CatalogTeamSkillPreparation[];
warnings: string[];
errors: string[];
}
export interface CatalogTeamInstallResult {
team: CatalogTeam;
portabilityImport: CompanyPortabilityImportResult;
skillPreparations: CatalogTeamSkillPreparation[];
warnings: string[];
}
/**
* Server-computed installed-team state for a company. Surfaced by
* `GET /api/companies/:companyId/teams/catalog/installed` and consumed by the
* Team Catalog UI to render the `INSTALLED · N` group, per-row out-of-date
* badges, and the detail header "Update available" chip (design
* [PAP-10238 §3.2 + §5]).
*
* `outOfDate` is true when at least one installed agent carries a
* `metadata.paperclip.catalogTeam.originHash` that differs from the catalog
* team's current `contentHash`. `present` is false when the installed team no
* longer resolves to a catalog entry (e.g. removed from the package) in that
* case the comparison is unknown and `outOfDate` stays false.
*/
export interface InstalledCatalogTeam {
catalogId: string;
catalogKey: string | null;
/** True when the installed catalogId still resolves to a current catalog team. */
present: boolean;
/** Current catalog `contentHash` for this team, or null when not present. */
currentContentHash: string | null;
/** Distinct `originHash` values recorded across installed agents. */
installedOriginHashes: string[];
/** Number of non-terminated agents carrying this team's provenance. */
agentCount: number;
/** True when a present team has at least one stale installed originHash. */
outOfDate: boolean;
}
@@ -256,6 +256,7 @@ export const portabilityAdapterOverrideSchema = z.object({
export const companyPortabilityImportSchema = companyPortabilityPreviewSchema.extend({
adapterOverrides: z.record(z.string().min(1), portabilityAdapterOverrideSchema).optional(),
secretValues: z.record(z.string().min(1), z.string()).optional(),
});
export type CompanyPortabilityImport = z.infer<typeof companyPortabilityImportSchema>;
+21
View File
@@ -97,6 +97,27 @@ export {
type CompanySkillInstallUpdate,
type CompanySkillReset,
} from "./company-skill.js";
export {
catalogTeamKindSchema,
catalogTeamTrustLevelSchema,
catalogTeamCompatibilitySchema,
catalogTeamFileKindSchema,
catalogTeamSkillRequirementTypeSchema,
catalogTeamSkillRequirementSchema,
catalogTeamEnvInputSummarySchema,
catalogTeamSourceRefSchema,
catalogTeamFileSchema,
catalogTeamSchema,
catalogTeamListQuerySchema,
catalogTeamFileDetailSchema,
catalogTeamSourcePolicySchema,
catalogTeamPreviewSchema,
catalogTeamInstallSchema,
catalogTeamSkillPreparationSchema,
type CatalogTeamListQuery,
type CatalogTeamPreview,
type CatalogTeamInstall,
} from "./teams-catalog.js";
export {
agentSkillStateSchema,
agentSkillSyncModeSchema,
@@ -0,0 +1,171 @@
import { z } from "zod";
import {
portabilityAdapterOverrideSchema,
portabilityAgentSelectionSchema,
portabilityCollisionStrategySchema,
portabilityIncludeSchema,
} from "./company-portability.js";
export const catalogTeamKindSchema = z.enum(["bundled", "optional"]);
export const catalogTeamTrustLevelSchema = z.enum([
"markdown_only",
"assets",
"scripts_executables",
"external_sources",
]);
export const catalogTeamCompatibilitySchema = z.enum(["compatible", "unknown", "invalid"]);
export const catalogTeamFileKindSchema = z.enum([
"team",
"agent",
"project",
"task",
"skill",
"extension",
"readme",
"reference",
"script",
"asset",
"markdown",
"other",
]);
export const catalogTeamSkillRequirementTypeSchema = z.enum([
"catalog",
"local",
"skills_sh",
"github",
"url",
"local_path",
"agent_package",
]);
export const catalogTeamSkillRequirementSchema = z.object({
type: catalogTeamSkillRequirementTypeSchema,
ref: z.string().min(1),
agentSlugs: z.array(z.string().min(1)),
resolved: z.boolean(),
catalogSkillId: z.string().min(1).optional(),
catalogSkillKey: z.string().min(1).optional(),
localPath: z.string().min(1).optional(),
sourceLocator: z.string().min(1).optional(),
sourceRef: z.string().min(1).optional(),
});
export const catalogTeamEnvInputSummarySchema = z.object({
key: z.string().min(1),
agentSlug: z.string().min(1).nullable(),
projectSlug: z.string().min(1).nullable(),
kind: z.enum(["secret", "plain"]),
requirement: z.enum(["required", "optional"]),
});
export const catalogTeamSourceRefSchema = z.object({
type: z.enum(["skills_sh", "github", "url", "local_path", "agent_package", "include"]),
ref: z.string().min(1),
pinned: z.boolean(),
});
export const catalogTeamFileSchema = z.object({
path: z.string().min(1),
kind: catalogTeamFileKindSchema,
sizeBytes: z.number().int().nonnegative(),
sha256: z.string().min(1),
});
export const catalogTeamSchema = z.object({
id: z.string().min(1),
key: z.string().min(1),
kind: catalogTeamKindSchema,
category: z.string().min(1),
slug: z.string().min(1),
name: z.string().min(1),
description: z.string(),
path: z.string().min(1),
entrypoint: z.literal("TEAM.md"),
schema: z.literal("agentcompanies/v1"),
defaultInstall: z.boolean(),
recommendedForCompanyTypes: z.array(z.string()),
tags: z.array(z.string()),
counts: z.object({
agents: z.number().int().nonnegative(),
projects: z.number().int().nonnegative(),
tasks: z.number().int().nonnegative(),
routines: z.number().int().nonnegative(),
localSkills: z.number().int().nonnegative(),
catalogSkills: z.number().int().nonnegative(),
externalSkillSources: z.number().int().nonnegative(),
}),
rootAgentSlugs: z.array(z.string()),
agentSlugs: z.array(z.string()),
projectSlugs: z.array(z.string()),
requiredSkills: z.array(catalogTeamSkillRequirementSchema),
envInputs: z.array(catalogTeamEnvInputSummarySchema),
sourceRefs: z.array(catalogTeamSourceRefSchema),
files: z.array(catalogTeamFileSchema),
trustLevel: catalogTeamTrustLevelSchema,
compatibility: catalogTeamCompatibilitySchema,
contentHash: z.string().min(1),
packageName: z.string().min(1).optional(),
packageVersion: z.string().min(1).optional(),
});
export const catalogTeamListQuerySchema = z.object({
kind: catalogTeamKindSchema.optional(),
category: z.string().min(1).optional(),
q: z.string().min(1).optional(),
});
export const catalogTeamFileDetailSchema = z.object({
catalogTeamId: z.string().min(1),
path: z.string().min(1),
kind: catalogTeamFileKindSchema,
content: z.string(),
language: z.string().nullable(),
markdown: z.boolean(),
});
export const catalogTeamSourcePolicySchema = z.object({
allowExternalSources: z.boolean().optional(),
allowUnpinnedOptionalSources: z.boolean().optional(),
allowLocalPathSources: z.boolean().optional(),
}).strict();
export const catalogTeamPreviewSchema = z.object({
targetManagerAgentId: z.string().min(1).nullable().optional(),
targetManagerSlug: z.string().min(1).nullable().optional(),
include: portabilityIncludeSchema.omit({ company: true }).strict().optional(),
agents: portabilityAgentSelectionSchema.optional(),
collisionStrategy: portabilityCollisionStrategySchema.optional(),
nameOverrides: z.record(z.string().min(1), z.string().min(1)).optional(),
selectedFiles: z.array(z.string().min(1)).optional(),
sourcePolicy: catalogTeamSourcePolicySchema.optional(),
}).strict();
export const catalogTeamInstallSchema = catalogTeamPreviewSchema.extend({
adapterOverrides: z.record(z.string().min(1), portabilityAdapterOverrideSchema).optional(),
secretValues: z.record(z.string().min(1), z.string()).optional(),
}).strict();
export const catalogTeamSkillPreparationSchema = z.object({
type: catalogTeamSkillRequirementTypeSchema,
ref: z.string().min(1),
agentSlugs: z.array(z.string().min(1)),
action: z.enum([
"already_in_package",
"catalog_install_required",
"external_import_required",
"blocked",
]),
catalogSkillId: z.string().min(1).nullable(),
catalogSkillKey: z.string().min(1).nullable(),
sourceLocator: z.string().min(1).nullable(),
sourceRef: z.string().min(1).nullable(),
reason: z.string().min(1).nullable(),
});
export type CatalogTeamListQuery = z.infer<typeof catalogTeamListQuerySchema>;
export type CatalogTeamPreview = z.infer<typeof catalogTeamPreviewSchema>;
export type CatalogTeamInstall = z.infer<typeof catalogTeamInstallSchema>;