feat(FN-1188): add Agent Companies parser and core exports
- Add Agent Companies manifest types for company, team, agent, project, task, and skill files - Implement YAML frontmatter parsing, directory/tar.gz package parsing, and contextual parse errors - Convert agent manifests to AgentCreateInput with role inference and skip-existing import support - Export Agent Companies parser/type APIs from @fusion/core and add the yaml dependency - Add comprehensive parser and type tests covering happy paths, archive handling, and validation failures
This commit is contained in:
525
packages/core/src/agent-companies-parser.test.ts
Normal file
525
packages/core/src/agent-companies-parser.test.ts
Normal file
@@ -0,0 +1,525 @@
|
||||
import { execSync } from "node:child_process";
|
||||
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
AgentCompaniesParseError,
|
||||
agentManifestToAgentCreateInput,
|
||||
convertAgentCompanies,
|
||||
parseAgentManifest,
|
||||
parseCompanyArchive,
|
||||
parseCompanyDirectory,
|
||||
parseCompanyManifest,
|
||||
parseProjectManifest,
|
||||
parseSkillManifest,
|
||||
parseTaskManifest,
|
||||
parseTeamManifest,
|
||||
parseYamlFrontmatter,
|
||||
} from "./agent-companies-parser.js";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
function createTempDir(): string {
|
||||
const dir = mkdtempSync(join(tmpdir(), "agent-companies-test-"));
|
||||
tempDirs.push(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
function writeTextFile(path: string, content: string): void {
|
||||
mkdirSync(dirname(path), { recursive: true });
|
||||
writeFileSync(path, content, "utf-8");
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
while (tempDirs.length > 0) {
|
||||
const dir = tempDirs.pop();
|
||||
if (dir) {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
describe("agent-companies-parser", () => {
|
||||
describe("parseYamlFrontmatter", () => {
|
||||
it("parses YAML frontmatter with markdown body", () => {
|
||||
const content = `---
|
||||
name: CEO
|
||||
skills:
|
||||
- review
|
||||
---
|
||||
You are the CEO agent.`;
|
||||
|
||||
const { frontmatter, body } = parseYamlFrontmatter(content);
|
||||
|
||||
expect(frontmatter.name).toBe("CEO");
|
||||
expect(frontmatter.skills).toEqual(["review"]);
|
||||
expect(body).toBe("You are the CEO agent.");
|
||||
});
|
||||
|
||||
it("parses frontmatter with no body", () => {
|
||||
const content = `---
|
||||
name: Lean Dev Shop
|
||||
---`;
|
||||
const { frontmatter, body } = parseYamlFrontmatter(content);
|
||||
|
||||
expect(frontmatter.name).toBe("Lean Dev Shop");
|
||||
expect(body).toBe("");
|
||||
});
|
||||
|
||||
it("throws on missing frontmatter delimiters", () => {
|
||||
expect(() => parseYamlFrontmatter("name: no delimiters")).toThrow(
|
||||
AgentCompaniesParseError,
|
||||
);
|
||||
expect(() => parseYamlFrontmatter("name: no delimiters")).toThrow(
|
||||
"Missing YAML frontmatter delimiters",
|
||||
);
|
||||
});
|
||||
|
||||
it("throws on malformed YAML", () => {
|
||||
const malformed = `---
|
||||
name: CEO
|
||||
skills: [review
|
||||
---
|
||||
body`;
|
||||
expect(() => parseYamlFrontmatter(malformed)).toThrow(AgentCompaniesParseError);
|
||||
expect(() => parseYamlFrontmatter(malformed)).toThrow("Malformed YAML frontmatter");
|
||||
});
|
||||
|
||||
it("throws when YAML parses to null", () => {
|
||||
const content = `---
|
||||
null
|
||||
---`;
|
||||
expect(() => parseYamlFrontmatter(content)).toThrow("must parse to an object");
|
||||
});
|
||||
|
||||
it("throws when YAML parses to an array", () => {
|
||||
const content = `---
|
||||
- one
|
||||
- two
|
||||
---`;
|
||||
expect(() => parseYamlFrontmatter(content)).toThrow("must parse to an object");
|
||||
});
|
||||
|
||||
it("handles multiline frontmatter fields", () => {
|
||||
const content = `---
|
||||
name: CEO
|
||||
description: |
|
||||
Leads strategy
|
||||
Reviews direction
|
||||
---
|
||||
Body`;
|
||||
const { frontmatter } = parseYamlFrontmatter(content);
|
||||
|
||||
expect(String(frontmatter.description)).toContain("Leads strategy");
|
||||
expect(String(frontmatter.description)).toContain("Reviews direction");
|
||||
});
|
||||
|
||||
it("handles array fields in frontmatter", () => {
|
||||
const content = `---
|
||||
name: Reviewer
|
||||
skills:
|
||||
- review
|
||||
- security-review
|
||||
---
|
||||
Body`;
|
||||
const { frontmatter } = parseYamlFrontmatter(content);
|
||||
|
||||
expect(frontmatter.skills).toEqual(["review", "security-review"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("individual manifest parsing", () => {
|
||||
it("parses AGENTS.md with full frontmatter and body", () => {
|
||||
const content = `---
|
||||
name: CEO
|
||||
title: Chief Executive Officer
|
||||
reportsTo: null
|
||||
skills:
|
||||
- plan-ceo-review
|
||||
- review
|
||||
---
|
||||
You are the CEO agent. Your job is to lead.`;
|
||||
|
||||
const manifest = parseAgentManifest(content);
|
||||
|
||||
expect(manifest.name).toBe("CEO");
|
||||
expect(manifest.title).toBe("Chief Executive Officer");
|
||||
expect(manifest.reportsTo).toBeNull();
|
||||
expect(manifest.skills).toEqual(["plan-ceo-review", "review"]);
|
||||
expect(manifest.instructionBody).toContain("You are the CEO agent");
|
||||
});
|
||||
|
||||
it("parses AGENTS.md with minimal fields", () => {
|
||||
const manifest = parseAgentManifest(`---
|
||||
name: Minimal Agent
|
||||
---`);
|
||||
expect(manifest.name).toBe("Minimal Agent");
|
||||
expect(manifest.instructionBody).toBe("");
|
||||
});
|
||||
|
||||
it("parses COMPANY.md with schema and slug", () => {
|
||||
const manifest = parseCompanyManifest(`---
|
||||
name: Lean Dev Shop
|
||||
description: Small engineering-focused AI company
|
||||
slug: lean-dev-shop
|
||||
schema: agentcompanies/v1
|
||||
---`);
|
||||
|
||||
expect(manifest.schema).toBe("agentcompanies/v1");
|
||||
expect(manifest.slug).toBe("lean-dev-shop");
|
||||
});
|
||||
|
||||
it("parses TEAM.md with manager and includes", () => {
|
||||
const manifest = parseTeamManifest(`---
|
||||
name: Engineering
|
||||
manager: ../cto/AGENTS.md
|
||||
includes:
|
||||
- ../platform-lead/AGENTS.md
|
||||
- ../../skills/review/SKILL.md
|
||||
---`);
|
||||
|
||||
expect(manifest.manager).toBe("../cto/AGENTS.md");
|
||||
expect(manifest.includes).toEqual([
|
||||
"../platform-lead/AGENTS.md",
|
||||
"../../skills/review/SKILL.md",
|
||||
]);
|
||||
});
|
||||
|
||||
it("parses PROJECT.md", () => {
|
||||
const manifest = parseProjectManifest(`---
|
||||
name: Q2 Launch
|
||||
description: Launch execution project
|
||||
slug: q2-launch
|
||||
---`);
|
||||
|
||||
expect(manifest.name).toBe("Q2 Launch");
|
||||
expect(manifest.slug).toBe("q2-launch");
|
||||
});
|
||||
|
||||
it("parses TASK.md with assignee, project, and schedule", () => {
|
||||
const manifest = parseTaskManifest(`---
|
||||
name: Monday Review
|
||||
slug: monday-review
|
||||
description: Weekly code review
|
||||
assignee: ./agents/ceo/AGENTS.md
|
||||
project: ./projects/q2-launch/PROJECT.md
|
||||
schedule:
|
||||
timezone: America/New_York
|
||||
startsAt: "2025-01-06T09:00:00"
|
||||
---`);
|
||||
|
||||
expect(manifest.assignee).toBe("./agents/ceo/AGENTS.md");
|
||||
expect(manifest.project).toBe("./projects/q2-launch/PROJECT.md");
|
||||
expect(manifest.schedule).toEqual({
|
||||
timezone: "America/New_York",
|
||||
startsAt: "2025-01-06T09:00:00",
|
||||
});
|
||||
});
|
||||
|
||||
it("parses SKILL.md with provides and requirements", () => {
|
||||
const manifest = parseSkillManifest(`---
|
||||
name: Code Review
|
||||
provides:
|
||||
- code-review
|
||||
requirements:
|
||||
- typescript
|
||||
---`);
|
||||
|
||||
expect(manifest.provides).toEqual(["code-review"]);
|
||||
expect(manifest.requirements).toEqual(["typescript"]);
|
||||
});
|
||||
|
||||
it("throws on missing required name field", () => {
|
||||
const invalid = `---
|
||||
description: Missing required field
|
||||
---`;
|
||||
expect(() => parseTeamManifest(invalid)).toThrow(AgentCompaniesParseError);
|
||||
expect(() => parseTeamManifest(invalid)).toThrow("team manifest is missing required field: name");
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseCompanyDirectory", () => {
|
||||
it("parses a full directory structure", () => {
|
||||
const root = createTempDir();
|
||||
|
||||
writeTextFile(
|
||||
join(root, "COMPANY.md"),
|
||||
`---
|
||||
name: Lean Dev Shop
|
||||
description: Small engineering-focused AI company
|
||||
schema: agentcompanies/v1
|
||||
---`,
|
||||
);
|
||||
writeTextFile(
|
||||
join(root, "agents", "ceo", "AGENTS.md"),
|
||||
`---
|
||||
name: CEO
|
||||
title: Chief Executive Officer
|
||||
skills:
|
||||
- review
|
||||
---
|
||||
You are the CEO agent.`,
|
||||
);
|
||||
writeTextFile(
|
||||
join(root, "teams", "engineering", "TEAM.md"),
|
||||
`---
|
||||
name: Engineering
|
||||
manager: ../cto/AGENTS.md
|
||||
---`,
|
||||
);
|
||||
writeTextFile(
|
||||
join(root, "tasks", "review", "TASK.md"),
|
||||
`---
|
||||
name: Monday Review
|
||||
assignee: ./agents/ceo/AGENTS.md
|
||||
---`,
|
||||
);
|
||||
writeTextFile(
|
||||
join(root, "skills", "code-review", "SKILL.md"),
|
||||
`---
|
||||
name: Code Review
|
||||
provides:
|
||||
- code-review
|
||||
---`,
|
||||
);
|
||||
|
||||
const pkg = parseCompanyDirectory(root);
|
||||
|
||||
expect(pkg.company?.name).toBe("Lean Dev Shop");
|
||||
expect(pkg.agents).toHaveLength(1);
|
||||
expect(pkg.teams).toHaveLength(1);
|
||||
expect(pkg.tasks).toHaveLength(1);
|
||||
expect(pkg.skills).toHaveLength(1);
|
||||
expect(pkg.projects).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("handles directory with only agents and no COMPANY.md", () => {
|
||||
const root = createTempDir();
|
||||
writeTextFile(
|
||||
join(root, "agents", "ceo", "AGENTS.md"),
|
||||
`---
|
||||
name: CEO
|
||||
---`,
|
||||
);
|
||||
|
||||
const pkg = parseCompanyDirectory(root);
|
||||
expect(pkg.company).toBeUndefined();
|
||||
expect(pkg.agents).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("handles empty directory", () => {
|
||||
const root = createTempDir();
|
||||
const pkg = parseCompanyDirectory(root);
|
||||
|
||||
expect(pkg).toEqual({
|
||||
company: undefined,
|
||||
agents: [],
|
||||
teams: [],
|
||||
projects: [],
|
||||
tasks: [],
|
||||
skills: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("throws on non-existent directory", () => {
|
||||
const root = createTempDir();
|
||||
const missingPath = join(root, "missing");
|
||||
expect(() => parseCompanyDirectory(missingPath)).toThrow("does not exist");
|
||||
});
|
||||
|
||||
it("throws when path is not a directory", () => {
|
||||
const root = createTempDir();
|
||||
const filePath = join(root, "not-a-directory.md");
|
||||
writeTextFile(filePath, "hello");
|
||||
|
||||
expect(() => parseCompanyDirectory(filePath)).toThrow("is not a directory");
|
||||
});
|
||||
|
||||
it("ignores non-directory entries in section folders", () => {
|
||||
const root = createTempDir();
|
||||
writeTextFile(join(root, "agents", "README.md"), "not a manifest folder");
|
||||
writeTextFile(join(root, "agents", "ceo", "AGENTS.md"), `---
|
||||
name: CEO
|
||||
---`);
|
||||
|
||||
const pkg = parseCompanyDirectory(root);
|
||||
expect(pkg.agents).toHaveLength(1);
|
||||
expect(pkg.agents[0].name).toBe("CEO");
|
||||
});
|
||||
|
||||
it("includes file path context in parse errors", () => {
|
||||
const root = createTempDir();
|
||||
writeTextFile(join(root, "agents", "ceo", "AGENTS.md"), `---
|
||||
description: missing name
|
||||
---`);
|
||||
|
||||
expect(() => parseCompanyDirectory(root)).toThrow("AGENTS.md");
|
||||
expect(() => parseCompanyDirectory(root)).toThrow("missing required field: name");
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseCompanyArchive", () => {
|
||||
it("throws a clear error for zip archives", async () => {
|
||||
const zipPath = join(createTempDir(), "company.zip");
|
||||
await expect(parseCompanyArchive(zipPath)).rejects.toThrow(
|
||||
"Zip archives are not yet supported",
|
||||
);
|
||||
});
|
||||
|
||||
it("parses a tar.gz archive", async () => {
|
||||
const temp = createTempDir();
|
||||
const packageDirName = "company-package";
|
||||
const packageDir = join(temp, packageDirName);
|
||||
|
||||
writeTextFile(
|
||||
join(packageDir, "COMPANY.md"),
|
||||
`---
|
||||
name: Lean Dev Shop
|
||||
schema: agentcompanies/v1
|
||||
---`,
|
||||
);
|
||||
writeTextFile(
|
||||
join(packageDir, "agents", "ceo", "AGENTS.md"),
|
||||
`---
|
||||
name: CEO
|
||||
skills:
|
||||
- review
|
||||
---
|
||||
You are the CEO agent.`,
|
||||
);
|
||||
|
||||
const archivePath = join(temp, "company.tgz");
|
||||
execSync(
|
||||
`tar czf ${JSON.stringify(archivePath)} -C ${JSON.stringify(temp)} ${JSON.stringify(packageDirName)}`,
|
||||
);
|
||||
|
||||
const pkg = await parseCompanyArchive(archivePath);
|
||||
expect(pkg.company?.name).toBe("Lean Dev Shop");
|
||||
expect(pkg.agents).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("handles archive with a single file entry", async () => {
|
||||
const temp = createTempDir();
|
||||
writeTextFile(join(temp, "README.md"), "hello");
|
||||
|
||||
const archivePath = join(temp, "single-file.tgz");
|
||||
execSync(
|
||||
`tar czf ${JSON.stringify(archivePath)} -C ${JSON.stringify(temp)} README.md`,
|
||||
);
|
||||
|
||||
const pkg = await parseCompanyArchive(archivePath);
|
||||
expect(pkg.company).toBeUndefined();
|
||||
expect(pkg.agents).toEqual([]);
|
||||
expect(pkg.teams).toEqual([]);
|
||||
expect(pkg.projects).toEqual([]);
|
||||
expect(pkg.tasks).toEqual([]);
|
||||
expect(pkg.skills).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("conversion", () => {
|
||||
it("converts AgentManifest to AgentCreateInput", () => {
|
||||
const input = agentManifestToAgentCreateInput({
|
||||
name: "CEO",
|
||||
title: "Chief Executive Officer",
|
||||
instructionBody: "Lead the company",
|
||||
skills: ["review"],
|
||||
reportsTo: "../founder/AGENTS.md",
|
||||
});
|
||||
|
||||
expect(input.name).toBe("CEO");
|
||||
expect(input.title).toBe("Chief Executive Officer");
|
||||
expect(input.instructionsText).toBe("Lead the company");
|
||||
expect(input.metadata).toEqual({ skills: ["review"] });
|
||||
expect(input.reportsTo).toBe("../founder/AGENTS.md");
|
||||
expect(input.role).toBe("reviewer");
|
||||
});
|
||||
|
||||
it("defaults to custom role when no skills are provided", () => {
|
||||
const input = agentManifestToAgentCreateInput({
|
||||
name: "Unknown",
|
||||
});
|
||||
|
||||
expect(input.role).toBe("custom");
|
||||
});
|
||||
|
||||
it("infers role from reportsTo when skills are absent", () => {
|
||||
const input = agentManifestToAgentCreateInput({
|
||||
name: "Planner",
|
||||
reportsTo: "../triage-lead/AGENTS.md",
|
||||
});
|
||||
|
||||
expect(input.role).toBe("triage");
|
||||
expect(input.reportsTo).toBe("../triage-lead/AGENTS.md");
|
||||
});
|
||||
|
||||
it("prefers skills over reportsTo for role inference", () => {
|
||||
const input = agentManifestToAgentCreateInput({
|
||||
name: "Mixed",
|
||||
skills: ["review"],
|
||||
reportsTo: "../executor-lead/AGENTS.md",
|
||||
});
|
||||
|
||||
expect(input.role).toBe("reviewer");
|
||||
});
|
||||
|
||||
it("infers role from skills containing role hints", () => {
|
||||
const input = agentManifestToAgentCreateInput({
|
||||
name: "Triager",
|
||||
skills: ["plan-triage-review"],
|
||||
});
|
||||
|
||||
expect(input.role).toBe("triage");
|
||||
});
|
||||
|
||||
it("maps instructionBody to instructionsText", () => {
|
||||
const input = agentManifestToAgentCreateInput({
|
||||
name: "Writer",
|
||||
instructionBody: "Write docs",
|
||||
});
|
||||
|
||||
expect(input.instructionsText).toBe("Write docs");
|
||||
});
|
||||
|
||||
it("converts package agents with skipExisting logic", () => {
|
||||
const { inputs, result } = convertAgentCompanies(
|
||||
{
|
||||
company: { name: "Lean Dev Shop" },
|
||||
agents: [
|
||||
{ name: "Existing", skills: ["review"] },
|
||||
{ name: "New Agent", skills: ["executor"] },
|
||||
],
|
||||
teams: [],
|
||||
projects: [],
|
||||
tasks: [],
|
||||
skills: [],
|
||||
},
|
||||
{ skipExisting: ["Existing"] },
|
||||
);
|
||||
|
||||
expect(inputs).toHaveLength(1);
|
||||
expect(inputs[0].name).toBe("New Agent");
|
||||
expect(inputs[0].role).toBe("executor");
|
||||
expect(result.created).toEqual(["New Agent"]);
|
||||
expect(result.skipped).toEqual(["Existing"]);
|
||||
expect(result.errors).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("error handling", () => {
|
||||
it("AgentCompaniesParseError has correct name", () => {
|
||||
const err = new AgentCompaniesParseError("boom");
|
||||
expect(err.name).toBe("AgentCompaniesParseError");
|
||||
expect(err.message).toBe("boom");
|
||||
});
|
||||
|
||||
it("error messages include parsing context", () => {
|
||||
expect(() => parseCompanyManifest("---\ndescription: missing name\n---")).toThrow(
|
||||
"company manifest is missing required field: name",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
386
packages/core/src/agent-companies-parser.ts
Normal file
386
packages/core/src/agent-companies-parser.ts
Normal file
@@ -0,0 +1,386 @@
|
||||
/**
|
||||
* Parser for Agent Companies markdown manifests.
|
||||
*
|
||||
* Supports YAML frontmatter extraction, per-manifest parsing,
|
||||
* directory/package parsing, archive parsing, and conversion into
|
||||
* Fusion `AgentCreateInput` payloads.
|
||||
*
|
||||
* @module agent-companies-parser
|
||||
*/
|
||||
|
||||
import { execSync } from "node:child_process";
|
||||
import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync } from "node:fs";
|
||||
import { join, resolve } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { parse as parseYaml } from "yaml";
|
||||
|
||||
import type {
|
||||
AgentCompaniesImportResult,
|
||||
AgentCompaniesPackage,
|
||||
AgentManifest,
|
||||
CompanyManifest,
|
||||
ProjectManifest,
|
||||
SkillManifest,
|
||||
TaskManifest,
|
||||
TeamManifest,
|
||||
} from "./agent-companies-types.js";
|
||||
import { mapRoleToCapability } from "./companies-sh-parser.js";
|
||||
import type { AgentCapability, AgentCreateInput } from "./types.js";
|
||||
|
||||
export { mapRoleToCapability } from "./companies-sh-parser.js";
|
||||
|
||||
// ── Parsing Errors ───────────────────────────────────────────────────────
|
||||
|
||||
export class AgentCompaniesParseError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "AgentCompaniesParseError";
|
||||
}
|
||||
}
|
||||
|
||||
// ── Frontmatter Parsing ──────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Extract YAML frontmatter and markdown body from a manifest file.
|
||||
*
|
||||
* @throws {AgentCompaniesParseError} On missing or malformed frontmatter.
|
||||
*/
|
||||
export function parseYamlFrontmatter(content: string): {
|
||||
frontmatter: Record<string, unknown>;
|
||||
body: string;
|
||||
} {
|
||||
if (typeof content !== "string" || content.length === 0) {
|
||||
throw new AgentCompaniesParseError("Manifest content is empty or not a string");
|
||||
}
|
||||
|
||||
const match = content.match(/^---\s*\r?\n([\s\S]*?)\r?\n---(?:\r?\n([\s\S]*))?$/);
|
||||
if (!match) {
|
||||
throw new AgentCompaniesParseError("Missing YAML frontmatter delimiters (---)");
|
||||
}
|
||||
|
||||
const yamlContent = match[1];
|
||||
const body = match[2] ?? "";
|
||||
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = parseYaml(yamlContent);
|
||||
} catch (err) {
|
||||
throw new AgentCompaniesParseError(
|
||||
`Malformed YAML frontmatter: ${(err as Error).message}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||
throw new AgentCompaniesParseError("YAML frontmatter must parse to an object");
|
||||
}
|
||||
|
||||
return {
|
||||
frontmatter: parsed as Record<string, unknown>,
|
||||
body,
|
||||
};
|
||||
}
|
||||
|
||||
function validateRequiredFields(
|
||||
frontmatter: Record<string, unknown>,
|
||||
kind: string,
|
||||
requiredFields: string[],
|
||||
): void {
|
||||
for (const field of requiredFields) {
|
||||
const value = frontmatter[field];
|
||||
if (typeof value !== "string" || value.trim() === "") {
|
||||
throw new AgentCompaniesParseError(
|
||||
`${kind} manifest is missing required field: ${field}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and validate a manifest frontmatter shape.
|
||||
*/
|
||||
function parseManifest<T>(content: string, kind: string, requiredFields: string[]): T {
|
||||
const { frontmatter } = parseYamlFrontmatter(content);
|
||||
validateRequiredFields(frontmatter, kind, requiredFields);
|
||||
return frontmatter as T;
|
||||
}
|
||||
|
||||
// ── Individual Manifest Parsers ─────────────────────────────────────────
|
||||
|
||||
export function parseCompanyManifest(content: string): CompanyManifest {
|
||||
return parseManifest<CompanyManifest>(content, "company", ["name"]);
|
||||
}
|
||||
|
||||
export function parseTeamManifest(content: string): TeamManifest {
|
||||
return parseManifest<TeamManifest>(content, "team", ["name"]);
|
||||
}
|
||||
|
||||
export function parseAgentManifest(content: string): AgentManifest {
|
||||
const manifest = parseManifest<AgentManifest>(content, "agent", ["name"]);
|
||||
const { body } = parseYamlFrontmatter(content);
|
||||
return {
|
||||
...manifest,
|
||||
instructionBody: body,
|
||||
};
|
||||
}
|
||||
|
||||
export function parseProjectManifest(content: string): ProjectManifest {
|
||||
return parseManifest<ProjectManifest>(content, "project", ["name"]);
|
||||
}
|
||||
|
||||
export function parseTaskManifest(content: string): TaskManifest {
|
||||
return parseManifest<TaskManifest>(content, "task", ["name"]);
|
||||
}
|
||||
|
||||
export function parseSkillManifest(content: string): SkillManifest {
|
||||
return parseManifest<SkillManifest>(content, "skill", ["name"]);
|
||||
}
|
||||
|
||||
// ── Directory + Archive Parsing ─────────────────────────────────────────
|
||||
|
||||
function parseManifestFile<T>(
|
||||
filePath: string,
|
||||
parser: (content: string) => T,
|
||||
): T {
|
||||
try {
|
||||
const content = readFileSync(filePath, "utf-8");
|
||||
return parser(content);
|
||||
} catch (err) {
|
||||
if (err instanceof AgentCompaniesParseError) {
|
||||
throw new AgentCompaniesParseError(`${filePath}: ${err.message}`);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
function parseManifestSubdirectories<T>(
|
||||
rootDir: string,
|
||||
sectionDir: string,
|
||||
filename: string,
|
||||
parser: (content: string) => T,
|
||||
): T[] {
|
||||
const sectionPath = join(rootDir, sectionDir);
|
||||
if (!existsSync(sectionPath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const entries = readdirSync(sectionPath, { withFileTypes: true });
|
||||
const parsed: T[] = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const manifestPath = join(sectionPath, entry.name, filename);
|
||||
if (!existsSync(manifestPath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
parsed.push(parseManifestFile(manifestPath, parser));
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
export function parseCompanyDirectory(dirPath: string): AgentCompaniesPackage {
|
||||
const resolvedDir = resolve(dirPath);
|
||||
|
||||
if (!existsSync(resolvedDir)) {
|
||||
throw new AgentCompaniesParseError(`Company directory does not exist: ${resolvedDir}`);
|
||||
}
|
||||
if (!statSync(resolvedDir).isDirectory()) {
|
||||
throw new AgentCompaniesParseError(`Company path is not a directory: ${resolvedDir}`);
|
||||
}
|
||||
|
||||
const companyPath = join(resolvedDir, "COMPANY.md");
|
||||
|
||||
return {
|
||||
company: existsSync(companyPath)
|
||||
? parseManifestFile(companyPath, parseCompanyManifest)
|
||||
: undefined,
|
||||
agents: parseManifestSubdirectories(resolvedDir, "agents", "AGENTS.md", parseAgentManifest),
|
||||
teams: parseManifestSubdirectories(resolvedDir, "teams", "TEAM.md", parseTeamManifest),
|
||||
projects: parseManifestSubdirectories(resolvedDir, "projects", "PROJECT.md", parseProjectManifest),
|
||||
tasks: parseManifestSubdirectories(resolvedDir, "tasks", "TASK.md", parseTaskManifest),
|
||||
skills: parseManifestSubdirectories(resolvedDir, "skills", "SKILL.md", parseSkillManifest),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveArchiveRoot(tempDir: string): string {
|
||||
if (existsSync(join(tempDir, "COMPANY.md"))) {
|
||||
return tempDir;
|
||||
}
|
||||
|
||||
const entries = readdirSync(tempDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
const childPath = join(tempDir, entry.name);
|
||||
if (existsSync(join(childPath, "COMPANY.md"))) {
|
||||
return childPath;
|
||||
}
|
||||
}
|
||||
|
||||
if (entries.length === 1 && entries[0].isDirectory()) {
|
||||
return join(tempDir, entries[0].name);
|
||||
}
|
||||
|
||||
return tempDir;
|
||||
}
|
||||
|
||||
export async function parseCompanyArchive(archivePath: string): Promise<AgentCompaniesPackage> {
|
||||
const resolvedArchivePath = resolve(archivePath);
|
||||
|
||||
if (resolvedArchivePath.endsWith(".zip")) {
|
||||
throw new AgentCompaniesParseError(
|
||||
"Zip archives are not yet supported for Agent Companies imports. Please use .tar.gz or .tgz.",
|
||||
);
|
||||
}
|
||||
|
||||
if (!resolvedArchivePath.endsWith(".tar.gz") && !resolvedArchivePath.endsWith(".tgz")) {
|
||||
throw new AgentCompaniesParseError(
|
||||
"Unsupported archive format. Expected .tar.gz or .tgz.",
|
||||
);
|
||||
}
|
||||
|
||||
const tempDir = mkdtempSync(join(tmpdir(), "agent-companies-"));
|
||||
|
||||
try {
|
||||
execSync(
|
||||
`tar xzf ${JSON.stringify(resolvedArchivePath)} -C ${JSON.stringify(tempDir)}`,
|
||||
{ stdio: "pipe" },
|
||||
);
|
||||
|
||||
const extractionRoot = resolveArchiveRoot(tempDir);
|
||||
return parseCompanyDirectory(extractionRoot);
|
||||
} catch (err) {
|
||||
if (err instanceof AgentCompaniesParseError) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
throw new AgentCompaniesParseError(
|
||||
`Failed to parse Agent Companies archive: ${(err as Error).message}`,
|
||||
);
|
||||
} finally {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
// ── Conversion to Fusion Agent Inputs ───────────────────────────────────
|
||||
|
||||
const ROLE_HINT_ALIASES: Record<string, AgentCapability> = {
|
||||
triage: "triage",
|
||||
planner: "triage",
|
||||
planning: "triage",
|
||||
executor: "executor",
|
||||
execute: "executor",
|
||||
reviewer: "reviewer",
|
||||
review: "reviewer",
|
||||
merger: "merger",
|
||||
merge: "merger",
|
||||
scheduler: "scheduler",
|
||||
schedule: "scheduler",
|
||||
engineer: "engineer",
|
||||
engineering: "engineer",
|
||||
custom: "custom",
|
||||
};
|
||||
|
||||
function extractRoleFromHint(hint: string): AgentCapability {
|
||||
const normalized = hint.toLowerCase();
|
||||
const tokens = normalized.split(/[^a-z]+/g).filter(Boolean);
|
||||
|
||||
for (const token of tokens) {
|
||||
const mapped = ROLE_HINT_ALIASES[token];
|
||||
if (mapped) {
|
||||
return mapRoleToCapability(mapped);
|
||||
}
|
||||
}
|
||||
|
||||
return mapRoleToCapability("custom");
|
||||
}
|
||||
|
||||
function inferRole(agent: AgentManifest): AgentCapability {
|
||||
if (agent.skills && agent.skills.length > 0) {
|
||||
for (const skill of agent.skills) {
|
||||
const role = extractRoleFromHint(skill);
|
||||
if (role !== "custom") {
|
||||
return role;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof agent.reportsTo === "string" && agent.reportsTo.trim() !== "") {
|
||||
const role = extractRoleFromHint(agent.reportsTo);
|
||||
if (role !== "custom") {
|
||||
return role;
|
||||
}
|
||||
}
|
||||
|
||||
return mapRoleToCapability("custom");
|
||||
}
|
||||
|
||||
export function agentManifestToAgentCreateInput(agent: AgentManifest): AgentCreateInput {
|
||||
const metadata: Record<string, unknown> = {};
|
||||
|
||||
if (agent.skills && agent.skills.length > 0) {
|
||||
metadata.skills = agent.skills;
|
||||
}
|
||||
|
||||
const input: AgentCreateInput = {
|
||||
name: agent.name,
|
||||
role: inferRole(agent),
|
||||
};
|
||||
|
||||
if (agent.title) {
|
||||
input.title = agent.title;
|
||||
}
|
||||
|
||||
if (agent.instructionBody !== undefined) {
|
||||
input.instructionsText = agent.instructionBody;
|
||||
}
|
||||
|
||||
if (agent.reportsTo !== null && agent.reportsTo !== undefined) {
|
||||
input.reportsTo = agent.reportsTo;
|
||||
}
|
||||
|
||||
if (Object.keys(metadata).length > 0) {
|
||||
input.metadata = metadata;
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
export function convertAgentCompanies(
|
||||
pkg: AgentCompaniesPackage,
|
||||
options?: { skipExisting?: string[] },
|
||||
): { inputs: AgentCreateInput[]; result: AgentCompaniesImportResult } {
|
||||
const existingNames = new Set(options?.skipExisting ?? []);
|
||||
const inputs: AgentCreateInput[] = [];
|
||||
const result: AgentCompaniesImportResult = {
|
||||
created: [],
|
||||
skipped: [],
|
||||
errors: [],
|
||||
};
|
||||
|
||||
for (const agent of pkg.agents) {
|
||||
if (existingNames.has(agent.name)) {
|
||||
result.skipped.push(agent.name);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const input = agentManifestToAgentCreateInput(agent);
|
||||
inputs.push(input);
|
||||
result.created.push(agent.name);
|
||||
} catch (err) {
|
||||
result.errors.push({
|
||||
name: agent.name,
|
||||
error: (err as Error).message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { inputs, result };
|
||||
}
|
||||
190
packages/core/src/agent-companies-types.test.ts
Normal file
190
packages/core/src/agent-companies-types.test.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import type {
|
||||
AgentCompaniesFrontmatter,
|
||||
AgentCompaniesImportResult,
|
||||
AgentCompaniesKind,
|
||||
AgentCompaniesPackage,
|
||||
AgentCompaniesSchema,
|
||||
AgentManifest,
|
||||
CompanyManifest,
|
||||
ProjectManifest,
|
||||
SkillManifest,
|
||||
SourceReference,
|
||||
TaskManifest,
|
||||
TeamManifest,
|
||||
} from "./agent-companies-types.js";
|
||||
|
||||
describe("agent-companies-types", () => {
|
||||
it("accepts AgentCompaniesSchema literal", () => {
|
||||
const schema: AgentCompaniesSchema = "agentcompanies/v1";
|
||||
expect(schema).toBe("agentcompanies/v1");
|
||||
});
|
||||
|
||||
it("accepts all AgentCompaniesKind variants", () => {
|
||||
const kinds: AgentCompaniesKind[] = [
|
||||
"company",
|
||||
"team",
|
||||
"agent",
|
||||
"project",
|
||||
"task",
|
||||
"skill",
|
||||
];
|
||||
expect(kinds).toHaveLength(6);
|
||||
});
|
||||
|
||||
it("accepts AgentCompaniesFrontmatter base fields", () => {
|
||||
const frontmatter: AgentCompaniesFrontmatter = {
|
||||
name: "Lean Dev Shop",
|
||||
description: "Small engineering-focused AI company",
|
||||
slug: "lean-dev-shop",
|
||||
schema: "agentcompanies/v1",
|
||||
kind: "company",
|
||||
version: "1.0.0",
|
||||
license: "MIT",
|
||||
authors: ["Team"],
|
||||
tags: ["engineering", "ai"],
|
||||
metadata: {
|
||||
sources: [{ kind: "git", repo: "acme/repo" }],
|
||||
customField: true,
|
||||
},
|
||||
};
|
||||
|
||||
expect(frontmatter.name).toBe("Lean Dev Shop");
|
||||
expect(frontmatter.metadata?.sources).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("accepts minimal AgentManifest", () => {
|
||||
const manifest: AgentManifest = {
|
||||
name: "CEO",
|
||||
};
|
||||
|
||||
expect(manifest.name).toBe("CEO");
|
||||
expect(manifest.skills).toBeUndefined();
|
||||
});
|
||||
|
||||
it("accepts fully populated AgentManifest", () => {
|
||||
const manifest: AgentManifest = {
|
||||
name: "CEO",
|
||||
description: "Runs strategy",
|
||||
slug: "ceo",
|
||||
kind: "agent",
|
||||
title: "Chief Executive Officer",
|
||||
reportsTo: null,
|
||||
skills: ["plan-ceo-review", "review"],
|
||||
instructionBody: "You are the CEO.",
|
||||
};
|
||||
|
||||
expect(manifest.title).toBe("Chief Executive Officer");
|
||||
expect(manifest.reportsTo).toBeNull();
|
||||
expect(manifest.skills).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("accepts CompanyManifest with schema and slug", () => {
|
||||
const manifest: CompanyManifest = {
|
||||
name: "Lean Dev Shop",
|
||||
description: "Small engineering-focused AI company",
|
||||
slug: "lean-dev-shop",
|
||||
schema: "agentcompanies/v1",
|
||||
};
|
||||
|
||||
expect(manifest.schema).toBe("agentcompanies/v1");
|
||||
expect(manifest.slug).toBe("lean-dev-shop");
|
||||
});
|
||||
|
||||
it("accepts TeamManifest with manager and includes", () => {
|
||||
const manifest: TeamManifest = {
|
||||
name: "Engineering",
|
||||
manager: "../cto/AGENTS.md",
|
||||
includes: ["../platform-lead/AGENTS.md", "../../skills/review/SKILL.md"],
|
||||
};
|
||||
|
||||
expect(manifest.manager).toContain("AGENTS.md");
|
||||
expect(manifest.includes).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("accepts ProjectManifest", () => {
|
||||
const manifest: ProjectManifest = {
|
||||
name: "Q2 Launch",
|
||||
description: "Launch execution project",
|
||||
slug: "q2-launch",
|
||||
};
|
||||
|
||||
expect(manifest.name).toBe("Q2 Launch");
|
||||
});
|
||||
|
||||
it("accepts TaskManifest with assignee, project, and schedule", () => {
|
||||
const manifest: TaskManifest = {
|
||||
name: "Monday Review",
|
||||
slug: "monday-review",
|
||||
description: "Weekly code review",
|
||||
assignee: "./agents/ceo/AGENTS.md",
|
||||
project: "./projects/q2-launch/PROJECT.md",
|
||||
schedule: {
|
||||
timezone: "America/New_York",
|
||||
startsAt: "2025-01-06T09:00:00",
|
||||
},
|
||||
};
|
||||
|
||||
expect(manifest.assignee).toContain("AGENTS.md");
|
||||
expect(manifest.schedule?.timezone).toBe("America/New_York");
|
||||
});
|
||||
|
||||
it("accepts SkillManifest with provides and requirements", () => {
|
||||
const manifest: SkillManifest = {
|
||||
name: "Code Review",
|
||||
provides: ["code-review", "security-review"],
|
||||
requirements: ["typescript"],
|
||||
};
|
||||
|
||||
expect(manifest.provides).toHaveLength(2);
|
||||
expect(manifest.requirements).toEqual(["typescript"]);
|
||||
});
|
||||
|
||||
it("accepts SourceReference with optional fields", () => {
|
||||
const source: SourceReference = {
|
||||
kind: "git",
|
||||
repo: "acme/repo",
|
||||
path: "skills/review",
|
||||
commit: "abc123",
|
||||
hash: "sha256:xyz",
|
||||
url: "https://example.com/spec",
|
||||
trackingRef: "v1",
|
||||
};
|
||||
|
||||
expect(source.repo).toBe("acme/repo");
|
||||
expect(source.trackingRef).toBe("v1");
|
||||
});
|
||||
|
||||
it("accepts AgentCompaniesPackage with nested manifests", () => {
|
||||
const pkg: AgentCompaniesPackage = {
|
||||
company: {
|
||||
name: "Lean Dev Shop",
|
||||
schema: "agentcompanies/v1",
|
||||
},
|
||||
agents: [{ name: "CEO" }],
|
||||
teams: [{ name: "Engineering" }],
|
||||
projects: [{ name: "Q2 Launch" }],
|
||||
tasks: [{ name: "Monday Review" }],
|
||||
skills: [{ name: "Code Review" }],
|
||||
};
|
||||
|
||||
expect(pkg.company?.name).toBe("Lean Dev Shop");
|
||||
expect(pkg.agents).toHaveLength(1);
|
||||
expect(pkg.teams).toHaveLength(1);
|
||||
expect(pkg.projects).toHaveLength(1);
|
||||
expect(pkg.tasks).toHaveLength(1);
|
||||
expect(pkg.skills).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("accepts AgentCompaniesImportResult", () => {
|
||||
const result: AgentCompaniesImportResult = {
|
||||
created: ["CEO", "CTO"],
|
||||
skipped: ["Reviewer"],
|
||||
errors: [{ name: "Broken", error: "missing name" }],
|
||||
};
|
||||
|
||||
expect(result.created).toHaveLength(2);
|
||||
expect(result.skipped).toEqual(["Reviewer"]);
|
||||
expect(result.errors[0].name).toBe("Broken");
|
||||
});
|
||||
});
|
||||
132
packages/core/src/agent-companies-types.ts
Normal file
132
packages/core/src/agent-companies-types.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* TypeScript type definitions for the Agent Companies markdown manifest format.
|
||||
*
|
||||
* Agent Companies packages are directory-based and use YAML frontmatter inside
|
||||
* markdown files (COMPANY.md, TEAM.md, AGENTS.md, PROJECT.md, TASK.md, SKILL.md).
|
||||
*
|
||||
* @module agent-companies-types
|
||||
*/
|
||||
|
||||
// ── Schema + Kinds ──────────────────────────────────────────────────────
|
||||
|
||||
/** Current Agent Companies schema literal. */
|
||||
export type AgentCompaniesSchema = "agentcompanies/v1";
|
||||
|
||||
/** Supported manifest kinds in Agent Companies packages. */
|
||||
export type AgentCompaniesKind =
|
||||
| "company"
|
||||
| "team"
|
||||
| "agent"
|
||||
| "project"
|
||||
| "task"
|
||||
| "skill";
|
||||
|
||||
// ── Provenance Metadata ─────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Source/provenance reference for imported or pinned external artifacts.
|
||||
* Stored under `metadata.sources` when present.
|
||||
*/
|
||||
export interface SourceReference {
|
||||
kind: string;
|
||||
repo?: string;
|
||||
path?: string;
|
||||
commit?: string;
|
||||
hash?: string;
|
||||
url?: string;
|
||||
trackingRef?: string;
|
||||
}
|
||||
|
||||
// ── Common Frontmatter ──────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Shared frontmatter fields across Agent Companies manifest files.
|
||||
*/
|
||||
export interface AgentCompaniesFrontmatter {
|
||||
/** Human-readable name (required). */
|
||||
name: string;
|
||||
/** Optional short discovery description. */
|
||||
description?: string;
|
||||
/** Stable portable identifier. */
|
||||
slug?: string;
|
||||
/** Schema identifier (typically set at package roots). */
|
||||
schema?: AgentCompaniesSchema;
|
||||
/** Explicit kind override (often implied by filename). */
|
||||
kind?: AgentCompaniesKind;
|
||||
/** Optional semantic version for package/manifests. */
|
||||
version?: string;
|
||||
/** License identifier. */
|
||||
license?: string;
|
||||
/** Attribution metadata. */
|
||||
authors?: string[];
|
||||
/** Search and classification tags. */
|
||||
tags?: string[];
|
||||
/** Tool-specific extension metadata. */
|
||||
metadata?: {
|
||||
/** Optional source/provenance references. */
|
||||
sources?: SourceReference[];
|
||||
[key: string]: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
// ── Manifest Types ──────────────────────────────────────────────────────
|
||||
|
||||
/** Root COMPANY.md manifest. */
|
||||
export interface CompanyManifest extends AgentCompaniesFrontmatter {
|
||||
// Company currently uses common frontmatter fields only.
|
||||
}
|
||||
|
||||
/** TEAM.md manifest. */
|
||||
export interface TeamManifest extends AgentCompaniesFrontmatter {
|
||||
manager?: string;
|
||||
includes?: string[];
|
||||
}
|
||||
|
||||
/** AGENTS.md manifest. */
|
||||
export interface AgentManifest extends AgentCompaniesFrontmatter {
|
||||
title?: string;
|
||||
reportsTo?: string | null;
|
||||
skills?: string[];
|
||||
/** Markdown content after YAML frontmatter. */
|
||||
instructionBody?: string;
|
||||
}
|
||||
|
||||
/** PROJECT.md manifest. */
|
||||
export interface ProjectManifest extends AgentCompaniesFrontmatter {
|
||||
// Project currently uses common frontmatter fields only.
|
||||
}
|
||||
|
||||
/** SKILL.md manifest. */
|
||||
export interface SkillManifest extends AgentCompaniesFrontmatter {
|
||||
provides?: string[];
|
||||
requirements?: string[];
|
||||
}
|
||||
|
||||
/** TASK.md manifest. */
|
||||
export interface TaskManifest extends AgentCompaniesFrontmatter {
|
||||
assignee?: string;
|
||||
project?: string;
|
||||
schedule?: {
|
||||
timezone?: string;
|
||||
startsAt?: string;
|
||||
};
|
||||
}
|
||||
|
||||
// ── Package + Import Result ─────────────────────────────────────────────
|
||||
|
||||
/** Parsed Agent Companies package from a directory or archive. */
|
||||
export interface AgentCompaniesPackage {
|
||||
company?: CompanyManifest;
|
||||
agents: AgentManifest[];
|
||||
teams: TeamManifest[];
|
||||
projects: ProjectManifest[];
|
||||
tasks: TaskManifest[];
|
||||
skills: SkillManifest[];
|
||||
}
|
||||
|
||||
/** Result of converting/importing Agent Companies agents into Fusion agent inputs. */
|
||||
export interface AgentCompaniesImportResult {
|
||||
created: string[];
|
||||
skipped: string[];
|
||||
errors: Array<{ name: string; error: string }>;
|
||||
}
|
||||
@@ -228,3 +228,37 @@ export {
|
||||
mapRoleToCapability,
|
||||
CompaniesShParseError,
|
||||
} from "./companies-sh-parser.js";
|
||||
|
||||
// ── Agent Companies Types ──────────────────────────────────
|
||||
|
||||
export type {
|
||||
AgentCompaniesPackage,
|
||||
AgentCompaniesKind,
|
||||
AgentCompaniesSchema,
|
||||
AgentCompaniesFrontmatter,
|
||||
AgentCompaniesImportResult,
|
||||
CompanyManifest,
|
||||
TeamManifest,
|
||||
AgentManifest,
|
||||
ProjectManifest,
|
||||
SkillManifest,
|
||||
TaskManifest,
|
||||
SourceReference,
|
||||
} from "./agent-companies-types.js";
|
||||
|
||||
// ── Agent Companies Parser ────────────────────────────────
|
||||
|
||||
export {
|
||||
parseYamlFrontmatter,
|
||||
parseCompanyManifest,
|
||||
parseTeamManifest,
|
||||
parseAgentManifest,
|
||||
parseProjectManifest,
|
||||
parseTaskManifest,
|
||||
parseSkillManifest,
|
||||
parseCompanyDirectory,
|
||||
parseCompanyArchive,
|
||||
agentManifestToAgentCreateInput,
|
||||
convertAgentCompanies,
|
||||
AgentCompaniesParseError,
|
||||
} from "./agent-companies-parser.js";
|
||||
|
||||
Reference in New Issue
Block a user