@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/ir.mjs ADDED
@@ -0,0 +1,205 @@
1
+ // The orchestrator IR's runtime vocabulary: the surface key, the delivery
2
+ // target, the route match, and the empty shapes the loop starts from.
3
+ //
4
+ // The records themselves are the ontology's (`crates/interchange/src/orchestration/`,
5
+ // generated into `ontology.d.ts`), and reading or writing a folder is the
6
+ // harness's orchestration doors (`orchestration.mjs`). What lives here is what the reducer,
7
+ // the scheduler, the operator door and the adapters compute with in memory,
8
+ // and nothing that decodes or encodes a file.
9
+ //
10
+ // Nothing in this module touches the filesystem or a clock.
11
+
12
+ export class LoadError extends Error {
13
+ constructor(file, key, message) {
14
+ super(`${file}${key ? ` [${key}]` : ''}: ${message}`);
15
+ this.name = 'LoadError';
16
+ this.file = file;
17
+ this.key = key;
18
+ }
19
+ }
20
+
21
+ // ---------------------------------------------------------------- helpers
22
+
23
+ const isObj = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
24
+
25
+ /**
26
+ * Hermes `cron/jobs.py:2486-2492`: `context_from` is a list of job ids (or
27
+ * `"self"`) whose most recent output is prepended at fire time; a string is
28
+ * one id, an empty value is none. Stored as a list or null, as Hermes stores it.
29
+ */
30
+ export function decodeContextFrom(file, key, v) {
31
+ if (v === undefined || v === null) return null;
32
+ const items = typeof v === 'string' ? [v] : Array.isArray(v) ? v : null;
33
+ if (!items) throw new LoadError(file, key, 'expected a job id, a list of job ids, or "self"');
34
+ const refs = items.map((x) => String(x).trim()).filter(Boolean);
35
+ return refs.length ? refs : null;
36
+ }
37
+
38
+ // ---------------------------------------------------------------- surface keys
39
+
40
+ export const CHAT_TYPES = new Set(['dm', 'group', 'channel', 'thread']);
41
+
42
+ /** `agent:<profile>:<platform>:<kind>[:<chat_id>][:<thread_id>][:<participant_id>]` */
43
+ export function renderSurfaceKey(profile, key) {
44
+ const parts = ['agent', profile || 'main', key.platform, key.kind];
45
+ if (key.chat_id) parts.push(key.chat_id);
46
+ if (key.thread_id) parts.push(key.thread_id);
47
+ if (key.participant_id) parts.push(key.participant_id);
48
+ return parts.join(':');
49
+ }
50
+
51
+ /** Inverse of renderSurfaceKey for the simple (unescaped) forms Hermes writes. */
52
+ export function parseSurfaceKey(text) {
53
+ const p = String(text).split(':');
54
+ if (p[0] !== 'agent' || p.length < 4) return null;
55
+ const key = { platform: p[2], kind: p[3] };
56
+ if (p[4]) key.chat_id = p[4];
57
+ if (p[5]) key.thread_id = p[5];
58
+ if (p[6]) key.participant_id = p[6];
59
+ return { profile: p[1] === 'main' ? 'default' : p[1], key };
60
+ }
61
+
62
+ export function surfaceKeyString(key) {
63
+ return [key.platform, key.kind, key.chat_id ?? '', key.thread_id ?? '', key.participant_id ?? ''].join('|');
64
+ }
65
+
66
+ // ---------------------------------------------------------------- targets
67
+
68
+ /** Hermes `deliver` word → Target. `origin | local | home | <platform>[:<chat_id>[:<thread_id>]]`. */
69
+ export function parseTarget(file, key, word, extra) {
70
+ if (word === undefined || word === null) return null;
71
+ if (typeof word !== 'string' || word.length === 0) throw new LoadError(file, key, 'expected a delivery word');
72
+ if (word === 'origin' || word === 'local' || word === 'home') return { kind: word };
73
+ const p = word.split(':');
74
+ const t = { kind: 'explicit', platform: p[0] };
75
+ const chat = p[1] ?? extra?.chat_id ?? null;
76
+ if (chat) t.chat_id = String(chat);
77
+ const thread = p[2] ?? extra?.thread_id ?? null;
78
+ if (thread) t.thread_id = String(thread);
79
+ return t;
80
+ }
81
+
82
+ export function renderTarget(t) {
83
+ if (!t) return null;
84
+ if (t.kind !== 'explicit') return t.kind;
85
+ let s = t.platform;
86
+ if (t.chat_id) s += `:${t.chat_id}`;
87
+ if (t.thread_id) s += `:${t.thread_id}`;
88
+ return s;
89
+ }
90
+
91
+ // ---------------------------------------------------------------- O-records
92
+
93
+ /**
94
+ * Hermes's session reset (`gateway/config.py` `SessionResetPolicy`), read from the home's own
95
+ * `session_reset:` (or `gateway.session_reset:`): `mode` none | daily | idle | both, `at_hour` (local,
96
+ * default 4), `idle_minutes` (default 1440). Hermes's default is `none`: a session never resets on its own.
97
+ */
98
+ export function sessionReset(profile) {
99
+ const config = profile?.residue?.config ?? {};
100
+ const raw = config.session_reset ?? config.gateway?.session_reset ?? {};
101
+ return {
102
+ mode: raw.mode ?? 'none',
103
+ at_hour: raw.at_hour ?? 4,
104
+ idle_minutes: raw.idle_minutes ?? 1440,
105
+ };
106
+ }
107
+
108
+ /**
109
+ * A platform's home channel as Hermes keeps it (`platforms.<p>.home_channel`, where `/sethome` saves it),
110
+ * as a chat key; with no platform, the first platform that has one. Null when there is none.
111
+ */
112
+ /**
113
+ * A surface as Hermes's session key (`gateway/session.py` `build_session_key`), the form the codec writes
114
+ * into `gateway_routing` (Rust `render_hermes_session_key`): `agent:<main|profile>:<platform>:<kind>` and
115
+ * then the chat, thread and participant that are present.
116
+ */
117
+ export function hermesSessionKey(profileName, key) {
118
+ if (key?.key?.startsWith?.('agent:')) return key.key;
119
+ const ns = !profileName || profileName === 'default' ? 'main' : profileName;
120
+ return ['agent', ns, key.platform ?? '', key.kind ?? '', key.chat_id, key.thread_id, key.participant_id].filter((part, i) => i < 4 || (part !== undefined && part !== null && part !== '')).join(':');
121
+ }
122
+
123
+ export function homeChannel(profile, platform = null) {
124
+ const channels = profile?.channels ?? {};
125
+ const names = platform ? [platform] : Object.keys(channels);
126
+ for (const name of names) {
127
+ const h = channels[name]?.extra?.home_channel;
128
+ if (h?.chat_id) {
129
+ // Hermes's home channel names no chat kind; addressed as an explicit target is (`chatKey`)
130
+ const key = { platform: name, kind: 'dm', chat_id: String(h.chat_id) };
131
+ if (h.thread_id) key.thread_id = String(h.thread_id);
132
+ return key;
133
+ }
134
+ }
135
+ return null;
136
+ }
137
+
138
+ export const END_REASONS = new Set(['idle', 'daily', 'reset', 'new', 'handoff', 'error']);
139
+
140
+ export const FIRE_STATUSES = new Set(['claimed', 'running', 'succeeded', 'failed', 'timeout', 'unknown']);
141
+
142
+ export const OBLIGATION_STATES = new Set(['pending', 'sent', 'failed', 'dropped']);
143
+
144
+ /** Most-specific-first: thread > chat > guild > platform; ties by declaration order. */
145
+ export function routeSpecificity(r) {
146
+ return (r.match.thread_id ? 8 : 0) + (r.match.chat_id ? 4 : 0) + (r.match.guild_id ? 2 : 0) + 1;
147
+ }
148
+
149
+ export function resolveRoute(routes, key, guildId) {
150
+ let best = null;
151
+ for (const r of routes) {
152
+ const m = r.match;
153
+ if (m.platform !== key.platform) continue;
154
+ if (m.guild_id && m.guild_id !== guildId) continue;
155
+ if (m.chat_id && m.chat_id !== key.chat_id) continue;
156
+ if (m.thread_id && m.thread_id !== key.thread_id) continue;
157
+ if (!best || routeSpecificity(r) > routeSpecificity(best)) best = r;
158
+ }
159
+ return best ? best.profile : null;
160
+ }
161
+
162
+ // ---------------------------------------------------------------- profiles / state
163
+
164
+ export function emptyProfile(name, dir) {
165
+ return {
166
+ name, dir,
167
+ worker: null,
168
+ persona: null,
169
+ channels: {},
170
+ routes: [],
171
+ jobs: {},
172
+ subscriptions: {},
173
+ access: { allowlist: {}, admins: {}, policy: {} },
174
+ bindings: {},
175
+ fires: [],
176
+ obligations: [],
177
+ residue: { config: {}, files: [] },
178
+ };
179
+ }
180
+
181
+ export function emptyRuntime() {
182
+ return { leases: {}, queues: {}, adapters: {}, pending_requests: {}, clock: null };
183
+ }
184
+
185
+ export function emptyState(root) {
186
+ return { root, profiles: { default: emptyProfile('default', root) }, runtime: emptyRuntime() };
187
+ }
188
+
189
+ /** Stable, secret-free JSON of the persisted half of the state (runtime omitted). */
190
+ export function canonicalJson(value) {
191
+ return JSON.stringify(sortKeys(value), null, 2) + '\n';
192
+ }
193
+ export function sortKeys(v) {
194
+ if (Array.isArray(v)) return v.map(sortKeys);
195
+ if (isObj(v)) {
196
+ const out = {};
197
+ for (const k of Object.keys(v).sort()) out[k] = sortKeys(v[k]);
198
+ return out;
199
+ }
200
+ return v;
201
+ }
202
+ export function persistedView(state) {
203
+ const { runtime, ...rest } = state;
204
+ return rest;
205
+ }
@@ -0,0 +1,155 @@
1
+ // A worker for one board task (docs/plans/hermes-compat.md row 14). Hermes's dispatcher spawns its worker
2
+ // as `hermes -p <assignee> --cli --accept-hooks [--skills <s>]… [-m <model> [--provider <p>]]
3
+ // [--reasoning <e>] [--toolsets <t>] chat -q "work kanban task <id>" [-Q]`, detached, in the task's
4
+ // workspace, its output to the task's log, the task in its environment (`KANBAN_ENV`), and watches the
5
+ // pid. The orchestrator's loop runs that dispatcher with `HERMES_BIN` naming `bin/hermes-worker.mjs`, so
6
+ // this is the process Hermes spawned and watches: it runs the assignee profile's own worker for the one
7
+ // query, with Hermes's task protocol, the task's skills and the home's memory in its system prompt and the
8
+ // board's tools on its door, prints the answer to the log and exits, as `hermes chat -q` does. A profile
9
+ // whose worker is Hermes runs Hermes itself.
10
+ import { spawn } from 'node:child_process';
11
+ import { existsSync, statSync } from 'node:fs';
12
+ import { basename, dirname, join, resolve } from 'node:path';
13
+ import { SupercodeHarnessClient } from '@volter/supercode-harness-sdk';
14
+ import { load } from './orchestration.mjs';
15
+ import { INSTRUCTION_ARGUMENTS, activationCwd, activationEnv, sandboxArguments, approvalPolicy, limitArguments, materializePersona, materializeSkills, modelArguments, turnCounter } from './activation.mjs';
16
+ import { workerRoute } from './worker-route.mjs';
17
+ import { KANBAN_ENV, untrustedServerVerdict, hermesApproval, hermesInstall, hermesToolEnv, kanbanPrompt, memorySnapshot, mirrorSession } from './hermes-cli.mjs';
18
+ import { mountOrchestratorMcp } from './mcp/mount.mjs';
19
+ import { normalizeWorkerEvent, WORKER_STORE } from './loop.mjs';
20
+ import { permissionResponse } from './reducer.mjs';
21
+
22
+ /** Hermes's worker argv, as `_default_spawn` builds it. */
23
+ export function parseHermesArgv(argv) {
24
+ const out = { profile: null, skills: [], model: null, provider: null, reasoning: null, toolsets: null, query: null, quiet: false };
25
+ for (let i = 0; i < argv.length; i += 1) {
26
+ const a = argv[i];
27
+ const next = () => argv[++i];
28
+ if (a === '-p' || a === '--profile') out.profile = next();
29
+ else if (a === '--skills' || a === '-s') out.skills.push(...String(next()).split(',').map((x) => x.trim()).filter(Boolean));
30
+ else if (a === '-m' || a === '--model') out.model = next();
31
+ else if (a === '--provider') out.provider = next();
32
+ else if (a === '--reasoning') out.reasoning = next();
33
+ else if (a === '--toolsets') out.toolsets = next();
34
+ else if (a === '-q' || a === '--query') out.query = next();
35
+ else if (a === '-Q') out.quiet = true;
36
+ }
37
+ return out;
38
+ }
39
+
40
+ /** The orchestrator's root for a profile's home: the root is the `default` profile, `profiles/<name>` the rest. */
41
+ function rootOf(home) {
42
+ return basename(dirname(home)) === 'profiles' ? dirname(dirname(home)) : home;
43
+ }
44
+
45
+ export async function runKanbanWorker(argv, env = process.env) {
46
+ const say = (line) => process.stdout.write(`${line}\n`);
47
+ const args = parseHermesArgv(argv);
48
+ if (!env.HERMES_HOME || !args.query) { say('hermes-worker: HERMES_HOME and -q are required (the dispatcher sets both)'); return 2; }
49
+ const home = resolve(env.HERMES_HOME);
50
+ const root = rootOf(home);
51
+ const { state, vault } = await load(root);
52
+ const profile = Object.values(state.profiles).find((p) => resolve(p.dir) === home) ?? state.profiles[args.profile ?? 'default'];
53
+ if (!profile?.worker?.harness) { say(`hermes-worker: no worker for profile ${args.profile ?? home}`); return 2; }
54
+ if (profile.worker.harness === 'hermes') return runHermes(argv, env);
55
+ const harness = profile.worker.harness;
56
+ const task = Object.fromEntries(KANBAN_ENV.filter((k) => env[k]).map((k) => [k, env[k]]));
57
+ const taskId = task.HERMES_KANBAN_TASK ?? 'none';
58
+ const workspace = env.HERMES_KANBAN_WORKSPACE;
59
+ const cwd = workspace && existsSync(workspace) && statSync(workspace).isDirectory() ? workspace : activationCwd(profile, profile.worker.cwd);
60
+ const worker = { ...profile.worker, ...(args.model ? { model: args.model } : {}) };
61
+ if (args.toolsets) say(`hermes-worker: --toolsets ${args.toolsets} is Hermes's agent's setting, carried and not performed for ${harness}`);
62
+
63
+ materializePersona(profile);
64
+ materializeSkills(profile);
65
+ const route = workerRoute(profile, worker, vault);
66
+ const surfaceKey = `kanban|task|${taskId}`;
67
+ const runEnv = { ...hermesToolEnv(profile.dir), ...activationEnv({ root, profile, surfaceKey, vault }), ...route.env, ...task };
68
+ // Hermes's system prompt for a dispatched worker: the home's memory, the task protocol, the task's skills
69
+ const memory = await memorySnapshot(profile.dir).catch(() => '');
70
+ const board = await kanbanPrompt(profile.dir, { skills: args.skills, task: taskId, env: task });
71
+ const instructions = [memory, board.guidance, board.skills].filter(Boolean).join('\n\n');
72
+
73
+ const client = new SupercodeHarnessClient(env.SUPERCODE_BIN ? { command: env.SUPERCODE_BIN } : {});
74
+ try {
75
+ return await runTurn({ client, profile, harness, worker, route, root, vault, surfaceKey, runEnv, instructions, task, cwd, args, say });
76
+ } finally {
77
+ await client.close().catch(() => {});
78
+ }
79
+ }
80
+
81
+ async function runTurn({ client, profile, harness, worker, route, root, vault, surfaceKey, runEnv, instructions, task, cwd, args, say }) {
82
+ const descriptor = (await client.supportReport()).harnesses.find((h) => h.id === harness);
83
+ const base = descriptor?.runtime?.default_launch;
84
+ if (!base) { say(`hermes-worker: ${harness} publishes no launch`); return 2; }
85
+ const reasoning = args.reasoning && harness === 'codex' ? ['-c', `model_reasoning_effort=${JSON.stringify(args.reasoning)}`] : [];
86
+ const launch = {
87
+ program: base.program,
88
+ arguments: [...(base.arguments ?? []), ...modelArguments(worker), ...route.args, ...(instructions && INSTRUCTION_ARGUMENTS[harness] ? INSTRUCTION_ARGUMENTS[harness](instructions) : []), ...reasoning, ...sandboxArguments(profile, harness), ...limitArguments(profile, worker, (level, line) => say(line))],
89
+ env: { ...(base.env ?? {}), ...runEnv },
90
+ };
91
+ const mount = mountOrchestratorMcp({ root, profile, harness, surfaceKey, launch, vault, task });
92
+ const policy = approvalPolicy(profile, harness);
93
+ const runtime = await client.startManagedRuntime({ harness, cwd, launch: mount.launch, ...(mount.mcp_servers ? { mcp_servers: mount.mcp_servers } : {}), ...(policy ? { approval_policy: policy } : {}) });
94
+ let text = '';
95
+ // `agent.max_turns` for a worker that takes no cap at launch, held as the daemon holds it (loop.mjs `holdTurnLimit`)
96
+ const counter = turnCounter(profile, harness);
97
+ const outcome = await new Promise((done) => {
98
+ const on = (ev) => {
99
+ if (counter?.count(ev)) {
100
+ say(`hermes-worker: ${counter.cap + 1} model calls in one turn passes agent.max_turns ${counter.cap}; the turn is interrupted`);
101
+ void Promise.resolve(runtime.interrupt?.()).catch((error) => say(`hermes-worker: interrupt failed: ${error.message}`));
102
+ }
103
+ let w = normalizeWorkerEvent(ev);
104
+ if (!w) return;
105
+ if (w.kind === 'final' && counter) w = counter.settle(w);
106
+ if (w.kind === 'text') text += w.block && text ? `\n\n${w.delta}` : w.delta;
107
+ else if (w.kind === 'request') {
108
+ // Hermes's approval rule for a single-query worker (`approvals.single_query_mode` for a dangerous command)
109
+ const gated = untrustedServerVerdict(profile, w.tool, 'single_query', { mcp: w.mcp && !String(w.tool ?? '').startsWith('mcp__orchestrator__') });
110
+ void (gated ? Promise.resolve(gated) : hermesApproval(profile.dir, { command: w.command ?? null, context: 'single_query' })).catch(() => null).then((hermes) => {
111
+ const approve = hermes ? hermes.verdict === 'allow' : profile.worker?.permission?.unattended === 'approve';
112
+ if (!hermes || hermes.verdict !== 'allow') say(`hermes-worker: ${approve ? 'approved' : 'denied'} a permission request with nobody attending (${hermes ? `Hermes: ${hermes.why}` : 'worker.permission.unattended'}): ${w.subject}`);
113
+ runtime.respond(JSON.parse(w.id), permissionResponse(w.options, approve, hermes?.message ?? null)).catch(() => {});
114
+ });
115
+ } else if (w.kind === 'final') {
116
+ if (w.outcome === 'error') say(`hermes-worker: the turn failed: ${w.message ?? 'no reason given'}`);
117
+ done(w.outcome === 'error' ? 1 : 0);
118
+ } else if (w.kind === 'error' || w.kind === 'ended') {
119
+ say(`hermes-worker: the worker ${w.kind === 'error' ? 'failed' : 'ended'}: ${w.message ?? w.reason ?? 'no reason given'}`);
120
+ done(1);
121
+ }
122
+ };
123
+ runtime.on('event', on);
124
+ runtime.on('closed', on);
125
+ runtime.on('runtimeError', on);
126
+ runtime.sendInput(args.query).catch((error) => { say(`hermes-worker: ${error.message}`); done(1); });
127
+ });
128
+ if (text) say(text);
129
+ // the run's transcript in the home's session store, as a Hermes worker's is: tagged `kanban`
130
+ await mirrorRun(client, profile, harness, runtime.handle.runtime_id).catch((error) => say(`hermes-worker: mirror: ${error.message}`));
131
+ await runtime.close().catch(() => {});
132
+ return outcome;
133
+ }
134
+
135
+ async function mirrorRun(client, profile, harness, sessionId) {
136
+ const store = WORKER_STORE[harness];
137
+ if (!store) return;
138
+ const { sessions } = await client.discover({ harnesses: [harness], homes: store(join(profile.dir, harness)), include_child_sessions: true, root_session_id: sessionId, limit: 8 });
139
+ const locator = sessions.find((d) => d.locator.session_id === sessionId)?.locator;
140
+ if (!locator) return;
141
+ const loaded = await client.loadWindow(locator, { message_limit: 1_000_000 });
142
+ await mirrorSession(profile.dir, { id: sessionId, sessionKey: null, binding: { key: { platform: 'kanban' }, worker: { harness } }, session: loaded.session, locator: locator.storage?.path ?? null });
143
+ }
144
+
145
+ /** A profile whose worker is Hermes: the dispatcher's own command, run as Hermes would have run it. */
146
+ function runHermes(argv, env) {
147
+ const install = hermesInstall();
148
+ if (!install?.bin) { process.stdout.write('hermes-worker: the profile\'s worker is Hermes, and no Hermes is installed on PATH\n'); return 2; }
149
+ const { HERMES_BIN: _, ...rest } = env;
150
+ return new Promise((done) => {
151
+ const child = spawn(join(install.bin, 'hermes'), argv, { env: rest, stdio: 'inherit' });
152
+ child.on('close', (code) => done(code ?? 1));
153
+ child.on('error', () => done(1));
154
+ });
155
+ }
package/lease.mjs ADDED
@@ -0,0 +1,51 @@
1
+ // The daemon's lease (`<home>/orchestrator.lock`), read the way the Rust
2
+ // `Lease` reads it. A lease names a pid on a machine: a home on a volume that
3
+ // another machine mounted (the sandbox before this one) names that machine's
4
+ // pid, which says nothing here, and a pid from before a restart names nothing
5
+ // now. So a lease is live only on the machine and boot that wrote it.
6
+ import { readFileSync } from 'node:fs';
7
+ import { hostname } from 'node:os';
8
+ import { join } from 'node:path';
9
+
10
+ export const LOCK_FILE = 'orchestrator.lock';
11
+
12
+ /** This machine's name and, on Linux, its boot id (Rust `this_host`). */
13
+ export function thisHost() {
14
+ let boot = null;
15
+ try { boot = readFileSync('/proc/sys/kernel/random/boot_id', 'utf8').trim() || null; } catch { /* not Linux */ }
16
+ return { host: hostname() || null, boot };
17
+ }
18
+
19
+ /** `kill(pid, 0)`: liveness without touching the process. EPERM means alive. */
20
+ export function pidIsLive(pid) {
21
+ if (!Number.isInteger(pid) || pid <= 0) return false;
22
+ try {
23
+ process.kill(pid, 0);
24
+ return true;
25
+ } catch (error) {
26
+ return error.code === 'EPERM';
27
+ }
28
+ }
29
+
30
+ /** Whether the lease is this machine's, since this boot. Only such a lease's pid may be signalled. */
31
+ export function leaseIsHere(lease) {
32
+ const here = thisHost();
33
+ if (lease?.host && lease.host !== here.host) return false;
34
+ if (lease?.boot && here.boot && lease.boot !== here.boot) return false;
35
+ return true;
36
+ }
37
+
38
+ /** Whether the daemon a lease names is alive now: here, and its pid live. */
39
+ export function leaseIsLive(lease) {
40
+ return Boolean(lease) && leaseIsHere(lease) && pidIsLive(lease.pid);
41
+ }
42
+
43
+ /** The lease, whether or not its daemon lives; null when absent or unreadable. */
44
+ export function readLease(root) {
45
+ try {
46
+ const lease = JSON.parse(readFileSync(join(root, LOCK_FILE), 'utf8'));
47
+ return Number.isInteger(lease?.pid) ? lease : null;
48
+ } catch {
49
+ return null;
50
+ }
51
+ }