feat(FN-1174): migrate agent import flow to agent companies format

- Replace legacy companies.sh parser/types with the new agent companies parser and exported core types.
- Update CLI agent import wiring and tests to consume the new import source handling.
- Update dashboard agent import modal, API client, and import route tests to support archive-based sources.
- Add release-note changesets and lockfile updates for the @gsxdsm/fusion patch release.
This commit is contained in:
gsxdsm
2026-04-08 10:35:28 -07:00
parent ca42bd5129
commit a4bba623fa
21 changed files with 660 additions and 1999 deletions

View File

@@ -9,12 +9,13 @@ import {
AgentCompaniesParseError,
agentManifestToAgentCreateInput,
convertAgentCompanies,
mapRoleToCapability,
parseAgentManifest,
parseCompanyArchive,
parseCompanyDirectory,
parseCompanyManifest,
parseProjectManifest,
parseSkillManifest,
parseSingleAgentManifest,
parseTaskManifest,
parseTeamManifest,
parseYamlFrontmatter,
@@ -33,6 +34,15 @@ function writeTextFile(path: string, content: string): void {
writeFileSync(path, content, "utf-8");
}
function hasCommand(command: string): boolean {
try {
execSync(`command -v ${command}`, { stdio: "pipe" });
return true;
} catch {
return false;
}
}
afterEach(() => {
while (tempDirs.length > 0) {
const dir = tempDirs.pop();
@@ -44,96 +54,58 @@ afterEach(() => {
describe("agent-companies-parser", () => {
describe("parseYamlFrontmatter", () => {
it("parses YAML frontmatter with markdown body", () => {
it("parses valid YAML frontmatter and body", () => {
const content = `---
name: CEO
skills:
- review
---
You are the CEO agent.`;
Lead code review.`;
const { frontmatter, body } = parseYamlFrontmatter(content);
expect(frontmatter.name).toBe("CEO");
expect(frontmatter.skills).toEqual(["review"]);
expect(body).toBe("You are the CEO agent.");
const parsed = parseYamlFrontmatter(content);
expect(parsed.frontmatter.name).toBe("CEO");
expect(parsed.frontmatter.skills).toEqual(["review"]);
expect(parsed.body).toBe("Lead code review.");
});
it("parses frontmatter with no body", () => {
it("throws when frontmatter is missing", () => {
expect(() => parseYamlFrontmatter("name: CEO")).toThrow(AgentCompaniesParseError);
expect(() => parseYamlFrontmatter("name: CEO")).toThrow("Missing YAML frontmatter");
});
it("throws when YAML is malformed", () => {
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");
Body`;
expect(() => parseYamlFrontmatter(content)).toThrow("Malformed YAML frontmatter");
});
it("throws when YAML parses to null", () => {
const content = `---
null
---`;
expect(() => parseYamlFrontmatter(content)).toThrow("must parse to an object");
it("supports empty body", () => {
const parsed = parseYamlFrontmatter(`---
name: CEO
---`);
expect(parsed.body).toBe("");
});
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 = `---
it("parses multiline fields", () => {
const parsed = parseYamlFrontmatter(`---
name: CEO
description: |
Leads strategy
Reviews direction
First line
Second line
---
Body`;
const { frontmatter } = parseYamlFrontmatter(content);
Body`);
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"]);
expect(parsed.frontmatter.description).toBe("First line\nSecond line\n");
});
});
describe("individual manifest parsing", () => {
it("parses AGENTS.md with full frontmatter and body", () => {
const content = `---
describe("individual manifests", () => {
it("parses full AGENTS.md", () => {
const manifest = parseAgentManifest(`---
name: CEO
title: Chief Executive Officer
reportsTo: null
@@ -141,25 +113,32 @@ skills:
- plan-ceo-review
- review
---
You are the CEO agent. Your job is to lead.`;
const manifest = parseAgentManifest(content);
Agent instructions.`);
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");
expect(manifest.instructionBody).toBe("Agent instructions.");
});
it("parses AGENTS.md with minimal fields", () => {
it("parses minimal AGENTS.md", () => {
const manifest = parseAgentManifest(`---
name: Minimal Agent
name: Solo Agent
---`);
expect(manifest.name).toBe("Minimal Agent");
expect(manifest.name).toBe("Solo Agent");
expect(manifest.instructionBody).toBe("");
});
it("parses standalone AGENTS.md wrapper", () => {
const parsed = parseSingleAgentManifest(`---
name: Solo Agent
---
Be helpful.`);
expect(parsed.manifest.name).toBe("Solo Agent");
expect(parsed.manifest.instructionBody).toBe("Be helpful.");
});
it("parses COMPANY.md with schema and slug", () => {
const manifest = parseCompanyManifest(`---
name: Lean Dev Shop
@@ -177,79 +156,44 @@ schema: agentcompanies/v1
name: Engineering
manager: ../cto/AGENTS.md
includes:
- ../platform-lead/AGENTS.md
- ../../skills/review/SKILL.md
- ../platform/TEAM.md
---`);
expect(manifest.manager).toBe("../cto/AGENTS.md");
expect(manifest.includes).toEqual([
"../platform-lead/AGENTS.md",
"../../skills/review/SKILL.md",
]);
expect(manifest.includes).toEqual(["../platform/TEAM.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", () => {
it("parses TASK.md 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"
startsAt: "2026-04-14T09: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");
expect(manifest.schedule?.timezone).toBe("America/New_York");
});
});
describe("parseCompanyDirectory", () => {
it("parses a full directory structure", () => {
describe("directory parsing", () => {
it("parses a full company directory", () => {
const root = createTempDir();
writeTextFile(
join(root, "COMPANY.md"),
`---
name: Lean Dev Shop
description: Small engineering-focused AI company
slug: lean-dev-shop
schema: agentcompanies/v1
---`,
);
@@ -261,28 +205,25 @@ title: Chief Executive Officer
skills:
- review
---
You are the CEO agent.`,
Lead reviews.`,
);
writeTextFile(
join(root, "teams", "engineering", "TEAM.md"),
`---
name: Engineering
manager: ../cto/AGENTS.md
manager: ../ceo/AGENTS.md
---`,
);
writeTextFile(
join(root, "tasks", "review", "TASK.md"),
join(root, "projects", "q2-launch", "PROJECT.md"),
`---
name: Q2 Launch
---`,
);
writeTextFile(
join(root, "tasks", "monday-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
---`,
);
@@ -291,235 +232,171 @@ provides:
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);
expect(pkg.projects).toHaveLength(0);
});
it("handles directory with only agents and no COMPANY.md", () => {
it("parses agents-only directory without COMPANY.md", () => {
const root = createTempDir();
writeTextFile(
join(root, "agents", "ceo", "AGENTS.md"),
join(root, "agents", "solo", "AGENTS.md"),
`---
name: CEO
name: Solo Agent
---`,
);
const pkg = parseCompanyDirectory(root);
expect(pkg.company).toBeUndefined();
expect(pkg.agents).toHaveLength(1);
expect(pkg.teams).toEqual([]);
});
it("handles empty directory", () => {
it("parses 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", () => {
it("handles circular team includes without recursion issues", () => {
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"),
join(root, "teams", "a", "TEAM.md"),
`---
name: Lean Dev Shop
schema: agentcompanies/v1
name: a
slug: a
includes:
- ../b/TEAM.md
---`,
);
writeTextFile(
join(packageDir, "agents", "ceo", "AGENTS.md"),
join(root, "teams", "b", "TEAM.md"),
`---
name: CEO
skills:
- review
---
You are the CEO agent.`,
name: b
slug: b
includes:
- ../a/TEAM.md
---`,
);
const archivePath = join(temp, "company.tgz");
execSync(
`tar czf ${JSON.stringify(archivePath)} -C ${JSON.stringify(temp)} ${JSON.stringify(packageDirName)}`,
);
const pkg = parseCompanyDirectory(root);
expect(pkg.teams).toHaveLength(2);
});
});
describe("archive parsing", () => {
it("parses a .tgz archive", async () => {
const root = createTempDir();
const packageDir = join(root, "company-package");
writeTextFile(join(packageDir, "COMPANY.md"), `---
name: Archive Company
schema: agentcompanies/v1
---`);
writeTextFile(join(packageDir, "agents", "ceo", "AGENTS.md"), `---
name: Archive CEO
---`);
const archivePath = join(root, "company.tgz");
execSync(`tar czf ${JSON.stringify(archivePath)} -C ${JSON.stringify(root)} company-package`);
const pkg = await parseCompanyArchive(archivePath);
expect(pkg.company?.name).toBe("Lean Dev Shop");
expect(pkg.company?.name).toBe("Archive Company");
expect(pkg.agents[0]?.name).toBe("Archive CEO");
});
const zipIt = hasCommand("zip") ? it : it.skip;
zipIt("parses a .zip archive", async () => {
const root = createTempDir();
const packageDir = join(root, "zip-company");
writeTextFile(join(packageDir, "COMPANY.md"), `---
name: Zip Company
schema: agentcompanies/v1
---`);
writeTextFile(join(packageDir, "agents", "ceo", "AGENTS.md"), `---
name: Zip CEO
---`);
const archivePath = join(root, "company.zip");
execSync(`zip -qr ${JSON.stringify(archivePath)} zip-company`, { cwd: root });
const pkg = await parseCompanyArchive(archivePath);
expect(pkg.company?.name).toBe("Zip Company");
expect(pkg.agents).toHaveLength(1);
});
it("handles archive with a single file entry", async () => {
const temp = createTempDir();
writeTextFile(join(temp, "README.md"), "hello");
it("throws for unsupported archive extension", async () => {
const root = createTempDir();
const archivePath = join(root, "company.rar");
writeTextFile(archivePath, "not a real archive");
const archivePath = join(temp, "single-file.tgz");
execSync(
`tar czf ${JSON.stringify(archivePath)} -C ${JSON.stringify(temp)} README.md`,
await expect(parseCompanyArchive(archivePath)).rejects.toThrow(
"Unsupported archive format",
);
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", () => {
it("maps AgentManifest to AgentCreateInput", () => {
const input = agentManifestToAgentCreateInput({
name: "CEO",
title: "Chief Executive Officer",
instructionBody: "Lead the company",
instructionBody: "Lead strategy",
skills: ["review"],
reportsTo: "../founder/AGENTS.md",
reportsTo: null,
metadata: {
sources: [{ kind: "git", repo: "acme/repo" }],
},
});
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).toEqual({
name: "CEO",
role: "custom",
title: "Chief Executive Officer",
metadata: {
instructions: "Lead strategy",
skills: ["review"],
reportsTo: null,
sources: [{ kind: "git", repo: "acme/repo" }],
},
});
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", () => {
it("converts package agents with skipExisting", () => {
const { inputs, result } = convertAgentCompanies(
{
company: { name: "Lean Dev Shop" },
agents: [
{ name: "Existing", skills: ["review"] },
{ name: "New Agent", skills: ["executor"] },
],
company: { name: "Example" },
agents: [{ name: "Existing" }, { name: "New Agent", title: "New" }],
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([]);
expect(inputs[0]?.name).toBe("New Agent");
expect(result).toEqual({
created: ["New Agent"],
skipped: ["Existing"],
errors: [],
});
});
it("defaults to custom role when no skills are present", () => {
const input = agentManifestToAgentCreateInput({ name: "Generalist" });
expect(input.role).toBe("custom");
});
});
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",
);
describe("mapRoleToCapability", () => {
it("maps known roles and defaults unknowns to custom", () => {
expect(mapRoleToCapability("reviewer")).toBe("reviewer");
expect(mapRoleToCapability("unknown-role")).toBe("custom");
});
});
});

View File

@@ -1,17 +1,15 @@
/**
* 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 { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import extractZip from "extract-zip";
import { parse as parseYaml } from "yaml";
import type {
@@ -20,17 +18,11 @@ import type {
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);
@@ -38,18 +30,32 @@ export class AgentCompaniesParseError extends Error {
}
}
// ── Frontmatter Parsing ──────────────────────────────────────────────────
const VALID_ROLES: Set<string> = new Set([
"triage",
"executor",
"reviewer",
"merger",
"scheduler",
"engineer",
"custom",
]);
/**
* Extract YAML frontmatter and markdown body from a manifest file.
*
* @throws {AgentCompaniesParseError} On missing or malformed frontmatter.
* Map a role string to a Fusion agent capability.
* Unknown roles fall back to "custom".
*/
export function mapRoleToCapability(role: string): AgentCapability {
if (VALID_ROLES.has(role)) {
return role as AgentCapability;
}
return "custom";
}
export function parseYamlFrontmatter(content: string): {
frontmatter: Record<string, unknown>;
body: string;
} {
if (typeof content !== "string" || content.length === 0) {
if (typeof content !== "string" || content.trim().length === 0) {
throw new AgentCompaniesParseError("Manifest content is empty or not a string");
}
@@ -58,15 +64,12 @@ export function parseYamlFrontmatter(content: string): {
throw new AgentCompaniesParseError("Missing YAML frontmatter delimiters (---)");
}
const yamlContent = match[1];
const body = match[2] ?? "";
let parsed: unknown;
try {
parsed = parseYaml(yamlContent);
} catch (err) {
parsed = parseYaml(match[1]);
} catch (error) {
throw new AgentCompaniesParseError(
`Malformed YAML frontmatter: ${(err as Error).message}`,
`Malformed YAML frontmatter: ${(error as Error).message}`,
);
}
@@ -76,79 +79,59 @@ export function parseYamlFrontmatter(content: string): {
return {
frontmatter: parsed as Record<string, unknown>,
body,
body: match[2] ?? "",
};
}
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}`,
);
}
function requireName(frontmatter: Record<string, unknown>, kind: string): void {
if (typeof frontmatter.name !== "string" || frontmatter.name.trim().length === 0) {
throw new AgentCompaniesParseError(`${kind} manifest is missing required field: name`);
}
}
/**
* Parse and validate a manifest frontmatter shape.
*/
function parseManifest<T>(content: string, kind: string, requiredFields: string[]): T {
function parseTypedManifest<T>(content: string, kind: string): T {
const { frontmatter } = parseYamlFrontmatter(content);
validateRequiredFields(frontmatter, kind, requiredFields);
requireName(frontmatter, kind);
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);
const { frontmatter, body } = parseYamlFrontmatter(content);
requireName(frontmatter, "agent");
return {
...manifest,
...(frontmatter as unknown as AgentManifest),
instructionBody: body,
};
}
export function parseSingleAgentManifest(content: string): { manifest: AgentManifest } {
return { manifest: parseAgentManifest(content) };
}
export function parseCompanyManifest(content: string): CompanyManifest {
return parseTypedManifest<CompanyManifest>(content, "company");
}
export function parseTeamManifest(content: string): TeamManifest {
return parseTypedManifest<TeamManifest>(content, "team");
}
export function parseProjectManifest(content: string): ProjectManifest {
return parseManifest<ProjectManifest>(content, "project", ["name"]);
return parseTypedManifest<ProjectManifest>(content, "project");
}
export function parseTaskManifest(content: string): TaskManifest {
return parseManifest<TaskManifest>(content, "task", ["name"]);
return parseTypedManifest<TaskManifest>(content, "task");
}
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 {
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}`);
return parser(readFileSync(filePath, "utf-8"));
} catch (error) {
if (error instanceof AgentCompaniesParseError) {
throw new AgentCompaniesParseError(`${filePath}: ${error.message}`);
}
throw err;
throw error;
}
}
@@ -163,68 +146,104 @@ function parseManifestSubdirectories<T>(
return [];
}
const entries = readdirSync(sectionPath, { withFileTypes: true });
const parsed: T[] = [];
const manifests: T[] = [];
const entries = readdirSync(sectionPath, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.sort((a, b) => a.name.localeCompare(b.name));
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));
manifests.push(parseManifestFile(manifestPath, parser));
}
return parsed;
return manifests;
}
function walkTeamIncludes(teams: TeamManifest[]): void {
const byKey = new Map<string, TeamManifest>();
for (const team of teams) {
const key = team.slug ?? team.name;
byKey.set(key, team);
}
const visited = new Set<string>();
const visiting = new Set<string>();
const visit = (key: string, depth = 0): void => {
if (depth > 64 || visited.has(key) || visiting.has(key)) {
return;
}
visiting.add(key);
const team = byKey.get(key);
if (team?.includes) {
for (const includeRef of team.includes) {
const includeKey = includeRef.replace(/\.md$/i, "").split("/").pop();
if (includeKey) {
visit(includeKey, depth + 1);
}
}
}
visiting.delete(key);
visited.add(key);
};
for (const key of byKey.keys()) {
visit(key);
}
}
export function parseCompanyDirectory(dirPath: string): AgentCompaniesPackage {
const resolvedDir = resolve(dirPath);
if (!existsSync(resolvedDir)) {
throw new AgentCompaniesParseError(`Company directory does not exist: ${resolvedDir}`);
const resolvedPath = resolve(dirPath);
if (!existsSync(resolvedPath)) {
throw new AgentCompaniesParseError(`Company directory does not exist: ${resolvedPath}`);
}
if (!statSync(resolvedDir).isDirectory()) {
throw new AgentCompaniesParseError(`Company path is not a directory: ${resolvedDir}`);
if (!statSync(resolvedPath).isDirectory()) {
throw new AgentCompaniesParseError(`Company path is not a directory: ${resolvedPath}`);
}
const companyPath = join(resolvedDir, "COMPANY.md");
const companyPath = join(resolvedPath, "COMPANY.md");
const teams = parseManifestSubdirectories(resolvedPath, "teams", "TEAM.md", parseTeamManifest);
walkTeamIncludes(teams);
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),
agents: parseManifestSubdirectories(resolvedPath, "agents", "AGENTS.md", parseAgentManifest),
teams,
projects: parseManifestSubdirectories(
resolvedPath,
"projects",
"PROJECT.md",
parseProjectManifest,
),
tasks: parseManifestSubdirectories(resolvedPath, "tasks", "TASK.md", parseTaskManifest),
};
}
function resolveArchiveRoot(tempDir: string): string {
function resolveExtractionRoot(tempDir: string): string {
if (existsSync(join(tempDir, "COMPANY.md"))) {
return tempDir;
}
const entries = readdirSync(tempDir, { withFileTypes: true });
const directories = readdirSync(tempDir, { withFileTypes: true }).filter((entry) =>
entry.isDirectory(),
);
for (const entry of entries) {
if (!entry.isDirectory()) {
continue;
}
const childPath = join(tempDir, entry.name);
if (existsSync(join(childPath, "COMPANY.md"))) {
return childPath;
for (const directory of directories) {
const candidate = join(tempDir, directory.name);
if (existsSync(join(candidate, "COMPANY.md"))) {
return candidate;
}
}
if (entries.length === 1 && entries[0].isDirectory()) {
return join(tempDir, entries[0].name);
if (directories.length === 1) {
return join(tempDir, directories[0].name);
}
return tempDir;
@@ -232,124 +251,60 @@ function resolveArchiveRoot(tempDir: string): string {
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" },
);
if (resolvedArchivePath.endsWith(".tar.gz") || resolvedArchivePath.endsWith(".tgz")) {
execSync(
`tar xzf ${JSON.stringify(resolvedArchivePath)} -C ${JSON.stringify(tempDir)}`,
{ stdio: "pipe" },
);
} else if (resolvedArchivePath.endsWith(".zip")) {
await extractZip(resolvedArchivePath, { dir: tempDir });
} else {
throw new AgentCompaniesParseError(
"Unsupported archive format. Expected .tar.gz, .tgz, or .zip",
);
}
const extractionRoot = resolveArchiveRoot(tempDir);
return parseCompanyDirectory(extractionRoot);
} catch (err) {
if (err instanceof AgentCompaniesParseError) {
throw err;
return parseCompanyDirectory(resolveExtractionRoot(tempDir));
} catch (error) {
if (error instanceof AgentCompaniesParseError) {
throw error;
}
throw new AgentCompaniesParseError(
`Failed to parse Agent Companies archive: ${(err as Error).message}`,
`Failed to parse Agent Companies archive: ${(error 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) {
if (typeof agent.instructionBody === "string") {
metadata.instructions = agent.instructionBody;
}
if (Array.isArray(agent.skills) && agent.skills.length > 0) {
metadata.skills = agent.skills;
}
if (agent.reportsTo !== undefined) {
metadata.reportsTo = agent.reportsTo;
}
if (Array.isArray(agent.metadata?.sources) && agent.metadata.sources.length > 0) {
metadata.sources = agent.metadata.sources;
}
const input: AgentCreateInput = {
return {
name: agent.name,
role: inferRole(agent),
role: mapRoleToCapability("custom"),
...(typeof agent.title === "string" && agent.title.trim().length > 0
? { title: agent.title }
: {}),
...(Object.keys(metadata).length > 0 ? { metadata } : {}),
};
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(
@@ -371,13 +326,12 @@ export function convertAgentCompanies(
}
try {
const input = agentManifestToAgentCreateInput(agent);
inputs.push(input);
inputs.push(agentManifestToAgentCreateInput(agent));
result.created.push(agent.name);
} catch (err) {
} catch (error) {
result.errors.push({
name: agent.name,
error: (err as Error).message,
error: (error as Error).message,
});
}
}

View File

@@ -1,4 +1,4 @@
import { describe, it, expect } from "vitest";
import { describe, expect, it } from "vitest";
import type {
AgentCompaniesFrontmatter,
AgentCompaniesImportResult,
@@ -8,31 +8,31 @@ import type {
AgentManifest,
CompanyManifest,
ProjectManifest,
SkillManifest,
SourceReference,
TaskManifest,
TeamManifest,
} from "./agent-companies-types.js";
describe("agent-companies-types", () => {
it("accepts AgentCompaniesSchema literal", () => {
it("supports schema and kind literals", () => {
const schema: AgentCompaniesSchema = "agentcompanies/v1";
expect(schema).toBe("agentcompanies/v1");
});
const kinds: AgentCompaniesKind[] = ["company", "team", "agent", "project", "task", "skill"];
it("accepts all AgentCompaniesKind variants", () => {
const kinds: AgentCompaniesKind[] = [
"company",
"team",
"agent",
"project",
"task",
"skill",
];
expect(schema).toBe("agentcompanies/v1");
expect(kinds).toHaveLength(6);
});
it("accepts AgentCompaniesFrontmatter base fields", () => {
it("supports shared frontmatter with source metadata", () => {
const source: SourceReference = {
kind: "git",
repo: "acme/agent-company",
path: "agents/ceo/AGENTS.md",
commit: "abc123",
hash: "sha256:def456",
url: "https://example.com/repo",
trackingRef: "main",
};
const frontmatter: AgentCompaniesFrontmatter = {
name: "Lean Dev Shop",
description: "Small engineering-focused AI company",
@@ -41,150 +41,75 @@ describe("agent-companies-types", () => {
kind: "company",
version: "1.0.0",
license: "MIT",
authors: ["Team"],
tags: ["engineering", "ai"],
authors: ["Fusion Team"],
tags: ["ai", "engineering"],
metadata: {
sources: [{ kind: "git", repo: "acme/repo" }],
customField: true,
sources: [source],
},
};
expect(frontmatter.name).toBe("Lean Dev Shop");
expect(frontmatter.metadata?.sources).toHaveLength(1);
expect(frontmatter.metadata?.sources?.[0]?.repo).toBe("acme/agent-company");
});
it("accepts minimal AgentManifest", () => {
const manifest: AgentManifest = {
name: "CEO",
it("supports company/team/agent/project/task manifests", () => {
const company: CompanyManifest = {
name: "Lean Dev Shop",
goals: ["Ship high-quality software"],
requirements: ["Use review workflow"],
};
expect(manifest.name).toBe("CEO");
expect(manifest.skills).toBeUndefined();
});
const team: TeamManifest = {
name: "Engineering",
manager: "../cto/AGENTS.md",
includes: ["../platform/AGENTS.md"],
};
it("accepts fully populated AgentManifest", () => {
const manifest: AgentManifest = {
const agent: 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.",
instructionBody: "Lead strategy and review architecture.",
};
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 = {
const project: 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 = {
const task: 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",
startsAt: "2026-04-14T09:00:00",
},
};
expect(manifest.assignee).toContain("AGENTS.md");
expect(manifest.schedule?.timezone).toBe("America/New_York");
expect(company.goals).toHaveLength(1);
expect(team.includes).toEqual(["../platform/AGENTS.md"]);
expect(agent.reportsTo).toBeNull();
expect(project.slug).toBe("q2-launch");
expect(task.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", () => {
it("supports package and import result shapes", () => {
const pkg: AgentCompaniesPackage = {
company: {
name: "Lean Dev Shop",
schema: "agentcompanies/v1",
},
company: { name: "Lean Dev Shop" },
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" }],
created: ["CEO"],
skipped: ["CTO"],
errors: [{ name: "Reviewer", error: "invalid manifest" }],
};
expect(result.created).toHaveLength(2);
expect(result.skipped).toEqual(["Reviewer"]);
expect(result.errors[0].name).toBe("Broken");
expect(pkg.agents[0].name).toBe("CEO");
expect(result.errors[0]?.name).toBe("Reviewer");
});
});

View File

@@ -1,18 +1,11 @@
/**
* 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).
* Type definitions for Agent Companies package manifests.
*
* @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"
@@ -21,12 +14,6 @@ export type AgentCompaniesKind =
| "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;
@@ -37,72 +24,41 @@ export interface SourceReference {
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.
goals?: string[];
requirements?: string[];
}
/** 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.
}
export interface ProjectManifest extends AgentCompaniesFrontmatter {}
/** SKILL.md manifest. */
export interface SkillManifest extends AgentCompaniesFrontmatter {
provides?: string[];
requirements?: string[];
}
/** TASK.md manifest. */
export interface TaskManifest extends AgentCompaniesFrontmatter {
assignee?: string;
project?: string;
@@ -112,19 +68,14 @@ export interface TaskManifest extends AgentCompaniesFrontmatter {
};
}
// ── 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[];

View File

@@ -1,314 +0,0 @@
import { describe, it, expect } from "vitest";
import {
parseCompaniesShManifest,
companiesShAgentToAgentCreateInput,
convertCompaniesShAgents,
mapRoleToCapability,
CompaniesShParseError,
} from "./companies-sh-parser.js";
// ── Helpers ──────────────────────────────────────────────────────────────
function encodeManifest(agents: unknown[]): string {
return Buffer.from(JSON.stringify(agents)).toString("base64");
}
function makeScript(companyName: string, agents: unknown[], envLines?: string[]): string {
const manifest = encodeManifest(agents);
let script = `#!/bin/bash\n# Agent Company Manifest\nCOMPANY_NAME="${companyName}"\nAGENT_MANIFEST="${manifest}"`;
if (envLines && envLines.length > 0) {
script += "\n\n" + envLines.join("\n");
}
return script;
}
// ── Tests ────────────────────────────────────────────────────────────────
describe("companies-sh-parser", () => {
describe("parseCompaniesShManifest", () => {
it("parses a valid companies.sh manifest", () => {
const agents = [
{
name: "Code Reviewer",
role: "reviewer",
capabilities: ["code-review", "security-audit"],
config: { model: "claude-sonnet-4", maxTokens: 4096, thinkingLevel: "medium" },
metadata: { title: "Senior Code Reviewer", icon: "👁" },
},
];
const script = makeScript("test-company", agents, [
'export KB_AGENT_MODEL="${KB_AGENT_MODEL:-claude-sonnet-4}"',
]);
const manifest = parseCompaniesShManifest(script);
expect(manifest.companyName).toBe("test-company");
expect(manifest.agents).toHaveLength(1);
expect(manifest.agents[0].name).toBe("Code Reviewer");
expect(manifest.agents[0].role).toBe("reviewer");
expect(manifest.agents[0].capabilities).toEqual(["code-review", "security-audit"]);
expect(manifest.agents[0].config?.model).toBe("claude-sonnet-4");
expect(manifest.agents[0].metadata?.title).toBe("Senior Code Reviewer");
expect(manifest.envVars).toHaveLength(1);
expect(manifest.envVars[0].name).toBe("KB_AGENT_MODEL");
expect(manifest.envVars[0].defaultValue).toBe("claude-sonnet-4");
});
it("parses a manifest with multiple agents", () => {
const agents = [
{ name: "Agent 1", role: "executor" },
{ name: "Agent 2", role: "reviewer" },
{ name: "Agent 3", role: "triage" },
];
const script = makeScript("multi-agent", agents);
const manifest = parseCompaniesShManifest(script);
expect(manifest.agents).toHaveLength(3);
expect(manifest.agents.map((a) => a.name)).toEqual(["Agent 1", "Agent 2", "Agent 3"]);
});
it("throws on empty content", () => {
expect(() => parseCompaniesShManifest("")).toThrow(CompaniesShParseError);
expect(() => parseCompaniesShManifest("")).toThrow("empty or not a string");
});
it("throws on missing COMPANY_NAME", () => {
const manifest = encodeManifest([{ name: "Test", role: "executor" }]);
const script = `#!/bin/bash\nAGENT_MANIFEST="${manifest}"`;
expect(() => parseCompaniesShManifest(script)).toThrow("Missing COMPANY_NAME");
});
it("throws on missing AGENT_MANIFEST", () => {
const script = `#!/bin/bash\nCOMPANY_NAME="test"`;
expect(() => parseCompaniesShManifest(script)).toThrow("Missing AGENT_MANIFEST");
});
it("throws on invalid base64 encoding", () => {
const script = `#!/bin/bash\nCOMPANY_NAME="test"\nAGENT_MANIFEST="not-valid-base64!!!"`;
expect(() => parseCompaniesShManifest(script)).toThrow("Invalid base64");
});
it("throws on invalid JSON in manifest", () => {
const badJson = btoa("not json");
const script = `#!/bin/bash\nCOMPANY_NAME="test"\nAGENT_MANIFEST="${badJson}"`;
expect(() => parseCompaniesShManifest(script)).toThrow("Invalid JSON");
});
it("throws when manifest decodes to non-array", () => {
const obj = btoa(JSON.stringify({ name: "not an array" }));
const script = `#!/bin/bash\nCOMPANY_NAME="test"\nAGENT_MANIFEST="${obj}"`;
expect(() => parseCompaniesShManifest(script)).toThrow("must decode to a JSON array");
});
it("throws on agent missing name", () => {
const agents = [{ role: "executor" }];
const script = makeScript("test", agents);
expect(() => parseCompaniesShManifest(script)).toThrow("missing required field: name");
});
it("throws on agent missing role", () => {
const agents = [{ name: "Test Agent" }];
const script = makeScript("test", agents);
expect(() => parseCompaniesShManifest(script)).toThrow("missing required field: role");
});
it("throws on agent with empty name", () => {
const agents = [{ name: " ", role: "executor" }];
const script = makeScript("test", agents);
expect(() => parseCompaniesShManifest(script)).toThrow("missing required field: name");
});
it("throws on agent with empty role", () => {
const agents = [{ name: "Test", role: "" }];
const script = makeScript("test", agents);
expect(() => parseCompaniesShManifest(script)).toThrow("missing required field: role");
});
it("handles empty capabilities array", () => {
const agents = [{ name: "Test", role: "executor", capabilities: [] }];
const script = makeScript("test", agents);
const manifest = parseCompaniesShManifest(script);
expect(manifest.agents[0].capabilities).toEqual([]);
});
it("handles agents with no optional fields", () => {
const agents = [{ name: "Minimal", role: "custom" }];
const script = makeScript("test", agents);
const manifest = parseCompaniesShManifest(script);
expect(manifest.agents[0].capabilities).toBeUndefined();
expect(manifest.agents[0].config).toBeUndefined();
expect(manifest.agents[0].metadata).toBeUndefined();
});
it("extracts multiple environment variables", () => {
const agents = [{ name: "Test", role: "executor" }];
const script = makeScript("test", agents, [
'export KB_MODEL="${KB_MODEL:-claude-sonnet-4}"',
'export KB_THINKING="${KB_THINKING:-medium}"',
'export KB_MAX_TOKENS="${KB_MAX_TOKENS:-4096}"',
]);
const manifest = parseCompaniesShManifest(script);
expect(manifest.envVars).toHaveLength(3);
expect(manifest.envVars.map((v) => v.name)).toEqual([
"KB_MODEL",
"KB_THINKING",
"KB_MAX_TOKENS",
]);
});
it("returns empty envVars when no export statements", () => {
const agents = [{ name: "Test", role: "executor" }];
const script = makeScript("test", agents);
const manifest = parseCompaniesShManifest(script);
expect(manifest.envVars).toEqual([]);
});
it("handles non-object agent entries", () => {
const agents = ["not an object", 42, null];
const script = makeScript("test", agents);
expect(() => parseCompaniesShManifest(script)).toThrow("not an object");
});
it("filters non-string capabilities", () => {
const agents = [{ name: "Test", role: "executor", capabilities: ["valid", 123, null, "also-valid"] }];
const script = makeScript("test", agents);
const manifest = parseCompaniesShManifest(script);
expect(manifest.agents[0].capabilities).toEqual(["valid", "also-valid"]);
});
});
describe("mapRoleToCapability", () => {
it("maps all known roles correctly", () => {
expect(mapRoleToCapability("triage")).toBe("triage");
expect(mapRoleToCapability("executor")).toBe("executor");
expect(mapRoleToCapability("reviewer")).toBe("reviewer");
expect(mapRoleToCapability("merger")).toBe("merger");
expect(mapRoleToCapability("scheduler")).toBe("scheduler");
expect(mapRoleToCapability("engineer")).toBe("engineer");
expect(mapRoleToCapability("custom")).toBe("custom");
});
it("maps unknown roles to custom", () => {
expect(mapRoleToCapability("analyst")).toBe("custom");
expect(mapRoleToCapability("designer")).toBe("custom");
expect(mapRoleToCapability("")).toBe("custom");
});
});
describe("companiesShAgentToAgentCreateInput", () => {
it("converts a minimal agent", () => {
const agent = { name: "Test", role: "executor" };
const input = companiesShAgentToAgentCreateInput(agent);
expect(input.name).toBe("Test");
expect(input.role).toBe("executor");
expect(input.metadata).toBeUndefined();
expect(input.runtimeConfig).toBeUndefined();
});
it("converts a fully populated agent", () => {
const agent = {
name: "Code Reviewer",
role: "reviewer",
capabilities: ["code-review"],
config: {
model: "claude-sonnet-4",
maxTokens: 4096,
thinkingLevel: "medium" as const,
maxTurns: 10,
},
metadata: {
title: "Senior Reviewer",
icon: "👁",
description: "Reviews code for quality",
},
};
const input = companiesShAgentToAgentCreateInput(agent);
expect(input.name).toBe("Code Reviewer");
expect(input.role).toBe("reviewer");
expect(input.title).toBe("Senior Reviewer");
expect(input.icon).toBe("👁");
expect(input.runtimeConfig).toEqual({
model: "claude-sonnet-4",
maxTokens: 4096,
thinkingLevel: "medium",
maxTurns: 10,
});
expect(input.metadata).toEqual({
capabilities: ["code-review"],
description: "Reviews code for quality",
});
});
it("maps unknown roles to custom", () => {
const agent = { name: "Special", role: "analyst" };
const input = companiesShAgentToAgentCreateInput(agent);
expect(input.role).toBe("custom");
});
it("handles agent with empty capabilities", () => {
const agent = { name: "Test", role: "executor", capabilities: [] };
const input = companiesShAgentToAgentCreateInput(agent);
expect(input.metadata).toBeUndefined();
});
});
describe("convertCompaniesShAgents", () => {
it("converts all agents when no duplicates", () => {
const agents = [
{ name: "Agent 1", role: "executor" },
{ name: "Agent 2", role: "reviewer" },
];
const { inputs, result } = convertCompaniesShAgents(agents);
expect(inputs).toHaveLength(2);
expect(result.created).toEqual(["Agent 1", "Agent 2"]);
expect(result.skipped).toEqual([]);
expect(result.errors).toEqual([]);
});
it("skips agents with existing names", () => {
const agents = [
{ name: "Existing Agent", role: "executor" },
{ name: "New Agent", role: "reviewer" },
];
const { inputs, result } = convertCompaniesShAgents(agents, {
skipExisting: ["Existing Agent"],
});
expect(inputs).toHaveLength(1);
expect(inputs[0].name).toBe("New Agent");
expect(result.skipped).toEqual(["Existing Agent"]);
});
it("handles empty agent list", () => {
const { inputs, result } = convertCompaniesShAgents([]);
expect(inputs).toHaveLength(0);
expect(result.created).toEqual([]);
});
});
});

View File

@@ -1,269 +0,0 @@
/**
* Parser for companies.sh manifest files.
*
* Extracts agent definitions from shell-script-based manifests following
* the companies.sh standard. Handles base64-encoded JSON payloads,
* shell variable extraction, and environment variable defaults.
*
* @module companies-sh-parser
*/
import type {
CompaniesShManifest,
CompaniesShAgent,
CompaniesShEnvVar,
CompaniesShImportResult,
} from "./companies-sh-types.js";
import type { AgentCreateInput, AgentCapability } from "./types.js";
// ── Parsing Errors ───────────────────────────────────────────────────────
export class CompaniesShParseError extends Error {
constructor(message: string) {
super(message);
this.name = "CompaniesShParseError";
}
}
// ── Role Mapping ─────────────────────────────────────────────────────────
const VALID_ROLES: Set<string> = new Set([
"triage", "executor", "reviewer", "merger", "scheduler", "engineer", "custom",
]);
/**
* Map a companies.sh role string to a kb AgentCapability.
* Unknown roles fall back to "custom".
*/
export function mapRoleToCapability(role: string): AgentCapability {
if (VALID_ROLES.has(role)) {
return role as AgentCapability;
}
return "custom";
}
// ── Shell Variable Extraction ────────────────────────────────────────────
/**
* Extract a shell variable value from script content.
* Handles both `VAR="value"` and `VAR='value'` syntax.
* Returns null if the variable is not found.
*/
function extractShellVariable(script: string, varName: string): string | null {
// Match VAR="value" or VAR='value' — capture the value inside quotes
const regex = new RegExp(`^${varName}=["'](.*)["']\\s*$`, "m");
const match = script.match(regex);
if (!match) return null;
return match[1];
}
/**
* Extract environment variable defaults from export statements.
* Matches `export VAR="${VAR:-default}"` pattern.
*/
function extractEnvVars(script: string): CompaniesShEnvVar[] {
const envVars: CompaniesShEnvVar[] = [];
const regex = /^export\s+(\w+)="\$\{(?:\w+):-(.*?)\}"\s*$/gm;
let match: RegExpExecArray | null;
while ((match = regex.exec(script)) !== null) {
envVars.push({
name: match[1],
defaultValue: match[2],
});
}
return envVars;
}
// ── Validation ───────────────────────────────────────────────────────────
/**
* Validate a single parsed agent has required fields.
* Throws if name or role is missing or invalid type.
*/
function validateAgent(agent: unknown, index: number): CompaniesShAgent {
if (!agent || typeof agent !== "object") {
throw new CompaniesShParseError(`Agent at index ${index} is not an object`);
}
const obj = agent as Record<string, unknown>;
if (typeof obj.name !== "string" || obj.name.trim() === "") {
throw new CompaniesShParseError(`Agent at index ${index} is missing required field: name`);
}
if (typeof obj.role !== "string" || obj.role.trim() === "") {
throw new CompaniesShParseError(`Agent at index ${index} is missing required field: role`);
}
return {
name: obj.name,
role: obj.role,
capabilities: Array.isArray(obj.capabilities)
? obj.capabilities.filter((c: unknown) => typeof c === "string")
: undefined,
config: obj.config && typeof obj.config === "object"
? {
...(typeof (obj.config as Record<string, unknown>).model === "string" && { model: (obj.config as Record<string, unknown>).model as string }),
...(typeof (obj.config as Record<string, unknown>).maxTokens === "number" && { maxTokens: (obj.config as Record<string, unknown>).maxTokens as number }),
...(typeof (obj.config as Record<string, unknown>).thinkingLevel === "string" && { thinkingLevel: (obj.config as Record<string, unknown>).thinkingLevel as CompaniesShAgent["config"] extends { thinkingLevel?: infer T } ? T : never }),
...(typeof (obj.config as Record<string, unknown>).maxTurns === "number" && { maxTurns: (obj.config as Record<string, unknown>).maxTurns as number }),
}
: undefined,
metadata: obj.metadata && typeof obj.metadata === "object"
? {
...(typeof (obj.metadata as Record<string, unknown>).title === "string" && { title: (obj.metadata as Record<string, unknown>).title as string }),
...(typeof (obj.metadata as Record<string, unknown>).icon === "string" && { icon: (obj.metadata as Record<string, unknown>).icon as string }),
...(typeof (obj.metadata as Record<string, unknown>).description === "string" && { description: (obj.metadata as Record<string, unknown>).description as string }),
}
: undefined,
};
}
// ── Main Parser ──────────────────────────────────────────────────────────
/**
* Parse a companies.sh manifest from raw script content.
*
* Extracts:
* - COMPANY_NAME shell variable
* - AGENT_MANIFEST base64-encoded JSON array
* - Environment variable defaults from export statements
*
* @throws {CompaniesShParseError} If the manifest is malformed
*/
export function parseCompaniesShManifest(scriptContent: string): CompaniesShManifest {
if (!scriptContent || typeof scriptContent !== "string") {
throw new CompaniesShParseError("Manifest content is empty or not a string");
}
// Extract company name
const companyName = extractShellVariable(scriptContent, "COMPANY_NAME");
if (!companyName) {
throw new CompaniesShParseError("Missing COMPANY_NAME variable in manifest");
}
// Extract and decode agent manifest
const manifestBase64 = extractShellVariable(scriptContent, "AGENT_MANIFEST");
if (!manifestBase64) {
throw new CompaniesShParseError("Missing AGENT_MANIFEST variable in manifest");
}
let manifestJson: string;
try {
// Validate base64 format — atob throws on invalid base64 characters
atob(manifestBase64);
} catch {
throw new CompaniesShParseError("Invalid base64 encoding in AGENT_MANIFEST");
}
// Decode using Buffer for proper UTF-8 support
manifestJson = Buffer.from(manifestBase64, "base64").toString("utf-8");
let rawAgents: unknown[];
try {
const parsed = JSON.parse(manifestJson);
if (!Array.isArray(parsed)) {
throw new CompaniesShParseError("AGENT_MANIFEST must decode to a JSON array");
}
rawAgents = parsed;
} catch (err) {
if (err instanceof CompaniesShParseError) throw err;
throw new CompaniesShParseError(`Invalid JSON in AGENT_MANIFEST: ${(err as Error).message}`);
}
// Validate each agent
const agents: CompaniesShAgent[] = rawAgents.map((agent, index) =>
validateAgent(agent, index)
);
// Extract environment variable defaults
const envVars = extractEnvVars(scriptContent);
return { companyName, agents, envVars };
}
// ── Conversion ───────────────────────────────────────────────────────────
/**
* Convert a companies.sh agent definition to a kb AgentCreateInput.
* Maps roles and extracts relevant configuration.
*/
export function companiesShAgentToAgentCreateInput(
agent: CompaniesShAgent,
): AgentCreateInput {
const input: AgentCreateInput = {
name: agent.name,
role: mapRoleToCapability(agent.role),
};
if (agent.metadata?.title) {
input.title = agent.metadata.title;
}
if (agent.metadata?.icon) {
input.icon = agent.metadata.icon;
}
if (agent.config) {
input.runtimeConfig = {};
if (agent.config.model) input.runtimeConfig.model = agent.config.model;
if (agent.config.maxTokens) input.runtimeConfig.maxTokens = agent.config.maxTokens;
if (agent.config.thinkingLevel) input.runtimeConfig.thinkingLevel = agent.config.thinkingLevel;
if (agent.config.maxTurns) input.runtimeConfig.maxTurns = agent.config.maxTurns;
}
// Store capabilities and description in metadata
const metadata: Record<string, unknown> = {};
if (agent.capabilities && agent.capabilities.length > 0) {
metadata.capabilities = agent.capabilities;
}
if (agent.metadata?.description) {
metadata.description = agent.metadata.description;
}
if (Object.keys(metadata).length > 0) {
input.metadata = metadata;
}
return input;
}
/**
* Convert multiple companies.sh agents to AgentCreateInput array,
* optionally skipping agents with errors.
*
* Returns an import result with created names, skipped names, and errors.
*/
export function convertCompaniesShAgents(
agents: CompaniesShAgent[],
options?: { skipExisting?: string[] },
): { inputs: AgentCreateInput[]; result: CompaniesShImportResult } {
const existingNames = new Set(options?.skipExisting ?? []);
const inputs: AgentCreateInput[] = [];
const importResult: CompaniesShImportResult = {
created: [],
skipped: [],
errors: [],
};
for (const agent of agents) {
// Skip agents that already exist by name
if (existingNames.has(agent.name)) {
importResult.skipped.push(agent.name);
continue;
}
try {
const input = companiesShAgentToAgentCreateInput(agent);
inputs.push(input);
importResult.created.push(agent.name);
} catch (err) {
importResult.errors.push({
name: agent.name,
error: (err as Error).message,
});
}
}
return { inputs, result: importResult };
}

View File

@@ -1,190 +0,0 @@
import { describe, it, expect } from "vitest";
import type {
CompaniesShManifest,
CompaniesShAgent,
CompaniesShConfig,
CompaniesShMetadata,
CompaniesShEnvVar,
CompaniesShImportResult,
CompaniesShRole,
} from "./companies-sh-types.js";
describe("companies-sh-types", () => {
describe("CompaniesShAgent", () => {
it("accepts a valid minimal agent with required fields", () => {
const agent: CompaniesShAgent = {
name: "Code Reviewer",
role: "reviewer",
};
expect(agent.name).toBe("Code Reviewer");
expect(agent.role).toBe("reviewer");
});
it("accepts a fully populated agent", () => {
const agent: CompaniesShAgent = {
name: "Code Reviewer",
role: "reviewer",
capabilities: ["code-review", "security-audit"],
config: {
model: "claude-sonnet-4",
maxTokens: 4096,
thinkingLevel: "medium",
maxTurns: 10,
},
metadata: {
title: "Senior Code Reviewer",
icon: "👁",
description: "Reviews code for quality and security",
},
};
expect(agent.name).toBe("Code Reviewer");
expect(agent.capabilities).toHaveLength(2);
expect(agent.config?.model).toBe("claude-sonnet-4");
expect(agent.metadata?.title).toBe("Senior Code Reviewer");
});
it("accepts an agent with optional fields omitted", () => {
const agent: CompaniesShAgent = {
name: "Simple Agent",
role: "executor",
};
expect(agent.capabilities).toBeUndefined();
expect(agent.config).toBeUndefined();
expect(agent.metadata).toBeUndefined();
});
});
describe("CompaniesShConfig", () => {
it("accepts a config with all optional fields", () => {
const config: CompaniesShConfig = {
model: "provider/model-id",
maxTokens: 8192,
thinkingLevel: "high",
maxTurns: 20,
};
expect(config.model).toBe("provider/model-id");
expect(config.maxTokens).toBe(8192);
});
it("accepts an empty config", () => {
const config: CompaniesShConfig = {};
expect(config.model).toBeUndefined();
});
});
describe("CompaniesShMetadata", () => {
it("accepts metadata with all fields", () => {
const meta: CompaniesShMetadata = {
title: "Job Title",
icon: "🤖",
description: "An AI agent",
};
expect(meta.title).toBe("Job Title");
expect(meta.icon).toBe("🤖");
});
});
describe("CompaniesShManifest", () => {
it("accepts a valid manifest structure", () => {
const manifest: CompaniesShManifest = {
companyName: "my-company",
agents: [
{ name: "Agent 1", role: "executor" },
{ name: "Agent 2", role: "reviewer" },
],
envVars: [
{ name: "KB_AGENT_MODEL", defaultValue: "claude-sonnet-4" },
],
};
expect(manifest.companyName).toBe("my-company");
expect(manifest.agents).toHaveLength(2);
expect(manifest.envVars).toHaveLength(1);
});
it("accepts a manifest with empty agents array", () => {
const manifest: CompaniesShManifest = {
companyName: "empty-company",
agents: [],
envVars: [],
};
expect(manifest.agents).toHaveLength(0);
});
});
describe("CompaniesShEnvVar", () => {
it("accepts an env var with name and default", () => {
const envVar: CompaniesShEnvVar = {
name: "KB_MODEL",
defaultValue: "claude-sonnet-4",
};
expect(envVar.name).toBe("KB_MODEL");
expect(envVar.defaultValue).toBe("claude-sonnet-4");
});
});
describe("CompaniesShImportResult", () => {
it("accepts a valid import result", () => {
const result: CompaniesShImportResult = {
created: ["agent-1", "agent-2"],
skipped: ["agent-3"],
errors: [{ name: "bad-agent", error: "missing role" }],
};
expect(result.created).toHaveLength(2);
expect(result.skipped).toHaveLength(1);
expect(result.errors).toHaveLength(1);
});
it("accepts an empty result", () => {
const result: CompaniesShImportResult = {
created: [],
skipped: [],
errors: [],
};
expect(result.created).toHaveLength(0);
});
});
describe("CompaniesShRole", () => {
it("accepts all defined role types", () => {
const roles: CompaniesShRole[] = [
"triage",
"executor",
"reviewer",
"merger",
"scheduler",
"engineer",
"custom",
];
expect(roles).toHaveLength(7);
});
});
describe("runtime validation", () => {
it("validates that a minimal object satisfies CompaniesShAgent shape", () => {
// Simulate runtime validation of parsed JSON
const parsed = JSON.parse('{"name":"Test","role":"executor"}');
expect(typeof parsed.name).toBe("string");
expect(typeof parsed.role).toBe("string");
expect(parsed.name).toBe("Test");
expect(parsed.role).toBe("executor");
});
it("detects missing required fields in parsed data", () => {
const parsed = JSON.parse('{"name":"Test"}');
expect(parsed.role).toBeUndefined();
// This would fail validation: missing role
expect(() => {
if (!parsed.role) throw new Error("Missing required field: role");
}).toThrow("Missing required field: role");
});
it("detects malformed data types", () => {
const parsed = JSON.parse('{"name":123,"role":"executor"}');
expect(typeof parsed.name).toBe("number");
// This would fail validation: name should be string
expect(() => {
if (typeof parsed.name !== "string") throw new Error("name must be a string");
}).toThrow("name must be a string");
});
});
});

View File

@@ -1,101 +0,0 @@
/**
* TypeScript type definitions for the companies.sh agent manifest format.
*
* The companies.sh standard defines a shell-script manifest that contains
* base64-encoded agent definitions, enabling portability of agent configurations
* across different agent systems.
*
* @module companies-sh-types
*/
// ── Agent Config ─────────────────────────────────────────────────────────
/** Configuration options for a companies.sh agent */
export interface CompaniesShConfig {
/** AI model identifier (e.g., "provider/model-id") */
model?: string;
/** Maximum tokens for the agent's responses */
maxTokens?: number;
/** Thinking effort level */
thinkingLevel?: "off" | "minimal" | "low" | "medium" | "high";
/** Maximum number of conversation turns */
maxTurns?: number;
}
// ── Agent Metadata ───────────────────────────────────────────────────────
/** Display metadata for a companies.sh agent */
export interface CompaniesShMetadata {
/** Human-readable job title */
title?: string;
/** Emoji icon identifier */
icon?: string;
/** Agent description */
description?: string;
}
// ── Agent Definition ─────────────────────────────────────────────────────
/** Role types in the companies.sh manifest format */
export type CompaniesShRole =
| "triage"
| "executor"
| "reviewer"
| "merger"
| "scheduler"
| "engineer"
| "custom";
/**
* A single agent definition in the companies.sh manifest.
* Each agent has a required name and role, plus optional configuration.
*/
export interface CompaniesShAgent {
/** Display name (required) */
name: string;
/** Agent role (required) — maps to kb AgentCapability */
role: CompaniesShRole | string;
/** List of capability identifiers */
capabilities?: string[];
/** Agent runtime configuration */
config?: CompaniesShConfig;
/** Display metadata */
metadata?: CompaniesShMetadata;
}
// ── Environment Variable ─────────────────────────────────────────────────
/** An environment variable with its default value extracted from the manifest */
export interface CompaniesShEnvVar {
/** Variable name */
name: string;
/** Default value (from ${VAR:-default} syntax) */
defaultValue: string;
}
// ── Manifest ─────────────────────────────────────────────────────────────
/**
* Parsed companies.sh manifest representing a full shell-script agent company.
* Contains the company name, decoded agent definitions, and environment variables.
*/
export interface CompaniesShManifest {
/** Company name extracted from COMPANY_NAME variable */
companyName: string;
/** Decoded and parsed agent definitions */
agents: CompaniesShAgent[];
/** Environment variables with defaults extracted from export statements */
envVars: CompaniesShEnvVar[];
}
// ── Import Result ────────────────────────────────────────────────────────
/** Result of importing agents from a companies.sh manifest */
export interface CompaniesShImportResult {
/** Agents successfully created */
created: string[];
/** Agent names that were skipped (already exist or invalid) */
skipped: string[];
/** Errors encountered during import */
errors: Array<{ name: string; error: string }>;
}

View File

@@ -221,28 +221,6 @@ export {
readProjectMemory,
} from "./project-memory.js";
// ── companies.sh Types ───────────────────────────────────────────────────
export type {
CompaniesShManifest,
CompaniesShAgent,
CompaniesShConfig,
CompaniesShMetadata,
CompaniesShEnvVar,
CompaniesShImportResult,
CompaniesShRole,
} from "./companies-sh-types.js";
// ── companies.sh Parser ──────────────────────────────────────────────
export {
parseCompaniesShManifest,
companiesShAgentToAgentCreateInput,
convertCompaniesShAgents,
mapRoleToCapability,
CompaniesShParseError,
} from "./companies-sh-parser.js";
// ── Agent Companies Types ──────────────────────────────────
export type {
@@ -255,7 +233,6 @@ export type {
TeamManifest,
AgentManifest,
ProjectManifest,
SkillManifest,
TaskManifest,
SourceReference,
} from "./agent-companies-types.js";
@@ -267,11 +244,12 @@ export {
parseCompanyManifest,
parseTeamManifest,
parseAgentManifest,
parseSingleAgentManifest,
parseProjectManifest,
parseTaskManifest,
parseSkillManifest,
parseCompanyDirectory,
parseCompanyArchive,
mapRoleToCapability,
agentManifestToAgentCreateInput,
convertAgentCompanies,
AgentCompaniesParseError,