@krapht/ork 1.0.0-alpha.1 → 1.0.0-alpha.11

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 (43) hide show
  1. package/README.md +3 -4
  2. package/dist/{__build_info__-D9lSlMFy.mjs → __build_info__-CIchF6nr.mjs} +2 -2
  3. package/dist/bin.mjs +1479 -600
  4. package/dist/chips-BA7SrrH5.mjs +43 -0
  5. package/dist/{clean-CA_Oi2Wl.mjs → clean-Di-KT7Mf.mjs} +5 -2
  6. package/dist/{completion-DBGfL-gN.mjs → completion-C5D0sEs5.mjs} +23 -16
  7. package/dist/context-BaNshWr9.mjs +695 -0
  8. package/dist/{decide-CoIJh34e.mjs → decide-B6PnKeqo.mjs} +3 -3
  9. package/dist/{find-rBBk7Jui.mjs → find-BicBbGMg.mjs} +1 -1
  10. package/dist/format-lWhN-jYR.mjs +286 -0
  11. package/dist/{in-a-run-DdAakpRR.mjs → in-a-run-CgkVouyG.mjs} +1 -1
  12. package/dist/{limits-BPM9wUlV.mjs → limits-jZQp9ZJC.mjs} +1 -1
  13. package/dist/{list-Bv5Lxu7z.mjs → list-CEaJ07jd.mjs} +11 -4
  14. package/dist/{mcp-DM30MFBw.mjs → mcp-DYgVA9f5.mjs} +6 -6
  15. package/dist/output-CM95njYt.mjs +421 -0
  16. package/dist/{pause-C7lJkSma.mjs → pause-D6PuSlNl.mjs} +4 -4
  17. package/dist/{relay-BipJYszF.mjs → relay-DNx7be9k.mjs} +2 -2
  18. package/dist/{remove-BDs9isB6.mjs → remove-CpMb4NDl.mjs} +14 -7
  19. package/dist/{report-DK4LGqwF.mjs → report-CEN8VoUb.mjs} +31 -20
  20. package/dist/{send-nbkjf3sc.mjs → send-CLs_fyg7.mjs} +3 -3
  21. package/dist/{src-BxUJPGa5.mjs → src-9iGkjnsy.mjs} +487 -341
  22. package/dist/{src-B0TT7DBi.mjs → src-B6KvTm7B.mjs} +106 -90
  23. package/dist/src-BnZgZGrf.mjs +2697 -0
  24. package/dist/{src-hrj2h0IZ.mjs → src-D6_xjm-x.mjs} +6719 -3389
  25. package/dist/ui/THIRD_PARTY_LICENSES.txt +594 -0
  26. package/dist/ui/assets/app-CxURph7a.js +75 -0
  27. package/dist/ui/assets/app-QoHVi-mb.css +1 -0
  28. package/dist/ui/assets/{index-CPx74lFf.js → index-By_8KKG6.js} +3 -3
  29. package/dist/ui/assets/theme-CdXvqTQ6.js +1 -0
  30. package/dist/ui/index.html +2 -2
  31. package/dist/{ui-B98FSboa.mjs → ui-CIci4UBW.mjs} +4 -4
  32. package/dist/{usage-EW7DI6Z7.mjs → usage-GceBGMjU.mjs} +43 -32
  33. package/dist/{watch-BAkSGaOp.mjs → watch-BmoybMQg.mjs} +28 -16
  34. package/dist/{workflows-vsEohqa7.mjs → workflows-DZvW3i1e.mjs} +4 -3
  35. package/dist/{worktrees-Ahj9rdWK.mjs → worktrees-Be51E2rB.mjs} +84 -24
  36. package/package.json +2 -4
  37. package/dist/context-6x3kXREO.mjs +0 -274
  38. package/dist/format-zMmhqyLE.mjs +0 -427
  39. package/dist/output-TV_j7Ni0.mjs +0 -950
  40. package/dist/src-B9v0lDc6.mjs +0 -1204
  41. package/dist/ui/assets/app-BNT0SuIz.css +0 -1
  42. package/dist/ui/assets/app-CmkynZyW.js +0 -67
  43. package/dist/ui/assets/theme-3xH5jxKs.js +0 -1
