@krapht/ork 1.0.0-alpha.2 → 1.0.0-alpha.20

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