@volter/supercode-orchestrator 0.4.150 → 0.4.152

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 CHANGED
@@ -6,6 +6,43 @@ import { dirname, join, relative } from 'node:path';
6
6
  import { createHash } from 'node:crypto';
7
7
  import { branchCheckouts, checkoutOf, resolveWorkspace } from './workspace.mjs';
8
8
 
9
+ // GitHub is read through its REST API with Node's own fetch, not the gh binary: Node takes a World's trust bundle and
10
+ // injector (NODE_EXTRA_CA_CERTS, NODE_OPTIONS), so a review World's GitHub twin answers it, where gh (Go) trusts only
11
+ // the system keychain. The read runs in a child Node so the callers stay synchronous.
12
+ const GITHUB_READ = `
13
+ const [path, all] = process.argv.slice(1);
14
+ const base = (process.env.GITHUB_API_URL || 'https://api.github.com').replace(/\\/$/, '');
15
+ const token = process.env.GH_TOKEN || process.env.GITHUB_TOKEN;
16
+ const headers = { accept: 'application/vnd.github+json', 'user-agent': 'supercode-board', ...(token ? { authorization: 'Bearer ' + token } : {}) };
17
+ let url = base + path, out = [];
18
+ for (let page = 0; url && page < 10; page += 1) {
19
+ const r = await fetch(url, { headers });
20
+ if (!r.ok) { process.stderr.write('GitHub ' + r.status + ' for ' + path + ': ' + (await r.text()).slice(0, 300)); process.exit(1); }
21
+ const body = await r.json();
22
+ if (all !== '1') { process.stdout.write(JSON.stringify(body)); process.exit(0); }
23
+ out = out.concat(body);
24
+ url = /<([^>]+)>;\\s*rel="next"/.exec(r.headers.get('link') ?? '')?.[1] ?? null;
25
+ }
26
+ process.stdout.write(JSON.stringify(out));`;
27
+ let githubToken;
28
+ /** One GitHub REST read (`all`: every page of a list). The token is the environment's, else gh's stored login. */
29
+ function github(path, all = false) {
30
+ if (githubToken === undefined) {
31
+ githubToken = process.env.GH_TOKEN || process.env.GITHUB_TOKEN || null;
32
+ if (!githubToken) try { githubToken = run('gh', ['auth', 'token']) || null; } catch { githubToken = null; }
33
+ }
34
+ const env = githubToken ? { ...process.env, GH_TOKEN: githubToken } : process.env;
35
+ const result = spawnSync(process.execPath, ['--input-type=module', '-e', GITHUB_READ, path, all ? '1' : '0'], { env, encoding: 'utf8', timeout: 30_000, maxBuffer: 16 * 1024 * 1024 });
36
+ if (result.status !== 0) throw new Error(`GitHub: ${(result.stderr || result.error?.message || 'read failed').trim().slice(-500)}`);
37
+ return JSON.parse(result.stdout);
38
+ }
39
+ const repositoryOf = (url) => new URL(url).pathname.split('/').slice(1, 3).join('/');
40
+ const numberOf = (url) => Number(new URL(url).pathname.split('/')[4]);
41
+ /** A PR as the board reads it (gh's field names). */
42
+ const pullRequest = (repository, p) => ({ url: p.html_url, number: p.number, state: p.merged_at ? 'MERGED' : String(p.state).toUpperCase(), headRefOid: p.head.sha,
43
+ headRefName: p.head.ref, baseRefName: p.base.ref, defaultBranch: p.base.repo?.default_branch ?? null, isCrossRepository: p.head.repo?.full_name?.toLowerCase() !== repository.toLowerCase(), isDraft: Boolean(p.draft) });
44
+ const readPullRequest = (url) => pullRequest(repositoryOf(url), github(`/repos/${repositoryOf(url)}/pulls/${numberOf(url)}`));
45
+
9
46
  const observations = new Map();
10
47
  const key = (board, id) => `${board.root}\0${board.slug}\0${id}`;
