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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/{__build_info__-DFF18K8y.mjs → __build_info__-CIchF6nr.mjs} +3 -72
  2. package/dist/bin.mjs +1066 -387
  3. package/dist/chips-BA7SrrH5.mjs +43 -0
  4. package/dist/{clean-DVaEH9e7.mjs → clean-Di-KT7Mf.mjs} +1 -1
  5. package/dist/{completion-Dv5kf598.mjs → completion-C5D0sEs5.mjs} +22 -15
  6. package/dist/{context-CEaKOVmG.mjs → context-BaNshWr9.mjs} +43 -31
  7. package/dist/{decide-3l9q8UtX.mjs → decide-B6PnKeqo.mjs} +1 -1
  8. package/dist/{find-DEjjOlTG.mjs → find-BicBbGMg.mjs} +1 -1
  9. package/dist/{format-Cb5_m-Qd.mjs → format-lWhN-jYR.mjs} +130 -35
  10. package/dist/{in-a-run-BS9Nc-JW.mjs → in-a-run-CgkVouyG.mjs} +1 -1
  11. package/dist/{limits-BSwEjQST.mjs → limits-jZQp9ZJC.mjs} +1 -1
  12. package/dist/{list-Qxr6rwsY.mjs → list-CEaJ07jd.mjs} +1 -1
  13. package/dist/{mcp-CslnG5wf.mjs → mcp-DYgVA9f5.mjs} +6 -6
  14. package/dist/output-CM95njYt.mjs +421 -0
  15. package/dist/{pause-D7OVB5Ac.mjs → pause-D6PuSlNl.mjs} +4 -4
  16. package/dist/{relay-EceRIebO.mjs → relay-DNx7be9k.mjs} +2 -2
  17. package/dist/{remove-0IQZVWmI.mjs → remove-CpMb4NDl.mjs} +3 -3
  18. package/dist/{report-DK4LGqwF.mjs → report-CEN8VoUb.mjs} +31 -20
  19. package/dist/{send-C0uwfUYi.mjs → send-CLs_fyg7.mjs} +3 -3
  20. package/dist/{src-cmmXKHn1.mjs → src-9iGkjnsy.mjs} +234 -150
  21. package/dist/{src-i5k2Y-kf.mjs → src-B6KvTm7B.mjs} +78 -11
  22. package/dist/{src-oaJYIddp.mjs → src-BnZgZGrf.mjs} +523 -81
  23. package/dist/{src-Df6KOIDC.mjs → src-D6_xjm-x.mjs} +1722 -825
  24. package/dist/ui/assets/{app-qs8Lofv6.js → app-CxURph7a.js} +16 -16
  25. package/dist/ui/assets/app-QoHVi-mb.css +1 -0
  26. package/dist/ui/assets/{index-DV1ZaOk-.js → index-By_8KKG6.js} +2 -2
  27. package/dist/ui/index.html +1 -1
  28. package/dist/{ui-CBkHJdqk.mjs → ui-CIci4UBW.mjs} +4 -4
  29. package/dist/{usage-Cd8pEYyK.mjs → usage-GceBGMjU.mjs} +43 -32
  30. package/dist/{watch-DW5R2OPO.mjs → watch-BmoybMQg.mjs} +28 -16
  31. package/dist/{workflows-BT1gDRIV.mjs → workflows-DZvW3i1e.mjs} +3 -3
  32. package/dist/{worktrees-g1MQMGie.mjs → worktrees-Be51E2rB.mjs} +11 -10
  33. package/package.json +1 -1
  34. package/dist/output-Bqljtmp_.mjs +0 -40
  35. package/dist/ui/assets/app-C8uhHVYp.css +0 -1
package/dist/bin.mjs CHANGED
@@ -1,23 +1,24 @@
1
1
  #!/usr/bin/env node
2
- import { $ as adoptedCalls, A as inspectProject, An as inside, B as createCalls, Cn as newProjectId, Ct as runTree, D as openStore, Dn as runDirOf, E as isNodeEvent, En as projectsHome, F as clearGitLocalEnv, G as peerOf, H as endLink, Hn as beforeOf, Ht as FEEDBACK_KINDS, I as parseInputPairs, It as PROJECT_FILE, J as flattenTree, Jt as sendPost, K as tellNode, Kn as serializeWorkflow, Kt as planPost, L as resolveInputs, Lt as gitRulesOf, On as shownPath, Ot as nestItems, Q as tellLiveRun, Qn as settingsOf, Qt as normalizeTitle, Rt as loadProject, Sn as inRepoPlace, St as runStatus, Tn as placeFor, U as endLinksOf, Ut as FEEDBACK_SEVERITIES, V as addressOf, Wt as FEEDBACK_TARGETS, Xt as createFeedback, Yn as WorkflowSchema, Z as requestStop, Zt as fingerprint, _n as createAgent, an as socketPath, at as followEvents, bn as loadAgents, bt as liveStatus, ct as planNodeSession, d as runningRunIds, dn as planAgentMigration, dr as readyDriver, er as isClaudeNode, et as detachResume, f as runningScript, fn as applySteps, fr as AGENT_TOOL_LAYOUTS, gn as toolsInUse, gt as startRun, h as isNewer, hn as toolLinks, ht as walkSummary, i as NOT_STARTED, in as serveRun, ir as retiredDriverHint, it as detachRun, jn as projectRootOf, l as planUpgrade, ln as maskReplacer, mn as linkTools, mr as AGENT_DRIVERS, n as resolveBase, nt as listKey, pn as planMoveInto, pr as ORK_LAYOUT, rt as DETACHED_ENV, sn as wakeRun, t as createWorkspace, tt as resumable, u as releaseNotes, un as maskSecrets, ur as driverChecks, ut as sessionDriver, vn as deleteAgent, vt as userMessageText, wn as placeFiles, wt as templateOf, xn as hasInRepoFiles, yn as catalogAgent } from "./src-Df6KOIDC.mjs";
3
- import { C as placeOf, T as openFixRun, _ as nodeSession, b as recordNote, f as readUpdateCache, g as activeTree, h as writeUpdateCache, i as registryOf, l as fetchLatest, m as updateCachePath, n as ORK_VERSION, s as runInstall, t as ORK_NAME, v as previewPost, w as feedbackActions, x as acceptItem, y as checkNote } from "./__build_info__-DFF18K8y.mjs";
4
- import { i as builtinWorkflowFile, n as pendingDecisions, r as builtinProblem, t as answerDecision } from "./decide-3l9q8UtX.mjs";
5
- import { n as newRunId, t as findRun } from "./find-DEjjOlTG.mjs";
6
- import { n as listOrkWorktrees, t as createdWorktrees } from "./list-Qxr6rwsY.mjs";
7
- import { a as planClean, t as applyClean } from "./clean-DVaEH9e7.mjs";
2
+ import { $ as DETACHED_ENV, $t as endOf, An as linkTools, Ar as AGENT_DRIVERS, At as nestItems, Bn as placeFiles, C as openStore, Cn as wakeRun, Ct as runStatus, Dn as planAgentMigration, Dr as readyDriver, E as inspectProject, En as maskSecrets, Er as driverChecks, Et as templateOf, Fn as catalogAgent, Gt as PROJECT_FILE, H as tellNode, Hn as projectsHome, I as createCalls, In as loadAgents, J as tellLiveRun, Jt as machineRulesOf, Kn as inside, Kt as gitRulesOf, L as addressOf, Ln as hasInRepoFiles, M as parseInputPairs, Mn as toolsInUse, N as resolveInputs, Nn as createAgent, On as applySteps, Or as AGENT_TOOL_LAYOUTS, Pn as deleteAgent, Q as listKey, Qt as createSignals, R as endLink, Rn as inRepoPlace, S as isNodeEvent, Tn as maskReplacer, Un as runDirOf, V as peerOf, Vn as placeFor, W as flattenTree, Wn as shownPath, X as detachResume, Y as adoptedCalls, Z as resumable, _t as startRun, a as releaseNotes, an as FEEDBACK_SEVERITIES, bn as serveRun, br as retiredDriverHint, cn as planPost, cr as serializeWorkflow, dr as WorkflowSchema, dt as sessionDriver, en as outcomeEnd, et as detachRun, fn as createFeedback, gr as isClaudeNode, gt as walkSummary, i as planUpgrade, in as FEEDBACK_KINDS, ir as beforeOf, j as clearGitLocalEnv, jn as toolLinks, kn as planMoveInto, kr as ORK_LAYOUT, lt as planNodeSession, mn as normalizeTitle, mr as settingsOf, n as resolveBase, nn as END_KINDS, nt as NOT_STARTED, o as runningRunIds, on as FEEDBACK_TARGETS, q as requestStop, qn as projectRootOf, qt as loadProject, s as runningScript, t as createWorkspace, tt as followEvents, u as isNewer, un as sendPost, wt as runTree, x as activeTree, xn as socketPath, xt as liveStatus, yt as userMessageText, z as endLinksOf, zn as newProjectId, zt as slotWaitLine } from "./src-D6_xjm-x.mjs";
3
+ import { C as feedbackActions, S as placeOf, _ as previewPost, b as acceptItem, f as readUpdateCache, g as nodeSession, h as writeUpdateCache, i as registryOf, l as fetchLatest, m as updateCachePath, n as ORK_VERSION, s as runInstall, t as ORK_NAME, v as checkNote, w as openFixRun, y as recordNote } from "./__build_info__-CIchF6nr.mjs";
4
+ import { i as builtinWorkflowFile, n as pendingDecisions, r as builtinProblem, t as answerDecision } from "./decide-B6PnKeqo.mjs";
5
+ import { n as newRunId, t as findRun } from "./find-BicBbGMg.mjs";
6
+ import { n as listOrkWorktrees, t as createdWorktrees } from "./list-CEaJ07jd.mjs";
7
+ import { a as planClean, t as applyClean } from "./clean-Di-KT7Mf.mjs";
8
8
  import { c as subcommandsOf, i as argsOf, n as groupedEntries, o as publicArgs, s as resolved, t as groupOf } from "./help-groups-BwYoAWsh.mjs";
9
- import { a as write, i as style, n as cost, o as writeErr, t as confirm } from "./output-Bqljtmp_.mjs";
10
- import { _ as namedMcpEntries, a as builtinExecutors, d as maskedEnv, f as maskedNamesOfRun, g as clearOutputFiles, h as sessionEnv, i as onPath, l as clearStateFiles, m as runScrubber, n as missingProgramMessage, r as missingPrograms, t as envProblems, v as resultOf } from "./src-cmmXKHn1.mjs";
11
- import { c as registeredProjects, i as refusedBuiltin, n as WORKFLOW_SUFFIX, o as workflowArg, s as projectAt, t as WORKFLOW_DIR } from "./workflows-BT1gDRIV.mjs";
12
- import { C as startUpdateCheck, S as showUpdateNotice, _ as runChecks, a as validate, c as warningLines, d as runReadyDriver, f as unfitLines, g as orkCheck, h as headWarning, i as loadWorkflow, l as checksHere, m as experimentsCheck, o as validateFile, p as exec, r as formatReport, s as walkLoaded, t as context, u as chooseReadyDriver, v as socketCheck, w as updateCheckOff, x as runUpdateChild, y as triggerChecks } from "./context-CEaKOVmG.mjs";
13
- import { a as nodeVerdict, i as hasConflict, n as formatRuns, o as firstLine, r as formatTree, s as formatBusEvent, t as ago } from "./format-Cb5_m-Qd.mjs";
14
- import { i as runLimits, r as limitsRefusal, t as chatNotice } from "./limits-BSwEjQST.mjs";
15
- import { i as decideRefusal, n as chatRefusal } from "./in-a-run-BS9Nc-JW.mjs";
9
+ import { C as widthOf, S as hang, T as readKeys, _ as canKey, a as write, b as termOf, d as bold, f as paint, g as withdrawn, h as readLine, i as style, m as interrupt, n as cost, o as writeErr, p as createRegion, r as plain, s as confirmOrStop, t as confirm, u as apply$1, v as plainEnv, w as wrap, x as cut, y as takePlainFlag } from "./output-CM95njYt.mjs";
10
+ import { n as chip, r as nodeChip, t as CHIP_LABELS } from "./chips-BA7SrrH5.mjs";
11
+ import { _ as namedMcpEntries, a as builtinExecutors, d as maskedEnv, f as maskedNamesOfRun, g as clearOutputFiles, h as sessionEnv, i as onPath, l as clearStateFiles, m as runScrubber, n as missingProgramMessage, r as missingPrograms, t as envProblems, v as resultOf } from "./src-9iGkjnsy.mjs";
12
+ import { c as registeredProjects, i as refusedBuiltin, n as WORKFLOW_SUFFIX, o as workflowArg, s as projectAt, t as WORKFLOW_DIR } from "./workflows-DZvW3i1e.mjs";
13
+ import { C as startUpdateCheck, S as showUpdateNotice, _ as runChecks, a as validateFile, c as warningLines, d as runReadyDriver, f as unfitLines, g as orkCheck, h as headWarning, i as validate, l as checksHere, m as experimentsCheck, o as walkLoaded, p as exec, r as loadWorkflow, s as formatReport, t as context, u as chooseReadyDriver, v as socketCheck, w as updateCheckOff, x as runUpdateChild, y as triggerChecks } from "./context-BaNshWr9.mjs";
14
+ import { c as nodeVerdict, d as createView, f as styler, i as listedStatus, l as firstLine, n as formatRuns, r as formatTree, s as hasConflict, t as ago, u as formatBusEvent } from "./format-lWhN-jYR.mjs";
15
+ import { i as runLimits, r as limitsRefusal, t as chatNotice } from "./limits-jZQp9ZJC.mjs";
16
+ import { i as decideRefusal, n as chatRefusal } from "./in-a-run-CgkVouyG.mjs";
16
17
  import path from "node:path";
17
18
  import { stringify } from "yaml";
18
19
  import { z } from "zod";
19
- import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, writeFileSync } from "node:fs";
20
- import { createHash } from "node:crypto";
20
+ import { appendFileSync, existsSync, linkSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
21
+ import { createHash, randomUUID } from "node:crypto";
21
22
  import { homedir } from "node:os";
22
23
  import { setTimeout } from "node:timers/promises";
23
24
  import { defineCommand, renderUsage, runMain } from "citty";
@@ -306,165 +307,6 @@ const startControl = async (options) => {
306
307
  }
307
308
  };
308
309
  };
