@volter/supercode-orchestrator 0.5.227 → 0.5.229

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.
Files changed (80) hide show
  1. package/README.md +18 -2
  2. package/activation.mjs +1 -1
  3. package/adapters/agent-mailbox.mjs +10 -0
  4. package/adapters/api-server.mjs +6 -0
  5. package/agents.mjs +149 -40
  6. package/apply/doors.mjs +3 -1
  7. package/bin/agent-channel.mjs +7 -2
  8. package/bin/connection-runner.mjs +60 -19
  9. package/bin/hermes-worker.mjs +2 -1
  10. package/bin/orchestrator.mjs +3 -3
  11. package/bin/runtime.mjs +3 -2
  12. package/board/agent-current.mjs +82 -0
  13. package/board/agent-declarations.mjs +10 -3
  14. package/board/args.mjs +137 -6
  15. package/board/call.mjs +16 -1
  16. package/board/caller-session.mjs +3 -1
  17. package/board/capacity.mjs +59 -45
  18. package/board/card-state.mjs +14 -5
  19. package/board/ci.mjs +521 -178
  20. package/board/cli.mjs +614 -209
  21. package/board/client.mjs +64 -21
  22. package/board/close-current.mjs +36 -0
  23. package/board/current-records.mjs +44 -0
  24. package/board/custody.mjs +2 -1
  25. package/board/dispatch.mjs +1973 -846
  26. package/board/doors.mjs +2 -2
  27. package/board/engine.mjs +623 -85
  28. package/board/event-stream.mjs +10 -5
  29. package/board/fence.mjs +2 -0
  30. package/board/files.mjs +93 -62
  31. package/board/generation-owner.mjs +188 -53
  32. package/board/git-document.mjs +3 -1
  33. package/board/github-rest.mjs +29 -7
  34. package/board/incidents.mjs +5 -17
  35. package/board/listing.mjs +14 -8
  36. package/board/local-admission.mjs +59 -0
  37. package/board/lock-holder.mjs +18 -6
  38. package/board/machine-fleet.mjs +66 -13
  39. package/board/owner-door-worker.mjs +3 -3
  40. package/board/owner-door.mjs +17 -7
  41. package/board/owner-health.mjs +12 -15
  42. package/board/owner-interface.mjs +6 -0
  43. package/board/publication-reader.mjs +50 -0
  44. package/board/published.mjs +40 -12
  45. package/board/queries.mjs +24 -4
  46. package/board/record-view.mjs +30 -3
  47. package/board/request-mail.mjs +37 -6
  48. package/board/resting-admission.mjs +318 -0
  49. package/board/resting-codec-qualifications.json +33 -0
  50. package/board/resting-protocol.mjs +71 -0
  51. package/board/resting-state-protocol.json +120 -0
  52. package/board/resting-state.mjs +113 -0
  53. package/board/runtime-door.mjs +2 -2
  54. package/board/session-continuity.mjs +127 -0
  55. package/board/session-work.mjs +25 -13
  56. package/board/standalone-declaration.mjs +24 -0
  57. package/board/storage-contract.mjs +129 -7
  58. package/board/store.mjs +73 -11
  59. package/board/tools.mjs +56 -13
  60. package/board/wake.mjs +18 -5
  61. package/board/workflow.mjs +284 -82
  62. package/board/workspace.mjs +86 -9
  63. package/board/ztrack.mjs +344 -91
  64. package/connection.mjs +72 -17
  65. package/gates.mjs +2 -1
  66. package/hand-off.mjs +4 -3
  67. package/hermes-cli.mjs +5 -3
  68. package/hermes-locks.mjs +5 -2
  69. package/hosted.mjs +2 -1
  70. package/index.d.ts +2 -0
  71. package/index.mjs +1 -1
  72. package/kanban-worker.mjs +63 -13
  73. package/loop.mjs +33 -0
  74. package/mcp/hermes-kanban-tools.json +109 -50
  75. package/mcp/server.mjs +7 -1
  76. package/ontology.d.ts +26 -0
  77. package/orchestration.mjs +15 -4
  78. package/package.json +19 -12
  79. package/session-recovery.mjs +2 -1
  80. package/worker-route.mjs +37 -19
package/loop.mjs CHANGED
@@ -1,3 +1,6 @@
1
+ import { registerWorkService, closeWorkLedger } from '@volter/supercode-harness-sdk/work-ledger';
2
+ import { registerWorkOperations, measureWork } from "@volter/supercode-harness-sdk/work-ledger";
3
+ registerWorkOperations([{"kind":"pass","operation":"gateway.save"},{"kind":"pass","operation":"gateway.jobs"},{"kind":"pass","operation":"gateway.tick"},{"kind":"pass","operation":"gateway.apply"},{"kind":"pass","operation":"gateway.effect"},{"kind":"call","operation":"gateway.input"},{"kind":"call","operation":"gateway.delivery"}]);
1
4
  // The orchestrator loop: events in, `step` in the middle, effects out over the
2
5
  // SDK and the adapters (docs/ORCHESTRATOR-IR.md §4–§5, §7).
3
6
  //
