@volter/supercode-orchestrator 0.5.56 → 0.5.58

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/ci.mjs CHANGED
@@ -4,7 +4,7 @@ import { spawnSync } from 'node:child_process';
4
4
  import { readFileSync, realpathSync, writeFileSync, renameSync, statSync } from 'node:fs';
5
5
  import { dirname, join, relative } from 'node:path';
6
6
  import { createHash } from 'node:crypto';
7
- import { branchCheckouts, checkoutOf, resolveWorkspace } from './workspace.mjs';
7
+ import { branchCheckouts, checkoutOf, isLocalWorkspace, resolveWorkspace } from './workspace.mjs';
8
8
 
9
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
10
  // injector (NODE_EXTRA_CA_CERTS, NODE_OPTIONS), so a review World's GitHub twin answers it, where gh (Go) trusts only
@@ -128,17 +128,23 @@ export const candidatePullRequests = (ci) => ci?.prs ?? (ci?.pr ? [{ repository:
128
128
  // The repository a checkout declares (remote.origin.url), not where a URL rewrite sends its transport.
129
129
  const originOf = (path) => /(?:github\.com[:/])([^/]+\/[^/]+?)(?:\.git)?$/.exec(run('git', ['config', '--get', 'remote.origin.url'], path))?.[1] ?? null;
130
130
 
131
+ /** The card's checkout as its machine answers for it (path, repository, branch), and whether it is on this machine:
132
+ * git runs only in a checkout here, and another machine's is known by what its door said. */
131
133
  function repositoryWorkspace(board, id) {
132
134
  const task = board.db.prepare('SELECT t.*,c.subtask_of,c.workspace_anchor,c.machine FROM tasks t LEFT JOIN supercode_cards c ON c.task_id=t.id WHERE t.id=?').get(id);
133
135
  const parent = task?.subtask_of && board.db.prepare('SELECT t.*,c.workspace_anchor,c.machine FROM tasks t LEFT JOIN supercode_cards c ON c.task_id=t.id WHERE t.id=?').get(task.subtask_of);
134
136
  const workspace = task?.workspace_kind === 'scratch' ? parent : task;
135
- return workspace?.workspace_kind !== 'scratch' && workspace?.workspace_anchor ? resolveWorkspace(workspace, false).path : workspace?.workspace_kind !== 'scratch' ? workspace?.workspace_path : null;
137
+ if (!workspace || workspace.workspace_kind === 'scratch') return null;
138
+ const here = isLocalWorkspace(workspace);
139
+ if (workspace.workspace_anchor) return { ...resolveWorkspace(workspace, false), here };
140
+ return workspace.workspace_path ? { path: workspace.workspace_path, here } : null;
136
141
  }
137
142
 
138
143
  const pullRequestLists = new Map();
139
144
 
140
145
  function branchCheckout(board, id, repository, branch) {
141
146
  const task = board.db.prepare('SELECT c.machine FROM tasks t LEFT JOIN supercode_cards c ON c.task_id=t.id WHERE t.id=?').get(id);
147
+ if (!isLocalWorkspace(task ?? {})) return null;
142
148
  try { return checkoutOf(task?.machine, repository, branch)?.path ?? null; } catch { return null; }
143
149
  }
144
150
 
@@ -179,9 +185,9 @@ export function openReviewPullRequests(board, id) {
179
185
  const own = repositoryWorkspace(board, id);
180
186
  if (!own) return null;
181
187
  const task = board.db.prepare('SELECT branch_name FROM tasks WHERE id=?').get(id);
182
- const ownRepository = originOf(own);
188
+ const ownRepository = own.repository ?? (own.here ? originOf(own.path) : null);
183
189
  if (!ownRepository) return null;
184
- const current = task?.branch_name ? null : run('git', ['branch', '--show-current'], own);
190
+ const current = task?.branch_name ? null : own.branch ?? (own.here ? run('git', ['branch', '--show-current'], own.path) : null);
185
191
  let branch = task?.branch_name ?? (current || null);
186
192
  const registered = registeredBranches(board, id);
187
193
  const mine = (repository, head) => (repository === ownRepository && head === branch) || namedWorkBranch(id, head)
@@ -200,13 +206,13 @@ export function openReviewPullRequests(board, id) {
200
206
  }
201
207
  // The repository's default branch is not a work branch, declared or checked out (a card working in a `dir` on main).
202
208
  if (repository === ownRepository && branch) {
203
- 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; } })();
209
+ const defaultBranch = list.items.find((pr) => pr.defaultBranch)?.defaultBranch ?? (() => { try { return run('git', ['symbolic-ref', '--quiet', '--short', 'refs/remotes/origin/HEAD'], own.path).replace(/^origin\//, ''); } catch { return null; } })();
204
210
  if (branch === defaultBranch) branch = null;
205
211
  }
206
212
  // 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
207
213
  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 });
208
214
  }
209
- 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` };
215
+ 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.path} is on ${current}, its repository's default` : `its checkout ${own.path} is on no branch`}): give it one with --branch, or open its PR from a wt/${id} branch` };
210
216
  return { pr: prs.map((p) => p.url), pr_head: prs.map((p) => p.head), branch };
211
217
  }
212
218
 
@@ -214,8 +220,9 @@ export function openReviewPullRequests(board, id) {
214
220
  * beyond its repository's default branch, locally or on origin, its checkout holds no uncommitted work, no PR was
215
221
  * ever opened from it, and it registered no other branch. Answers what its review reads instead of a diff, or null when the arc carries (or may carry) code. */
216
222
  export function noCodeChange(board, id) {
217
- const own = repositoryWorkspace(board, id);
218
- if (!own) return null;
223
+ const checkout = repositoryWorkspace(board, id);
224
+ if (!checkout?.here) return null;
225
+ const own = checkout.path;
219
226
  try {
220
227
  const repository = originOf(own);
221
228
  const task = board.db.prepare('SELECT branch_name FROM tasks WHERE id=?').get(id);
@@ -291,7 +298,7 @@ export function inspectCompletionEvidence(board, id, entry, payload = {}, stage
291
298
  if (stage === 'completion' || stage === 'acceptance') {
292
299
  const supplied = payload.commit ?? payload.metadata?.commit;
293
300
  if (supplied) {
294
- const path = repositoryWorkspace(board, id);
301
+ const path = repositoryWorkspace(board, id)?.path;
295
302
  if (!path) throw new Error('The named commit needs a resolved repository workspace.');
296
303
  Object.assign(state, { repository: originOf(path), commit: run('git', ['rev-parse', '--verify', `${supplied}^{commit}`], path) });
297
304
  }
@@ -351,9 +358,9 @@ function verifyPullRequest(board, id, entry, own, url, supplied, stage) {
351
358
  const repository = new URL(url).pathname.split('/').slice(1, 3).join('/');
352
359
  // An arc may change several repositories. A PR is read from GitHub; its pushed head is the reviewed one. The
353
360
  // card's own checkout, and this machine's checkout of the PR's branch when it keeps one, must also hold that head
354
- // with nothing uncommitted.
361
+ // with nothing uncommitted. A card on another machine is reviewed at its pushed head: its checkout is not read from here.
355
362
  const observed = readPullRequest(url);
356
- const path = originOf(own) === repository && run('git', ['branch', '--show-current'], own) === observed.headRefName ? own : branchCheckout(board, id, repository, observed.headRefName);
363
+ const path = !own.here ? null : originOf(own.path) === repository && run('git', ['branch', '--show-current'], own.path) === observed.headRefName ? own.path : branchCheckout(board, id, repository, observed.headRefName);
357
364
  // A PR merged before its review (an outage fix landed first) is still reviewed: its head is fixed for good, so the
358
365
  // verdict is recorded on that head, and nothing about a worktree or its base can move under it any more.
359
366
  const merged = observed.state === 'MERGED';
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,32 +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
- // The answer is filed in the sender's mailbox like every other board mail (D138 step 6): a filing that fails is
2345
- // reported and leaves the round going, since the verb above is already applied.
2346
- try { fileMail(board, 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']);
2347
2353
  }
2348
2354
  }
2349
2355
 
@@ -2353,11 +2359,8 @@ function readMailbox(board, address, report) {
2353
2359
 
2354
2360
  // ------------------------------------------------------------------ the tick
2355
2361
 
2356
- /** The board's name in notices: the home's owner folder (`…/volter-manager/home` → `volter-manager`), and the slug. */
2357
- export function boardLabel(root, slug) {
2358
- const home = basename(root) === 'home' ? basename(dirname(root)) : basename(root);
2359
- return slug && slug !== 'default' ? `${home}/${slug}` : home;
2360
- }
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 };
2361
2364
 
2362
2365
  // A dispatcher told to stop (its serve's SIGTERM, SIGINT or SIGHUP) starts nothing more in the round it is in: no further
2363
2366
  // board, card start, board message or notice. What it leaves unfiled is not a failure; the next dispatcher files it.
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.56",
3
+ "version": "0.5.58",
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
  },