@volter/supercode-orchestrator 0.4.77 → 0.4.79

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.
@@ -5,7 +5,7 @@
5
5
  // the engine's primitives (the rows Hermes's tables hold); the I/O an action asks for (stopping a worker, closing a
6
6
  // pane, sending a session a message, cleaning a workspace) is answered as effects the caller runs after commit.
7
7
  import { spawnSync } from 'node:child_process';
8
- import { existsSync, readFileSync } from 'node:fs';
8
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
9
9
  import { resolve, relative, isAbsolute } from 'node:path';
10
10
  import { randomBytes } from 'node:crypto';
11
11
  import { actorOf, event as record, json, now } from './store.mjs';
@@ -78,11 +78,33 @@ function pattern(p) {
78
78
 
79
79
  // the time of the latest commit that names `id` in its message, on any ref of the repository at `path` (a worktree's
80
80
  // pushes land on the shared remote-tracking refs), or null (asked only when an expression does)
81
+ const commitTimes = new Map();
81
82
  function lastCommitAt(path, id) {
82
83
  if (!path) return null;
83
- const r = spawnSync('git', ['-C', path, 'log', '--all', '-1', '--format=%ct', '--fixed-strings', `--grep=${id}`], { encoding: 'utf8', timeout: 10_000 });
84
- const t = Number(r.stdout?.trim());
85
- return r.status === 0 && Number.isFinite(t) && t > 0 ? t : null;
84
+ const key = `${path}:${id}`;
85
+ const cached = commitTimes.get(key);
86
+ if (cached && now() - cached.at < 15) return cached.value;
87
+ const readCommit = (repo) => {
88
+ const r = spawnSync('git', ['-C', repo, 'log', '--all', '-1', '--format=%ct', '--fixed-strings', `--grep=${id}`], { encoding: 'utf8', timeout: 10_000 });
89
+ const value = Number(r.stdout?.trim());
90
+ return { repo: r.status === 0, value: Number.isFinite(value) && value > 0 ? value : null };
91
+ };
92
+ const own = readCommit(path);
93
+ let value = own.value;
94
+ if (!own.repo) {
95
+ // A fleet card may span sibling repositories under its declared workspace.
96
+ // Only direct children with a Git door participate; never scan arbitrary home trees.
97
+ let entries = []; try { entries = readdirSync(path, { withFileTypes: true }); } catch {}
98
+ for (const child of entries) {
99
+ if (!child.isDirectory() && !child.isSymbolicLink()) continue;
100
+ const repo = resolve(path, child.name);
101
+ if (!existsSync(resolve(repo, '.git'))) continue;
102
+ const t = readCommit(repo).value;
103
+ if (t != null) value = Math.max(value ?? 0, t);
104
+ }
105
+ }
106
+ commitTimes.set(key, { at: now(), value });
107
+ return value;
86
108
  }
87
109
 
88
110
  /** The first answer to message `id` in its sender's mailbox (`supercode message show --replies`), as its id; or null. */
@@ -490,8 +512,8 @@ const ACTIONS = {
490
512
  cardField(ctx, 'every', interval == null ? null : String(interval));
491
513
  cardField(ctx, 'next_run_at', interval == null ? null : now());
492
514
  },
493
- card_event(ctx, { name, payload = {} }) {
494
- const result = fire(ctx.db, ctx.board, ctx.entry, ctx.id, name, { roles: ['platform'], payload, extra: ctx.scope });
515
+ card_event(ctx, { name, payload = {}, as = ['platform'] }) {
516
+ const result = fire(ctx.db, ctx.board, ctx.entry, ctx.id, name, { roles: [].concat(as), payload, extra: { lanes: lanesIn(ctx.entry) } });
495
517
  ctx.effects.push(...result.effects);
496
518
  },
497
519
  record(ctx, args, scope) {
@@ -762,12 +784,21 @@ function settleRelations(db, board, entry, id, effects) {
762
784
  const row = one(db, 'SELECT blocked_by FROM supercode_cards WHERE task_id = ?', id);
763
785
  const blocks = json(row?.blocked_by, []) ?? [];
764
786
  for (const block of blocks) {
787
+ if (!json(one(db, 'SELECT blocked_by FROM supercode_cards WHERE task_id = ?', id)?.blocked_by, []).some((b) => b.id === block.id)) continue;
765
788
  const dependency = block.kind === 'card' ? one(db, 'SELECT * FROM tasks WHERE id = ?', block.id) : null;
766
789
  const status = dependency ? statusOf(db, dependency) : null;
767
790
  const answer = block.kind === 'message' ? answerTo(block.id) : null;
768
791
  const policy = entry.workflow.relations.blocked_by;
769
792
  const name = answer ? policy.answered : status === 'done' ? policy.completed : status === 'cancelled' ? policy.cancelled : null;
770
793
  if (name) effects.push(...fire(db, board, entry, id, name, { roles: ['platform'], payload: { on: block.id, reason: answer ? `reply ${answer}` : `${block.id} ${status}` } }).effects);
794
+ else if (policy.aged && policy.age_seconds != null) {
795
+ const seconds = Number(evaluate(String(policy.age_seconds), { params: entry.params }));
796
+ if (seconds > 0 && now() - Number(block.notified_at ?? block.since ?? now()) >= seconds) {
797
+ const current = json(one(db, 'SELECT blocked_by FROM supercode_cards WHERE task_id = ?', id)?.blocked_by, []);
798
+ run(db, 'UPDATE supercode_cards SET blocked_by = ? WHERE task_id = ?', JSON.stringify(current.map((b) => b.id === block.id ? { ...b, notified_at: now() } : b)), id);
799
+ effects.push(...fire(db, board, entry, id, policy.aged, { roles: ['platform'], payload: { on: block.id, reason: block.reason } }).effects);
800
+ }
801
+ }
771
802
  }
772
803
  }
773
804
 
package/board/ztrack.mjs CHANGED
@@ -228,6 +228,11 @@ class ZtrackBoard {
228
228
  for (const [id, value] of Object.entries(current)) {
229
229
  const old = previous[id];
230
230
  if (old?.hash === value.hash) continue;
231
+ let entry; try { entry = workflowFor(this.root); } catch {}
232
+ if (old && entry?.workflow.verbs?.document_changed) {
233
+ const change = fire(this.mem, this.board(), entry, id, 'document_changed', { roles: ['platform'], payload: { previous_status: old.status, by, source: 'document' } });
234
+ for (const effect of change.effects) this.mem.prepare('INSERT INTO supercode_effects (effect) VALUES (?)').run(JSON.stringify(effect));
235
+ }
231
236
  const kind = !old ? 'created' : old.status !== value.status ? 'status' : old.assignee !== value.assignee ? 'assigned' : 'specified';
232
237
  event(this.mem, id, kind, { by, source: 'document', status: value.status, assignee: value.assignee, author: by });
233
238
  if (!old && value.parent) event(this.mem, value.parent, 'subtask_added', { by, subtask: id, title: this.mem.prepare('SELECT title FROM tasks WHERE id = ?').get(id).title });
@@ -318,6 +323,10 @@ class ZtrackBoard {
318
323
  else if (row.kind === 'completed') { name = 'complete'; words.summary = payload.summary ?? task.result; }
319
324
  else if (row.kind === 'blocked' || row.kind === 'dependency_wait') { name = 'block'; words.on = payload.on ?? payload.parent ?? null; words.hermes = true; }
320
325
  else if (row.kind === 'archived' && statusOf(this.mem, task) !== 'done') name = 'cancel';
326
+ if (row.kind === 'unblocked' && entry.workflow.verbs?.hermes_unpause) {
327
+ const resumed = fire(this.mem, this.board(), entry, task.id, 'hermes_unpause', { roles, payload: { hermes: true, reason: 'unblocked in Hermes' } });
328
+ for (const effect of resumed.effects) this.mem.prepare('INSERT INTO supercode_effects (effect) VALUES (?)').run(JSON.stringify(effect));
329
+ }
321
330
  if (name) {
322
331
  const result = fire(this.mem, this.board(), entry, task.id, name, { roles, payload: words });
323
332
  for (const effect of result.effects) this.mem.prepare('INSERT INTO supercode_effects (effect) VALUES (?)').run(JSON.stringify(effect));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-orchestrator",
3
- "version": "0.4.77",
3
+ "version": "0.4.79",
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": {