11
48
  const run = (program, args, cwd) => {
@@ -85,18 +122,22 @@ function registeredBranches(board, id) {
85
122
  return out;
86
123
  }
87
124
 
88
- /** Every opened PR of the arc: each open PR whose head is the card's branch or a `wt/<card id>` branch, in the
89
- * card's own repository and in every repository this machine keeps a checkout of, and each open PR on a sibling
90
- * checkout the arc registered (`registeredBranches`). One review covers them all; the caller decides the transition. */
125
+ /** Every opened PR of the arc, each proposed to its repository's default branch: an open PR whose head is the card's
126
+ * work branch in the card's own repository, a `wt/<card id>` branch in any repository this machine keeps a checkout
127
+ * of, or a sibling checkout the arc registered (`registeredBranches`). The repository's default branch, declared or
128
+ * checked out (or no branch), is not a work branch: such a card's PRs are only its `wt/<card>` and registered ones, and with none it
129
+ * answers `{ waits }` saying so. One review covers them all; the caller decides the transition. */
91
130
  export function openReviewPullRequests(board, id) {
92
131
  const own = repositoryWorkspace(board, id);
93
132
  if (!own) return null;
94
133
  const task = board.db.prepare('SELECT branch_name FROM tasks WHERE id=?').get(id);
95
- const branch = task?.branch_name ?? run('git', ['branch', '--show-current'], own);
96
134
  const ownRepository = originOf(own);
97
135
  if (!ownRepository) return null;
136
+ const current = task?.branch_name ? null : run('git', ['branch', '--show-current'], own);
137
+ let branch = task?.branch_name ?? (current || null);
98
138
  const registered = registeredBranches(board, id);
99
- const mine = (repository, head) => head === branch || [`wt/${id}`, `wt/${id.replace(/^t_/, 't-')}`].some((prefix) => head === prefix || head.startsWith(`${prefix}-`) || head.startsWith(`${prefix}/`))
139
+ const named = (head) => [`wt/${id}`, `wt/${id.replace(/^t_/, 't-')}`].some((prefix) => head === prefix || head.startsWith(`${prefix}-`) || head.startsWith(`${prefix}/`));
140
+ const mine = (repository, head) => (repository === ownRepository && head === branch) || named(head)
100
141
  || registered.some((r) => r.repository === repository && r.branch === head);
101
142
  const prs = [];
102
143
  for (const repository of new Set([ownRepository, ...registered.map((r) => r.repository), ...knownRepositories()])) {
@@ -104,16 +145,21 @@ export function openReviewPullRequests(board, id) {
104
145
  let list = pullRequestLists.get(repository);
105
146
  if (!list || Date.now() - list.at >= 60_000) {
106
147
  let items = [];
107
- try { items = JSON.parse(run('gh', ['pr', 'list', '--repo', repository, '--state', 'open', '--limit', '1000', '--json', 'url,headRefOid,headRefName,baseRefName,isCrossRepository,isDraft'])); }
148
+ try { items = github(`/repos/${repository}/pulls?state=open&per_page=100`, true).map((p) => pullRequest(repository, p)); }
108
149
  catch (error) { if (repository === ownRepository || registered.some((r) => r.repository === repository)) throw error; } // another repository unreachable here is not this arc's
109
150
  list = { at: Date.now(), items };
110
151
  if (pullRequestLists.size >= 64) pullRequestLists.delete(pullRequestLists.keys().next().value);
111
152
  pullRequestLists.set(repository, list);
112
153
  }
113
- // a draft is not handed over yet: its review starts when it is marked ready
114
- for (const pr of list.items) if (!pr.isCrossRepository && !pr.isDraft && mine(repository, pr.headRefName)) prs.push({ url: pr.url, head: pr.headRefOid });
154
+ // The repository's default branch is not a work branch, declared or checked out (a card working in a `dir` on main).
155
+ if (repository === ownRepository && branch) {
156
+ const defaultBranch = list.items.find((pr) => pr.defaultBranch)?.defaultBranch ?? (() => { try { return run('git', ['symbolic-ref', '--quiet', '--short', 'refs/remotes/origin/HEAD'], own).replace(/^origin\//, ''); } catch { return null; } })();
157
+ if (branch === defaultBranch) branch = null;
158
+ }
159
+ // a draft is not handed over yet: its review starts when it is marked ready; a PR to another base (main→prod) is a release, not the arc's work
160
+ for (const pr of list.items) if (!pr.isCrossRepository && !pr.isDraft && pr.baseRefName === pr.defaultBranch && mine(repository, pr.headRefName)) prs.push({ url: pr.url, head: pr.headRefOid });
115
161
  }
116
- if (!prs.length) return null;
162
+ if (!prs.length) return branch ? null : { waits: `the card has no work branch (${task?.branch_name ? `it declares ${task.branch_name}, its repository's default` : current ? `its checkout ${own} is on ${current}, its repository's default` : `its checkout ${own} is on no branch`}): give it one with --branch, or open its PR from a wt/${id} branch` };
117
163
  return { pr: prs.map((p) => p.url), pr_head: prs.map((p) => p.head), branch };
118
164
  }
119
165
 
@@ -122,17 +168,35 @@ function knownRepositories() {
122
168
  try { return [...new Set(branchCheckouts(null, '*').map((row) => row.repository))]; } catch { return []; }
123
169
  }
124
170
 
125
- /** Whether the arc's open PRs (`[{ url, head }]`) call for its review. Before any verdict: while some head has none.
126
- * After a verdict: only once a PR that verdict judged shows a new head. A PR that joins the arc, or one that merged or
127
- * closed, does not by itself bring the arc back: after changes are requested the next run is the implementer's. */
171
+ /** Whether the arc's open PRs (`[{ url, head }]`) call for its review: some head no verdict judged that is new work.
172
+ * Before any verdict every unjudged head is. After a verdict, a PR it judged with a new head is, and so is a head
173
+ * committed after that verdict (a new PR carrying the fixes, or any PR after a verdict that named none). A PR that
174
+ * joins with a head committed before the verdict is not: after changes are requested the next run is the
175
+ * implementer's until it pushes. */
128
176
  export function reviewDue(board, id, prs) {
129
- const unjudged = prs.some((p) => !judgedHead(board, id, p.head));
130
- const last = board.db.prepare(`SELECT r.payload FROM task_events r WHERE r.task_id = ? AND r.kind = 'review_requested'
177
+ const unjudged = prs.filter((p) => !judgedHead(board, id, p.head));
178
+ if (!unjudged.length) return false;
179
+ const last = board.db.prepare(`SELECT r.payload, (SELECT MIN(v.created_at) FROM task_events v WHERE v.task_id = r.task_id
180
+ AND v.kind IN ('changes_requested', 'review_pass') AND v.id > r.id) AS verdict_at
181
+ FROM task_events r WHERE r.task_id = ? AND r.kind = 'review_requested'
131
182
  AND EXISTS (SELECT 1 FROM task_events v WHERE v.task_id = r.task_id AND v.kind IN ('changes_requested', 'review_pass') AND v.id > r.id)
132
183
  ORDER BY r.id DESC LIMIT 1`).get(id);
133
- if (!last) return unjudged;
184
+ if (!last) return true;
134
185
  const judged = candidatePullRequests(JSON.parse(last.payload)?.ci);
135
- return unjudged && judged.some((j) => prs.some((p) => p.url === j.pr.url && p.head !== j.commit));
186
+ if (judged.some((j) => prs.some((p) => p.url === j.pr.url && p.head !== j.commit))) return true;
187
+ return unjudged.some((p) => (commitTime(p) ?? 0) > Number(last.verdict_at));
188
+ }
189
+
190
+ const commitTimes = new Map();
191
+ /** When a PR's head was committed (seconds), read from GitHub once per head; null when it cannot be read. */
192
+ function commitTime({ url, head }) {
193
+ if (commitTimes.has(head)) return commitTimes.get(head);
194
+ let at = null;
195
+ try {
196
+ at = Math.floor(Date.parse(github(`/repos/${repositoryOf(url)}/commits/${head}`).commit.committer.date) / 1000) || null;
197
+ } catch { /* unreadable: not counted as new */ }
198
+ commitTimes.set(head, at);
199
+ return at;
136
200
  }
137
201
 
138
202
  /** The verdict already recorded on `head` of the card's PR: a changes-requested or pass that answered
@@ -163,7 +227,7 @@ export function inspectCompletionEvidence(board, id, entry, payload = {}, stage
163
227
  const wanted = candidatePullRequests(candidate);
164
228
  if (wanted.length !== passed.length || wanted.some((w) => !passed.some((p) => p.pr.url === w.pr.url && p.commit === w.commit))) throw new Error('The current PR heads need their independent verdict before acceptance.');
165
229
  for (const w of wanted) {
166
- const pr = JSON.parse(run('gh', ['pr', 'view', w.pr.url, '--repo', w.repository, '--json', 'state,headRefOid']));
230
+ const pr = readPullRequest(w.pr.url);
167
231
  if (!['OPEN', 'MERGED'].includes(pr.state) || pr.headRefOid !== w.commit) throw new Error(`The head of ${w.pr.url} changed after its verdict; request a new review.`);
168
232
  }
169
233
  Object.assign(state, { ...candidate, ready: true, reason: null });
@@ -211,7 +275,7 @@ function verifyPullRequest(board, id, entry, own, url, supplied, stage) {
211
275
  // An arc may change several repositories. A PR is read from GitHub; its pushed head is the reviewed one. The
212
276
  // card's own checkout, and this machine's checkout of the PR's branch when it keeps one, must also hold that head
213
277
  // with nothing uncommitted.
214
- const observed = JSON.parse(run('gh', ['pr', 'view', url, '--repo', repository, '--json', 'url,number,state,headRefOid,headRefName,baseRefName,isCrossRepository,isDraft']));
278
+ const observed = readPullRequest(url);
215
279
  const path = originOf(own) === repository && run('git', ['branch', '--show-current'], own) === observed.headRefName ? own : branchCheckout(board, id, repository, observed.headRefName);
216
280
  if (observed.state !== 'OPEN' || observed.isCrossRepository) throw new Error(`Source review needs an open PR from its repository (${url}).`);
217
281
  if (observed.isDraft) throw new Error(`${url} is a draft; its review starts when it is marked ready for review.`);
@@ -23,7 +23,7 @@ import { hostname, tmpdir, totalmem } from 'node:os';
23
23
  import { basename, dirname, join } from 'node:path';
24
24
  import { createHash, randomUUID } from 'node:crypto';
25
25
  import { fileURLToPath } from 'node:url';
26
- import { view, boardPath, claimer, event, json, listBoards, logsDir, now, openBoard, setActor, tx } from './store.mjs';
26
+ import { view, boardCopyOf, boardPath, claimer, copyRefusal, event, json, listBoards, logsDir, now, openBoard, setActor, tx } from './store.mjs';
27
27
  import { doors, fire, loadWorkflow, resumeOf, runSpec, runWorkingSeconds, requestSessionClose, retireSupersededRuns, refreshCardInputs, refreshRelationAnswers, transitionsFor, scopeOf, statusOf, templateValue, workflowFor } from './workflow.mjs';
28
28
  import { checkWorkspace, assertWorkspaceOwner, notePresence, resolveWorkspace } from './workspace.mjs';
29
29
  import { evaluate, holds, render } from './expr.mjs';
@@ -428,6 +428,7 @@ function limitsOf(db, board, entry, task, session) {
428
428
 
429
429
  function stopSession(board, session) {
430
430
  if (!session) return;
431
+ if (boardCopyOf()) throw copyRefusal('closes no session');
431
432
  if (session.surface === 'headless' && pidAlive(session.pid)) {
432
433
  // SIGTERM to the worker's process group, then SIGKILL if it is still there after a grace, as Hermes stops its own
433
434
  const signal = (sig) => { try { process.kill(-session.pid, sig); } catch { try { process.kill(session.pid, sig); } catch { /* gone */ } } };
@@ -1354,6 +1355,7 @@ function resolveLocalWorkspace(board, task) {
1354
1355
  */
1355
1356
  export function requestAdoption(board, id, who, { workspace = null, assignee = null, role = null } = {}) {
1356
1357
  if (!who) throw new Error('name the session to adopt');
1358
+ if (boardCopyOf()) throw copyRefusal('adopts no session');
1357
1359
  tx(board, db => {
1358
1360
  if (!getTask(db, id)) throw new Error(`no card ${id}`);
1359
1361
  event(db, id, 'dispatch_held', { session: who, reason: 'adoption requested', workspace, assignee, role });
@@ -1442,6 +1444,7 @@ function adoptSession(board, id, who, { workspace = null, ttl, assignee = null,
1442
1444
 
1443
1445
  // a notice sent within a transaction's caller that cannot await (the launcher): the same door, synchronously
1444
1446
  function deliverNow(board, to, body, key) {
1447
+ if (boardCopyOf()) throw copyRefusal('sends no notice to sessions');
1445
1448
  const request = { op: 'send', from: boardAddress(board.label), from_name: `${board.label} board`, to, queue: false, id: key, subject: String(body).split('\n')[0].slice(0, 160),
1446
1449
  body: `[Board notice from the ${board.label} board] ${body}\n\n(An automated notice from the board's dispatcher; replies to this address go nowhere.)` };
1447
1450
  const r = sc(['message', 'remote-deliver'], { input: JSON.stringify(request), timeout: 120_000 });
@@ -1451,6 +1454,7 @@ function deliverNow(board, to, body, key) {
1451
1454
  }
1452
1455
 
1453
1456
  async function send(from, label, to, body, key, { queue = false, ask = false, subject } = {}) {
1457
+ if (boardCopyOf()) throw copyRefusal('sends no mail to sessions or managers');
1454
1458
  const request = {
1455
1459
  op: 'send', from, from_name: `${label} board`, to, queue, id: key, subject: String(subject ?? String(body).split('\n')[0]).slice(0, 160),
1456
1460
  // a question the board waits on is answered by a reply; a notice is not
@@ -1563,11 +1567,20 @@ async function notify(board, fleet, report, entry) {
1563
1567
  // a worker whose run has ended is not woken by a notice (a second session would start working the card beside
1564
1568
  // the one the next run gets): the notice is filed in its mailbox, read when it next works
1565
1569
  const working = role !== 'worker' || view(board, (db) => addressOf(currentRun(db, sub.task_id)?.metadata?.supercode) === sub.chat_id);
1570
+ // An idle Claude session cannot hold a notice in its mailbox (it starts a turn for every message it is given), so
1571
+ // a notice that must not wake it waits, unsent, for the session's next work.
1572
+ if (!working && fleet.ok && fleet.sessions.some((s) => s.address === sub.chat_id && s.harness === 'claude-code' && s.status === 'idle')) {
1573
+ report.notices_waiting = (report.notices_waiting ?? 0) + 1;
1574
+ return;
1575
+ }
1566
1576
  let cursor = Number(sub.last_event_id);
1567
1577
  let failed = null;
1568
1578
  try {
1579
+ // an event older than the workflow's notice_max_age_seconds is history, not news: it is passed over, never mailed
1580
+ const maxAge = Number(entry.params.notice_max_age_seconds) || null;
1569
1581
  for (const ev of events) {
1570
1582
  const p = json(ev.payload, {}) ?? {};
1583
+ if (maxAge && now() - Number(ev.created_at) > maxAge) { cursor = ev.id; (report.stale_notices_passed ??= 0); report.stale_notices_passed += 1; continue; }
1571
1584
  // nobody is told of their own act
1572
1585
  if (p.by && p.by === sub.chat_id) { cursor = ev.id; continue; }
1573
1586
  let text;
@@ -1593,7 +1606,8 @@ async function notify(board, fleet, report, entry) {
1593
1606
  const kind = 'notice_failed';
1594
1607
  const previous = db.prepare('SELECT payload FROM task_events WHERE task_id=? AND kind=? ORDER BY id DESC LIMIT 1').get(sub.task_id, kind);
1595
1608
  const payload = { after_event: cursor, address: sub.chat_id, error: failed, retry: true };
1596
- if (JSON.stringify(json(previous?.payload, {})) !== JSON.stringify(payload)) event(db, sub.task_id, kind, payload);
1609
+ const { by: _, ...said } = json(previous?.payload, {}) ?? {};
1610
+ if (JSON.stringify(said) !== JSON.stringify(payload)) event(db, sub.task_id, kind, payload);
1597
1611
  });
1598
1612
  report.undelivered.push([sub.task_id, sub.chat_id, failed]);
1599
1613
  }
@@ -1865,7 +1879,12 @@ async function activatePullRequestReviews(board, entry, report, effects, { only
1865
1879
  payload = openReviewPullRequests(board, id);
1866
1880
  // The arc's open PRs ask for one review only while some head has no verdict: after changes are
1867
1881
  // requested the next run is the implementer's, until a new head is pushed.
1868
- if (!payload || !reviewDue(board, id, payload.pr.map((url, index) => ({ url, head: payload.pr_head[index] })))) continue;
1882
+ if (!payload) { report.queued.push([id, 'review waits: no open PR on its branch, a wt/<card> branch or a registered checkout']); continue; }
1883
+ if (payload.waits) { report.queued.push([id, `review waits: ${payload.waits}`]); payload = null; continue; }
1884
+ if (!reviewDue(board, id, payload.pr.map((url, index) => ({ url, head: payload.pr_head[index] })))) {
1885
+ report.queued.push([id, `review waits: every open PR head (${payload.pr.join(', ')}) has a verdict or predates the last one; push a commit that answers it`]);
1886
+ continue;
1887
+ }
1869
1888
  const evidence = inspectCompletionEvidence(board, id, entry, payload, 'request_review');
1870
1889
  if (!evidence.ready) throw new Error(evidence.reason);
1871
1890
  refreshCardInputs(board, id, { commits: true });
@@ -1912,6 +1931,8 @@ export async function tick({ root, lanes, settings, only = null, slugs = null, d
1912
1931
  const path = boardPath(root, slug);
1913
1932
  const marker = `${path}.corrupt-seen`;
1914
1933
  if (existsSync(marker) && readFileSync(marker, 'utf8').trim() === fingerprint(path)) { report.error = 'skipped: the board file was corrupt and has not changed since (`supercode workflow repair`)'; out.push(report); continue; }
1934
+ // a copy of a home dispatches nothing: its runs, sessions and managers are the original's
1935
+ if (boardCopyOf()) { report.error = `skipped: ${copyRefusal('starts, supervises, adopts and mails nothing').message}`; out.push(report); continue; }
1915
1936
  let board;
1916
1937
  try {
1917
1938
  board = openBoard(root, slug);
package/board/store.mjs CHANGED
@@ -10,7 +10,7 @@
10
10
  // are in `engine.mjs`. Hermes's own tools may write a `kanban.db` at the same time: every write there takes SQLite's
11
11
  // native transaction boundary and retries an optimistic conflict.
12
12
  import { randomBytes } from 'node:crypto';
13
- import { existsSync, mkdirSync, readdirSync, readFileSync } from 'node:fs';
13
+ import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
14
14
  import { hostname } from 'node:os';
15
15
  import { dirname, join } from 'node:path';
16
16
  import { DatabaseSync } from 'node:sqlite';
@@ -142,7 +142,23 @@ export function layOut(db, { fresh }) {
142
142
  * Open (creating when `create`, in `backing`) board `slug`: `{ db, root, slug, path, backing, tx, close }`. `db` answers
143
143
  * SQLite's statement API (`prepare(sql).get|all|run`, `exec`) whatever the backing.
144
144
  */
145
+ // A home records where it was made (`.board-origin` at its root). A copy of the home carries that record elsewhere and
146
+ // is a copy: it may be read and written, but it addresses no session and no manager, which are the original's.
147
+ let copyOfHome = null;
148
+ /** The original home's path when the board opened last is a copy of it; null for a home of its own. */
149
+ export const boardCopyOf = () => copyOfHome;
150
+ export const copyRefusal = (what) => new Error(`this board is a copy of the home at ${copyOfHome}; it ${what}, which belong to the original`);
151
+ function noteOrigin(root) {
152
+ if (!existsSync(root)) return;
153
+ const here = realpathSync(root), file = join(root, '.board-origin');
154
+ let origin = null;
155
+ try { origin = JSON.parse(readFileSync(file, 'utf8')).root ?? null; } catch { /* none yet */ }
156
+ if (!origin) { writeFileSync(file, `${JSON.stringify({ root: here })}\n`); origin = here; }
157
+ copyOfHome = origin === here ? null : origin;
158
+ }
159
+
145
160
  export function openBoard(root, slug = DEFAULT_BOARD, { create = false, backing = null, document = null } = {}) {
161
+ noteOrigin(root);
146
162
  const kept = backingOf(root, slug);
147
163
  if (!kept && !create) throw new Error(`no board ${slug} in ${root} (${boardPath(root, slug)})`);
148
164
  if (kept && backing && kept !== backing) throw new Error(`board ${slug} in ${root} is kept by the ${kept} backing, not ${backing}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-orchestrator",
3
- "version": "0.4.150",
3
+ "version": "0.4.152",
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": {