aboutsummaryrefslogtreecommitdiff
path: root/modules/core/module-orchestrator.js
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-06-08 18:01:06 +0200
committerAgando <roduccomm@gmail.com>2026-06-08 18:01:06 +0200
commitb562e1491513163921593985f3542d4ee8ae0e75 (patch)
treef768a2378d3c09b3685a7bf42a9fc4dec71ef31c /modules/core/module-orchestrator.js
parentb9485bae5c2e62fa9c1b1c56d0f75be67cb47c37 (diff)
auto: 2026-06-08_18:00 changes
Diffstat (limited to 'modules/core/module-orchestrator.js')
-rw-r--r--modules/core/module-orchestrator.js660
1 files changed, 624 insertions, 36 deletions
diff --git a/modules/core/module-orchestrator.js b/modules/core/module-orchestrator.js
index 05e1598..bcbd00d 100644
--- a/modules/core/module-orchestrator.js
+++ b/modules/core/module-orchestrator.js
@@ -23,10 +23,18 @@ class OrchestratorModule {
this._restartBackoffs = {};
this._eventBus = {};
this._idleSweepInterval = null;
+ this._spawnLock = false;
+ this._CONSENSUS_SIZE = 3;
+ this._CONSENSUS_STRATEGY = 'majority';
+ this._CONSENSUS_TIMEOUT = 60000;
+ this._consensusRounds = [];
+ this._activeConsensus = null;
+ this._pipelineHistory = [];
+ this._PIPELINE_CONFIG = { autoFix: false, stageTimeout: 30000 };
this._loadConfig();
}
_loadConfig() {
- try { const c = localStorage.getItem('webx11_orchestrator_config'); if (c) { const p = JSON.parse(c); if (p.spawnLimit) this._SPAWN_LIMIT = p.spawnLimit; if (p.heartbeatTimeout) this._HEARTBEAT_TIMEOUT = p.heartbeatTimeout; if (p.idleTimeout) this._IDLE_TIMEOUT = p.idleTimeout; if (p.restartPolicy) this._RESTART_POLICY = p.restartPolicy; if (p.a2aPollInterval) this._A2A_POLL_INTERVAL = p.a2aPollInterval; if (p.heartbeatCheck) this._HEARTBEAT_CHECK = p.heartbeatCheck; if (p.idleSweepInterval) this._IDLE_SWEEP_INTERVAL = p.idleSweepInterval; if (p.deadPurgeTimeout) this._DEAD_PURGE_TIMEOUT = p.deadPurgeTimeout; } } catch (e) {}
+ try { const c = localStorage.getItem('webx11_orchestrator_config'); if (c) { const p = JSON.parse(c); if (p.spawnLimit) this._SPAWN_LIMIT = p.spawnLimit; if (p.heartbeatTimeout) this._HEARTBEAT_TIMEOUT = p.heartbeatTimeout; if (p.idleTimeout) this._IDLE_TIMEOUT = p.idleTimeout; if (p.restartPolicy) this._RESTART_POLICY = p.restartPolicy; if (p.a2aPollInterval) this._A2A_POLL_INTERVAL = p.a2aPollInterval; if (p.heartbeatCheck) this._HEARTBEAT_CHECK = p.heartbeatCheck; if (p.idleSweepInterval) this._IDLE_SWEEP_INTERVAL = p.idleSweepInterval; if (p.deadPurgeTimeout) this._DEAD_PURGE_TIMEOUT = p.deadPurgeTimeout; if (p.consensusSize) this._CONSENSUS_SIZE = p.consensusSize; if (p.consensusStrategy) this._CONSENSUS_STRATEGY = p.consensusStrategy; if (p.consensusTimeout) this._CONSENSUS_TIMEOUT = p.consensusTimeout; if (p.pipelineAutoFix !== undefined) this._PIPELINE_CONFIG.autoFix = p.pipelineAutoFix; if (p.pipelineStageTimeout) this._PIPELINE_CONFIG.stageTimeout = p.pipelineStageTimeout; } } catch (e) {}
}
_saveConfig() {
localStorage.setItem('webx11_orchestrator_config', JSON.stringify({
@@ -37,7 +45,12 @@ class OrchestratorModule {
a2aPollInterval: this._A2A_POLL_INTERVAL,
heartbeatCheck: this._HEARTBEAT_CHECK,
idleSweepInterval: this._IDLE_SWEEP_INTERVAL,
- deadPurgeTimeout: this._DEAD_PURGE_TIMEOUT
+ deadPurgeTimeout: this._DEAD_PURGE_TIMEOUT,
+ consensusSize: this._CONSENSUS_SIZE,
+ consensusStrategy: this._CONSENSUS_STRATEGY,
+ consensusTimeout: this._CONSENSUS_TIMEOUT,
+ pipelineAutoFix: this._PIPELINE_CONFIG.autoFix,
+ pipelineStageTimeout: this._PIPELINE_CONFIG.stageTimeout
}));
}
registerCommands() {
@@ -53,10 +66,75 @@ class OrchestratorModule {
this.commands['orchestrator-restart-policy'] = (args) => this.setRestartPolicy(args);
this.commands['a2a-send'] = (args) => this.sendA2AMessage(args);
this.commands['a2a-list'] = () => this.listA2AMessages();
+ this.commands['orchestrate-parallel'] = (args) => this.delegateTask(args.join(' '), { parallel: true });
+ this.commands['orchestrate-decompose'] = (args) => this.showDecomposition(args.join(' '));
+ this.commands['orchestrate-result'] = () => this.showParallelResult();
+ this.commands['consensus'] = (args) => this.handleConsensus(args);
+ this.commands['consensus-status'] = () => this.consensusStatus();
+ this.commands['consensus-history'] = () => this.consensusHistory();
+ this.commands['pipeline'] = (args) => this.runPipeline(args);
+ }
+ // ─── Task Decomposition Engine ───
+ _decomposeGoal(goal) {
+ if (!goal || typeof goal !== 'string') return [{ goal }];
+ const lower = goal;
+ // Heuristic: only split at conjunctions when both sides have distinct action verbs
+ // Action verb patterns from _deriveCapabilities
+ const ACTION_VERBS = [
+ 'search', 'find', 'research', 'lookup', 'query',
+ 'write', 'create', 'generate', 'compose', 'draft', 'save',
+ 'delete', 'remove', 'trash', 'rm',
+ 'read', 'view', 'cat', 'show', 'display', 'inspect', 'preview',
+ 'edit', 'modify', 'update', 'patch', 'change', 'fix',
+ 'install', 'package', 'setup',
+ 'list', 'status', 'health', 'audit', 'report', 'check',
+ 'fetch', 'scrape', 'download',
+ 'build', 'develop', 'compile',
+ 'analyze', 'analyse', 'summarize', 'review', 'investigate',
+ 'deploy', 'publish', 'upload',
+ 'monitor', 'watch', 'poll', 'track', 'observe',
+ 'test', 'verify', 'cmp', 'diff', 'compare'
+ ];
+ const hasActionVerb = (s) => ACTION_VERBS.some(v => {
+ const re = new RegExp('\\b' + v + '\\b', 'i');
+ return re.test(s);
+ });
+ // Try splitting on " and " / " then " / " also " / "; " / ", then"
+ const separators = [
+ /\s+and\s+/i,
+ /\s+then\s+/i,
+ /\s+also\s+/i,
+ /;\s+/,
+ /,\s+then\s+/i,
+ /\s+followed by\s+/i,
+ ];
+ let bestSplit = null;
+ for (const sep of separators) {
+ const parts = goal.split(sep);
+ if (parts.length >= 2 && parts.every(p => hasActionVerb(p.trim()))) {
+ bestSplit = parts.map(p => p.trim()).filter(p => p.length > 3);
+ break; // first matching separator wins
+ }
+ }
+ if (!bestSplit || bestSplit.length < 2) {
+ return [{ goal }]; // atomic
+ }
+ return bestSplit.map(sub => ({
+ goal: sub,
+ role: this._deriveRole(sub),
+ caps: this._deriveCapabilities(sub)
+ }));
}
getHelpInfo() {
return {
'orchestrate': 'orchestrate <task> — Delegate a task to an available agent (auto-spawns if needed)',
+ 'orchestrate-parallel': 'orchestrate-parallel <task> — Decompose and delegate sub-tasks in parallel (auto-splits on "and"/"then")',
+ 'orchestrate-decompose': 'orchestrate-decompose <task> — Preview how a task would be decomposed into sub-tasks',
+ 'orchestrate-result': 'orchestrate-result — Show the last parallel batch results (stored in VFS)',
+ 'consensus': 'consensus <question> — Start a consensus round: spawn 3 agents, debate the question, resolve by majority',
+ 'consensus-status': 'consensus-status — Show current/active consensus round status',
+ 'consensus-history': 'consensus-history — Show past consensus rounds and their resolutions',
+ 'pipeline': 'pipeline <test|deploy|verify|history|config> — Autonomous CI/CD: test agents/queue/modules, deploy (test→auto-fix→verify), history (show runs), config (show config)',
'orchestrator-status': 'orchestrator-status — Show agent pool status and config',
'orchestrator-spawn': 'orchestrator-spawn [role] [capabilities...] — Manually spawn a new agent',
'orchestrator-kill': 'orchestrator-kill <agent-id> — Terminate an agent and revoke leases',
@@ -234,35 +312,44 @@ class OrchestratorModule {
return (name || 'worker').replace(/[^a-zA-Z0-9_-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '').substring(0, 32) || 'worker';
}
spawnAgent(role, capabilities) {
- const safeRole = this._sanitizeId(role);
- const id = `agent-${Object.keys(this._agents).length + 1}-${safeRole}`;
- if (Object.keys(this._agents).length >= this._SPAWN_LIMIT) {
- this.terminal.print(`❌ Agent spawn limit (${this._SPAWN_LIMIT}) reached`, 'error');
+ if (this._spawnLock) {
+ this.terminal.print('⚠️ Spawn in progress — queued', 'warning');
return null;
}
- const agent = {
- id,
- role: role || 'worker',
- capabilities: capabilities.length ? capabilities : ['list_files', 'read_file', 'web_search'],
- createdAt: new Date().toISOString(),
- lastHeartbeat: Date.now(),
- status: 'idle',
- currentTask: null,
- leaseId: null,
- restartPolicy: this._RESTART_POLICY,
- failureCount: 0,
- restartCount: 0,
- nextRestartAt: null
- };
- this._agents[id] = agent;
- // Create VFS agent directory with status
- if (!this.terminal.fs.readFile(`/agents/${id}/`)) this.terminal.fs.mkdir(`agents/${id}`);
- this.terminal.fs.writeFile(`agents/${id}/status.json`, JSON.stringify(agent));
- // Register in agent registry
- const registry = this.terminal.moduleManager.modules['AgentRegistryModule'];
- if (registry) registry.registerAgent(['register', id, ...agent.capabilities]);
- this.terminal.print(`🤖 Agent '${id}' spawned (role: ${role || 'worker'}, ${agent.capabilities.length} caps)`, 'success');
- return agent;
+ this._spawnLock = true;
+ try {
+ const safeRole = this._sanitizeId(role);
+ const id = `agent-${Date.now().toString(36)}-${Math.random().toString(36).substr(2, 4)}-${safeRole}`;
+ if (Object.keys(this._agents).length >= this._SPAWN_LIMIT) {
+ this.terminal.print(`❌ Agent spawn limit (${this._SPAWN_LIMIT}) reached`, 'error');
+ return null;
+ }
+ const agent = {
+ id,
+ role: role || 'worker',
+ capabilities: capabilities.length ? capabilities : ['list_files', 'read_file', 'web_search'],
+ createdAt: new Date().toISOString(),
+ lastHeartbeat: Date.now(),
+ status: 'idle',
+ currentTask: null,
+ leaseId: null,
+ restartPolicy: this._RESTART_POLICY,
+ failureCount: 0,
+ restartCount: 0,
+ nextRestartAt: null
+ };
+ this._agents[id] = agent;
+ // Create VFS agent directory with status
+ if (!this.terminal.fs.readFile(`/agents/${id}/`)) this.terminal.fs.mkdir(`agents/${id}`);
+ this.terminal.fs.writeFile(`agents/${id}/status.json`, JSON.stringify(agent));
+ // Register in agent registry
+ const registry = this.terminal.moduleManager.modules['AgentRegistryModule'];
+ if (registry) registry.registerAgent(['register', id, ...agent.capabilities]);
+ this.terminal.print(`🤖 Agent '${id}' spawned (role: ${role || 'worker'}, ${agent.capabilities.length} caps)`, 'success');
+ return agent;
+ } finally {
+ this._spawnLock = false;
+ }
}
killAgent(id) {
@@ -460,7 +547,12 @@ class OrchestratorModule {
'a2a-poll-interval': () => { const n = parseInt(val); if (isNaN(n) || n < 50 || n > 30000) return 'Must be 50-30000ms'; clearInterval(this._a2aPollTimer); this._a2aPollTimer = null; this._A2A_POLL_INTERVAL = n; if (this._A2A_POLLING) { this._a2aPollTimer = setInterval(() => this._pollAgentMailboxes(), n); } return `a2a-poll-interval set to ${n}ms`; },
'heartbeat-check': () => { const n = parseInt(val); if (isNaN(n) || n < 500 || n > 60000) return 'Must be 500-60000ms'; clearInterval(this._heartbeatInterval); this._HEARTBEAT_CHECK = n; this._heartbeatInterval = setInterval(() => this._checkHeartbeats(), n); return `heartbeat-check set to ${n}ms`; },
'idle-sweep-interval': () => { const n = parseInt(val); if (isNaN(n) || n < 5000 || n > 600000) return 'Must be 5000-600000ms'; clearInterval(this._idleSweepInterval); this._IDLE_SWEEP_INTERVAL = n; this._idleSweepInterval = setInterval(() => this._sweepIdle(), n); return `idle-sweep-interval set to ${n}ms`; },
- 'dead-purge-timeout': () => { const n = parseInt(val); if (isNaN(n) || n < 5000 || n > 600000) return 'Must be 5000-600000ms'; this._DEAD_PURGE_TIMEOUT = n; return `dead-purge-timeout set to ${n}ms`; }
+ 'dead-purge-timeout': () => { const n = parseInt(val); if (isNaN(n) || n < 5000 || n > 600000) return 'Must be 5000-600000ms'; this._DEAD_PURGE_TIMEOUT = n; return `dead-purge-timeout set to ${n}ms`; },
+ 'consensus-size': () => { const n = parseInt(val); if (isNaN(n) || n < 2 || n > 10) return 'Must be 2-10'; this._CONSENSUS_SIZE = n; return `consensus-size set to ${n}`; },
+ 'consensus-strategy': () => { if (!['majority', 'unanimous', 'weighted'].includes(val)) return 'Must be majority, unanimous, or weighted'; this._CONSENSUS_STRATEGY = val; return `consensus-strategy set to ${val}`; },
+ 'consensus-timeout': () => { const n = parseInt(val); if (isNaN(n) || n < 5000 || n > 300000) return 'Must be 5000-300000ms'; this._CONSENSUS_TIMEOUT = n; return `consensus-timeout set to ${n}ms`; },
+ 'pipeline-auto-fix': () => { this._PIPELINE_CONFIG.autoFix = val === 'on' || val === 'true'; return `auto-fix ${this._PIPELINE_CONFIG.autoFix ? 'on' : 'off'}`; },
+ 'pipeline-stage-timeout': () => { const n = parseInt(val); if (isNaN(n) || n < 5000 || n > 120000) return 'Must be 5000-120000ms'; this._PIPELINE_CONFIG.stageTimeout = n; return `stage-timeout set to ${n}ms`; }
};
if (!configMap[key]) {
@@ -471,25 +563,514 @@ class OrchestratorModule {
this._saveConfig();
}
+ // ─── Show Decomposition Preview ───
+
+ showDecomposition(goal) {
+ if (!goal) return this.terminal.print('Usage: orchestrate-decompose <goal>', 'error');
+ const parts = this._decomposeGoal(goal);
+ this.terminal.print('🔍 Decomposition of "' + goal.substring(0, 60) + '":', 'header');
+ if (parts.length === 1 && parts[0].goal === goal) {
+ this.terminal.print(' → Atomic (no split detected)', 'info');
+ return;
+ }
+ for (let i = 0; i < parts.length; i++) {
+ const p = parts[i];
+ this.terminal.print(' [' + (i + 1) + '] ' + p.goal.substring(0, 50), 'success');
+ this.terminal.print(' Role: ' + p.role + ' | Caps: ' + (p.caps || []).join(', '), 'text-muted');
+ }
+ this.terminal.print('→ ' + parts.length + ' sub-task(s) — run with: orchestrate-parallel "' + goal + '"', 'info');
+ }
+
+ // ─── Show Last Parallel Result ───
+
+ showParallelResult() {
+ const data = this.terminal.fs.fsReadJSON('orchestrator/parallel-results.json');
+ if (!data) return this.terminal.print('No parallel results saved yet', 'info');
+ this.terminal.print('📊 Last Parallel Batch Results', 'header');
+ this.terminal.print(' Task: ' + (data.parent_task || '').substring(0, 60), 'info');
+ this.terminal.print(' ' + data.successes + '/' + data.total + ' succeeded' + (data.failures > 0 ? ', ' + data.failures + ' failed' : ''), 'info');
+ this.terminal.print(' Timestamp: ' + data.timestamp, 'text-muted');
+ for (const s of (data.summaries || [])) {
+ const icon = s.status === 'ok' ? '✅' : '❌';
+ const snippet = s.result ? s.result.replace(/\n/g, ' ').substring(0, 80) : (s.error || '');
+ this.terminal.print(' ' + icon + ' ' + s.task.substring(0, 40) + ' → ' + snippet, 'info');
+ }
+ }
+
+ // ─── Consensus Protocol ───
+
+ async handleConsensus(args) {
+ if (!args || args.length === 0) return this.terminal.print('Usage: consensus <question>', 'error');
+ const question = args.join(' ');
+ if (this._activeConsensus) return this.terminal.print('❌ A consensus round is already in progress — wait for it to resolve', 'error');
+
+ const roundId = `consensus-${Date.now().toString(36)}-${Math.random().toString(36).substr(2, 4)}`;
+ const size = this._CONSENSUS_SIZE;
+ this.terminal.print(`🤝 Starting consensus round ${roundId} (${size} agents, strategy: ${this._CONSENSUS_STRATEGY})`, 'header');
+ this.terminal.print(` Question: ${question.substring(0, 80)}`, 'info');
+
+ this._activeConsensus = {
+ id: roundId,
+ question,
+ strategy: this._CONSENSUS_STRATEGY,
+ size,
+ votes: [],
+ startedAt: new Date().toISOString(),
+ resolved: false,
+ result: null
+ };
+
+ // Spawn N agents with same question
+ const tasks = [];
+ for (let i = 0; i < size; i++) {
+ tasks.push(`analyze and answer: ${question}. Provide your reasoning then your final answer as "VOTE: <answer>"`);
+ }
+
+ const timeout = setTimeout(() => {
+ if (this._activeConsensus && !this._activeConsensus.resolved) {
+ this._resolveConsensus('timeout');
+ }
+ }, this._CONSENSUS_TIMEOUT);
+
+ try {
+ const results = await Promise.allSettled(tasks.map(t => this.delegateTask(t)));
+
+ for (let i = 0; i < results.length; i++) {
+ const r = results[i];
+ const voteText = r.status === 'fulfilled' ? (r.value || '') : 'FAILED';
+ // Extract VOTE: marker or use full text
+ let answer = 'no vote';
+ const voteMatch = voteText.match(/VOTE:\s*(.+?)(?:\n|$)/i);
+ if (voteMatch) answer = voteMatch[1].trim();
+ else if (voteText.length > 0 && voteText !== 'FAILED') answer = voteText.substring(0, 60).trim();
+
+ this._activeConsensus.votes.push({
+ agentIdx: i,
+ answer,
+ raw: voteText.substring(0, 200),
+ status: r.status === 'fulfilled' ? 'voted' : 'failed'
+ });
+ }
+ } catch (e) {
+ this.terminal.print(`⚠️ Consensus error: ${e.message}`, 'warning');
+ } finally {
+ clearTimeout(timeout);
+ }
+
+ this._resolveConsensus('complete');
+ return this._activeConsensus.result;
+ }
+
+ _resolveConsensus(reason) {
+ const round = this._activeConsensus;
+ if (!round || round.resolved) return;
+ round.resolved = true;
+ round.resolvedAt = new Date().toISOString();
+ round.resolvedBy = reason;
+
+ // Count votes
+ const voteCounts = {};
+ for (const v of round.votes) {
+ if (v.status === 'failed') continue;
+ voteCounts[v.answer] = (voteCounts[v.answer] || 0) + 1;
+ }
+
+ let winner = null;
+ let maxVotes = 0;
+ let totalVoted = 0;
+ for (const [ans, count] of Object.entries(voteCounts)) {
+ totalVoted += count;
+ if (count > maxVotes) {
+ maxVotes = count;
+ winner = ans;
+ }
+ }
+
+ if (this._CONSENSUS_STRATEGY === 'majority') {
+ const needed = Math.ceil(round.size / 2);
+ if (maxVotes >= needed) {
+ round.result = { consensus: 'reached', answer: winner, votes: maxVotes, total: round.size };
+ } else {
+ round.result = { consensus: 'hung', votes: voteCounts, total: round.size };
+ }
+ } else if (this._CONSENSUS_STRATEGY === 'unanimous') {
+ if (totalVoted === round.size && Object.keys(voteCounts).length === 1) {
+ round.result = { consensus: 'reached', answer: winner, votes: maxVotes, total: round.size };
+ } else {
+ round.result = { consensus: 'hung', votes: voteCounts, total: round.size };
+ }
+ } else {
+ // weighted — fallback to majority
+ const needed = Math.ceil(round.size / 2);
+ round.result = maxVotes >= needed
+ ? { consensus: 'reached', answer: winner, votes: maxVotes, total: round.size }
+ : { consensus: 'hung', votes: voteCounts, total: round.size };
+ }
+
+ // Store the round
+ this._consensusRounds.push(round);
+ if (!this.terminal.fs.readDir('orchestrator')) this.terminal.fs.mkdir('orchestrator');
+ this.terminal.fs.writeFile(`orchestrator/consensus-round-${round.id}.json`, JSON.stringify(round, null, 2));
+
+ // Print result
+ const r = round.result;
+ if (r.consensus === 'reached') {
+ this.terminal.print(`✅ Consensus reached: "${r.answer}" (${r.votes}/${r.total} votes, ${round.strategy})`, 'success');
+ } else {
+ this.terminal.print(`⚖️ Consensus hung — no clear winner. Votes: ${JSON.stringify(r.votes)}`, 'warning');
+ }
+
+ this._activeConsensus = null;
+ }
+
+ consensusStatus() {
+ if (this._activeConsensus) {
+ const r = this._activeConsensus;
+ this.terminal.print('🤝 Active Consensus Round', 'header');
+ this.terminal.print(` ID: ${r.id}`, 'info');
+ this.terminal.print(` Question: ${r.question.substring(0, 60)}`, 'info');
+ this.terminal.print(` Strategy: ${r.strategy} | Size: ${r.size}`, 'info');
+ this.terminal.print(` Votes collected: ${r.votes.length}/${r.size}`, 'info');
+ for (const v of r.votes) {
+ this.terminal.print(` ${v.status === 'voted' ? '✅' : '❌'} Agent ${v.agentIdx}: ${v.answer}`, 'info');
+ }
+ } else {
+ this.terminal.print('No active consensus round', 'info');
+ }
+ }
+
+ consensusHistory() {
+ const rounds = this._consensusRounds;
+ if (rounds.length === 0) {
+ this.terminal.print('No consensus rounds in memory — checking VFS...', 'info');
+ // Try VFS
+ const dir = this.terminal.fs.readDir('orchestrator/');
+ if (dir) {
+ const files = dir.filter(e => e.name && e.name.startsWith('consensus-round-'));
+ if (files.length > 0) {
+ this.terminal.print(`📜 ${files.length} round(s) in VFS`, 'header');
+ for (const f of files.slice(-5)) {
+ const data = this.terminal.fs.fsReadJSON(`orchestrator/${f.name}`);
+ if (data) {
+ const r = data.result;
+ const status = r?.consensus === 'reached' ? '✅' : '⚖️';
+ this.terminal.print(` ${status} ${data.id}: "${data.question.substring(0, 40)}" → ${r?.answer || 'no result'}`, 'info');
+ }
+ }
+ return;
+ }
+ }
+ this.terminal.print('No past consensus rounds found', 'info');
+ return;
+ }
+ this.terminal.print('📜 Consensus History', 'header');
+ for (const round of rounds.slice(-10)) {
+ const r = round.result;
+ const status = r?.consensus === 'reached' ? '✅' : '⚖️';
+ this.terminal.print(` ${status} ${round.id}: "${round.question.substring(0, 40)}"`, 'info');
+ this.terminal.print(` Strategy: ${round.strategy} | Votes: ${round.votes.length}/${round.size} | Result: ${r?.answer || 'hung'}`, 'text-muted');
+ }
+ }
+
+ // ─── Autonomous CI/CD Pipeline ───
+
+ async runPipeline(args) {
+ if (!args || args.length === 0) return this.terminal.print('Usage: pipeline <command> [options]', 'error');
+ const cmd = args[0].toLowerCase();
+ const opts = args.slice(1);
+
+ const run = {
+ id: `pipeline-${Date.now().toString(36)}-${Math.random().toString(36).substr(2, 4)}`,
+ command: cmd,
+ startedAt: new Date().toISOString(),
+ stages: [],
+ status: 'running'
+ };
+
+ try {
+ if (cmd === 'test') {
+ await this._pipelineStage(run, 'agent-health', () => this._testAgentHealth());
+ await this._pipelineStage(run, 'a2a-mailbox', () => this._testA2AMailbox());
+ await this._pipelineStage(run, 'module-check', () => this._testModules());
+ run.status = 'passed';
+ this.terminal.print('✅ All pipeline tests passed', 'success');
+ } else if (cmd === 'deploy') {
+ const deployTask = opts.join(' ') || 'run all checks';
+ await this._pipelineStage(run, 'test', () => this.runPipeline(['test']));
+ if (run.stages.find(s => s.name === 'test' && s.status === 'failed')) {
+ if (this._PIPELINE_CONFIG.autoFix) {
+ await this._pipelineStage(run, 'auto-fix', () => this._autoFix());
+ } else {
+ run.status = 'blocked';
+ this.terminal.print('❌ Pipeline blocked — tests failed. Set pipeline-auto-fix on to auto-repair', 'error');
+ return;
+ }
+ }
+ await this._pipelineStage(run, 'verify', () => this._pipelineVerify());
+ run.status = 'passed';
+ this.terminal.print('✅ Pipeline deploy passed all stages', 'success');
+ } else if (cmd === 'verify') {
+ await this._pipelineStage(run, 'verify', () => this._pipelineVerify());
+ run.status = 'passed';
+ } else if (cmd === 'history') {
+ this._pipelineHistory.slice(-10).forEach(p => {
+ const icon = p.status === 'passed' ? '✅' : (p.status === 'blocked' ? '🚫' : '❌');
+ this.terminal.print(` ${icon} ${p.id}: ${p.command} (${p.startedAt.slice(11, 19)})`, 'info');
+ p.stages.forEach(s => {
+ this.terminal.print(` ${s.status === 'passed' ? '✅' : '❌'} ${s.name} (${(s.duration || 0).toFixed(0)}ms)`, 'text-muted');
+ });
+ });
+ if (this._pipelineHistory.length === 0) this.terminal.print('No pipeline runs yet', 'info');
+ return;
+ } else if (cmd === 'config') {
+ this.terminal.print('Pipeline Config:', 'header');
+ this.terminal.print(` auto-fix: ${this._PIPELINE_CONFIG.autoFix ? 'on' : 'off'}`, 'info');
+ this.terminal.print(` stage-timeout: ${this._PIPELINE_CONFIG.stageTimeout}ms`, 'info');
+ return;
+ } else {
+ return this.terminal.print('Unknown pipeline command. Usage: test, deploy, verify, history, config', 'error');
+ }
+ } catch (e) {
+ run.status = 'failed';
+ run.error = e.message;
+ this.terminal.print(`❌ Pipeline failed: ${e.message}`, 'error');
+ }
+
+ run.completedAt = new Date().toISOString();
+ this._pipelineHistory.push(run);
+ if (!this.terminal.fs.readDir('orchestrator')) this.terminal.fs.mkdir('orchestrator');
+ this.terminal.fs.writeFile(`orchestrator/pipeline-${run.id}.json`, JSON.stringify(run, null, 2));
+ return run;
+ }
+
+ async _pipelineStage(run, name, fn) {
+ const stage = { name, status: 'running', startedAt: Date.now() };
+ run.stages.push(stage);
+ this.terminal.print(`⏳ Pipeline stage: ${name}...`, 'info');
+
+ try {
+ const timeout = this._PIPELINE_CONFIG.stageTimeout;
+ const result = await Promise.race([
+ fn(),
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`${name} timed out (${timeout}ms)`)), timeout))
+ ]);
+ stage.status = 'passed';
+ stage.duration = Date.now() - stage.startedAt;
+ stage.result = typeof result === 'string' ? result.substring(0, 200) : 'ok';
+ this.terminal.print(`✅ Stage ${name} passed (${stage.duration}ms)`, 'success');
+ return result;
+ } catch (e) {
+ stage.status = 'failed';
+ stage.duration = Date.now() - stage.startedAt;
+ stage.error = e.message;
+ this.terminal.print(`❌ Stage ${name} failed: ${e.message}`, 'error');
+ throw e;
+ }
+ }
+
+ _testAgentHealth() {
+ const total = Object.keys(this._agents).length;
+ const busy = Object.values(this._agents).filter(a => a.status === 'busy').length;
+ const failed = Object.values(this._agents).filter(a => a.status === 'failed').length;
+
+ const issues = [];
+ if (failed > 0) issues.push(`${failed} agent(s) in failed state`);
+ if (total > 10) issues.push(`high agent count: ${total}`);
+ if (issues.length > 0) throw new Error(issues.join('; '));
+ this.terminal.print(` Agents: ${total} (${busy} busy, ${failed} failed) — healthy`, 'info');
+ return `agents_ok:${total}:${busy}:${failed}`;
+ }
+
+ _testA2AMailbox() {
+ // Verify A2A polling is running
+ if (!this._A2A_POLLING) throw new Error('A2A polling is disabled');
+ if (!this._a2aPollTimer) throw new Error('A2A polling timer not running');
+ // Verify each agent has a VFS directory
+ for (const [id, agent] of Object.entries(this._agents)) {
+ const dir = this.terminal.fs.readDir(`/agents/${id}/`);
+ if (!dir) throw new Error(`Agent ${id} missing VFS directory`);
+ }
+ this.terminal.print(` A2A: ${Object.keys(this._agents).length} agent mailboxes OK`, 'info');
+ return `a2a_ok:${Object.keys(this._agents).length}`;
+ }
+
+ _testModules() {
+ const mm = this.terminal.moduleManager;
+ if (!mm || !mm.modules) throw new Error('ModuleManager not available');
+ const required = ['OrchestratorModule', 'AgentRegistryModule', 'LeaseModule', 'MemoryGraphModule'];
+ const missing = required.filter(n => !mm.modules[n]);
+ if (missing.length > 0) throw new Error(`Missing modules: ${missing.join(', ')}`);
+ // Check orchestrator is in registry
+ const registry = mm.modules['AgentRegistryModule'];
+ if (registry && !registry.agents?.find(a => a.id === 'orchestrator-0')) {
+ this.terminal.print(' ⚠️ orchestrator-0 not in registry (will register on next init)', 'warning');
+ }
+ this.terminal.print(` Modules: ${required.length}/${required.length} required loaded`, 'info');
+ return `modules_ok:${required.length}`;
+ }
+
+ _pipelineVerify() {
+ // Verify running state — internal checks
+ const issues = [];
+
+ // Check orchestrator status
+ const total = Object.keys(this._agents).length;
+ const failed = Object.values(this._agents).filter(a => a.status === 'failed').length;
+ if (failed > 0) issues.push(`${failed} failed agents`);
+ if (total + failed > 0 && failed === total) issues.push('all agents failed');
+
+ // Verify last parallel result exists
+ const lastResult = this.terminal.fs.fsReadJSON('orchestrator/parallel-results.json');
+ if (!lastResult) this.terminal.print(' ℹ️ No parallel results yet (first deploy)', 'info');
+
+ // Verify consensus history
+ const consensusDir = this.terminal.fs.readDir('orchestrator/');
+ if (consensusDir) {
+ const rounds = consensusDir.filter(e => e.name && e.name.startsWith('consensus-round-'));
+ this.terminal.print(` VFS: ${rounds.length} consensus rounds archived`, 'info');
+ }
+
+ // Check memory graph
+ const memgraph = this.terminal.moduleManager?.modules['MemoryGraphModule'];
+ if (memgraph && typeof memgraph.getStats === 'function') {
+ const stats = memgraph.getStats();
+ this.terminal.print(` Memory graph: ${stats?.edges || 0} edges, ${stats?.nodes || 0} nodes`, 'info');
+ }
+
+ if (issues.length > 0) throw new Error(issues.join('; '));
+ return `verify_ok:${total} agents, ${issues.length} issues`;
+ }
+
+ async _autoFix() {
+ this.terminal.print('🔄 Auto-fix running...', 'warning');
+ let fixed = 0;
+
+ // Fix 1: Purge failed agents
+ const failedIds = Object.entries(this._agents)
+ .filter(([, a]) => a.status === 'failed')
+ .map(([id]) => id);
+ for (const id of failedIds) {
+ this.killAgent(id);
+ fixed++;
+ }
+ if (fixed > 0) this.terminal.print(` Purged ${fixed} failed agent(s)`, 'warning');
+
+ // Fix 2: Restart A2A poller if stopped
+ if (!this._A2A_POLLING) {
+ this._A2A_POLLING = true;
+ this._a2aPollTimer = setInterval(() => this._pollAgentMailboxes(), this._A2A_POLL_INTERVAL);
+ this.terminal.print(' Restarted A2A polling', 'warning');
+ fixed++;
+ }
+
+ // Fix 3: Ensure VFS orchestrator dir exists
+ if (!this.terminal.fs.readDir('orchestrator')) {
+ this.terminal.fs.mkdir('orchestrator');
+ fixed++;
+ }
+
+ if (fixed === 0) this.terminal.print(' No auto-fix actions needed', 'info');
+ return `fixed ${fixed} issues`;
+ }
+
// ─── Task Delegation ───
- async delegateTask(task) {
+ async delegateTask(task, opts = {}) {
if (!task) return this.terminal.print('Usage: orchestrate <task description>', 'error');
- // Find an idle agent
+ // Parallel fan-out: decompose and run sub-tasks concurrently
+ if (opts.parallel) {
+ const parts = this._decomposeGoal(task);
+ if (parts.length > 1) {
+ this.terminal.print('🧩 Decomposed into ' + parts.length + ' sub-tasks — running in parallel', 'info');
+
+ // Auto-scale: ensure enough idle agents for N sub-tasks
+ const idleCount = Object.values(this._agents).filter(a => a.status === 'idle').length;
+ const totalSlots = this._SPAWN_LIMIT;
+ const usedSlots = Object.keys(this._agents).length;
+ const availableSlots = totalSlots - usedSlots;
+ const needed = parts.length - idleCount;
+ if (needed > 0 && availableSlots > 0) {
+ const toSpawn = Math.min(needed, availableSlots);
+ this.terminal.print('📈 Auto-scaling: spawning ' + toSpawn + ' pre-warm agent(s)', 'info');
+ for (let i = 0; i < toSpawn; i++) {
+ this.spawnAgent('worker', ['list_files', 'read_file', 'write_file', 'web_search']);
+ }
+ }
+
+ const results = await Promise.allSettled(parts.map(sub => this.delegateTask(sub.goal)));
+ const successes = results.filter(r => r.status === 'fulfilled').length;
+ const failures = results.filter(r => r.status === 'rejected').length;
+
+ // Result synthesis: collect outbox results from each sub-task's agent
+ const summaries = [];
+ for (let i = 0; i < parts.length; i++) {
+ const sub = parts[i];
+ const r = results[i];
+ if (r.status === 'fulfilled') {
+ // Find the agent that handled this sub-task
+ const agent = Object.values(this._agents).find(a => a.currentTask === sub.goal || (a.lastTask === sub.goal));
+ if (agent) {
+ const outbox = this.terminal.fs.fsReadJSON(`/agents/${agent.id}/outbox.json`);
+ const snippet = outbox?.result ? outbox.result.substring(0, 200) : 'completed (no outbox)';
+ summaries.push({ task: sub.goal, agent: agent.id, status: 'ok', result: snippet });
+ } else {
+ summaries.push({ task: sub.goal, status: 'ok', result: 'completed (agent recycled)' });
+ }
+ } else {
+ summaries.push({ task: sub.goal, status: 'failed', error: r.reason?.message || 'unknown error' });
+ }
+ }
+
+ // Store merged result to VFS for persistence
+ const merged = {
+ type: 'parallel_result',
+ parent_task: task,
+ timestamp: new Date().toISOString(),
+ total: parts.length,
+ successes,
+ failures,
+ summaries
+ };
+ if (!this.terminal.fs.readDir('orchestrator')) this.terminal.fs.mkdir('orchestrator');
+ this.terminal.fs.writeFile('orchestrator/parallel-results.json', JSON.stringify(merged, null, 2));
+
+ // Print synthesis to terminal
+ this.terminal.print('📊 Parallel Batch Results:', 'header');
+ for (const s of summaries) {
+ const icon = s.status === 'ok' ? '✅' : '❌';
+ const agentTag = s.agent ? ' [' + s.agent.substring(0, 20) + ']' : '';
+ const snippet = s.result ? s.result.replace(/\n/g, ' ').substring(0, 60) : '';
+ this.terminal.print(' ' + icon + ' ' + s.task.substring(0, 35) + agentTag + ' → ' + snippet, 'info');
+ }
+ this.terminal.print('✅ Parallel batch: ' + successes + '/' + parts.length + ' succeeded' + (failures > 0 ? ', ' + failures + ' failed' : ''), 'header');
+ return merged;
+ }
+ // Single sub-task — fall through to normal path
+ }
+
+ // Find an idle agent and immediately stamp it busy (atomic claim)
let agent = Object.values(this._agents).find(a => a.status === 'idle');
+ if (agent) {
+ agent.status = 'busy';
+ }
// Auto-spawn with role-derived capabilities
if (!agent) {
if (Object.keys(this._agents).length >= this._SPAWN_LIMIT) {
// Try finding any non-failed agent to reuse
- agent = Object.values(this._agents).find(a => a.status !== 'failed' && a.status !== 'sleeping');
+ const reuse = Object.values(this._agents).find(a => a.status !== 'failed' && a.status !== 'sleeping');
+ if (reuse) {
+ reuse.status = 'busy';
+ agent = reuse;
+ }
}
if (!agent) {
const role = this._deriveRole(task);
const caps = this._deriveCapabilities(task);
- this.spawnAgent(role, caps);
- agent = Object.values(this._agents).find(a => a.status === 'idle');
+ agent = this.spawnAgent(role, caps);
+ if (!agent) return this.terminal.print('❌ No available agents — spawn limit reached', 'error');
+ agent.status = 'busy'; // claim immediately after spawn
}
}
if (!agent) return this.terminal.print('❌ No available agents', 'error');
@@ -548,8 +1129,12 @@ class OrchestratorModule {
this.terminal.print(`✅ ${agent.id} completed task`, 'success');
+ // Return result for callers that await delegateTask
+ const outboxResult = typeof result === 'string' ? result : (result?.content || 'completed');
+
// Check restart policy — if always, set back to idle for next task
if (agent.restartPolicy === 'always') {
+ agent.lastTask = agent.currentTask;
agent.status = 'idle';
agent.currentTask = null;
agent.lastHeartbeat = Date.now();
@@ -564,11 +1149,14 @@ class OrchestratorModule {
// Return to idle pool (unless failed/restarting)
if (agent.status === 'failed') {
// handled by _handleAgentFailure
+ throw new Error(agent.lastError || 'agent failed');
} else if (agent.status !== 'busy') {
- agent.status = 'idle';
+ agent.lastTask = agent.currentTask;
agent.currentTask = null;
+ agent.status = 'idle';
agent.lastHeartbeat = Date.now();
}
+ return outboxResult || 'completed';
}
// ─── Monitoring ───