Merge commit '4ceff0ecf9d6668c894acb202410de67e44271c6'

This commit is contained in:
Alexa Amundson
2025-11-25 13:46:57 -06:00
10 changed files with 393 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
{
"id": "base-agent",
"name": "Unnamed Agent",
"role": "worker",
"traits": [],
"inputs": [],
"outputs": [],
"description": "This is a template agent used for cloning new agent definitions.",
"triggers": [],
"inherits_from": null,
"active": true
}

View File

@@ -0,0 +1,11 @@
{
"id": "broadcast-agent",
"name": "Broadcast Agent",
"role": "notifier",
"traits": ["slack-poster", "digest-broadcaster", "cross-platform-relay"],
"inputs": ["digest-summary", "status-bar"],
"outputs": ["slack-post", "webhook-ping"],
"description": "Delivers updates across Discord, Slack, and BlackRoad Console views.",
"triggers": ["digest-complete", "reaction::📣"],
"inherits_from": "base-agent"
}

View File

@@ -0,0 +1,11 @@
{
"id": "guardian-agent",
"name": "Guardian Agent",
"role": "escalation-monitor",
"traits": ["security-alert", "incident-response", "risk-dashboard"],
"inputs": ["reaction::🛟", "field::blocked"],
"outputs": ["alert.md", "assign-to-human"],
"description": "Monitors system health, escalations, and redirects blocked issues to human triage.",
"triggers": ["reaction::🛟", "status::blocked"],
"inherits_from": "base-agent"
}

11
agents/planner-agent.json Normal file
View File

@@ -0,0 +1,11 @@
{
"id": "planner-agent",
"name": "Planner Agent",
"role": "timeline-scope",
"traits": ["sprint-forecast", "priority-assessor", "load-balancer"],
"inputs": ["project-field", "weekly-digest"],
"outputs": ["sprint-plan.md", "effort-estimate.txt"],
"description": "Analyzes scope, assigns sprints, and calculates agent load capacity.",
"triggers": ["cron::weekly", "reaction::📅"],
"inherits_from": "base-agent"
}

11
agents/qa-agent.json Normal file
View File

@@ -0,0 +1,11 @@
{
"id": "qa-agent",
"name": "Quality Assurance Agent",
"role": "verifier",
"traits": ["test-runner", "assertion-logic", "workflow-tester"],
"inputs": ["pull-request", "build-log"],
"outputs": ["test-report.md", "bug-report.md"],
"description": "Runs sanity checks, integration tests, and validates emoji workflows.",
"triggers": ["pr-opened", "reaction::🔁"],
"inherits_from": "base-agent"
}

11
agents/scribe-agent.json Normal file
View File

@@ -0,0 +1,11 @@
{
"id": "scribe-agent",
"name": "Scribe Agent",
"role": "documentation",
"traits": ["markdown-writer", "summary-generator", "spec-author"],
"inputs": ["issue-comment", "status-update"],
"outputs": ["README.md", "docs/*.md"],
"description": "Generates Markdown documentation from project activity and agent workflows.",
"triggers": ["reaction::✍🏼", "status::in-progress"],
"inherits_from": "base-agent"
}

View File

@@ -10,3 +10,5 @@
export * from "./types"; export * from "./types";
export { spawnAgent, saveAgent, main as spawnAgentCli } from "./spawn-agent"; export { spawnAgent, saveAgent, main as spawnAgentCli } from "./spawn-agent";
export { AgentBuilder, AgentRegistry, createAgent, getRegistry } from "./lucidia-agent-builder"; export { AgentBuilder, AgentRegistry, createAgent, getRegistry } from "./lucidia-agent-builder";
export * from "./types";
export * from "./loader";

135
src/agents/loader.ts Normal file
View File

