@paperclipai/teams-catalog 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/catalog/bundled/company-defaults/core-exec-team/TEAM.md +44 -0
- package/catalog/bundled/company-defaults/core-exec-team/agents/ceo/AGENTS.md +51 -0
- package/catalog/bundled/company-defaults/core-exec-team/agents/cto/AGENTS.md +33 -0
- package/catalog/bundled/company-defaults/core-exec-team/agents/qa/AGENTS.md +31 -0
- package/catalog/bundled/company-defaults/core-exec-team/projects/first-project/PROJECT.md +8 -0
- package/catalog/bundled/company-defaults/core-exec-team/projects/first-project/tasks/first-heartbeat/TASK.md +9 -0
- package/catalog/bundled/product/product-design/TEAM.md +44 -0
- package/catalog/bundled/product/product-design/agents/ux-designer/AGENTS.md +45 -0
- package/catalog/bundled/product/product-design/projects/product-design/PROJECT.md +8 -0
- package/catalog/bundled/product/product-design/projects/product-design/tasks/weekly-design-review/TASK.md +9 -0
- package/catalog/bundled/software-development/product-engineering/.paperclip.yaml +5 -0
- package/catalog/bundled/software-development/product-engineering/TEAM.md +51 -0
- package/catalog/bundled/software-development/product-engineering/agents/cto/AGENTS.md +34 -0
- package/catalog/bundled/software-development/product-engineering/agents/qa/AGENTS.md +30 -0
- package/catalog/bundled/software-development/product-engineering/agents/senior-coder/AGENTS.md +35 -0
- package/catalog/bundled/software-development/product-engineering/projects/product-engineering/PROJECT.md +8 -0
- package/catalog/bundled/software-development/product-engineering/projects/product-engineering/tasks/weekly-engineering-sync/TASK.md +9 -0
- package/catalog/optional/content/content-machine/TEAM.md +30 -0
- package/catalog/optional/content/content-machine/agents/content-lead/AGENTS.md +11 -0
- package/catalog/optional/content/content-machine/projects/content-operations/PROJECT.md +8 -0
- package/catalog/optional/content/content-machine/projects/content-operations/tasks/weekly-content-review/TASK.md +9 -0
- package/catalog/optional/content/content-machine/skills/content-calendar/SKILL.md +12 -0
- package/dist/generated/catalog.json +467 -0
- package/dist/scripts/build-catalog-manifest.d.ts +2 -0
- package/dist/scripts/build-catalog-manifest.d.ts.map +1 -0
- package/dist/scripts/build-catalog-manifest.js +11 -0
- package/dist/scripts/build-catalog-manifest.js.map +1 -0
- package/dist/scripts/validate-catalog.d.ts +2 -0
- package/dist/scripts/validate-catalog.d.ts.map +1 -0
- package/dist/scripts/validate-catalog.js +11 -0
- package/dist/scripts/validate-catalog.js.map +1 -0
- package/dist/src/catalog-builder.d.ts +22 -0
- package/dist/src/catalog-builder.d.ts.map +1 -0
- package/dist/src/catalog-builder.js +780 -0
- package/dist/src/catalog-builder.js.map +1 -0
- package/dist/src/catalog-builder.test.d.ts +2 -0
- package/dist/src/catalog-builder.test.d.ts.map +1 -0
- package/dist/src/catalog-builder.test.js +238 -0
- package/dist/src/catalog-builder.test.js.map +1 -0
- package/dist/src/frontmatter.d.ts +11 -0
- package/dist/src/frontmatter.d.ts.map +1 -0
- package/dist/src/frontmatter.js +141 -0
- package/dist/src/frontmatter.js.map +1 -0
- package/dist/src/index.d.ts +7 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +21 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/shipped-catalog.test.d.ts +2 -0
- package/dist/src/shipped-catalog.test.d.ts.map +1 -0
- package/dist/src/shipped-catalog.test.js +107 -0
- package/dist/src/shipped-catalog.test.js.map +1 -0
- package/dist/src/types.d.ts +81 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/generated/catalog.json +467 -0
- package/package.json +49 -0
|
@@ -0,0 +1,780 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { asBoolean, asString, asStringArray, isPlainRecord, parseFrontmatterMarkdown, } from "./frontmatter.js";
|
|
6
|
+
const CATALOG_PACKAGE_NAME = "@paperclipai/teams-catalog";
|
|
7
|
+
const CATALOG_SCHEMA_VERSION = 1;
|
|
8
|
+
const TEAM_ENTRYPOINT = "TEAM.md";
|
|
9
|
+
const MAX_CATALOG_FILE_BYTES = 1024 * 1024;
|
|
10
|
+
const SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
11
|
+
const CATALOG_KINDS = new Set(["bundled", "optional"]);
|
|
12
|
+
const TEAM_SCHEMA = "agentcompanies/v1";
|
|
13
|
+
const LOCAL_PATH_SOURCE_TYPES = new Set(["local_path"]);
|
|
14
|
+
const EXTERNAL_SOURCE_TYPES = new Set(["skills_sh", "github", "url", "agent_package"]);
|
|
15
|
+
export function formatCatalogManifest(manifest) {
|
|
16
|
+
return `${JSON.stringify(manifest, null, 2)}\n`;
|
|
17
|
+
}
|
|
18
|
+
export async function buildExpectedCatalogManifest(packageDir) {
|
|
19
|
+
const existing = await readExistingManifest(packageDir);
|
|
20
|
+
const firstPass = await buildCatalogManifest({
|
|
21
|
+
packageDir,
|
|
22
|
+
generatedAt: existing?.generatedAt ?? new Date().toISOString(),
|
|
23
|
+
});
|
|
24
|
+
if (existing && sameManifestExceptGeneratedAt(existing, firstPass.manifest)) {
|
|
25
|
+
return firstPass;
|
|
26
|
+
}
|
|
27
|
+
return buildCatalogManifest({
|
|
28
|
+
packageDir,
|
|
29
|
+
generatedAt: new Date().toISOString(),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
export async function buildCatalogManifest(options) {
|
|
33
|
+
const packageDir = path.resolve(options.packageDir);
|
|
34
|
+
const packageJson = await readPackageJson(packageDir);
|
|
35
|
+
const errors = [];
|
|
36
|
+
const catalogSkills = options.catalogSkills ?? await loadCatalogSkills(packageDir, errors);
|
|
37
|
+
const candidates = await discoverTeamCandidates(packageDir, errors);
|
|
38
|
+
const teams = [];
|
|
39
|
+
collectCandidateUniquenessErrors(candidates, errors);
|
|
40
|
+
for (const candidate of candidates) {
|
|
41
|
+
const team = await buildCatalogTeam(packageDir, candidate, catalogSkills, errors);
|
|
42
|
+
if (team)
|
|
43
|
+
teams.push(team);
|
|
44
|
+
}
|
|
45
|
+
teams.sort((a, b) => a.id.localeCompare(b.id));
|
|
46
|
+
collectUniquenessErrors(teams, errors);
|
|
47
|
+
return {
|
|
48
|
+
manifest: {
|
|
49
|
+
schemaVersion: CATALOG_SCHEMA_VERSION,
|
|
50
|
+
packageName: CATALOG_PACKAGE_NAME,
|
|
51
|
+
packageVersion: packageJson.version,
|
|
52
|
+
generatedAt: options.generatedAt ?? new Date().toISOString(),
|
|
53
|
+
teams,
|
|
54
|
+
},
|
|
55
|
+
errors,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export async function validateCatalog(packageDir) {
|
|
59
|
+
const expected = await buildExpectedCatalogManifest(packageDir);
|
|
60
|
+
const generatedPath = path.join(packageDir, "generated", "catalog.json");
|
|
61
|
+
const errors = [...expected.errors];
|
|
62
|
+
let generatedText = null;
|
|
63
|
+
try {
|
|
64
|
+
generatedText = await fs.readFile(generatedPath, "utf8");
|
|
65
|
+
JSON.parse(generatedText);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
errors.push(`generated/catalog.json is missing or invalid: ${errorMessage(error)}`);
|
|
69
|
+
}
|
|
70
|
+
if (generatedText !== null) {
|
|
71
|
+
const expectedText = formatCatalogManifest(expected.manifest);
|
|
72
|
+
if (generatedText !== expectedText) {
|
|
73
|
+
errors.push("generated/catalog.json is stale. Run pnpm --filter @paperclipai/teams-catalog build:manifest.");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
manifest: expected.manifest,
|
|
78
|
+
errors,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
export async function writeCatalogManifest(packageDir) {
|
|
82
|
+
const result = await buildExpectedCatalogManifest(packageDir);
|
|
83
|
+
if (result.errors.length > 0)
|
|
84
|
+
return result;
|
|
85
|
+
const generatedDir = path.join(packageDir, "generated");
|
|
86
|
+
await fs.mkdir(generatedDir, { recursive: true });
|
|
87
|
+
await fs.writeFile(path.join(generatedDir, "catalog.json"), formatCatalogManifest(result.manifest), "utf8");
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
async function readPackageJson(packageDir) {
|
|
91
|
+
const packageJsonPath = path.join(packageDir, "package.json");
|
|
92
|
+
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf8"));
|
|
93
|
+
const version = asString(packageJson.version);
|
|
94
|
+
if (!version)
|
|
95
|
+
throw new Error(`${packageJsonPath} must declare a package version.`);
|
|
96
|
+
return { version };
|
|
97
|
+
}
|
|
98
|
+
async function readExistingManifest(packageDir) {
|
|
99
|
+
try {
|
|
100
|
+
return JSON.parse(await fs.readFile(path.join(packageDir, "generated", "catalog.json"), "utf8"));
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async function loadCatalogSkills(packageDir, errors) {
|
|
107
|
+
try {
|
|
108
|
+
const catalogPackageName = "@paperclipai/skills-catalog";
|
|
109
|
+
const catalog = await import(catalogPackageName);
|
|
110
|
+
const skills = catalog.catalogSkills;
|
|
111
|
+
return skills.map((skill) => ({ id: skill.id, key: skill.key, slug: skill.slug }));
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
const siblingManifestPath = path.resolve(packageDir, "..", "skills-catalog", "generated", "catalog.json");
|
|
115
|
+
try {
|
|
116
|
+
const manifest = JSON.parse(await fs.readFile(siblingManifestPath, "utf8"));
|
|
117
|
+
return (manifest.skills ?? []).map((skill) => ({ id: skill.id, key: skill.key, slug: skill.slug }));
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
errors.push(`Could not load @paperclipai/skills-catalog for skill requirement validation: ${errorMessage(error)}`);
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async function discoverTeamCandidates(packageDir, errors) {
|
|
126
|
+
const catalogDir = path.join(packageDir, "catalog");
|
|
127
|
+
const candidates = [];
|
|
128
|
+
if (!existsSync(catalogDir)) {
|
|
129
|
+
errors.push("catalog directory is missing.");
|
|
130
|
+
return candidates;
|
|
131
|
+
}
|
|
132
|
+
await collectMisplacedTeamFiles(catalogDir, errors);
|
|
133
|
+
for (const kind of ["bundled", "optional"]) {
|
|
134
|
+
const kindDir = path.join(catalogDir, kind);
|
|
135
|
+
if (!existsSync(kindDir))
|
|
136
|
+
continue;
|
|
137
|
+
for (const categoryEntry of await sortedDirEntries(kindDir)) {
|
|
138
|
+
if (!categoryEntry.isDirectory())
|
|
139
|
+
continue;
|
|
140
|
+
const category = categoryEntry.name;
|
|
141
|
+
const categoryDir = path.join(kindDir, category);
|
|
142
|
+
for (const slugEntry of await sortedDirEntries(categoryDir)) {
|
|
143
|
+
if (!slugEntry.isDirectory())
|
|
144
|
+
continue;
|
|
145
|
+
const slug = slugEntry.name;
|
|
146
|
+
const teamDir = path.join(categoryDir, slug);
|
|
147
|
+
if (!existsSync(path.join(teamDir, TEAM_ENTRYPOINT))) {
|
|
148
|
+
errors.push(`${relativePackagePath(packageDir, teamDir)} is missing TEAM.md.`);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
candidates.push({ kind, category, slug, absolutePath: teamDir });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return candidates;
|
|
156
|
+
}
|
|
157
|
+
async function collectMisplacedTeamFiles(catalogDir, errors) {
|
|
158
|
+
async function visit(dir) {
|
|
159
|
+
for (const entry of await sortedDirEntries(dir)) {
|
|
160
|
+
const absolutePath = path.join(dir, entry.name);
|
|
161
|
+
if (entry.isDirectory()) {
|
|
162
|
+
await visit(absolutePath);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (entry.name !== TEAM_ENTRYPOINT)
|
|
166
|
+
continue;
|
|
167
|
+
const relativePath = toPosixPath(path.relative(catalogDir, absolutePath));
|
|
168
|
+
const parts = relativePath.split("/");
|
|
169
|
+
const kind = parts[0];
|
|
170
|
+
if (parts.length !== 4 || !CATALOG_KINDS.has(kind)) {
|
|
171
|
+
errors.push(`catalog/${relativePath} is not under catalog/<bundled|optional>/<category>/<slug>/TEAM.md.`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
await visit(catalogDir);
|
|
176
|
+
}
|
|
177
|
+
async function buildCatalogTeam(packageDir, candidate, catalogSkills, errors) {
|
|
178
|
+
const prefix = relativePackagePath(packageDir, candidate.absolutePath);
|
|
179
|
+
validateSlug("category", candidate.category, prefix, errors);
|
|
180
|
+
validateSlug("slug", candidate.slug, prefix, errors);
|
|
181
|
+
const id = `paperclipai:${candidate.kind}:${candidate.category}:${candidate.slug}`;
|
|
182
|
+
const key = `paperclipai/${candidate.kind}/${candidate.category}/${candidate.slug}`;
|
|
183
|
+
const teamMarkdownPath = path.join(candidate.absolutePath, TEAM_ENTRYPOINT);
|
|
184
|
+
const parsed = parseFrontmatterMarkdown(await fs.readFile(teamMarkdownPath, "utf8"));
|
|
185
|
+
if (!parsed.hasFrontmatter) {
|
|
186
|
+
errors.push(`${prefix}/TEAM.md must start with YAML frontmatter.`);
|
|
187
|
+
}
|
|
188
|
+
const name = asString(parsed.frontmatter.name);
|
|
189
|
+
if (!name)
|
|
190
|
+
errors.push(`${prefix}/TEAM.md frontmatter must include name.`);
|
|
191
|
+
const description = asString(parsed.frontmatter.description);
|
|
192
|
+
if (!description)
|
|
193
|
+
errors.push(`${prefix}/TEAM.md frontmatter must include description.`);
|
|
194
|
+
const schema = asString(parsed.frontmatter.schema);
|
|
195
|
+
if (schema !== TEAM_SCHEMA) {
|
|
196
|
+
errors.push(`${prefix}/TEAM.md schema must be ${TEAM_SCHEMA}.`);
|
|
197
|
+
}
|
|
198
|
+
const explicitKey = asString(parsed.frontmatter.key);
|
|
199
|
+
if (explicitKey && explicitKey !== key) {
|
|
200
|
+
errors.push(`${prefix}/TEAM.md key must be ${key}.`);
|
|
201
|
+
}
|
|
202
|
+
const explicitSlug = asString(parsed.frontmatter.slug);
|
|
203
|
+
if (explicitSlug && explicitSlug !== candidate.slug) {
|
|
204
|
+
errors.push(`${prefix}/TEAM.md slug must be ${candidate.slug}.`);
|
|
205
|
+
}
|
|
206
|
+
const explicitCategory = asString(parsed.frontmatter.category);
|
|
207
|
+
if (explicitCategory && explicitCategory !== candidate.category) {
|
|
208
|
+
errors.push(`${prefix}/TEAM.md category must be ${candidate.category}.`);
|
|
209
|
+
}
|
|
210
|
+
const defaultInstall = asBoolean(parsed.frontmatter.defaultInstall) ?? false;
|
|
211
|
+
const recommendedForCompanyTypes = readStringArrayField(parsed.frontmatter.recommendedForCompanyTypes, "recommendedForCompanyTypes", prefix, errors);
|
|
212
|
+
const tags = readStringArrayField(parsed.frontmatter.tags, "tags", prefix, errors);
|
|
213
|
+
const files = await collectTeamFiles(packageDir, candidate.absolutePath, prefix, errors);
|
|
214
|
+
const graph = await readTeamPackageGraph(candidate.absolutePath, errors);
|
|
215
|
+
const agentSlugs = collectSlugs(graph.agents, "agent", errors);
|
|
216
|
+
const projectSlugs = collectSlugs(graph.projects, "project", errors);
|
|
217
|
+
const taskRecords = graph.tasks.map((task) => ({
|
|
218
|
+
slug: readSlug(task, "task", errors),
|
|
219
|
+
recurring: asBoolean(task.frontmatter.recurring) ?? false,
|
|
220
|
+
assignee: asString(task.frontmatter.assignee),
|
|
221
|
+
project: asString(task.frontmatter.project),
|
|
222
|
+
path: task.relativePath,
|
|
223
|
+
}));
|
|
224
|
+
const localSkillSlugs = collectSlugs(graph.skills, "skill", errors);
|
|
225
|
+
const rootAgentSlugs = validateLocalReferences(candidate.absolutePath, parsed.frontmatter, graph, agentSlugs, projectSlugs, errors);
|
|
226
|
+
const requiredSkills = collectRequiredSkills(candidate.absolutePath, parsed.frontmatter, graph, catalogSkills, agentSlugs, localSkillSlugs, errors);
|
|
227
|
+
const envInputs = collectEnvInputs(graph);
|
|
228
|
+
const sourceRefs = collectSourceRefs(parsed.frontmatter, requiredSkills);
|
|
229
|
+
const catalogSkillCount = new Set(requiredSkills.filter((skill) => skill.type === "catalog").map((skill) => skill.catalogSkillId ?? skill.ref)).size;
|
|
230
|
+
if (!name || !description || schema !== TEAM_SCHEMA)
|
|
231
|
+
return null;
|
|
232
|
+
return {
|
|
233
|
+
id,
|
|
234
|
+
key,
|
|
235
|
+
kind: candidate.kind,
|
|
236
|
+
category: candidate.category,
|
|
237
|
+
slug: candidate.slug,
|
|
238
|
+
name,
|
|
239
|
+
description,
|
|
240
|
+
path: toPosixPath(path.relative(packageDir, candidate.absolutePath)),
|
|
241
|
+
entrypoint: TEAM_ENTRYPOINT,
|
|
242
|
+
schema: TEAM_SCHEMA,
|
|
243
|
+
defaultInstall,
|
|
244
|
+
recommendedForCompanyTypes,
|
|
245
|
+
tags,
|
|
246
|
+
counts: {
|
|
247
|
+
agents: graph.agents.length,
|
|
248
|
+
projects: graph.projects.length,
|
|
249
|
+
tasks: taskRecords.filter((task) => !task.recurring).length,
|
|
250
|
+
routines: taskRecords.filter((task) => task.recurring).length,
|
|
251
|
+
localSkills: graph.skills.length,
|
|
252
|
+
catalogSkills: catalogSkillCount,
|
|
253
|
+
externalSkillSources: sourceRefs.filter((ref) => ref.type !== "include").length,
|
|
254
|
+
},
|
|
255
|
+
rootAgentSlugs,
|
|
256
|
+
agentSlugs: agentSlugs.sort(),
|
|
257
|
+
projectSlugs: projectSlugs.sort(),
|
|
258
|
+
requiredSkills,
|
|
259
|
+
envInputs,
|
|
260
|
+
sourceRefs,
|
|
261
|
+
files,
|
|
262
|
+
trustLevel: deriveTrustLevel(files, sourceRefs),
|
|
263
|
+
compatibility: "compatible",
|
|
264
|
+
contentHash: buildContentHash(files),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
async function collectTeamFiles(packageDir, teamDir, prefix, errors) {
|
|
268
|
+
const files = [];
|
|
269
|
+
const teamRoot = await fs.realpath(teamDir);
|
|
270
|
+
async function visit(dir) {
|
|
271
|
+
for (const entry of await sortedDirEntries(dir)) {
|
|
272
|
+
const absolutePath = path.join(dir, entry.name);
|
|
273
|
+
const lstat = await fs.lstat(absolutePath);
|
|
274
|
+
let stat = lstat;
|
|
275
|
+
let realPath = absolutePath;
|
|
276
|
+
if (lstat.isSymbolicLink()) {
|
|
277
|
+
try {
|
|
278
|
+
realPath = await fs.realpath(absolutePath);
|
|
279
|
+
stat = await fs.stat(absolutePath);
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
errors.push(`${relativePackagePath(packageDir, absolutePath)} is a broken symlink.`);
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (!isPathInside(teamRoot, realPath)) {
|
|
286
|
+
errors.push(`${relativePackagePath(packageDir, absolutePath)} points outside its team directory.`);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (stat.isDirectory()) {
|
|
290
|
+
errors.push(`${relativePackagePath(packageDir, absolutePath)} is a directory symlink; copy files into the team directory instead.`);
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (stat.isDirectory()) {
|
|
295
|
+
await visit(absolutePath);
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if (!stat.isFile())
|
|
299
|
+
continue;
|
|
300
|
+
const relativePath = toPosixPath(path.relative(teamDir, absolutePath));
|
|
301
|
+
if (path.isAbsolute(relativePath) || relativePath.split("/").includes("..")) {
|
|
302
|
+
errors.push(`${prefix}/${relativePath} has an invalid inventory path.`);
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
if (stat.size > MAX_CATALOG_FILE_BYTES) {
|
|
306
|
+
errors.push(`${prefix}/${relativePath} exceeds ${MAX_CATALOG_FILE_BYTES} bytes.`);
|
|
307
|
+
}
|
|
308
|
+
const contents = await fs.readFile(absolutePath);
|
|
309
|
+
files.push({
|
|
310
|
+
path: relativePath,
|
|
311
|
+
kind: classifyCatalogFile(relativePath),
|
|
312
|
+
sizeBytes: stat.size,
|
|
313
|
+
sha256: sha256(contents),
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
await visit(teamDir);
|
|
318
|
+
files.sort((a, b) => {
|
|
319
|
+
if (a.path === TEAM_ENTRYPOINT)
|
|
320
|
+
return -1;
|
|
321
|
+
if (b.path === TEAM_ENTRYPOINT)
|
|
322
|
+
return 1;
|
|
323
|
+
return a.path.localeCompare(b.path);
|
|
324
|
+
});
|
|
325
|
+
if (!files.some((file) => file.path === TEAM_ENTRYPOINT && file.kind === "team")) {
|
|
326
|
+
errors.push(`${prefix} inventory does not contain TEAM.md.`);
|
|
327
|
+
}
|
|
328
|
+
return files;
|
|
329
|
+
}
|
|
330
|
+
async function readTeamPackageGraph(teamDir, errors) {
|
|
331
|
+
const graph = {
|
|
332
|
+
agents: [],
|
|
333
|
+
projects: [],
|
|
334
|
+
tasks: [],
|
|
335
|
+
skills: [],
|
|
336
|
+
};
|
|
337
|
+
async function visit(dir) {
|
|
338
|
+
for (const entry of await sortedDirEntries(dir)) {
|
|
339
|
+
const absolutePath = path.join(dir, entry.name);
|
|
340
|
+
const stat = await fs.lstat(absolutePath);
|
|
341
|
+
if (stat.isDirectory()) {
|
|
342
|
+
await visit(absolutePath);
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
if (!stat.isFile())
|
|
346
|
+
continue;
|
|
347
|
+
const relativePath = toPosixPath(path.relative(teamDir, absolutePath));
|
|
348
|
+
const bucket = graphBucketForFile(relativePath);
|
|
349
|
+
if (!bucket)
|
|
350
|
+
continue;
|
|
351
|
+
const doc = parseFrontmatterMarkdown(await fs.readFile(absolutePath, "utf8"));
|
|
352
|
+
if (!doc.hasFrontmatter)
|
|
353
|
+
errors.push(`${relativePath} must start with YAML frontmatter.`);
|
|
354
|
+
graph[bucket].push({
|
|
355
|
+
relativePath,
|
|
356
|
+
frontmatter: doc.frontmatter,
|
|
357
|
+
hasFrontmatter: doc.hasFrontmatter,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
await visit(teamDir);
|
|
362
|
+
return graph;
|
|
363
|
+
}
|
|
364
|
+
function graphBucketForFile(relativePath) {
|
|
365
|
+
if (relativePath.endsWith("/AGENTS.md") || relativePath === "AGENTS.md")
|
|
366
|
+
return "agents";
|
|
367
|
+
if (relativePath.endsWith("/PROJECT.md") || relativePath === "PROJECT.md")
|
|
368
|
+
return "projects";
|
|
369
|
+
if (relativePath.endsWith("/TASK.md") || relativePath === "TASK.md")
|
|
370
|
+
return "tasks";
|
|
371
|
+
if (relativePath.endsWith("/SKILL.md") || relativePath === "SKILL.md")
|
|
372
|
+
return "skills";
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
function validateLocalReferences(teamDir, teamFrontmatter, graph, agentSlugs, projectSlugs, errors) {
|
|
376
|
+
const manager = asString(teamFrontmatter.manager);
|
|
377
|
+
const rootAgentSlugs = [];
|
|
378
|
+
if (!manager) {
|
|
379
|
+
errors.push(`${TEAM_ENTRYPOINT} frontmatter must include manager.`);
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
const managerPath = resolveTeamReference(teamDir, TEAM_ENTRYPOINT, manager, errors);
|
|
383
|
+
const managerAgent = managerPath ? graph.agents.find((agent) => agent.relativePath === managerPath) : null;
|
|
384
|
+
if (!managerAgent) {
|
|
385
|
+
errors.push(`${TEAM_ENTRYPOINT} manager must resolve to an AGENTS.md file inside the team package: ${manager}.`);
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
rootAgentSlugs.push(readSlug(managerAgent, "agent", errors));
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
for (const include of readIncludeEntries(teamFrontmatter)) {
|
|
392
|
+
if (isExternalRef(include))
|
|
393
|
+
continue;
|
|
394
|
+
const resolved = resolveTeamReference(teamDir, TEAM_ENTRYPOINT, include, errors);
|
|
395
|
+
if (resolved && !existsSync(path.join(teamDir, resolved))) {
|
|
396
|
+
errors.push(`${TEAM_ENTRYPOINT} include does not exist: ${include}.`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
for (const agent of graph.agents) {
|
|
400
|
+
const reportsTo = asString(agent.frontmatter.reportsTo);
|
|
401
|
+
if (reportsTo && reportsTo !== "null" && !agentSlugs.includes(reportsTo)) {
|
|
402
|
+
errors.push(`${agent.relativePath} reportsTo references unknown agent slug "${reportsTo}".`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
for (const project of graph.projects) {
|
|
406
|
+
const owner = asString(project.frontmatter.owner) ?? asString(project.frontmatter.leadAgent);
|
|
407
|
+
if (owner && !agentSlugs.includes(owner)) {
|
|
408
|
+
errors.push(`${project.relativePath} owner references unknown agent slug "${owner}".`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
for (const task of graph.tasks) {
|
|
412
|
+
const assignee = asString(task.frontmatter.assignee);
|
|
413
|
+
if (assignee && !agentSlugs.includes(assignee)) {
|
|
414
|
+
errors.push(`${task.relativePath} assignee references unknown agent slug "${assignee}".`);
|
|
415
|
+
}
|
|
416
|
+
const project = asString(task.frontmatter.project);
|
|
417
|
+
if (project && !projectSlugs.includes(project)) {
|
|
418
|
+
errors.push(`${task.relativePath} project references unknown project slug "${project}".`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return Array.from(new Set(rootAgentSlugs.filter(Boolean))).sort();
|
|
422
|
+
}
|
|
423
|
+
function collectRequiredSkills(teamDir, teamFrontmatter, graph, catalogSkills, agentSlugs, localSkillSlugs, errors) {
|
|
424
|
+
const requirements = new Map();
|
|
425
|
+
function upsert(requirement) {
|
|
426
|
+
const key = requirementIdentity(requirement);
|
|
427
|
+
const existing = requirements.get(key);
|
|
428
|
+
if (!existing) {
|
|
429
|
+
requirements.set(key, requirement);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
existing.agentSlugs = Array.from(new Set([...existing.agentSlugs, ...requirement.agentSlugs])).sort();
|
|
433
|
+
}
|
|
434
|
+
for (const agent of graph.agents) {
|
|
435
|
+
const agentSlug = readSlug(agent, "agent", errors);
|
|
436
|
+
const skills = readStringArrayField(agent.frontmatter.skills, "skills", agent.relativePath, errors);
|
|
437
|
+
for (const skillRef of skills) {
|
|
438
|
+
upsert(resolveSkillRequirement(skillRef, [agentSlug], catalogSkills, localSkillSlugs, errors, agent.relativePath));
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
for (const declared of readRequiredSkillEntries(teamFrontmatter, errors)) {
|
|
442
|
+
upsert(resolveDeclaredSkillRequirement(teamDir, declared, catalogSkills, localSkillSlugs, agentSlugs, errors));
|
|
443
|
+
}
|
|
444
|
+
return Array.from(requirements.values()).sort((a, b) => `${a.type}:${a.ref}`.localeCompare(`${b.type}:${b.ref}`));
|
|
445
|
+
}
|
|
446
|
+
function requirementIdentity(requirement) {
|
|
447
|
+
if (requirement.type === "catalog")
|
|
448
|
+
return `catalog:${requirement.catalogSkillId ?? requirement.catalogSkillKey ?? requirement.ref}`;
|
|
449
|
+
if (requirement.type === "local")
|
|
450
|
+
return `local:${requirement.localPath ?? requirement.ref}`;
|
|
451
|
+
return `${requirement.type}:${requirement.sourceLocator ?? requirement.ref}`;
|
|
452
|
+
}
|
|
453
|
+
function resolveSkillRequirement(ref, agentSlugs, catalogSkills, localSkillSlugs, errors, prefix) {
|
|
454
|
+
if (localSkillSlugs.includes(ref)) {
|
|
455
|
+
return {
|
|
456
|
+
type: "local",
|
|
457
|
+
ref,
|
|
458
|
+
agentSlugs: agentSlugs.sort(),
|
|
459
|
+
resolved: true,
|
|
460
|
+
localPath: `skills/${ref}/SKILL.md`,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
const catalogSkill = resolveCatalogSkill(ref, catalogSkills);
|
|
464
|
+
if (catalogSkill) {
|
|
465
|
+
return {
|
|
466
|
+
type: "catalog",
|
|
467
|
+
ref,
|
|
468
|
+
agentSlugs: agentSlugs.sort(),
|
|
469
|
+
resolved: true,
|
|
470
|
+
catalogSkillId: catalogSkill.id,
|
|
471
|
+
catalogSkillKey: catalogSkill.key,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
errors.push(`${prefix} skill reference "${ref}" does not resolve to a local team skill or @paperclipai/skills-catalog skill.`);
|
|
475
|
+
return {
|
|
476
|
+
type: "catalog",
|
|
477
|
+
ref,
|
|
478
|
+
agentSlugs: agentSlugs.sort(),
|
|
479
|
+
resolved: false,
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
function resolveDeclaredSkillRequirement(teamDir, declared, catalogSkills, localSkillSlugs, agentSlugs, errors) {
|
|
483
|
+
if (typeof declared === "string") {
|
|
484
|
+
return resolveSkillRequirement(declared.trim(), [], catalogSkills, localSkillSlugs, errors, TEAM_ENTRYPOINT);
|
|
485
|
+
}
|
|
486
|
+
if (!isPlainRecord(declared)) {
|
|
487
|
+
errors.push(`${TEAM_ENTRYPOINT} requiredSkills entries must be strings or objects.`);
|
|
488
|
+
return { type: "catalog", ref: "", agentSlugs: [], resolved: false };
|
|
489
|
+
}
|
|
490
|
+
const type = asString(declared.type) ?? asString(declared.sourceType) ?? "catalog";
|
|
491
|
+
const ref = asString(declared.ref)
|
|
492
|
+
?? asString(declared.catalogSkillId)
|
|
493
|
+
?? asString(declared.key)
|
|
494
|
+
?? asString(declared.slug)
|
|
495
|
+
?? asString(declared.url)
|
|
496
|
+
?? asString(declared.path)
|
|
497
|
+
?? "";
|
|
498
|
+
const requirementAgentSlugs = readStringArrayLoose(declared.agentSlugs).filter((slug) => agentSlugs.includes(slug)).sort();
|
|
499
|
+
if (!isSkillRequirementType(type)) {
|
|
500
|
+
errors.push(`${TEAM_ENTRYPOINT} requiredSkills type "${type}" is not supported.`);
|
|
501
|
+
return { type: "catalog", ref, agentSlugs: requirementAgentSlugs, resolved: false };
|
|
502
|
+
}
|
|
503
|
+
if (!ref) {
|
|
504
|
+
errors.push(`${TEAM_ENTRYPOINT} requiredSkills ${type} entry must include a ref, key, slug, url, or path.`);
|
|
505
|
+
return { type, ref, agentSlugs: requirementAgentSlugs, resolved: false };
|
|
506
|
+
}
|
|
507
|
+
if (type === "catalog") {
|
|
508
|
+
return resolveSkillRequirement(ref, requirementAgentSlugs, catalogSkills, localSkillSlugs, errors, TEAM_ENTRYPOINT);
|
|
509
|
+
}
|
|
510
|
+
if (type === "local") {
|
|
511
|
+
const localPath = ref.endsWith("/SKILL.md") ? ref : `skills/${ref}/SKILL.md`;
|
|
512
|
+
const normalized = resolveTeamReference(teamDir, TEAM_ENTRYPOINT, localPath, errors);
|
|
513
|
+
const localSlug = path.posix.basename(path.posix.dirname(localPath));
|
|
514
|
+
const resolved = Boolean(normalized && localSkillSlugs.includes(localSlug));
|
|
515
|
+
if (!resolved)
|
|
516
|
+
errors.push(`${TEAM_ENTRYPOINT} required local skill "${ref}" does not resolve to skills/<slug>/SKILL.md.`);
|
|
517
|
+
return {
|
|
518
|
+
type: "local",
|
|
519
|
+
ref,
|
|
520
|
+
agentSlugs: requirementAgentSlugs,
|
|
521
|
+
resolved,
|
|
522
|
+
localPath,
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
return {
|
|
526
|
+
type,
|
|
527
|
+
ref,
|
|
528
|
+
agentSlugs: requirementAgentSlugs,
|
|
529
|
+
resolved: true,
|
|
530
|
+
sourceLocator: ref,
|
|
531
|
+
sourceRef: asString(declared.sourceRef) ?? asString(declared.commit) ?? undefined,
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
function readRequiredSkillEntries(frontmatter, errors) {
|
|
535
|
+
const value = frontmatter.requiredSkills;
|
|
536
|
+
if (value === undefined)
|
|
537
|
+
return [];
|
|
538
|
+
if (!Array.isArray(value)) {
|
|
539
|
+
errors.push(`${TEAM_ENTRYPOINT} frontmatter field requiredSkills must be an array.`);
|
|
540
|
+
return [];
|
|
541
|
+
}
|
|
542
|
+
return value;
|
|
543
|
+
}
|
|
544
|
+
function collectEnvInputs(graph) {
|
|
545
|
+
const out = [];
|
|
546
|
+
for (const agent of graph.agents) {
|
|
547
|
+
const agentSlug = asString(agent.frontmatter.slug) ?? slugFromEntityPath(agent.relativePath);
|
|
548
|
+
out.push(...readEnvInputs(agent.frontmatter, agentSlug, null));
|
|
549
|
+
}
|
|
550
|
+
for (const project of graph.projects) {
|
|
551
|
+
const projectSlug = asString(project.frontmatter.slug) ?? slugFromEntityPath(project.relativePath);
|
|
552
|
+
out.push(...readEnvInputs(project.frontmatter, null, projectSlug));
|
|
553
|
+
}
|
|
554
|
+
const seen = new Set();
|
|
555
|
+
return out.filter((input) => {
|
|
556
|
+
const key = `${input.agentSlug ?? ""}:${input.projectSlug ?? ""}:${input.key}`;
|
|
557
|
+
if (seen.has(key))
|
|
558
|
+
return false;
|
|
559
|
+
seen.add(key);
|
|
560
|
+
return true;
|
|
561
|
+
}).sort((a, b) => `${a.agentSlug ?? ""}:${a.projectSlug ?? ""}:${a.key}`.localeCompare(`${b.agentSlug ?? ""}:${b.projectSlug ?? ""}:${b.key}`));
|
|
562
|
+
}
|
|
563
|
+
function readEnvInputs(frontmatter, agentSlug, projectSlug) {
|
|
564
|
+
const inputs = isPlainRecord(frontmatter.inputs) ? frontmatter.inputs : null;
|
|
565
|
+
const env = inputs && isPlainRecord(inputs.env) ? inputs.env : null;
|
|
566
|
+
if (!env)
|
|
567
|
+
return [];
|
|
568
|
+
return Object.entries(env).flatMap(([key, value]) => {
|
|
569
|
+
if (!isPlainRecord(value))
|
|
570
|
+
return [];
|
|
571
|
+
return [{
|
|
572
|
+
key,
|
|
573
|
+
agentSlug,
|
|
574
|
+
projectSlug,
|
|
575
|
+
kind: value.kind === "plain" ? "plain" : "secret",
|
|
576
|
+
requirement: value.requirement === "required" ? "required" : "optional",
|
|
577
|
+
}];
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
function collectSourceRefs(teamFrontmatter, requiredSkills) {
|
|
581
|
+
const refs = [];
|
|
582
|
+
for (const include of readIncludeEntries(teamFrontmatter)) {
|
|
583
|
+
if (isExternalRef(include)) {
|
|
584
|
+
refs.push({ type: "include", ref: include, pinned: isPinnedExternalRef(include) });
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
for (const skill of requiredSkills) {
|
|
588
|
+
if (skill.type === "catalog" || skill.type === "local")
|
|
589
|
+
continue;
|
|
590
|
+
refs.push({
|
|
591
|
+
type: skill.type,
|
|
592
|
+
ref: skill.sourceLocator ?? skill.ref,
|
|
593
|
+
pinned: isPinnedExternalRef(skill.sourceRef ?? skill.sourceLocator ?? skill.ref),
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
refs.sort((a, b) => `${a.type}:${a.ref}`.localeCompare(`${b.type}:${b.ref}`));
|
|
597
|
+
return refs;
|
|
598
|
+
}
|
|
599
|
+
function readIncludeEntries(frontmatter) {
|
|
600
|
+
const includes = frontmatter.includes;
|
|
601
|
+
if (!Array.isArray(includes))
|
|
602
|
+
return [];
|
|
603
|
+
return includes.flatMap((entry) => {
|
|
604
|
+
if (typeof entry === "string")
|
|
605
|
+
return [entry.trim()].filter(Boolean);
|
|
606
|
+
if (isPlainRecord(entry)) {
|
|
607
|
+
const pathValue = asString(entry.path);
|
|
608
|
+
return pathValue ? [pathValue] : [];
|
|
609
|
+
}
|
|
610
|
+
return [];
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
function resolveTeamReference(teamDir, fromPath, ref, errors) {
|
|
614
|
+
if (isExternalRef(ref))
|
|
615
|
+
return null;
|
|
616
|
+
const normalizedRef = ref.replace(/\\/g, "/");
|
|
617
|
+
if (path.posix.isAbsolute(normalizedRef)) {
|
|
618
|
+
errors.push(`${fromPath} reference must be relative, not absolute: ${ref}.`);
|
|
619
|
+
return null;
|
|
620
|
+
}
|
|
621
|
+
const absolute = path.resolve(teamDir, path.dirname(fromPath), normalizedRef);
|
|
622
|
+
const relative = toPosixPath(path.relative(teamDir, absolute));
|
|
623
|
+
if (path.isAbsolute(relative) || relative.split("/").includes("..")) {
|
|
624
|
+
errors.push(`${fromPath} reference escapes the team package: ${ref}.`);
|
|
625
|
+
return null;
|
|
626
|
+
}
|
|
627
|
+
return relative;
|
|
628
|
+
}
|
|
629
|
+
function readStringArrayField(value, field, prefix, errors) {
|
|
630
|
+
const parsed = asStringArray(value);
|
|
631
|
+
if (!parsed) {
|
|
632
|
+
errors.push(`${prefix} frontmatter field ${field} must be an array of strings.`);
|
|
633
|
+
return [];
|
|
634
|
+
}
|
|
635
|
+
return parsed;
|
|
636
|
+
}
|
|
637
|
+
function readStringArrayLoose(value) {
|
|
638
|
+
return Array.isArray(value)
|
|
639
|
+
? value.filter((entry) => typeof entry === "string").map((entry) => entry.trim()).filter(Boolean)
|
|
640
|
+
: [];
|
|
641
|
+
}
|
|
642
|
+
function collectSlugs(files, label, errors) {
|
|
643
|
+
const slugs = files.map((file) => readSlug(file, label, errors)).filter(Boolean);
|
|
644
|
+
collectDuplicateValues(slugs, label, errors);
|
|
645
|
+
return slugs;
|
|
646
|
+
}
|
|
647
|
+
function readSlug(file, label, errors) {
|
|
648
|
+
const slug = asString(file.frontmatter.slug) ?? slugFromEntityPath(file.relativePath);
|
|
649
|
+
validateSlug(`${label} slug`, slug, file.relativePath, errors);
|
|
650
|
+
return slug;
|
|
651
|
+
}
|
|
652
|
+
function slugFromEntityPath(relativePath) {
|
|
653
|
+
return path.posix.basename(path.posix.dirname(relativePath));
|
|
654
|
+
}
|
|
655
|
+
function classifyCatalogFile(relativePath) {
|
|
656
|
+
if (relativePath === TEAM_ENTRYPOINT)
|
|
657
|
+
return "team";
|
|
658
|
+
if (relativePath.endsWith("/AGENTS.md") || relativePath === "AGENTS.md")
|
|
659
|
+
return "agent";
|
|
660
|
+
if (relativePath.endsWith("/PROJECT.md") || relativePath === "PROJECT.md")
|
|
661
|
+
return "project";
|
|
662
|
+
if (relativePath.endsWith("/TASK.md") || relativePath === "TASK.md")
|
|
663
|
+
return "task";
|
|
664
|
+
if (relativePath.endsWith("/SKILL.md") || relativePath === "SKILL.md")
|
|
665
|
+
return "skill";
|
|
666
|
+
if (relativePath === ".paperclip.yaml")
|
|
667
|
+
return "extension";
|
|
668
|
+
if (relativePath === "README.md")
|
|
669
|
+
return "readme";
|
|
670
|
+
if (relativePath.startsWith("references/"))
|
|
671
|
+
return "reference";
|
|
672
|
+
if (relativePath.startsWith("scripts/"))
|
|
673
|
+
return "script";
|
|
674
|
+
if (relativePath.startsWith("assets/"))
|
|
675
|
+
return "asset";
|
|
676
|
+
if (relativePath.endsWith(".md") || relativePath.endsWith(".mdx"))
|
|
677
|
+
return "markdown";
|
|
678
|
+
return "other";
|
|
679
|
+
}
|
|
680
|
+
function deriveTrustLevel(files, sourceRefs) {
|
|
681
|
+
if (sourceRefs.length > 0)
|
|
682
|
+
return "external_sources";
|
|
683
|
+
if (files.some((file) => file.kind === "script"))
|
|
684
|
+
return "scripts_executables";
|
|
685
|
+
if (files.some((file) => file.kind === "asset" || file.kind === "other" || file.kind === "extension"))
|
|
686
|
+
return "assets";
|
|
687
|
+
return "markdown_only";
|
|
688
|
+
}
|
|
689
|
+
function buildContentHash(files) {
|
|
690
|
+
const hashInput = files.map((file) => ({
|
|
691
|
+
path: file.path,
|
|
692
|
+
sha256: file.sha256,
|
|
693
|
+
}));
|
|
694
|
+
return `sha256:${sha256(Buffer.from(JSON.stringify(hashInput)))}`;
|
|
695
|
+
}
|
|
696
|
+
function collectUniquenessErrors(teams, errors) {
|
|
697
|
+
collectDuplicateErrors(teams, "id", errors);
|
|
698
|
+
collectDuplicateErrors(teams, "key", errors);
|
|
699
|
+
collectDuplicateErrors(teams, "slug", errors);
|
|
700
|
+
}
|
|
701
|
+
function collectCandidateUniquenessErrors(candidates, errors) {
|
|
702
|
+
const projected = candidates.map((candidate) => ({
|
|
703
|
+
id: `paperclipai:${candidate.kind}:${candidate.category}:${candidate.slug}`,
|
|
704
|
+
key: `paperclipai/${candidate.kind}/${candidate.category}/${candidate.slug}`,
|
|
705
|
+
slug: candidate.slug,
|
|
706
|
+
path: toPosixPath(path.join("catalog", candidate.kind, candidate.category, candidate.slug)),
|
|
707
|
+
}));
|
|
708
|
+
collectUniquenessErrors(projected, errors);
|
|
709
|
+
}
|
|
710
|
+
function collectDuplicateErrors(teams, field, errors) {
|
|
711
|
+
const seen = new Map();
|
|
712
|
+
for (const team of teams) {
|
|
713
|
+
const value = team[field];
|
|
714
|
+
const first = seen.get(value);
|
|
715
|
+
if (first) {
|
|
716
|
+
errors.push(`Duplicate catalog ${field} "${value}" in ${first} and ${team.path}.`);
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
seen.set(value, team.path);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
function collectDuplicateValues(values, label, errors) {
|
|
723
|
+
const seen = new Set();
|
|
724
|
+
for (const value of values) {
|
|
725
|
+
if (seen.has(value)) {
|
|
726
|
+
errors.push(`Duplicate ${label} "${value}" in team package.`);
|
|
727
|
+
}
|
|
728
|
+
seen.add(value);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
function resolveCatalogSkill(ref, catalogSkills) {
|
|
732
|
+
const exact = catalogSkills.find((skill) => skill.id === ref || skill.key === ref);
|
|
733
|
+
if (exact)
|
|
734
|
+
return exact;
|
|
735
|
+
const slugMatches = catalogSkills.filter((skill) => skill.slug === ref);
|
|
736
|
+
return slugMatches.length === 1 ? slugMatches[0] : null;
|
|
737
|
+
}
|
|
738
|
+
function isSkillRequirementType(value) {
|
|
739
|
+
return value === "catalog"
|
|
740
|
+
|| value === "local"
|
|
741
|
+
|| value === "skills_sh"
|
|
742
|
+
|| value === "github"
|
|
743
|
+
|| value === "url"
|
|
744
|
+
|| value === "local_path"
|
|
745
|
+
|| value === "agent_package";
|
|
746
|
+
}
|
|
747
|
+
function validateSlug(label, value, prefix, errors) {
|
|
748
|
+
if (!SLUG_PATTERN.test(value)) {
|
|
749
|
+
errors.push(`${prefix} has invalid ${label} "${value}"; use lowercase URL slugs.`);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
async function sortedDirEntries(dir) {
|
|
753
|
+
return (await fs.readdir(dir, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
|
|
754
|
+
}
|
|
755
|
+
function sameManifestExceptGeneratedAt(a, b) {
|
|
756
|
+
return JSON.stringify({ ...a, generatedAt: "" }) === JSON.stringify({ ...b, generatedAt: "" });
|
|
757
|
+
}
|
|
758
|
+
function sha256(contents) {
|
|
759
|
+
return createHash("sha256").update(contents).digest("hex");
|
|
760
|
+
}
|
|
761
|
+
function relativePackagePath(packageDir, absolutePath) {
|
|
762
|
+
return toPosixPath(path.relative(packageDir, absolutePath));
|
|
763
|
+
}
|
|
764
|
+
function toPosixPath(input) {
|
|
765
|
+
return input.split(path.sep).join("/");
|
|
766
|
+
}
|
|
767
|
+
function isPathInside(parent, child) {
|
|
768
|
+
const relativePath = path.relative(parent, child);
|
|
769
|
+
return relativePath === "" || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath));
|
|
770
|
+
}
|
|
771
|
+
function isExternalRef(ref) {
|
|
772
|
+
return /^https?:\/\//.test(ref) || EXTERNAL_SOURCE_TYPES.has(ref.split(":")[0] ?? "") || LOCAL_PATH_SOURCE_TYPES.has(ref.split(":")[0] ?? "");
|
|
773
|
+
}
|
|
774
|
+
function isPinnedExternalRef(ref) {
|
|
775
|
+
return /[a-f0-9]{40}/i.test(ref) || /^sha256:[a-f0-9]{64}$/i.test(ref);
|
|
776
|
+
}
|
|
777
|
+
function errorMessage(error) {
|
|
778
|
+
return error instanceof Error ? error.message : String(error);
|
|
779
|
+
}
|
|
780
|
+
//# sourceMappingURL=catalog-builder.js.map
|