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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/{__build_info__-DFF18K8y.mjs → __build_info__-CIchF6nr.mjs} +3 -72
  2. package/dist/bin.mjs +1066 -387
  3. package/dist/chips-BA7SrrH5.mjs +43 -0
  4. package/dist/{clean-DVaEH9e7.mjs → clean-Di-KT7Mf.mjs} +1 -1
  5. package/dist/{completion-Dv5kf598.mjs → completion-C5D0sEs5.mjs} +22 -15
  6. package/dist/{context-CEaKOVmG.mjs → context-BaNshWr9.mjs} +43 -31
  7. package/dist/{decide-3l9q8UtX.mjs → decide-B6PnKeqo.mjs} +1 -1
  8. package/dist/{find-DEjjOlTG.mjs → find-BicBbGMg.mjs} +1 -1
  9. package/dist/{format-Cb5_m-Qd.mjs → format-lWhN-jYR.mjs} +130 -35
  10. package/dist/{in-a-run-BS9Nc-JW.mjs → in-a-run-CgkVouyG.mjs} +1 -1
  11. package/dist/{limits-BSwEjQST.mjs → limits-jZQp9ZJC.mjs} +1 -1
  12. package/dist/{list-Qxr6rwsY.mjs → list-CEaJ07jd.mjs} +1 -1
  13. package/dist/{mcp-CslnG5wf.mjs → mcp-DYgVA9f5.mjs} +6 -6
  14. package/dist/output-CM95njYt.mjs +421 -0
  15. package/dist/{pause-D7OVB5Ac.mjs → pause-D6PuSlNl.mjs} +4 -4
  16. package/dist/{relay-EceRIebO.mjs → relay-DNx7be9k.mjs} +2 -2
  17. package/dist/{remove-0IQZVWmI.mjs → remove-CpMb4NDl.mjs} +3 -3
  18. package/dist/{report-DK4LGqwF.mjs → report-CEN8VoUb.mjs} +31 -20
  19. package/dist/{send-C0uwfUYi.mjs → send-CLs_fyg7.mjs} +3 -3
  20. package/dist/{src-cmmXKHn1.mjs → src-9iGkjnsy.mjs} +234 -150
  21. package/dist/{src-i5k2Y-kf.mjs → src-B6KvTm7B.mjs} +78 -11
  22. package/dist/{src-oaJYIddp.mjs → src-BnZgZGrf.mjs} +523 -81
  23. package/dist/{src-Df6KOIDC.mjs → src-D6_xjm-x.mjs} +1722 -825
  24. package/dist/ui/assets/{app-qs8Lofv6.js → app-CxURph7a.js} +16 -16
  25. package/dist/ui/assets/app-QoHVi-mb.css +1 -0
  26. package/dist/ui/assets/{index-DV1ZaOk-.js → index-By_8KKG6.js} +2 -2
  27. package/dist/ui/index.html +1 -1
  28. package/dist/{ui-CBkHJdqk.mjs → ui-CIci4UBW.mjs} +4 -4
  29. package/dist/{usage-Cd8pEYyK.mjs → usage-GceBGMjU.mjs} +43 -32
  30. package/dist/{watch-DW5R2OPO.mjs → watch-BmoybMQg.mjs} +28 -16
  31. package/dist/{workflows-BT1gDRIV.mjs → workflows-DZvW3i1e.mjs} +3 -3
  32. package/dist/{worktrees-g1MQMGie.mjs → worktrees-Be51E2rB.mjs} +11 -10
  33. package/package.json +1 -1
  34. package/dist/output-Bqljtmp_.mjs +0 -40
  35. package/dist/ui/assets/app-C8uhHVYp.css +0 -1
@@ -1,4 +1,4 @@
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";
1
+ import { Ar as AGENT_DRIVERS, Bt as lowerChild, En as maskSecrets, Vt as startAt, an as FEEDBACK_SEVERITIES, en as outcomeEnd, ft as parseReport, in as FEEDBACK_KINDS, nn as END_KINDS, on as FEEDBACK_TARGETS, rn as endFor, wn as MASK } from "./src-D6_xjm-x.mjs";
2
2
  import path from "node:path";
3
3
  import { z } from "zod";
4
4
  import { existsSync, linkSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
@@ -26,6 +26,113 @@ const holdAtTool = async (hold, signal, windowMs) => {
26
26
  }
27
27
  return hold.held() && !signal.aborted ? "defer" : "go";
28
28
  };
29
+ // ../runtime/src/heavy-hook.ts
30
+ /**
31
+ * @file A session's slot for a heavy Bash call (the project's `heavy` rule),
32
+ * taken in the hook before a tool call, where a pause holds the call too
33
+ * (ADR-041): a slot that frees within the hold window lets the call go; past
34
+ * it the call is deferred, as a pause defers it, and the session resumes once
35
+ * the slot is taken. The slot is released after the call (the hook after it),
36
+ * at the session's next call, or at the session's end. The wait itself (its
37
+ * events, the clock standing still) is the node's machine port.
38
+ */
39
+ const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
40
+ /** The command of a Bash call's hook input, as Claude Code sends it. */
41
+ const bashCommand = (input) => {
42
+ if (!isRecord$1(input) || input["tool_name"] !== "Bash") return void 0;
43
+ const tool = input["tool_input"];
44
+ const command = isRecord$1(tool) ? tool["command"] : void 0;
45
+ return typeof command === "string" ? command : void 0;
46
+ };
47
+ /** The hook for one session; with no machine port, every call goes. */
48
+ const heavyHook = (machine) => {
49
+ let held;
50
+ let waiting;
51
+ const free = () => {
52
+ held?.release();
53
+ held = void 0;
54
+ };
55
+ /** Gives up the wait; a slot it already got is given back. */
56
+ const abandon = () => {
57
+ const pending = waiting;
58
+ waiting = void 0;
59
+ if (pending === void 0) return;
60
+ pending.stop.abort();
61
+ pending.slot.then((release) => {
62
+ release();
63
+ return null;
64
+ });
65
+ };
66
+ return {
67
+ before: async (input, signal, windowMs) => {
68
+ const command = bashCommand(input);
69
+ if (held !== void 0 && !held.ran && held.command === command) {
70
+ held.ran = true;
71
+ return "go";
72
+ }
73
+ free();
74
+ if (waiting !== void 0 && waiting.command !== command) abandon();
75
+ if (machine === void 0 || command === void 0) return "go";
76
+ if (waiting === void 0) {
77
+ const stop = new AbortController();
78
+ waiting = {
79
+ command,
80
+ stop,
81
+ slot: machine.slot(command, stop.signal)
82
+ };
83
+ }
84
+ const pending = waiting;
85
+ const window = new AbortController();
86
+ const release = await Promise.race([pending.slot, setTimeout$1(windowMs, null, { signal: AbortSignal.any([signal, window.signal]) }).catch(() => null)]);
87
+ window.abort();
88
+ if (release === null) return signal.aborted ? "go" : "defer";
89
+ waiting = void 0;
90
+ held = {
91
+ command,
92
+ release,
93
+ ran: true
94
+ };
95
+ return "go";
96
+ },
97
+ after: () => {
98
+ if (held?.ran === true) free();
99
+ },
100
+ ready: async () => {
101
+ const pending = waiting;
102
+ if (pending === void 0) return;
103
+ const release = await pending.slot;
104
+ if (waiting !== pending) return;
105
+ waiting = void 0;
106
+ held = {
107
+ command: pending.command,
108
+ release,
109
+ ran: false
110
+ };
111
+ },
112
+ release: () => {
113
+ abandon();
114
+ free();
115
+ }
116
+ };
117
+ };
118
+ /** Whether a hook's input is from after a tool call. */
119
+ const afterCall = (input) => isRecord$1(input) && input["hook_event_name"] === "PostToolUse";
120
+ /**
121
+ * A session's hook on its tool calls. Before one, a pause holds it (ADR-041),
122
+ * then a heavy Bash call waits for its slot; past the window either defers it
123
+ * (`onDefer`). After a call, its slot is freed.
124
+ */
125
+ const toolHook = (given) => async (gone, input) => {
126
+ if (afterCall(input)) {
127
+ given.heavy.after();
128
+ return {};
129
+ }
130
+ const signal = AbortSignal.any([gone, given.ended]);
131
+ let answer = await holdAtTool(given.hold, signal, given.windowMs);
132
+ if (answer === "go") answer = await given.heavy.before(input, signal, given.windowMs);
133
+ if (answer === "defer") given.onDefer();
134
+ return given.answer(answer);
135
+ };
29
136
  /** The name Claude Code gives an Ork tool: `mcp__ork__<name>`. */