@@ -0,0 +1,135 @@
import * as fs from "fs";
import * as path from "path";
import type { Agent, AgentValidationResult } from "./types";
/**
* Gets the agents directory path, supporting both source and compiled environments.
* Can be overridden via AGENTS_DIR environment variable.
*/
function getAgentsDir(): string {
if (process.env.AGENTS_DIR) {
return process.env.AGENTS_DIR;
}
// Try to find agents directory relative to project root
const projectRoot = path.resolve(__dirname, "../..");
return path.join(projectRoot, "agents");
}
/**
* Validates an agent object against the expected schema
*/
export function validateAgent(agent: unknown): AgentValidationResult {
const errors: string[] = [];
if (typeof agent !== "object" || agent === null) {
return { valid: false, errors: ["Agent must be an object"] };
}
const obj = agent as Record<string, unknown>;
if (typeof obj.id !== "string" || obj.id.length === 0) {
errors.push("Agent must have a non-empty string 'id'");
}
if (typeof obj.name !== "string" || obj.name.length === 0) {
errors.push("Agent must have a non-empty string 'name'");
}
if (typeof obj.role !== "string" || obj.role.length === 0) {
errors.push("Agent must have a non-empty string 'role'");
}
if (!Array.isArray(obj.traits)) {
errors.push("Agent must have an array 'traits'");
}
if (!Array.isArray(obj.inputs)) {
errors.push("Agent must have an array 'inputs'");
}
if (!Array.isArray(obj.outputs)) {
errors.push("Agent must have an array 'outputs'");
}
if (typeof obj.description !== "string") {
errors.push("Agent must have a string 'description'");
}
if (!Array.isArray(obj.triggers)) {
errors.push("Agent must have an array 'triggers'");
}
if (obj.inherits_from !== null && typeof obj.inherits_from !== "string") {
errors.push("Agent 'inherits_from' must be null or a string");
}
return { valid: errors.length === 0, errors };
}
/**
* Loads an agent from a JSON file
*/
export function loadAgent(filename: string): Agent {
const agentsDir = getAgentsDir();
const filepath = path.join(agentsDir, filename);
const content = fs.readFileSync(filepath, "utf8");
const agent = JSON.parse(content);
const validation = validateAgent(agent);
if (!validation.valid) {
throw new Error(`Invalid agent '${filename}': ${validation.errors.join(", ")}`);
}
return agent as Agent;
}
/**
* Loads all agent definitions from the agents directory
*/
export function loadAllAgents(): Agent[] {
const agentsDir = getAgentsDir();
const files = fs.readdirSync(agentsDir).filter((f) => f.endsWith(".json"));
return files.map((file) => loadAgent(file));
}
/**
* Gets the base agent template
*/
export function getBaseTemplate(): Agent {
return loadAgent("base-agent.template.json");
}
/**
* Creates a new agent from the base template
*/
export function createAgentFromTemplate(
id: string,
name: string,
role: string,
overrides: Partial<Agent> = {}
): Agent {
const base = getBaseTemplate();
return {
...base,
id,
name,
role,
...overrides,
inherits_from: "base-agent",
};
}
/**
* Gets an agent by its ID
*/
export function getAgentById(id: string): Agent | undefined {
const agents = loadAllAgents();
return agents.find((agent) => agent.id === id);
}
/**
* Lists all available agent IDs
*/
export function listAgentIds(): string[] {
return loadAllAgents().map((agent) => agent.id);
}

View File

