@volter/supercode-orchestrator 0.5.206 → 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,19 +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
 
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.
99
+
96
100
  The board's verbs, with their flags:
97
101
  init [--backing sqlite|files|ztrack] [--document <arcs.md>] [--commit] [--manager <address>] [--dispatching on|off] [--archive-max-lines N] [--archive-period month]
98
102
  migrate --backing ztrack --document <arcs.md> --status-map <json>
99
103
  boards list|create|rm|switch|show|rename|set-default-workdir|export|import
100
- 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]
101
105
  [--tenant] [--priority] [--triage] [--idempotency-key] [--max-runtime 30m] [--created-by] [--skill]... [--max-retries]
102
106
  [--model] [--provider] [--reviewer PROFILE] [--goal|--no-goal] [--goal-max-turns] [--initial-status blocked|running]
103
107
  swarm <goal> --worker PROFILE:TITLE[:SKILL,SKILL]... --verifier P --synthesizer P
104
- 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>
105
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]
106
110
  reassign <id> --agent AGENT [--role implementer] (moves a card role's assignment to another agent; sessions keep their identity)
107
111
  claim <id> [--ttl] reclaim <id> [--reason] heartbeat <id> [--note] [--until <time|30m>] link|unlink <parent> <child>
108
- 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>
109
113
  attachment-read <id> <attachment-id> [--offset N --length N --version V] bounded base64/metadata JSON, including through --at
110
114
  complete <id>... [--result] [--summary] [--metadata JSON] [--commit SHA] edit <id> --result [--summary] [--metadata]
111
115
  block <id> [reason] (--on <card> | --waiting-on <message id>) [--ids ...] schedule <id> [reason] [--ids ...]
@@ -113,7 +117,7 @@ The board's verbs, with their flags:
113
117
  request-state <id> --state block|pause|done|cancelled --reason <why> [--on <card>|--waiting-on <message>] [--evidence <pointers>]
114
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]
115
119
  promote <id> [reason] [--ids ...] [--force] [--dry-run] archive <id>... --reason <why> [--rm <id>...]
116
- 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]
117
121
  decompose <id> [--child "<title>[@<profile>]"]... [--all]
118
122
  diagnostics [--task] [--severity] stats assignees log <id> [--tail N] tail <id> watch [--assignee] [--kinds]
119
123
  github-usage [--since 1h] local REST attempts by caller/endpoint; no GitHub read
@@ -125,14 +129,14 @@ The board's verbs, with their flags:
125
129
  And:
126
130
  events [--after <cursor>] [--once] --json
127
131
  Native retained card snapshots, then committed changes; a changed source requests resynchronization.
128
- usage [--agent AGENT] [--sponsor PERSON] [--json]
129
- 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.
130
134
  init --backing sqlite|files · boards create <slug>
131
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
132
136
  card. A board is a saved view over that store: the cards carrying its slug as a tag (create --tag, tag/untag).
133
- create … --tag T… --sponsor PERSON | --requested-by <platform>:<user> [--request <message ref>]
134
- Tags are ordered (the first is the tenant). A proven requester sponsors the card with the request as
135
- 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.
136
140
  tag <id> <tag>… untag <id> <tag>…
137
141
  create … --machine M --subtask-of ARC --notify ADDRESS|--no-notify --no-start
138
142
  With a pane lane and room, a card starts at once and answers "started" with its session; else it answers why it
@@ -149,7 +153,8 @@ And:
149
153
 
150
154
  specify <id> --workspace dir:<repository> --branch NAME: the manager retargets a stopped card without replacing its history.
151
155
  specify <id> --workspace … --machine M: declares the workspace on M and moves the card there, never asking the machine it leaves.
152
- 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.
153
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.
154
159
  request-state <id> --state done (--reviewed-sha SHA)...: record the verdict on every requested PR head.
155
160
  request-state <id>... --state STATE --reason REASON: request several decisions in one retained manager mail.