@@ -0,0 +1,2697 @@
1
+ import { Ar as AGENT_DRIVERS, Bt as lowerChild, En as maskSecrets, Vt as startAt, an as FEEDBACK_SEVERITIES, en as outcomeEnd, ft as parseReport, in as FEEDBACK_KINDS, nn as END_KINDS, on as FEEDBACK_TARGETS, rn as endFor, wn as MASK } from "./src-D6_xjm-x.mjs";
2
+ import path from "node:path";
3
+ import { z } from "zod";
4
+ import { existsSync, linkSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
5
+ import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
6
+ import { homedir, tmpdir } from "node:os";
7
+ import { createInterface } from "node:readline";
8
+ import { execFile, spawn } from "node:child_process";
9
+ import { setTimeout as setTimeout$1 } from "node:timers/promises";
10
+ import { createServer } from "node:http";
11
+ // ../runtime/src/hold.ts
12
+ /**
13
+ * Waits at a tool call while `hold` is held: "go" once it is released within
14
+ * the window (or at once when it is not held), "defer" after the window.
15
+ */
16
+ const holdAtTool = async (hold, signal, windowMs) => {
17
+ if (!hold.held()) return "go";
18
+ const window = new AbortController();
19
+ const timer = setTimeout(() => {
20
+ window.abort();
21
+ }, windowMs);
22
+ try {
23
+ await hold.wait("tool", AbortSignal.any([signal, window.signal]));
24
+ } finally {
25
+ clearTimeout(timer);
26
+ }
27
+ return hold.held() && !signal.aborted ? "defer" : "go";
28
+ };
29
+ // ../runtime/src/heavy-hook.ts
30
+ /**
31
+ * @file A session's slot for a heavy Bash call (the project's `heavy` rule),
32
+ * taken in the hook before a tool call, where a pause holds the call too
33
+ * (ADR-041): a slot that frees within the hold window lets the call go; past
34
+ * it the call is deferred, as a pause defers it, and the session resumes once
35
+ * the slot is taken. The slot is released after the call (the hook after it),
36
+ * at the session's next call, or at the session's end. The wait itself (its
37
+ * events, the clock standing still) is the node's machine port.
38
+ */
39
+ const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
40
+ /** The command of a Bash call's hook input, as Claude Code sends it. */
41
+ const bashCommand = (input) => {
42
+ if (!isRecord$1(input) || input["tool_name"] !== "Bash") return void 0;
43
+ const tool = input["tool_input"];
44
+ const command = isRecord$1(tool) ? tool["command"] : void 0;
45
+ return typeof command === "string" ? command : void 0;
46
+ };
47
+ /** The hook for one session; with no machine port, every call goes. */
48
+ const heavyHook = (machine) => {
49
+ let held;
50
+ let waiting;
51
+ const free = () => {
52
+ held?.release();
53
+ held = void 0;
54
+ };
55
+ /** Gives up the wait; a slot it already got is given back. */
56
+ const abandon = () => {
57
+ const pending = waiting;
58
+ waiting = void 0;
59
+ if (pending === void 0) return;
60
+ pending.stop.abort();
61
+ pending.slot.then((release) => {
62
+ release();
63
+ return null;
64
+ });
65
+ };
66
+ return {
67
+ before: async (input, signal, windowMs) => {
68
+ const command = bashCommand(input);
69
+ if (held !== void 0 && !held.ran && held.command === command) {
70
+ held.ran = true;
71
+ return "go";
72
+ }
73
+ free();
74
+ if (waiting !== void 0 && waiting.command !== command) abandon();
75
+ if (machine === void 0 || command === void 0) return "go";
76
+ if (waiting === void 0) {
77
+ const stop = new AbortController();
78
+ waiting = {
79
+ command,
80
+ stop,
81
+ slot: machine.slot(command, stop.signal)
82
+ };
83
+ }
84
+ const pending = waiting;
85
+ const window = new AbortController();
86
+ const release = await Promise.race([pending.slot, setTimeout$1(windowMs, null, { signal: AbortSignal.any([signal, window.signal]) }).catch(() => null)]);
87
+ window.abort();
88
+ if (release === null) return signal.aborted ? "go" : "defer";
89
+ waiting = void 0;
90
+ held = {
91
+ command,
92
+ release,
93
+ ran: true
94
+ };
95
+ return "go";
96
+ },
97
+ after: () => {
98
+ if (held?.ran === true) free();
99
+ },
100
+ ready: async () => {
101
+ const pending = waiting;
102
+ if (pending === void 0) return;
103
+ const release = await pending.slot;
104
+ if (waiting !== pending) return;
105
+ waiting = void 0;
106
+ held = {
107
+ command: pending.command,
108
+ release,
109
+ ran: false
110
+ };
111
+ },
112
+ release: () => {
113
+ abandon();
114
+ free();
115
+ }
116
+ };
117
+ };
118
+ /** Whether a hook's input is from after a tool call. */
119
+ const afterCall = (input) => isRecord$1(input) && input["hook_event_name"] === "PostToolUse";
120
+ /**
121
+ * A session's hook on its tool calls. Before one, a pause holds it (ADR-041),
122
+ * then a heavy Bash call waits for its slot; past the window either defers it
123
+ * (`onDefer`). After a call, its slot is freed.
124
+ */
125
+ const toolHook = (given) => async (gone, input) => {
126
+ if (afterCall(input)) {
127
+ given.heavy.after();
128
+ return {};
129
+ }
130
+ const signal = AbortSignal.any([gone, given.ended]);
131
+ let answer = await holdAtTool(given.hold, signal, given.windowMs);
132
+ if (answer === "go") answer = await given.heavy.before(input, signal, given.windowMs);
133
+ if (answer === "defer") given.onDefer();
134
+ return given.answer(answer);
135
+ };
136
+ /** The name Claude Code gives an Ork tool: `mcp__ork__<name>`. */
137
+ const toolId = (name) => `mcp__ork__${name}`;
138
+ const text = (value) => ({ content: [{
139
+ type: "text",
140
+ text: value
141
+ }] });
142
+ const issueAt = (issue) => {
143
+ const where = z.core.toDotPath(issue.path);
144
+ return where === "" ? "" : `${where}: `;
145
+ };
146
+ /** Whether `issue` is about a field of the object `parent` is about. */
147
+ const isFieldOf = (issue, parent) => issue.path.length === parent.path.length + 1 && parent.path.every((key, index) => key === issue.path[index]);
148
+ const expectedField = (issue) => `; the field is \`${String(issue.path.at(-1))}\`${issue.code === "invalid_value" ? `, one of: ${issue.values.join(", ")}` : ""}`;
149
+ /**
150
+ * A failed parse as text a model can act on. A stray key is reported with the
151
+ * fields its object lacks, read from the schema's own issues: a wrong name is
152
+ * otherwise only a "missing" error that hides what was sent.
153
+ */
154
+ const readableIssues = (issues) => {
155
+ const strays = issues.filter((issue) => issue.code === "unrecognized_keys");
156
+ const lacking = issues.filter((issue) => issue.input === void 0 && strays.some((stray) => isFieldOf(issue, stray)));
157
+ return [...strays.map((stray) => `${issueAt(stray)}unknown field ${stray.keys.map((key) => `\`${key}\``).join(", ")}${lacking.filter((issue) => isFieldOf(issue, stray)).map((issue) => expectedField(issue)).join("")}`), ...issues.filter((issue) => issue.code !== "unrecognized_keys" && !lacking.includes(issue)).map((issue) => `${issueAt(issue)}${issue.message}`)].join("\n");
158
+ };
159
+ const defineTool = (definition) => ({
160
+ name: definition.name,
161
+ bind: (context) => {
162
+ if (!(definition.available?.(context) ?? true)) return null;
163
+ const shape = typeof definition.shape === "function" ? definition.shape(context) : definition.shape;
164
+ const schema = z.object(shape);
165
+ return {
166
+ name: definition.name,
167
+ description: typeof definition.description === "function" ? definition.description(context) : definition.description,
168
+ shape,
169
+ schema,
170
+ preapproved: definition.preapproved ?? true,
171
+ call: (args) => {
172
+ const parsed = schema.safeParse(args, { reportInput: true });
173
+ return parsed.success ? definition.run(context, parsed.data) : Promise.resolve({
174
+ ...text(readableIssues(parsed.error.issues)),
175
+ isError: true
176
+ });
177
+ }
178
+ };
179
+ }
180
+ });
181
+ // ../runtime/src/mcp-http.ts
182
+ /**
183
+ * @file A minimal MCP server over the streamable HTTP transport, for the
184
+ * `claude` CLI driver. It answers with plain JSON (no SSE stream), which the
185
+ * transport allows, and implements only what Ork needs: `initialize`, `ping`,
186
+ * `tools/list` and `tools/call`. Beside it, `/hold` answers the session's
187
+ * `PreToolUse` HTTP hook, which waits while the node is paused (ADR-041) or
188
+ * for a heavy command's slot, and its `PostToolUse` hook, which frees it.
189
+ */
190
+ const PATH = "/mcp";
191
+ const HOLD_PATH = "/hold";
192
+ const MAX_BODY = 1048576;
193
+ const MCP_PROTOCOL_FALLBACK = "2025-06-18";
194
+ const JsonRpcSchema = z.object({
195
+ jsonrpc: z.literal("2.0"),
196
+ id: z.union([z.string(), z.number()]).optional(),
197
+ method: z.string(),
198
+ params: z.record(z.string(), z.unknown()).optional()
199
+ });
200
+ /** Handles one JSON-RPC request; `null` for a notification (no reply). */
201
+ const handleRpc = async (tools, message) => {
202
+ const parsed = JsonRpcSchema.safeParse(message);
203
+ if (!parsed.success) return { error: {
204
+ code: -32600,
205
+ message: "invalid request"
206
+ } };
207
+ const { id, method, params } = parsed.data;
208
+ if (id === void 0) return null;
209
+ switch (method) {
210
+ case "initialize": {
211
+ const requested = params?.["protocolVersion"];
212
+ return { result: {
213
+ protocolVersion: typeof requested === "string" ? requested : MCP_PROTOCOL_FALLBACK,
214
+ capabilities: { tools: {} },
215
+ serverInfo: {
216
+ name: "ork",
217
+ version: "0.0.0"
218
+ }
219
+ } };
220
+ }
221
+ case "ping": return { result: {} };
222
+ case "tools/list": return { result: { tools: tools.map((t) => ({
223
+ name: t.name,
224
+ description: t.description,
225
+ inputSchema: z.toJSONSchema(t.schema)
226
+ })) } };
227
+ case "tools/call": {
228
+ const tool = tools.find((t) => t.name === params?.["name"]);
229
+ if (tool === void 0) return { error: {
230
+ code: -32602,
231
+ message: "unknown tool"
232
+ } };
233
+ try {
234
+ return { result: await tool.call(params?.["arguments"] ?? {}) };
235
+ } catch (error) {
236
+ return { result: {
237
+ content: [{
238
+ type: "text",
239
+ text: error instanceof Error ? error.message : "tool failed"
240
+ }],
241
+ isError: true
242
+ } };
243
+ }
244
+ }
245
+ default: return { error: {
246
+ code: -32601,
247
+ message: `unknown method ${method}`
248
+ } };
249
+ }
250
+ };
251
+ const authorized = (req, token) => {
252
+ const expected = Buffer.from(`Bearer ${token}`);
253
+ const given = Buffer.from(req.headers.authorization ?? "");
254
+ return given.length === expected.length && timingSafeEqual(given, expected);
255
+ };
256
+ const readBody = async (req) => {
257
+ const chunks = [];
258
+ let size = 0;
259
+ for await (const chunk of req) {
260
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
261
+ size += buffer.length;
262
+ if (size > MAX_BODY) return null;
263
+ chunks.push(buffer);
264
+ }
265
+ return Buffer.concat(chunks).toString("utf8");
266
+ };
267
+ const respond = (res, status, body) => {
268
+ if (body === void 0) {
269
+ res.writeHead(status).end();
270
+ return;
271
+ }
272
+ res.writeHead(status, { "content-type": "application/json" }).end(JSON.stringify(body));
273
+ };
274
+ /** The hook's POST: waits for `hold`; a closed connection gives up. */
275
+ const serveHold = async (req, res, hold) => {
276
+ if (req.method !== "POST") {
277
+ respond(res, 405);
278
+ return;
279
+ }
280
+ const body = await readBody(req);
281
+ if (body === null) {
282
+ respond(res, 413);
283
+ return;
284
+ }
285
+ let input;
286
+ try {
287
+ input = JSON.parse(body);
288
+ } catch {
289
+ input = void 0;
290
+ }
291
+ const gone = new AbortController();
292
+ res.once("close", () => {
293
+ gone.abort();
294
+ });
295
+ const output = await hold(gone.signal, input);
296
+ if (!gone.signal.aborted) respond(res, 200, output);
297
+ };
298
+ const serve = async (req, res, tools, token, hold) => {
299
+ if (!authorized(req, token)) {
300
+ respond(res, 401);
301
+ return;
302
+ }
303
+ if (req.url === HOLD_PATH && hold !== void 0) {
304
+ await serveHold(req, res, hold);
305
+ return;
306
+ }
307
+ if (req.url !== PATH) {
308
+ respond(res, 404);
309
+ return;
310
+ }
311
+ if (req.method !== "POST") {
312
+ respond(res, 405);
313
+ return;
314
+ }
315
+ const body = await readBody(req);
316
+ if (body === null) {
317
+ respond(res, 413);
318
+ return;
319
+ }
320
+ let message;
321
+ try {
322
+ message = JSON.parse(body);
323
+ } catch {
324
+ respond(res, 400);
325
+ return;
326
+ }
327
+ const id = message !== null && typeof message === "object" && "id" in message ? message.id : null;
328
+ const reply = await handleRpc(tools, message);
329
+ if (reply === null) {
330
+ respond(res, 202);
331
+ return;
332
+ }
333
+ respond(res, 200, {
334
+ jsonrpc: "2.0",
335
+ id,
336
+ ...reply
337
+ });
338
+ };
339
+ /**
340
+ * Serves Ork's tools to one `claude` CLI session: on 127.0.0.1 only, on a
341
+ * random port, behind a per-session bearer token.
342
+ */
343
+ const startOrkMcpHttp = async (tools, hold) => {
344
+ const token = randomBytes(24).toString("hex");
345
+ const http = createServer((req, res) => {
346
+ serve(req, res, tools, token, hold).catch(() => {
347
+ if (!res.headersSent) respond(res, 500);
348
+ });
349
+ });
350
+ await new Promise((resolve) => {
351
+ http.listen(0, "127.0.0.1", resolve);
352
+ });
353
+ const address = http.address();
354
+ const port = typeof address === "object" && address !== null ? address.port : 0;
355
+ return {
356
+ url: `http://127.0.0.1:${port}${PATH}`,
357
+ holdUrl: `http://127.0.0.1:${port}${HOLD_PATH}`,
358
+ token,
359
+ close: () => new Promise((resolve) => {
360
+ http.closeAllConnections();
361
+ http.close(() => {
362
+ resolve();
363
+ });
364
+ })
365
+ };
366
+ };
367
+ // ../runtime/src/provider-errors.ts
368
+ /**
369
+ * @file The provider-error classifier every driver's adapter uses (specs/
370
+ * runtime/session-ends.md): an error a model provider sent back (Anthropic,
371
+ * OpenAI-style, or a proxy such as OpenCode's `commandcode`) to one end kind.
372
+ * It reads the error's type or code first, its HTTP status next, and its
373
+ * words last, so a changed sentence rarely changes the kind.
374
+ */
375
+ /** Error types and codes the providers document, by the kind they mean. */
376
+ const TYPES = {
377
+ rate_limit_error: "rate_limited",
378
+ overloaded_error: "provider_unavailable",
379
+ api_error: "provider_unavailable",
380
+ timeout_error: "provider_unavailable",
381
+ authentication_error: "auth",
382
+ permission_error: "auth",
383
+ billing_error: "out_of_credits",
384
+ not_found_error: "model_unavailable",
385
+ request_too_large: "context_too_long",
386
+ insufficient_quota: "out_of_credits",
387
+ invalid_api_key: "auth",
388
+ context_length_exceeded: "context_too_long",
389
+ model_not_found: "model_unavailable",
390
+ rate_limit_exceeded: "rate_limited",
391
+ server_error: "provider_unavailable",
392
+ ProviderAuthError: "auth",
393
+ MessageAbortedError: "interrupted"
394
+ };
395
+ const TYPE = new RegExp(`\\b(${Object.keys(TYPES).join("|")})\\b`, "u");
396
+ /** HTTP statuses that decide by themselves; 400 needs the words. */
397
+ const statusKind = (status) => {
398
+ if (status === 401 || status === 403) return "auth";
399
+ if (status === 402) return "out_of_credits";
400
+ if (status === 404) return "model_unavailable";
401
+ if (status === 413) return "context_too_long";
402
+ if (status === 429) return "rate_limited";
403
+ if (status === 408 || status === 529 || status >= 500 && status < 600) return "provider_unavailable";
404
+ return null;
405
+ };
406
+ /** The words, when neither type nor status decided; the first match wins. */
407
+ const WORDS = [
408
+ ["plan_limit", /\busage limit reached\b|\bhit your (?:usage )?limit\b|\blimit will reset\b/iu],
409
+ ["context_too_long", /\bprompt is too long\b|\bcontext (?:length|window)\b|\bmaximum context\b|\btoo many tokens\b/iu],
410
+ ["out_of_credits", /\bcredit balance\b|\binsufficient (?:credits?|funds|balance|quota)\b|\bout of credits?\b|\bexceeded your current quota\b/iu],
411
+ ["rate_limited", /\brate.?limit(?:ed)?\b|\btoo many requests\b/iu],
412
+ ["auth", /\binvalid (?:x-)?api.?key\b|\bnot (?:logged|signed) in\b|\bplease run \/login\b|\bunauthori[sz]ed\b|\bOAuth token has expired\b/iu],
413
+ ["provider_unavailable", /\boverloaded\b|\bservice unavailable\b|\bbad gateway\b|\bECONNRESET\b|\bECONNREFUSED\b|\bETIMEDOUT\b|\bENOTFOUND\b|\bEAI_AGAIN\b|\bfetch failed\b|\bsocket hang up\b|\bnetwork error\b/iu],
414
+ ["model_unavailable", /\bassistant message prefill\b|\bInvalid `signature` in `thinking` block\b|\bmodel(?: \S+)? (?:not found|does not exist|is not supported)\b|\bdoes not support\b|\bunknown model\b/iu]
415
+ ];
416
+ /** Generic refusals: the provider would not serve the request as it was. */
417
+ const REFUSED$1 = /\binvalid_request_error\b|\binvalid_request\b/u;
418
+ /** A status written in the text, e.g. Claude Code's `API Error: 529 {…}`. */
419
+ const STATUS_IN_TEXT = /\bAPI Error:? (\d{3})\b|"(?:status|statusCode)"\s*:\s*(\d{3})\b|\b(?:status|HTTP)[ :]+(\d{3})\b/u;
420
+ const statusIn = (text) => {
421
+ const found = STATUS_IN_TEXT.exec(text);
422
+ const digits = found?.[1] ?? found?.[2] ?? found?.[3];
423
+ return digits === void 0 ? void 0 : Number(digits);
424
+ };
425
+ const classed = (kind) => ({
426
+ kind,
427
+ message: END_KINDS[kind].message({})
428
+ });
429
+ /**
430
+ * The end kind a provider's error means, with Ork's sentence for it; `null`
431
+ * when nothing in it is recognized. `status` is the HTTP status when the caller
432
+ * has it; otherwise one written in the text is read.
433
+ */
434
+ const classifyProviderError = (text, status) => {
435
+ const type = TYPE.exec(text)?.[1];
436
+ if (type !== void 0) return classed(TYPES[type] ?? "unknown");
437
+ const code = status ?? statusIn(text);
438
+ const byStatus = code === void 0 ? null : statusKind(code);
439
+ if (byStatus !== null) return classed(byStatus);
440
+ const byWords = WORDS.find(([, pattern]) => pattern.test(text))?.[0];
441
+ if (byWords !== void 0) return classed(byWords);
442
+ return REFUSED$1.test(text) || code === 400 ? classed("model_unavailable") : null;
443
+ };
444
+ // ../runtime/src/session-end.ts
445
+ /**
446
+ * @file How a session ended, as its adapter's decoder and the session runner
447
+ * settle it (specs/runtime/session-ends.md): a decoder names the signal only
448
+ * it can see (a turn cap, an exit code, a provider's error), and the report
449
+ * the session handed in decides the rest. The runner then writes the end in
450
+ * the node's numbers, its detail scrubbed with the feedback redactor. A
451
+ * session the driver could not run ends as `driverFailure` says.
452
+ */
453
+ /** The end a report gives: done, a reported failure, or none handed in. */
454
+ const reportEnd = (report) => {
455
+ if (report === null) return endFor("no_report");
456
+ return endFor(report.status === "fail" ? "reported_failure" : "done");
457
+ };
458
+ /**
459
+ * A session's end: a report it handed in decides it (the node's result is that
460
+ * report), then the decoder's `signal`, then `no_report`.
461
+ */
462
+ const sessionEnd = (signal, report) => report !== null || signal === null ? reportEnd(report) : signal;
463
+ /**
464
+ * The end an error's text means: the provider classifier's kind, or `fallback`
465
+ * when it recognizes nothing. The text is the detail either way.
466
+ */
467
+ const errorEnd = (text, fallback = "unknown", status) => endFor(classifyProviderError(text, status)?.kind ?? fallback, {}, text);
468
+ /**
469
+ * The outcome with its end settled: a report decides it, also one handed in
470
+ * through Ork's `report` tool after the decoder's end said none; the message is
471
+ * written with the node's numbers; the detail is scrubbed and capped. An
472
+ * adapter whose decoder sets no end gets one read from the outcome.
473
+ */
474
+ const settleEnd = (outcome, facts, scrub) => {
475
+ const end = outcome.report === null ? outcome.end ?? outcomeEnd(outcome) : reportEnd(outcome.report);
476
+ return {
477
+ ...outcome,
478
+ end: endFor(end.kind, facts, end.detail, scrub)
479
+ };
480
+ };
481
+ /**
482
+ * The kind of a failure to run the driver: its program missing, else what a
483
+ * provider's words in it say, else a crash.
484
+ */
485
+ const failureKind = (error) => {
486
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return "tool_missing";
487
+ const text = error instanceof Error ? error.message : String(error);
488
+ if (/\bENOENT\b/u.test(text)) return "tool_missing";
489
+ return classifyProviderError(text)?.kind ?? "driver_crashed";
490
+ };
491
+ /**
492
+ * A finished event for a session the driver could not run, ended as `kind` with
493
+ * `message` (Ork's words or the tool's, masked) as its detail.
494
+ */
495
+ const driverFailure = (message, kind, scope) => ({
496
+ type: "session.finished",
497
+ outcome: {
498
+ subtype: "error_driver",
499
+ end: endFor(kind, scope.facts, kind === "interrupted" ? void 0 : message, scope.scrub),
500
+ sessionId: "",
501
+ report: null,
502
+ text: "",
503
+ usage: {
504
+ costUsd: 0,
505
+ inputTokens: 0,
506
+ outputTokens: 0,
507
+ cacheReadTokens: 0,
508
+ cacheWriteTokens: 0,
509
+ turns: 0,
510
+ durationMs: 0
511
+ },
512
+ errors: [message]
513
+ }
514
+ });
515
+ // ../runtime/src/session-leg.ts
516
+ /**
517
+ * @file One process (leg) of a session the runner drives: it writes the queued
518
+ * stdin lines, yields the adapter's decoded events, and says how it ended. A
519
+ * session is one leg, or several when a paused node's tool call was deferred
520
+ * or a message waits for a tool that takes none mid-session; a resumed leg
521
+ * carries what the legs before it spent, and its start is not news.
522
+ */
523
+ const REASON_LIMIT = 500;
524
+ /** Marks a session's start with the driver that runs it. */
525
+ const withDriver = (event, driver) => event.type === "session.started" ? {
526
+ ...event,
527
+ driver
528
+ } : event;
529
+ /**
530
+ * A session's outcome with its report (the one Ork's `report` tool accepted,
531
+ * when the session has that tool) and its end settled.
532
+ */
533
+ const settled = (outcome, input) => settleEnd(input.reported === void 0 ? outcome : {
534
+ ...outcome,
535
+ report: input.reported()
536
+ }, input.scope.facts, input.scope.scrub);
537
+ /** The kinds of a session that ended well, so a queued message continues it. */
538
+ const wentWell = (kind) => kind === "done" || kind === "reported_failure" || kind === "no_report";
539
+ /**
540
+ * What one decoded line yields, and how the leg ends if it does: a result is
541
+ * the session's end, unless the hold hook deferred a call or a message waits; a
542
+ * resumed leg's start is not news, and its end carries what the legs before it
543
+ * spent.
544
+ */
545
+ const legStep = (decoded, input) => {
546
+ const { adapter, carried } = input;
547
+ const deferred = input.deferred() && decoded.deferred === true;
548
+ const events = [];
549
+ let ended;
550
+ for (const said of decoded.events) {
551
+ const event = said.type === "session.finished" ? {
552
+ ...said,
553
+ outcome: settled(said.outcome, input)
554
+ } : said;
555
+ if (event.type === "session.started" && carried !== void 0) continue;
556
+ if (event.type !== "session.finished") events.push(withDriver(event, adapter.driver));
557
+ else if (deferred) ended = {
558
+ kind: "deferred",
559
+ sessionId: event.outcome.sessionId,
560
+ usage: event.outcome.usage,
561
+ last: event
562
+ };
563
+ else {
564
+ const usage = carried === void 0 ? event.outcome.usage : adapter.usage.combine(carried, event.outcome.usage);
565
+ const last = {
566
+ ...event,
567
+ outcome: {
568
+ ...event.outcome,
569
+ usage
570
+ }
571
+ };
572
+ if (input.withhold() && wentWell(event.outcome.end?.kind)) ended = {
573
+ kind: "continued",
574
+ sessionId: event.outcome.sessionId,
575
+ usage: event.outcome.usage,
576
+ last
577
+ };
578
+ else {
579
+ events.push(last);
580
+ ended = { kind: "finished" };
581
+ }
582
+ }
583
+ }
584
+ return ended === void 0 ? { events } : {
585
+ events,
586
+ ended
587
+ };
588
+ };
589
+ /** Noise a Node crash ends stderr with, or a stack frame: never the reason. */
590
+ const isNoise = (line) => /^Node\.js v\d/u.test(line) || /^\s+at /u.test(line);
591
+ /**
592
+ * Why a process ended without a result: the last block of its stderr as one
593
+ * masked, capped line (the reason reaches the terminal, which is not masked).
594
+ */
595
+ const failureReason = (name, stderr, code) => {
596
+ const block = stderr.split("\n").filter((line) => !isNoise(line)).join("\n").split(/\n\s*\n/u).map((text) => text.trim().replaceAll(/\s*\n\s*/gu, " ")).findLast((text) => text !== "");
597
+ return block === void 0 ? `${name} exited with code ${String(code)}` : maskSecrets(block).slice(0, REASON_LIMIT);
598
+ };
599
+ /** Runs one process of a session, yielding its events; returns how it ended. */
600
+ const runLeg = async function* (input) {
601
+ const [bin, args] = startAt(input.priority, input.bin, input.args, input.env);
602
+ const child = spawn(bin, args, {
603
+ cwd: input.cwd,
604
+ env: { ...input.env },
605
+ stdio: [
606
+ "pipe",
607
+ "pipe",
608
+ "pipe"
609
+ ]
610
+ });
611
+ lowerChild(input.priority, child.pid);
612
+ const exited = new Promise((resolve) => {
613
+ child.on("close", (code, signal) => {
614
+ resolve({
615
+ code,
616
+ signal
617
+ });
618
+ });
619
+ });
620
+ const spawnError = new Promise((resolve) => {
621
+ child.on("error", resolve);
622
+ });
623
+ let stderr = "";
624
+ child.stderr.setEncoding("utf8");
625
+ child.stderr.on("data", (chunk) => {
626
+ stderr = (stderr + chunk).slice(-16384);
627
+ });
628
+ const write = (line) => {
629
+ if (child.stdin.writable) child.stdin.write(line);
630
+ };
631
+ let stopped = false;
632
+ input.attach({
633
+ write,
634
+ stop: () => {
635
+ stopped = true;
636
+ child.kill("SIGINT");
637
+ }
638
+ });
639
+ for (const line of input.pending.splice(0)) write(line);
640
+ if (input.adapter.command.input === void 0) child.stdin.end();
641
+ let outcome;
642
+ const { decoder } = input;
643
+ const decode = decoder?.decode ?? input.adapter.decode;
644
+ try {
645
+ for await (const line of createInterface({ input: child.stdout })) {
646
+ const { events, ended } = legStep(decode(line), input);
647
+ yield* events;
648
+ if (ended !== void 0) {
649
+ child.stdin.end();
650
+ outcome = ended;
651
+ }
652
+ }
653
+ const end = await Promise.race([exited, spawnError.then((error) => error)]);
654
+ if (outcome !== void 0) return outcome;
655
+ if (decoder !== void 0 && !(end instanceof Error) && !stopped) {
656
+ const { events, ended } = legStep({ events: decoder.exit(end.code) }, input);
657
+ yield* events;
658
+ if (ended !== void 0) return ended;
659
+ }
660
+ if (end instanceof Error) return {
661
+ kind: "failed",
662
+ reason: `could not start ${input.bin}: ${end.message}`,
663
+ end: failureKind(end)
664
+ };
665
+ if (stopped || end.signal !== null) return {
666
+ kind: "failed",
667
+ reason: "interrupted",
668
+ end: "interrupted"
669
+ };
670
+ const reason = failureReason(input.adapter.command.bin, stderr, end.code);
671
+ return {
672
+ kind: "failed",
673
+ reason,
674
+ end: classifyProviderError(reason)?.kind ?? "driver_crashed"
675
+ };
676
+ } finally {
677
+ input.attach();
678
+ if (child.exitCode === null) child.kill();
679
+ }
680
+ };
681
+ // ../runtime/src/tools/ask-user.ts
682
+ const askUserTool = defineTool({
683
+ name: "ask_user",
684
+ description: "Ask the user to decide something only they can decide. Give two to four options and the one you recommend. The run waits for the answer.",
685
+ shape: {
686
+ question: z.string().describe("One clear question."),
687
+ options: z.array(z.object({
688
+ id: z.string().describe("A short id, e.g. 'keep-api'."),
689
+ label: z.string().optional(),
690
+ description: z.string().optional().describe("What choosing it means.")
691
+ })).min(2).max(4),
692
+ recommended: z.string().optional().describe("The id you recommend."),
693
+ context: z.string().optional().describe("What the user needs to know to decide, in a few sentences.")
694
+ },
695
+ run: async ({ nodeId, ports }, args) => {
696
+ let note;
697
+ let owner;
698
+ const question = {
699
+ nodeId,
700
+ onNote: (given) => {
701
+ note = given;
702
+ },
703
+ onOwner: (given) => {
704
+ owner = given;
705
+ },
706
+ question: args.question,
707
+ options: args.options.map((o) => {
708
+ const choice = { id: o.id };
709
+ if (o.label !== void 0) choice.label = o.label;
710
+ if (o.description !== void 0) choice.description = o.description;
711
+ return choice;
712
+ })
713
+ };
714
+ if (args.recommended !== void 0) question.recommended = args.recommended;
715
+ if (args.context !== void 0) question.context = args.context;
716
+ const answer = await ports.human.ask(question);
717
+ const who = owner === void 0 ? "The user" : `Your owner, \`${owner}\`,`;
718
+ return text(note === void 0 ? `${who} chose: ${answer}` : `${who} chose: ${answer}\nThey said: ${note}`);
719
+ }
720
+ });
721
+ // ../runtime/src/tools/claims.ts
722
+ /**
723
+ * @file `claim` and `release`: a session's side of the run's claims. A node
724
+ * claims a file, folder or other resource before changing it, so nodes
725
+ * running at the same time do not change the same thing; a refused claim says
726
+ * who holds it. Only sessions that may write while another does get them.
727
+ */
728
+ const contended = ({ ports }) => ports.claims?.contended() === true;
729
+ const resource = z.string().min(1).describe("A path from the project root (a folder covers everything in it), or a name for something else shared, e.g. 'db:migrations'.");
730
+ const claimTool = defineTool({
731
+ name: "claim",
732
+ description: "Claim a file, folder or other shared resource before you change it, so other nodes running in this run leave it alone. If another node holds it (or something overlapping), you are told who; Ork tells you when it is released.",
733
+ shape: { resource },
734
+ available: contended,
735
+ run: ({ ports }, args) => {
736
+ const claimed = ports.claims?.claim(args.resource);
737
+ if (claimed === void 0) return Promise.resolve(text("No claims."));
738
+ if (!claimed.ok) return Promise.resolve(text(`Not claimed: ${claimed.error}.`));
739
+ return Promise.resolve(text(claimed.already ? `You already hold ${claimed.resource}.` : `Claimed ${claimed.resource}.`));
740
+ }
741
+ });
742
+ const releaseTool = defineTool({
743
+ name: "release",
744
+ description: "Release a claim you are done with, so other nodes can take it. Ork releases your remaining claims when you finish.",
745
+ shape: { resource },
746
+ available: contended,
747
+ run: ({ ports }, args) => {
748
+ const released = ports.claims?.release(args.resource);
749
+ if (released === void 0) return Promise.resolve(text("No claims."));
750
+ return Promise.resolve(text(released.ok ? `Released ${released.resource}.` : `Not released: ${released.error}.`));
751
+ }
752
+ });
753
+ // ../runtime/src/tools/delegate.ts
754
+ const BRIEF = z.string().min(1).describe("Self-contained: the goal, the files that matter, and what done looks like. The child does not see your conversation.");
755
+ /** What the leader reads back: each report without the usage details. */
756
+ const reportLine = (report) => {
757
+ const { usage, ...output } = report.output;
758
+ const cost = usage !== null && typeof usage === "object" && "costUsd" in usage ? usage.costUsd : void 0;
759
+ return {
760
+ child: report.child,
761
+ ok: report.ok,
762
+ ...output,
763
+ ...cost === void 0 ? {} : { costUsd: cost },
764
+ ...report.ok || report.error === void 0 || report.error === output["summary"] ? {} : { error: report.error }
765
+ };
766
+ };
767
+ const delegationText = (delegation) => text(JSON.stringify({
768
+ reports: delegation.reports.map(reportLine),
769
+ ...delegation.notes.length > 0 ? { notes: delegation.notes } : {}
770
+ }));
771
+ /** Why a child the leader owns isn't offered, so it doesn't go looking. */
772
+ const fannedNote = (fanned) => fanned.length === 0 ? "" : ` Not listed: ${fanned.join(", ")}; such a node runs once per item of a list, from its edges, so it can't be delegated to.`;
773
+ const delegateTool = defineTool({
774
+ name: "delegate",
775
+ description: ({ ports }) => `Hand tasks to nodes you own. Tasks in one call run in parallel; the call returns every child's report when all are done. Ork merges a passing child's branch into yours.${fannedNote(ports.ownership?.fanned ?? [])}`,
776
+ shape: ({ ports }) => {
777
+ const ids = ports.ownership?.children.map((child) => child.id) ?? [];
778
+ return { tasks: z.array(z.strictObject({
779
+ child: z.enum(ids).describe(`The id of a node you own: ${ids.join(", ")}.`),
780
+ brief: BRIEF
781
+ })).min(1) };
782
+ },
783
+ available: ({ ports }) => (ports.ownership?.children.length ?? 0) > 0,
784
+ run: async ({ ports }, args) => {
785
+ if (ports.ownership === void 0) return text("You own no nodes.");
786
+ return delegationText(await ports.ownership.delegate(args.tasks));
787
+ }
788
+ });
789
+ const spawnWorkerTool = defineTool({
790
+ name: "spawn_worker",
791
+ description: "Start new instances of a child worker for work that splits into independent parts. Each runs in its own worktree; the call returns their reports when all are done.",
792
+ shape: ({ ports }) => {
793
+ const ids = ports.ownership?.templates ?? [];
794
+ return { tasks: z.array(z.strictObject({
795
+ template: z.enum(ids).describe(`The id of a child worker you own: ${ids.join(", ")}.`),
796
+ brief: BRIEF
797
+ })).min(1) };
798
+ },
799
+ available: ({ ports }) => ports.ownership !== void 0 && ports.ownership.templates.length > 0 && ports.ownership.workersLeft() > 0,
800
+ run: async ({ ports }, args) => {
801
+ if (ports.ownership === void 0) return text("You own no workers.");
802
+ return delegationText(await ports.ownership.spawn(args.tasks));
803
+ }
804
+ });
805
+ // ../runtime/src/tools/feedback.ts
806
+ /**
807
+ * @file `feedback`: a session's way to report what got in its way, kept on the
808
+ * user's machine (specs/feedback.md). Every session inside a run gets it; Ork
809
+ * caps reports per session and drops a run's duplicates.
810
+ */
811
+ const feedbackTool = defineTool({
812
+ name: "feedback",
813
+ description: "Report something that materially got in the way of your task: a tool that failed or misled you, context or instructions that were missing or wrong, a skill that did not fit, or a flaw in the workflow. One line plus a short body; at most a few per session. Do not report routine work or your own mistakes.",
814
+ shape: {
815
+ kind: z.enum(FEEDBACK_KINDS),
816
+ target: z.enum(FEEDBACK_TARGETS).optional().describe("What it is about: ork (the harness and its tools), workflow (this workflow's briefs, models, budgets, loops), or project (AGENTS.md, CLAUDE.md, skills, docs). Default: from the kind."),
817
+ severity: z.enum(FEEDBACK_SEVERITIES).optional(),
818
+ title: z.string().min(1).describe("One line."),
819
+ body: z.string().optional().describe("A few sentences: what happened and what would have helped."),
820
+ evidence: z.array(z.string()).optional().describe("File paths, tool names; never pasted file contents.")
821
+ },
822
+ available: ({ ports }) => ports.feedback !== void 0,
823
+ run: ({ ports }, args) => {
824
+ const draft = {
825
+ kind: args.kind,
826
+ title: args.title
827
+ };
828
+ if (args.target !== void 0) draft.target = args.target;
829
+ if (args.severity !== void 0) draft.severity = args.severity;
830
+ if (args.body !== void 0) draft.body = args.body;
831
+ if (args.evidence !== void 0) draft.evidence = args.evidence;
832
+ const sent = ports.feedback?.report(draft);
833
+ if (sent === void 0) return Promise.resolve(text("No feedback here."));
834
+ return Promise.resolve(text(sent.ok ? "Recorded. Thank you; carry on with your task." : `Not recorded: ${sent.error}.`));
835
+ }
836
+ });
837
+ // ../runtime/src/tools/input.ts
838
+ /**
839
+ * @file `read_input`: what the workflow sent a node back with, whole. A loop's
840
+ * re-entry note shows the first 8 KiB of it; when that cut it, the session
841
+ * gets this tool and reads the rest a page at a time, so a long review's last
842
+ * findings reach the node.
843
+ */
844
+ /** The characters one call returns. */
845
+ const INPUT_PAGE = 32e3;
846
+ const readInputTool = defineTool({
847
+ name: "read_input",
848
+ description: `Read what the workflow sent your task back with, whole, when Ork's message cut it. Returns up to ${INPUT_PAGE.toLocaleString("en-US")} characters, from \`from\`.`,
849
+ shape: { from: z.number().int().nonnegative().optional().describe("The character to start at. Default: 0.") },
850
+ available: ({ ports }) => ports.input !== void 0,
851
+ run: ({ ports }, args) => {
852
+ const whole = ports.input ?? "";
853
+ const from = Math.min(args.from ?? 0, whole.length);
854
+ const end = Math.min(from + INPUT_PAGE, whole.length);
855
+ const where = end < whole.length ? `[characters ${String(from)}–${String(end)} of ${String(whole.length)}; call read_input with from: ${String(end)} for the rest]` : `[characters ${String(from)}–${String(end)} of ${String(whole.length)}: the end]`;
856
+ return Promise.resolve(text(`${whole.slice(from, end)}\n\n${where}`));
857
+ }
858
+ });
859
+ // ../runtime/src/tools/messages.ts
860
+ /**
861
+ * @file `send_message`, `read_inbox` and `answer`: a session's side of the
862
+ * run's message bus. A message lands in the recipient's inbox and a live
863
+ * session gets a one-line notice; `answer` replies to a question Ork asked an
864
+ * owner about a node it owns. Only sessions inside a run get them.
865
+ */
866
+ /**
867
+ * Peers are mutual: a node no one may write to has no inbox to read, unless it
868
+ * may be linked later, since a session's tools are set when it starts.
869
+ */
870
+ const hasPeers = ({ ports }) => (ports.mailbox?.peers().length ?? 0) > 0 || ports.mailbox?.linkable() === true;
871
+ /** A program or a rule may write to a node with no one to write to itself. */
872
+ const hasInbox = (context) => hasPeers(context) || context.ports.mailbox?.hears() === true;
873
+ const MAX_WAIT_SECONDS = 3600;
874
+ const DEFAULT_WAIT_SECONDS = 300;
875
+ const inboxText = (unread) => JSON.stringify({ messages: unread.map((m) => ({
876
+ from: m.from,
877
+ ...m.via === void 0 ? {} : { via: m.via },
878
+ body: m.body
879
+ })) });
880
+ const sendMessageTool = defineTool({
881
+ name: "send_message",
882
+ description: "Send a short message to a node you can reach: your owner, a node you own, one you share a declared channel with, or a node of another run the user linked you to. It sees a notice and reads the message with read_inbox. Use it for what they need to know now; your report still goes to your owner at the end.",
883
+ shape: {
884
+ to: z.string().describe("The node's id, or <run>/<node> for a linked node."),
885
+ body: z.string().min(1).describe("Self-contained: the recipient does not see your work.")
886
+ },
887
+ available: hasPeers,
888
+ run: ({ ports }, args) => {
889
+ const sent = ports.mailbox?.send(args.to, args.body);
890
+ if (sent === void 0) return Promise.resolve(text("No message bus."));
891
+ if (!sent.ok) return Promise.resolve(text(`Not sent: ${sent.error}.`));
892
+ return Promise.resolve(text(sent.delivered === "now" ? `Sent to ${args.to}; it sees a notice at its next step.` : `Sent to ${args.to}; it is not running now and sees the message if it starts again.`));
893
+ }
894
+ });
895
+ const readInboxTool = defineTool({
896
+ name: "read_inbox",
897
+ description: "Read your unread messages from other nodes, oldest first. Ork tells you when one arrives.",
898
+ shape: {},
899
+ available: hasInbox,
900
+ run: ({ ports }) => {
901
+ const unread = ports.mailbox?.read() ?? [];
902
+ return Promise.resolve(text(unread.length === 0 ? "No unread messages." : inboxText(unread)));
903
+ }
904
+ });
905
+ const waitForMessagesTool = defineTool({
906
+ name: "wait_for_messages",
907
+ description: "Wait for a message from another node when you cannot go on without its reply, instead of ending your session. Returns what arrived, or says none did in time. You hold no session slot while you wait.",
908
+ shape: { timeoutSeconds: z.number().int().positive().max(MAX_WAIT_SECONDS).optional().describe(`How long to wait. Default: ${String(DEFAULT_WAIT_SECONDS)}.`) },
909
+ available: hasInbox,
910
+ run: async ({ ports }, args) => {
911
+ const seconds = args.timeoutSeconds ?? DEFAULT_WAIT_SECONDS;
912
+ await ports.mailbox?.wait(seconds * 1e3);
913
+ const unread = ports.mailbox?.read() ?? [];
914
+ return text(unread.length === 0 ? `No message yet, after waiting up to ${String(seconds)} seconds. Ork may have told you something; if not, wait again or go on.` : inboxText(unread));
915
+ }
916
+ });
917
+ const answerTool = defineTool({
918
+ name: "answer",
919
+ description: "Answer a question Ork asked you as the owner of a node, such as a loop of yours that ran out of rounds. The question's id and options are in Ork's message.",
920
+ shape: {
921
+ question: z.string().describe("The question's id, e.g. 'q1'."),
922
+ choice: z.string().describe("One of its options."),
923
+ note: z.string().optional().describe("Why, in a sentence or two; it is recorded with your answer.")
924
+ },
925
+ available: ({ ports }) => ports.mailbox !== void 0 && ports.ownership !== void 0,
926
+ run: ({ ports }, args) => {
927
+ const answered = ports.mailbox?.answer(args.question, args.choice, args.note);
928
+ if (answered === void 0) return Promise.resolve(text("No questions."));
929
+ return Promise.resolve(text(answered.ok ? `Answered ${args.question}: ${args.choice}.` : `Not answered: ${answered.error}.`));
930
+ }
931
+ });
932
+ // ../runtime/src/tools/permission.ts
933
+ /**
934
+ * @file `permission`: the `claude` CLI's `--permission-prompt-tool`, which
935
+ * sends each permission prompt to the run's `HumanChannel`.
936
+ */
937
+ const permissionTool = defineTool({
938
+ name: "permission",
939
+ description: "Ork's permission prompt for the claude CLI.",
940
+ shape: {
941
+ tool_name: z.string(),
942
+ input: z.record(z.string(), z.unknown()),
943
+ tool_use_id: z.string().optional()
944
+ },
945
+ preapproved: false,
946
+ available: ({ driver }) => driver === "claude-code",
947
+ run: async ({ nodeId, ports }, args) => {
948
+ const decision = await ports.human.permit({
949
+ nodeId,
950
+ toolName: args.tool_name,
951
+ input: args.input
952
+ });
953
+ return text(JSON.stringify(decision.allow ? {
954
+ behavior: "allow",
955
+ updatedInput: args.input
956
+ } : {
957
+ behavior: "deny",
958
+ message: decision.message
959
+ }));
960
+ }
961
+ });
962
+ // ../runtime/src/tools/report.ts
963
+ /**
964
+ * @file The `report` tool: how a session hands Ork its final report when its
965
+ * driver cannot hold the final answer to a JSON Schema itself (`reportSchema`
966
+ * false, RFC-014). The report is checked against the node's report schema
967
+ * with zod; a report that breaks it comes back to the model as a tool error
968
+ * naming each violation, so it can fix it and call again. One that fits is
969
+ * kept as the session's report, and the session runner ends the session with
970
+ * it instead of what the tool's last line says.
971
+ */
972
+ const REPORT_TOOL = "report";
973
+ /** Whether the driver holds a session's final answer to its schema itself. */
974
+ const checksItsReport = (driver) => AGENT_DRIVERS.find((d) => d.id === driver)?.capabilities.reportSchema ?? true;
975
+ /**
976
+ * The report schema as zod. shortcut: a schema zod cannot read takes any
977
+ * object, and `parseReport` still demands a status and a summary; upgrade if a
978
+ * node's `output.schema` ever uses JSON Schema zod does not support.
979
+ */
980
+ const reportValidator = (schema) => {
981
+ try {
982
+ return z.fromJSONSchema(schema);
983
+ } catch {
984
+ return z.record(z.string(), z.unknown());
985
+ }
986
+ };
987
+ const failed = (why) => ({
988
+ ...text(`The report was not accepted; fix it and call ${REPORT_TOOL} again.\n${why}`),
989
+ isError: true
990
+ });
991
+ const bound = (report) => {
992
+ const schema = reportValidator(report.schema);
993
+ return {
994
+ name: REPORT_TOOL,
995
+ description: "Hand in your final report: call this once your work is done, as your last step, with the report the task asks for. A report that does not fit comes back with what to fix; call again with the fixed report. The last report accepted is the one your owner gets.",
996
+ shape: schema instanceof z.ZodObject ? schema.shape : {},
997
+ schema,
998
+ preapproved: true,
999
+ call: (args) => {
1000
+ const parsed = schema.safeParse(args, { reportInput: true });
1001
+ if (!parsed.success) return Promise.resolve(failed(readableIssues(parsed.error.issues)));
1002
+ const kept = parseReport(parsed.data);
1003
+ if (kept === null) return Promise.resolve(failed("A report needs a `status` and a `summary`."));
1004
+ report.submit(kept);
1005
+ return Promise.resolve(text("Report accepted. End your turn now; there is nothing more to do."));
1006
+ }
1007
+ };
1008
+ };
1009
+ // ../runtime/src/tools/registry.ts
1010
+ /**
1011
+ * @file The registry of Ork's MCP tools. Both drivers serve what it binds for a
1012
+ * session; a new tool is one file here and one line in `ORK_TOOLS`.
1013
+ */
1014
+ const ORK_TOOLS = [
1015
+ askUserTool,
1016
+ permissionTool,
1017
+ delegateTool,
1018
+ spawnWorkerTool,
1019
+ sendMessageTool,
1020
+ readInboxTool,
1021
+ waitForMessagesTool,
1022
+ answerTool,
1023
+ readInputTool,
1024
+ claimTool,
1025
+ releaseTool,
1026
+ feedbackTool,
1027
+ {
1028
+ name: REPORT_TOOL,
1029
+ bind: (context) => context.report === void 0 || checksItsReport(context.driver) ? null : bound(context.report)
1030
+ }
1031
+ ];
1032
+ /** The tools one session gets. */
1033
+ const orkTools = (context) => ORK_TOOLS.flatMap((tool) => tool.bind(context) ?? []);
1034
+ /** Their Claude Code names, for the session's allowed tools. */
1035
+ const preapprovedTools = (tools) => tools.filter((t) => t.preapproved).map((t) => toolId(t.name));
1036
+ // ../runtime/src/session-runner.ts
1037
+ /**
1038
+ * @file The session runner: one agent CLI session, driven through an adapter's
1039
+ * strategies (`adapter.ts`), written once for every driver. It serves Ork's
1040
+ * tools on a local MCP endpoint, wires the session to it, and runs the tool
1041
+ * as one or more processes (legs). A paused node's tool call is held, and
1042
+ * past the hold window deferred; the next leg resumes the session once the
1043
+ * node continues (ADR-041). A tool that takes no message mid-session gets one
1044
+ * at the next leg, which resumes the session with it. A driver that cannot
1045
+ * hold the final answer to the report schema gets Ork's `report` tool, and
1046
+ * the session ends with the report it accepted. Interrupting stops the
1047
+ * running process.
1048
+ */
1049
+ /**
1050
+ * What a session's end is written with: the node's limits and program, and its
1051
+ * folder and the home folder taken out of the detail.
1052
+ */
1053
+ const scopeOf = (spec, bin, env) => ({
1054
+ facts: {
1055
+ ...spec.maxTurns === void 0 ? {} : { maxTurns: spec.maxTurns },
1056
+ ...spec.budgetUsd === void 0 ? {} : { budgetUsd: spec.budgetUsd },
1057
+ program: path.basename(bin)
1058
+ },
1059
+ scrub: {
1060
+ roots: [spec.cwd],
1061
+ home: env["HOME"] ?? homedir()
1062
+ }
1063
+ });
1064
+ /** A spec that resumes `sessionId`; a fork's later legs continue the fork. */
1065
+ const resumed = (spec, sessionId) => ({
1066
+ ...spec,
1067
+ resume: sessionId,
1068
+ fork: false
1069
+ });
1070
+ /**
1071
+ * Wires a session to Ork's endpoint, yielding a `session.waiting` event each
1072
+ * time the wiring says it waits (a lease waits for its checkout).
1073
+ */
1074
+ const wireSaying = async function* (adapter, spec, endpoint, signal, options) {
1075
+ const waits = [];
1076
+ let woke;
1077
+ const woken = () => new Promise((resolve) => {
1078
+ woke = () => {
1079
+ resolve(false);
1080
+ };
1081
+ });
1082
+ const wiring = adapter.mcp.wire(spec, endpoint, {
1083
+ waiting: (reason) => {
1084
+ waits.push(reason);
1085
+ woke?.();
1086
+ },
1087
+ signal,
1088
+ bin: options.bin ?? adapter.command.bin,
1089
+ env: options.env ?? process.env,
1090
+ ...options.runs === void 0 ? {} : { runs: options.runs }
1091
+ });
1092
+ const done = wiring.then(() => true, () => true);
1093
+ for (;;) {
1094
+ const finished = waits.length === 0 && await Promise.race([done, woken()]);
1095
+ for (const reason of waits.splice(0)) yield {
1096
+ type: "session.waiting",
1097
+ reason
1098
+ };
1099
+ if (finished) return wiring;
1100
+ }
1101
+ };
1102
+ /**
1103
+ * A runtime that drives `adapter`'s tool, as the user installed and logged it
1104
+ * in. Ork never reads or passes its credentials.
1105
+ */
1106
+ const createSessionRuntime = (adapter, options = {}) => ({
1107
+ driver: adapter.driver,
1108
+ start: (spec, ports) => {
1109
+ const { input } = adapter.command;
1110
+ const pending = input === void 0 ? [] : [input(spec.prompt, false)];
1111
+ const queued = [];
1112
+ let io;
1113
+ const ended = new AbortController();
1114
+ /** The hold hook deferred a call in the running process. */
1115
+ let deferring = false;
1116
+ /** The last report the `report` tool accepted, across legs. */
1117
+ let reported = null;
1118
+ const { hold } = ports;
1119
+ const hook = hold === void 0 ? void 0 : adapter.hold;
1120
+ const heavy = heavyHook(ports.machine);
1121
+ ended.signal.addEventListener("abort", () => {
1122
+ heavy.release();
1123
+ }, { once: true });
1124
+ const bin = options.bin ?? adapter.command.bin;
1125
+ const scope = scopeOf(spec, bin, options.env ?? process.env);
1126
+ const events = async function* () {
1127
+ const tools = orkTools({
1128
+ nodeId: spec.nodeId,
1129
+ driver: adapter.driver,
1130
+ ports,
1131
+ report: {
1132
+ schema: spec.reportSchema,
1133
+ submit: (report) => {
1134
+ reported = report;
1135
+ }
1136
+ }
1137
+ });
1138
+ const byTool = tools.some((tool) => tool.name === REPORT_TOOL);
1139
+ const mcp = await startOrkMcpHttp(tools, hold === void 0 || hook === void 0 ? void 0 : toolHook({
1140
+ hold,
1141
+ heavy,
1142
+ answer: hook.answer,
1143
+ windowMs: options.holdWindowMs ?? hook.windowMs,
1144
+ ended: ended.signal,
1145
+ onDefer: () => {
1146
+ deferring = true;
1147
+ }
1148
+ }));
1149
+ const endpoint = hook === void 0 ? {
1150
+ url: mcp.url,
1151
+ token: mcp.token
1152
+ } : mcp;
1153
+ const allowed = preapprovedTools(tools);
1154
+ let wired;
1155
+ try {
1156
+ try {
1157
+ wired = yield* wireSaying(adapter, spec, endpoint, ended.signal, options);
1158
+ } catch (error) {
1159
+ yield driverFailure(`could not reach Ork's tools: ${error instanceof Error ? error.message : String(error)}`, failureKind(error), scope);
1160
+ return;
1161
+ }
1162
+ const env = {
1163
+ ...options.env ?? process.env,
1164
+ PWD: spec.cwd,
1165
+ ...adapter.context.env(spec),
1166
+ ...wired.env
1167
+ };
1168
+ const legOf = (legSpec, carried) => ({
1169
+ adapter,
1170
+ bin,
1171
+ env,
1172
+ scope,
1173
+ cwd: spec.cwd,
1174
+ ...ports.machine === void 0 ? {} : { priority: ports.machine.priority },
1175
+ args: [...adapter.command.args(legSpec, {
1176
+ endpoint,
1177
+ allowed
1178
+ }), ...wired?.args ?? []],
1179
+ carried,
1180
+ ...adapter.legDecoder === void 0 ? {} : { decoder: adapter.legDecoder(legSpec) },
1181
+ pending,
1182
+ deferred: () => deferring,
1183
+ withhold: () => input === void 0 && queued.length > 0,
1184
+ ...byTool ? { reported: () => reported } : {},
1185
+ attach: (attached) => {
1186
+ io = attached;
1187
+ }
1188
+ });
1189
+ let legSpec = spec;
1190
+ let spent;
1191
+ for (;;) {
1192
+ if (ended.signal.aborted) {
1193
+ yield driverFailure("interrupted", "interrupted", scope);
1194
+ return;
1195
+ }
1196
+ const leg = yield* runLeg(legOf(legSpec, spent));
1197
+ if (leg.kind === "failed") {
1198
+ yield driverFailure(leg.reason, leg.end, scope);
1199
+ return;
1200
+ }
1201
+ if (leg.kind === "finished") return;
1202
+ deferring = false;
1203
+ spent = spent === void 0 ? leg.usage : adapter.usage.combine(spent, leg.usage);
1204
+ if (leg.kind === "deferred") {
1205
+ await heavy.ready();
1206
+ await hold?.wait("tool", ended.signal);
1207
+ }
1208
+ if (ended.signal.aborted) {
1209
+ yield leg.last;
1210
+ return;
1211
+ }
1212
+ legSpec = leg.kind === "continued" ? {
1213
+ ...resumed(spec, leg.sessionId),
1214
+ prompt: queued.splice(0).join("\n\n")
1215
+ } : resumed(spec, leg.sessionId);
1216
+ }
1217
+ } finally {
1218
+ heavy.release();
1219
+ await wired?.release();
1220
+ await mcp.close();
1221
+ }
1222
+ };
1223
+ return {
1224
+ events: events(),
1225
+ send: (text) => {
1226
+ if (input === void 0) {
1227
+ queued.push(text);
1228
+ return;
1229
+ }
1230
+ const line = input(text, true);
1231
+ if (io === void 0) pending.push(line);
1232
+ else io.write(line);
1233
+ },
1234
+ interrupt: () => {
1235
+ ended.abort();
1236
+ io?.stop();
1237
+ return Promise.resolve();
1238
+ }
1239
+ };
1240
+ }
1241
+ });
1242
+ // ../runtime/src/adapters/claude-code-events.ts
1243
+ /**
1244
+ * @file The `claude-code` adapter's `EventDecoder` and `UsageReader`: maps the
1245
+ * `claude` CLI's `stream-json` lines to Ork's runtime events, marks the
1246
+ * result of a turn the hold hook deferred, and adds up a session's legs.
1247
+ */
1248
+ const isObject$2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
1249
+ const get$1 = (value, key) => isObject$2(value) ? value[key] : void 0;
1250
+ const num$2 = (value) => typeof value === "number" ? value : 0;
1251
+ const str$2 = (value) => typeof value === "string" ? value : "";
1252
+ const blocks = (message) => {
1253
+ const content = get$1(get$1(message, "message"), "content");
1254
+ return Array.isArray(content) ? content.filter((item) => isObject$2(item)) : [];
1255
+ };
1256
+ const usageOf = (result) => {
1257
+ const usage = get$1(result, "usage");
1258
+ return {
1259
+ costUsd: num$2(result["total_cost_usd"]),
1260
+ inputTokens: num$2(get$1(usage, "input_tokens")),
1261
+ outputTokens: num$2(get$1(usage, "output_tokens")),
1262
+ cacheReadTokens: num$2(get$1(usage, "cache_read_input_tokens")),
1263
+ cacheWriteTokens: num$2(get$1(usage, "cache_creation_input_tokens")),
1264
+ turns: num$2(result["num_turns"]),
1265
+ durationMs: num$2(result["duration_ms"])
1266
+ };
1267
+ };
1268
+ /** Claude Code's error subtypes that name their own kind. */
1269
+ const SUBTYPE_KINDS = {
1270
+ error_max_turns: "turn_limit",
1271
+ error_max_budget_usd: "budget_limit",
1272
+ error_max_structured_output_retries: "invalid_report"
1273
+ };
1274
+ /**
1275
+ * What a result says went wrong, if anything: a subtype that names its kind, or
1276
+ * an error (`is_error`, or an error subtype) read by the provider classifier,
1277
+ * `unknown` when it says nothing Ork knows.
1278
+ */
1279
+ const signalOf$1 = (subtype, result, errors) => {
1280
+ const text = [...errors, str$2(result["result"])].join("\n").trim();
1281
+ const named = SUBTYPE_KINDS[subtype];
1282
+ if (named !== void 0) return endFor(named, {}, text);
1283
+ if (subtype === "success" && result["is_error"] !== true) return null;
1284
+ return errorEnd(text);
1285
+ };
1286
+ /** The outcome a `result` message describes. */
1287
+ const outcomeOf = (result) => {
1288
+ const said = result["errors"];
1289
+ const errors = Array.isArray(said) ? said.filter((e) => typeof e === "string") : [];
1290
+ const subtype = str$2(result["subtype"]) || "unknown";
1291
+ const report = parseReport(result["structured_output"]);
1292
+ return {
1293
+ subtype,
1294
+ end: sessionEnd(signalOf$1(subtype, result, errors), report),
1295
+ sessionId: str$2(result["session_id"]),
1296
+ report,
1297
+ text: str$2(result["result"]),
1298
+ usage: usageOf(result),
1299
+ errors
1300
+ };
1301
+ };
1302
+ /** The year 2286: a reset beyond this is not in seconds. */
1303
+ const MAX_RESET_SECONDS = 1e10;
1304
+ /**
1305
+ * What a subscription's limit said (RFC-007). Only the fields Claude Code
1306
+ * documents are read, so `utilization`, often absent, plays no part.
1307
+ */
1308
+ const planLimitOf = (info) => {
1309
+ const status = get$1(info, "status");
1310
+ if (status !== "allowed" && status !== "allowed_warning" && status !== "rejected") return [];
1311
+ const window = get$1(info, "rateLimitType");
1312
+ const resets = get$1(info, "resetsAt");
1313
+ const overage = get$1(info, "overageStatus");
1314
+ return [{
1315
+ type: "plan.limit",
1316
+ status: status === "rejected" && !(status === "rejected" && overage !== "allowed" && overage !== "allowed_warning") ? "allowed_warning" : status,
1317
+ window: typeof window === "string" ? window : "plan",
1318
+ ...typeof resets === "number" && resets > 0 && resets < MAX_RESET_SECONDS ? { resetsAt: (/* @__PURE__ */ new Date(resets * 1e3)).toISOString() } : {}
1319
+ }];
1320
+ };
1321
+ /** Maps one driver message to zero or more normalized events. */
1322
+ const normalize = (message) => {
1323
+ if (!isObject$2(message)) return [];
1324
+ switch (message["type"]) {
1325
+ case "system": {
1326
+ if (message["subtype"] !== "init") return [];
1327
+ const started = {
1328
+ type: "session.started",
1329
+ sessionId: str$2(message["session_id"]),
1330
+ model: str$2(message["model"])
1331
+ };
1332
+ if (str$2(message["cwd"]) !== "") started.cwd = str$2(message["cwd"]);
1333
+ return [started];
1334
+ }
1335
+ case "assistant": return blocks(message).flatMap((block) => {
1336
+ if (block["type"] === "text" && str$2(block["text"]).trim() !== "") return [{
1337
+ type: "assistant.text",
1338
+ text: str$2(block["text"])
1339
+ }];
1340
+ if (block["type"] === "tool_use") {
1341
+ const input = block["input"];
1342
+ return [{
1343
+ type: "tool.called",
1344
+ id: str$2(block["id"]),
1345
+ name: str$2(block["name"]),
1346
+ input: isObject$2(input) ? input : {}
1347
+ }];
1348
+ }
1349
+ return [];
1350
+ });
1351
+ case "user": return blocks(message).filter((block) => block["type"] === "tool_result").map((block) => ({
1352
+ type: "tool.result",
1353
+ id: str$2(block["tool_use_id"]),
1354
+ isError: block["is_error"] === true
1355
+ }));
1356
+ case "rate_limit_event": return planLimitOf(message["rate_limit_info"]);
1357
+ case "result": return [{
1358
+ type: "session.finished",
1359
+ outcome: outcomeOf(message)
1360
+ }];
1361
+ default: return [];
1362
+ }
1363
+ };
1364
+ /** Whether a stream-json message is the result of a deferred tool call. */
1365
+ const isDeferredResult = (message) => get$1(message, "type") === "result" && get$1(message, "stop_reason") === "tool_deferred";
1366
+ const parseLine$2 = (line) => {
1367
+ try {
1368
+ return JSON.parse(line);
1369
+ } catch {
1370
+ return;
1371
+ }
1372
+ };
1373
+ /** One `stream-json` line, decoded. */
1374
+ const decodeClaudeLine = (line) => {
1375
+ const message = parseLine$2(line);
1376
+ return {
1377
+ events: normalize(message),
1378
+ deferred: isDeferredResult(message)
1379
+ };
1380
+ };
1381
+ /**
1382
+ * Two legs of one session, as one: the tokens of both, and the later leg's
1383
+ * cost, which is already the session's total (ADR-046).
1384
+ */
1385
+ const addUsage = (a, b) => ({
1386
+ costUsd: Math.max(a.costUsd, b.costUsd),
1387
+ inputTokens: a.inputTokens + b.inputTokens,
1388
+ outputTokens: a.outputTokens + b.outputTokens,
1389
+ cacheReadTokens: a.cacheReadTokens + b.cacheReadTokens,
1390
+ cacheWriteTokens: a.cacheWriteTokens + b.cacheWriteTokens,
1391
+ turns: a.turns + b.turns,
1392
+ durationMs: a.durationMs + b.durationMs
1393
+ });
1394
+ /** `claude` reports dollars (a client-side estimate) and tokens. */
1395
+ const claudeUsage = {
1396
+ dollars: true,
1397
+ combine: addUsage
1398
+ };
1399
+ // ../runtime/src/adapters/claude-code.ts
1400
+ /**
1401
+ * Per-request timeout for Ork's MCP server, in ms. The CLI otherwise gives an
1402
+ * HTTP server's request 60 seconds and drops a call silent for 5 minutes, and
1403
+ * `delegate` waits for whole child sessions (the per-server `timeout` raises
1404
+ * both; `MCP_TOOL_TIMEOUT` still caps it).
1405
+ */
1406
+ const ORK_TOOL_TIMEOUT_MS = 864e5;
1407
+ /** Claude Code's switch for the one-hour prompt cache. */
1408
+ const PROMPT_CACHE_1H = "ENABLE_PROMPT_CACHING_1H";
1409
+ /** Claude Code's switch that turns its auto-memory off. */
1410
+ const NO_AUTO_MEMORY = "CLAUDE_CODE_DISABLE_AUTO_MEMORY";
1411
+ /**
1412
+ * The CLI flag that moves the per-machine sections (working directory, memory
1413
+ * path, git status) out of the system prompt.
1414
+ */
1415
+ const EXCLUDE_DYNAMIC_SECTIONS = "--exclude-dynamic-system-prompt-sections";
1416
+ /** How long a hook holds a call before it defers it instead. */
1417
+ const HOLD_WINDOW_MS = 54e4;
1418
+ /** The variables a session's prompt cache asks of Claude Code. */
1419
+ const cacheEnv = (spec) => spec.promptCache === "1h" ? { [PROMPT_CACHE_1H]: "1" } : {};
1420
+ /** Whether the session loads no MCP server but Ork's. */
1421
+ const strictMcpOf = (spec) => spec.tools.strictMcp === true || spec.machineClaude !== true;
1422
+ /** The variables that keep the machine's auto-memory out of a session. */
1423
+ const memoryEnv = (spec) => spec.machineClaude === true ? {} : { [NO_AUTO_MEMORY]: "1" };
1424
+ /** Everything a session adds to its driver's environment. */
1425
+ const driverEnv = (spec) => ({
1426
+ ...cacheEnv(spec),
1427
+ ...memoryEnv(spec)
1428
+ });
1429
+ /** A later message goes in at the next step, even mid-turn. */
1430
+ const userLine = (text, later = false) => `${JSON.stringify({
1431
+ type: "user",
1432
+ message: {
1433
+ role: "user",
1434
+ content: text
1435
+ },
1436
+ parent_tool_use_id: null,
1437
+ ...later ? { priority: "next" } : {}
1438
+ })}\n`;
1439
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1440
+ /**
1441
+ * The node's `--settings` with Ork's `PreToolUse` HTTP hook added: before every
1442
+ * tool call, pre-approved ones included, the session asks whether its node may
1443
+ * go on. Its `PostToolUse` hook after a Bash call frees the call's slot.
1444
+ */
1445
+ const withHoldHook = (settings, holdUrl, token) => {
1446
+ const hooks = isRecord(settings?.["hooks"]) ? settings["hooks"] : {};
1447
+ const listed = (event) => {
1448
+ const own = hooks[event];
1449
+ return Array.isArray(own) ? [...own] : [];
1450
+ };
1451
+ const hook = {
1452
+ type: "http",
1453
+ url: holdUrl,
1454
+ headers: { Authorization: `Bearer ${token}` },
1455
+ timeout: 600
1456
+ };
1457
+ return {
1458
+ ...settings,
1459
+ hooks: {
1460
+ ...hooks,
1461
+ PreToolUse: [...listed("PreToolUse"), {
1462
+ matcher: "*",
1463
+ hooks: [hook]
1464
+ }],
1465
+ PostToolUse: [...listed("PostToolUse"), {
1466
+ matcher: "Bash",
1467
+ hooks: [hook]
1468
+ }]
1469
+ }
1470
+ };
1471
+ };
1472
+ /** The hook's answer: nothing to say for "go", the deferral otherwise. */
1473
+ const hookOutput = (answer) => answer === "go" ? {} : { hookSpecificOutput: {
1474
+ hookEventName: "PreToolUse",
1475
+ permissionDecision: "defer",
1476
+ permissionDecisionReason: "Ork paused this node; it goes on from this call when it continues."
1477
+ } };
1478
+ const claudeHold = {
1479
+ answer: hookOutput,
1480
+ windowMs: HOLD_WINDOW_MS
1481
+ };
1482
+ /**
1483
+ * The `claude -p` arguments for a session spec. The brief goes on stdin;
1484
+ * `allowed` are the Ork tools that need no permission prompt.
1485
+ */
1486
+ const buildCliArgs = (spec, mcp, allowed) => {
1487
+ const mcpConfig = { mcpServers: {
1488
+ ...spec.mcpServers,
1489
+ ["ork"]: {
1490
+ type: "http",
1491
+ url: mcp.url,
1492
+ headers: { Authorization: `Bearer ${mcp.token}` },
1493
+ timeout: ORK_TOOL_TIMEOUT_MS
1494
+ }
1495
+ } };
1496
+ const args = [
1497
+ "-p",
1498
+ "--input-format",
1499
+ "stream-json",
1500
+ "--output-format",
1501
+ "stream-json",
1502
+ "--verbose",
1503
+ "--setting-sources",
1504
+ spec.settingSources.join(","),
1505
+ "--append-system-prompt",
1506
+ spec.systemAppend,
1507
+ EXCLUDE_DYNAMIC_SECTIONS,
1508
+ "--permission-mode",
1509
+ spec.permissionMode,
1510
+ "--json-schema",
1511
+ JSON.stringify(spec.reportSchema),
1512
+ "--mcp-config",
1513
+ JSON.stringify(mcpConfig),
1514
+ "--permission-prompt-tool",
1515
+ toolId("permission"),
1516
+ "--allowedTools",
1517
+ [...spec.tools.allow, ...allowed].join(",")
1518
+ ];
1519
+ if (spec.tools.only !== void 0) args.push("--tools", spec.tools.only.join(","));
1520
+ if (strictMcpOf(spec)) args.push("--strict-mcp-config");
1521
+ if (spec.agents !== void 0) args.push("--agents", JSON.stringify(spec.agents));
1522
+ if (spec.tools.deny.length > 0) args.push("--disallowedTools", spec.tools.deny.join(","));
1523
+ if (spec.model !== void 0) args.push("--model", spec.model);
1524
+ if (spec.effort !== void 0) args.push("--effort", spec.effort);
1525
+ if (spec.maxTurns !== void 0) args.push("--max-turns", String(spec.maxTurns));
1526
+ if (spec.budgetUsd !== void 0) args.push("--max-budget-usd", String(spec.budgetUsd));
1527
+ if (spec.resume !== void 0) args.push("--resume", spec.resume);
1528
+ if (spec.resume !== void 0 && spec.fork === true) args.push("--fork-session");
1529
+ for (const dir of spec.plugins ?? []) args.push("--plugin-dir", dir);
1530
+ const settings = mcp.holdUrl === void 0 ? spec.settings : withHoldHook(spec.settings, mcp.holdUrl, mcp.token);
1531
+ if (settings !== void 0) args.push("--settings", JSON.stringify(settings));
1532
+ return args;
1533
+ };
1534
+ /** `claude`, as the session runner's strategies. */
1535
+ const claudeCodeAdapter = {
1536
+ driver: "claude-code",
1537
+ command: {
1538
+ bin: "claude",
1539
+ args: (spec, leg) => buildCliArgs(spec, leg.endpoint, leg.allowed),
1540
+ input: userLine
1541
+ },
1542
+ decode: decodeClaudeLine,
1543
+ mcp: {
1544
+ kind: "flag",
1545
+ wire: () => Promise.resolve({
1546
+ args: [],
1547
+ env: {},
1548
+ release: () => Promise.resolve()
1549
+ })
1550
+ },
1551
+ permissions: {
1552
+ kind: "callback",
1553
+ denyByDefault: false
1554
+ },
1555
+ usage: claudeUsage,
1556
+ context: {
1557
+ instructions: ["CLAUDE.md", "AGENTS.md"],
1558
+ skills: ".claude/skills",
1559
+ agents: ".claude/agents",
1560
+ env: driverEnv
1561
+ },
1562
+ hold: claudeHold
1563
+ };
1564
+ // ../runtime/src/lease-ledger.ts
1565
+ /**
1566
+ * @file The ledger of Command Code leases (RFC-014, `lease.ts`): one record per
1567
+ * checkout in a folder of the project's runs folder, naming the session's
1568
+ * process and its `ork` entry, and the sweep that removes the entry of every
1569
+ * record whose process is gone (the next session's start, and `ork doctor`).
1570
+ * Only the `ork` entry is touched; the user's other entries stay as they
1571
+ * were.
1572
+ */
1573
+ /** One session's hold on a checkout. `pid` 0: its entry could not be removed. */
1574
+ const LeaseSchema = z.object({
1575
+ id: z.string(),
1576
+ folder: z.string(),
1577
+ pid: z.number(),
1578
+ entry: z.string()
1579
+ });
1580
+ /** A local-scope file: only `mcpServers` is read; the rest is kept as is. */
1581
+ const LocalScopeSchema = z.looseObject({ mcpServers: z.record(z.string(), z.unknown()).optional() });
1582
+ const codeOf = (error) => error instanceof Error && "code" in error ? error.code : void 0;
1583
+ const CMD_TIMEOUT_MS = 3e4;
1584
+ /** Where a project's lease records live, inside its runs folder. */
1585
+ const leaseLedger = (runsDir) => path.join(runsDir, ".leases");
1586
+ const alive = (pid) => {
1587
+ if (pid <= 0) return false;
1588
+ try {
1589
+ process.kill(pid, 0);
1590
+ return true;
1591
+ } catch (error) {
1592
+ return codeOf(error) === "EPERM";
1593
+ }
1594
+ };
1595
+ const readLease = (file) => {
1596
+ try {
1597
+ return LeaseSchema.parse(JSON.parse(readFileSync(file, "utf8")));
1598
+ } catch {
1599
+ return;
1600
+ }
1601
+ };
1602
+ const runCmd = (options, args, cwd) => new Promise((resolve) => {
1603
+ execFile(options.bin, args, {
1604
+ cwd,
1605
+ env: { ...options.env ?? process.env },
1606
+ encoding: "utf8",
1607
+ timeout: CMD_TIMEOUT_MS
1608
+ }, (error, stdout, stderr) => {
1609
+ const said = `${stdout}${stderr}`.trim();
1610
+ const missing = error?.code === "ENOENT" ? `spawn ${options.bin} ENOENT` : "";
1611
+ resolve({
1612
+ ok: error === null,
1613
+ out: said || missing
1614
+ });
1615
+ });
1616
+ });
1617
+ /** Drops the `ork` entry from a local-scope file whose folder is gone. */
1618
+ const dropFromFile = (entry) => {
1619
+ if (!existsSync(entry)) return true;
1620
+ try {
1621
+ const config = LocalScopeSchema.parse(JSON.parse(readFileSync(entry, "utf8")));
1622
+ const { ["ork"]: ours, ...others } = config.mcpServers ?? {};
1623
+ if (ours === void 0) return true;
1624
+ const kept = {
1625
+ ...config,
1626
+ mcpServers: others
1627
+ };
1628
+ writeFileSync(entry, `${JSON.stringify(kept, null, 2)}\n`, { mode: 384 });
1629
+ return true;
1630
+ } catch {
1631
+ return false;
1632
+ }
1633
+ };
1634
+ /** Removes a lease's `ork` entry; true once it is gone. */
1635
+ const removeEntry = async (options, lease) => {
1636
+ if (!existsSync(lease.folder)) return dropFromFile(lease.entry);
1637
+ const removed = await runCmd(options, [
1638
+ "mcp",
1639
+ "remove",
1640
+ "--scope",
1641
+ "local",
1642
+ "ork"
1643
+ ], lease.folder);
1644
+ return removed.ok || removed.out.includes("not found");
1645
+ };
1646
+ /** Deletes a record if it is still `lease`'s, not a newer one's. */
1647
+ const dropRecord = (file, lease) => {
1648
+ if (readLease(file)?.id === lease.id) rmSync(file, { force: true });
1649
+ };
1650
+ /**
1651
+ * Deletes a record that exists but can't be read: records are written whole (a
1652
+ * link, a rename), so one that does not parse is corrupt, and stale.
1653
+ */
1654
+ const dropIfCorrupt = (file) => {
1655
+ let text;
1656
+ try {
1657
+ text = readFileSync(file, "utf8");
1658
+ } catch {
1659
+ return;
1660
+ }
1661
+ try {
1662
+ LeaseSchema.parse(JSON.parse(text));
1663
+ } catch {
1664
+ rmSync(file, { force: true });
1665
+ }
1666
+ };
1667
+ /**
1668
+ * Removes the `ork` entry of every lease whose session is gone (a crash, or a
1669
+ * removal that failed), and its record. Live leases are left alone.
1670
+ */
1671
+ const cleanStaleLeases = async (options) => {
1672
+ const sweep = {
1673
+ cleaned: [],
1674
+ failed: []
1675
+ };
1676
+ if (!existsSync(options.ledger)) return sweep;
1677
+ for (const name of readdirSync(options.ledger)) {
1678
+ if (!name.endsWith(".json")) continue;
1679
+ const file = path.join(options.ledger, name);
1680
+ const lease = readLease(file);
1681
+ if (lease === void 0) dropIfCorrupt(file);
1682
+ if (lease === void 0 || alive(lease.pid)) continue;
1683
+ if (await removeEntry(options, lease)) {
1684
+ dropRecord(file, lease);
1685
+ sweep.cleaned.push(lease.folder);
1686
+ } else sweep.failed.push(lease.folder);
1687
+ }
1688
+ return sweep;
1689
+ };
1690
+ // ../runtime/src/lease.ts
1691
+ /**
1692
+ * @file The `lease` kind of `McpWiring` (RFC-014): Command Code reads MCP
1693
+ * servers from a per-folder local scope (`~/.commandcode/projects/<slug>/`),
1694
+ * so a session gets Ork's endpoint through an `ork` entry added there with
1695
+ * `cmd mcp add --scope local` and removed with `cmd mcp remove` after it.
1696
+ * Because the entry is per folder, a checkout holds one session at a time: a
1697
+ * record in the ledger (`lease-ledger.ts`) is the lock, and a second session
1698
+ * waits for it, and says so.
1699
+ */
1700
+ /**
1701
+ * Command Code's folder name for a checkout, as measured on 1.88.5: its real
1702
+ * path with every character but letters and digits made `-`, the leading one
1703
+ * dropped (`/private/tmp/a b` -> `private-tmp-a-b`). Leases key on it, so two
1704
+ * folders Command Code would share also share one lease.
1705
+ */
1706
+ const slugOf = (folder) => realpathSync(folder).replaceAll(/[^a-zA-Z0-9]/gu, "-").replace(/^-+/u, "");
1707
+ const entryFileOf = (env, slug) => path.join(env["HOME"] ?? homedir(), ".commandcode", "projects", slug, "mcp.json");
1708
+ /** Takes the record for `slug`, or returns the live lease that holds it. */
1709
+ const tryTake = (file, lease) => {
1710
+ const draft = `${file}.${lease.id}`;
1711
+ writeFileSync(draft, JSON.stringify(lease), { mode: 384 });
1712
+ try {
1713
+ linkSync(draft, file);
1714
+ return;
1715
+ } catch (error) {
1716
+ if (codeOf(error) !== "EEXIST") throw error;
1717
+ const holder = readLease(file);
1718
+ if (holder !== void 0) return holder;
1719
+ dropIfCorrupt(file);
1720
+ return {
1721
+ ...lease,
1722
+ pid: 0
1723
+ };
1724
+ } finally {
1725
+ rmSync(draft, { force: true });
1726
+ }
1727
+ };
1728
+ const masked = (text, endpoint) => maskSecrets(text.replaceAll(endpoint.token, MASK));
1729
+ /**
1730
+ * The lease's options: those given, else the runner's program, environment and
1731
+ * the project's runs folder (the ledger lives there, where `ork doctor`
1732
+ * looks).
1733
+ */
1734
+ const optionsOf = (given, hooks) => {
1735
+ const ledger = given.ledger ?? (hooks.runs === void 0 ? void 0 : leaseLedger(hooks.runs));
1736
+ if (ledger === void 0) throw new Error("a Command Code session needs the project's runs folder for its lease");
1737
+ const env = given.env ?? hooks.env;
1738
+ return {
1739
+ ...given,
1740
+ bin: given.bin ?? hooks.bin ?? "cmd",
1741
+ ledger,
1742
+ ...env === void 0 ? {} : { env }
1743
+ };
1744
+ };
1745
+ /**
1746
+ * Ork's endpoint for a Command Code session, through an `ork` entry in the
1747
+ * checkout's local scope, held for the session. A second session in the same
1748
+ * checkout waits for the first, and says so.
1749
+ */
1750
+ const leaseWiring = (given) => ({
1751
+ kind: "lease",
1752
+ wire: async (spec, endpoint, hooks) => {
1753
+ const options = optionsOf(given, hooks);
1754
+ const env = options.env ?? process.env;
1755
+ const slug = slugOf(spec.cwd);
1756
+ const file = path.join(options.ledger, `${slug}.json`);
1757
+ const lease = {
1758
+ id: randomUUID(),
1759
+ folder: realpathSync(spec.cwd),
1760
+ pid: process.pid,
1761
+ entry: entryFileOf(env, slug)
1762
+ };
1763
+ const fail = (message) => {
1764
+ dropRecord(file, lease);
1765
+ throw new Error(masked(message, endpoint));
1766
+ };
1767
+ mkdirSync(options.ledger, {
1768
+ recursive: true,
1769
+ mode: 448
1770
+ });
1771
+ await cleanStaleLeases(options);
1772
+ let told = false;
1773
+ for (;;) {
1774
+ const holder = tryTake(file, lease);
1775
+ if (holder === void 0) break;
1776
+ if (alive(holder.pid)) {
1777
+ if (!told) {
1778
+ hooks.waiting(`waiting for ${lease.folder}: Command Code runs one session per checkout`);
1779
+ told = true;
1780
+ }
1781
+ await setTimeout$1(options.pollMs ?? 1e3, void 0, { signal: hooks.signal });
1782
+ } else {
1783
+ const { failed } = await cleanStaleLeases(options);
1784
+ if (failed.includes(holder.folder)) fail(`could not remove the \`ork\` MCP entry a stopped session left in ${holder.folder}; run \`ork doctor\``);
1785
+ }
1786
+ }
1787
+ const existing = await runCmd(options, [
1788
+ "mcp",
1789
+ "get",
1790
+ "ork"
1791
+ ], lease.folder);
1792
+ if (existing.ok && /Scope:\s+local/u.test(existing.out)) fail(`Command Code already has an \`ork\` MCP entry for ${lease.folder}; remove it with \`cmd mcp remove --scope local ork\` there`);
1793
+ const added = await runCmd(options, [
1794
+ "mcp",
1795
+ "add",
1796
+ "--transport",
1797
+ "http",
1798
+ "--scope",
1799
+ "local",
1800
+ "ork",
1801
+ endpoint.url,
1802
+ "--header",
1803
+ `Authorization: Bearer ${endpoint.token}`
1804
+ ], lease.folder);
1805
+ if (!added.ok) fail(`cmd mcp add failed: ${added.out}`);
1806
+ return {
1807
+ args: [],
1808
+ env: {},
1809
+ release: async () => {
1810
+ if (await removeEntry(options, lease)) {
1811
+ dropRecord(file, lease);
1812
+ return;
1813
+ }
1814
+ const draft = `${file}.${lease.id}`;
1815
+ writeFileSync(draft, JSON.stringify({
1816
+ ...lease,
1817
+ pid: 0
1818
+ }), { mode: 384 });
1819
+ renameSync(draft, file);
1820
+ }
1821
+ };
1822
+ }
1823
+ });
1824
+ // ../runtime/src/adapters/command-code-events.ts
1825
+ /**
1826
+ * @file The `command-code` adapter's `LegDecoder` and `UsageReader`: maps
1827
+ * Command Code's `--output-format json` lines (`{"type":"event"}` frames,
1828
+ * then one `{"type":"result"}` line; measured on 1.88.5,
1829
+ * specs/architecture.md, "Command Code") to Ork's runtime events. Tool names
1830
+ * are given Ork's (Claude Code's) names, so logs and the node's rules read
1831
+ * the same on every driver. One decoder reads one leg: the session id comes
1832
+ * in an early `run_start` frame and the result line counts no turns, so it
1833
+ * keeps both. The result line is held until the process exits: its exit code
1834
+ * says how the session ended (specs/architecture/command-code.md).
1835
+ */
1836
+ const isObject$1 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
1837
+ const num$1 = (value) => typeof value === "number" ? value : 0;
1838
+ const str$1 = (value) => typeof value === "string" ? value : "";
1839
+ /** Command Code's own tool names, as Ork (and Claude Code) names them. */
1840
+ const COMMAND_CODE_TOOLS = {
1841
+ shell: "Bash",
1842
+ shell_command: "Bash",
1843
+ read_file: "Read",
1844
+ read_multiple_files: "Read",
1845
+ read_directory: "Read",
1846
+ glob: "Glob",
1847
+ grep: "Grep",
1848
+ edit_file: "Edit",
1849
+ write_file: "Write",
1850
+ web_fetch: "WebFetch",
1851
+ web_search: "WebSearch",
1852
+ agent: "Task"
1853
+ };
1854
+ /** A tool's name as Ork shows it; MCP and unknown names stay as they are. */
1855
+ const orkToolName = (name) => COMMAND_CODE_TOOLS[name] ?? name;
1856
+ /** The result line's subtypes, as Ork's outcome names them. */
1857
+ const SUBTYPES = {
1858
+ success: "success",
1859
+ max_turns: "error_max_turns",
1860
+ error: "error_during_execution"
1861
+ };
1862
+ /**
1863
+ * Command Code's exit codes, by the kind they mean; 0 is a session that ended
1864
+ * well and 1 an error its text explains.
1865
+ */
1866
+ const COMMAND_CODE_EXITS = {
1867
+ 3: "auth",
1868
+ 4: "auth",
1869
+ 5: "rate_limited",
1870
+ 6: "provider_unavailable",
1871
+ 7: "provider_unavailable",
1872
+ 8: "turn_limit",
1873
+ 9: "provider_unavailable",
1874
+ 10: "out_of_credits",
1875
+ 130: "interrupted"
1876
+ };
1877
+ /** What a leg's exit code and result line say went wrong, if anything. */
1878
+ const signalOf = (code, error) => {
1879
+ const kind = code === null ? void 0 : COMMAND_CODE_EXITS[code];
1880
+ if (kind !== void 0) return endFor(kind, {}, error);
1881
+ if ((code === 0 || code === null) && error === "") return null;
1882
+ return errorEnd(error);
1883
+ };
1884
+ /** The text blocks of a `message_end` frame's content. */
1885
+ const textOf = (content) => (Array.isArray(content) ? content : []).filter((item) => isObject$1(item)).filter((block) => block["type"] === "text").map((block) => str$1(block["text"])).join("");
1886
+ /** The frames that end a tool call as an error: failed, denied, or blocked. */
1887
+ const REFUSED = /* @__PURE__ */ new Set([
1888
+ "tool_errored",
1889
+ "tool_denied",
1890
+ "tool_hook_blocked"
1891
+ ]);
1892
+ const parseLine$1 = (line) => {
1893
+ try {
1894
+ return JSON.parse(line);
1895
+ } catch {
1896
+ return;
1897
+ }
1898
+ };
1899
+ /**
1900
+ * A decoder for one leg of a Command Code session; its end comes at the exit,
1901
+ * from the held result line and the exit code.
1902
+ */
1903
+ const commandCodeDecoder = () => {
1904
+ let sessionId = "";
1905
+ let turns = 0;
1906
+ let held;
1907
+ const frame = (event) => {
1908
+ const type = event["type"];
1909
+ const id = str$1(event["toolCallId"]);
1910
+ const name = orkToolName(str$1(event["toolName"]));
1911
+ if (type === "run_start") {
1912
+ sessionId = str$1(event["sessionId"]);
1913
+ return [{
1914
+ type: "session.started",
1915
+ sessionId,
1916
+ model: ""
1917
+ }];
1918
+ }
1919
+ if (type === "turn_start") turns += 1;
1920
+ if (type === "message_end") {
1921
+ const text = textOf(event["content"]);
1922
+ return text.trim() === "" ? [] : [{
1923
+ type: "assistant.text",
1924
+ text
1925
+ }];
1926
+ }
1927
+ if (type === "tool_queued") {
1928
+ const input = event["input"];
1929
+ return [{
1930
+ type: "tool.called",
1931
+ id,
1932
+ name,
1933
+ input: isObject$1(input) ? input : {}
1934
+ }];
1935
+ }
1936
+ if (type === "tool_completed") return [{
1937
+ type: "tool.result",
1938
+ id,
1939
+ isError: false
1940
+ }];
1941
+ if (typeof type === "string" && REFUSED.has(type)) return [{
1942
+ type: "tool.result",
1943
+ id,
1944
+ isError: true
1945
+ }];
1946
+ return [];
1947
+ };
1948
+ const result = (line, code) => {
1949
+ const usage = isObject$1(line["usage"]) ? line["usage"] : {};
1950
+ const error = str$1(line["error"]);
1951
+ const subtype = str$1(line["subtype"]);
1952
+ const signal = subtype === "max_turns" ? endFor("turn_limit", {}, error) : signalOf(code, error);
1953
+ return {
1954
+ type: "session.finished",
1955
+ outcome: {
1956
+ subtype: SUBTYPES[subtype] ?? "error_during_execution",
1957
+ end: signal ?? endFor("no_report"),
1958
+ sessionId: str$1(line["sessionId"]) || sessionId,
1959
+ report: null,
1960
+ text: str$1(line["finalText"]),
1961
+ usage: {
1962
+ costUsd: 0,
1963
+ inputTokens: num$1(usage["inputTokens"]),
1964
+ outputTokens: num$1(usage["outputTokens"]),
1965
+ cacheReadTokens: num$1(usage["cacheReadTokens"]),
1966
+ cacheWriteTokens: num$1(usage["cacheWriteTokens"]),
1967
+ turns,
1968
+ durationMs: num$1(line["durationMs"])
1969
+ },
1970
+ errors: error === "" ? [] : [error]
1971
+ }
1972
+ };
1973
+ };
1974
+ return {
1975
+ decode: (line) => {
1976
+ const message = parseLine$1(line);
1977
+ if (!isObject$1(message)) return { events: [] };
1978
+ if (message["type"] === "result") {
1979
+ held = message;
1980
+ return { events: [] };
1981
+ }
1982
+ const event = message["event"];
1983
+ return { events: message["type"] === "event" && isObject$1(event) ? frame(event) : [] };
1984
+ },
1985
+ exit: (code) => {
1986
+ if (held !== void 0) return [result(held, code)];
1987
+ return (code === null ? void 0 : COMMAND_CODE_EXITS[code]) === void 0 ? [] : [result({ error: "" }, code)];
1988
+ }
1989
+ };
1990
+ };
1991
+ /** Command Code reports tokens, never dollars. */
1992
+ const commandCodeUsage = {
1993
+ dollars: false,
1994
+ combine: (a, b) => addUsage(a, b)
1995
+ };
1996
+ // ../runtime/src/adapters/command-code-mod.ts
1997
+ /**
1998
+ * @file The Command Code mod that holds a session to its node's tool rules
1999
+ * (RFC-014, deny by default). Command Code takes permission rules only from
2000
+ * settings files (the user's, and two in the checkout), so Ork runs it with
2001
+ * `--permission-mode yolo` and loads, with `--mod`, a mod written for the
2002
+ * session outside the checkout: its `beforeToolCall` hook blocks every call
2003
+ * the node did not allow. Ork's own tools (`mcp__ork__*`) always go through.
2004
+ * Command Code's tool loader passes, and reads inside the worktree need no
2005
+ * rule, as with Claude Code; one reaching outside it does. The rules are the
2006
+ * node's, in Claude Code's words (`Bash(git:*)`, `Read`, `mcp__github`); a
2007
+ * specifier Ork does not read never allows a call and always denies one.
2008
+ */
2009
+ /** The rules of a session: an `acceptEdits` node may edit without a rule. */
2010
+ const modRules = (spec) => ({
2011
+ cwd: spec.cwd,
2012
+ allow: [...spec.tools.allow, ...spec.permissionMode === "acceptEdits" ? ["Edit"] : []],
2013
+ deny: spec.tools.deny,
2014
+ ...spec.tools.only === void 0 ? {} : { only: spec.tools.only }
2015
+ });
2016
+ /**
2017
+ * The mod's source. It is plain JavaScript that Command Code loads (a default
2018
+ * export taking the mod API), so it carries its own matcher.
2019
+ */
2020
+ const modSource = (rules) => `// Ork's rules for one session; written by Ork, removed after it.
2021
+ import { relative, resolve } from "node:path";
2022
+ const RULES = ${JSON.stringify(rules)};
2023
+ // Command Code's own: loading a deferred tool's schema, Ork's report included.
2024
+ const META = new Set(["search_tools", "tool_search"]);
2025
+ // Reads a session makes in its worktree without a rule, as Claude Code does.
2026
+ const READS = new Set(["read_file", "read_multiple_files", "read_directory", "glob", "grep"]);
2027
+ const pathsOf = input => Object.entries(input ?? {})
2028
+ .filter(([key]) => /path|file|dir/i.test(key))
2029
+ .flatMap(([, value]) => (Array.isArray(value) ? value : [value]))
2030
+ .filter(value => typeof value === "string");
2031
+ const inside = input => pathsOf(input).every(each => {
2032
+ const from = relative(RULES.cwd, resolve(RULES.cwd, each));
2033
+ return from === "" || (!from.startsWith("..") && !from.startsWith("/"));
2034
+ });
2035
+ const TOOLS = ${JSON.stringify(COMMAND_CODE_TOOLS)};
2036
+ // A rule's word, as Command Code reads it, covers these of Ork's tool names.
2037
+ const COVERS = { read: ["Read", "Glob", "Grep"], edit: ["Edit", "Write"] };
2038
+ const norm = text => text.trim().replace(/\\s+/g, " ");
2039
+ const split = rule => {
2040
+ const at = rule.indexOf("(");
2041
+ return at < 0 || !rule.endsWith(")")
2042
+ ? { token: rule.trim() }
2043
+ : { token: rule.slice(0, at).trim(), spec: rule.slice(at + 1, -1) };
2044
+ };
2045
+ const shellMatches = (command, pattern) => {
2046
+ let p = norm(pattern);
2047
+ const c = norm(command);
2048
+ if (p.endsWith(":*")) p = p.slice(0, -2) + " *";
2049
+ if (!p.endsWith(" *")) return c === p;
2050
+ const head = p.slice(0, -2);
2051
+ return c === head || c.startsWith(head + " ");
2052
+ };
2053
+ // true: the rule covers the call; false: it does not; null: Ork can't tell.
2054
+ const matches = (rule, name, input) => {
2055
+ const { token, spec } = split(rule);
2056
+ if (token.startsWith("mcp__")) {
2057
+ const rest = token.slice(5);
2058
+ const at = rest.indexOf("__");
2059
+ if (at < 0) return name.startsWith("mcp__" + rest + "__");
2060
+ const tool = rest.slice(at + 2);
2061
+ return tool === "*" ? name.startsWith("mcp__" + rest.slice(0, at) + "__") : name === token;
2062
+ }
2063
+ const ork = TOOLS[name] ?? name;
2064
+ const word = token.toLowerCase();
2065
+ const covered = (COVERS[word] ?? [token]).some(each => each.toLowerCase() === ork.toLowerCase());
2066
+ if (!covered && name !== token) return false;
2067
+ if (spec === undefined || spec === "" || spec === "*") return true;
2068
+ if (ork === "Bash" && typeof input?.command === "string") return shellMatches(input.command, spec);
2069
+ return null;
2070
+ };
2071
+ export const decide = (name, input) => {
2072
+ if (RULES.deny.some(rule => matches(rule, name, input) !== false)) return "denied for this node";
2073
+ if (name.startsWith("mcp__ork__") || META.has(name)) return undefined;
2074
+ if (RULES.only !== undefined && !RULES.only.some(rule => matches(rule, name, input) === true)) {
2075
+ return "not one of this node's tools";
2076
+ }
2077
+ if (READS.has(name) && inside(input)) return undefined;
2078
+ if (RULES.allow.some(rule => matches(rule, name, input) === true)) return undefined;
2079
+ return "not allowed for this node";
2080
+ };
2081
+ export default cc => {
2082
+ cc.hooks({
2083
+ beforeToolCall: async call => {
2084
+ const why = decide(call.toolName, call.input);
2085
+ return why === undefined
2086
+ ? undefined
2087
+ : { block: true, additionalContext: "Ork: " + call.toolName + " is " + why + ". Do not retry it." };
2088
+ },
2089
+ });
2090
+ };
2091
+ `;
2092
+ /**
2093
+ * Writes a session's mod outside the checkout (a fresh folder under the
2094
+ * system's temp folder, 0700, the file 0600); it holds no secret.
2095
+ */
2096
+ const writeMod = (spec) => {
2097
+ const dir = mkdtempSync(path.join(tmpdir(), "ork-cmd-mod-"));
2098
+ const file = path.join(dir, "ork-rules.mjs");
2099
+ writeFileSync(file, modSource(modRules(spec)), { mode: 384 });
2100
+ return {
2101
+ file,
2102
+ remove: () => {
2103
+ rmSync(dir, {
2104
+ recursive: true,
2105
+ force: true
2106
+ });
2107
+ }
2108
+ };
2109
+ };
2110
+ // ../runtime/src/adapters/command-code.ts
2111
+ /**
2112
+ * The brief, with Ork's instructions before it on a session's first leg:
2113
+ * Command Code has no flag that adds to its system prompt. A resumed leg's
2114
+ * session already holds them.
2115
+ */
2116
+ const queryOf = (spec) => {
2117
+ const text = spec.resume === void 0 ? `${spec.systemAppend}\n\n${spec.prompt}` : spec.prompt;
2118
+ return text.startsWith("-") ? ` ${text}` : text;
2119
+ };
2120
+ /**
2121
+ * The `cmd` arguments for a session spec. `--dangerously-skip-permissions`: in
2122
+ * `-p` mode only it lifts Command Code's own gate on edits and shell (the
2123
+ * permission mode alone does not), so the session's mod, which refuses what the
2124
+ * node did not allow, is the only gate.
2125
+ */
2126
+ const commandCodeArgs = (spec) => {
2127
+ const args = [
2128
+ "-p",
2129
+ queryOf(spec),
2130
+ "--output-format",
2131
+ "json",
2132
+ "--permission-mode",
2133
+ "yolo",
2134
+ "--dangerously-skip-permissions",
2135
+ "--skip-onboarding",
2136
+ "--no-auto-update"
2137
+ ];
2138
+ if (spec.model !== void 0) args.push("--model", spec.model);
2139
+ if (spec.effort !== void 0) args.push("--effort", spec.effort);
2140
+ if (spec.maxTurns !== void 0) args.push("--max-turns", String(spec.maxTurns));
2141
+ if (spec.resume !== void 0) args.push("--resume", spec.resume);
2142
+ if (spec.resume !== void 0 && spec.fork === true) args.push("--fork-session");
2143
+ return args;
2144
+ };
2145
+ const lease = leaseWiring({});
2146
+ /** `cmd`, as the session runner's strategies. */
2147
+ const commandCodeAdapter = {
2148
+ driver: "command-code",
2149
+ command: {
2150
+ bin: "cmd",
2151
+ args: (spec) => commandCodeArgs(spec)
2152
+ },
2153
+ decode: (line) => commandCodeDecoder().decode(line),
2154
+ legDecoder: () => commandCodeDecoder(),
2155
+ mcp: {
2156
+ kind: "lease",
2157
+ wire: async (spec, endpoint, hooks) => {
2158
+ const wired = await lease.wire(spec, endpoint, hooks);
2159
+ try {
2160
+ const mod = writeMod(spec);
2161
+ return {
2162
+ args: [
2163
+ ...wired.args,
2164
+ "--mod",
2165
+ mod.file
2166
+ ],
2167
+ env: wired.env,
2168
+ release: async () => {
2169
+ mod.remove();
2170
+ await wired.release();
2171
+ }
2172
+ };
2173
+ } catch (error) {
2174
+ await wired.release();
2175
+ throw error;
2176
+ }
2177
+ }
2178
+ },
2179
+ permissions: {
2180
+ kind: "rules",
2181
+ denyByDefault: true
2182
+ },
2183
+ usage: commandCodeUsage,
2184
+ context: {
2185
+ instructions: ["AGENTS.md"],
2186
+ skills: ".commandcode/skills",
2187
+ agents: ".commandcode/agents",
2188
+ env: () => ({})
2189
+ }
2190
+ };
2191
+ // ../runtime/src/secret-file.ts
2192
+ /**
2193
+ * @file A session's private folder for files that hold a secret, such as a
2194
+ * config carrying Ork's bearer token (RFC-014): a 0700 folder outside the
2195
+ * checkout, under the system's temporary folder, named after the process that
2196
+ * made it, with each file written at mode 0600. The session removes it when
2197
+ * it ends; a folder whose process is gone (a crash) is removed when the next
2198
+ * one is made. Nothing here logs what it writes.
2199
+ */
2200
+ /** Where sessions' private folders live unless a caller names another place. */
2201
+ const secretRoot = () => path.join(tmpdir(), "ork-sessions");
2202
+ /** Removes the folders under `root` whose process is gone; returns them. */
2203
+ const sweepSecretFolders = (root) => {
2204
+ let names;
2205
+ try {
2206
+ names = readdirSync(root);
2207
+ } catch {
2208
+ return [];
2209
+ }
2210
+ return names.flatMap((name) => {
2211
+ const pid = Number(name.split("-")[0]);
2212
+ if (!Number.isInteger(pid) || alive(pid)) return [];
2213
+ const dir = path.join(root, name);
2214
+ rmSync(dir, {
2215
+ recursive: true,
2216
+ force: true
2217
+ });
2218
+ return [dir];
2219
+ });
2220
+ };
2221
+ /** A new private folder under `root`, after sweeping the stale ones. */
2222
+ const makeSecretFolder = (root = secretRoot()) => {
2223
+ mkdirSync(root, {
2224
+ recursive: true,
2225
+ mode: 448
2226
+ });
2227
+ sweepSecretFolders(root);
2228
+ const dir = mkdtempSync(path.join(root, `${String(process.pid)}-`));
2229
+ return {
2230
+ dir,
2231
+ write: (name, content) => {
2232
+ const file = path.join(dir, name);
2233
+ writeFileSync(file, content, { mode: 384 });
2234
+ return file;
2235
+ },
2236
+ remove: () => {
2237
+ rmSync(dir, {
2238
+ recursive: true,
2239
+ force: true
2240
+ });
2241
+ }
2242
+ };
2243
+ };
2244
+ /** Claude Code tool names, as OpenCode's permission actions. */
2245
+ const ACTIONS = {
2246
+ Read: "read",
2247
+ Glob: "glob",
2248
+ Grep: "grep",
2249
+ Edit: "edit",
2250
+ Write: "edit",
2251
+ MultiEdit: "edit",
2252
+ NotebookEdit: "edit",
2253
+ Bash: "shell",
2254
+ WebFetch: "webfetch",
2255
+ WebSearch: "websearch",
2256
+ Task: "subagent",
2257
+ Agent: "subagent",
2258
+ Skill: "skill",
2259
+ TodoWrite: "todowrite"
2260
+ };
2261
+ const BUILT_IN = new Set(Object.values(ACTIONS));
2262
+ /**
2263
+ * Actions whose resource is a path. OpenCode matches it relative to the
2264
+ * worktree (an absolute path inside it too), `*` across folders, and asks
2265
+ * `external_directory`, denied first, for any path outside it.
2266
+ */
2267
+ const PATH_ACTIONS = /* @__PURE__ */ new Set(["read", "edit"]);
2268
+ /** A rule's path as OpenCode matches it: relative to the worktree. */
2269
+ const pathResource = (spec, cwd) => {
2270
+ return (path.isAbsolute(spec) ? path.relative(cwd, spec) : spec).replace(/^\.\//u, "") || "*";
2271
+ };
2272
+ /** OpenCode's name for an MCP server's tool: other characters become `_`. */
2273
+ const mcpAction = (server, tool) => `${server}_${tool}`.replaceAll(/[^A-Za-z0-9_*-]/gu, "_");
2274
+ /** A shell rule's resource: Claude Code's `git status:*` is `git status *`. */
2275
+ const shellResource = (spec) => spec.replace(/:\*$/u, " *");
2276
+ /**
2277
+ * One Claude Code rule (`Bash(git status:*)`, `Edit`, `mcp__github__*`) as an
2278
+ * OpenCode rule, or none for a tool OpenCode has no action for (still denied).
2279
+ */
2280
+ const ruleOf = (rule, effect, cwd) => {
2281
+ const match = /^([^()]+)(?:\((.*)\))?$/su.exec(rule.trim());
2282
+ if (match === null) return void 0;
2283
+ const [, name = "", spec] = match;
2284
+ const mcp = /^mcp__([^_]+(?:_[^_]+)*)(?:__(.*))?$/u.exec(name);
2285
+ if (mcp !== null) {
2286
+ const [, server = "", tool] = mcp;
2287
+ return {
2288
+ action: mcpAction(server, tool ?? "*"),
2289
+ resource: "*",
2290
+ effect
2291
+ };
2292
+ }
2293
+ const action = ACTIONS[name];
2294
+ if (action === void 0) return void 0;
2295
+ if (spec !== void 0) return {
2296
+ action,
2297
+ resource: action === "shell" ? shellResource(spec) : PATH_ACTIONS.has(action) ? pathResource(spec, cwd) : spec,
2298
+ effect
2299
+ };
2300
+ return {
2301
+ action,
2302
+ resource: "*",
2303
+ effect
2304
+ };
2305
+ };
2306
+ const rulesOf = (rules, effect, cwd) => rules.flatMap((rule) => ruleOf(rule, effect, cwd) ?? []);
2307
+ /** An action allowed anywhere in the worktree; outside it is denied. */
2308
+ const worktree = (action) => ({
2309
+ action,
2310
+ resource: "*",
2311
+ effect: "allow"
2312
+ });
2313
+ /**
2314
+ * The session's rules, in order: deny everything; read in the worktree (as
2315
+ * Claude Code reads without asking); edits there under `acceptEdits`; the
2316
+ * node's allows (only its `only` tools, if it names them); Ork's tools; then
2317
+ * `.env` reads, plan mode's edits and shell, and the node's denies, refused.
2318
+ */
2319
+ const permissionRules = (spec) => {
2320
+ const { cwd, tools } = spec;
2321
+ const only = tools.only === void 0 ? void 0 : new Set(rulesOf(tools.only, "allow", cwd).map((r) => r.action));
2322
+ const inOnly = (rule) => only === void 0 || only.has(rule.action) || !BUILT_IN.has(rule.action);
2323
+ return [
2324
+ {
2325
+ action: "*",
2326
+ resource: "*",
2327
+ effect: "deny"
2328
+ },
2329
+ ...[
2330
+ worktree("read"),
2331
+ ...spec.permissionMode === "acceptEdits" ? [worktree("edit")] : [],
2332
+ ...rulesOf(tools.allow, "allow", cwd)
2333
+ ].filter((rule) => inOnly(rule)),
2334
+ {
2335
+ action: `ork_*`,
2336
+ resource: "*",
2337
+ effect: "allow"
2338
+ },
2339
+ {
2340
+ action: "read",
2341
+ resource: "*.env",
2342
+ effect: "deny"
2343
+ },
2344
+ ...spec.permissionMode === "plan" ? ["edit", "shell"].map((action) => ({
2345
+ action,
2346
+ resource: "*",
2347
+ effect: "deny"
2348
+ })) : [],
2349
+ ...rulesOf(tools.deny, "deny", cwd)
2350
+ ];
2351
+ };
2352
+ /**
2353
+ * A `.mcp.json` entry as an OpenCode server: `local` with a command, else
2354
+ * `remote`.
2355
+ */
2356
+ const serverOf = (entry) => {
2357
+ const { command, args, env, url, headers } = entry;
2358
+ const listed = Array.isArray(args) ? args.map(String) : [];
2359
+ return typeof command === "string" ? {
2360
+ type: "local",
2361
+ command: [command, ...listed],
2362
+ ...env === void 0 ? {} : { environment: env },
2363
+ codemode: false
2364
+ } : {
2365
+ type: "remote",
2366
+ url,
2367
+ oauth: false,
2368
+ codemode: false,
2369
+ headers
2370
+ };
2371
+ };
2372
+ /** The whole config file for one session. */
2373
+ const sessionConfig = (spec, endpoint, instructions) => ({
2374
+ instructions: [instructions],
2375
+ mcp: { servers: {
2376
+ ...Object.fromEntries(Object.entries(spec.mcpServers ?? {}).map(([name, entry]) => [name, serverOf(entry)])),
2377
+ ["ork"]: {
2378
+ type: "remote",
2379
+ url: endpoint.url,
2380
+ oauth: false,
2381
+ codemode: false,
2382
+ headers: { Authorization: `Bearer ${endpoint.token}` }
2383
+ }
2384
+ } },
2385
+ permissions: permissionRules(spec),
2386
+ agents: { ["ork"]: {
2387
+ mode: "primary",
2388
+ ...spec.maxTurns === void 0 ? {} : { steps: spec.maxTurns }
2389
+ } }
2390
+ });
2391
+ // ../runtime/src/adapters/open-code-events.ts
2392
+ /**
2393
+ * @file The `open-code` adapter's decoder and usage reader. `opencode run
2394
+ * --format json` prints one JSON object per line, `{type, timestamp,
2395
+ * sessionID, part}` (2.1.0, read from its source): `step_start`, `text`,
2396
+ * `tool_use` (a finished call, `state.status` `completed` or `error`),
2397
+ * `step_finish` (`cost` in dollars, `tokens`) and `error`. There is no result
2398
+ * line: the last `text` is the final text and the process's exit ends the
2399
+ * session. Tool names come back in Claude Code's words (`shell` is `Bash`,
2400
+ * `ork_report` is `mcp__ork__report`), the words a node's rules use.
2401
+ */
2402
+ const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
2403
+ const get = (value, key) => isObject(value) ? value[key] : void 0;
2404
+ const num = (value) => typeof value === "number" ? value : 0;
2405
+ const str = (value) => typeof value === "string" ? value : "";
2406
+ /** OpenCode's built-in tools, by Claude Code's names. */
2407
+ const TOOL_NAMES = {
2408
+ shell: "Bash",
2409
+ read: "Read",
2410
+ edit: "Edit",
2411
+ write: "Write",
2412
+ patch: "Edit",
2413
+ glob: "Glob",
2414
+ grep: "Grep",
2415
+ webfetch: "WebFetch",
2416
+ websearch: "WebSearch",
2417
+ subagent: "Task",
2418
+ skill: "Skill",
2419
+ todowrite: "TodoWrite"
2420
+ };
2421
+ const ORK_PREFIX = `ork_`;
2422
+ /** A tool's name as a node's rules and Ork's views say it. */
2423
+ const toolName = (name) => TOOL_NAMES[name] ?? (name.startsWith(ORK_PREFIX) ? toolId(name.slice(ORK_PREFIX.length)) : name);
2424
+ const parseLine = (line) => {
2425
+ try {
2426
+ return JSON.parse(line);
2427
+ } catch {
2428
+ return;
2429
+ }
2430
+ };
2431
+ /** One `step_finish`'s spending, as a whole session's of one turn. */
2432
+ const stepUsage = (part) => {
2433
+ const tokens = get(part, "tokens");
2434
+ const cache = get(tokens, "cache");
2435
+ return {
2436
+ costUsd: num(get(part, "cost")),
2437
+ inputTokens: num(get(tokens, "input")),
2438
+ outputTokens: num(get(tokens, "output")) + num(get(tokens, "reasoning")),
2439
+ cacheReadTokens: num(get(cache, "read")),
2440
+ cacheWriteTokens: num(get(cache, "write")),
2441
+ turns: 1,
2442
+ durationMs: 0
2443
+ };
2444
+ };
2445
+ /** Two spans of one session, as one: OpenCode reports each step's own cost. */
2446
+ const sumUsage = (a, b) => ({
2447
+ costUsd: a.costUsd + b.costUsd,
2448
+ inputTokens: a.inputTokens + b.inputTokens,
2449
+ outputTokens: a.outputTokens + b.outputTokens,
2450
+ cacheReadTokens: a.cacheReadTokens + b.cacheReadTokens,
2451
+ cacheWriteTokens: a.cacheWriteTokens + b.cacheWriteTokens,
2452
+ turns: a.turns + b.turns,
2453
+ durationMs: a.durationMs + b.durationMs
2454
+ });
2455
+ const openCodeUsage = {
2456
+ dollars: true,
2457
+ combine: sumUsage
2458
+ };
2459
+ const NONE = {
2460
+ costUsd: 0,
2461
+ inputTokens: 0,
2462
+ outputTokens: 0,
2463
+ cacheReadTokens: 0,
2464
+ cacheWriteTokens: 0,
2465
+ turns: 0,
2466
+ durationMs: 0
2467
+ };
2468
+ /** One finished tool call: its call and its result. */
2469
+ const toolEvents = (part) => {
2470
+ const id = str(get(part, "id")) || str(get(part, "partID"));
2471
+ const state = get(part, "state");
2472
+ const input = get(state, "input");
2473
+ return [{
2474
+ type: "tool.called",
2475
+ id,
2476
+ name: toolName(str(get(part, "tool"))),
2477
+ input: isObject(input) ? input : {}
2478
+ }, {
2479
+ type: "tool.result",
2480
+ id,
2481
+ isError: get(state, "status") === "error"
2482
+ }];
2483
+ };
2484
+ /** What one line says by itself: text and finished tool calls. */
2485
+ const decodeOpenCodeLine = (line) => {
2486
+ const message = parseLine(line);
2487
+ const part = get(message, "part");
2488
+ switch (get(message, "type")) {
2489
+ case "text": {
2490
+ const said = str(get(part, "text"));
2491
+ return { events: said.trim() === "" ? [] : [{
2492
+ type: "assistant.text",
2493
+ text: said
2494
+ }] };
2495
+ }
2496
+ case "tool_use": return { events: toolEvents(part) };
2497
+ default: return { events: [] };
2498
+ }
2499
+ };
2500
+ /** An `error` line's words, for the session's errors. */
2501
+ const errorText = (error) => str(get(get(error, "data"), "message")) || str(get(error, "message")) || str(get(error, "name")) || "error";
2502
+ /** An `error` line's end: its name, status and body, through the classifier. */
2503
+ const errorLineEnd = (error) => {
2504
+ const data = get(error, "data");
2505
+ const status = get(data, "statusCode");
2506
+ const said = [
2507
+ str(get(error, "name")),
2508
+ errorText(error),
2509
+ str(get(data, "responseBody"))
2510
+ ].filter((part) => part !== "").join("\n");
2511
+ return errorEnd(said, "unknown", typeof status === "number" ? status : void 0);
2512
+ };
2513
+ /** The exit code OpenCode gives an interrupted session. */
2514
+ const INTERRUPTED = 130;
2515
+ /**
2516
+ * A decoder for one leg of a session: it starts the session at the first line
2517
+ * that names it, and keeps the last text, the steps' usage and the errors for
2518
+ * the end. OpenCode prints nothing at its step cap (`steps`, the node's
2519
+ * `maxTurns`): a session that used every step and handed in no report reached
2520
+ * its turn limit, which the runner settles once it knows the report.
2521
+ */
2522
+ const openCodeDecoder = (spec) => {
2523
+ const began = Date.now();
2524
+ let sessionId = "";
2525
+ let text = "";
2526
+ let usage = NONE;
2527
+ const errors = [];
2528
+ let failed = null;
2529
+ return {
2530
+ decode: (line) => {
2531
+ const message = parseLine(line);
2532
+ if (!isObject(message)) return { events: [] };
2533
+ const events = [];
2534
+ if (sessionId === "" && str(message["sessionID"]) !== "") {
2535
+ sessionId = str(message["sessionID"]);
2536
+ events.push({
2537
+ type: "session.started",
2538
+ sessionId,
2539
+ model: spec.model ?? "default",
2540
+ cwd: spec.cwd
2541
+ });
2542
+ }
2543
+ const said = decodeOpenCodeLine(line).events;
2544
+ events.push(...said);
2545
+ const last = said.findLast((event) => event.type === "assistant.text");
2546
+ if (last !== void 0) text = last.text;
2547
+ switch (message["type"]) {
2548
+ case "step_finish":
2549
+ usage = sumUsage(usage, stepUsage(message["part"]));
2550
+ break;
2551
+ case "error":
2552
+ errors.push(errorText(message["error"]));
2553
+ failed ??= errorLineEnd(message["error"]);
2554
+ }
2555
+ return { events };
2556
+ },
2557
+ exit: (code) => {
2558
+ const interrupted = code === INTERRUPTED;
2559
+ if (code !== 0 && errors.length === 0 && !interrupted) return [];
2560
+ const capped = spec.maxTurns !== void 0 && usage.turns >= spec.maxTurns;
2561
+ const end = interrupted ? endFor("interrupted", {}, errors.join("\n")) : failed ?? endFor(capped ? "turn_limit" : "no_report");
2562
+ return [{
2563
+ type: "session.finished",
2564
+ outcome: {
2565
+ subtype: code === 0 ? "success" : "error_during_execution",
2566
+ end,
2567
+ sessionId,
2568
+ report: null,
2569
+ text,
2570
+ usage: {
2571
+ ...usage,
2572
+ durationMs: Date.now() - began
2573
+ },
2574
+ errors
2575
+ }
2576
+ }];
2577
+ }
2578
+ };
2579
+ };
2580
+ // ../runtime/src/adapters/open-code.ts
2581
+ /**
2582
+ * @file The `open-code` adapter: the user's own `opencode run` (2.1.0), on a
2583
+ * private server (`--standalone`; the background service would ignore the
2584
+ * session's environment), started in the node's worktree, which pins its
2585
+ * working folder. Ork's endpoint, the permission rules and the `ork` agent go
2586
+ * in a config file named by `OPENCODE_CONFIG` (the `config-file` wiring),
2587
+ * written at 0600 in a private folder outside the checkout and removed after
2588
+ * the session (`secret-file.ts`). The machine's and the project's OpenCode
2589
+ * config stay out: `XDG_CONFIG_HOME` is an empty folder beside the file, and
2590
+ * the project's config is switched off. The brief and each later message are
2591
+ * the message argument of a leg; a message waits for the next leg, which
2592
+ * continues the session. Never `--auto`: it approves what no rule denies.
2593
+ */
2594
+ /** The `opencode run` arguments for one leg; the message is the last one. */
2595
+ const openCodeArgs = (spec) => [
2596
+ "run",
2597
+ "--standalone",
2598
+ "--format",
2599
+ "json",
2600
+ "--agent",
2601
+ "ork",
2602
+ ...spec.model === void 0 ? [] : ["--model", spec.model],
2603
+ ...spec.resume === void 0 ? [] : ["--session", spec.resume],
2604
+ ...spec.resume !== void 0 && spec.fork === true ? ["--fork"] : [],
2605
+ spec.prompt.startsWith("-") ? ` ${spec.prompt}` : spec.prompt
2606
+ ];
2607
+ /**
2608
+ * What keeps the machine's and the project's own OpenCode setup out, and
2609
+ * OpenCode's reading of `.claude/` and `CLAUDE.md` (RFC-014 open question 3).
2610
+ */
2611
+ const OPEN_CODE_ENV = {
2612
+ OPENCODE_DISABLE_PROJECT_CONFIG: "1",
2613
+ OPENCODE_DISABLE_CLAUDE_CODE: "1",
2614
+ OPENCODE_DISABLE_CLAUDE_CODE_SKILLS: "1",
2615
+ OPENCODE_CONFIG_DIR: "",
2616
+ OPENCODE_CONFIG_CONTENT: ""
2617
+ };
2618
+ /** The config file, Ork's instructions and an empty config home, per session. */
2619
+ const configFile = (root) => ({
2620
+ kind: "config-file",
2621
+ wire: (spec, endpoint) => {
2622
+ const folder = makeSecretFolder(root);
2623
+ try {
2624
+ const instructions = folder.write("ork.md", spec.systemAppend);
2625
+ const config = folder.write("opencode.json", JSON.stringify(sessionConfig(spec, endpoint, instructions)));
2626
+ const home = path.join(folder.dir, "config");
2627
+ mkdirSync(home, { mode: 448 });
2628
+ return Promise.resolve({
2629
+ args: [],
2630
+ env: {
2631
+ OPENCODE_CONFIG: config,
2632
+ XDG_CONFIG_HOME: home
2633
+ },
2634
+ release: () => {
2635
+ folder.remove();
2636
+ return Promise.resolve();
2637
+ }
2638
+ });
2639
+ } catch (error) {
2640
+ folder.remove();
2641
+ throw error;
2642
+ }
2643
+ }
2644
+ });
2645
+ /** `opencode`, as the session runner's strategies. */
2646
+ const createOpenCodeAdapter = (options = {}) => ({
2647
+ driver: "open-code",
2648
+ command: {
2649
+ bin: "opencode",
2650
+ args: openCodeArgs
2651
+ },
2652
+ decode: decodeOpenCodeLine,
2653
+ legDecoder: openCodeDecoder,
2654
+ mcp: configFile(options.secretRoot ?? secretRoot()),
2655
+ permissions: {
2656
+ kind: "rules",
2657
+ denyByDefault: true
2658
+ },
2659
+ usage: openCodeUsage,
2660
+ context: {
2661
+ instructions: ["AGENTS.md"],
2662
+ skills: ".opencode/skills",
2663
+ agents: ".opencode/agents",
2664
+ env: () => OPEN_CODE_ENV
2665
+ }
2666
+ });
2667
+ const openCodeAdapter = createOpenCodeAdapter();
2668
+ // ../runtime/src/registry.ts
2669
+ /** The message a session on a driver with no adapter yet ends with. */
2670
+ const notBuiltYet = (driver) => `The ${driver} driver is not built yet; use claude-code.`;
2671
+ const notBuilt = (driver) => ({
2672
+ driver,
2673
+ start: () => ({
2674
+ events: (async function* () {
2675
+ yield driverFailure(notBuiltYet(driver), "model_unavailable", { facts: {} });
2676
+ })(),
2677
+ send: () => {},
2678
+ interrupt: () => Promise.resolve()
2679
+ })
2680
+ });
2681
+ const ADAPTERS = {
2682
+ "claude-code": claudeCodeAdapter,
2683
+ "open-code": openCodeAdapter,
2684
+ "command-code": commandCodeAdapter
2685
+ };
2686
+ /**
2687
+ * The runtime for a driver id, with the user's environment and the project's
2688
+ * runs folder (where a Command Code lease is recorded).
2689
+ */
2690
+ const createRuntime = (driver, env, runs) => {
2691
+ const adapter = ADAPTERS[driver];
2692
+ return adapter === null ? notBuilt(driver) : createSessionRuntime(adapter, {
2693
+ env,
2694
+ ...runs === void 0 ? {} : { runs }
2695
+ });
2696
+ };
2697
+ export { cleanStaleLeases, createRuntime, leaseLedger };