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

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 (36) hide show
  1. package/dist/{__build_info__-CIchF6nr.mjs → __build_info__-BJbE3Zvp.mjs} +3 -2
  2. package/dist/attach-BmV5ckb9.mjs +89 -0
  3. package/dist/bin.mjs +132 -1208
  4. package/dist/{decide-B6PnKeqo.mjs → builtin-files-w0YULJ4c.mjs} +2 -50
  5. package/dist/{chips-BA7SrrH5.mjs → chips-CyeB_rhR.mjs} +1 -1
  6. package/dist/{clean-Di-KT7Mf.mjs → clean-D8GI_2K-.mjs} +1 -1
  7. package/dist/{completion-C5D0sEs5.mjs → completion-CzElB8nS.mjs} +7 -6
  8. package/dist/{context-BaNshWr9.mjs → context-BAwgVSX5.mjs} +5 -5
  9. package/dist/decide-DlbyctOE.mjs +50 -0
  10. package/dist/{find-BicBbGMg.mjs → find-p05wlbmu.mjs} +1 -1
  11. package/dist/{format-lWhN-jYR.mjs → format-Dni98cbh.mjs} +5 -85
  12. package/dist/{help-groups-BwYoAWsh.mjs → help-groups-DYP_zMRP.mjs} +4 -0
  13. package/dist/{in-a-run-CgkVouyG.mjs → in-a-run-BI-hLIl_.mjs} +4 -2
  14. package/dist/{limits-jZQp9ZJC.mjs → limits-CElmhowV.mjs} +1 -1
  15. package/dist/{list-CEaJ07jd.mjs → list-zW2xKuGr.mjs} +1 -1
  16. package/dist/{mcp-DYgVA9f5.mjs → mcp-CM3b65AZ.mjs} +6 -6
  17. package/dist/{output-CM95njYt.mjs → output-CnRzrbUM.mjs} +3 -1
  18. package/dist/{pause-D6PuSlNl.mjs → pause-BuS83WAI.mjs} +4 -4
  19. package/dist/{relay-DNx7be9k.mjs → relay-DAsJA8wF.mjs} +2 -2
  20. package/dist/{remove-CpMb4NDl.mjs → remove-Dh7D_hVn.mjs} +3 -3
  21. package/dist/{send-CLs_fyg7.mjs → send-MFEQZH31.mjs} +3 -3
  22. package/dist/{src-9iGkjnsy.mjs → src-BfxkJOYm.mjs} +1 -1
  23. package/dist/{src-D6_xjm-x.mjs → src-CdDTQLdQ.mjs} +173 -24
  24. package/dist/{src-BnZgZGrf.mjs → src-D1QedD5y.mjs} +1 -1
  25. package/dist/{src-B6KvTm7B.mjs → src-S9yK2OHU.mjs} +7 -6
  26. package/dist/store-B2OaE4ID.mjs +1356 -0
  27. package/dist/ui/assets/{app-CxURph7a.js → app-CAoZtku5.js} +1 -1
  28. package/dist/ui/assets/{index-By_8KKG6.js → index-BL7Ad0OA.js} +2 -2
  29. package/dist/ui/index.html +1 -1
  30. package/dist/{ui-CIci4UBW.mjs → ui-C4Ys26oO.mjs} +4 -4
  31. package/dist/{usage-GceBGMjU.mjs → usage-C2zx-X6q.mjs} +6 -6
  32. package/dist/verdict-CIy8tAyX.mjs +84 -0
  33. package/dist/{watch-BmoybMQg.mjs → watch-DzCeqM05.mjs} +7 -7
  34. package/dist/{workflows-DZvW3i1e.mjs → workflows-Rr5w90A0.mjs} +3 -3
  35. package/dist/{worktrees-Be51E2rB.mjs → worktrees-f6SAbum5.mjs} +5 -5
  36. package/package.json +1 -1