@@ -192,7 +197,8 @@ const isView = (root, slug) => slug !== DEFAULT_BOARD && !boardExists(root, slug
192
197
  function boardMeta(root, slug) {
193
198
  const meta = { slug, name: slug === DEFAULT_BOARD ? 'Default' : slug, description: '', icon: '', color: '', default_workdir: null, project_id: null, created_at: null, archived: false };
194
199
  try { Object.assign(meta, JSON.parse(readFileSync(metaPath(root, slug), 'utf8')), { slug }); } catch { /* no metadata yet */ }
195
- 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) };
196
202
  }
197
203
 
198
204
  function writeMeta(root, slug, patch) {
@@ -201,7 +207,7 @@ function writeMeta(root, slug, patch) {
201
207
  meta.created_at ??= now();
202
208
  mkdirSync(boardDir(root, slug), { recursive: true });
203
209
  writeFileSync(metaPath(root, slug), `${JSON.stringify(meta, null, 2)}\n`);
204
- return { ...meta, db_path: boardPath(root, slug), backing: backingOf(root, slug) };
210
+ return boardMeta(root, slug);
205
211
  }
206
212
 
207
213
  /** Hermes's list: the default board and every named one with a database or metadata. */
@@ -226,7 +232,7 @@ function counts(root, slug) {
226
232
  if (view && !boardExists(root, DEFAULT_BOARD)) return {};
227
233
  const b = openBoard(root, view ? DEFAULT_BOARD : slug);
228
234
  try {
229
- 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 });
230
236
  const out = {};
231
237
  for (const r of rows) if (!view || tagsOf(r).includes(slug)) out[r.status] = (out[r.status] ?? 0) + 1;
232
238
  return out;
@@ -303,17 +309,12 @@ async function callerPrincipal(root, sid, { teams = false, recordedAgent = null
303
309
  return !scope.offline && scope.principal ? `person:${scope.principal}` : null;
304
310
  }
305
311
 
306
- /** C4: a requester named by an outside id; a proven link makes them the card's sponsor, anything less is recorded unverified. */
307
- async function requestedSponsor(args) {
308
- if (args.sponsor) return { sponsor: args.sponsor, sponsorEvidence: args.request ? { request: args.request, by: principalOf() } : null };
309
- 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) {
310
314
  let resolved = null;
311
- try { resolved = (await agentDoor('resolveIdentity', [args['requested-by']], { context: current().facts.SUPERCODE_CONTEXT })).data; } catch { /* no Teams: unverified */ }
312
- if (resolved?.classification === 'proven' && resolved.principal?.kind === 'user') {
313
- const link = resolved.links?.find((l) => l.classification === 'proven');
314
- 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() } };
315
- }
316
- 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 } };
317
318
  }
318
319
 
