@volter/supercode-orchestrator 0.5.228 → 0.5.230
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 +18 -2
- package/activation.mjs +1 -1
- package/adapters/agent-mailbox.mjs +10 -0
- package/adapters/api-server.mjs +6 -0
- package/agents.mjs +149 -40
- package/apply/doors.mjs +3 -1
- package/bin/agent-channel.mjs +7 -2
- package/bin/connection-runner.mjs +60 -19
- package/bin/hermes-worker.mjs +2 -1
- package/bin/orchestrator.mjs +3 -3
- package/bin/runtime.mjs +3 -2
- package/board/agent-current.mjs +82 -0
- package/board/agent-declarations.mjs +10 -3
- package/board/args.mjs +137 -6
- package/board/call.mjs +16 -1
- package/board/caller-session.mjs +3 -1
- package/board/capacity.mjs +59 -45
- package/board/card-state.mjs +14 -5
- package/board/ci.mjs +521 -178
- package/board/cli.mjs +614 -209
- package/board/client.mjs +64 -21
- package/board/close-current.mjs +36 -0
- package/board/current-records.mjs +44 -0
- package/board/custody.mjs +2 -1
- package/board/dispatch.mjs +1973 -846
- package/board/doors.mjs +2 -2
- package/board/engine.mjs +623 -85
- package/board/event-stream.mjs +10 -5
- package/board/fence.mjs +2 -0
- package/board/files.mjs +93 -62
- package/board/generation-owner.mjs +188 -53
- package/board/git-document.mjs +3 -1
- package/board/github-rest.mjs +29 -7
- package/board/incidents.mjs +5 -17
- package/board/listing.mjs +14 -8
- package/board/local-admission.mjs +59 -0
- package/board/lock-holder.mjs +18 -6
- package/board/machine-fleet.mjs +66 -13
- package/board/owner-door-worker.mjs +3 -3
- package/board/owner-door.mjs +17 -7
- package/board/owner-health.mjs +12 -15
- package/board/owner-interface.mjs +6 -0
- package/board/publication-reader.mjs +50 -0
- package/board/published.mjs +40 -12
- package/board/queries.mjs +24 -4
- package/board/record-view.mjs +30 -3
- package/board/request-mail.mjs +37 -6
- package/board/resting-admission.mjs +318 -0
- package/board/resting-codec-qualifications.json +33 -0
- package/board/resting-protocol.mjs +71 -0
- package/board/resting-state-protocol.json +120 -0
- package/board/resting-state.mjs +113 -0
- package/board/runtime-door.mjs +2 -2
- package/board/session-continuity.mjs +127 -0
- package/board/session-work.mjs +25 -13
- package/board/standalone-declaration.mjs +24 -0
- package/board/storage-contract.mjs +129 -7
- package/board/store.mjs +73 -11
- package/board/tools.mjs +56 -13
- package/board/wake.mjs +18 -5
- package/board/workflow.mjs +284 -82
- package/board/workspace.mjs +86 -9
- package/board/ztrack.mjs +344 -91
- package/connection.mjs +72 -17
- package/gates.mjs +2 -1
- package/hand-off.mjs +4 -3
- package/hermes-cli.mjs +5 -3
- package/hermes-locks.mjs +5 -2
- package/hosted.mjs +2 -1
- package/index.d.ts +2 -0
- package/index.mjs +1 -1
- package/kanban-worker.mjs +63 -13
- package/loop.mjs +33 -0
- package/mcp/hermes-kanban-tools.json +109 -50
- package/mcp/server.mjs +7 -1
- package/ontology.d.ts +26 -0
- package/orchestration.mjs +15 -4
- package/package.json +19 -12
- package/session-recovery.mjs +2 -1
- package/worker-route.mjs +37 -19
package/board/client.mjs
CHANGED
|
@@ -15,8 +15,10 @@ import { noteCommandError, noteStep } from '@volter/supercode-harness-sdk/slow-l
|
|
|
15
15
|
* The run-on-the-machine verbs (BOARD_OPERATION_VERBS: the owner itself, its init, repair and housekeeping, and the
|
|
16
16
|
* verbs that hold the call open) and help are acts of their own, run here.
|
|
17
17
|
*/
|
|
18
|
-
export async function runWorkflow(argv) {
|
|
19
|
-
|
|
18
|
+
export async function runWorkflow(argv, { serveBoardOwner } = {}) {
|
|
19
|
+
// The owner retains malformed or unsupported requests for its manager. The
|
|
20
|
+
// client reads only enough context to address that owner, preserving argv.
|
|
21
|
+
const args = parse(argv, { contextOnly: true });
|
|
20
22
|
const [verb] = args._;
|
|
21
23
|
// This home's REST ledger is a local diagnostic, not a card verb. It needs neither a running board owner nor
|
|
22
24
|
// a workflow/identity/network read just to explain which callers already issued requests.
|
|
@@ -29,32 +31,65 @@ export async function runWorkflow(argv) {
|
|
|
29
31
|
return 0;
|
|
30
32
|
}
|
|
31
33
|
const help = !verb || verb === 'help' || args.help;
|
|
34
|
+
const service = !help && ['serve', 'daemon'].includes(verb);
|
|
32
35
|
const root = help ? null : homeOf(args);
|
|
33
36
|
if (!help && verb === 'handover') return sendToOwner(root, argv, args);
|
|
34
|
-
|
|
37
|
+
// Planning reads the admitted owner's card view. A standalone planner has no incumbent-reader grant and must
|
|
38
|
+
// not open/migrate the backing just to explain eligibility (including a bounded pause or backlog).
|
|
39
|
+
if (!help && verb === 'dispatch' && args['dry-run']) return sendToOwner(root, argv, args);
|
|
40
|
+
if (service) {
|
|
35
41
|
const { inBoardGeneration } = await import('./generation.mjs');
|
|
36
42
|
if (inBoardGeneration) {
|
|
43
|
+
// A generation is its owner's service, never its starter's caller, whichever release's owner started it (ADR 0013,
|
|
44
|
+
// "A service carries no caller's identity"): what an older owner passed on is dropped here, so a handover to
|
|
45
|
+
// this code cleans the generation that dispatches.
|
|
46
|
+
(await import('@volter/supercode-teams/service-environment')).dropOwnCallerIdentity();
|
|
37
47
|
process.send({ type: 'startup-step', step: 'loading the board dispatcher', budget_ms: 8000 });
|
|
38
|
-
return (await import('./cli.mjs')).serveBoardGeneration(root,
|
|
48
|
+
return (await import('./cli.mjs')).serveBoardGeneration(root, argv);
|
|
39
49
|
}
|
|
40
|
-
return (await import('./generation-owner.mjs')).serveBoardOwner(root, args);
|
|
41
50
|
}
|
|
42
51
|
if (help || BOARD_OPERATION_VERBS.has(verb)) {
|
|
43
|
-
// The board has one writer.
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
52
|
+
// The board has one writer. Writing operations and initial service admission hold its lock; a service then
|
|
53
|
+
// releases the admission lease so its stable owner can acquire the lifetime lease. Other operations only read.
|
|
54
|
+
const writes = !help && (service || exclusive(verb, args));
|
|
55
|
+
// its own process reads and writes the boards on the incumbent's resting-state reader (local-admission.mjs), from
|
|
56
|
+
// before it takes the lock: taking it fences every board in a transaction (fence.mjs takeEpoch)
|
|
57
|
+
if (!help) await (await import('./local-admission.mjs')).admitLocalRestingReader(root);
|
|
47
58
|
let lock = null;
|
|
48
59
|
if (writes) {
|
|
49
|
-
try { lock = await takeHome(root); }
|
|
60
|
+
try { lock = await takeHome(root, service); }
|
|
50
61
|
catch (error) {
|
|
51
62
|
noteCommandError(error, { phase: 'board ownership' });
|
|
52
|
-
|
|
53
|
-
|
|
63
|
+
if (error?.code === 'EHELD') {
|
|
64
|
+
if (!service) return sendToOwner(root, argv, args);
|
|
65
|
+
try { parse(argv); }
|
|
66
|
+
catch { return sendToOwner(root, argv, args, { preserveArgv: true }); }
|
|
67
|
+
}
|
|
68
|
+
throw error;
|
|
54
69
|
}
|
|
55
70
|
}
|
|
56
|
-
const [{ runVerb }, { closeWorld }] = await Promise.all([import('./cli.mjs'), import('../orchestration.mjs')]);
|
|
57
|
-
|
|
71
|
+
const [{ runVerb, admitBoardService }, { closeWorld }] = await Promise.all([import('./cli.mjs'), import('../orchestration.mjs')]);
|
|
72
|
+
let failure, failed = false, admission = null, code = null;
|
|
73
|
+
try {
|
|
74
|
+
if (!service) return await runVerb(help ? ['help'] : argv);
|
|
75
|
+
admission = await admitBoardService(argv);
|
|
76
|
+
if (!admission) return 0;
|
|
77
|
+
const firstReaderEpoch = lock.firstReaderEpoch;
|
|
78
|
+
await lock.release(); lock = null;
|
|
79
|
+
code = await serveBoardOwner(admission.root, admission.args, firstReaderEpoch);
|
|
80
|
+
return code;
|
|
81
|
+
}
|
|
82
|
+
catch (error) { failed = true; failure = error; throw error; }
|
|
83
|
+
finally {
|
|
84
|
+
const cleanup = await Promise.allSettled([Promise.resolve().then(() => lock?.release()), Promise.resolve().then(() => closeWorld())]);
|
|
85
|
+
const errors = cleanup.filter(result => result.status === 'rejected').map(result => result.reason);
|
|
86
|
+
if (errors.length) {
|
|
87
|
+
const error = new AggregateError([...(failed ? [failure] : []), ...errors], 'board operation cleanup failed', { cause: failed ? failure : errors[0] });
|
|
88
|
+
admission?.finish(error, 1);
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
admission?.finish(failed ? failure : null, code);
|
|
92
|
+
}
|
|
58
93
|
}
|
|
59
94
|
return sendToOwner(root, argv, args);
|
|
60
95
|
}
|
|
@@ -70,22 +105,30 @@ function exclusive(verb, args) {
|
|
|
70
105
|
}
|
|
71
106
|
|
|
72
107
|
/** Hold the home's lock and take a fencing epoch, as its owner does; rejects when an owner holds the lock. */
|
|
73
|
-
async function takeHome(root) {
|
|
108
|
+
async function takeHome(root, service) {
|
|
74
109
|
mkdirSync(root, { recursive: true });
|
|
75
110
|
const [{ holdLock }, { takeEpoch }, { listBoards, openBoard }] = await Promise.all([import('./lock-holder.mjs'), import('./fence.mjs'), import('./store.mjs')]);
|
|
76
111
|
const lock = await holdLock(join(boardHome(root), 'board.lock'), { onLost: (error) => { process.stderr.write(`supercode workflow: ${error.message}; stopping\n`); process.exit(1); } });
|
|
77
|
-
|
|
78
|
-
|
|
112
|
+
try {
|
|
113
|
+
const fresh = service && (await import('./resting-admission.mjs')).pristineHome(root);
|
|
114
|
+
const epoch = takeEpoch(root, () => listBoards(root).map((slug) => openBoard(root, slug)));
|
|
115
|
+
return { release: lock.release, firstReaderEpoch: fresh ? epoch : null };
|
|
116
|
+
} catch (error) {
|
|
117
|
+
try { await lock.release(); }
|
|
118
|
+
catch (cleanup) { throw new AggregateError([error, cleanup], 'board ownership startup and cleanup failed', { cause: error }); }
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
79
121
|
}
|
|
80
122
|
|
|
81
|
-
async function sendToOwner(root, argv, args) {
|
|
123
|
+
async function sendToOwner(root, argv, args, { preserveArgv = false } = {}) {
|
|
82
124
|
const began = performance.now();
|
|
83
125
|
const sid = processSession();
|
|
84
126
|
noteStep('board caller session', performance.now() - began);
|
|
85
127
|
const preparing = performance.now();
|
|
86
128
|
const harness = sid ? (sid === process.env.CODEX_THREAD_ID ? 'codex' : 'claude-code') : null;
|
|
87
|
-
//
|
|
88
|
-
|
|
129
|
+
// Malformed service input travels verbatim for retention; its unsupported file options acquire no stdin.
|
|
130
|
+
// A file named `-` on an ordinary verb is the caller's standard input, read here and sent with the verb.
|
|
131
|
+
const stdin = !preserveArgv && [args.file, args['body-file']].includes('-') ? readFileSync(0, 'utf8') : null;
|
|
89
132
|
// The caller's own selections travel with the verb, so the owner never answers it with its own: its board (the
|
|
90
133
|
// board it names, else the one its environment names) and its Teams context (named, else the one selected here).
|
|
91
134
|
const board = args.board ?? process.env.HERMES_KANBAN_BOARD ?? null;
|
|
@@ -94,7 +137,7 @@ async function sendToOwner(root, argv, args) {
|
|
|
94
137
|
const asking = performance.now();
|
|
95
138
|
let answer;
|
|
96
139
|
try {
|
|
97
|
-
answer = await askBoardOwner(root, { argv: withoutHome(argv), board, caller: null, session: sid ? { id: sid, harness } : null, facts, cwd: process.cwd(), stdin });
|
|
140
|
+
answer = await askBoardOwner(root, { argv: preserveArgv ? argv : withoutHome(argv), board, caller: null, session: sid ? { id: sid, harness } : null, facts, cwd: process.cwd(), stdin });
|
|
98
141
|
} finally { noteStep('board owner answer', performance.now() - asking); }
|
|
99
142
|
// the owner took the verb and the call ended with no answer: its outcome is unknown, never a plain failure
|
|
100
143
|
if (answer.unanswered === true) {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { claimer, json, now, view } from './store.mjs';
|
|
2
|
+
import { detachedCard, detachedSession } from './custody.mjs';
|
|
3
|
+
import { ACTIVE_RUN_IDS } from './queries.mjs';
|
|
4
|
+
|
|
5
|
+
const identity = s => [s?.pane ?? null, s?.session_id ?? null, s?.machine ?? null,
|
|
6
|
+
s?.harness ?? null, s?.surface ?? null, s?.address ?? null, s?.pid ?? null, s?.launch_key ?? null];
|
|
7
|
+
const machineName = name => String(name ?? '').split('.')[0].toLowerCase().replace(/[^a-z0-9_-]/g, '-');
|
|
8
|
+
/** A candidate grants no close authority after an await. This guard runs inside the public
|
|
9
|
+
* close's reservation, immediately before each physical mutation, and again before confirmation. */
|
|
10
|
+
export function deferredCloseCurrent(board, row, session, claim) {
|
|
11
|
+
return view(board, db => {
|
|
12
|
+
const latest = db.prepare('SELECT task_id,ended_at,metadata FROM task_runs WHERE id=?').get(row.id);
|
|
13
|
+
const current = json(latest?.metadata, {})?.supercode;
|
|
14
|
+
if (!latest || latest.task_id !== row.task_id || latest.ended_at !== row.ended_at || !current
|
|
15
|
+
|| JSON.stringify(identity(current)) !== JSON.stringify(identity(session))
|
|
16
|
+
|| detachedCard(db, row.task_id) || detachedSession(db, current) || current.close_confirmed_at
|
|
17
|
+
|| current.close_transferred_to || current.released || !current.close_requested
|
|
18
|
+
|| current.close_requested !== claim.requested || current.close_claim?.lock !== claimer()
|
|
19
|
+
|| current.close_claim?.lock !== claim.lock || current.close_claim?.expires !== claim.expires
|
|
20
|
+
|| !(Number(current.close_claim?.expires) > now())
|
|
21
|
+
|| current.monitor_held !== claim.monitor || current.retain_after_run !== claim.retained) return false;
|
|
22
|
+
return !db.prepare(`SELECT metadata FROM task_runs WHERE id IN (${ACTIVE_RUN_IDS})`).all().some(run => {
|
|
23
|
+
const s = json(run.metadata, {})?.supercode;
|
|
24
|
+
if (!s) return false;
|
|
25
|
+
const sameMachine = machineName(s.machine) === machineName(session.machine);
|
|
26
|
+
return sameMachine && ((session.pane && s.pane === session.pane)
|
|
27
|
+
|| (session.session_id && s.session_id === session.session_id && (s.harness ?? 'claude-code') === (session.harness ?? 'claude-code'))
|
|
28
|
+
|| (session.surface === 'headless' && session.pid && s.pid === session.pid));
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function requireDeferredClose(board, row, session, claim) {
|
|
34
|
+
if (!deferredCloseCurrent(board, row, session, claim)) throw Object.assign(
|
|
35
|
+
new Error('close dropped: its run, claim, retention or active session binding changed'), { closeSuperseded: true });
|
|
36
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// A held connection's current card projection. TEMP triggers identify its own writes, including
|
|
2
|
+
// writes without public events. They persist no state: a foreign commit, reload, lost watermark or
|
|
3
|
+
// overlay/schema change requires a bounded rebuild. Rollback marks are harmless extra refreshes.
|
|
4
|
+
import { LIST_CONNECTION, listRevision, readBoundary } from './listing.mjs';
|
|
5
|
+
|
|
6
|
+
export const currentRecordConnection = db => db[LIST_CONNECTION] ?? db;
|
|
7
|
+
const tracked = new WeakMap();
|
|
8
|
+
const RETAINED_CARDS = 4096;
|
|
9
|
+
export function trackCurrentRecords(db) {
|
|
10
|
+
if (tracked.has(db)) return;
|
|
11
|
+
const state = { epoch: 0, floor: 0, reset: 0, rewritten: 0, cards: new Map() };
|
|
12
|
+
tracked.set(db, state);
|
|
13
|
+
db.function('supercode_current_changed', (table, id, rewrite) => {
|
|
14
|
+
const epoch = ++state.epoch;
|
|
15
|
+
if (table === 'custody') state.reset = epoch;
|
|
16
|
+
if (table === 'events' && rewrite) state.rewritten = epoch;
|
|
17
|
+
if (id != null) { state.cards.delete(id); state.cards.set(id, epoch); }
|
|
18
|
+
if (state.cards.size > RETAINED_CARDS) {
|
|
19
|
+
const first = state.cards.keys().next().value;
|
|
20
|
+
state.floor = state.cards.get(first); state.cards.delete(first);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
const specs = [['tasks', 'id', 'cards'], ['supercode_cards', 'task_id', 'cards'],
|
|
24
|
+
['task_runs', 'task_id', 'runs'], ['task_events', 'task_id', 'events'],
|
|
25
|
+
['task_links', 'parent_id', 'links'], ['supercode_session_custody', 'task_id', 'custody']];
|
|
26
|
+
for (const [table, key, kind] of specs) for (const [verb, rows] of [['INSERT', ['NEW']], ['UPDATE', ['OLD', 'NEW']], ['DELETE', ['OLD']]]) {
|
|
27
|
+
const calls = rows.flatMap(row => [key, ...(table === 'task_links' ? ['child_id'] : [])]
|
|
28
|
+
.map(column => `SELECT supercode_current_changed('${kind}',${row}.${column},${verb === 'INSERT' ? 0 : 1});`)).join(' ');
|
|
29
|
+
db.exec(`CREATE TEMP TRIGGER IF NOT EXISTS supercode_current_${table}_${verb.toLowerCase()} AFTER ${verb} ON main.${table} BEGIN ${calls} END`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Snapshot changes at one read boundary; never trust row hints across a foreign commit. */
|
|
34
|
+
export function currentRecordChanges(db, previous = null) {
|
|
35
|
+
return readBoundary(db, () => {
|
|
36
|
+
const revision = listRevision(db), connection = currentRecordConnection(db), state = tracked.get(connection);
|
|
37
|
+
const token = { ...revision, epoch: state?.epoch ?? null, rewritten: state?.rewritten ?? null };
|
|
38
|
+
const reset = !previous || !state || previous.connection !== token.connection || previous.data !== token.data
|
|
39
|
+
|| previous.mainSchema !== token.mainSchema || previous.tempSchema !== token.tempSchema
|
|
40
|
+
|| previous.epoch == null || previous.epoch < state.floor || previous.epoch < state.reset;
|
|
41
|
+
const cards = reset ? null : previous.epoch === state.epoch ? [] : [...state.cards].filter(([, epoch]) => epoch > previous.epoch).map(([id]) => id);
|
|
42
|
+
return { token, reset, cards, eventsRewritten: reset || previous.rewritten !== token.rewritten };
|
|
43
|
+
});
|
|
44
|
+
}
|
package/board/custody.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { latestEvent } from './queries.mjs';
|
|
1
2
|
// Native identity survives a handback. Custody decides what this board may do with it.
|
|
2
3
|
import { randomUUID } from 'node:crypto';
|
|
3
4
|
import { hostname } from 'node:os';
|
|
@@ -73,7 +74,7 @@ export function detachSession(board, id, person) {
|
|
|
73
74
|
return sessionAddress(s) && sessionAddress(s) !== address && s.close_requested && !s.close_confirmed_at && !s.close_transferred_to && !s.detached_at;
|
|
74
75
|
})) throw new Error('another native session on this card has an unresolved close; settle that receipt before detaching');
|
|
75
76
|
assertNoSessionOperation(db, id, address);
|
|
76
|
-
const adoption = db
|
|
77
|
+
const adoption = latestEvent(db, id, ['dispatch_held','dispatch_released','session_adopted','adoption_failed','adoption_claimed'], 'id,kind,payload');
|
|
77
78
|
const request = json(adoption?.payload, {});
|
|
78
79
|
if (adoption?.kind === 'adoption_claimed' && request.lock && operationAlive({ owner: request.lock })) throw new Error('an adoption is already being bound; wait for its answer before detaching');
|
|
79
80
|
for (const other of db.prepare('SELECT r.id,r.task_id,r.metadata FROM task_runs r JOIN tasks t ON t.current_run_id=r.id WHERE t.id<>?').all(id)) {
|