@volter/supercode-orchestrator 0.4.148 → 0.4.149
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/board/ci.mjs +129 -56
- package/board/cli.mjs +21 -23
- package/board/dispatch.mjs +39 -24
- package/board/engine.mjs +50 -18
- package/board/store.mjs +6 -3
- package/board/tools.mjs +2 -2
- package/board/workflow.mjs +20 -27
- package/board/workspace.mjs +26 -0
- package/board/ztrack.mjs +779 -542
- package/hermes-locks.mjs +1 -20
- package/mcp/hermes-kanban-tools.json +40 -18
- package/package.json +2 -2
- package/board/rows.mjs +0 -132
package/hermes-locks.mjs
CHANGED
|
@@ -13,7 +13,7 @@ import { spawn, spawnSync } from 'node:child_process';
|
|
|
13
13
|
import { createHash } from 'node:crypto';
|
|
14
14
|
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, unlinkSync, writeSync } from 'node:fs';
|
|
15
15
|
import { homedir } from 'node:os';
|
|
16
|
-
import {
|
|
16
|
+
import { join, resolve } from 'node:path';
|
|
17
17
|
|
|
18
18
|
/** Hermes's gateway kind, the `kind` of every record it writes. */
|
|
19
19
|
export const GATEWAY_KIND = 'hermes-gateway';
|
|
@@ -143,25 +143,6 @@ export function holdGatewayLock(home, { python = 'python3' } = {}) {
|
|
|
143
143
|
return holdFileLock(join(resolve(home), 'gateway.lock'), home, { python, what: `${resolve(home)} is served by`, tail: 'one runtime serves a home at a time' });
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
/**
|
|
147
|
-
* Hold the board's one dispatcher lock, `<root>/kanban/.dispatcher.lock` (Hermes's
|
|
148
|
-
* `_acquire_singleton_lock`, which every Hermes gateway takes before it dispatches). Resolves
|
|
149
|
-
* `{ release }`; rejects, naming the holder, when another dispatcher holds it.
|
|
150
|
-
*/
|
|
151
|
-
export function holdDispatcherLock(root, { python = 'python3' } = {}) {
|
|
152
|
-
const path = join(resolve(root), 'kanban', '.dispatcher.lock');
|
|
153
|
-
mkdirSync(join(resolve(root), 'kanban'), { recursive: true });
|
|
154
|
-
return holdFileLock(path, root, { python, what: `the board at ${resolve(root)} is dispatched by`, tail: 'one dispatcher serves a board' });
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Hold one board's per-tick lock, `<kanban.db>.dispatch.lock` (Hermes's `_dispatch_tick_lock`), for one tick: a
|
|
159
|
-
* one-shot `dispatch` never ticks a board beside another dispatcher. Rejects, naming the holder, when it is held.
|
|
160
|
-
*/
|
|
161
|
-
export function holdBoardTickLock(dbPath, { python = 'python3' } = {}) {
|
|
162
|
-
return holdFileLock(`${dbPath}.dispatch.lock`, dirname(dbPath), { python, what: `the board ${dbPath} is being ticked by`, tail: 'one tick at a time' });
|
|
163
|
-
}
|
|
164
|
-
|
|
165
146
|
function holdFileLock(path, home, { python, what, tail }) {
|
|
166
147
|
return new Promise((done, fail) => {
|
|
167
148
|
let child;
|
|
@@ -183,17 +183,25 @@
|
|
|
183
183
|
"type": "string",
|
|
184
184
|
"description": "Exact pushed worktree branch head for independent review."
|
|
185
185
|
},
|
|
186
|
-
"branch": {
|
|
187
|
-
"type": "string",
|
|
188
|
-
"description": "The worktree branch whose pushed head is reviewed before merge."
|
|
189
|
-
},
|
|
190
186
|
"pr": {
|
|
191
|
-
"type":
|
|
192
|
-
|
|
187
|
+
"type": [
|
|
188
|
+
"string",
|
|
189
|
+
"array"
|
|
190
|
+
],
|
|
191
|
+
"items": {
|
|
192
|
+
"type": "string"
|
|
193
|
+
},
|
|
194
|
+
"description": "Every opened PR the arc changed, one URL each (its own repository's and any other repository it changed)."
|
|
193
195
|
},
|
|
194
196
|
"pr_head": {
|
|
195
|
-
"type":
|
|
196
|
-
|
|
197
|
+
"type": [
|
|
198
|
+
"string",
|
|
199
|
+
"array"
|
|
200
|
+
],
|
|
201
|
+
"items": {
|
|
202
|
+
"type": "string"
|
|
203
|
+
},
|
|
204
|
+
"description": "The full 40-character pushed head SHA of each PR, in the same order as pr."
|
|
197
205
|
}
|
|
198
206
|
},
|
|
199
207
|
"required": [
|
|
@@ -361,10 +369,6 @@
|
|
|
361
369
|
"inputSchema": {
|
|
362
370
|
"type": "object",
|
|
363
371
|
"properties": {
|
|
364
|
-
"review_only": {
|
|
365
|
-
"type": "boolean",
|
|
366
|
-
"description": "Declare a standalone independent review job, where permitted by the home's manager policy. Its verdict is accepted without another review of the review; implementation arcs keep their independent review requirement."
|
|
367
|
-
},
|
|
368
372
|
"title": {
|
|
369
373
|
"type": "string",
|
|
370
374
|
"description": "Short task title (required)."
|
|
@@ -563,16 +567,34 @@
|
|
|
563
567
|
"description": "The pushed worktree branch the reviewer inspected."
|
|
564
568
|
},
|
|
565
569
|
"reviewed_sha": {
|
|
566
|
-
"type":
|
|
567
|
-
|
|
570
|
+
"type": [
|
|
571
|
+
"string",
|
|
572
|
+
"array"
|
|
573
|
+
],
|
|
574
|
+
"items": {
|
|
575
|
+
"type": "string"
|
|
576
|
+
},
|
|
577
|
+
"description": "Every requested PR head this independent reviewer inspected (full SHAs)."
|
|
568
578
|
},
|
|
569
579
|
"pr": {
|
|
570
|
-
"type":
|
|
571
|
-
|
|
580
|
+
"type": [
|
|
581
|
+
"string",
|
|
582
|
+
"array"
|
|
583
|
+
],
|
|
584
|
+
"items": {
|
|
585
|
+
"type": "string"
|
|
586
|
+
},
|
|
587
|
+
"description": "PR URLs, optional when using the recorded review candidate."
|
|
572
588
|
},
|
|
573
589
|
"pr_head": {
|
|
574
|
-
"type":
|
|
575
|
-
|
|
590
|
+
"type": [
|
|
591
|
+
"string",
|
|
592
|
+
"array"
|
|
593
|
+
],
|
|
594
|
+
"items": {
|
|
595
|
+
"type": "string"
|
|
596
|
+
},
|
|
597
|
+
"description": "Full PR head SHAs; reviewers can use reviewed_sha instead."
|
|
576
598
|
}
|
|
577
599
|
},
|
|
578
600
|
"required": [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-orchestrator",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.149",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The orchestrator runtime over the Volter Harness ontology: one typed operational model whose folder is its serialization, read and written through the harness orchestration doors (docs/ORCHESTRATOR-IR.md)",
|
|
6
6
|
"exports": {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@volter/supercode-harness-sdk": "^0.3.41",
|
|
28
|
-
"@volter/ztrack": "^2.4.
|
|
28
|
+
"@volter/ztrack": "^2.4.21",
|
|
29
29
|
"yaml": "^2.9.0",
|
|
30
30
|
"@volter/supercode-teams": "^0.2.145"
|
|
31
31
|
},
|
package/board/rows.mjs
DELETED
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
// Normalized runtime rows. Card text remains in the document; this stores only runtime
|
|
2
|
-
// tables, run fields and small indexing metadata. No application locks or snapshot rewrites.
|
|
3
|
-
import { json } from './store.mjs';
|
|
4
|
-
const quote = s => `"${s.replaceAll('"', '""')}"`;
|
|
5
|
-
const RUNTIME = new Set(['claim_lock','claim_expires','worker_pid','last_heartbeat_at','current_run_id','consecutive_failures','last_failure_error','block_recurrences','session_id','workspace_path']);
|
|
6
|
-
const OMIT = new Set(['tasks','supercode_cards','task_links','sqlite_sequence']);
|
|
7
|
-
export class RuntimeRows {
|
|
8
|
-
constructor(db) { this.db=db; this.revision=null; this.tables=new Map(); this.runtime={}; this.clocks={}; this.sequences={}; this.cachedTasks={}; this.cachedCards={}; }
|
|
9
|
-
normalized() { return Boolean(this.db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='runtime_rows'").get()); }
|
|
10
|
-
watch(mem) {
|
|
11
|
-
mem.exec('CREATE TEMP TABLE IF NOT EXISTS _runtime_dirty (name TEXT, key TEXT)');
|
|
12
|
-
for (const {name,sql} of mem.prepare("SELECT name,sql FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").all()) {
|
|
13
|
-
const columns=mem.prepare(`PRAGMA table_info(${quote(name)})`).all();
|
|
14
|
-
const keys=columns.filter(c=>c.pk).sort((a,b)=>a.pk-b.pk).map(c=>c.name);
|
|
15
|
-
if (!keys.length) keys.push('rowid');
|
|
16
|
-
this.tables.set(name,{name,sql,keys,columns});
|
|
17
|
-
const key=prefix=>`json_array(${keys.map(k=>`${prefix}.${quote(k)}`).join(',')})`;
|
|
18
|
-
for(const [suffix,verb,prefix] of [['i','INSERT','NEW'],['u','UPDATE','NEW'],['d','DELETE','OLD']])
|
|
19
|
-
mem.exec(`CREATE TEMP TRIGGER IF NOT EXISTS ${quote(`_runtime_${name}_${suffix}`)} AFTER ${verb} ON main.${quote(name)} BEGIN INSERT OR IGNORE INTO _runtime_dirty VALUES ('${name}',${key(prefix)}); END`);
|
|
20
|
-
}
|
|
21
|
-
mem.exec('DELETE FROM _runtime_dirty');
|
|
22
|
-
}
|
|
23
|
-
insert(mem,name,row,key=null) {
|
|
24
|
-
if(row===null) {
|
|
25
|
-
const table=this.tables.get(name);if(!table)return;
|
|
26
|
-
mem.prepare(`DELETE FROM ${quote(name)} WHERE ${table.keys.map(k=>`${quote(k)} IS ?`).join(' AND ')}`).run(...JSON.parse(key));return;
|
|
27
|
-
}
|
|
28
|
-
const keys=Object.keys(row);
|
|
29
|
-
mem.prepare(`INSERT OR REPLACE INTO ${quote(name)} (${keys.map(quote).join(',')}) VALUES (${keys.map(()=>'?').join(',')})`).run(...keys.map(k=>row[k]??null));
|
|
30
|
-
}
|
|
31
|
-
load(mem,{fresh=false}={}) {
|
|
32
|
-
if(!this.normalized())return false;
|
|
33
|
-
if(fresh) {
|
|
34
|
-
for(const row of this.db.prepare('SELECT name,sql FROM runtime_schema').all()) mem.exec(row.sql.replace('CREATE TABLE ','CREATE TABLE IF NOT EXISTS '));
|
|
35
|
-
this.watch(mem);
|
|
36
|
-
}
|
|
37
|
-
const query=this.revision===null?'SELECT * FROM runtime_rows':'SELECT * FROM runtime_rows WHERE revision>?';
|
|
38
|
-
const rows=this.revision===null?this.db.prepare(query).all():this.db.prepare(query).all(this.revision);
|
|
39
|
-
for(const row of rows) {
|
|
40
|
-
const value=json(row.value,null);
|
|
41
|
-
if(row.name==='@runtime') { if(value)this.runtime[row.key]=value;else delete this.runtime[row.key]; }
|
|
42
|
-
else if(row.name==='@clock') { if(value)this.clocks[row.key]=value;else delete this.clocks[row.key]; }
|
|
43
|
-
else if(row.name==='@sequence') { if(value)this.sequences[row.key]=value;else delete this.sequences[row.key]; }
|
|
44
|
-
else if(row.name==='@task_cache') { if(value)this.cachedTasks[row.key]=value;else delete this.cachedTasks[row.key]; }
|
|
45
|
-
else if(row.name==='@card_cache') { if(value)this.cachedCards[row.key]=value;else delete this.cachedCards[row.key]; }
|
|
46
|
-
else this.insert(mem,row.name,value,row.key);
|
|
47
|
-
}
|
|
48
|
-
if (mem.prepare("SELECT 1 FROM sqlite_master WHERE name='sqlite_sequence'").get()) {
|
|
49
|
-
for (const [name, value] of Object.entries(this.sequences)) {
|
|
50
|
-
const sequence = mem.prepare('SELECT seq FROM sqlite_sequence WHERE name=?').get(name);
|
|
51
|
-
if (sequence) mem.prepare('UPDATE sqlite_sequence SET seq=MAX(seq,?) WHERE name=?').run(value.seq,name);
|
|
52
|
-
else mem.prepare('INSERT INTO sqlite_sequence(name,seq) VALUES(?,?)').run(name,value.seq);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
this.revision=Number(this.db.prepare("SELECT value FROM state WHERE key='rows_revision'").get()?.value??0);
|
|
56
|
-
return true;
|
|
57
|
-
}
|
|
58
|
-
changes(mem,{all=false}={}) {
|
|
59
|
-
const dirty=all?[...this.tables.values()].flatMap(t=>mem.prepare(`SELECT ${t.keys.map(quote).join(',')} FROM ${quote(t.name)}`).all().map(row=>({name:t.name,key:JSON.stringify(t.keys.map(k=>row[k]))}))):mem.prepare('SELECT DISTINCT name,key FROM _runtime_dirty').all();
|
|
60
|
-
const changes = dirty.map(({name,key})=>{
|
|
61
|
-
const table=this.tables.get(name);
|
|
62
|
-
const row=mem.prepare(`SELECT * FROM ${quote(name)} WHERE ${table.keys.map(k=>`${quote(k)} IS ?`).join(' AND ')}`).get(...JSON.parse(key))??null;
|
|
63
|
-
if(name==='tasks')return {name:'@runtime',key:JSON.parse(key)[0],value:row&&Object.fromEntries(Object.entries(row).filter(([k])=>RUNTIME.has(k))),source:{name,key,row}};
|
|
64
|
-
if(name==='supercode_cards')return {name:'@clock',key:JSON.parse(key)[0],value:row&&{workspace_root:row.workspace_root,since:row.since,after_fired:row.after_fired,next_run_at:row.next_run_at},source:{name,key,row}};
|
|
65
|
-
return {name,key,value:row,source:{name,key,row}};
|
|
66
|
-
}).filter(change=>{
|
|
67
|
-
if(all)return true;
|
|
68
|
-
const {name,key,value,source}=change;
|
|
69
|
-
if(name==='task_links') {
|
|
70
|
-
const [parent,child]=JSON.parse(key);
|
|
71
|
-
const before=json(this.cachedCards[child]?.blocked_by,[]).some(b=>b.kind==='card'&&b.id===parent);
|
|
72
|
-
return before!==Boolean(source.row);
|
|
73
|
-
}
|
|
74
|
-
const before=this.db.prepare('SELECT value FROM runtime_rows WHERE name=? AND key=?').get(name,key)?.value??null;
|
|
75
|
-
if(before!==(value===null?null:JSON.stringify(value)))return true;
|
|
76
|
-
if(name==='@runtime') {
|
|
77
|
-
const fields=row=>row&&Object.fromEntries(Object.entries(row).filter(([k])=>k!=='status'));
|
|
78
|
-
return JSON.stringify(fields(source.row))!==JSON.stringify(fields(this.cachedTasks[key]??null));
|
|
79
|
-
}
|
|
80
|
-
if(name==='@clock')return JSON.stringify(source.row)!==JSON.stringify(this.cachedCards[key]??null);
|
|
81
|
-
return false;
|
|
82
|
-
});
|
|
83
|
-
if (mem.prepare("SELECT 1 FROM sqlite_master WHERE name='sqlite_sequence'").get()) {
|
|
84
|
-
for (const row of mem.prepare('SELECT name,seq FROM sqlite_sequence').all()) {
|
|
85
|
-
if (Number(this.sequences[row.name]?.seq) !== Number(row.seq)) changes.push({name:'@sequence',key:row.name,value:row,source:{name:'sqlite_sequence',key:JSON.stringify([row.name]),row}});
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
return changes;
|
|
89
|
-
}
|
|
90
|
-
mirror(changes, revision) {
|
|
91
|
-
const previous=Number(this.db.prepare("SELECT value FROM state WHERE key='read_revision'").get()?.value??-1);
|
|
92
|
-
for(const name of ['tasks','supercode_cards']) {
|
|
93
|
-
const table=this.tables.get(name); if(!table)return;
|
|
94
|
-
this.db.exec(table.sql.replace(/^CREATE TABLE (?:IF NOT EXISTS )?(?:"[^"]+"|\w+)/,`CREATE TABLE IF NOT EXISTS ${quote('read_'+name)}`));
|
|
95
|
-
const columns=this.db.prepare(`PRAGMA table_info(${quote('read_'+name)})`).all();
|
|
96
|
-
for(const c of table.columns) if(!columns.some(old=>old.name===c.name))this.db.exec(`ALTER TABLE ${quote('read_'+name)} ADD COLUMN ${quote(c.name)} ${c.type}${c.dflt_value==null?'':` DEFAULT ${c.dflt_value}`}`);
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
const rows=previous===revision-1?changes.filter(c=>['tasks','supercode_cards'].includes(c.source.name)).map(c=>c.source):this.db.prepare("SELECT name,key,value FROM runtime_rows WHERE name IN ('@task_cache','@card_cache')").all().map(r=>({name:r.name==='@task_cache'?'tasks':'supercode_cards',key:JSON.stringify([r.key]),row:json(r.value,null)}));
|
|
100
|
-
for(const {name,key,row} of rows) {
|
|
101
|
-
const table=this.tables.get(name), target=quote('read_'+name);
|
|
102
|
-
if(!row) {this.db.prepare(`DELETE FROM ${target} WHERE ${table.keys.map(k=>`${quote(k)} IS ?`).join(' AND ')}`).run(...JSON.parse(key));continue;}
|
|
103
|
-
const keys=Object.keys(row);
|
|
104
|
-
this.db.prepare(`INSERT OR REPLACE INTO ${target} (${keys.map(quote)}) VALUES (${keys.map(()=>'?')})`).run(...keys.map(k=>row[k]??null));
|
|
105
|
-
}
|
|
106
|
-
this.db.prepare("INSERT INTO state VALUES('read_revision',?) ON CONFLICT(key) DO UPDATE SET value=excluded.value").run(String(revision));
|
|
107
|
-
}
|
|
108
|
-
write(changes) {
|
|
109
|
-
this.db.exec(`CREATE TABLE IF NOT EXISTS runtime_schema(name TEXT PRIMARY KEY,sql TEXT NOT NULL); CREATE TABLE IF NOT EXISTS runtime_rows(name TEXT,key TEXT,value TEXT,revision INTEGER NOT NULL,PRIMARY KEY(name,key)); CREATE INDEX IF NOT EXISTS runtime_rows_revision ON runtime_rows(revision); CREATE INDEX IF NOT EXISTS runtime_rows_task ON runtime_rows(name,json_extract(value,'$.task_id'),json_extract(value,'$.id')); CREATE INDEX IF NOT EXISTS runtime_rows_id ON runtime_rows(name,json_extract(value,'$.id'))`);
|
|
110
|
-
const revision=Number(this.db.prepare("SELECT value FROM state WHERE key='rows_revision'").get()?.value??0)+1;
|
|
111
|
-
const statement=this.db.prepare('INSERT INTO runtime_rows VALUES(?,?,?,?) ON CONFLICT(name,key) DO UPDATE SET value=excluded.value,revision=excluded.revision');
|
|
112
|
-
for(const {name,key,value,source} of changes) {
|
|
113
|
-
if (OMIT.has(name)) continue;
|
|
114
|
-
const encoded=value===null?null:JSON.stringify(value);
|
|
115
|
-
if (this.db.prepare('SELECT value FROM runtime_rows WHERE name=? AND key=?').get(name,key)?.value !== encoded) statement.run(name,key,encoded,revision);
|
|
116
|
-
if(name==='@runtime' || name==='@clock') {
|
|
117
|
-
const cacheName=name==='@runtime'?'@task_cache':'@card_cache';
|
|
118
|
-
const cache=name==='@runtime'?this.cachedTasks:this.cachedCards;
|
|
119
|
-
const encodedCache=source?.row?JSON.stringify(source.row):null;
|
|
120
|
-
if (this.db.prepare('SELECT value FROM runtime_rows WHERE name=? AND key=?').get(cacheName,key)?.value !== encodedCache) statement.run(cacheName,key,encodedCache,revision);
|
|
121
|
-
if(source?.row)cache[key]=source.row;else delete cache[key];
|
|
122
|
-
}
|
|
123
|
-
if(name==='@runtime') { if(value)this.runtime[key]=value;else delete this.runtime[key]; }
|
|
124
|
-
if(name==='@clock') { if(value)this.clocks[key]=value;else delete this.clocks[key]; }
|
|
125
|
-
if(name==='@sequence') { if(value)this.sequences[key]=value;else delete this.sequences[key]; }
|
|
126
|
-
}
|
|
127
|
-
for(const {name,sql} of this.tables.values())if(!OMIT.has(name))this.db.prepare('INSERT OR IGNORE INTO runtime_schema VALUES(?,?)').run(name,sql);
|
|
128
|
-
this.db.prepare("INSERT INTO state VALUES('rows_revision',?) ON CONFLICT(key) DO UPDATE SET value=excluded.value").run(String(revision));
|
|
129
|
-
this.mirror(changes,revision);
|
|
130
|
-
this.revision=revision;
|
|
131
|
-
}
|
|
132
|
-
}
|