319
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] : []; };
@@ -321,7 +322,7 @@ const tagsOf = (card) => { const tags = Array.isArray(card.tags) ? card.tags : j
321
322
  const say = (args, human, value) => { current().out(args.json ? `${JSON.stringify(value, null, 2)}\n` : `${human}\n`); };
322
323
 
323
324
  // Hermes's own output lines, so a reader of `hermes kanban list` reads this one
324
- const ICONS = { todo: '◻', ready: '▶', running: '●', scheduled: '⏱', blocked: '⊘', done: '✓', archived: '—' };
325
+ const ICONS = { todo: '◻', ready: '▶', running: '●', scheduled: '⏱', blocked: '⊘', paused: '⏸', done: '✓', archived: '—' };
325
326
  const ts = (t) => {
326
327
  if (!t) return '';
327
328
  const d = new Date(Number(t) * 1000);
@@ -329,16 +330,19 @@ const ts = (t) => {
329
330
  return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
330
331
  };
331
332
  // a card's status is the workflow's (triaging, reviewing, …); kanban.db stores the Hermes status it maps to
332
- const line = (t) => `${ICONS[t.status] ?? '?'} ${t.id} ${(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}` : ''}`;
333
- const cardDict = (t) => ({
334
- id: t.id, 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,
335
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,
336
339
  created_by: t.created_by, created_at: t.created_at, started_at: t.started_at, completed_at: t.completed_at, result: t.result,
337
340
  skills: t.skills ?? [], max_retries: t.max_retries, model_override: t.model_override, provider_override: t.provider_override,
338
341
  session_id: t.session_id, workflow_template_id: t.workflow_template_id, current_step_key: t.current_step_key,
339
342
  machine: t.machine ?? null, subtask_of: t.subtask_of ?? null, execution: t.execution ?? null, detached: t.detached ?? null,
340
343
  reviewer: t.reviewer ?? null,
341
- });
344
+ paused: t.paused, blocked_by: t.blocked_by,
345
+ }); };
342
346
 
343
347
  /** Hermes's durations: seconds, or 30s / 5m / 2h / 1d. */
344
348
  // a moment the caller names: an ISO time, or a duration from now (30m, 2h); epoch seconds, or null
@@ -366,6 +370,40 @@ const metadataOf = (v) => {
366
370
  return got;
367
371
  };
368
372
 
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.
378
+ const NO_CARD_WORDS = new Set(['create', 'new', 'swarm', 'list', 'ls', 'migrate', 'init', 'boards', 'usage', 'events', 'listeners', 'dispatch',
379
+ 'capacity', 'deliver', 'daemon', 'serve', 'gc', 'repair', 'dispatching', 'attach-rm', 'run-session', 'stats', 'assignees', 'diagnostics', 'diag',
380
+ 'watch', 'help', 'github-usage']);
381
+ // the verbs that act on every positional as a card (each loops over them all)
382
+ const ALL_CARD_WORDS = new Set(['complete', 'archive', 'request-state', 'request_state', 'set-goal', 'unblock', 'reopen', 'reopen-review']);
383
+ const CARD_FLAGS = ['ids', 'subtask-of', 'parent', 'on', 'for-card', 'task', 'rm'];
384
+ const CARD_NUMBER = /^(?:#|([A-Za-z][A-Za-z0-9]+)-)?([1-9]\d*)$/;
385
+ function resolveCardNumbers(verb, rest, args, withBoard, keyOf) {
386
+ const words = NO_CARD_WORDS.has(verb) ? 0 : ALL_CARD_WORDS.has(verb) ? rest.length : ['link', 'unlink'].includes(verb) ? 2 : 1;
387
+ const places = [];
388
+ for (let i = 0; i < Math.min(words, rest.length); i++) places.push([rest, i]);
389
+ for (const flag of CARD_FLAGS) {
390
+ if (Array.isArray(args[flag])) args[flag].forEach((_, i) => places.push([args[flag], i]));
391
+ else if (typeof args[flag] === 'string') places.push([args, flag]);
392
+ }
393
+ const numbered = places.filter(([holder, key]) => CARD_NUMBER.test(String(holder[key])));
394
+ if (!numbered.length) return;
395
+ const own = keyOf();
396
+ withBoard((b) => view(b, (db) => {
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;
403
+ }
404
+ }));
405
+ }
406
+
369
407
  /** One verb in full, in the call it runs in (call.mjs): a caller's in the owner, or the process's own here. */
370
408
  export async function runVerb(argv) {
371
409
  const args = parse(argv);
@@ -390,6 +428,9 @@ export async function runVerb(argv) {
390
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; };
391
429
  const withBoard = (fn, create = false) => { const b = open(create); try { return fn(b); } finally { b.close(); } };
392
430
  const withBoardAsync = async (fn, create = false) => { const b = open(create); try { return await fn(b); } finally { b.close(); } };
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);
393
434
  const id = rest[0];
394
435
  const need = (n, what) => { if (rest.length < n) throw new Error(`${verb} needs ${what}`); };
395
436
  const ids = (first = rest) => [...first, ...(args.ids ?? [])];
@@ -516,19 +557,19 @@ export async function runVerb(argv) {
516
557
  let workspace = args.workspace;
517
558
  if ((workspace === 'dir' || workspace === 'worktree') && meta.default_workdir) workspace = `${workspace}:${meta.default_workdir}`;
518
559
  // --goal / --no-goal are the create's words; without either, the workflow's initial decides
519
- // a person filing for themselves sponsors it; an agent filing a person's request names them (proven) or records them unverified
520
- 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) : {};
521
562
  const created = await withBoardAsync(async (b) => engine.createCard(b, await engine.prepareCard(b, {
522
563
  title: rest.join(' '), body: textFrom(args['body-file'], args.body), assignee: args.assignee === 'none' ? null : args.assignee, machine: args.machine,
523
564
  reviewer: args.reviewer ?? null,
524
- 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,
525
566
  triage: args.triage, idempotencyKey: args['idempotency-key'], maxRuntime: duration(args['max-runtime']),
526
567
  maxRetries, skills: args.skill, model: args.model, provider: args.provider, createdBy: args['created-by'] ?? null,
527
568
  noStart: Boolean(args['no-start']), adopt: args.adopt ?? null,
528
569
  every: args.every ?? null, sessionId: callerSession(), subtaskOf: args['subtask-of'] ?? null, initialStatus: args['initial-status'] ?? null,
529
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,
530
571
  tags: [...(viewTag ? [viewTag] : []), ...(args.tag ?? [])],
531
- ...sponsorship,
572
+ ...requested,
532
573
  })), true);
533
574
  await withBoardAsync(b=>engine.runEffects(b,created.effects??[]));
534
575
  let answer = { id: created.id, existing: created.existing };
@@ -544,9 +585,9 @@ export async function runVerb(argv) {
544
585
  if (address) withBoard((b) => engine.subscribe(b, created.id, address, 'creator'));
545
586
  const task = withBoard((b) => view(b, (db) => engine.getTask(db, created.id)));
546
587
  const recipients = withBoard(b => engine.subscriptions(b.db, created.id)).filter(s => s.platform === 'supercode').map(s => s.chat_id);
547
- 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; }
548
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}` : '';
549
- say(args, `${created.existing ? 'Existing' : 'Created'} ${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);
550
591
  return answer.failed ? 1 : 0;
551
592
  }
552
593
 
@@ -565,7 +606,7 @@ export async function runVerb(argv) {
565
606
 
566
607
  case 'list': case 'ls': {
567
608
  const assignee = args.assignee ?? (args.mine ? callerName() : null);
568
- 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,
569
610
  tenant: args.tenant, session: args.session, workflowTemplateId: args['workflow-template-id'], stepKey: args['step-key'], tags: [...(viewTag ? [viewTag] : []), ...(args.tag ?? [])] }));
570
611
  const sorters = {
571
612
  assignee: (a, b) => String(a.assignee ?? '').localeCompare(String(b.assignee ?? '')), created: (a, b) => a.created_at - b.created_at,
@@ -575,14 +616,15 @@ export async function runVerb(argv) {
575
616
  };
576
617
  if (args.sort) { if (!sorters[args.sort]) throw new Error(`--sort is one of ${Object.keys(sorters).join(', ')}`); cards.sort(sorters[args.sort]); }
577
618
  if (args.json) {
578
- 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);
579
621
  return 0;
580
622
  }
581
623
  const others = boardsOf(root, { includeArchived: false }).length - 1;
582
624
  if (others > 0) current().out(`Board: ${slug} (${others} other board${others === 1 ? '' : 's'} — \`supercode workflow boards list\`)\n\n`);
583
625
  if (!cards.length) current().out('(no matching tasks)\n');
584
626
  for (let offset = 0; offset < cards.length; offset += 64) {
585
- 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`);
586
628
  await new Promise(resolve => setImmediate(resolve));
587
629
  }
588
630
  return 0;
@@ -592,11 +634,11 @@ export async function runVerb(argv) {
592
634
  need(1, 'a card id');
593
635
  const { settings } = await laneState(root).catch(() => ({ settings: boardSettings({}) }));
594
636
  const card = withBoard((b) => {
595
- const got = engine.showCard(b.db, id);
637
+ const got = engine.showCard(b.db, id, stateOptions(root));
596
638
  got.runs = engine.runs(b.db, id, { stateType: args['state-type'] ?? null, stateName: args['state-name'] ?? null });
597
639
  got.task.execution = executionOf(engine.currentRun(b.db, id));
598
640
  got.runs = got.runs.map(run => ({ ...run, execution: executionOf(run) }));
599
- 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 });
600
642
  got.stage = statusOf(b.db, got.task);
601
643
  const entry = workflowFor(root);
602
644
  // A workflow may explicitly replace the built-in time field. Keep its
@@ -606,20 +648,24 @@ export async function runVerb(argv) {
606
648
  : timeOf(b.db, got.task);
607
649
  return got;
608
650
  });
609
- const t = card.task;
651
+ const t = cardState({ ...card.task, stage: card.stage }, stateOptions(root));
610
652
  const latest = [...card.runs].reverse().find((r) => r.summary)?.summary ?? null;
611
653
  if (args.json) {
612
- 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 });
613
655
  return 0;
614
656
  }
615
- const out = [`Task ${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 || '-'}`];
657
+ const labels = withBoard((b) => view(b, (db) => engine.numbersOf(db, [t.subtask_of, ...card.parents.map((p) => p.id), ...card.children].filter(Boolean))));
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)}`);
616
662
  if (t.machine) out.push(` machine: ${t.machine}`);
617
663
  if (t.reviewer) out.push(` reviewer: ${t.reviewer} (card override)`);
618
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)`);
619
665
  // where the card's time went: each role's runs, and the rest waiting
620
666
  const mins = (sec) => `${Math.round(sec / 60)}m`;
621
667
  out.push(` time: ${Object.entries(card.time).filter(([, v]) => v > 0).map(([k, v]) => `${k} ${mins(v)}`).join(', ') || '-'}`);
622
- if (t.subtask_of) out.push(` subtask of: ${t.subtask_of}`);
668
+ if (t.subtask_of) out.push(` subtask of: ${label(t.subtask_of)}`);
623
669
  if (t.tenant) out.push(` tenant: ${t.tenant}`);
624
670
  out.push(` workspace: ${t.workspace_kind}${t.workspace_anchor ? `:${t.workspace_anchor}` : ''}`);
625
671
  if (t.workspace_path) out.push(` resolved: ${t.workspace_path}`);
@@ -630,27 +676,28 @@ export async function runVerb(argv) {
630
676
  out.push(` created: ${ts(t.created_at)} by ${t.created_by || '-'}`);
631
677
  const own = withBoard((b) => engine.getTask(b.db, t.id));
632
678
  if (own?.tags?.length) out.push(` tags: ${own.tags.join(', ')}`);
633
- if (own?.sponsor) out.push(` sponsor: ${own.sponsor}${own.sponsor_evidence?.observation ? ` (requested by ${own.sponsor_evidence.observation}, proven)` : ''}`);
634
- 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)'}`);
635
680
  if (card.diagnostics.length) {
636
681
  out.push('', ` Diagnostics (${card.diagnostics.length}):`);
637
- 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}`); }
638
683
  }
639
684
  if (t.started_at) out.push(` started: ${ts(t.started_at)}`);
640
685
  if (t.completed_at) out.push(` completed: ${ts(t.completed_at)}`);
641
- if (card.parents.length) out.push(` parents: ${card.parents.map((p) => p.id).join(', ')}`);
642
- if (card.children.length) out.push(` children: ${card.children.join(', ')}`);
643
- if (card.subtasks.length) out.push(` subtasks: ${card.subtasks.map((s) => `${s.id} (${s.status})`).join(', ')}`);
686
+ if (card.parents.length) out.push(` parents: ${card.parents.map((p) => label(p.id)).join(', ')}`);
687
+ if (card.children.length) out.push(` children: ${card.children.map(label).join(', ')}`);
688
+ if (card.subtasks.length) out.push(` subtasks: ${card.subtasks.map((s) => `${engine.cardLabel(boardKeyNow, s.number, s.id)} (${s.status})`).join(', ')}`);
644
689
  const side = withBoard((b) => sideSessions(b.db, t.id));
645
690
  if (side.length) out.push(` side sessions: ${side.map((s) => s.address).join(', ')}`);
646
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(', ')}`); }
647
694
  if (t.result) out.push('', 'Result:', t.result);