30
137
  const toolId = (name) => `mcp__ork__${name}`;
31
138
  const text = (value) => ({ content: [{
@@ -77,7 +184,8 @@ const defineTool = (definition) => ({
77
184
  * `claude` CLI driver. It answers with plain JSON (no SSE stream), which the
78
185
  * transport allows, and implements only what Ork needs: `initialize`, `ping`,
79
186
  * `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).
187
+ * `PreToolUse` HTTP hook, which waits while the node is paused (ADR-041) or
188
+ * for a heavy command's slot, and its `PostToolUse` hook, which frees it.
81
189
  */
82
190
  const PATH = "/mcp";
83
191
  const HOLD_PATH = "/hold";
@@ -169,15 +277,22 @@ const serveHold = async (req, res, hold) => {
169
277
  respond(res, 405);
170
278
  return;
171
279
  }
172
- if (await readBody(req) === null) {
280
+ const body = await readBody(req);
281
+ if (body === null) {
173
282
  respond(res, 413);
174
283
  return;
175
284
  }
285
+ let input;
286
+ try {
287
+ input = JSON.parse(body);
288
+ } catch {
289
+ input = void 0;
290
+ }
176
291
  const gone = new AbortController();
177
292
  res.once("close", () => {
178
293
  gone.abort();
179
294
  });
180
- const output = await hold(gone.signal);
295
+ const output = await hold(gone.signal, input);
181
296
  if (!gone.signal.aborted) respond(res, 200, output);
182
297
  };
183
298
  const serve = async (req, res, tools, token, hold) => {
@@ -249,25 +364,139 @@ const startOrkMcpHttp = async (tools, hold) => {
249
364
  })
250
365
  };
251
366
  };
252
- // ../runtime/src/session-leg.ts
367
+ // ../runtime/src/provider-errors.ts
253
368
  /**
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.
369
+ * @file The provider-error classifier every driver's adapter uses (specs/
370
+ * runtime/session-ends.md): an error a model provider sent back (Anthropic,
371
+ * OpenAI-style, or a proxy such as OpenCode's `commandcode`) to one end kind.
372
+ * It reads the error's type or code first, its HTTP status next, and its
373
+ * words last, so a changed sentence rarely changes the kind.
259
374
  */
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) => ({
375
+ /** Error types and codes the providers document, by the kind they mean. */
376
+ const TYPES = {
377
+ rate_limit_error: "rate_limited",
378
+ overloaded_error: "provider_unavailable",
379
+ api_error: "provider_unavailable",
380
+ timeout_error: "provider_unavailable",
381
+ authentication_error: "auth",
382
+ permission_error: "auth",
383
+ billing_error: "out_of_credits",
384
+ not_found_error: "model_unavailable",
385
+ request_too_large: "context_too_long",
386
+ insufficient_quota: "out_of_credits",
387
+ invalid_api_key: "auth",
388
+ context_length_exceeded: "context_too_long",
389
+ model_not_found: "model_unavailable",
390
+ rate_limit_exceeded: "rate_limited",
391
+ server_error: "provider_unavailable",
392
+ ProviderAuthError: "auth",
393
+ MessageAbortedError: "interrupted"
394
+ };
395
+ const TYPE = new RegExp(`\\b(${Object.keys(TYPES).join("|")})\\b`, "u");
396
+ /** HTTP statuses that decide by themselves; 400 needs the words. */
397
+ const statusKind = (status) => {
398
+ if (status === 401 || status === 403) return "auth";
399
+ if (status === 402) return "out_of_credits";
400
+ if (status === 404) return "model_unavailable";
401
+ if (status === 413) return "context_too_long";
402
+ if (status === 429) return "rate_limited";
403
+ if (status === 408 || status === 529 || status >= 500 && status < 600) return "provider_unavailable";
404
+ return null;
405
+ };
406
+ /** The words, when neither type nor status decided; the first match wins. */
407
+ const WORDS = [
408
+ ["plan_limit", /\busage limit reached\b|\bhit your (?:usage )?limit\b|\blimit will reset\b/iu],
409
+ ["context_too_long", /\bprompt is too long\b|\bcontext (?:length|window)\b|\bmaximum context\b|\btoo many tokens\b/iu],
410
+ ["out_of_credits", /\bcredit balance\b|\binsufficient (?:credits?|funds|balance|quota)\b|\bout of credits?\b|\bexceeded your current quota\b/iu],
411
+ ["rate_limited", /\brate.?limit(?:ed)?\b|\btoo many requests\b/iu],
412
+ ["auth", /\binvalid (?:x-)?api.?key\b|\bnot (?:logged|signed) in\b|\bplease run \/login\b|\bunauthori[sz]ed\b|\bOAuth token has expired\b/iu],
413
+ ["provider_unavailable", /\boverloaded\b|\bservice unavailable\b|\bbad gateway\b|\bECONNRESET\b|\bECONNREFUSED\b|\bETIMEDOUT\b|\bENOTFOUND\b|\bEAI_AGAIN\b|\bfetch failed\b|\bsocket hang up\b|\bnetwork error\b/iu],
414
+ ["model_unavailable", /\bassistant message prefill\b|\bInvalid `signature` in `thinking` block\b|\bmodel(?: \S+)? (?:not found|does not exist|is not supported)\b|\bdoes not support\b|\bunknown model\b/iu]
415
+ ];
416
+ /** Generic refusals: the provider would not serve the request as it was. */
417
+ const REFUSED$1 = /\binvalid_request_error\b|\binvalid_request\b/u;
418
+ /** A status written in the text, e.g. Claude Code's `API Error: 529 {…}`. */
419
+ const STATUS_IN_TEXT = /\bAPI Error:? (\d{3})\b|"(?:status|statusCode)"\s*:\s*(\d{3})\b|\b(?:status|HTTP)[ :]+(\d{3})\b/u;
420
+ const statusIn = (text) => {
421
+ const found = STATUS_IN_TEXT.exec(text);
422
+ const digits = found?.[1] ?? found?.[2] ?? found?.[3];
423
+ return digits === void 0 ? void 0 : Number(digits);
424
+ };
425
+ const classed = (kind) => ({
426
+ kind,
427
+ message: END_KINDS[kind].message({})
428
+ });
429
+ /**
430
+ * The end kind a provider's error means, with Ork's sentence for it; `null`
431
+ * when nothing in it is recognized. `status` is the HTTP status when the caller
432
+ * has it; otherwise one written in the text is read.
433
+ */
434
+ const classifyProviderError = (text, status) => {
435
+ const type = TYPE.exec(text)?.[1];
436
+ if (type !== void 0) return classed(TYPES[type] ?? "unknown");
437
+ const code = status ?? statusIn(text);
438
+ const byStatus = code === void 0 ? null : statusKind(code);
439
+ if (byStatus !== null) return classed(byStatus);
440
+ const byWords = WORDS.find(([, pattern]) => pattern.test(text))?.[0];
441
+ if (byWords !== void 0) return classed(byWords);
442
+ return REFUSED$1.test(text) || code === 400 ? classed("model_unavailable") : null;
443
+ };
444
+ // ../runtime/src/session-end.ts
445
+ /**
446
+ * @file How a session ended, as its adapter's decoder and the session runner
447
+ * settle it (specs/runtime/session-ends.md): a decoder names the signal only
448
+ * it can see (a turn cap, an exit code, a provider's error), and the report
449
+ * the session handed in decides the rest. The runner then writes the end in
450
+ * the node's numbers, its detail scrubbed with the feedback redactor. A
451
+ * session the driver could not run ends as `driverFailure` says.
452
+ */
453
+ /** The end a report gives: done, a reported failure, or none handed in. */
454
+ const reportEnd = (report) => {
455
+ if (report === null) return endFor("no_report");
456
+ return endFor(report.status === "fail" ? "reported_failure" : "done");
457
+ };
458
+ /**
459
+ * A session's end: a report it handed in decides it (the node's result is that
460
+ * report), then the decoder's `signal`, then `no_report`.
461
+ */
462
+ const sessionEnd = (signal, report) => report !== null || signal === null ? reportEnd(report) : signal;
463
+ /**
464
+ * The end an error's text means: the provider classifier's kind, or `fallback`
465
+ * when it recognizes nothing. The text is the detail either way.
466
+ */
467
+ const errorEnd = (text, fallback = "unknown", status) => endFor(classifyProviderError(text, status)?.kind ?? fallback, {}, text);
468
+ /**
469
+ * The outcome with its end settled: a report decides it, also one handed in
470
+ * through Ork's `report` tool after the decoder's end said none; the message is
471
+ * written with the node's numbers; the detail is scrubbed and capped. An
472
+ * adapter whose decoder sets no end gets one read from the outcome.
473
+ */
474
+ const settleEnd = (outcome, facts, scrub) => {
475
+ const end = outcome.report === null ? outcome.end ?? outcomeEnd(outcome) : reportEnd(outcome.report);
476
+ return {
477
+ ...outcome,
478
+ end: endFor(end.kind, facts, end.detail, scrub)
479
+ };
480
+ };
481
+ /**
482
+ * The kind of a failure to run the driver: its program missing, else what a
483
+ * provider's words in it say, else a crash.
484
+ */
485
+ const failureKind = (error) => {
486
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return "tool_missing";
487
+ const text = error instanceof Error ? error.message : String(error);
488
+ if (/\bENOENT\b/u.test(text)) return "tool_missing";
489
+ return classifyProviderError(text)?.kind ?? "driver_crashed";
490
+ };
491
+ /**
492
+ * A finished event for a session the driver could not run, ended as `kind` with
493
+ * `message` (Ork's words or the tool's, masked) as its detail.
494
+ */
495
+ const driverFailure = (message, kind, scope) => ({
268
496
  type: "session.finished",
269
497
  outcome: {
270
498
  subtype: "error_driver",
499
+ end: endFor(kind, scope.facts, kind === "interrupted" ? void 0 : message, scope.scrub),
271
500
  sessionId: "",
272
501
  report: null,
273
502
  text: "",
@@ -283,6 +512,30 @@ const driverFailure = (message) => ({
283
512
  errors: [message]
284
513
  }
285
514
  });
515
+ // ../runtime/src/session-leg.ts
516
+ /**
517
+ * @file One process (leg) of a session the runner drives: it writes the queued
518
+ * stdin lines, yields the adapter's decoded events, and says how it ended. A
519
+ * session is one leg, or several when a paused node's tool call was deferred
520
+ * or a message waits for a tool that takes none mid-session; a resumed leg
521
+ * carries what the legs before it spent, and its start is not news.
522
+ */
523
+ const REASON_LIMIT = 500;
524
+ /** Marks a session's start with the driver that runs it. */
525
+ const withDriver = (event, driver) => event.type === "session.started" ? {
526
+ ...event,
527
+ driver
528
+ } : event;
529
+ /**
530
+ * A session's outcome with its report (the one Ork's `report` tool accepted,
531
+ * when the session has that tool) and its end settled.
532
+ */
533
+ const settled = (outcome, input) => settleEnd(input.reported === void 0 ? outcome : {
534
+ ...outcome,
535
+ report: input.reported()
536
+ }, input.scope.facts, input.scope.scrub);
537
+ /** The kinds of a session that ended well, so a queued message continues it. */
538
+ const wentWell = (kind) => kind === "done" || kind === "reported_failure" || kind === "no_report";
286
539
  /**
287
540
  * What one decoded line yields, and how the leg ends if it does: a result is
288
541
  * the session's end, unless the hold hook deferred a call or a message waits; a
@@ -295,12 +548,9 @@ const legStep = (decoded, input) => {
295
548
  const events = [];
296
549
  let ended;
297
550
  for (const said of decoded.events) {
298
- const event = said.type === "session.finished" && input.reported !== void 0 ? {
551
+ const event = said.type === "session.finished" ? {
299
552
  ...said,
300
- outcome: {
301
- ...said.outcome,
302
- report: input.reported()
303
- }
553
+ outcome: settled(said.outcome, input)
304
554
  } : said;
305
555
  if (event.type === "session.started" && carried !== void 0) continue;
306
556
  if (event.type !== "session.finished") events.push(withDriver(event, adapter.driver));
@@ -319,7 +569,7 @@ const legStep = (decoded, input) => {
319
569
  usage
320
570
  }
321
571
  };
322
- if (input.withhold() && event.outcome.subtype === "success") ended = {
572
+ if (input.withhold() && wentWell(event.outcome.end?.kind)) ended = {
323
573
  kind: "continued",
324
574
  sessionId: event.outcome.sessionId,
325
575
  usage: event.outcome.usage,
@@ -348,7 +598,8 @@ const failureReason = (name, stderr, code) => {
348
598
  };
349
599
  /** Runs one process of a session, yielding its events; returns how it ended. */
350
600
  const runLeg = async function* (input) {
351
- const child = spawn(input.bin, input.args, {
601
+ const [bin, args] = startAt(input.priority, input.bin, input.args, input.env);
602
+ const child = spawn(bin, args, {
352
603
  cwd: input.cwd,
353
604
  env: { ...input.env },
354
605
  stdio: [
@@ -357,6 +608,7 @@ const runLeg = async function* (input) {
357
608
  "pipe"
358
609
  ]
359
610
  });
611
+ lowerChild(input.priority, child.pid);
360
612
  const exited = new Promise((resolve) => {
361
613
  child.on("close", (code, signal) => {
362
614
  resolve({
@@ -388,7 +640,7 @@ const runLeg = async function* (input) {
388
640
  if (input.adapter.command.input === void 0) child.stdin.end();
389
641
  let outcome;
390
642
  const { decoder } = input;
391
- const decode = decoder?.decode ?? input.adapter.decoder?.() ?? input.adapter.decode;
643
+ const decode = decoder?.decode ?? input.adapter.decode;
392
644
  try {
393
645
  for await (const line of createInterface({ input: child.stdout })) {
394
646
  const { events, ended } = legStep(decode(line), input);
@@ -400,18 +652,26 @@ const runLeg = async function* (input) {
400
652
  }
401
653
  const end = await Promise.race([exited, spawnError.then((error) => error)]);
402
654
  if (outcome !== void 0) return outcome;
403
- if (decoder !== void 0 && !(end instanceof Error) && !stopped && end.signal === null) {
655
+ if (decoder !== void 0 && !(end instanceof Error) && !stopped) {
404
656
  const { events, ended } = legStep({ events: decoder.exit(end.code) }, input);
405
657
  yield* events;
406
658
  if (ended !== void 0) return ended;
407
659
  }
408
660
  if (end instanceof Error) return {
409
661
  kind: "failed",
410
- reason: `could not start ${input.bin}: ${end.message}`
662
+ reason: `could not start ${input.bin}: ${end.message}`,
663
+ end: failureKind(end)
664
+ };
665
+ if (stopped || end.signal !== null) return {
666
+ kind: "failed",
667
+ reason: "interrupted",
668
+ end: "interrupted"
411
669
  };
670
+ const reason = failureReason(input.adapter.command.bin, stderr, end.code);
412
671
  return {
413
672
  kind: "failed",
414
- reason: stopped || end.signal !== null ? "interrupted" : failureReason(input.adapter.command.bin, stderr, end.code)
673
+ reason,
674
+ end: classifyProviderError(reason)?.kind ?? "driver_crashed"
415
675
  };
416
676
  } finally {
417
677
  input.attach();
@@ -774,6 +1034,33 @@ const orkTools = (context) => ORK_TOOLS.flatMap((tool) => tool.bind(context) ??
774
1034
  /** Their Claude Code names, for the session's allowed tools. */
775
1035
  const preapprovedTools = (tools) => tools.filter((t) => t.preapproved).map((t) => toolId(t.name));
776
1036
  // ../runtime/src/session-runner.ts
1037
+ /**
1038
+ * @file The session runner: one agent CLI session, driven through an adapter's
1039
+ * strategies (`adapter.ts`), written once for every driver. It serves Ork's
1040
+ * tools on a local MCP endpoint, wires the session to it, and runs the tool
1041
+ * as one or more processes (legs). A paused node's tool call is held, and
1042
+ * past the hold window deferred; the next leg resumes the session once the
1043
+ * node continues (ADR-041). A tool that takes no message mid-session gets one
1044
+ * at the next leg, which resumes the session with it. A driver that cannot
1045
+ * hold the final answer to the report schema gets Ork's `report` tool, and
1046
+ * the session ends with the report it accepted. Interrupting stops the
1047
+ * running process.
1048
+ */
1049
+ /**
1050
+ * What a session's end is written with: the node's limits and program, and its
1051
+ * folder and the home folder taken out of the detail.
1052
+ */
1053
+ const scopeOf = (spec, bin, env) => ({
1054
+ facts: {
1055
+ ...spec.maxTurns === void 0 ? {} : { maxTurns: spec.maxTurns },
1056
+ ...spec.budgetUsd === void 0 ? {} : { budgetUsd: spec.budgetUsd },
1057
+ program: path.basename(bin)
1058
+ },
1059
+ scrub: {
1060
+ roots: [spec.cwd],
1061
+ home: env["HOME"] ?? homedir()
1062
+ }
1063
+ });
777
1064
  /** A spec that resumes `sessionId`; a fork's later legs continue the fork. */
778
1065
  const resumed = (spec, sessionId) => ({
779
1066
  ...spec,
@@ -830,6 +1117,12 @@ const createSessionRuntime = (adapter, options = {}) => ({
830
1117
  let reported = null;
831
1118
  const { hold } = ports;
832
1119
  const hook = hold === void 0 ? void 0 : adapter.hold;
1120
+ const heavy = heavyHook(ports.machine);
1121
+ ended.signal.addEventListener("abort", () => {
1122
+ heavy.release();
1123
+ }, { once: true });
1124
+ const bin = options.bin ?? adapter.command.bin;
1125
+ const scope = scopeOf(spec, bin, options.env ?? process.env);
833
1126
  const events = async function* () {
834
1127
  const tools = orkTools({
835
1128
  nodeId: spec.nodeId,
@@ -843,11 +1136,16 @@ const createSessionRuntime = (adapter, options = {}) => ({
843
1136
  }
844
1137
  });
845
1138
  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
- });
1139
+ const mcp = await startOrkMcpHttp(tools, hold === void 0 || hook === void 0 ? void 0 : toolHook({
1140
+ hold,
1141
+ heavy,
1142
+ answer: hook.answer,
1143
+ windowMs: options.holdWindowMs ?? hook.windowMs,
1144
+ ended: ended.signal,
1145
+ onDefer: () => {
1146
+ deferring = true;
1147
+ }
1148
+ }));
851
1149
  const endpoint = hook === void 0 ? {
852
1150
  url: mcp.url,
853
1151
  token: mcp.token
@@ -858,19 +1156,22 @@ const createSessionRuntime = (adapter, options = {}) => ({
858
1156
  try {
859
1157
  wired = yield* wireSaying(adapter, spec, endpoint, ended.signal, options);
860
1158
  } catch (error) {
861
- yield driverFailure(`could not reach Ork's tools: ${error instanceof Error ? error.message : String(error)}`);
1159
+ yield driverFailure(`could not reach Ork's tools: ${error instanceof Error ? error.message : String(error)}`, failureKind(error), scope);
862
1160
  return;
863
1161
  }
864
1162
  const env = {
865
1163
  ...options.env ?? process.env,
1164
+ PWD: spec.cwd,
866
1165
  ...adapter.context.env(spec),
867
1166
  ...wired.env
868
1167
  };
869
1168
  const legOf = (legSpec, carried) => ({
870
1169
  adapter,
871
- bin: options.bin ?? adapter.command.bin,
1170
+ bin,
872
1171
  env,
1172
+ scope,
873
1173
  cwd: spec.cwd,
1174
+ ...ports.machine === void 0 ? {} : { priority: ports.machine.priority },
874
1175
  args: [...adapter.command.args(legSpec, {
875
1176
  endpoint,
876
1177
  allowed
@@ -889,18 +1190,21 @@ const createSessionRuntime = (adapter, options = {}) => ({
889
1190
  let spent;
890
1191
  for (;;) {
891
1192
  if (ended.signal.aborted) {
892
- yield driverFailure("interrupted");
1193
+ yield driverFailure("interrupted", "interrupted", scope);
893
1194
  return;
894
1195
  }
895
1196
  const leg = yield* runLeg(legOf(legSpec, spent));
896
1197
  if (leg.kind === "failed") {
897
- yield driverFailure(leg.reason);
1198
+ yield driverFailure(leg.reason, leg.end, scope);
898
1199
  return;
899
1200
  }
900
1201
  if (leg.kind === "finished") return;
901
1202
  deferring = false;
902
1203
  spent = spent === void 0 ? leg.usage : adapter.usage.combine(spent, leg.usage);
903
- if (leg.kind === "deferred") await hold?.wait("tool", ended.signal);
1204
+ if (leg.kind === "deferred") {
1205
+ await heavy.ready();
1206
+ await hold?.wait("tool", ended.signal);
1207
+ }
904
1208
  if (ended.signal.aborted) {
905
1209
  yield leg.last;
906
1210
  return;
@@ -911,6 +1215,7 @@ const createSessionRuntime = (adapter, options = {}) => ({
911
1215
  } : resumed(spec, leg.sessionId);
912
1216
  }
913
1217
  } finally {
1218
+ heavy.release();
914
1219
  await wired?.release();
915
1220
  await mcp.close();
916
1221
  }
@@ -960,16 +1265,38 @@ const usageOf = (result) => {
960
1265
  durationMs: num$2(result["duration_ms"])
961
1266
  };
962
1267
  };
1268
+ /** Claude Code's error subtypes that name their own kind. */
1269
+ const SUBTYPE_KINDS = {
1270
+ error_max_turns: "turn_limit",
1271
+ error_max_budget_usd: "budget_limit",
1272
+ error_max_structured_output_retries: "invalid_report"
1273
+ };
1274
+ /**
1275
+ * What a result says went wrong, if anything: a subtype that names its kind, or
1276
+ * an error (`is_error`, or an error subtype) read by the provider classifier,
1277
+ * `unknown` when it says nothing Ork knows.
1278
+ */
1279
+ const signalOf$1 = (subtype, result, errors) => {
1280
+ const text = [...errors, str$2(result["result"])].join("\n").trim();
1281
+ const named = SUBTYPE_KINDS[subtype];
1282
+ if (named !== void 0) return endFor(named, {}, text);
1283
+ if (subtype === "success" && result["is_error"] !== true) return null;
1284
+ return errorEnd(text);
1285
+ };
963
1286
  /** The outcome a `result` message describes. */
964
1287
  const outcomeOf = (result) => {
965
- const errors = result["errors"];
1288
+ const said = result["errors"];
1289
+ const errors = Array.isArray(said) ? said.filter((e) => typeof e === "string") : [];
1290
+ const subtype = str$2(result["subtype"]) || "unknown";
1291
+ const report = parseReport(result["structured_output"]);
966
1292
  return {
967
- subtype: str$2(result["subtype"]) || "unknown",
1293
+ subtype,
1294
+ end: sessionEnd(signalOf$1(subtype, result, errors), report),
968
1295
  sessionId: str$2(result["session_id"]),
969
- report: parseReport(result["structured_output"]),
1296
+ report,
970
1297
  text: str$2(result["result"]),
971
1298
  usage: usageOf(result),
972
- errors: Array.isArray(errors) ? errors.filter((e) => typeof e === "string") : []
1299
+ errors
973
1300
  };
974
1301
  };
975
1302
  /** The year 2286: a reset beyond this is not in seconds. */
@@ -1113,12 +1440,14 @@ const isRecord = (value) => typeof value === "object" && value !== null && !Arra
1113
1440
  /**
1114
1441
  * The node's `--settings` with Ork's `PreToolUse` HTTP hook added: before every
1115
1442
  * tool call, pre-approved ones included, the session asks whether its node may
1116
- * go on.
1443
+ * go on. Its `PostToolUse` hook after a Bash call frees the call's slot.
1117
1444
  */
1118
1445
  const withHoldHook = (settings, holdUrl, token) => {
1119
1446
  const hooks = isRecord(settings?.["hooks"]) ? settings["hooks"] : {};
1120
- const listed = hooks["PreToolUse"];
1121
- const before = Array.isArray(listed) ? [...listed] : [];
1447
+ const listed = (event) => {
1448
+ const own = hooks[event];
1449
+ return Array.isArray(own) ? [...own] : [];
1450
+ };
1122
1451
  const hook = {
1123
1452
  type: "http",
1124
1453
  url: holdUrl,
@@ -1129,9 +1458,13 @@ const withHoldHook = (settings, holdUrl, token) => {
1129
1458
  ...settings,
1130
1459
  hooks: {
1131
1460
  ...hooks,
1132
- PreToolUse: [...before, {
1461
+ PreToolUse: [...listed("PreToolUse"), {
1133
1462
  matcher: "*",
1134
1463
  hooks: [hook]
1464
+ }],
1465
+ PostToolUse: [...listed("PostToolUse"), {
1466
+ matcher: "Bash",
1467
+ hooks: [hook]
1135
1468
  }]
1136
1469
  }
1137
1470
  };
@@ -1273,9 +1606,11 @@ const runCmd = (options, args, cwd) => new Promise((resolve) => {
1273
1606
  encoding: "utf8",
1274
1607
  timeout: CMD_TIMEOUT_MS
1275
1608
  }, (error, stdout, stderr) => {
1609
+ const said = `${stdout}${stderr}`.trim();
1610
+ const missing = error?.code === "ENOENT" ? `spawn ${options.bin} ENOENT` : "";
1276
1611
  resolve({
1277
1612
  ok: error === null,
1278
- out: `${stdout}${stderr}`.trim()
1613
+ out: said || missing
1279
1614
  });
1280
1615
  });
1281
1616
  });
@@ -1487,6 +1822,17 @@ const leaseWiring = (given) => ({
1487
1822
  }
1488
1823
  });
1489
1824
  // ../runtime/src/adapters/command-code-events.ts
1825
+ /**
1826
+ * @file The `command-code` adapter's `LegDecoder` and `UsageReader`: maps
1827
+ * Command Code's `--output-format json` lines (`{"type":"event"}` frames,
1828
+ * then one `{"type":"result"}` line; measured on 1.88.5,
1829
+ * specs/architecture.md, "Command Code") to Ork's runtime events. Tool names
1830
+ * are given Ork's (Claude Code's) names, so logs and the node's rules read
1831
+ * the same on every driver. One decoder reads one leg: the session id comes
1832
+ * in an early `run_start` frame and the result line counts no turns, so it
1833
+ * keeps both. The result line is held until the process exits: its exit code
1834
+ * says how the session ended (specs/architecture/command-code.md).
1835
+ */
1490
1836
  const isObject$1 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
1491
1837
  const num$1 = (value) => typeof value === "number" ? value : 0;
1492
1838
  const str$1 = (value) => typeof value === "string" ? value : "";
@@ -1513,6 +1859,28 @@ const SUBTYPES = {
1513
1859
  max_turns: "error_max_turns",
1514
1860
  error: "error_during_execution"
1515
1861
  };
1862
+ /**
1863
+ * Command Code's exit codes, by the kind they mean; 0 is a session that ended
1864
+ * well and 1 an error its text explains.
1865
+ */
1866
+ const COMMAND_CODE_EXITS = {
1867
+ 3: "auth",
1868
+ 4: "auth",
1869
+ 5: "rate_limited",
1870
+ 6: "provider_unavailable",
1871
+ 7: "provider_unavailable",
1872
+ 8: "turn_limit",
1873
+ 9: "provider_unavailable",
1874
+ 10: "out_of_credits",
1875
+ 130: "interrupted"
1876
+ };
1877
+ /** What a leg's exit code and result line say went wrong, if anything. */
1878
+ const signalOf = (code, error) => {
1879
+ const kind = code === null ? void 0 : COMMAND_CODE_EXITS[code];
1880
+ if (kind !== void 0) return endFor(kind, {}, error);
1881
+ if ((code === 0 || code === null) && error === "") return null;
1882
+ return errorEnd(error);
1883
+ };
1516
1884
  /** The text blocks of a `message_end` frame's content. */
1517
1885
  const textOf = (content) => (Array.isArray(content) ? content : []).filter((item) => isObject$1(item)).filter((block) => block["type"] === "text").map((block) => str$1(block["text"])).join("");
1518
1886
  /** The frames that end a tool call as an error: failed, denied, or blocked. */
@@ -1528,10 +1896,14 @@ const parseLine$1 = (line) => {
1528
1896
  return;
1529
1897
  }
1530
1898
  };
1531
- /** A decoder for one leg of a Command Code session. */
1899
+ /**
1900
+ * A decoder for one leg of a Command Code session; its end comes at the exit,
1901
+ * from the held result line and the exit code.
1902
+ */
1532
1903
  const commandCodeDecoder = () => {
1533
1904
  let sessionId = "";
1534
1905
  let turns = 0;
1906
+ let held;
1535
1907
  const frame = (event) => {
1536
1908
  const type = event["type"];
1537
1909
  const id = str$1(event["toolCallId"]);
@@ -1573,13 +1945,16 @@ const commandCodeDecoder = () => {
1573
1945
  }];
1574
1946
  return [];
1575
1947
  };
1576
- const result = (line) => {
1948
+ const result = (line, code) => {
1577
1949
  const usage = isObject$1(line["usage"]) ? line["usage"] : {};
1578
1950
  const error = str$1(line["error"]);
1951
+ const subtype = str$1(line["subtype"]);
1952
+ const signal = subtype === "max_turns" ? endFor("turn_limit", {}, error) : signalOf(code, error);
1579
1953
  return {
1580
1954
  type: "session.finished",
1581
1955
  outcome: {
1582
- subtype: SUBTYPES[str$1(line["subtype"])] ?? "error_during_execution",
1956
+ subtype: SUBTYPES[subtype] ?? "error_during_execution",
1957
+ end: signal ?? endFor("no_report"),
1583
1958
  sessionId: str$1(line["sessionId"]) || sessionId,
1584
1959
  report: null,
1585
1960
  text: str$1(line["finalText"]),
@@ -1596,12 +1971,21 @@ const commandCodeDecoder = () => {
1596
1971
  }
1597
1972
  };
1598
1973
  };
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) : [] };
1974
+ return {
1975
+ decode: (line) => {
1976
+ const message = parseLine$1(line);
1977
+ if (!isObject$1(message)) return { events: [] };
1978
+ if (message["type"] === "result") {
1979
+ held = message;
1980
+ return { events: [] };
1981
+ }
1982
+ const event = message["event"];
1983
+ return { events: message["type"] === "event" && isObject$1(event) ? frame(event) : [] };
1984
+ },
1985
+ exit: (code) => {
1986
+ if (held !== void 0) return [result(held, code)];
1987
+ return (code === null ? void 0 : COMMAND_CODE_EXITS[code]) === void 0 ? [] : [result({ error: "" }, code)];
1988
+ }
1605
1989
  };
1606
1990
  };
1607
1991
  /** Command Code reports tokens, never dollars. */
@@ -1617,12 +2001,14 @@ const commandCodeUsage = {
1617
2001
  * `--permission-mode yolo` and loads, with `--mod`, a mod written for the
1618
2002
  * session outside the checkout: its `beforeToolCall` hook blocks every call
1619
2003
  * 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.
2004
+ * Command Code's tool loader passes, and reads inside the worktree need no
2005
+ * rule, as with Claude Code; one reaching outside it does. The rules are the
2006
+ * node's, in Claude Code's words (`Bash(git:*)`, `Read`, `mcp__github`); a
2007
+ * specifier Ork does not read never allows a call and always denies one.
1623
2008
  */
1624
2009
  /** The rules of a session: an `acceptEdits` node may edit without a rule. */
1625
2010
  const modRules = (spec) => ({
2011
+ cwd: spec.cwd,
1626
2012
  allow: [...spec.tools.allow, ...spec.permissionMode === "acceptEdits" ? ["Edit"] : []],
1627
2013
  deny: spec.tools.deny,
1628
2014
  ...spec.tools.only === void 0 ? {} : { only: spec.tools.only }
@@ -1632,7 +2018,20 @@ const modRules = (spec) => ({
1632
2018
  * export taking the mod API), so it carries its own matcher.
1633
2019
  */
1634
2020
  const modSource = (rules) => `// Ork's rules for one session; written by Ork, removed after it.
2021
+ import { relative, resolve } from "node:path";
1635
2022
  const RULES = ${JSON.stringify(rules)};
2023
+ // Command Code's own: loading a deferred tool's schema, Ork's report included.
2024
+ const META = new Set(["search_tools", "tool_search"]);
2025
+ // Reads a session makes in its worktree without a rule, as Claude Code does.
2026
+ const READS = new Set(["read_file", "read_multiple_files", "read_directory", "glob", "grep"]);
2027
+ const pathsOf = input => Object.entries(input ?? {})
2028
+ .filter(([key]) => /path|file|dir/i.test(key))
2029
+ .flatMap(([, value]) => (Array.isArray(value) ? value : [value]))
2030
+ .filter(value => typeof value === "string");
2031
+ const inside = input => pathsOf(input).every(each => {
2032
+ const from = relative(RULES.cwd, resolve(RULES.cwd, each));
2033
+ return from === "" || (!from.startsWith("..") && !from.startsWith("/"));
2034
+ });
1636
2035
  const TOOLS = ${JSON.stringify(COMMAND_CODE_TOOLS)};
1637
2036
  // A rule's word, as Command Code reads it, covers these of Ork's tool names.
1638
2037
  const COVERS = { read: ["Read", "Glob", "Grep"], edit: ["Edit", "Write"] };
@@ -1671,10 +2070,11 @@ const matches = (rule, name, input) => {
1671
2070
  };
1672
2071
  export const decide = (name, input) => {
1673
2072
  if (RULES.deny.some(rule => matches(rule, name, input) !== false)) return "denied for this node";
1674
- if (name.startsWith("mcp__ork__")) return undefined;
2073
+ if (name.startsWith("mcp__ork__") || META.has(name)) return undefined;
1675
2074
  if (RULES.only !== undefined && !RULES.only.some(rule => matches(rule, name, input) === true)) {
1676
2075
  return "not one of this node's tools";
1677
2076
  }
2077
+ if (READS.has(name) && inside(input)) return undefined;
1678
2078
  if (RULES.allow.some(rule => matches(rule, name, input) === true)) return undefined;
1679
2079
  return "not allowed for this node";
1680
2080
  };
@@ -1718,8 +2118,10 @@ const queryOf = (spec) => {
1718
2118
  return text.startsWith("-") ? ` ${text}` : text;
1719
2119
  };
1720
2120
  /**
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.
2121
+ * The `cmd` arguments for a session spec. `--dangerously-skip-permissions`: in
2122
+ * `-p` mode only it lifts Command Code's own gate on edits and shell (the
2123
+ * permission mode alone does not), so the session's mod, which refuses what the
2124
+ * node did not allow, is the only gate.
1723
2125
  */
1724
2126
  const commandCodeArgs = (spec) => {
1725
2127
  const args = [
@@ -1729,6 +2131,7 @@ const commandCodeArgs = (spec) => {
1729
2131
  "json",
1730
2132
  "--permission-mode",
1731
2133
  "yolo",
2134
+ "--dangerously-skip-permissions",
1732
2135
  "--skip-onboarding",
1733
2136
  "--no-auto-update"
1734
2137
  ];
@@ -1747,8 +2150,8 @@ const commandCodeAdapter = {
1747
2150
  bin: "cmd",
1748
2151
  args: (spec) => commandCodeArgs(spec)
1749
2152
  },
1750
- decode: (line) => commandCodeDecoder()(line),
1751
- decoder: commandCodeDecoder,
2153
+ decode: (line) => commandCodeDecoder().decode(line),
2154
+ legDecoder: () => commandCodeDecoder(),
1752
2155
  mcp: {
1753
2156
  kind: "lease",
1754
2157
  wire: async (spec, endpoint, hooks) => {
@@ -1856,8 +2259,16 @@ const ACTIONS = {
1856
2259
  TodoWrite: "todowrite"
1857
2260
  };
1858
2261
  const BUILT_IN = new Set(Object.values(ACTIONS));
1859
- /** Actions whose resource is a path: a bare allow stays in the worktree. */
2262
+ /**
2263
+ * Actions whose resource is a path. OpenCode matches it relative to the
2264
+ * worktree (an absolute path inside it too), `*` across folders, and asks
2265
+ * `external_directory`, denied first, for any path outside it.
2266
+ */
1860
2267
  const PATH_ACTIONS = /* @__PURE__ */ new Set(["read", "edit"]);
2268
+ /** A rule's path as OpenCode matches it: relative to the worktree. */
2269
+ const pathResource = (spec, cwd) => {
2270
+ return (path.isAbsolute(spec) ? path.relative(cwd, spec) : spec).replace(/^\.\//u, "") || "*";
2271
+ };
1861
2272
  /** OpenCode's name for an MCP server's tool: other characters become `_`. */
1862
2273
  const mcpAction = (server, tool) => `${server}_${tool}`.replaceAll(/[^A-Za-z0-9_*-]/gu, "_");
1863
2274
  /** A shell rule's resource: Claude Code's `git status:*` is `git status *`. */
@@ -1883,16 +2294,22 @@ const ruleOf = (rule, effect, cwd) => {
1883
2294
  if (action === void 0) return void 0;
1884
2295
  if (spec !== void 0) return {
1885
2296
  action,
1886
- resource: action === "shell" ? shellResource(spec) : PATH_ACTIONS.has(action) ? path.resolve(cwd, spec) : spec,
2297
+ resource: action === "shell" ? shellResource(spec) : PATH_ACTIONS.has(action) ? pathResource(spec, cwd) : spec,
1887
2298
  effect
1888
2299
  };
1889
2300
  return {
1890
2301
  action,
1891
- resource: effect === "allow" && PATH_ACTIONS.has(action) ? `${cwd}/*` : "*",
2302
+ resource: "*",
1892
2303
  effect
1893
2304
  };
1894
2305
  };
1895
2306
  const rulesOf = (rules, effect, cwd) => rules.flatMap((rule) => ruleOf(rule, effect, cwd) ?? []);
2307
+ /** An action allowed anywhere in the worktree; outside it is denied. */
2308
+ const worktree = (action) => ({
2309
+ action,
2310
+ resource: "*",
2311
+ effect: "allow"
2312
+ });
1896
2313
  /**
1897
2314
  * The session's rules, in order: deny everything; read in the worktree (as
1898
2315
  * Claude Code reads without asking); edits there under `acceptEdits`; the
@@ -1903,11 +2320,6 @@ const permissionRules = (spec) => {
1903
2320
  const { cwd, tools } = spec;
1904
2321
  const only = tools.only === void 0 ? void 0 : new Set(rulesOf(tools.only, "allow", cwd).map((r) => r.action));
1905
2322
  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
2323
  return [
1912
2324
  {
1913
2325
  action: "*",
@@ -1977,6 +2389,16 @@ const sessionConfig = (spec, endpoint, instructions) => ({
1977
2389
  } }
1978
2390
  });
1979
2391
  // ../runtime/src/adapters/open-code-events.ts
2392
+ /**
2393
+ * @file The `open-code` adapter's decoder and usage reader. `opencode run
2394
+ * --format json` prints one JSON object per line, `{type, timestamp,
2395
+ * sessionID, part}` (2.1.0, read from its source): `step_start`, `text`,
2396
+ * `tool_use` (a finished call, `state.status` `completed` or `error`),
2397
+ * `step_finish` (`cost` in dollars, `tokens`) and `error`. There is no result
2398
+ * line: the last `text` is the final text and the process's exit ends the
2399
+ * session. Tool names come back in Claude Code's words (`shell` is `Bash`,
2400
+ * `ork_report` is `mcp__ork__report`), the words a node's rules use.
2401
+ */
1980
2402
  const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
1981
2403
  const get = (value, key) => isObject(value) ? value[key] : void 0;
1982
2404
  const num = (value) => typeof value === "number" ? value : 0;
@@ -2075,10 +2497,27 @@ const decodeOpenCodeLine = (line) => {
2075
2497
  default: return { events: [] };
2076
2498
  }
2077
2499
  };
2500
+ /** An `error` line's words, for the session's errors. */
2501
+ const errorText = (error) => str(get(get(error, "data"), "message")) || str(get(error, "message")) || str(get(error, "name")) || "error";
2502
+ /** An `error` line's end: its name, status and body, through the classifier. */
2503
+ const errorLineEnd = (error) => {
2504
+ const data = get(error, "data");
2505
+ const status = get(data, "statusCode");
2506
+ const said = [
2507
+ str(get(error, "name")),
2508
+ errorText(error),
2509
+ str(get(data, "responseBody"))
2510
+ ].filter((part) => part !== "").join("\n");
2511
+ return errorEnd(said, "unknown", typeof status === "number" ? status : void 0);
2512
+ };
2513
+ /** The exit code OpenCode gives an interrupted session. */
2514
+ const INTERRUPTED = 130;
2078
2515
  /**
2079
2516
  * A decoder for one leg of a session: it starts the session at the first line
2080
2517
  * that names it, and keeps the last text, the steps' usage and the errors for
2081
- * the end.
2518
+ * the end. OpenCode prints nothing at its step cap (`steps`, the node's
2519
+ * `maxTurns`): a session that used every step and handed in no report reached
2520
+ * its turn limit, which the runner settles once it knows the report.
2082
2521
  */
2083
2522
  const openCodeDecoder = (spec) => {
2084
2523
  const began = Date.now();
@@ -2086,6 +2525,7 @@ const openCodeDecoder = (spec) => {
2086
2525
  let text = "";
2087
2526
  let usage = NONE;
2088
2527
  const errors = [];
2528
+ let failed = null;
2089
2529
  return {
2090
2530
  decode: (line) => {
2091
2531
  const message = parseLine(line);
@@ -2108,20 +2548,22 @@ const openCodeDecoder = (spec) => {
2108
2548
  case "step_finish":
2109
2549
  usage = sumUsage(usage, stepUsage(message["part"]));
2110
2550
  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
- }
2551
+ case "error":
2552
+ errors.push(errorText(message["error"]));
2553
+ failed ??= errorLineEnd(message["error"]);
2116
2554
  }
2117
2555
  return { events };
2118
2556
  },
2119
2557
  exit: (code) => {
2120
- if (code !== 0 && errors.length === 0) return [];
2558
+ const interrupted = code === INTERRUPTED;
2559
+ if (code !== 0 && errors.length === 0 && !interrupted) return [];
2560
+ const capped = spec.maxTurns !== void 0 && usage.turns >= spec.maxTurns;
2561
+ const end = interrupted ? endFor("interrupted", {}, errors.join("\n")) : failed ?? endFor(capped ? "turn_limit" : "no_report");
2121
2562
  return [{
2122
2563
  type: "session.finished",
2123
2564
  outcome: {
2124
2565
  subtype: code === 0 ? "success" : "error_during_execution",
2566
+ end,
2125
2567
  sessionId,
2126
2568
  report: null,
2127
2569
  text,
@@ -2230,7 +2672,7 @@ const notBuilt = (driver) => ({
2230
2672
  driver,
2231
2673
  start: () => ({
2232
2674
  events: (async function* () {
2233
- yield driverFailure(notBuiltYet(driver));
2675
+ yield driverFailure(notBuiltYet(driver), "model_unavailable", { facts: {} });
2234
2676
  })(),
2235
2677
  send: () => {},
2236
2678
  interrupt: () => Promise.resolve()