@volter/supercode-orchestrator 0.5.8 → 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 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
- if (observed.state !== 'OPEN' || observed.isCrossRepository) throw new Error(`Source review needs an open PR from its repository (${url}).`);
328
- if (observed.isDraft) throw new Error(`${url} is a draft; its review starts when it is marked ready for review.`);
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
- if (branch === mainBranch || observed.baseRefName !== mainBranch) throw new Error(`${url} must propose its worktree branch to the repository main branch.`);
335
- if (stage === 'request_review' && path) {
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, branch) };
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: [] };
@@ -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 { boardSourceName, recordCardPublications } from './published.mjs';
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??error.message}`);
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;
@@ -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
+ }
@@ -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
- record(ctx.db, ctx.id, 'workspace_changed', { from: { kind: task.workspace_kind, repository: prior?.workspace_anchor, branch: task.branch_name }, to: { kind: ws.kind, repository: ws.repository, branch: ws.branch } }, null);
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/supercode-teams/board-process-guard';
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.8",
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/supercode-teams": "0.3.7",
29
- "@volter/teams": "0.2.4",
28
+ "@volter/teams": "0.2.5",
30
29
  "@volter/ztrack": "^2.4.23",
31
30
  "yaml": "^2.9.0"
32
31
  },