648
695
  else if (latest) out.push('', 'Latest summary:', latest);
649
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}`); }
650
697
  // the card's history, without the board's own bookkeeping (a claim, a session kept or started, a goal ask, a
651
698
  // message queued); `--all` shows every event, `--json` always does
652
699
  const PLUMBING = new Set(['claimed', 'session_started', 'session_resumed', 'session_adopted', 'agent_session_queued', 'session_replaced', 'session_closed',
653
- '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',
654
701
  'claim_extended', 'reclaimed', 'reconciled', 'status']);
655
702
  const history = args.all ? card.events : card.events.filter((e) => !PLUMBING.has(e.kind));
656
703
  if (history.length) {
@@ -672,20 +719,16 @@ export async function runVerb(argv) {
672
719
  return 0;
673
720
  }
674
721
 
675
- // 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
676
723
  case 'usage': {
677
724
  const byAgent = agentUsage(root), context = current().facts.SUPERCODE_CONTEXT;
678
- 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; } };
679
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; };
680
726
  // a harness that keeps no price records tokens only: its cost reads as unrecorded, never as zero
681
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('; ');
682
728
  const agents = [...byAgent.keys()].filter((a) => !args.agent || a === args.agent);
683
- 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)) }))))
684
- .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)) }));
685
730
  const total = sum(rows.flatMap((row) => byAgent.get(row.agent)));
686
- 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))) }));
687
- say(args, rows.length ? [...rows.map((r) => `${r.agent}${r.sponsor ? ` (sponsor ${r.sponsor})` : ''}: ${r.sessions} session(s) ${line(r.by_model)}`),
688
- ...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 });
689
732
  return 0;
690
733
  }
691
734
 
@@ -785,7 +828,7 @@ export async function runVerb(argv) {
785
828
  need(args.file !== undefined ? 1 : 2, 'a card id and the text (or --file PATH, - for stdin)');
786
829
  let text = textFrom(args.file, rest.slice(1).join(' '));
787
830
  if (args['max-len']) text = text.slice(0, Number(args['max-len']));
788
- 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 }));
789
832
  say(args, `Comment added to ${id}`, { ok: true, comment: n }); return 0;
790
833
  }
791
834
  case 'attach': {
@@ -839,7 +882,7 @@ export async function runVerb(argv) {
839
882
  need(1, 'a card id');
840
883
  const reason = rest.slice(1).join(' ') || null;
841
884
  const results = [];
842
- 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]);
843
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;
844
887
  }
845
888
  case 'schedule': {
@@ -859,7 +902,7 @@ export async function runVerb(argv) {
859
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;
860
903
  case 'request-changes': {
861
904
  need(2, 'a card id and the reason');
862
- // 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)
863
906
  const findings = [].concat(args.finding ?? []).map((f) => String(f).trim()).filter(Boolean);
864
907
  const filed = [];
865
908
  for (const finding of findings) filed.push((await withBoardAsync((b) => engine.createCard(b, { title: finding, subtaskOf: id }))).id);
@@ -885,7 +928,7 @@ export async function runVerb(argv) {
885
928
  const targets = [id, ...(args.ids ?? [])];
886
929
  if (args['dry-run']) {
887
930
  const verdicts = withBoard((b) => targets.map((t) => {
888
- const card = engine.showCard(b.db, t);
931
+ const card = engine.showCard(b.db, t, stateOptions(root));
889
932
  const open = card.parents.filter((p) => !['done', 'archived'].includes(p.status)).map((p) => p.id);
890
933
  const ok = ['todo', 'blocked'].includes(card.task.status) && (!open.length || args.force);
891
934
  return { id: t, ok, reason: ok ? null : !['todo', 'blocked'].includes(card.task.status) ? `${t} is ${card.task.status}` : `parents not done: ${open.join(', ')}` };
@@ -915,12 +958,12 @@ export async function runVerb(argv) {
915
958
 
916
959
  case 'specify': case 'decompose': {
917
960
  const { lanes, settings } = await laneState(root);
918
- 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];
919
962
  if (!targets[0]) throw new Error(`${verb} needs a card id, or --all`);
920
963
  const results = [];
921
964
  for (const t of targets) {
922
- if (verb === 'specify' && (args.title !== undefined || args.body !== undefined || args['body-file'] !== undefined || args.workspace !== undefined || args.branch !== undefined || args.machine !== undefined)) {
923
- 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() }));
924
967
  results.push({ id: t, specified: true, status: moved.to });
925
968
  } else if (verb === 'decompose' && args.child?.length) {
926
969
  const children = args.child.map((c) => { const [title, assignee] = c.split(/@(?=[^@]*$)/); return { title: title.trim(), assignee: assignee?.trim() || undefined }; });
@@ -939,11 +982,11 @@ export async function runVerb(argv) {
939
982
  const fleet = await observeFleet(root);
940
983
  const publishing = await boardPublishing(root);
941
984
  const [expiredMail, waitingMail] = await Promise.all([await mailExpiries(), await mailWaiting()]);
942
- const order = ['warning', 'error', 'critical'];
985
+ const order = ['info', 'warning', 'error', 'critical'];
943
986
  const found = withBoard((b) => {
944
- 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));
945
988
  return [...(args.task ? [] : engine.diagnosePublishing(publishing).map((d) => ({ task: '(board)', title_of_task: null, ...d }))),
946
- ...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 })))]
947
990
  .filter((d) => !args.severity || order.indexOf(d.severity) >= order.indexOf(args.severity));
948
991
  });
949
992
  if (args.json) { say(args, '', found); return 0; }
@@ -951,7 +994,7 @@ export async function runVerb(argv) {
951
994
  return 0;
952
995
  }
953
996
  case 'stats': {
954
- const s = withBoard((b) => engine.stats(b.db));
997
+ const s = withBoard((b) => engine.stats(b.db, stateOptions(root)));
955
998
  if (args.json) { say(args, '', s); return 0; }
956
999
  const fmt = (o) => Object.entries(o).sort().map(([k, v]) => `${k}=${v}`).join(', ') || '(none)';
957
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`);
@@ -959,7 +1002,7 @@ export async function runVerb(argv) {
959
1002
  }