309
- // ../core/src/feedback/signals.ts
310
- const ORK_TOOL = "mcp__ork__";
311
- /**
312
- * An observer for one run's events: give it each event, and record what it
313
- * returns, in order (the event, its origin filled in when it is feedback, then
314
- * any feedback it signals).
315
- */
316
- const createSignals = (options = {}) => {
317
- let workflow;
318
- const nodeTypes = /* @__PURE__ */ new Map();
319
- const sessions = /* @__PURE__ */ new Map();
320
- const tools = /* @__PURE__ */ new Map();
321
- const asked = /* @__PURE__ */ new Map();
322
- const seen = /* @__PURE__ */ new Set();
323
- const originOf = (node) => {
324
- const origin = {};
325
- if (workflow !== void 0) origin.workflow = workflow;
326
- if (options.workflowHash !== void 0) origin.workflowHash = options.workflowHash;
327
- const type = node === void 0 ? void 0 : nodeTypes.get(node);
328
- if (type !== void 0) origin.nodeType = type;
329
- const session = node === void 0 ? void 0 : sessions.get(node);
330
- if (session !== void 0) origin.model = session.model;
331
- if (session?.driver !== void 0) origin.driver = session.driver;
332
- if (options.version !== void 0) origin.version = options.version;
333
- return origin;
334
- };
335
- const record = (node, signal) => {
336
- const print = fingerprint(signal);
337
- if (seen.has(print)) return [];
338
- seen.add(print);
339
- return [{
340
- type: "feedback.recorded",
341
- ...node === void 0 ? {} : { node },
342
- feedback: {
343
- source: "system",
344
- kind: signal.kind,
345
- target: signal.target,
346
- severity: signal.severity,
347
- title: signal.title,
348
- body: signal.body,
349
- evidence: signal.evidence ?? [],
350
- code: signal.code,
351
- fingerprint: print,
352
- origin: originOf(node)
353
- }
354
- }];
355
- };
356
- const answered = (question, answer) => {
357
- if (question.kind === "permission") {
358
- if (answer !== "deny") return null;
359
- const tool = question.question.replace(/^Allow /u, "").replace(/\?$/u, "");
360
- return [question.node, {
361
- code: "permission.denied",
362
- kind: "friction",
363
- target: "workflow",
364
- severity: "low",
365
- title: `${question.node} was denied ${tool}`,
366
- body: "Allow it in the node's `tools.allow` if it needs it, or tell it in its prompt not to use it.",
367
- evidence: [tool]
368
- }];
369
- }
370
- if (question.recommended === void 0 || answer === question.recommended) return null;
371
- return [question.node, {
372
- code: "recommendation.overridden",
373
- kind: "context-gap",
374
- target: "workflow",
375
- severity: "low",
376
- title: `The answer to ${question.node}'s question was not the one it recommended`,
377
- body: `Asked: ${question.question} Recommended: ${question.recommended}. Answered: ${answer}.`
378
- }];
379
- };
380
- /** The feedback an event signals, if any, and the node it is about. */
381
- const signalOf = (event) => {
382
- if (event.type === "loop.exhausted") return [event.from, {
383
- code: "loop.exhausted",
384
- kind: "workflow",
385
- target: "workflow",
386
- severity: "medium",
387
- title: `The loop ${event.from} -> ${event.to} ran out of its ${String(event.maxRounds)} rounds`,
388
- body: "Raise `maxRounds`, or make what the loop's nodes are asked to fix clearer so it settles sooner."
389
- }];
390
- if (event.type === "run.finished" && event.result.status === "budget_exhausted") return [void 0, {
391
- code: "budget.exhausted",
392
- kind: "workflow",
393
- target: "workflow",
394
- severity: "medium",
395
- title: "The run's budget ran out",
396
- body: `The run stopped after spending $${event.result.costUsd.toFixed(4)}. Raise \`limits.budgetUsd\`, or give nodes cheaper models or smaller budgets.`
397
- }];
398
- if (event.type === "decision.answered") {
399
- const question = asked.get(event.decision);
400
- return question === void 0 ? null : answered(question, event.answer);
401
- }
402
- if (event.type === "node.event" && event.event.type === "tool.result") {
403
- const name = tools.get(`${event.id}\0${event.event.id}`);
404
- if (!event.event.isError || name?.startsWith(ORK_TOOL) !== true) return null;
405
- const tool = name.slice(10);
406
- return [event.id, {
407
- code: "tool.error",
408
- kind: "bug",
409
- target: "ork",
410
- severity: "medium",
411
- title: `Ork's ${tool} tool returned an error`,
412
- body: `A call to \`${tool}\` failed; the node's events show the call.`,
413
- evidence: [name]
414
- }];
415
- }
416
- if (event.type === "node.finished" && nodeTypes.get(event.id) === "human.gate" && event.output["answeredBy"] === "timeout") return [event.id, {
417
- code: "gate.timeout",
418
- kind: "workflow",
419
- target: "workflow",
420
- severity: "low",
421
- title: `The gate ${event.id} timed out and took its default`,
422
- body: "No one answered in time. Lengthen its `timeout`, or keep it if the default was right."
423
- }];
424
- return null;
425
- };
426
- /** Keeps what later events and origins need. */
427
- const observe = (event) => {
428
- if (event.type === "run.started") workflow = event.workflow;
429
- if (event.type === "node.started") nodeTypes.set(event.id, event.nodeType);
430
- if (event.type === "decision.opened") {
431
- const question = {
432
- node: event.node,
433
- kind: event.kind,
434
- question: event.question
435
- };
436
- if (event.recommended !== void 0) question.recommended = event.recommended;
437
- asked.set(event.decision, question);
438
- }
439
- if (event.type !== "node.event") return;
440
- const inner = event.event;
441
- if (inner.type === "session.started" && inner.fork !== true) sessions.set(event.id, {
442
- model: inner.model,
443
- ...inner.driver === void 0 ? {} : { driver: inner.driver }
444
- });
445
- if (inner.type === "tool.called") tools.set(`${event.id}\0${inner.id}`, inner.name);
446
- };
447
- return (event) => {
448
- observe(event);
449
- if (event.type === "feedback.recorded") {
450
- if (seen.has(event.feedback.fingerprint)) return [];
451
- seen.add(event.feedback.fingerprint);
452
- const origin = {
453
- ...originOf(event.node),
454
- ...event.feedback.origin
455
- };
456
- return [{
457
- ...event,
458
- feedback: {
459
- ...event.feedback,
460
- origin
461
- }
462
- }];
463
- }
464
- const signal = signalOf(event);
465
- return signal === null ? [event] : [event, ...record(...signal)];
466
- };
467
- };
468
310
  /** A title's words, as fingerprints compare them. */
469
311
  const titleWords = (title) => new Set(normalizeTitle(title).split(" ").filter((word) => word !== ""));
470
312
  /** The share of two titles' words they have in common (Jaccard). */
@@ -506,6 +348,140 @@ const proposeMerges = (items) => {
506
348
  };
507
349
  }).filter((merge) => merge !== void 0).toSorted((a, b) => firstSeen(a.into, b.into));
508
350
  };
351
+ /** `ORK_HEAVY_SLOTS` as a count; anything but a positive integer is the default. */
352
+ const heavySlots = (env = process.env) => {
353
+ const count = Number(env["ORK_HEAVY_SLOTS"]);
354
+ return Number.isInteger(count) && count > 0 ? count : 2;
355
+ };
356
+ const codeOf = (error) => error instanceof Error && "code" in error ? error.code : void 0;
357
+ const alive = (pid) => {
358
+ if (pid <= 0) return false;
359
+ try {
360
+ process.kill(pid, 0);
361
+ return true;
362
+ } catch (error) {
363
+ return codeOf(error) === "EPERM";
364
+ }
365
+ };
366
+ const read = (file) => {
367
+ try {
368
+ const value = JSON.parse(readFileSync(file, "utf8"));
369
+ if (typeof value !== "object" || value === null || !("id" in value && "run" in value && "node" in value && "pid" in value)) return;
370
+ const { id, run, node, pid } = value;
371
+ return typeof id === "string" && typeof run === "string" && typeof node === "string" && typeof pid === "number" ? {
372
+ id,
373
+ run,
374
+ node,
375
+ pid
376
+ } : void 0;
377
+ } catch {
378
+ return;
379
+ }
380
+ };
381
+ const listed = (dir) => {
382
+ try {
383
+ return readdirSync(dir).filter((name) => name.endsWith(".json")).toSorted();
384
+ } catch {
385
+ return [];
386
+ }
387
+ };
388
+ /** Written whole (a rename), so no reader sees half a record. */
389
+ const writeWhole = (file, record) => {
390
+ const draft = `${file}.${record.id}.tmp`;
391
+ writeFileSync(draft, JSON.stringify(record), { mode: 384 });
392
+ renameSync(draft, file);
393
+ };
394
+ /** Deletes a record if it is still `id`'s. */
395
+ const dropIf = (file, id) => {
396
+ if (read(file)?.id === id) rmSync(file, { force: true });
397
+ };
398
+ let tickets = 0;
399
+ /** A ticket's name: sorted by when it was queued. */
400
+ const ticketName = (id) => `${String(Date.now()).padStart(15, "0")}-${String(process.pid).padStart(8, "0")}-${String(tickets += 1).padStart(6, "0")}-${id}.json`;
401
+ /** The ledger in `dir`, with `slots` slots. */
402
+ const fileSlotLedger = (options) => {
403
+ const { dir, slots } = options;
404
+ const queue = path.join(dir, "queue");
405
+ const slotFile = (index) => path.join(dir, `slot-${String(index)}.json`);
406
+ const records = () => Array.from({ length: slots }, (_, index) => {
407
+ const file = slotFile(index);
408
+ return {
409
+ file,
410
+ held: read(file)
411
+ };
412
+ });
413
+ const holders = () => records().flatMap(({ held }) => held === void 0 ? [] : [held]);
414
+ const sweep = () => {
415
+ const gone = [];
416
+ for (const file of [...records().map((record) => record.file), ...listed(queue).map((name) => path.join(queue, name))]) {
417
+ const held = read(file);
418
+ if (held === void 0 || alive(held.pid)) continue;
419
+ dropIf(file, held.id);
420
+ gone.push(held);
421
+ }
422
+ return gone;
423
+ };
424
+ /** Links `draft` into a free slot; true when it got one. */
425
+ const tryTake = (draft) => {
426
+ for (const { file, held } of records()) {
427
+ if (held !== void 0) continue;
428
+ try {
429
+ linkSync(draft, file);
430
+ return true;
431
+ } catch (error) {
432
+ if (codeOf(error) !== "EEXIST") throw error;
433
+ }
434
+ }
435
+ return false;
436
+ };
437
+ const take = async (holder, { signal, onWait }) => {
438
+ mkdirSync(queue, {
439
+ recursive: true,
440
+ mode: 448
441
+ });
442
+ const record = {
443
+ id: randomUUID(),
444
+ ...holder
445
+ };
446
+ const ticket = path.join(queue, ticketName(record.id));
447
+ writeWhole(ticket, record);
448
+ let told = false;
449
+ try {
450
+ for (;;) {
451
+ if (signal.aborted) return null;
452
+ sweep();
453
+ const free = records().filter(({ held }) => held === void 0).length;
454
+ if (listed(queue).indexOf(path.basename(ticket)) < free && tryTake(ticket)) {
455
+ const release = () => {
456
+ for (const { file, held } of records()) if (held?.id === record.id) dropIf(file, record.id);
457
+ };
458
+ return release;
459
+ }
460
+ if (!told) {
461
+ told = true;
462
+ onWait?.(holders());
463
+ }
464
+ try {
465
+ await setTimeout(options.pollMs ?? 500, void 0, { signal });
466
+ } catch {
467
+ return null;
468
+ }
469
+ }
470
+ } finally {
471
+ rmSync(ticket, { force: true });
472
+ }
473
+ };
474
+ return {
475
+ take,
476
+ holders,
477
+ sweep
478
+ };
479
+ };
480
+ /** The machine's ledger: `~/.ork/slots`, `ORK_HEAVY_SLOTS` slots. */
481
+ const machineSlotLedger = (options = {}) => fileSlotLedger({
482
+ dir: path.join(options.home ?? homedir(), ".ork", "slots"),
483
+ slots: heavySlots(options.env)
484
+ });
509
485
  // ../core/src/remote/relay.ts
510
486
  /**
511
487
  * @file A run's side of the relay (RFC-003): its nodes' links, read from the
@@ -716,6 +692,11 @@ const createRecorder = (store, run, runDir) => {
716
692
  // src/help-root.ts
717
693
  /** Where the user docs start. */
718
694
  const DOCS_URL = "https://github.com/mimshins/ork/blob/main/docs/README.md";
