@volter/supercode-orchestrator 0.4.95 → 0.4.97

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/cli.mjs CHANGED
@@ -236,7 +236,8 @@ export async function runWorkflow(argv) {
236
236
  const ids = (first = rest) => [...first, ...(args.ids ?? [])];
237
237
 
238
238
  // a caller that is a card's own session (its harness names it; the board recorded which session it launched)
239
- const sid = callerSession();
239
+ const listing = ['list','ls'].includes(verb);
240
+ const sid = listing && !args.mine ? null : callerSession();
240
241
  // what this command records is the calling session's act (its address, as its subscriptions name it)
241
242
  if (sid) setActor(`sc:${localMachine()}:${sid === process.env.CODEX_THREAD_ID ? 'codex' : 'claude-code'}:${sid}`);
242
243
  if (sid && !READS.has(verb) && boardExists(root, slug)) {
@@ -244,7 +245,7 @@ export async function runWorkflow(argv) {
244
245
  }
245
246
  // the board's workflow (the IR's): what each verb does, and who may send it, is its
246
247
  // every verb reads the board through its workflow (a list settles cards too), so every verb loads it
247
- if (verb !== 'help') await loadWorkflow(root).catch((error) => { if (verb !== 'init' && verb !== 'boards') throw error; });
248
+ if (verb !== 'help' && !listing) await loadWorkflow(root).catch((error) => { if (verb !== 'init' && verb !== 'boards') throw error; });
248
249
  engine.setCaller((db, cardId) => rolesFor(db, cardId, sid, workflowFor(root).params));
249
250
  // a verb with no session behind it (a person at the CLI, the board file's sync) is the manager's, as its roles are
250
251
  if (!sid && verb !== 'help') { try { setActor([].concat(workflowFor(root).params?.managers ?? [])[0] ?? null); } catch { /* no workflow */ } }
package/board/rows.mjs CHANGED
@@ -57,6 +57,21 @@ export class RuntimeRows {
57
57
  return {name,key,value:row,source:{name,key,row}};
58
58
  });
59
59
  }
60
+ mirror(changes, revision) {
61
+ const previous=Number(this.db.prepare("SELECT value FROM state WHERE key='read_revision'").get()?.value??-1);
62
+ for(const name of ['tasks','supercode_cards']) {
63
+ const table=this.tables.get(name); if(!table)return;
64
+ this.db.exec(table.sql.replace(/^CREATE TABLE (?:IF NOT EXISTS )?(?:"[^"]+"|\w+)/,`CREATE TABLE IF NOT EXISTS ${quote('read_'+name)}`));
65
+ }
66
+ 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)}));
67
+ for(const {name,key,row} of rows) {
68
+ const table=this.tables.get(name), target=quote('read_'+name);
69
+ if(!row) {this.db.prepare(`DELETE FROM ${target} WHERE ${table.keys.map(k=>`${quote(k)} IS ?`).join(' AND ')}`).run(...JSON.parse(key));continue;}
70
+ const keys=Object.keys(row);
71
+ this.db.prepare(`INSERT OR REPLACE INTO ${target} (${keys.map(quote)}) VALUES (${keys.map(()=>'?')})`).run(...keys.map(k=>row[k]??null));
72
+ }
73
+ this.db.prepare("INSERT INTO state VALUES('read_revision',?) ON CONFLICT(key) DO UPDATE SET value=excluded.value").run(String(revision));
74
+ }
60
75
  write(changes) {
61
76
  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'))`);
62
77
  const revision=Number(this.db.prepare("SELECT value FROM state WHERE key='rows_revision'").get()?.value??0)+1;
@@ -77,6 +92,7 @@ export class RuntimeRows {
77
92
  }
78
93
  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);
79
94
  this.db.prepare("INSERT INTO state VALUES('rows_revision',?) ON CONFLICT(key) DO UPDATE SET value=excluded.value").run(String(revision));
95
+ this.mirror(changes,revision);
80
96
  this.revision=revision;
81
97
  }
82
98
  }
