@krapht/ork 1.0.0-alpha.1 → 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 (43) hide show
  1. package/README.md +3 -4
  2. package/dist/{__build_info__-D9lSlMFy.mjs → __build_info__-CIchF6nr.mjs} +2 -2
  3. package/dist/bin.mjs +1479 -600
  4. package/dist/chips-BA7SrrH5.mjs +43 -0
  5. package/dist/{clean-CA_Oi2Wl.mjs → clean-Di-KT7Mf.mjs} +5 -2
  6. package/dist/{completion-DBGfL-gN.mjs → completion-C5D0sEs5.mjs} +23 -16
  7. package/dist/context-BaNshWr9.mjs +695 -0
  8. package/dist/{decide-CoIJh34e.mjs → decide-B6PnKeqo.mjs} +3 -3
  9. package/dist/{find-rBBk7Jui.mjs → find-BicBbGMg.mjs} +1 -1
  10. package/dist/format-lWhN-jYR.mjs +286 -0
  11. package/dist/{in-a-run-DdAakpRR.mjs → in-a-run-CgkVouyG.mjs} +1 -1
  12. package/dist/{limits-BPM9wUlV.mjs → limits-jZQp9ZJC.mjs} +1 -1
  13. package/dist/{list-Bv5Lxu7z.mjs → list-CEaJ07jd.mjs} +11 -4
  14. package/dist/{mcp-DM30MFBw.mjs → mcp-DYgVA9f5.mjs} +6 -6
  15. package/dist/output-CM95njYt.mjs +421 -0
  16. package/dist/{pause-C7lJkSma.mjs → pause-D6PuSlNl.mjs} +4 -4
  17. package/dist/{relay-BipJYszF.mjs → relay-DNx7be9k.mjs} +2 -2
  18. package/dist/{remove-BDs9isB6.mjs → remove-CpMb4NDl.mjs} +14 -7
  19. package/dist/{report-DK4LGqwF.mjs → report-CEN8VoUb.mjs} +31 -20
  20. package/dist/{send-nbkjf3sc.mjs → send-CLs_fyg7.mjs} +3 -3
  21. package/dist/{src-BxUJPGa5.mjs → src-9iGkjnsy.mjs} +487 -341
  22. package/dist/{src-B0TT7DBi.mjs → src-B6KvTm7B.mjs} +106 -90
  23. package/dist/src-BnZgZGrf.mjs +2697 -0
  24. package/dist/{src-hrj2h0IZ.mjs → src-D6_xjm-x.mjs} +6719 -3389
  25. package/dist/ui/THIRD_PARTY_LICENSES.txt +594 -0
  26. package/dist/ui/assets/app-CxURph7a.js +75 -0
  27. package/dist/ui/assets/app-QoHVi-mb.css +1 -0
  28. package/dist/ui/assets/{index-CPx74lFf.js → index-By_8KKG6.js} +3 -3
  29. package/dist/ui/assets/theme-CdXvqTQ6.js +1 -0
  30. package/dist/ui/index.html +2 -2
  31. package/dist/{ui-B98FSboa.mjs → ui-CIci4UBW.mjs} +4 -4
  32. package/dist/{usage-EW7DI6Z7.mjs → usage-GceBGMjU.mjs} +43 -32
  33. package/dist/{watch-BAkSGaOp.mjs → watch-BmoybMQg.mjs} +28 -16
  34. package/dist/{workflows-vsEohqa7.mjs → workflows-DZvW3i1e.mjs} +4 -3
  35. package/dist/{worktrees-Ahj9rdWK.mjs → worktrees-Be51E2rB.mjs} +84 -24
  36. package/package.json +2 -4
  37. package/dist/context-6x3kXREO.mjs +0 -274
  38. package/dist/format-zMmhqyLE.mjs +0 -427
  39. package/dist/output-TV_j7Ni0.mjs +0 -950
  40. package/dist/src-B9v0lDc6.mjs +0 -1204
  41. package/dist/ui/assets/app-BNT0SuIz.css +0 -1
  42. package/dist/ui/assets/app-CmkynZyW.js +0 -67
  43. package/dist/ui/assets/theme-3xH5jxKs.js +0 -1
package/dist/bin.mjs CHANGED
@@ -1,58 +1,28 @@
1
1
  #!/usr/bin/env node
2
- import { Bn as serializeWorkflow, Bt as FEEDBACK_TARGETS, Cn as runDirOf, D as inspectProject, Dn as projectRootOf, Dt as userMessageText, En as inside, Ft as PROJECT_FILE, H as tellNode, Ht as planPost, I as createCalls, It as gitRulesOf, J as tellLiveRun, Jn as beforeOf, Jt as normalizeTitle, Kn as settingsOf, Kt as createFeedback, L as addressOf, Lt as loadProject, M as clearGitLocalEnv, N as parseInputPairs, P as resolveInputs, R as endLink, Rt as FEEDBACK_KINDS, Sn as projectsHome, Tt as startRun, Un as WorkflowSchema, V as peerOf, W as flattenTree, Wt as sendPost, Xt as maskReplacer, Yn as isClaudeNode, _ as lateTriggers, _n as hasInRepoFiles, _t as runStatus, an as wakeRun, ar as CLAUDE_DRIVERS, bn as placeFiles, cn as planMoveInto, d as runningRunIds, dn as toolsInUse, dt as namingNoDriver, er as INTEGRATIONS, et as adoptedCalls, f as runningScript, fn as createAgent, ft as sessionDriver, h as isNewer, hn as loadAgents, ht as liveStatus, i as NOT_STARTED, in as socketRoom, ir as ORK_LAYOUT, it as replayLoopAnswers, l as planUpgrade, ln as linkTools, mn as catalogAgent, n as resolveBase, nn as serveRun, nt as resumable, on as planAgentMigration, or as firstLine, ot as detachRun, pn as deleteAgent, q as requestStop, qt as fingerprint, rn as socketPath, rr as AGENT_TOOL_LAYOUTS, rt as createReplay, sn as applySteps, st as followEvents, t as createWorkspace, tt as detachResume, u as releaseNotes, un as toolLinks, ut as planNodeSession, v as triggersReport, vn as inRepoPlace, vt as runTree, wn as shownPath, wt as walkSummary, xn as placeFor, yn as newProjectId, z as endLinksOf, zt as FEEDBACK_SEVERITIES } from "./src-hrj2h0IZ.mjs";
3
- import { C as feedbackActions, S as placeOf, _ as previewPost, b as acceptItem, c as checkDue, d as pendingNotice, f as readUpdateCache, g as nodeSession, h as writeUpdateCache, i as registryOf, l as fetchLatest, m as updateCachePath, n as ORK_VERSION, o as updateSwitchOff, p as refreshUpdateCache, r as isSwitchedOn, s as runInstall, t as ORK_NAME, u as newerVersion$1, v as checkNote, w as openFixRun, y as recordNote } from "./__build_info__-D9lSlMFy.mjs";
4
- import { i as builtinWorkflowFile, n as pendingDecisions, r as builtinProblem, t as answerDecision } from "./decide-CoIJh34e.mjs";
5
- import { n as newRunId, t as findRun } from "./find-rBBk7Jui.mjs";
6
- import { a as write, c as openStore, i as style, l as storePath, n as cost, o as writeErr, s as nodeOf, t as confirm } from "./output-TV_j7Ni0.mjs";
7
- import { n as listOrkWorktrees, t as createdWorktrees } from "./list-Bv5Lxu7z.mjs";
8
- import { a as planClean, t as applyClean } from "./clean-CA_Oi2Wl.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";
9
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";
10
- import { a as formatRunEvent, i as formatEvent, n as formatRuns, o as createTerminalHuman, r as formatTree, t as ago } from "./format-zMmhqyLE.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-vsEohqa7.mjs";
12
- import { a as validate, c as warningLines, i as loadWorkflow, o as validateFile, r as formatReport, s as walkLoaded, t as context } from "./context-6x3kXREO.mjs";
13
- import { i as runLimits, r as limitsRefusal, t as chatNotice } from "./limits-BPM9wUlV.mjs";
14
- import { _ as resultOf, 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 } from "./src-BxUJPGa5.mjs";
15
- import { i as decideRefusal, n as chatRefusal } from "./in-a-run-DdAakpRR.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
- import { execFile, spawn } from "node:child_process";
23
23
  import { setTimeout } from "node:timers/promises";
24
24
  import { defineCommand, renderUsage, runMain } from "citty";
25
25
  import { createInterface } from "node:readline/promises";
26
- import { styleText } from "node:util";
27
- // ../integrations/src/ready.ts
28
- /**
29
- * @file Which Claude driver is ready on this machine: each driver's own check
30
- * (what `ork doctor` shows), and the one a run takes for the nodes that name
31
- * no driver, so a committed workflow never carries one person's login.
32
- */
33
- /** Each registered Claude driver's own check, in registry order. */
34
- const driverChecks = (run, env) => Promise.all(CLAUDE_DRIVERS.map(async (driver) => ({
35
- id: driver.id,
36
- result: await driver.doctor(run, env)
37
- })));
38
- /**
39
- * The driver ready here: the one whose check passes; with more than one,
40
- * {@link PREFERRED_DRIVER}; with none, each driver's hint.
41
- */
42
- const readyDriver = (checks) => {
43
- const ready = checks.filter(({ result }) => result.status === "ok");
44
- const pick = ready.find(({ id }) => id === "cli") ?? ready.at(0);
45
- if (pick === void 0) return {
46
- ok: false,
47
- hints: checks.flatMap(({ result }) => result.hint === void 0 ? [] : [result.hint])
48
- };
49
- return {
50
- ok: true,
51
- driver: pick.id,
52
- detail: pick.result.detail,
53
- ready: ready.length
54
- };
55
- };
56
26
  // ../core/src/control/queue.ts
