@volter/supercode-orchestrator 0.5.207 → 0.5.208

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/board/cli.mjs CHANGED
@@ -10,6 +10,7 @@ import { DatabaseSync } from 'node:sqlite';
10
10
  import { homeOf, parse } from './args.mjs';
11
11
  import { BOARD_OPERATION_VERBS, boardHome, boardOwnerSocket, CALLER_FACTS } from '@volter/teams/board-owner';
12
12
  import { current, isCallersCall, ownCall, within } from './call.mjs';
13
+ import { cardState, holdDescription } from './card-state.mjs';
13
14
  import { detachSession } from './custody.mjs';
14
15
  import { taskStatusOf } from './published.mjs';
15
16
  import { executionOf } from './session-work.mjs';
@@ -24,14 +25,14 @@ import { holdLock } from './lock-holder.mjs';
24
25
  import { takeEpoch } from './fence.mjs';
25
26
  import { dispatcherNotice, holdOwnerHealth, holdRound, holdSwitch } from './owner-health.mjs';
26
27
  import { boardTool } from './tools.mjs';
27
- import { assertSourceReceipt } from './ci.mjs';
28
+ import { arcSources, assertSourceReceipt } from './ci.mjs';
28
29
  import { noteStep, recordSlow, takeSteps } from '@volter/supercode-harness-sdk/slow-log';
29
30
  // read by name, so an older harness SDK without them still loads: the owner then keeps no per-verb or loop lines
30
31
  import * as slowLog from '@volter/supercode-harness-sdk/slow-log';
31
32
  import { basename, dirname, join, resolve } from 'node:path';
32
33
  import { fileURLToPath } from 'node:url';
33
34
  import { homedir, tmpdir, userInfo } from 'node:os';
34
- import { actorOf, CALLER_RUN_ADDRESS, CALLER_RUN_AGENT, SESSION_CURRENT_RUNS, SESSION_RUNS, sessionIndexed, heldConfigWatched, heldDocumentsChanged, heldDocumentsWatched, holdBoards, retireHeldBoards, releaseHeldBoards, publishMarked, view, backingPaths, layOut, backingOf, boardExists, boardCopyOf, boardPath, briefPath, DEFAULT_BOARD, event, json, listBoards, logsDir, now, openBoard, setActor, setPrincipal, principalOf, isCaller, tx } from './store.mjs';
35
+ import { boardKey, cardRef, actorOf, CALLER_RUN_ADDRESS, CALLER_RUN_AGENT, SESSION_CURRENT_RUNS, SESSION_RUNS, sessionIndexed, heldConfigWatched, heldDocumentsChanged, heldDocumentsWatched, holdBoards, retireHeldBoards, releaseHeldBoards, publishMarked, view, backingPaths, layOut, backingOf, boardExists, boardCopyOf, boardPath, briefPath, DEFAULT_BOARD, event, json, listBoards, logsDir, now, openBoard, setActor, setPrincipal, principalOf, isCaller, tx } from './store.mjs';
35
36
  import { isLocalWorkspace } from './workspace.mjs';
36
37
  import { openZtrackBoard } from './ztrack.mjs';
37
38
  import * as engine from './engine.mjs';
@@ -93,21 +94,22 @@ const callerPath = (named) => notRuntime(resolve(current().cwd, named));
93
94
  const SAFE_SESSION = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,199}$/;
