@volter/supercode-orchestrator 0.5.7 → 0.5.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/board/ci.mjs +8 -4
- package/board/cli.mjs +4 -3
- package/board/engine.mjs +7 -4
- package/board/event-relay.mjs +5 -3
- package/board/published.mjs +29 -0
- package/board/workflow.mjs +3 -1
- package/kanban-worker.mjs +1 -1
- package/package.json +2 -3
package/board/ci.mjs
CHANGED
|
@@ -324,15 +324,19 @@ function verifyPullRequest(board, id, entry, own, url, supplied, stage) {
|
|
|
324
324
|
// with nothing uncommitted.
|
|
325
325
|
const observed = readPullRequest(url);
|
|
326
326
|
const path = originOf(own) === repository && run('git', ['branch', '--show-current'], own) === observed.headRefName ? own : branchCheckout(board, id, repository, observed.headRefName);
|
|
327
|
-
|
|
328
|
-
|
|
327
|
+
// A PR merged before its review (an outage fix landed first) is still reviewed: its head is fixed for good, so the
|
|
328
|
+
// verdict is recorded on that head, and nothing about a worktree or its base can move under it any more.
|
|
329
|
+
const merged = observed.state === 'MERGED';
|
|
330
|
+
if ((observed.state !== 'OPEN' && !merged) || observed.isCrossRepository) throw new Error(`Source review needs an open or merged PR from its repository (${url}).`);
|
|
331
|
+
if (!merged && observed.isDraft) throw new Error(`${url} is a draft; its review starts when it is marked ready for review.`);
|
|
329
332
|
const head = supplied ?? observed.headRefOid;
|
|
330
333
|
if (!/^[a-f0-9]{40}$/.test(head)) throw new Error('Name the full PR head commit with --pr-head (reviewer: --reviewed-sha).');
|
|
331
334
|
if (observed.headRefOid !== head) throw new Error(`The head of ${url} changed to ${observed.headRefOid}; request review of that head before recording a verdict.`);
|
|
332
335
|
const branch = observed.headRefName;
|
|
333
336
|
const mainBranch = entry.params.completion_ci?.main_branches?.[repository] ?? 'main';
|
|
334
|
-
|
|
335
|
-
if (
|
|
337
|
+
// A merged hotfix may have landed on another base (prod); a PR from the main branch itself is a release, never arc work.
|
|
338
|
+
if (branch === mainBranch || (!merged && observed.baseRefName !== mainBranch)) throw new Error(`${url} must propose its worktree branch to the repository main branch.`);
|
|
339
|
+
if (stage === 'request_review' && path && !merged) {
|
|
336
340
|
run('git', ['check-ref-format', '--branch', branch], path);
|
|
337
341
|
if (run('git', ['rev-parse', 'HEAD'], path) !== head) throw new Error(`The head named for ${url} must be its worktree head.`);
|
|
338
342
|
const uncommitted = run('git', ['status', '--porcelain', '--untracked-files=normal'], path).split('\n').filter(Boolean);
|
package/board/cli.mjs
CHANGED
|
@@ -42,7 +42,7 @@ Hermes Kanban's verbs, with their flags:
|
|
|
42
42
|
request-state <id> --state block|pause|done|cancelled --reason <why> [--on <card>|--waiting-on <message>] [--evidence <pointers>]
|
|
43
43
|
unblock <id>... --on <blocker> --reason <what arrived> request-review <id> (--pr URL --pr-head SHA)... [--summary] [--reviewer] [--force] request-changes <id> <reason> [--finding <finding>]... reopen-review <id>... reopen <id>... [--reason]
|
|
44
44
|
promote <id> [reason] [--ids ...] [--force] [--dry-run] archive <id>... [--rm <id>...]
|
|
45
|
-
specify <id> [--title --body] [--workspace scratch|worktree:<repository>|dir:<repository>] [--branch NAME] [--all]
|
|
45
|
+
specify <id> [--title --body] [--workspace scratch|worktree:<repository>|dir:<repository>] [--branch NAME] [--machine M|none] [--all]
|
|
46
46
|
decompose <id> [--child "<title>[@<profile>]"]... [--all]
|
|
47
47
|
diagnostics [--task] [--severity] stats assignees log <id> [--tail N] tail <id> watch [--assignee] [--kinds]
|
|
48
48
|
notify-subscribe <id> --platform P --chat-id C [--thread-id] notify-list [<id>] notify-unsubscribe <id> --platform P --chat-id C
|
|
@@ -76,6 +76,7 @@ supercode's own:
|
|
|
76
76
|
goto <id> <status> [reason] [--<field> value]: the event the workflow says takes the card to that status, sent.
|
|
77
77
|
|
|
78
78
|
specify <id> --workspace dir:<repository> --branch NAME: the manager retargets a stopped card without replacing its history.
|
|
79
|
+
specify <id> --workspace … --machine M: declares the workspace on M and moves the card there, never asking the machine it leaves.
|
|
79
80
|
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.
|
|
80
81
|
request-state <id> --state done (--reviewed-sha SHA)...: record the verdict on every requested PR head.
|
|
81
82
|
request-state <id>... --state STATE --reason REASON: request several decisions in one retained manager mail.
|
|
@@ -763,8 +764,8 @@ export async function runWorkflow(argv) {
|
|
|
763
764
|
if (!targets[0]) throw new Error(`${verb} needs a card id, or --all`);
|
|
764
765
|
const results = [];
|
|
765
766
|
for (const t of targets) {
|
|
766
|
-
if (verb === 'specify' && (args.title !== undefined || args.body !== undefined || args.workspace !== undefined || args.branch !== undefined)) {
|
|
767
|
-
const moved = await withBoardAsync((b) => engine.specify(b, t, { title: args.title ?? null, body: args.body ?? null, workspace: args.workspace ?? null, branch: args.branch ?? null, author: args.author ?? callerAddress() ?? callerName() }));
|
|
767
|
+
if (verb === 'specify' && (args.title !== undefined || args.body !== undefined || args.workspace !== undefined || args.branch !== undefined || args.machine !== undefined)) {
|
|
768
|
+
const moved = await withBoardAsync((b) => engine.specify(b, t, { title: args.title ?? null, body: args.body ?? null, workspace: args.workspace ?? null, branch: args.branch ?? null, machine: args.machine ?? null, author: args.author ?? callerAddress() ?? callerName() }));
|
|
768
769
|
results.push({ id: t, specified: true, status: moved.to });
|
|
769
770
|
} else if (verb === 'decompose' && args.child?.length) {
|
|
770
771
|
const children = args.child.map((c) => { const [title, assignee] = c.split(/@(?=[^@]*$)/); return { title: title.trim(), assignee: assignee?.trim() || undefined }; });
|
package/board/engine.mjs
CHANGED
|
@@ -561,18 +561,21 @@ export const schedule = (board, id, reason = null, { roles } = {}) => act(board,
|
|
|
561
561
|
export const promote = (board, id, { actor = 'user', reason = null, force = false, roles } = {}) => act(board, id, 'promote', { roles, payload: { actor, reason, force } });
|
|
562
562
|
export const reclaim = (board, id, reason = null, { roles } = {}) => act(board, id, 'reclaim', { roles, payload: { reason } });
|
|
563
563
|
export const archive = (board, id, { roles } = {}) => act(board, id, 'archive', { roles });
|
|
564
|
-
export async function specify(board, id, { title = null, body = null, workspace = null, branch = null, author = 'user', roles } = {}) {
|
|
564
|
+
export async function specify(board, id, { title = null, body = null, workspace = null, branch = null, machine = null, author = 'user', roles } = {}) {
|
|
565
565
|
let declaration = null;
|
|
566
|
-
if (workspace != null || branch != null) {
|
|
566
|
+
if (workspace != null || branch != null || machine != null) {
|
|
567
567
|
const task = getTask(board.db, id);
|
|
568
|
+
// `machine`: the workspace is declared on the machine the card moves to, never asked of the one it leaves (which may
|
|
569
|
+
// be offline or run a daemon too old to answer), and the card moves with the declaration in one act.
|
|
570
|
+
const target = machine == null ? task.machine : machine === 'none' ? null : machine;
|
|
568
571
|
const word = workspace ?? `${task.workspace_kind}${task.workspace_anchor || task.workspace_path ? `:${task.workspace_anchor ?? task.workspace_path}` : ''}`;
|
|
569
572
|
const ws = parseWorkspace(word);
|
|
570
573
|
if (task.current_run_id) throw new Error('end the current run before changing its workspace');
|
|
571
574
|
if (branch != null && ws.kind === 'scratch') throw new Error('a scratch workspace has no repository branch');
|
|
572
|
-
declaration = { kind: ws.kind, ...declareWorkspace(ws.kind, ws.path, task.machine
|
|
575
|
+
declaration = { kind: ws.kind, ...declareWorkspace(ws.kind, ws.path, target, branch), ...(machine != null && target !== task.machine ? { machine: target } : {}) };
|
|
573
576
|
if (declaration.repository === task.workspace_anchor) declaration.branch ??= task.branch_name;
|
|
574
577
|
if (declaration.kind === 'worktree') declaration.branch ??= `wt/${id}`;
|
|
575
|
-
if (declaration.kind === 'dir') declaration.path = checkWorkspace(board, { ...task, workspace_kind: declaration.kind, workspace_anchor: declaration.repository, workspace_path: declaration.path, branch_name: declaration.branch }).path;
|
|
578
|
+
if (declaration.kind === 'dir') declaration.path = checkWorkspace(board, { ...task, machine: target, workspace_kind: declaration.kind, workspace_anchor: declaration.repository, workspace_path: declaration.path, branch_name: declaration.branch }).path;
|
|
576
579
|
}
|
|
577
580
|
// an arc's new task lines are filed as its subtasks once the specify itself is accepted
|
|
578
581
|
const steps = body != null && !one(board.db, 'SELECT subtask_of FROM supercode_cards WHERE task_id = ?', id)?.subtask_of ? bodyTasks(body) : { body, tasks: [] };
|
package/board/event-relay.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// App-owned source names/maps live here; the Teams core knows only a scoped event publisher.
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
3
|
import { eventClient } from '@volter/teams/event-client';
|
|
4
|
-
import {
|
|
4
|
+
import { EVENT_DATA_LIMIT } from '@volter/teams/event-limits';
|
|
5
|
+
import { boardSourceName, publishableCard, recordCardPublications } from './published.mjs';
|
|
5
6
|
import { listBoards, openBoard } from './store.mjs';
|
|
6
7
|
|
|
7
8
|
export function boardEventRelay({root,context,supercodeHome,log=()=>{}}) {
|
|
@@ -26,7 +27,8 @@ export function boardEventRelay({root,context,supercodeHome,log=()=>{}}) {
|
|
|
26
27
|
for(const row of rows){
|
|
27
28
|
if(closed)return;
|
|
28
29
|
const id=`event_${createHash('sha256').update(`${row.id}:${destination}`).digest('hex')}`;
|
|
29
|
-
await team.events.publish(source,{id,kind:row.kind,resource:{kind:'task',id:`${source}:${row.resource_id}`},revision:row.sequence,data:{board:slug,card:JSON.parse(row.snapshot)},actor:{session:row.actor?`session:${row.actor}`:null}},{idempotencyKey:id})
|
|
30
|
+
await team.events.publish(source,{id,kind:row.kind,resource:{kind:'task',id:`${source}:${row.resource_id}`},revision:row.sequence,data:{board:slug,card:publishableCard(JSON.parse(row.snapshot),slug,EVENT_DATA_LIMIT)},actor:{session:row.actor?`session:${row.actor}`:null}},{idempotencyKey:id})
|
|
31
|
+
.catch(error=>{throw Object.assign(error,{message:`${row.resource_id} ${row.kind} (sequence ${row.sequence}): ${error.message}`});});
|
|
30
32
|
board.tx(db=>db.prepare('INSERT OR IGNORE INTO supercode_publication_receipts VALUES (?,?)').run(row.id,destination));
|
|
31
33
|
}
|
|
32
34
|
}
|
|
@@ -36,7 +38,7 @@ export function boardEventRelay({root,context,supercodeHome,log=()=>{}}) {
|
|
|
36
38
|
const wake=(refresh=true)=>{
|
|
37
39
|
reconcile ||= refresh;dirty=true;if(work||closed)return work;
|
|
38
40
|
work=(async()=>{while(dirty&&!closed){dirty=false;await run();}})().catch(error=>{
|
|
39
|
-
log(`board event delivery pending: ${error.code
|
|
41
|
+
log(`board event delivery pending: ${error.code?`${error.code}: `:''}${error.message}`);
|
|
40
42
|
// Retry durable delivery, not a read of the board's current state.
|
|
41
43
|
retry=setTimeout(()=>wake(false),5000);retry.unref?.();
|
|
42
44
|
}).finally(()=>{work=null;});return work;
|
package/board/published.mjs
CHANGED
|
@@ -84,3 +84,32 @@ export function recordCardPublications(db,actor) {
|
|
|
84
84
|
return changed;
|
|
85
85
|
}
|
|
86
86
|
export const boardSourceName = (root,slug) => `board:${hash(realpathSync(root)).slice(0,24)}:${slug}`;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The card as one published event may carry it (`limit`: the bytes its `data`, `{ board, card }`, may serialize to).
|
|
90
|
+
* The board keeps every card whole; a card too large to send keeps its newest history: the oldest comments, attempts
|
|
91
|
+
* and reviews go first, then the body's tail, and `omitted` counts what the event leaves out.
|
|
92
|
+
*/
|
|
93
|
+
export function publishableCard(card,board,limit) {
|
|
94
|
+
const bytes=value=>Buffer.byteLength(JSON.stringify(value));
|
|
95
|
+
const size=value=>bytes({board,card:value});
|
|
96
|
+
if(size(card)<=limit)return card;
|
|
97
|
+
const omitted={comments:0,attempts:0,reviews:0,body_chars:0};
|
|
98
|
+
const fitted={...card,omitted};
|
|
99
|
+
// Counted once per item (its bytes and a separator), then checked whole: a card of megabytes fits in one pass.
|
|
100
|
+
let over=size(fitted)-limit;
|
|
101
|
+
for(const field of ['comments','attempts','reviews']){
|
|
102
|
+
const items=card[field]??[];let drop=0;
|
|
103
|
+
while(drop<items.length&&over>0){over-=bytes(items[drop])+1;drop+=1;}
|
|
104
|
+
fitted[field]=items.slice(drop);omitted[field]=drop;
|
|
105
|
+
}
|
|
106
|
+
for(const field of ['comments','attempts','reviews']){
|
|
107
|
+
while(fitted[field].length&&size(fitted)>limit){fitted[field]=fitted[field].slice(1);omitted[field]+=1;}
|
|
108
|
+
}
|
|
109
|
+
const body=String(card.body??'');
|
|
110
|
+
for(let keep=body.length;size(fitted)>limit&&keep>0;){
|
|
111
|
+
keep=Math.max(0,Math.min(keep-1,keep-(size(fitted)-limit)-64));
|
|
112
|
+
fitted.body=body.slice(0,keep);omitted.body_chars=body.length-keep;
|
|
113
|
+
}
|
|
114
|
+
return fitted;
|
|
115
|
+
}
|
package/board/workflow.mjs
CHANGED
|
@@ -807,10 +807,12 @@ const ACTIONS = {
|
|
|
807
807
|
if (task.current_run_id) throw new Error('end the current run before changing its workspace');
|
|
808
808
|
ACTIONS.forget_session(ctx, '*');
|
|
809
809
|
const prior = one(ctx.db, 'SELECT workspace_anchor FROM supercode_cards WHERE task_id=?', ctx.id);
|
|
810
|
-
|
|
810
|
+
const moves = Object.hasOwn(ws, 'machine');
|
|
811
|
+
record(ctx.db, ctx.id, 'workspace_changed', { from: { kind: task.workspace_kind, repository: prior?.workspace_anchor, branch: task.branch_name, ...(moves ? { machine: task.machine } : {}) }, to: { kind: ws.kind, repository: ws.repository, branch: ws.branch, ...(moves ? { machine: ws.machine } : {}) } }, null);
|
|
811
812
|
run(ctx.db, 'UPDATE tasks SET workspace_kind=?,workspace_path=?,branch_name=? WHERE id=?', ws.kind, ws.path ?? null, ws.branch ?? null, ctx.id);
|
|
812
813
|
cardField(ctx, 'workspace_anchor', ws.repository ?? null);
|
|
813
814
|
cardField(ctx, 'workspace_root', null);
|
|
815
|
+
if (moves) cardField(ctx, 'machine', ws.machine ?? null);
|
|
814
816
|
}
|
|
815
817
|
if (said?.title && String(said.title).trim()) run(ctx.db, 'UPDATE tasks SET title = ? WHERE id = ?', String(said.title).trim(), ctx.id);
|
|
816
818
|
if (said?.body !== undefined && said?.body !== null) run(ctx.db, 'UPDATE tasks SET body = ? WHERE id = ?', said.body, ctx.id);
|
package/kanban-worker.mjs
CHANGED
|
@@ -21,7 +21,7 @@ import { normalizeWorkerEvent, widensSandbox } from './loop.mjs';
|
|
|
21
21
|
import { DEFAULT_BOARD, json, openBoard, tx } from './board/store.mjs';
|
|
22
22
|
import { setRunSession } from './board/engine.mjs';
|
|
23
23
|
import { permissionResponse } from './reducer.mjs';
|
|
24
|
-
import { boardGuardArguments } from '@volter/
|
|
24
|
+
import { boardGuardArguments } from '@volter/teams/board-process-guard';
|
|
25
25
|
|
|
26
26
|
/** Hermes's worker argv, as `_default_spawn` builds it. */
|
|
27
27
|
export function parseHermesArgv(argv) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-orchestrator",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The orchestrator runtime over the Volter Harness ontology: one typed operational model whose folder is its serialization, read and written through the harness orchestration doors (docs/ORCHESTRATOR-IR.md)",
|
|
6
6
|
"exports": {
|
|
@@ -25,8 +25,7 @@
|
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@volter/supercode-harness-sdk": "^0.3.41",
|
|
28
|
-
"@volter/
|
|
29
|
-
"@volter/teams": "0.2.3",
|
|
28
|
+
"@volter/teams": "0.2.5",
|
|
30
29
|
"@volter/ztrack": "^2.4.23",
|
|
31
30
|
"yaml": "^2.9.0"
|
|
32
31
|
},
|