@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/capacity.mjs
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
// caps; each names where it keeps one count (`scope`), what it counts (`counts`), which cards and sessions it is about
|
|
3
3
|
// (`where`, less `unless`) and its `max`. This module counts what a home holds now and answers whether one more card may start.
|
|
4
4
|
|
|
5
|
-
import { json } from './store.mjs';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
5
|
+
import { json, RUN_SLOT, runField } from './store.mjs';
|
|
6
|
+
import { currentRecordChanges, currentRecordConnection } from './current-records.mjs';
|
|
7
|
+
import { sessionAddress } from './custody.mjs';
|
|
8
8
|
import { setImmediate as yieldToOwner } from 'node:timers/promises';
|
|
9
|
-
import { listRevision } from './listing.mjs';
|
|
9
|
+
import { listRevision, readBoundary } from './listing.mjs';
|
|
10
|
+
import { backlogQualification } from './resting-state.mjs';
|
|
10
11
|
|
|
11
12
|
/** The scope's group a counted thing or a card falls in. */
|
|
12
13
|
const GROUP = {
|
|
@@ -38,26 +39,49 @@ export function matches(filter, thing) {
|
|
|
38
39
|
/** Whether a cap is about `thing`: its `where` matches and its `unless` (when it names anything) does not. */
|
|
39
40
|
const applies = (cap, thing) => matches(cap.where, thing) && !(Object.keys(cap.unless ?? {}).length && matches(cap.unless, thing));
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
42
|
+
// Held card rows and latest retained role slots, never a cached fleet/capacity verdict.
|
|
43
|
+
const projections = new WeakMap();
|
|
44
|
+
const CARD_ROWS = `SELECT t.id,t.status,COALESCE(c.stage,t.status) AS stage,t.assignee,t.current_run_id,t.model_override,
|
|
45
|
+
c.machine,c.subtask_of,c.paused,c.backlog,c.blocked_by,c.archived,c.detached FROM tasks t LEFT JOIN supercode_cards c ON c.task_id=t.id`;
|
|
46
|
+
async function currentHoldings(db, unchanged) {
|
|
47
|
+
const projection = projections.get(currentRecordConnection(db)) ?? { token: null, cards: new Map() };
|
|
48
|
+
projections.set(currentRecordConnection(db), projection);
|
|
49
|
+
const change = currentRecordChanges(db, projection.token);
|
|
50
|
+
const refresh = (row) => {
|
|
51
|
+
if (!row || row.status === 'archived' || row.detached != null) return;
|
|
52
|
+
// A flag cannot erase unresolved work, even if an external SQLite writer bypassed import admission.
|
|
53
|
+
if (json(row.backlog, null) && backlogQualification(db, row.id).qualified) return;
|
|
54
|
+
// The index groups a card's slot names; only their newest session-bearing run is decoded.
|
|
55
|
+
const slots = db.prepare(`SELECT id,metadata FROM task_runs WHERE id IN (
|
|
56
|
+
SELECT MAX(id) FROM task_runs WHERE task_id=? AND ${runField('session_id')} IS NOT NULL
|
|
57
|
+
AND ${runField('session_id')} NOT IN ('',0) GROUP BY ${RUN_SLOT}) ORDER BY id DESC`).all(row.id)
|
|
58
|
+
.map(run => ({ ...json(run.metadata, {})?.supercode, run: run.id }));
|
|
59
|
+
const current = row.current_run_id ? { ...json(db.prepare('SELECT metadata FROM task_runs WHERE id=?').get(row.current_run_id)?.metadata, {})?.supercode, run: row.current_run_id } : null;
|
|
60
|
+
projection.cards.set(row.id, { row, slots, current });
|
|
61
|
+
};
|
|
62
|
+
if (change.reset) {
|
|
63
|
+
projection.cards.clear();
|
|
64
|
+
let cursor = '';
|
|
65
|
+
for (;;) {
|
|
66
|
+
await yieldToOwner(); if (!unchanged()) return null;
|
|
67
|
+
const rows = readBoundary(db, () => db.prepare(`${CARD_ROWS} WHERE t.id>? ORDER BY t.id LIMIT 32`).all(cursor));
|
|
68
|
+
if (!rows.length) break;
|
|
69
|
+
readBoundary(db, () => { for (const row of rows) refresh(row); });
|
|
70
|
+
cursor = rows.at(-1).id;
|
|
71
|
+
if (rows.length < 32) break;
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
for (let at = 0; at < change.cards.length; at += 32) {
|
|
75
|
+
await yieldToOwner(); if (!unchanged()) return null;
|
|
76
|
+
readBoundary(db, () => { for (const id of change.cards.slice(at, at + 32)) {
|
|
77
|
+
projection.cards.delete(id);
|
|
78
|
+
refresh(db.prepare(`${CARD_ROWS} WHERE t.id=?`).get(id));
|
|
79
|
+
} });
|
|
55
80
|
}
|
|
56
|
-
if (rows.length < 64) break;
|
|
57
|
-
cursor = rows.at(-1).id;
|
|
58
|
-
await yieldToOwner();
|
|
59
81
|
}
|
|
60
|
-
|
|
82
|
+
if (change.reset || change.cards.length) projection.cards = new Map([...projection.cards].sort(([a],[b]) => a < b ? -1 : a > b ? 1 : 0));
|
|
83
|
+
projection.token = change.token;
|
|
84
|
+
return projection.cards;
|
|
61
85
|
}
|
|
62
86
|
|
|
63
87
|
const sameRevision = (a, b) => Object.keys(a).every(key => a[key] === b[key]);
|
|
@@ -89,28 +113,18 @@ export async function holdings(board, options) {
|
|
|
89
113
|
*/
|
|
90
114
|
async function readHoldings(board, { lanes, fleetOk, alive, liveRecord, modelOf = () => null, storedOf = null, machineOf, home = null, runLaneOf = null }, unchanged) {
|
|
91
115
|
const out = [];
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const candidates = board.db.prepare('SELECT id FROM tasks WHERE id>? AND id<=? ORDER BY id LIMIT 32').all(cursor, high);
|
|
99
|
-
if (!candidates.length) break;
|
|
100
|
-
cursor = candidates.at(-1).id;
|
|
101
|
-
const rows = board.db.prepare(`SELECT t.id,t.status,t.assignee,t.current_run_id,t.model_override,c.machine,c.subtask_of
|
|
102
|
-
FROM tasks t LEFT JOIN supercode_cards c ON c.task_id=t.id
|
|
103
|
-
WHERE t.id IN (SELECT value FROM json_each(?)) AND t.status!='archived' AND c.detached IS NULL`)
|
|
104
|
-
.all(JSON.stringify(candidates.map(row => row.id)));
|
|
105
|
-
for (const row of rows) {
|
|
106
|
-
await yieldToOwner();
|
|
107
|
-
if (!unchanged()) return null;
|
|
116
|
+
const rows = await currentHoldings(board.db, unchanged);
|
|
117
|
+
if (rows === null) return null;
|
|
118
|
+
const custody = new Set(board.db.prepare('SELECT address FROM supercode_session_custody').all().map(row => row.address));
|
|
119
|
+
let at = 0;
|
|
120
|
+
for (const { row, slots, current: retainedCurrent } of rows.values()) {
|
|
121
|
+
if (at++ % 32 === 0) { await yieldToOwner(); if (!unchanged()) return null; }
|
|
108
122
|
const lane = lanes[row.assignee] ?? {};
|
|
109
123
|
const card = {
|
|
110
|
-
card: row.id, board: board.slug, assignee: row.assignee ?? null, status:
|
|
124
|
+
card: row.id, board: board.slug, assignee: row.assignee ?? null, status: row.stage,
|
|
111
125
|
// what the board stores for an arc is read through its workflow's stored_as rules now, never the column a write has
|
|
112
126
|
// not yet brought up to date (a paused card counted as running until its row caught up)
|
|
113
|
-
stored: !row.subtask_of && storedOf ? storedOf(row.id) : row.status,
|
|
127
|
+
stored: json(row.backlog, null) ? row.stage : !row.subtask_of && storedOf ? storedOf(row.id, row) : row.status,
|
|
114
128
|
// a card that names no machine runs on this home's: a cap on this machine counts it
|
|
115
129
|
machine: row.machine ? machineOf(row.machine) : home,
|
|
116
130
|
model: row.model_override ?? lane.model ?? null, harness: lane.harness ?? null,
|
|
@@ -124,15 +138,16 @@ async function readHoldings(board, { lanes, fleetOk, alive, liveRecord, modelOf
|
|
|
124
138
|
: executionHost(runLaneOf ? runLaneOf(row) : lane, row.machine, home, machineOf), harness: s.harness ?? card.harness ?? 'claude-code',
|
|
125
139
|
model: (s.session_id ? modelOf(s) : null) ?? configured };
|
|
126
140
|
};
|
|
127
|
-
const sessions = fleetOk ?
|
|
128
|
-
if (sessions === null) return null;
|
|
141
|
+
const sessions = fleetOk ? slots : [];
|
|
129
142
|
const held = [];
|
|
130
143
|
let current = null;
|
|
131
144
|
if (row.current_run_id) {
|
|
132
|
-
current =
|
|
145
|
+
current = retainedCurrent;
|
|
133
146
|
out.push({ ...sessionOf(current, current.slot ?? 'worker'), session: undefined, slot: undefined, counts: 'runs' });
|
|
134
147
|
}
|
|
135
|
-
for (const
|
|
148
|
+
for (const s of sessions) {
|
|
149
|
+
const slot = s.slot ?? 'worker';
|
|
150
|
+
if (s.detached_at || s.close_confirmed_at || s.released || custody.has(sessionAddress(s))) continue;
|
|
136
151
|
if (!alive(s)) continue;
|
|
137
152
|
const session = sessionOf(s, slot);
|
|
138
153
|
held.push(session);
|
|
@@ -145,7 +160,6 @@ async function readHoldings(board, { lanes, fleetOk, alive, liveRecord, modelOf
|
|
|
145
160
|
// until it is done); its model and harness are its run's session's, else its newest live session's, else its own
|
|
146
161
|
// a subtask is a task of its arc: it never runs or holds a slot of its own
|
|
147
162
|
if (!row.subtask_of) out.push({ ...(held[0] ?? { ...card, unconfigured: !card.model }), session: undefined, slot: undefined, counts: 'cards' });
|
|
148
|
-
}
|
|
149
163
|
}
|
|
150
164
|
return out;
|
|
151
165
|
}
|
package/board/card-state.mjs
CHANGED
|
@@ -11,19 +11,28 @@ export const cardStateSql = ({ nativeHolds = false } = {}) => nativeHolds ? `CAS
|
|
|
11
11
|
WHEN COALESCE(c.stage,t.status) IN ('done','cancelled','archived') OR t.status = 'archived' THEN t.status
|
|
12
12
|
WHEN c.paused IS NOT NULL AND c.paused != 'null' THEN 'paused'
|
|
13
13
|
WHEN CASE WHEN json_valid(c.blocked_by) THEN json_array_length(c.blocked_by) ELSE 0 END > 0 THEN 'blocked'
|
|
14
|
+
WHEN c.backlog IS NOT NULL AND c.backlog != 'null' THEN 'todo'
|
|
14
15
|
WHEN t.status IN ('paused','blocked') THEN CASE WHEN COALESCE(c.stage,t.status) IN ('paused','blocked') THEN 'todo' ELSE COALESCE(c.stage,t.status) END
|
|
15
16
|
ELSE t.status END` : 't.status';
|
|
16
17
|
|
|
17
18
|
export function cardState(task, { nativeHolds = false } = {}) {
|
|
18
19
|
const paused = parse(task.paused, null);
|
|
20
|
+
const backlog = parse(task.backlog, null);
|
|
19
21
|
const blocked_by = parse(task.blocked_by, []);
|
|
20
22
|
const lane = task.stage ?? task.workflow_status ?? task.status;
|
|
21
23
|
const ended = ['done', 'cancelled', 'archived'].includes(lane) || task.status === 'archived';
|
|
22
24
|
const status = !nativeHolds ? task.status : ended ? task.status : paused ? 'paused' : blocked_by.length ? 'blocked'
|
|
23
|
-
: task.status === 'blocked' || task.status === 'paused' ? (['blocked', 'paused'].includes(lane) ? 'todo' : lane) : task.status;
|
|
24
|
-
return { ...task, status, paused, blocked_by };
|
|
25
|
+
: backlog ? 'todo' : task.status === 'blocked' || task.status === 'paused' ? (['blocked', 'paused'].includes(lane) ? 'todo' : lane) : task.status;
|
|
26
|
+
return { ...task, status, paused, backlog, blocked_by };
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
const deadlineText = (until) => {
|
|
30
|
+
const date = new Date(Number.isFinite(until) ? until * 1000 : NaN);
|
|
31
|
+
return Number.isFinite(date.getTime()) ? date.toISOString() : null;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const holdDescription = (task) => [
|
|
35
|
+
task.backlog && `backlog by ${task.backlog.by ?? 'unknown'}: ${task.backlog.reason ?? 'reason not recorded'}`,
|
|
36
|
+
task.status === 'paused' && task.paused && `paused by ${task.paused.by ?? 'unknown'}${deadlineText(task.paused.until) ? ` until ${deadlineText(task.paused.until)}` : ' (deadline not recorded)'}: ${task.paused.reason ?? 'reason not recorded'}`,
|
|
37
|
+
task.status === 'blocked' && task.blocked_by?.length && `blocked by ${task.blocked_by.map(b => `${b.id}${b.reason ? `: ${b.reason}` : ''}`).join(', ')}`,
|
|
38
|
+
].filter(Boolean).join('; ') || null;
|