94
95
  const HELP = `supercode workflow <verb> [--root <home>] [--board <slug>] [--json]
95
96
 
96
- A card is named by its id (t_4406c88a) or by its number (42 or #42): the short number the board gave it when it was made.
97
+ A card is named by its id (t_4406c88a) or by its number, keyed or bare (VO-42, vo-42, #42 or 42): the short number the board
98
+ gave it when it was made, read with the board's key, the first letters of the board's name.
97
99
 
98
100
  The board's verbs, with their flags:
99
101
  init [--backing sqlite|files|ztrack] [--document <arcs.md>] [--commit] [--manager <address>] [--dispatching on|off] [--archive-max-lines N] [--archive-period month]
100
102
  migrate --backing ztrack --document <arcs.md> --status-map <json>
101
103
  boards list|create|rm|switch|show|rename|set-default-workdir|export|import
102
- create <title> [--body | --body-file PATH|-] [--assignee] [--parent]... [--workspace scratch|worktree:<repository>|dir:<repository>] [--branch] [--project]
104
+ create <title> [--readable-id ID] [--body | --body-file PATH|-] [--assignee] [--parent]... [--workspace scratch|worktree:<repository>|dir:<repository>] [--branch] [--project]
103
105
  [--tenant] [--priority] [--triage] [--idempotency-key] [--max-runtime 30m] [--created-by] [--skill]... [--max-retries]
104
106
  [--model] [--provider] [--reviewer PROFILE] [--goal|--no-goal] [--goal-max-turns] [--initial-status blocked|running]
105
107
  swarm <goal> --worker PROFILE:TITLE[:SKILL,SKILL]... --verifier P --synthesizer P
106
- list [--mine] [--assignee] [--status] [--tenant] [--session] [--archived] [--sort] show <id> context <id> brief <id> [--run N] runs <id>
108
+ list [--mine] [--assignee] [--status] [--tenant] [--session] [--subtasks] [--archived] [--sort] show <id> context <id> brief <id> [--run N] runs <id>
107
109
  set <id> <assignee|machine|model|every|reviewer> <value|none> [--provider] [--ids ...] assign <id> <profile|none> set-model <id> [model] [--provider] set-goal <id>... [--goal-max-turns N] [--off]
108
110
  reassign <id> --agent AGENT [--role implementer] (moves a card role's assignment to another agent; sessions keep their identity)
109
111
  claim <id> [--ttl] reclaim <id> [--reason] heartbeat <id> [--note] [--until <time|30m>] link|unlink <parent> <child>
110
- comment <id> (<text> | --file PATH|-) [--author] [--max-len] [(--pr URL --pr-head SHA)...] attach <id> <path> [--name] [--content-type] attachments <id> attach-rm <n>
112
+ comment <id> (<text> | --file PATH|-) [--author] [--max-len] [(--pr URL --pr-head SHA)...] [--source owner/repo:branch]... attach <id> <path> [--name] [--content-type] attachments <id> attach-rm <n>
111
113
  attachment-read <id> <attachment-id> [--offset N --length N --version V] bounded base64/metadata JSON, including through --at
112
114
  complete <id>... [--result] [--summary] [--metadata JSON] [--commit SHA] edit <id> --result [--summary] [--metadata]
113
115
  block <id> [reason] (--on <card> | --waiting-on <message id>) [--ids ...] schedule <id> [reason] [--ids ...]
@@ -115,7 +117,7 @@ The board's verbs, with their flags:
115
117
  request-state <id> --state block|pause|done|cancelled --reason <why> [--on <card>|--waiting-on <message>] [--evidence <pointers>]
116
118
  unblock <id>... --on <blocker> --reason <what arrived> request-review <id> (--pr URL --pr-head SHA)... [--summary] [--reviewer] [--force] [--commit SHA] request-changes <id> <reason> [--finding <finding>]... reopen-review <id>... reopen <id>... [--reason]
117
119
  promote <id> [reason] [--ids ...] [--force] [--dry-run] archive <id>... --reason <why> [--rm <id>...]
118
- specify <id> [--title --body | --body-file PATH|-] [--workspace scratch|worktree:<repository>|dir:<repository>] [--branch NAME] [--machine M|none] [--all]
120
+ specify <id> [--readable-id ID] [--title --body | --body-file PATH|-] [--workspace scratch|worktree:<repository>|dir:<repository>] [--branch NAME] [--machine M|none] [--all]
119
121
  decompose <id> [--child "<title>[@<profile>]"]... [--all]
120
122
  diagnostics [--task] [--severity] stats assignees log <id> [--tail N] tail <id> watch [--assignee] [--kinds]
121
123
  github-usage [--since 1h] local REST attempts by caller/endpoint; no GitHub read
@@ -127,14 +129,14 @@ The board's verbs, with their flags:
127
129
  And:
128
130
  events [--after <cursor>] [--once] --json
129
131
  Native retained card snapshots, then committed changes; a changed source requests resynchronization.
130
- usage [--agent AGENT] [--sponsor PERSON] [--json]
131
- Tokens each agent's ended card sessions recorded (the harness's own records), per model, summed on its sponsor.
132
+ usage [--agent AGENT] [--json]
133
+ Tokens each agent's ended card sessions recorded (the harness's own records), per model.
132
134
  init --backing sqlite|files · boards create <slug>
133
135
  Where the Org's one task store is kept: sqlite is one database file, kanban.db (the default); files is one JSON file per
134
136
  card. A board is a saved view over that store: the cards carrying its slug as a tag (create --tag, tag/untag).
135
- create … --tag T… --sponsor PERSON | --requested-by <platform>:<user> [--request <message ref>]
136
- Tags are ordered (the first is the tenant). A proven requester sponsors the card with the request as
137
- evidence; an unproven one is recorded as requested by (unverified) and the sponsor stays inherited.
137
+ create … --tag T… --requested-by <platform>:<user> [--request <message ref>]
138
+ Tags are ordered (the first is the tenant). The requester is recorded with what Teams' identity record says of
139
+ them (proven or not) and the request.
138
140
  tag <id> <tag>… untag <id> <tag>…
139
141
  create … --machine M --subtask-of ARC --notify ADDRESS|--no-notify --no-start
140
142
  With a pane lane and room, a card starts at once and answers "started" with its session; else it answers why it
@@ -151,7 +153,8 @@ And:
151
153
 
152
154
  specify <id> --workspace dir:<repository> --branch NAME: the manager retargets a stopped card without replacing its history.
153
155
  specify <id> --workspace … --machine M: declares the workspace on M and moves the card there, never asking the machine it leaves.
154
- comment <id> TEXT (--pr URL --pr-head SHA)...: the card's implementer or the manager adds PRs (including merged ones) to the arc's discovered source set, in the card's own repositories, each followed at its current head; the latest declaration replaces the previous one. This records evidence; only the board starts its review.
156
+ comment <id> TEXT (--pr URL --pr-head SHA)...: the card's implementer or the manager adds PRs (including merged ones) to the arc's discovered source set, in the arc's repositories (its workspace, its declared sources, its tasks'), each followed at its current head; the latest declaration replaces the previous one. This records evidence; only the board starts its review.
157
+ comment <arc> TEXT --source owner/repo:branch...: the arc's implementer or the manager declares every repository and branch the arc changes, on the arc itself; its review discovers PRs from them and accepts --pr declarations in them, with no task registered there. The latest declaration is the complete set.
155
158
  request-review <id> (--pr URL --pr-head SHA)...: hand over every open PR the arc changed, each at its pushed head; one review covers them all.
156
159
  request-state <id> --state done (--reviewed-sha SHA)...: record the verdict on every requested PR head.
157
160
  request-state <id>... --state STATE --reason REASON: request several decisions in one retained manager mail.
@@ -194,7 +197,8 @@ const isView = (root, slug) => slug !== DEFAULT_BOARD && !boardExists(root, slug
194
197
  function boardMeta(root, slug) {
195
198
  const meta = { slug, name: slug === DEFAULT_BOARD ? 'Default' : slug, description: '', icon: '', color: '', default_workdir: null, project_id: null, created_at: null, archived: false };
196
199
  try { Object.assign(meta, JSON.parse(readFileSync(metaPath(root, slug), 'utf8')), { slug }); } catch { /* no metadata yet */ }
197
- return { ...meta, db_path: boardPath(root, slug), backing: backingOf(root, slug) };
200
+ const store = boardExists(root, slug) ? slug : DEFAULT_BOARD;
201
+ return { ...meta, key: boardKey(root, store), db_path: boardPath(root, store), backing: backingOf(root, store) };
198
202
  }
199
203
 
200
204
  function writeMeta(root, slug, patch) {
@@ -203,7 +207,7 @@ function writeMeta(root, slug, patch) {
203
207
  meta.created_at ??= now();
204
208
  mkdirSync(boardDir(root, slug), { recursive: true });
205
209
  writeFileSync(metaPath(root, slug), `${JSON.stringify(meta, null, 2)}\n`);
206
- return { ...meta, db_path: boardPath(root, slug), backing: backingOf(root, slug) };
210
+ return boardMeta(root, slug);
207
211
  }
208
212
 
209
213
  /** Hermes's list: the default board and every named one with a database or metadata. */
@@ -228,7 +232,7 @@ function counts(root, slug) {
228
232
  if (view && !boardExists(root, DEFAULT_BOARD)) return {};
229
233
  const b = openBoard(root, view ? DEFAULT_BOARD : slug);
230
234
  try {
231
- const rows = b.db.prepare('SELECT t.status, t.tenant, c.tags FROM tasks t LEFT JOIN supercode_cards c ON c.task_id = t.id').all();
235
+ const rows = engine.listCards(b.db, { ...stateOptions(root), includeSubtasks: false });
232
236
  const out = {};
233
237
  for (const r of rows) if (!view || tagsOf(r).includes(slug)) out[r.status] = (out[r.status] ?? 0) + 1;
234
238
  return out;
@@ -305,17 +309,12 @@ async function callerPrincipal(root, sid, { teams = false, recordedAgent = null
305
309
  return !scope.offline && scope.principal ? `person:${scope.principal}` : null;
306
310
  }
307
311
 
308
- /** C4: a requester named by an outside id; a proven link makes them the card's sponsor, anything less is recorded unverified. */
309
- async function requestedSponsor(args) {
310
- if (args.sponsor) return { sponsor: args.sponsor, sponsorEvidence: args.request ? { request: args.request, by: principalOf() } : null };
311
- if (!args['requested-by']) return {};
312
+ /** C4: a requester named by an outside id, recorded with what Teams' identity record says of them: a record only. */
313
+ async function requester(args) {
312
314
  let resolved = null;
313
- try { resolved = (await agentDoor('resolveIdentity', [args['requested-by']], { context: current().facts.SUPERCODE_CONTEXT })).data; } catch { /* no Teams: unverified */ }
314
- if (resolved?.classification === 'proven' && resolved.principal?.kind === 'user') {
315
- const link = resolved.links?.find((l) => l.classification === 'proven');
316
- return { sponsor: resolved.principal.id, sponsorEvidence: { observation: args['requested-by'], link: link?.id ?? null, proof: link?.proof?.kind ?? null, request: args.request ?? null, by: principalOf() } };
317
- }
318
- return { requestedByUnverified: { observation: args['requested-by'], label: resolved?.label ?? 'unresolved', request: args.request ?? null } };
315
+ try { resolved = (await agentDoor('resolveIdentity', [args['requested-by']], { context: current().facts.SUPERCODE_CONTEXT })).data; } catch { /* no Teams: unresolved */ }
316
+ const link = resolved?.classification === 'proven' ? resolved.links?.find((l) => l.classification === 'proven') : null;
317
+ return { requestedByUnverified: { observation: args['requested-by'], label: resolved?.label ?? 'unresolved', ...(link ? { proven: { principal: resolved.principal?.id ?? null, link: link.id, proof: link.proof?.kind ?? null } } : {}), request: args.request ?? null } };
319
318
  }
320
319
 
321
320
  const tagsOf = (card) => { const tags = Array.isArray(card.tags) ? card.tags : json(card.tags, null); return Array.isArray(tags) ? tags : card.tenant ? [card.tenant] : []; };
@@ -323,7 +322,7 @@ const tagsOf = (card) => { const tags = Array.isArray(card.tags) ? card.tags : j
323
322
  const say = (args, human, value) => { current().out(args.json ? `${JSON.stringify(value, null, 2)}\n` : `${human}\n`); };
324
323
 
325
324
  // Hermes's own output lines, so a reader of `hermes kanban list` reads this one
326
- const ICONS = { todo: '◻', ready: '▶', running: '●', scheduled: '⏱', blocked: '⊘', done: '✓', archived: '—' };
325
+ const ICONS = { todo: '◻', ready: '▶', running: '●', scheduled: '⏱', blocked: '⊘', paused: '⏸', done: '✓', archived: '—' };
327
326
  const ts = (t) => {
328
327
  if (!t) return '';
329
328
  const d = new Date(Number(t) * 1000);
@@ -331,16 +330,19 @@ const ts = (t) => {
331
330
  return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
332
331
  };
333
332
  // a card's status is the workflow's (triaging, reviewing, …); kanban.db stores the Hermes status it maps to
334
- const line = (t) => `${ICONS[t.status] ?? '?'} ${engine.cardLabel(t.number, t.id).padEnd(16)} ${(t.stage ?? t.status).padEnd(8)}${t.detached ? ` [session belongs to ${t.detached.person}]` : ['running','review'].includes(t.status) ? ` [session ${t.execution?.state ?? 'unknown'}]` : ''} ${(t.assignee || '(unassigned)').padEnd(20)}${t.tenant ? ` [${t.tenant}]` : ''} ${t.title}${t.machine ? ` @${t.machine}` : ''}`;
335
- const cardDict = (t) => ({
336
- id: t.id, number: t.number ?? null, title: t.title, body: t.body, assignee: t.assignee, status: t.status, priority: t.priority, tenant: t.tenant,
333
+ const line = (key) => (t) => `${ICONS[t.status] ?? '?'} ${engine.cardLabel(key, t.number, t.id, t.readable_id).padEnd(18)} ${(['paused','blocked'].includes(t.status) ? t.status : t.stage ?? t.status).padEnd(8)}${holdDescription(t) ? ` [${holdDescription(t)}]` : t.detached ? ` [session belongs to ${t.detached.person}]` : ['running','review'].includes(t.status) ? ` [session ${t.execution?.state ?? 'unknown'}]` : ''} ${(t.assignee || '(unassigned)').padEnd(20)}${t.tenant ? ` [${t.tenant}]` : ''} ${t.title}${t.machine ? ` @${t.machine}` : ''}`;
334
+ // `key` is the card's board's (store.mjs boardKey); `ref` the card as a person names it (`VO-856`)
335
+ const stateOptions = root => ({ nativeHolds: Boolean(workflowFor(root).workflow.flags?.paused) });
336
+ const cardDict = (raw, key, options) => { const t = cardState(raw, options); return ({
337
+ id: t.id, readable_id: t.readable_id ?? null, number: t.number ?? null, key, ref: cardRef(key, t.number) ?? null, title: t.title, body: t.body, assignee: t.assignee, status: t.status, priority: t.priority, tenant: t.tenant,
337
338
  workspace_kind: t.workspace_kind, repository: t.workspace_anchor ?? null, workspace_path: t.workspace_path, branch_name: t.branch_name, project_id: t.project_id,
338
339
  created_by: t.created_by, created_at: t.created_at, started_at: t.started_at, completed_at: t.completed_at, result: t.result,
339
340
  skills: t.skills ?? [], max_retries: t.max_retries, model_override: t.model_override, provider_override: t.provider_override,
340
341
  session_id: t.session_id, workflow_template_id: t.workflow_template_id, current_step_key: t.current_step_key,
341
342
  machine: t.machine ?? null, subtask_of: t.subtask_of ?? null, execution: t.execution ?? null, detached: t.detached ?? null,
342
343
  reviewer: t.reviewer ?? null,
343
- });
344
+ paused: t.paused, blocked_by: t.blocked_by,
345
+ }); };
344
346
 
345
347
  /** Hermes's durations: seconds, or 30s / 5m / 2h / 1d. */
346
348
  // a moment the caller names: an ISO time, or a duration from now (30m, 2h); epoch seconds, or null
@@ -368,17 +370,19 @@ const metadataOf = (v) => {
368
370
  return got;
369
371
  };
370
372
 
371
- // A card is named by its id or by its number (`42`, `#42`, store.mjs layOutNumbers) in every place a verb takes a card:
372
- // its card positionals (none, the first, the first two, or all of them) and its card flags. A number is read as its
373
- // card's id before anything else reads the verb (its authorization included); a number no card here has is refused.
373
+ // A card is named by its id or by its number, keyed or bare (`VO-42`, `vo-42`, `#42`, `42`: store.mjs boardKey,
374
+ // layOutNumbers) in every place a verb takes a card: its card positionals (none, the first, the first two, or all of
375
+ // them) and its card flags. A number is read as its card's id before anything else reads the verb (its authorization
376
+ // included). A number no card here has, or a key that is not this board's, is refused. A key is two or more letters, so
377
+ // a message id (`m-…`, `a-…`) is never read as one.
374
378
  const NO_CARD_WORDS = new Set(['create', 'new', 'swarm', 'list', 'ls', 'migrate', 'init', 'boards', 'usage', 'events', 'listeners', 'dispatch',
375
379
  'capacity', 'deliver', 'daemon', 'serve', 'gc', 'repair', 'dispatching', 'attach-rm', 'run-session', 'stats', 'assignees', 'diagnostics', 'diag',
376
380
  'watch', 'help', 'github-usage']);
