@krapht/ork 1.0.0-alpha.7 → 1.0.0-alpha.9

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 (33) hide show
  1. package/dist/{__build_info__-BGoaUwLd.mjs → __build_info__-y1TPvZvp.mjs} +2 -2
  2. package/dist/bin.mjs +598 -51
  3. package/dist/{clean-BPITM-6p.mjs → clean-BRUXyk87.mjs} +5 -2
  4. package/dist/{completion-CCn8xWmO.mjs → completion-cpZHtv50.mjs} +5 -5
  5. package/dist/{context-CZNM454F.mjs → context-Dbay8l7P.mjs} +8 -7
  6. package/dist/{decide-DsA6kOT7.mjs → decide-BphP3ONM.mjs} +1 -1
  7. package/dist/{find-ntyniv2E.mjs → find-BXZjliwr.mjs} +1 -1
  8. package/dist/format-DeT7vL-8.mjs +191 -0
  9. package/dist/{in-a-run-D_fnj1Mw.mjs → in-a-run-C4E9HfED.mjs} +1 -1
  10. package/dist/{limits-BKzVMwkt.mjs → limits-CBeoYrUH.mjs} +1 -1
  11. package/dist/{list-D9HPWLPH.mjs → list-BmxgNyd3.mjs} +11 -4
  12. package/dist/{mcp-BFl3cIDp.mjs → mcp-DTreb7Nq.mjs} +6 -6
  13. package/dist/{output-CwqlG1Le.mjs → output-BVkI6ZVv.mjs} +1 -1
  14. package/dist/{pause-DhgKQORW.mjs → pause-h66SC4rV.mjs} +4 -4
  15. package/dist/{relay-Cp8JGrdv.mjs → relay-BxRhxL6n.mjs} +2 -2
  16. package/dist/{remove-CSpT_nJH.mjs → remove-CYGd9Csj.mjs} +14 -7
  17. package/dist/{send-CHiYkM6X.mjs → send-BskOwN4p.mjs} +3 -3
  18. package/dist/{src-BsLHKQRc.mjs → src-BwFxTank.mjs} +335 -107
  19. package/dist/{src-CUeMUVwW.mjs → src-W1i5Bl6N.mjs} +23 -9
  20. package/dist/{src-DhGLtYR-.mjs → src-bOImrq43.mjs} +30 -11
  21. package/dist/{src-BwnitlJh.mjs → src-xd9DbQw8.mjs} +8 -8
  22. package/dist/ui/assets/{app-D4CoH-lh.js → app-BMg0OOv4.js} +19 -19
  23. package/dist/ui/assets/app-C8uhHVYp.css +1 -0
  24. package/dist/ui/assets/{index-DxDGdEwz.js → index-DY1q4SDl.js} +2 -2
  25. package/dist/ui/index.html +1 -1
  26. package/dist/{ui-BxE0L7V0.mjs → ui-B46Z3e9E.mjs} +4 -4
  27. package/dist/{usage-BuzDsEhq.mjs → usage-DpHXbQEu.mjs} +5 -5
  28. package/dist/{watch-CCj0vU3B.mjs → watch-CJO1ee6Y.mjs} +6 -6
  29. package/dist/{workflows-BNCDD6J-.mjs → workflows-BJdSHnkr.mjs} +4 -3
  30. package/dist/{worktrees-DBBcaimX.mjs → worktrees-Ccf2lUhE.mjs} +72 -19
  31. package/package.json +1 -1
  32. package/dist/format-DaROJUgG.mjs +0 -662
  33. package/dist/ui/assets/app-Dt_tjFj6.css +0 -1