57
27
  const PERMISSION_OPTIONS = [{
58
28
  id: "allow",
@@ -64,14 +34,33 @@ const PERMISSION_OPTIONS = [{
64
34
  const DENIED = "The user did not allow this tool.";
65
35
  /** The deny message a session gets, with the person's words when given. */
66
36
  const deniedMessage = (note) => note === void 0 ? DENIED : `${DENIED} They said: ${note}`;
67
- /** At most `max` characters of a tool's input, for the decision's context. */
68
- const inputPreview = (input, max = 400) => {
69
- const json = JSON.stringify(input);
70
- return json.length > max ? `${json.slice(0, max - 1)}…` : json;
37
+ /** The input fields that say what a tool will do, the first one present. */
38
+ const TOOL_INPUT_KEYS = [
39
+ "command",
40
+ "file_path",
41
+ "path",
42
+ "url",
43
+ "pattern"
44
+ ];
45
+ /** A tool's input for the decision's context: as written, cut at `max`. */
46
+ const inputContext = (input, max = 4e3) => {
47
+ const key = TOOL_INPUT_KEYS.find((k) => typeof input[k] === "string");
48
+ const text = key === void 0 ? JSON.stringify(input, void 0, 2) : String(input[key]);
49
+ return text.length > max ? `${text.slice(0, max - 1)}…` : text;
71
50
  };
72
51
  const createDecisionQueue = (options) => {
73
52
  const { store, runId, emit } = options;
74
- let count = 0;
53
+ const earlier = store.decisions({ runId });
54
+ let count = Math.max(0, ...earlier.map(({ id }) => Number(id.slice(runId.length + 1)) || 0));
55
+ for (const { id, node, status } of earlier) {
56
+ if (status !== "open") continue;
57
+ store.withdrawDecision(id);
58
+ emit({
59
+ type: "decision.withdrawn",
60
+ node,
61
+ decision: id
62
+ });
63
+ }
75
64
  /** Opens a decision and settles it with the first answer. */
76
65
  const decide = (open, local, withdrawn) => {
77
66
  count += 1;
@@ -155,12 +144,16 @@ const createDecisionQueue = (options) => {
155
144
  };
156
145
  if (question.recommended !== void 0) open.recommended = question.recommended;
157
146
  if (question.context !== void 0) open.context = question.context;
147
+ if (question.document !== void 0) open.document = question.document;
158
148
  if (question.owner !== void 0) open.owner = question.owner;
159
149
  const { local } = options;
160
150
  return decide(open, local === void 0 ? void 0 : (signal) => {
161
151
  let note;
162
152
  return local.ask({
163
153
  ...question,
154
+ question: maskSecrets(question.question),
155
+ ...question.context === void 0 ? {} : { context: maskSecrets(question.context) },
156
+ ...question.document === void 0 ? {} : { document: maskSecrets(question.document) },
164
157
  signal,
165
158
  onNote: (given) => {
166
159
  note = given;
@@ -219,7 +212,7 @@ const createDecisionQueue = (options) => {
219
212
  kind: "permission",
220
213
  question: `Allow ${request.toolName}?`,
221
214
  options: PERMISSION_OPTIONS,
222
- context: inputPreview(request.input)
215
+ context: inputContext(request.input)
223
216
  }, local === void 0 ? void 0 : (signal) => {
224
217
  let said;
225
218
  return local.permit({
@@ -236,7 +229,7 @@ const createDecisionQueue = (options) => {
236
229
  note: said
237
230
  };
238
231
  });
239
- });
232
+ }, request.signal);
240
233
  if (note !== void 0) request.onNote?.(note);
241
234
  return answer === "allow" ? { allow: true } : {
242
235
  allow: false,
@@ -314,165 +307,6 @@ const startControl = async (options) => {
314
307
  }
315
308
  };
316
309
  };
317
- // ../core/src/feedback/signals.ts
318
- const ORK_TOOL = "mcp__ork__";
319
- /**
320
- * An observer for one run's events: give it each event, and record what it
321
- * returns, in order (the event, its origin filled in when it is feedback, then
322
- * any feedback it signals).
323
- */
324
- const createSignals = (options = {}) => {
325
- let workflow;
326
- const nodeTypes = /* @__PURE__ */ new Map();
327
- const sessions = /* @__PURE__ */ new Map();
328
- const tools = /* @__PURE__ */ new Map();
329
- const asked = /* @__PURE__ */ new Map();
330
- const seen = /* @__PURE__ */ new Set();
331
- const originOf = (node) => {
332
- const origin = {};
333
- if (workflow !== void 0) origin.workflow = workflow;
334
- if (options.workflowHash !== void 0) origin.workflowHash = options.workflowHash;
335
- const type = node === void 0 ? void 0 : nodeTypes.get(node);
336
- if (type !== void 0) origin.nodeType = type;
337
- const session = node === void 0 ? void 0 : sessions.get(node);
338
- if (session !== void 0) origin.model = session.model;
339
- if (session?.driver !== void 0) origin.driver = session.driver;
340
- if (options.version !== void 0) origin.version = options.version;
341
- return origin;
342
- };
343
- const record = (node, signal) => {
344
- const print = fingerprint(signal);
345
- if (seen.has(print)) return [];
346
- seen.add(print);
347
- return [{
348
- type: "feedback.recorded",
349
- ...node === void 0 ? {} : { node },
350
- feedback: {
351
- source: "system",
352
- kind: signal.kind,
353
- target: signal.target,
354
- severity: signal.severity,
355
- title: signal.title,
356
- body: signal.body,
357
- evidence: signal.evidence ?? [],
358
- code: signal.code,
359
- fingerprint: print,
360
- origin: originOf(node)
361
- }
362
- }];
363
- };
364
- const answered = (question, answer) => {
365
- if (question.kind === "permission") {
366
- if (answer !== "deny") return null;
367
- const tool = question.question.replace(/^Allow /u, "").replace(/\?$/u, "");
368
- return [question.node, {
369
- code: "permission.denied",
370
- kind: "friction",
371
- target: "workflow",
372
- severity: "low",
373
- title: `${question.node} was denied ${tool}`,
374
- body: "Allow it in the node's `tools.allow` if it needs it, or tell it in its prompt not to use it.",
375
- evidence: [tool]
376
- }];
377
- }
378
- if (question.recommended === void 0 || answer === question.recommended) return null;
379
- return [question.node, {
380
- code: "recommendation.overridden",
381
- kind: "context-gap",
382
- target: "workflow",
383
- severity: "low",
384
- title: `The answer to ${question.node}'s question was not the one it recommended`,
385
- body: `Asked: ${question.question} Recommended: ${question.recommended}. Answered: ${answer}.`
386
- }];
387
- };
388
- /** The feedback an event signals, if any, and the node it is about. */
389
- const signalOf = (event) => {
390
- if (event.type === "loop.exhausted") return [event.from, {
391
- code: "loop.exhausted",
392
- kind: "workflow",
393
- target: "workflow",
394
- severity: "medium",
395
- title: `The loop ${event.from} -> ${event.to} ran out of its ${String(event.maxRounds)} rounds`,
396
- body: "Raise `maxRounds`, or make what the loop's nodes are asked to fix clearer so it settles sooner."
397
- }];
398
- if (event.type === "run.finished" && event.result.status === "budget_exhausted") return [void 0, {
399
- code: "budget.exhausted",
400
- kind: "workflow",
401
- target: "workflow",
402
- severity: "medium",
403
- title: "The run's budget ran out",
404
- body: `The run stopped after spending $${event.result.costUsd.toFixed(4)}. Raise \`limits.budgetUsd\`, or give nodes cheaper models or smaller budgets.`
405
- }];
406
- if (event.type === "decision.answered") {
407
- const question = asked.get(event.decision);
408
- return question === void 0 ? null : answered(question, event.answer);
409
- }
410
- if (event.type === "node.event" && event.event.type === "tool.result") {
411
- const name = tools.get(`${event.id}\0${event.event.id}`);
412
- if (!event.event.isError || name?.startsWith(ORK_TOOL) !== true) return null;
413
- const tool = name.slice(10);
414
- return [event.id, {
415
- code: "tool.error",
416
- kind: "bug",
417
- target: "ork",
418
- severity: "medium",
419
- title: `Ork's ${tool} tool returned an error`,
420
- body: `A call to \`${tool}\` failed; the node's events show the call.`,
421
- evidence: [name]
422
- }];
423
- }
424
- if (event.type === "node.finished" && nodeTypes.get(event.id) === "human.gate" && event.output["answeredBy"] === "timeout") return [event.id, {
425
- code: "gate.timeout",
426
- kind: "workflow",
427
- target: "workflow",
428
- severity: "low",
429
- title: `The gate ${event.id} timed out and took its default`,
430
- body: "No one answered in time. Lengthen its `timeout`, or keep it if the default was right."
431
- }];
432
- return null;
433
- };
434
- /** Keeps what later events and origins need. */
435
- const observe = (event) => {
436
- if (event.type === "run.started") workflow = event.workflow;
437
- if (event.type === "node.started") nodeTypes.set(event.id, event.nodeType);
438
- if (event.type === "decision.opened") {
439
- const question = {
440
- node: event.node,
441
- kind: event.kind,
442
- question: event.question
443
- };
444
- if (event.recommended !== void 0) question.recommended = event.recommended;
445
- asked.set(event.decision, question);
446
- }
447
- if (event.type !== "node.event") return;
448
- const inner = event.event;
449
- if (inner.type === "session.started" && inner.fork !== true) sessions.set(event.id, {
450
- model: inner.model,
451
- ...inner.driver === void 0 ? {} : { driver: inner.driver }
452
- });
453
- if (inner.type === "tool.called") tools.set(`${event.id}\0${inner.id}`, inner.name);
454
- };
455
- return (event) => {
456
- observe(event);
457
- if (event.type === "feedback.recorded") {
458
- if (seen.has(event.feedback.fingerprint)) return [];
459
- seen.add(event.feedback.fingerprint);
460
- const origin = {
461
- ...originOf(event.node),
462
- ...event.feedback.origin
463
- };
464
- return [{
465
- ...event,
466
- feedback: {
467
- ...event.feedback,
468
- origin
469
- }
470
- }];
471
- }
472
- const signal = signalOf(event);
473
- return signal === null ? [event] : [event, ...record(...signal)];
474
- };
475
- };
476
310
  /** A title's words, as fingerprints compare them. */
477
311
  const titleWords = (title) => new Set(normalizeTitle(title).split(" ").filter((word) => word !== ""));
478
312
  /** The share of two titles' words they have in common (Jaccard). */
@@ -514,6 +348,140 @@ const proposeMerges = (items) => {
514
348
  };
515
349
  }).filter((merge) => merge !== void 0).toSorted((a, b) => firstSeen(a.into, b.into));
516
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
+ });
517
485
  // ../core/src/remote/relay.ts
518
486
  /**
519
487
  * @file A run's side of the relay (RFC-003): its nodes' links, read from the
@@ -613,6 +581,89 @@ const createRelay = (options) => {
613
581
  }
614
582
  };
615
583
  };
584
+ // ../core/src/run/replay.ts
585
+ const NO_FAN_OUTS = {
586
+ lists: /* @__PURE__ */ new Map(),
587
+ items: /* @__PURE__ */ new Map()
588
+ };
589
+ /** Whether an activation's input carries a result this run made anew. */
590
+ const carries = (fresh, input) => {
591
+ if (input === void 0) return false;
592
+ if (fresh.has(input)) return true;
593
+ const results = input["results"];
594
+ if (results === null || typeof results !== "object") return false;
595
+ return Object.values(results).some((value) => value !== null && typeof value === "object" && fresh.has(value));
596
+ };
597
+ /**
598
+ * Hands each node's recorded results back in order, until the node runs anew
599
+ * once (it failed or never finished last time, or its input is new); from then
600
+ * on it always runs. A node cut off mid-session continues that session, once.
601
+ */
602
+ const createReplay = (history, cutOff = /* @__PURE__ */ new Map(), mail, earlier, fanOuts = NO_FAN_OUTS) => {
603
+ const sessions = new Map(cutOff);
604
+ const used = /* @__PURE__ */ new Map();
605
+ const anew = /* @__PURE__ */ new Set();
606
+ const fresh = /* @__PURE__ */ new WeakSet();
607
+ /** Fanned nodes whose list was resolved anew: their items can't be replayed. */
608
+ const renewed = /* @__PURE__ */ new Set();
609
+ const takeItem = (activation, of) => {
610
+ const recorded = fanOuts.items.get(activation.id)?.get(activation.round ?? 0);
611
+ if (recorded !== void 0 && !renewed.has(of) && !carries(fresh, activation.input)) return recorded;
612
+ anew.add(activation.id);
613
+ return null;
614
+ };
615
+ return {
616
+ take: (activation) => {
617
+ const { id } = activation;
618
+ if (activation.brief !== void 0 || anew.has(id)) return null;
619
+ if (activation.item !== void 0) return takeItem(activation, activation.item.of);
620
+ const index = used.get(id) ?? 0;
621
+ const recorded = history.get(id)?.[index];
622
+ if (recorded === void 0 || carries(fresh, activation.input)) {
623
+ anew.add(id);
624
+ return null;
625
+ }
626
+ used.set(id, index + 1);
627
+ return recorded;
628
+ },
629
+ ran: (result) => {
630
+ fresh.add(result.output);
631
+ },
632
+ cutOff: (activation) => {
633
+ const session = activation.brief === void 0 && (activation.item === void 0 || !renewed.has(activation.item.of)) ? sessions.get(activation.id) : void 0;
634
+ sessions.delete(activation.id);
635
+ return session;
636
+ },
637
+ listed: (id, round, source) => {
638
+ const list = fanOuts.lists.get(listKey(id, round));
639
+ const ranAnew = typeof source === "object" && source !== null && fresh.has(source);
640
+ if (list !== void 0 && !ranAnew) return list;
641
+ renewed.add(id);
642
+ return null;
643
+ },
644
+ previousSession: ({ id, item, round = 0 }) => item === void 0 || renewed.has(item.of) ? void 0 : [...fanOuts.items.get(id) ?? []].findLast(([before]) => before < round)?.[1].sessionId,
645
+ lastOutput: (id) => history.get(id)?.at(-1)?.output,
646
+ kept: () => [...history.keys(), ...fanOuts.items.keys()],
647
+ ...mail === void 0 ? {} : { mail },
648
+ ...earlier === void 0 ? {} : { earlier }
649
+ };
650
+ };
651
+ /**
652
+ * The run's `HumanChannel`, answering questions about loops out of rounds as
653
+ * they were answered last time, in order; anything else goes to `human`.
654
+ */
655
+ const replayLoopAnswers = (human, answers) => {
656
+ const left = [...answers];
657
+ return {
658
+ permit: (request) => human.permit(request),
659
+ ...human.settled === void 0 ? {} : { settled: human.settled },
660
+ ask: (question) => {
661
+ const index = left.findIndex((a) => a.question === question.question);
662
+ const [given] = index === -1 ? [] : left.splice(index, 1);
663
+ return given === void 0 ? human.ask(question) : Promise.resolve(given.answer);
664
+ }
665
+ };
666
+ };
616
667
  // ../core/src/store/recorder.ts
617
668
  /**
618
669
  * @file Records a run as it happens: each event appended to the store and
@@ -641,6 +692,11 @@ const createRecorder = (store, run, runDir) => {
641
692
  // src/help-root.ts
642
693
  /** Where the user docs start. */
643
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
+ }];
644
700
  const rowOf = (entry) => {
645
701
  const short = groupOf(entry.name)?.short ?? entry.description;
646
702
  const alias = entry.aliases.length === 0 ? "" : ` (alias: ${entry.aliases.join(", ")})`;
@@ -668,6 +724,9 @@ const renderRootHelp = async (cmd) => {
668
724
  ...groups.flatMap((group) => groupLines(group, width)),
669
725
  `Usage: ${name} <command> [flags]`,
670
726
  "",
727
+ "Flags for every command:",
728
+ ...GLOBAL_FLAGS.map((f) => ` ${f.name} ${f.description}`),
729
+ "",
671
730
  `Use "${name} <command> --help" for more about a command.`,
672
731
  `${name} v${meta.version ?? ""}`
673
732
  ].join("\n");
@@ -691,299 +750,870 @@ const renderHelp = async (cmd, parent) => {
691
750
  if (cmd.subCommands !== void 0 && parent === void 0) return renderRootHelp(shown);
692
751
  const above = parent?.meta === void 0 ? void 0 : { meta: parent.meta };
693
752
  const usage = await renderUsage(shown, above);
694
- 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}`;
695
755
  const choices = await choicesOf(shown);
696
- 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}`;
697
757
  };
698
758
  /** Prints {@link renderHelp}'s usage, as citty prints its own. */
699
759
  const showHelp = async (cmd, parent) => {
700
760
  process.stdout.write(`${await renderHelp(cmd, parent)}\n\n`);
701
761
  };
702
- // src/update/notify.ts
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
+ };
703
802
  /**
704
- * @file The new-version notice (ADR-038). At most once a day a command starts a
705
- * detached copy of `ork` that asks the npm registry for the latest version
706
- * and records it in `~/.ork/update-check.json`, so no command waits on the
707
- * network; a later command, when a newer version is known, ends with one line
708
- * on stderr (once a day). Off in CI, without a terminal, with `--json`, and
709
- * with `ORK_NO_UPDATE_CHECK=1` or `NO_UPDATE_NOTIFIER=1`.
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.
710
806
  */
711
- /** Set in the background copy of `ork` that only runs the check. */
712
- const UPDATE_CHILD_ENV = "ORK_UPDATE_CHECK_CHILD";
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
713
814
  /**
714
- * Why the automatic check is off for this command, or `null` when it is on: the
715
- * environment's switches (core's, which `ork ui` reads too), then where a
716
- * notice on stderr would go.
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.
717
821
  */
718
- const updateCheckOff = (env, argv, terminal) => {
719
- const switched = updateSwitchOff(env);
720
- if (switched !== null) return switched === "CI" ? "in CI" : "turned off";
721
- if (isSwitchedOn(env["ORK_DETACHED"]) || !terminal) return "no terminal";
722
- if (argv.includes("--json")) return "JSON output";
723
- if (argv[0] === "upgrade") return "ork upgrade checks for itself";
724
- if (argv[0] === "ui" && argv.includes("--serve")) return "ork ui's server checks for itself";
725
- return null;
726
- };
727
- /** The notice's text. */
728
- const updateNotice = (latest, current) => `Ork ${latest} is out (you have ${current}). Upgrade with: ork upgrade`;
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
+ };
729
852
  /**
730
- * Starts the background check when one is due. Records the attempt first, so
731
- * commands started in the next moments do not start one too.
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.
732
856
  */
733
- const startUpdateCheck = (file = updateCachePath(), now = /* @__PURE__ */ new Date()) => {
734
- try {
735
- const cache = readUpdateCache(file);
736
- if (!checkDue(cache, now)) return;
737
- writeUpdateCache(file, {
738
- ...cache,
739
- checkedAt: now.toISOString()
740
- });
741
- const child = spawn(process.execPath, [...process.execArgv, process.argv[1] ?? ""], {
742
- detached: true,
743
- stdio: "ignore",
744
- env: {
745
- ...process.env,
746
- [UPDATE_CHILD_ENV]: "1"
747
- }
748
- });
749
- child.on("error", () => {});
750
- child.unref();
751
- } catch {}
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
+ };
752
865
  };
753
- /** The background copy's whole job: ask the registry, record the answer. */
754
- const runUpdateChild = async () => {
755
- const registry = registryOf(process.env);
756
- await refreshUpdateCache(updateCachePath(), ORK_NAME, /* @__PURE__ */ new Date(), {
757
- current: ORK_VERSION,
758
- ...registry === void 0 ? {} : { registry }
759
- });
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);
760
870
  };
761
- /** Prints the notice when a newer version is known and not told today. */
762
- const showUpdateNotice = (file = updateCachePath(), now = /* @__PURE__ */ new Date()) => {
763
- try {
764
- const cache = readUpdateCache(file);
765
- const latest = pendingNotice(cache, ORK_VERSION, now);
766
- if (cache === null || latest === null) return;
767
- const text = updateNotice(latest, ORK_VERSION);
768
- process.stderr.write(`\n${styleText("yellow", text, { stream: process.stderr })}\n`);
769
- writeUpdateCache(file, {
770
- ...cache,
771
- notified: {
772
- version: latest,
773
- at: now.toISOString()
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();
774
924
  }
775
925
  });
776
- } catch {}
926
+ io.output.on("resize", render);
927
+ signal?.addEventListener("abort", onAbort, { once: true });
928
+ render();
929
+ });
777
930
  };
778
- // src/update/upgrade.ts
931
+ // src/term/select.ts
779
932
  /**
780
- * @file `ork doctor`'s line for Ork itself (ADR-038): the running version
781
- * against the last update check, read from the record without asking the
782
- * network. What `ork upgrade` does is core's (`planUpgrade`, `runInstall`),
783
- * shared with the server behind `ork ui`.
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.
784
941
  */
