@volter/supercode-orchestrator 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +532 -0
- package/activation.mjs +270 -0
- package/adapters/api-server.mjs +615 -0
- package/adapters/chat-sdk.mjs +475 -0
- package/adapters/chunks.mjs +14 -0
- package/adapters/loopback.mjs +37 -0
- package/adapters/rh2.mjs +349 -0
- package/apply/doors.d.ts +2 -0
- package/apply/doors.mjs +292 -0
- package/apply/hermes_door.py +129 -0
- package/apply/index.d.ts +4 -0
- package/apply/index.mjs +441 -0
- package/apply/state.mjs +142 -0
- package/bin/hermes-worker.mjs +11 -0
- package/bin/orchestrator.mjs +730 -0
- package/board.mjs +81 -0
- package/commands.mjs +37 -0
- package/cron.mjs +967 -0
- package/events.mjs +107 -0
- package/gates.mjs +281 -0
- package/hermes-cli.mjs +440 -0
- package/hermes-home.mjs +264 -0
- package/hermes-locks.mjs +188 -0
- package/hosted.mjs +81 -0
- package/index.d.ts +508 -0
- package/index.mjs +38 -0
- package/ir.mjs +205 -0
- package/kanban-worker.mjs +155 -0
- package/lease.mjs +51 -0
- package/loop.mjs +864 -0
- package/mcp/bridge.mjs +133 -0
- package/mcp/hermes-kanban-tools.json +477 -0
- package/mcp/hermes-tools.json +288 -0
- package/mcp/mount.mjs +161 -0
- package/mcp/server.mjs +160 -0
- package/mcp/tools.mjs +57 -0
- package/mcp/verbs.mjs +348 -0
- package/ontology.d.ts +557 -0
- package/operator.mjs +426 -0
- package/orchestration.mjs +166 -0
- package/package.json +51 -0
- package/reducer.mjs +792 -0
- package/webhook.mjs +263 -0
- package/worker-route.mjs +122 -0
package/board.mjs
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// The board's dispatcher (docs/plans/hermes-compat.md row 14). A home's board is Hermes's: `kanban.db` at
|
|
2
|
+
// the root, above the profiles, in Hermes's schema, written by Hermes's code. Where a Hermes gateway
|
|
3
|
+
// would host the dispatcher, the orchestrator hosts it the same way: gated by `kanban.dispatch_in_gateway`
|
|
4
|
+
// (and `HERMES_KANBAN_DISPATCH_IN_GATEWAY`), holding Hermes's one-dispatcher lock, one tick every
|
|
5
|
+
// `kanban.dispatch_interval_seconds` (60, at least 1), the first after five seconds; each tick is Hermes's
|
|
6
|
+
// own (`kanbanTick`). What differs is the worker: Hermes's spawn runs `HERMES_BIN`, here a wrapper that
|
|
7
|
+
// starts `bin/hermes-worker.mjs` on this daemon's own runtime, so each task runs on its assignee
|
|
8
|
+
// profile's worker harness. `hermes kanban` reads and writes the same file.
|
|
9
|
+
import { createHash } from 'node:crypto';
|
|
10
|
+
import { chmodSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
11
|
+
import { homedir } from 'node:os';
|
|
12
|
+
import { dirname, join, resolve } from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import { hermesInstall, kanbanTick } from './hermes-cli.mjs';
|
|
15
|
+
import { holdDispatcherLock } from './hermes-locks.mjs';
|
|
16
|
+
|
|
17
|
+
const WORKER = fileURLToPath(new URL('./bin/hermes-worker.mjs', import.meta.url));
|
|
18
|
+
|
|
19
|
+
/** The `HERMES_BIN` for a root: a script that runs the worker command on this very runtime. */
|
|
20
|
+
function workerCommand(root) {
|
|
21
|
+
const dir = join(process.env.XDG_STATE_HOME || join(homedir(), '.local', 'state'), 'supercode', 'hermes-home', createHash('sha256').update(resolve(root)).digest('hex').slice(0, 16));
|
|
22
|
+
mkdirSync(dir, { recursive: true });
|
|
23
|
+
const file = join(dir, 'hermes-worker');
|
|
24
|
+
writeFileSync(file, `#!/bin/sh\nexec ${JSON.stringify(process.execPath)} ${JSON.stringify(WORKER)} "$@"\n`);
|
|
25
|
+
chmodSync(file, 0o755);
|
|
26
|
+
return file;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Start the board's dispatcher for the home at `root`; answers `{ stop }`, or null when it does not dispatch. */
|
|
30
|
+
export async function startBoard({ root, config = {}, log = () => {} }) {
|
|
31
|
+
const kanban = config.kanban ?? {};
|
|
32
|
+
const off = String(process.env.HERMES_KANBAN_DISPATCH_IN_GATEWAY ?? '').trim().toLowerCase();
|
|
33
|
+
if (['0', 'false', 'no', 'off'].includes(off)) { log('info', 'board: not dispatched (HERMES_KANBAN_DISPATCH_IN_GATEWAY)'); return null; }
|
|
34
|
+
if (kanban.dispatch_in_gateway === false) { log('info', 'board: not dispatched (kanban.dispatch_in_gateway: false)'); return null; }
|
|
35
|
+
const install = hermesInstall();
|
|
36
|
+
if (!install?.python) { log('warn', 'board: not dispatched; the board is Hermes\'s and no Hermes is installed on PATH'); return null; }
|
|
37
|
+
let lock;
|
|
38
|
+
try { lock = await holdDispatcherLock(root, { python: install.python }); }
|
|
39
|
+
catch (error) { log('info', `board: not dispatched: ${error.message}`); return null; }
|
|
40
|
+
const env = {
|
|
41
|
+
HERMES_BIN: workerCommand(root),
|
|
42
|
+
PATH: `${dirname(process.execPath)}:${process.env.PATH ?? ''}`,
|
|
43
|
+
...(process.env.SUPERCODE_BIN ? { SUPERCODE_BIN: process.env.SUPERCODE_BIN } : {}),
|
|
44
|
+
...(process.env.XDG_STATE_HOME ? { XDG_STATE_HOME: process.env.XDG_STATE_HOME } : {}),
|
|
45
|
+
};
|
|
46
|
+
const seconds = Math.max(Number(kanban.dispatch_interval_seconds) || 60, 1);
|
|
47
|
+
let running = false;
|
|
48
|
+
let stopped = false;
|
|
49
|
+
const said = new Set();
|
|
50
|
+
// what a Hermes gateway does around the dispatcher and this one does not, said once per board
|
|
51
|
+
const unserved = (board) => {
|
|
52
|
+
if (board.subscriptions && !said.has(`subs:${board.board}`)) { said.add(`subs:${board.board}`); log('warn', `board ${board.board}: ${board.subscriptions} chat subscription(s) are delivered by Hermes's gateway notifier, which this orchestrator does not run`); }
|
|
53
|
+
if (board.triage && !said.has(`triage:${board.board}`)) { said.add(`triage:${board.board}`); log('warn', `board ${board.board}: ${board.triage} triage task(s) wait for decomposition, which Hermes's gateway runs on its auxiliary model and this orchestrator does not`); }
|
|
54
|
+
};
|
|
55
|
+
const tick = async () => {
|
|
56
|
+
if (running || stopped) return;
|
|
57
|
+
running = true;
|
|
58
|
+
try {
|
|
59
|
+
for (const board of await kanbanTick(root, env)) {
|
|
60
|
+
if (board.error) log('warn', `board ${board.board}: ${board.error}`);
|
|
61
|
+
unserved(board);
|
|
62
|
+
for (const [task, assignee] of board.spawned ?? []) log('info', `board ${board.board}: ${task} spawned for ${assignee}`);
|
|
63
|
+
for (const [what, ids] of [['reclaimed', board.reclaimed], ['crashed', board.crashed], ['timed out', board.timed_out]]) {
|
|
64
|
+
if (Array.isArray(ids) && ids.length) log('info', `board ${board.board}: ${what} ${ids.join(', ')}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
} catch (error) {
|
|
68
|
+
log('warn', `board: tick failed: ${error.message}`);
|
|
69
|
+
} finally {
|
|
70
|
+
running = false;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const first = setTimeout(() => { void tick(); }, 5_000);
|
|
74
|
+
const timer = setInterval(() => { void tick(); }, seconds * 1000);
|
|
75
|
+
first.unref?.(); timer.unref?.();
|
|
76
|
+
log('info', `board: dispatching every ${seconds}s, each task on its assignee's worker`);
|
|
77
|
+
return {
|
|
78
|
+
tick,
|
|
79
|
+
stop() { stopped = true; clearTimeout(first); clearInterval(timer); lock.release(); },
|
|
80
|
+
};
|
|
81
|
+
}
|
package/commands.mjs
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Chat commands (docs/ORCHESTRATOR-IR.md §2.7, §4.5) and the channel frame.
|
|
2
|
+
// Pure: no node imports.
|
|
3
|
+
|
|
4
|
+
export const COMMANDS = new Set(['new', 'reset', 'sethome', 'status', 'whoami', 'model', 'sessions', 'handoff', 'bg']);
|
|
5
|
+
|
|
6
|
+
/** `/word rest` → { name, args } for a known command; null otherwise (unknown `/word` goes to the worker as text). */
|
|
7
|
+
export function parseCommand(text) {
|
|
8
|
+
const m = /^\/([a-z]+)(?:\s+([\s\S]*))?$/.exec(text.trim());
|
|
9
|
+
if (!m || !COMMANDS.has(m[1])) return null;
|
|
10
|
+
return { name: m[1], args: (m[2] ?? '').trim() };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The channel frame: who said it, where, and when, as text prepended to the
|
|
15
|
+
* message. Text because the orchestrator does not own the worker's system
|
|
16
|
+
* prompt (§0.3). One line, stable, greppable by the mock worker in tests.
|
|
17
|
+
*/
|
|
18
|
+
export function frame(profile, key, sender, senderName, at, text) {
|
|
19
|
+
const where = [key.platform, key.kind, key.chat_id, key.thread_id ? `thread ${key.thread_id}` : null].filter(Boolean).join(' ');
|
|
20
|
+
// In a Room the author reads as the token that addresses them (`aaron <@prn_…>`): written back, it is a mention RH2
|
|
21
|
+
// records and notifies, so the agent learns the form from every line it hears.
|
|
22
|
+
const who = key.platform === 'rh2' && /^prn_[a-z0-9]+$/.test(String(sender)) ? `${senderName ?? sender} <@${sender}>` : senderName ? `${senderName} (${sender})` : sender;
|
|
23
|
+
return `[channel: ${where} · from: ${who} · at: ${at} · profile: ${profile}]\n${text}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const REPLY = Object.freeze({
|
|
27
|
+
newSession: 'New session started.',
|
|
28
|
+
reset: 'Session reset.',
|
|
29
|
+
homeSet: 'Home chat set.',
|
|
30
|
+
// Hermes's gateway's own words (gateway/run.py), so a stranger is told the same thing under either runtime
|
|
31
|
+
notPaired: (code, platform, profile) => `Hi~ I don't recognize you yet!\n\nHere's your pairing code: \`${code}\`\n\nAsk the bot owner to run:\n\`hermes ${profile && profile !== 'default' ? `-p ${profile} ` : ''}pairing approve ${platform} ${code}\``,
|
|
32
|
+
pairingBusy: 'Too many pairing requests right now~ Please try again later!',
|
|
33
|
+
paired: 'Paired.',
|
|
34
|
+
modelApplied: (m) => `Model set to ${m} for this conversation; applies on the next session.`,
|
|
35
|
+
bgQueued: (id) => `Queued as job ${id}.`,
|
|
36
|
+
interrupted: 'Session interrupted by a restart; send your message again.',
|
|
37
|
+
});
|