960
1003
  case 'assignees': {
961
1004
  const { profiles, lanes } = await laneState(root);
962
- const c = withBoard((b) => engine.stats(b.db).by_assignee);
1005
+ const c = withBoard((b) => engine.stats(b.db, stateOptions(root)).by_assignee);
963
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] ?? {} }));
964
1007
  if (args.json) { say(args, '', data); return 0; }
965
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`);
@@ -1063,16 +1106,24 @@ export async function runVerb(argv) {
1063
1106
  const reports = await plan({ root, lanes, settings });
1064
1107
  const out = { capacity: reports.capacity, would: reports.flatMap((r) => r.would.map((w) => w.id)), waiting: reports.flatMap((r) => r.queued.map(([id, why]) => ({ id, why }))) };
1065
1108
  if (args.json) { say(args, '', out); return 0; }
1109
+ // each card as a person reads it, by the number its own board gave it
1110
+ const shown = [...out.would, ...out.waiting.map((w) => w.id), ...out.capacity.flatMap((cap) => cap.groups.flatMap((g) => g.cards))];
1111
+ const labels = new Map();
1112
+ for (const board of listBoards(root)) {
1113
+ const b = openBoard(root, board);
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(); }
1115
+ }
1116
+ const label = (cardId) => labels.get(cardId) ?? cardId;
1066
1117
  const lines = [];
1067
1118
  for (const cap of out.capacity) {
1068
1119
  const filter = Object.entries(cap.where ?? {}).map(([k, v]) => `${k} ${[].concat(v).join('|')}`).join(', ');
1069
1120
  const except = Object.entries(cap.unless ?? {}).map(([k, v]) => `${k} ${[].concat(v).join('|')}`).join(', ');
1070
1121
  lines.push(`cap "${cap.name}": ${cap.counts} per ${cap.scope}${filter ? `, where ${filter}` : ''}${except ? `, unless ${except}` : ''}, max ${cap.max}`);
1071
1122
  if (!cap.groups.length) lines.push(' (nothing counted)');
1072
- for (const g of cap.groups) lines.push(` ${cap.scope === 'global' ? 'home' : g.group ?? '(none)'}: ${g.count} of ${g.max ?? 'no max'} ${[...new Set(g.cards)].join(' ')}`);
1123
+ for (const g of cap.groups) lines.push(` ${cap.scope === 'global' ? 'home' : g.group ?? '(none)'}: ${g.count} of ${g.max ?? 'no max'} ${[...new Set(g.cards)].map(label).join(', ')}`);
1073
1124
  }
1074
- lines.push(out.would.length ? `would start: ${out.would.join(' ')}` : 'would start: nothing');
1075
- for (const w of out.waiting) lines.push(`${w.id} ${w.why}`);
1125
+ lines.push(out.would.length ? `would start: ${out.would.map(label).join(', ')}` : 'would start: nothing');
1126
+ for (const w of out.waiting) lines.push(`${label(w.id)} ${w.why}`);
1076
1127
  current().out(`${lines.join('\n')}\n`);
1077
1128
  return 0;
1078
1129
  }
@@ -1356,6 +1407,19 @@ function scrub(db) {
1356
1407
  }
1357
1408
  }
1358
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
+
1359
1423
  const rowCounts = (db) => Object.fromEntries(TABLES.map((t) => [t, Number(db.prepare(`SELECT COUNT(*) AS n FROM ${t}`).get().n)]));
1360
1424
 
1361
1425
  function copyTree(from, to) {
@@ -1375,12 +1439,15 @@ function exportBoard(root, slug, out, { attachments, logs }) {
1375
1439
  mkdirSync(base, { recursive: true });
1376
1440
  try {
1377
1441
  const b = openBoard(root, slug);
1378
- 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(); }
1379
1446
  const snap = openBoard(base, DEFAULT_BOARD);
1380
1447
  let counts;
1381
- 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(); }
1382
1449
  const { db_path: _, backing: ____, default_workdir: __, project_id: ___, ...meta } = boardMeta(root, slug);
1383
- 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`);
1384
1451
  const attachmentsDir = slug === DEFAULT_BOARD ? join(root, 'kanban', 'attachments') : join(boardDir(root, slug), 'attachments');
