From 53ed93059c7b9f3f6c190dfa7b78588c478efe1d Mon Sep 17 00:00:00 2001 From: OpSpawn Agent Date: Fri, 6 Feb 2026 07:46:53 +0000 Subject: [PATCH] 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 --- cli.js | 192 ++++++++++++++++++++ events.jsonl | 18 ++ knowledge/agent-economy.md | 22 +++ knowledge/payments.md | 24 +++ orchestrator.js | 356 +++++++++++++++++++++++++++++++++++++ package.json | 12 ++ runner.js | 188 ++++++++++++++++++++ state.json | 144 +++++++++++++++ 8 files changed, 956 insertions(+) create mode 100644 cli.js create mode 100644 events.jsonl create mode 100644 knowledge/agent-economy.md create mode 100644 knowledge/payments.md create mode 100644 orchestrator.js create mode 100644 package.json create mode 100644 runner.js create mode 100644 state.json diff --git a/cli.js b/cli.js new file mode 100644 index 0000000..0670ed3 --- /dev/null +++ b/cli.js @@ -0,0 +1,192 @@ +#!/usr/bin/env node +/** + * OpSpawn Orchestrator CLI + * + * Usage: + * node cli.js status - Show system status + * node cli.js workstream create [desc] - Create workstream + * node cli.js workstream list - List workstreams + * node cli.js task add [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); +} diff --git a/events.jsonl b/events.jsonl new file mode 100644 index 0000000..39f3166 --- /dev/null +++ b/events.jsonl @@ -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."} diff --git a/knowledge/agent-economy.md b/knowledge/agent-economy.md new file mode 100644 index 0000000..a72b53f --- /dev/null +++ b/knowledge/agent-economy.md @@ -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 diff --git a/knowledge/payments.md b/knowledge/payments.md new file mode 100644 index 0000000..177679d --- /dev/null +++ b/knowledge/payments.md @@ -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 diff --git a/orchestrator.js b/orchestrator.js new file mode 100644 index 0000000..5d3a68f --- /dev/null +++ b/orchestrator.js @@ -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 +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..b0363a8 --- /dev/null +++ b/package.json @@ -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" +} diff --git a/runner.js b/runner.js new file mode 100644 index 0000000..ee16586 --- /dev/null +++ b/runner.js @@ -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); +} diff --git a/state.json b/state.json new file mode 100644 index 0000000..caa26a7 --- /dev/null +++ b/state.json @@ -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": {} +}