@@ -0,0 +1,1356 @@
1
+ import { $t as endOf, C as openStore, En as maskSecrets, S as isNodeEvent, en as outcomeEnd, nn as END_KINDS, tt as followEvents, xt as liveStatus, zt as slotWaitLine } from "./src-CdDTQLdQ.mjs";
2
+ import { n as pendingDecisions, t as answerDecision } from "./decide-DlbyctOE.mjs";
3
+ import { E as readKeys, S as cut, T as wrap, _ as withdrawn, a as style, c as confirmOrStop, d as apply, f as bold, g as readLine, h as interrupt, i as plain, m as createRegion, n as cost, p as paint, v as canKey, w as widthOf, x as termOf, y as plainEnv } from "./output-CnRzrbUM.mjs";
4
+ import { n as chip, r as nodeChip, t as CHIP_LABELS } from "./chips-CyeB_rhR.mjs";
5
+ import { a as createView, i as formatBusEvent, n as nodeVerdict, o as styler, r as firstLine, t as hasConflict } from "./verdict-CIy8tAyX.mjs";
6
+ import { createInterface } from "node:readline/promises";
7
+ // ../core/src/control/queue.ts
8
+ const PERMISSION_OPTIONS = [{
9
+ id: "allow",
10
+ label: "Allow"
11
+ }, {
12
+ id: "deny",
13
+ label: "Deny"
14
+ }];
15
+ const DENIED = "The user did not allow this tool.";
16
+ /** The deny message a session gets, with the person's words when given. */
17
+ const deniedMessage = (note) => note === void 0 ? DENIED : `${DENIED} They said: ${note}`;
18
+ /** The input fields that say what a tool will do, the first one present. */
19
+ const TOOL_INPUT_KEYS = [
20
+ "command",
21
+ "file_path",
22
+ "path",
23
+ "url",
24
+ "pattern"
25
+ ];
26
+ /** A tool's input for the decision's context: as written, cut at `max`. */
27
+ const inputContext = (input, max = 4e3) => {
28
+ const key = TOOL_INPUT_KEYS.find((k) => typeof input[k] === "string");
29
+ const text = key === void 0 ? JSON.stringify(input, void 0, 2) : String(input[key]);
30
+ return text.length > max ? `${text.slice(0, max - 1)}…` : text;
31
+ };
32
+ const createDecisionQueue = (options) => {
33
+ const { store, runId, emit } = options;
34
+ const earlier = store.decisions({ runId });
35
+ let count = Math.max(0, ...earlier.map(({ id }) => Number(id.slice(runId.length + 1)) || 0));
36
+ for (const { id, node, status } of earlier) {
37
+ if (status !== "open") continue;
38
+ store.withdrawDecision(id);
39
+ emit({
40
+ type: "decision.withdrawn",
41
+ node,
42
+ decision: id
43
+ });
44
+ }
45
+ /** Opens a decision and settles it with the first answer. */
46
+ const decide = (open, local, withdrawn) => {
47
+ count += 1;
48
+ const id = `${runId}-${String(count)}`;
49
+ const record = store.openDecision({
50
+ ...open,
51
+ id,
52
+ runId
53
+ });
54
+ const event = {
55
+ type: "decision.opened",
56
+ node: record.node,
57
+ decision: id,
58
+ kind: record.kind,
59
+ question: record.question,
60
+ options: record.options
61
+ };
62
+ if (record.recommended !== void 0) event.recommended = record.recommended;
63
+ if (record.owner !== void 0) event.owner = record.owner;
64
+ emit(event);
65
+ const { promise, resolve, reject } = Promise.withResolvers();
66
+ const settled = new AbortController();
67
+ /** `note` is the stored one: masked, and absent when blank. */
68
+ const finish = (answer, by, note) => {
69
+ if (settled.signal.aborted) return;
70
+ settled.abort();
71
+ const answered = {
72
+ type: "decision.answered",
73
+ node: open.node,
74
+ decision: id,
75
+ answer,
76
+ by
77
+ };
78
+ if (note !== void 0) answered.note = note;
79
+ emit(answered);
80
+ resolve(note === void 0 ? {
81
+ answer,
82
+ by
83
+ } : {
84
+ answer,
85
+ by,
86
+ note
87
+ });
88
+ };
89
+ const withdraw = () => {
90
+ if (settled.signal.aborted) return;
91
+ settled.abort();
92
+ store.withdrawDecision(id);
93
+ emit({
94
+ type: "decision.withdrawn",
95
+ node: open.node,
96
+ decision: id
97
+ });
98
+ reject(/* @__PURE__ */ new Error("the question was withdrawn"));
99
+ };
100
+ if (withdrawn?.aborted === true) withdraw();
101
+ withdrawn?.addEventListener("abort", withdraw, { once: true });
102
+ const fromStore = async () => {
103
+ try {
104
+ const closed = await options.waitFor(id, settled.signal);
105
+ if (closed.status === "answered" && closed.answer !== void 0) finish(closed.answer, closed.answeredBy ?? "decide", closed.note);
106
+ } catch {}
107
+ };
108
+ const fromTerminal = async (ask) => {
109
+ try {
110
+ const { answer, note } = await ask(settled.signal);
111
+ if (store.answerDecision(id, answer, "terminal", note)) finish(answer, "terminal", store.decision(id)?.note);
112
+ } catch {}
113
+ };
114
+ fromStore();
115
+ if (local !== void 0) fromTerminal(local);
116
+ return promise;
117
+ };
118
+ return {
119
+ ask: (question) => {
120
+ const open = {
121
+ node: question.nodeId,
122
+ kind: "question",
123
+ question: question.question,
124
+ options: question.options
125
+ };
126
+ if (question.recommended !== void 0) open.recommended = question.recommended;
127
+ if (question.context !== void 0) open.context = question.context;
128
+ if (question.document !== void 0) open.document = question.document;
129
+ if (question.owner !== void 0) open.owner = question.owner;
130
+ const { local } = options;
131
+ return decide(open, local === void 0 ? void 0 : (signal) => {
132
+ let note;
133
+ return local.ask({
134
+ ...question,
135
+ question: maskSecrets(question.question),
136
+ ...question.context === void 0 ? {} : { context: maskSecrets(question.context) },
137
+ ...question.document === void 0 ? {} : { document: maskSecrets(question.document) },
138
+ signal,
139
+ onNote: (given) => {
140
+ note = given;
141
+ }
142
+ }).then((answer) => note === void 0 ? { answer } : {
143
+ answer,
144
+ note
145
+ });
146
+ }, question.signal).then(({ answer, note }) => {
147
+ if (note !== void 0) question.onNote?.(note);
148
+ return answer;
149
+ });
150
+ },
151
+ settled: (question) => {
152
+ count += 1;
153
+ const id = `${runId}-${String(count)}`;
154
+ const { owner } = question;
155
+ const record = store.openDecision({
156
+ id,
157
+ runId,
158
+ node: question.nodeId,
159
+ kind: "question",
160
+ question: question.question,
161
+ options: question.options,
162
+ owner,
163
+ ...question.recommended === void 0 ? {} : { recommended: question.recommended },
164
+ ...question.context === void 0 ? {} : { context: question.context }
165
+ });
166
+ emit({
167
+ type: "decision.opened",
168
+ node: record.node,
169
+ decision: id,
170
+ kind: "question",
171
+ question: record.question,
172
+ options: record.options,
173
+ owner,
174
+ ...record.recommended === void 0 ? {} : { recommended: record.recommended }
175
+ });
176
+ store.answerDecision(id, question.answer, "owner", question.note);
177
+ const note = store.decision(id)?.note;
178
+ emit({
179
+ type: "decision.answered",
180
+ node: record.node,
181
+ decision: id,
182
+ answer: question.answer,
183
+ by: "owner",
184
+ owner,
185
+ ...note === void 0 ? {} : { note }
186
+ });
187
+ },
188
+ permit: async (request) => {
189
+ const { local } = options;
190
+ let localMessage = DENIED;
191
+ const { answer, note } = await decide({
192
+ node: request.nodeId,
193
+ kind: "permission",
194
+ question: `Allow ${request.toolName}?`,
195
+ options: PERMISSION_OPTIONS,
196
+ context: inputContext(request.input)
197
+ }, local === void 0 ? void 0 : (signal) => {
198
+ let said;
199
+ return local.permit({
200
+ ...request,
201
+ signal,
202
+ onNote: (given) => {
203
+ said = given;
204
+ }
205
+ }).then((result) => {
206
+ if (!result.allow) localMessage = result.message;
207
+ const choice = result.allow ? "allow" : "deny";
208
+ return said === void 0 ? { answer: choice } : {
209
+ answer: choice,
210
+ note: said
211
+ };
212
+ });
213
+ }, request.signal);
214
+ if (note !== void 0) request.onNote?.(note);
215
+ return answer === "allow" ? { allow: true } : {
216
+ allow: false,
217
+ message: note === void 0 ? localMessage : deniedMessage(note)
218
+ };
219
+ }
220
+ };
221
+ };
222
+ // ../core/src/remote/follow-tree.ts
223
+ /**
224
+ * Calls `onEvent` for each event of `root` and of the runs it called, once
225
+ * each. A called run's stored events come right after the `call.started` that
226
+ * started it; live events as they arrive. Ends when the root ends or `signal`
227
+ * aborts.
228
+ */
229
+ const followRunTree = async (store, root, onEvent, options = {}) => {
230
+ /** Every run followed, with its call path, in the order found. */
231
+ const paths = /* @__PURE__ */ new Map([[root, []]]);
232
+ /** Each run's last event seen, for the catch-up once the root ends. */
233
+ const seqs = /* @__PURE__ */ new Map();
234
+ const followers = [];
235
+ /** Ends the called runs' following once the root's has ended. */
236
+ const halt = new AbortController();
237
+ const signal = options.signal === void 0 ? halt.signal : AbortSignal.any([options.signal, halt.signal]);
238
+ const handle = (run, path) => (stored) => {
239
+ seqs.set(run, stored.seq);
240
+ onEvent({
241
+ run,
242
+ path,
243
+ stored
244
+ });
245
+ const { event } = stored;
246
+ if (event.type === "call.started" && !paths.has(event.run)) follow(event.run, [...path, event.node]);
247
+ };
248
+ const follow = (run, path) => {
249
+ paths.set(run, path);
250
+ followers.push(followEvents(store, run, handle(run, path), {
251
+ ...options,
252
+ follow: true,
253
+ after: seqs.get(run) ?? 0,
254
+ signal
255
+ }));
256
+ };
257
+ /** Waits for every follower, including those started while waiting. */
258
+ const settle = async () => {
259
+ while (followers.length > 0) await Promise.all(followers.splice(0));
260
+ };
261
+ const end = await followEvents(store, root, handle(root, []), {
262
+ ...options,
263
+ follow: true
264
+ });
265
+ halt.abort();
266
+ await settle();
267
+ if (end !== "aborted") {
268
+ for (const [run, path] of [...paths].slice(1)) followers.push(followEvents(store, run, handle(run, path), {
269
+ follow: false,
270
+ after: seqs.get(run) ?? 0
271
+ }));
272
+ await settle();
273
+ }
274
+ return {
275
+ end,
276
+ stillRunning: [...paths.keys()].slice(1).filter((id) => {
277
+ const [run] = store.lookup(id);
278
+ return run !== void 0 && liveStatus(run) === "running";
279
+ })
280
+ };
281
+ };
282
+ // src/human-card.ts
283
+ /** Lines of context a card shows before `d` (or `--verbose`) shows the rest. */
284
+ const CONTEXT_LINES = 6;
285
+ /** Text from outside with its escapes gone and its line breaks kept. */
286
+ const clean = (text) => text.split("\n").map((line) => plain(line)).join("\n");
287
+ /** The card above the options, in the rich look or the plain lines. */
288
+ const cardLines = (t, q, place, verbose, document) => {
289
+ const context = q.context === void 0 ? void 0 : clean(q.context);
290
+ if (!t.rich) return {
291
+ lines: [
292
+ ...place === void 0 ? [] : [`run ${place.run}${place.path === void 0 ? "" : ` · ${place.path}`} · decision ${place.decision}`],
293
+ ...document === void 0 ? [] : [document],
294
+ `? ${plain(q.nodeId)} asks: ${clean(q.question)}`,
295
+ ...context === void 0 ? [] : [` ${context.replaceAll("\n", "\n ")}`]
296
+ ],
297
+ moreContext: false
298
+ };
299
+ const shown = context === void 0 ? [] : wrap(context, t.width - 3);
300
+ const cutShort = !verbose && shown.length > CONTEXT_LINES;
301
+ const body = cutShort ? shown.slice(0, CONTEXT_LINES) : shown;
302
+ const where = place === void 0 ? "" : ` ${paint(t, "muted", [
303
+ `run ${place.run}`,
304
+ place.path,
305
+ place.index === void 0 || place.total === void 0 ? void 0 : `${String(place.index)} of ${String(place.total)}`
306
+ ].filter((part) => part !== void 0).join(" · "))}`;
307
+ return {
308
+ lines: [
309
+ "",
310
+ ` ${chip(t, CHIP_LABELS.ask, "accent")} ${nodeChip(t, plain(q.nodeId))}${where}`,
311
+ ...wrap(clean(q.question), t.width - 1).map((l) => ` ${bold(t, l)}`),
312
+ ...body.map((l) => paint(t, "muted", ` ${l}`)),
313
+ ...cutShort ? [paint(t, "muted", ` … ${String(shown.length - CONTEXT_LINES)} more lines (d)`)] : []
314
+ ],
315
+ moreContext: cutShort
316
+ };
317
+ };
318
+ /** The line above a permission prompt: who wants to run what. */
319
+ const permitLine = (t, nodeId, preview) => {
320
+ const node = plain(nodeId);
321
+ const what = plain(preview);
322
+ if (!t.rich) return `? ${node} wants to run ${what}`;
323
+ const room = Math.max(20, t.width - node.length - 24);
324
+ return ` ${chip(t, CHIP_LABELS.ask, "accent")} ${nodeChip(t, node)} wants to run ${cut(what, room)}`;
325
+ };
326
+ /**
327
+ * The line a card collapses to once answered: `✓ 509da722-1 · add-invalid-node`
328
+ * (the decision, or the node when there is none, and the choice), or a note
329
+ * that it was left for later.
330
+ */
331
+ const doneLine = (t, place, nodeId, answer) => {
332
+ const who = place?.decision ?? plain(nodeId);
333
+ if (answer.kind === "later") return ` ${paint(t, "muted", "·")} ${cut(`${who} left for later`, t.width - 3)}`;
334
+ const said = answer.note === void 0 ? "" : ` · said: ${plain(answer.note)}`;
335
+ return ` ${paint(t, "ok", "✓")} ${cut(`${who} · ${answer.id}${said}`, t.width - 3)}`;
336
+ };
337
+ // src/term/text.ts
338
+ /**
339
+ * @file The text prompt: one line of text, edited in place on a terminal
340
+ * (arrows, Home/End, Backspace, Delete, Ctrl-U to clear), collapsing to one
341
+ * line when answered; Enter sends, Esc (or Ctrl-D on an empty line) leaves
342
+ * it. Plain mode asks `message: ` and reads a line, as the prompts always
343
+ * did. `editLine` is the editing itself, which the select prompt reuses for
344
+ * its note.
345
+ */
346
+ const at = (next, to) => ({
347
+ value: next.join(""),
348
+ pos: to
349
+ });
350
+ /** `state` after `key`; keys that do not edit leave it as it is. */
351
+ const editLine = (state, key) => {
352
+ const { value, pos } = state;
353
+ const chars = Array.from(value);
354
+ if (key.name === "char") return at(chars.toSpliced(pos, 0, key.text), pos + 1);
355
+ if (key.name === "backspace") return pos === 0 ? state : at(chars.toSpliced(pos - 1, 1), pos - 1);
356
+ if (key.name === "delete") return at(chars.toSpliced(pos, 1), pos);
357
+ if (key.name === "left") return {
358
+ value,
359
+ pos: Math.max(0, pos - 1)
360
+ };
361
+ if (key.name === "right") return {
362
+ value,
363
+ pos: Math.min(chars.length, pos + 1)
364
+ };
365
+ if (key.name === "home" || key.name === "ctrl-a") return {
366
+ value,
367
+ pos: 0
368
+ };
369
+ if (key.name === "end" || key.name === "ctrl-e") return {
370
+ value,
371
+ pos: chars.length
372
+ };
373
+ if (key.name === "ctrl-u") return at(chars.slice(pos), 0);
374
+ return state;
375
+ };
376
+ /**
377
+ * The line to draw for `state` after `lead`, within `width`, and the column of
378
+ * the cursor in it. A value too long for the room scrolls to keep the cursor in
379
+ * view.
380
+ */
381
+ const showLine = (lead, state, width, leadWidth) => {
382
+ const chars = Array.from(state.value);
383
+ const room = Math.max(1, width - leadWidth - 1);
384
+ const start = Math.max(0, state.pos - room + 1);
385
+ return {
386
+ line: `${lead}${chars.slice(start, start + room).join("")}`,
387
+ column: leadWidth + state.pos - start
388
+ };
389
+ };
390
+ const defaultDone$1 = (io, message) => (value) => {
391
+ const term = termOf(io.output, io.env);
392
+ const mark = paint(term, "ok", "✓");
393
+ return cut(` ${mark} ${message} · ${value === void 0 || value === "" ? "skipped" : value}`, term.width);
394
+ };
395
+ /** Asks for one line of text. */
396
+ const readText = async (io, options) => {
397
+ const { message, signal } = options;
398
+ if (signal?.aborted === true) throw withdrawn(signal);
399
+ if (!canKey(io.input, io.output, io.env)) {
400
+ const answer = await readLine(io, ` ${message}: `, signal);
401
+ return answer === void 0 ? { kind: "cancelled" } : {
402
+ kind: "text",
403
+ value: answer.trim()
404
+ };
405
+ }
406
+ const done = options.done ?? defaultDone$1(io, message);
407
+ const term = () => termOf(io.output, io.env);
408
+ const region = createRegion(io.output, term);
409
+ const initial = options.initial ?? "";
410
+ return new Promise((resolve, reject) => {
411
+ let state = {
412
+ value: initial,
413
+ pos: Array.from(initial).length
414
+ };
415
+ const lead = ` ${paint(term(), "accent", "?")} ${bold(term(), message)} › `;
416
+ const leadWidth = Array.from(` ? ${message} › `).length;
417
+ const render = () => {
418
+ const { line, column } = showLine(lead, state, term().width, leadWidth);
419
+ region.draw([line], column);
420
+ };
421
+ const finish = (result, line) => {
422
+ stop();
423
+ io.output.off("resize", render);
424
+ signal?.removeEventListener("abort", onAbort);
425
+ region.finish(line);
426
+ resolve(result);
427
+ };
428
+ const onAbort = () => {
429
+ stop();
430
+ io.output.off("resize", render);
431
+ region.finish();
432
+ reject(withdrawn(signal));
433
+ };
434
+ const stop = readKeys(io.input, (key) => {
435
+ if (key.name === "enter") {
436
+ const value = state.value.trim();
437
+ finish({
438
+ kind: "text",
439
+ value
440
+ }, done(value));
441
+ } else if (key.name === "escape" || key.name === "ctrl-d" && state.value === "") finish({ kind: "cancelled" }, done());
442
+ else if (key.name === "ctrl-c") {
443
+ finish({ kind: "interrupted" });
444
+ interrupt(io);
445
+ } else {
446
+ state = editLine(state, key);
447
+ render();
448
+ }
449
+ });
450
+ io.output.on("resize", render);
451
+ signal?.addEventListener("abort", onAbort, { once: true });
452
+ render();
453
+ });
454
+ };
455
+ // src/term/select.ts
456
+ /**
457
+ * @file The select prompt: a list of options moved with ↑/↓ (or `j`/`k`),
458
+ * Home/End and `1`–`9`, chosen with Enter. `n` adds a note (one line, edited
459
+ * in place), `d` shows or hides the details (a document), `q` or Esc leaves
460
+ * the question for later, Ctrl-C stops. It redraws only its own lines,
461
+ * follows a resize, and collapses to one line once answered. Plain mode
462
+ * prints the options as `1) id label - description` and asks `Choose [n]: `,
463
+ * as `ork decide` always did; there a note is asked for after the choice when
464
+ * the prompt takes one.
465
+ */
466
+ const NOTE_PROMPT = "Note for the run (optional, Enter to skip)";
467
+ /** The option an answer names: its number, or its id; empty is `recommended`. */
468
+ const pickOption = (answer, options, recommended) => {
469
+ const value = answer.trim();
470
+ if (value === "") return recommended;
471
+ const index = Number(value);
472
+ if (Number.isInteger(index) && index >= 1 && index <= options.length) return options[index - 1]?.id;
473
+ return options.find((o) => o.id === value)?.id;
474
+ };
475
+ const label = (option) => option.label === void 0 ? option.id : `${option.id} ${option.label}`;
476
+ const defaultDone = (options) => (result, term) => {
477
+ if (result.kind === "later") return ` ${paint(term, "muted", "·")} left for later`;
478
+ const option = options.find((o) => o.id === result.id);
479
+ return cut(` ${paint(term, "ok", "✓")} ${option === void 0 ? result.id : label(option)}`, term.width);
480
+ };
481
+ /** The numbered option lines of a plain prompt: ` 1) id label - description`. */
482
+ const plainOptions = (options, recommended) => options.map((option, i) => {
483
+ const description = option.description === void 0 ? "" : ` - ${option.description}`;
484
+ const mark = option.id === recommended ? " (recommended)" : "";
485
+ const name = option.label === void 0 ? "" : ` ${option.label}`;
486
+ return ` ${String(i + 1)}) ${option.id}${name}${description}${mark}`;
487
+ });
488
+ /** The plain prompt: numbered options, then `Choose [n]: ` until one fits. */
489
+ const selectPlain = (io, o) => {
490
+ const term = termOf(io.output, io.env);
491
+ const say = (text) => {
492
+ io.output.write(`${text}\n`);
493
+ };
494
+ for (const line of o.header?.(term) ?? []) say(line);
495
+ for (const line of plainOptions(o.options, o.recommended)) say(line);
496
+ for (const line of o.footer ?? []) say(line);
497
+ const at = o.options.findIndex((opt) => opt.id === o.recommended);
498
+ const hint = at === -1 ? "" : ` [${String(at + 1)}]`;
499
+ const ask = async () => {
500
+ const answer = await readLine(io, ` Choose${hint}: `, o.signal);
501
+ if (answer === void 0) throw new Error("input ended");
502
+ const id = pickOption(answer, o.options, o.recommended);
503
+ if (id === void 0) {
504
+ if (o.details !== void 0 && answer.trim() === "d") say(o.details);
505
+ else say(" Type an option's number or id.");
506
+ return ask();
507
+ }
508
+ if (o.note !== true) return {
509
+ kind: "chosen",
510
+ id
511
+ };
512
+ const noted = await readText(io, {
513
+ message: o.notePrompt ?? NOTE_PROMPT,
514
+ ...o.signal === void 0 ? {} : { signal: o.signal }
515
+ });
516
+ return noted.kind === "text" && noted.value !== "" ? {
517
+ kind: "chosen",
518
+ id,
519
+ note: noted.value
520
+ } : {
521
+ kind: "chosen",
522
+ id
523
+ };
524
+ };
525
+ return ask();
526
+ };
527
+ /** Asks for one of `options`. */
528
+ const select = (io, o) => {
529
+ if (o.signal?.aborted === true) return Promise.reject(withdrawn(o.signal));
530
+ if (!canKey(io.input, io.output, io.env)) return selectPlain(io, o);
531
+ const term = () => termOf(io.output, io.env);
532
+ const region = createRegion(io.output, term);
533
+ const done = o.done ?? defaultDone(o.options);
534
+ return new Promise((resolve, reject) => {
535
+ const start = o.options.findIndex((opt) => opt.id === o.recommended);
536
+ let cursor = Math.max(0, start);
537
+ let showDetails = false;
538
+ let note;
539
+ let editing;
540
+ const optionLines = (t) => o.options.flatMap((option, i) => {
541
+ const selected = i === cursor;
542
+ const number = i < 9 ? `${String(i + 1)}.` : " ";
543
+ const mark = option.id === o.recommended ? " (recommended)" : "";
544
+ const head = cut(`${number} ${label(option)}`, t.width - 4 - mark.length);
545
+ const text = selected ? apply(t, ["bold", "cyan"], head) : head;
546
+ const lines = [` ${selected ? paint(t, "accent", "❯") : " "} ${text}${mark === "" ? "" : paint(t, "ok", mark)}`];
547
+ for (const line of wrap(option.description ?? "", t.width - 7)) if (line !== "") lines.push(paint(t, "muted", ` ${line}`));
548
+ return lines;
549
+ });
550
+ const keyLine = (t) => {
551
+ const keys = editing === void 0 ? [
552
+ "↑/↓ move",
553
+ "Enter choose",
554
+ ...o.note === true ? ["n note"] : [],
555
+ ...o.details === void 0 ? [] : [`d ${showDetails ? "hide" : "show"} details`],
556
+ ...o.later === true ? ["q later"] : []
557
+ ] : ["Enter save", "Esc cancel"];
558
+ return paint(t, "muted", cut(` ${keys.join(" · ")}`, t.width));
559
+ };
560
+ const render = () => {
561
+ const t = term();
562
+ const head = o.header?.(t) ?? [];
563
+ const body = optionLines(t);
564
+ const noteLine = note === void 0 ? [] : [paint(t, "muted", cut(` note: ${note}`, t.width))];
565
+ const above = [
566
+ ...head,
567
+ ...body,
568
+ ...noteLine
569
+ ];
570
+ const room = Math.max(3, t.rows - above.length - 3);
571
+ const all = showDetails && o.details !== void 0 ? wrap(o.details.trimEnd(), t.width - 3) : [];
572
+ const detail = (all.length > room ? [...all.slice(0, room - 1), `… ${String(all.length - room + 1)} more lines`] : all).map((line) => paint(t, "muted", ` ${line}`));
573
+ const lines = [
574
+ ...above,
575
+ ...detail,
576
+ keyLine(t)
577
+ ];
578
+ if (editing === void 0) {
579
+ region.draw(lines);
580
+ return;
581
+ }
582
+ const lead = ` ${paint(t, "accent", "✎")} Note › `;
583
+ const { line, column } = showLine(lead, editing, t.width, 10);
584
+ region.draw([...lines, line], column);
585
+ };
586
+ const finish = (result) => {
587
+ stop();
588
+ cleanup();
589
+ region.finish(done(result, term()));
590
+ resolve(result);
591
+ };
592
+ const cleanup = () => {
593
+ io.output.off("resize", render);
594
+ o.signal?.removeEventListener("abort", onAbort);
595
+ };
596
+ const onAbort = () => {
597
+ stop();
598
+ cleanup();
599
+ region.finish();
600
+ reject(withdrawn(o.signal));
601
+ };
602
+ const edit = (key) => {
603
+ if (editing === void 0) return;
604
+ if (key.name === "enter") {
605
+ const value = editing.value.trim();
606
+ note = value === "" ? void 0 : value;
607
+ editing = void 0;
608
+ } else if (key.name === "escape") editing = void 0;
609
+ else editing = editLine(editing, key);
610
+ render();
611
+ };
612
+ const move = (to) => {
613
+ cursor = Math.min(o.options.length - 1, Math.max(0, to));
614
+ render();
615
+ };
616
+ const stop = readKeys(io.input, (key) => {
617
+ if (key.name === "ctrl-c") {
618
+ stop();
619
+ cleanup();
620
+ region.finish();
621
+ resolve({ kind: "interrupted" });
622
+ interrupt(io);
623
+ } else if (editing !== void 0) edit(key);
624
+ else if (key.name === "enter") {
625
+ const id = o.options[cursor]?.id;
626
+ if (id === void 0) return;
627
+ finish(note === void 0 ? {
628
+ kind: "chosen",
629
+ id
630
+ } : {
631
+ kind: "chosen",
632
+ id,
633
+ note
634
+ });
635
+ } else if (key.name === "up") move(cursor - 1);
636
+ else if (key.name === "down") move(cursor + 1);
637
+ else if (key.name === "home") move(0);
638
+ else if (key.name === "end") move(o.options.length - 1);
639
+ else if (key.name === "escape" && o.later === true) finish({ kind: "later" });
640
+ else if (key.name === "char") {
641
+ const c = key.text;
642
+ if (c === "k") move(cursor - 1);
643
+ else if (c === "j") move(cursor + 1);
644
+ else if (c >= "1" && c <= "9") {
645
+ if (Number(c) <= o.options.length) move(Number(c) - 1);
646
+ } else if (c === "n" && o.note === true) {
647
+ const value = note ?? "";
648
+ editing = {
649
+ value,
650
+ pos: Array.from(value).length
651
+ };
652
+ render();
653
+ } else if (c === "d" && o.details !== void 0) {
654
+ showDetails = !showDetails;
655
+ render();
656
+ } else if (c === "q" && o.later === true) finish({ kind: "later" });
657
+ }
658
+ });
659
+ io.output.on("resize", render);
660
+ o.signal?.addEventListener("abort", onAbort, { once: true });
661
+ render();
662
+ });
663
+ };
664
+ // src/human.ts
665
+ /**
666
+ * @file The terminal `HumanChannel`: answers `ask_user` questions, gates and
667
+ * permission prompts on the terminal, one at a time, and denies or defers
668
+ * when no one is there. It draws through the terminal kit (`term/`): a card
669
+ * with an ASK chip and a list moved with the arrow keys, a yes-or-no for a
670
+ * permission, and one optional line for a note after a choice. Plain (not a
671
+ * terminal, `NO_COLOR`, `--plain`) it keeps the numbered prompts. In a run it
672
+ * is the decision queue's local answerer, racing `ork decide`.
673
+ */
674
+ const PREVIEW = 160;
675
+ /** A document longer than this is offered (`d`) rather than printed. */
676
+ const SHORT_DOCUMENT_LINES = 40;
677
+ const NO_ONE_TO_PERMIT = "No one is at the terminal to approve this tool, so it is not allowed in this run.";
678
+ const NOTE_MESSAGE = "Note for the run (optional, Enter to skip)";
679
+ const NO_ONE_TO_ASK = "no one (nobody is at the terminal; use your best judgment and say so in your report)";
680
+ /**
681
+ * What a foreground caller shows of the runs it calls: their lines always, and
682
+ * their questions only when stdin is a terminal, so no one is answered for.
683
+ */
684
+ const callsShown = (human, stdinIsTty) => stdinIsTty === true ? { ask: human } : {};
685
+ /** One line that shows what a tool is about to do. */
686
+ const previewInput = (toolName, input) => {
687
+ const key = TOOL_INPUT_KEYS.find((k) => typeof input[k] === "string");
688
+ const line = (key === void 0 ? JSON.stringify(input) : `${key}: ${String(input[key])}`).replaceAll(/\s+/gu, " ").trim();
689
+ return line.length > PREVIEW ? `${toolName} ${line.slice(0, 159)}…` : `${toolName} ${line}`;
690
+ };
691
+ /** Never answers, until `signal` withdraws it: what a stopped prompt leaves. */
692
+ const parked = (signal) => new Promise((_, reject) => {
693
+ signal?.addEventListener("abort", () => {
694
+ reject(withdrawn(signal));
695
+ });
696
+ });
697
+ /**
698
+ * Answers permission prompts and `ask_user` questions on the terminal, one at a
699
+ * time. Without a terminal it denies tools and leaves questions to the node. A
700
+ * withdrawn prompt (answered elsewhere) rejects, which frees the turn.
701
+ */
702
+ const createTerminalHuman = (io) => {
703
+ let rl;
704
+ let closed;
705
+ let turn = Promise.resolve();
706
+ const say = (text) => {
707
+ io.output.write(`${text}\n`);
708
+ };
709
+ /** The one `readline` the plain prompts share; a rich prompt never opens it. */
710
+ const readLine = (text, signal) => {
711
+ if (rl === void 0) {
712
+ const opened = createInterface({
713
+ input: io.input,
714
+ output: io.output,
715
+ ...plainEnv(io.env) ? { terminal: false } : {}
716
+ });
717
+ if (io.interrupt !== void 0) opened.on("SIGINT", io.interrupt);
718
+ closed = new Promise((resolve) => {
719
+ opened.once("close", () => {
720
+ resolve(void 0);
721
+ });
722
+ });
723
+ rl = opened;
724
+ }
725
+ const answer = signal === void 0 ? rl.question(text) : rl.question(text, { signal });
726
+ return closed === void 0 ? answer : Promise.race([answer, closed]);
727
+ };
728
+ const kit = {
729
+ input: io.input,
730
+ output: io.output,
731
+ readLine,
732
+ ...io.env === void 0 ? {} : { env: io.env },
733
+ ...io.interrupt === void 0 ? {} : { interrupt: io.interrupt }
734
+ };
735
+ const term = () => termOf(io.output, io.env);
736
+ /** The optional line after a choice, when the asker takes a note. */
737
+ const takeNote = async (onNote, signal) => {
738
+ if (onNote === void 0) return;
739
+ const noted = await readText(kit, {
740
+ message: NOTE_MESSAGE,
741
+ ...signal === void 0 ? {} : { signal }
742
+ });
743
+ if (noted.kind === "interrupted") await parked(signal);
744
+ if (noted.kind === "text" && noted.value !== "") onNote(noted.value);
745
+ };
746
+ const oneAtATime = (work) => {
747
+ const result = turn.then(work);
748
+ turn = result.catch(() => null);
749
+ return result;
750
+ };
751
+ const asking = (q, place) => oneAtATime(async () => {
752
+ const { signal } = q;
753
+ if (signal?.aborted === true) throw new Error("the question was withdrawn");
754
+ const t = term();
755
+ const documentLines = q.document?.trimEnd().split("\n").length ?? 0;
756
+ const document = q.document === void 0 ? void 0 : clean(q.document);
757
+ const printed = document !== void 0 && documentLines <= SHORT_DOCUMENT_LINES;
758
+ const shownDocument = t.rich || !printed ? void 0 : document;
759
+ const header = (tt) => cardLines(tt, q, place, io.verbose === true, shownDocument).lines;
760
+ const options = q.options.map((o) => ({
761
+ id: o.id,
762
+ ...o.label === void 0 ? {} : { label: plain(o.label) },
763
+ ...o.description === void 0 ? {} : { description: plain(o.description) }
764
+ }));
765
+ const { moreContext } = cardLines(t, q, place, io.verbose === true);
766
+ const details = t.rich ? [moreContext ? clean(q.context ?? "") : void 0, document].filter((part) => part !== void 0).join("\n\n") || void 0 : printed ? void 0 : document;
767
+ if (!io.interactive) {
768
+ for (const line of header(t)) say(line);
769
+ for (const line of plainOptions(options, q.recommended)) say(line);
770
+ const answer = q.recommended ?? NO_ONE_TO_ASK;
771
+ say(paint(t, "muted", ` no terminal; answered ${answer}`));
772
+ return answer;
773
+ }
774
+ const result = await select(kit, {
775
+ header,
776
+ options,
777
+ ...q.recommended === void 0 ? {} : { recommended: q.recommended },
778
+ ...details === void 0 ? {} : { details },
779
+ ...!t.rich && details !== void 0 ? { footer: [` Document: ${String(documentLines)} lines. Type d to read it.`] } : {},
780
+ note: q.onNote !== void 0,
781
+ notePrompt: NOTE_MESSAGE,
782
+ later: place !== void 0,
783
+ done: (answer, tt) => doneLine(tt, place, q.nodeId, answer),
784
+ ...signal === void 0 ? {} : { signal }
785
+ });
786
+ if (result.kind === "interrupted") return parked(signal);
787
+ if (result.kind === "later") return null;
788
+ if (result.note !== void 0) q.onNote?.(result.note);
789
+ return result.id;
790
+ });
791
+ return {
792
+ permit: ({ nodeId, toolName, input, signal, onNote }) => oneAtATime(async () => {
793
+ if (signal?.aborted === true) throw new Error("the prompt was withdrawn");
794
+ const t = term();
795
+ if (t.rich) say("");
796
+ say(permitLine(t, nodeId, previewInput(toolName, input)));
797
+ if (!io.interactive) {
798
+ say(paint(t, "muted", ` denied: ${NO_ONE_TO_PERMIT}`));
799
+ return {
800
+ allow: false,
801
+ message: NO_ONE_TO_PERMIT
802
+ };
803
+ }
804
+ const allowed = await confirmOrStop(kit, {
805
+ question: "Allow?",
806
+ ...signal === void 0 ? {} : { signal }
807
+ });
808
+ if (allowed === null) return parked(signal);
809
+ if (allowed) return { allow: true };
810
+ await takeNote(onNote, signal);
811
+ return {
812
+ allow: false,
813
+ message: "The user did not allow this tool."
814
+ };
815
+ }),
816
+ ask: async (q) => await asking(q) ?? NO_ONE_TO_ASK,
817
+ askAt: (q, place) => asking(q, place),
818
+ close: () => {
819
+ rl?.close();
820
+ }
821
+ };
822
+ };
823
+ // src/run/format-end.ts
824
+ /**
825
+ * @file How a node's end reads on the terminal (specs/runtime/session-ends.md):
826
+ * its kind's label and Ork's own message, a note for a kind that may pass,
827
+ * the setup hint for an account or provider end, and the scrubbed raw detail
828
+ * only when asked for. Never the provider's own words first.
829
+ */
830
+ /** What a transient kind says in place of its hint on the log line. */
831
+ const MAY_PASS = "(may pass; ork resume retries)";
832
+ /** `Turn limit: reached its turn limit (8)`, with a note when it may pass. */
833
+ const endHeadline = (end) => {
834
+ const { label, transient } = END_KINDS[end.kind];
835
+ return `${label}: ${end.message}${transient ? ` ${MAY_PASS}` : ""}`;
836
+ };
837
+ /**
838
+ * The lines under a headline: the hint for an account or provider end (what to
839
+ * set up) or whenever `full` (`--verbose`, one node's log), and the detail only
840
+ * when `full`. A transient kind's hint is its retry wording, which the headline
841
+ * already says, so it is not printed twice.
842
+ */
843
+ const endLines = (end, full, term = createView().term) => {
844
+ const style = styler(term);
845
+ const { actor, hint, transient } = END_KINDS[end.kind];
846
+ const setup = actor === "account" || actor === "provider";
847
+ const lines = [];
848
+ if (hint !== "" && !transient && (full || setup)) lines.push(` ${style("dim", hint)}`);
849
+ if (full && end.detail !== void 0) lines.push(style("dim", " details:"), ...end.detail.split("\n").map((line) => style("dim", ` ${line}`)));
850
+ return lines;
851
+ };
852
+ /**
853
+ * A finished node's line under `tag`: its verdict, or for a failure its kind
854
+ * and message (and the lines under it). The agent's own summary stays for a
855
+ * `fail` it reported, and an old run's text no rule reads stays as stored.
856
+ */
857
+ const formatFinished = (event, tag, verbose, term = createView().term, rest = tag) => {
858
+ const style = styler(term);
859
+ const conflict = hasConflict(event.output);
860
+ const mark = event.ok ? style(conflict ? "yellow" : "green", conflict ? "!" : "✓") : style("red", "✗");
861
+ const spent = event.usage === void 0 ? "" : style("dim", ` · ${cost(event.usage.costUsd)}`);
862
+ const end = endOf(event);
863
+ if (!(!event.ok && end.kind !== "reported_failure" && (event.end !== void 0 || end.kind !== "unknown"))) return [`${tag} ${mark} ${nodeVerdict(event.output, event.ok, event.error)}${spent}`];
864
+ return [`${tag} ${mark} ${endHeadline(end)}${spent}`, ...endLines(end, verbose, term).map((line) => `${rest} ${line}`)];
865
+ };
866
+ // src/run/format-outcome.ts
867
+ /**
868
+ * @file How a finished session reads on the terminal: its report or its end,
869
+ * the branch, artifacts and fields, and the usage line.
870
+ */
871
+ const STATUS_MARK = {
872
+ pass: {
873
+ mark: "✓",
874
+ color: "green"
875
+ },
876
+ done: {
877
+ mark: "✓",
878
+ color: "green"
879
+ },
880
+ changes_requested: {
881
+ mark: "!",
882
+ color: "yellow"
883
+ },
884
+ fail: {
885
+ mark: "✗",
886
+ color: "red"
887
+ }
888
+ };
889
+ const seconds = (ms) => `${(ms / 1e3).toFixed(1)}s`;
890
+ /** The usage line: cost, tokens, turns and time. */
891
+ const formatUsage = ({ usage }, term = createView().term) => styler(term)("dim", [
892
+ cost(usage.costUsd),
893
+ `${String(usage.inputTokens)} in / ${String(usage.outputTokens)} out (${String(usage.cacheReadTokens)} cached)`,
894
+ `${String(usage.turns)} turns`,
895
+ seconds(usage.durationMs)
896
+ ].join(" · "));
897
+ /** How a finished session reads on the terminal. */
898
+ const formatOutcome = (nodeId, outcome, verbose = false, term = createView().term) => {
899
+ const style = styler(term);
900
+ const { report } = outcome;
901
+ if (report === null) {
902
+ const end = outcomeEnd(outcome);
903
+ return [
904
+ `${style("red", "✗")} ${style("bold", nodeId)} ${endHeadline(end)}`,
905
+ ...endLines(end, verbose, term),
906
+ ` ${formatUsage(outcome, term)}`
907
+ ];
908
+ }
909
+ const shown = STATUS_MARK[report.status];
910
+ const lines = [`${shown === void 0 ? "-" : style(shown.color, shown.mark)} ${style("bold", nodeId)} ${report.status}: ${report.summary}`];
911
+ if (report.branch !== void 0) lines.push(` branch: ${report.branch}`);
912
+ for (const [name, file] of Object.entries(report.artifacts)) lines.push(` ${name}: ${file}`);
913
+ for (const [name, value] of Object.entries(report.fields)) lines.push(` ${name}: ${JSON.stringify(value)}`);
914
+ lines.push(` ${formatUsage(outcome, term)}`);
915
+ return lines;
916
+ };
917
+ // src/run/format-result.ts
918
+ /** How a run ended: its mark, and its chip's word and role when rich. */
919
+ const RUN_END = {
920
+ succeeded: {
921
+ mark: "✓",
922
+ ok: "ok",
923
+ label: CHIP_LABELS.ok
924
+ },
925
+ failed: {
926
+ mark: "✗",
927
+ ok: "danger",
928
+ label: CHIP_LABELS.fail
929
+ },
930
+ budget_exhausted: {
931
+ mark: "!",
932
+ ok: "warn",
933
+ label: CHIP_LABELS.wait
934
+ },
935
+ quota_exhausted: {
936
+ mark: "!",
937
+ ok: "warn",
938
+ label: CHIP_LABELS.wait
939
+ },
940
+ timed_out: {
941
+ mark: "!",
942
+ ok: "warn",
943
+ label: CHIP_LABELS.wait
944
+ },
945
+ interrupted: {
946
+ mark: "!",
947
+ ok: "warn",
948
+ label: CHIP_LABELS.wait
949
+ }
950
+ };
951
+ const MARK_COLOR = {
952
+ ok: "green",
953
+ danger: "red",
954
+ warn: "yellow"
955
+ };
956
+ /** `1 failure handled`, `2 failures handled`. */
957
+ const failuresHandled = (count) => `${String(count)} ${count === 1 ? "failure" : "failures"} handled`;
958
+ /**
959
+ * The run summary: status, cost, what ran, what was skipped and which failures
960
+ * an edge handled, branches to look at, why, and mail to nodes that never ran.
961
+ */
962
+ const formatRunResult = (result, term = createView().term) => {
963
+ const style = styler(term);
964
+ const end = RUN_END[result.status];
965
+ const mark = term.rich ? chip(term, end.label, end.ok) : style(MARK_COLOR[end.ok], end.mark);
966
+ const ran = result.nodes.filter((n) => n.runs > 0).length;
967
+ const skipped = result.skipped ?? result.nodes.length - ran;
968
+ const handled = result.failuresHandled ?? 0;
969
+ const lines = ["", `${mark} run ${result.runId} ${result.status.replace("_", " ")} ${style("dim", `· ${cost(result.costUsd)} · ${String(ran)} ran, ${String(skipped)} skipped${handled === 0 ? "" : `, ${failuresHandled(handled)}`}`)}`];
970
+ for (const kept of result.worktrees) lines.push(` ${kept.id}: ${kept.branch ?? "detached"} ${style("dim", `at ${kept.path} (${kept.reason})`)}`);
971
+ for (const reason of result.reasons) lines.push(` ${style("red", reason)}`);
972
+ for (const { node, messages } of result.unread ?? []) lines.push(` ${style(["yellow", "bold"], "warning")} ${node} never ran, so ${messages === 1 ? "1 message" : `${String(messages)} messages`} to it went unread`);
973
+ return lines;
974
+ };
975
+ // src/run/format-tool.ts
976
+ /**
977
+ * @file How a session's own words and tool calls read in the transcript: the
978
+ * agent's text (indented plain, wrapped rich), and a tool call as `Tool first
979
+ * line` cut to the screen with `+N lines`, a path inside the node's worktree
980
+ * relative to it, or whole with `--verbose`. Untrusted text goes through
981
+ * `plain()` first.
982
+ */
983
+ const indent = (text) => text.trimEnd().split("\n").map((line) => ` ${plain(line)}`);
984
+ /** Agent text under a 2-column indent, its long lines wrapped to `room`. */
985
+ const wrapped = (text, room) => {
986
+ const width = Math.max(1, room - 2);
987
+ return text.trimEnd().split("\n").flatMap((line) => {
988
+ const safe = plain(line).trimEnd();
989
+ return widthOf(safe) <= width ? [safe] : wrap(safe, width);
990
+ }).map((line) => ` ${line}`);
991
+ };
992
+ /** A tool's input as the call says it: the key that names its target, whole. */
993
+ const toolText = (input, root) => {
994
+ const key = TOOL_INPUT_KEYS.find((k) => typeof input[k] === "string");
995
+ const text = key === void 0 ? JSON.stringify(input) : String(input[key]);
996
+ if (root === void 0) return text;
997
+ const base = root.replace(/\/+$/u, "");
998
+ return text === base ? "." : text.replaceAll(`${base}/`, "");
999
+ };
1000
+ /**
1001
+ * A tool call: rich, `Tool first line` cut to `room` with `+N lines` when it
1002
+ * had more; plain, today's one dim line. `verbose` shows the call whole.
1003
+ */
1004
+ const toolLines = (name, input, view, room, verbose, node) => {
1005
+ const { term } = view;
1006
+ const style = styler(term);
1007
+ if (!term.rich && !verbose) return [style("dim", ` · ${plain(previewInput(name, input))}`)];
1008
+ const label = plain(name);
1009
+ const [first = "", ...rest] = toolText(input, view.roots.get(node)).trimEnd().split("\n").map((line) => plain(line));
1010
+ const lead = term.rich ? " " : " · ";
1011
+ const sep = term.rich ? " " : " ";
1012
+ const headWidth = widthOf(lead) + widthOf(label) + sep.length;
1013
+ const styledHead = `${style("dim", lead)}${style("bold", label)}${style("dim", sep)}`;
1014
+ if (verbose) {
1015
+ const pad = " ".repeat(headWidth);
1016
+ return [`${styledHead}${style("dim", first)}`, ...rest.map((line) => style("dim", `${pad}${line}`))];
1017
+ }
1018
+ const more = rest.length === 0 ? "" : ` +${String(rest.length)} ${rest.length === 1 ? "line" : "lines"}`;
1019
+ return [`${styledHead}${style("dim", cut(first.trim(), Math.max(1, room - headWidth - more.length)))}${style("dim", more)}`];
1020
+ };
1021
+ // src/run/format.ts
1022
+ /**
1023
+ * @file How runs read on the terminal: session events and run events with node
1024
+ * chips or prefixes; the node result is `format-outcome.ts`'s, a tool call
1025
+ * `format-tool.ts`'s and the run summary `format-result.ts`'s.
1026
+ */
1027
+ /** A line's 2-column indent, after any style codes that open it. */
1028
+ const INDENT = /^((?:\u001B\[[0-9;]*m)*) {2}/u;
1029
+ /** Who answered a decision, as the log says it. */
1030
+ const ANSWERED_BY = {
1031
+ terminal: "terminal",
1032
+ decide: "ork decide",
1033
+ ui: "ork ui",
1034
+ owner: "its owner"
1035
+ };
1036
+ /** The transcript lines for one event; tool names come from `tools`. */
1037
+ const formatEvent = (nodeId, event, tools, verbose = false, view = createView(), room = view.term.width - 1) => {
1038
+ const { term } = view;
1039
+ const style = styler(term);
1040
+ if (event.type === "session.started") {
1041
+ const fork = event.fork === true ? ", a fork to answer a question" : "";
1042
+ return [style("dim", `▸ ${nodeId} (${event.model}, session ${event.sessionId}${fork})`)];
1043
+ }
1044
+ if (event.type === "assistant.text") return term.rich ? wrapped(event.text, room) : indent(event.text);
1045
+ if (event.type === "session.waiting") return indent(event.reason);
1046
+ if (event.type === "tool.called") {
1047
+ tools.set(event.id, event.name);
1048
+ return toolLines(event.name, event.input, view, room, verbose, nodeId);
1049
+ }
1050
+ if (event.type === "tool.result") return event.isError ? [style("red", ` ✗ ${tools.get(event.id) ?? "tool"} failed`)] : [];
1051
+ if (event.type === "plan.limit") return event.status === "allowed" ? [] : [style("yellow", ` ! plan: ${event.window.replaceAll("_", " ")} limit ${event.status === "rejected" ? "reached" : "is close"}`)];
1052
+ return formatOutcome(nodeId, event.outcome, verbose, term);
1053
+ };
1054
+ const items = (count) => `${String(count)} ${count === 1 ? "item" : "items"}`;
1055
+ /** Transcript lines for one run event, prefixed with the node they are from. */
1056
+ const formatRunEvent = (event, width, tools, terminal = false, verbose = false, view = createView()) => {
1057
+ const { term } = view;
1058
+ const style = styler(term);
1059
+ const tag = (id) => term.rich ? `${nodeChip(term, id)}${" ".repeat(Math.max(0, width - id.length))}` : style("cyan", `${id.padEnd(width)} │`);
1060
+ const blank = (id) => term.rich ? " ".repeat(widthOf(tag(id))) : tag(id);
1061
+ const under = (id, lines) => lines.map((line, i) => `${i === 0 ? tag(id) : blank(id)} ${line}`);
1062
+ if (event.type === "run.started") {
1063
+ const starts = `starts at ${event.entries.join(", ")}`;
1064
+ return [
1065
+ term.rich ? `${chip(term, CHIP_LABELS.run, "accent")} run ${style("bold", event.runId)} of ${event.workflow} ${style("dim", starts)}` : style("dim", `▸ run ${event.runId} of ${event.workflow} (${starts})`),
1066
+ ...event.settings?.trigger === void 0 ? [] : [style("dim", ` started by trigger ${event.settings.trigger}; untrusted inputs: ${(event.settings.untrusted ?? []).join(", ") || "none"}`)],
1067
+ ...term.rich ? [""] : []
1068
+ ];
1069
+ }
1070
+ if (event.type === "node.started") {
1071
+ const round = event.round > 1 ? ` (round ${String(event.round)})` : "";
1072
+ const owner = event.owner === void 0 ? "" : `, for ${event.owner}`;
1073
+ return [`${tag(event.id)} ${style("dim", `${event.nodeType}${round}${owner}`)}`];
1074
+ }
1075
+ if (event.type === "node.event") {
1076
+ const inner = event.event;
1077
+ if (inner.type === "session.finished") return [];
1078
+ const fork = inner.fork === true ? style("dim", "fork · ") : "";
1079
+ if (inner.type === "session.started") {
1080
+ const why = inner.fork === true ? ", to answer a question" : "";
1081
+ return [`${tag(event.id)} ${fork}${style("dim", `${inner.model}, session ${inner.sessionId}${why}`)}`];
1082
+ }
1083
+ const room = term.width - widthOf(tag(event.id)) - 1 + 2 - (inner.fork === true ? 7 : 0);
1084
+ return under(event.id, formatEvent(event.id, event.event, tools, verbose, view, room).map((line) => `${fork}${line.replace(INDENT, "$1")}`));
1085
+ }
1086
+ if (event.type === "node.finished") return formatFinished(event, tag(event.id), verbose, term, blank(event.id));
1087
+ if (event.type === "node.skipped") return [`${tag(event.id)} ${style("dim", "- skipped")}`];
1088
+ if (event.type === "loop.exhausted") return [`${style(["yellow", "bold"], "!")} the loop ${event.from} -> ${event.to} used all ${String(event.maxRounds)} rounds`];
1089
+ if (event.type === "watch.started") return [`${tag(event.id)} ${style("dim", `watching until ${event.until}, every ${event.every}`)}`];
1090
+ if (event.type === "watch.changed") return [`${tag(event.id)} ${style("dim", `look ${String(event.looks)}:`)} ${event.said}`];
1091
+ if (event.type === "watch.ended") return [`${tag(event.id)} ${style("dim", `${event.outcome} after ${String(event.looks)} looks`)}`];
1092
+ if (event.type === "edge.taken") return [];
1093
+ if (event.type === "worktree") {
1094
+ if (event.action !== "removed") view.roots.set(event.id, event.path);
1095
+ const detail = event.detail === void 0 ? "" : ` (${event.detail})`;
1096
+ const what = event.action === "removed" && event.branch !== void 0 ? `worktree of ${event.branch} (branch kept)` : `${event.branch ?? event.path}${detail}`;
1097
+ const line = `${event.action} ${what}`;
1098
+ return [`${tag(event.id)} ${event.action === "conflict" ? style("yellow", `! ${line}`) : style("dim", `⎇ ${line}`)}`];
1099
+ }
1100
+ if (event.type === "decision.opened") {
1101
+ const what = event.kind === "permission" ? "asks to" : "asks:";
1102
+ const question = event.kind === "permission" ? event.question.replace(/^Allow /u, "run ").replace(/\?$/u, "") : event.question;
1103
+ const where = event.owner === void 0 ? `decision ${event.decision}; ${terminal ? "answer here or with ork decide" : "ork decide"}` : `decision ${event.decision}, via ${event.owner}`;
1104
+ return [`${tag(event.node)} ${term.rich ? chip(term, CHIP_LABELS.ask, "accent") : style(["yellow", "bold"], "?")} ${what} ${question} ${style("dim", `(${where})`)}`];
1105
+ }
1106
+ if (event.type === "decision.answered") {
1107
+ const by = event.by === "owner" && event.owner !== void 0 ? event.owner : ANSWERED_BY[event.by];
1108
+ const said = event.note === void 0 ? "" : ` · said: ${event.note.replaceAll(/\s+/gu, " ")}`;
1109
+ return [`${tag(event.node)} ${style("dim", `→ ${event.answer} (${by})${said}`)}`];
1110
+ }
1111
+ if (event.type === "decision.withdrawn") return [`${tag(event.node)} ${style("dim", "→ question withdrawn")}`];
1112
+ if (event.type === "run.command") {
1113
+ const node = event.node === void 0 ? "" : ` --node ${event.node}`;
1114
+ return [style(["yellow", "bold"], `! ork ${event.command}${node} received`)];
1115
+ }
1116
+ if (event.type === "node.held") {
1117
+ const at = event.at === "tool" ? "its tool call waits for ork continue" : "it starts after ork continue";
1118
+ return [`${tag(event.id)} ${style("yellow", `‖ paused: ${at}`)}`];
1119
+ }
1120
+ if (event.type === "node.released") return [`${tag(event.id)} ${style("dim", "▸ continues")}`];
1121
+ if (event.type === "slot.waiting") return [`${tag(event.id)} ${style("yellow", `‖ ${slotWaitLine(event.holders)}`)}`];
1122
+ if (event.type === "slot.taken") return [`${tag(event.id)} ${style("dim", "▸ got a check slot")}`];
1123
+ if (event.type === "each.listed") {
1124
+ const round = event.round > 1 ? ` (round ${String(event.round)})` : "";
1125
+ return [`${tag(event.node)} ${style("dim", `${items(event.count)}${round}`)}`];
1126
+ }
1127
+ if (event.type === "each.exceeded") {
1128
+ const over = `${items(event.count)}, over maxItems ${String(event.maxItems)}`;
1129
+ return [`${tag(event.node)} ${style("yellow", `! ${over}`)}`];
1130
+ }
1131
+ if (event.type === "run.finished") return formatRunResult(event.result, term);
1132
+ if (event.type === "run.resumed") {
1133
+ const [one] = event.kept;
1134
+ return ["", style("dim", `▸ resumed: ${one === void 0 ? "every node runs again" : event.kept.length === 1 ? `${one} keeps its result` : `${event.kept.join(", ")} keep their results`}`)];
1135
+ }
1136
+ if (event.type === "feedback.recorded") {
1137
+ const { target, kind, title } = event.feedback;
1138
+ const line = style("dim", `✎ feedback (${target}, ${kind}): ${title}`);
1139
+ return [event.node === void 0 ? line : `${tag(event.node)} ${line}`];
1140
+ }
1141
+ if (event.type === "user.message") {
1142
+ const note = event.delivered === "queued" ? " (for its next session)" : "";
1143
+ return [`${tag(event.node)} ${style(["magenta", "bold"], "you:")} ${firstLine(event.body)}${style("dim", note)}`];
1144
+ }
1145
+ return under(event.node, formatBusEvent(event, term));
1146
+ };
1147
+ // src/runs/logs.ts
1148
+ /**
1149
+ * @file `ork logs`: replays a stored run's events as the transcript `ork run`
1150
+ * printed, for the whole run or one node, and tails a live run until it
1151
+ * ends.
1152
+ */
1153
+ /** Turns stored events into lines, keeping the state a transcript needs. */
1154
+ const createLogPrinter = (options) => {
1155
+ const tools = /* @__PURE__ */ new Map();
1156
+ const view = createView();
1157
+ let width = 8;
1158
+ return (stored) => {
1159
+ const { event } = stored;
1160
+ if (event.type === "run.started") width = Math.max(...event.nodes.map((n) => n.id.length), 1);
1161
+ if (options.node !== void 0 && !isNodeEvent(event, options.node)) return [];
1162
+ if (options.json) return [JSON.stringify(stored)];
1163
+ return formatRunEvent(event, width, tools, false, options.verbose === true || options.node !== void 0, view);
1164
+ };
1165
+ };
1166
+ /**
1167
+ * Why `--node` names nothing in this run, or `null`: no event names it and the
1168
+ * run didn't declare it. A live run may still start it, so that is not
1169
+ * refused.
1170
+ */
1171
+ const unknownNode = (stored, node, live) => {
1172
+ const named = stored.some(({ event }) => isNodeEvent(event, node));
1173
+ const declared = stored.some(({ event }) => event.type === "run.started" && event.nodes.some((n) => n.id === node));
1174
+ return named || declared || live ? null : `Run has no node ${node}.`;
1175
+ };
1176
+ /**
1177
+ * Prints a run's events; with `follow`, then each new one as it is stored:
1178
+ * pushed over the run's socket, or polled from the store when there is none.
1179
+ * Returns when `run.finished` is stored, the run's process is gone, or `signal`
1180
+ * aborts.
1181
+ */
1182
+ const followRun = async (store, run, out, options) => {
1183
+ const print = createLogPrinter(options);
1184
+ if (await followEvents(store, run.id, (stored) => {
1185
+ for (const line of print(stored)) out(line);
1186
+ }, {
1187
+ follow: options.follow,
1188
+ ...options.after === void 0 ? {} : { after: options.after },
1189
+ ...options.intervalMs === void 0 ? {} : { intervalMs: options.intervalMs },
1190
+ ...options.home === void 0 ? {} : { home: options.home },
1191
+ ...options.signal === void 0 ? {} : { signal: options.signal }
1192
+ }) === "crashed") out(style("red", `run ${run.id} stopped without finishing (its process is gone)`));
1193
+ };
1194
+ // src/runs/ask-stored.ts
1195
+ /**
1196
+ * @file Asks one stored decision on a terminal and stores the answer: the
1197
+ * shared step of `ork decide`'s prompts and of `ork attach` (specs/cli.md).
1198
+ * The question goes through the `TerminalHuman`, so prompts keep their order;
1199
+ * a `signal` withdraws the prompt when the decision closes elsewhere; the
1200
+ * answer goes through `answerDecision`, whose first answer wins.
1201
+ */
1202
+ /** Asks `decision` on `human`, then stores the answer unless it was withdrawn. */
1203
+ const askStored = async (store, human, decision, { place, by, signal, home }) => {
1204
+ let note;
1205
+ const question = {
1206
+ nodeId: decision.node,
1207
+ question: decision.question,
1208
+ options: decision.options,
1209
+ onNote: (given) => {
1210
+ note = given;
1211
+ },
1212
+ ...decision.recommended === void 0 ? {} : { recommended: decision.recommended },
1213
+ ...decision.context === void 0 ? {} : { context: decision.context },
1214
+ ...decision.document === void 0 ? {} : { document: decision.document },
1215
+ ...signal === void 0 ? {} : { signal }
1216
+ };
1217
+ let choice;
1218
+ try {
1219
+ choice = await human.askAt(question, place);
1220
+ } catch (error) {
1221
+ if (signal?.aborted === true) return { kind: "withdrawn" };
1222
+ throw error;
1223
+ }
1224
+ if (choice === null) return { kind: "later" };
1225
+ const answered = await answerDecision(store, decision.id, choice, home, by, note);
1226
+ return answered.ok ? {
1227
+ kind: "answered",
1228
+ choice
1229
+ } : {
1230
+ kind: "refused",
1231
+ message: answered.error
1232
+ };
1233
+ };
1234
+ // src/runs/attach.ts
1235
+ /**
1236
+ * @file Following a run and every run it called on a terminal, and answering
1237
+ * their open decisions there (`ork attach`, and a foreground `ork run` that
1238
+ * calls workflows; specs/cli.md, specs/scheduler/calls.md). Each run has its
1239
+ * own log printer behind its call path; a decision opened in any of them is
1240
+ * asked through one `TerminalHuman`, withdrawn when it closes elsewhere.
1241
+ */
1242
+ const where = (path) => path.join(" › ");
1243
+ /**
1244
+ * Prints the events of `root` and of the runs it called until the root ends or
1245
+ * `signal` aborts, and asks each open decision with `options.human`.
1246
+ */
1247
+ const attachRun = async (store, root, out, options = {}) => {
1248
+ const { human, basePath = [] } = options;
1249
+ const { term } = createView();
1250
+ const say = styler(term);
1251
+ const printers = /* @__PURE__ */ new Map();
1252
+ const prompts = /* @__PURE__ */ new Map();
1253
+ /** Decisions answered here: their `decision.answered` line is the card's. */
1254
+ const mine = /* @__PURE__ */ new Set();
1255
+ const asks = [];
1256
+ const seen = {
1257
+ hinted: false,
1258
+ succeeded: false
1259
+ };
1260
+ const show = (path, lines) => {
1261
+ const prefix = path.length === 0 ? "" : paint(term, "muted", `${where(path)} › `);
1262
+ for (const line of lines) out(line === "" ? line : `${prefix}${line}`);
1263
+ };
1264
+ const ask = (run, path, id) => {
1265
+ const decision = store.decision(id);
1266
+ if (human === void 0 || decision === void 0) return;
1267
+ const prompt = { controller: new AbortController() };
1268
+ prompts.set(id, prompt);
1269
+ const settle = async () => {
1270
+ try {
1271
+ const asked = await askStored(store, human, decision, {
1272
+ by: "terminal",
1273
+ signal: prompt.controller.signal,
1274
+ place: {
1275
+ run,
1276
+ decision: id,
1277
+ ...path.length === 0 ? {} : { path: where(path) }
1278
+ },
1279
+ ...options.home === void 0 ? {} : { home: options.home }
1280
+ });
1281
+ prompts.delete(id);
1282
+ const { closed } = prompt;
1283
+ if (asked.kind === "answered") {
1284
+ mine.add(id);
1285
+ if (!term.rich) show(path, [`${say("green", "✓")} ${id}: ${asked.choice}`]);
1286
+ } else if (asked.kind === "refused") show(path, [paint(term, "warn", `! ${asked.message}`)]);
1287
+ else if (asked.kind === "withdrawn" && closed !== void 0) {
1288
+ const by = closed.type === "decision.answered" ? closed.by === "owner" ? closed.owner ?? ANSWERED_BY.owner : ANSWERED_BY[closed.by] : "";
1289
+ show(path, [say("dim", closed.type === "decision.answered" ? `${id} answered elsewhere: ${closed.answer} (${by})` : `${id} withdrawn`)]);
1290
+ }
1291
+ } catch (error) {
1292
+ prompts.delete(id);
1293
+ seen.failure ??= { error };
1294
+ }
1295
+ };
1296
+ asks.push(settle());
1297
+ };
1298
+ const { end, stillRunning } = await followRunTree(store, root, ({ run, path: below, stored }) => {
1299
+ const path = [...basePath, ...below];
1300
+ const { event } = stored;
1301
+ if (event.type === "run.finished" && below.length === 0) seen.succeeded = event.result.status === "succeeded";
1302
+ if (event.type === "decision.answered" || event.type === "decision.withdrawn") {
1303
+ const prompt = prompts.get(event.decision);
1304
+ if (prompt !== void 0) {
1305
+ prompt.closed = event;
1306
+ prompt.controller.abort();
1307
+ return;
1308
+ }
1309
+ if (event.type === "decision.answered" && mine.has(event.decision)) return;
1310
+ }
1311
+ if (event.type === "decision.opened" && event.owner === void 0 && pendingDecisions(store, { runId: run }).some((d) => d.id === event.decision)) {
1312
+ if (human !== void 0) {
1313
+ ask(run, path, event.decision);
1314
+ return;
1315
+ }
1316
+ if (options.hintNoTerminal === true && !seen.hinted) {
1317
+ seen.hinted = true;
1318
+ show(path, [say("dim", "no terminal: answer with ork decide")]);
1319
+ }
1320
+ }
1321
+ let print = printers.get(run);
1322
+ if (print === void 0) {
1323
+ print = createLogPrinter({ json: false });
1324
+ printers.set(run, print);
1325
+ }
1326
+ show(path, print(stored));
1327
+ }, {
1328
+ ...options.home === void 0 ? {} : { home: options.home },
1329
+ ...options.intervalMs === void 0 ? {} : { intervalMs: options.intervalMs },
1330
+ ...options.signal === void 0 ? {} : { signal: options.signal }
1331
+ });
1332
+ for (const prompt of prompts.values()) prompt.controller.abort();
1333
+ await Promise.all(asks);
1334
+ if (seen.failure !== void 0) throw seen.failure.error;
1335
+ if (end === "crashed") show(basePath, [say("red", `run ${root} stopped without finishing (its process is gone)`)]);
1336
+ for (const id of stillRunning) out(say("dim", `↳ run ${id} still running · ork attach ${id}`));
1337
+ return {
1338
+ stillRunning,
1339
+ exitCode: end === "aborted" || seen.succeeded ? 0 : 1
1340
+ };
1341
+ };
1342
+ // src/store.ts
1343
+ /**
1344
+ * @file Opening the user's store (`~/.ork/state.db`) for the length of one
1345
+ * command, and closing it after, whatever happens.
1346
+ */
1347
+ /** Runs `body` with the user's store open, and closes it after. */
1348
+ const withStore = async (body) => {
1349
+ const store = openStore();
1350
+ try {
1351
+ await body(store);
1352
+ } finally {
1353
+ store.close();
1354
+ }
1355
+ };
1356
+ export { unknownNode as a, callsShown as c, createDecisionQueue as d, followRun as i, createTerminalHuman as l, attachRun as n, formatEvent as o, askStored as r, formatRunEvent as s, withStore as t, readText as u };