@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/workspace.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// Filesystem/RPC work happens before a board transaction; the ownership check is pure.
|
|
2
2
|
import { existsSync, realpathSync } from 'node:fs';
|
|
3
|
-
import { basename, dirname } from 'node:path';
|
|
3
|
+
import { basename, dirname, posix, win32 } from 'node:path';
|
|
4
4
|
import { runtimeDoor } from './runtime-door.mjs';
|
|
5
5
|
import { inspectCheckout } from '@volter/teams/checkout';
|
|
6
6
|
import { localMachine, normalMachine, commandEnv } from './dispatch.mjs';
|
|
7
|
-
import { cardHandleOf } from './store.mjs';
|
|
7
|
+
import { cardHandleOf, json, REVIEW_ALLOCATION_KEY, REVIEW_ALLOCATION_MACHINE, REVIEW_ALLOCATION_PATH } from './store.mjs';
|
|
8
8
|
|
|
9
9
|
// Repository identities are portable input values; the daemon owns their checkout resolution.
|
|
10
10
|
function repositoryId(value) {
|
|
@@ -63,6 +63,35 @@ export async function cardCommitsOn(machine, path, card, branches = [], ref = nu
|
|
|
63
63
|
if (!answer || typeof answer !== 'object' || !('at' in answer) || (answer.at !== null && !(Number(answer.at) > 0))) throw new Error(`machine ${name} answered no commit reading`);
|
|
64
64
|
return { at: answer.at === null ? null : Number(answer.at) };
|
|
65
65
|
}
|
|
66
|
+
/** Card worktree folder `path` on `machine` as its machine reads it now: its folder as that machine resolves it (its
|
|
67
|
+
* links followed), or null when the folder is not there (this machine's own file reading; another's inspect door, whose
|
|
68
|
+
* git says the folder is gone). Throws when it could not be read. */
|
|
69
|
+
export async function worktreePresent(machine, path) {
|
|
70
|
+
if (!machine || normalMachine(machine) === localMachine()) {
|
|
71
|
+
if (!existsSync(path)) return null;
|
|
72
|
+
try { return realpathSync(path); } catch { return path; }
|
|
73
|
+
}
|
|
74
|
+
try { const facts = await workspaceDoor(machine, 'inspect', path); return facts?.path ?? facts?.root ?? path; }
|
|
75
|
+
catch (error) { if (folderGone(error)) return null; throw error; }
|
|
76
|
+
}
|
|
77
|
+
/** Card worktree `path` on `machine` at its card's close (teams workspaces.mjs retireCardWorktree): removed through git
|
|
78
|
+
* when it is on the card's `branch`, its branch merged and its tree clean, else kept and described. Every machine's
|
|
79
|
+
* folder, this one's included, is read and retired by that machine's daemon through its door, which also forgets a
|
|
80
|
+
* removed checkout in its workspace map: the board reaches the machine layer only through its doors. A machine that
|
|
81
|
+
* cannot answer throws, so the board asks again. */
|
|
82
|
+
export async function retireWorktree(machine, path, { merged = [], branch = null } = {}) {
|
|
83
|
+
const local = !machine || normalMachine(machine) === localMachine();
|
|
84
|
+
const name = local ? localMachine() : normalMachine(machine);
|
|
85
|
+
if (!local && presence?.known === true && presence.offline?.has(name)) throw new Error(`worktree ${path} on ${machine}: machine ${name} is offline`);
|
|
86
|
+
// its two network reads (origin's default branch, its fetch) are each bounded at 30 s there
|
|
87
|
+
const r = await runtimeDoor(local ? null : machine, 'harness.v1.workspaces.retire', { path, merged, branch }, { timeout: 120_000, env: commandEnv() });
|
|
88
|
+
if (r.code !== 0) {
|
|
89
|
+
let refusal = null; try { refusal = JSON.parse(String(r.stderr).trim().split('\n').pop()); } catch { /* said in its own words */ }
|
|
90
|
+
if (refusal?.code === -32601 || /unknown method/.test(refusal?.message ?? '')) throw new Error(`machine ${name}'s daemon has no worktree retiring door (harness.v1.workspaces.retire); its card worktree ${path} is retired once that machine runs a supercode that has it`);
|
|
91
|
+
throw new Error(`worktree ${path} on ${machine}: ${String(refusal?.message ?? (r.stderr || r.stdout || 'its door did not answer')).trim().slice(0, 300)}`);
|
|
92
|
+
}
|
|
93
|
+
return JSON.parse(r.stdout);
|
|
94
|
+
}
|
|
66
95
|
/** A workspace declaration that asks no machine (scratch, or a repository named by id), else null: a path is declared
|
|
67
96
|
* through its machine's door (declareWorkspace). */
|
|
68
97
|
export function plainWorkspace(kind, value, branch = null) {
|
|
@@ -140,7 +169,12 @@ export function unrecordedMaking(db, id) {
|
|
|
140
169
|
}
|
|
141
170
|
/** Card `task` as its workspace is resolved: its handle, the branch the board last named for it and, when `claims`, its
|
|
142
171
|
* claim on that branch (branchClaim). */
|
|
172
|
+
const workspaceTags = (board, task) => {
|
|
173
|
+
const raw = task.tags ?? (task.id ? board.db.prepare('SELECT tags FROM supercode_cards WHERE task_id=?').get(task.id)?.tags : null);
|
|
174
|
+
return Array.isArray(raw) ? raw : raw ? JSON.parse(raw) : task.tenant ? [task.tenant] : [];
|
|
175
|
+
};
|
|
143
176
|
export const workspaceTask = (board, task, { claims = false } = {}) => ({ ...task, handle: task.handle ?? cardHandleOf(board, task.id),
|
|
177
|
+
tags: workspaceTags(board, task),
|
|
144
178
|
named_branch: task.id ? lastNamed(board.db, task.id)?.branch ?? null : null,
|
|
145
179
|
...(claims ? { branch_claim: task.workspace_kind === 'worktree' ? branchClaim(board.db, task.id, task.branch_name) : null } : {}) });
|
|
146
180
|
/** A `dir` workspace declared as a folder its machine answers is no git checkout at all (git's "not a git repository (or
|
|
@@ -155,6 +189,7 @@ const plainFolder = (declared, error) => {
|
|
|
155
189
|
};
|
|
156
190
|
/** Its machine's git answered that the folder is not there (git's "cannot change to '<path>': No such file or directory"). */
|
|
157
191
|
const folderGone = (error) => /cannot change to .*: No such file or directory/.test(error?.message ?? '');
|
|
192
|
+
const recordingOptions = task => ({ include_recordings: Array.isArray(task.tags) && task.tags.includes('workspace-recordings') });
|
|
158
193
|
/** A card's worktree folder deleted from disk (removed through git, or by hand) is made again from the card's branch by
|
|
159
194
|
* its machine's resolve door (teams workspaces.mjs resolve: its stale registration forgotten, the branch's pushed head
|
|
160
195
|
* fetched), at the folder it had: a card whose recorded worktree is gone is re-prepared at launch, never guarded. A
|
|
@@ -166,11 +201,24 @@ async function remadeDirWorktree(task, declared, error, create) {
|
|
|
166
201
|
if (!folderGone(error) || !task.branch_name || basename(dirname(path)) !== '.worktrees') throw error;
|
|
167
202
|
const checkout = await workspaceDoor(task.machine, 'inspect', dirname(dirname(path))).catch(() => null);
|
|
168
203
|
if (!checkout?.repository || checkout.linked) throw error;
|
|
169
|
-
const made = await workspaceDoor(task.machine, 'resolve', checkout.repository, { branch: task.branch_name, solo: true, card: basename(path), create });
|
|
204
|
+
const made = await workspaceDoor(task.machine, 'resolve', checkout.repository, { branch: task.branch_name, solo: true, card: basename(path), create, ...recordingOptions(task) });
|
|
170
205
|
if (made.path !== path) throw error;
|
|
171
206
|
return { ...made, root: made.path };
|
|
172
207
|
}
|
|
173
|
-
export async function resolveWorkspace(task, create = false, { move = false } = {}) {
|
|
208
|
+
export async function resolveWorkspace(task, create = false, { move = false, role = null, cwd = null, reviewWorkspace = null } = {}) {
|
|
209
|
+
if (task.workspace_kind === 'worktree' && role === 'reviewer') {
|
|
210
|
+
// A conversation's cwd is part of its native resume identity, including reviewers predating isolated allocations.
|
|
211
|
+
if (cwd) return { ...await workspaceDoor(task.machine, 'inspect', cwd), path: cwd, reviewWorkspace };
|
|
212
|
+
const beside = reviewWorkspace?.path ?? task.workspace_path;
|
|
213
|
+
const paths = /^(?:[A-Za-z]:[\\/]|\\\\)/.test(beside ?? '') ? win32 : posix;
|
|
214
|
+
const checkout = beside && paths.basename(paths.dirname(beside)) === '.worktrees' ? paths.dirname(paths.dirname(beside)) : null;
|
|
215
|
+
const made = await workspaceDoor(task.machine, 'resolve', task.workspace_anchor, {
|
|
216
|
+
solo: true, detached: true, card: `${task.id}-review`, create, ...(checkout ? { checkout } : {}),
|
|
217
|
+
});
|
|
218
|
+
if (!made.linked || made.branch !== null)
|
|
219
|
+
throw new Error(`workspace resolve on ${task.machine ?? localMachine()} did not provide the detached reviewer allocation for ${task.id}; its answer names branch ${made.branch ?? '(none)'}`);
|
|
220
|
+
return { ...made, reviewWorkspace: { path: made.path, repository: made.repository, machine: normalMachine(task.machine ?? localMachine()) } };
|
|
221
|
+
}
|
|
174
222
|
const declared = task.workspace_anchor ?? task.workspace_path;
|
|
175
223
|
if (isWorkspacePath(declared)) {
|
|
176
224
|
if (task.workspace_kind === 'dir') return workspaceDoor(task.machine, 'inspect', declared)
|
|
@@ -196,7 +244,7 @@ export async function resolveWorkspace(task, create = false, { move = false } =
|
|
|
196
244
|
// Another machine's folder is read there, and one its git answers is gone is made again through that machine's
|
|
197
245
|
// resolve door at the same folder, as a missing folder on this machine is by the solo resolve after this branch.
|
|
198
246
|
const cached = await workspaceDoor(task.machine, 'inspect', task.workspace_path).catch((error) => { if (folderGone(error)) return null; throw error; });
|
|
199
|
-
if (!cached) return workspaceDoor(task.machine, 'resolve', declared, { branch: worktreeBranch(task), solo: true, card: worktreeFolder(task), create });
|
|
247
|
+
if (!cached) return workspaceDoor(task.machine, 'resolve', declared, { branch: worktreeBranch(task), solo: true, card: worktreeFolder(task), create, ...recordingOptions(task) });
|
|
200
248
|
if (cached.linked && basename(cached.path) === worktreeFolder(task) && basename(dirname(cached.path)) === '.worktrees' &&
|
|
201
249
|
cached.repository === repositoryId(declared)) {
|
|
202
250
|
if (cached.branch === worktreeBranch(task)) {
|
|
@@ -206,14 +254,14 @@ export async function resolveWorkspace(task, create = false, { move = false } =
|
|
|
206
254
|
// Off its branch, it is put on it only for a start of this card that no other current run shares (`move`), by its
|
|
207
255
|
// machine's resolve door when that loses nothing, which otherwise answers why not. Every other read refuses as before.
|
|
208
256
|
if (move) {
|
|
209
|
-
const made = await workspaceDoor(task.machine, 'resolve', declared, { branch: worktreeBranch(task), solo: true, card: worktreeFolder(task), create, move });
|
|
257
|
+
const made = await workspaceDoor(task.machine, 'resolve', declared, { branch: worktreeBranch(task), solo: true, card: worktreeFolder(task), create, move, ...recordingOptions(task) });
|
|
210
258
|
if (made.path === cached.path) return { ...made, root: dirname(dirname(made.path)) };
|
|
211
259
|
}
|
|
212
260
|
}
|
|
213
261
|
throw new Error(`card worktree ${task.workspace_path} does not match ${declared}, branch ${worktreeBranch(task)}; retarget its workspace explicitly`);
|
|
214
262
|
}
|
|
215
263
|
const solo = task.workspace_kind === 'worktree';
|
|
216
|
-
return workspaceDoor(task.machine, 'resolve', declared, { branch: solo ? worktreeBranch(task) : task.branch_name, solo, card: solo ? worktreeFolder(task) : task.id, create, ...(solo && task.branch_claim ? { claim: task.branch_claim } : {}) });
|
|
264
|
+
return workspaceDoor(task.machine, 'resolve', declared, { branch: solo ? worktreeBranch(task) : task.branch_name, solo, card: solo ? worktreeFolder(task) : task.id, create, ...(solo ? recordingOptions(task) : {}), ...(solo && task.branch_claim ? { claim: task.branch_claim } : {}) });
|
|
217
265
|
}
|
|
218
266
|
/** Whether another card's current run holds `task`'s recorded worktree folder on the same machine. */
|
|
219
267
|
function heldByAnotherRun(db, task) {
|
|
@@ -222,12 +270,12 @@ function heldByAnotherRun(db, task) {
|
|
|
222
270
|
return db.prepare(`SELECT t.id,t.workspace_path,c.machine FROM tasks t LEFT JOIN supercode_cards c ON c.task_id=t.id WHERE t.current_run_id IS NOT NULL AND t.workspace_kind='worktree' AND t.id!=?`).all(task.id)
|
|
223
271
|
.some(row => normalMachine(row.machine ?? localMachine()) === normalMachine(task.machine ?? localMachine()) && workspacePath(row.workspace_path, local) === mine);
|
|
224
272
|
}
|
|
225
|
-
export async function checkWorkspace(board, task) {
|
|
273
|
+
export async function checkWorkspace(board, task, options = {}) {
|
|
226
274
|
if (task.workspace_kind === 'scratch') return null;
|
|
227
275
|
task = workspaceTask(board, task, { claims: true });
|
|
228
276
|
// Every caller starts this card (a claim, its launch attempt, a person's start), so its own worktree may be put on its
|
|
229
277
|
// branch, unless another card's current run holds the same folder on this machine; a peer is only ever read.
|
|
230
|
-
const facts = await resolveWorkspace(task, false, { move: !heldByAnotherRun(board.db, task) }), peers = [];
|
|
278
|
+
const facts = await resolveWorkspace(task, false, { ...options, move: !heldByAnotherRun(board.db, task) }), peers = [];
|
|
231
279
|
const workspaceRoot = task.workspace_kind === 'worktree' ? facts.path : facts.root;
|
|
232
280
|
// New claims retain their exact checkout receipt before launch. Legacy claims without a receipt are resolved
|
|
233
281
|
// once for this check, including subdirectories and remote aliases that their retained path alone cannot identify.
|
|
@@ -248,6 +296,27 @@ const workspacePath = (path, local) => {
|
|
|
248
296
|
if (local) { try { return realpathSync(path); } catch { /* a pre-launch reservation may not exist yet */ } }
|
|
249
297
|
return path.replace(/\/+$/, '');
|
|
250
298
|
};
|
|
299
|
+
/** Allocations the board made for this card's reviewer slots, retained independently of their current conversation. */
|
|
300
|
+
export function reviewWorkspaces(db, id) {
|
|
301
|
+
// Advance by distinct indexed keys, skipping all repetitions of an allocation in prior review runs.
|
|
302
|
+
return db.prepare(`WITH RECURSIVE allocations(key) AS (
|
|
303
|
+
SELECT (SELECT ${REVIEW_ALLOCATION_KEY} FROM task_runs WHERE task_id=? AND ${REVIEW_ALLOCATION_KEY} IS NOT NULL
|
|
304
|
+
ORDER BY ${REVIEW_ALLOCATION_KEY} LIMIT 1)
|
|
305
|
+
UNION ALL SELECT (SELECT ${REVIEW_ALLOCATION_KEY} FROM task_runs WHERE task_id=? AND ${REVIEW_ALLOCATION_KEY} IS NOT NULL
|
|
306
|
+
AND ${REVIEW_ALLOCATION_KEY}>allocations.key ORDER BY ${REVIEW_ALLOCATION_KEY} LIMIT 1)
|
|
307
|
+
FROM allocations WHERE key IS NOT NULL)
|
|
308
|
+
SELECT metadata FROM task_runs WHERE id IN (SELECT (SELECT id FROM task_runs WHERE task_id=?
|
|
309
|
+
AND ${REVIEW_ALLOCATION_KEY} IS NOT NULL AND ${REVIEW_ALLOCATION_KEY}=allocations.key ORDER BY id DESC LIMIT 1)
|
|
310
|
+
FROM allocations WHERE key IS NOT NULL) ORDER BY id DESC`).all(id, id, id)
|
|
311
|
+
.map(row => { const session = json(row.metadata, {}).supercode; return { ...session.review_workspace, slot: session.slot ?? 'worker' }; });
|
|
312
|
+
}
|
|
313
|
+
/** All board-owned worktrees of a card; an adopted session's arbitrary cwd is never an allocation. */
|
|
314
|
+
export function cardWorktrees(db, task) {
|
|
315
|
+
if (task.workspace_kind !== 'worktree') return [];
|
|
316
|
+
const worktrees = [...(task.workspace_path ? [{ path: task.workspace_path, machine: task.machine, branch: task.branch_name }] : []),
|
|
317
|
+
...reviewWorkspaces(db, task.id).map(row => ({ ...row, branch: null }))];
|
|
318
|
+
return [...new Map(worktrees.map(row => [`${normalMachine(row.machine ?? localMachine())}:${row.path}`, row])).values()];
|
|
319
|
+
}
|
|
251
320
|
export function assertWorkspaceOwner(db, task, checked) {
|
|
252
321
|
if (!checked?.workspaceRoot) return;
|
|
253
322
|
const machine = normalMachine(task.machine ?? localMachine()), local = machine === localMachine();
|
|
@@ -256,6 +325,14 @@ export function assertWorkspaceOwner(db, task, checked) {
|
|
|
256
325
|
.find(row => row.id !== task.id && normalMachine(row.machine ?? localMachine()) === machine &&
|
|
257
326
|
(workspacePath(row.workspace_root, local) === path || workspacePath(row.workspace_path, local) === path || checked.peers?.includes(row.id)));
|
|
258
327
|
if (conflict) throw new Error(`workspace ${checked.path} is already used by running card ${conflict.id}; declare worktree:${checked.repository} for a solo worktree`);
|
|
328
|
+
const reviewer = db.prepare(`WITH RECURSIVE owners(id) AS (
|
|
329
|
+
SELECT (SELECT task_id FROM task_runs WHERE ${REVIEW_ALLOCATION_PATH}=? AND ${REVIEW_ALLOCATION_MACHINE}=?
|
|
330
|
+
ORDER BY task_id LIMIT 1)
|
|
331
|
+
UNION ALL SELECT (SELECT task_id FROM task_runs WHERE ${REVIEW_ALLOCATION_PATH}=? AND ${REVIEW_ALLOCATION_MACHINE}=?
|
|
332
|
+
AND task_id>owners.id ORDER BY task_id LIMIT 1) FROM owners WHERE id IS NOT NULL)
|
|
333
|
+
SELECT t.id FROM owners JOIN tasks t ON t.id=owners.id WHERE t.id!=? AND t.workspace_kind='worktree'
|
|
334
|
+
AND t.status NOT IN ('done','archived','cancelled') LIMIT 1`).get(path, machine, path, machine, task.id);
|
|
335
|
+
if (reviewer) throw new Error(`workspace ${checked.path} belongs to reviewer of card ${reviewer.id}; declare worktree:${checked.repository} for a solo worktree`);
|
|
259
336
|
}
|
|
260
337
|
|
|
261
338
|
export const isLocalWorkspace = task => !task.machine || normalMachine(task.machine) === localMachine();
|