@volter/supercode-orchestrator 0.4.25 → 0.4.26

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/cli.mjs CHANGED
@@ -20,10 +20,10 @@ Hermes Kanban's verbs, with their flags:
20
20
  init · boards list|create|rm|switch|show|rename|set-default-workdir|export|import
21
21
  create <title> [--body] [--assignee] [--parent]... [--workspace scratch|worktree[:<path>]|dir:<path>] [--branch] [--project]
22
22
  [--tenant] [--priority] [--triage] [--idempotency-key] [--max-runtime 30m] [--created-by] [--skill]... [--max-retries]
23
- [--model] [--provider] [--goal] [--goal-max-turns] [--initial-status blocked|running]
23
+ [--model] [--provider] [--goal|--no-goal] [--goal-max-turns] [--initial-status blocked|running]
24
24
  swarm <goal> --worker PROFILE:TITLE[:SKILL,SKILL]... --verifier P --synthesizer P
25
25
  list [--mine] [--assignee] [--status] [--tenant] [--session] [--archived] [--sort] show <id> context <id> runs <id>
26
- assign <id> <profile|none> reassign <id> <profile> [--reclaim] [--reason] set-model <id> [model] [--provider]
26
+ assign <id> <profile|none> reassign <id> <profile> [--reclaim] [--reason] set-model <id> [model] [--provider] set-goal <id>... [--goal-max-turns N] [--off]
27
27
  claim <id> [--ttl] reclaim <id> [--reason] heartbeat <id> [--note] link|unlink <parent> <child>
28
28
  comment <id> <text> [--author] [--max-len] attach <id> <path> [--name] [--content-type] attachments <id> attach-rm <n>
29
29
  complete <id>... [--result] [--summary] [--metadata JSON] edit <id> --result [--summary] [--metadata]
@@ -51,7 +51,7 @@ supercode's own:
51
51
 
52
52
  const MULTI = new Set(['parent', 'skill', 'worker', 'child', 'ids', 'rm']);
53
53
  const FLAGS = new Set(['json', 'triage', 'no-notify', 'no-start', 'all', 'force', 'help', 'mine', 'archived', 'reclaim', 'dry-run',
54
- 'goal', 'switch', 'delete', 'no-attachments', 'include-logs', 'verbose']);
54
+ 'goal', 'no-goal', 'off', 'switch', 'delete', 'no-attachments', 'include-logs', 'verbose']);
55
55
  // flags that take every following word up to the next flag (Hermes's nargs='+')
56
56
  const GREEDY = new Set(['ids', 'rm']);
57
57
 