785
- /** `ork doctor`'s line for Ork itself, from the last check. */
786
- const versionCheck = (current, cache, checksOff) => {
787
- const newer = newerVersion$1(cache, current);
788
- if (newer !== null) return {
789
- name: "ork",
790
- status: "warn",
791
- detail: `${current}; ${newer} is out`,
792
- hint: "Run ork upgrade."
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`);
793
969
  };
794
- return {
795
- name: "ork",
796
- status: "ok",
797
- detail: `${current} (${cache?.latest === void 0 ? checksOff ? "update check off" : "not checked for updates yet" : `the latest as of ${cache.checkedAt.slice(0, 10)}`})`
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
+ };
798
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
+ });
799
1139
  };
800
- // src/doctor/checks.ts
1140
+ // src/human.ts
801
1141
  /**
802
- * @file The checks behind `ork doctor`: Ork's own version against the last
803
- * update check, each tool's presence and login status (the drivers' own
804
- * checks, which `ork run` also reads to pick the one ready), and whether a
805
- * run's socket path fits, through an injectable `exec` so tests need no real
806
- * tools. Never prints a token or an account.
1142
+ * @file The terminal `HumanChannel`: answers `ask_user` questions, gates and
1143
+ * permission prompts on the terminal, one at a time, and denies or defers
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`.
807
1149
  */
808
- const TIMEOUT_MS = 15e3;
809
- const exec = (program, args) => new Promise((resolve) => {
810
- execFile(program, args, {
811
- timeout: TIMEOUT_MS,
812
- encoding: "utf8"
813
- }, (error, stdout) => {
814
- resolve({
815
- ok: error === null,
816
- stdout
817
- });
1150
+ const PREVIEW = 160;
1151
+ /** A document longer than this is offered (`d`) rather than printed. */
1152
+ const SHORT_DOCUMENT_LINES = 40;
1153
+ const NO_ONE_TO_PERMIT = "No one is at the terminal to approve this tool, so it is not allowed in this run.";
1154
+ const NOTE_MESSAGE = "Note for the run (optional, Enter to skip)";
1155
+ const NO_ONE_TO_ASK = "no one (nobody is at the terminal; use your best judgment and say so in your report)";
1156
+ /** One line that shows what a tool is about to do. */
1157
+ const previewInput = (toolName, input) => {
1158
+ const key = TOOL_INPUT_KEYS.find((k) => typeof input[k] === "string");
1159
+ const line = (key === void 0 ? JSON.stringify(input) : `${key}: ${String(input[key])}`).replaceAll(/\s+/gu, " ").trim();
1160
+ return line.length > PREVIEW ? `${toolName} ${line.slice(0, 159)}…` : `${toolName} ${line}`;
1161
+ };
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));
818
1166
  });
819
1167
  });
820
- /** Ork's own version, against the last update check (no network). */
821
- const orkCheck = () => versionCheck(ORK_VERSION, readUpdateCache(updateCachePath()), updateCheckOff(process.env, [], true) !== null);
822
- const MIN_NODE = [24, 20];
823
- const checkNode = (version) => {
824
- const [major = 0, minor = 0] = version.split(".").map(Number);
825
- const [minMajor, minMinor] = MIN_NODE;
826
- return major > minMajor || major === minMajor && minor >= minMinor ? {
827
- name: "node",
828
- status: "ok",
829
- detail: `v${version}`
830
- } : {
831
- name: "node",
832
- status: "fail",
833
- detail: `v${version}`,
834
- hint: `Ork needs Node ${MIN_NODE.join(".")} or later.`
1168
+ /**
1169
+ * Answers permission prompts and `ask_user` questions on the terminal, one at a
1170
+ * time. Without a terminal it denies tools and leaves questions to the node. A
1171
+ * withdrawn prompt (answered elsewhere) rejects, which frees the turn.
1172
+ */
1173
+ const createTerminalHuman = (io) => {
1174
+ let rl;
1175
+ let closed;
1176
+ let turn = Promise.resolve();
1177
+ const say = (text) => {
1178
+ io.output.write(`${text}\n`);
1179
+ };
1180
+ /** The one `readline` the plain prompts share; a rich prompt never opens it. */
1181
+ const readLine = (text, signal) => {
1182
+ if (rl === void 0) {
1183
+ const opened = createInterface({
1184
+ input: io.input,
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
+ });
1193
+ });
1194
+ rl = opened;
1195
+ }
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 }
1205
+ };
1206
+ const term = () => termOf(io.output, io.env);
1207
+ /** The optional line after a choice, when the asker takes a note. */
1208
+ const takeNote = async (onNote, signal) => {
1209
+ if (onNote === void 0) return;
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);
1216
+ };
1217
+ const oneAtATime = (work) => {
1218
+ const result = turn.then(work);
1219
+ turn = result.catch(() => null);
1220
+ return result;
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
+ });
1262
+ return {
1263
+ permit: ({ nodeId, toolName, input, signal, onNote }) => oneAtATime(async () => {
1264
+ if (signal?.aborted === true) throw new Error("the prompt was withdrawn");
1265
+ const t = term();
1266
+ if (t.rich) say("");
1267
+ say(permitLine(t, nodeId, previewInput(toolName, input)));
1268
+ if (!io.interactive) {
1269
+ say(paint(t, "muted", ` denied: ${NO_ONE_TO_PERMIT}`));
1270
+ return {
1271
+ allow: false,
1272
+ message: NO_ONE_TO_PERMIT
1273
+ };
1274
+ }
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 };
1281
+ await takeNote(onNote, signal);
1282
+ return {
1283
+ allow: false,
1284
+ message: "The user did not allow this tool."
1285
+ };
1286
+ }),
1287
+ ask: async (q) => await asking(q) ?? NO_ONE_TO_ASK,
1288
+ askAt: (q, place) => asking(q, place),
1289
+ close: () => {
1290
+ rl?.close();
1291
+ }
835
1292
  };
836
1293
  };
837
- const checkGit = async (run) => {
838
- const version = await run("git", ["--version"]);
839
- if (!version.ok) return [{
840
- name: "git",
841
- status: "fail",
842
- detail: "not found",
843
- hint: "Install git; Ork gives each writing node its own worktree."
844
- }];
845
- const inside = await run("git", ["rev-parse", "--is-inside-work-tree"]);
846
- return [{
847
- name: "git",
848
- status: "ok",
849
- detail: firstLine(version.stdout)
850
- }, inside.ok ? {
851
- name: "git repo",
852
- status: "ok",
853
- detail: "inside a work tree"
854
- } : {
855
- name: "git repo",
856
- status: "warn",
857
- detail: "not a git repository",
858
- hint: "Run Ork inside a git repository so nodes can use worktrees."
859
- }];
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}` : ""}`;
860
1307
  };
861
1308
  /**
862
- * Runs every check: Node and git, then every registered Claude driver and
863
- * integration (ADR-022). Node, git, and at least one driver are required;
864
- * integrations are optional.
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.
865
1313
  */
866
- const runChecks = async (run, env, nodeVersion) => {
867
- const [git, checked, tools] = await Promise.all([
868
- checkGit(run),
869
- driverChecks(run, env),
870
- Promise.all(INTEGRATIONS.flatMap((i) => i.doctor === void 0 ? [] : [i.doctor(run, env)]))
871
- ]);
872
- const drivers = checked.map(({ result }) => result);
873
- const results = [
874
- checkNode(nodeVersion),
875
- ...git,
876
- ...drivers,
877
- ...tools
878
- ];
879
- return {
880
- results,
881
- ok: drivers.some((d) => d.status === "ok") && !results.some((r) => r.status === "fail")
882
- };
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;
883
1322
  };
884
1323
  /**
885
- * Whether a run's socket fits under `~` (L38). Past the limit a run can't
886
- * listen, and nothing else says so: answers, commands and followers fall back
887
- * to the store, a second at a time. `home` is the user's `~`.
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.
888
1327
  */
889
- const socketCheck = (home, platform) => {
890
- const { bytes, max } = socketRoom(home, platform);
891
- if (max === void 0 || bytes <= max) return {
892
- name: "sockets",
893
- status: "ok",
894
- detail: max === void 0 ? "not Unix sockets here" : `${String(bytes)} of ${String(max)} bytes`
895
- };
896
- return {
897
- name: "sockets",
898
- status: "warn",
899
- detail: `a run's socket path is ${String(bytes)} bytes, past the ${String(max)} this system allows`,
900
- hint: "Runs still work, but answers, commands and `ork logs --follow` wait up to a second for each, as they read the store instead. A shorter home folder path fixes it."
901
- };
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}`)];
902
1336
  };
1337
+ // src/run/format-outcome.ts
903
1338
  /**
904
- * What it says of triggers (RFC-011): a long-lived process can die quietly, so
905
- * a live trigger that hasn't looked when it should have is worth saying. `home`
906
- * is the user's `~` (tests pass a folder).
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.
907
1341
  */
908
- const triggerChecks = (home) => {
909
- const store = openStore(storePath(home));
910
- try {
911
- const report = triggersReport(store, store.projects(), home);
912
- const broken = report.broken.map(({ file, message }) => ({
913
- name: "triggers",
914
- status: "warn",
915
- detail: `${file}: ${message}`,
916
- hint: "Its triggers are off until it can be read."
917
- }));
918
- const live = report.triggers.filter(({ standing }) => standing === "live");
919
- if (live.length === 0) return broken;
920
- const late = lateTriggers(report, /* @__PURE__ */ new Date());
921
- if (late.length === 0) return [...broken, {
922
- name: "ork watch",
923
- status: "ok",
924
- detail: `${String(live.length)} trigger(s) looking`
925
- }];
926
- return [...broken, {
927
- name: "ork watch",
928
- status: "warn",
929
- detail: `not looking: ${late.map(({ trigger }) => trigger).join(", ")}`,
930
- hint: report.pid === void 0 ? "Start it with `ork watch --detach`." : "See `ork watch --status`."
931
- }];
932
- } finally {
933
- store.close();
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"
934
1358
  }
935
1359
  };
936
- // src/run/ready-driver.ts
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
+ };
1388
+ // src/run/format-result.ts
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"
1426
+ };
1427
+ /** `1 failure handled`, `2 failures handled`. */
1428
+ const failuresHandled = (count) => `${String(count)} ${count === 1 ? "failure" : "failures"} handled`;
937
1429
  /**
938
- * @file The driver a run takes for the session nodes that name none (core's
939
- * `driver.ts` has the whole rule): the one `ork doctor`'s own checks find
940
- * ready on this machine, said in one line. With none ready, the run stops
941
- * before any session starts, with the hints `ork doctor` gives. A resumed run
942
- * keeps the driver it recorded.
1430
+ * The run summary: status, cost, what ran, what was skipped and which failures
1431
+ * an edge handled, branches to look at, why, and mail to nodes that never ran.
943
1432
  */
944
- /** The real checks: the tools on this machine and its environment. */
945
- const checksHere = () => driverChecks(exec, process.env);
946
- const subject = (names, all) => {
947
- if (all) return "No driver is named, so the run uses";
948
- const [only] = names;
949
- return names.length === 1 && only !== void 0 ? `${only} names no driver, so it uses` : `${names.join(", ")} name no driver, so they use`;
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);
1437
+ const ran = result.nodes.filter((n) => n.runs > 0).length;
1438
+ const skipped = result.skipped ?? result.nodes.length - ran;
1439
+ const handled = result.failuresHandled ?? 0;
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)}`}`)}`];
1441
+ for (const kept of result.worktrees) lines.push(` ${kept.id}: ${kept.branch ?? "detached"} ${style("dim", `at ${kept.path} (${kept.reason})`)}`);
1442
+ for (const reason of result.reasons) lines.push(` ${style("red", reason)}`);
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`);
1444
+ return lines;
950
1445
  };
1446
+ // src/run/format-tool.ts
951
1447
  /**
952
- * The driver for `names`, the session nodes that name none, from `checks`.
953
- * `all` says whether they are every session node of the run.
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.
954
1453
  */
955
- const chooseReadyDriver = (names, all, checks) => {
956
- const ready = readyDriver(checks);
957
- if (!ready.ok) return {
958
- ok: false,
959
- lines: [
960
- `No Claude driver is ready here, and ${names.join(", ")} ${names.length === 1 ? "names" : "name"} none.`,
961
- ...ready.hints,
962
- "See `ork doctor`."
963
- ]
964
- };
965
- const why = ready.ready === 1 ? `the one ready here (${ready.detail})` : `more than one is ready, and a run takes ${ready.driver} first`;
966
- return {
967
- ok: true,
968
- driver: ready.driver,
969
- line: `${subject(names, all)} the ${ready.driver} driver: ${why}.`
970
- };
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}/`, "");
971
1470
  };
972
1471
  /**
973
- * What a run takes for its session nodes that name no driver: nothing when
974
- * `--driver` overrides them all or each names one; the driver a resumed run
975
- * recorded; otherwise the one ready here.
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.
976
1474
  */