@@ -58,6 +61,7 @@ export class Orchestrator {
58
61
  * @param {(level, message) => void} [o.log]
59
62
  */
60
63
  constructor({ state, vault = new Map(), client, adapters = {}, clock = null, launchFor = null, executeJob = null, log = () => {} }) {
64
+ registerWorkService({ role: 'orchestrator-gateway', scope: {kind:'install',id:state.root} });
61
65
  this.state = state;
62
66
  this.executeJob = executeJob;
63
67
  this.hostedTasks = new Set();
@@ -150,6 +154,7 @@ export class Orchestrator {
150
154
 
151
155
  /** The folder written from state, and each profile's `cron/jobs.json` as that write left it. */
152
156
  async save() {
157
+ return measureWork("pass", "gateway.save", async () => {
153
158
  const { rejected } = await save(this.state, this.state.root);
154
159
  // An entry the store could not read was written as the home last held it (or, never held, left out), and is again
155
160
  // at every save while it stays as it is; the log names it once per reason, not once per save.
@@ -164,6 +169,9 @@ export class Orchestrator {
164
169
  : `${collection} ${key} could not be saved (${reason}): it is left out of the home, and the orchestrator will restart without it`);
165
170
  }
166
171
  this.jobsWritten = this.jobsFiles();
172
+
173
+
174
+ });
167
175
  }
168
176
 
169
177
  jobsFiles() {
@@ -184,6 +192,7 @@ export class Orchestrator {
184
192
  * every other job keeps this daemon's own state, so a change not yet saved here survives the read.
185
193
  */
186
194
  async adoptOutsideJobs() {
195
+ return measureWork("pass", "gateway.jobs", async () => {
187
196
  const now = this.jobsFiles();
188
197
  const changed = [...now].filter(([path, text]) => this.jobsWritten.has(path) && this.jobsWritten.get(path) !== text).map(([path]) => path);
189
198
  if (!changed.length) { this.jobsWritten = now; return; }
@@ -202,15 +211,22 @@ export class Orchestrator {
202
211
  this.log('info', `profile ${name}: cron/jobs.json changed outside the daemon; jobs read back: ${touched.join(', ') || 'none'}`);
203
212
  }
204
213
  this.jobsWritten = now;
214
+
215
+
216
+ });
205
217
  }
206
218
 
207
219
  /** One clock tick: expiry, due cron jobs, obligation retry, breaker windows (§4.3). */
208
220
  tick(at = this.clock()) {
221
+ return measureWork("pass", "gateway.tick", () => {
209
222
  this.queue = this.queue.then(() => this.adoptOutsideJobs()).catch((error) => this.log('warn', `cron/jobs.json not read back: ${error.message}`));
210
223
  return this.dispatch(events.tick(at, cronInactivitySeconds(), cronMaxParallel())).then(
211
224
  (result) => { this.tickerHeartbeat(true); void this.declareLocalAgents().catch((error) => this.log('warn', `agent hand-off: ${error.message}`));return result; },
212
225
  (error) => { this.tickerHeartbeat(false); throw error; },
213
226
  );
227
+
228
+
229
+ });
214
230
  }
215
231
 
216
232
  /** The home's agent declarations and ends a launch could not hand to this machine's daemon (none was running) are
@@ -280,6 +296,7 @@ export class Orchestrator {
280
296
  this.retired.clear();
281
297
  for (const adapter of Object.values(this.adapters)) await attempt('adapter', () => adapter.close?.());
282
298
  await attempt('World reference', () => releaseWorld());
299
+ await closeWorkLedger();
283
300
  if (this.stopErrors.length) throw new AggregateError(this.stopErrors, this.stopErrors.map(error => error.message).join('; '));
284
301
  }
285
302
 
@@ -380,6 +397,7 @@ export class Orchestrator {
380
397
  }
381
398
 
382
399
  async apply(event) {
400
+ return measureWork("pass", "gateway.apply", async () => {
383
401
  if (event.type === 'webhook' && this.webhookAdmissions.get(event.admission_id)?.outcome?.error) return [];
384
402
  if (event.type === 'worker' && ['error', 'ended'].includes(event.ev.kind)) {
385
403
  const binding = this.state.profiles[event.profile]?.bindings?.[event.keyString];
@@ -423,9 +441,13 @@ export class Orchestrator {
423
441
  }
424
442
  }
425
443
  return effects;
444
+
445
+
446
+ });
426
447
  }
427
448
 
428
449
  async perform(effect) {
450
+ return measureWork("pass", "gateway.effect", async () => {
429
451
  switch (effect.type) {
430
452
  case 'webhook_input': {
431
453
  const admission = this.webhookAdmissions.get(effect.admission_id);
@@ -489,6 +511,9 @@ export class Orchestrator {
489
511
  case 'refused': this.log('info', effect.message); return;
490
512
  default: this.log('warn', `unknown effect ${effect.type}`);
491
513
  }
514
+
515
+
516
+ });
492
517
  }
493
518
 
494
519
  executeHostedJob({ profile: name, job_id, fire_id, machine = null }) {
@@ -1046,6 +1071,7 @@ export class Orchestrator {
1046
1071
  }
1047
1072
 
1048
1073
  async sendInput({ profile, keyString, text, context_from = null, skills = [], job_id = null, history = null, webhook_input_id = null }) {
1074
+ return measureWork("call", "gateway.input", async () => {
1049
1075
  const admission = webhook_input_id ? this.webhookAdmissions.get(webhook_input_id) : null;
1050
1076
  // Shutdown refused this unsent request (or its handler already failed).
1051
1077
  // It must not be delivered later by a queued started/final completion.
@@ -1079,6 +1105,9 @@ export class Orchestrator {
1079
1105
  this.enqueue(events.worker({ profile, keyString, runtime_id: runtime.handle.runtime_id, ev: { kind: 'error', message: `send failed: ${error.message}` }, at: this.clock() }));
1080
1106
  admission?.finish(error);
1081
1107
  }
1108
+
1109
+
1110
+ });
1082
1111
  }
1083
1112
 
1084
1113
  /**
@@ -1150,6 +1179,7 @@ export class Orchestrator {
1150
1179
  * open it; any success closes it.
1151
1180
  */
1152
1181
  async deliver({ profile: profileName, obligation_id }) {
1182
+ return measureWork("call", "gateway.delivery", async () => {
1153
1183
  const profile = this.state.profiles[profileName];
1154
1184
  const o = profile.obligations.find((x) => x.id === obligation_id);
1155
1185
  if (!o || o.state !== 'pending') return;
@@ -1195,6 +1225,9 @@ export class Orchestrator {
1195
1225
  if (o.attempts >= MAX_ATTEMPTS) { o.state = 'failed'; this.recordDelivery(profile, o, error.message); }
1196
1226
  }
1197
1227
  await this.save();
1228
+
1229
+
1230
+ });
1198
1231
  }
1199
1232
 
1200
1233
  /**
@@ -1,7 +1,7 @@
1
1
  [
2
2
  {
3
3
  "name": "kanban_show",
4
- "description": "Read a task's full state \u2014 title, body, assignee, parent task handoffs, your prior attempts on this task if any, comments, and recent events. Use this to (re)orient yourself before starting work, especially on retries. The response includes a pre-formatted ``worker_context`` string suitable for inclusion verbatim in your reasoning.",
4
+ "description": "Read a task's full state — title, body, assignee, parent task handoffs, your prior attempts on this task if any, comments, and recent events. Use this to (re)orient yourself before starting work, especially on retries. The response includes a pre-formatted ``worker_context`` string suitable for inclusion verbatim in your reasoning.",
5
5
  "inputSchema": {
6
6
  "type": "object",
7
7
  "properties": {
@@ -11,7 +11,7 @@
11
11
  },
12
12
  "board": {
13
13
  "type": "string",
14
- "description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env \u2192 HERMES_KANBAN_BOARD env \u2192 the 'current' symlink under the kanban home \u2192 'default'. Pass an explicit slug only when the caller (e.g. a Telegram routing layer) needs to override the env-pinned active board for this one call."
14
+ "description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env → HERMES_KANBAN_BOARD env → the 'current' symlink under the kanban home → 'default'. Pass an explicit slug only when the caller (e.g. a Telegram routing layer) needs to override the env-pinned active board for this one call."
15
15
  }
16
16
  },
17
17
  "required": []
@@ -19,7 +19,7 @@
19
19
  },
20
20
  {
21
21
  "name": "kanban_list",
22
- "description": "List Kanban task summaries so an orchestrator profile can discover work to route. Supports the same core filters as the CLI: assignee, status, tenant, include_archived, and limit. Returns compact rows with ids, title, status, assignee, priority, parent/child ids, and counts. Bounded to 50 rows by default, 200 max, with truncation metadata. Also recomputes ready tasks before listing, matching the CLI. Orchestrator-only \u2014 dispatcher-spawned task workers never see this tool.",
22
+ "description": "List Kanban task summaries so an orchestrator profile can discover work to route. Supports the same core filters as the CLI: assignee, status, tenant, include_archived, and limit. Returns compact rows with ids, title, status, assignee, priority, parent/child ids, and counts. Bounded to 50 rows by default, 200 max, with truncation metadata. Also recomputes ready tasks before listing, matching the CLI. Orchestrator-only — dispatcher-spawned task workers never see this tool.",
23
23
  "inputSchema": {
24
24
  "type": "object",
25
25
  "properties": {
@@ -58,7 +58,7 @@
58
58
  },
59
59
  "board": {
60
60
  "type": "string",
61
- "description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env \u2192 HERMES_KANBAN_BOARD env \u2192 the 'current' symlink under the kanban home \u2192 'default'. Pass an explicit slug only when the caller (e.g. a Telegram routing layer) needs to override the env-pinned active board for this one call."
61
+ "description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env → HERMES_KANBAN_BOARD env → the 'current' symlink under the kanban home → 'default'. Pass an explicit slug only when the caller (e.g. a Telegram routing layer) needs to override the env-pinned active board for this one call."
62
62
  }
63
63
  },
64
64
  "required": []
@@ -66,7 +66,7 @@
66
66
  },
67
67
  {
68
68
  "name": "kanban_complete",
69
- "description": "Complete through the home's declared workflow permissions with a structured handoff. Homes may reserve terminal closure to the recorded creator or manager; use kanban_request_state when you need their decision. Prefer ``summary`` for a human-readable 1-3 sentence description of what you did; put machine-readable facts in ``metadata`` (changed_files, tests_run, decisions, findings, etc). At least one of ``summary`` or ``result`` is required. If you created new tasks via ``kanban_create`` during this run, list their ids in ``created_cards`` \u2014 the kernel verifies them so phantom references are caught before they leak into downstream automation. If you produced deliverable files (charts, PDFs, spreadsheets, generated images), list their absolute paths in ``artifacts`` \u2014 the gateway notifier will upload them as native attachments to the human who subscribed to the task, so the deliverable lands in their chat alongside the summary instead of being a path they have to fetch by hand.",
69
+ "description": "Complete through the home's declared workflow permissions with a structured handoff. Homes may reserve terminal closure to the recorded creator or manager; use kanban_request_state when you need their decision. Prefer ``summary`` for a human-readable 1-3 sentence description of what you did; put machine-readable facts in ``metadata`` (changed_files, tests_run, decisions, findings, etc). At least one of ``summary`` or ``result`` is required. If you created new tasks via ``kanban_create`` during this run, list their ids in ``created_cards`` — the kernel verifies them so phantom references are caught before they leak into downstream automation. If you produced deliverable files (charts, PDFs, spreadsheets, generated images), list their absolute paths in ``artifacts`` — the gateway notifier will upload them as native attachments to the human who subscribed to the task, so the deliverable lands in their chat alongside the summary instead of being a path they have to fetch by hand.",
70
70
  "inputSchema": {
71
71
  "type": "object",
72
72
  "properties": {
@@ -80,7 +80,7 @@
80
80
  },
81
81
  "metadata": {
82
82
  "type": "object",
83
- "description": "Free-form dict of structured facts about this attempt \u2014 {\"changed_files\": [...], \"tests_run\": 12, \"findings\": [...]}. Surfaced to downstream workers alongside ``summary``."
83
+ "description": "Free-form dict of structured facts about this attempt — {\"changed_files\": [...], \"tests_run\": 12, \"findings\": [...]}. Surfaced to downstream workers alongside ``summary``."
84
84
  },
85
85
  "result": {
86
86
  "type": "string",
@@ -91,18 +91,18 @@
91
91
  "items": {
92
92
  "type": "string"
93
93
  },
94
- "description": "Optional structured manifest of task ids you created via ``kanban_create`` during this run. The kernel verifies each id exists and was created by this worker's profile; any phantom id blocks the completion with an error listing what went wrong (auditable in the task's events). Only list ids you got back from a successful ``kanban_create`` call \u2014 do not invent or remember ids from prose. Omit the field if you did not create any cards."
94
+ "description": "Optional structured manifest of task ids you created via ``kanban_create`` during this run. The kernel verifies each id exists and was created by this worker's profile; any phantom id blocks the completion with an error listing what went wrong (auditable in the task's events). Only list ids you got back from a successful ``kanban_create`` call — do not invent or remember ids from prose. Omit the field if you did not create any cards."
95
95
  },
96
96
  "artifacts": {
97
97
  "type": "array",
98
98
  "items": {
99
99
  "type": "string"
100
100
  },
101
- "description": "Optional list of absolute paths to deliverable files you produced during this run \u2014 generated charts, PDFs, spreadsheets, images, archives. Examples: [\"/tmp/q3-revenue.png\", \"/tmp/report.pdf\"]. The gateway notifier uploads each path as a native attachment to the subscribed chat (images embed inline, everything else uploads as a file) so the deliverable lands with the completion notification. Skip intermediate scratch files and references that are not the deliverable. The path must exist on disk at completion. Files inside a managed scratch workspace are copied to durable task attachments before cleanup; a missing declared scratch artifact keeps the task in-flight so you can fix the path and retry."
101
+ "description": "Optional list of absolute paths to deliverable files you produced during this run — generated charts, PDFs, spreadsheets, images, archives. Examples: [\"/tmp/q3-revenue.png\", \"/tmp/report.pdf\"]. The gateway notifier uploads each path as a native attachment to the subscribed chat (images embed inline, everything else uploads as a file) so the deliverable lands with the completion notification. Skip intermediate scratch files and references that are not the deliverable. The path must exist on disk at completion. Files inside a managed scratch workspace are copied to durable task attachments before cleanup; a missing declared scratch artifact keeps the task in-flight so you can fix the path and retry."
102
102
  },
103
103
  "board": {
104
104
  "type": "string",
105
- "description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env \u2192 HERMES_KANBAN_BOARD env \u2192 the 'current' symlink under the kanban home \u2192 'default'. Pass an explicit slug only when the caller (e.g. a Telegram routing layer) needs to override the env-pinned active board for this one call."
105
+ "description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env → HERMES_KANBAN_BOARD env → the 'current' symlink under the kanban home → 'default'. Pass an explicit slug only when the caller (e.g. a Telegram routing layer) needs to override the env-pinned active board for this one call."
106
106
  },
107
107
  "commit": {
108
108
  "type": "string",
@@ -140,21 +140,19 @@
140
140
  },
141
141
  "waiting_on": {
142
142
  "type": "string",
143
- "description": "The id of the message (m-\u2026 or a-\u2026) whose answer this waits for."
143
+ "description": "The id of the message (m-… or a-…) whose answer this waits for."
144
144
  },
145
145
  "board": {
146
146
  "type": "string",
147
- "description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env \u2192 HERMES_KANBAN_BOARD env \u2192 the 'current' symlink under the kanban home \u2192 'default'. Pass an explicit slug only when the caller (e.g. a Telegram routing layer) needs to override the env-pinned active board for this one call."
147
+ "description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env → HERMES_KANBAN_BOARD env → the 'current' symlink under the kanban home → 'default'. Pass an explicit slug only when the caller (e.g. a Telegram routing layer) needs to override the env-pinned active board for this one call."
148
148
  }
149
149
  },
150
- "required": [
151
- "reason"
152
- ]
150
+ "required": []
153
151
  }
154
152
  },
155
153
  {
156
154
  "name": "kanban_request_review",
157
- "description": "Request independent review of the opened PR at its exact pushed head. The dispatcher starts the declared reviewer when the arc tasks are closed. Deployment CI is not a review gate.",
155
+ "description": "Request independent review of one ready PR at its exact pushed head while coding and other tasks continue. A request during review queues after the current review. Explicit supersedes corrects one named queued request after verifying its replacement, preserving its receipt and the active review. Missing source details come from recorded board facts when available. Only the board starts the final arc review.",
158
156
  "inputSchema": {
159
157
  "type": "object",
160
158
  "properties": {
@@ -164,12 +162,17 @@
164
162
  },
165
163
  "summary": {
166
164
  "type": "string",
167
- "description": "What was implemented and how it was verified, in one or two sentences \u2014 shown to the reviewer. Don't paste the whole diff; the reviewer has the board and the PR."
165
+ "description": "What was implemented and how it was verified, in one or two sentences — shown to the reviewer. Don't paste the whole diff; the reviewer has the board and the PR."
168
166
  },
169
167
  "reviewer": {
170
168
  "type": "string",
171
169
  "description": "Optional reviewer profile. When provided, the task is reassigned to that profile before review dispatch."
172
170
  },
171
+ "supersedes": {
172
+ "type": "integer",
173
+ "minimum": 1,
174
+ "description": "Optional pending review_queued event ID on this card to supersede with the supplied ready PR/head. Fresh admission must succeed before only that request is settled; the active review stays intact. The replacement keeps the original FIFO position."
175
+ },
173
176
  "metadata": {
174
177
  "type": "object",
175
178
  "description": "Optional structured handoff facts for the reviewer, such as changed_files, tests_run, commit, or decisions.",
@@ -177,7 +180,7 @@
177
180
  },
178
181
  "board": {
179
182
  "type": "string",
180
- "description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env \u2192 HERMES_KANBAN_BOARD env \u2192 the 'current' symlink under the kanban home \u2192 'default'. Pass an explicit slug only when the caller (e.g. a Telegram routing layer) needs to override the env-pinned active board for this one call."
183
+ "description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env → HERMES_KANBAN_BOARD env → the 'current' symlink under the kanban home → 'default'. Pass an explicit slug only when the caller (e.g. a Telegram routing layer) needs to override the env-pinned active board for this one call."
181
184
  },
182
185
  "commit": {
183
186
  "type": "string",
@@ -191,7 +194,7 @@
191
194
  "items": {
192
195
  "type": "string"
193
196
  },
194
- "description": "Every opened PR the arc changed, one URL each (its own repository's and any other repository it changed)."
197
+ "description": "The ready PR URL to review. Defaults to the sole recorded PR when omitted."
195
198
  },
196
199
  "pr_head": {
197
200
  "type": [
@@ -208,9 +211,7 @@
208
211
  "description": "Ask the plan review of a big arc's proposed ADR (its PR in pr, its pushed head in pr_head) before any code. Without it, one PR named with its head is that PR's review."
209
212
  }
210
213
  },
211
- "required": [
212
- "summary"
213
- ]
214
+ "required": []
214
215
  }
215
216
  },
216
217
  {
@@ -229,7 +230,7 @@
229
230
  },
230
231
  "board": {
231
232
  "type": "string",
232
- "description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env \u2192 HERMES_KANBAN_BOARD env \u2192 the 'current' symlink under the kanban home \u2192 'default'. Pass an explicit slug only when the caller (e.g. a Telegram routing layer) needs to override the env-pinned active board for this one call."
233
+ "description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env → HERMES_KANBAN_BOARD env → the 'current' symlink under the kanban home → 'default'. Pass an explicit slug only when the caller (e.g. a Telegram routing layer) needs to override the env-pinned active board for this one call."
233
234
  },
234
235
  "findings": {
235
236
  "type": "array",
@@ -239,14 +240,12 @@
239
240
  "description": "Each finding, filed as a task on the arc before the card returns; an arc with no open task is refused."
240
241
  }
241
242
  },
242
- "required": [
243
- "reason"
244
- ]
243
+ "required": []
245
244
  }
246
245
  },
247
246
  {
248
247
  "name": "kanban_heartbeat",
249
- "description": "Signal that you're still alive during a long operation (training, encoding, large crawls). Call every few minutes so humans see liveness separately from PID checks. Pure side effect \u2014 no work changes.",
248
+ "description": "Record progress and optionally a waiting deadline for this run. A deadline changes no card lane and ends no run; idle supervision honors it until expiry.",
250
249
  "inputSchema": {
251
250
  "type": "object",
252
251
  "properties": {
@@ -260,7 +259,14 @@
260
259
  },
261
260
  "board": {
262
261
  "type": "string",
263
- "description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env \u2192 HERMES_KANBAN_BOARD env \u2192 the 'current' symlink under the kanban home \u2192 'default'. Pass an explicit slug only when the caller (e.g. a Telegram routing layer) needs to override the env-pinned active board for this one call."
262
+ "description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env → HERMES_KANBAN_BOARD env → the 'current' symlink under the kanban home → 'default'. Pass an explicit slug only when the caller (e.g. a Telegram routing layer) needs to override the env-pinned active board for this one call."
263
+ },
264
+ "until": {
265
+ "type": [
266
+ "string",
267
+ "number"
268
+ ],
269
+ "description": "Optional waiting deadline: an ISO time, a duration such as 30m, or epoch seconds."
264
270
  }
265
271
  },
266
272
  "required": []
@@ -268,13 +274,13 @@
268
274
  },
269
275
  {
270
276
  "name": "kanban_comment",
271
- "description": "Append a comment to a task's thread. Use for durable notes that should outlive this run (questions for the next worker, partial findings, rationale). Ephemeral reasoning doesn't belong here \u2014 use your normal response instead.",
277
+ "description": "Append a comment to a task's thread. Use for durable notes that should outlive this run (questions for the next worker, partial findings, rationale). Ephemeral reasoning doesn't belong here — use your normal response instead.",
272
278
  "inputSchema": {
273
279
  "type": "object",
274
280
  "properties": {
275
281
  "task_id": {
276
282
  "type": "string",
277
- "description": "Task id. Required (may be your own task or another's \u2014 comment threads are per-task)."
283
+ "description": "Task id. Required (may be your own task or another's — comment threads are per-task)."
278
284
  },
279
285
  "body": {
280
286
  "type": "string",
@@ -282,7 +288,55 @@
282
288
  },
283
289
  "board": {
284
290
  "type": "string",
285
- "description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env \u2192 HERMES_KANBAN_BOARD env \u2192 the 'current' symlink under the kanban home \u2192 'default'. Pass an explicit slug only when the caller (e.g. a Telegram routing layer) needs to override the env-pinned active board for this one call."
291
+ "description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env → HERMES_KANBAN_BOARD env → the 'current' symlink under the kanban home → 'default'. Pass an explicit slug only when the caller (e.g. a Telegram routing layer) needs to override the env-pinned active board for this one call."
292
+ },
293
+ "pr": {
294
+ "type": "string",
295
+ "description": "Exact GitHub PR URL for a source or PR evidence declaration."
296
+ },
297
+ "pr_head": {
298
+ "type": "string",
299
+ "pattern": "^[a-f0-9]{40}$"
300
+ },
301
+ "release_target": {
302
+ "type": "string",
303
+ "description": "Intended exact target for a release-only disposition."
304
+ },
305
+ "pr_disposition": {
306
+ "type": "string",
307
+ "enum": [
308
+ "merge",
309
+ "release-only"
310
+ ],
311
+ "description": "Implementer/manager exact-head disposition. Default merge. Does not grant release permission."
312
+ },
313
+ "reason": {
314
+ "type": "string"
315
+ },
316
+ "release_readback": {
317
+ "type": "object",
318
+ "properties": {
319
+ "target": {
320
+ "type": "string"
321
+ },
322
+ "artifact": {
323
+ "type": "string",
324
+ "pattern": "^[a-f0-9]{40}$"
325
+ },
326
+ "evidence": {
327
+ "type": "string"
328
+ },
329
+ "attest": {
330
+ "type": "boolean",
331
+ "description": "Explicit manager attestation for a target without a machine verifier."
332
+ }
333
+ },
334
+ "required": [
335
+ "target",
336
+ "artifact",
337
+ "evidence"
338
+ ],
339
+ "additionalProperties": false
286
340
  }
287
341
  },
288
342
  "required": [
@@ -293,7 +347,7 @@
293
347
  },
294
348
  {
295
349
  "name": "kanban_attach",
296
- "description": "Attach a file to a task by passing its bytes inline (base64). Use for genuine file artifacts the next worker or a human should be able to download \u2014 generated reports, images, exports. The file is stored as a real attachment (not a comment link) under the task's attachments dir, capped at 25 MB. Prefer kanban_attach_url when you only have a URL.",
350
+ "description": "Attach a file to a task by passing its bytes inline (base64). Use for genuine file artifacts the next worker or a human should be able to download — generated reports, images, exports. The file is stored as a real attachment (not a comment link) under the task's attachments dir, capped at 25 MB. Prefer kanban_attach_url when you only have a URL.",
297
351
  "inputSchema": {
298
352
  "type": "object",
299
353
  "properties": {
@@ -315,7 +369,7 @@
315
369
  },
316
370
  "board": {
317
371
  "type": "string",
318
- "description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env \u2192 HERMES_KANBAN_BOARD env \u2192 the 'current' symlink under the kanban home \u2192 'default'. Pass an explicit slug only when the caller (e.g. a Telegram routing layer) needs to override the env-pinned active board for this one call."
372
+ "description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env → HERMES_KANBAN_BOARD env → the 'current' symlink under the kanban home → 'default'. Pass an explicit slug only when the caller (e.g. a Telegram routing layer) needs to override the env-pinned active board for this one call."
319
373
  }
320
374
  },
321
375
  "required": [
@@ -326,7 +380,7 @@
326
380
  },
327
381
  {
328
382
  "name": "kanban_attach_url",
329
- "description": "Attach a file to a task by URL \u2014 Hermes downloads it server-side and stores it as a real attachment (capped at 25 MB). Use when you have a link rather than the bytes. Only http/https URLs are accepted.",
383
+ "description": "Attach a file to a task by URL — Hermes downloads it server-side and stores it as a real attachment (capped at 25 MB). Use when you have a link rather than the bytes. Only http/https URLs are accepted.",
330
384
  "inputSchema": {
331
385
  "type": "object",
332
386
  "properties": {
@@ -348,7 +402,7 @@
348
402
  },
349
403
  "board": {
350
404
  "type": "string",
351
- "description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env \u2192 HERMES_KANBAN_BOARD env \u2192 the 'current' symlink under the kanban home \u2192 'default'. Pass an explicit slug only when the caller (e.g. a Telegram routing layer) needs to override the env-pinned active board for this one call."
405
+ "description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env → HERMES_KANBAN_BOARD env → the 'current' symlink under the kanban home → 'default'. Pass an explicit slug only when the caller (e.g. a Telegram routing layer) needs to override the env-pinned active board for this one call."
352
406
  }
353
407
  },
354
408
  "required": [
@@ -388,7 +442,7 @@
388
442
  },
389
443
  "board": {
390
444
  "type": "string",
391
- "description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env \u2192 HERMES_KANBAN_BOARD env \u2192 the 'current' symlink under the kanban home \u2192 'default'. Pass an explicit slug only when the caller (e.g. a Telegram routing layer) needs to override the env-pinned active board for this one call."
445
+ "description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env → HERMES_KANBAN_BOARD env → the 'current' symlink under the kanban home → 'default'. Pass an explicit slug only when the caller (e.g. a Telegram routing layer) needs to override the env-pinned active board for this one call."
392
446
  }
393
447
  },
394
448
  "required": []
@@ -396,7 +450,7 @@
396
450
  },
397
451
  {
398
452
  "name": "kanban_create",
399
- "description": "Create a new kanban task, optionally as a child of the current one (pass the current task id in ``parents``). Used by orchestrator workers to fan out \u2014 decompose work into child tasks with specific assignees, link them into a pipeline, then complete your own task. The dispatcher picks up the new tasks on its next tick and spawns the assigned profiles.",
453
+ "description": "Create a new kanban task, optionally as a child of the current one (pass the current task id in ``parents``). Used by orchestrator workers to fan out — decompose work into child tasks with specific assignees, link them into a pipeline, then complete your own task. The dispatcher picks up the new tasks on its next tick and spawns the assigned profiles.",
400
454
  "inputSchema": {
401
455
  "type": "object",
402
456
  "properties": {
@@ -410,7 +464,7 @@
410
464
  },
411
465
  "assignee": {
412
466
  "type": "string",
413
- "description": "Profile name that should execute this task (e.g. 'researcher-a', 'reviewer', 'writer'). Required \u2014 tasks without an assignee are never dispatched."
467
+ "description": "Profile name that should execute this task (e.g. 'researcher-a', 'reviewer', 'writer'). Required — tasks without an assignee are never dispatched."
414
468
  },
415
469
  "body": {
416
470
  "type": "string",
@@ -450,7 +504,7 @@
450
504
  },
451
505
  "triage": {
452
506
  "type": "boolean",
453
- "description": "If true, task lands in 'triage' instead of 'todo' \u2014 a specifier profile is expected to flesh out the body before work starts."
507
+ "description": "If true, task lands in 'triage' instead of 'todo' — a specifier profile is expected to flesh out the body before work starts."
454
508
  },
455
509
  "idempotency_key": {
456
510
  "type": "string",
@@ -473,7 +527,7 @@
473
527
  "items": {
474
528
  "type": "string"
475
529
  },
476
- "description": "Skill names to force-load into the dispatched worker. The kanban lifecycle is already injected automatically; use this to pin a task to a specialist context \u2014 e.g. ['translation'] for a translation task, ['github-code-review'] for a reviewer task. The names must match skills installed on the assignee's profile."
530
+ "description": "Skill names to force-load into the dispatched worker. The kanban lifecycle is already injected automatically; use this to pin a task to a specialist context — e.g. ['translation'] for a translation task, ['github-code-review'] for a reviewer task. The names must match skills installed on the assignee's profile."
477
531
  },
478
532
  "goal_mode": {
479
533
  "type": "boolean",
@@ -489,11 +543,11 @@
489
543
  },
490
544
  "provider": {
491
545
  "type": "string",
492
- "description": "Provider the 'model' belongs to (e.g. 'openrouter', 'anthropic', 'nous'). Set this whenever the model is not from the assignee profile's configured provider \u2014 a model name alone is resolved against the profile's provider and will fail if it belongs to a different one. Requires 'model'."
546
+ "description": "Provider the 'model' belongs to (e.g. 'openrouter', 'anthropic', 'nous'). Set this whenever the model is not from the assignee profile's configured provider — a model name alone is resolved against the profile's provider and will fail if it belongs to a different one. Requires 'model'."
493
547
  },
494
548
  "board": {
495
549
  "type": "string",
496
- "description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env \u2192 HERMES_KANBAN_BOARD env \u2192 the 'current' symlink under the kanban home \u2192 'default'. Pass an explicit slug only when the caller (e.g. a Telegram routing layer) needs to override the env-pinned active board for this one call."
550
+ "description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env → HERMES_KANBAN_BOARD env → the 'current' symlink under the kanban home → 'default'. Pass an explicit slug only when the caller (e.g. a Telegram routing layer) needs to override the env-pinned active board for this one call."
497
551
  }
498
552
  },
499
553
  "required": [
@@ -505,7 +559,7 @@
505
559
  },
506
560
  {
507
561
  "name": "kanban_unblock",
508
- "description": "Unblock a Kanban task. It moves to ready when all parents are done, or todo while any parent remains open. Orchestrator-only \u2014 only profiles with the kanban toolset can unblock routed work; dispatcher-spawned task workers never see this tool.",
562
+ "description": "Lift a named blocker or all current blockers when on is omitted, through the home workflow. A missing reason records none given. The worker may use this for its own card when its workflow permits it.",
509
563
  "inputSchema": {
510
564
  "type": "object",
511
565
  "properties": {
@@ -515,17 +569,23 @@
515
569
  },
516
570
  "board": {
517
571
  "type": "string",
518
- "description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env \u2192 HERMES_KANBAN_BOARD env \u2192 the 'current' symlink under the kanban home \u2192 'default'. Pass an explicit slug only when the caller (e.g. a Telegram routing layer) needs to override the env-pinned active board for this one call."
572
+ "description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env → HERMES_KANBAN_BOARD env → the 'current' symlink under the kanban home → 'default'. Pass an explicit slug only when the caller (e.g. a Telegram routing layer) needs to override the env-pinned active board for this one call."
573
+ },
574
+ "on": {
575
+ "type": "string",
576
+ "description": "Optional blocker id; omitted means all current blockers."
577
+ },
578
+ "reason": {
579
+ "type": "string",
580
+ "description": "What arrived; omitted records none given."
519
581
  }
520
582
  },
521
- "required": [
522
- "task_id"
523
- ]
583
+ "required": []
524
584
  }
525
585
  },
526
586
  {
527
587
  "name": "kanban_link",
528
- "description": "Add a parent\u2192child dependency edge after both tasks already exist. The child won't promote to 'ready' until all parents are 'done'. Cycles and self-links are rejected.",
588
+ "description": "Add a parent→child dependency edge after both tasks already exist. The child won't promote to 'ready' until all parents are 'done'. Cycles and self-links are rejected.",
529
589
  "inputSchema": {
530
590
  "type": "object",
531
591
  "properties": {
@@ -539,7 +599,7 @@
539
599
  },
540
600
  "board": {
541
601
  "type": "string",
542
- "description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env \u2192 HERMES_KANBAN_BOARD env \u2192 the 'current' symlink under the kanban home \u2192 'default'. Pass an explicit slug only when the caller (e.g. a Telegram routing layer) needs to override the env-pinned active board for this one call."
602
+ "description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env → HERMES_KANBAN_BOARD env → the 'current' symlink under the kanban home → 'default'. Pass an explicit slug only when the caller (e.g. a Telegram routing layer) needs to override the env-pinned active board for this one call."
543
603
  }
544
604
  },
545
605
  "required": [
@@ -626,8 +686,7 @@
626
686
  }
627
687
  },
628
688
  "required": [
629
- "state",
630
- "reason"
689
+ "state"
631
690
  ]
632
691
  }
633
692
  }
package/mcp/server.mjs CHANGED
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env node
2
+ import { registerWorkOperations, measureWork } from "@volter/supercode-harness-sdk/work-ledger";
3
+ registerWorkOperations([{"kind":"door","operation":"orchestrator.mcp"}]);
2
4
  // The orchestrator's model-facing door: ONE stdio MCP server per worker
3
5
  // session, bound to the conversation that spawned it (docs/ORCHESTRATOR-IR.md
4
6
  // §7).
@@ -99,6 +101,7 @@ export function ask(binding, verb, args) {
99
101
 
100
102
  /** `crates/harness/src/mcp.rs::handle_request`'s shape, in JavaScript. */
101
103
  export async function handleRequest(binding, request) {
104
+ return measureWork("door", "orchestrator.mcp", async () => {
102
105
  await knowLocalMachine();
103
106
  const id = request?.id;
104
107
  const method = request?.method ?? '';
@@ -135,7 +138,10 @@ export async function handleRequest(binding, request) {
135
138
  if (id === undefined || id === null) return null; // a notification
136
139
  return { jsonrpc: '2.0', id, error: { code: -32601, message: `unknown method \`${method}\`` } };
137
140
  }
138
- }
141
+
142
+
143
+ });
144
+ }
139
145
 
140
146
  /**
141
147
  * Every result names what it did (`ran`) first, then carries its rows; a tool Hermes's own handler served
package/ontology.d.ts CHANGED
@@ -652,6 +652,32 @@ export interface Posted {
652
652
  message_id: string;
653
653
  }
654
654
 
655
+ export interface PrDispositionRecorded {
656
+ kind: 'pr_disposition_recorded';
657
+ url: string;
658
+ head: string;
659
+ by?: string;
660
+ actor: string;
661
+ reason: string;
662
+ target: string | null;
663
+ disposition: 'merge' | 'release-only';
664
+ }
665
+
666
+ export interface PrReleaseReadback {
667
+ kind: 'pr_release_readback';
668
+ url: string;
669
+ head: string;
670
+ by?: string;
671
+ actor: string;
672
+ reason: string;
673
+ target: string;
674
+ artifact: string;
675
+ evidence: string;
676
+ disposition_event: number;
677
+ verdict: number;
678
+ verification: { kind: 'machine' | 'manager-attestation'; source: string; target: string; observed_at: number; by?: string; actor?: string; receipt?: Record<string, unknown> };
679
+ }
680
+
655
681
  /** One profile: a folder with `AGENTS.md` at its root. */
656
682
  export interface Profile {
657
683
  /** `default` for the root, else the folder name. */