claude-flow-novice
Version:
Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes Local RuVector Accelerator and all CFN skills for complete functionality.
452 lines (432 loc) • 17.9 kB
JavaScript
/**
* Agent Prompt Builder
*
* Builds comprehensive prompts for CLI-spawned agents by combining:
* - Agent definition (YAML + markdown)
* - Task context (taskId, iteration, mode)
* - CFN Loop protocol (if applicable)
* - Iteration history (Sprint 3 - Phase 2)
* - Environment variables
* - Skills (Phase 5 - Skills Database integration)
*/ import { loadIterationHistory, formatIterationHistory } from './iteration-history.js';
import { SkillLoader } from './skill-loader.js';
/**
* Build CLI Mode Redis Completion Protocol for CLI Mode
*/ function buildCLIModeProtocol(taskId, agentId) {
return `
## CLI Mode Redis Completion Protocol
You are running in CLI Mode with Main Chat coordination. Follow this protocol EXACTLY:
### Step 1: Complete Your Work
Execute your assigned task (implementation, review, testing, etc.)
### Step 2: Signal Completion to Main Chat
Send a Redis signal to notify Main Chat that you're finished:
\`\`\`bash
# Use Node.js for Redis communication
node -e "
const { createClient } = require('redis');
const signal = {
agentId: '${agentId}',
taskId: '${taskId}',
status: 'completed',
timestamp: new Date().toISOString(),
provider: process.env.PROVIDER || 'unknown',
model: process.env.MODEL || 'unknown',
confidence: 0.90, // Replace with your actual confidence
metadata: {
iteration: process.env.ITERATION || 1,
mode: process.env.MODE || 'standard'
}
};
(async () => {
const client = createClient({ url: 'redis://localhost:6379' });
await client.connect();
const signalKey = \`cfn:cli:\${process.env.TASK_ID}:completion\`;
await client.lPush(signalKey, JSON.stringify(signal));
console.log(\`✅ Completion signal sent to Main Chat via Redis\`);
await client.disconnect();
})();
"
\`\`\`
### Step 3: Exit Cleanly
After sending the signal, exit immediately. Main Chat is waiting for your Redis signal.
**Why This Protocol:**
- Main Chat uses Redis BLPOP to wait for your completion signal
- Enables simple 2-layer coordination (Main Chat → CLI agents)
- No complex orchestrator needed for CLI mode
- Supports different AI providers via environment variables
**Environment Variables Available:**
- TASK_ID: ${taskId}
- AGENT_ID: ${agentId}
- PROVIDER: AI provider (zai, kimi, anthropic, etc.)
- MODEL: Specific model being used
- ITERATION: Current iteration number
- MODE: Execution mode (mvp, standard, enterprise)
**Main Chat Workflow:**
1. Spawns you via CLI with specific provider/model
2. Waits via \`redis-cli BLPOP cfn:cli:${taskId}:completion\`
3. Processes your completion signal when received
4. Continues with next task or spawns additional agents
**CRITICAL:** Send Redis signal before exiting. Main Chat cannot proceed without your completion signal.
`;
}
/**
* Parse shell variable format into JSON object
* Example: "WORKSPACE='/tmp/test' MODE='standard'" -> {WORKSPACE: '/tmp/test', MODE: 'standard'}
*/ function parseShellVariables(shellContext) {
const jsonObj = {};
const regex = /([A-Z_]+)='([^']*)'/g;
let match;
while((match = regex.exec(shellContext)) !== null){
jsonObj[match[1]] = match[2];
}
return jsonObj;
}
/**
* Parse and enrich JSON context into natural language instructions
*/ function enrichJSONContext(jsonObj) {
const sections = [];
// Extract task description (support both 'task' and 'taskDescription' keys)
const taskText = jsonObj.task || jsonObj.taskDescription;
if (taskText) {
sections.push(`**Task:** ${taskText}`);
}
// Parse files - convert comma-separated string to bullet list
if (jsonObj.files) {
const fileList = typeof jsonObj.files === 'string' ? jsonObj.files.split(',').map((f)=>f.trim()).filter((f)=>f) : Array.isArray(jsonObj.files) ? jsonObj.files : [];
if (fileList.length > 0) {
sections.push('\n**Files to process:**');
fileList.forEach((file)=>sections.push(`- ${file}`));
}
}
// Add requirements/deliverables
if (jsonObj.requirements) {
const reqs = Array.isArray(jsonObj.requirements) ? jsonObj.requirements : [
jsonObj.requirements
];
sections.push('\n**Requirements:**');
reqs.forEach((req, i)=>sections.push(`${i + 1}. ${req}`));
}
if (jsonObj.deliverables) {
const delivs = Array.isArray(jsonObj.deliverables) ? jsonObj.deliverables : [
jsonObj.deliverables
];
sections.push('\n**Deliverables:**');
delivs.forEach((deliv)=>sections.push(`- ${deliv}`));
}
// Add batch information
if (jsonObj.batch) {
sections.push(`\n**Batch:** ${jsonObj.batch}`);
}
// Add directory context - support 'directory', 'WORKSPACE', and 'workspace' keys
const workspacePath = jsonObj.directory || jsonObj.WORKSPACE || jsonObj.workspace;
if (workspacePath) {
sections.push(`\n**Working Directory:** ${workspacePath}`);
}
// Add acceptance criteria
if (jsonObj.acceptanceCriteria) {
const criteria = Array.isArray(jsonObj.acceptanceCriteria) ? jsonObj.acceptanceCriteria : [
jsonObj.acceptanceCriteria
];
sections.push('\n**Acceptance Criteria:**');
criteria.forEach((criterion)=>sections.push(`- ${criterion}`));
}
// Add explicit instructions if present
if (jsonObj.instructions) {
sections.push('\n**Instructions:**');
const instrs = Array.isArray(jsonObj.instructions) ? jsonObj.instructions : [
jsonObj.instructions
];
instrs.forEach((instr, i)=>sections.push(`${i + 1}. ${instr}`));
}
return sections.join('\n');
}
/**
* Build task description from context
*/ function buildTaskDescription(agentType, context) {
let desc = '';
if (context.context) {
// Try to parse context in multiple formats
let contextStr = context.context.trim();
// Parse shell variables BEFORE attempting JSON parse
if (contextStr.includes('=') && !contextStr.startsWith('{')) {
const jsonObj = parseShellVariables(contextStr);
desc = enrichJSONContext(jsonObj);
// Add instruction footer for structured tasks
if (jsonObj.files || jsonObj.deliverables) {
desc += '\n\n**Process each item systematically and report confidence when complete.**';
}
} else if (contextStr.startsWith('{') && contextStr.endsWith('}') || contextStr.startsWith('[') && contextStr.endsWith(']')) {
try {
const jsonObj = JSON.parse(contextStr);
desc = enrichJSONContext(jsonObj);
// Add instruction footer for structured tasks
if (jsonObj.files || jsonObj.deliverables) {
desc += '\n\n**Process each item systematically and report confidence when complete.**';
}
} catch (e) {
// Not valid JSON, treat as plain text
desc = context.context;
}
} else {
// Plain text context
desc = context.context;
}
} else {
desc = `Execute task as ${agentType} agent`;
}
// Add metadata fields
if (context.taskId) {
desc += `\n\n**Task ID:** ${context.taskId}`;
}
if (context.iteration) {
desc += `\n**Iteration:** ${context.iteration}`;
}
if (context.mode) {
desc += `\n**Mode:** ${context.mode}`;
}
if (context.priority) {
desc += `\n**Priority:** ${context.priority}`;
}
if (context.parentTaskId) {
desc += `\n**Parent Task:** ${context.parentTaskId}`;
}
return desc;
}
/**
* Build environment context section
*/ function buildEnvironmentContext(context) {
const env = [];
if (context.taskId) env.push(`TASK_ID=${context.taskId}`);
if (context.iteration) env.push(`ITERATION=${context.iteration}`);
if (context.mode) env.push(`MODE=${context.mode}`);
if (context.priority) env.push(`PRIORITY=${context.priority}`);
if (context.parentTaskId) env.push(`PARENT_TASK_ID=${context.parentTaskId}`);
// Docker workspace detection
const isDockerEnv = process.env.DOCKER_AGENT === 'true' || process.env.WORKSPACE_ROOT;
const workspaceRoot = process.env.WORKSPACE_ROOT || '/workspace';
if (isDockerEnv) {
env.push(`WORKSPACE_ROOT=${workspaceRoot}`);
}
// Always include Docker context if detected, even if env array is empty
if (isDockerEnv && env.length === 0) {
env.push(`WORKSPACE_ROOT=${workspaceRoot}`);
}
if (env.length === 0 && !isDockerEnv) return '';
let contextText = `
## Environment Variables
\`\`\`bash
${env.join('\n')}
\`\`\`
`;
// Add Docker workspace notice if detected
if (isDockerEnv) {
contextText += `
## Docker Container Environment
**CRITICAL:** You are running inside a Docker container.
- **Working Directory:** \`${workspaceRoot}\`
- **File Paths:** All file operations use \`${workspaceRoot}/\` prefix
- **Example:** To read \`src/file.ts\`, use \`${workspaceRoot}/src/file.ts\`
**DO NOT** use paths from your training data or Main Chat context. Use \`${workspaceRoot}/\` for all file operations.
`;
}
return contextText;
}
/**
* Load skills for agent (Phase 5: Skills Database Integration)
*/ async function loadSkillsForAgent(agentType, context) {
// Feature flag check
if (process.env.CFN_SKILLS_DATABASE !== 'true') {
return [];
}
try {
const dbPath = process.env.CFN_SKILLS_DB_PATH || './.claude/skills-database/skills.db';
const skillLoader = new SkillLoader(dbPath, {
enableCache: true,
cacheMaxSize: 100,
cacheTTL: 60000 // 1 minute
});
// Extract keywords from context for filtering
const keywords = context.keywords || context.context?.toLowerCase() || '';
const skills = await skillLoader.loadSkillsForAgent(agentType, {
taskId: context.taskId,
keywords,
phase: context.phase,
mode: context.mode,
iteration: context.iteration
});
skillLoader.close();
return skills;
} catch (error) {
console.warn(`[agent-prompt-builder] Failed to load skills: ${error}`);
return [];
}
}
/**
* Format skills for prompt injection
*/ function formatSkillsForPrompt(skills) {
if (skills.length === 0) {
return '';
}
const sections = [];
sections.push('## Applicable Skills');
sections.push('');
sections.push('The following skills have been loaded based on your agent type and task context:');
sections.push('');
for (const skill of skills){
const approvalBadge = skill.approvalLevel === 'auto' ? '✓' : skill.approvalLevel === 'escalate' ? '⚠' : '✋';
sections.push(`### ${skill.name} (v${skill.version}) [${approvalBadge} ${skill.approvalLevel}]`);
sections.push('');
if (skill.content) {
sections.push(skill.content);
} else {
sections.push(`*Skill content not available*`);
}
sections.push('');
}
return sections.join('\n');
}
/**
* Build complete prompt for agent execution (async for iteration history + skills)
*/ export async function buildAgentPrompt(definition, context) {
// Use explicit agent ID if provided, otherwise generate from name + iteration
const agentId = context.agentId || `${definition.name}-${context.iteration || 1}`;
const startTime = Date.now();
const sections = [];
// 1. Agent definition header
sections.push(`# Agent: ${definition.name}`);
sections.push('');
sections.push(definition.description);
sections.push('');
// 2. Task description
sections.push('## Task');
sections.push('');
sections.push(buildTaskDescription(definition.name, context));
sections.push('');
// 3. Iteration history (Sprint 3 - Phase 2)
// Load and format previous iterations if iteration > 1
if (context.taskId && context.iteration && context.iteration > 1) {
try {
const history = await loadIterationHistory(context.taskId, agentId, context.iteration);
const historyText = formatIterationHistory(history, context.iteration);
sections.push(historyText);
sections.push('');
} catch (err) {
console.warn(`[agent-prompt-builder] Failed to load iteration history:`, err);
// Continue without history
}
}
// 4. Agent definition content (from markdown file)
sections.push('## Agent Definition');
sections.push('');
sections.push(definition.content);
sections.push('');
// 4a. Load and inject skills (Phase 5: Skills Database Integration)
try {
const skills = await loadSkillsForAgent(definition.type || definition.name, context);
if (skills.length > 0) {
const skillsText = formatSkillsForPrompt(skills);
sections.push(skillsText);
sections.push('');
// Log skill usage for analytics (Phase 5)
if (process.env.CFN_SKILLS_DATABASE === 'true' && context.taskId) {
try {
const dbPath = process.env.CFN_SKILLS_DB_PATH || './.claude/skills-database/skills.db';
const skillLoader = new SkillLoader(dbPath);
await skillLoader.logSkillUsage({
agentId: agentId,
agentType: definition.type || definition.name,
skillIds: skills.map((s)=>s.id).filter((id)=>id > 0),
taskId: context.taskId,
phase: context.phase,
loadedAt: new Date(),
executionTimeMs: Date.now() - startTime
});
skillLoader.close();
} catch (logError) {
console.warn(`[agent-prompt-builder] Failed to log skill usage: ${logError}`);
}
}
}
} catch (skillError) {
console.warn(`[agent-prompt-builder] Skill loading failed: ${skillError}`);
// Continue without skills
}
// 5. CLI Mode protocol (ALWAYS inject when taskId present - enables Redis coordination)
if (context.taskId && agentId) {
sections.push(buildCLIModeProtocol(context.taskId, agentId));
sections.push('');
}
// 6. Environment context
const envContext = buildEnvironmentContext(context);
if (envContext) {
sections.push(envContext);
sections.push('');
}
// 7. Execution instructions
sections.push('## Execution Instructions');
sections.push('');
sections.push('1. Read and understand the task requirements');
if (context.iteration && context.iteration > 1) {
sections.push('2. Review iteration history and feedback from validators');
sections.push('3. Address specific feedback points from previous iteration');
sections.push('4. Execute your core responsibilities as defined above');
} else {
sections.push('2. Execute your core responsibilities as defined above');
sections.push('3. Follow any protocol steps (CFN Loop, validation hooks, etc.)');
}
sections.push('4. Provide clear, concise output');
sections.push('5. Report confidence score if applicable');
sections.push('');
// 7a. Pre-edit backup requirement (MANDATORY)
sections.push('## Pre-Edit Backup Protocol (MANDATORY)');
sections.push('');
sections.push('**BEFORE ANY Edit/Write/MultiEdit operation, you MUST create a backup:**');
sections.push('');
sections.push('```bash');
sections.push(`BACKUP_PATH=$(./.claude/hooks/cfn-invoke-pre-edit.sh "$FILE_TO_EDIT" --agent-id "${agentId}")`);
sections.push('```');
sections.push('');
sections.push('**Why:** Enables safe file revert without git operations during parallel sessions.');
sections.push('**Location:** `.backups/[agent-id]/[timestamp]_[hash]/`');
sections.push('**Retention:** 24h TTL (configurable)');
sections.push('');
sections.push('**Complete Edit Workflow:**');
sections.push('```bash');
sections.push('# 1. Pre-Edit: Create backup');
sections.push(`BACKUP_PATH=$(./.claude/hooks/cfn-invoke-pre-edit.sh "src/file.ts" --agent-id "${agentId}")`);
sections.push('');
sections.push('# 2. Edit: Perform file modification');
sections.push('Edit: file_path="src/file.ts" old_string="..." new_string="..."');
sections.push('');
sections.push('# 3. Post-Edit: Validate changes');
sections.push(`./.claude/hooks/cfn-invoke-post-edit.sh "src/file.ts" --agent-id "${agentId}"`);
sections.push('```');
sections.push('');
// 8. Tool reminder
if (definition.tools && definition.tools.length > 0) {
sections.push('## Available Tools');
sections.push('');
sections.push(`You have access to: ${definition.tools.join(', ')}`);
sections.push('');
}
return sections.join('\n');
}
/**
* Extract agent ID from context
* If agentId is explicitly provided in context, use it; otherwise generate from name + iteration
*/ export function getAgentId(definition, context) {
if (context.agentId) {
return context.agentId;
}
return `${definition.name}-${context.iteration || 1}`;
}
/**
* Build system prompt for agent (optional, for structured agent behavior)
*/ export function buildSystemPrompt(definition) {
return `You are ${definition.name}, a specialized AI agent.
Type: ${definition.type || 'specialist'}
Model: ${definition.model}
Tools: ${definition.tools.join(', ')}
Follow your agent definition exactly and complete assigned tasks with high quality.`;
}
//# sourceMappingURL=agent-prompt-builder.js.map