Merge branch origin/codex/run-all-tests into main
This commit is contained in:
@@ -1,103 +1,12 @@
|
|||||||
{
|
{
|
||||||
"id": "guardian-clone-vault",
|
"id": "guardian-clone-vault",
|
||||||
|
"name": "Guardian Clone Vault",
|
||||||
"role": "sentinel",
|
"role": "sentinel",
|
||||||
"traits": ["incident-response", "triage", "overflow"],
|
"traits": ["incident-response", "triage", "overflow"],
|
||||||
"ttl": "96h",
|
"inputs": [],
|
||||||
|
"outputs": ["escalation_resolved", "priority_updated", "agent_assigned"],
|
||||||
|
"description": "Temporary overflow clone of guardian-agent to absorb burst escalations.",
|
||||||
|
"triggers": ["escalation_created", "high_priority_issue", "sla_breach"],
|
||||||
"inherits_from": "guardian-agent",
|
"inherits_from": "guardian-agent",
|
||||||
"spawned_by": "lucidia.spawn-runner.js",
|
"active": true
|
||||||
"spawn_reason": "escalation-overflow",
|
|
||||||
"metadata": {
|
|
||||||
"created_at": "2025-11-24T23:11:14.529Z",
|
|
||||||
"escalations_logged": 18,
|
|
||||||
"parent_load_capacity": "85%",
|
|
||||||
"auto_deactivate": true
|
|
||||||
}
|
|
||||||
"name": "guardian-clone-vault",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"role": "sentinel",
|
|
||||||
"inherits_from": "guardian-agent",
|
|
||||||
"ttl": "96h",
|
|
||||||
"description": "Temporary overflow clone of guardian-agent to absorb burst escalations",
|
|
||||||
"created_at": "2025-11-24T23:10:38.453Z",
|
|
||||||
"created_by": "lucidia",
|
|
||||||
"capabilities": [
|
|
||||||
"monitor_escalations",
|
|
||||||
"auto_triage",
|
|
||||||
"priority_assignment",
|
|
||||||
"alert_routing"
|
|
||||||
],
|
|
||||||
"triggers": [
|
|
||||||
"escalation_created",
|
|
||||||
"high_priority_issue",
|
|
||||||
"sla_breach"
|
|
||||||
],
|
|
||||||
"outputs": [
|
|
||||||
"escalation_resolved",
|
|
||||||
"priority_updated",
|
|
||||||
"agent_assigned"
|
|
||||||
]
|
|
||||||
"name": "guardian-clone-vault",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Self-spawned agent responsible for cloning and vaulting guardian configurations, maintaining secure backups and version control of guardian states.",
|
|
||||||
"type": "autonomous",
|
|
||||||
"parent": "lucidia",
|
|
||||||
"capabilities": [
|
|
||||||
"clone-guardian-configs",
|
|
||||||
"vault-state-snapshots",
|
|
||||||
"restore-guardian-state",
|
|
||||||
"verify-integrity",
|
|
||||||
"sync-configurations"
|
|
||||||
],
|
|
||||||
"triggers": {
|
|
||||||
"events": [
|
|
||||||
"guardian.config.updated",
|
|
||||||
"guardian.state.changed",
|
|
||||||
"vault.backup.scheduled",
|
|
||||||
"restore.requested"
|
|
||||||
],
|
|
||||||
"schedule": "0 */6 * * *"
|
|
||||||
},
|
|
||||||
"permissions": {
|
|
||||||
"read": [
|
|
||||||
"agents/*",
|
|
||||||
"configs/*"
|
|
||||||
],
|
|
||||||
"write": [
|
|
||||||
"vault/*",
|
|
||||||
"backups/*"
|
|
||||||
],
|
|
||||||
"execute": [
|
|
||||||
"clone",
|
|
||||||
"backup",
|
|
||||||
"restore",
|
|
||||||
"verify"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"configuration": {
|
|
||||||
"vault_path": "./vault/guardian-clones",
|
|
||||||
"max_snapshots": 10,
|
|
||||||
"compression": true,
|
|
||||||
"encryption": true,
|
|
||||||
"integrity_check_interval": "1h"
|
|
||||||
},
|
|
||||||
"dependencies": [
|
|
||||||
"guardian-agent"
|
|
||||||
],
|
|
||||||
"metadata": {
|
|
||||||
"spawned_by": "lucidia",
|
|
||||||
"spawn_date": "2025-11-24",
|
|
||||||
"status": "active"
|
|
||||||
"inheritsFrom": "guardian-agent",
|
|
||||||
"metadata": {
|
|
||||||
"createdBy": "lucidia.spawn-runner.js",
|
|
||||||
"reason": "Auto-spawned to support main guardian-agent during short-term escalation storm",
|
|
||||||
"escalationsLogged": 18,
|
|
||||||
"escalationPeriod": "72h",
|
|
||||||
"parentLoadCapacity": "85%"
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"autoDeactivate": true,
|
|
||||||
"deactivateAfter": "96h",
|
|
||||||
"manualExtendable": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
2597
package-lock.json
generated
2597
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
85
package.json
85
package.json
@@ -1,53 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "blackroad-os",
|
"name": "blackroad-os",
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "BlackRoad OS - A microservice infrastructure management platform",
|
|
||||||
"main": "dist/index.js",
|
|
||||||
"scripts": {
|
|
||||||
"test": "vitest",
|
|
||||||
"build": "tsc",
|
|
||||||
"start": "node dist/index.js",
|
|
||||||
"dev": "ts-node src/index.ts"
|
|
||||||
"spawn-agent": "tsx src/agents/spawn-agent.ts"
|
|
||||||
"spawn-agent": "node scripts/spawn-agent.js"
|
|
||||||
},
|
|
||||||
"keywords": [],
|
|
||||||
"author": "",
|
|
||||||
"license": "ISC",
|
|
||||||
"type": "commonjs",
|
|
||||||
"dependencies": {
|
|
||||||
"@actions/core": "^1.11.1",
|
|
||||||
"@actions/github": "^6.0.1",
|
|
||||||
"@octokit/rest": "^21.1.1",
|
|
||||||
"bullmq": "^5.64.1",
|
|
||||||
"express": "^5.1.0",
|
|
||||||
"fastify": "^5.6.2",
|
|
||||||
"ioredis": "^5.8.2",
|
|
||||||
"js-yaml": "^4.1.0",
|
|
||||||
"js-yaml": "^4.1.1",
|
|
||||||
"node-cron": "^4.2.1",
|
|
||||||
"react": "^19.2.0",
|
|
||||||
"react-dom": "^19.2.0",
|
|
||||||
"yaml": "^2.8.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@testing-library/jest-dom": "^6.9.1",
|
|
||||||
"@testing-library/react": "^16.3.0",
|
|
||||||
"@types/express": "^5.0.5",
|
|
||||||
"@types/node": "^24.10.1",
|
|
||||||
"@types/react": "^19.2.6",
|
|
||||||
"@types/react-dom": "^19.2.3",
|
|
||||||
"@types/supertest": "^6.0.3",
|
|
||||||
"@vitejs/plugin-react": "^5.1.1",
|
|
||||||
"jsdom": "^27.2.0",
|
|
||||||
"supertest": "^7.1.4",
|
|
||||||
"ts-node": "^10.9.2",
|
|
||||||
"tsx": "^4.20.6",
|
|
||||||
"typescript": "^5.9.3",
|
|
||||||
"vitest": "^4.0.13"
|
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"license": "MIT",
|
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"bin": {
|
"bin": {
|
||||||
"br-orchestrate": "dist/src/cli.js"
|
"br-orchestrate": "dist/src/cli.js"
|
||||||
@@ -63,25 +18,49 @@
|
|||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"postbuild": "tsx scripts/postbuild.ts",
|
"postbuild": "tsx scripts/postbuild.ts",
|
||||||
"br-orchestrate": "tsx src/cli.ts"
|
"br-orchestrate": "tsx src/cli.ts",
|
||||||
|
"start": "node dist/index.js",
|
||||||
|
"dev": "ts-node src/index.ts",
|
||||||
|
"spawn-agent": "node scripts/spawn-agent.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@actions/core": "^1.11.1",
|
||||||
|
"@actions/github": "^6.0.1",
|
||||||
|
"@octokit/rest": "^21.1.1",
|
||||||
|
"bullmq": "^5.64.1",
|
||||||
|
"express": "^5.1.0",
|
||||||
|
"fastify": "^5.6.2",
|
||||||
|
"ioredis": "^5.8.2",
|
||||||
|
"js-yaml": "^4.1.1",
|
||||||
|
"node-cron": "^4.2.1",
|
||||||
|
"react": "^19.2.0",
|
||||||
|
"react-dom": "^19.2.0",
|
||||||
|
"yaml": "^2.8.1"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
|
"@testing-library/react": "^16.3.0",
|
||||||
|
"@types/express": "^5.0.5",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
|
"@types/node": "^24.10.1",
|
||||||
|
"@types/react": "^19.2.6",
|
||||||
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"@types/supertest": "^6.0.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||||
"@typescript-eslint/parser": "^7.0.0",
|
"@typescript-eslint/parser": "^7.0.0",
|
||||||
|
"@vitejs/plugin-react": "^5.1.1",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
"js-yaml": "^4.1.0",
|
"jsdom": "^27.2.0",
|
||||||
"prettier": "^3.1.0",
|
"prettier": "^3.1.0",
|
||||||
|
"supertest": "^7.1.4",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tsx": "^4.19.2",
|
"tsx": "^4.20.6",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.9.3",
|
||||||
"vitest": "^1.0.4",
|
"vitest": "^4.0.13",
|
||||||
"yaml-lint": "^1.2.4",
|
"yaml-lint": "^1.2.4",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
"vitest": "^4.0.13",
|
|
||||||
"yaml": "^2.8.1"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"agent": "Orchestrator-Gen-0",
|
"agent": "Orchestrator-Gen-0",
|
||||||
"ts": 1718217600000
|
"ts": 1764504396933
|
||||||
}
|
}
|
||||||
@@ -1,377 +1,66 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
/**
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
* spawn-agent CLI Tool
|
const __dirname = path.dirname(__filename);
|
||||||
*
|
|
||||||
* Creates a new agent with full spec, prompt, workflow, and docs.
|
|
||||||
*
|
|
||||||
* Usage: node scripts/spawn-agent.js <agent-id>
|
|
||||||
* Example: pnpm spawn-agent scribe-support
|
|
||||||
*/
|
|
||||||
|
|
||||||
const fs = require('fs');
|
const AGENTS_DIR = path.join(__dirname, "..", "agents");
|
||||||
const path = require('path');
|
const WORKFLOWS_DIR = path.join(__dirname, "..", ".github", "workflows");
|
||||||
|
const DOCS_DIR = path.join(__dirname, "..", "docs", "agents");
|
||||||
|
|
||||||
// Configuration
|
function toId(name) {
|
||||||
const TEMPLATES_DIR = path.join(__dirname, '..', 'templates');
|
return name.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
|
||||||
const AGENTS_DIR = path.join(__dirname, '..', 'agents');
|
}
|
||||||
const WORKFLOWS_DIR = path.join(__dirname, '..', '.github', 'workflows');
|
|
||||||
const DOCS_DIR = path.join(__dirname, '..', 'docs', 'agents');
|
|
||||||
|
|
||||||
/**
|
function toTitle(id) {
|
||||||
* Convert agent ID to human-readable name
|
|
||||||
* e.g., "scribe-support" -> "Scribe Support"
|
|
||||||
*/
|
|
||||||
function idToName(id) {
|
|
||||||
return id
|
return id
|
||||||
.split('-')
|
.split("-")
|
||||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
||||||
.join(' ');
|
.join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
const rawName = process.argv[2];
|
||||||
* Generate role based on agent naming conventions
|
|
||||||
*/
|
|
||||||
function inferRole(id) {
|
|
||||||
const parts = id.toLowerCase().split('-');
|
|
||||||
|
|
||||||
// Common role patterns
|
if (!rawName) {
|
||||||
const roleMap = {
|
console.error("Please provide an agent name");
|
||||||
'scribe': 'Documentation and note-taking specialist',
|
|
||||||
'support': 'User assistance and support handler',
|
|
||||||
'review': 'Code review and quality assurance agent',
|
|
||||||
'reviewer': 'Code review and quality assurance agent',
|
|
||||||
'deploy': 'Deployment and release automation agent',
|
|
||||||
'monitor': 'System monitoring and alerting agent',
|
|
||||||
'test': 'Testing and validation agent',
|
|
||||||
'security': 'Security scanning and vulnerability assessment agent',
|
|
||||||
'data': 'Data processing and analysis agent',
|
|
||||||
'notify': 'Notification and communication agent',
|
|
||||||
'sync': 'Synchronization and integration agent',
|
|
||||||
'build': 'Build and compilation agent',
|
|
||||||
'clean': 'Cleanup and maintenance agent'
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const part of parts) {
|
|
||||||
if (roleMap[part]) {
|
|
||||||
return roleMap[part];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return `Specialized agent for ${idToName(id)} tasks`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate traits based on agent ID
|
|
||||||
*/
|
|
||||||
function inferTraits(id) {
|
|
||||||
const baseTraits = ['autonomous', 'reliable'];
|
|
||||||
const parts = id.toLowerCase().split('-');
|
|
||||||
|
|
||||||
const traitMap = {
|
|
||||||
'scribe': ['detailed', 'organized'],
|
|
||||||
'support': ['helpful', 'responsive'],
|
|
||||||
'review': ['thorough', 'analytical'],
|
|
||||||
'reviewer': ['thorough', 'analytical'],
|
|
||||||
'deploy': ['cautious', 'systematic'],
|
|
||||||
'monitor': ['vigilant', 'proactive'],
|
|
||||||
'test': ['meticulous', 'comprehensive'],
|
|
||||||
'security': ['vigilant', 'strict'],
|
|
||||||
'data': ['analytical', 'efficient'],
|
|
||||||
'notify': ['timely', 'clear'],
|
|
||||||
'sync': ['coordinated', 'accurate'],
|
|
||||||
'build': ['efficient', 'robust'],
|
|
||||||
'clean': ['systematic', 'thorough']
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const part of parts) {
|
|
||||||
if (traitMap[part]) {
|
|
||||||
return [...baseTraits, ...traitMap[part]];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return baseTraits;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate tags based on agent ID
|
|
||||||
*/
|
|
||||||
function inferTags(id) {
|
|
||||||
const baseTags = ['agent', 'blackroad-os'];
|
|
||||||
const parts = id.toLowerCase().split('-');
|
|
||||||
return [...baseTags, ...parts];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read template file
|
|
||||||
*/
|
|
||||||
function readTemplate(templateName) {
|
|
||||||
const templatePath = path.join(TEMPLATES_DIR, templateName);
|
|
||||||
return fs.readFileSync(templatePath, 'utf8');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Replace all placeholders in template
|
|
||||||
*/
|
|
||||||
function processTemplate(template, replacements) {
|
|
||||||
let result = template;
|
|
||||||
for (const [key, value] of Object.entries(replacements)) {
|
|
||||||
const regex = new RegExp(`{{${key}}}`, 'g');
|
|
||||||
result = result.replace(regex, value);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensure directory exists
|
|
||||||
*/
|
|
||||||
function ensureDir(dirPath) {
|
|
||||||
if (!fs.existsSync(dirPath)) {
|
|
||||||
fs.mkdirSync(dirPath, { recursive: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Main spawn agent function
|
|
||||||
*/
|
|
||||||
function spawnAgent(agentId, options = {}) {
|
|
||||||
const {
|
|
||||||
skipDocs = false,
|
|
||||||
verbose = false
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
// Validate agent ID
|
|
||||||
if (!agentId || typeof agentId !== 'string') {
|
|
||||||
throw new Error('Agent ID is required');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normalize agent ID
|
|
||||||
const normalizedId = agentId.toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
||||||
|
|
||||||
// Generate agent metadata
|
|
||||||
const agentName = idToName(normalizedId);
|
|
||||||
const agentRole = inferRole(normalizedId);
|
|
||||||
const agentTraits = inferTraits(normalizedId);
|
|
||||||
const agentTags = inferTags(normalizedId);
|
|
||||||
const createdAt = new Date().toISOString();
|
|
||||||
const description = `${agentName} agent for the BlackRoad-OS ecosystem`;
|
|
||||||
|
|
||||||
// Prepare replacements
|
|
||||||
const replacements = {
|
|
||||||
'AGENT_ID': normalizedId,
|
|
||||||
'AGENT_NAME': agentName,
|
|
||||||
'AGENT_ROLE': agentRole,
|
|
||||||
'AGENT_DESCRIPTION': description,
|
|
||||||
'AGENT_TRAITS': JSON.stringify(agentTraits),
|
|
||||||
'AGENT_TRAITS_LIST': agentTraits.map(t => `- ${t}`).join('\n'),
|
|
||||||
'AGENT_TRAITS_MDX': agentTraits.map(t => `- **${t}**`).join('\n'),
|
|
||||||
'AGENT_TAGS': JSON.stringify(agentTags),
|
|
||||||
'CREATED_AT': createdAt
|
|
||||||
};
|
|
||||||
|
|
||||||
// Ensure output directories exist
|
|
||||||
ensureDir(AGENTS_DIR);
|
|
||||||
ensureDir(WORKFLOWS_DIR);
|
|
||||||
if (!skipDocs) {
|
|
||||||
ensureDir(DOCS_DIR);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if agent already exists
|
|
||||||
const agentJsonPath = path.join(AGENTS_DIR, `${normalizedId}.agent.json`);
|
|
||||||
if (fs.existsSync(agentJsonPath)) {
|
|
||||||
throw new Error(`Agent '${normalizedId}' already exists at ${agentJsonPath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process and write templates
|
|
||||||
const outputs = [];
|
|
||||||
|
|
||||||
// 1. Agent JSON spec
|
|
||||||
const agentJson = processTemplate(readTemplate('base-agent.template.json'), replacements);
|
|
||||||
fs.writeFileSync(agentJsonPath, agentJson);
|
|
||||||
outputs.push(`agents/${normalizedId}.agent.json`);
|
|
||||||
|
|
||||||
// 2. Agent prompt
|
|
||||||
const promptPath = path.join(AGENTS_DIR, `${normalizedId}.prompt.txt`);
|
|
||||||
const agentPrompt = processTemplate(readTemplate('base-agent.prompt.template.txt'), replacements);
|
|
||||||
fs.writeFileSync(promptPath, agentPrompt);
|
|
||||||
outputs.push(`agents/${normalizedId}.prompt.txt`);
|
|
||||||
|
|
||||||
// 3. Workflow YAML
|
|
||||||
const workflowPath = path.join(WORKFLOWS_DIR, `${normalizedId}.workflow.yml`);
|
|
||||||
const agentWorkflow = processTemplate(readTemplate('base-agent.workflow.template.yml'), replacements);
|
|
||||||
fs.writeFileSync(workflowPath, agentWorkflow);
|
|
||||||
outputs.push(`.github/workflows/${normalizedId}.workflow.yml`);
|
|
||||||
|
|
||||||
// 4. MDX docs (optional)
|
|
||||||
if (!skipDocs) {
|
|
||||||
const mdxPath = path.join(DOCS_DIR, `${normalizedId}.mdx`);
|
|
||||||
const agentMdx = processTemplate(readTemplate('base-agent.mdx.template'), replacements);
|
|
||||||
fs.writeFileSync(mdxPath, agentMdx);
|
|
||||||
outputs.push(`docs/agents/${normalizedId}.mdx`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
agentId: normalizedId,
|
|
||||||
agentName,
|
|
||||||
outputs,
|
|
||||||
metadata: {
|
|
||||||
role: agentRole,
|
|
||||||
traits: agentTraits,
|
|
||||||
tags: agentTags,
|
|
||||||
createdAt
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CLI entry point
|
|
||||||
*/
|
|
||||||
function main() {
|
|
||||||
const args = process.argv.slice(2);
|
|
||||||
|
|
||||||
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
|
|
||||||
console.log(`
|
|
||||||
🛠️ spawn-agent – BlackRoad-OS Agent Generator
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
node scripts/spawn-agent.js <agent-id> [options]
|
|
||||||
pnpm spawn-agent <agent-id> [options]
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
agent-id Unique identifier for the agent (e.g., scribe-support)
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--skip-docs Skip generating MDX documentation
|
|
||||||
--verbose Show detailed output
|
|
||||||
--help, -h Show this help message
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
pnpm spawn-agent scribe-support
|
|
||||||
pnpm spawn-agent code-reviewer --skip-docs
|
|
||||||
pnpm spawn-agent deploy-bot --verbose
|
|
||||||
`);
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
const agentId = args[0];
|
|
||||||
const skipDocs = args.includes('--skip-docs');
|
|
||||||
const verbose = args.includes('--verbose');
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log(`\n🛠️ Spawning agent: ${agentId}\n`);
|
|
||||||
|
|
||||||
const result = spawnAgent(agentId, { skipDocs, verbose });
|
|
||||||
|
|
||||||
console.log(`✔ Created agent: ${result.agentName}`);
|
|
||||||
result.outputs.forEach((output, index) => {
|
|
||||||
const prefix = index === result.outputs.length - 1 ? '└─' : '├─';
|
|
||||||
console.log(`${prefix} ${output}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (verbose) {
|
|
||||||
console.log('\n📋 Metadata:');
|
|
||||||
console.log(` Role: ${result.metadata.role}`);
|
|
||||||
console.log(` Traits: ${result.metadata.traits.join(', ')}`);
|
|
||||||
console.log(` Tags: ${result.metadata.tags.join(', ')}`);
|
|
||||||
console.log(` Created: ${result.metadata.createdAt}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\n💚 Agent spawned successfully!\n');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`\n❌ Error: ${error.message}\n`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export for testing
|
|
||||||
module.exports = {
|
|
||||||
spawnAgent,
|
|
||||||
idToName,
|
|
||||||
inferRole,
|
|
||||||
inferTraits,
|
|
||||||
inferTags,
|
|
||||||
processTemplate
|
|
||||||
};
|
|
||||||
|
|
||||||
// Run CLI if executed directly
|
|
||||||
if (require.main === module) {
|
|
||||||
main();
|
|
||||||
}
|
|
||||||
const fs = require("fs");
|
|
||||||
const path = require("path");
|
|
||||||
|
|
||||||
const agentName = process.argv[2];
|
|
||||||
|
|
||||||
if (!agentName) {
|
|
||||||
console.error("❌ Please provide an agent name: `npm run spawn-agent <agent-name>`");
|
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const toTitleCase = (str) => str.replace(/-/g, " ").replace(/\b\w/g, c => c.toUpperCase());
|
const agentId = toId(rawName);
|
||||||
|
const displayName = toTitle(agentId);
|
||||||
|
|
||||||
const agentId = agentName.toLowerCase().replace(/\s+/g, "-");
|
const agentJsonPath = path.join(AGENTS_DIR, `${agentId}.agent.json`);
|
||||||
const displayName = toTitleCase(agentId);
|
const promptPath = path.join(AGENTS_DIR, `${agentId}.prompt.txt`);
|
||||||
|
const workflowPath = path.join(WORKFLOWS_DIR, `${agentId}.workflow.yml`);
|
||||||
|
const docsPath = path.join(DOCS_DIR, `${agentId}.mdx`);
|
||||||
|
|
||||||
const output = {
|
fs.mkdirSync(AGENTS_DIR, { recursive: true });
|
||||||
|
fs.mkdirSync(WORKFLOWS_DIR, { recursive: true });
|
||||||
|
fs.mkdirSync(DOCS_DIR, { recursive: true });
|
||||||
|
|
||||||
|
const agentData = {
|
||||||
id: agentId,
|
id: agentId,
|
||||||
name: displayName,
|
name: displayName,
|
||||||
role: "worker",
|
role: "worker",
|
||||||
traits: ["emoji-native"],
|
traits: ["emoji-native", "autonomous"],
|
||||||
inputs: [],
|
inherits_from: "base-agent",
|
||||||
outputs: [],
|
|
||||||
description: `This is the ${displayName} agent.`,
|
|
||||||
triggers: [],
|
|
||||||
inherits_from: "base-agent"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Paths
|
fs.writeFileSync(agentJsonPath, JSON.stringify(agentData, null, 2) + "\n");
|
||||||
const jsonPath = `agents/${agentId}.agent.json`;
|
|
||||||
const promptPath = `agents/${agentId}.prompt.txt`;
|
|
||||||
const workflowPath = `.github/workflows/${agentId}.workflow.yml`;
|
|
||||||
const docPath = `docs/agents/${agentId}.mdx`;
|
|
||||||
|
|
||||||
// Files
|
|
||||||
fs.mkdirSync("agents", { recursive: true });
|
|
||||||
fs.writeFileSync(jsonPath, JSON.stringify(output, null, 2));
|
|
||||||
|
|
||||||
fs.writeFileSync(promptPath, `SYSTEM:\nYou are the ${displayName} agent. Your job is to...`);
|
fs.writeFileSync(promptPath, `SYSTEM:\nYou are the ${displayName} agent. Your job is to...`);
|
||||||
|
|
||||||
fs.mkdirSync(".github/workflows", { recursive: true });
|
|
||||||
fs.writeFileSync(workflowPath, `name: ${displayName} Workflow\non:\n workflow_dispatch:\njobs:\n run:\n runs-on: ubuntu-latest\n steps:\n - run: echo "${displayName} agent triggered!"`);
|
|
||||||
|
|
||||||
fs.mkdirSync("docs/agents", { recursive: true });
|
|
||||||
fs.writeFileSync(docPath, `# ${displayName} Agent\n\nAuto-generated.\n\n## Purpose\nTBD`);
|
|
||||||
|
|
||||||
console.log(`✅ Created agent: ${agentId}`);
|
|
||||||
console.log(`├─ ${jsonPath}`);
|
|
||||||
console.log(`├─ ${promptPath}`);
|
|
||||||
console.log(`├─ ${workflowPath}`);
|
|
||||||
console.log(`└─ ${docPath}`);
|
|
||||||
const fs = require("fs");
|
|
||||||
const path = require("path");
|
|
||||||
|
|
||||||
const agentId = process.argv[2];
|
|
||||||
|
|
||||||
if (!agentId) {
|
|
||||||
console.error("❌ No agent ID provided.");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`🧬 Scaffolding agent: ${agentId}`);
|
|
||||||
|
|
||||||
const agentDir = path.join(__dirname, "..", "agents", agentId);
|
|
||||||
if (!fs.existsSync(agentDir)) {
|
|
||||||
fs.mkdirSync(agentDir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
const indexFile = path.join(agentDir, "index.js");
|
|
||||||
if (!fs.existsSync(indexFile)) {
|
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
indexFile,
|
workflowPath,
|
||||||
`// Agent: ${agentId}\nconsole.log("🤖 Agent ${agentId} initialized");\n`
|
`name: ${displayName} Workflow\non:\n workflow_dispatch:\njobs:\n run:\n runs-on: ubuntu-latest\n steps:\n - run: echo "${displayName} agent triggered!"`
|
||||||
);
|
);
|
||||||
console.log(`✅ Agent scaffolded at: ${agentDir}`);
|
fs.writeFileSync(
|
||||||
} else {
|
docsPath,
|
||||||
console.log(`⚠️ Agent ${agentId} already exists.`);
|
`# ${displayName} Agent\n\nAuto-generated documentation for ${displayName}.\n`
|
||||||
}
|
);
|
||||||
|
|
||||||
|
console.log(`Created agent: ${agentId}`);
|
||||||
|
console.log(agentJsonPath);
|
||||||
|
console.log(promptPath);
|
||||||
|
console.log(workflowPath);
|
||||||
|
console.log(docsPath);
|
||||||
|
|||||||
@@ -88,7 +88,9 @@ export function loadAgent(filename: string): Agent {
|
|||||||
*/
|
*/
|
||||||
export function loadAllAgents(): Agent[] {
|
export function loadAllAgents(): Agent[] {
|
||||||
const agentsDir = getAgentsDir();
|
const agentsDir = getAgentsDir();
|
||||||
const files = fs.readdirSync(agentsDir).filter((f) => f.endsWith(".json"));
|
const files = fs
|
||||||
|
.readdirSync(agentsDir)
|
||||||
|
.filter((f) => f.endsWith(".json") && f !== "lucidia.agent-spec.json");
|
||||||
return files.map((file) => loadAgent(file));
|
return files.map((file) => loadAgent(file));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export class AgentBuilder {
|
|||||||
*/
|
*/
|
||||||
withTrigger(emoji: string, action: string): AgentBuilder {
|
withTrigger(emoji: string, action: string): AgentBuilder {
|
||||||
this.agent.triggers = this.agent.triggers || [];
|
this.agent.triggers = this.agent.triggers || [];
|
||||||
this.agent.triggers.push({ emoji, action });
|
(this.agent.triggers as (AgentTrigger | string)[]).push({ emoji, action });
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,8 +213,10 @@ export class AgentRegistry {
|
|||||||
if (this.agents.size === 0) {
|
if (this.agents.size === 0) {
|
||||||
this.loadAll();
|
this.loadAll();
|
||||||
}
|
}
|
||||||
return Array.from(this.agents.values()).filter(a =>
|
return Array.from(this.agents.values()).filter(agent =>
|
||||||
a.triggers.some(t => t.emoji === emoji)
|
agent.triggers.some((trigger: AgentTrigger | string) =>
|
||||||
|
typeof trigger === "string" ? trigger === emoji : trigger.emoji === emoji
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,7 +228,7 @@ export class AgentRegistry {
|
|||||||
if (!parent || !parent.childAgents) {
|
if (!parent || !parent.childAgents) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return parent.childAgents.map(id => this.get(id)).filter(Boolean) as Agent[];
|
return parent.childAgents.map((id: string) => this.get(id)).filter(Boolean) as Agent[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -258,7 +260,7 @@ export class AgentRegistry {
|
|||||||
canSpawn(agentId: string): boolean {
|
canSpawn(agentId: string): boolean {
|
||||||
const agent = this.get(agentId);
|
const agent = this.get(agentId);
|
||||||
if (!agent) return false;
|
if (!agent) return false;
|
||||||
return agent.capabilities.some(c => c.name === "spawn-agent" && c.enabled);
|
return agent.capabilities?.some((c: AgentCapability) => c.name === "spawn-agent" && c.enabled) ?? false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -172,9 +172,12 @@ export function spawnAgent(name: string, options: SpawnOptions = {}): Agent {
|
|||||||
role,
|
role,
|
||||||
description: options.description || `${generateDisplayName(id)} - A ${role} agent for the Lucidia system`,
|
description: options.description || `${generateDisplayName(id)} - A ${role} agent for the Lucidia system`,
|
||||||
traits: options.traits || roleConfig.traits,
|
traits: options.traits || roleConfig.traits,
|
||||||
|
inputs: [],
|
||||||
|
outputs: [],
|
||||||
triggers: options.emoji
|
triggers: options.emoji
|
||||||
? [{ emoji: options.emoji, action: "activate" }, ...roleConfig.triggers]
|
? [{ emoji: options.emoji, action: "activate" }, ...roleConfig.triggers]
|
||||||
: roleConfig.triggers,
|
: roleConfig.triggers,
|
||||||
|
inherits_from: options.parent || "base-agent",
|
||||||
capabilities: allCapabilities,
|
capabilities: allCapabilities,
|
||||||
metadata: {
|
metadata: {
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
@@ -308,7 +311,9 @@ export function main(): void {
|
|||||||
console.log(` Role: ${agent.role}`);
|
console.log(` Role: ${agent.role}`);
|
||||||
console.log(` File: ${filePath}`);
|
console.log(` File: ${filePath}`);
|
||||||
console.log(` Traits: ${agent.traits.join(", ")}`);
|
console.log(` Traits: ${agent.traits.join(", ")}`);
|
||||||
console.log(` Triggers: ${agent.triggers.map(t => t.emoji).join(" ")}`);
|
console.log(
|
||||||
|
` Triggers: ${agent.triggers.map(trigger => (typeof trigger === "string" ? trigger : trigger.emoji)).join(" ")}`
|
||||||
|
);
|
||||||
|
|
||||||
if (options.parent) {
|
if (options.parent) {
|
||||||
updateParentAgent(options.parent, agent.id);
|
updateParentAgent(options.parent, agent.id);
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Agent Type Definitions for Lucidia DSL Agent System
|
* Agent Type Definitions for Lucidia DSL Agent System
|
||||||
*
|
|
||||||
* These types define the schema for agents that can be:
|
|
||||||
* - Triggerable via emojis
|
|
||||||
* - Linkable to issues and PRs
|
|
||||||
* - Self-describing
|
|
||||||
* - Able to spawn more agents
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export interface AgentTrigger {
|
export interface AgentTrigger {
|
||||||
@@ -26,6 +20,7 @@ export interface AgentMetadata {
|
|||||||
lastModified?: string;
|
lastModified?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
* Agent type definition for BlackRoad OS Genesis Agents
|
* Agent type definition for BlackRoad OS Genesis Agents
|
||||||
*/
|
*/
|
||||||
export interface Agent {
|
export interface Agent {
|
||||||
@@ -34,9 +29,13 @@ export interface Agent {
|
|||||||
role: string;
|
role: string;
|
||||||
description: string;
|
description: string;
|
||||||
traits: string[];
|
traits: string[];
|
||||||
triggers: AgentTrigger[];
|
inputs: string[];
|
||||||
capabilities: AgentCapability[];
|
outputs: string[];
|
||||||
metadata: AgentMetadata;
|
triggers: (AgentTrigger | string)[];
|
||||||
|
inherits_from: string | null;
|
||||||
|
active?: boolean;
|
||||||
|
capabilities?: AgentCapability[];
|
||||||
|
metadata?: AgentMetadata;
|
||||||
parentAgent?: string;
|
parentAgent?: string;
|
||||||
childAgents?: string[];
|
childAgents?: string[];
|
||||||
}
|
}
|
||||||
@@ -44,27 +43,10 @@ export interface Agent {
|
|||||||
export interface AgentTemplate {
|
export interface AgentTemplate {
|
||||||
$schema?: string;
|
$schema?: string;
|
||||||
templateVersion: string;
|
templateVersion: string;
|
||||||
defaults: Omit<Partial<Agent>, 'metadata'> & { metadata?: Partial<AgentMetadata> };
|
defaults: Omit<Partial<Agent>, "metadata"> & { metadata?: Partial<AgentMetadata> };
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AgentRole =
|
export type AgentRole = string;
|
||||||
| "scribe"
|
|
||||||
| "qa"
|
|
||||||
| "planner"
|
|
||||||
| "broadcast"
|
|
||||||
| "guardian"
|
|
||||||
| "digest"
|
|
||||||
| "archive"
|
|
||||||
| "support"
|
|
||||||
| "custom";
|
|
||||||
traits: string[];
|
|
||||||
inputs: string[];
|
|
||||||
outputs: string[];
|
|
||||||
description: string;
|
|
||||||
triggers: string[];
|
|
||||||
inherits_from: string | null;
|
|
||||||
active?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Agent validation result
|
* Agent validation result
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
import chroniclesData from "./lucidia-chronicles.json";
|
import chroniclesData from "./lucidia-chronicles.json";
|
||||||
|
|
||||||
export function getChroniclesRegistry(): ChroniclesRegistry {
|
export function getChroniclesRegistry(): ChroniclesRegistry {
|
||||||
return chroniclesData as ChroniclesRegistry;
|
return chroniclesData as unknown as ChroniclesRegistry;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getEpisodes(): ChronicleEpisode[] {
|
export function getEpisodes(): ChronicleEpisode[] {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export * from "./chronicles";
|
|||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import type { Chronicles, Episode, EpisodeFrontmatter } from "./types";
|
import type { Chronicles, Episode, EpisodeFrontmatter } from "./types";
|
||||||
|
import { getEpisodeById as getRegistryEpisodeById } from "../../chronicles/index";
|
||||||
|
|
||||||
const CHRONICLES_DIR = path.join(process.cwd(), "lucidia-chronicles");
|
const CHRONICLES_DIR = path.join(process.cwd(), "lucidia-chronicles");
|
||||||
const CHRONICLES_JSON = path.join(CHRONICLES_DIR, "chronicles.json");
|
const CHRONICLES_JSON = path.join(CHRONICLES_DIR, "chronicles.json");
|
||||||
@@ -24,8 +25,13 @@ export function addEpisode(episode: Episode): Chronicles {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getEpisodeById(id: string): Episode | undefined {
|
export function getEpisodeById(id: string): Episode | undefined {
|
||||||
|
const registryEpisode = getRegistryEpisodeById(id);
|
||||||
|
if (registryEpisode) {
|
||||||
|
return registryEpisode as Episode;
|
||||||
|
}
|
||||||
const chronicles = readChronicles();
|
const chronicles = readChronicles();
|
||||||
return chronicles.episodes.find((ep) => ep.id === id);
|
const normalizedId = id.startsWith("episode-") ? id : `episode-${id}`;
|
||||||
|
return chronicles.episodes.find((ep) => ep.id === id || ep.id === normalizedId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listEpisodes(): Episode[] {
|
export function listEpisodes(): Episode[] {
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import * as path from "path";
|
||||||
import {
|
import {
|
||||||
createEpisodeId,
|
createEpisodeId,
|
||||||
formatEpisodeDigest,
|
formatEpisodeDigest,
|
||||||
@@ -6,12 +8,35 @@ import {
|
|||||||
} from "../src/types/chronicles";
|
} from "../src/types/chronicles";
|
||||||
import {
|
import {
|
||||||
episode001,
|
episode001,
|
||||||
getEpisodeById,
|
getEpisodeById as getRegistryEpisodeById,
|
||||||
getLatestEpisode,
|
getLatestEpisode,
|
||||||
getEpisodesByTag,
|
getEpisodesByTag,
|
||||||
getEpisodesByStatus,
|
getEpisodesByStatus,
|
||||||
chronicleRegistry,
|
chronicleRegistry,
|
||||||
} from "../chronicles/index";
|
} from "../chronicles/index";
|
||||||
|
import {
|
||||||
|
readChronicles,
|
||||||
|
writeChronicles,
|
||||||
|
addEpisode,
|
||||||
|
getEpisodeById,
|
||||||
|
listEpisodes,
|
||||||
|
generateEpisodeMdx,
|
||||||
|
getNextEpisodeId,
|
||||||
|
} from "../src/chronicles";
|
||||||
|
import type { Chronicles, Episode, EpisodeFrontmatter } from "../src/chronicles/types";
|
||||||
|
|
||||||
|
vi.mock("fs", () => ({
|
||||||
|
readFileSync: vi.fn(),
|
||||||
|
writeFileSync: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("path", async () => {
|
||||||
|
const actual = await vi.importActual<typeof import("path")>("path");
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
join: vi.fn((...args: string[]) => args.join("/")),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
describe("chronicles types", () => {
|
describe("chronicles types", () => {
|
||||||
describe("createEpisodeId", () => {
|
describe("createEpisodeId", () => {
|
||||||
@@ -33,7 +58,7 @@ describe("chronicles types", () => {
|
|||||||
|
|
||||||
describe("formatEpisodeDigest", () => {
|
describe("formatEpisodeDigest", () => {
|
||||||
it("formats episode into PR comment markdown", () => {
|
it("formats episode into PR comment markdown", () => {
|
||||||
const digest = formatEpisodeDigest(episode001);
|
const digest = formatEpisodeDigest(episode001 as ChronicleEpisode);
|
||||||
|
|
||||||
expect(digest).toContain("LUCIDIA CINEMATIC UNIVERSE");
|
expect(digest).toContain("LUCIDIA CINEMATIC UNIVERSE");
|
||||||
expect(digest).toContain("THE CLONE AWAKENS");
|
expect(digest).toContain("THE CLONE AWAKENS");
|
||||||
@@ -72,34 +97,14 @@ describe("chronicles registry", () => {
|
|||||||
expect(chronicleRegistry.episodes).toContain(episode001);
|
expect(chronicleRegistry.episodes).toContain(episode001);
|
||||||
expect(chronicleRegistry.totalEpisodes).toBe(1);
|
expect(chronicleRegistry.totalEpisodes).toBe(1);
|
||||||
expect(chronicleRegistry.latestEpisodeId).toBe("001");
|
expect(chronicleRegistry.latestEpisodeId).toBe("001");
|
||||||
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
|
||||||
import * as fs from "fs";
|
|
||||||
import * as path from "path";
|
|
||||||
|
|
||||||
// Mock fs module
|
|
||||||
vi.mock("fs", () => ({
|
|
||||||
readFileSync: vi.fn(),
|
|
||||||
writeFileSync: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("path", async () => {
|
|
||||||
const actual = await vi.importActual("path");
|
|
||||||
return {
|
|
||||||
...actual,
|
|
||||||
join: vi.fn((...args: string[]) => args.join("/")),
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
import {
|
it("fetches episode by id", () => {
|
||||||
readChronicles,
|
const episode = getRegistryEpisodeById("001");
|
||||||
writeChronicles,
|
expect(episode).toBe(episode001);
|
||||||
addEpisode,
|
});
|
||||||
getEpisodeById,
|
});
|
||||||
listEpisodes,
|
});
|
||||||
generateEpisodeMdx,
|
|
||||||
getNextEpisodeId,
|
|
||||||
} from "../src/chronicles";
|
|
||||||
import type { Chronicles, Episode, EpisodeFrontmatter } from "../src/chronicles/types";
|
|
||||||
|
|
||||||
describe("Chronicles", () => {
|
describe("Chronicles", () => {
|
||||||
const mockChronicles: Chronicles = {
|
const mockChronicles: Chronicles = {
|
||||||
@@ -208,8 +213,6 @@ describe("Chronicles", () => {
|
|||||||
it("returns empty array for non-matching status", () => {
|
it("returns empty array for non-matching status", () => {
|
||||||
const episodes = getEpisodesByStatus("completed");
|
const episodes = getEpisodesByStatus("completed");
|
||||||
expect(episodes).toHaveLength(0);
|
expect(episodes).toHaveLength(0);
|
||||||
const result = getEpisodeById("episode-001");
|
|
||||||
expect(result).toEqual(mockChronicles.episodes[0]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns undefined when not found", () => {
|
it("returns undefined when not found", () => {
|
||||||
@@ -242,7 +245,7 @@ describe("Chronicles", () => {
|
|||||||
voice: "/audio/test.mp3",
|
voice: "/audio/test.mp3",
|
||||||
transcript: true,
|
transcript: true,
|
||||||
};
|
};
|
||||||
const narrative = "> **\"This is Lucidia.\"**\n> Test narrative.";
|
const narrative = '> **"This is Lucidia."**\n> Test narrative.';
|
||||||
|
|
||||||
const result = generateEpisodeMdx(frontmatter, narrative);
|
const result = generateEpisodeMdx(frontmatter, narrative);
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { describe, expect, it } from "vitest";
|
|||||||
import { readFileSync } from "fs";
|
import { readFileSync } from "fs";
|
||||||
import { parse } from "yaml";
|
import { parse } from "yaml";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
|
import { Lucidia, createLucidia } from "../src/lucidia";
|
||||||
|
import type { SpawnRulesConfig, Metrics } from "../src/lucidia/types";
|
||||||
|
|
||||||
describe("lucidia.yml", () => {
|
describe("lucidia.yml", () => {
|
||||||
const lucidiaPath = join(__dirname, "..", "lucidia.yml");
|
const lucidiaPath = join(__dirname, "..", "lucidia.yml");
|
||||||
@@ -73,8 +75,8 @@ describe("lucidia.yml", () => {
|
|||||||
expect(lucidia.outputs["markdown-summary"].format).toBe("markdown");
|
expect(lucidia.outputs["markdown-summary"].format).toBe("markdown");
|
||||||
expect(lucidia.outputs["actionable-recommendations"]).toBeDefined();
|
expect(lucidia.outputs["actionable-recommendations"]).toBeDefined();
|
||||||
expect(lucidia.outputs["escalation-alerts"]).toBeDefined();
|
expect(lucidia.outputs["escalation-alerts"]).toBeDefined();
|
||||||
import { Lucidia, createLucidia } from "../src/lucidia";
|
});
|
||||||
import type { SpawnRulesConfig, Metrics } from "../src/lucidia/types";
|
});
|
||||||
|
|
||||||
const testConfig: SpawnRulesConfig = {
|
const testConfig: SpawnRulesConfig = {
|
||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
|
|||||||
@@ -1,44 +1,24 @@
|
|||||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||||
import * as fs from "fs";
|
import { execSync } from "child_process";
|
||||||
import * as path from "path";
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
import { spawnAgent, saveAgent } from "../src/agents/spawn-agent";
|
import { spawnAgent, saveAgent } from "../src/agents/spawn-agent";
|
||||||
import { AgentBuilder, AgentRegistry, createAgent } from "../src/agents/lucidia-agent-builder";
|
import { AgentBuilder, AgentRegistry, createAgent } from "../src/agents/lucidia-agent-builder";
|
||||||
import type { Agent } from "../src/agents/types";
|
import type { Agent } from "../src/agents/types";
|
||||||
|
|
||||||
const TEST_AGENTS_DIR = path.join(__dirname, "../src/agents");
|
const TEST_AGENTS_DIR = path.join(__dirname, "../src/agents");
|
||||||
const TEST_OUTPUT_DIR = "/tmp/test-agents";
|
const TEST_OUTPUT_DIR = "/tmp/test-agents";
|
||||||
|
|
||||||
describe("spawn-agent", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
// Create test output directory
|
|
||||||
if (!fs.existsSync(TEST_OUTPUT_DIR)) {
|
|
||||||
fs.mkdirSync(TEST_OUTPUT_DIR, { recursive: true });
|
|
||||||
import { describe, expect, it, beforeEach, afterEach } from "vitest";
|
|
||||||
import { execSync } from "child_process";
|
|
||||||
import fs from "fs";
|
|
||||||
import path from "path";
|
|
||||||
|
|
||||||
const TEST_AGENT_NAME = "test-agent-xyz";
|
const TEST_AGENT_NAME = "test-agent-xyz";
|
||||||
const ROOT_DIR = path.join(__dirname, "..");
|
const ROOT_DIR = path.join(__dirname, "..");
|
||||||
|
|
||||||
describe("spawn-agent.js", () => {
|
describe("spawn-agent utilities", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// Clean up any existing test agent files before each test
|
if (!fs.existsSync(TEST_OUTPUT_DIR)) {
|
||||||
const paths = [
|
fs.mkdirSync(TEST_OUTPUT_DIR, { recursive: true });
|
||||||
path.join(ROOT_DIR, "agents", `${TEST_AGENT_NAME}.agent.json`),
|
|
||||||
path.join(ROOT_DIR, "agents", `${TEST_AGENT_NAME}.prompt.txt`),
|
|
||||||
path.join(ROOT_DIR, ".github", "workflows", `${TEST_AGENT_NAME}.workflow.yml`),
|
|
||||||
path.join(ROOT_DIR, "docs", "agents", `${TEST_AGENT_NAME}.mdx`),
|
|
||||||
];
|
|
||||||
for (const p of paths) {
|
|
||||||
if (fs.existsSync(p)) {
|
|
||||||
fs.unlinkSync(p);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
// Clean up test files
|
|
||||||
if (fs.existsSync(TEST_OUTPUT_DIR)) {
|
if (fs.existsSync(TEST_OUTPUT_DIR)) {
|
||||||
const files = fs.readdirSync(TEST_OUTPUT_DIR);
|
const files = fs.readdirSync(TEST_OUTPUT_DIR);
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
@@ -83,7 +63,7 @@ describe("spawn-agent.js", () => {
|
|||||||
it("should include parent agent reference", () => {
|
it("should include parent agent reference", () => {
|
||||||
const agent = spawnAgent("child-agent", { parent: "parent-agent" });
|
const agent = spawnAgent("child-agent", { parent: "parent-agent" });
|
||||||
expect(agent.parentAgent).toBe("parent-agent");
|
expect(agent.parentAgent).toBe("parent-agent");
|
||||||
expect(agent.metadata.createdBy).toContain("spawned-by");
|
expect(agent.metadata?.createdBy).toContain("spawned-by");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should include custom traits", () => {
|
it("should include custom traits", () => {
|
||||||
@@ -94,7 +74,7 @@ describe("spawn-agent.js", () => {
|
|||||||
|
|
||||||
it("should include base capabilities", () => {
|
it("should include base capabilities", () => {
|
||||||
const agent = spawnAgent("my-agent");
|
const agent = spawnAgent("my-agent");
|
||||||
const capabilityNames = agent.capabilities.map(c => c.name);
|
const capabilityNames = agent.capabilities?.map((c) => c.name) ?? [];
|
||||||
expect(capabilityNames).toContain("self-describe");
|
expect(capabilityNames).toContain("self-describe");
|
||||||
expect(capabilityNames).toContain("issue-link");
|
expect(capabilityNames).toContain("issue-link");
|
||||||
expect(capabilityNames).toContain("pr-link");
|
expect(capabilityNames).toContain("pr-link");
|
||||||
@@ -102,20 +82,20 @@ describe("spawn-agent.js", () => {
|
|||||||
|
|
||||||
it("should include role-specific triggers", () => {
|
it("should include role-specific triggers", () => {
|
||||||
const agent = spawnAgent("scribe-agent");
|
const agent = spawnAgent("scribe-agent");
|
||||||
const triggerEmojis = agent.triggers.map(t => t.emoji);
|
const triggerEmojis = (agent.triggers as any[]).map((t) => (typeof t === "string" ? t : t.emoji));
|
||||||
expect(triggerEmojis).toContain("📝");
|
expect(triggerEmojis).toContain("📝");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should include custom emoji trigger", () => {
|
it("should include custom emoji trigger", () => {
|
||||||
const agent = spawnAgent("my-agent", { emoji: "🔮" });
|
const agent = spawnAgent("my-agent", { emoji: "🔮" });
|
||||||
const triggerEmojis = agent.triggers.map(t => t.emoji);
|
const triggerEmojis = (agent.triggers as any[]).map((t) => (typeof t === "string" ? t : t.emoji));
|
||||||
expect(triggerEmojis).toContain("🔮");
|
expect(triggerEmojis).toContain("🔮");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should include metadata with timestamp", () => {
|
it("should include metadata with timestamp", () => {
|
||||||
const agent = spawnAgent("my-agent");
|
const agent = spawnAgent("my-agent");
|
||||||
expect(agent.metadata.createdAt).toBeDefined();
|
expect(agent.metadata?.createdAt).toBeDefined();
|
||||||
expect(agent.metadata.version).toBe("1.0.0");
|
expect(agent.metadata?.version).toBe("1.0.0");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -127,7 +107,7 @@ describe("spawn-agent.js", () => {
|
|||||||
expect(fs.existsSync(filePath)).toBe(true);
|
expect(fs.existsSync(filePath)).toBe(true);
|
||||||
|
|
||||||
const content = fs.readFileSync(filePath, "utf-8");
|
const content = fs.readFileSync(filePath, "utf-8");
|
||||||
const savedAgent = JSON.parse(content);
|
const savedAgent = JSON.parse(content) as Agent;
|
||||||
expect(savedAgent.id).toBe("test-save-agent");
|
expect(savedAgent.id).toBe("test-save-agent");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -148,25 +128,22 @@ describe("AgentBuilder", () => {
|
|||||||
expect(agent.role).toBe("guardian");
|
expect(agent.role).toBe("guardian");
|
||||||
expect(agent.description).toBe("A custom guardian agent");
|
expect(agent.description).toBe("A custom guardian agent");
|
||||||
expect(agent.traits).toContain("vigilant");
|
expect(agent.traits).toContain("vigilant");
|
||||||
expect(agent.triggers.some(t => t.emoji === "🛡️")).toBe(true);
|
expect(agent.triggers.some((t) => (typeof t === "string" ? t === "🛡️" : t.emoji === "🛡️"))).toBe(
|
||||||
expect(agent.capabilities.some(c => c.name === "monitoring")).toBe(true);
|
true
|
||||||
|
);
|
||||||
|
expect(agent.capabilities?.some((c) => c.name === "monitoring")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should set parent agent", () => {
|
it("should set parent agent", () => {
|
||||||
const agent = createAgent("Child Agent")
|
const agent = createAgent("Child Agent").withRole("support").withParent("parent-id").build();
|
||||||
.withRole("support")
|
|
||||||
.withParent("parent-id")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
expect(agent.parentAgent).toBe("parent-id");
|
expect(agent.parentAgent).toBe("parent-id");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should add default capabilities if none provided", () => {
|
it("should add default capabilities if none provided", () => {
|
||||||
const agent = createAgent("Simple Agent")
|
const agent = createAgent("Simple Agent").withRole("custom").build();
|
||||||
.withRole("custom")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
const capabilityNames = agent.capabilities.map(c => c.name);
|
const capabilityNames = agent.capabilities?.map((c) => c.name) ?? [];
|
||||||
expect(capabilityNames).toContain("self-describe");
|
expect(capabilityNames).toContain("self-describe");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -178,8 +155,7 @@ describe("AgentRegistry", () => {
|
|||||||
|
|
||||||
expect(agents.length).toBeGreaterThan(0);
|
expect(agents.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
// Should have loaded the predefined agents
|
const agentIds = agents.map((a) => a.id);
|
||||||
const agentIds = agents.map(a => a.id);
|
|
||||||
expect(agentIds).toContain("scribe-agent");
|
expect(agentIds).toContain("scribe-agent");
|
||||||
expect(agentIds).toContain("qa-agent");
|
expect(agentIds).toContain("qa-agent");
|
||||||
expect(agentIds).toContain("guardian-agent");
|
expect(agentIds).toContain("guardian-agent");
|
||||||
@@ -216,7 +192,11 @@ describe("AgentRegistry", () => {
|
|||||||
|
|
||||||
expect(ids.length).toBeGreaterThan(0);
|
expect(ids.length).toBeGreaterThan(0);
|
||||||
expect(ids).toContain("scribe-agent");
|
expect(ids).toContain("scribe-agent");
|
||||||
// Clean up test agent files after each test
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("spawn-agent script", () => {
|
||||||
|
const cleanupFiles = () => {
|
||||||
const paths = [
|
const paths = [
|
||||||
path.join(ROOT_DIR, "agents", `${TEST_AGENT_NAME}.agent.json`),
|
path.join(ROOT_DIR, "agents", `${TEST_AGENT_NAME}.agent.json`),
|
||||||
path.join(ROOT_DIR, "agents", `${TEST_AGENT_NAME}.prompt.txt`),
|
path.join(ROOT_DIR, "agents", `${TEST_AGENT_NAME}.prompt.txt`),
|
||||||
@@ -228,16 +208,20 @@ describe("AgentRegistry", () => {
|
|||||||
fs.unlinkSync(p);
|
fs.unlinkSync(p);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Clean up directories if empty
|
const dirs = [path.join(ROOT_DIR, "agents"), path.join(ROOT_DIR, "docs", "agents")];
|
||||||
const dirs = [
|
|
||||||
path.join(ROOT_DIR, "agents"),
|
|
||||||
path.join(ROOT_DIR, "docs", "agents"),
|
|
||||||
];
|
|
||||||
for (const d of dirs) {
|
for (const d of dirs) {
|
||||||
if (fs.existsSync(d) && fs.readdirSync(d).length === 0) {
|
if (fs.existsSync(d) && fs.readdirSync(d).length === 0) {
|
||||||
fs.rmSync(d, { recursive: true });
|
fs.rmSync(d, { recursive: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
cleanupFiles();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanupFiles();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show error when no agent name provided", () => {
|
it("should show error when no agent name provided", () => {
|
||||||
@@ -259,7 +243,6 @@ describe("AgentRegistry", () => {
|
|||||||
|
|
||||||
expect(output).toContain(`Created agent: ${TEST_AGENT_NAME}`);
|
expect(output).toContain(`Created agent: ${TEST_AGENT_NAME}`);
|
||||||
|
|
||||||
// Check agent JSON file
|
|
||||||
const jsonPath = path.join(ROOT_DIR, "agents", `${TEST_AGENT_NAME}.agent.json`);
|
const jsonPath = path.join(ROOT_DIR, "agents", `${TEST_AGENT_NAME}.agent.json`);
|
||||||
expect(fs.existsSync(jsonPath)).toBe(true);
|
expect(fs.existsSync(jsonPath)).toBe(true);
|
||||||
const jsonContent = JSON.parse(fs.readFileSync(jsonPath, "utf-8"));
|
const jsonContent = JSON.parse(fs.readFileSync(jsonPath, "utf-8"));
|
||||||
@@ -269,21 +252,18 @@ describe("AgentRegistry", () => {
|
|||||||
expect(jsonContent.traits).toContain("emoji-native");
|
expect(jsonContent.traits).toContain("emoji-native");
|
||||||
expect(jsonContent.inherits_from).toBe("base-agent");
|
expect(jsonContent.inherits_from).toBe("base-agent");
|
||||||
|
|
||||||
// Check prompt file
|
|
||||||
const promptPath = path.join(ROOT_DIR, "agents", `${TEST_AGENT_NAME}.prompt.txt`);
|
const promptPath = path.join(ROOT_DIR, "agents", `${TEST_AGENT_NAME}.prompt.txt`);
|
||||||
expect(fs.existsSync(promptPath)).toBe(true);
|
expect(fs.existsSync(promptPath)).toBe(true);
|
||||||
const promptContent = fs.readFileSync(promptPath, "utf-8");
|
const promptContent = fs.readFileSync(promptPath, "utf-8");
|
||||||
expect(promptContent).toContain("SYSTEM:");
|
expect(promptContent).toContain("SYSTEM:");
|
||||||
expect(promptContent).toContain("Test Agent Xyz");
|
expect(promptContent).toContain("Test Agent Xyz");
|
||||||
|
|
||||||
// Check workflow file
|
|
||||||
const workflowPath = path.join(ROOT_DIR, ".github", "workflows", `${TEST_AGENT_NAME}.workflow.yml`);
|
const workflowPath = path.join(ROOT_DIR, ".github", "workflows", `${TEST_AGENT_NAME}.workflow.yml`);
|
||||||
expect(fs.existsSync(workflowPath)).toBe(true);
|
expect(fs.existsSync(workflowPath)).toBe(true);
|
||||||
const workflowContent = fs.readFileSync(workflowPath, "utf-8");
|
const workflowContent = fs.readFileSync(workflowPath, "utf-8");
|
||||||
expect(workflowContent).toContain("name: Test Agent Xyz Workflow");
|
expect(workflowContent).toContain("name: Test Agent Xyz Workflow");
|
||||||
expect(workflowContent).toContain("workflow_dispatch");
|
expect(workflowContent).toContain("workflow_dispatch");
|
||||||
|
|
||||||
// Check docs file
|
|
||||||
const docPath = path.join(ROOT_DIR, "docs", "agents", `${TEST_AGENT_NAME}.mdx`);
|
const docPath = path.join(ROOT_DIR, "docs", "agents", `${TEST_AGENT_NAME}.mdx`);
|
||||||
expect(fs.existsSync(docPath)).toBe(true);
|
expect(fs.existsSync(docPath)).toBe(true);
|
||||||
const docContent = fs.readFileSync(docPath, "utf-8");
|
const docContent = fs.readFileSync(docPath, "utf-8");
|
||||||
@@ -305,7 +285,6 @@ describe("AgentRegistry", () => {
|
|||||||
const jsonPath = path.join(ROOT_DIR, "agents", `${expectedId}.agent.json`);
|
const jsonPath = path.join(ROOT_DIR, "agents", `${expectedId}.agent.json`);
|
||||||
expect(fs.existsSync(jsonPath)).toBe(true);
|
expect(fs.existsSync(jsonPath)).toBe(true);
|
||||||
} finally {
|
} finally {
|
||||||
// Cleanup
|
|
||||||
const paths = [
|
const paths = [
|
||||||
path.join(ROOT_DIR, "agents", `${expectedId}.agent.json`),
|
path.join(ROOT_DIR, "agents", `${expectedId}.agent.json`),
|
||||||
path.join(ROOT_DIR, "agents", `${expectedId}.prompt.txt`),
|
path.join(ROOT_DIR, "agents", `${expectedId}.prompt.txt`),
|
||||||
|
|||||||
@@ -1,31 +1,25 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2020",
|
|
||||||
"module": "CommonJS",
|
|
||||||
"moduleResolution": "Node",
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"jsx": "react-jsx",
|
|
||||||
"strict": true,
|
|
||||||
"baseUrl": "./",
|
|
||||||
"outDir": "./dist",
|
|
||||||
"rootDir": "./src",
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"types": ["node", "vitest/globals", "@testing-library/jest-dom"]
|
|
||||||
},
|
|
||||||
"include": ["src"],
|
|
||||||
"exclude": ["node_modules", "dist"]
|
|
||||||
"include": ["src", "lib", "components", "tests", "app", "chronicles", "vitest.config.ts"],
|
|
||||||
"exclude": ["node_modules"]
|
|
||||||
"target": "ES2021",
|
"target": "ES2021",
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "Node",
|
"moduleResolution": "Bundler",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"outDir": "dist"
|
"outDir": "dist",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"types": ["node", "vitest/globals", "@testing-library/jest-dom"]
|
||||||
},
|
},
|
||||||
"include": ["src", "tests", "scripts"],
|
"include": [
|
||||||
"exclude": ["node_modules", "dist"]
|
"src",
|
||||||
|
"scripts",
|
||||||
|
"lib",
|
||||||
|
"components",
|
||||||
|
"app",
|
||||||
|
"chronicles",
|
||||||
|
"vitest.config.ts"
|
||||||
|
],
|
||||||
|
"exclude": ["node_modules", "dist", "tests"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export default defineConfig({
|
|||||||
test: {
|
test: {
|
||||||
environment: "jsdom",
|
environment: "jsdom",
|
||||||
setupFiles: "./vitest.setup.ts",
|
setupFiles: "./vitest.setup.ts",
|
||||||
globals: true
|
globals: true,
|
||||||
|
exclude: ["dist/**", "node_modules/**"]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user