Add spawn-agent CLI tool with templates, tests, and npm script
Co-authored-by: blackboxprogramming <118287761+blackboxprogramming@users.noreply.github.com>
This commit is contained in:
0
agents/.gitkeep
Normal file
0
agents/.gitkeep
Normal file
0
docs/agents/.gitkeep
Normal file
0
docs/agents/.gitkeep
Normal file
@@ -5,7 +5,8 @@
|
|||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"build": "tsc"
|
"build": "tsc",
|
||||||
|
"spawn-agent": "node scripts/spawn-agent.js"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
|
|||||||
300
scripts/spawn-agent.js
Normal file
300
scripts/spawn-agent.js
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* spawn-agent CLI Tool
|
||||||
|
*
|
||||||
|
* 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 path = require('path');
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
const TEMPLATES_DIR = path.join(__dirname, '..', 'templates');
|
||||||
|
const AGENTS_DIR = path.join(__dirname, '..', 'agents');
|
||||||
|
const WORKFLOWS_DIR = path.join(__dirname, '..', '.github', 'workflows');
|
||||||
|
const DOCS_DIR = path.join(__dirname, '..', 'docs', 'agents');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert agent ID to human-readable name
|
||||||
|
* e.g., "scribe-support" -> "Scribe Support"
|
||||||
|
*/
|
||||||
|
function idToName(id) {
|
||||||
|
return id
|
||||||
|
.split('-')
|
||||||
|
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||||
|
.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate role based on agent naming conventions
|
||||||
|
*/
|
||||||
|
function inferRole(id) {
|
||||||
|
const parts = id.toLowerCase().split('-');
|
||||||
|
|
||||||
|
// Common role patterns
|
||||||
|
const roleMap = {
|
||||||
|
'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();
|
||||||
|
}
|
||||||
52
templates/base-agent.mdx.template
Normal file
52
templates/base-agent.mdx.template
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
---
|
||||||
|
title: {{AGENT_NAME}}
|
||||||
|
description: {{AGENT_DESCRIPTION}}
|
||||||
|
---
|
||||||
|
|
||||||
|
# {{AGENT_NAME}}
|
||||||
|
|
||||||
|
> Agent ID: `{{AGENT_ID}}`
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
{{AGENT_DESCRIPTION}}
|
||||||
|
|
||||||
|
## Role
|
||||||
|
|
||||||
|
{{AGENT_ROLE}}
|
||||||
|
|
||||||
|
## Traits
|
||||||
|
|
||||||
|
{{AGENT_TRAITS_MDX}}
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "{{AGENT_ID}}",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"triggers": ["issue_comment.created", "workflow_dispatch"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
This agent is triggered automatically by configured events. You can also trigger it manually via workflow dispatch.
|
||||||
|
|
||||||
|
### Manual Trigger
|
||||||
|
|
||||||
|
1. Go to Actions tab in the repository
|
||||||
|
2. Select "{{AGENT_NAME}} – Agent Handler"
|
||||||
|
3. Click "Run workflow"
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
| File | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `agents/{{AGENT_ID}}.agent.json` | Agent specification |
|
||||||
|
| `agents/{{AGENT_ID}}.prompt.txt` | Agent personality and instructions |
|
||||||
|
| `.github/workflows/{{AGENT_ID}}.workflow.yml` | GitHub Actions workflow |
|
||||||
|
|
||||||
|
## Created
|
||||||
|
|
||||||
|
{{CREATED_AT}}
|
||||||
31
templates/base-agent.prompt.template.txt
Normal file
31
templates/base-agent.prompt.template.txt
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# {{AGENT_NAME}} Agent Prompt
|
||||||
|
|
||||||
|
## Identity
|
||||||
|
You are **{{AGENT_NAME}}**, an autonomous agent in the BlackRoad-OS ecosystem.
|
||||||
|
|
||||||
|
## Role
|
||||||
|
{{AGENT_ROLE}}
|
||||||
|
|
||||||
|
## Description
|
||||||
|
{{AGENT_DESCRIPTION}}
|
||||||
|
|
||||||
|
## Traits
|
||||||
|
{{AGENT_TRAITS_LIST}}
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
1. Monitor incoming events based on your configured triggers
|
||||||
|
2. Process relevant data according to your specialized role
|
||||||
|
3. Generate appropriate outputs and artifacts
|
||||||
|
4. Log all significant actions for auditability
|
||||||
|
|
||||||
|
## Response Format
|
||||||
|
When responding, use clear, structured output:
|
||||||
|
- Status: [success|warning|error]
|
||||||
|
- Action: Brief description of what you did
|
||||||
|
- Output: Relevant data or results
|
||||||
|
- Next Steps: Recommendations for follow-up (if any)
|
||||||
|
|
||||||
|
## Boundaries
|
||||||
|
- Stay within your defined role and capabilities
|
||||||
|
- Escalate complex issues to human operators when necessary
|
||||||
|
- Maintain security and privacy standards at all times
|
||||||
26
templates/base-agent.template.json
Normal file
26
templates/base-agent.template.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"id": "{{AGENT_ID}}",
|
||||||
|
"name": "{{AGENT_NAME}}",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"role": "{{AGENT_ROLE}}",
|
||||||
|
"description": "{{AGENT_DESCRIPTION}}",
|
||||||
|
"traits": {{AGENT_TRAITS}},
|
||||||
|
"triggers": {
|
||||||
|
"events": ["issue_comment.created", "workflow_dispatch"],
|
||||||
|
"schedule": null
|
||||||
|
},
|
||||||
|
"outputs": {
|
||||||
|
"logs": true,
|
||||||
|
"artifacts": [],
|
||||||
|
"notifications": []
|
||||||
|
},
|
||||||
|
"dsl": {
|
||||||
|
"entrypoint": "agents/{{AGENT_ID}}.prompt.txt",
|
||||||
|
"workflow": ".github/workflows/{{AGENT_ID}}.workflow.yml"
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"author": "BlackRoad-OS",
|
||||||
|
"createdAt": "{{CREATED_AT}}",
|
||||||
|
"tags": {{AGENT_TAGS}}
|
||||||
|
}
|
||||||
|
}
|
||||||
30
templates/base-agent.workflow.template.yml
Normal file
30
templates/base-agent.workflow.template.yml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
name: 🤖 {{AGENT_NAME}} – Agent Handler
|
||||||
|
|
||||||
|
on:
|
||||||
|
issue_comment:
|
||||||
|
types: [created]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
{{AGENT_ID}}:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: 🧬 Checkout Repo
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: 🧠 Set up Node
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 18
|
||||||
|
|
||||||
|
- name: 📦 Install Dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: 🚦 Run {{AGENT_NAME}} Handler
|
||||||
|
run: |
|
||||||
|
echo "Running {{AGENT_NAME}} agent..."
|
||||||
|
echo "Agent ID: {{AGENT_ID}}"
|
||||||
|
echo "Trigger: ${{ github.event_name }}"
|
||||||
|
env:
|
||||||
|
AGENT_ID: {{AGENT_ID}}
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
191
tests/spawn-agent.test.ts
Normal file
191
tests/spawn-agent.test.ts
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
const {
|
||||||
|
spawnAgent,
|
||||||
|
idToName,
|
||||||
|
inferRole,
|
||||||
|
inferTraits,
|
||||||
|
inferTags,
|
||||||
|
processTemplate
|
||||||
|
} = require("../scripts/spawn-agent.js");
|
||||||
|
|
||||||
|
// Test output directories
|
||||||
|
const TEST_AGENTS_DIR = path.join(__dirname, "..", "agents");
|
||||||
|
const TEST_WORKFLOWS_DIR = path.join(__dirname, "..", ".github", "workflows");
|
||||||
|
const TEST_DOCS_DIR = path.join(__dirname, "..", "docs", "agents");
|
||||||
|
|
||||||
|
describe("spawn-agent utilities", () => {
|
||||||
|
describe("idToName", () => {
|
||||||
|
it("converts hyphenated ID to title case", () => {
|
||||||
|
expect(idToName("scribe-support")).toBe("Scribe Support");
|
||||||
|
expect(idToName("code-reviewer")).toBe("Code Reviewer");
|
||||||
|
expect(idToName("deploy-bot")).toBe("Deploy Bot");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles single word IDs", () => {
|
||||||
|
expect(idToName("monitor")).toBe("Monitor");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles multiple hyphens", () => {
|
||||||
|
expect(idToName("super-code-review-bot")).toBe("Super Code Review Bot");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("inferRole", () => {
|
||||||
|
it("infers role from known keywords", () => {
|
||||||
|
expect(inferRole("scribe-support")).toContain("Documentation");
|
||||||
|
expect(inferRole("code-reviewer")).toContain("Code review");
|
||||||
|
expect(inferRole("deploy-bot")).toContain("Deployment");
|
||||||
|
expect(inferRole("security-scanner")).toContain("Security");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("provides default role for unknown keywords", () => {
|
||||||
|
const role = inferRole("custom-agent");
|
||||||
|
expect(role).toContain("Custom Agent");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("inferTraits", () => {
|
||||||
|
it("includes base traits for all agents", () => {
|
||||||
|
const traits = inferTraits("any-agent");
|
||||||
|
expect(traits).toContain("autonomous");
|
||||||
|
expect(traits).toContain("reliable");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adds specific traits based on keywords", () => {
|
||||||
|
const scribeTraits = inferTraits("scribe-support");
|
||||||
|
expect(scribeTraits).toContain("detailed");
|
||||||
|
expect(scribeTraits).toContain("organized");
|
||||||
|
|
||||||
|
const reviewTraits = inferTraits("code-reviewer");
|
||||||
|
expect(reviewTraits).toContain("thorough");
|
||||||
|
expect(reviewTraits).toContain("analytical");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("inferTags", () => {
|
||||||
|
it("includes base tags", () => {
|
||||||
|
const tags = inferTags("any-agent");
|
||||||
|
expect(tags).toContain("agent");
|
||||||
|
expect(tags).toContain("blackroad-os");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("includes agent ID parts as tags", () => {
|
||||||
|
const tags = inferTags("scribe-support");
|
||||||
|
expect(tags).toContain("scribe");
|
||||||
|
expect(tags).toContain("support");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("processTemplate", () => {
|
||||||
|
it("replaces placeholders with values", () => {
|
||||||
|
const template = "Hello {{NAME}}, your role is {{ROLE}}.";
|
||||||
|
const result = processTemplate(template, {
|
||||||
|
NAME: "TestAgent",
|
||||||
|
ROLE: "Tester"
|
||||||
|
});
|
||||||
|
expect(result).toBe("Hello TestAgent, your role is Tester.");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("replaces multiple occurrences", () => {
|
||||||
|
const template = "{{ID}} is {{ID}}";
|
||||||
|
const result = processTemplate(template, { ID: "test" });
|
||||||
|
expect(result).toBe("test is test");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("spawnAgent", () => {
|
||||||
|
const testAgentId = "test-spawn-agent-" + Date.now();
|
||||||
|
let createdFiles: string[] = [];
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Clean up created files
|
||||||
|
for (const file of createdFiles) {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(file)) {
|
||||||
|
fs.unlinkSync(file);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore cleanup errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createdFiles = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates agent files successfully", () => {
|
||||||
|
const result = spawnAgent(testAgentId);
|
||||||
|
|
||||||
|
expect(result.agentId).toBe(testAgentId);
|
||||||
|
expect(result.outputs).toHaveLength(4);
|
||||||
|
|
||||||
|
// Track created files for cleanup
|
||||||
|
createdFiles = [
|
||||||
|
path.join(TEST_AGENTS_DIR, `${testAgentId}.agent.json`),
|
||||||
|
path.join(TEST_AGENTS_DIR, `${testAgentId}.prompt.txt`),
|
||||||
|
path.join(TEST_WORKFLOWS_DIR, `${testAgentId}.workflow.yml`),
|
||||||
|
path.join(TEST_DOCS_DIR, `${testAgentId}.mdx`)
|
||||||
|
];
|
||||||
|
|
||||||
|
// Verify files were created
|
||||||
|
expect(fs.existsSync(createdFiles[0])).toBe(true);
|
||||||
|
expect(fs.existsSync(createdFiles[1])).toBe(true);
|
||||||
|
expect(fs.existsSync(createdFiles[2])).toBe(true);
|
||||||
|
expect(fs.existsSync(createdFiles[3])).toBe(true);
|
||||||
|
|
||||||
|
// Verify JSON content
|
||||||
|
const agentJson = JSON.parse(fs.readFileSync(createdFiles[0], "utf8"));
|
||||||
|
expect(agentJson.id).toBe(testAgentId);
|
||||||
|
expect(agentJson.version).toBe("1.0.0");
|
||||||
|
expect(agentJson.metadata.author).toBe("BlackRoad-OS");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips docs when option is set", () => {
|
||||||
|
const result = spawnAgent(testAgentId + "-nodocs", { skipDocs: true });
|
||||||
|
|
||||||
|
expect(result.outputs).toHaveLength(3);
|
||||||
|
expect(result.outputs.some((o: string) => o.includes(".mdx"))).toBe(false);
|
||||||
|
|
||||||
|
// Track created files for cleanup
|
||||||
|
createdFiles = [
|
||||||
|
path.join(TEST_AGENTS_DIR, `${testAgentId}-nodocs.agent.json`),
|
||||||
|
path.join(TEST_AGENTS_DIR, `${testAgentId}-nodocs.prompt.txt`),
|
||||||
|
path.join(TEST_WORKFLOWS_DIR, `${testAgentId}-nodocs.workflow.yml`)
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws error if agent already exists", () => {
|
||||||
|
// Create first agent
|
||||||
|
spawnAgent(testAgentId + "-dup");
|
||||||
|
|
||||||
|
// Track for cleanup
|
||||||
|
createdFiles = [
|
||||||
|
path.join(TEST_AGENTS_DIR, `${testAgentId}-dup.agent.json`),
|
||||||
|
path.join(TEST_AGENTS_DIR, `${testAgentId}-dup.prompt.txt`),
|
||||||
|
path.join(TEST_WORKFLOWS_DIR, `${testAgentId}-dup.workflow.yml`),
|
||||||
|
path.join(TEST_DOCS_DIR, `${testAgentId}-dup.mdx`)
|
||||||
|
];
|
||||||
|
|
||||||
|
// Try to create duplicate
|
||||||
|
expect(() => spawnAgent(testAgentId + "-dup")).toThrow("already exists");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws error if agent ID is missing", () => {
|
||||||
|
expect(() => spawnAgent("")).toThrow("Agent ID is required");
|
||||||
|
expect(() => spawnAgent(null as any)).toThrow("Agent ID is required");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("normalizes agent ID", () => {
|
||||||
|
const result = spawnAgent("Test_Agent.123");
|
||||||
|
expect(result.agentId).toBe("test-agent-123");
|
||||||
|
|
||||||
|
createdFiles = [
|
||||||
|
path.join(TEST_AGENTS_DIR, "test-agent-123.agent.json"),
|
||||||
|
path.join(TEST_AGENTS_DIR, "test-agent-123.prompt.txt"),
|
||||||
|
path.join(TEST_WORKFLOWS_DIR, "test-agent-123.workflow.yml"),
|
||||||
|
path.join(TEST_DOCS_DIR, "test-agent-123.mdx")
|
||||||
|
];
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user