977
- const runReadyDriver = async (workflow, options) => {
978
- if (options.override !== void 0) return { ok: true };
979
- const names = namingNoDriver(workflow);
980
- if (names.length === 0) return { ok: true };
981
- if (options.recorded !== void 0) return {
982
- ok: true,
983
- driver: options.recorded
984
- };
985
- const sessions = Object.values(workflow.nodes).filter((node) => node.type === "claude.leader" || node.type === "claude.worker").length;
986
- return chooseReadyDriver(names, names.length === sessions, await (options.checks ?? checksHere)());
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
+ };
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;
1500
+ /** Who answered a decision, as the log says it. */
1501
+ const ANSWERED_BY = {
1502
+ terminal: "terminal",
1503
+ decide: "ork decide",
1504
+ ui: "ork ui",
1505
+ owner: "its owner"
1506
+ };
1507
+ /** The transcript lines for one event; tool names come from `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);
1511
+ if (event.type === "session.started") {
1512
+ const fork = event.fork === true ? ", a fork to answer a question" : "";
1513
+ return [style("dim", `▸ ${nodeId} (${event.model}, session ${event.sessionId}${fork})`)];
1514
+ }
1515
+ if (event.type === "assistant.text") return term.rich ? wrapped(event.text, room) : indent(event.text);
1516
+ if (event.type === "session.waiting") return indent(event.reason);
1517
+ if (event.type === "tool.called") {
1518
+ tools.set(event.id, event.name);
1519
+ return toolLines(event.name, event.input, view, room, verbose, nodeId);
1520
+ }
1521
+ if (event.type === "tool.result") return event.isError ? [style("red", ` ✗ ${tools.get(event.id) ?? "tool"} failed`)] : [];
1522
+ if (event.type === "plan.limit") return event.status === "allowed" ? [] : [style("yellow", ` ! plan: ${event.window.replaceAll("_", " ")} limit ${event.status === "rejected" ? "reached" : "is close"}`)];
1523
+ return formatOutcome(nodeId, event.outcome, verbose, term);
1524
+ };
1525
+ const items = (count) => `${String(count)} ${count === 1 ? "item" : "items"}`;
1526
+ /** Transcript lines for one run event, prefixed with the node they are from. */
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
+ }
1541
+ if (event.type === "node.started") {
1542
+ const round = event.round > 1 ? ` (round ${String(event.round)})` : "";
1543
+ const owner = event.owner === void 0 ? "" : `, for ${event.owner}`;
1544
+ return [`${tag(event.id)} ${style("dim", `${event.nodeType}${round}${owner}`)}`];
1545
+ }
1546
+ if (event.type === "node.event") {
1547
+ const inner = event.event;
1548
+ if (inner.type === "session.finished") return [];
1549
+ const fork = inner.fork === true ? style("dim", "fork · ") : "";
1550
+ if (inner.type === "session.started") {
1551
+ const why = inner.fork === true ? ", to answer a question" : "";
1552
+ return [`${tag(event.id)} ${fork}${style("dim", `${inner.model}, session ${inner.sessionId}${why}`)}`];
1553
+ }
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")}`));
1556
+ }
1557
+ if (event.type === "node.finished") return formatFinished(event, tag(event.id), verbose, term, blank(event.id));
1558
+ if (event.type === "node.skipped") return [`${tag(event.id)} ${style("dim", "- skipped")}`];
1559
+ if (event.type === "loop.exhausted") return [`${style(["yellow", "bold"], "!")} the loop ${event.from} -> ${event.to} used all ${String(event.maxRounds)} rounds`];
1560
+ if (event.type === "watch.started") return [`${tag(event.id)} ${style("dim", `watching until ${event.until}, every ${event.every}`)}`];
1561
+ if (event.type === "watch.changed") return [`${tag(event.id)} ${style("dim", `look ${String(event.looks)}:`)} ${event.said}`];
1562
+ if (event.type === "watch.ended") return [`${tag(event.id)} ${style("dim", `${event.outcome} after ${String(event.looks)} looks`)}`];
1563
+ if (event.type === "edge.taken") return [];
1564
+ if (event.type === "worktree") {
1565
+ if (event.action !== "removed") view.roots.set(event.id, event.path);
1566
+ const detail = event.detail === void 0 ? "" : ` (${event.detail})`;
1567
+ const what = event.action === "removed" && event.branch !== void 0 ? `worktree of ${event.branch} (branch kept)` : `${event.branch ?? event.path}${detail}`;
1568
+ const line = `${event.action} ${what}`;
1569
+ return [`${tag(event.id)} ${event.action === "conflict" ? style("yellow", `! ${line}`) : style("dim", `⎇ ${line}`)}`];
1570
+ }
1571
+ if (event.type === "decision.opened") {
1572
+ const what = event.kind === "permission" ? "asks to" : "asks:";
1573
+ const question = event.kind === "permission" ? event.question.replace(/^Allow /u, "run ").replace(/\?$/u, "") : event.question;
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}`;
1575
+ return [`${tag(event.node)} ${term.rich ? chip(term, CHIP_LABELS.ask, "accent") : style(["yellow", "bold"], "?")} ${what} ${question} ${style("dim", `(${where})`)}`];
1576
+ }
1577
+ if (event.type === "decision.answered") {
1578
+ const by = event.by === "owner" && event.owner !== void 0 ? event.owner : ANSWERED_BY[event.by];
1579
+ const said = event.note === void 0 ? "" : ` · said: ${event.note.replaceAll(/\s+/gu, " ")}`;
1580
+ return [`${tag(event.node)} ${style("dim", `→ ${event.answer} (${by})${said}`)}`];
1581
+ }
1582
+ if (event.type === "decision.withdrawn") return [`${tag(event.node)} ${style("dim", "→ question withdrawn")}`];
1583
+ if (event.type === "run.command") {
1584
+ const node = event.node === void 0 ? "" : ` --node ${event.node}`;
1585
+ return [style(["yellow", "bold"], `! ork ${event.command}${node} received`)];
1586
+ }
1587
+ if (event.type === "node.held") {
1588
+ const at = event.at === "tool" ? "its tool call waits for ork continue" : "it starts after ork continue";
1589
+ return [`${tag(event.id)} ${style("yellow", `‖ paused: ${at}`)}`];
1590
+ }
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")}`];
1594
+ if (event.type === "each.listed") {
1595
+ const round = event.round > 1 ? ` (round ${String(event.round)})` : "";
1596
+ return [`${tag(event.node)} ${style("dim", `${items(event.count)}${round}`)}`];
1597
+ }
1598
+ if (event.type === "each.exceeded") {
1599
+ const over = `${items(event.count)}, over maxItems ${String(event.maxItems)}`;
1600
+ return [`${tag(event.node)} ${style("yellow", `! ${over}`)}`];
1601
+ }
1602
+ if (event.type === "run.finished") return formatRunResult(event.result, term);
1603
+ if (event.type === "run.resumed") {
1604
+ const [one] = event.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`}`)];
1606
+ }
1607
+ if (event.type === "feedback.recorded") {
1608
+ const { target, kind, title } = event.feedback;
1609
+ const line = style("dim", `✎ feedback (${target}, ${kind}): ${title}`);
1610
+ return [event.node === void 0 ? line : `${tag(event.node)} ${line}`];
1611
+ }
1612
+ if (event.type === "user.message") {
1613
+ const note = event.delivered === "queued" ? " (for its next session)" : "";
1614
+ return [`${tag(event.node)} ${style(["magenta", "bold"], "you:")} ${firstLine(event.body)}${style("dim", note)}`];
1615
+ }
1616
+ return under(event.node, formatBusEvent(event, term));
987
1617
  };
988
1618
  // src/runs/chat.ts
989
1619
  /**
@@ -994,12 +1624,11 @@ const runReadyDriver = async (workflow, options) => {
994
1624
  * where it ran, as the run's sessions were set up (ADR-065), and the exchange
995
1625
  * is added to the run's record.
996
1626
  */
997
- /** The declared node a spawned worker (`<template>-<n>`) is an instance of. */
998
- const templateOf = (nodes, id) => {
999
- if (id in nodes) return void 0;
1000
- const template = id.replace(/-\d+$/u, "");
1001
- return template in nodes ? template : void 0;
1002
- };
1627
+ /**
1628
+ * The declared node a spawned worker or an item of a fanned node is an instance
1629
+ * of.
1630
+ */
1631
+ const instanceOf = (nodes, id) => id in nodes ? void 0 : templateOf(id, new Set(Object.keys(nodes)));
1003
1632
  /** The run's sessions loaded the machine's Claude setup, so this one does. */
1004
1633
  const machineClaudeOf = (events) => events.some(({ event }) => event.type === "run.started" && event.settings?.machineClaude === true) ? { machineClaude: true } : {};
1005
1634
  /**
@@ -1020,7 +1649,7 @@ const resumeChat = async (options) => {
1020
1649
  const notice = chatNotice(store, place, options.home);
1021
1650
  if (notice !== null) options.out(notice);
1022
1651
  const runDir = runDirOf(place, run.id);
1023
- const template = templateOf(workflow.nodes, node);
1652
+ const template = instanceOf(workflow.nodes, node);
1024
1653
  const plan = planNodeSession(workflow, node, {
1025
1654
  cwd,
1026
1655
  inputs: {},
@@ -1035,10 +1664,12 @@ const resumeChat = async (options) => {
1035
1664
  ...template === void 0 ? {} : { template }
1036
1665
  });
1037
1666
  if (!plan.ok) return plan.error;
1667
+ const servers = namedMcpEntries(run.project, plan.mcp);
1668
+ if (!servers.ok) return servers.error;
1038
1669
  let driver = options.driver ?? session.driver ?? plan.driver;
1039
1670
  if (driver === void 0) {
1040
1671
  const ready = readyDriver(await (options.driverChecks ?? checksHere)());
1041
- if (!ready.ok) return ["No Claude driver is ready here.", ...ready.hints].join(" ");
1672
+ if (!ready.ok) return ["No agent driver is ready here.", ...ready.hints].join(" ");
1042
1673
  driver = ready.driver;
1043
1674
  }
1044
1675
  const record = appendRecorder(store, run.id, runDir);
@@ -1048,8 +1679,9 @@ const resumeChat = async (options) => {
1048
1679
  body: options.body,
1049
1680
  delivered: "resumed"
1050
1681
  });
1051
- const started = (await options.runtime(driver)).start({
1682
+ const started = (await options.runtime(driver, placeFiles(place).runs)).start({
1052
1683
  ...plan.spec,
1684
+ ...servers.value,
1053
1685
  resume: session.sessionId,
1054
1686
  prompt: options.body,
1055
1687
  ...machineClaudeOf(events)
@@ -1080,16 +1712,27 @@ const resumeChat = async (options) => {
1080
1712
  /** Turns stored events into lines, keeping the state a transcript needs. */
1081
1713
  const createLogPrinter = (options) => {
1082
1714
  const tools = /* @__PURE__ */ new Map();
1715
+ const view = createView();
1083
1716
  let width = 8;
1084
1717
  return (stored) => {
1085
1718
  const { event } = stored;
1086
1719
  if (event.type === "run.started") width = Math.max(...event.nodes.map((n) => n.id.length), 1);
1087
- if (options.node !== void 0 && nodeOf(event) !== options.node) return [];
1720
+ if (options.node !== void 0 && !isNodeEvent(event, options.node)) return [];
1088
1721
  if (options.json) return [JSON.stringify(stored)];
1089
- return formatRunEvent(event, width, tools);
1722
+ return formatRunEvent(event, width, tools, false, options.verbose === true || options.node !== void 0, view);
1090
1723
  };
1091
1724
  };
1092
1725
  /**
1726
+ * Why `--node` names nothing in this run, or `null`: no event names it and the
1727
+ * run didn't declare it. A live run may still start it, so that is not
1728
+ * refused.
1729
+ */
1730
+ const unknownNode = (stored, node, live) => {
1731
+ const named = stored.some(({ event }) => isNodeEvent(event, node));
1732
+ const declared = stored.some(({ event }) => event.type === "run.started" && event.nodes.some((n) => n.id === node));
1733
+ return named || declared || live ? null : `Run has no node ${node}.`;
1734
+ };
1735
+ /**
1093
1736
  * Prints a run's events; with `follow`, then each new one as it is stored:
1094
1737
  * pushed over the run's socket, or polled from the store when there is none.
1095
1738
  * Returns when `run.finished` is stored, the run's process is gone, or `signal`
@@ -1107,6 +1750,59 @@ const followRun = async (store, run, out, options) => {
1107
1750
  ...options.signal === void 0 ? {} : { signal: options.signal }
1108
1751
  }) === "crashed") out(style("red", `run ${run.id} stopped without finishing (its process is gone)`));
1109
1752
  };
1753
+ // src/run/node-notes.ts
1754
+ /**
1755
+ * @file What a run of one node (`ork run --node`) leaves out, said before its
1756
+ * session starts: the messages, steps and `forEach` it does not run. A node
1757
+ * with `forEach` (RFC-013) runs once with no item when it has `items`, and is
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).
1761
+ */
1762
+ /** A warning line, as the run prints it. */
1763
+ const noteLines = (lines) => lines.map((line) => `${style(["yellow", "bold"], "warning")} ${line}`);
1764
+ /** Why a node can't run alone, or what a run of it leaves out. */
1765
+ const oneNodeNotes = (workflow, id) => {
1766
+ const node = workflow.nodes[id];
1767
+ if (node === void 0) return { warnings: [] };
1768
+ const each = "forEach" in node ? node.forEach : void 0;
1769
+ if (each !== void 0 && "from" in each) return {
1770
+ error: `\`${id}\` runs once for each item of ${each.from}, a list another node reports, and a run of one node does not run that node; run the whole workflow, or give it \`items\` of its own`,
1771
+ warnings: []
1772
+ };
1773
+ const steps = [...beforeOf(node, workflow.defaults) === void 0 ? [] : ["before"], ..."verify" in node && node.verify !== void 0 ? ["verify"] : []];
1774
+ return { warnings: [
1775
+ ..."notify" in node && node.notify ? [`${id}'s notify: does nothing here: a run of one node has no message bus.`] : [],
1776
+ ...each === void 0 ? [] : [`${id}'s forEach: does not fan out in a run of one node: it runs once, with no item.`],
1777
+ ...steps.map((step) => `${id}'s ${step}: does not run in a run of one node.`)
1778
+ ] };
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
+ };
1110
1806
  // src/run/origin.ts
1111
1807
  /**
1112
1808
  * @file Where a run's feedback comes from, beyond what its events say: Ork's
@@ -1114,6 +1810,8 @@ const followRun = async (store, run, out, options) => {
1114
1810
  * content hash.
1115
1811
  */
1116
1812
  /** Ork's version and the workflow's hash, for the run's feedback records. */