@@ -1,662 +0,0 @@
1
- import { bt as liveStatus, cn as maskSecrets } from "./src-BsLHKQRc.mjs";
2
- import { i as style, n as cost } from "./output-CwqlG1Le.mjs";
3
- import { createInterface } from "node:readline/promises";
4
- // ../core/src/control/queue.ts
5
- const PERMISSION_OPTIONS = [{
6
- id: "allow",
7
- label: "Allow"
8
- }, {
9
- id: "deny",
10
- label: "Deny"
11
- }];
12
- const DENIED = "The user did not allow this tool.";
13
- /** The deny message a session gets, with the person's words when given. */
14
- const deniedMessage = (note) => note === void 0 ? DENIED : `${DENIED} They said: ${note}`;
15
- /** The input fields that say what a tool will do, the first one present. */
16
- const TOOL_INPUT_KEYS = [
17
- "command",
18
- "file_path",
19
- "path",
20
- "url",
21
- "pattern"
22
- ];
23
- /** A tool's input for the decision's context: as written, cut at `max`. */
24
- const inputContext = (input, max = 4e3) => {
25
- const key = TOOL_INPUT_KEYS.find((k) => typeof input[k] === "string");
26
- const text = key === void 0 ? JSON.stringify(input, void 0, 2) : String(input[key]);
27
- return text.length > max ? `${text.slice(0, max - 1)}…` : text;
28
- };
29
- const createDecisionQueue = (options) => {
30
- const { store, runId, emit } = options;
31
- let count = 0;
32
- /** Opens a decision and settles it with the first answer. */
33
- const decide = (open, local, withdrawn) => {
34
- count += 1;
35
- const id = `${runId}-${String(count)}`;
36
- const record = store.openDecision({
37
- ...open,
38
- id,
39
- runId
40
- });
41
- const event = {
42
- type: "decision.opened",
43
- node: record.node,
44
- decision: id,
45
- kind: record.kind,
46
- question: record.question,
47
- options: record.options
48
- };
49
- if (record.recommended !== void 0) event.recommended = record.recommended;
50
- if (record.owner !== void 0) event.owner = record.owner;
51
- emit(event);
52
- const { promise, resolve, reject } = Promise.withResolvers();
53
- const settled = new AbortController();
54
- /** `note` is the stored one: masked, and absent when blank. */
55
- const finish = (answer, by, note) => {
56
- if (settled.signal.aborted) return;
57
- settled.abort();
58
- const answered = {
59
- type: "decision.answered",
60
- node: open.node,
61
- decision: id,
62
- answer,
63
- by
64
- };
65
- if (note !== void 0) answered.note = note;
66
- emit(answered);
67
- resolve(note === void 0 ? {
68
- answer,
69
- by
70
- } : {
71
- answer,
72
- by,
73
- note
74
- });
75
- };
76
- const withdraw = () => {
77
- if (settled.signal.aborted) return;
78
- settled.abort();
79
- store.withdrawDecision(id);
80
- emit({
81
- type: "decision.withdrawn",
82
- node: open.node,
83
- decision: id
84
- });
85
- reject(/* @__PURE__ */ new Error("the question was withdrawn"));
86
- };
87
- if (withdrawn?.aborted === true) withdraw();
88
- withdrawn?.addEventListener("abort", withdraw, { once: true });
89
- const fromStore = async () => {
90
- try {
91
- const closed = await options.waitFor(id, settled.signal);
92
- if (closed.status === "answered" && closed.answer !== void 0) finish(closed.answer, closed.answeredBy ?? "decide", closed.note);
93
- } catch {}
94
- };
95
- const fromTerminal = async (ask) => {
96
- try {
97
- const { answer, note } = await ask(settled.signal);
98
- if (store.answerDecision(id, answer, "terminal", note)) finish(answer, "terminal", store.decision(id)?.note);
99
- } catch {}
100
- };
101
- fromStore();
102
- if (local !== void 0) fromTerminal(local);
103
- return promise;
104
- };
105
- return {
106
- ask: (question) => {
107
- const open = {
108
- node: question.nodeId,
109
- kind: "question",
110
- question: question.question,
111
- options: question.options
112
- };
113
- if (question.recommended !== void 0) open.recommended = question.recommended;
114
- if (question.context !== void 0) open.context = question.context;
115
- if (question.document !== void 0) open.document = question.document;
116
- if (question.owner !== void 0) open.owner = question.owner;
117
- const { local } = options;
118
- return decide(open, local === void 0 ? void 0 : (signal) => {
119
- let note;
120
- return local.ask({
121
- ...question,
122
- question: maskSecrets(question.question),
123
- ...question.context === void 0 ? {} : { context: maskSecrets(question.context) },
124
- ...question.document === void 0 ? {} : { document: maskSecrets(question.document) },
125
- signal,
126
- onNote: (given) => {
127
- note = given;
128
- }
129
- }).then((answer) => note === void 0 ? { answer } : {
130
- answer,
131
- note
132
- });
133
- }, question.signal).then(({ answer, note }) => {
134
- if (note !== void 0) question.onNote?.(note);
135
- return answer;
136
- });
137
- },
138
- settled: (question) => {
139
- count += 1;
140
- const id = `${runId}-${String(count)}`;
141
- const { owner } = question;
142
- const record = store.openDecision({
143
- id,
144
- runId,
145
- node: question.nodeId,
146
- kind: "question",
147
- question: question.question,
148
- options: question.options,
149
- owner,
150
- ...question.recommended === void 0 ? {} : { recommended: question.recommended },
151
- ...question.context === void 0 ? {} : { context: question.context }
152
- });
153
- emit({
154
- type: "decision.opened",
155
- node: record.node,
156
- decision: id,
157
- kind: "question",
158
- question: record.question,
159
- options: record.options,
160
- owner,
161
- ...record.recommended === void 0 ? {} : { recommended: record.recommended }
162
- });
163
- store.answerDecision(id, question.answer, "owner", question.note);
164
- const note = store.decision(id)?.note;
165
- emit({
166
- type: "decision.answered",
167
- node: record.node,
168
- decision: id,
169
- answer: question.answer,
170
- by: "owner",
171
- owner,
172
- ...note === void 0 ? {} : { note }
173
- });
174
- },
175
- permit: async (request) => {
176
- const { local } = options;
177
- let localMessage = DENIED;
178
- const { answer, note } = await decide({
179
- node: request.nodeId,
180
- kind: "permission",
181
- question: `Allow ${request.toolName}?`,
182
- options: PERMISSION_OPTIONS,
183
- context: inputContext(request.input)
184
- }, local === void 0 ? void 0 : (signal) => {
185
- let said;
186
- return local.permit({
187
- ...request,
188
- signal,
189
- onNote: (given) => {
190
- said = given;
191
- }
192
- }).then((result) => {
193
- if (!result.allow) localMessage = result.message;
194
- const choice = result.allow ? "allow" : "deny";
195
- return said === void 0 ? { answer: choice } : {
196
- answer: choice,
197
- note: said
198
- };
199
- });
200
- });
201
- if (note !== void 0) request.onNote?.(note);
202
- return answer === "allow" ? { allow: true } : {
203
- allow: false,
204
- message: note === void 0 ? localMessage : deniedMessage(note)
205
- };
206
- }
207
- };
208
- };
209
- // src/human.ts
210
- /**
211
- * @file The terminal `HumanChannel`: answers `ask_user` questions, gates and
212
- * permission prompts on the terminal, one at a time, and denies or defers
213
- * when no one is there. After a choice it offers one optional line for a
214
- * note, which Enter skips. In a run it is the decision queue's local
215
- * answerer, racing `ork decide`.
216
- */
217
- const PREVIEW = 160;
218
- /** A document longer than this is offered (`d`) rather than printed. */
219
- const SHORT_DOCUMENT_LINES = 40;
220
- const NO_ONE_TO_PERMIT = "No one is at the terminal to approve this tool, so it is not allowed in this run.";
221
- const NOTE_PROMPT = " Note for the run (optional, Enter to skip): ";
222
- const NO_ONE_TO_ASK = "no one (nobody is at the terminal; use your best judgment and say so in your report)";
223
- /** One line that shows what a tool is about to do. */
224
- const previewInput = (toolName, input) => {
225
- const key = TOOL_INPUT_KEYS.find((k) => typeof input[k] === "string");
226
- const line = (key === void 0 ? JSON.stringify(input) : `${key}: ${String(input[key])}`).replaceAll(/\s+/gu, " ").trim();
227
- return line.length > PREVIEW ? `${toolName} ${line.slice(0, 159)}…` : `${toolName} ${line}`;
228
- };
229
- /** The option an answer names: its number, or its id. */
230
- const pickOption = (answer, options, recommended) => {
231
- const value = answer.trim();
232
- if (value === "") return recommended;
233
- const index = Number(value);
234
- if (Number.isInteger(index) && index >= 1 && index <= options.length) return options[index - 1]?.id;
235
- return options.find((o) => o.id === value)?.id;
236
- };
237
- const optionLine = (option, index, recommended) => {
238
- const label = option.label === void 0 ? "" : ` ${option.label}`;
239
- const description = option.description === void 0 ? "" : style("dim", ` - ${option.description}`);
240
- const mark = option.id === recommended ? style("green", " (recommended)") : "";
241
- return ` ${String(index + 1)}) ${style("bold", option.id)}${label}${description}${mark}`;
242
- };
243
- /**
244
- * Answers permission prompts and `ask_user` questions on the terminal, one at a
245
- * time. Without a terminal it denies tools and leaves questions to the node. A
246
- * withdrawn prompt (answered elsewhere) rejects, which frees the turn.
247
- */
248
- const createTerminalHuman = (io) => {
249
- let rl;
250
- let turn = Promise.resolve();
251
- const say = (text) => {
252
- io.output.write(`${text}\n`);
253
- };
254
- const prompt = (text, signal) => {
255
- rl ??= createInterface({
256
- input: io.input,
257
- output: io.output
258
- });
259
- return signal === void 0 ? rl.question(text) : rl.question(text, { signal });
260
- };
261
- /** The optional line after a choice, when the asker takes a note. */
262
- const takeNote = async (onNote, signal) => {
263
- if (onNote === void 0) return;
264
- const note = (await prompt(NOTE_PROMPT, signal)).trim();
265
- if (note !== "") onNote(note);
266
- };
267
- const choose = async (hint, options, recommended, signal, document) => {
268
- const answer = await prompt(` Choose${hint}: `, signal);
269
- const picked = pickOption(answer, options, recommended);
270
- if (picked !== void 0) return picked;
271
- if (document !== void 0 && answer.trim() === "d") say(document);
272
- else say(style("dim", " Type an option's number or id."));
273
- return choose(hint, options, recommended, signal, document);
274
- };
275
- const oneAtATime = (work) => {
276
- const result = turn.then(work);
277
- turn = result.catch(() => null);
278
- return result;
279
- };
280
- return {
281
- permit: ({ nodeId, toolName, input, signal, onNote }) => oneAtATime(async () => {
282
- if (signal?.aborted === true) throw new Error("the prompt was withdrawn");
283
- say(`${style(["yellow", "bold"], "?")} ${style("bold", nodeId)} wants to run ${previewInput(toolName, input)}`);
284
- if (!io.interactive) {
285
- say(style("dim", ` denied: ${NO_ONE_TO_PERMIT}`));
286
- return {
287
- allow: false,
288
- message: NO_ONE_TO_PERMIT
289
- };
290
- }
291
- const answer = (await prompt(" Allow? [y/N] ", signal)).trim().toLowerCase();
292
- if (answer === "y" || answer === "yes") return { allow: true };
293
- await takeNote(onNote, signal);
294
- return {
295
- allow: false,
296
- message: "The user did not allow this tool."
297
- };
298
- }),
299
- ask: ({ nodeId, question, options, recommended, context, document, signal, onNote }) => oneAtATime(async () => {
300
- if (signal?.aborted === true) throw new Error("the question was withdrawn");
301
- const documentLines = document?.trimEnd().split("\n").length ?? 0;
302
- if (document !== void 0 && documentLines <= SHORT_DOCUMENT_LINES) say(document);
303
- say(`${style(["yellow", "bold"], "?")} ${style("bold", nodeId)} asks: ${question}`);
304
- if (context !== void 0) say(style("dim", ` ${context.replaceAll("\n", "\n ")}`));
305
- for (const [index, option] of options.entries()) say(optionLine(option, index, recommended));
306
- if (!io.interactive) {
307
- const answer = recommended ?? NO_ONE_TO_ASK;
308
- say(style("dim", ` no terminal; answered ${answer}`));
309
- return answer;
310
- }
311
- const fallback = options.findIndex((o) => o.id === recommended);
312
- const hint = fallback === -1 ? "" : ` [${String(fallback + 1)}]`;
313
- if (documentLines > SHORT_DOCUMENT_LINES) say(` Document: ${String(documentLines)} lines. Type d to read it.`);
314
- const picked = await choose(hint, options, recommended, signal, document);
315
- await takeNote(onNote, signal);
316
- return picked;
317
- }),
318
- close: () => {
319
- rl?.close();
320
- }
321
- };
322
- };
323
- // src/run/format-bus.ts
324
- /** The first line of a message body, cut to fit one terminal line. */
325
- const firstLine = (body) => {
326
- const [line = ""] = body.trim().split("\n");
327
- return line.length > 100 ? `${line.slice(0, 99)}…` : line;
328
- };
329
- /** Messages, owner questions and claims, without the node prefix. */
330
- const formatBusEvent = (event) => {
331
- if (event.type === "claim.taken") return [style("dim", `⚑ claimed ${event.resource}`)];
332
- if (event.type === "claim.refused") {
333
- const held = event.held === event.resource ? "" : ` (it holds ${event.held})`;
334
- return [style("yellow", `⚑ ${event.resource} is ${event.holder}'s${held}`)];
335
- }
336
- if (event.type === "claim.released") return [style("dim", `⚑ released ${event.resource}`)];
337
- if (event.type === "message.sent") {
338
- const how = event.via === void 0 ? "" : ` (${event.via})`;
339
- return [style("dim", `✉ to ${event.to}${how}: ${firstLine(event.body)}`)];
340
- }
341
- if (event.type === "message.received") {
342
- const how = event.via === void 0 ? "" : ` (${event.via})`;
343
- return [style("dim", `✉ from ${event.from}${how}: ${firstLine(event.body)}`)];
344
- }
345
- if (event.type === "call.started") return [style("dim", `↳ started ${event.workflow} as run ${event.run}`)];
346
- if (event.type === "link.notice") return [style("dim", `↔ ${event.text}`)];
347
- if (event.type === "message.read") return [];
348
- if (event.type === "owner.asked") return [`${style(["yellow", "bold"], "?")} asked as the owner of ${event.about}: ${event.text} ${style("dim", `(${event.question})`)}`];
349
- if (event.type === "owner.answered") return [style("dim", `→ ${event.answer} (owner)`)];
350
- return [style("dim", "→ its session ended first; asking you")];
351
- };
352
- // src/run/format-result.ts
353
- const RUN_MARK = {
354
- succeeded: style("green", "✓"),
355
- failed: style("red", "✗"),
356
- budget_exhausted: style("yellow", "!"),
357
- quota_exhausted: style("yellow", "!"),
358
- timed_out: style("yellow", "!"),
359
- interrupted: style("yellow", "!")
360
- };
361
- /** `1 failure handled`, `2 failures handled`. */
362
- const failuresHandled = (count) => `${String(count)} ${count === 1 ? "failure" : "failures"} handled`;
363
- /**
364
- * The run summary: status, cost, what ran, what was skipped and which failures
365
- * an edge handled, branches to look at, why, and mail to nodes that never ran.
366
- */
367
- const formatRunResult = (result) => {
368
- const ran = result.nodes.filter((n) => n.runs > 0).length;
369
- const skipped = result.skipped ?? result.nodes.length - ran;
370
- const handled = result.failuresHandled ?? 0;
371
- const lines = ["", `${RUN_MARK[result.status]} run ${result.runId} ${result.status.replace("_", " ")} ${style("dim", `· ${cost(result.costUsd)} · ${String(ran)} ran, ${String(skipped)} skipped${handled === 0 ? "" : `, ${failuresHandled(handled)}`}`)}`];
372
- for (const kept of result.worktrees) lines.push(` ${kept.id}: ${kept.branch ?? "detached"} ${style("dim", `at ${kept.path} (${kept.reason})`)}`);
373
- for (const reason of result.reasons) lines.push(` ${style("red", reason)}`);
374
- 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`);
375
- return lines;
376
- };
377
- // src/run/format.ts
378
- /** Who answered a decision, as the log says it. */
379
- const ANSWERED_BY = {
380
- terminal: "terminal",
381
- decide: "ork decide",
382
- ui: "ork ui",
383
- owner: "its owner"
384
- };
385
- const STATUS_MARK = {
386
- pass: style("green", "✓"),
387
- done: style("green", "✓"),
388
- changes_requested: style("yellow", "!"),
389
- fail: style("red", "✗")
390
- };
391
- const seconds = (ms) => `${(ms / 1e3).toFixed(1)}s`;
392
- const indent = (text) => text.trimEnd().split("\n").map((line) => ` ${line}`);
393
- /** The usage line: cost, tokens, turns and time. */
394
- const formatUsage = ({ usage }) => style("dim", [
395
- cost(usage.costUsd),
396
- `${String(usage.inputTokens)} in / ${String(usage.outputTokens)} out (${String(usage.cacheReadTokens)} cached)`,
397
- `${String(usage.turns)} turns`,
398
- seconds(usage.durationMs)
399
- ].join(" · "));
400
- /** How a finished session reads on the terminal. */
401
- const formatOutcome = (nodeId, outcome) => {
402
- const { report } = outcome;
403
- if (report === null) return [
404
- `${style("red", "✗")} ${style("bold", nodeId)} ended without a report (${outcome.subtype})`,
405
- ...outcome.errors.map((e) => ` ${style("red", e)}`),
406
- ` ${formatUsage(outcome)}`
407
- ];
408
- const lines = [`${STATUS_MARK[report.status] ?? "-"} ${style("bold", nodeId)} ${report.status}: ${report.summary}`];
409
- if (report.branch !== void 0) lines.push(` branch: ${report.branch}`);
410
- for (const [name, file] of Object.entries(report.artifacts)) lines.push(` ${name}: ${file}`);
411
- for (const [name, value] of Object.entries(report.fields)) lines.push(` ${name}: ${JSON.stringify(value)}`);
412
- lines.push(` ${formatUsage(outcome)}`);
413
- return lines;
414
- };
415
- /** The transcript lines for one event; tool names come from `tools`. */
416
- const formatEvent = (nodeId, event, tools) => {
417
- if (event.type === "session.started") {
418
- const fork = event.fork === true ? ", a fork to answer a question" : "";
419
- return [style("dim", `▸ ${nodeId} (${event.model}, session ${event.sessionId}${fork})`)];
420
- }
421
- if (event.type === "assistant.text") return indent(event.text);
422
- if (event.type === "tool.called") {
423
- tools.set(event.id, event.name);
424
- return [style("dim", ` · ${previewInput(event.name, event.input)}`)];
425
- }
426
- if (event.type === "tool.result") return event.isError ? [style("red", ` ✗ ${tools.get(event.id) ?? "tool"} failed`)] : [];
427
- if (event.type === "plan.limit") return event.status === "allowed" ? [] : [style("yellow", ` ! plan: ${event.window.replaceAll("_", " ")} limit ${event.status === "rejected" ? "reached" : "is close"}`)];
428
- return formatOutcome(nodeId, event.outcome);
429
- };
430
- /** A one-line verdict for a finished node, by what its type outputs. */
431
- const nodeVerdict = (output) => {
432
- const { status, summary, exitCode, choice, answeredBy, count } = output;
433
- if (typeof status === "string") return typeof summary === "string" && summary !== "" ? `${status}: ${summary}` : status;
434
- if (typeof exitCode === "number") return `exit ${String(exitCode)}`;
435
- if (typeof choice === "string") return `${choice} (${String(answeredBy)})`;
436
- if (typeof count === "number") return `${String(count)} ran`;
437
- return "done";
438
- };
439
- const items = (count) => `${String(count)} ${count === 1 ? "item" : "items"}`;
440
- /** Transcript lines for one run event, prefixed with the node they are from. */
441
- const formatRunEvent = (event, width, tools) => {
442
- const tag = (id) => style("cyan", `${id.padEnd(width)} │`);
443
- if (event.type === "run.started") return [style("dim", `▸ run ${event.runId} of ${event.workflow} (starts at ${event.entries.join(", ")})`), ...event.settings?.trigger === void 0 ? [] : [style("dim", ` started by trigger ${event.settings.trigger}; untrusted inputs: ${(event.settings.untrusted ?? []).join(", ") || "none"}`)]];
444
- if (event.type === "node.started") {
445
- const round = event.round > 1 ? ` (round ${String(event.round)})` : "";
446
- const owner = event.owner === void 0 ? "" : `, for ${event.owner}`;
447
- return [`${tag(event.id)} ${style("dim", `${event.nodeType}${round}${owner}`)}`];
448
- }
449
- if (event.type === "node.event") {
450
- const inner = event.event;
451
- if (inner.type === "session.finished") return [];
452
- const fork = inner.fork === true ? style("dim", "fork · ") : "";
453
- if (inner.type === "session.started") {
454
- const why = inner.fork === true ? ", to answer a question" : "";
455
- return [`${tag(event.id)} ${fork}${style("dim", `${inner.model}, session ${inner.sessionId}${why}`)}`];
456
- }
457
- return formatEvent(event.id, event.event, tools).map((line) => `${tag(event.id)} ${fork}${line.replace(/^ {2}/u, "")}`);
458
- }
459
- if (event.type === "node.finished") {
460
- const mark = event.ok ? style("green", "✓") : style("red", "✗");
461
- const spent = event.usage === void 0 ? "" : style("dim", ` · ${cost(event.usage.costUsd)}`);
462
- return [`${tag(event.id)} ${mark} ${nodeVerdict(event.output)}${spent}`];
463
- }
464
- if (event.type === "node.skipped") return [`${tag(event.id)} ${style("dim", "- skipped")}`];
465
- if (event.type === "loop.exhausted") return [`${style(["yellow", "bold"], "!")} the loop ${event.from} -> ${event.to} used all ${String(event.maxRounds)} rounds`];
466
- if (event.type === "watch.started") return [`${tag(event.id)} ${style("dim", `watching until ${event.until}, every ${event.every}`)}`];
467
- if (event.type === "watch.changed") return [`${tag(event.id)} ${style("dim", `look ${String(event.looks)}:`)} ${event.said}`];
468
- if (event.type === "watch.ended") return [`${tag(event.id)} ${style("dim", `${event.outcome} after ${String(event.looks)} looks`)}`];
469
- if (event.type === "edge.taken") return [];
470
- if (event.type === "worktree") {
471
- const detail = event.detail === void 0 ? "" : ` (${event.detail})`;
472
- const line = `${event.action} ${event.branch ?? event.path}${detail}`;
473
- return [`${tag(event.id)} ${event.action === "conflict" ? style("yellow", `! ${line}`) : style("dim", `⎇ ${line}`)}`];
474
- }
475
- if (event.type === "decision.opened") {
476
- const what = event.kind === "permission" ? "asks to" : "asks:";
477
- const question = event.kind === "permission" ? event.question.replace(/^Allow /u, "run ").replace(/\?$/u, "") : event.question;
478
- const where = event.owner === void 0 ? `decision ${event.decision}; ork decide` : `decision ${event.decision}, via ${event.owner}`;
479
- return [`${tag(event.node)} ${style(["yellow", "bold"], "?")} ${what} ${question} ${style("dim", `(${where})`)}`];
480
- }
481
- if (event.type === "decision.answered") {
482
- const by = event.by === "owner" && event.owner !== void 0 ? event.owner : ANSWERED_BY[event.by];
483
- const said = event.note === void 0 ? "" : ` · said: ${event.note.replaceAll(/\s+/gu, " ")}`;
484
- return [`${tag(event.node)} ${style("dim", `→ ${event.answer} (${by})${said}`)}`];
485
- }
486
- if (event.type === "decision.withdrawn") return [`${tag(event.node)} ${style("dim", "→ question withdrawn")}`];
487
- if (event.type === "run.command") {
488
- const node = event.node === void 0 ? "" : ` --node ${event.node}`;
489
- return [style(["yellow", "bold"], `! ork ${event.command}${node} received`)];
490
- }
491
- if (event.type === "node.held") {
492
- const at = event.at === "tool" ? "its tool call waits for ork continue" : "it starts after ork continue";
493
- return [`${tag(event.id)} ${style("yellow", `‖ paused: ${at}`)}`];
494
- }
495
- if (event.type === "node.released") return [`${tag(event.id)} ${style("dim", "▸ continues")}`];
496
- if (event.type === "each.listed") {
497
- const round = event.round > 1 ? ` (round ${String(event.round)})` : "";
498
- return [`${tag(event.node)} ${style("dim", `${items(event.count)}${round}`)}`];
499
- }
500
- if (event.type === "each.exceeded") {
501
- const over = `${items(event.count)}, over maxItems ${String(event.maxItems)}`;
502
- return [`${tag(event.node)} ${style("yellow", `! ${over}`)}`];
503
- }
504
- if (event.type === "run.finished") return formatRunResult(event.result);
505
- if (event.type === "run.resumed") {
506
- const [one] = event.kept;
507
- const kept = one === void 0 ? "every node runs again" : event.kept.length === 1 ? `${one} keeps its result` : `${event.kept.join(", ")} keep their results`;
508
- return ["", style("dim", `▸ resumed: ${kept}`)];
509
- }
510
- if (event.type === "feedback.recorded") {
511
- const { target, kind, title } = event.feedback;
512
- const line = style("dim", `✎ feedback (${target}, ${kind}): ${title}`);
513
- return [event.node === void 0 ? line : `${tag(event.node)} ${line}`];
514
- }
515
- if (event.type === "user.message") {
516
- const note = event.delivered === "queued" ? " (for its next session)" : "";
517
- return [`${tag(event.node)} ${style(["magenta", "bold"], "you:")} ${firstLine(event.body)}${style("dim", note)}`];
518
- }
519
- return formatBusEvent(event).map((line) => `${tag(event.node)} ${line}`);
520
- };
521
- // src/runs/format.ts
522
- /**
523
- * @file How stored runs read on the terminal: the `ork ps` table and the `ork
524
- * tree` ownership tree with each node's state, tokens and cost, and what is
525
- * paused (ADR-041).
526
- */
527
- const STATUS_STYLE = {
528
- running: "cyan",
529
- paused: "yellow",
530
- succeeded: "green",
531
- failed: "red",
532
- crashed: "red",
533
- budget_exhausted: "yellow",
534
- quota_exhausted: "yellow",
535
- timed_out: "yellow",
536
- interrupted: "yellow"
537
- };
538
- /** `just now`, `5m ago`, `3h ago`, `2d ago`, then the date. */
539
- const ago = (iso, now = Date.now()) => {
540
- const minutes = Math.floor((now - Date.parse(iso)) / 6e4);
541
- if (minutes < 1) return "just now";
542
- if (minutes < 60) return `${String(minutes)}m ago`;
543
- if (minutes < 1440) return `${String(Math.floor(minutes / 60))}h ago`;
544
- if (minutes < 10080) return `${String(Math.floor(minutes / 1440))}d ago`;
545
- return iso.slice(0, 10);
546
- };
547
- /** `42s`, `3m12s`, `1h05m`. */
548
- const duration = (ms) => {
549
- const s = Math.max(0, Math.round(ms / 1e3));
550
- if (s < 60) return `${String(s)}s`;
551
- if (s < 3600) return `${String(Math.floor(s / 60))}m${String(s % 60).padStart(2, "0")}s`;
552
- return `${String(Math.floor(s / 3600))}h${String(Math.floor(s % 3600 / 60)).padStart(2, "0")}m`;
553
- };
554
- /**
555
- * The `ork ps` table; `withProject` adds each run's project. A called run's
556
- * cost is dimmed and says its caller's, whose cost already counts it.
557
- */
558
- const formatRuns = (runs, withProject, now = Date.now(), statusOf = liveStatus) => {
559
- if (runs.length === 0) return ["No runs."];
560
- const rows = runs.map((run) => {
561
- const status = statusOf(run);
562
- const end = run.finishedAt === void 0 ? now : Date.parse(run.finishedAt);
563
- return {
564
- id: run.id,
565
- workflow: run.workflow,
566
- status,
567
- when: `${ago(run.startedAt, now)} · ${duration(end - Date.parse(run.startedAt))}`,
568
- cost: run.parent === void 0 ? cost(run.costUsd) : `${cost(run.costUsd)} in ${run.parent}`,
569
- called: run.parent !== void 0,
570
- project: run.project
571
- };
572
- });
573
- const width = (pick, title) => Math.max(title.length, ...rows.map((r) => pick(r).length));
574
- const w = {
575
- id: width((r) => r.id, "RUN"),
576
- workflow: width((r) => r.workflow, "WORKFLOW"),
577
- status: width((r) => r.status, "STATUS"),
578
- when: width((r) => r.when, "STARTED"),
579
- cost: width((r) => r.cost, "COST")
580
- };
581
- const header = [
582
- "RUN".padEnd(w.id),
583
- "WORKFLOW".padEnd(w.workflow),
584
- "STATUS".padEnd(w.status),
585
- "STARTED".padEnd(w.when),
586
- "COST".padStart(w.cost),
587
- ...withProject ? ["PROJECT"] : []
588
- ].join(" ");
589
- return [style("dim", header), ...rows.map((r) => [
590
- style("bold", r.id.padEnd(w.id)),
591
- r.workflow.padEnd(w.workflow),
592
- style(STATUS_STYLE[r.status] ?? "reset", r.status.padEnd(w.status)),
593
- style("dim", r.when.padEnd(w.when)),
594
- r.called ? style("dim", r.cost.padStart(w.cost)) : r.cost.padStart(w.cost),
595
- ...withProject ? [style("dim", r.project)] : []
596
- ].join(" "))];
597
- };
598
- const STATE_MARK = {
599
- waiting: style("dim", "·"),
600
- running: style("cyan", "●"),
601
- ok: style("green", "✓"),
602
- failed: style("red", "✗"),
603
- skipped: style("dim", "-")
604
- };
605
- /** `950`, `12.3k`, `1.2M`. */
606
- const count = (n) => {
607
- if (n < 1e3) return String(n);
608
- if (n < 1e6) return `${(n / 1e3).toFixed(1)}k`;
609
- return `${(n / 1e6).toFixed(1)}M`;
610
- };
611
- const stateText = (node) => {
612
- if (node.state === "running") return node.runs > 1 ? `running (round ${String(node.runs)})` : "running";
613
- if (node.state === "ok" || node.state === "failed") {
614
- const verdict = nodeVerdict(node.output ?? {});
615
- const line = verdict.length > 60 ? `${verdict.slice(0, 59)}…` : verdict;
616
- return node.runs > 1 ? `${line} (${String(node.runs)} runs)` : line;
617
- }
618
- return node.state;
619
- };
620
- const isItem = (node) => node.template !== void 0 && /\.\d+$/u.test(node.id);
621
- /**
622
- * Moves each item of a node with `forEach` (`build.2`) under its node, in list
623
- * order, from beside it or under its owner, where the events put it.
624
- */
625
- const nestItems = (roots) => {
626
- const all = [];
627
- const collect = (node) => {
628
- all.push(node);
629
- for (const child of node.children) collect(child);
630
- };
631
- for (const root of roots) collect(root);
632
- const items = all.filter((node) => isItem(node));
633
- const home = new Map(all.map((node) => [node.id, node]));
634
- const isMoved = (node) => items.includes(node) && home.has(node.template ?? "");
635
- for (const node of all) node.children = node.children.filter((c) => !isMoved(c));
636
- for (const item of items) home.get(item.template ?? "")?.children.push(item);
637
- return roots.filter((root) => !isMoved(root));
638
- };
639
- /** The ownership tree, one line per node, children under their owner. */
640
- const formatTree = (roots, now = Date.now()) => {
641
- const lines = [];
642
- const walk = (node, lead, branch) => {
643
- lines.push({
644
- label: `${lead}${branch}${node.id}`,
645
- node
646
- });
647
- const next = lead + (branch === "├─ " ? "│ " : branch === "" ? "" : " ");
648
- node.children.forEach((child, index) => {
649
- walk(child, next, index === node.children.length - 1 ? "└─ " : "├─ ");
650
- });
651
- };
652
- for (const root of roots) walk(root, "", "");
653
- const label = Math.max(...lines.map((l) => l.label.length));
654
- const type = Math.max(...lines.map((l) => l.node.type.length));
655
- return lines.map(({ label: text, node }) => {
656
- const usage = node.inputTokens + node.outputTokens === 0 ? "" : style("dim", ` ${count(node.inputTokens)} in / ${count(node.outputTokens)} out · ${cost(node.costUsd)}`);
657
- const state = node.held === true ? `${style("yellow", "‖")} paused` : `${STATE_MARK[node.state]} ${stateText(node)}`;
658
- const spent = isItem(node) && node.startedAt !== void 0 ? style("dim", ` ${duration((node.finishedAt === void 0 ? now : Date.parse(node.finishedAt)) - Date.parse(node.startedAt))}`) : "";
659
- return `${text.padEnd(label)} ${style("dim", node.type.padEnd(type))} ${state}${spent}${usage}`;
660
- });
661
- };
662
- export { formatEvent as a, createDecisionQueue as c, nestItems as i, formatRuns as n, formatRunEvent as o, formatTree as r, createTerminalHuman as s, ago as t };