@volter/supercode-orchestrator 0.4.4

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/mcp/bridge.mjs ADDED
@@ -0,0 +1,133 @@
1
+ // The loop side of the model-facing door (docs/ORCHESTRATOR-IR.md §7).
2
+ //
3
+ // One local Unix socket per running orchestrator, at `<root>/orchestrator.sock`,
4
+ // opened by `Orchestrator.start()` and closed by `stop()`. Every per-session
5
+ // MCP server (`mcp/server.mjs`) is a client of it: one JSON line in, one JSON
6
+ // line out, one request per connection.
7
+ //
8
+ // TWO families share the one line protocol, told apart by which key the line
9
+ // carries — `verb` (the model's tools) or `op` (the operator's verbs, ORC-13):
10
+ //
11
+ // MODEL (§7, one per published MCP tool; the binding comes from the loop)
12
+ // in {"verb":"cronjob_create","args":{…},"profile":"coder","surface":"loopback|group|coder-room||"}
13
+ // out {"ok":true,"result":{"ran":"created cron job …", …}}
14
+ // {"ok":false,"error":"…"}
15
+ //
16
+ // OPERATOR (§4.6, the write door `supercode <noun> <verb> --harness
17
+ // orchestrator` drives; `profile` defaults to `default`, no surface)
18
+ // in {"op":"jobs.create","args":{…},"profile":"coder"}
19
+ // out {"ok":true,"result":{"ran":"created cron job …","job":{…}}}
20
+ // {"ok":false,"error":"jobs_delete: no job job_x"}
21
+ //
22
+ // op ∈ jobs.create | jobs.update | jobs.pause | jobs.resume | jobs.run |
23
+ // jobs.delete | profiles.create | profiles.delete | sessions.new |
24
+ // sessions.reset | access.pair_approve
25
+ //
26
+ // Both families dispatch `events.operator(...)` on the LOOP'S OWN QUEUE, so an
27
+ // operator write is ordered against inbound messages and ticks like anything
28
+ // else, and `save()` inside this package stays the only writer of the folder.
29
+ // The answer's `result` re-reads the affected record out of state after the
30
+ // reducer ran; a verb the reducer refused answers `{ok:false}` carrying the
31
+ // reducer's own log line, never a silent no-op.
32
+ //
33
+ // The socket is the trust boundary: it lives inside the orchestrator root, so
34
+ // its reachability is the root's file permissions. The binding it is told
35
+ // about is the one the loop itself put in the worker's environment.
36
+
37
+ import { SOCKET_ENV } from './server.mjs';
38
+ import { createServer } from 'node:net';
39
+ import { rmSync } from 'node:fs';
40
+ import { runVerb } from './verbs.mjs';
41
+ import { applyOperator } from '../operator.mjs';
42
+
43
+ export { socketPathFor } from './server.mjs';
44
+
45
+ /**
46
+ * Open the bridge. `orch` is the running Orchestrator; each line is dispatched
47
+ * through `runVerb`, whose effects are performed on the loop's own queue so a
48
+ * tool call is ordered against inbound messages and ticks.
49
+ */
50
+ export function startBridge({ orch, socketPath, log = () => {} }) {
51
+ // a Unix socket's path is bounded by the kernel (104 bytes on macOS, 108 on Linux); past it listen fails
52
+ // with a bare EINVAL, so the bound is said by name, with the way around it
53
+ const limit = process.platform === 'darwin' ? 103 : 107;
54
+ if (Buffer.byteLength(socketPath) > limit) {
55
+ return Promise.reject(new Error(`the orchestrator's socket path ${socketPath} is ${Buffer.byteLength(socketPath)} bytes, past this system's ${limit}; set ${SOCKET_ENV} to a shorter path (for example under /tmp), or use a shorter home`));
56
+ }
57
+ rmSync(socketPath, { force: true }); // a socket left by a killed daemon
58
+ const sockets = new Set();
59
+ const server = createServer((socket) => {
60
+ sockets.add(socket);
61
+ socket.setEncoding('utf8');
62
+ socket.on('close', () => sockets.delete(socket));
63
+ socket.on('error', () => sockets.delete(socket));
64
+ let buffer = '';
65
+ socket.on('data', async (chunk) => {
66
+ buffer += chunk;
67
+ const nl = buffer.indexOf('\n');
68
+ if (nl < 0) return;
69
+ const line = buffer.slice(0, nl);
70
+ buffer = buffer.slice(nl + 1);
71
+ let answer;
72
+ try {
73
+ const call = JSON.parse(line);
74
+ answer = await dispatch(orch, call, log);
75
+ } catch (error) {
76
+ answer = { ok: false, error: error.message };
77
+ }
78
+ socket.write(`${JSON.stringify(answer)}\n`);
79
+ socket.end();
80
+ });
81
+ });
82
+ return new Promise((resolve, reject) => {
83
+ server.on('error', reject);
84
+ server.listen(socketPath, () => {
85
+ server.removeListener('error', reject);
86
+ resolve({
87
+ path: socketPath,
88
+ async close() {
89
+ for (const socket of sockets) socket.destroy();
90
+ sockets.clear();
91
+ await new Promise((done) => server.close(done));
92
+ rmSync(socketPath, { force: true });
93
+ },
94
+ });
95
+ });
96
+ });
97
+ }
98
+
99
+ async function dispatch(orch, call, log) {
100
+ const { verb, op, args, profile, surface } = call ?? {};
101
+ if (typeof op === 'string' && op) return dispatchOperator(orch, { op, args, profile }, log);
102
+ if (typeof verb !== 'string' || !verb) return { ok: false, error: 'verb (a model tool) or op (an operator verb) is required' };
103
+ if (typeof profile !== 'string' || !profile) return { ok: false, error: 'profile is required' };
104
+ try {
105
+ // Mutating verbs dispatch operator events onto the loop's queue themselves
106
+ // (§4.6): the reducer is the only thing that changes state.
107
+ const { result, error } = await runVerb(orch, { verb, args: args ?? {}, profile, surface: surface ?? null });
108
+ if (error) return { ok: false, error };
109
+ log('info', `mcp ${verb} on ${profile}: ${result.ran}`);
110
+ return { ok: true, result };
111
+ } catch (error) {
112
+ log('warn', `mcp ${verb} on ${profile}: ${error.message}`);
113
+ return { ok: false, error: error.message };
114
+ }
115
+ }
116
+
117
+ /**
118
+ * The operator family (§4.6). One shared implementation with the cold path
119
+ * (`operator.mjs`); the only thing the daemon adds is that the events land on
120
+ * the loop's queue.
121
+ */
122
+ async function dispatchOperator(orch, { op, args, profile }, log) {
123
+ const name = typeof profile === 'string' && profile ? profile : 'default';
124
+ let answer;
125
+ try {
126
+ answer = await applyOperator(orch.state, op, args ?? {}, name, orch.clock(), (event) => orch.dispatch(event));
127
+ } catch (error) {
128
+ answer = { ok: false, error: error.message };
129
+ }
130
+ if (answer.ok) log('info', `operator ${op} on ${name}: ${answer.result.ran}`);
131
+ else log('warn', `operator ${op} on ${name}: ${answer.error}`);
132
+ return answer;
133
+ }
@@ -0,0 +1,477 @@
1
+ [
2
+ {
3
+ "name": "kanban_show",
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
+ "inputSchema": {
6
+ "type": "object",
7
+ "properties": {
8
+ "task_id": {
9
+ "type": "string",
10
+ "description": "Task id. If omitted, defaults to HERMES_KANBAN_TASK from the env (the task the dispatcher spawned you to work on)."
11
+ },
12
+ "board": {
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 → 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
+ }
16
+ },
17
+ "required": []
18
+ }
19
+ },
20
+ {
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 — dispatcher-spawned task workers never see this tool.",
23
+ "inputSchema": {
24
+ "type": "object",
25
+ "properties": {
26
+ "assignee": {
27
+ "type": "string",
28
+ "description": "Optional assignee/profile filter."
29
+ },
30
+ "status": {
31
+ "type": "string",
32
+ "enum": [
33
+ "triage",
34
+ "todo",
35
+ "ready",
36
+ "running",
37
+ "blocked",
38
+ "done",
39
+ "archived"
40
+ ],
41
+ "description": "Optional task status filter."
42
+ },
43
+ "tenant": {
44
+ "type": "string",
45
+ "description": "Optional tenant/project namespace filter."
46
+ },
47
+ "include_archived": {
48
+ "type": "boolean",
49
+ "description": "Include archived tasks. Defaults to false."
50
+ },
51
+ "limit": {
52
+ "type": "integer",
53
+ "description": "Optional maximum rows to return (default 50, max 200)."
54
+ },
55
+ "board": {
56
+ "type": "string",
57
+ "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."
58
+ }
59
+ },
60
+ "required": []
61
+ }
62
+ },
63
+ {
64
+ "name": "kanban_complete",
65
+ "description": "Mark your current task done with a structured handoff for downstream workers and humans. 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.",
66
+ "inputSchema": {
67
+ "type": "object",
68
+ "properties": {
69
+ "task_id": {
70
+ "type": "string",
71
+ "description": "Task id. If omitted, defaults to HERMES_KANBAN_TASK from the env (the task the dispatcher spawned you to work on)."
72
+ },
73
+ "summary": {
74
+ "type": "string",
75
+ "description": "Human-readable handoff, 1-3 sentences. Appears in Run History on the dashboard and in downstream workers' context."
76
+ },
77
+ "metadata": {
78
+ "type": "object",
79
+ "description": "Free-form dict of structured facts about this attempt — {\"changed_files\": [...], \"tests_run\": 12, \"findings\": [...]}. Surfaced to downstream workers alongside ``summary``."
80
+ },
81
+ "result": {
82
+ "type": "string",
83
+ "description": "Short result log line (legacy field, maps to task.result). Use ``summary`` instead when possible; this exists for compatibility with callers that still set --result on the CLI."
84
+ },
85
+ "created_cards": {
86
+ "type": "array",
87
+ "items": {
88
+ "type": "string"
89
+ },
90
+ "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."
91
+ },
92
+ "artifacts": {
93
+ "type": "array",
94
+ "items": {
95
+ "type": "string"
96
+ },
97
+ "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."
98
+ },
99
+ "board": {
100
+ "type": "string",
101
+ "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."
102
+ }
103
+ },
104
+ "required": []
105
+ }
106
+ },
107
+ {
108
+ "name": "kanban_block",
109
+ "description": "Stop work on this task and route it according to WHY you're stuck. Set ``kind`` to say which: 'dependency' (waiting on another task — goes to todo and auto-resumes when that task finishes, no human needed), 'needs_input' (you need a human decision/answer), 'capability' (a hard wall: no access, missing credentials, an action no agent can do), or 'transient' (a flaky failure that may clear). ``reason`` is shown to the human on the board. If a task keeps getting unblocked and re-blocked for the same reason, it is auto-escalated to triage. Use for genuine blockers only — don't block on things you can resolve yourself.",
110
+ "inputSchema": {
111
+ "type": "object",
112
+ "properties": {
113
+ "task_id": {
114
+ "type": "string",
115
+ "description": "Task id. If omitted, defaults to HERMES_KANBAN_TASK from the env (the task the dispatcher spawned you to work on)."
116
+ },
117
+ "reason": {
118
+ "type": "string",
119
+ "description": "What you need answered or what stopped you, in one or two sentences. Don't paste the whole conversation; the human has the board and can ask follow-ups via comments."
120
+ },
121
+ "kind": {
122
+ "type": "string",
123
+ "enum": [
124
+ "dependency",
125
+ "needs_input",
126
+ "capability",
127
+ "transient"
128
+ ],
129
+ "description": "Why you're blocked. 'dependency' waits in todo and resumes automatically; the others surface to a human. Omit only if none apply."
130
+ },
131
+ "board": {
132
+ "type": "string",
133
+ "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."
134
+ }
135
+ },
136
+ "required": [
137
+ "reason"
138
+ ]
139
+ }
140
+ },
141
+ {
142
+ "name": "kanban_request_review",
143
+ "description": "Hand the task off for review: implementation, self-review, and verification are complete and you want a human (or reviewer) to look before it is marked done. Moves the task to the 'review' column and notifies the subscriber. Unlike ``kanban_block`` this is NOT a blocker — it never counts toward unblock-loop detection, so a task can cycle through review across follow-ups without ever being falsely escalated to triage. Use this instead of blocking with a free-form 'review-required:' reason.",
144
+ "inputSchema": {
145
+ "type": "object",
146
+ "properties": {
147
+ "task_id": {
148
+ "type": "string",
149
+ "description": "Task id. If omitted, defaults to HERMES_KANBAN_TASK from the env (the task the dispatcher spawned you to work on)."
150
+ },
151
+ "summary": {
152
+ "type": "string",
153
+ "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."
154
+ },
155
+ "reviewer": {
156
+ "type": "string",
157
+ "description": "Optional reviewer profile. When provided, the task is reassigned to that profile before review dispatch."
158
+ },
159
+ "metadata": {
160
+ "type": "object",
161
+ "description": "Optional structured handoff facts for the reviewer, such as changed_files, tests_run, commit, or decisions.",
162
+ "additionalProperties": true
163
+ },
164
+ "board": {
165
+ "type": "string",
166
+ "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."
167
+ }
168
+ },
169
+ "required": [
170
+ "summary"
171
+ ]
172
+ }
173
+ },
174
+ {
175
+ "name": "kanban_request_changes",
176
+ "description": "Reviewer verdict: return the current review run to the original implementer with concrete required changes. This closes the review run, reapplies parent dependency gating, and requeues the task without using block-loop accounting. Only use from a task claimed from the review column; use kanban_block only for a genuine external blocker.",
177
+ "inputSchema": {
178
+ "type": "object",
179
+ "properties": {
180
+ "task_id": {
181
+ "type": "string",
182
+ "description": "Task id. If omitted, defaults to HERMES_KANBAN_TASK from the env (the task the dispatcher spawned you to work on)."
183
+ },
184
+ "reason": {
185
+ "type": "string",
186
+ "description": "Specific, actionable changes the implementer must make before requesting another review."
187
+ },
188
+ "board": {
189
+ "type": "string",
190
+ "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."
191
+ }
192
+ },
193
+ "required": [
194
+ "reason"
195
+ ]
196
+ }
197
+ },
198
+ {
199
+ "name": "kanban_heartbeat",
200
+ "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 — no work changes.",
201
+ "inputSchema": {
202
+ "type": "object",
203
+ "properties": {
204
+ "task_id": {
205
+ "type": "string",
206
+ "description": "Task id. If omitted, defaults to HERMES_KANBAN_TASK from the env (the task the dispatcher spawned you to work on)."
207
+ },
208
+ "note": {
209
+ "type": "string",
210
+ "description": "Optional short note describing current progress. Shown in the event log."
211
+ },
212
+ "board": {
213
+ "type": "string",
214
+ "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."
215
+ }
216
+ },
217
+ "required": []
218
+ }
219
+ },
220
+ {
221
+ "name": "kanban_comment",
222
+ "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.",
223
+ "inputSchema": {
224
+ "type": "object",
225
+ "properties": {
226
+ "task_id": {
227
+ "type": "string",
228
+ "description": "Task id. Required (may be your own task or another's — comment threads are per-task)."
229
+ },
230
+ "body": {
231
+ "type": "string",
232
+ "description": "Markdown-supported comment body."
233
+ },
234
+ "board": {
235
+ "type": "string",
236
+ "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."
237
+ }
238
+ },
239
+ "required": [
240
+ "task_id",
241
+ "body"
242
+ ]
243
+ }
244
+ },
245
+ {
246
+ "name": "kanban_attach",
247
+ "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.",
248
+ "inputSchema": {
249
+ "type": "object",
250
+ "properties": {
251
+ "task_id": {
252
+ "type": "string",
253
+ "description": "Task id. If omitted, defaults to HERMES_KANBAN_TASK from the env (the task the dispatcher spawned you to work on)."
254
+ },
255
+ "filename": {
256
+ "type": "string",
257
+ "description": "File name to store it under (e.g. 'report.pdf'). Directory components are stripped; only the leaf is kept."
258
+ },
259
+ "content_base64": {
260
+ "type": "string",
261
+ "description": "The file contents, base64-encoded. Max 25 MB decoded."
262
+ },
263
+ "content_type": {
264
+ "type": "string",
265
+ "description": "Optional MIME type (e.g. 'application/pdf')."
266
+ },
267
+ "board": {
268
+ "type": "string",
269
+ "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."
270
+ }
271
+ },
272
+ "required": [
273
+ "filename",
274
+ "content_base64"
275
+ ]
276
+ }
277
+ },
278
+ {
279
+ "name": "kanban_attach_url",
280
+ "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.",
281
+ "inputSchema": {
282
+ "type": "object",
283
+ "properties": {
284
+ "task_id": {
285
+ "type": "string",
286
+ "description": "Task id. If omitted, defaults to HERMES_KANBAN_TASK from the env (the task the dispatcher spawned you to work on)."
287
+ },
288
+ "url": {
289
+ "type": "string",
290
+ "description": "http(s) URL to fetch and store."
291
+ },
292
+ "filename": {
293
+ "type": "string",
294
+ "description": "Optional name to store it under. Defaults to the URL path's leaf component."
295
+ },
296
+ "content_type": {
297
+ "type": "string",
298
+ "description": "Optional MIME type override. Defaults to the Content-Type the server returns."
299
+ },
300
+ "board": {
301
+ "type": "string",
302
+ "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."
303
+ }
304
+ },
305
+ "required": [
306
+ "url"
307
+ ]
308
+ }
309
+ },
310
+ {
311
+ "name": "kanban_attachments",
312
+ "description": "List the files attached to a task: id, filename, content_type, size, who uploaded it, and the absolute on-disk path you can read.",
313
+ "inputSchema": {
314
+ "type": "object",
315
+ "properties": {
316
+ "task_id": {
317
+ "type": "string",
318
+ "description": "Task id. If omitted, defaults to HERMES_KANBAN_TASK from the env (the task the dispatcher spawned you to work on)."
319
+ },
320
+ "board": {
321
+ "type": "string",
322
+ "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."
323
+ }
324
+ },
325
+ "required": []
326
+ }
327
+ },
328
+ {
329
+ "name": "kanban_create",
330
+ "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.",
331
+ "inputSchema": {
332
+ "type": "object",
333
+ "properties": {
334
+ "title": {
335
+ "type": "string",
336
+ "description": "Short task title (required)."
337
+ },
338
+ "assignee": {
339
+ "type": "string",
340
+ "description": "Profile name that should execute this task (e.g. 'researcher-a', 'reviewer', 'writer'). Required — tasks without an assignee are never dispatched."
341
+ },
342
+ "body": {
343
+ "type": "string",
344
+ "description": "Opening post: full spec, acceptance criteria, links. The assigned worker reads this as part of its context."
345
+ },
346
+ "parents": {
347
+ "type": "array",
348
+ "items": {
349
+ "type": "string"
350
+ },
351
+ "description": "Parent task ids. The new task stays in 'todo' until every parent reaches 'done'; then it auto-promotes to 'ready'. Typical fan-in: list all the researcher task ids when creating a synthesizer task."
352
+ },
353
+ "tenant": {
354
+ "type": "string",
355
+ "description": "Optional namespace for multi-project isolation. Defaults to HERMES_TENANT env if set."
356
+ },
357
+ "priority": {
358
+ "type": "integer",
359
+ "description": "Dispatcher tiebreaker. Higher = picked sooner when multiple ready tasks share an assignee."
360
+ },
361
+ "workspace_kind": {
362
+ "type": "string",
363
+ "enum": [
364
+ "scratch",
365
+ "dir",
366
+ "worktree"
367
+ ],
368
+ "description": "Workspace flavor: 'scratch' (fresh tmp dir, default), 'dir' (shared directory, requires absolute workspace_path), 'worktree' (git worktree)."
369
+ },
370
+ "workspace_path": {
371
+ "type": "string",
372
+ "description": "Absolute path for 'dir' or 'worktree' workspace. Relative paths are rejected at dispatch."
373
+ },
374
+ "project": {
375
+ "type": "string",
376
+ "description": "Optional project id or slug to link the task to. When set, the task becomes a git worktree under the project's primary repo with a deterministic branch (project slug + task id), instead of a random branch."
377
+ },
378
+ "triage": {
379
+ "type": "boolean",
380
+ "description": "If true, task lands in 'triage' instead of 'todo' — a specifier profile is expected to flesh out the body before work starts."
381
+ },
382
+ "idempotency_key": {
383
+ "type": "string",
384
+ "description": "If a non-archived task with this key already exists, return that task's id instead of creating a duplicate. Useful for retry-safe automation."
385
+ },
386
+ "max_runtime_seconds": {
387
+ "type": "integer",
388
+ "description": "Per-task runtime cap. When exceeded, the dispatcher SIGTERMs the worker and re-queues the task with outcome='timed_out'."
389
+ },
390
+ "initial_status": {
391
+ "type": "string",
392
+ "enum": [
393
+ "running",
394
+ "blocked"
395
+ ],
396
+ "description": "Initial card status. Use 'blocked' for tasks that require immediate human ops (R3 gate) to skip the brief running-to-blocked transition. Defaults to 'running', which preserves the usual dispatch path."
397
+ },
398
+ "skills": {
399
+ "type": "array",
400
+ "items": {
401
+ "type": "string"
402
+ },
403
+ "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."
404
+ },
405
+ "goal_mode": {
406
+ "type": "boolean",
407
+ "description": "Run the dispatched worker in a goal loop. When true, after each turn an auxiliary judge checks the worker's response against this card's title/body; if the work isn't done and budget remains, the worker keeps going in the same session until the judge agrees it's complete (or the goal-turn budget is exhausted, which blocks the task for human review). Use this for open-ended cards where one shot rarely finishes the work. Defaults to false (classic single-shot worker)."
408
+ },
409
+ "goal_max_turns": {
410
+ "type": "integer",
411
+ "description": "Turn budget for goal_mode workers. Caps how many continuation turns the worker may take before the task is blocked for review. Ignored unless goal_mode is true. Defaults to the goal-engine default (20)."
412
+ },
413
+ "model": {
414
+ "type": "string",
415
+ "description": "Pin the dispatched worker to this model instead of the assignee profile's configured model. Use the exact model name the target provider expects. Omit to use the profile default."
416
+ },
417
+ "provider": {
418
+ "type": "string",
419
+ "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'."
420
+ },
421
+ "board": {
422
+ "type": "string",
423
+ "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."
424
+ }
425
+ },
426
+ "required": [
427
+ "title",
428
+ "assignee"
429
+ ]
430
+ }
431
+ },
432
+ {
433
+ "name": "kanban_unblock",
434
+ "description": "Unblock a Kanban task. It moves to ready when all parents are done, or todo while any parent remains open. Orchestrator-only — only profiles with the kanban toolset can unblock routed work; dispatcher-spawned task workers never see this tool.",
435
+ "inputSchema": {
436
+ "type": "object",
437
+ "properties": {
438
+ "task_id": {
439
+ "type": "string",
440
+ "description": "Blocked task id to move to ready or parent-gated todo."
441
+ },
442
+ "board": {
443
+ "type": "string",
444
+ "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."
445
+ }
446
+ },
447
+ "required": [
448
+ "task_id"
449
+ ]
450
+ }
451
+ },
452
+ {
453
+ "name": "kanban_link",
454
+ "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.",
455
+ "inputSchema": {
456
+ "type": "object",
457
+ "properties": {
458
+ "parent_id": {
459
+ "type": "string",
460
+ "description": "Parent task id."
461
+ },
462
+ "child_id": {
463
+ "type": "string",
464
+ "description": "Child task id."
465
+ },
466
+ "board": {
467
+ "type": "string",
468
+ "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."
469
+ }
470
+ },
471
+ "required": [
472
+ "parent_id",
473
+ "child_id"
474
+ ]
475
+ }
476
+ }
477
+ ]