695
+ /** The flags every command takes, which `bin.ts` handles before citty. */
696
+ const GLOBAL_FLAGS = [{
697
+ name: "--plain",
698
+ description: "Plain output: no color, no redrawn prompts (also ORK_PLAIN=1, NO_COLOR)"
699
+ }];
719
700
  const rowOf = (entry) => {
720
701
  const short = groupOf(entry.name)?.short ?? entry.description;
721
702
  const alias = entry.aliases.length === 0 ? "" : ` (alias: ${entry.aliases.join(", ")})`;
@@ -743,6 +724,9 @@ const renderRootHelp = async (cmd) => {
743
724
  ...groups.flatMap((group) => groupLines(group, width)),
744
725
  `Usage: ${name} <command> [flags]`,
745
726
  "",
727
+ "Flags for every command:",
728
+ ...GLOBAL_FLAGS.map((f) => ` ${f.name} ${f.description}`),
729
+ "",
746
730
  `Use "${name} <command> --help" for more about a command.`,
747
731
  `${name} v${meta.version ?? ""}`
748
732
  ].join("\n");
@@ -766,27 +750,408 @@ const renderHelp = async (cmd, parent) => {
766
750
  if (cmd.subCommands !== void 0 && parent === void 0) return renderRootHelp(shown);
767
751
  const above = parent?.meta === void 0 ? void 0 : { meta: parent.meta };
768
752
  const usage = await renderUsage(shown, above);
769
- if (cmd.subCommands === void 0) return usage;
753
+ const globals = `GLOBAL OPTIONS\n\n${GLOBAL_FLAGS.map((f) => ` ${f.name} ${f.description}`).join("\n")}`;
754
+ if (cmd.subCommands === void 0) return `${usage}\n\n${globals}`;
770
755
  const choices = await choicesOf(shown);
771
- return usage.replace(` [OPTIONS] ${choices}`, ` <command> [flags]`).replace(` ${choices}`, ` <command> [flags]`);
756
+ return `${usage.replace(` [OPTIONS] ${choices}`, ` <command> [flags]`).replace(` ${choices}`, ` <command> [flags]`)}\n\n${globals}`;
772
757
  };
773
758
  /** Prints {@link renderHelp}'s usage, as citty prints its own. */
774
759
  const showHelp = async (cmd, parent) => {
775
760
  process.stdout.write(`${await renderHelp(cmd, parent)}\n\n`);
776
761
  };
762
+ // src/human-card.ts
763
+ /** Lines of context a card shows before `d` (or `--verbose`) shows the rest. */
764
+ const CONTEXT_LINES = 6;
765
+ /** Text from outside with its escapes gone and its line breaks kept. */
766
+ const clean = (text) => text.split("\n").map((line) => plain(line)).join("\n");
767
+ /** The card above the options, in the rich look or the plain lines. */
768
+ const cardLines = (t, q, place, verbose, document) => {
769
+ const context = q.context === void 0 ? void 0 : clean(q.context);
770
+ if (!t.rich) return {
771
+ lines: [
772
+ ...place === void 0 ? [] : [`run ${place.run} · decision ${place.decision}`],
773
+ ...document === void 0 ? [] : [document],
774
+ `? ${plain(q.nodeId)} asks: ${clean(q.question)}`,
775
+ ...context === void 0 ? [] : [` ${context.replaceAll("\n", "\n ")}`]
776
+ ],
777
+ moreContext: false
778
+ };
779
+ const shown = context === void 0 ? [] : wrap(context, t.width - 3);
780
+ const cutShort = !verbose && shown.length > CONTEXT_LINES;
781
+ const body = cutShort ? shown.slice(0, CONTEXT_LINES) : shown;
782
+ const where = place === void 0 ? "" : ` ${paint(t, "muted", `run ${place.run} · ${String(place.index)} of ${String(place.total)}`)}`;
783
+ return {
784
+ lines: [
785
+ "",
786
+ ` ${chip(t, CHIP_LABELS.ask, "accent")} ${nodeChip(t, plain(q.nodeId))}${where}`,
787
+ ...wrap(clean(q.question), t.width - 1).map((l) => ` ${bold(t, l)}`),
788
+ ...body.map((l) => paint(t, "muted", ` ${l}`)),
789
+ ...cutShort ? [paint(t, "muted", ` … ${String(shown.length - CONTEXT_LINES)} more lines (d)`)] : []
790
+ ],
791
+ moreContext: cutShort
792
+ };
793
+ };
794
+ /** The line above a permission prompt: who wants to run what. */
795
+ const permitLine = (t, nodeId, preview) => {
796
+ const node = plain(nodeId);
797
+ const what = plain(preview);
798
+ if (!t.rich) return `? ${node} wants to run ${what}`;
799
+ const room = Math.max(20, t.width - node.length - 24);
800
+ return ` ${chip(t, CHIP_LABELS.ask, "accent")} ${nodeChip(t, node)} wants to run ${cut(what, room)}`;
801
+ };
802
+ /**
803
+ * The line a card collapses to once answered: `✓ 509da722-1 · add-invalid-node`
804
+ * (the decision, or the node when there is none, and the choice), or a note
805
+ * that it was left for later.
806
+ */
807
+ const doneLine = (t, place, nodeId, answer) => {
808
+ const who = place?.decision ?? plain(nodeId);
809
+ if (answer.kind === "later") return ` ${paint(t, "muted", "·")} ${cut(`${who} left for later`, t.width - 3)}`;
810
+ const said = answer.note === void 0 ? "" : ` · said: ${plain(answer.note)}`;
811
+ return ` ${paint(t, "ok", "✓")} ${cut(`${who} · ${answer.id}${said}`, t.width - 3)}`;
812
+ };
813
+ // src/term/text.ts
814
+ /**
815
+ * @file The text prompt: one line of text, edited in place on a terminal
816
+ * (arrows, Home/End, Backspace, Delete, Ctrl-U to clear), collapsing to one
817
+ * line when answered; Enter sends, Esc (or Ctrl-D on an empty line) leaves
818
+ * it. Plain mode asks `message: ` and reads a line, as the prompts always
819
+ * did. `editLine` is the editing itself, which the select prompt reuses for
820
+ * its note.
821
+ */
822
+ const at = (next, to) => ({
823
+ value: next.join(""),
824
+ pos: to
825
+ });
826
+ /** `state` after `key`; keys that do not edit leave it as it is. */
827
+ const editLine = (state, key) => {
828
+ const { value, pos } = state;
829
+ const chars = Array.from(value);
830
+ if (key.name === "char") return at(chars.toSpliced(pos, 0, key.text), pos + 1);
831
+ if (key.name === "backspace") return pos === 0 ? state : at(chars.toSpliced(pos - 1, 1), pos - 1);
832
+ if (key.name === "delete") return at(chars.toSpliced(pos, 1), pos);
833
+ if (key.name === "left") return {
834
+ value,
835
+ pos: Math.max(0, pos - 1)
836
+ };
837
+ if (key.name === "right") return {
838
+ value,
839
+ pos: Math.min(chars.length, pos + 1)
840
+ };
841
+ if (key.name === "home" || key.name === "ctrl-a") return {
842
+ value,
843
+ pos: 0
844
+ };
845
+ if (key.name === "end" || key.name === "ctrl-e") return {
846
+ value,
847
+ pos: chars.length
848
+ };
849
+ if (key.name === "ctrl-u") return at(chars.slice(pos), 0);
850
+ return state;
851
+ };
852
+ /**
853
+ * The line to draw for `state` after `lead`, within `width`, and the column of
854
+ * the cursor in it. A value too long for the room scrolls to keep the cursor in
855
+ * view.
856
+ */
857
+ const showLine = (lead, state, width, leadWidth) => {
858
+ const chars = Array.from(state.value);
859
+ const room = Math.max(1, width - leadWidth - 1);
860
+ const start = Math.max(0, state.pos - room + 1);
861
+ return {
862
+ line: `${lead}${chars.slice(start, start + room).join("")}`,
863
+ column: leadWidth + state.pos - start
864
+ };
865
+ };
866
+ const defaultDone$1 = (io, message) => (value) => {
867
+ const term = termOf(io.output, io.env);
868
+ const mark = paint(term, "ok", "✓");
869
+ return cut(` ${mark} ${message} · ${value === void 0 || value === "" ? "skipped" : value}`, term.width);
870
+ };
871
+ /** Asks for one line of text. */
872
+ const readText = async (io, options) => {
873
+ const { message, signal } = options;
874
+ if (signal?.aborted === true) throw withdrawn(signal);
875
+ if (!canKey(io.input, io.output, io.env)) {
876
+ const answer = await readLine(io, ` ${message}: `, signal);
877
+ return answer === void 0 ? { kind: "cancelled" } : {
878
+ kind: "text",
879
+ value: answer.trim()
880
+ };
881
+ }
882
+ const done = options.done ?? defaultDone$1(io, message);
883
+ const term = () => termOf(io.output, io.env);
884
+ const region = createRegion(io.output, term);
885
+ const initial = options.initial ?? "";
886
+ return new Promise((resolve, reject) => {
887
+ let state = {
888
+ value: initial,
889
+ pos: Array.from(initial).length
890
+ };
891
+ const lead = ` ${paint(term(), "accent", "?")} ${bold(term(), message)} › `;
892
+ const leadWidth = Array.from(` ? ${message} › `).length;
893
+ const render = () => {
894
+ const { line, column } = showLine(lead, state, term().width, leadWidth);
895
+ region.draw([line], column);
896
+ };
897
+ const finish = (result, line) => {
898
+ stop();
899
+ io.output.off("resize", render);
900
+ signal?.removeEventListener("abort", onAbort);
901
+ region.finish(line);
902
+ resolve(result);
903
+ };
904
+ const onAbort = () => {
905
+ stop();
906
+ io.output.off("resize", render);
907
+ region.finish();
908
+ reject(withdrawn(signal));
909
+ };
910
+ const stop = readKeys(io.input, (key) => {
911
+ if (key.name === "enter") {
912
+ const value = state.value.trim();
913
+ finish({
914
+ kind: "text",
915
+ value
916
+ }, done(value));
917
+ } else if (key.name === "escape" || key.name === "ctrl-d" && state.value === "") finish({ kind: "cancelled" }, done());
918
+ else if (key.name === "ctrl-c") {
919
+ finish({ kind: "interrupted" });
920
+ interrupt(io);
921
+ } else {
922
+ state = editLine(state, key);
923
+ render();
924
+ }
925
+ });
926
+ io.output.on("resize", render);
927
+ signal?.addEventListener("abort", onAbort, { once: true });
928
+ render();
929
+ });
930
+ };
931
+ // src/term/select.ts
932
+ /**
933
+ * @file The select prompt: a list of options moved with ↑/↓ (or `j`/`k`),
934
+ * Home/End and `1`–`9`, chosen with Enter. `n` adds a note (one line, edited
935
+ * in place), `d` shows or hides the details (a document), `q` or Esc leaves
936
+ * the question for later, Ctrl-C stops. It redraws only its own lines,
937
+ * follows a resize, and collapses to one line once answered. Plain mode
938
+ * prints the options as `1) id label - description` and asks `Choose [n]: `,
939
+ * as `ork decide` always did; there a note is asked for after the choice when
940
+ * the prompt takes one.
941
+ */
942
+ const NOTE_PROMPT = "Note for the run (optional, Enter to skip)";
943
+ /** The option an answer names: its number, or its id; empty is `recommended`. */
944
+ const pickOption = (answer, options, recommended) => {
945
+ const value = answer.trim();
946
+ if (value === "") return recommended;
947
+ const index = Number(value);
948
+ if (Number.isInteger(index) && index >= 1 && index <= options.length) return options[index - 1]?.id;
949
+ return options.find((o) => o.id === value)?.id;
950
+ };
951
+ const label = (option) => option.label === void 0 ? option.id : `${option.id} ${option.label}`;
952
+ const defaultDone = (options) => (result, term) => {
953
+ if (result.kind === "later") return ` ${paint(term, "muted", "·")} left for later`;
954
+ const option = options.find((o) => o.id === result.id);
955
+ return cut(` ${paint(term, "ok", "✓")} ${option === void 0 ? result.id : label(option)}`, term.width);
956
+ };
957
+ /** The numbered option lines of a plain prompt: ` 1) id label - description`. */
958
+ const plainOptions = (options, recommended) => options.map((option, i) => {
959
+ const description = option.description === void 0 ? "" : ` - ${option.description}`;
960
+ const mark = option.id === recommended ? " (recommended)" : "";
961
+ const name = option.label === void 0 ? "" : ` ${option.label}`;
962
+ return ` ${String(i + 1)}) ${option.id}${name}${description}${mark}`;
963
+ });
964
+ /** The plain prompt: numbered options, then `Choose [n]: ` until one fits. */
965
+ const selectPlain = (io, o) => {
966
+ const term = termOf(io.output, io.env);
967
+ const say = (text) => {
968
+ io.output.write(`${text}\n`);
969
+ };
970
+ for (const line of o.header?.(term) ?? []) say(line);
971
+ for (const line of plainOptions(o.options, o.recommended)) say(line);
972
+ for (const line of o.footer ?? []) say(line);
973
+ const at = o.options.findIndex((opt) => opt.id === o.recommended);
974
+ const hint = at === -1 ? "" : ` [${String(at + 1)}]`;
975
+ const ask = async () => {
976
+ const answer = await readLine(io, ` Choose${hint}: `, o.signal);
977
+ if (answer === void 0) throw new Error("input ended");
978
+ const id = pickOption(answer, o.options, o.recommended);
979
+ if (id === void 0) {
980
+ if (o.details !== void 0 && answer.trim() === "d") say(o.details);
981
+ else say(" Type an option's number or id.");
982
+ return ask();
983
+ }
984
+ if (o.note !== true) return {
985
+ kind: "chosen",
986
+ id
987
+ };
988
+ const noted = await readText(io, {
989
+ message: o.notePrompt ?? NOTE_PROMPT,
990
+ ...o.signal === void 0 ? {} : { signal: o.signal }
991
+ });
992
+ return noted.kind === "text" && noted.value !== "" ? {
993
+ kind: "chosen",
994
+ id,
995
+ note: noted.value
996
+ } : {
997
+ kind: "chosen",
998
+ id
999
+ };
1000
+ };
1001
+ return ask();
1002
+ };
1003
+ /** Asks for one of `options`. */
1004
+ const select = (io, o) => {
1005
+ if (o.signal?.aborted === true) return Promise.reject(withdrawn(o.signal));
1006
+ if (!canKey(io.input, io.output, io.env)) return selectPlain(io, o);
1007
+ const term = () => termOf(io.output, io.env);
1008
+ const region = createRegion(io.output, term);
1009
+ const done = o.done ?? defaultDone(o.options);
1010
+ return new Promise((resolve, reject) => {
1011
+ const start = o.options.findIndex((opt) => opt.id === o.recommended);
1012
+ let cursor = Math.max(0, start);
1013
+ let showDetails = false;
1014
+ let note;
1015
+ let editing;
1016
+ const optionLines = (t) => o.options.flatMap((option, i) => {
1017
+ const selected = i === cursor;
1018
+ const number = i < 9 ? `${String(i + 1)}.` : " ";
1019
+ const mark = option.id === o.recommended ? " (recommended)" : "";
1020
+ const head = cut(`${number} ${label(option)}`, t.width - 4 - mark.length);
1021
+ const text = selected ? apply$1(t, ["bold", "cyan"], head) : head;
1022
+ const lines = [` ${selected ? paint(t, "accent", "❯") : " "} ${text}${mark === "" ? "" : paint(t, "ok", mark)}`];
1023
+ for (const line of wrap(option.description ?? "", t.width - 7)) if (line !== "") lines.push(paint(t, "muted", ` ${line}`));
1024
+ return lines;
1025
+ });
1026
+ const keyLine = (t) => {
1027
+ const keys = editing === void 0 ? [
1028
+ "↑/↓ move",
1029
+ "Enter choose",
1030
+ ...o.note === true ? ["n note"] : [],
1031
+ ...o.details === void 0 ? [] : [`d ${showDetails ? "hide" : "show"} details`],
1032
+ ...o.later === true ? ["q later"] : []
1033
+ ] : ["Enter save", "Esc cancel"];
1034
+ return paint(t, "muted", cut(` ${keys.join(" · ")}`, t.width));
1035
+ };
1036
+ const render = () => {
1037
+ const t = term();
1038
+ const head = o.header?.(t) ?? [];
1039
+ const body = optionLines(t);
1040
+ const noteLine = note === void 0 ? [] : [paint(t, "muted", cut(` note: ${note}`, t.width))];
1041
+ const above = [
1042
+ ...head,
1043
+ ...body,
1044
+ ...noteLine
1045
+ ];
1046
+ const room = Math.max(3, t.rows - above.length - 3);
1047
+ const all = showDetails && o.details !== void 0 ? wrap(o.details.trimEnd(), t.width - 3) : [];
1048
+ const detail = (all.length > room ? [...all.slice(0, room - 1), `… ${String(all.length - room + 1)} more lines`] : all).map((line) => paint(t, "muted", ` ${line}`));
1049
+ const lines = [
1050
+ ...above,
1051
+ ...detail,
1052
+ keyLine(t)
1053
+ ];
1054
+ if (editing === void 0) {
1055
+ region.draw(lines);
1056
+ return;
1057
+ }
1058
+ const lead = ` ${paint(t, "accent", "✎")} Note › `;
1059
+ const { line, column } = showLine(lead, editing, t.width, 10);
1060
+ region.draw([...lines, line], column);
1061
+ };
1062
+ const finish = (result) => {
1063
+ stop();
1064
+ cleanup();
1065
+ region.finish(done(result, term()));
1066
+ resolve(result);
1067
+ };
1068
+ const cleanup = () => {
1069
+ io.output.off("resize", render);
1070
+ o.signal?.removeEventListener("abort", onAbort);
1071
+ };
1072
+ const onAbort = () => {
1073
+ stop();
1074
+ cleanup();
1075
+ region.finish();
1076
+ reject(withdrawn(o.signal));
1077
+ };
1078
+ const edit = (key) => {
1079
+ if (editing === void 0) return;
1080
+ if (key.name === "enter") {
1081
+ const value = editing.value.trim();
1082
+ note = value === "" ? void 0 : value;
1083
+ editing = void 0;
1084
+ } else if (key.name === "escape") editing = void 0;
1085
+ else editing = editLine(editing, key);
1086
+ render();
1087
+ };
1088
+ const move = (to) => {
1089
+ cursor = Math.min(o.options.length - 1, Math.max(0, to));
1090
+ render();
1091
+ };
1092
+ const stop = readKeys(io.input, (key) => {
1093
+ if (key.name === "ctrl-c") {
1094
+ stop();
1095
+ cleanup();
1096
+ region.finish();
1097
+ resolve({ kind: "interrupted" });
1098
+ interrupt(io);
1099
+ } else if (editing !== void 0) edit(key);
1100
+ else if (key.name === "enter") {
1101
+ const id = o.options[cursor]?.id;
1102
+ if (id === void 0) return;
1103
+ finish(note === void 0 ? {
1104
+ kind: "chosen",
1105
+ id
1106
+ } : {
1107
+ kind: "chosen",
1108
+ id,
1109
+ note
1110
+ });
1111
+ } else if (key.name === "up") move(cursor - 1);
1112
+ else if (key.name === "down") move(cursor + 1);
1113
+ else if (key.name === "home") move(0);
1114
+ else if (key.name === "end") move(o.options.length - 1);
1115
+ else if (key.name === "escape" && o.later === true) finish({ kind: "later" });
1116
+ else if (key.name === "char") {
1117
+ const c = key.text;
1118
+ if (c === "k") move(cursor - 1);
1119
+ else if (c === "j") move(cursor + 1);
1120
+ else if (c >= "1" && c <= "9") {
1121
+ if (Number(c) <= o.options.length) move(Number(c) - 1);
1122
+ } else if (c === "n" && o.note === true) {
1123
+ const value = note ?? "";
1124
+ editing = {
1125
+ value,
1126
+ pos: Array.from(value).length
1127
+ };
1128
+ render();
1129
+ } else if (c === "d" && o.details !== void 0) {
1130
+ showDetails = !showDetails;
1131
+ render();
1132
+ } else if (c === "q" && o.later === true) finish({ kind: "later" });
1133
+ }
1134
+ });
1135
+ io.output.on("resize", render);
1136
+ o.signal?.addEventListener("abort", onAbort, { once: true });
1137
+ render();
1138
+ });
1139
+ };
777
1140
  // src/human.ts
778
1141
  /**
779
1142
  * @file The terminal `HumanChannel`: answers `ask_user` questions, gates and
780
1143
  * permission prompts on the terminal, one at a time, and denies or defers
781
- * when no one is there. After a choice it offers one optional line for a
782
- * note, which Enter skips. In a run it is the decision queue's local
783
- * answerer, racing `ork decide`.
1144
+ * when no one is there. It draws through the terminal kit (`term/`): a card
1145
+ * with an ASK chip and a list moved with the arrow keys, a yes-or-no for a
1146
+ * permission, and one optional line for a note after a choice. Plain (not a
1147
+ * terminal, `NO_COLOR`, `--plain`) it keeps the numbered prompts. In a run it
1148
+ * is the decision queue's local answerer, racing `ork decide`.
784
1149
  */
785
1150
  const PREVIEW = 160;
786
1151
  /** A document longer than this is offered (`d`) rather than printed. */
787
1152
  const SHORT_DOCUMENT_LINES = 40;
788
1153
  const NO_ONE_TO_PERMIT = "No one is at the terminal to approve this tool, so it is not allowed in this run.";
789
- const NOTE_PROMPT = " Note for the run (optional, Enter to skip): ";
1154
+ const NOTE_MESSAGE = "Note for the run (optional, Enter to skip)";
790
1155
  const NO_ONE_TO_ASK = "no one (nobody is at the terminal; use your best judgment and say so in your report)";
791
1156
  /** One line that shows what a tool is about to do. */
792
1157
  const previewInput = (toolName, input) => {
@@ -794,20 +1159,12 @@ const previewInput = (toolName, input) => {
794
1159
  const line = (key === void 0 ? JSON.stringify(input) : `${key}: ${String(input[key])}`).replaceAll(/\s+/gu, " ").trim();
795
1160
  return line.length > PREVIEW ? `${toolName} ${line.slice(0, 159)}…` : `${toolName} ${line}`;
796
1161
  };
797
- /** The option an answer names: its number, or its id. */
798
- const pickOption = (answer, options, recommended) => {
799
- const value = answer.trim();
800
- if (value === "") return recommended;
801
- const index = Number(value);
802
- if (Number.isInteger(index) && index >= 1 && index <= options.length) return options[index - 1]?.id;
803
- return options.find((o) => o.id === value)?.id;
804
- };
805
- const optionLine = (option, index, recommended) => {
806
- const label = option.label === void 0 ? "" : ` ${option.label}`;
807
- const description = option.description === void 0 ? "" : style("dim", ` - ${option.description}`);
808
- const mark = option.id === recommended ? style("green", " (recommended)") : "";
809
- return ` ${String(index + 1)}) ${style("bold", option.id)}${label}${description}${mark}`;
810
- };
1162
+ /** Never answers, until `signal` withdraws it: what a stopped prompt leaves. */
1163
+ const parked = (signal) => new Promise((_, reject) => {
1164
+ signal?.addEventListener("abort", () => {
1165
+ reject(withdrawn(signal));
1166
+ });
1167
+ });
811
1168
  /**
812
1169
  * Answers permission prompts and `ask_user` questions on the terminal, one at a
813
1170
  * time. Without a terminal it denies tools and leaves questions to the node. A
@@ -815,90 +1172,257 @@ const optionLine = (option, index, recommended) => {
815
1172
  */
816
1173
  const createTerminalHuman = (io) => {
817
1174
  let rl;
1175
+ let closed;
818
1176
  let turn = Promise.resolve();
819
1177
  const say = (text) => {
820
1178
  io.output.write(`${text}\n`);
821
1179
  };
822
- const prompt = (text, signal) => {
1180
+ /** The one `readline` the plain prompts share; a rich prompt never opens it. */
1181
+ const readLine = (text, signal) => {
823
1182
  if (rl === void 0) {
824
- rl = createInterface({
1183
+ const opened = createInterface({
825
1184
  input: io.input,
826
- output: io.output
1185
+ output: io.output,
1186
+ ...plainEnv(io.env) ? { terminal: false } : {}
1187
+ });
1188
+ if (io.interrupt !== void 0) opened.on("SIGINT", io.interrupt);
1189
+ closed = new Promise((resolve) => {
1190
+ opened.once("close", () => {
1191
+ resolve(void 0);
1192
+ });
827
1193
  });
828
- if (io.interrupt !== void 0) rl.on("SIGINT", io.interrupt);
1194
+ rl = opened;
829
1195
  }
830
- return signal === void 0 ? rl.question(text) : rl.question(text, { signal });
1196
+ const answer = signal === void 0 ? rl.question(text) : rl.question(text, { signal });
1197
+ return closed === void 0 ? answer : Promise.race([answer, closed]);
1198
+ };
1199
+ const kit = {
1200
+ input: io.input,
1201
+ output: io.output,
1202
+ readLine,
1203
+ ...io.env === void 0 ? {} : { env: io.env },
1204
+ ...io.interrupt === void 0 ? {} : { interrupt: io.interrupt }
831
1205
  };
1206
+ const term = () => termOf(io.output, io.env);
832
1207
  /** The optional line after a choice, when the asker takes a note. */
833
1208
  const takeNote = async (onNote, signal) => {
834
1209
  if (onNote === void 0) return;
835
- const note = (await prompt(NOTE_PROMPT, signal)).trim();
836
- if (note !== "") onNote(note);
837
- };
838
- const choose = async (hint, options, recommended, signal, document) => {
839
- const answer = await prompt(` Choose${hint}: `, signal);
840
- const picked = pickOption(answer, options, recommended);
841
- if (picked !== void 0) return picked;
842
- if (document !== void 0 && answer.trim() === "d") say(document);
843
- else say(style("dim", " Type an option's number or id."));
844
- return choose(hint, options, recommended, signal, document);
1210
+ const noted = await readText(kit, {
1211
+ message: NOTE_MESSAGE,
1212
+ ...signal === void 0 ? {} : { signal }
1213
+ });
1214
+ if (noted.kind === "interrupted") await parked(signal);
1215
+ if (noted.kind === "text" && noted.value !== "") onNote(noted.value);
845
1216
  };
846
1217
  const oneAtATime = (work) => {
847
1218
  const result = turn.then(work);
848
1219
  turn = result.catch(() => null);
849
1220
  return result;
850
1221
  };
1222
+ const asking = (q, place) => oneAtATime(async () => {
1223
+ const { signal } = q;
1224
+ if (signal?.aborted === true) throw new Error("the question was withdrawn");
1225
+ const t = term();
1226
+ const documentLines = q.document?.trimEnd().split("\n").length ?? 0;
1227
+ const document = q.document === void 0 ? void 0 : clean(q.document);
1228
+ const printed = document !== void 0 && documentLines <= SHORT_DOCUMENT_LINES;
1229
+ const shownDocument = t.rich || !printed ? void 0 : document;
1230
+ const header = (tt) => cardLines(tt, q, place, io.verbose === true, shownDocument).lines;
1231
+ const options = q.options.map((o) => ({
1232
+ id: o.id,
1233
+ ...o.label === void 0 ? {} : { label: plain(o.label) },
1234
+ ...o.description === void 0 ? {} : { description: plain(o.description) }
1235
+ }));
1236
+ const { moreContext } = cardLines(t, q, place, io.verbose === true);
1237
+ 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;
1238
+ if (!io.interactive) {
1239
+ for (const line of header(t)) say(line);
1240
+ for (const line of plainOptions(options, q.recommended)) say(line);
1241
+ const answer = q.recommended ?? NO_ONE_TO_ASK;
1242
+ say(paint(t, "muted", ` no terminal; answered ${answer}`));
1243
+ return answer;
1244
+ }
1245
+ const result = await select(kit, {
1246
+ header,
1247
+ options,
1248
+ ...q.recommended === void 0 ? {} : { recommended: q.recommended },
1249
+ ...details === void 0 ? {} : { details },
1250
+ ...!t.rich && details !== void 0 ? { footer: [` Document: ${String(documentLines)} lines. Type d to read it.`] } : {},
1251
+ note: q.onNote !== void 0,
1252
+ notePrompt: NOTE_MESSAGE,
1253
+ later: place !== void 0,
1254
+ done: (answer, tt) => doneLine(tt, place, q.nodeId, answer),
1255
+ ...signal === void 0 ? {} : { signal }
1256
+ });
1257
+ if (result.kind === "interrupted") return parked(signal);
1258
+ if (result.kind === "later") return null;
1259
+ if (result.note !== void 0) q.onNote?.(result.note);
1260
+ return result.id;
1261
+ });
851
1262
  return {
852
1263
  permit: ({ nodeId, toolName, input, signal, onNote }) => oneAtATime(async () => {
853
1264
  if (signal?.aborted === true) throw new Error("the prompt was withdrawn");
854
- say(`${style(["yellow", "bold"], "?")} ${style("bold", nodeId)} wants to run ${previewInput(toolName, input)}`);
1265
+ const t = term();
1266
+ if (t.rich) say("");
1267
+ say(permitLine(t, nodeId, previewInput(toolName, input)));
855
1268
  if (!io.interactive) {
856
- say(style("dim", ` denied: ${NO_ONE_TO_PERMIT}`));
1269
+ say(paint(t, "muted", ` denied: ${NO_ONE_TO_PERMIT}`));
857
1270
  return {
858
1271
  allow: false,
859
1272
  message: NO_ONE_TO_PERMIT
860
1273
  };
861
1274
  }
862
- const answer = (await prompt(" Allow? [y/N] ", signal)).trim().toLowerCase();
863
- if (answer === "y" || answer === "yes") return { allow: true };
1275
+ const allowed = await confirmOrStop(kit, {
1276
+ question: "Allow?",
1277
+ ...signal === void 0 ? {} : { signal }
1278
+ });
1279
+ if (allowed === null) return parked(signal);
1280
+ if (allowed) return { allow: true };
864
1281
  await takeNote(onNote, signal);
865
1282
  return {
866
1283
  allow: false,
867
1284
  message: "The user did not allow this tool."
868
1285
  };
869
1286
  }),
870
- ask: ({ nodeId, question, options, recommended, context, document, signal, onNote }) => oneAtATime(async () => {
871
- if (signal?.aborted === true) throw new Error("the question was withdrawn");
872
- const documentLines = document?.trimEnd().split("\n").length ?? 0;
873
- if (document !== void 0 && documentLines <= SHORT_DOCUMENT_LINES) say(document);
874
- say(`${style(["yellow", "bold"], "?")} ${style("bold", nodeId)} asks: ${question}`);
875
- if (context !== void 0) say(style("dim", ` ${context.replaceAll("\n", "\n ")}`));
876
- for (const [index, option] of options.entries()) say(optionLine(option, index, recommended));
877
- if (!io.interactive) {
878
- const answer = recommended ?? NO_ONE_TO_ASK;
879
- say(style("dim", ` no terminal; answered ${answer}`));
880
- return answer;
881
- }
882
- const fallback = options.findIndex((o) => o.id === recommended);
883
- const hint = fallback === -1 ? "" : ` [${String(fallback + 1)}]`;
884
- if (documentLines > SHORT_DOCUMENT_LINES) say(` Document: ${String(documentLines)} lines. Type d to read it.`);
885
- const picked = await choose(hint, options, recommended, signal, document);
886
- await takeNote(onNote, signal);
887
- return picked;
888
- }),
1287
+ ask: async (q) => await asking(q) ?? NO_ONE_TO_ASK,
1288
+ askAt: (q, place) => asking(q, place),
889
1289
  close: () => {
890
1290
  rl?.close();
891
1291
  }
892
1292
  };
893
1293
  };
1294
+ // src/run/format-end.ts
1295
+ /**
1296
+ * @file How a node's end reads on the terminal (specs/runtime/session-ends.md):
1297
+ * its kind's label and Ork's own message, a note for a kind that may pass,
1298
+ * the setup hint for an account or provider end, and the scrubbed raw detail
1299
+ * only when asked for. Never the provider's own words first.
1300
+ */
1301
+ /** What a transient kind says in place of its hint on the log line. */
1302
+ const MAY_PASS = "(may pass; ork resume retries)";
1303
+ /** `Turn limit: reached its turn limit (8)`, with a note when it may pass. */
1304
+ const endHeadline = (end) => {
1305
+ const { label, transient } = END_KINDS[end.kind];
1306
+ return `${label}: ${end.message}${transient ? ` ${MAY_PASS}` : ""}`;
1307
+ };
1308
+ /**
1309
+ * The lines under a headline: the hint for an account or provider end (what to
1310
+ * set up) or whenever `full` (`--verbose`, one node's log), and the detail only
1311
+ * when `full`. A transient kind's hint is its retry wording, which the headline
1312
+ * already says, so it is not printed twice.
1313
+ */
1314
+ const endLines = (end, full, term = createView().term) => {
1315
+ const style = styler(term);
1316
+ const { actor, hint, transient } = END_KINDS[end.kind];
1317
+ const setup = actor === "account" || actor === "provider";
1318
+ const lines = [];
1319
+ if (hint !== "" && !transient && (full || setup)) lines.push(` ${style("dim", hint)}`);
1320
+ if (full && end.detail !== void 0) lines.push(style("dim", " details:"), ...end.detail.split("\n").map((line) => style("dim", ` ${line}`)));
1321
+ return lines;
1322
+ };
1323
+ /**
1324
+ * A finished node's line under `tag`: its verdict, or for a failure its kind
1325
+ * and message (and the lines under it). The agent's own summary stays for a
1326
+ * `fail` it reported, and an old run's text no rule reads stays as stored.
1327
+ */
1328
+ const formatFinished = (event, tag, verbose, term = createView().term, rest = tag) => {
1329
+ const style = styler(term);
1330
+ const conflict = hasConflict(event.output);
1331
+ const mark = event.ok ? style(conflict ? "yellow" : "green", conflict ? "!" : "✓") : style("red", "✗");
1332
+ const spent = event.usage === void 0 ? "" : style("dim", ` · ${cost(event.usage.costUsd)}`);
1333
+ const end = endOf(event);
1334
+ 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}`];
1335
+ return [`${tag} ${mark} ${endHeadline(end)}${spent}`, ...endLines(end, verbose, term).map((line) => `${rest} ${line}`)];
1336
+ };
1337
+ // src/run/format-outcome.ts
1338
+ /**
1339
+ * @file How a finished session reads on the terminal: its report or its end,
1340
+ * the branch, artifacts and fields, and the usage line.
1341
+ */
1342
+ const STATUS_MARK = {
1343
+ pass: {
1344
+ mark: "✓",
1345
+ color: "green"
1346
+ },
1347
+ done: {
1348
+ mark: "✓",
1349
+ color: "green"
1350
+ },
1351
+ changes_requested: {
1352
+ mark: "!",
1353
+ color: "yellow"
1354
+ },
1355
+ fail: {
1356
+ mark: "✗",
1357
+ color: "red"
1358
+ }
1359
+ };
1360
+ const seconds = (ms) => `${(ms / 1e3).toFixed(1)}s`;
1361
+ /** The usage line: cost, tokens, turns and time. */
1362
+ const formatUsage = ({ usage }, term = createView().term) => styler(term)("dim", [
1363
+ cost(usage.costUsd),
1364
+ `${String(usage.inputTokens)} in / ${String(usage.outputTokens)} out (${String(usage.cacheReadTokens)} cached)`,
1365
+ `${String(usage.turns)} turns`,
1366
+ seconds(usage.durationMs)
1367
+ ].join(" · "));
1368
+ /** How a finished session reads on the terminal. */
1369
+ const formatOutcome = (nodeId, outcome, verbose = false, term = createView().term) => {
1370
+ const style = styler(term);
1371
+ const { report } = outcome;
1372
+ if (report === null) {
1373
+ const end = outcomeEnd(outcome);
1374
+ return [
1375
+ `${style("red", "✗")} ${style("bold", nodeId)} ${endHeadline(end)}`,
1376
+ ...endLines(end, verbose, term),
1377
+ ` ${formatUsage(outcome, term)}`
1378
+ ];
1379
+ }
1380
+ const shown = STATUS_MARK[report.status];
1381
+ const lines = [`${shown === void 0 ? "-" : style(shown.color, shown.mark)} ${style("bold", nodeId)} ${report.status}: ${report.summary}`];
1382
+ if (report.branch !== void 0) lines.push(` branch: ${report.branch}`);
1383
+ for (const [name, file] of Object.entries(report.artifacts)) lines.push(` ${name}: ${file}`);
1384
+ for (const [name, value] of Object.entries(report.fields)) lines.push(` ${name}: ${JSON.stringify(value)}`);
1385
+ lines.push(` ${formatUsage(outcome, term)}`);
1386
+ return lines;
1387
+ };
894
1388
  // src/run/format-result.ts
895
- const RUN_MARK = {
896
- succeeded: style("green", "✓"),
897
- failed: style("red", "✗"),
898
- budget_exhausted: style("yellow", "!"),
899
- quota_exhausted: style("yellow", "!"),
900
- timed_out: style("yellow", "!"),
901
- interrupted: style("yellow", "!")
1389
+ /** How a run ended: its mark, and its chip's word and role when rich. */
1390
+ const RUN_END = {
1391
+ succeeded: {
1392
+ mark: "✓",
1393
+ ok: "ok",
1394
+ label: CHIP_LABELS.ok
1395
+ },
1396
+ failed: {
1397
+ mark: "✗",
1398
+ ok: "danger",
1399
+ label: CHIP_LABELS.fail
1400
+ },
1401
+ budget_exhausted: {
1402
+ mark: "!",
1403
+ ok: "warn",
1404
+ label: CHIP_LABELS.wait
1405
+ },
1406
+ quota_exhausted: {
1407
+ mark: "!",
1408
+ ok: "warn",
1409
+ label: CHIP_LABELS.wait
1410
+ },
1411
+ timed_out: {
1412
+ mark: "!",
1413
+ ok: "warn",
1414
+ label: CHIP_LABELS.wait
1415
+ },
1416
+ interrupted: {
1417
+ mark: "!",
1418
+ ok: "warn",
1419
+ label: CHIP_LABELS.wait
1420
+ }
1421
+ };
1422
+ const MARK_COLOR = {
1423
+ ok: "green",
1424
+ danger: "red",
1425
+ warn: "yellow"
902
1426
  };
903
1427
  /** `1 failure handled`, `2 failures handled`. */
904
1428
  const failuresHandled = (count) => `${String(count)} ${count === 1 ? "failure" : "failures"} handled`;
@@ -906,17 +1430,73 @@ const failuresHandled = (count) => `${String(count)} ${count === 1 ? "failure" :
906
1430
  * The run summary: status, cost, what ran, what was skipped and which failures
907
1431
  * an edge handled, branches to look at, why, and mail to nodes that never ran.
908
1432
  */
909
- const formatRunResult = (result) => {
1433
+ const formatRunResult = (result, term = createView().term) => {
1434
+ const style = styler(term);
1435
+ const end = RUN_END[result.status];
1436
+ const mark = term.rich ? chip(term, end.label, end.ok) : style(MARK_COLOR[end.ok], end.mark);
910
1437
  const ran = result.nodes.filter((n) => n.runs > 0).length;
911
1438
  const skipped = result.skipped ?? result.nodes.length - ran;
912
1439
  const handled = result.failuresHandled ?? 0;
913
- 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)}`}`)}`];
1440
+ const lines = ["", `${mark} run ${result.runId} ${result.status.replace("_", " ")} ${style("dim", `· ${cost(result.costUsd)} · ${String(ran)} ran, ${String(skipped)} skipped${handled === 0 ? "" : `, ${failuresHandled(handled)}`}`)}`];
914
1441
  for (const kept of result.worktrees) lines.push(` ${kept.id}: ${kept.branch ?? "detached"} ${style("dim", `at ${kept.path} (${kept.reason})`)}`);
915
1442
  for (const reason of result.reasons) lines.push(` ${style("red", reason)}`);
916
1443
  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`);
917
1444
  return lines;
918
1445
  };
1446
+ // src/run/format-tool.ts
1447
+ /**
1448
+ * @file How a session's own words and tool calls read in the transcript: the
1449
+ * agent's text (indented plain, wrapped rich), and a tool call as `Tool first
1450
+ * line` cut to the screen with `+N lines`, a path inside the node's worktree
1451
+ * relative to it, or whole with `--verbose`. Untrusted text goes through
1452
+ * `plain()` first.
1453
+ */
1454
+ const indent = (text) => text.trimEnd().split("\n").map((line) => ` ${plain(line)}`);
1455
+ /** Agent text under a 2-column indent, its long lines wrapped to `room`. */
1456
+ const wrapped = (text, room) => {
1457
+ const width = Math.max(1, room - 2);
1458
+ return text.trimEnd().split("\n").flatMap((line) => {
1459
+ const safe = plain(line).trimEnd();
1460
+ return widthOf(safe) <= width ? [safe] : wrap(safe, width);
1461
+ }).map((line) => ` ${line}`);
1462
+ };
1463
+ /** A tool's input as the call says it: the key that names its target, whole. */
1464
+ const toolText = (input, root) => {
1465
+ const key = TOOL_INPUT_KEYS.find((k) => typeof input[k] === "string");
1466
+ const text = key === void 0 ? JSON.stringify(input) : String(input[key]);
1467
+ if (root === void 0) return text;
1468
+ const base = root.replace(/\/+$/u, "");
1469
+ return text === base ? "." : text.replaceAll(`${base}/`, "");
1470
+ };
1471
+ /**
1472
+ * A tool call: rich, `Tool first line` cut to `room` with `+N lines` when it
1473
+ * had more; plain, today's one dim line. `verbose` shows the call whole.
1474
+ */
1475
+ const toolLines = (name, input, view, room, verbose, node) => {
1476
+ const { term } = view;
1477
+ const style = styler(term);
1478
+ if (!term.rich && !verbose) return [style("dim", ` · ${plain(previewInput(name, input))}`)];
1479
+ const label = plain(name);
1480
+ const [first = "", ...rest] = toolText(input, view.roots.get(node)).trimEnd().split("\n").map((line) => plain(line));
1481
+ const lead = term.rich ? " " : " · ";
1482
+ const sep = term.rich ? " " : " ";
1483
+ const headWidth = widthOf(lead) + widthOf(label) + sep.length;
1484
+ const styledHead = `${style("dim", lead)}${style("bold", label)}${style("dim", sep)}`;
1485
+ if (verbose) {
1486
+ const pad = " ".repeat(headWidth);
1487
+ return [`${styledHead}${style("dim", first)}`, ...rest.map((line) => style("dim", `${pad}${line}`))];
1488
+ }
1489
+ const more = rest.length === 0 ? "" : ` +${String(rest.length)} ${rest.length === 1 ? "line" : "lines"}`;
1490
+ return [`${styledHead}${style("dim", cut(first.trim(), Math.max(1, room - headWidth - more.length)))}${style("dim", more)}`];
1491
+ };
919
1492
  // src/run/format.ts
1493
+ /**
1494
+ * @file How runs read on the terminal: session events and run events with node
1495
+ * chips or prefixes; the node result is `format-outcome.ts`'s, a tool call
1496
+ * `format-tool.ts`'s and the run summary `format-result.ts`'s.
1497
+ */
1498
+ /** A line's 2-column indent, after any style codes that open it. */
1499
+ const INDENT = /^((?:\u001B\[[0-9;]*m)*) {2}/u;
920
1500
  /** Who answered a decision, as the log says it. */
921
1501
  const ANSWERED_BY = {
922
1502
  terminal: "terminal",
@@ -924,57 +1504,40 @@ const ANSWERED_BY = {
924
1504
  ui: "ork ui",
925
1505
  owner: "its owner"
926
1506
  };
927
- const STATUS_MARK = {
928
- pass: style("green", "✓"),
929
- done: style("green", "✓"),
930
- changes_requested: style("yellow", "!"),
931
- fail: style("red", "✗")
932
- };
933
- const seconds = (ms) => `${(ms / 1e3).toFixed(1)}s`;
934
- const indent = (text) => text.trimEnd().split("\n").map((line) => ` ${line}`);
935
- /** The usage line: cost, tokens, turns and time. */
936
- const formatUsage = ({ usage }) => style("dim", [
937
- cost(usage.costUsd),
938
- `${String(usage.inputTokens)} in / ${String(usage.outputTokens)} out (${String(usage.cacheReadTokens)} cached)`,
939
- `${String(usage.turns)} turns`,
940
- seconds(usage.durationMs)
941
- ].join(" · "));
942
- /** How a finished session reads on the terminal. */
943
- const formatOutcome = (nodeId, outcome) => {
944
- const { report } = outcome;
945
- if (report === null) return [
946
- `${style("red", "✗")} ${style("bold", nodeId)} ended without a report (${outcome.subtype})`,
947
- ...outcome.errors.map((e) => ` ${style("red", e)}`),
948
- ` ${formatUsage(outcome)}`
949
- ];
950
- const lines = [`${STATUS_MARK[report.status] ?? "-"} ${style("bold", nodeId)} ${report.status}: ${report.summary}`];
951
- if (report.branch !== void 0) lines.push(` branch: ${report.branch}`);
952
- for (const [name, file] of Object.entries(report.artifacts)) lines.push(` ${name}: ${file}`);
953
- for (const [name, value] of Object.entries(report.fields)) lines.push(` ${name}: ${JSON.stringify(value)}`);
954
- lines.push(` ${formatUsage(outcome)}`);
955
- return lines;
956
- };
957
1507
  /** The transcript lines for one event; tool names come from `tools`. */
958
- const formatEvent = (nodeId, event, tools) => {
1508
+ const formatEvent = (nodeId, event, tools, verbose = false, view = createView(), room = view.term.width - 1) => {
1509
+ const { term } = view;
1510
+ const style = styler(term);
959
1511
  if (event.type === "session.started") {
960
1512
  const fork = event.fork === true ? ", a fork to answer a question" : "";
961
1513
  return [style("dim", `▸ ${nodeId} (${event.model}, session ${event.sessionId}${fork})`)];
962
1514
  }
963
- if (event.type === "assistant.text") return indent(event.text);
1515
+ if (event.type === "assistant.text") return term.rich ? wrapped(event.text, room) : indent(event.text);
964
1516
  if (event.type === "session.waiting") return indent(event.reason);
965
1517
  if (event.type === "tool.called") {
966
1518
  tools.set(event.id, event.name);
967
- return [style("dim", ` · ${previewInput(event.name, event.input)}`)];
1519
+ return toolLines(event.name, event.input, view, room, verbose, nodeId);
968
1520
  }
969
1521
  if (event.type === "tool.result") return event.isError ? [style("red", ` ✗ ${tools.get(event.id) ?? "tool"} failed`)] : [];
970
1522
  if (event.type === "plan.limit") return event.status === "allowed" ? [] : [style("yellow", ` ! plan: ${event.window.replaceAll("_", " ")} limit ${event.status === "rejected" ? "reached" : "is close"}`)];
971
- return formatOutcome(nodeId, event.outcome);
1523
+ return formatOutcome(nodeId, event.outcome, verbose, term);
972
1524
  };
973
1525
  const items = (count) => `${String(count)} ${count === 1 ? "item" : "items"}`;
974
1526
  /** Transcript lines for one run event, prefixed with the node they are from. */
975
- const formatRunEvent = (event, width, tools, terminal = false) => {
976
- const tag = (id) => style("cyan", `${id.padEnd(width)} │`);
977
- 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"}`)]];
1527
+ const formatRunEvent = (event, width, tools, terminal = false, verbose = false, view = createView()) => {
1528
+ const { term } = view;
1529
+ const style = styler(term);
1530
+ const tag = (id) => term.rich ? `${nodeChip(term, id)}${" ".repeat(Math.max(0, width - id.length))}` : style("cyan", `${id.padEnd(width)} │`);
1531
+ const blank = (id) => term.rich ? " ".repeat(widthOf(tag(id))) : tag(id);
1532
+ const under = (id, lines) => lines.map((line, i) => `${i === 0 ? tag(id) : blank(id)} ${line}`);
1533
+ if (event.type === "run.started") {
1534
+ const starts = `starts at ${event.entries.join(", ")}`;
1535
+ return [
1536
+ term.rich ? `${chip(term, CHIP_LABELS.run, "accent")} run ${style("bold", event.runId)} of ${event.workflow} ${style("dim", starts)}` : style("dim", `▸ run ${event.runId} of ${event.workflow} (${starts})`),
1537
+ ...event.settings?.trigger === void 0 ? [] : [style("dim", ` started by trigger ${event.settings.trigger}; untrusted inputs: ${(event.settings.untrusted ?? []).join(", ") || "none"}`)],
1538
+ ...term.rich ? [""] : []
1539
+ ];
1540
+ }
978
1541
  if (event.type === "node.started") {
979
1542
  const round = event.round > 1 ? ` (round ${String(event.round)})` : "";
980
1543
  const owner = event.owner === void 0 ? "" : `, for ${event.owner}`;
@@ -988,14 +1551,10 @@ const formatRunEvent = (event, width, tools, terminal = false) => {
988
1551
  const why = inner.fork === true ? ", to answer a question" : "";
989
1552
  return [`${tag(event.id)} ${fork}${style("dim", `${inner.model}, session ${inner.sessionId}${why}`)}`];
990
1553
  }
991
- return formatEvent(event.id, event.event, tools).map((line) => `${tag(event.id)} ${fork}${line.replace(/^ {2}/u, "")}`);
992
- }
993
- if (event.type === "node.finished") {
994
- const conflict = hasConflict(event.output);
995
- const mark = event.ok ? style(conflict ? "yellow" : "green", conflict ? "!" : "✓") : style("red", "✗");
996
- const spent = event.usage === void 0 ? "" : style("dim", ` · ${cost(event.usage.costUsd)}`);
997
- return [`${tag(event.id)} ${mark} ${nodeVerdict(event.output, event.ok, event.error)}${spent}`];
1554
+ const room = term.width - widthOf(tag(event.id)) - 1 + 2 - (inner.fork === true ? 7 : 0);
1555
+ return under(event.id, formatEvent(event.id, event.event, tools, verbose, view, room).map((line) => `${fork}${line.replace(INDENT, "$1")}`));
998
1556
  }
1557
+ if (event.type === "node.finished") return formatFinished(event, tag(event.id), verbose, term, blank(event.id));
999
1558
  if (event.type === "node.skipped") return [`${tag(event.id)} ${style("dim", "- skipped")}`];
1000
1559
  if (event.type === "loop.exhausted") return [`${style(["yellow", "bold"], "!")} the loop ${event.from} -> ${event.to} used all ${String(event.maxRounds)} rounds`];
1001
1560
  if (event.type === "watch.started") return [`${tag(event.id)} ${style("dim", `watching until ${event.until}, every ${event.every}`)}`];
@@ -1003,6 +1562,7 @@ const formatRunEvent = (event, width, tools, terminal = false) => {
1003
1562
  if (event.type === "watch.ended") return [`${tag(event.id)} ${style("dim", `${event.outcome} after ${String(event.looks)} looks`)}`];
1004
1563
  if (event.type === "edge.taken") return [];
1005
1564
  if (event.type === "worktree") {
1565
+ if (event.action !== "removed") view.roots.set(event.id, event.path);
1006
1566
  const detail = event.detail === void 0 ? "" : ` (${event.detail})`;
1007
1567
  const what = event.action === "removed" && event.branch !== void 0 ? `worktree of ${event.branch} (branch kept)` : `${event.branch ?? event.path}${detail}`;
1008
1568
  const line = `${event.action} ${what}`;
@@ -1012,7 +1572,7 @@ const formatRunEvent = (event, width, tools, terminal = false) => {
1012
1572
  const what = event.kind === "permission" ? "asks to" : "asks:";
1013
1573
  const question = event.kind === "permission" ? event.question.replace(/^Allow /u, "run ").replace(/\?$/u, "") : event.question;
1014
1574
  const where = event.owner === void 0 ? `decision ${event.decision}; ${terminal ? "answer here or with ork decide" : "ork decide"}` : `decision ${event.decision}, via ${event.owner}`;
1015
- return [`${tag(event.node)} ${style(["yellow", "bold"], "?")} ${what} ${question} ${style("dim", `(${where})`)}`];
1575
+ return [`${tag(event.node)} ${term.rich ? chip(term, CHIP_LABELS.ask, "accent") : style(["yellow", "bold"], "?")} ${what} ${question} ${style("dim", `(${where})`)}`];
1016
1576
  }
1017
1577
  if (event.type === "decision.answered") {
1018
1578
  const by = event.by === "owner" && event.owner !== void 0 ? event.owner : ANSWERED_BY[event.by];
@@ -1029,6 +1589,8 @@ const formatRunEvent = (event, width, tools, terminal = false) => {
1029
1589
  return [`${tag(event.id)} ${style("yellow", `‖ paused: ${at}`)}`];
1030
1590
  }
1031
1591
  if (event.type === "node.released") return [`${tag(event.id)} ${style("dim", "▸ continues")}`];
1592
+ if (event.type === "slot.waiting") return [`${tag(event.id)} ${style("yellow", `‖ ${slotWaitLine(event.holders)}`)}`];
1593
+ if (event.type === "slot.taken") return [`${tag(event.id)} ${style("dim", "▸ got a check slot")}`];
1032
1594
  if (event.type === "each.listed") {
1033
1595
  const round = event.round > 1 ? ` (round ${String(event.round)})` : "";
1034
1596
  return [`${tag(event.node)} ${style("dim", `${items(event.count)}${round}`)}`];
@@ -1037,11 +1599,10 @@ const formatRunEvent = (event, width, tools, terminal = false) => {
1037
1599
  const over = `${items(event.count)}, over maxItems ${String(event.maxItems)}`;
1038
1600
  return [`${tag(event.node)} ${style("yellow", `! ${over}`)}`];
1039
1601
  }
1040
- if (event.type === "run.finished") return formatRunResult(event.result);
1602
+ if (event.type === "run.finished") return formatRunResult(event.result, term);
1041
1603
  if (event.type === "run.resumed") {
1042
1604
  const [one] = event.kept;
1043
- const kept = one === void 0 ? "every node runs again" : event.kept.length === 1 ? `${one} keeps its result` : `${event.kept.join(", ")} keep their results`;
1044
- return ["", style("dim", `▸ resumed: ${kept}`)];
1605
+ 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`}`)];
1045
1606
  }
1046
1607
  if (event.type === "feedback.recorded") {
1047
1608
  const { target, kind, title } = event.feedback;
@@ -1052,7 +1613,7 @@ const formatRunEvent = (event, width, tools, terminal = false) => {
1052
1613
  const note = event.delivered === "queued" ? " (for its next session)" : "";
1053
1614
  return [`${tag(event.node)} ${style(["magenta", "bold"], "you:")} ${firstLine(event.body)}${style("dim", note)}`];
1054
1615
  }
1055
- return formatBusEvent(event).map((line) => `${tag(event.node)} ${line}`);
1616
+ return under(event.node, formatBusEvent(event, term));
1056
1617
  };
1057
1618
  // src/runs/chat.ts
1058
1619
  /**
@@ -1151,13 +1712,14 @@ const resumeChat = async (options) => {
1151
1712
  /** Turns stored events into lines, keeping the state a transcript needs. */
1152
1713
  const createLogPrinter = (options) => {
1153
1714
  const tools = /* @__PURE__ */ new Map();
1715
+ const view = createView();
1154
1716
  let width = 8;
1155
1717
  return (stored) => {
1156
1718
  const { event } = stored;
1157
1719
  if (event.type === "run.started") width = Math.max(...event.nodes.map((n) => n.id.length), 1);
1158
1720
  if (options.node !== void 0 && !isNodeEvent(event, options.node)) return [];
1159
1721
  if (options.json) return [JSON.stringify(stored)];
1160
- return formatRunEvent(event, width, tools);
1722
+ return formatRunEvent(event, width, tools, false, options.verbose === true || options.node !== void 0, view);
1161
1723
  };
1162
1724
  };
1163
1725
  /**
@@ -1194,6 +1756,8 @@ const followRun = async (store, run, out, options) => {
1194
1756
  * session starts: the messages, steps and `forEach` it does not run. A node
1195
1757
  * with `forEach` (RFC-013) runs once with no item when it has `items`, and is
1196
1758
  * refused when it has `from`, as the node that reports its list is not run.
1759
+ * And, after it, its run summary: how the node ended
1760
+ * (specs/runtime/session-ends.md).
1197
1761
  */
1198
1762
  /** A warning line, as the run prints it. */
1199
1763
  const noteLines = (lines) => lines.map((line) => `${style(["yellow", "bold"], "warning")} ${line}`);
@@ -1213,6 +1777,32 @@ const oneNodeNotes = (workflow, id) => {
1213
1777
  ...steps.map((step) => `${id}'s ${step}: does not run in a run of one node.`)
1214
1778
  ] };
1215
1779
  };
1780
+ /** The `node.finished` fields that say how it ended: its error and its end. */
1781
+ const endedFields = (result) => ({
1782
+ ...result.error === void 0 ? {} : { error: result.error },
1783
+ end: endOf(result)
1784
+ });
1785
+ /** The one-node run's result, its reason the node's message. */
1786
+ const oneNodeResult = (runId, id, result, ok) => {
1787
+ const costUsd = result.usage?.costUsd ?? 0;
1788
+ return {
1789
+ runId,
1790
+ status: ok ? "succeeded" : "failed",
1791
+ costUsd,
1792
+ nodes: [{
1793
+ id,
1794
+ runs: 1,
1795
+ ok: result.ok,
1796
+ output: result.output,
1797
+ costUsd,
1798
+ ...endedFields(result)
1799
+ }],
1800
+ reasons: result.error === void 0 ? [] : [`${id}: ${result.error}`],
1801
+ worktrees: [],
1802
+ skipped: 0,
1803
+ failuresHandled: 0
1804
+ };
1805
+ };
1216
1806
  // src/run/origin.ts
1217
1807
  /**
1218
1808
  * @file Where a run's feedback comes from, beyond what its events say: Ork's
@@ -1326,10 +1916,11 @@ const runNode = async (options) => {
1326
1916
  ...options.home === void 0 ? {} : { home: options.home }
1327
1917
  });
1328
1918
  const signals = createSignals(feedbackOrigin(path.resolve(options.cwd, options.file)));
1919
+ const view = createView();
1329
1920
  const record = (raw) => {
1330
1921
  for (const event of signals(raw)) {
1331
1922
  control.publish(recorder(event));
1332
- if (event.type === "feedback.recorded" && !options.json) for (const line of formatRunEvent(event, id.length, /* @__PURE__ */ new Map())) options.out(line);
1923
+ if (event.type === "feedback.recorded" && !options.json) for (const line of formatRunEvent(event, id.length, /* @__PURE__ */ new Map(), false, options.verbose, view)) options.out(line);
1333
1924
  }
1334
1925
  };
1335
1926
  const nodeType = workflow.nodes[id]?.type ?? "claude.worker";
@@ -1374,10 +1965,9 @@ const runNode = async (options) => {
1374
1965
  });
1375
1966
  if (event.type === "session.finished") outcome = event.outcome;
1376
1967
  if (options.json) options.out(JSON.stringify(event));
1377
- else for (const line of formatEvent(options.nodeId, event, tools)) options.out(line);
1968
+ else for (const line of formatEvent(options.nodeId, event, tools, options.verbose, view)) options.out(line);
1378
1969
  }
1379
1970
  const result = resultOf(outcome);
1380
- const costUsd = result.usage?.costUsd ?? 0;
1381
1971
  record({
1382
1972
  type: "node.finished",
1383
1973
  id,
@@ -1385,27 +1975,11 @@ const runNode = async (options) => {
1385
1975
  ok: result.ok,
1386
1976
  output: result.output,
1387
1977
  ...result.usage === void 0 ? {} : { usage: result.usage },
1388
- ...result.error === void 0 ? {} : { error: result.error }
1978
+ ...endedFields(result)
1389
1979
  });
1390
1980
  record({
1391
1981
  type: "run.finished",
1392
- result: {
1393
- runId,
1394
- status: succeeded(outcome) ? "succeeded" : "failed",
1395
- costUsd,
1396
- nodes: [{
1397
- id,
1398
- runs: 1,
1399
- ok: result.ok,
1400
- output: result.output,
1401
- costUsd,
1402
- ...result.error === void 0 ? {} : { error: result.error }
1403
- }],
1404
- reasons: result.error === void 0 ? [] : [`${id}: ${result.error}`],
1405
- worktrees: [],
1406
- skipped: 0,
1407
- failuresHandled: 0
1408
- }
1982
+ result: oneNodeResult(runId, id, result, succeeded(outcome))
1409
1983
  });
1410
1984
  await control.close();
1411
1985
  return {
@@ -1693,6 +2267,7 @@ const runWorkflowFile = async (options) => {
1693
2267
  }, runDir) : appendRecorder(options.store, runId, runDir);
1694
2268
  const width = Math.max(...Object.keys(workflow.nodes).map((id) => id.length));
1695
2269
  const tools = /* @__PURE__ */ new Map();
2270
+ const view = createView();
1696
2271
  const started = Promise.withResolvers();
1697
2272
  const live = {};
1698
2273
  const control = await startControl({
@@ -1712,7 +2287,7 @@ const runWorkflowFile = async (options) => {
1712
2287
  for (const event of signals(raw)) {
1713
2288
  control.publish(record(event));
1714
2289
  if (options.json) options.out(JSON.stringify(event));
1715
- else for (const line of formatRunEvent(event, width, tools, options.human !== void 0)) options.out(line);
2290
+ else for (const line of formatRunEvent(event, width, tools, options.human !== void 0, options.verbose, view)) options.out(line);
1716
2291
  }
1717
2292
  };
1718
2293
  const workspace = root === void 0 ? void 0 : createWorkspace({
@@ -1779,6 +2354,10 @@ const runWorkflowFile = async (options) => {
1779
2354
  ...workspace === void 0 ? {} : { workspace },
1780
2355
  scrub: runScrubber(maskedNames, process.env),
1781
2356
  limits: ready.limits,
2357
+ machine: {
2358
+ rules: machineRulesOf(loadProject(place.root, placeFiles(place).rules).project),
2359
+ ledger: machineSlotLedger(options.home === void 0 ? {} : { home: options.home })
2360
+ },
1782
2361
  beforeFinish: takeLastCommands(control, started.promise)
1783
2362
  });
1784
2363
  live.handle = handle;
@@ -1813,7 +2392,7 @@ const isDriver = (value) => AGENT_DRIVERS.some((d) => d.id === value);
1813
2392
  /** Why `--driver` refuses a value: a retired id names its replacement. */
1814
2393
  const badDriver = (value) => `--${retiredDriverHint(value) ?? `driver must be one of ${AGENT_DRIVERS.map((d) => d.id).join(", ")}, got \`${value}\``}`;
1815
2394
  /** Loads the drivers only when a run starts, so other commands stay fast. */
1816
- const loadRuntime = async (driver, runs) => (await import("./src-oaJYIddp.mjs")).createRuntime(driver, sessionEnv(process.env), runs);
2395
+ const loadRuntime = async (driver, runs) => (await import("./src-BnZgZGrf.mjs")).createRuntime(driver, sessionEnv(process.env), runs);
1817
2396
  /**
1818
2397
  * Ctrl-C asks the run to stop, and so does a terminal closing or a polite kill,
1819
2398
  * so that the run ends tidily and leaves no result unscrubbed.
@@ -1886,6 +2465,11 @@ const run = defineCommand({
1886
2465
  description: "Print the events as JSON lines",
1887
2466
  default: false
1888
2467
  },
2468
+ verbose: {
2469
+ type: "boolean",
2470
+ description: "Show each tool call whole, and why each failed node stopped, with its scrubbed details",
2471
+ default: false
2472
+ },
1889
2473
  detach: {
1890
2474
  type: "boolean",
1891
2475
  description: "Run in the background; follow it with ork logs -f, answer with ork decide",
@@ -1967,6 +2551,7 @@ const run = defineCommand({
1967
2551
  inPlace: args["in-place"],
1968
2552
  ...args["machine-claude"] ? { machineClaude: true } : {},
1969
2553
  json: args.json,
2554
+ verbose: args.verbose,
1970
2555
  runtime: loadRuntime,
1971
2556
  store,
1972
2557
  ...human === void 0 ? {} : { human },
@@ -2012,22 +2597,25 @@ const run = defineCommand({
2012
2597
  * each message at its session's next step, and its replies stream here; a
2013
2598
  * node of a finished run is answered by resuming its last session.
2014
2599
  */
2015
- /** Asks for one line; `null` once the input ends (Ctrl-D). */
2016
- const askLine = async () => {
2017
- const rl = createInterface({
2600
+ /**
2601
+ * Asks for one line; `null` once the input ends (Ctrl-D), Esc leaves, or
2602
+ * `signal` withdraws it. A live chat prints replies while it waits, which a
2603
+ * redrawn prompt can't share the screen with, so it asks line by line.
2604
+ */
2605
+ const askLine = async (live = false, signal) => {
2606
+ const asked = await readText({
2018
2607
  input: process.stdin,
2019
- output: process.stdout
2020
- });
2021
- const closed = new Promise((resolve) => {
2022
- rl.once("close", () => {
2023
- resolve(null);
2024
- });
2025
- });
2026
- try {
2027
- return await Promise.race([rl.question(style("bold", "you › ")), closed]);
2028
- } finally {
2029
- rl.close();
2030
- }
2608
+ output: process.stdout,
2609
+ ...live ? { env: {
2610
+ ...process.env,
2611
+ ORK_PLAIN: "1"
2612
+ } } : {}
2613
+ }, {
2614
+ message: "you",
2615
+ done: (value) => ` you › ${value ?? ""}`,
2616
+ ...signal === void 0 ? {} : { signal }
2617
+ }).catch(() => ({ kind: "cancelled" }));
2618
+ return asked.kind === "text" ? asked.value : null;
2031
2619
  };
2032
2620
  /** Chats with a node of a live run until the input ends or the run does. */
2033
2621
  const chatLive = async (store, run, node, message) => {
@@ -2049,7 +2637,7 @@ const chatLive = async (store, run, node, message) => {
2049
2637
  abort.abort();
2050
2638
  });
2051
2639
  for (;;) {
2052
- const line = await Promise.race([askLine(), following.then(() => null)]);
2640
+ const line = await Promise.race([askLine(true, abort.signal), following.then(() => null)]);
2053
2641
  if (line === null || abort.signal.aborted) break;
2054
2642
  if (line.trim() !== "") await tellLiveRun(store, run, node, line);
2055
2643
  }
@@ -2145,6 +2733,27 @@ const chat = defineCommand({
2145
2733
  }
2146
2734
  }
2147
2735
  });
2736
+ // src/commands/decide-list.ts
2737
+ /**
2738
+ * One line per open decision, for `--list`: on a terminal an ASK chip and the
2739
+ * node's chip with the id and the question after, cut to the screen; plain, the
2740
+ * id, the node and the question.
2741
+ */
2742
+ const formatDecisions = (decisions, workflow, t = termOf(process.stdout)) => {
2743
+ if (decisions.length === 0) return [workflow === void 0 ? "No open decisions." : `No open decisions in workflow ${workflow}.`];
2744
+ const idWidth = Math.max(...decisions.map((d) => d.id.length));
2745
+ const nodeWidth = Math.max(...decisions.map((d) => plain(d.node).length));
2746
+ return decisions.map((d) => {
2747
+ const node = plain(d.node);
2748
+ const options = `[${d.options.map((o) => o.id === d.recommended ? `${o.id}*` : o.id).join(", ")}]`;
2749
+ const document = d.document === void 0 ? "" : " +document";
2750
+ const via = d.owner === void 0 ? "" : ` via ${plain(d.owner)}`;
2751
+ if (!t.rich) return `${d.id} ${node}${via} ${plain(d.question)}${document} ${options}`;
2752
+ const lead = `${chip(t, CHIP_LABELS.ask, "accent")} ${nodeChip(t, node)}${" ".repeat(nodeWidth - node.length)} ${bold(t, d.id.padEnd(idWidth))} `;
2753
+ const room = Math.max(20, t.width - (7 + nodeWidth + 2 + idWidth + 2));
2754
+ return `${lead}${cut(`${plain(d.question)}${document} ${options}${via}`, room)}`;
2755
+ });
2756
+ };
2148
2757
  // src/commands/decide.ts
2149
2758
  /**
2150
2759
  * @file `ork decide`: answer a live run's open decisions from any terminal (it
@@ -2190,8 +2799,6 @@ const saidLine = (store, id) => {
2190
2799
  const note = store.decision(id)?.note;
2191
2800
  return note === void 0 ? "" : style("dim", ` · said: ${note}`);
2192
2801
  };
2193
- /** One line per open decision, for `--list`. */
2194
- const formatDecisions = (decisions, workflow) => decisions.length === 0 ? [workflow === void 0 ? "No open decisions." : `No open decisions in workflow ${workflow}.`] : decisions.map((d) => `${style("bold", d.id)} ${style("cyan", d.node)}${d.owner === void 0 ? "" : style("dim", ` via ${d.owner}`)} ${d.question} ${d.document === void 0 ? "" : `${style("dim", "+document")} `}${style("dim", `[${d.options.map((o) => o.id === d.recommended ? `${o.id}*` : o.id).join(", ")}]`)}`);
2195
2802
  const decide = defineCommand({
2196
2803
  meta: {
2197
2804
  name: "decide",
@@ -2246,6 +2853,11 @@ const decide = defineCommand({
2246
2853
  rating: {
2247
2854
  type: "string",
2248
2855
  description: "…rating it good or bad"
2856
+ },
2857
+ verbose: {
2858
+ type: "boolean",
2859
+ description: "Show each question's whole context, not its first lines",
2860
+ default: false
2249
2861
  }
2250
2862
  },
2251
2863
  run: async ({ args }) => {
@@ -2258,7 +2870,8 @@ const decide = defineCommand({
2258
2870
  const human = createTerminalHuman({
2259
2871
  input: process.stdin,
2260
2872
  output: process.stdout,
2261
- interactive: process.stdin.isTTY
2873
+ interactive: process.stdin.isTTY,
2874
+ verbose: args.verbose
2262
2875
  });
2263
2876
  try {
2264
2877
  if (args.document) {
@@ -2298,7 +2911,7 @@ const decide = defineCommand({
2298
2911
  return;
2299
2912
  }
2300
2913
  if (args.say !== void 0) {
2301
- failed$2("--say goes with --id and --choose; answering here asks for a note after each choice.");
2914
+ failed$2("--say goes with --id and --choose; answering here, press n to add a note (plain: it asks for one after each choice).");
2302
2915
  return;
2303
2916
  }
2304
2917
  const filter = {};
@@ -2320,8 +2933,8 @@ const decide = defineCommand({
2320
2933
  for (const line of formatDecisions(open, args.workflow)) write(line);
2321
2934
  return;
2322
2935
  }
2323
- for (const d of open) {
2324
- write(style("dim", `run ${d.runId} · decision ${d.id}`));
2936
+ const collapses = canKey(process.stdin, process.stdout);
2937
+ for (const [index, d] of open.entries()) {
2325
2938
  const question = {
2326
2939
  nodeId: d.node,
2327
2940
  question: d.question,
@@ -2334,9 +2947,16 @@ const decide = defineCommand({
2334
2947
  question.onNote = (given) => {
2335
2948
  note = given;
2336
2949
  };
2337
- const choice = await human.ask(question);
2950
+ const choice = await human.askAt(question, {
2951
+ run: d.runId,
2952
+ decision: d.id,
2953
+ index: index + 1,
2954
+ total: open.length
2955
+ });
2956
+ if (choice === null) continue;
2338
2957
  const answered = await answerDecision(store, d.id, choice, void 0, "decide", note);
2339
- write(answered.ok ? `${style("green", "✓")} ${d.id}: ${choice}${saidLine(store, d.id)}\n` : `${style("yellow", "!")} ${answered.error}\n`);
2958
+ if (!answered.ok) write(`${style("yellow", "!")} ${answered.error}\n`);
2959
+ else if (!collapses) write(`${style("green", "✓")} ${d.id}: ${choice}${saidLine(store, d.id)}\n`);
2340
2960
  }
2341
2961
  } finally {
2342
2962
  human?.close();
@@ -2496,6 +3116,46 @@ const resume = defineCommand({
2496
3116
  }
2497
3117
  }
2498
3118
  });
3119
+ // src/doctor/format.ts
3120
+ /** Marks are made when used, so `--plain` (set after load) is read. */
3121
+ const MARK = {
3122
+ ok: () => style("green", "✓"),
3123
+ warn: () => style("yellow", "!"),
3124
+ fail: () => style("red", "✗"),
3125
+ skip: () => style("dim", "-")
3126
+ };
3127
+ const CHECK_CHIP = {
3128
+ ok: {
3129
+ label: CHIP_LABELS.ok,
3130
+ role: "ok"
3131
+ },
3132
+ warn: {
3133
+ label: CHIP_LABELS.warn,
3134
+ role: "warn"
3135
+ },
3136
+ fail: {
3137
+ label: CHIP_LABELS.fail,
3138
+ role: "danger"
3139
+ },
3140
+ skip: {
3141
+ label: CHIP_LABELS.skip,
3142
+ role: "muted"
3143
+ }
3144
+ };
3145
+ /**
3146
+ * A check's lines. Rich: a chip (its words padded to line the details up), the
3147
+ * detail wrapped under its column, the hint dimmed and indented; plain: a mark
3148
+ * and the lines as they always were.
3149
+ */
3150
+ const formatCheck = (result, term = termOf(process.stdout)) => {
3151
+ if (!term.rich) {
3152
+ const line = `${MARK[result.status]()} ${result.name.padEnd(20)} ${result.detail}`;
3153
+ return result.hint === void 0 ? [line] : [line, ` ${style("dim", result.hint)}`];
3154
+ }
3155
+ const { label, role } = CHECK_CHIP[result.status];
3156
+ const lead = `${chip(term, label.padEnd(4), role)} ${result.name.padEnd(20)} `;
3157
+ return [...hang(lead, result.detail, term.width), ...result.hint === void 0 ? [] : wrap(result.hint, term.width - 2).map((line) => ` ${paint(term, "muted", line)}`)];
3158
+ };
2499
3159
  // src/doctor/lease-checks.ts
2500
3160
  /**
2501
3161
  * @file What `ork doctor` says of Command Code leases (RFC-014): it removes the
@@ -2508,7 +3168,7 @@ const resume = defineCommand({
2508
3168
  * folder. Says nothing when there were none. `bin` is the `cmd` program.
2509
3169
  */
2510
3170
  const leaseChecks = async (place, env = process.env, bin = "cmd") => {
2511
- const { cleanStaleLeases, leaseLedger } = await import("./src-oaJYIddp.mjs");
3171
+ const { cleanStaleLeases, leaseLedger } = await import("./src-BnZgZGrf.mjs");
2512
3172
  const { cleaned, failed } = await cleanStaleLeases({
2513
3173
  bin,
2514
3174
  ledger: leaseLedger(placeFiles(place).runs),
@@ -2637,16 +3297,17 @@ const DESCRIBE = {
2637
3297
  folder: (link) => `${link.path} is a real folder, left as it is`
2638
3298
  };
2639
3299
  const describe = (link) => DESCRIBE[link.state](link);
3300
+ /** Labels are made when used, so `--plain` (set after load) is read. */
2640
3301
  const LABEL = {
2641
- linked: style("dim", "ok "),
2642
- missing: style("yellow", "missing"),
2643
- broken: style("red", "broken "),
2644
- elsewhere: style("yellow", "kept "),
2645
- folder: style("yellow", "kept ")
3302
+ linked: () => style("dim", "ok "),
3303
+ missing: () => style("yellow", "missing"),
3304
+ broken: () => style("red", "broken "),
3305
+ elsewhere: () => style("yellow", "kept "),
3306
+ folder: () => style("yellow", "kept ")
2646
3307
  };
2647
3308
  /** One line per folder; `created` marks the links this call made. */
2648
3309
  const formatLinks = (links, created = []) => links.map((link) => {
2649
- return `${created.includes(link.path) ? style("green", "linked ") : LABEL[link.state]} ${describe(link)}`;
3310
+ return `${created.includes(link.path) ? style("green", "linked ") : LABEL[link.state]()} ${describe(link)}`;
2650
3311
  });
2651
3312
  const checkOf = (tool, links) => {
2652
3313
  const name = `${AGENT_TOOL_LAYOUTS.find((l) => l.id === tool)?.name ?? tool} links`;
@@ -2698,16 +3359,6 @@ const ORK_META = {
2698
3359
  version: ORK_VERSION,
2699
3360
  description: "Orchestrate Claude agents on your machine"
2700
3361
  };
2701
- const MARK = {
2702
- ok: style("green", "✓"),
2703
- warn: style("yellow", "!"),
2704
- fail: style("red", "✗"),
2705
- skip: style("dim", "-")
2706
- };
2707
- const formatCheck = (result) => {
2708
- const line = `${MARK[result.status]} ${result.name.padEnd(20)} ${result.detail}`;
2709
- return result.hint === void 0 ? [line] : [line, ` ${style("dim", result.hint)}`];
2710
- };
2711
3362
  const doctor = defineCommand({
2712
3363
  meta: {
2713
3364
  name: "doctor",
@@ -2765,7 +3416,7 @@ const agreed = (yes, command) => {
2765
3416
  write(`Run it yourself: ${command}\nor run ork upgrade --yes.`);
2766
3417
  return Promise.resolve(false);
2767
3418
  }
2768
- return confirm(`Run ${style("bold", command)}? [y/N] `);
3419
+ return confirm(`Run ${command}?`);
2769
3420
  };
2770
3421
  const upgrade = defineCommand({
2771
3422
  meta: {
@@ -3146,10 +3797,25 @@ const relocateProject = (store, id, target) => {
3146
3797
  created: false
3147
3798
  };
3148
3799
  };
3149
- /** One project's lines for `ork projects`. */
3150
- const formatProject = (project) => {
3151
- const gone = existsSync(project.root) ? "" : style("yellow", " (missing)");
3152
- return [`${style("bold", project.id)} ${style("dim", project.mode)} ${project.root}${gone}`, style("dim", ` files: ${shownPath(project.dir, project.root)}`)];
3800
+ /**
3801
+ * One project's lines for `ork projects`; rich, its mode and a missing clone
3802
+ * are chips, its id is padded to `idWidth` and its root is cut to the screen.
3803
+ */
3804
+ const formatProject = (project, term = termOf(process.stdout), idWidth = 0) => {
3805
+ const missing = !existsSync(project.root);
3806
+ if (!term.rich) {
3807
+ const gone = missing ? style("yellow", " (missing)") : "";
3808
+ return [`${style("bold", project.id)} ${style("dim", project.mode)} ${project.root}${gone}`, style("dim", ` files: ${shownPath(project.dir, project.root)}`)];
3809
+ }
3810
+ const mode = chip(term, project.mode.toUpperCase(), "muted");
3811
+ const gone = missing ? ` ${chip(term, "MISSING", "warn")}` : "";
3812
+ const room = term.width - (idWidth + 2 + project.mode.length + 2 + 2) - (missing ? 10 : 0);
3813
+ return [`${style("bold", project.id.padEnd(idWidth))} ${mode} ${cut(project.root, Math.max(8, room))}${gone}`, style("dim", cut(` files: ${shownPath(project.dir, project.root)}`, term.width))];
3814
+ };
3815
+ /** Every project's lines, their ids aligned when rich. */
3816
+ const formatProjects = (projects, term = termOf(process.stdout)) => {
3817
+ const idWidth = term.rich ? Math.max(0, ...projects.map((p) => p.id.length)) : 0;
3818
+ return projects.flatMap((p) => formatProject(p, term, idWidth));
3153
3819
  };
3154
3820
  /** What `ork register` prints: the project, and where its files go. */
3155
3821
  const formatRegistered = (project, created) => {
@@ -3378,7 +4044,7 @@ const migrateHere = async (cwd, options) => {
3378
4044
  fail("Pass --yes to migrate without a terminal.");
3379
4045
  return;
3380
4046
  }
3381
- if (!options.yes && !await confirm("Go ahead? [y/N] ")) return;
4047
+ if (!options.yes && !await confirm("Go ahead?")) return;
3382
4048
  applyMigration(plan, store);
3383
4049
  write(`${style("green", "✓")} migrated. ${afterMigration(plan)}`);
3384
4050
  } finally {
@@ -3498,7 +4164,7 @@ const projects = defineCommand({
3498
4164
  const all = withStore$1((store) => store.projects());
3499
4165
  if (args.json) write(JSON.stringify(all, void 0, 2));
3500
4166
  else if (all.length === 0) write("No projects registered yet: ork init (in-repo) or ork register (attached).");
3501
- else for (const project of all) for (const line of formatProject(project)) write(line);
4167
+ else for (const line of formatProjects(all)) write(line);
3502
4168
  }
3503
4169
  });
3504
4170
  const migrate = defineCommand({
@@ -3592,13 +4258,16 @@ const SEVERITY_STYLE = {
3592
4258
  };
3593
4259
  const about = (item) => `${item.target}/${item.kind}`;
3594
4260
  /** One line per item, most recently seen first. */
3595
- const formatFeedbackItems = (items, now = Date.now()) => {
4261
+ const formatFeedbackItems = (items, now = Date.now(), term = termOf(process.stdout)) => {
3596
4262
  if (items.length === 0) return ["No feedback yet."];
3597
4263
  const width = Math.max(...items.map((i) => about(i).length));
3598
4264
  return items.map((item) => {
3599
- const status = item.status === "new" ? "" : ` ${style("dim", `[${item.status}]`)}`;
4265
+ const mark = item.status === "new" ? "" : term.rich ? ` ${chip(term, item.status.toUpperCase(), item.status === "dismissed" ? "muted" : "accent")}` : ` ${style("dim", `[${item.status}]`)}`;
3600
4266
  const times = `${String(item.count)}×`.padStart(4);
3601
- return `${style("dim", item.id)} ${times} ${style(SEVERITY_STYLE[item.severity], about(item).padEnd(width))} ${item.title}${status} ${style("dim", `· ${ago(item.lastSeen, now)}`)}`;
4267
+ const when = ` ${style("dim", `· ${ago(item.lastSeen, now)}`)}`;
4268
+ const room = term.width - (item.id.length + 1 + times.length + 1 + width + 2) - widthOf(mark) - widthOf(when);
4269
+ const title = term.rich ? cut(item.title, Math.max(8, room)) : item.title;
4270
+ return `${style("dim", item.id)} ${times} ${style(SEVERITY_STYLE[item.severity], about(item).padEnd(width))} ${title}${mark}${when}`;
3602
4271
  });
3603
4272
  };
3604
4273
  const plural = (n, word) => `${String(n)} ${word}${n === 1 ? "" : "s"}`;
@@ -4365,7 +5034,7 @@ const ps = defineCommand({
4365
5034
  limit: Number.isInteger(limit) && limit > 0 ? limit : 20,
4366
5035
  ...args.all ? {} : { project: placeFor(process.cwd(), store.projects()).root }
4367
5036
  });
4368
- const statusOf = (run) => runStatus(run, run.status === "running" ? store.events(run.id).map((s) => s.event) : []);
5037
+ const statusOf = (run) => listedStatus(run, run.status === "running" ? store.events(run.id).map((s) => s.event) : []);
4369
5038
  if (args.json) write(JSON.stringify(runs, void 0, 2));
4370
5039
  else for (const line of formatRuns(runs, args.all, Date.now(), statusOf)) write(line);
4371
5040
  })
@@ -4396,6 +5065,11 @@ const logs = defineCommand({
4396
5065
  type: "boolean",
4397
5066
  description: "Print the stored events as JSON lines",
4398
5067
  default: false
5068
+ },
5069
+ verbose: {
5070
+ type: "boolean",
5071
+ description: "Show each tool call whole, and why each failed node stopped, with its scrubbed details (always on with --node)",
5072
+ default: false
4399
5073
  }
4400
5074
  },
4401
5075
  run: ({ args }) => withStore(async (store) => {
@@ -4418,6 +5092,7 @@ const logs = defineCommand({
4418
5092
  await followRun(store, found.run, write, {
4419
5093
  follow: args.follow,
4420
5094
  json: args.json,
5095
+ verbose: args.verbose,
4421
5096
  signal: abort.signal,
4422
5097
  ...args.node === void 0 ? {} : { node: args.node }
4423
5098
  });
@@ -4504,7 +5179,7 @@ const addAgent = (options) => {
4504
5179
  * doesn't, and otherwise it asks on a terminal, once, saying the plugin has
4505
5180
  * them too, or says how to add them later.
4506
5181
  */
4507
- const QUESTION = "Add Ork's skills for Claude Code to .ork/skills/, so it writes workflows Ork accepts? Ork's Claude Code plugin has them too. [y/N] ";
5182
+ const QUESTION = "Add Ork's skills for Claude Code to .ork/skills/, so it writes workflows Ork accepts? Ork's Claude Code plugin has them too.";
4508
5183
  const HINT = "Ork's skills for Claude Code: add them here with `ork init --skills`, or install Ork's plugin in Claude Code (/plugin marketplace add mimshins/ork, then /plugin install ork@ork).";
4509
5184
  /** Whether to write the skills: as asked, or the person's answer. */
4510
5185
  const wanted = (root, asked) => {
@@ -4775,27 +5450,27 @@ const main = defineCommand({
4775
5450
  ps,
4776
5451
  logs,
4777
5452
  tree,
4778
- usage: () => import("./usage-Cd8pEYyK.mjs").then((module) => module.usage),
4779
- prune: () => import("./usage-Cd8pEYyK.mjs").then((module) => module.prune),
5453
+ usage: () => import("./usage-GceBGMjU.mjs").then((module) => module.usage),
5454
+ prune: () => import("./usage-GceBGMjU.mjs").then((module) => module.prune),
4780
5455
  decide,
4781
- relay: () => import("./relay-EceRIebO.mjs").then((module) => module.relay),
4782
- pause: () => import("./pause-D7OVB5Ac.mjs").then((module) => module.pause),
4783
- continue: () => import("./pause-D7OVB5Ac.mjs").then((module) => module.continueRun),
5456
+ relay: () => import("./relay-DNx7be9k.mjs").then((module) => module.relay),
5457
+ pause: () => import("./pause-D6PuSlNl.mjs").then((module) => module.pause),
5458
+ continue: () => import("./pause-D6PuSlNl.mjs").then((module) => module.continueRun),
4784
5459
  stop,
4785
5460
  chat,
4786
- send: () => import("./send-C0uwfUYi.mjs").then((module) => module.send),
5461
+ send: () => import("./send-CLs_fyg7.mjs").then((module) => module.send),
4787
5462
  resume,
4788
5463
  feedback,
4789
5464
  context,
4790
- worktrees: () => import("./worktrees-g1MQMGie.mjs").then((module) => module.worktrees),
4791
- wt: () => import("./worktrees-g1MQMGie.mjs").then((module) => module.worktrees),
4792
- watch: () => import("./watch-DW5R2OPO.mjs").then((module) => module.watch),
4793
- mcp: () => import("./mcp-CslnG5wf.mjs").then((module) => module.mcp),
4794
- ui: () => import("./ui-CBkHJdqk.mjs").then((module) => module.ui),
5465
+ worktrees: () => import("./worktrees-Be51E2rB.mjs").then((module) => module.worktrees),
5466
+ wt: () => import("./worktrees-Be51E2rB.mjs").then((module) => module.worktrees),
5467
+ watch: () => import("./watch-BmoybMQg.mjs").then((module) => module.watch),
5468
+ mcp: () => import("./mcp-DYgVA9f5.mjs").then((module) => module.mcp),
5469
+ ui: () => import("./ui-CIci4UBW.mjs").then((module) => module.ui),
4795
5470
  doctor,
4796
5471
  upgrade,
4797
- completion: () => import("./completion-Dv5kf598.mjs").then((module) => module.completion),
4798
- __complete: () => import("./completion-Dv5kf598.mjs").then((module) => module.completeCommand(main))
5472
+ completion: () => import("./completion-C5D0sEs5.mjs").then((module) => module.completion),
5473
+ __complete: () => import("./completion-C5D0sEs5.mjs").then((module) => module.completeCommand(main))
4799
5474
  }
4800
5475
  });
4801
5476
  // src/bin.ts
@@ -4805,15 +5480,16 @@ const main = defineCommand({
4805
5480
  * starts the daily background check and ends with the notice (ADR-038); the
4806
5481
  * background copy it starts only runs the check. It first clears git's local
4807
5482
  * variables (a hook's `GIT_DIR`, `GIT_INDEX_FILE`, …), so nothing Ork starts
4808
- * acts on the hook's repository (specs/worktrees.md). `--help` leaves out the
4809
- * flags only Ork passes to itself. `ork __complete`, which tab completion
4810
- * runs on every Tab, goes first: no update check, and no citty, so a typed
4811
- * `--help` is a word to complete.
5483
+ * acts on the hook's repository (specs/worktrees.md). The global `--plain`
5484
+ * flag is taken out of the arguments and set as `ORK_PLAIN=1`. `--help`
5485
+ * leaves out the flags only Ork passes to itself. `ork __complete`, which tab
5486
+ * completion runs on every Tab, goes first: no update check, and no citty, so
5487
+ * a typed `--help` is a word to complete.
4812
5488
  */
4813
5489
  clearGitLocalEnv();
4814
- const argv = process.argv.slice(2);
5490
+ const argv = takePlainFlag(process.argv.slice(2));
4815
5491
  if (argv[0] === "__complete") {
4816
- const { printCompletions } = await import("./completion-Dv5kf598.mjs");
5492
+ const { printCompletions } = await import("./completion-C5D0sEs5.mjs");
4817
5493
  await printCompletions(main, argv.slice(1));
4818
5494
  } else if (process.env["ORK_UPDATE_CHECK_CHILD"] === "1") await runUpdateChild();
4819
5495
  else {
@@ -4823,6 +5499,9 @@ else {
4823
5499
  showUpdateNotice();
4824
5500
  });
4825
5501
  }
4826
- await runMain(main, { showUsage: showHelp });
5502
+ await runMain(main, {
5503
+ rawArgs: argv,
5504
+ showUsage: showHelp
5505
+ });
4827
5506
  }
4828
5507
  export {};