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