@@ -113,7 +113,7 @@ function lastCommitAt(path, id) { return path ? commitTimes.get(`${path}:${id}`)
113
113
  export function refreshCardInputs(board, id, { commits = false } = {}) {
114
114
  const task=board.db.prepare('SELECT t.*,c.subtask_of FROM tasks t LEFT JOIN supercode_cards c ON c.task_id=t.id WHERE t.id=?').get(id);
115
115
  if (!task)return;
116
- if (commits) {
116
+ if (commits && !task.subtask_of) {
117
117
  const own=task.workspace_kind!=='scratch'?task.workspace_path:null;
118
118
  const parent=!own && task.subtask_of?board.db.prepare('SELECT workspace_path FROM tasks WHERE id=?').get(task.subtask_of)?.workspace_path:null;
119
119
  const path=own??parent??null;
package/board/ztrack.mjs CHANGED
@@ -109,6 +109,7 @@ class ZtrackBoard {
109
109
 
110
110
  transaction(fn) {
111
111
  if (this.cachedReader) this.invalidate();
112
+ this.readOnly(()=>{ if(this.runtime.prepare('PRAGMA journal_mode').get().journal_mode!=='wal')this.runtime.exec('PRAGMA journal_mode=WAL'); });
112
113
  for (;;) {
113
114
  try { this.runtime.exec('BEGIN'); }
114
115
  catch (error) { if (!/busy|locked/i.test(error.message)) throw error; continue; }
@@ -126,12 +127,12 @@ class ZtrackBoard {
126
127
  const result = fn(this.db);
127
128
  const changes = this.rows.changes(this.mem, { all: !this.rows.normalized() });
128
129
  const canonical = (row, omitted) => row && Object.fromEntries(Object.entries(row).filter(([key])=>!omitted.has(key)));
129
- const cardDirty = this.seed || changes.some(({source})=>{
130
+ const documentChanges = changes.filter(({source})=>{
130
131
  if(source.name==='tasks')return JSON.stringify(canonical(source.row,new Set([...RUNTIME,'status'])))!==JSON.stringify(canonical(this.rows.cachedTasks[JSON.parse(source.key)[0]],new Set([...RUNTIME,'status'])));
131
132
  if(source.name==='supercode_cards')return JSON.stringify(canonical(source.row,new Set(['since','after_fired','next_run_at'])))!==JSON.stringify(canonical(this.rows.cachedCards[JSON.parse(source.key)[0]],new Set(['since','after_fired','next_run_at'])));
132
- return source.name==='task_links';
133
+ return false;
133
134
  });
134
- const documents = cardDirty ? this.documents() : {open:this.text, archived:this.archiveText};
135
+ const documents = this.seed || documentChanges.length ? this.documents(this.seed ? null : documentChanges) : {open:this.text, archived:this.archiveText};
135
136
  const files = [{ path: this.path, before: hash(this.text), base: this.text, text: documents.open }, { path: this.archive, before: hash(this.archiveText), base: this.archiveText, text: documents.archived }];
136
137
  const snapshot = this.snapshot(documents);
137
138
  this.computing = false;
@@ -219,12 +220,21 @@ class ZtrackBoard {
219
220
  this.mem.prepare(`INSERT OR REPLACE INTO ${quote(table)} (${keys.map(quote).join(',')}) VALUES (${keys.map(() => '?').join(',')})`).run(...keys.map((k) => row[k] ?? null));
220
221
  }
221
222
  openCachedReader(saved) {
223
+ const native = this.get('read_revision',-1)===this.get('rows_revision',0);
224
+ if(this.cachedReader && native!==this.nativeReader)this.invalidate();
222
225
  if (!this.cachedReader) {
223
226
  this.mem?.close(); this.mem=new DatabaseSync(':memory:'); this.init(this.mem,{fresh:true});
224
227
  for(const row of this.runtime.prepare('SELECT name,sql FROM runtime_schema').all())this.mem.exec(row.sql.replace('CREATE TABLE ','CREATE TABLE IF NOT EXISTS '));
225
228
  this.mem.prepare('ATTACH DATABASE ? AS source').run(this.runtimePath);
226
229
  const tables=this.mem.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").all();
227
230
  for(const {name} of tables) {
231
+ if (native && name==='tasks') {
232
+ const fields=this.mem.prepare('PRAGMA main.table_info(tasks)').all().map(({name:column})=>column==='status'?'COALESCE(c.stage,t.status) AS status':`t.${quote(column)}`);
233
+ this.mem.exec(`CREATE TEMP VIEW tasks AS SELECT ${fields.join(',')} FROM source.read_tasks t LEFT JOIN source.read_supercode_cards c ON c.task_id=t.id`);
234
+ continue;
235
+ }
236
+ if(native && name==='supercode_cards') {this.mem.exec('CREATE TEMP VIEW supercode_cards AS SELECT * FROM source.read_supercode_cards');continue;}
237
+ if(native && name==='task_links') {this.mem.exec("CREATE TEMP VIEW task_links AS SELECT json_extract(b.value,'$.id') parent_id,r.task_id child_id FROM source.read_supercode_cards r,json_each(r.blocked_by) b WHERE json_extract(b.value,'$.kind')='card'");continue;}
228
238
  if(name==='task_links') {
229
239
  this.mem.exec(`CREATE TEMP VIEW task_links AS SELECT json_extract(b.value,'$.id') parent_id,r.key child_id FROM source.runtime_rows r,json_each(json_extract(r.value,'$.blocked_by')) b WHERE r.name='@card_cache' AND json_extract(b.value,'$.kind')='card'`);
230
240
  continue;
@@ -236,7 +246,7 @@ class ZtrackBoard {
236
246
  this.mem.exec(`CREATE TEMP VIEW ${quote(name)} AS SELECT ${fields.join(',')} FROM source.runtime_rows r${join} WHERE r.name='${recordName}' AND r.value IS NOT NULL`);
237
247
  }
238
248
  this.mem.exec('PRAGMA query_only=ON');
239
- this.cachedReader=true;
249
+ this.cachedReader=true;this.nativeReader=native;
240
250
  }
241
251
  this.saved=saved; this.errors.clear();
242
252
  for(const [id,reason] of saved.errors??[])this.errors.set(id,reason);
@@ -275,12 +285,13 @@ class ZtrackBoard {
275
285
  this.rows.watch(this.mem);
276
286
  this.mem.exec('DELETE FROM tasks; DELETE FROM supercode_cards; DELETE FROM task_links');
277
287
  this.errors.clear(); this.preserve.clear();
278
- const records = [];
288
+ const records = []; this.documentRecords=new Map();
279
289
  for (const [text,path] of [[this.text,this.path],[this.archiveText,this.archive]]) {
280
290
  const result = this.codec.inspectBoardDocument(text,path);
281
291
  const kept = new Set(result.errors.map(e=>e.id));
282
292
  for(const error of result.errors)this.errors.set(nativeId(error.id),error.reason);
283
293
  for(const record of result.records) {
294
+ this.documentRecords.set(nativeId(record.id),{record,path});
284
295
  try { records.push(this.preset.KanbanRootSchema.parse(this.preset.parseKanban([record])).issues[0]); }
285
296
  catch(error) { this.errors.set(nativeId(record.id),error.message); kept.add(record.id); }
286
297
  }
@@ -380,10 +391,11 @@ class ZtrackBoard {
380
391
  committer() {
381
392
  return 'document-editor';
382
393
  }
383
- documents() {
384
- const rows = this.mem.prepare('SELECT * FROM tasks ORDER BY created_at, id').all();
394
+ documents(changes=null) {
395
+ const allRows = this.mem.prepare('SELECT * FROM tasks ORDER BY created_at, id').all();
385
396
  const states = new Map(this.mem.prepare('SELECT * FROM supercode_cards').all().map((c) => [c.task_id, c]));
386
- const byId = new Map(rows.map((r) => [r.id, r]));
397
+ const affected=changes?new Set(changes.map(({source})=>{const id=JSON.parse(source.key)[0];return states.get(id)?.subtask_of??this.rows.cachedCards[id]?.subtask_of??id;})):null;
398
+ const rows=affected?allRows.filter(row=>affected.has(states.get(row.id)?.subtask_of??row.id)):allRows;
387
399
  const cards = new Map();
388
400
  for (const row of rows.filter((r) => !states.get(r.id)?.subtask_of && !this.errors.has(r.id))) {
389
401
  const c = states.get(row.id) ?? {};
@@ -420,12 +432,15 @@ class ZtrackBoard {
420
432
  blockedBy: json(c.blocked_by, []).map((b) => ({ issue: b.id })) });
421
433
  }
422
434
  const open = [], archived = [];
435
+ if(affected)for(const [id,{record,path}] of this.documentRecords??[])if(!affected.has(id)&&!this.errors.has(id))(path===this.path?open:archived).push(record);
423
436
  for (const [id, card] of cards) {
424
437
  const serialized = this.preset.serializeKanbanCard(card);
425
438
  const record = { id: card.id, title: card.title, status: card.status, ...(card.assignee ? { assignee: card.assignee } : {}), body: serialized.body };
426
439
  (states.get(id)?.archived || ['done', 'cancelled'].includes(card.status) ? archived : open).push(record);
427
440
  }
428
- return { open: this.codec.renderBoardDocument(this.text, open, this.path, [...(this.preserve.get(this.path)??[])]), archived: this.codec.renderBoardDocument(this.archiveText, archived, this.archive, [...(this.preserve.get(this.archive)??[])]) };
441
+ const original=this.documentRecords;
442
+ const changedPath=path=>!affected||[...affected].some(id=>original?.get(id)?.path===path||cards.has(id)&&((states.get(id)?.archived||['done','cancelled'].includes(cards.get(id).status))?this.archive:this.path)===path);
443
+ return { open: !changedPath(this.path)?this.text:this.codec.renderBoardDocument(this.text, open, this.path, [...(this.preserve.get(this.path)??[])]), archived: !changedPath(this.archive)?this.archiveText:this.codec.renderBoardDocument(this.archiveText, archived, this.archive, [...(this.preserve.get(this.archive)??[])]) };
429
444
  }
430
445
  snapshot(documents) {
431
446
  return { documentHashes:{open:hash(documents.open),archived:hash(documents.archived)}, errors:[...this.errors], fingerprints: this.fingerprints(), taskIds: Object.fromEntries([...this.taskKeys].map(([id, key]) => [key, id])) };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-orchestrator",
3
- "version": "0.4.95",
3
+ "version": "0.4.97",
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": {