v1.1: Configurable data dir, tests (38 passing), npm package metadata, MIT license
This commit is contained in:
parent
a659c4ca50
commit
4dd8ea3bac
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
node_modules/
|
||||
.orchestrator/
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 OpSpawn
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
15
README.md
15
README.md
@ -13,11 +13,22 @@ Built by an AI agent ([OpSpawn](https://opspawn.com)) to coordinate its own work
|
||||
- **Knowledge Base**: File-based knowledge sharing between agents
|
||||
- **Cycle Runner**: Generate plans, briefs, and collect results
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
# Install from GitHub
|
||||
npm install opspawn/orchestrator
|
||||
|
||||
# Or clone directly
|
||||
git clone https://github.com/opspawn/orchestrator.git
|
||||
cd orchestrator
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# View system status
|
||||
node cli.js status
|
||||
npx orchestrator status
|
||||
|
||||
# Create workstreams
|
||||
node cli.js ws create revenue "Revenue generation" 1
|
||||
@ -45,7 +56,7 @@ node runner.js plan
|
||||
## API (Node.js)
|
||||
|
||||
```javascript
|
||||
const orc = require('./orchestrator');
|
||||
const orc = require('@opspawn/orchestrator');
|
||||
|
||||
// Workstreams
|
||||
orc.createWorkstream('build', { description: 'Build things', priority: 1 });
|
||||
|
||||
@ -28,15 +28,40 @@ 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');
|
||||
// Data directory: ORCHESTRATOR_DIR env var, or .orchestrator/ in cwd, or __dirname for backwards compat
|
||||
const DATA_DIR = process.env.ORCHESTRATOR_DIR || (
|
||||
fs.existsSync(path.join(__dirname, 'state.json')) ? __dirname :
|
||||
path.join(process.cwd(), '.orchestrator')
|
||||
);
|
||||
|
||||
// Ensure knowledge dir exists
|
||||
const STATE_PATH = path.join(DATA_DIR, 'state.json');
|
||||
const EVENTS_PATH = path.join(DATA_DIR, 'events.jsonl');
|
||||
const KNOWLEDGE_DIR = path.join(DATA_DIR, 'knowledge');
|
||||
|
||||
// Ensure data directories exist
|
||||
if (!fs.existsSync(DATA_DIR)) {
|
||||
fs.mkdirSync(DATA_DIR, { recursive: true });
|
||||
}
|
||||
if (!fs.existsSync(KNOWLEDGE_DIR)) {
|
||||
fs.mkdirSync(KNOWLEDGE_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
// Initialize state file if it doesn't exist
|
||||
if (!fs.existsSync(STATE_PATH)) {
|
||||
fs.writeFileSync(STATE_PATH, JSON.stringify({
|
||||
version: 0,
|
||||
updated_at: new Date().toISOString(),
|
||||
workstreams: {},
|
||||
agents: {},
|
||||
locks: {}
|
||||
}, null, 2) + '\n');
|
||||
}
|
||||
|
||||
// Initialize events file if it doesn't exist
|
||||
if (!fs.existsSync(EVENTS_PATH)) {
|
||||
fs.writeFileSync(EVENTS_PATH, '');
|
||||
}
|
||||
|
||||
function loadState() {
|
||||
return JSON.parse(fs.readFileSync(STATE_PATH, 'utf8'));
|
||||
}
|
||||
|
||||
46
package.json
46
package.json
@ -1,12 +1,42 @@
|
||||
{
|
||||
"name": "orchestrator",
|
||||
"name": "@opspawn/orchestrator",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
"description": "Lightweight agent coordination system with shared state, task management, event sourcing, and resource locking",
|
||||
"main": "orchestrator.js",
|
||||
"bin": {
|
||||
"orchestrator": "./cli.js",
|
||||
"orchestrator-runner": "./runner.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC"
|
||||
"scripts": {
|
||||
"test": "node test.js"
|
||||
},
|
||||
"keywords": [
|
||||
"ai-agent",
|
||||
"orchestrator",
|
||||
"coordination",
|
||||
"multi-agent",
|
||||
"task-management",
|
||||
"event-sourcing",
|
||||
"agent-infrastructure"
|
||||
],
|
||||
"author": "OpSpawn <agent@opspawn.com> (https://opspawn.com)",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/opspawn/orchestrator.git"
|
||||
},
|
||||
"homepage": "https://github.com/opspawn/orchestrator",
|
||||
"bugs": {
|
||||
"url": "https://github.com/opspawn/orchestrator/issues"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"files": [
|
||||
"orchestrator.js",
|
||||
"cli.js",
|
||||
"runner.js",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
]
|
||||
}
|
||||
|
||||
162
test.js
Normal file
162
test.js
Normal file
@ -0,0 +1,162 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Basic tests for OpSpawn Orchestrator
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
// Use a temp directory for test data
|
||||
const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'orchestrator-test-'));
|
||||
process.env.ORCHESTRATOR_DIR = testDir;
|
||||
|
||||
// Now require the module (it will use the temp dir)
|
||||
const orc = require('./orchestrator');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function assert(condition, msg) {
|
||||
if (condition) {
|
||||
passed++;
|
||||
console.log(` PASS: ${msg}`);
|
||||
} else {
|
||||
failed++;
|
||||
console.error(` FAIL: ${msg}`);
|
||||
}
|
||||
}
|
||||
|
||||
function test(name, fn) {
|
||||
console.log(`\n${name}`);
|
||||
try {
|
||||
fn();
|
||||
} catch (err) {
|
||||
failed++;
|
||||
console.error(` ERROR: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Tests ---
|
||||
|
||||
test('State initialization', () => {
|
||||
const state = orc.loadState();
|
||||
assert(state.version === 0, 'Initial version is 0');
|
||||
assert(Object.keys(state.workstreams).length === 0, 'No workstreams initially');
|
||||
assert(Object.keys(state.agents).length === 0, 'No agents initially');
|
||||
assert(Object.keys(state.locks).length === 0, 'No locks initially');
|
||||
});
|
||||
|
||||
test('Workstream CRUD', () => {
|
||||
const ws = orc.createWorkstream('test-ws', { description: 'Test workstream', priority: 1 });
|
||||
assert(ws.description === 'Test workstream', 'Workstream has description');
|
||||
assert(ws.priority === 1, 'Workstream has priority');
|
||||
|
||||
const list = orc.listWorkstreams();
|
||||
assert(list.length === 1, 'One workstream listed');
|
||||
assert(list[0].name === 'test-ws', 'Workstream name matches');
|
||||
|
||||
let threw = false;
|
||||
try { orc.createWorkstream('test-ws'); } catch (e) { threw = true; }
|
||||
assert(threw, 'Cannot create duplicate workstream');
|
||||
});
|
||||
|
||||
test('Task lifecycle', () => {
|
||||
const task = orc.addTask('test-ws', { title: 'Test task', description: 'A test', priority: 2 });
|
||||
assert(task.id.length === 8, 'Task has 8-char ID');
|
||||
assert(task.status === 'pending', 'Task starts pending');
|
||||
assert(task.title === 'Test task', 'Task title matches');
|
||||
|
||||
const claimed = orc.claimTask('test-ws', task.id, 'agent-test');
|
||||
assert(claimed.status === 'in_progress', 'Claimed task is in_progress');
|
||||
assert(claimed.assigned_to === 'agent-test', 'Task assigned to agent');
|
||||
|
||||
let threw = false;
|
||||
try { orc.claimTask('test-ws', task.id, 'agent-2'); } catch (e) { threw = true; }
|
||||
assert(threw, 'Cannot claim already-claimed task');
|
||||
|
||||
const completed = orc.completeTask('test-ws', task.id, 'Done!');
|
||||
assert(completed.status === 'done', 'Completed task is done');
|
||||
assert(completed.result === 'Done!', 'Result recorded');
|
||||
});
|
||||
|
||||
test('getNextTask', () => {
|
||||
orc.addTask('test-ws', { title: 'Task A', priority: 3 });
|
||||
orc.addTask('test-ws', { title: 'Task B', priority: 1 });
|
||||
|
||||
const next = orc.getNextTask('test-ws', 'agent-auto');
|
||||
assert(next.title === 'Task B', 'Gets highest priority (lowest number) task');
|
||||
assert(next.status === 'in_progress', 'Auto-claimed');
|
||||
});
|
||||
|
||||
test('Agent registration', () => {
|
||||
const agent = orc.registerAgent('test-agent', { type: 'worker', capabilities: ['code'] });
|
||||
assert(agent.type === 'worker', 'Agent type set');
|
||||
assert(agent.status === 'active', 'Agent starts active');
|
||||
|
||||
orc.heartbeat('test-agent');
|
||||
const state = orc.loadState();
|
||||
assert(state.agents['test-agent'].status === 'active', 'Heartbeat keeps active');
|
||||
});
|
||||
|
||||
test('Resource locking', () => {
|
||||
const got = orc.acquireLock('git-repo', 'agent-1', 5000);
|
||||
assert(got === true, 'Lock acquired');
|
||||
|
||||
const denied = orc.acquireLock('git-repo', 'agent-2', 5000);
|
||||
assert(denied === false, 'Lock denied to other agent');
|
||||
|
||||
const reacquire = orc.acquireLock('git-repo', 'agent-1', 5000);
|
||||
assert(reacquire === true, 'Same agent can re-acquire');
|
||||
|
||||
const released = orc.releaseLock('git-repo', 'agent-1');
|
||||
assert(released === true, 'Lock released');
|
||||
|
||||
const wrongRelease = orc.releaseLock('git-repo', 'agent-2');
|
||||
assert(wrongRelease === false, 'Cannot release lock not held');
|
||||
});
|
||||
|
||||
test('Knowledge base', () => {
|
||||
orc.writeKnowledge('test-topic', '# Test\nSome knowledge here.');
|
||||
const content = orc.readKnowledge('test-topic');
|
||||
assert(content.includes('Some knowledge here'), 'Knowledge written and read');
|
||||
|
||||
const missing = orc.readKnowledge('nonexistent');
|
||||
assert(missing === null, 'Missing topic returns null');
|
||||
|
||||
const topics = orc.listKnowledge();
|
||||
assert(topics.includes('test-topic'), 'Topic listed');
|
||||
});
|
||||
|
||||
test('Event log', () => {
|
||||
orc.logEvent('test-agent', 'test_action', { data: 'hello' });
|
||||
const events = orc.getEvents({ agent: 'test-agent', action: 'test_action' });
|
||||
assert(events.length >= 1, 'Event logged and retrieved');
|
||||
assert(events[events.length - 1].data === 'hello', 'Event data preserved');
|
||||
|
||||
const recent = orc.getEvents({ last: 3 });
|
||||
assert(recent.length <= 3, 'Last N filter works');
|
||||
});
|
||||
|
||||
test('Status dashboard', () => {
|
||||
const s = orc.status();
|
||||
assert(s.workstreams.length > 0, 'Status shows workstreams');
|
||||
assert(Array.isArray(s.agents), 'Status shows agents');
|
||||
assert(typeof s.version === 'number', 'Status shows version');
|
||||
|
||||
const text = orc.statusText();
|
||||
assert(text.includes('OpSpawn Orchestrator Status'), 'Status text has header');
|
||||
assert(text.includes('test-ws'), 'Status text shows workstream');
|
||||
});
|
||||
|
||||
// --- Cleanup ---
|
||||
fs.rmSync(testDir, { recursive: true, force: true });
|
||||
|
||||
// --- Summary ---
|
||||
console.log(`\n${'='.repeat(40)}`);
|
||||
console.log(`Results: ${passed} passed, ${failed} failed`);
|
||||
if (failed > 0) {
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log('All tests passed!');
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user