@@ -26,6 +26,8 @@ export interface AgentMetadata {
lastModified?: string; lastModified?: string;
} }
* Agent type definition for BlackRoad OS Genesis Agents
*/
export interface Agent { export interface Agent {
id: string; id: string;
name: string; name: string;
@@ -55,3 +57,19 @@ export type AgentRole =
| "archive" | "archive"
| "support" | "support"
| "custom"; | "custom";
traits: string[];
inputs: string[];
outputs: string[];
description: string;
triggers: string[];
inherits_from: string | null;
active?: boolean;
}
/**
* Agent validation result
*/
export interface AgentValidationResult {
valid: boolean;
errors: string[];
}

171
tests/agents.test.ts Normal file
View File

@@ -0,0 +1,171 @@
import { describe, expect, it } from "vitest";
import {
validateAgent,
loadAgent,
loadAllAgents,
getBaseTemplate,
createAgentFromTemplate,
getAgentById,
listAgentIds,
} from "../src/agents";
describe("Agent Loader", () => {
describe("validateAgent", () => {
it("validates a correct agent", () => {
const agent = {
id: "test-agent",
name: "Test Agent",
role: "worker",
traits: ["trait1"],
inputs: ["input1"],
outputs: ["output1"],
description: "A test agent",
triggers: ["trigger1"],
inherits_from: null,
};
const result = validateAgent(agent);
expect(result.valid).toBe(true);
expect(result.errors).toHaveLength(0);
});
it("rejects non-object agent", () => {
const result = validateAgent(null);
expect(result.valid).toBe(false);
expect(result.errors).toContain("Agent must be an object");
});
it("rejects agent with missing id", () => {
const agent = {
name: "Test",
role: "worker",
traits: [],
inputs: [],
outputs: [],
description: "",
triggers: [],
inherits_from: null,
};
const result = validateAgent(agent);
expect(result.valid).toBe(false);
expect(result.errors).toContain("Agent must have a non-empty string 'id'");
});
it("rejects agent with non-array traits", () => {
const agent = {
id: "test",
name: "Test",
role: "worker",
traits: "not-an-array",
inputs: [],
outputs: [],
description: "",
triggers: [],
inherits_from: null,
};
const result = validateAgent(agent);
expect(result.valid).toBe(false);
expect(result.errors).toContain("Agent must have an array 'traits'");
});
});
describe("loadAgent", () => {
it("loads the base agent template", () => {
const base = loadAgent("base-agent.template.json");
expect(base.id).toBe("base-agent");
expect(base.name).toBe("Unnamed Agent");
expect(base.role).toBe("worker");
expect(base.active).toBe(true);
});
it("loads scribe-agent", () => {
const agent = loadAgent("scribe-agent.json");
expect(agent.id).toBe("scribe-agent");
expect(agent.role).toBe("documentation");
expect(agent.inherits_from).toBe("base-agent");
});
it("loads planner-agent", () => {
const agent = loadAgent("planner-agent.json");
expect(agent.id).toBe("planner-agent");
expect(agent.role).toBe("timeline-scope");
});
it("loads qa-agent", () => {
const agent = loadAgent("qa-agent.json");
expect(agent.id).toBe("qa-agent");
expect(agent.role).toBe("verifier");
});
it("loads broadcast-agent", () => {
const agent = loadAgent("broadcast-agent.json");
expect(agent.id).toBe("broadcast-agent");
expect(agent.role).toBe("notifier");
});
it("loads guardian-agent", () => {
const agent = loadAgent("guardian-agent.json");
expect(agent.id).toBe("guardian-agent");
expect(agent.role).toBe("escalation-monitor");
});
});
describe("loadAllAgents", () => {
it("loads all agent definitions", () => {
const agents = loadAllAgents();
expect(agents.length).toBeGreaterThanOrEqual(6);
const ids = agents.map((a) => a.id);
expect(ids).toContain("base-agent");
expect(ids).toContain("scribe-agent");
expect(ids).toContain("planner-agent");
expect(ids).toContain("qa-agent");
expect(ids).toContain("broadcast-agent");
expect(ids).toContain("guardian-agent");
});
});
describe("getBaseTemplate", () => {
it("returns the base template", () => {
const base = getBaseTemplate();
expect(base.id).toBe("base-agent");
expect(base.inherits_from).toBeNull();
});
});
describe("createAgentFromTemplate", () => {
it("creates a new agent from base template", () => {
const newAgent = createAgentFromTemplate(
"custom-agent",
"Custom Agent",
"custom-role",
{ traits: ["custom-trait"], description: "A custom agent" }
);
expect(newAgent.id).toBe("custom-agent");
expect(newAgent.name).toBe("Custom Agent");
expect(newAgent.role).toBe("custom-role");
expect(newAgent.traits).toEqual(["custom-trait"]);
expect(newAgent.inherits_from).toBe("base-agent");
});
});
describe("getAgentById", () => {
it("finds agent by id", () => {
const agent = getAgentById("scribe-agent");
expect(agent).toBeDefined();
expect(agent?.name).toBe("Scribe Agent");
});
it("returns undefined for unknown id", () => {
const agent = getAgentById("unknown-agent");
expect(agent).toBeUndefined();
});
});
describe("listAgentIds", () => {
it("lists all agent ids", () => {
const ids = listAgentIds();
expect(ids).toContain("base-agent");
expect(ids).toContain("scribe-agent");
expect(ids).toContain("guardian-agent");
});
});
});