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

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