@@ -235,13 +235,15 @@ export async function runWorkflow(argv) {
235
235
  const meta = boardMeta(root, slug);
236
236
  let workspace = args.workspace;
237
237
  if ((workspace === 'dir' || workspace === 'worktree') && meta.default_workdir) workspace = `${workspace}:${meta.default_workdir}`;
238
+ // a home whose `kanban.goal_mode_default` is true makes every new card a goal card (a subtask is never started alone)
239
+ const goalDefault = !args.goal && !args['no-goal'] && !args['subtask-of'] && (await laneState(root)).kanban.goal_mode_default === true;
238
240
  const created = withBoard((b) => engine.createCard(b, {
239
241
  title: rest.join(' '), body: args.body, assignee: args.assignee === 'none' ? null : args.assignee, machine: args.machine,
240
242
  workspace, branch: args.branch, parents: args.parent, priority: args.priority, tenant: args.tenant,
241
243
  triage: args.triage, idempotencyKey: args['idempotency-key'], maxRuntime: duration(args['max-runtime']),
242
244
  maxRetries, skills: args.skill, model: args.model, provider: args.provider, createdBy: args['created-by'] ?? callerName(),
243
245
  sessionId: process.env.CLAUDE_CODE_SESSION_ID ?? null, subtaskOf: args['subtask-of'] ?? null, initialStatus: args['initial-status'] ?? null,
244
- project: args.project ?? meta.project_id ?? null, goal: args.goal ? { maxTurns: args['goal-max-turns'] ? Number(args['goal-max-turns']) : null } : null,
246
+ project: args.project ?? meta.project_id ?? null, goal: args.goal || goalDefault ? { maxTurns: args['goal-max-turns'] ? Number(args['goal-max-turns']) : null } : null,
245
247
  }), true);
246
248
  let answer = { id: created.id, existing: created.existing };
247
249
  if (!args['no-start'] && !created.existing && !args['subtask-of']) {
@@ -377,6 +379,14 @@ export async function runWorkflow(argv) {
377
379
  say(args, `Reassigned ${id} to ${rest[1]}`, { ok: true }); return 0;
378
380
  }
379
381
  case 'set-model': need(1, 'a card id'); withBoard((b) => engine.setModel(b, id, rest[1] ?? null, args.provider ?? null)); say(args, rest[1] ? `${id} runs on model ${rest[1]}${args.provider ? ` (${args.provider})` : ''} from its next start` : `${id} uses its profile's model`, { ok: true }); return 0;
382
+ case 'set-goal': {
383
+ // a card into goal mode (or out of it with --off) from its next idle turn: the heartbeat keeps its session going
384
+ need(1, 'a card id');
385
+ const maxTurns = args['goal-max-turns'] ? Number(args['goal-max-turns']) : null;
386
+ if (maxTurns !== null && !(Number.isInteger(maxTurns) && maxTurns >= 1)) throw new Error('--goal-max-turns is a whole number of turns, at least 1');
387
+ for (const t of ids()) withBoard((b) => engine.setGoal(b, t, { on: !args.off, maxTurns }));
388
+ say(args, ids().map((t) => args.off ? `${t} is no longer a goal card` : `${t} is a goal card${maxTurns ? ` (${maxTurns} turns)` : ''}`).join('\n'), { ok: true }); return 0;
389
+ }
380
390
  case 'move': need(2, 'a card id and a machine (or none)'); withBoard((b) => engine.setMachine(b, id, rest[1] === 'none' ? null : rest[1])); say(args, `${id} runs on ${rest[1]}`, { ok: true }); return 0;
381
391
 
382
392
  case 'claim': {
@@ -942,7 +952,7 @@ function authorize(db, verb, rest, args, own) {
942
952
  }
943
953
  if (verb === 'boards') { if (!['list', 'ls', 'show', 'current', undefined].includes(rest[0])) refuse('does not change the home\'s boards'); return; }
944
954
  // verbs that act on the board rather than on a card are the manager's and the board's, never a card's session's
945
- if (['pause', 'resume', 'swarm', 'dispatch', 'daemon', 'serve', 'gc', 'repair', 'adopt', 'close', 'move', 'reassign', 'assign', 'set-model', 'claim', 'reclaim', 'archive', 'promote', 'schedule', 'link', 'unlink', 'edit', 'reopen-review', 'reopen', 'attach-rm'].includes(verb)) refuse(`does not ${verb}; the fleet's manager does`);
955
+ if (['pause', 'resume', 'swarm', 'dispatch', 'daemon', 'serve', 'gc', 'repair', 'adopt', 'close', 'move', 'reassign', 'assign', 'set-model', 'set-goal', 'claim', 'reclaim', 'archive', 'promote', 'schedule', 'link', 'unlink', 'edit', 'reopen-review', 'reopen', 'attach-rm'].includes(verb)) refuse(`does not ${verb}; the fleet's manager does`);
946
956
  if (args.all) refuse(`does not ${verb} every card; only its own`);
947
957
  const card = engine.getTask(db, rest[0]);
948
958
  if (!card) return; // the verb itself answers "no card"
@@ -474,8 +474,9 @@ function launchPane(board, task, runId, lane, fleet, { input = null } = {}) {
474
474
  let replaced = null;
475
475
  if (prev && prev.machine === machine) {
476
476
  if (sessionAlive(fleet, prev)) {
477
- session = { ...prev, mode: 'adopt', launched_at: now(), seen_at: now() };
478
- delete session.missing_since;
477
+ // the session is kept; the heartbeat's count belongs to the run that ended, so the new run starts it again
478
+ const { missing_since: _m, goal_turns: _t, goal_nudged_at: _n, idle_since: _i, worked_since_ask: _w, ...kept } = prev;
479
+ session = { ...kept, mode: 'adopt', launched_at: now(), seen_at: now() };
479
480
  } else if (prev.resumable !== false) {
480
481
  const r = sc([...base, '--key', `board-${task.id}-r${runId}-resume`, '--input',
481
482
  `The board resumed this session after it was lost; you are still on card ${task.id}: ${task.title}. Carry on.`, '--', '--resume', prev.session_id, ...tail], { timeout: 180_000 });
package/board/engine.mjs CHANGED
@@ -768,6 +768,15 @@ export function setModel(board, id, model = null, provider = null) {
768
768
  });
769
769
  }
770
770
 
771
+ /** Goal mode on a card (Hermes's goal loop, which Hermes sets only at creation): on or off, with its turn budget. */
772
+ export function setGoal(board, id, { on = true, maxTurns = null } = {}) {
773
+ return tx(board, (db) => {
774
+ must(db, id);
775
+ run(db, 'UPDATE tasks SET goal_mode = ?, goal_max_turns = ? WHERE id = ?', on ? 1 : 0, on ? maxTurns : null, id);
776
+ event(db, id, 'goal_set', { goal_mode: Boolean(on), goal_max_turns: on ? maxTurns : null });
777
+ });
778
+ }
779
+
771
780
  export function addAttachment(board, id, { file, name, contentType = null, author = 'user', dir }) {
772
781
  return tx(board, (db) => {
773
782
  must(db, id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-orchestrator",
3
- "version": "0.4.25",
3
+ "version": "0.4.26",
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": {