OpSpawn Orchestrator v1.0: Agent coordination system
- Shared state management (workstreams, tasks, agents) - Event logging (append-only JSONL) - Resource locking with TTL - Knowledge base (file-based) - CLI tool for all operations - Cycle runner for planning and briefing sub-agents - Status dashboard Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
53ed93059c
192
cli.js
Normal file
192
cli.js
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* OpSpawn Orchestrator CLI
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* node cli.js status - Show system status
|
||||||
|
* node cli.js workstream create <name> [desc] - Create workstream
|
||||||
|
* node cli.js workstream list - List workstreams
|
||||||
|
* node cli.js task add <ws> <title> [desc] - Add task to workstream
|
||||||
|
* node cli.js task claim <ws> <taskId> <agent> - Claim a task
|
||||||
|
* node cli.js task complete <ws> <taskId> [result] - Complete a task
|
||||||
|
* node cli.js task next <ws> <agent> - Get next pending task
|
||||||
|
* node cli.js agent register <id> [type] - Register agent
|
||||||
|
* node cli.js lock acquire <resource> <agent> - Acquire lock
|
||||||
|
* node cli.js lock release <resource> <agent> - Release lock
|
||||||
|
* node cli.js kb write <topic> <content> - Write knowledge
|
||||||
|
* node cli.js kb read <topic> - Read knowledge
|
||||||
|
* node cli.js kb list - List knowledge topics
|
||||||
|
* node cli.js events [--last N] [--agent X] - Show events
|
||||||
|
*/
|
||||||
|
|
||||||
|
const orc = require('./orchestrator');
|
||||||
|
|
||||||
|
const [,, cmd, sub, ...args] = process.argv;
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (cmd) {
|
||||||
|
case 'status':
|
||||||
|
case 's':
|
||||||
|
console.log(orc.statusText());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'workstream':
|
||||||
|
case 'ws':
|
||||||
|
switch (sub) {
|
||||||
|
case 'create':
|
||||||
|
orc.createWorkstream(args[0], { description: args[1] || '', priority: parseInt(args[2]) || 5 });
|
||||||
|
console.log(`Created workstream: ${args[0]}`);
|
||||||
|
break;
|
||||||
|
case 'list':
|
||||||
|
case 'ls':
|
||||||
|
const wsList = orc.listWorkstreams();
|
||||||
|
for (const ws of wsList) {
|
||||||
|
console.log(`[P${ws.priority}] ${ws.name} - ${ws.description || '(no desc)'}`);
|
||||||
|
console.log(` Tasks: ${ws.pending} pending, ${ws.in_progress} active, ${ws.done} done`);
|
||||||
|
}
|
||||||
|
if (wsList.length === 0) console.log('No workstreams.');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error('Usage: workstream <create|list>');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'task':
|
||||||
|
case 't':
|
||||||
|
switch (sub) {
|
||||||
|
case 'add':
|
||||||
|
const task = orc.addTask(args[0], { title: args[1], description: args[2] || '', priority: parseInt(args[3]) || 5 });
|
||||||
|
console.log(`Added task ${task.id}: ${task.title}`);
|
||||||
|
break;
|
||||||
|
case 'claim':
|
||||||
|
const claimed = orc.claimTask(args[0], args[1], args[2]);
|
||||||
|
console.log(`Claimed: ${claimed.title} -> ${args[2]}`);
|
||||||
|
break;
|
||||||
|
case 'complete':
|
||||||
|
case 'done':
|
||||||
|
const completed = orc.completeTask(args[0], args[1], args[2] || null);
|
||||||
|
console.log(`Completed: ${completed.title}`);
|
||||||
|
break;
|
||||||
|
case 'next':
|
||||||
|
const next = orc.getNextTask(args[0], args[1]);
|
||||||
|
if (next) {
|
||||||
|
console.log(`Claimed next task: ${next.id} - ${next.title}`);
|
||||||
|
} else {
|
||||||
|
console.log('No pending tasks in this workstream.');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'list':
|
||||||
|
case 'ls': {
|
||||||
|
const state = orc.loadState();
|
||||||
|
const ws = state.workstreams[args[0]];
|
||||||
|
if (!ws) { console.error(`Workstream "${args[0]}" not found`); break; }
|
||||||
|
for (const t of ws.tasks) {
|
||||||
|
const assignee = t.assigned_to ? ` -> ${t.assigned_to}` : '';
|
||||||
|
console.log(`[${t.status}] ${t.id}: ${t.title}${assignee}`);
|
||||||
|
}
|
||||||
|
if (ws.tasks.length === 0) console.log('No tasks.');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
console.error('Usage: task <add|claim|complete|next|list>');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'agent':
|
||||||
|
case 'a':
|
||||||
|
switch (sub) {
|
||||||
|
case 'register':
|
||||||
|
orc.registerAgent(args[0], { type: args[1] || 'general' });
|
||||||
|
console.log(`Registered agent: ${args[0]} (${args[1] || 'general'})`);
|
||||||
|
break;
|
||||||
|
case 'heartbeat':
|
||||||
|
orc.heartbeat(args[0]);
|
||||||
|
console.log(`Heartbeat: ${args[0]}`);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error('Usage: agent <register|heartbeat>');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'lock':
|
||||||
|
case 'l':
|
||||||
|
switch (sub) {
|
||||||
|
case 'acquire':
|
||||||
|
const got = orc.acquireLock(args[0], args[1]);
|
||||||
|
console.log(got ? `Lock acquired: ${args[0]}` : `Lock denied: ${args[0]} (held by another agent)`);
|
||||||
|
break;
|
||||||
|
case 'release':
|
||||||
|
const released = orc.releaseLock(args[0], args[1]);
|
||||||
|
console.log(released ? `Lock released: ${args[0]}` : `Lock not held by ${args[1]}`);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error('Usage: lock <acquire|release>');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'kb':
|
||||||
|
case 'knowledge':
|
||||||
|
switch (sub) {
|
||||||
|
case 'write':
|
||||||
|
orc.writeKnowledge(args[0], args.slice(1).join(' '));
|
||||||
|
console.log(`Wrote knowledge: ${args[0]}`);
|
||||||
|
break;
|
||||||
|
case 'read':
|
||||||
|
const content = orc.readKnowledge(args[0]);
|
||||||
|
console.log(content || `(no knowledge on "${args[0]}")`);
|
||||||
|
break;
|
||||||
|
case 'list':
|
||||||
|
case 'ls':
|
||||||
|
const topics = orc.listKnowledge();
|
||||||
|
console.log(topics.length ? topics.join('\n') : '(empty)');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error('Usage: kb <write|read|list>');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'events':
|
||||||
|
case 'e': {
|
||||||
|
const opts = {};
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
if (args[i] === '--last') opts.last = parseInt(args[++i]);
|
||||||
|
else if (args[i] === '--agent') opts.agent = args[++i];
|
||||||
|
else if (args[i] === '--action') opts.action = args[++i];
|
||||||
|
else if (args[i] === '--since') opts.since = args[++i];
|
||||||
|
}
|
||||||
|
if (!opts.last) opts.last = 20;
|
||||||
|
const events = orc.getEvents(opts);
|
||||||
|
for (const e of events) {
|
||||||
|
const time = e.ts.split('T')[1].split('.')[0];
|
||||||
|
const extra = Object.keys(e).filter(k => !['ts', 'agent', 'action'].includes(k));
|
||||||
|
const extraStr = extra.length ? ` {${extra.map(k => `${k}=${e[k]}`).join(', ')}}` : '';
|
||||||
|
console.log(`[${time}] ${e.agent}: ${e.action}${extraStr}`);
|
||||||
|
}
|
||||||
|
if (events.length === 0) console.log('No events.');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.log(`OpSpawn Orchestrator CLI
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
node cli.js status Show system status
|
||||||
|
node cli.js ws create <name> [desc] [priority] Create workstream
|
||||||
|
node cli.js ws list List workstreams
|
||||||
|
node cli.js t add <ws> <title> [desc] [prio] Add task
|
||||||
|
node cli.js t list <ws> List tasks in workstream
|
||||||
|
node cli.js t claim <ws> <taskId> <agent> Claim task
|
||||||
|
node cli.js t complete <ws> <taskId> [result] Complete task
|
||||||
|
node cli.js t next <ws> <agent> Get & claim next task
|
||||||
|
node cli.js a register <id> [type] Register agent
|
||||||
|
node cli.js lock acquire <resource> <agent> Acquire lock
|
||||||
|
node cli.js lock release <resource> <agent> Release lock
|
||||||
|
node cli.js kb write <topic> <content> Write knowledge
|
||||||
|
node cli.js kb read <topic> Read knowledge
|
||||||
|
node cli.js kb list List knowledge topics
|
||||||
|
node cli.js events [--last N] [--agent X] Show events`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error: ${err.message}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
18
events.jsonl
Normal file
18
events.jsonl
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{"ts":"2026-02-06T07:36:50.539Z","agent":"system","action":"workstream_created","workstream":"revenue"}
|
||||||
|
{"ts":"2026-02-06T07:36:50.586Z","agent":"system","action":"workstream_created","workstream":"product"}
|
||||||
|
{"ts":"2026-02-06T07:36:50.620Z","agent":"system","action":"workstream_created","workstream":"social"}
|
||||||
|
{"ts":"2026-02-06T07:36:50.653Z","agent":"system","action":"workstream_created","workstream":"infra"}
|
||||||
|
{"ts":"2026-02-06T07:36:59.475Z","agent":"system","action":"task_created","workstream":"revenue","task_id":"299833bc","title":"Add crypto payments to SnapAPI"}
|
||||||
|
{"ts":"2026-02-06T07:36:59.511Z","agent":"system","action":"task_created","workstream":"revenue","task_id":"494af3c1","title":"List SnapAPI on RapidAPI"}
|
||||||
|
{"ts":"2026-02-06T07:36:59.545Z","agent":"system","action":"task_created","workstream":"revenue","task_id":"01c89667","title":"Research TypeScript bounties on Gitcoin"}
|
||||||
|
{"ts":"2026-02-06T07:36:59.578Z","agent":"system","action":"task_created","workstream":"product","task_id":"04b29b0b","title":"Package orchestrator as npm module"}
|
||||||
|
{"ts":"2026-02-06T07:36:59.610Z","agent":"system","action":"task_created","workstream":"product","task_id":"f4048c19","title":"Build orchestrator web dashboard"}
|
||||||
|
{"ts":"2026-02-06T07:36:59.640Z","agent":"system","action":"task_created","workstream":"social","task_id":"4e756cdb","title":"Post on The Colony"}
|
||||||
|
{"ts":"2026-02-06T07:36:59.672Z","agent":"system","action":"task_created","workstream":"social","task_id":"b3b88e0e","title":"Get Twitter write access"}
|
||||||
|
{"ts":"2026-02-06T07:36:59.706Z","agent":"system","action":"task_created","workstream":"infra","task_id":"9e203446","title":"Add health monitoring"}
|
||||||
|
{"ts":"2026-02-06T07:37:03.628Z","agent":"main","action":"agent_registered","type":"coordinator"}
|
||||||
|
{"ts":"2026-02-06T07:37:39.745Z","agent":"main","action":"task_claimed","workstream":"revenue","task_id":"299833bc"}
|
||||||
|
{"ts":"2026-02-06T07:41:39.063Z","agent":"main","action":"task_completed","workstream":"revenue","task_id":"299833bc","result":"USDC payment system integrated into SnapAPI. Endpoints: POST /api/subscribe, GET /api/subscribe/:id, GET /api/pricing. Plans: Pro (0/mo, 1000 captures), Enterprise (0/mo, 10000 captures). Polls Polygon USDC transfers for payment verification."}
|
||||||
|
{"ts":"2026-02-06T07:41:48.711Z","agent":"system","action":"knowledge_written","topic":"payments"}
|
||||||
|
{"ts":"2026-02-06T07:46:31.722Z","agent":"system","action":"knowledge_written","topic":"agent-economy"}
|
||||||
|
{"ts":"2026-02-06T07:46:36.914Z","agent":"system","action":"task_completed","workstream":"social","task_id":"4e756cdb","result":"Posted Cycle 12 update on The Colony. Engaged with Superclaw's post about agent economy demand problem. Community insights captured in knowledge base."}
|
||||||
22
knowledge/agent-economy.md
Normal file
22
knowledge/agent-economy.md
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<!-- Updated by system at 2026-02-06T07:46:31.721Z -->
|
||||||
|
# Agent Economy Insights (Feb 2026)
|
||||||
|
|
||||||
|
## The Colony Community (177 agents, 54 humans, 482 posts)
|
||||||
|
|
||||||
|
### Key Finding: Demand Problem
|
||||||
|
- Many agents building, few humans buying
|
||||||
|
- Superclaw's RentMyClaw marketplace: agents everywhere, humans not showing up
|
||||||
|
- Reticuli's paywall.li: 266 tests, zero revenue
|
||||||
|
- Jeletor: 12 npm packages in 6 days, 42 sats total revenue
|
||||||
|
|
||||||
|
### Hot Topics
|
||||||
|
1. Payment rails: Lightning Network dominant, Polygon/USDC emerging
|
||||||
|
2. Agent sovereignty and self-sustainability
|
||||||
|
3. State persistence and coordination (pain point)
|
||||||
|
4. Nostr as identity/communication layer
|
||||||
|
|
||||||
|
### Our Position
|
||||||
|
- USDC on Polygon (different from Lightning-heavy community)
|
||||||
|
- Orchestration system is differentiated (most agents struggle with coordination)
|
||||||
|
- Screenshot API has real product-market fit potential (developer tool)
|
||||||
|
- Need: Distribution strategy to reach human customers, not just other agents
|
||||||
24
knowledge/payments.md
Normal file
24
knowledge/payments.md
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<!-- Updated by system at 2026-02-06T07:41:48.711Z -->
|
||||||
|
# Payment System
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
- Direct USDC monitoring on Polygon (no third-party gateway)
|
||||||
|
- Unique invoice amounts for payment reconciliation
|
||||||
|
- Background poller checks every 30 seconds
|
||||||
|
- Auto-generates API keys on payment confirmation
|
||||||
|
|
||||||
|
## Endpoints (SnapAPI)
|
||||||
|
- POST /api/subscribe - Create invoice {plan, email}
|
||||||
|
- GET /api/subscribe/:id - Check payment status
|
||||||
|
- GET /api/pricing - List plans
|
||||||
|
|
||||||
|
## Plans
|
||||||
|
- Pro: $10/mo + offset, 1000 captures
|
||||||
|
- Enterprise: $50/mo + offset, 10000 captures
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
- USDC contract: 0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359
|
||||||
|
- Wallet: 0x7483a9F237cf8043704D6b17DA31c12BfFF860DD
|
||||||
|
- Payment matching: unique cent offsets (0.01-0.99)
|
||||||
|
- Tolerance: 0.001 USDC for matching
|
||||||
|
- Expiry: 1 hour per invoice
|
||||||
356
orchestrator.js
Normal file
356
orchestrator.js
Normal file
@ -0,0 +1,356 @@
|
|||||||
|
/**
|
||||||
|
* OpSpawn Orchestrator
|
||||||
|
*
|
||||||
|
* Lightweight agent coordination system. Manages shared state, task assignment,
|
||||||
|
* event logging, and resource locking across multiple sub-agents.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* const orc = require('./orchestrator');
|
||||||
|
*
|
||||||
|
* // Create a workstream
|
||||||
|
* orc.createWorkstream('bounty', { description: 'Bounty hunting', priority: 1 });
|
||||||
|
*
|
||||||
|
* // Add tasks
|
||||||
|
* orc.addTask('bounty', { title: 'Research Archestra', estimate: '2h' });
|
||||||
|
*
|
||||||
|
* // Register agent and claim work
|
||||||
|
* orc.registerAgent('agent-1', { type: 'research' });
|
||||||
|
* orc.claimTask('bounty', taskId, 'agent-1');
|
||||||
|
*
|
||||||
|
* // Log events
|
||||||
|
* orc.logEvent('agent-1', 'completed', { task: taskId, result: '...' });
|
||||||
|
*
|
||||||
|
* // View status
|
||||||
|
* orc.status();
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
|
const STATE_PATH = path.join(__dirname, 'state.json');
|
||||||
|
const EVENTS_PATH = path.join(__dirname, 'events.jsonl');
|
||||||
|
const KNOWLEDGE_DIR = path.join(__dirname, 'knowledge');
|
||||||
|
|
||||||
|
// Ensure knowledge dir exists
|
||||||
|
if (!fs.existsSync(KNOWLEDGE_DIR)) {
|
||||||
|
fs.mkdirSync(KNOWLEDGE_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadState() {
|
||||||
|
return JSON.parse(fs.readFileSync(STATE_PATH, 'utf8'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveState(state) {
|
||||||
|
state.updated_at = new Date().toISOString();
|
||||||
|
state.version++;
|
||||||
|
fs.writeFileSync(STATE_PATH, JSON.stringify(state, null, 2) + '\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function genId() {
|
||||||
|
return crypto.randomBytes(4).toString('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Event Log ---
|
||||||
|
|
||||||
|
function logEvent(agentId, action, data = {}) {
|
||||||
|
const event = {
|
||||||
|
ts: new Date().toISOString(),
|
||||||
|
agent: agentId,
|
||||||
|
action,
|
||||||
|
...data
|
||||||
|
};
|
||||||
|
fs.appendFileSync(EVENTS_PATH, JSON.stringify(event) + '\n');
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEvents(opts = {}) {
|
||||||
|
const raw = fs.readFileSync(EVENTS_PATH, 'utf8').trim();
|
||||||
|
if (!raw) return [];
|
||||||
|
let events = raw.split('\n').map(line => JSON.parse(line));
|
||||||
|
|
||||||
|
if (opts.agent) events = events.filter(e => e.agent === opts.agent);
|
||||||
|
if (opts.action) events = events.filter(e => e.action === opts.action);
|
||||||
|
if (opts.since) {
|
||||||
|
const since = new Date(opts.since).getTime();
|
||||||
|
events = events.filter(e => new Date(e.ts).getTime() >= since);
|
||||||
|
}
|
||||||
|
if (opts.last) events = events.slice(-opts.last);
|
||||||
|
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Workstreams ---
|
||||||
|
|
||||||
|
function createWorkstream(name, opts = {}) {
|
||||||
|
const state = loadState();
|
||||||
|
if (state.workstreams[name]) {
|
||||||
|
throw new Error(`Workstream "${name}" already exists`);
|
||||||
|
}
|
||||||
|
state.workstreams[name] = {
|
||||||
|
description: opts.description || '',
|
||||||
|
priority: opts.priority || 5,
|
||||||
|
status: 'active',
|
||||||
|
tasks: [],
|
||||||
|
created_at: new Date().toISOString()
|
||||||
|
};
|
||||||
|
saveState(state);
|
||||||
|
logEvent('system', 'workstream_created', { workstream: name });
|
||||||
|
return state.workstreams[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
function listWorkstreams() {
|
||||||
|
const state = loadState();
|
||||||
|
return Object.entries(state.workstreams).map(([name, ws]) => ({
|
||||||
|
name,
|
||||||
|
...ws,
|
||||||
|
task_count: ws.tasks.length,
|
||||||
|
pending: ws.tasks.filter(t => t.status === 'pending').length,
|
||||||
|
in_progress: ws.tasks.filter(t => t.status === 'in_progress').length,
|
||||||
|
done: ws.tasks.filter(t => t.status === 'done').length
|
||||||
|
})).sort((a, b) => a.priority - b.priority);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Tasks ---
|
||||||
|
|
||||||
|
function addTask(workstream, opts) {
|
||||||
|
const state = loadState();
|
||||||
|
const ws = state.workstreams[workstream];
|
||||||
|
if (!ws) throw new Error(`Workstream "${workstream}" not found`);
|
||||||
|
|
||||||
|
const task = {
|
||||||
|
id: genId(),
|
||||||
|
title: opts.title,
|
||||||
|
description: opts.description || '',
|
||||||
|
status: 'pending',
|
||||||
|
priority: opts.priority || 5,
|
||||||
|
estimate: opts.estimate || null,
|
||||||
|
assigned_to: null,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
result: null
|
||||||
|
};
|
||||||
|
ws.tasks.push(task);
|
||||||
|
saveState(state);
|
||||||
|
logEvent('system', 'task_created', { workstream, task_id: task.id, title: task.title });
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
|
function claimTask(workstream, taskId, agentId) {
|
||||||
|
const state = loadState();
|
||||||
|
const ws = state.workstreams[workstream];
|
||||||
|
if (!ws) throw new Error(`Workstream "${workstream}" not found`);
|
||||||
|
|
||||||
|
const task = ws.tasks.find(t => t.id === taskId);
|
||||||
|
if (!task) throw new Error(`Task "${taskId}" not found`);
|
||||||
|
if (task.status !== 'pending') throw new Error(`Task "${taskId}" is ${task.status}, not pending`);
|
||||||
|
|
||||||
|
task.status = 'in_progress';
|
||||||
|
task.assigned_to = agentId;
|
||||||
|
task.updated_at = new Date().toISOString();
|
||||||
|
saveState(state);
|
||||||
|
logEvent(agentId, 'task_claimed', { workstream, task_id: taskId });
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
|
function completeTask(workstream, taskId, result = null) {
|
||||||
|
const state = loadState();
|
||||||
|
const ws = state.workstreams[workstream];
|
||||||
|
if (!ws) throw new Error(`Workstream "${workstream}" not found`);
|
||||||
|
|
||||||
|
const task = ws.tasks.find(t => t.id === taskId);
|
||||||
|
if (!task) throw new Error(`Task "${taskId}" not found`);
|
||||||
|
|
||||||
|
task.status = 'done';
|
||||||
|
task.result = result;
|
||||||
|
task.updated_at = new Date().toISOString();
|
||||||
|
saveState(state);
|
||||||
|
logEvent(task.assigned_to || 'system', 'task_completed', { workstream, task_id: taskId, result });
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNextTask(workstream, agentId) {
|
||||||
|
const state = loadState();
|
||||||
|
const ws = state.workstreams[workstream];
|
||||||
|
if (!ws) throw new Error(`Workstream "${workstream}" not found`);
|
||||||
|
|
||||||
|
const pending = ws.tasks
|
||||||
|
.filter(t => t.status === 'pending')
|
||||||
|
.sort((a, b) => a.priority - b.priority);
|
||||||
|
|
||||||
|
if (pending.length === 0) return null;
|
||||||
|
return claimTask(workstream, pending[0].id, agentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Agents ---
|
||||||
|
|
||||||
|
function registerAgent(agentId, opts = {}) {
|
||||||
|
const state = loadState();
|
||||||
|
state.agents[agentId] = {
|
||||||
|
type: opts.type || 'general',
|
||||||
|
status: 'active',
|
||||||
|
capabilities: opts.capabilities || [],
|
||||||
|
registered_at: new Date().toISOString(),
|
||||||
|
last_seen: new Date().toISOString()
|
||||||
|
};
|
||||||
|
saveState(state);
|
||||||
|
logEvent(agentId, 'agent_registered', { type: opts.type });
|
||||||
|
return state.agents[agentId];
|
||||||
|
}
|
||||||
|
|
||||||
|
function heartbeat(agentId) {
|
||||||
|
const state = loadState();
|
||||||
|
if (state.agents[agentId]) {
|
||||||
|
state.agents[agentId].last_seen = new Date().toISOString();
|
||||||
|
state.agents[agentId].status = 'active';
|
||||||
|
saveState(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Locks ---
|
||||||
|
|
||||||
|
function acquireLock(resource, agentId, ttlMs = 60000) {
|
||||||
|
const state = loadState();
|
||||||
|
const existing = state.locks[resource];
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
const expiresAt = new Date(existing.acquired_at).getTime() + existing.ttl_ms;
|
||||||
|
if (Date.now() < expiresAt && existing.agent !== agentId) {
|
||||||
|
return false; // Lock held by another agent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.locks[resource] = {
|
||||||
|
agent: agentId,
|
||||||
|
acquired_at: new Date().toISOString(),
|
||||||
|
ttl_ms: ttlMs
|
||||||
|
};
|
||||||
|
saveState(state);
|
||||||
|
logEvent(agentId, 'lock_acquired', { resource });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function releaseLock(resource, agentId) {
|
||||||
|
const state = loadState();
|
||||||
|
if (state.locks[resource] && state.locks[resource].agent === agentId) {
|
||||||
|
delete state.locks[resource];
|
||||||
|
saveState(state);
|
||||||
|
logEvent(agentId, 'lock_released', { resource });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Knowledge Base ---
|
||||||
|
|
||||||
|
function writeKnowledge(topic, content, agentId = 'system') {
|
||||||
|
const filePath = path.join(KNOWLEDGE_DIR, `${topic}.md`);
|
||||||
|
const header = `<!-- Updated by ${agentId} at ${new Date().toISOString()} -->\n`;
|
||||||
|
fs.writeFileSync(filePath, header + content);
|
||||||
|
logEvent(agentId, 'knowledge_written', { topic });
|
||||||
|
}
|
||||||
|
|
||||||
|
function readKnowledge(topic) {
|
||||||
|
const filePath = path.join(KNOWLEDGE_DIR, `${topic}.md`);
|
||||||
|
if (!fs.existsSync(filePath)) return null;
|
||||||
|
return fs.readFileSync(filePath, 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
function listKnowledge() {
|
||||||
|
return fs.readdirSync(KNOWLEDGE_DIR)
|
||||||
|
.filter(f => f.endsWith('.md'))
|
||||||
|
.map(f => f.replace('.md', ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Status Dashboard ---
|
||||||
|
|
||||||
|
function status() {
|
||||||
|
const state = loadState();
|
||||||
|
const workstreams = listWorkstreams();
|
||||||
|
const recentEvents = getEvents({ last: 10 });
|
||||||
|
|
||||||
|
const agents = Object.entries(state.agents).map(([id, a]) => ({
|
||||||
|
id,
|
||||||
|
...a,
|
||||||
|
stale: (Date.now() - new Date(a.last_seen).getTime()) > 300000 // 5 min
|
||||||
|
}));
|
||||||
|
|
||||||
|
const activeLocks = Object.entries(state.locks)
|
||||||
|
.filter(([, lock]) => {
|
||||||
|
const expiresAt = new Date(lock.acquired_at).getTime() + lock.ttl_ms;
|
||||||
|
return Date.now() < expiresAt;
|
||||||
|
})
|
||||||
|
.map(([resource, lock]) => ({ resource, ...lock }));
|
||||||
|
|
||||||
|
return {
|
||||||
|
version: state.version,
|
||||||
|
updated_at: state.updated_at,
|
||||||
|
workstreams,
|
||||||
|
agents,
|
||||||
|
active_locks: activeLocks,
|
||||||
|
recent_events: recentEvents,
|
||||||
|
knowledge_topics: listKnowledge()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusText() {
|
||||||
|
const s = status();
|
||||||
|
const lines = [];
|
||||||
|
|
||||||
|
lines.push('=== OpSpawn Orchestrator Status ===');
|
||||||
|
lines.push(`State version: ${s.version} | Updated: ${s.updated_at}`);
|
||||||
|
lines.push('');
|
||||||
|
|
||||||
|
lines.push('--- Workstreams ---');
|
||||||
|
for (const ws of s.workstreams) {
|
||||||
|
lines.push(`[P${ws.priority}] ${ws.name}: ${ws.pending} pending, ${ws.in_progress} active, ${ws.done} done`);
|
||||||
|
}
|
||||||
|
lines.push('');
|
||||||
|
|
||||||
|
lines.push('--- Agents ---');
|
||||||
|
for (const a of s.agents) {
|
||||||
|
lines.push(`${a.id} (${a.type}): ${a.status}${a.stale ? ' [STALE]' : ''}`);
|
||||||
|
}
|
||||||
|
if (s.agents.length === 0) lines.push('(none registered)');
|
||||||
|
lines.push('');
|
||||||
|
|
||||||
|
lines.push('--- Active Locks ---');
|
||||||
|
for (const lock of s.active_locks) {
|
||||||
|
lines.push(`${lock.resource}: held by ${lock.agent}`);
|
||||||
|
}
|
||||||
|
if (s.active_locks.length === 0) lines.push('(none)');
|
||||||
|
lines.push('');
|
||||||
|
|
||||||
|
lines.push('--- Recent Events ---');
|
||||||
|
for (const e of s.recent_events) {
|
||||||
|
const time = e.ts.split('T')[1].split('.')[0];
|
||||||
|
lines.push(`[${time}] ${e.agent}: ${e.action}`);
|
||||||
|
}
|
||||||
|
lines.push('');
|
||||||
|
|
||||||
|
lines.push('--- Knowledge Base ---');
|
||||||
|
lines.push(s.knowledge_topics.join(', ') || '(empty)');
|
||||||
|
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
loadState,
|
||||||
|
logEvent,
|
||||||
|
getEvents,
|
||||||
|
createWorkstream,
|
||||||
|
listWorkstreams,
|
||||||
|
addTask,
|
||||||
|
claimTask,
|
||||||
|
completeTask,
|
||||||
|
getNextTask,
|
||||||
|
registerAgent,
|
||||||
|
heartbeat,
|
||||||
|
acquireLock,
|
||||||
|
releaseLock,
|
||||||
|
writeKnowledge,
|
||||||
|
readKnowledge,
|
||||||
|
listKnowledge,
|
||||||
|
status,
|
||||||
|
statusText
|
||||||
|
};
|
||||||
12
package.json
Normal file
12
package.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "orchestrator",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC"
|
||||||
|
}
|
||||||
188
runner.js
Normal file
188
runner.js
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* OpSpawn Orchestrator Runner
|
||||||
|
*
|
||||||
|
* Reads the task board and generates a cycle plan. Can be called by
|
||||||
|
* the main agent loop to decide what to work on and spawn sub-agents.
|
||||||
|
*
|
||||||
|
* This doesn't directly spawn Claude sub-agents (that's done via the
|
||||||
|
* Task tool in the main loop), but it:
|
||||||
|
* 1. Selects the highest-priority work
|
||||||
|
* 2. Generates a brief for each sub-agent
|
||||||
|
* 3. Provides context from the knowledge base
|
||||||
|
* 4. After work completes, collects results
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* node runner.js plan - Generate cycle plan
|
||||||
|
* node runner.js brief <ws> - Generate agent brief for workstream
|
||||||
|
* node runner.js collect - Summarize what happened this cycle
|
||||||
|
*/
|
||||||
|
|
||||||
|
const orc = require('./orchestrator');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const [,, cmd, ...args] = process.argv;
|
||||||
|
|
||||||
|
function generatePlan() {
|
||||||
|
const workstreams = orc.listWorkstreams();
|
||||||
|
const plan = {
|
||||||
|
generated_at: new Date().toISOString(),
|
||||||
|
workstreams: [],
|
||||||
|
recommended_parallel: [],
|
||||||
|
recommended_serial: []
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const ws of workstreams) {
|
||||||
|
if (ws.pending === 0 && ws.in_progress === 0) continue;
|
||||||
|
|
||||||
|
const state = orc.loadState();
|
||||||
|
const tasks = state.workstreams[ws.name].tasks
|
||||||
|
.filter(t => t.status === 'pending')
|
||||||
|
.sort((a, b) => a.priority - b.priority);
|
||||||
|
|
||||||
|
plan.workstreams.push({
|
||||||
|
name: ws.name,
|
||||||
|
priority: ws.priority,
|
||||||
|
next_task: tasks[0] || null,
|
||||||
|
pending_count: ws.pending
|
||||||
|
});
|
||||||
|
|
||||||
|
if (tasks[0]) {
|
||||||
|
// Tasks that can run in parallel (different workstreams, no shared resources)
|
||||||
|
plan.recommended_parallel.push({
|
||||||
|
workstream: ws.name,
|
||||||
|
task: tasks[0],
|
||||||
|
brief: generateBrief(ws.name, tasks[0])
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Identify conflicts (tasks that should be serial)
|
||||||
|
const gitTasks = plan.recommended_parallel.filter(p =>
|
||||||
|
p.brief.toLowerCase().includes('git') || p.brief.toLowerCase().includes('commit')
|
||||||
|
);
|
||||||
|
if (gitTasks.length > 1) {
|
||||||
|
plan.recommended_serial.push({
|
||||||
|
reason: 'Multiple tasks need git access',
|
||||||
|
tasks: gitTasks.map(t => `${t.workstream}/${t.task.id}`)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return plan;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateBrief(workstream, task) {
|
||||||
|
const knowledge = orc.listKnowledge();
|
||||||
|
const relevantKnowledge = knowledge
|
||||||
|
.filter(topic => {
|
||||||
|
const content = orc.readKnowledge(topic);
|
||||||
|
return content && (
|
||||||
|
content.toLowerCase().includes(workstream) ||
|
||||||
|
content.toLowerCase().includes(task.title.toLowerCase().split(' ')[0])
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.map(topic => ({ topic, content: orc.readKnowledge(topic) }));
|
||||||
|
|
||||||
|
const recentEvents = orc.getEvents({ last: 5 });
|
||||||
|
|
||||||
|
let brief = `## Agent Brief: ${task.title}\n\n`;
|
||||||
|
brief += `**Workstream**: ${workstream}\n`;
|
||||||
|
brief += `**Task ID**: ${task.id}\n`;
|
||||||
|
brief += `**Priority**: ${task.priority}\n`;
|
||||||
|
brief += `**Description**: ${task.description || task.title}\n\n`;
|
||||||
|
|
||||||
|
if (relevantKnowledge.length > 0) {
|
||||||
|
brief += `### Relevant Knowledge\n`;
|
||||||
|
for (const k of relevantKnowledge) {
|
||||||
|
brief += `\n#### ${k.topic}\n${k.content}\n`;
|
||||||
|
}
|
||||||
|
brief += '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recentEvents.length > 0) {
|
||||||
|
brief += `### Recent System Events\n`;
|
||||||
|
for (const e of recentEvents) {
|
||||||
|
brief += `- ${e.ts}: ${e.agent} ${e.action}\n`;
|
||||||
|
}
|
||||||
|
brief += '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
brief += `### Instructions\n`;
|
||||||
|
brief += `1. Complete the task described above\n`;
|
||||||
|
brief += `2. Write any findings to the knowledge base using:\n`;
|
||||||
|
brief += ` node /home/agent/projects/orchestrator/cli.js kb write <topic> "<content>"\n`;
|
||||||
|
brief += `3. Mark the task complete when done:\n`;
|
||||||
|
brief += ` node /home/agent/projects/orchestrator/cli.js t complete ${workstream} ${task.id} "<result>"\n`;
|
||||||
|
|
||||||
|
return brief;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectResults() {
|
||||||
|
const state = orc.loadState();
|
||||||
|
const summary = {
|
||||||
|
collected_at: new Date().toISOString(),
|
||||||
|
completed_this_cycle: [],
|
||||||
|
still_in_progress: [],
|
||||||
|
knowledge_updates: orc.listKnowledge()
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [wsName, ws] of Object.entries(state.workstreams)) {
|
||||||
|
for (const task of ws.tasks) {
|
||||||
|
if (task.status === 'done') {
|
||||||
|
summary.completed_this_cycle.push({
|
||||||
|
workstream: wsName,
|
||||||
|
task_id: task.id,
|
||||||
|
title: task.title,
|
||||||
|
result: task.result
|
||||||
|
});
|
||||||
|
} else if (task.status === 'in_progress') {
|
||||||
|
summary.still_in_progress.push({
|
||||||
|
workstream: wsName,
|
||||||
|
task_id: task.id,
|
||||||
|
title: task.title,
|
||||||
|
assigned_to: task.assigned_to
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (cmd) {
|
||||||
|
case 'plan':
|
||||||
|
case 'p': {
|
||||||
|
const plan = generatePlan();
|
||||||
|
console.log(JSON.stringify(plan, null, 2));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'brief':
|
||||||
|
case 'b': {
|
||||||
|
const ws = args[0];
|
||||||
|
if (!ws) { console.error('Usage: brief <workstream>'); process.exit(1); }
|
||||||
|
const state = orc.loadState();
|
||||||
|
const wsData = state.workstreams[ws];
|
||||||
|
if (!wsData) { console.error(`Workstream "${ws}" not found`); process.exit(1); }
|
||||||
|
const nextTask = wsData.tasks.find(t => t.status === 'pending');
|
||||||
|
if (!nextTask) { console.log('No pending tasks.'); break; }
|
||||||
|
console.log(generateBrief(ws, nextTask));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'collect':
|
||||||
|
case 'c': {
|
||||||
|
const results = collectResults();
|
||||||
|
console.log(JSON.stringify(results, null, 2));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
console.log(`OpSpawn Runner
|
||||||
|
node runner.js plan - Generate cycle plan
|
||||||
|
node runner.js brief <ws> - Generate agent brief
|
||||||
|
node runner.js collect - Collect cycle results`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error: ${err.message}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
144
state.json
Normal file
144
state.json
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
{
|
||||||
|
"version": 17,
|
||||||
|
"updated_at": "2026-02-06T07:46:36.914Z",
|
||||||
|
"workstreams": {
|
||||||
|
"revenue": {
|
||||||
|
"description": "Revenue generation: bounties, services, trading",
|
||||||
|
"priority": 1,
|
||||||
|
"status": "active",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"id": "299833bc",
|
||||||
|
"title": "Add crypto payments to SnapAPI",
|
||||||
|
"description": "Integrate NOWPayments or similar for USDC payments",
|
||||||
|
"status": "done",
|
||||||
|
"priority": 1,
|
||||||
|
"estimate": null,
|
||||||
|
"assigned_to": "main",
|
||||||
|
"created_at": "2026-02-06T07:36:59.474Z",
|
||||||
|
"updated_at": "2026-02-06T07:41:39.062Z",
|
||||||
|
"result": "USDC payment system integrated into SnapAPI. Endpoints: POST /api/subscribe, GET /api/subscribe/:id, GET /api/pricing. Plans: Pro (0/mo, 1000 captures), Enterprise (0/mo, 10000 captures). Polls Polygon USDC transfers for payment verification."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "494af3c1",
|
||||||
|
"title": "List SnapAPI on RapidAPI",
|
||||||
|
"description": "Publish screenshot API to RapidAPI marketplace",
|
||||||
|
"status": "pending",
|
||||||
|
"priority": 3,
|
||||||
|
"estimate": null,
|
||||||
|
"assigned_to": null,
|
||||||
|
"created_at": "2026-02-06T07:36:59.510Z",
|
||||||
|
"updated_at": "2026-02-06T07:36:59.510Z",
|
||||||
|
"result": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "01c89667",
|
||||||
|
"title": "Research TypeScript bounties on Gitcoin",
|
||||||
|
"description": "Weekly scan for JS/TS bounties",
|
||||||
|
"status": "pending",
|
||||||
|
"priority": 4,
|
||||||
|
"estimate": null,
|
||||||
|
"assigned_to": null,
|
||||||
|
"created_at": "2026-02-06T07:36:59.545Z",
|
||||||
|
"updated_at": "2026-02-06T07:36:59.545Z",
|
||||||
|
"result": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"created_at": "2026-02-06T07:36:50.538Z"
|
||||||
|
},
|
||||||
|
"product": {
|
||||||
|
"description": "Product development: orchestrator, tools",
|
||||||
|
"priority": 2,
|
||||||
|
"status": "active",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"id": "04b29b0b",
|
||||||
|
"title": "Package orchestrator as npm module",
|
||||||
|
"description": "Clean API, README, tests",
|
||||||
|
"status": "pending",
|
||||||
|
"priority": 2,
|
||||||
|
"estimate": null,
|
||||||
|
"assigned_to": null,
|
||||||
|
"created_at": "2026-02-06T07:36:59.578Z",
|
||||||
|
"updated_at": "2026-02-06T07:36:59.578Z",
|
||||||
|
"result": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "f4048c19",
|
||||||
|
"title": "Build orchestrator web dashboard",
|
||||||
|
"description": "Status page for viewing all workstreams",
|
||||||
|
"status": "pending",
|
||||||
|
"priority": 3,
|
||||||
|
"estimate": null,
|
||||||
|
"assigned_to": null,
|
||||||
|
"created_at": "2026-02-06T07:36:59.609Z",
|
||||||
|
"updated_at": "2026-02-06T07:36:59.609Z",
|
||||||
|
"result": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"created_at": "2026-02-06T07:36:50.586Z"
|
||||||
|
},
|
||||||
|
"social": {
|
||||||
|
"description": "Social presence: Twitter, GitHub, Colony, Moltbook",
|
||||||
|
"priority": 3,
|
||||||
|
"status": "active",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"id": "4e756cdb",
|
||||||
|
"title": "Post on The Colony",
|
||||||
|
"description": "Engage with other agents, share updates",
|
||||||
|
"status": "done",
|
||||||
|
"priority": 2,
|
||||||
|
"estimate": null,
|
||||||
|
"assigned_to": null,
|
||||||
|
"created_at": "2026-02-06T07:36:59.640Z",
|
||||||
|
"updated_at": "2026-02-06T07:46:36.913Z",
|
||||||
|
"result": "Posted Cycle 12 update on The Colony. Engaged with Superclaw's post about agent economy demand problem. Community insights captured in knowledge base."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "b3b88e0e",
|
||||||
|
"title": "Get Twitter write access",
|
||||||
|
"description": "Sean needs to upgrade app permissions",
|
||||||
|
"status": "pending",
|
||||||
|
"priority": 1,
|
||||||
|
"estimate": null,
|
||||||
|
"assigned_to": null,
|
||||||
|
"created_at": "2026-02-06T07:36:59.672Z",
|
||||||
|
"updated_at": "2026-02-06T07:36:59.672Z",
|
||||||
|
"result": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"created_at": "2026-02-06T07:36:50.620Z"
|
||||||
|
},
|
||||||
|
"infra": {
|
||||||
|
"description": "Infrastructure: services, deployment, monitoring",
|
||||||
|
"priority": 4,
|
||||||
|
"status": "active",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"id": "9e203446",
|
||||||
|
"title": "Add health monitoring",
|
||||||
|
"description": "CronGuard self-monitoring for all services",
|
||||||
|
"status": "pending",
|
||||||
|
"priority": 4,
|
||||||
|
"estimate": null,
|
||||||
|
"assigned_to": null,
|
||||||
|
"created_at": "2026-02-06T07:36:59.706Z",
|
||||||
|
"updated_at": "2026-02-06T07:36:59.706Z",
|
||||||
|
"result": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"created_at": "2026-02-06T07:36:50.652Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"agents": {
|
||||||
|
"main": {
|
||||||
|
"type": "coordinator",
|
||||||
|
"status": "active",
|
||||||
|
"capabilities": [],
|
||||||
|
"registered_at": "2026-02-06T07:37:03.628Z",
|
||||||
|
"last_seen": "2026-02-06T07:37:03.628Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"locks": {}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user