@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/README.md +30 -2
- package/agents.mjs +27 -12
- package/bin/connection-runner.mjs +12 -21
- package/bin/orchestrator.mjs +16 -3
- package/board/agent-declarations.mjs +3 -3
- package/board/args.mjs +2 -2
- package/board/card-state.mjs +29 -0
- package/board/ci.mjs +87 -29
- package/board/cli.mjs +160 -83
- package/board/codex-pane-inputs.mjs +44 -0
- package/board/dispatch.mjs +358 -85
- package/board/engine.mjs +84 -46
- package/board/event-stream.mjs +6 -1
- package/board/files.mjs +20 -4
- package/board/listing.mjs +15 -12
- package/board/machine-fleet.mjs +7 -5
- package/board/notices.mjs +5 -0
- package/board/published.mjs +119 -32
- package/board/readable-id.mjs +34 -0
- package/board/record-view.mjs +10 -1
- package/board/request-mail.mjs +9 -3
- package/board/store.mjs +124 -14
- package/board/tools.mjs +10 -8
- package/board/visibility.mjs +11 -0
- package/board/workflow.mjs +62 -18
- package/board/workspace.mjs +21 -9
- package/board/ztrack.mjs +203 -27
- package/codex-pane-resume.mjs +1 -0
- package/connection.mjs +95 -26
- package/index.d.ts +6 -2
- package/loop.mjs +51 -3
- package/mcp/bridge.mjs +4 -1
- package/mcp/hermes-kanban-tools.json +5 -1
- package/ontology.d.ts +7 -24
- package/orchestration.mjs +6 -3
- package/package.json +7 -6
- package/worker-route.mjs +31 -3
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] [--
|
|
129
|
-
Tokens each agent's ended card sessions recorded (the harness's own records), per model
|
|
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… --
|
|
134
|
-
Tags are ordered (the first is the tenant).
|
|
135
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
307
|
-
async function
|
|
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:
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
334
|
-
|
|
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
|
-
//
|
|
520
|
-
const
|
|
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
|
-
...
|
|
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
|
-
|
|
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
|
|
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?.
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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.
|
|
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
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
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; }
|