377
381
  // the verbs that act on every positional as a card (each loops over them all)
378
382
  const ALL_CARD_WORDS = new Set(['complete', 'archive', 'request-state', 'request_state', 'set-goal', 'unblock', 'reopen', 'reopen-review']);
379
383
  const CARD_FLAGS = ['ids', 'subtask-of', 'parent', 'on', 'for-card', 'task', 'rm'];
380
- const CARD_NUMBER = /^#?([1-9]\d*)$/;
381
- function resolveCardNumbers(verb, rest, args, withBoard) {
384
+ const CARD_NUMBER = /^(?:#|([A-Za-z][A-Za-z0-9]+)-)?([1-9]\d*)$/;
385
+ function resolveCardNumbers(verb, rest, args, withBoard, keyOf) {
382
386
  const words = NO_CARD_WORDS.has(verb) ? 0 : ALL_CARD_WORDS.has(verb) ? rest.length : ['link', 'unlink'].includes(verb) ? 2 : 1;
383
387
  const places = [];
384
388
  for (let i = 0; i < Math.min(words, rest.length); i++) places.push([rest, i]);
@@ -388,12 +392,14 @@ function resolveCardNumbers(verb, rest, args, withBoard) {
388
392
  }
389
393
  const numbered = places.filter(([holder, key]) => CARD_NUMBER.test(String(holder[key])));
390
394
  if (!numbered.length) return;
395
+ const own = keyOf();
391
396
  withBoard((b) => view(b, (db) => {
392
- for (const [holder, key] of numbered) {
393
- const n = Number(CARD_NUMBER.exec(String(holder[key]))[1]);
394
- const id = engine.cardByNumber(db, n);
395
- if (!id) throw new Error(`no card #${n} on this board`);
396
- holder[key] = id;
397
+ for (const [holder, at] of numbered) {
398
+ const [, named, digits] = CARD_NUMBER.exec(String(holder[at]));
399
+ if (named && named.toUpperCase() !== own) throw new Error(`${holder[at]} names a card of board ${named.toUpperCase()}, not of this board (${own}); nothing was done`);
400
+ const id = engine.cardByNumber(db, Number(digits));
401
+ if (!id) throw new Error(`no card ${own}-${digits} on this board`);
402
+ holder[at] = id;
397
403
  }
398
404
  }));
399
405
  }
@@ -422,7 +428,9 @@ export async function runVerb(argv) {
422
428
  const open = (create = false) => { const b = openBoard(root, slug, { create, document: args.document ?? null, backing: create ? args.backing ?? null : null }); b.label = boardLabel(root, slug); return b; };
423
429
  const withBoard = (fn, create = false) => { const b = open(create); try { return fn(b); } finally { b.close(); } };
424
430
  const withBoardAsync = async (fn, create = false) => { const b = open(create); try { return await fn(b); } finally { b.close(); } };
425
- resolveCardNumbers(verb, rest, args, withBoard);
431
+ // the board's key (store.mjs boardKey), read when a card is named or shown by its number
432
+ const keyOf = () => boardKey(root, slug);
433
+ resolveCardNumbers(verb, rest, args, withBoard, keyOf);
426
434
  const id = rest[0];
427
435
  const need = (n, what) => { if (rest.length < n) throw new Error(`${verb} needs ${what}`); };
428
436
  const ids = (first = rest) => [...first, ...(args.ids ?? [])];
@@ -549,19 +557,19 @@ export async function runVerb(argv) {
549
557
  let workspace = args.workspace;
550
558
  if ((workspace === 'dir' || workspace === 'worktree') && meta.default_workdir) workspace = `${workspace}:${meta.default_workdir}`;
551
559
  // --goal / --no-goal are the create's words; without either, the workflow's initial decides
552
- // a person filing for themselves sponsors it; an agent filing a person's request names them (proven) or records them unverified
553
- const sponsorship = args.sponsor || args['requested-by'] ? await requestedSponsor(args) : principalOf()?.startsWith('person:') ? { sponsor: principalOf().slice('person:'.length) } : {};
560
+ // an agent filing a person's request records who asked
561
+ const requested = args['requested-by'] ? await requester(args) : {};
554
562
  const created = await withBoardAsync(async (b) => engine.createCard(b, await engine.prepareCard(b, {
555
563
  title: rest.join(' '), body: textFrom(args['body-file'], args.body), assignee: args.assignee === 'none' ? null : args.assignee, machine: args.machine,
556
564
  reviewer: args.reviewer ?? null,
557
- workspace, branch: args.branch, parents: args.parent, priority: args.priority, tenant: args.tenant,
565
+ readableId: args['readable-id'], workspace, branch: args.branch, parents: args.parent, priority: args.priority, tenant: args.tenant,
558
566
  triage: args.triage, idempotencyKey: args['idempotency-key'], maxRuntime: duration(args['max-runtime']),
559
567
  maxRetries, skills: args.skill, model: args.model, provider: args.provider, createdBy: args['created-by'] ?? null,
560
568
  noStart: Boolean(args['no-start']), adopt: args.adopt ?? null,
561
569
  every: args.every ?? null, sessionId: callerSession(), subtaskOf: args['subtask-of'] ?? null, initialStatus: args['initial-status'] ?? null,
562
570
  project: args.project ?? meta.project_id ?? null, goal: args.goal ? { maxTurns: args['goal-max-turns'] ? Number(args['goal-max-turns']) : null } : args['no-goal'] ? false : null,
563
571
  tags: [...(viewTag ? [viewTag] : []), ...(args.tag ?? [])],
564
- ...sponsorship,
572
+ ...requested,
565
573
  })), true);
566
574
  await withBoardAsync(b=>engine.runEffects(b,created.effects??[]));
567
575
  let answer = { id: created.id, existing: created.existing };
@@ -577,9 +585,9 @@ export async function runVerb(argv) {
577
585
  if (address) withBoard((b) => engine.subscribe(b, created.id, address, 'creator'));
578
586
  const task = withBoard((b) => view(b, (db) => engine.getTask(db, created.id)));
579
587
  const recipients = withBoard(b => engine.subscriptions(b.db, created.id)).filter(s => s.platform === 'supercode').map(s => s.chat_id);
580
- if (args.json) { say(args, '', { ...cardDict(task), ...answer, notify: address ?? null, recipients }); return answer.failed ? 1 : 0; }
588
+ if (args.json) { say(args, '', { ...cardDict(task, keyOf(), stateOptions(root)), ...answer, notify: address ?? null, recipients }); return answer.failed ? 1 : 0; }
581
589
  const said = answer.adoption_requested ? `; held for ${answer.adoption_requested}, which the dispatcher binds on its next tick` : answer.started ? `; started session ${answer.session} (${answer.mode})` : answer.failed ? `; did not start: ${answer.failed}` : answer.waits ? `; waits: ${answer.waits}` : '';
582
- say(args, `${created.existing ? 'Existing' : 'Created'} ${engine.cardLabel(task.number, created.id)} (${task.status}, assignee=${task.assignee || '-'})${said}${recipients.length ? `; notices go to ${recipients.join(', ')}` : ''}`, null);
590
+ say(args, `${created.existing ? 'Existing' : 'Created'} ${engine.cardLabel(keyOf(), task.number, created.id, task.readable_id)} (${task.status}, assignee=${task.assignee || '-'})${said}${recipients.length ? `; notices go to ${recipients.join(', ')}` : ''}`, null);
583
591
  return answer.failed ? 1 : 0;
584
592
  }
585
593
 
@@ -598,7 +606,7 @@ export async function runVerb(argv) {
598
606
 
599
607
  case 'list': case 'ls': {
600
608
  const assignee = args.assignee ?? (args.mine ? callerName() : null);
601
- const cards = await withBoardAsync(b => listCardsInTurns(b.db, { detailed: Boolean(args.json), status: args.status, assignee, machine: args.machine, includeArchived: args.archived || args.all,
609
+ const cards = await withBoardAsync(b => listCardsInTurns(b.db, { ...stateOptions(root), detailed: Boolean(args.json), status: args.status, assignee, machine: args.machine, includeArchived: args.archived || args.all, includeSubtasks: args.subtasks || args.all,
602
610
  tenant: args.tenant, session: args.session, workflowTemplateId: args['workflow-template-id'], stepKey: args['step-key'], tags: [...(viewTag ? [viewTag] : []), ...(args.tag ?? [])] }));
603
611
  const sorters = {
604
612
  assignee: (a, b) => String(a.assignee ?? '').localeCompare(String(b.assignee ?? '')), created: (a, b) => a.created_at - b.created_at,
@@ -608,14 +616,15 @@ export async function runVerb(argv) {
608
616
  };
609
617
  if (args.sort) { if (!sorters[args.sort]) throw new Error(`--sort is one of ${Object.keys(sorters).join(', ')}`); cards.sort(sorters[args.sort]); }
610
618
  if (args.json) {
611
- await writeCardList(cards, t => ({ ...cardDict(t), workflow_status: t.workflow_status, parents: t.parents, run: t.run, waiting_on: t.waiting_on }), current().out);
619
+ const own = keyOf();
620
+ await writeCardList(cards, t => ({ ...cardDict(t, own, stateOptions(root)), workflow_status: t.workflow_status, parents: t.parents, run: t.run, waiting_on: t.waiting_on }), current().out);
612
621
  return 0;
613
622
  }
614
623
  const others = boardsOf(root, { includeArchived: false }).length - 1;
615
624
  if (others > 0) current().out(`Board: ${slug} (${others} other board${others === 1 ? '' : 's'} — \`supercode workflow boards list\`)\n\n`);
616
625
  if (!cards.length) current().out('(no matching tasks)\n');
617
626
  for (let offset = 0; offset < cards.length; offset += 64) {
618
- current().out(`${cards.slice(offset, offset + 64).map(line).join('\n')}\n`);
627
+ current().out(`${cards.slice(offset, offset + 64).map(line(keyOf())).join('\n')}\n`);
619
628
  await new Promise(resolve => setImmediate(resolve));
620
629
  }
621
630
  return 0;
@@ -625,11 +634,11 @@ export async function runVerb(argv) {
625
634
  need(1, 'a card id');
626
635
  const { settings } = await laneState(root).catch(() => ({ settings: boardSettings({}) }));
627
636
  const card = withBoard((b) => {
628
- const got = engine.showCard(b.db, id);
637
+ const got = engine.showCard(b.db, id, stateOptions(root));
629
638
  got.runs = engine.runs(b.db, id, { stateType: args['state-type'] ?? null, stateName: args['state-name'] ?? null });
630
639
  got.task.execution = executionOf(engine.currentRun(b.db, id));
631
640
  got.runs = got.runs.map(run => ({ ...run, execution: executionOf(run) }));
632
- got.diagnostics = engine.diagnose(b.db, got.task, { failureLimit: settings.failureLimit, launchWindowSeconds: settings.launchWindow });
641
+ got.diagnostics = engine.diagnose(b.db, got.task, { ...stateOptions(root), failureLimit: settings.failureLimit, launchWindowSeconds: settings.launchWindow });
633
642
  got.stage = statusOf(b.db, got.task);
634
643
  const entry = workflowFor(root);
635
644
  // A workflow may explicitly replace the built-in time field. Keep its
@@ -639,15 +648,17 @@ export async function runVerb(argv) {
639
648
  : timeOf(b.db, got.task);
640
649
  return got;
641
650
  });
642
- const t = card.task;
651
+ const t = cardState({ ...card.task, stage: card.stage }, stateOptions(root));
643
652
  const latest = [...card.runs].reverse().find((r) => r.summary)?.summary ?? null;
644
653
  if (args.json) {
645
- say(args, '', { task: cardDict(t), side_sessions: withBoard((b) => sideSessions(b.db, t.id)), latest_summary: latest, parents: card.parents.map((p) => p.id), children: card.children, subtasks: card.subtasks, comments: card.comments, events: card.events, runs: card.runs, time: card.time, subscriptions: card.subscriptions, diagnostics: card.diagnostics });
654
+ say(args, '', { task: cardDict(t, keyOf(), stateOptions(root)), sources: withBoard((b) => arcSources(b.db, t.id)), side_sessions: withBoard((b) => sideSessions(b.db, t.id)), latest_summary: latest, parents: card.parents.map((p) => p.id), children: card.children, subtasks: card.subtasks, comments: card.comments, events: card.events, runs: card.runs, time: card.time, subscriptions: card.subscriptions, diagnostics: card.diagnostics });
646
655
  return 0;
647
656
  }
648
657
  const labels = withBoard((b) => view(b, (db) => engine.numbersOf(db, [t.subtask_of, ...card.parents.map((p) => p.id), ...card.children].filter(Boolean))));
649
- const label = (cardId) => engine.cardLabel(labels.get(cardId), cardId);
650
- const out = [`Task ${engine.cardLabel(t.number, t.id)}: ${t.title}`, ` status: ${taskStatusOf(card.stage, t.completed_at)} (lane ${card.stage}${card.stage !== t.status ? `, ${t.status} in kanban.db` : ''})`, ` assignee: ${t.assignee || '-'}`];
658
+ const boardKeyNow = keyOf();
659
+ const label = (cardId) => engine.cardLabel(boardKeyNow, labels.get(cardId), cardId);
660
+ const out = [`Task ${engine.cardLabel(boardKeyNow, t.number, t.id, t.readable_id)}: ${t.title}`, ` status: ${['paused','blocked'].includes(t.status) ? t.status : taskStatusOf(card.stage, t.completed_at)} (lane ${card.stage})`, ` assignee: ${t.assignee || '-'}`];
661
+ if (holdDescription(t)) out.push(` hold: ${holdDescription(t)}`);
651
662
  if (t.machine) out.push(` machine: ${t.machine}`);
652
663
  if (t.reviewer) out.push(` reviewer: ${t.reviewer} (card override)`);
653
664
  if (t.detached) out.push(` session: ${t.detached.session.address} belongs to ${t.detached.person} (detached ${ts(t.detached.at)}; dispatcher does not own it)`);
@@ -665,27 +676,28 @@ export async function runVerb(argv) {
665
676
  out.push(` created: ${ts(t.created_at)} by ${t.created_by || '-'}`);
666
677
  const own = withBoard((b) => engine.getTask(b.db, t.id));
667
678
  if (own?.tags?.length) out.push(` tags: ${own.tags.join(', ')}`);
668
- if (own?.sponsor) out.push(` sponsor: ${own.sponsor}${own.sponsor_evidence?.observation ? ` (requested by ${own.sponsor_evidence.observation}, proven)` : ''}`);
669
- if (own?.requested_by_unverified) out.push(` requested by (unverified): ${own.requested_by_unverified.label ?? own.requested_by_unverified.observation}`);
679
+ if (own?.requested_by_unverified) out.push(` requested by: ${own.requested_by_unverified.label ?? own.requested_by_unverified.observation}${own.requested_by_unverified.proven ? ' (proven)' : ' (unverified)'}`);
670
680
  if (card.diagnostics.length) {
671
681
  out.push('', ` Diagnostics (${card.diagnostics.length}):`);
672
- for (const d of card.diagnostics) { out.push(` ${{ warning: '⚠', error: '!!', critical: '!!!' }[d.severity]} [${d.severity}] ${d.title}`); for (const a of d.actions.slice(0, 1)) out.push(` → ${a}`); }
682
+ for (const d of card.diagnostics) { out.push(` ${{ info: 'ℹ', warning: '⚠', error: '!!', critical: '!!!' }[d.severity]} [${d.severity}] ${d.title}`); for (const a of d.actions.slice(0, 1)) out.push(` → ${a}`); }
673
683
  }
674
684
  if (t.started_at) out.push(` started: ${ts(t.started_at)}`);
675
685
  if (t.completed_at) out.push(` completed: ${ts(t.completed_at)}`);
676
686
  if (card.parents.length) out.push(` parents: ${card.parents.map((p) => label(p.id)).join(', ')}`);
677
687
  if (card.children.length) out.push(` children: ${card.children.map(label).join(', ')}`);
678
- if (card.subtasks.length) out.push(` subtasks: ${card.subtasks.map((s) => `${engine.cardLabel(s.number, s.id)} (${s.status})`).join(', ')}`);
688
+ if (card.subtasks.length) out.push(` subtasks: ${card.subtasks.map((s) => `${engine.cardLabel(boardKeyNow, s.number, s.id)} (${s.status})`).join(', ')}`);
679
689
  const side = withBoard((b) => sideSessions(b.db, t.id));
680
690
  if (side.length) out.push(` side sessions: ${side.map((s) => s.address).join(', ')}`);
681
691
  if (t.body) out.push('', 'Body:', t.body);
692
+ // the repositories and branches the arc declares it changes (comment --source)
693
+ { const sources = withBoard((b) => arcSources(b.db, t.id)); if (sources.length) out.push('', `Sources: ${sources.map((x) => `${x.repository}:${x.branch}`).join(', ')}`); }
682
694
  if (t.result) out.push('', 'Result:', t.result);
683
695
  else if (latest) out.push('', 'Latest summary:', latest);
684
696
  if (card.comments.length) { out.push('', `Comments (${card.comments.length}):`); for (const c of card.comments) out.push(` [${ts(c.created_at)}] ${c.author}: ${c.body}`); }
685
697
  // the card's history, without the board's own bookkeeping (a claim, a session kept or started, a goal ask, a
686
698
  // message queued); `--all` shows every event, `--json` always does
687
699
  const PLUMBING = new Set(['claimed', 'session_started', 'session_resumed', 'session_adopted', 'agent_session_queued', 'session_replaced', 'session_closed',
688
- 'goal_set', 'goal_continued', 'decision_asked', 'board_message', 'board_message_sent', 'heartbeat', 'promoted', 'respawn_guarded',
700
+ 'goal_set', 'goal_continued', 'decision_asked', 'board_message', 'board_message_sent', 'review_wait_cleared', 'heartbeat', 'promoted', 'respawn_guarded',
689
701
  'claim_extended', 'reclaimed', 'reconciled', 'status']);
690
702
  const history = args.all ? card.events : card.events.filter((e) => !PLUMBING.has(e.kind));
691
703
  if (history.length) {
@@ -707,20 +719,16 @@ export async function runVerb(argv) {
707
719
  return 0;
708
720
  }
709
721
 
710
- // G3: usage from each run's own harness records, summed on agents and on their sponsors
722
+ // G3: usage from each run's own harness records, summed on agents
711
723
  case 'usage': {
712
724
  const byAgent = agentUsage(root), context = current().facts.SUPERCODE_CONTEXT;
713
- const sponsorOf = async (agent) => { try { return (await agentDoor('get', [agent], { context })).data.sponsor ?? null; } catch { for (const row of activeAgentAssignments(root).filter((a) => a.agent_id === agent)) { const local = localAgentRecord(root, row.org, agent); if (local) return local.identity.sponsor ?? null; } return null; } };
714
725
  const sum = (records) => { const out = {}; for (const u of records) { const m = out[u.model] ??= { input_tokens: 0, output_tokens: 0, cache_read_tokens: 0, cache_write_tokens: 0, cost: 0 }; m.input_tokens += u.input_tokens ?? 0; m.output_tokens += u.output_tokens ?? 0; m.cache_read_tokens += u.cache_read_tokens ?? 0; m.cache_write_tokens += u.cache_write_tokens ?? 0; m.cost += u.cost?.amount ?? 0; if (u.cost?.amount != null) { m.cost_recorded = true; m.currency = u.cost.currency ?? m.currency ?? null; } else m.cost_unrecorded = true; } return out; };
715
726
  // a harness that keeps no price records tokens only: its cost reads as unrecorded, never as zero
716
727
  const line = (byModel) => Object.entries(byModel).map(([m, t]) => `${m} in ${t.input_tokens} out ${t.output_tokens} cache read ${t.cache_read_tokens} write ${t.cache_write_tokens} cost ${t.cost_recorded ? `${t.cost.toFixed(4)} ${t.currency ?? ''}${t.cost_unrecorded ? ' (some sessions unrecorded)' : ''}`.trim() : 'unrecorded'}`).join('; ');
717
728
  const agents = [...byAgent.keys()].filter((a) => !args.agent || a === args.agent);
718
- const rows = (await Promise.all(agents.map(async (agent) => ({ agent, sponsor: args.sponsor || !args.agent ? await sponsorOf(agent) : undefined, sessions: new Set(byAgent.get(agent).map((u) => u.session)).size, by_model: sum(byAgent.get(agent)) }))))
719
- .filter((row) => !args.sponsor || row.sponsor === args.sponsor);
729
+ const rows = agents.map((agent) => ({ agent, sessions: new Set(byAgent.get(agent).map((u) => u.session)).size, by_model: sum(byAgent.get(agent)) }));
720
730
  const total = sum(rows.flatMap((row) => byAgent.get(row.agent)));
721
- const sponsors = [...new Set(rows.map((row) => row.sponsor).filter(Boolean))].map((sponsor) => ({ sponsor, agents: rows.filter((row) => row.sponsor === sponsor).length, by_model: sum(rows.filter((row) => row.sponsor === sponsor).flatMap((row) => byAgent.get(row.agent))) }));
722
- say(args, rows.length ? [...rows.map((r) => `${r.agent}${r.sponsor ? ` (sponsor ${r.sponsor})` : ''}: ${r.sessions} session(s) ${line(r.by_model)}`),
723
- ...sponsors.map((s) => `sponsor ${s.sponsor}: ${s.agents} agent(s) ${line(s.by_model)}`), `total: ${line(total)}`].join('\n') : '(no recorded usage)', { agents: rows, sponsors, total, ...(args.sponsor ? { sponsor: args.sponsor } : {}) });
731
+ say(args, rows.length ? [...rows.map((r) => `${r.agent}: ${r.sessions} session(s) ${line(r.by_model)}`), `total: ${line(total)}`].join('\n') : '(no recorded usage)', { agents: rows, total });
724
732
  return 0;
725
733
  }
726
734
 
@@ -820,7 +828,7 @@ export async function runVerb(argv) {
820
828
  need(args.file !== undefined ? 1 : 2, 'a card id and the text (or --file PATH, - for stdin)');
821
829
  let text = textFrom(args.file, rest.slice(1).join(' '));
822
830
  if (args['max-len']) text = text.slice(0, Number(args['max-len']));
823
- const n = await withBoardAsync((b) => engine.comment(b, id, isLatestReviewer(b.db, id, actorOf()) ? actorOf() : args.author ?? callerAddress() ?? callerName(), text, { pr: args.pr ?? null, pr_head: args['pr-head'] ?? null }));
831
+ const n = await withBoardAsync((b) => engine.comment(b, id, isLatestReviewer(b.db, id, actorOf()) ? actorOf() : args.author ?? callerAddress() ?? callerName(), text, { pr: args.pr ?? null, pr_head: args['pr-head'] ?? null, source: args.source ?? null }));
824
832
  say(args, `Comment added to ${id}`, { ok: true, comment: n }); return 0;
825
833
  }
826
834
  case 'attach': {
@@ -874,7 +882,7 @@ export async function runVerb(argv) {
874
882
  need(1, 'a card id');
875
883
  const reason = rest.slice(1).join(' ') || null;
876
884
  const results = [];
877
- for (const t of [id, ...(args.ids ?? [])]) results.push([t, (await withBoardAsync((b) => engine.block(b, t, { reason, on: args.on ?? null, waitingOn: args['waiting-on'] ?? null }))).to]);
885
+ for (const t of [id, ...(args.ids ?? [])]) results.push([t, (await withBoardAsync((b) => engine.block(b, t, { reason, kind: args.kind ?? 'needs_input', on: args.on ?? null, waitingOn: args['waiting-on'] ?? null }))).to]);
878
886
  say(args, results.map(([t, s]) => `${s === 'todo' ? 'Waiting on dependencies' : s === 'triage' ? 'Routed to triage' : 'Blocked'} ${t}`).join('\n'), { ok: true, results }); return 0;
879
887
  }
880
888
  case 'schedule': {
@@ -894,7 +902,7 @@ export async function runVerb(argv) {
894
902
  case 'request-review': need(1, 'a card id'); await withBoardAsync((b) => engine.requestReview(b, id, { summary: args.summary ?? null, reviewer: args.reviewer ?? null, metadata: metadataOf(args.metadata) ?? null, commit: args.commit ?? null, branch: args.branch ?? null, pr: args.pr ?? null, pr_head: args['pr-head'] ?? args.head ?? args.commit ?? null, force: Boolean(args.force), expectedRunId: workerRun(b.db, id, sid) })); say(args, `${id} → review`, { ok: true }); return 0;
895
903
  case 'request-changes': {
896
904
  need(2, 'a card id and the reason');
897
- // each finding is filed as a task on the arc first, so the arc stays open until a pushed head fixes it
905
+ // each finding is filed as a task on the arc first: the arc's review is due again once every one is closed (ci.mjs answeredChanges)
898
906
  const findings = [].concat(args.finding ?? []).map((f) => String(f).trim()).filter(Boolean);
899
907
  const filed = [];
900
908
  for (const finding of findings) filed.push((await withBoardAsync((b) => engine.createCard(b, { title: finding, subtaskOf: id }))).id);
@@ -920,7 +928,7 @@ export async function runVerb(argv) {
920
928
  const targets = [id, ...(args.ids ?? [])];
921
929
  if (args['dry-run']) {
922
930
  const verdicts = withBoard((b) => targets.map((t) => {
923
- const card = engine.showCard(b.db, t);
931
+ const card = engine.showCard(b.db, t, stateOptions(root));
924
932
  const open = card.parents.filter((p) => !['done', 'archived'].includes(p.status)).map((p) => p.id);
925
933
  const ok = ['todo', 'blocked'].includes(card.task.status) && (!open.length || args.force);
926
934
  return { id: t, ok, reason: ok ? null : !['todo', 'blocked'].includes(card.task.status) ? `${t} is ${card.task.status}` : `parents not done: ${open.join(', ')}` };
@@ -950,12 +958,12 @@ export async function runVerb(argv) {
950
958
 
951
959
  case 'specify': case 'decompose': {
952
960
  const { lanes, settings } = await laneState(root);
953
- const targets = args.all ? withBoard((b) => engine.listCards(b.db, { status: 'triage' }).filter((t) => !args.tenant || t.tenant === args.tenant).map((t) => t.id)) : [id];
961
+ const targets = args.all ? withBoard((b) => engine.listCards(b.db, { ...stateOptions(root), status: 'triage' }).filter((t) => !args.tenant || t.tenant === args.tenant).map((t) => t.id)) : [id];
954
962
  if (!targets[0]) throw new Error(`${verb} needs a card id, or --all`);
955
963
  const results = [];
956
964
  for (const t of targets) {
957
- if (verb === 'specify' && (args.title !== undefined || args.body !== undefined || args['body-file'] !== undefined || args.workspace !== undefined || args.branch !== undefined || args.machine !== undefined)) {
958
- const moved = await withBoardAsync((b) => engine.specify(b, t, { title: args.title ?? null, body: textFrom(args['body-file'], args.body) ?? null, workspace: args.workspace ?? null, branch: args.branch ?? null, machine: args.machine ?? null, author: args.author ?? callerAddress() ?? callerName() }));
965
+ if (verb === 'specify' && (args['readable-id'] !== undefined || args.title !== undefined || args.body !== undefined || args['body-file'] !== undefined || args.workspace !== undefined || args.branch !== undefined || args.machine !== undefined)) {
966
+ const moved = await withBoardAsync((b) => engine.specify(b, t, { readableId: args['readable-id'] ?? null, title: args.title ?? null, body: textFrom(args['body-file'], args.body) ?? null, workspace: args.workspace ?? null, branch: args.branch ?? null, machine: args.machine ?? null, author: args.author ?? callerAddress() ?? callerName() }));
959
967
  results.push({ id: t, specified: true, status: moved.to });
960
968
  } else if (verb === 'decompose' && args.child?.length) {
961
969
  const children = args.child.map((c) => { const [title, assignee] = c.split(/@(?=[^@]*$)/); return { title: title.trim(), assignee: assignee?.trim() || undefined }; });
@@ -974,11 +982,11 @@ export async function runVerb(argv) {
974
982
  const fleet = await observeFleet(root);
975
983
  const publishing = await boardPublishing(root);
976
984
  const [expiredMail, waitingMail] = await Promise.all([await mailExpiries(), await mailWaiting()]);
977
- const order = ['warning', 'error', 'critical'];
985
+ const order = ['info', 'warning', 'error', 'critical'];
978
986
  const found = withBoard((b) => {
979
- const cards = args.task ? [engine.getTask(b.db, args.task)].filter(Boolean) : engine.listCards(b.db, {});
987
+ const cards = args.task ? [engine.getTask(b.db, args.task)].filter(Boolean) : engine.listCards(b.db, stateOptions(root));
980
988
  return [...(args.task ? [] : engine.diagnosePublishing(publishing).map((d) => ({ task: '(board)', title_of_task: null, ...d }))),
981
- ...cards.flatMap((t) => engine.diagnose(b.db, t, { failureLimit: settings.failureLimit, fleet, launchWindowSeconds: settings.launchWindow, publishing, expiredMail, waitingMail }).map((d) => ({ task: t.id, title_of_task: t.title, ...d })))]
989
+ ...cards.flatMap((t) => engine.diagnose(b.db, t, { ...stateOptions(root), failureLimit: settings.failureLimit, fleet, launchWindowSeconds: settings.launchWindow, publishing, expiredMail, waitingMail }).map((d) => ({ task: t.id, title_of_task: t.title, ...d })))]
982
990
  .filter((d) => !args.severity || order.indexOf(d.severity) >= order.indexOf(args.severity));
983
991
  });
984
992
  if (args.json) { say(args, '', found); return 0; }
@@ -986,7 +994,7 @@ export async function runVerb(argv) {
986
994
  return 0;
987
995
  }
988
996
  case 'stats': {
989
- const s = withBoard((b) => engine.stats(b.db));
997
+ const s = withBoard((b) => engine.stats(b.db, stateOptions(root)));
990
998
  if (args.json) { say(args, '', s); return 0; }
991
999
  const fmt = (o) => Object.entries(o).sort().map(([k, v]) => `${k}=${v}`).join(', ') || '(none)';
992
1000
  current().out(`By status: ${fmt(s.by_status)}\nBy assignee:\n${Object.entries(s.by_assignee).map(([a, c]) => ` ${a.padEnd(20)} ${fmt(c)}`).join('\n') || ' (none)'}\nBy machine:\n${Object.entries(s.by_machine).map(([m, c]) => ` ${m.padEnd(20)} ${fmt(c)}`).join('\n') || ' (none)'}\nOldest ready: ${s.oldest_ready_age_seconds === null ? '-' : `${Math.floor(s.oldest_ready_age_seconds / 60)}m`}\n`);
@@ -994,7 +1002,7 @@ export async function runVerb(argv) {
994
1002
  }
995
1003
  case 'assignees': {
996
1004
  const { profiles, lanes } = await laneState(root);
997
- const c = withBoard((b) => engine.stats(b.db).by_assignee);
1005
+ const c = withBoard((b) => engine.stats(b.db, stateOptions(root)).by_assignee);
998
1006
  const data = [...new Set([...profiles, ...Object.keys(c)])].sort().map((name) => ({ name, on_disk: profiles.includes(name), surface: lanes[name]?.surface ?? null, counts: c[name] ?? {} }));
999
1007
  if (args.json) { say(args, '', data); return 0; }
1000
1008
  current().out(`${'NAME'.padEnd(20)} ${'ON DISK'.padEnd(8)} ${'LANE'.padEnd(9)} COUNTS\n${data.map((e) => `${e.name.padEnd(20)} ${(e.on_disk ? 'yes' : 'no').padEnd(8)} ${(e.surface ?? '-').padEnd(9)} ${Object.entries(e.counts).sort().map(([k, v]) => `${k}=${v}`).join(', ') || '(idle)'}`).join('\n')}\n`);
@@ -1103,7 +1111,7 @@ export async function runVerb(argv) {
1103
1111
  const labels = new Map();
1104
1112
  for (const board of listBoards(root)) {
1105
1113
  const b = openBoard(root, board);
1106
- try { for (const [cardId, n] of view(b, (db) => engine.numbersOf(db, shown))) labels.set(cardId, engine.cardLabel(n, cardId)); } finally { b.close(); }
1114
+ try { const own = boardKey(root, board); for (const [cardId, n] of view(b, (db) => engine.numbersOf(db, shown))) labels.set(cardId, engine.cardLabel(own, n, cardId)); } finally { b.close(); }
1107
1115
  }
1108
1116
  const label = (cardId) => labels.get(cardId) ?? cardId;
1109
1117
  const lines = [];
@@ -1399,6 +1407,19 @@ function scrub(db) {
1399
1407
  }
1400
1408
  }
1401
1409
 
1410
+ // Only the transfer copy is lowered. Keep supercode_cards.paused (actor/reason)
1411
+ // and add Hermes's ordinary blocked event so its own tools can explain/unblock it.
1412
+ function lowerHermesHolds(db) {
1413
+ const rows = db.prepare(`SELECT t.id,c.paused FROM tasks t LEFT JOIN supercode_cards c ON c.task_id=t.id
1414
+ WHERE t.status = 'paused' OR (t.status NOT IN ('done','archived','cancelled') AND c.paused IS NOT NULL AND c.paused != 'null')`).all();
1415
+ for (const row of rows) {
1416
+ const pause = json(row.paused, {}) ?? {};
1417
+ db.prepare("UPDATE tasks SET status='blocked',block_kind='needs_input' WHERE id=?").run(row.id);
1418
+ db.prepare('INSERT INTO task_events(task_id,kind,payload,created_at) VALUES (?,?,?,?)').run(row.id, 'blocked',
1419
+ JSON.stringify({ kind: 'needs_input', reason: pause.reason ?? 'Paused on the native board', by: pause.by ?? null, source: 'native_pause_export' }), now());
1420
+ }
1421
+ }
1422
+
1402
1423
  const rowCounts = (db) => Object.fromEntries(TABLES.map((t) => [t, Number(db.prepare(`SELECT COUNT(*) AS n FROM ${t}`).get().n)]));
1403
1424
 
1404
1425
  function copyTree(from, to) {
@@ -1418,12 +1439,15 @@ function exportBoard(root, slug, out, { attachments, logs }) {
1418
1439
  mkdirSync(base, { recursive: true });
1419
1440
  try {
1420
1441
  const b = openBoard(root, slug);
1421
- try { b.db.prepare('VACUUM INTO ?').run(join(base, 'kanban.db')); } finally { b.close(); }
1442
+ try {
1443
+ const path = join(base, 'kanban.db');
1444
+ if (b.snapshot) b.snapshot(path); else b.db.prepare('VACUUM INTO ?').run(path);
1445
+ } finally { b.close(); }
1422
1446
  const snap = openBoard(base, DEFAULT_BOARD);
1423
1447
  let counts;
1424
- try { tx(snap, (db) => scrub(db)); counts = rowCounts(snap.db); } finally { snap.close(); }
1448
+ try { tx(snap, (db) => { scrub(db); lowerHermesHolds(db); }); counts = rowCounts(snap.db); } finally { snap.close(); }
1425
1449
  const { db_path: _, backing: ____, default_workdir: __, project_id: ___, ...meta } = boardMeta(root, slug);
1426
- writeFileSync(join(base, 'board.json'), `${JSON.stringify(meta, null, 2)}\n`);
1450
+ writeFileSync(join(base, 'board.json'), `${JSON.stringify({ ...meta, key: boardKey(root, slug) }, null, 2)}\n`);
1427
1451
  const attachmentsDir = slug === DEFAULT_BOARD ? join(root, 'kanban', 'attachments') : join(boardDir(root, slug), 'attachments');
1428
1452
  const logsFrom = logsDir(root, slug);
1429
1453
  const files = attachments ? copyTree(attachmentsDir, join(base, 'attachments')) : 0;
@@ -1455,11 +1479,12 @@ function importBoard(root, archive, as) {
1455
1479
  const requested = normalSlug(as ?? manifest.board ?? tops[0]);
1456
1480
  let slug = requested === DEFAULT_BOARD ? `${requested}-imported` : requested;
1457
1481
  for (let n = 2; existsSync(boardDir(root, slug)); n += 1) slug = `${requested}-${n}`;
1482
+ const staged = json(existsSync(join(extracted, 'board.json')) ? readFileSync(join(extracted, 'board.json'), 'utf8') : null, {}) ?? {};
1483
+ boardKey(root, slug, { creating: true, key: staged.key ?? null });
1458
1484
  const dest = boardDir(root, slug);
1459
1485
  mkdirSync(dest, { recursive: true });
1460
1486
  renameSync(join(extracted, 'kanban.db'), join(dest, 'kanban.db'));
1461
1487
  for (const tree of ['attachments', 'logs']) if (existsSync(join(extracted, tree))) renameSync(join(extracted, tree), join(dest, tree));
1462
- const staged = json(existsSync(join(extracted, 'board.json')) ? readFileSync(join(extracted, 'board.json'), 'utf8') : null, {}) ?? {};
1463
1488
  const name = staged.name || manifest.board_name || slug;
1464
1489
  writeMeta(root, slug, { name, description: staged.description ?? '', icon: staged.icon ?? '', color: staged.color ?? '', archived: false });
1465
1490
  const warnings = [];
@@ -1554,7 +1579,8 @@ const READS = new Set(['list', 'ls', 'show', 'context', 'brief', 'runs', 'log',
1554
1579
 
1555
1580
  /** The main session of each agent the workflow names a manager (`sc:<machine>:agent:<name>`), asked of this machine's
1556
1581
  * daemon, which keeps agents' records (mail step 1c), once per call before its verb runs: a manager named by its agent
1557
- * address is that agent's main session on the board. None for an agent the daemon does not know, or when it is down. */
1582
+ * address is that agent's main session on the board. Only a positive not-declared answer means no main; an unread
1583
+ * identity stops the call before authorization, rather than denying a role it could not establish. */
1558
1584
  async function readAgentMains(root) {
1559
1585
  // A lookup belongs to this call, including a refusal. A startup miss must not become the resident owner's
1560
1586
  // permanent answer, and a replacement main must revoke the old main's manager role on the next call.
@@ -1563,10 +1589,18 @@ async function readAgentMains(root) {
1563
1589
  for (const address of managers) {
1564
1590
  const name = /^sc:[^:\s]+:agent:([^:\s]+)$/.exec(address)?.[1];
1565
1591
  if (!name || agentMains.has(address)) continue;
1566
- let main = null;
1567
- try { main = (await machineCall(null, 'harness.v1.agent.show', { name, claim: { pid: process.pid, ppid: process.ppid, session: null, named: null, owner: null } }, { timeoutMs: 5_000 }))?.agents?.[0]?.main_session ?? null; }
1568
- catch { /* not known: the agent is no manager's session in this call */ }
1569
- agentMains.set(address, typeof main === 'string' && main ? main : null);
1592
+ try {
1593
+ const answer = await machineCall(null, 'harness.v1.agent.show', { name, records_only: true, claim: { pid: process.pid, ppid: process.ppid, session: null, named: null, owner: null } }, { timeoutMs: 5_000 });
1594
+ // EXIT_UNKNOWN is the daemon's explicit answer that this named agent is not declared.
1595
+ if (answer?.code === 3) { agentMains.set(address, null); continue; }
1596
+ const agent = answer?.agents?.[0];
1597
+ if (answer?.code !== 0 || answer.agents?.length !== 1 || agent?.name !== name ||
1598
+ typeof agent.main_session !== 'string' || !/^sc:[^:\s]+:[^:\s]+:[^:\s]+$/.test(agent.main_session))
1599
+ throw new Error(answer?.text ?? 'the daemon returned no valid named agent and main session');
1600
+ agentMains.set(address, agent.main_session);
1601
+ } catch (cause) {
1602
+ throw Object.assign(new Error(`Could not read who the caller is: manager ${address} could not be looked up (${cause.message ?? cause}). No role decision was made.`, { cause }), { code: 'manager_identity_unavailable' });
1603
+ }
1570
1604
  }
1571
1605
  }
1572
1606
  function agentMain(address) { return current().agentMains.get(address) ?? null; }