@volter/supercode-orchestrator 0.5.107 → 0.5.109

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/activation.mjs CHANGED
@@ -176,6 +176,9 @@ export const MODEL_ARGUMENTS = Object.freeze({
176
176
  'claude-code': (model) => ['--model', model],
177
177
  codex: (model) => ['-c', `model=${JSON.stringify(model)}`],
178
178
  gemini: (model) => ['-m', model],
179
+ // The Runhuman harness runs a routine (runhuman-2 ADR 0038 §13): the worker's model names it, `routine:<org>/<name>`;
180
+ // with none, the routine is the `.runhuman` folder in the worker's cwd. The card role's agent is the run's agent.
181
+ runhuman: (model) => ['--routine', model],
179
182
  });
180
183
 
181
184
  /**
@@ -1010,9 +1010,15 @@ export async function closeRequested(board, report, { taskId = null, caller = nu
1010
1010
  (report.closed ??= []).push({ task_id: row.task_id, ...session, released: true });
1011
1011
  continue;
1012
1012
  }
1013
- // A close that failed for a reason another attempt cannot change is tried once and raised as an incident, not tried
1014
- // every round (each attempt is a process). A deferred close (an offline machine, a run that still owns the pane) waits.
1015
- if (session.close_failed_at && session.close_error && !/^close deferred/.test(session.close_error)) continue;
1013
+ // A final refusal stops further close attempts. Only an authorized observation of this exact pane may establish
1014
+ // absence: panes.list can be filtered or carry missing context keys. Unknown/invisible keeps the original failure.
1015
+ const finalFailure = Boolean(session.close_failed_at && session.close_error && !/^close deferred/.test(session.close_error));
1016
+ if (finalFailure) {
1017
+ if (!session.pane) continue;
1018
+ const r = await doorOn(session.machine ?? localMachine(), 'harness.v1.panes.presence', { pane: session.pane }, { timeout: 10_000, as: boardAddress(board.label) });
1019
+ const observed = r.code === 0 ? json(r.stdout.trim(), null) : null;
1020
+ if (observed?.pane !== session.pane || observed.present !== false) continue;
1021
+ }
1016
1022
  const active = view(board, (db) => db.prepare(`SELECT r.id, r.metadata FROM task_runs r WHERE r.ended_at IS NULL OR EXISTS (SELECT 1 FROM tasks t WHERE t.current_run_id = r.id)`).all());
1017
1023
  // Persist retries even for historical ends that did not have a close_session action. The close is claimed in the
1018
1024
  // same transaction, before the pane or process is touched: of two rounds (or a round and a verb) that read one close,
@@ -1039,7 +1045,7 @@ export async function closeRequested(board, report, { taskId = null, caller = nu
1039
1045
  if (row.ended_at) { const inTurn = await turnAtClose(session); if (inTurn) throw new Error(`close deferred: ${inTurn}`); }
1040
1046
  await stopSession(board, session);
1041
1047
  }
1042
- } else {
1048
+ } else if (!finalFailure) {
1043
1049
  if (!panes.has(machine)) {
1044
1050
  const r = await doorOn(machine, 'harness.v1.panes.list',{}, { timeout: 60_000, as: boardAddress(board.label) });
1045
1051
  const list = r.code === 0 ? json(r.stdout.trim(), null) : null;
@@ -1088,7 +1094,8 @@ export async function closeRequested(board, report, { taskId = null, caller = nu
1088
1094
  const subscribed = db.prepare('SELECT r.id, r.metadata FROM task_runs r WHERE r.ended_at IS NULL OR EXISTS (SELECT 1 FROM tasks t WHERE t.current_run_id = r.id)').all()
1089
1095
  .some((r) => addressOf(json(r.metadata, {})?.supercode) === address && r.id !== row.id);
1090
1096
  const unsubscribed = subscribed ? 0 : db.prepare("DELETE FROM kanban_notify_subs WHERE platform = 'supercode' AND chat_id = ? AND (task_id = ? OR json_extract(delivery_metadata, '$.role') = 'worker')").run(address, row.task_id).changes;
1091
- event(db, row.task_id, 'session_closed', { session_id: session.session_id, pane: session.pane, machine: session.machine, unsubscribed }, row.id);
1097
+ event(db, row.task_id, 'session_closed', { session_id: session.session_id, pane: session.pane, machine: session.machine, unsubscribed,
1098
+ ...(finalFailure ? { reason: 'authorized pane presence observed absence after an earlier close refusal' } : {}) }, row.id);
1092
1099
  }
1093
1100
  });
1094
1101
  if (error) report.undelivered.push([row.task_id, addressOf(session), error, 'close']);
package/kanban-worker.mjs CHANGED
@@ -19,6 +19,9 @@ import { pinnedSkills, taskProtocol } from './board/worker-prompt.mjs';
19
19
  import { mountOrchestratorMcp } from './mcp/mount.mjs';
20
20
  import { normalizeWorkerEvent, widensSandbox } from './loop.mjs';
21
21
  import { askBoardOwner, callerFacts, unansweredText } from '@volter/teams/board-owner';
22
+ // A card's own words are read from its board (a routine has no board tools); what the worker writes goes through the board's owner.
23
+ import { DEFAULT_BOARD, openBoard, view } from './board/store.mjs';
24
+ import { getTask } from './board/engine.mjs';
22
25
  import { permissionResponse } from './reducer.mjs';
23
26
  import { boardGuardArguments } from '@volter/teams/board-process-guard';
24
27
 
@@ -85,6 +88,8 @@ export async function runKanbanWorker(argv, env = process.env) {
85
88
 
86
89
  async function runTurn({ client, profile, harness, worker, route, root, vault, surfaceKey, runEnv, instructions, task, cwd, args, say }) {
87
90
  const descriptor = (await client.supportReport()).harnesses.find((h) => h.id === harness);
91
+ // A worker's folder that is gone (a home moved or swept) fails its launch as the harness program missing; say which.
92
+ if (!existsSync(cwd) || !statSync(cwd).isDirectory()) { say(`hermes-worker: the worker's folder ${cwd} does not exist (the profile's worker.cwd)`); return 2; }
88
93
  const base = descriptor?.runtime?.default_launch;
89
94
  if (!base) { say(`hermes-worker: ${harness} publishes no launch`); return 2; }
90
95
  const reasoning = args.reasoning && harness === 'codex' ? ['-c', `model_reasoning_effort=${JSON.stringify(args.reasoning)}`] : [];
@@ -98,11 +103,15 @@ async function runTurn({ client, profile, harness, worker, route, root, vault, s
98
103
  const policy = approvalPolicy(profile, harness);
99
104
  const runtime = await client.startManagedRuntime({ harness, cwd, launch: mount.launch, ...(mount.mcp_servers ? { mcp_servers: mount.mcp_servers } : {}), ...(policy ? { approval_policy: policy } : {}) });
100
105
  try { await recordSession(root, task, harness, runtime.handle.runtime_id); } catch (error) { say(`hermes-worker: recording the session on its run: ${error.message}`); }
101
- let text = '';
106
+ let text = '', last = '';
102
107
  // `agent.max_turns` for a worker that takes no cap at launch, held as the daemon holds it (loop.mjs `holdTurnLimit`)
103
108
  const counter = turnCounter(profile, harness);
104
- const outcome = await new Promise((done) => {
109
+ const outcome = await new Promise((resolve) => {
110
+ // Settled once: the runtime's close after the turn's final answer is not the worker ending.
111
+ let settled = false;
112
+ const done = (code) => { settled = true; resolve(code); };
105
113
  const on = (ev) => {
114
+ if (settled) return;
106
115
  if (counter?.count(ev)) {
107
116
  say(`hermes-worker: ${counter.cap + 1} model calls in one turn passes agent.max_turns ${counter.cap}; the turn is interrupted`);
108
117
  void Promise.resolve(runtime.interrupt?.()).catch((error) => say(`hermes-worker: interrupt failed: ${error.message}`));
@@ -110,7 +119,7 @@ async function runTurn({ client, profile, harness, worker, route, root, vault, s
110
119
  let w = normalizeWorkerEvent(ev);
111
120
  if (!w) return;
112
121
  if (w.kind === 'final' && counter) w = counter.settle(w);
113
- if (w.kind === 'text') text += w.block && text ? `\n\n${w.delta}` : w.delta;
122
+ if (w.kind === 'text') { text += w.block && text ? `\n\n${w.delta}` : w.delta; last = w.delta; }
114
123
  else if (w.kind === 'request') {
115
124
  // nobody attends a dispatched worker: an untrusted server's tool is denied; anything else is answered by Hermes's
116
125
  // approval rule in single-query mode, as the daemon's path asks it (loop.mjs): a safe command runs, a dangerous
@@ -137,13 +146,42 @@ async function runTurn({ client, profile, harness, worker, route, root, vault, s
137
146
  runtime.on('event', on);
138
147
  runtime.on('closed', on);
139
148
  runtime.on('runtimeError', on);
140
- runtime.sendInput(args.query).catch((error) => { say(`hermes-worker: ${error.message}`); done(1); });
149
+ runtime.sendInput(harness === 'runhuman' ? cardWords(root, task) ?? args.query : args.query).catch((error) => { say(`hermes-worker: ${error.message}`); done(1); });
141
150
  });
142
151
  if (text) say(text);
143
152
  await runtime.close().catch(() => {});
153
+ // A routine has no board tools: its run's answer (the turn's last line, its returns) completes the card it worked.
154
+ if (harness === 'runhuman' && outcome === 0 && task.HERMES_KANBAN_TASK) {
155
+ const answer = last.trim();
156
+ const done = await askBoardOwner(root, {
157
+ // As the card's worker, by its own board tool (its card, run and board name it), as a harness's board tools do.
158
+ tool: { name: 'kanban_complete', args: { summary: answer || 'The run ended with no answer.', ...(answer ? { result: answer } : {}) }, env: task },
159
+ board: task.HERMES_KANBAN_BOARD || null, caller: null, session: null, cwd: root, stdin: null,
160
+ facts: { ...callerFacts(), SUPERCODE_CONTEXT: process.env.SUPERCODE_CONTEXT || 'local' },
161
+ }).catch((error) => ({ code: 1, stderr: error.message }));
162
+ // The tool's own refusal comes back as its JSON (an `error`), not as a failed request.
163
+ const refused = (() => { try { return JSON.parse(done.stdout ?? '{}')?.error; } catch { return undefined; } })();
164
+ if (refused && !done.code) Object.assign(done, { code: 1, stderr: String(refused) });
165
+ if (done.unanswered === true || done.code) { say(`hermes-worker: completing ${task.HERMES_KANBAN_TASK} with the run's answer: ${done.unanswered === true ? unansweredText(done, 'the card\'s completion').trim() : (done.stderr || 'the board owner did not complete it').trim()}`); return 1; }
166
+ }
144
167
  return outcome;
145
168
  }
146
169
 
170
+ /**
171
+ * A Runhuman worker's first input: the card's own words (its title and body), which a routine takes as its variables
172
+ * (a ```runhuman-vars block, its `ask`, its links). A routine has no board tools to read the card by its id.
173
+ */
174
+ function cardWords(root, task) {
175
+ if (!task.HERMES_KANBAN_TASK) return null;
176
+ const board = openBoard(root, task.HERMES_KANBAN_BOARD || DEFAULT_BOARD);
177
+ try {
178
+ const card = view(board, (db) => getTask(db, task.HERMES_KANBAN_TASK));
179
+ return card ? [card.title, card.body].filter(Boolean).join('\n\n') : null;
180
+ } finally {
181
+ board.close();
182
+ }
183
+ }
184
+
147
185
  /**
148
186
  * The worker's session on its run, where supercode keeps it (`metadata.supercode.session_id`, beside the pid the
149
187
  * dispatcher recorded): the harness's own transcript, found and resumed by supercode like any session. Written by the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-orchestrator",
3
- "version": "0.5.107",
3
+ "version": "0.5.109",
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,7 +25,7 @@
25
25
  },
26
26
  "dependencies": {
27
27
  "@volter/supercode-harness-sdk": "^0.3.60",
28
- "@volter/teams": "0.2.44",
28
+ "@volter/teams": "0.2.45",
29
29
  "@volter/ztrack": "^2.4.23",
30
30
  "yaml": "^2.9.0"
31
31
  },