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

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