1813
+ /** The Ork version a run records in `run.started` and `run.resumed`. */
1814
+ const ORK_RUN_VERSION = ORK_VERSION;
1117
1815
  const feedbackOrigin = (workflowFile) => {
1118
1816
  const origin = { version: ORK_VERSION };
1119
1817
  try {
@@ -1147,6 +1845,8 @@ const runNode = async (options) => {
1147
1845
  if (!pairs.ok) return failed([`--input: ${pairs.error}`]);
1148
1846
  const inputs = resolveInputs(workflow, pairs.raw);
1149
1847
  if (!inputs.ok) return failed(inputs.errors);
1848
+ const notes = oneNodeNotes(workflow, options.nodeId);
1849
+ if (notes.error !== void 0) return failed([notes.error]);
1150
1850
  const limits = runLimits(options.store, place, options.home);
1151
1851
  const refused = limitsRefusal(limits, workflow);
1152
1852
  if (refused.length > 0) return failed(refused);
@@ -1166,6 +1866,8 @@ const runNode = async (options) => {
1166
1866
  pluginDir: path.join(runDir, "plugins", options.nodeId)
1167
1867
  });
1168
1868
  if (!plan.ok) return failed([plan.error]);
1869
+ const servers = namedMcpEntries(projectRoot, plan.mcp);
1870
+ if (!servers.ok) return failed([servers.error]);
1169
1871
  const named = sessionDriver({
1170
1872
  override: options.driver,
1171
1873
  named: plan.driver
@@ -1174,12 +1876,13 @@ const runNode = async (options) => {
1174
1876
  if (ready?.ok === false) return failed(ready.lines);
1175
1877
  if (ready?.line !== void 0) options.err(ready.line);
1176
1878
  const driver = named ?? ready?.driver;
1177
- if (driver === void 0) return failed(["No Claude driver is ready here."]);
1178
- const planned = workflow.nodes[options.nodeId];
1179
- if (planned !== void 0 && "notify" in planned && planned.notify) options.err(`${style(["yellow", "bold"], "warning")} ${options.nodeId}'s notify: does nothing here: a run of one node has no message bus.`);
1180
- const skipped = planned === void 0 ? [] : [...beforeOf(planned, workflow.defaults) === void 0 ? [] : ["before"], ..."verify" in planned && planned.verify !== void 0 ? ["verify"] : []];
1181
- for (const step of skipped) options.err(`${style(["yellow", "bold"], "warning")} ${options.nodeId}'s ${step}: does not run in a run of one node.`);
1182
- for (const warning of plan.warnings) options.err(`${style(["yellow", "bold"], "warning")} ${warning}`);
1879
+ if (driver === void 0) return failed(["No agent driver is ready here."]);
1880
+ const unfit = unfitLines(workflow, options.drivers, {
1881
+ override: driver,
1882
+ only: options.nodeId
1883
+ });
1884
+ if (unfit.length > 0) return failed(unfit);
1885
+ for (const line of noteLines([...notes.warnings, ...plan.warnings])) options.err(line);
1183
1886
  const id = options.nodeId;
1184
1887
  const recorder = createRecorder(options.store, {
1185
1888
  id: runId,
@@ -1213,10 +1916,11 @@ const runNode = async (options) => {
1213
1916
  ...options.home === void 0 ? {} : { home: options.home }
1214
1917
  });
1215
1918
  const signals = createSignals(feedbackOrigin(path.resolve(options.cwd, options.file)));
1919
+ const view = createView();
1216
1920
  const record = (raw) => {
1217
1921
  for (const event of signals(raw)) {
1218
1922
  control.publish(recorder(event));
1219
- 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);
1220
1924
  }
1221
1925
  };
1222
1926
  const nodeType = workflow.nodes[id]?.type ?? "claude.worker";
@@ -1237,9 +1941,12 @@ const runNode = async (options) => {
1237
1941
  nodeType,
1238
1942
  round: 1
1239
1943
  });
1240
- const runtime = await options.runtime(driver);
1944
+ const runtime = await options.runtime(driver, placeFiles(place).runs);
1241
1945
  if (options.machineClaude === true) plan.spec.machineClaude = true;
1242
- const session = runtime.start(plan.spec, {
1946
+ const session = runtime.start({
1947
+ ...plan.spec,
1948
+ ...servers.value
1949
+ }, {
1243
1950
  human: control.human,
1244
1951
  ...graph === null ? {} : { feedback: createFeedback({
1245
1952
  graph,
@@ -1258,10 +1965,9 @@ const runNode = async (options) => {
1258
1965
  });
1259
1966
  if (event.type === "session.finished") outcome = event.outcome;
1260
1967
  if (options.json) options.out(JSON.stringify(event));
1261
- 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);
1262
1969
  }
1263
1970
  const result = resultOf(outcome);
1264
- const costUsd = result.usage?.costUsd ?? 0;
1265
1971
  record({
1266
1972
  type: "node.finished",
1267
1973
  id,
@@ -1269,27 +1975,11 @@ const runNode = async (options) => {
1269
1975
  ok: result.ok,
1270
1976
  output: result.output,
1271
1977
  ...result.usage === void 0 ? {} : { usage: result.usage },
1272
- ...result.error === void 0 ? {} : { error: result.error }
1978
+ ...endedFields(result)
1273
1979
  });
1274
1980
  record({
1275
1981
  type: "run.finished",
1276
- result: {
1277
- runId,
1278
- status: succeeded(outcome) ? "succeeded" : "failed",
1279
- costUsd,
1280
- nodes: [{
1281
- id,
1282
- runs: 1,
1283
- ok: result.ok,
1284
- output: result.output,
1285
- costUsd,
1286
- ...result.error === void 0 ? {} : { error: result.error }
1287
- }],
1288
- reasons: result.error === void 0 ? [] : [`${id}: ${result.error}`],
1289
- worktrees: [],
1290
- skipped: 0,
1291
- failuresHandled: 0
1292
- }
1982
+ result: oneNodeResult(runId, id, result, succeeded(outcome))
1293
1983
  });
1294
1984
  await control.close();
1295
1985
  return {
@@ -1498,6 +2188,14 @@ const preflight = async (options, given) => {
1498
2188
  lines: driver.lines
1499
2189
  };
1500
2190
  if (driver.line !== void 0) options.err(driver.line);
2191
+ const unfit = unfitLines(given.effective, options.drivers, {
2192
+ override: options.driver,
2193
+ ready: driver.driver ?? options.resume?.settings.readyDriver
2194
+ });
2195
+ if (unfit.length > 0) return {
2196
+ ok: false,
2197
+ lines: unfit
2198
+ };
1501
2199
  const checkout = await prepareCheckout(workflow, {
1502
2200
  ...options,
1503
2201
  cwd: place.root
@@ -1569,6 +2267,7 @@ const runWorkflowFile = async (options) => {
1569
2267
  }, runDir) : appendRecorder(options.store, runId, runDir);
1570
2268
  const width = Math.max(...Object.keys(workflow.nodes).map((id) => id.length));
1571
2269
  const tools = /* @__PURE__ */ new Map();
2270
+ const view = createView();
1572
2271
  const started = Promise.withResolvers();
1573
2272
  const live = {};
1574
2273
  const control = await startControl({
@@ -1588,7 +2287,7 @@ const runWorkflowFile = async (options) => {
1588
2287
  for (const event of signals(raw)) {
1589
2288
  control.publish(record(event));
1590
2289
  if (options.json) options.out(JSON.stringify(event));
1591
- else for (const line of formatRunEvent(event, width, tools)) options.out(line);
2290
+ else for (const line of formatRunEvent(event, width, tools, options.human !== void 0, options.verbose, view)) options.out(line);
1592
2291
  }
1593
2292
  };
1594
2293
  const workspace = root === void 0 ? void 0 : createWorkspace({
@@ -1605,7 +2304,7 @@ const runWorkflowFile = async (options) => {
1605
2304
  });
1606
2305
  const maskedNames = maskedNamesOfRun(workflow, place, projects);
1607
2306
  const executorOptions = {
1608
- runtimeFor: options.runtime,
2307
+ runtimeFor: (driver) => options.runtime(driver, placeFiles(place).runs),
1609
2308
  inPlace: options.inPlace,
1610
2309
  maskedNames
1611
2310
  };
@@ -1614,10 +2313,11 @@ const runWorkflowFile = async (options) => {
1614
2313
  if (options.tools !== void 0) executorOptions.tools = options.tools;
1615
2314
  const script = process.argv[1];
1616
2315
  if (script !== void 0 && onPath(script)) executorOptions.orkBin = script;
1617
- const replay = options.resume === void 0 ? void 0 : createReplay(options.resume.history, options.resume.cutOff, options.resume.mail, options.resume.earlier);
2316
+ const replay = options.resume === void 0 ? void 0 : createReplay(options.resume.history, options.resume.cutOff, options.resume.mail, options.resume.earlier, options.resume.fanOuts);
1618
2317
  if (replay !== void 0) onEvent({
1619
2318
  type: "run.resumed",
1620
- kept: replay.kept()
2319
+ kept: replay.kept(),
2320
+ ork: ORK_RUN_VERSION
1621
2321
  });
1622
2322
  const ports = callPorts({
1623
2323
  ...options,
@@ -1639,6 +2339,7 @@ const runWorkflowFile = async (options) => {
1639
2339
  executors: builtinExecutors(executorOptions),
1640
2340
  human: options.resume === void 0 ? control.human : replayLoopAnswers(control.human, options.resume.loopAnswers),
1641
2341
  onEvent,
2342
+ orkVersion: ORK_RUN_VERSION,
1642
2343
  settings: {
1643
2344
  inPlace: options.inPlace,
1644
2345
  ...options.driver === void 0 ? {} : { driver: options.driver },
@@ -1653,6 +2354,10 @@ const runWorkflowFile = async (options) => {
1653
2354
  ...workspace === void 0 ? {} : { workspace },
1654
2355
  scrub: runScrubber(maskedNames, process.env),
1655
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
+ },
1656
2361
  beforeFinish: takeLastCommands(control, started.promise)
1657
2362
  });
1658
2363
  live.handle = handle;
@@ -1683,9 +2388,11 @@ const flagValues = (rawArgs, flag) => {
1683
2388
  }
1684
2389
  return values;
1685
2390
  };
1686
- const isDriver = (value) => CLAUDE_DRIVERS.some((d) => d.id === value);
2391
+ const isDriver = (value) => AGENT_DRIVERS.some((d) => d.id === value);
2392
+ /** Why `--driver` refuses a value: a retired id names its replacement. */
2393
+ const badDriver = (value) => `--${retiredDriverHint(value) ?? `driver must be one of ${AGENT_DRIVERS.map((d) => d.id).join(", ")}, got \`${value}\``}`;
1687
2394
  /** Loads the drivers only when a run starts, so other commands stay fast. */
1688
- const loadRuntime = async (driver) => (await import("./src-B9v0lDc6.mjs")).createRuntime(driver, sessionEnv(process.env));
2395
+ const loadRuntime = async (driver, runs) => (await import("./src-BnZgZGrf.mjs")).createRuntime(driver, sessionEnv(process.env), runs);
1689
2396
  /**
1690
2397
  * Ctrl-C asks the run to stop, and so does a terminal closing or a polite kill,
1691
2398
  * so that the run ends tidily and leaves no result unscrubbed.
@@ -1703,11 +2410,18 @@ const onInterrupt = (stop) => {
1703
2410
  process.once("SIGHUP", interrupt);
1704
2411
  };
1705
2412
  /** The terminal that answers a run's questions; none in a detached run. */
1706
- const terminalHuman = () => process.env["ORK_DETACHED"] === "1" ? void 0 : createTerminalHuman({
1707
- input: process.stdin,
1708
- output: process.stderr,
1709
- interactive: process.stdin.isTTY
1710
- });
2413
+ const terminalHuman = () => {
2414
+ const detached = process.env[DETACHED_ENV] === "1";
2415
+ if (detached) writeErr(style("dim", "detached run: no terminal asks; answer with ork decide"));
2416
+ return detached ? void 0 : createTerminalHuman({
2417
+ input: process.stdin,
2418
+ output: process.stderr,
2419
+ interactive: process.stdin.isTTY,
2420
+ interrupt: () => {
2421
+ process.kill(process.pid, "SIGINT");
2422
+ }
2423
+ });
2424
+ };
1711
2425
  const run = defineCommand({
1712
2426
  meta: {
1713
2427
  name: "run",
@@ -1729,7 +2443,7 @@ const run = defineCommand({
1729
2443
  },
1730
2444
  driver: {
1731
2445
  type: "string",
1732
- description: "sdk (API key or cloud provider) or cli (your claude login)"
2446
+ description: "claude-code (default: the driver ready here)"
1733
2447
  },
1734
2448
  "in-place": {
1735
2449
  type: "boolean",
@@ -1751,6 +2465,11 @@ const run = defineCommand({
1751
2465
  description: "Print the events as JSON lines",
1752
2466
  default: false
1753
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
+ },
1754
2473
  detach: {
1755
2474
  type: "boolean",
1756
2475
  description: "Run in the background; follow it with ork logs -f, answer with ork decide",
@@ -1787,7 +2506,7 @@ const run = defineCommand({
1787
2506
  },
1788
2507
  run: async ({ args, rawArgs }) => {
1789
2508
  if (args.driver !== void 0 && !isDriver(args.driver)) {
1790
- writeErr(`--driver must be ${CLAUDE_DRIVERS.map((d) => d.id).join(" or ")}, got \`${args.driver}\``);
2509
+ writeErr(badDriver(args.driver));
1791
2510
  process.exitCode = 1;
1792
2511
  return;
1793
2512
  }
@@ -1832,6 +2551,7 @@ const run = defineCommand({
1832
2551
  inPlace: args["in-place"],
1833
2552
  ...args["machine-claude"] ? { machineClaude: true } : {},
1834
2553
  json: args.json,
2554
+ verbose: args.verbose,
1835
2555
  runtime: loadRuntime,
1836
2556
  store,
1837
2557
  ...human === void 0 ? {} : { human },
@@ -1877,22 +2597,25 @@ const run = defineCommand({
1877
2597
  * each message at its session's next step, and its replies stream here; a
1878
2598
  * node of a finished run is answered by resuming its last session.
1879
2599
  */
1880
- /** Asks for one line; `null` once the input ends (Ctrl-D). */
1881
- const askLine = async () => {
1882
- 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({
1883
2607
  input: process.stdin,
1884
- output: process.stdout
1885
- });
1886
- const closed = new Promise((resolve) => {
1887
- rl.once("close", () => {
1888
- resolve(null);
1889
- });
1890
- });
1891
- try {
1892
- return await Promise.race([rl.question(style("bold", "you › ")), closed]);
1893
- } finally {
1894
- rl.close();
1895
- }
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;
1896
2619
  };
1897
2620
  /** Chats with a node of a live run until the input ends or the run does. */
1898
2621
  const chatLive = async (store, run, node, message) => {
@@ -1914,7 +2637,7 @@ const chatLive = async (store, run, node, message) => {
1914
2637
  abort.abort();
1915
2638
  });
1916
2639
  for (;;) {
1917
- const line = await Promise.race([askLine(), following.then(() => null)]);
2640
+ const line = await Promise.race([askLine(true, abort.signal), following.then(() => null)]);
1918
2641
  if (line === null || abort.signal.aborted) break;
1919
2642
  if (line.trim() !== "") await tellLiveRun(store, run, node, line);
1920
2643
  }
@@ -1945,7 +2668,7 @@ const chat = defineCommand({
1945
2668
  },
1946
2669
  driver: {
1947
2670
  type: "string",
1948
- description: "For a finished run: sdk or cli (default: the one it ran with)"
2671
+ description: "For a finished run: claude-code (default: the one it ran with)"
1949
2672
  }
1950
2673
  },
1951
2674
  run: async ({ args }) => {
@@ -1971,7 +2694,7 @@ const chat = defineCommand({
1971
2694
  return;
1972
2695
  }
1973
2696
  if (args.driver !== void 0 && !isDriver(args.driver)) {
1974
- writeErr(`--driver must be sdk or cli, not \`${args.driver}\`.`);
2697
+ writeErr(`${badDriver(args.driver)}.`);
1975
2698
  process.exitCode = 1;
1976
2699
  return;
1977
2700
  }
@@ -2010,13 +2733,35 @@ const chat = defineCommand({
2010
2733
  }
2011
2734
  }
2012
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
+ };
2013
2757
  // src/commands/decide.ts
2014
2758
  /**
2015
2759
  * @file `ork decide`: answer a live run's open decisions from any terminal (it
2016
2760
  * writes to the store, then wakes the run on its socket; specs/messaging.md).
2017
2761
  * `--say` gives the answer a note, which goes to the run with it. With `--id`
2018
2762
  * and `--choose`, `--note` and `--rating` instead note how the decision's
2019
- * node went, as the run's feedback (specs/feedback.md).
2763
+ * node went, as the run's feedback (specs/feedback.md). `--document` prints
2764
+ * what a gate gave to read beside its question.
2020
2765
  */
2021
2766
  const failed$2 = (message) => {
2022
2767
  writeErr(message);
@@ -2054,8 +2799,6 @@ const saidLine = (store, id) => {
2054
2799
  const note = store.decision(id)?.note;
2055
2800
  return note === void 0 ? "" : style("dim", ` · said: ${note}`);
2056
2801
  };
2057
- /** One line per open decision, for `--list`. */
2058
- const formatDecisions = (decisions) => decisions.length === 0 ? ["No open decisions."] : decisions.map((d) => `${style("bold", d.id)} ${style("cyan", d.node)}${d.owner === void 0 ? "" : style("dim", ` via ${d.owner}`)} ${d.question} ${style("dim", `[${d.options.map((o) => o.id === d.recommended ? `${o.id}*` : o.id).join(", ")}]`)}`);
2059
2802
  const decide = defineCommand({
2060
2803
  meta: {
2061
2804
  name: "decide",
@@ -2082,6 +2825,15 @@ const decide = defineCommand({
2082
2825
  description: "List them as JSON",
2083
2826
  default: false
2084
2827
  },
2828
+ workflow: {
2829
+ type: "string",
2830
+ description: "Only the decisions of this workflow's runs"
2831
+ },
2832
+ document: {
2833
+ type: "boolean",
2834
+ description: "Print the document of the decision --id names",
2835
+ default: false
2836
+ },
2085
2837
  id: {
2086
2838
  type: "string",
2087
2839
  description: "Answer this decision…"
@@ -2101,10 +2853,15 @@ const decide = defineCommand({
2101
2853
  rating: {
2102
2854
  type: "string",
2103
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
2104
2861
  }
2105
2862
  },
2106
2863
  run: async ({ args }) => {
2107
- const refusal = args.id !== void 0 || args.choose !== void 0 || process.stdin.isTTY && !args.list && !args.json ? decideRefusal() : null;
2864
+ const refusal = !args.document && (args.id !== void 0 || args.choose !== void 0 || process.stdin.isTTY && !args.list && !args.json) ? decideRefusal() : null;
2108
2865
  if (refusal !== null) {
2109
2866
  failed$2(refusal);
2110
2867
  return;
@@ -2113,9 +2870,20 @@ const decide = defineCommand({
2113
2870
  const human = createTerminalHuman({
2114
2871
  input: process.stdin,
2115
2872
  output: process.stdout,
2116
- interactive: process.stdin.isTTY
2873
+ interactive: process.stdin.isTTY,
2874
+ verbose: args.verbose
2117
2875
  });
2118
2876
  try {
2877
+ if (args.document) {
2878
+ if (args.id === void 0 || args.choose !== void 0) {
2879
+ failed$2("--document goes with --id, without --choose.");
2880
+ return;
2881
+ }
2882
+ const { document } = store.decision(args.id) ?? {};
2883
+ if (document === void 0) failed$2(`${args.id} has no document.`);
2884
+ else write(document);
2885
+ return;
2886
+ }
2119
2887
  if (args.id !== void 0 || args.choose !== void 0) {
2120
2888
  if (args.id === void 0 || args.choose === void 0) {
2121
2889
  failed$2("--id and --choose go together.");
@@ -2143,10 +2911,11 @@ const decide = defineCommand({
2143
2911
  return;
2144
2912
  }
2145
2913
  if (args.say !== void 0) {
2146
- 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).");
2147
2915
  return;
2148
2916
  }
2149
2917
  const filter = {};
2918
+ if (args.workflow !== void 0) filter.workflow = args.workflow;
2150
2919
  if (args.run !== void 0 && args.run !== "") {
2151
2920
  const found = findRun(store, args.run, process.cwd());
2152
2921
  if (!found.ok) {
@@ -2161,11 +2930,11 @@ const decide = defineCommand({
2161
2930
  return;
2162
2931
  }
2163
2932
  if (args.list || !process.stdin.isTTY || open.length === 0) {
2164
- for (const line of formatDecisions(open)) write(line);
2933
+ for (const line of formatDecisions(open, args.workflow)) write(line);
2165
2934
  return;
2166
2935
  }
2167
- for (const d of open) {
2168
- 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()) {
2169
2938
  const question = {
2170
2939
  nodeId: d.node,
2171
2940
  question: d.question,
@@ -2173,13 +2942,21 @@ const decide = defineCommand({
2173
2942
  };
2174
2943
  if (d.recommended !== void 0) question.recommended = d.recommended;
2175
2944
  if (d.context !== void 0) question.context = d.context;
2945
+ if (d.document !== void 0) question.document = d.document;
2176
2946
  let note;
2177
2947
  question.onNote = (given) => {
2178
2948
  note = given;
2179
2949
  };
2180
- 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;
2181
2957
  const answered = await answerDecision(store, d.id, choice, void 0, "decide", note);
2182
- 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`);
2183
2960
  }
2184
2961
  } finally {
2185
2962
  human?.close();
@@ -2264,7 +3041,7 @@ const resume = defineCommand({
2264
3041
  },
2265
3042
  driver: {
2266
3043
  type: "string",
2267
- description: "sdk or cli (default: the one the run used)"
3044
+ description: "claude-code (default: the one the run used)"
2268
3045
  },
2269
3046
  "in-place": {
2270
3047
  type: "boolean",
@@ -2288,7 +3065,7 @@ const resume = defineCommand({
2288
3065
  },
2289
3066
  run: async ({ args, rawArgs }) => {
2290
3067
  if (args.driver !== void 0 && !isDriver(args.driver)) {
2291
- failed$1(`--driver must be sdk or cli, got \`${args.driver}\``);
3068
+ failed$1(badDriver(args.driver));
2292
3069
  return;
2293
3070
  }
2294
3071
  const store = openStore();
@@ -2339,6 +3116,76 @@ const resume = defineCommand({
2339
3116
  }
2340
3117
  }
2341
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
+ };
3159
+ // src/doctor/lease-checks.ts
3160
+ /**
3161
+ * @file What `ork doctor` says of Command Code leases (RFC-014): it removes the
3162
+ * `ork` MCP entries a crashed session left in its checkout's local scope, and
3163
+ * says where. Never prints a token.
3164
+ */
3165
+ /**
3166
+ * Removes the `ork` MCP entries a crashed Command Code session left in its
3167
+ * checkout's local scope (RFC-014), from the leases in the project's runs
3168
+ * folder. Says nothing when there were none. `bin` is the `cmd` program.
3169
+ */
3170
+ const leaseChecks = async (place, env = process.env, bin = "cmd") => {
3171
+ const { cleanStaleLeases, leaseLedger } = await import("./src-BnZgZGrf.mjs");
3172
+ const { cleaned, failed } = await cleanStaleLeases({
3173
+ bin,
3174
+ ledger: leaseLedger(placeFiles(place).runs),
3175
+ env
3176
+ });
3177
+ const name = "command-code";
3178
+ return [...cleaned.length === 0 ? [] : [{
3179
+ name,
3180
+ status: "ok",
3181
+ detail: `removed the \`ork\` MCP entry a stopped session left in ${cleaned.join(", ")}`
3182
+ }], ...failed.length === 0 ? [] : [{
3183
+ name,
3184
+ status: "warn",
3185
+ detail: `could not remove the \`ork\` MCP entry a stopped session left in ${failed.join(", ")}`,
3186
+ hint: "Run `cmd mcp remove --scope local ork` in each of those folders."
3187
+ }]];
3188
+ };
2342
3189
  // src/project/ork-skills.ts
2343
3190
  /**
2344
3191
  * @file The Claude Code plugin's skills (`plugins/ork/skills/`) as text, by
@@ -2349,8 +3196,8 @@ const resume = defineCommand({
2349
3196
  const ORK_SKILLS = {
2350
3197
  principles: "---\nname: principles\ndescription: Use when deciding how to split work into an Ork workflow - which steps are programs, which are Claude sessions, where a person decides - so it costs less than doing it by hand and can't go wrong quietly.\n---\n\n# How Ork splits work\n\nOrk exists to do long jobs for less than doing them by hand, and safely. Four\nrules decide the shape of a workflow.\n\n## 1. A program for what a program can do\n\nA model turn is for judgment. Anything a program can decide, it should: run the\ntests, read a pull request's state, count what changed, wait for CI.\n\n- **A tool node** (`tool.shell`, `tool.git`, `tool.gh`, `tool.aws`) or an\n **`mcp.call`** does it with no session. With `output: { format: json }`, its\n answer routes the run: `when: \"json.state == 'OPEN'\"`.\n- **`verify:`** on a session runs a check before its report counts, so a session\n can't claim success past failing tests. **`before:`** hands it what a program\n found, so it doesn't spend turns finding it.\n- **`watch:`** waits on CI or a review with a program that looks every few\n seconds; hours of waiting cost nothing.\n- **`notify:`** lets a program tell a running session what it found, with no\n model turn to carry the news.\n\n## 2. Spend where it pays\n\n- A small model and low effort for a narrow task (`model: haiku`,\n `effort: low`); the large ones where thinking pays.\n- `budgetUsd` on each session and on the run (`limits.budgetUsd`). A loop of\n sessions without a budget or `limits.timeout` gets a warning, for good reason.\n- Give a session what it needs in its `prompt` and `before:`, rather than\n letting it search; and `isolation: readonly` for a reviewer that only reads.\n\n## 3. Make a mistake impossible before checking for it\n\n- Declare what a node says: `output.schema` lists on tool JSON and Claude\n reports (`verdict: [approve, escalate]`). Conditions then compare against a\n closed list, and a run fails a value off it rather than matching nothing.\n- A verdict is a declared field, never words to search a summary for.\n- Give each condition its opposite, and each failure a handler (`when: \"!ok\"`).\n- Check every draft with Ork's `check` tool until it's clean.\n\n## 4. A person decides what can't be undone\n\nMerging, deploying, publishing, spending a lot: put a `human.gate` before it,\nwith the safe option `recommended`. For a run nobody watches, give it a\n`timeout` and the safe `default`. You never answer a gate yourself.\n\n## An example\n\nA review that a program checks first, and a person signs off:\n\n```yaml\nschemaVersion: 1\nname: careful-review\ndescription: Lint with a program, review with a small model, and ask before merging.\ninputs:\n pr:\n type: string\n required: true\nlimits:\n budgetUsd: 3\nnodes:\n lint:\n type: tool.shell\n command: pnpm lint\n allow: [pnpm lint]\n review:\n type: claude.worker\n prompt: \"Review pull request {{inputs.pr}}. The linter passed.\"\n model: haiku\n effort: low\n isolation: readonly\n budgetUsd: 1\n output:\n schema:\n verdict: [approve, escalate]\n ask:\n type: human.gate\n question: \"Merge pull request {{inputs.pr}}? The review says {{nodes.review.output.verdict}}.\"\n options: [merge, wait]\n recommended: wait\n merge:\n type: tool.gh\n command: pr merge {{inputs.pr}} --squash\n allow: [pr merge]\nedges:\n - lint -> review\n - review -> ask\n - from: ask\n to: merge\n when: \"choice == 'merge'\"\n```\n\n`lint` costs nothing, `review` is a small model with a cap, and nothing merges\nwithout a person.\n",
2351
3198
  run: "---\nname: run\ndescription: Use when starting, following or explaining an Ork run - what it's doing, why it failed, what it cost, and what waits on the person - and when the person asks about their Ork runs.\n---\n\n# Running and following Ork workflows\n\nOrk runs workflows on the person's machine with their accounts, so a run spends\ntheir money and can push, merge or deploy. Start one only when they ask, and\nnever answer a decision for them.\n\n## Starting\n\n1. Find it: Ork's `workflows` tool gives each workflow's name and inputs.\n2. Check it first with `check` (`workflow: <name>`). Don't start one with an\n error.\n3. Start it with `start_run`, giving every required input. Claude Code asks the\n person first, unless they've allowed it. It answers with the run's id; the run goes on in the\n background, whatever happens to this conversation.\n\nWithout Ork's tools: `ork run <workflow> --detach --input name=value`.\n\n## Following\n\n- `run` gives a run's status, what it waits on, its cost so far, and each\n node's state. `runs` lists the newest runs.\n- `run_events` gives its newest events; `node` narrows them, and `last` or\n `after` widens them. A `node.finished` event with `ok: false` says why a node\n failed.\n- `explain_context` says what a node's session was given: its agent,\n instructions, skills and files. Use it when a node didn't know something.\n- `usage` says what runs spent, by workflow and model, and how close the limits\n are.\n\nSay what you found plainly: which node, what it said, and what it cost. Quote a\nnode's own words rather than retelling them.\n\n## What only the person does\n\n- **Decisions.** `decisions` lists the questions waiting on them: a gate, a loop\n that ran out of rounds, a permission. Tell them what each asks, the options,\n and the recommended one, and that they answer in `ork ui` or with\n `ork decide` in their own terminal. Don't answer one: never run\n `ork decide --id … --choose …`; with Ork's plugin, a hook stops it.\n- **Stopping, pausing, resuming.** Tell them the command: `ork stop <run>`,\n `ork pause <run>`, `ork continue <run>`, `ork resume <run>`. A node can be\n stopped alone with `--node <id>`.\n\n## Without Ork's tools\n\n| Want | Command |\n| ---------------------- | ------------------------------ |\n| Runs, newest first | `ork ps` |\n| A run's transcript | `ork logs <run>` (`--follow`) |\n| Its nodes and cost | `ork tree <run>` |\n| What runs spent | `ork usage` |\n| What waits on a person | `ork decide --list` |\n| A node's context | `ork context explain <wf> <n>` |\n",
2352
- setup: "---\nname: setup\ndescription: Use when the person asks you to set up Ork, install it, or get started with it in a repository - checking it's installed, setting up the project, making Ork's tools available to you, and a first workflow.\n---\n\n# Setting up Ork\n\nOrk runs workflows on the person's machine with their own accounts. Setting it\nup changes their machine and their repository, so say what each step does and\nask before it. Work through these in order, and stop where they say no.\n\n## 1. Check\n\n```sh\nork --version\nork doctor\n```\n\n- **No `ork`:** it needs Node 24.20 or later (`node --version`). The install is\n `npm install --global @krapht/ork`; show it and run it only when they say\n yes. The guide:\n https://github.com/mimshins/ork/blob/main/docs/getting-started/installation.md\n- **Read `ork doctor` to them.** It needs git, a git repository with a commit,\n and one Claude driver. Don't run `git init` or a commit without asking.\n- **A driver not ready:** they log in themselves, with `claude` (the CLI\n driver), or by setting `ANTHROPIC_API_KEY` or a cloud provider in their own\n shell (the SDK driver). Never ask for a key, never take one pasted to you, and\n never write one into a file or a command. `gh` and `aws` are optional, and\n their logins are their own too (`gh auth login`, `aws sso login`).\n\n## 2. The project\n\n- **`ork init`** keeps Ork's files in the repository, in `.ork/`, committed with\n the code. It adds `.ork/runs/` to `.gitignore`, links `.claude/` and similar\n folders to `.ork/`, and writes a starter workflow, `hello`.\n- **`ork register`** keeps them beside it, in `~/.ork/projects/<id>/`, and\n writes nothing into the repository. For a repository whose process isn't\n theirs to change, or to try Ork first.\n\nExplain both and ask which. Skip the question when the repository decides it: a\n`.ork/` folder means `ork init` has run (`ork projects` lists what's\nregistered). Neither overwrites a file.\n\n## 3. Make Ork available to you\n\nIf Ork's tools (`workflows`, `check`) already answer you, skip this. Otherwise,\nby where you run, and asking before each change to a config:\n\n- **Claude Code:** Ork's plugin brings the skills and the tools. The person\n types these themselves:\n\n ```\n /plugin marketplace add mimshins/ork\n /plugin install ork@ork\n ```\n\n Or, in a project set up with `ork init`, the skills in the repository for\n everyone who clones it, then the tools:\n\n ```sh\n ork init --skills\n claude mcp add ork -- ork mcp\n ```\n\n New tools start when they restart Claude Code or reconnect `ork` in `/mcp`;\n so does the plugin's, once `ork` is installed.\n\n- **Another MCP client:** add a server named `ork` with command `ork` and args\n `[\"mcp\"]` to its MCP config, where its own docs say. Its tools find the\n project by its id, from `ork projects`.\n- **A shell only:** use the CLI. `ork --help` and each command's `--help` say\n what it takes.\n\n## 4. A first workflow\n\nOffer to write one for a task they name. Follow `/ork:write`, check each draft\n(the `check` tool, or `ork validate <file>`) until it has no error, then save\nit. Don't start it: a run spends their money. Start it only when they say so,\nin the background with its inputs (`start_run`, or\n`ork run <name> --detach --input name=value`), and follow it with `/ork:run`.\n\n## 5. Show them `ork ui`\n\nTell them to run `ork ui` in their own terminal: their runs, graphs and logs in\nthe browser, and where they answer what a run asks.\n\n## Never\n\n- Ask for, store or pass on a password, key or token.\n- Answer a run's decision: they answer in `ork ui` or with `ork decide` in\n their own terminal.\n- Install, edit a config, or start a run without a yes.\n",
2353
- write: "---\nname: write\ndescription: Use when writing or changing an Ork workflow (a `.ork/workflows/*.ork.yaml` file) - its nodes, edges, conditions and loops - and checking each draft until Ork accepts it.\n---\n\n# Writing an Ork workflow\n\nAn Ork workflow is one YAML file in the project, `.ork/workflows/<name>.ork.yaml`.\nIt names the nodes (Claude sessions, programs, a person's decisions) and the\nedges that carry work between them. Ork runs it on the person's machine with\ntheir own accounts.\n\n## How to work\n\n1. **Look first.** Ork's `workflows` tool lists the project's workflows; read\n one that's close. `agents` lists the agents a node can use. Before you split\n the work, read `/ork:principles`: a program where a program will do, a\n session only for judgment.\n2. **Don't guess a field.** `workflow_format` gives the format; ask it for one\n node type (`nodeType: \"human.gate\"`) to see that type's fields.\n3. **Check every draft** with Ork's `check` tool (`text`, or `workflow` for a\n file), and fix what it says until it has no error. Read its warnings: they\n mean something. Without Ork's tools, run `ork validate <file>`.\n4. **Then save the file**, beginning with the schema line below, so editors can\n check it too.\n\n## The shape of a file\n\n```yaml\n# yaml-language-server: $schema=https://raw.githubusercontent.com/mimshins/ork/main/packages/schema/workflow.schema.json\nschemaVersion: 1\nname: fix-and-ship\ndescription: Fix a failing test, check it with the test suite, and ask before opening a pull request.\ninputs:\n issue:\n type: string\n required: true\n description: What fails.\ndefaults:\n model: sonnet\n budgetUsd: 2\nlimits:\n budgetUsd: 10\nnodes:\n fix:\n type: claude.worker\n prompt: \"Fix this, and keep the change small: {{inputs.issue}}\"\n tests:\n type: tool.shell\n command: pnpm test\n allow: [pnpm test]\n ask:\n type: human.gate\n question: \"Open a pull request for {{nodes.fix.output.branch}}?\"\n options: [open, hold]\n recommended: hold\n pr:\n type: tool.gh\n command: pr create --fill --head {{nodes.fix.output.branch}}\n allow: [pr create]\nedges:\n - fix -> tests\n - from: tests\n to: fix\n when: \"!ok\"\n maxRounds: 2\n - from: tests\n to: ask\n when: ok\n - from: ask\n to: pr\n when: \"choice == 'open'\"\n```\n\nNode ids are lowercase: letters, digits, `-` and `_`. Secrets never go in the\nfile: a tool reads one with `env:` and `{{env.NAME}}`.\n\n## Nodes\n\n| Type | Does |\n| --------------------------------- | ------------------------------------------------------------------- |\n| `claude.worker` | A Claude session that does one task and reports |\n| `claude.leader` | A session that plans and delegates to the nodes it owns (`parent:`) |\n| `claude.subagent` | A subagent inside its parent's session; it takes no edges |\n| `tool.shell` | Any program, run without a shell; `allow:` lists command prefixes |\n| `tool.git`, `tool.gh`, `tool.aws` | The person's own `git`, `gh`, `aws`, with an allow list |\n| `mcp.call` | One tool call on an MCP server, with no session |\n| `human.gate` | A question with options and a recommended one |\n| `control.join` | Waits for its inputs: `all` (default), `any`, or `n` of them |\n| `control.router`, `control.split` | Pass their input on; a router takes its first matching edge |\n| `workflow.call` | Starts another workflow as its own run |\n\n- **A Claude node reports** `status` (`pass`, `changes_requested`, `done` or\n `fail`), `summary`, `artifacts`, `branch`, and any field its `output.schema`\n declares, such as `verdict: [approve, escalate]`. For another verdict, declare\n a field: `status` can't take other values.\n- **A tool node reports** `exitCode`, `stdout`, `stderr`, and with\n `output: { format: json, schema: { state: [OPEN, CLOSED] } }` its JSON as\n `json`. Declare the fields you read: a run then checks them, and a typo can't\n pass.\n- **A gate reports** `choice` and `answeredBy`. A `timeout` needs a `default`.\n\n## Edges and conditions\n\n- `a -> b`, `a -> b, c`, `b, c -> j` are plain edges, taken when the source\n succeeded. A failed node takes no plain edge: handle a failure with\n `when: \"!ok\"`, or `status == 'fail'` on a Claude node.\n- `{ from: a, to: b, when: \"...\" }` is taken when the condition holds. A bare\n name reads the source's output: `when: \"verdict == 'approve'\"`. Quote any\n condition that starts with `!`, or YAML reads it as a tag.\n- Conditions have `==`, `!=`, `<`, `>`, `&&`, `||`, `!`, and `any`, `all`,\n `count`, `len`, `contains`. A join's `statuses` suits `any(statuses, 'fail')`.\n- A node runs when any edge into it is taken; only a join waits. A node with no\n way in taken is skipped, and so is what follows it.\n- Templates, `{{inputs.x}}`, `{{nodes.a.output.field}}`, `{{run.id}}`, go in\n `prompt`, `instructions`, `command`, `cwd` and `question`. Each value stays\n one argument of a command, so it can't inject one.\n\n## Loops\n\nEvery cycle needs an edge with `maxRounds`. Give the way back a condition and\nthe way out its opposite, as `!ok` and `ok` above; a plain edge beside a loop\nfires every round. When a loop runs out, Ork asks a person what to do.\n\n## What `check` refuses, and why\n\n- **A value the source can't have** (`when.value`): `status == 'failed'` (it's\n `fail`), a gate's `choice == 'aprove'` for `approve`, a value outside a\n declared list. The message names the values it can have.\n- **A loop no condition ends**, a plain edge beside a loop, a loop of sessions\n with no `limits.budgetUsd` or `limits.timeout`: warnings, each worth fixing.\n- **`no way the walk tried reaches x`**: Ork tried every way through, and none\n got to `x`. A join waiting on a branch a first-match router never takes is\n the usual cause.\n"
3199
+ setup: "---\nname: setup\ndescription: Use when the person asks you to set up Ork, install it, or get started with it in a repository - checking it's installed, setting up the project, making Ork's tools available to you, and a first workflow.\n---\n\n# Setting up Ork\n\nOrk runs workflows on the person's machine with their own accounts. Setting it\nup changes their machine and their repository, so say what each step does and\nask before it. Work through these in order, and stop where they say no.\n\n## 1. Check\n\n```sh\nork --version\nork doctor\n```\n\n- **No `ork`:** it needs Node 24.20 or later (`node --version`). The install is\n `npm install --global @krapht/ork`; show it and run it only when they say\n yes. The guide:\n https://github.com/mimshins/ork/blob/main/docs/getting-started/installation.md\n- **Read `ork doctor` to them.** It needs git, a git repository with a commit,\n and one agent driver. Don't run `git init` or a commit without asking.\n- **A driver not ready:** they log in themselves, with `claude auth login`, or\n by setting `ANTHROPIC_API_KEY` or a cloud provider in their own shell (either\n makes the claude-code driver ready). Never ask for a key, never take one pasted to you, and\n never write one into a file or a command. `gh` and `aws` are optional, and\n their logins are their own too (`gh auth login`, `aws sso login`).\n\n## 2. The project\n\n- **`ork init`** keeps Ork's files in the repository, in `.ork/`, committed with\n the code. It adds `.ork/runs/` to `.gitignore`, links `.claude/` and similar\n folders to `.ork/`, and writes a starter workflow, `hello`.\n- **`ork register`** keeps them beside it, in `~/.ork/projects/<id>/`, and\n writes nothing into the repository. For a repository whose process isn't\n theirs to change, or to try Ork first.\n\nExplain both and ask which. Skip the question when the repository decides it: a\n`.ork/` folder means `ork init` has run (`ork projects` lists what's\nregistered). Neither overwrites a file.\n\n## 3. Make Ork available to you\n\nIf Ork's tools (`workflows`, `check`) already answer you, skip this. Otherwise,\nby where you run, and asking before each change to a config:\n\n- **Claude Code:** Ork's plugin brings the skills and the tools. The person\n types these themselves:\n\n ```\n /plugin marketplace add mimshins/ork\n /plugin install ork@ork\n ```\n\n Or, in a project set up with `ork init`, the skills in the repository for\n everyone who clones it, then the tools:\n\n ```sh\n ork init --skills\n claude mcp add ork -- ork mcp\n ```\n\n New tools start when they restart Claude Code or reconnect `ork` in `/mcp`;\n so does the plugin's, once `ork` is installed.\n\n- **Another MCP client:** add a server named `ork` with command `ork` and args\n `[\"mcp\"]` to its MCP config, where its own docs say. Its tools find the\n project by its id, from `ork projects`.\n- **A shell only:** use the CLI. `ork --help` and each command's `--help` say\n what it takes.\n\n## 4. A first workflow\n\nOffer to write one for a task they name. Follow `/ork:write`, check each draft\n(the `check` tool, or `ork validate <file>`) until it has no error, then save\nit. Don't start it: a run spends their money. Start it only when they say so,\nin the background with its inputs (`start_run`, or\n`ork run <name> --detach --input name=value`), and follow it with `/ork:run`.\n\n## 5. Show them `ork ui`\n\nTell them to run `ork ui` in their own terminal: their runs, graphs and logs in\nthe browser, and where they answer what a run asks.\n\n## Never\n\n- Ask for, store or pass on a password, key or token.\n- Answer a run's decision: they answer in `ork ui` or with `ork decide` in\n their own terminal.\n- Install, edit a config, or start a run without a yes.\n",
3200
+ write: "---\nname: write\ndescription: Use when writing or changing an Ork workflow (a `.ork/workflows/*.ork.yaml` file) - its nodes, edges, conditions and loops - and checking each draft until Ork accepts it.\n---\n\n# Writing an Ork workflow\n\nAn Ork workflow is one YAML file in the project, `.ork/workflows/<name>.ork.yaml`.\nIt names the nodes (Claude sessions, programs, a person's decisions) and the\nedges that carry work between them. Ork runs it on the person's machine with\ntheir own accounts.\n\n## How to work\n\n1. **Look first.** Ork's `workflows` tool lists the project's workflows; read\n one that's close. `agents` lists the agents a node can use. Before you split\n the work, read `/ork:principles`: a program where a program will do, a\n session only for judgment.\n2. **Don't guess a field.** `workflow_format` gives the format; ask it for one\n node type (`nodeType: \"human.gate\"`) to see that type's fields.\n3. **Check every draft** with Ork's `check` tool (`text`, or `workflow` for a\n file), and fix what it says until it has no error. Read its warnings: they\n mean something. Without Ork's tools, run `ork validate <file>`.\n4. **Then save the file**, beginning with the schema line below, so editors can\n check it too.\n\n## The shape of a file\n\n```yaml\n# yaml-language-server: $schema=https://raw.githubusercontent.com/mimshins/ork/main/packages/schema/workflow.schema.json\nschemaVersion: 1\nname: fix-and-ship\ndescription: Fix a failing test, check it with the test suite, and ask before opening a pull request.\ninputs:\n issue:\n type: string\n required: true\n description: What fails.\ndefaults:\n model: sonnet\n budgetUsd: 2\nlimits:\n budgetUsd: 10\nnodes:\n fix:\n type: claude.worker\n prompt: \"Fix this, and keep the change small: {{inputs.issue}}\"\n tests:\n type: tool.shell\n command: pnpm test\n allow: [pnpm test]\n ask:\n type: human.gate\n question: \"Open a pull request for {{nodes.fix.output.branch}}?\"\n options: [open, hold]\n recommended: hold\n pr:\n type: tool.gh\n command: pr create --fill --head {{nodes.fix.output.branch}}\n allow: [pr create]\nedges:\n - fix -> tests\n - from: tests\n to: fix\n when: \"!ok\"\n maxRounds: 2\n - from: tests\n to: ask\n when: ok\n - from: ask\n to: pr\n when: \"choice == 'open'\"\n```\n\nNode ids are lowercase: letters, digits, `-` and `_`. Secrets never go in the\nfile: a tool reads one with `env:` and `{{env.NAME}}`.\n\n## Nodes\n\n| Type | Does |\n| --------------------------------- | ------------------------------------------------------------------- |\n| `claude.worker` | A Claude session that does one task and reports |\n| `claude.leader` | A session that plans and delegates to the nodes it owns (`parent:`) |\n| `claude.subagent` | A subagent inside its parent's session; it takes no edges |\n| `tool.shell` | Any program, run without a shell; `allow:` lists command prefixes |\n| `tool.git`, `tool.gh`, `tool.aws` | The person's own `git`, `gh`, `aws`, with an allow list |\n| `mcp.call` | One tool call on an MCP server, with no session |\n| `human.gate` | A question with options and a recommended one |\n| `control.join` | Waits for its inputs: `all` (default), `any`, or `n` of them |\n| `control.router`, `control.split` | Pass their input on; a router takes its first matching edge |\n| `workflow.call` | Starts another workflow as its own run |\n\nA worker, tool node, `mcp.call` or `workflow.call` can run once per entry of a\nlist: see [One node per item](#one-node-per-item).\n\n- **A Claude node reports** `status` (`pass`, `changes_requested`, `done` or\n `fail`), `summary`, `artifacts`, `branch`, and any field its `output.schema`\n declares, such as `verdict: [approve, escalate]`. For another verdict, declare\n a field: `status` can't take other values.\n- **A tool node reports** `exitCode`, `stdout`, `stderr`, and with\n `output: { format: json, schema: { state: [OPEN, CLOSED] } }` its JSON as\n `json`. Declare the fields you read: a run then checks them, and a typo can't\n pass.\n- **A gate reports** `choice` and `answeredBy`. A `timeout` needs a `default`.\n\n## Edges and conditions\n\n- `a -> b`, `a -> b, c`, `b, c -> j` are plain edges, taken when the source\n succeeded. A failed node takes no plain edge: handle a failure with\n `when: \"!ok\"`, or `status == 'fail'` on a Claude node.\n- `{ from: a, to: b, when: \"...\" }` is taken when the condition holds. A bare\n name reads the source's output: `when: \"verdict == 'approve'\"`. Quote any\n condition that starts with `!`, or YAML reads it as a tag.\n- Conditions have `==`, `!=`, `<`, `>`, `&&`, `||`, `!`, and `any`, `all`,\n `count`, `len`, `contains`. A join's `statuses` suits `any(statuses, 'fail')`.\n- A node runs when any edge into it is taken; only a join waits. A node with no\n way in taken is skipped, and so is what follows it.\n- Templates, `{{inputs.x}}`, `{{nodes.a.output.field}}`, `{{run.id}}`, go in\n `prompt`, `instructions`, `command`, `cwd` and `question`. Each value stays\n one argument of a command, so it can't inject one.\n\n## One node per item\n\n`forEach` runs a worker, a tool node, an `mcp.call` or a `workflow.call` once\nper entry of a list, with no model turn to fan out or gather. Not a leader, a\ngate, a subagent or a control node.\n\n```yaml\nplan:\n type: claude.worker\n prompt: \"Split this into at most 6 independent tasks: {{inputs.goal}}\"\n isolation: readonly\n output:\n schema:\n tasks:\n list: { title: string, detail: string } # declare the list and its shape\nbuild:\n type: claude.worker\n prompt: \"Task {{index}}: {{item.title}}. {{item.detail}}\"\n isolation: worktree\n forEach:\n from: \"{{nodes.plan.output.tasks}}\" # exactly one path to a declared list\n maxItems: 6 # required with `from`: the cap on sessions started\n maxParallel: 3\n```\n\n- `forEach: { items: [a, b] }` takes a list written in the file instead.\n- `{{item}}` and `{{index}}` work only in a node with `forEach`, in `prompt`,\n `command`, `arguments`, `inputs`, `env` and `ref`. A program also gets\n `ORK_ITEM` and `ORK_INDEX`.\n- The next node reads `items`, `results` (each item's output, `summary`,\n `branch`), `statuses` and `count`: `{{nodes.build.output.results}}`,\n `when: \"all(statuses, 'pass')\"`. Items follow no edges; the node does.\n- `onFailure: finish` (default) runs every item; `stop` starts no more after a\n failure. A list over `maxItems` makes Ork ask a person.\n- Declare the source's `list`, or `check` warns `forEach.undeclared`. Fix\n `forEach.not-list`, `forEach.order` and `item.field` errors; they name the\n field or node.\n\n## Loops\n\nEvery cycle needs an edge with `maxRounds`. Give the way back a condition and\nthe way out its opposite, as `!ok` and `ok` above; a plain edge beside a loop\nfires every round. When a loop runs out, Ork asks a person what to do.\n\n## What `check` refuses, and why\n\n- **A value the source can't have** (`when.value`): `status == 'failed'` (it's\n `fail`), a gate's `choice == 'aprove'` for `approve`, a value outside a\n declared list. The message names the values it can have.\n- **A loop no condition ends**, a plain edge beside a loop, a loop of sessions\n with no `limits.budgetUsd` or `limits.timeout`: warnings, each worth fixing.\n- **`no way the walk tried reaches x`**: Ork tried every way through, and none\n got to `x`. A join waiting on a branch a first-match router never takes is\n the usual cause.\n"
2354
3201
  };
2355
3202
  // src/project/init-skills.ts
2356
3203
  /**
@@ -2450,16 +3297,17 @@ const DESCRIBE = {
2450
3297
  folder: (link) => `${link.path} is a real folder, left as it is`
2451
3298
  };
2452
3299
  const describe = (link) => DESCRIBE[link.state](link);
3300
+ /** Labels are made when used, so `--plain` (set after load) is read. */
2453
3301
  const LABEL = {
2454
- linked: style("dim", "ok "),
2455
- missing: style("yellow", "missing"),
2456
- broken: style("red", "broken "),
2457
- elsewhere: style("yellow", "kept "),
2458
- 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 ")
2459
3307
  };
2460
3308
  /** One line per folder; `created` marks the links this call made. */
2461
3309
  const formatLinks = (links, created = []) => links.map((link) => {
2462
- 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)}`;
2463
3311
  });
2464
3312
  const checkOf = (tool, links) => {
2465
3313
  const name = `${AGENT_TOOL_LAYOUTS.find((l) => l.id === tool)?.name ?? tool} links`;
@@ -2498,7 +3346,7 @@ const projectChecks = (root) => [...linkChecks(root), ...skillChecks(root, ORK_V
2498
3346
  * @file This installation of Ork: its name and version for the root command;
2499
3347
  * `ork doctor`, which prints Ork's version against the last update check and
2500
3348
  * the environment checks (Node, git, the user's own claude, gh and aws
2501
- * logins, the project's links) and fails when no Claude driver can run; and
3349
+ * logins, the project's links) and fails when no agent driver can run; and
2502
3350
  * `ork upgrade`, which asks the npm registry for the latest version and, when
2503
3351
  * it is newer, upgrades Ork the way it was installed after the user agrees
2504
3352
  * (`--yes` skips the question; `--check` only reports). A clone or `npx` gets
@@ -2511,16 +3359,6 @@ const ORK_META = {
2511
3359
  version: ORK_VERSION,
2512
3360
  description: "Orchestrate Claude agents on your machine"
2513
3361
  };
2514
- const MARK = {
2515
- ok: style("green", "✓"),
2516
- warn: style("yellow", "!"),
2517
- fail: style("red", "✗"),
2518
- skip: style("dim", "-")
2519
- };
2520
- const formatCheck = (result) => {
2521
- const line = `${MARK[result.status]} ${result.name.padEnd(20)} ${result.detail}`;
2522
- return result.hint === void 0 ? [line] : [line, ` ${style("dim", result.hint)}`];
2523
- };
2524
3362
  const doctor = defineCommand({
2525
3363
  meta: {
2526
3364
  name: "doctor",
@@ -2529,17 +3367,21 @@ const doctor = defineCommand({
2529
3367
  run: async () => {
2530
3368
  const { results, ok } = await runChecks(exec, process.env, process.versions.node);
2531
3369
  const place = projectAt(process.cwd());
3370
+ const experiments = experimentsCheck(place);
2532
3371
  for (const result of [
2533
3372
  orkCheck(),
2534
3373
  ...results,
2535
3374
  ...place.mode === "attached" ? [] : projectChecks(place.root),
2536
3375
  socketCheck(),
2537
- ...triggerChecks()
3376
+ ...triggerChecks(),
3377
+ ...await leaseChecks(place),
3378
+ experiments
2538
3379
  ]) for (const line of formatCheck(result)) write(line);
2539
3380
  if (!ok) {
2540
- write(`\n${style("red", "Not ready:")} Ork needs Node, git, and at least one Claude driver.`);
3381
+ write(`\n${style("red", "Not ready:")} Ork needs Node, git, and at least one agent driver.`);
2541
3382
  process.exitCode = 1;
2542
3383
  }
3384
+ if (experiments.status === "fail") process.exitCode = 1;
2543
3385
  }
2544
3386
  });
2545
3387
  /** The ids of runs still running anywhere on this machine. */
@@ -2574,7 +3416,7 @@ const agreed = (yes, command) => {
2574
3416
  write(`Run it yourself: ${command}\nor run ork upgrade --yes.`);
2575
3417
  return Promise.resolve(false);
2576
3418
  }
2577
- return confirm(`Run ${style("bold", command)}? [y/N] `);
3419
+ return confirm(`Run ${command}?`);
2578
3420
  };
2579
3421
  const upgrade = defineCommand({
2580
3422
  meta: {
@@ -2955,10 +3797,25 @@ const relocateProject = (store, id, target) => {
2955
3797
  created: false
2956
3798
  };
2957
3799
  };
2958
- /** One project's lines for `ork projects`. */
2959
- const formatProject = (project) => {
2960
- const gone = existsSync(project.root) ? "" : style("yellow", " (missing)");
2961
- 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));
2962
3819
  };
2963
3820
  /** What `ork register` prints: the project, and where its files go. */
2964
3821
  const formatRegistered = (project, created) => {
@@ -3034,7 +3891,7 @@ const driverLine = (checks, projectDefault) => {
3034
3891
  const why = ready.ready === 1 ? ready.detail : `more than one is ready, and ${ready.driver} comes first`;
3035
3892
  return `Runs use the driver that's ready when they start: ${ready.driver} here (${why}).`;
3036
3893
  }
3037
- return `Runs use the driver that's ready when they start, and none is ready here yet: ${checks.find(({ id }) => id === "cli")?.result.hint ?? "see `ork doctor`."}`;
3894
+ return `Runs use the driver that's ready when they start, and none is ready here yet: ${checks.find(({ id }) => id === "claude-code")?.result.hint ?? "see `ork doctor`."}`;
3038
3895
  };
3039
3896
  /**
3040
3897
  * The line for the project at `root`: `ork doctor`'s driver checks through
@@ -3187,7 +4044,7 @@ const migrateHere = async (cwd, options) => {
3187
4044
  fail("Pass --yes to migrate without a terminal.");
3188
4045
  return;
3189
4046
  }
3190
- if (!options.yes && !await confirm("Go ahead? [y/N] ")) return;
4047
+ if (!options.yes && !await confirm("Go ahead?")) return;
3191
4048
  applyMigration(plan, store);
3192
4049
  write(`${style("green", "✓")} migrated. ${afterMigration(plan)}`);
3193
4050
  } finally {
@@ -3307,7 +4164,7 @@ const projects = defineCommand({
3307
4164
  const all = withStore$1((store) => store.projects());
3308
4165
  if (args.json) write(JSON.stringify(all, void 0, 2));
3309
4166
  else if (all.length === 0) write("No projects registered yet: ork init (in-repo) or ork register (attached).");
3310
- else for (const project of all) for (const line of formatProject(project)) write(line);
4167
+ else for (const line of formatProjects(all)) write(line);
3311
4168
  }
3312
4169
  });
3313
4170
  const migrate = defineCommand({
@@ -3401,13 +4258,16 @@ const SEVERITY_STYLE = {
3401
4258
  };
3402
4259
  const about = (item) => `${item.target}/${item.kind}`;
3403
4260
  /** One line per item, most recently seen first. */
3404
- const formatFeedbackItems = (items, now = Date.now()) => {
4261
+ const formatFeedbackItems = (items, now = Date.now(), term = termOf(process.stdout)) => {
3405
4262
  if (items.length === 0) return ["No feedback yet."];
3406
4263
  const width = Math.max(...items.map((i) => about(i).length));
3407
4264
  return items.map((item) => {
3408
- 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}]`)}`;
3409
4266
  const times = `${String(item.count)}×`.padStart(4);
3410
- 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}`;
3411
4271
  });
3412
4272
  };
3413
4273
  const plural = (n, word) => `${String(n)} ${word}${n === 1 ? "" : "s"}`;
@@ -3635,7 +4495,7 @@ const apply = defineCommand({
3635
4495
  item: itemArg$1,
3636
4496
  driver: {
3637
4497
  type: "string",
3638
- description: "sdk (API key or cloud provider) or cli (your claude login)"
4498
+ description: "claude-code (default: the driver ready here)"
3639
4499
  },
3640
4500
  "run-id": {
3641
4501
  type: "string",
@@ -3644,7 +4504,7 @@ const apply = defineCommand({
3644
4504
  },
3645
4505
  run: async ({ args }) => {
3646
4506
  if (args.driver !== void 0 && !isDriver(args.driver)) {
3647
- writeErr(`--driver must be sdk or cli, got \`${args.driver}\``);
4507
+ writeErr(badDriver(args.driver));
3648
4508
  process.exitCode = 1;
3649
4509
  return;
3650
4510
  }
@@ -4174,7 +5034,7 @@ const ps = defineCommand({
4174
5034
  limit: Number.isInteger(limit) && limit > 0 ? limit : 20,
4175
5035
  ...args.all ? {} : { project: placeFor(process.cwd(), store.projects()).root }
4176
5036
  });
4177
- 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) : []);
4178
5038
  if (args.json) write(JSON.stringify(runs, void 0, 2));
4179
5039
  else for (const line of formatRuns(runs, args.all, Date.now(), statusOf)) write(line);
4180
5040
  })
@@ -4205,6 +5065,11 @@ const logs = defineCommand({
4205
5065
  type: "boolean",
4206
5066
  description: "Print the stored events as JSON lines",
4207
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
4208
5073
  }
4209
5074
  },
4210
5075
  run: ({ args }) => withStore(async (store) => {
@@ -4214,6 +5079,12 @@ const logs = defineCommand({
4214
5079
  process.exitCode = 1;
4215
5080
  return;
4216
5081
  }
5082
+ const problem = args.node === void 0 ? null : unknownNode(store.events(found.run.id), args.node, args.follow && found.run.status === "running");
5083
+ if (problem !== null) {
5084
+ writeErr(problem);
5085
+ process.exitCode = 1;
5086
+ return;
5087
+ }
4217
5088
  const abort = new AbortController();
4218
5089
  process.once("SIGINT", () => {
4219
5090
  abort.abort();
@@ -4221,6 +5092,7 @@ const logs = defineCommand({
4221
5092
  await followRun(store, found.run, write, {
4222
5093
  follow: args.follow,
4223
5094
  json: args.json,
5095
+ verbose: args.verbose,
4224
5096
  signal: abort.signal,
4225
5097
  ...args.node === void 0 ? {} : { node: args.node }
4226
5098
  });
@@ -4247,8 +5119,9 @@ const tree = defineCommand({
4247
5119
  return;
4248
5120
  }
4249
5121
  const { run } = found;
4250
- const events = store.events(run.id).map((s) => s.event);
4251
- const roots = runTree(events);
5122
+ const stored = store.events(run.id);
5123
+ const events = stored.map((s) => s.event);
5124
+ const roots = nestItems(activeTree(stored));
4252
5125
  if (args.json) {
4253
5126
  write(JSON.stringify(roots, void 0, 2));
4254
5127
  return;
@@ -4306,7 +5179,7 @@ const addAgent = (options) => {
4306
5179
  * doesn't, and otherwise it asks on a terminal, once, saying the plugin has
4307
5180
  * them too, or says how to add them later.
4308
5181
  */
4309
- 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.";
4310
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).";
4311
5184
  /** Whether to write the skills: as asked, or the person's answer. */
4312
5185
  const wanted = (root, asked) => {
@@ -4377,6 +5250,8 @@ const init = defineCommand({
4377
5250
  skills: args.skills,
4378
5251
  link: args.link
4379
5252
  });
5253
+ const noCommit = await headWarning(exec);
5254
+ if (noCommit !== null) write(style("yellow", `\n! git: ${noCommit}`));
4380
5255
  write(`\nNext: edit ${WORKFLOW_DIR}/, then run ${style("bold", "ork validate")}.`);
4381
5256
  }
4382
5257
  });
@@ -4575,27 +5450,27 @@ const main = defineCommand({
4575
5450
  ps,
4576
5451
  logs,
4577
5452
  tree,
4578
- usage: () => import("./usage-EW7DI6Z7.mjs").then((module) => module.usage),
4579
- prune: () => import("./usage-EW7DI6Z7.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),
4580
5455
  decide,
4581
- relay: () => import("./relay-BipJYszF.mjs").then((module) => module.relay),
4582
- pause: () => import("./pause-C7lJkSma.mjs").then((module) => module.pause),
4583
- continue: () => import("./pause-C7lJkSma.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),
4584
5459
  stop,
4585
5460
  chat,
4586
- send: () => import("./send-nbkjf3sc.mjs").then((module) => module.send),
5461
+ send: () => import("./send-CLs_fyg7.mjs").then((module) => module.send),
4587
5462
  resume,
4588
5463
  feedback,
4589
5464
  context,
4590
- worktrees: () => import("./worktrees-Ahj9rdWK.mjs").then((module) => module.worktrees),
4591
- wt: () => import("./worktrees-Ahj9rdWK.mjs").then((module) => module.worktrees),
4592
- watch: () => import("./watch-BAkSGaOp.mjs").then((module) => module.watch),
4593
- mcp: () => import("./mcp-DM30MFBw.mjs").then((module) => module.mcp),
4594
- ui: () => import("./ui-B98FSboa.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),
4595
5470
  doctor,
4596
5471
  upgrade,
4597
- completion: () => import("./completion-DBGfL-gN.mjs").then((module) => module.completion),
4598
- __complete: () => import("./completion-DBGfL-gN.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))
4599
5474
  }
4600
5475
  });
4601
5476
  // src/bin.ts
@@ -4605,15 +5480,16 @@ const main = defineCommand({
4605
5480
  * starts the daily background check and ends with the notice (ADR-038); the
4606
5481
  * background copy it starts only runs the check. It first clears git's local
4607
5482
  * variables (a hook's `GIT_DIR`, `GIT_INDEX_FILE`, …), so nothing Ork starts
4608
- * acts on the hook's repository (specs/worktrees.md). `--help` leaves out the
4609
- * flags only Ork passes to itself. `ork __complete`, which tab completion
4610
- * runs on every Tab, goes first: no update check, and no citty, so a typed
4611
- * `--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.
4612
5488
  */
4613
5489
  clearGitLocalEnv();
4614
- const argv = process.argv.slice(2);
5490
+ const argv = takePlainFlag(process.argv.slice(2));
4615
5491
  if (argv[0] === "__complete") {
4616
- const { printCompletions } = await import("./completion-DBGfL-gN.mjs");
5492
+ const { printCompletions } = await import("./completion-C5D0sEs5.mjs");
4617
5493
  await printCompletions(main, argv.slice(1));
4618
5494
  } else if (process.env["ORK_UPDATE_CHECK_CHILD"] === "1") await runUpdateChild();
4619
5495
  else {
@@ -4623,6 +5499,9 @@ else {
4623
5499
  showUpdateNotice();
4624
5500
  });
4625
5501
  }
4626
- await runMain(main, { showUsage: showHelp });
5502
+ await runMain(main, {
5503
+ rawArgs: argv,
5504
+ showUsage: showHelp
5505
+ });
4627
5506
  }
4628
5507
  export {};