@krapht/ork 1.0.0-alpha.0 → 1.0.0-alpha.10

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