@volter/supercode-orchestrator 0.5.55 → 0.5.57

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.
@@ -1,4 +1,12 @@
1
1
  #!/usr/bin/env node
2
+ // Any command on a home whose record names another orchestrator is that orchestrator's (hand-off.mjs).
3
+ {
4
+ const { handOff, homeOfArgs } = await import('../hand-off.mjs');
5
+ let handed;
6
+ try { handed = handOff(homeOfArgs(process.argv.slice(2)), process.argv.slice(2)); }
7
+ catch (error) { process.stderr.write(`supercode ${process.argv[2] ?? ''}: ${error.message}\n`); process.exit(1); }
8
+ if (handed !== null) process.exit(await handed);
9
+ }
2
10
  // Board commands do not load daemon transports, platform adapters or the MCP server.
3
11
  if (process.argv[2] === 'workflow') {
4
12
  const [{runWorkflow},{closeWorld},{timeCommand}]=await Promise.all([import('../board/cli.mjs'),import('../orchestration.mjs'),import('@volter/supercode-harness-sdk/slow-log')]);
package/board/cli.mjs CHANGED
@@ -8,7 +8,8 @@ import { taskStatusOf } from './published.mjs';
8
8
  import { appendFileSync, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
9
9
  import { spawnSync } from 'node:child_process';
10
10
  import { recordSlow, takeSteps } from '@volter/supercode-harness-sdk/slow-log';
11
- import { basename, join, resolve } from 'node:path';
11
+ import { basename, dirname, join, resolve } from 'node:path';
12
+ import { fileURLToPath } from 'node:url';
12
13
  import { homedir, tmpdir, userInfo } from 'node:os';
13
14
  import { read } from '../orchestration.mjs';
14
15
  import { actorOf, view, backingPaths, layOut, backingOf, boardExists, boardPath, DEFAULT_BOARD, event, json, listBoards, logsDir, now, openBoard, setActor, setPrincipal, principalOf, isCaller, tx } from './store.mjs';
@@ -43,7 +44,7 @@ function boardPublishing(root) {
43
44
  let here; try { here = realpathSync(root); } catch { here = resolve(root); }
44
45
  try { const listed = JSON.parse(read.stdout); const board = listed.boards?.find((b) => b.root === here); return board ? { ...board, refused: listed.refused ?? null } : null; } catch { return null; }
45
46
  }
46
- import { requestAdoption, alertManagers, boardAddress, boardLabel, boardSettings, callerSession, commandEnv, reconcileCallerLaunch, closeRequested, lanesOf, localMachine, observeFleet, paused, plan, startCard, stopDispatching, supercodeBin, tick, deliver, startDelivery } from './dispatch.mjs';
47
+ import { requestAdoption, alertManagers, boardAddress, boardLabel, boardSettings, boardCaller, callerSession, commandEnv, reconcileCallerLaunch, closeRequested, lanesOf, localMachine, observeFleet, paused, plan, startCard, stopDispatching, supercodeBin, tick, deliver, startDelivery } from './dispatch.mjs';
47
48
  import { watchBoard } from './wake.mjs';
48
49
  import { streamBoardPublications } from './event-stream.mjs';
49
50
  import { completionOwnerOf, isLatestReviewer, loadWorkflow, requestSessionClose, scopeOf, statusOf, useWorkflow, workflowEntry, workflowFor } from './workflow.mjs';
@@ -56,7 +57,7 @@ const textFrom = (file, inline) => file === undefined ? inline : readFileSync(fi
56
57
  const HELP = `supercode workflow <verb> [--root <home>] [--board <slug>] [--json]
57
58
 
58
59
  The board's verbs, with their flags:
59
- init [--backing sqlite|files|ztrack] [--document <arcs.md>]
60
+ init [--backing sqlite|files|ztrack] [--document <arcs.md>] [--commit] [--manager <address>] [--archive-max-lines N] [--archive-period month]
60
61
  migrate --backing ztrack --document <arcs.md> --status-map <json>
61
62
  boards list|create|rm|switch|show|rename|set-default-workdir|export|import
62
63
  create <title> [--body | --body-file PATH|-] [--assignee] [--parent]... [--workspace scratch|worktree:<repository>|dir:<repository>] [--branch] [--project]
@@ -120,7 +121,7 @@ And:
120
121
 
121
122
  const MULTI = new Set(['parent', 'skill', 'worker', 'child', 'ids', 'rm', 'tag', 'pr', 'pr-head', 'reviewed-sha', 'finding']);
122
123
  const FLAGS = new Set(['json', 'triage', 'no-notify', 'no-start', 'all', 'force', 'help', 'mine', 'archived', 'reclaim', 'dry-run',
123
- 'once', 'goal', 'no-goal', 'off', 'switch', 'delete', 'no-attachments', 'include-logs', 'verbose']);
124
+ 'once', 'goal', 'no-goal', 'off', 'switch', 'delete', 'no-attachments', 'include-logs', 'verbose', 'commit']);
124
125
  // flags that take every following word up to the next flag (Hermes's nargs='+')
125
126
  const GREEDY = new Set(['ids', 'rm']);
126
127
 
@@ -240,7 +241,7 @@ let recordedCaller = null;
240
241
  function callerAddress() {
241
242
  const id = callerSession();
242
243
  if (!id) return null;
243
- return recordedCaller ?? `sc:${localMachine()}:${id === process.env.CODEX_THREAD_ID ? 'codex' : 'claude-code'}:${id}`;
244
+ return recordedCaller ?? boardCaller()?.address ?? `sc:${localMachine()}:${id === process.env.CODEX_THREAD_ID ? 'codex' : 'claude-code'}:${id}`;
244
245
  }
245
246
 
246
247
  const callerName = () => process.env.HERMES_PROFILE || process.env.USER || 'user';
@@ -254,7 +255,7 @@ function callerPrincipal(root, sid, { teams = false } = {}) {
254
255
  if (process.env.SUPERCODE_AGENT_ID) return `agent:${process.env.SUPERCODE_AGENT_ID}`;
255
256
  if (process.env.SUPERCODE_AUTOMATION) return `automation:${process.env.SUPERCODE_AUTOMATION}`;
256
257
  if (sid) {
257
- const harness = sid === process.env.CODEX_THREAD_ID ? 'codex' : 'claude-code';
258
+ const harness = boardCaller()?.harness ?? (sid === process.env.CODEX_THREAD_ID ? 'codex' : 'claude-code');
258
259
  const owner = agentRunForSession(root, [`${harness}:${sid}`]);
259
260
  if (owner) return `agent:${owner.agent_id}`;
260
261
  // a session an agent declared elsewhere owns (a manager's main session) is that agent's, as Teams records it
@@ -361,11 +362,20 @@ function sayReleaseBehind() {
361
362
  if (behind) process.stderr.write(`board: error: ${behind.text}\n`);
362
363
  }
363
364
 
365
+ /**
366
+ * The verbs a session on another machine may not call on a board (through its machine's board door): running the
367
+ * board itself (its dispatcher, its init, its repair and housekeeping, adoption) and verbs that hold the call open (a
368
+ * watch, a tail, an attach, the event stream). They are run on the board's machine. A verb the door started reads this
369
+ * one list and refuses before it does anything.
370
+ */
371
+ export const NOT_FROM_ANOTHER_MACHINE = new Set(['serve', 'daemon', 'dispatch', 'init', 'adopt', 'boards', 'gc', 'repair', 'migrate', 'watch', 'tail', 'attach', 'events']);
372
+
364
373
  export async function runWorkflow(argv) {
365
374
  const args = parse(argv);
366
375
  const [verb, ...rest] = args._;
367
376
  if (!verb || verb === 'help' || args.help) { process.stdout.write(HELP); return 0; }
368
377
  const root = homeOf(args);
378
+ if (process.env.SUPERCODE_BOARD_CALL === '1' && NOT_FROM_ANOTHER_MACHINE.has(verb)) throw new Error(`${verb} is run on the board's own machine, not called from another`);
369
379
  // the dispatcher's own verbs run every round; its serve says it once when it starts
370
380
  if (!['dispatch', 'events'].includes(verb)) sayReleaseBehind();
371
381
  if (verb === 'events') return streamBoardPublications({root,after:args.after,once:args.once});
@@ -423,9 +433,41 @@ export async function runWorkflow(argv) {
423
433
  return 0;
424
434
  }
425
435
  case 'init': {
426
- const backing = withBoard((b) => b.backing, true);
436
+ // A board on the ztrack backing is made with what its ztrack.json declares besides its document: `--commit` (the
437
+ // documents live in a git checkout, each write committed and pushed), its manager (`--manager`, else the first one
438
+ // the home's workflow names), whose own writes it does not commit, and the archive's rotation. A board already there
439
+ // is kept as it is: one kept by another backing, or naming another document, is refused, naming what it is, and
440
+ // nothing is changed; the same board again is said and left.
441
+ const kept = boardExists(root, slug) ? backingOf(root, slug) : null;
442
+ let declared = null;
443
+ if (args.backing === 'ztrack') {
444
+ if (kept && kept !== 'ztrack') throw new Error(`board ${slug} in ${root} is kept by the ${kept} backing, not ztrack; it is left as it is (\`workflow migrate --backing ztrack\` moves its cards)`);
445
+ if (kept && args.document) {
446
+ const named = JSON.parse(readFileSync(backingPaths(root, slug).ztrack, 'utf8')).document;
447
+ const real = (path) => { try { return join(realpathSync(dirname(resolve(path))), basename(path)); } catch { return resolve(path); } };
448
+ if (real(named) !== real(args.document)) throw new Error(`board ${slug} in ${root} is kept on ${named}, not ${real(args.document)}; it is left as it is`);
449
+ }
450
+ // With no --manager, the home's one manager; a home naming several is asked which (the first is an accident of
451
+ // order), and one naming none makes a board whose every write is committed.
452
+ const managers = [(await loadWorkflow(root).catch(() => null))?.params?.managers ?? []].flat().filter(Boolean);
453
+ if (!args.manager && managers.length > 1) throw new Error(`the home's workflow names ${managers.length} managers (${managers.join(', ')}); name the board's with --manager. Nothing was made`);
454
+ const archive = args['archive-max-lines'] || args['archive-period'] ? { ...(args['archive-max-lines'] ? { max_lines: Number(args['archive-max-lines']) } : {}), ...(args['archive-period'] ? { period: args['archive-period'] } : {}) } : null;
455
+ declared = { commit: Boolean(args.commit), manager: args.manager ?? managers[0] ?? null, archive };
456
+ }
457
+ const made = openBoard(root, slug, { create: true, document: args.document ?? null, backing: args.backing ?? null, declared });
458
+ // A home records the orchestrator that serves it and the supercode it drives (<home>/orchestrator.json, read by
459
+ // hand-off.mjs for every command on the home); an install's keeper writes it, and a home made by hand gets it
460
+ // here, naming this orchestrator and the supercode that ran it. A record already there is the install's and stays.
461
+ const record = join(root, 'orchestrator.json');
462
+ if (!existsSync(record)) {
463
+ const named = (bin) => { try { const entry = realpathSync(bin); let version = null; try { version = JSON.parse(readFileSync(join(dirname(entry), '..', 'package.json'), 'utf8')).version ?? null; } catch { /* none beside it */ } return { entry, version }; } catch { return null; } };
464
+ const own = named(fileURLToPath(new URL('../bin/orchestrator.mjs', import.meta.url)));
465
+ writeFileSync(record, `${JSON.stringify({ package: '@volter/supercode-orchestrator', version: own?.version ?? null, entry: own?.entry ?? null, supercode: process.env.SUPERCODE_BIN ? named(process.env.SUPERCODE_BIN) : null })}\n`);
466
+ }
467
+ const backing = made.backing;
468
+ made.close();
427
469
  const { profiles } = await laneState(root).catch(() => ({ profiles: [] }));
428
- say(args, `${backing === 'sqlite' ? 'Kanban DB' : `Board (${backing} backing)`} initialized at ${boardPath(root, slug)}\n\n${profiles.length ? `Profiles of this home (any can be an --assignee): ${profiles.join(', ')}` : 'No profiles in this home.'}\nThe dispatcher: \`supercode workflow serve\`, or the orchestrator while it serves the home.`, { ok: true, board: slug, path: boardPath(root, slug), backing, profiles });
470
+ say(args, `${backing === 'sqlite' ? 'Kanban DB' : `Board (${backing} backing)`} ${kept ? 'already' : 'initialized'} at ${boardPath(root, slug)}\n\n${profiles.length ? `Profiles of this home (any can be an --assignee): ${profiles.join(', ')}` : 'No profiles in this home.'}\nThe dispatcher: \`supercode workflow serve\`, or the orchestrator while it serves the home.`, { ok: true, board: slug, path: boardPath(root, slug), backing, kept: Boolean(kept), profiles });
429
471
  return 0;
430
472
  }
431
473
 
@@ -1455,9 +1497,16 @@ async function serve(root, args = {}) {
1455
1497
  const watchRounds = async (reports) => {
1456
1498
  await tellReleaseBehind().catch(() => {});
1457
1499
  if (reports.every((r) => !r.error)) {
1500
+ // A publication the team refused holds this home's publishing where it is (nothing after it is skipped): a fact of
1501
+ // the same kind as rounds that stopped, so it goes where every board verb's error line reads it. Only a refusal
1502
+ // in hand counts: the team's 4xx answer, which the daemon records with the publication (from supercode-teams
1503
+ // 0.3.44). An attempt still being retried, or a daemon that records no answer, says nothing here.
1504
+ const held = boardPublishing(root)?.failing;
1505
+ const refused = held && Number(held.status) >= 400 && Number(held.status) < 500 ? held : null;
1506
+ const heldLine = refused ? [`publishing to the team is held: the team refused ${refused.card}'s ${refused.kind} (sequence ${refused.sequence}) with ${refused.status}${refused.code ? ` ${refused.code}` : ''}: ${String(refused.error ?? '').split('\n')[0].slice(0, 200)} (tried ${refused.attempts} time(s)); nothing of this home after it is published until the team takes it`] : [];
1458
1507
  const temporary = `${heartbeat}.${process.pid}.tmp`;
1459
1508
  try {
1460
- writeFileSync(temporary, `${JSON.stringify({ at: new Date().toISOString(), round, pid: process.pid, version: installed(), boards: reports.map((r) => ({ board: r.board, started: r.started?.length ?? 0, queued: r.queued?.length ?? 0, failed: r.failed?.length ?? 0, ms: r.ms ?? null })), errors: reports.flatMap((r) => [...(r.failed ?? []), ...(r.unsettled ?? [])].map(([t, why]) => `${r.board} ${t}: ${String(why).split('\n')[0].slice(0, 300)}`)).slice(0, 20) })}\n`);
1509
+ writeFileSync(temporary, `${JSON.stringify({ at: new Date().toISOString(), round, pid: process.pid, version: installed(), boards: reports.map((r) => ({ board: r.board, started: r.started?.length ?? 0, queued: r.queued?.length ?? 0, failed: r.failed?.length ?? 0, ms: r.ms ?? null })), errors: [...heldLine, ...reports.flatMap((r) => [...(r.failed ?? []), ...(r.unsettled ?? [])].map(([t, why]) => `${r.board} ${t}: ${String(why).split('\n')[0].slice(0, 300)}`))].slice(0, 20) })}\n`);
1461
1510
  renameSync(temporary, heartbeat);
1462
1511
  } catch { /* the next round writes it */ }
1463
1512
  }
@@ -18,6 +18,7 @@
18
18
  // a crash between the two steps is answered by the next round.
19
19
  import { monitorsRuns, failedObservation, surfaceIncident, auditEndedRuns, originOf } from './incidents.mjs';
20
20
  import { readMachineFleet } from './machine-fleet.mjs';
21
+ import { boardLabel, boardName } from '@volter/teams/board-name';
21
22
  let fleetRoot = null;
22
23
  import { inspectCompletionEvidence, noCodeChange, openReviewPullRequests, reviewDue, withCompletionEvidence, reviewCandidate, workflowRevision, retainWorkflowTick } from './ci.mjs';
23
24
  import { runtimeDoor } from './runtime-door.mjs';
@@ -91,12 +92,36 @@ const lastJson = (text) => {
91
92
  * What supercode reports about the fleet now: `{ ok, alive: Set<address>, online: Set<machine>, error }`. `alive` is
92
93
  * every session that can be messaged on every machine; `online` every enrolled machine connected to the team.
93
94
  */
95
+ /**
96
+ * The caller of a verb the machine daemon's board door runs here (another machine's session, `sc:<machine>:<harness>:
97
+ * <id>`), or null when the verb was not called through the door. The door is this verb's parent: it named the caller
98
+ * from the calling process itself and writes `{"caller": …}` on the verb's stdin, which this process reads to its end,
99
+ * so nothing it starts inherits it. `SUPERCODE_BOARD_CALL` only says to read it. It is trusted as the machine's user is:
100
+ * any process of that user can start a verb so, as it can name a session in CLAUDE_CODE_SESSION_ID.
101
+ */
102
+ let readBoardCaller;
103
+ export function boardCaller() {
104
+ if (readBoardCaller !== undefined) return readBoardCaller;
105
+ readBoardCaller = null;
106
+ if (process.env.SUPERCODE_BOARD_CALL !== '1') return null;
107
+ let said = null;
108
+ try { said = JSON.parse(readFileSync(0, 'utf8')); } catch { /* nothing said */ }
109
+ const m = /^sc:([^:\s]+):(claude-code|codex):([^:\s]+)$/.exec(said?.caller ?? '');
110
+ if (!m) throw new Error('this verb was started by the board door, which named no caller for it; nothing was done');
111
+ readBoardCaller = { address: m[0], machine: m[1], harness: m[2], session: m[3] };
112
+ return readBoardCaller;
113
+ }
114
+
94
115
  /**
95
116
  * The session a command runs in. Each harness names its own in a variable (Claude Code CLAUDE_CODE_SESSION_ID, Codex
96
117
  * CODEX_THREAD_ID), and a pane inherits every variable the program that started its terminal server had, so a Codex
97
118
  * pane can carry a Claude session's id too: the nearest harness among the command's ancestors says which is its own.
98
119
  */
99
120
  export function callerSession() {
121
+ // A verb another session called on this board (the machine daemon's board door): the daemon named its caller from the
122
+ // calling process itself, on the caller's own machine, and passes that address in place of the harness variables.
123
+ const called = boardCaller();
124
+ if (called) return called.session;
100
125
  const claude = process.env.CLAUDE_CODE_SESSION_ID || null;
101
126
  const codex = process.env.CODEX_THREAD_ID || null;
102
127
  if (!claude || !codex) return claude ?? codex;
@@ -1995,7 +2020,7 @@ export function fileBoardMail(board, report, taskId = null) {
1995
2020
  }
1996
2021
 
1997
2022
  /** The board's own address: where its notices come from and where card updates are mailed. */
1998
- export const boardAddress = (label) => `sc:${localMachine()}:board:${label.replace(/[^A-Za-z0-9_.-]/g, '-')}`;
2023
+ export const boardAddress = (label) => `sc:${localMachine()}:board:${boardName(label)}`;
1999
2024
 
2000
2025
  /** Consecutive failed sends after which a chat-platform subscription is dropped (Hermes's notifier drops a dead chat after 12). */
2001
2026
  const MAX_SEND_FAILURES = 12;
@@ -2291,9 +2316,6 @@ function fireHooks(board, settings, report) {
2291
2316
 
2292
2317
  // ------------------------------------------------------------------ the board's mailbox
2293
2318
 
2294
- const ENTRY = fileURLToPath(new URL('../bin/orchestrator.mjs', import.meta.url));
2295
- const NOT_BY_MAIL = new Set(['serve', 'daemon', 'dispatch', 'init', 'adopt', 'boards', 'gc', 'repair', 'watch', 'tail', 'attach']);
2296
-
2297
2319
  /** The board's mailbox on this machine: the directory supercode files mail for `address` in, or null. */
2298
2320
  function mailboxOf(address) {
2299
2321
  const config = process.env.SUPERCODE_HOME || (process.env.XDG_CONFIG_HOME ? join(process.env.XDG_CONFIG_HOME, 'supercode') : join(process.env.HOME ?? '', '.config', 'supercode'));
@@ -2307,9 +2329,10 @@ function mailboxOf(address) {
2307
2329
  }
2308
2330
 
2309
2331
  /**
2310
- * Card updates filed as mail (`supercode workflow <verb> … --at <this board's address>`, from any machine): each is
2311
- * run as its sender, the session supercode found the command running under, so a card's session gets exactly what
2312
- * it may do on this machine; the answer goes back as a notice and the envelope is filed as read.
2332
+ * The board's mailbox takes answers to the questions the board asked (what a card waits on, read by the card's
2333
+ * workflow) and notices about it (a machine's event, a delivery notice about a message the board sent), each filed as
2334
+ * read. Any other message is mail it does not take: it is refused to its sender, in its own mailbox, and filed as read
2335
+ * only once that refusal is filed.
2313
2336
  */
2314
2337
  function readMailbox(board, address, report) {
2315
2338
  const box = mailboxOf(address);
@@ -2318,30 +2341,15 @@ function readMailbox(board, address, report) {
2318
2341
  const path = join(box, 'new', name);
2319
2342
  let envelope;
2320
2343
  try { envelope = JSON.parse(readFileSync(path, 'utf8')); } catch { continue; }
2321
- try { renameSync(path, join(box, 'cur', name)); } catch { continue; } // another reader took it
2322
- // an answer to a question the board asked is what a card waits on (read by the card's workflow), not a verb
2323
- if (envelope.in_reply_to) continue;
2324
- // A notice is news for the board, never a card update: a machine's event, or Claude Code's delivery notice about a
2325
- // message the board sent (filed by the relay from `sc:<machine>:claude-code:notice`, which is no session). Its sender
2326
- // asked for no reply (reply_via none), so it is filed as read and never answered.
2327
- if (envelope.kind === 'notice' || envelope.reply_via?.mode === 'none') {
2328
- report.mailed.push([envelope.from, envelope.from === `sc:${localMachine()}:operator:teams-events` ? 'machine_event' : 'notice']);
2329
- continue;
2330
- }
2331
- const argv = json(envelope.body, {})?.workflow;
2332
- let answer;
2333
- if (!Array.isArray(argv) || !argv.length || NOT_BY_MAIL.has(argv[0])) answer = `not applied: a mailed card update is one \`supercode workflow\` verb other than ${[...NOT_BY_MAIL].join(', ')}`;
2334
- else {
2335
- const clean = [];
2336
- for (let i = 0; i < argv.length; i += 1) { if (argv[i] === '--root' || argv[i] === '--board') i += 1; else clean.push(String(argv[i])); }
2337
- const sid = String(envelope.from ?? '').split(':')[3] ?? '';
2338
- const r = spawnSync(process.execPath, [ENTRY, 'workflow', ...clean, '--root', board.root, '--board', board.slug], {
2339
- encoding: 'utf8', timeout: 180_000, env: { ...process.env, CLAUDE_CODE_SESSION_ID: sid, CODEX_THREAD_ID: '' },
2340
- });
2341
- answer = `${clean.slice(0, 2).join(' ')}: ${(r.status === 0 ? r.stdout : r.stderr || r.stdout).trim().slice(-600) || `exit ${r.status}`}`;
2344
+ const notice = envelope.kind === 'notice' || envelope.reply_via?.mode === 'none';
2345
+ if (!envelope.in_reply_to && !notice) {
2346
+ try {
2347
+ fileMail(board, envelope.from, `${address} takes answers to its questions and notices only; this message was not taken.`, `board-refused-${envelope.id}`);
2348
+ } catch (error) { report.undelivered.push(['mail', envelope.from, String(error.message).slice(-200)]); continue; }
2349
+ report.mailed.push([envelope.from, 'refused']);
2342
2350
  }
2343
- report.mailed.push([envelope.from, argv?.[0] ?? '?']);
2344
- report.answers.push(send(board, address, board.label, envelope.from, answer, `board-answer-${envelope.id}`).catch((error) => { report.undelivered.push(['mail', envelope.from, String(error.message).slice(-200)]); }));
2351
+ try { renameSync(path, join(box, 'cur', name)); } catch { continue; } // another reader took it
2352
+ if (!envelope.in_reply_to && notice) report.mailed.push([envelope.from, envelope.from === `sc:${localMachine()}:operator:teams-events` ? 'machine_event' : 'notice']);
2345
2353
  }
2346
2354
  }
2347
2355
 
@@ -2351,11 +2359,8 @@ function readMailbox(board, address, report) {
2351
2359
 
2352
2360
  // ------------------------------------------------------------------ the tick
2353
2361
 
2354
- /** The board's name in notices: the home's owner folder (`…/volter-manager/home` → `volter-manager`), and the slug. */
2355
- export function boardLabel(root, slug) {
2356
- const home = basename(root) === 'home' ? basename(dirname(root)) : basename(root);
2357
- return slug && slug !== 'default' ? `${home}/${slug}` : home;
2358
- }
2362
+ /** The board's name in notices: the home's owner folder, and the slug (@volter/teams/board-name, the rule the daemon reads too). */
2363
+ export { boardLabel };
2359
2364
 
2360
2365
  // A dispatcher told to stop (its serve's SIGTERM, SIGINT or SIGHUP) starts nothing more in the round it is in: no further
2361
2366
  // board, card start, board message or notice. What it leaves unfiled is not a failure; the next dispatcher files it.
@@ -2565,8 +2570,6 @@ export async function tick({ root, lanes, settings, only = null, slugs = null, d
2565
2570
  for (const slug of boards) {
2566
2571
  if (stopping) break;
2567
2572
  const report = { board: slug, ms: { fleet: fleetMs }, mailed: [], started: [], queued: [], failed: [], lost: [], crashed: [], timed_out: [], reclaimed: [], auto_blocked: [], promoted: 0, unassigned: [], nonspawnable: [], notified: [], undelivered: [], fleet: fleet.ok ? 'read' : fleet.error };
2568
- // the mailbox's answers in flight (not part of the report the caller prints)
2569
- Object.defineProperty(report, 'answers', { value: [], enumerable: false });
2570
2573
  const path = boardPath(root, slug);
2571
2574
  const marker = `${path}.corrupt-seen`;
2572
2575
  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; }
@@ -2610,7 +2613,6 @@ export async function tick({ root, lanes, settings, only = null, slugs = null, d
2610
2613
  report.error = error.message;
2611
2614
  if (CORRUPT.test(error.message)) writeFileSync(marker, fingerprint(path));
2612
2615
  } finally {
2613
- await Promise.all(report.answers);
2614
2616
  board?.close();
2615
2617
  }
2616
2618
  out.push(report);
package/board/engine.mjs CHANGED
@@ -985,10 +985,10 @@ export function diagnose(db, task, { failureLimit = DEFAULT_FAILURE_LIMIT, stran
985
985
  ? { kind: 'effect_failed', severity: 'warning', title: `its ${kind} failed for good after ${row.attempts} attempts: ${row.last_error ?? 'no reason recorded'}`, data: { effect: row.id, kind, attempts: row.attempts }, actions: [] }
986
986
  : { kind: 'effect_stalled', severity: 'error', title: `its ${kind} has failed ${row.attempts} times and is still tried: ${row.last_error ?? 'no reason recorded'}`, data: { effect: row.id, kind, attempts: row.attempts }, actions: [] });
987
987
  }
988
- // publications of the card the machine's connector abandoned: the team refused each one past its attempts
989
- for (const a of (publishing?.abandoned ?? []).filter((a) => a.card === task.id).slice(0, 20)) {
990
- out.push({ kind: 'publication_abandoned', severity: 'warning', title: `its ${a.kind} (sequence ${a.sequence}) was not published to the team: refused ${a.attempts} times (${a.status} ${a.code ?? ''}): ${a.error}`, data: { publication: a.publication, sequence: a.sequence, status: a.status, code: a.code }, actions: [] });
991
- }
988
+ // a publication of the card the team refuses: the home's publishing holds on it (none after it is published past it)
989
+ // and the machine's connector tries it again
990
+ const failing = publishing?.failing;
991
+ if (failing?.card === task.id) out.push({ kind: 'publication_refused', severity: 'error', title: `its ${failing.kind} (sequence ${failing.sequence}) is refused by the team (${failing.status} ${failing.code ?? ''}: ${failing.error}), tried ${failing.attempts} times; this board's publishing waits on it`, data: failing });
992
992
  // mail the board filed for the card that the mailbox still has to deliver (waiting), or gave up on (expired: it stays
993
993
  // unread there)
994
994
  if (expiredMail?.size || waitingMail?.size) {
package/board/store.mjs CHANGED
@@ -168,7 +168,7 @@ function noteOrigin(root) {
168
168
  copies.set(root, origin === here ? null : origin);
169
169
  }
170
170
 
171
- export function openBoard(root, slug = DEFAULT_BOARD, { create = false, backing = null, document = null } = {}) {
171
+ export function openBoard(root, slug = DEFAULT_BOARD, { create = false, backing = null, document = null, declared = null } = {}) {
172
172
  noteOrigin(root);
173
173
  const kept = backingOf(root, slug);
174
174
  if (!kept && !create) throw new Error(`no board ${slug} in ${root} (${boardPath(root, slug)})`);
@@ -176,7 +176,7 @@ export function openBoard(root, slug = DEFAULT_BOARD, { create = false, backing
176
176
  const use = kept ?? backing ?? 'sqlite';
177
177
  if (!BACKINGS.includes(use)) throw new Error(`a board's backing is ${BACKINGS.join(' | ')}, not ${use}`);
178
178
  const path = backingPaths(root, slug)[use];
179
- if (use === 'ztrack') return { ...openZtrackBoard(path, { create, document, init: layOut, root, slug }), root, slug, path, backing: use };
179
+ if (use === 'ztrack') return { ...openZtrackBoard(path, { create, document, declared, init: layOut, root, slug }), root, slug, path, backing: use };
180
180
  if (use === 'files') return { ...openFilesBoard(path, { create, init: layOut }), root, slug, path, backing: use };
181
181
  if (!kept) mkdirSync(dirname(path), { recursive: true });
182
182
  const db = new DatabaseSync(path);
package/board/ztrack.mjs CHANGED
@@ -9,7 +9,7 @@
9
9
  // region the edit touched. So a write costs what it changes, not the size of the board.
10
10
  import { createRequire } from 'node:module';
11
11
  import { createHash, randomBytes } from 'node:crypto';
12
- import { existsSync, linkSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
12
+ import { existsSync, linkSync, mkdirSync, readFileSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
13
13
  import { basename, dirname, join, resolve } from 'node:path';
14
14
  import { DatabaseSync } from 'node:sqlite';
15
15
  import { actorOf, contention, event, json, now, principalOf } from './store.mjs';
@@ -59,11 +59,18 @@ DROP TABLE IF EXISTS board_export;`;
59
59
  // written to the document). board_tasks: a subtask's key on its arc's Tasks list
60
60
  // (`<arc>:c3`) and its `source:` lines, which live only in the document.
61
61
 
62
- export function openZtrackBoard(marker, { create = false, document = null, seed = null, init, root, slug }) {
62
+ export function openZtrackBoard(marker, { create = false, document = null, declared = null, seed = null, init, root, slug }) {
63
63
  if (!existsSync(marker)) {
64
64
  if (!create) throw new Error(`no ztrack board ${marker}`);
65
65
  mkdirSync(dirname(marker), { recursive: true });
66
- atomic(marker, JSON.stringify({ format: 1, document: resolve(document ?? resolve(dirname(marker), 'arcs.md')) }) + '\n');
66
+ // What its creator declares besides the document (`init --commit --manager`, the archive's rotation) is written once,
67
+ // with it; a value left unnamed is not written.
68
+ const said = Object.fromEntries(Object.entries(declared ?? {}).filter(([, value]) => value !== null && value !== undefined && value !== false));
69
+ // The document by its real path (its folder's symlinks followed; the document itself may not exist yet): a commit
70
+ // names the document relative to its checkout, as git names the checkout's files, and a path through a symlink (a
71
+ // short state root) names a file outside it.
72
+ const named = resolve(document ?? resolve(dirname(marker), 'arcs.md'));
73
+ atomic(marker, JSON.stringify({ format: 1, document: join(realpathSync(dirname(named)), basename(named)), ...said }) + '\n');
67
74
  }
68
75
  const config = JSON.parse(read(marker));
69
76
  const backing = new ZtrackBoard({ ...config, seed, marker, init, root, slug });
package/hand-off.mjs ADDED
@@ -0,0 +1,46 @@
1
+ // THE ONE PLACE A HOME'S ORCHESTRATOR IS DECIDED (D152 row 11). Every command on a home ends in an orchestrator's entry
2
+ // (bin/orchestrator.mjs), whichever way it came: a shell's or a pane's supercode, the native binary (its \`workflow\`,
3
+ // \`orchestrator start\`, \`apps\`, \`access\` and its cold write door all run \`node <entry> … --root <home>\` with the
4
+ // entry it finds: SUPERCODE_ORCHESTRATOR_ENTRY, else the \`supercode-orchestrator\` on PATH, the machine's own), the
5
+ // machine daemon's follower and board door, \`supercode --quiet workflow …\`. A home whose install pins its own
6
+ // orchestrator names it in its record (<home>/orchestrator.json, written by the install's keeper and by init): the entry
7
+ // a command reached hands the command to the one named, driving the supercode named beside it, and runs it itself only
8
+ // when it is that one or when the home has no record (a home made by hand, served by the machine's own). So no home is
9
+ // served by two orchestrators. Nothing in Rust changes (D155); an orchestrator older than this one hands nothing off, so
10
+ // the machine's own moves to this release with the landing.
11
+ import { spawn } from 'node:child_process';
12
+ import { existsSync, readFileSync, realpathSync } from 'node:fs';
13
+ import { join, resolve } from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
15
+
16
+ /** The home a command (the whole of it, after the entry) names: --root, wherever it stands (the gateway's form,
17
+ * `node bin/orchestrator.mjs --root <home>`, has no verb before it); for `import`, which writes the home it names
18
+ * with --into, that; else the environment the orchestrator reads (SUPERCODE_ORCHESTRATOR_HOME, HERMES_HOME).
19
+ * (`export` names the home it reads with --from and writes none.) */
20
+ export function homeOfArgs(argv, env = process.env) {
21
+ const flag = argv[0] === 'import' ? '--into' : '--root';
22
+ const at = argv.findIndex((arg) => arg === flag || arg.startsWith(`${flag}=`));
23
+ const named = at < 0 ? env.SUPERCODE_ORCHESTRATOR_HOME ?? env.HERMES_HOME : argv[at].startsWith(`${flag}=`) ? argv[at].slice(flag.length + 1) : argv[at + 1];
24
+ return named ? resolve(named) : null;
25
+ }
26
+
27
+ /** The command (\`argv\`, after node and the entry) handed to the orchestrator the home's record names: its exit code, or
28
+ * null when this orchestrator serves it (it is the one named, or the home has no record). */
29
+ export function handOff(root, argv) {
30
+ if (!root) return null;
31
+ let record;
32
+ try { record = JSON.parse(readFileSync(join(root, 'orchestrator.json'), 'utf8')); } catch { return null; }
33
+ const own = realpathSync(fileURLToPath(new URL('./bin/orchestrator.mjs', import.meta.url)));
34
+ const version = (() => { try { return JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8')).version; } catch { return 'unknown'; } })();
35
+ let named;
36
+ try { named = realpathSync(record.entry); } catch { throw new Error(`${root} is served by orchestrator ${record.version ?? 'unknown'} (${record.entry ?? 'its record names none'}), which is not on this machine; this one is ${version}. Nothing was run`); }
37
+ const supercode = record.supercode?.entry;
38
+ if (!supercode || !existsSync(supercode)) throw new Error(`${root} is served by supercode ${record.supercode?.version ?? 'unknown'} (${supercode ?? 'its record names none'}), which is not on this machine. Nothing was run`);
39
+ if (named === own) { process.env.SUPERCODE_BIN = supercode; return null; }
40
+ return new Promise((done) => {
41
+ const child = spawn(process.execPath, [named, ...argv], { stdio: 'inherit', env: { ...process.env, SUPERCODE_BIN: supercode } });
42
+ for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) process.on(signal, () => { try { child.kill(signal); } catch { /* gone */ } });
43
+ child.on('error', (error) => { process.stderr.write(`supercode: could not run orchestrator ${record.version ?? ''} (${named}): ${error.message}\n`); done(1); });
44
+ child.on('close', (code, signal) => done(signal ? 1 : code ?? 1));
45
+ });
46
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-orchestrator",
3
- "version": "0.5.55",
3
+ "version": "0.5.57",
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.60",
28
- "@volter/teams": "0.2.18",
28
+ "@volter/teams": "0.2.19",
29
29
  "@volter/ztrack": "^2.4.23",
30
30
  "yaml": "^2.9.0"
31
31
  },