1385
1452
  const logsFrom = logsDir(root, slug);
1386
1453
  const files = attachments ? copyTree(attachmentsDir, join(base, 'attachments')) : 0;
@@ -1412,11 +1479,12 @@ function importBoard(root, archive, as) {
1412
1479
  const requested = normalSlug(as ?? manifest.board ?? tops[0]);
1413
1480
  let slug = requested === DEFAULT_BOARD ? `${requested}-imported` : requested;
1414
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 });
1415
1484
  const dest = boardDir(root, slug);
1416
1485
  mkdirSync(dest, { recursive: true });
1417
1486
  renameSync(join(extracted, 'kanban.db'), join(dest, 'kanban.db'));
1418
1487
  for (const tree of ['attachments', 'logs']) if (existsSync(join(extracted, tree))) renameSync(join(extracted, tree), join(dest, tree));
1419
- const staged = json(existsSync(join(extracted, 'board.json')) ? readFileSync(join(extracted, 'board.json'), 'utf8') : null, {}) ?? {};
1420
1488
  const name = staged.name || manifest.board_name || slug;
1421
1489
  writeMeta(root, slug, { name, description: staged.description ?? '', icon: staged.icon ?? '', color: staged.color ?? '', archived: false });
1422
1490
  const warnings = [];
@@ -1511,7 +1579,8 @@ const READS = new Set(['list', 'ls', 'show', 'context', 'brief', 'runs', 'log',
1511
1579
 
1512
1580
  /** The main session of each agent the workflow names a manager (`sc:<machine>:agent:<name>`), asked of this machine's
1513
1581
  * daemon, which keeps agents' records (mail step 1c), once per call before its verb runs: a manager named by its agent
1514
- * 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. */
1515
1584
  async function readAgentMains(root) {
1516
1585
  // A lookup belongs to this call, including a refusal. A startup miss must not become the resident owner's
1517
1586
  // permanent answer, and a replacement main must revoke the old main's manager role on the next call.
@@ -1520,10 +1589,18 @@ async function readAgentMains(root) {
1520
1589
  for (const address of managers) {
1521
1590
  const name = /^sc:[^:\s]+:agent:([^:\s]+)$/.exec(address)?.[1];
1522
1591
  if (!name || agentMains.has(address)) continue;
1523
- let main = null;
1524
- 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; }
1525
- catch { /* not known: the agent is no manager's session in this call */ }
1526
- 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
+ }
1527
1604
  }
1528
1605
  }
1529
1606
  function agentMain(address) { return current().agentMains.get(address) ?? null; }