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

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 (41) hide show
  1. package/README.md +3 -4
  2. package/dist/{__build_info__-D9lSlMFy.mjs → __build_info__-DFF18K8y.mjs} +72 -3
  3. package/dist/bin.mjs +579 -379
  4. package/dist/{clean-CA_Oi2Wl.mjs → clean-DVaEH9e7.mjs} +5 -2
  5. package/dist/{completion-DBGfL-gN.mjs → completion-Dv5kf598.mjs} +6 -6
  6. package/dist/context-CEaKOVmG.mjs +683 -0
  7. package/dist/{decide-CoIJh34e.mjs → decide-3l9q8UtX.mjs} +3 -3
  8. package/dist/{find-rBBk7Jui.mjs → find-DEjjOlTG.mjs} +1 -1
  9. package/dist/format-Cb5_m-Qd.mjs +191 -0
  10. package/dist/{in-a-run-DdAakpRR.mjs → in-a-run-BS9Nc-JW.mjs} +1 -1
  11. package/dist/{limits-BPM9wUlV.mjs → limits-BSwEjQST.mjs} +1 -1
  12. package/dist/{list-Bv5Lxu7z.mjs → list-Qxr6rwsY.mjs} +11 -4
  13. package/dist/{mcp-DM30MFBw.mjs → mcp-CslnG5wf.mjs} +6 -6
  14. package/dist/output-Bqljtmp_.mjs +40 -0
  15. package/dist/{pause-C7lJkSma.mjs → pause-D7OVB5Ac.mjs} +4 -4
  16. package/dist/{relay-BipJYszF.mjs → relay-EceRIebO.mjs} +2 -2
  17. package/dist/{remove-BDs9isB6.mjs → remove-0IQZVWmI.mjs} +14 -7
  18. package/dist/{send-nbkjf3sc.mjs → send-C0uwfUYi.mjs} +3 -3
  19. package/dist/{src-hrj2h0IZ.mjs → src-Df6KOIDC.mjs} +5678 -3245
  20. package/dist/{src-BxUJPGa5.mjs → src-cmmXKHn1.mjs} +265 -203
  21. package/dist/{src-B0TT7DBi.mjs → src-i5k2Y-kf.mjs} +35 -86
  22. package/dist/src-oaJYIddp.mjs +2255 -0
  23. package/dist/ui/THIRD_PARTY_LICENSES.txt +594 -0
  24. package/dist/ui/assets/app-C8uhHVYp.css +1 -0
  25. package/dist/ui/assets/app-qs8Lofv6.js +75 -0
  26. package/dist/ui/assets/{index-CPx74lFf.js → index-DV1ZaOk-.js} +3 -3
  27. package/dist/ui/assets/theme-CdXvqTQ6.js +1 -0
  28. package/dist/ui/index.html +2 -2
  29. package/dist/{ui-B98FSboa.mjs → ui-CBkHJdqk.mjs} +4 -4
  30. package/dist/{usage-EW7DI6Z7.mjs → usage-Cd8pEYyK.mjs} +5 -5
  31. package/dist/{watch-BAkSGaOp.mjs → watch-DW5R2OPO.mjs} +6 -6
  32. package/dist/{workflows-vsEohqa7.mjs → workflows-BT1gDRIV.mjs} +4 -3
  33. package/dist/{worktrees-Ahj9rdWK.mjs → worktrees-g1MQMGie.mjs} +78 -19
  34. package/package.json +2 -4
  35. package/dist/context-6x3kXREO.mjs +0 -274
  36. package/dist/format-zMmhqyLE.mjs +0 -427
  37. package/dist/output-TV_j7Ni0.mjs +0 -950
  38. package/dist/src-B9v0lDc6.mjs +0 -1204
  39. package/dist/ui/assets/app-BNT0SuIz.css +0 -1
  40. package/dist/ui/assets/app-CmkynZyW.js +0 -67
  41. package/dist/ui/assets/theme-3xH5jxKs.js +0 -1
package/dist/bin.mjs CHANGED
@@ -1,58 +1,27 @@
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 adoptedCalls, A as inspectProject, An as inside, B as createCalls, Cn as newProjectId, Ct as runTree, D as openStore, Dn as runDirOf, E as isNodeEvent, En as projectsHome, F as clearGitLocalEnv, G as peerOf, H as endLink, Hn as beforeOf, Ht as FEEDBACK_KINDS, I as parseInputPairs, It as PROJECT_FILE, J as flattenTree, Jt as sendPost, K as tellNode, Kn as serializeWorkflow, Kt as planPost, L as resolveInputs, Lt as gitRulesOf, On as shownPath, Ot as nestItems, Q as tellLiveRun, Qn as settingsOf, Qt as normalizeTitle, Rt as loadProject, Sn as inRepoPlace, St as runStatus, Tn as placeFor, U as endLinksOf, Ut as FEEDBACK_SEVERITIES, V as addressOf, Wt as FEEDBACK_TARGETS, Xt as createFeedback, Yn as WorkflowSchema, Z as requestStop, Zt as fingerprint, _n as createAgent, an as socketPath, at as followEvents, bn as loadAgents, bt as liveStatus, ct as planNodeSession, d as runningRunIds, dn as planAgentMigration, dr as readyDriver, er as isClaudeNode, et as detachResume, f as runningScript, fn as applySteps, fr as AGENT_TOOL_LAYOUTS, gn as toolsInUse, gt as startRun, h as isNewer, hn as toolLinks, ht as walkSummary, i as NOT_STARTED, in as serveRun, ir as retiredDriverHint, it as detachRun, jn as projectRootOf, l as planUpgrade, ln as maskReplacer, mn as linkTools, mr as AGENT_DRIVERS, n as resolveBase, nt as listKey, pn as planMoveInto, pr as ORK_LAYOUT, rt as DETACHED_ENV, sn as wakeRun, t as createWorkspace, tt as resumable, u as releaseNotes, un as maskSecrets, ur as driverChecks, ut as sessionDriver, vn as deleteAgent, vt as userMessageText, wn as placeFiles, wt as templateOf, xn as hasInRepoFiles, yn as catalogAgent } from "./src-Df6KOIDC.mjs";
3
+ import { C as placeOf, T as openFixRun, _ as nodeSession, b as recordNote, f as readUpdateCache, g as activeTree, h as writeUpdateCache, i as registryOf, l as fetchLatest, m as updateCachePath, n as ORK_VERSION, s as runInstall, t as ORK_NAME, v as previewPost, w as feedbackActions, x as acceptItem, y as checkNote } from "./__build_info__-DFF18K8y.mjs";
4
+ import { i as builtinWorkflowFile, n as pendingDecisions, r as builtinProblem, t as answerDecision } from "./decide-3l9q8UtX.mjs";
5
+ import { n as newRunId, t as findRun } from "./find-DEjjOlTG.mjs";
6
+ import { n as listOrkWorktrees, t as createdWorktrees } from "./list-Qxr6rwsY.mjs";
7
+ import { a as planClean, t as applyClean } from "./clean-DVaEH9e7.mjs";
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 { a as write, i as style, n as cost, o as writeErr, t as confirm } from "./output-Bqljtmp_.mjs";
10
+ import { _ as namedMcpEntries, a as builtinExecutors, d as maskedEnv, f as maskedNamesOfRun, g as clearOutputFiles, h as sessionEnv, i as onPath, l as clearStateFiles, m as runScrubber, n as missingProgramMessage, r as missingPrograms, t as envProblems, v as resultOf } from "./src-cmmXKHn1.mjs";
11
+ import { c as registeredProjects, i as refusedBuiltin, n as WORKFLOW_SUFFIX, o as workflowArg, s as projectAt, t as WORKFLOW_DIR } from "./workflows-BT1gDRIV.mjs";
12
+ import { C as startUpdateCheck, S as showUpdateNotice, _ as runChecks, a as validate, c as warningLines, d as runReadyDriver, f as unfitLines, g as orkCheck, h as headWarning, i as loadWorkflow, l as checksHere, m as experimentsCheck, o as validateFile, p as exec, r as formatReport, s as walkLoaded, t as context, u as chooseReadyDriver, v as socketCheck, w as updateCheckOff, x as runUpdateChild, y as triggerChecks } from "./context-CEaKOVmG.mjs";
13
+ import { a as nodeVerdict, i as hasConflict, n as formatRuns, o as firstLine, r as formatTree, s as formatBusEvent, t as ago } from "./format-Cb5_m-Qd.mjs";
14
+ import { i as runLimits, r as limitsRefusal, t as chatNotice } from "./limits-BSwEjQST.mjs";
15
+ import { i as decideRefusal, n as chatRefusal } from "./in-a-run-BS9Nc-JW.mjs";
16
16
  import path from "node:path";
17
17
  import { stringify } from "yaml";
18
18
  import { z } from "zod";
19
19
  import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, writeFileSync } from "node:fs";
20
20
  import { createHash } from "node:crypto";
21
21
  import { homedir } from "node:os";
22
- import { execFile, spawn } from "node:child_process";
23
22
  import { setTimeout } from "node:timers/promises";
24
23
  import { defineCommand, renderUsage, runMain } from "citty";
25
24
  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
25
  // ../core/src/control/queue.ts
57
26
  const PERMISSION_OPTIONS = [{
58
27
  id: "allow",
@@ -64,14 +33,33 @@ const PERMISSION_OPTIONS = [{
64
33
  const DENIED = "The user did not allow this tool.";
65
34
  /** The deny message a session gets, with the person's words when given. */
66
35
  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;
36
+ /** The input fields that say what a tool will do, the first one present. */
37
+ const TOOL_INPUT_KEYS = [
38
+ "command",
39
+ "file_path",
40
+ "path",
41
+ "url",
42
+ "pattern"
43
+ ];
44
+ /** A tool's input for the decision's context: as written, cut at `max`. */
45
+ const inputContext = (input, max = 4e3) => {
46
+ const key = TOOL_INPUT_KEYS.find((k) => typeof input[k] === "string");
47
+ const text = key === void 0 ? JSON.stringify(input, void 0, 2) : String(input[key]);
48
+ return text.length > max ? `${text.slice(0, max - 1)}…` : text;
71
49
  };
72
50
  const createDecisionQueue = (options) => {
73
51
  const { store, runId, emit } = options;
74
- let count = 0;
52
+ const earlier = store.decisions({ runId });
53
+ let count = Math.max(0, ...earlier.map(({ id }) => Number(id.slice(runId.length + 1)) || 0));
54
+ for (const { id, node, status } of earlier) {
55
+ if (status !== "open") continue;
56
+ store.withdrawDecision(id);
57
+ emit({
58
+ type: "decision.withdrawn",
59
+ node,
60
+ decision: id
61
+ });
62
+ }
75
63
  /** Opens a decision and settles it with the first answer. */
76
64
  const decide = (open, local, withdrawn) => {
77
65
  count += 1;
@@ -155,12 +143,16 @@ const createDecisionQueue = (options) => {
155
143
  };
156
144
  if (question.recommended !== void 0) open.recommended = question.recommended;
157
145
  if (question.context !== void 0) open.context = question.context;
146
+ if (question.document !== void 0) open.document = question.document;
158
147
  if (question.owner !== void 0) open.owner = question.owner;
159
148
  const { local } = options;
160
149
  return decide(open, local === void 0 ? void 0 : (signal) => {
161
150
  let note;
162
151
  return local.ask({
163
152
  ...question,
153
+ question: maskSecrets(question.question),
154
+ ...question.context === void 0 ? {} : { context: maskSecrets(question.context) },
155
+ ...question.document === void 0 ? {} : { document: maskSecrets(question.document) },
164
156
  signal,
165
157
  onNote: (given) => {
166
158
  note = given;
@@ -219,7 +211,7 @@ const createDecisionQueue = (options) => {
219
211
  kind: "permission",
220
212
  question: `Allow ${request.toolName}?`,
221
213
  options: PERMISSION_OPTIONS,
222
- context: inputPreview(request.input)
214
+ context: inputContext(request.input)
223
215
  }, local === void 0 ? void 0 : (signal) => {
224
216
  let said;
225
217
  return local.permit({
@@ -236,7 +228,7 @@ const createDecisionQueue = (options) => {
236
228
  note: said
237
229
  };
238
230
  });
239
- });
231
+ }, request.signal);
240
232
  if (note !== void 0) request.onNote?.(note);
241
233
  return answer === "allow" ? { allow: true } : {
242
234
  allow: false,
@@ -613,6 +605,89 @@ const createRelay = (options) => {
613
605
  }
614
606
  };
615
607
  };
608
+ // ../core/src/run/replay.ts
609
+ const NO_FAN_OUTS = {
610
+ lists: /* @__PURE__ */ new Map(),
611
+ items: /* @__PURE__ */ new Map()
612
+ };
613
+ /** Whether an activation's input carries a result this run made anew. */
614
+ const carries = (fresh, input) => {
615
+ if (input === void 0) return false;
616
+ if (fresh.has(input)) return true;
617
+ const results = input["results"];
618
+ if (results === null || typeof results !== "object") return false;
619
+ return Object.values(results).some((value) => value !== null && typeof value === "object" && fresh.has(value));
620
+ };
621
+ /**
622
+ * Hands each node's recorded results back in order, until the node runs anew
623
+ * once (it failed or never finished last time, or its input is new); from then
624
+ * on it always runs. A node cut off mid-session continues that session, once.
625
+ */
626
+ const createReplay = (history, cutOff = /* @__PURE__ */ new Map(), mail, earlier, fanOuts = NO_FAN_OUTS) => {
627
+ const sessions = new Map(cutOff);
628
+ const used = /* @__PURE__ */ new Map();
629
+ const anew = /* @__PURE__ */ new Set();
630
+ const fresh = /* @__PURE__ */ new WeakSet();
631
+ /** Fanned nodes whose list was resolved anew: their items can't be replayed. */
632
+ const renewed = /* @__PURE__ */ new Set();
633
+ const takeItem = (activation, of) => {
634
+ const recorded = fanOuts.items.get(activation.id)?.get(activation.round ?? 0);
635
+ if (recorded !== void 0 && !renewed.has(of) && !carries(fresh, activation.input)) return recorded;
636
+ anew.add(activation.id);
637
+ return null;
638
+ };
639
+ return {
640
+ take: (activation) => {
641
+ const { id } = activation;
642
+ if (activation.brief !== void 0 || anew.has(id)) return null;
643
+ if (activation.item !== void 0) return takeItem(activation, activation.item.of);
644
+ const index = used.get(id) ?? 0;
645
+ const recorded = history.get(id)?.[index];
646
+ if (recorded === void 0 || carries(fresh, activation.input)) {
647
+ anew.add(id);
648
+ return null;
649
+ }
650
+ used.set(id, index + 1);
651
+ return recorded;
652
+ },
653
+ ran: (result) => {
654
+ fresh.add(result.output);
655
+ },
656
+ cutOff: (activation) => {
657
+ const session = activation.brief === void 0 && (activation.item === void 0 || !renewed.has(activation.item.of)) ? sessions.get(activation.id) : void 0;
658
+ sessions.delete(activation.id);
659
+ return session;
660
+ },
661
+ listed: (id, round, source) => {
662
+ const list = fanOuts.lists.get(listKey(id, round));
663
+ const ranAnew = typeof source === "object" && source !== null && fresh.has(source);
664
+ if (list !== void 0 && !ranAnew) return list;
665
+ renewed.add(id);
666
+ return null;
667
+ },
668
+ previousSession: ({ id, item, round = 0 }) => item === void 0 || renewed.has(item.of) ? void 0 : [...fanOuts.items.get(id) ?? []].findLast(([before]) => before < round)?.[1].sessionId,
669
+ lastOutput: (id) => history.get(id)?.at(-1)?.output,
670
+ kept: () => [...history.keys(), ...fanOuts.items.keys()],
671
+ ...mail === void 0 ? {} : { mail },
672
+ ...earlier === void 0 ? {} : { earlier }
673
+ };
674
+ };
675
+ /**
676
+ * The run's `HumanChannel`, answering questions about loops out of rounds as
677
+ * they were answered last time, in order; anything else goes to `human`.
678
+ */
679
+ const replayLoopAnswers = (human, answers) => {
680
+ const left = [...answers];
681
+ return {
682
+ permit: (request) => human.permit(request),
683
+ ...human.settled === void 0 ? {} : { settled: human.settled },
684
+ ask: (question) => {
685
+ const index = left.findIndex((a) => a.question === question.question);
686
+ const [given] = index === -1 ? [] : left.splice(index, 1);
687
+ return given === void 0 ? human.ask(question) : Promise.resolve(given.answer);
688
+ }
689
+ };
690
+ };
616
691
  // ../core/src/store/recorder.ts
617
692
  /**
618
693
  * @file Records a run as it happens: each event appended to the store and
@@ -699,291 +774,285 @@ const renderHelp = async (cmd, parent) => {
699
774
  const showHelp = async (cmd, parent) => {
700
775
  process.stdout.write(`${await renderHelp(cmd, parent)}\n\n`);
701
776
  };
702
- // src/update/notify.ts
703
- /**
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`.
710
- */
711
- /** Set in the background copy of `ork` that only runs the check. */
712
- const UPDATE_CHILD_ENV = "ORK_UPDATE_CHECK_CHILD";
713
- /**
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.
717
- */
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`;
777
+ // src/human.ts
729
778
  /**
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.
779
+ * @file The terminal `HumanChannel`: answers `ask_user` questions, gates and
780
+ * permission prompts on the terminal, one at a time, and denies or defers
781
+ * when no one is there. After a choice it offers one optional line for a
782
+ * note, which Enter skips. In a run it is the decision queue's local
783
+ * answerer, racing `ork decide`.
732
784
  */
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 {}
785
+ const PREVIEW = 160;
786
+ /** A document longer than this is offered (`d`) rather than printed. */
787
+ const SHORT_DOCUMENT_LINES = 40;
788
+ const NO_ONE_TO_PERMIT = "No one is at the terminal to approve this tool, so it is not allowed in this run.";
789
+ const NOTE_PROMPT = " Note for the run (optional, Enter to skip): ";
790
+ const NO_ONE_TO_ASK = "no one (nobody is at the terminal; use your best judgment and say so in your report)";
791
+ /** One line that shows what a tool is about to do. */
792
+ const previewInput = (toolName, input) => {
793
+ const key = TOOL_INPUT_KEYS.find((k) => typeof input[k] === "string");
794
+ const line = (key === void 0 ? JSON.stringify(input) : `${key}: ${String(input[key])}`).replaceAll(/\s+/gu, " ").trim();
795
+ return line.length > PREVIEW ? `${toolName} ${line.slice(0, 159)}…` : `${toolName} ${line}`;
752
796
  };
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
- });
797
+ /** The option an answer names: its number, or its id. */
798
+ const pickOption = (answer, options, recommended) => {
799
+ const value = answer.trim();
800
+ if (value === "") return recommended;
801
+ const index = Number(value);
802
+ if (Number.isInteger(index) && index >= 1 && index <= options.length) return options[index - 1]?.id;
803
+ return options.find((o) => o.id === value)?.id;
760
804
  };
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()
774
- }
775
- });
776
- } catch {}
805
+ const optionLine = (option, index, recommended) => {
806
+ const label = option.label === void 0 ? "" : ` ${option.label}`;
807
+ const description = option.description === void 0 ? "" : style("dim", ` - ${option.description}`);
808
+ const mark = option.id === recommended ? style("green", " (recommended)") : "";
809
+ return ` ${String(index + 1)}) ${style("bold", option.id)}${label}${description}${mark}`;
777
810
  };
778
- // src/update/upgrade.ts
779
811
  /**
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`.
812
+ * Answers permission prompts and `ask_user` questions on the terminal, one at a
813
+ * time. Without a terminal it denies tools and leaves questions to the node. A
814
+ * withdrawn prompt (answered elsewhere) rejects, which frees the turn.
784
815
  */
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."
816
+ const createTerminalHuman = (io) => {
817
+ let rl;
818
+ let turn = Promise.resolve();
819
+ const say = (text) => {
820
+ io.output.write(`${text}\n`);
793
821
  };
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)}`})`
822
+ const prompt = (text, signal) => {
823
+ if (rl === void 0) {
824
+ rl = createInterface({
825
+ input: io.input,
826
+ output: io.output
827
+ });
828
+ if (io.interrupt !== void 0) rl.on("SIGINT", io.interrupt);
829
+ }
830
+ return signal === void 0 ? rl.question(text) : rl.question(text, { signal });
798
831
  };
799
- };
800
- // src/doctor/checks.ts
801
- /**
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.
807
- */
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
- });
818
- });
819
- });
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.`
832
+ /** The optional line after a choice, when the asker takes a note. */
833
+ const takeNote = async (onNote, signal) => {
834
+ if (onNote === void 0) return;
835
+ const note = (await prompt(NOTE_PROMPT, signal)).trim();
836
+ if (note !== "") onNote(note);
835
837
  };
836
- };
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
- }];
860
- };
861
- /**
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.
865
- */
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")
838
+ const choose = async (hint, options, recommended, signal, document) => {
839
+ const answer = await prompt(` Choose${hint}: `, signal);
840
+ const picked = pickOption(answer, options, recommended);
841
+ if (picked !== void 0) return picked;
842
+ if (document !== void 0 && answer.trim() === "d") say(document);
843
+ else say(style("dim", " Type an option's number or id."));
844
+ return choose(hint, options, recommended, signal, document);
882
845
  };
883
- };
884
- /**
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 `~`.
888
- */
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`
846
+ const oneAtATime = (work) => {
847
+ const result = turn.then(work);
848
+ turn = result.catch(() => null);
849
+ return result;
895
850
  };
896
851
  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."
852
+ permit: ({ nodeId, toolName, input, signal, onNote }) => oneAtATime(async () => {
853
+ if (signal?.aborted === true) throw new Error("the prompt was withdrawn");
854
+ say(`${style(["yellow", "bold"], "?")} ${style("bold", nodeId)} wants to run ${previewInput(toolName, input)}`);
855
+ if (!io.interactive) {
856
+ say(style("dim", ` denied: ${NO_ONE_TO_PERMIT}`));
857
+ return {
858
+ allow: false,
859
+ message: NO_ONE_TO_PERMIT
860
+ };
861
+ }
862
+ const answer = (await prompt(" Allow? [y/N] ", signal)).trim().toLowerCase();
863
+ if (answer === "y" || answer === "yes") return { allow: true };
864
+ await takeNote(onNote, signal);
865
+ return {
866
+ allow: false,
867
+ message: "The user did not allow this tool."
868
+ };
869
+ }),
870
+ ask: ({ nodeId, question, options, recommended, context, document, signal, onNote }) => oneAtATime(async () => {
871
+ if (signal?.aborted === true) throw new Error("the question was withdrawn");
872
+ const documentLines = document?.trimEnd().split("\n").length ?? 0;
873
+ if (document !== void 0 && documentLines <= SHORT_DOCUMENT_LINES) say(document);
874
+ say(`${style(["yellow", "bold"], "?")} ${style("bold", nodeId)} asks: ${question}`);
875
+ if (context !== void 0) say(style("dim", ` ${context.replaceAll("\n", "\n ")}`));
876
+ for (const [index, option] of options.entries()) say(optionLine(option, index, recommended));
877
+ if (!io.interactive) {
878
+ const answer = recommended ?? NO_ONE_TO_ASK;
879
+ say(style("dim", ` no terminal; answered ${answer}`));
880
+ return answer;
881
+ }
882
+ const fallback = options.findIndex((o) => o.id === recommended);
883
+ const hint = fallback === -1 ? "" : ` [${String(fallback + 1)}]`;
884
+ if (documentLines > SHORT_DOCUMENT_LINES) say(` Document: ${String(documentLines)} lines. Type d to read it.`);
885
+ const picked = await choose(hint, options, recommended, signal, document);
886
+ await takeNote(onNote, signal);
887
+ return picked;
888
+ }),
889
+ close: () => {
890
+ rl?.close();
891
+ }
901
892
  };
902
893
  };
903
- /**
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).
907
- */
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();
934
- }
894
+ // src/run/format-result.ts
895
+ const RUN_MARK = {
896
+ succeeded: style("green", "✓"),
897
+ failed: style("red", "✗"),
898
+ budget_exhausted: style("yellow", "!"),
899
+ quota_exhausted: style("yellow", "!"),
900
+ timed_out: style("yellow", "!"),
901
+ interrupted: style("yellow", "!")
935
902
  };
936
- // src/run/ready-driver.ts
903
+ /** `1 failure handled`, `2 failures handled`. */
904
+ const failuresHandled = (count) => `${String(count)} ${count === 1 ? "failure" : "failures"} handled`;
937
905
  /**
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.
906
+ * The run summary: status, cost, what ran, what was skipped and which failures
907
+ * an edge handled, branches to look at, why, and mail to nodes that never ran.
943
908
  */
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`;
909
+ const formatRunResult = (result) => {
910
+ const ran = result.nodes.filter((n) => n.runs > 0).length;
911
+ const skipped = result.skipped ?? result.nodes.length - ran;
912
+ const handled = result.failuresHandled ?? 0;
913
+ const lines = ["", `${RUN_MARK[result.status]} run ${result.runId} ${result.status.replace("_", " ")} ${style("dim", `· ${cost(result.costUsd)} · ${String(ran)} ran, ${String(skipped)} skipped${handled === 0 ? "" : `, ${failuresHandled(handled)}`}`)}`];
914
+ for (const kept of result.worktrees) lines.push(` ${kept.id}: ${kept.branch ?? "detached"} ${style("dim", `at ${kept.path} (${kept.reason})`)}`);
915
+ for (const reason of result.reasons) lines.push(` ${style("red", reason)}`);
916
+ for (const { node, messages } of result.unread ?? []) lines.push(` ${style(["yellow", "bold"], "warning")} ${node} never ran, so ${messages === 1 ? "1 message" : `${String(messages)} messages`} to it went unread`);
917
+ return lines;
950
918
  };
951
- /**
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.
954
- */
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
- };
919
+ // src/run/format.ts
920
+ /** Who answered a decision, as the log says it. */
921
+ const ANSWERED_BY = {
922
+ terminal: "terminal",
923
+ decide: "ork decide",
924
+ ui: "ork ui",
925
+ owner: "its owner"
971
926
  };
972
- /**
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.
976
- */
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)());
927
+ const STATUS_MARK = {
928
+ pass: style("green", "✓"),
929
+ done: style("green", "✓"),
930
+ changes_requested: style("yellow", "!"),
931
+ fail: style("red", "✗")
932
+ };
933
+ const seconds = (ms) => `${(ms / 1e3).toFixed(1)}s`;
934
+ const indent = (text) => text.trimEnd().split("\n").map((line) => ` ${line}`);
935
+ /** The usage line: cost, tokens, turns and time. */
936
+ const formatUsage = ({ usage }) => style("dim", [
937
+ cost(usage.costUsd),
938
+ `${String(usage.inputTokens)} in / ${String(usage.outputTokens)} out (${String(usage.cacheReadTokens)} cached)`,
939
+ `${String(usage.turns)} turns`,
940
+ seconds(usage.durationMs)
941
+ ].join(" · "));
942
+ /** How a finished session reads on the terminal. */
943
+ const formatOutcome = (nodeId, outcome) => {
944
+ const { report } = outcome;
945
+ if (report === null) return [
946
+ `${style("red", "✗")} ${style("bold", nodeId)} ended without a report (${outcome.subtype})`,
947
+ ...outcome.errors.map((e) => ` ${style("red", e)}`),
948
+ ` ${formatUsage(outcome)}`
949
+ ];
950
+ const lines = [`${STATUS_MARK[report.status] ?? "-"} ${style("bold", nodeId)} ${report.status}: ${report.summary}`];
951
+ if (report.branch !== void 0) lines.push(` branch: ${report.branch}`);
952
+ for (const [name, file] of Object.entries(report.artifacts)) lines.push(` ${name}: ${file}`);
953
+ for (const [name, value] of Object.entries(report.fields)) lines.push(` ${name}: ${JSON.stringify(value)}`);
954
+ lines.push(` ${formatUsage(outcome)}`);
955
+ return lines;
956
+ };
957
+ /** The transcript lines for one event; tool names come from `tools`. */
958
+ const formatEvent = (nodeId, event, tools) => {
959
+ if (event.type === "session.started") {
960
+ const fork = event.fork === true ? ", a fork to answer a question" : "";
961
+ return [style("dim", `▸ ${nodeId} (${event.model}, session ${event.sessionId}${fork})`)];
962
+ }
963
+ if (event.type === "assistant.text") return indent(event.text);
964
+ if (event.type === "session.waiting") return indent(event.reason);
965
+ if (event.type === "tool.called") {
966
+ tools.set(event.id, event.name);
967
+ return [style("dim", ` · ${previewInput(event.name, event.input)}`)];
968
+ }
969
+ if (event.type === "tool.result") return event.isError ? [style("red", ` ✗ ${tools.get(event.id) ?? "tool"} failed`)] : [];
970
+ if (event.type === "plan.limit") return event.status === "allowed" ? [] : [style("yellow", ` ! plan: ${event.window.replaceAll("_", " ")} limit ${event.status === "rejected" ? "reached" : "is close"}`)];
971
+ return formatOutcome(nodeId, event.outcome);
972
+ };
973
+ const items = (count) => `${String(count)} ${count === 1 ? "item" : "items"}`;
974
+ /** Transcript lines for one run event, prefixed with the node they are from. */
975
+ const formatRunEvent = (event, width, tools, terminal = false) => {
976
+ const tag = (id) => style("cyan", `${id.padEnd(width)} │`);
977
+ if (event.type === "run.started") return [style("dim", `▸ run ${event.runId} of ${event.workflow} (starts at ${event.entries.join(", ")})`), ...event.settings?.trigger === void 0 ? [] : [style("dim", ` started by trigger ${event.settings.trigger}; untrusted inputs: ${(event.settings.untrusted ?? []).join(", ") || "none"}`)]];
978
+ if (event.type === "node.started") {
979
+ const round = event.round > 1 ? ` (round ${String(event.round)})` : "";
980
+ const owner = event.owner === void 0 ? "" : `, for ${event.owner}`;
981
+ return [`${tag(event.id)} ${style("dim", `${event.nodeType}${round}${owner}`)}`];
982
+ }
983
+ if (event.type === "node.event") {
984
+ const inner = event.event;
985
+ if (inner.type === "session.finished") return [];
986
+ const fork = inner.fork === true ? style("dim", "fork · ") : "";
987
+ if (inner.type === "session.started") {
988
+ const why = inner.fork === true ? ", to answer a question" : "";
989
+ return [`${tag(event.id)} ${fork}${style("dim", `${inner.model}, session ${inner.sessionId}${why}`)}`];
990
+ }
991
+ return formatEvent(event.id, event.event, tools).map((line) => `${tag(event.id)} ${fork}${line.replace(/^ {2}/u, "")}`);
992
+ }
993
+ if (event.type === "node.finished") {
994
+ const conflict = hasConflict(event.output);
995
+ const mark = event.ok ? style(conflict ? "yellow" : "green", conflict ? "!" : "✓") : style("red", "✗");
996
+ const spent = event.usage === void 0 ? "" : style("dim", ` · ${cost(event.usage.costUsd)}`);
997
+ return [`${tag(event.id)} ${mark} ${nodeVerdict(event.output, event.ok, event.error)}${spent}`];
998
+ }
999
+ if (event.type === "node.skipped") return [`${tag(event.id)} ${style("dim", "- skipped")}`];
1000
+ if (event.type === "loop.exhausted") return [`${style(["yellow", "bold"], "!")} the loop ${event.from} -> ${event.to} used all ${String(event.maxRounds)} rounds`];
1001
+ if (event.type === "watch.started") return [`${tag(event.id)} ${style("dim", `watching until ${event.until}, every ${event.every}`)}`];
1002
+ if (event.type === "watch.changed") return [`${tag(event.id)} ${style("dim", `look ${String(event.looks)}:`)} ${event.said}`];
1003
+ if (event.type === "watch.ended") return [`${tag(event.id)} ${style("dim", `${event.outcome} after ${String(event.looks)} looks`)}`];
1004
+ if (event.type === "edge.taken") return [];
1005
+ if (event.type === "worktree") {
1006
+ const detail = event.detail === void 0 ? "" : ` (${event.detail})`;
1007
+ const what = event.action === "removed" && event.branch !== void 0 ? `worktree of ${event.branch} (branch kept)` : `${event.branch ?? event.path}${detail}`;
1008
+ const line = `${event.action} ${what}`;
1009
+ return [`${tag(event.id)} ${event.action === "conflict" ? style("yellow", `! ${line}`) : style("dim", `⎇ ${line}`)}`];
1010
+ }
1011
+ if (event.type === "decision.opened") {
1012
+ const what = event.kind === "permission" ? "asks to" : "asks:";
1013
+ const question = event.kind === "permission" ? event.question.replace(/^Allow /u, "run ").replace(/\?$/u, "") : event.question;
1014
+ const where = event.owner === void 0 ? `decision ${event.decision}; ${terminal ? "answer here or with ork decide" : "ork decide"}` : `decision ${event.decision}, via ${event.owner}`;
1015
+ return [`${tag(event.node)} ${style(["yellow", "bold"], "?")} ${what} ${question} ${style("dim", `(${where})`)}`];
1016
+ }
1017
+ if (event.type === "decision.answered") {
1018
+ const by = event.by === "owner" && event.owner !== void 0 ? event.owner : ANSWERED_BY[event.by];
1019
+ const said = event.note === void 0 ? "" : ` · said: ${event.note.replaceAll(/\s+/gu, " ")}`;
1020
+ return [`${tag(event.node)} ${style("dim", `→ ${event.answer} (${by})${said}`)}`];
1021
+ }
1022
+ if (event.type === "decision.withdrawn") return [`${tag(event.node)} ${style("dim", "→ question withdrawn")}`];
1023
+ if (event.type === "run.command") {
1024
+ const node = event.node === void 0 ? "" : ` --node ${event.node}`;
1025
+ return [style(["yellow", "bold"], `! ork ${event.command}${node} received`)];
1026
+ }
1027
+ if (event.type === "node.held") {
1028
+ const at = event.at === "tool" ? "its tool call waits for ork continue" : "it starts after ork continue";
1029
+ return [`${tag(event.id)} ${style("yellow", `‖ paused: ${at}`)}`];
1030
+ }
1031
+ if (event.type === "node.released") return [`${tag(event.id)} ${style("dim", "▸ continues")}`];
1032
+ if (event.type === "each.listed") {
1033
+ const round = event.round > 1 ? ` (round ${String(event.round)})` : "";
1034
+ return [`${tag(event.node)} ${style("dim", `${items(event.count)}${round}`)}`];
1035
+ }
1036
+ if (event.type === "each.exceeded") {
1037
+ const over = `${items(event.count)}, over maxItems ${String(event.maxItems)}`;
1038
+ return [`${tag(event.node)} ${style("yellow", `! ${over}`)}`];
1039
+ }
1040
+ if (event.type === "run.finished") return formatRunResult(event.result);
1041
+ if (event.type === "run.resumed") {
1042
+ const [one] = event.kept;
1043
+ const kept = one === void 0 ? "every node runs again" : event.kept.length === 1 ? `${one} keeps its result` : `${event.kept.join(", ")} keep their results`;
1044
+ return ["", style("dim", `▸ resumed: ${kept}`)];
1045
+ }
1046
+ if (event.type === "feedback.recorded") {
1047
+ const { target, kind, title } = event.feedback;
1048
+ const line = style("dim", `✎ feedback (${target}, ${kind}): ${title}`);
1049
+ return [event.node === void 0 ? line : `${tag(event.node)} ${line}`];
1050
+ }
1051
+ if (event.type === "user.message") {
1052
+ const note = event.delivered === "queued" ? " (for its next session)" : "";
1053
+ return [`${tag(event.node)} ${style(["magenta", "bold"], "you:")} ${firstLine(event.body)}${style("dim", note)}`];
1054
+ }
1055
+ return formatBusEvent(event).map((line) => `${tag(event.node)} ${line}`);
987
1056
  };
988
1057
  // src/runs/chat.ts
989
1058
  /**
@@ -994,12 +1063,11 @@ const runReadyDriver = async (workflow, options) => {
994
1063
  * where it ran, as the run's sessions were set up (ADR-065), and the exchange
995
1064
  * is added to the run's record.
996
1065
  */
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
- };
1066
+ /**
1067
+ * The declared node a spawned worker or an item of a fanned node is an instance
1068
+ * of.
1069
+ */
1070
+ const instanceOf = (nodes, id) => id in nodes ? void 0 : templateOf(id, new Set(Object.keys(nodes)));
1003
1071
  /** The run's sessions loaded the machine's Claude setup, so this one does. */
1004
1072
  const machineClaudeOf = (events) => events.some(({ event }) => event.type === "run.started" && event.settings?.machineClaude === true) ? { machineClaude: true } : {};
1005
1073
  /**
@@ -1020,7 +1088,7 @@ const resumeChat = async (options) => {
1020
1088
  const notice = chatNotice(store, place, options.home);
1021
1089
  if (notice !== null) options.out(notice);
1022
1090
  const runDir = runDirOf(place, run.id);
1023
- const template = templateOf(workflow.nodes, node);
1091
+ const template = instanceOf(workflow.nodes, node);
1024
1092
  const plan = planNodeSession(workflow, node, {
1025
1093
  cwd,
1026
1094
  inputs: {},
@@ -1035,10 +1103,12 @@ const resumeChat = async (options) => {
1035
1103
  ...template === void 0 ? {} : { template }
1036
1104
  });
1037
1105
  if (!plan.ok) return plan.error;
1106
+ const servers = namedMcpEntries(run.project, plan.mcp);
1107
+ if (!servers.ok) return servers.error;
1038
1108
  let driver = options.driver ?? session.driver ?? plan.driver;
1039
1109
  if (driver === void 0) {
1040
1110
  const ready = readyDriver(await (options.driverChecks ?? checksHere)());
1041
- if (!ready.ok) return ["No Claude driver is ready here.", ...ready.hints].join(" ");
1111
+ if (!ready.ok) return ["No agent driver is ready here.", ...ready.hints].join(" ");
1042
1112
  driver = ready.driver;
1043
1113
  }
1044
1114
  const record = appendRecorder(store, run.id, runDir);
@@ -1048,8 +1118,9 @@ const resumeChat = async (options) => {
1048
1118
  body: options.body,
1049
1119
  delivered: "resumed"
1050
1120
  });
1051
- const started = (await options.runtime(driver)).start({
1121
+ const started = (await options.runtime(driver, placeFiles(place).runs)).start({
1052
1122
  ...plan.spec,
1123
+ ...servers.value,
1053
1124
  resume: session.sessionId,
1054
1125
  prompt: options.body,
1055
1126
  ...machineClaudeOf(events)
@@ -1084,12 +1155,22 @@ const createLogPrinter = (options) => {
1084
1155
  return (stored) => {
1085
1156
  const { event } = stored;
1086
1157
  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 [];
1158
+ if (options.node !== void 0 && !isNodeEvent(event, options.node)) return [];
1088
1159
  if (options.json) return [JSON.stringify(stored)];
1089
1160
  return formatRunEvent(event, width, tools);
1090
1161
  };
1091
1162
  };
1092
1163
  /**
1164
+ * Why `--node` names nothing in this run, or `null`: no event names it and the
1165
+ * run didn't declare it. A live run may still start it, so that is not
1166
+ * refused.
1167
+ */
1168
+ const unknownNode = (stored, node, live) => {
1169
+ const named = stored.some(({ event }) => isNodeEvent(event, node));
1170
+ const declared = stored.some(({ event }) => event.type === "run.started" && event.nodes.some((n) => n.id === node));
1171
+ return named || declared || live ? null : `Run has no node ${node}.`;
1172
+ };
1173
+ /**
1093
1174
  * Prints a run's events; with `follow`, then each new one as it is stored:
1094
1175
  * pushed over the run's socket, or polled from the store when there is none.
1095
1176
  * Returns when `run.finished` is stored, the run's process is gone, or `signal`
@@ -1107,6 +1188,31 @@ const followRun = async (store, run, out, options) => {
1107
1188
  ...options.signal === void 0 ? {} : { signal: options.signal }
1108
1189
  }) === "crashed") out(style("red", `run ${run.id} stopped without finishing (its process is gone)`));
1109
1190
  };
1191
+ // src/run/node-notes.ts
1192
+ /**
1193
+ * @file What a run of one node (`ork run --node`) leaves out, said before its
1194
+ * session starts: the messages, steps and `forEach` it does not run. A node
1195
+ * with `forEach` (RFC-013) runs once with no item when it has `items`, and is
1196
+ * refused when it has `from`, as the node that reports its list is not run.
1197
+ */
1198
+ /** A warning line, as the run prints it. */
1199
+ const noteLines = (lines) => lines.map((line) => `${style(["yellow", "bold"], "warning")} ${line}`);
1200
+ /** Why a node can't run alone, or what a run of it leaves out. */
1201
+ const oneNodeNotes = (workflow, id) => {
1202
+ const node = workflow.nodes[id];
1203
+ if (node === void 0) return { warnings: [] };
1204
+ const each = "forEach" in node ? node.forEach : void 0;
1205
+ if (each !== void 0 && "from" in each) return {
1206
+ 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`,
1207
+ warnings: []
1208
+ };
1209
+ const steps = [...beforeOf(node, workflow.defaults) === void 0 ? [] : ["before"], ..."verify" in node && node.verify !== void 0 ? ["verify"] : []];
1210
+ return { warnings: [
1211
+ ..."notify" in node && node.notify ? [`${id}'s notify: does nothing here: a run of one node has no message bus.`] : [],
1212
+ ...each === void 0 ? [] : [`${id}'s forEach: does not fan out in a run of one node: it runs once, with no item.`],
1213
+ ...steps.map((step) => `${id}'s ${step}: does not run in a run of one node.`)
1214
+ ] };
1215
+ };
1110
1216
  // src/run/origin.ts
1111
1217
  /**
1112
1218
  * @file Where a run's feedback comes from, beyond what its events say: Ork's
@@ -1114,6 +1220,8 @@ const followRun = async (store, run, out, options) => {
1114
1220
  * content hash.
1115
1221
  */
1116
1222
  /** Ork's version and the workflow's hash, for the run's feedback records. */
1223
+ /** The Ork version a run records in `run.started` and `run.resumed`. */
1224
+ const ORK_RUN_VERSION = ORK_VERSION;
1117
1225
  const feedbackOrigin = (workflowFile) => {
1118
1226
  const origin = { version: ORK_VERSION };
1119
1227
  try {
@@ -1147,6 +1255,8 @@ const runNode = async (options) => {
1147
1255
  if (!pairs.ok) return failed([`--input: ${pairs.error}`]);
1148
1256
  const inputs = resolveInputs(workflow, pairs.raw);
1149
1257
  if (!inputs.ok) return failed(inputs.errors);
1258
+ const notes = oneNodeNotes(workflow, options.nodeId);
1259
+ if (notes.error !== void 0) return failed([notes.error]);
1150
1260
  const limits = runLimits(options.store, place, options.home);
1151
1261
  const refused = limitsRefusal(limits, workflow);
1152
1262
  if (refused.length > 0) return failed(refused);
@@ -1166,6 +1276,8 @@ const runNode = async (options) => {
1166
1276
  pluginDir: path.join(runDir, "plugins", options.nodeId)
1167
1277
  });
1168
1278
  if (!plan.ok) return failed([plan.error]);
1279
+ const servers = namedMcpEntries(projectRoot, plan.mcp);
1280
+ if (!servers.ok) return failed([servers.error]);
1169
1281
  const named = sessionDriver({
1170
1282
  override: options.driver,
1171
1283
  named: plan.driver
@@ -1174,12 +1286,13 @@ const runNode = async (options) => {
1174
1286
  if (ready?.ok === false) return failed(ready.lines);
1175
1287
  if (ready?.line !== void 0) options.err(ready.line);
1176
1288
  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}`);
1289
+ if (driver === void 0) return failed(["No agent driver is ready here."]);
1290
+ const unfit = unfitLines(workflow, options.drivers, {
1291
+ override: driver,
1292
+ only: options.nodeId
1293
+ });
1294
+ if (unfit.length > 0) return failed(unfit);
1295
+ for (const line of noteLines([...notes.warnings, ...plan.warnings])) options.err(line);
1183
1296
  const id = options.nodeId;
1184
1297
  const recorder = createRecorder(options.store, {
1185
1298
  id: runId,
@@ -1237,9 +1350,12 @@ const runNode = async (options) => {
1237
1350
  nodeType,
1238
1351
  round: 1
1239
1352
  });
1240
- const runtime = await options.runtime(driver);
1353
+ const runtime = await options.runtime(driver, placeFiles(place).runs);
1241
1354
  if (options.machineClaude === true) plan.spec.machineClaude = true;
1242
- const session = runtime.start(plan.spec, {
1355
+ const session = runtime.start({
1356
+ ...plan.spec,
1357
+ ...servers.value
1358
+ }, {
1243
1359
  human: control.human,
1244
1360
  ...graph === null ? {} : { feedback: createFeedback({
1245
1361
  graph,
@@ -1498,6 +1614,14 @@ const preflight = async (options, given) => {
1498
1614
  lines: driver.lines
1499
1615
  };
1500
1616
  if (driver.line !== void 0) options.err(driver.line);
1617
+ const unfit = unfitLines(given.effective, options.drivers, {
1618
+ override: options.driver,
1619
+ ready: driver.driver ?? options.resume?.settings.readyDriver
1620
+ });
1621
+ if (unfit.length > 0) return {
1622
+ ok: false,
1623
+ lines: unfit
1624
+ };
1501
1625
  const checkout = await prepareCheckout(workflow, {
1502
1626
  ...options,
1503
1627
  cwd: place.root
@@ -1588,7 +1712,7 @@ const runWorkflowFile = async (options) => {
1588
1712
  for (const event of signals(raw)) {
1589
1713
  control.publish(record(event));
1590
1714
  if (options.json) options.out(JSON.stringify(event));
1591
- else for (const line of formatRunEvent(event, width, tools)) options.out(line);
1715
+ else for (const line of formatRunEvent(event, width, tools, options.human !== void 0)) options.out(line);
1592
1716
  }
1593
1717
  };
1594
1718
  const workspace = root === void 0 ? void 0 : createWorkspace({
@@ -1605,7 +1729,7 @@ const runWorkflowFile = async (options) => {
1605
1729
  });
1606
1730
  const maskedNames = maskedNamesOfRun(workflow, place, projects);
1607
1731
  const executorOptions = {
1608
- runtimeFor: options.runtime,
1732
+ runtimeFor: (driver) => options.runtime(driver, placeFiles(place).runs),
1609
1733
  inPlace: options.inPlace,
1610
1734
  maskedNames
1611
1735
  };
@@ -1614,10 +1738,11 @@ const runWorkflowFile = async (options) => {
1614
1738
  if (options.tools !== void 0) executorOptions.tools = options.tools;
1615
1739
  const script = process.argv[1];
1616
1740
  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);
1741
+ 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
1742
  if (replay !== void 0) onEvent({
1619
1743
  type: "run.resumed",
1620
- kept: replay.kept()
1744
+ kept: replay.kept(),
1745
+ ork: ORK_RUN_VERSION
1621
1746
  });
1622
1747
  const ports = callPorts({
1623
1748
  ...options,
@@ -1639,6 +1764,7 @@ const runWorkflowFile = async (options) => {
1639
1764
  executors: builtinExecutors(executorOptions),
1640
1765
  human: options.resume === void 0 ? control.human : replayLoopAnswers(control.human, options.resume.loopAnswers),
1641
1766
  onEvent,
1767
+ orkVersion: ORK_RUN_VERSION,
1642
1768
  settings: {
1643
1769
  inPlace: options.inPlace,
1644
1770
  ...options.driver === void 0 ? {} : { driver: options.driver },
@@ -1683,9 +1809,11 @@ const flagValues = (rawArgs, flag) => {
1683
1809
  }
1684
1810
  return values;
1685
1811
  };
1686
- const isDriver = (value) => CLAUDE_DRIVERS.some((d) => d.id === value);
1812
+ const isDriver = (value) => AGENT_DRIVERS.some((d) => d.id === value);
1813
+ /** Why `--driver` refuses a value: a retired id names its replacement. */
1814
+ const badDriver = (value) => `--${retiredDriverHint(value) ?? `driver must be one of ${AGENT_DRIVERS.map((d) => d.id).join(", ")}, got \`${value}\``}`;
1687
1815
  /** 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));
1816
+ const loadRuntime = async (driver, runs) => (await import("./src-oaJYIddp.mjs")).createRuntime(driver, sessionEnv(process.env), runs);
1689
1817
  /**
1690
1818
  * Ctrl-C asks the run to stop, and so does a terminal closing or a polite kill,
1691
1819
  * so that the run ends tidily and leaves no result unscrubbed.
@@ -1703,11 +1831,18 @@ const onInterrupt = (stop) => {
1703
1831
  process.once("SIGHUP", interrupt);
1704
1832
  };
1705
1833
  /** 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
- });
1834
+ const terminalHuman = () => {
1835
+ const detached = process.env[DETACHED_ENV] === "1";
1836
+ if (detached) writeErr(style("dim", "detached run: no terminal asks; answer with ork decide"));
1837
+ return detached ? void 0 : createTerminalHuman({
1838
+ input: process.stdin,
1839
+ output: process.stderr,
1840
+ interactive: process.stdin.isTTY,
1841
+ interrupt: () => {
1842
+ process.kill(process.pid, "SIGINT");
1843
+ }
1844
+ });
1845
+ };
1711
1846
  const run = defineCommand({
1712
1847
  meta: {
1713
1848
  name: "run",
@@ -1729,7 +1864,7 @@ const run = defineCommand({
1729
1864
  },
1730
1865
  driver: {
1731
1866
  type: "string",
1732
- description: "sdk (API key or cloud provider) or cli (your claude login)"
1867
+ description: "claude-code (default: the driver ready here)"
1733
1868
  },
1734
1869
  "in-place": {
1735
1870
  type: "boolean",
@@ -1787,7 +1922,7 @@ const run = defineCommand({
1787
1922
  },
1788
1923
  run: async ({ args, rawArgs }) => {
1789
1924
  if (args.driver !== void 0 && !isDriver(args.driver)) {
1790
- writeErr(`--driver must be ${CLAUDE_DRIVERS.map((d) => d.id).join(" or ")}, got \`${args.driver}\``);
1925
+ writeErr(badDriver(args.driver));
1791
1926
  process.exitCode = 1;
1792
1927
  return;
1793
1928
  }
@@ -1945,7 +2080,7 @@ const chat = defineCommand({
1945
2080
  },
1946
2081
  driver: {
1947
2082
  type: "string",
1948
- description: "For a finished run: sdk or cli (default: the one it ran with)"
2083
+ description: "For a finished run: claude-code (default: the one it ran with)"
1949
2084
  }
1950
2085
  },
1951
2086
  run: async ({ args }) => {
@@ -1971,7 +2106,7 @@ const chat = defineCommand({
1971
2106
  return;
1972
2107
  }
1973
2108
  if (args.driver !== void 0 && !isDriver(args.driver)) {
1974
- writeErr(`--driver must be sdk or cli, not \`${args.driver}\`.`);
2109
+ writeErr(`${badDriver(args.driver)}.`);
1975
2110
  process.exitCode = 1;
1976
2111
  return;
1977
2112
  }
@@ -2016,7 +2151,8 @@ const chat = defineCommand({
2016
2151
  * writes to the store, then wakes the run on its socket; specs/messaging.md).
2017
2152
  * `--say` gives the answer a note, which goes to the run with it. With `--id`
2018
2153
  * and `--choose`, `--note` and `--rating` instead note how the decision's
2019
- * node went, as the run's feedback (specs/feedback.md).
2154
+ * node went, as the run's feedback (specs/feedback.md). `--document` prints
2155
+ * what a gate gave to read beside its question.
2020
2156
  */
2021
2157
  const failed$2 = (message) => {
2022
2158
  writeErr(message);
@@ -2055,7 +2191,7 @@ const saidLine = (store, id) => {
2055
2191
  return note === void 0 ? "" : style("dim", ` · said: ${note}`);
2056
2192
  };
2057
2193
  /** 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(", ")}]`)}`);
2194
+ const formatDecisions = (decisions, workflow) => decisions.length === 0 ? [workflow === void 0 ? "No open decisions." : `No open decisions in workflow ${workflow}.`] : decisions.map((d) => `${style("bold", d.id)} ${style("cyan", d.node)}${d.owner === void 0 ? "" : style("dim", ` via ${d.owner}`)} ${d.question} ${d.document === void 0 ? "" : `${style("dim", "+document")} `}${style("dim", `[${d.options.map((o) => o.id === d.recommended ? `${o.id}*` : o.id).join(", ")}]`)}`);
2059
2195
  const decide = defineCommand({
2060
2196
  meta: {
2061
2197
  name: "decide",
@@ -2082,6 +2218,15 @@ const decide = defineCommand({
2082
2218
  description: "List them as JSON",
2083
2219
  default: false
2084
2220
  },
2221
+ workflow: {
2222
+ type: "string",
2223
+ description: "Only the decisions of this workflow's runs"
2224
+ },
2225
+ document: {
2226
+ type: "boolean",
2227
+ description: "Print the document of the decision --id names",
2228
+ default: false
2229
+ },
2085
2230
  id: {
2086
2231
  type: "string",
2087
2232
  description: "Answer this decision…"
@@ -2104,7 +2249,7 @@ const decide = defineCommand({
2104
2249
  }
2105
2250
  },
2106
2251
  run: async ({ args }) => {
2107
- const refusal = args.id !== void 0 || args.choose !== void 0 || process.stdin.isTTY && !args.list && !args.json ? decideRefusal() : null;
2252
+ const refusal = !args.document && (args.id !== void 0 || args.choose !== void 0 || process.stdin.isTTY && !args.list && !args.json) ? decideRefusal() : null;
2108
2253
  if (refusal !== null) {
2109
2254
  failed$2(refusal);
2110
2255
  return;
@@ -2116,6 +2261,16 @@ const decide = defineCommand({
2116
2261
  interactive: process.stdin.isTTY
2117
2262
  });
2118
2263
  try {
2264
+ if (args.document) {
2265
+ if (args.id === void 0 || args.choose !== void 0) {
2266
+ failed$2("--document goes with --id, without --choose.");
2267
+ return;
2268
+ }
2269
+ const { document } = store.decision(args.id) ?? {};
2270
+ if (document === void 0) failed$2(`${args.id} has no document.`);
2271
+ else write(document);
2272
+ return;
2273
+ }
2119
2274
  if (args.id !== void 0 || args.choose !== void 0) {
2120
2275
  if (args.id === void 0 || args.choose === void 0) {
2121
2276
  failed$2("--id and --choose go together.");
@@ -2147,6 +2302,7 @@ const decide = defineCommand({
2147
2302
  return;
2148
2303
  }
2149
2304
  const filter = {};
2305
+ if (args.workflow !== void 0) filter.workflow = args.workflow;
2150
2306
  if (args.run !== void 0 && args.run !== "") {
2151
2307
  const found = findRun(store, args.run, process.cwd());
2152
2308
  if (!found.ok) {
@@ -2161,7 +2317,7 @@ const decide = defineCommand({
2161
2317
  return;
2162
2318
  }
2163
2319
  if (args.list || !process.stdin.isTTY || open.length === 0) {
2164
- for (const line of formatDecisions(open)) write(line);
2320
+ for (const line of formatDecisions(open, args.workflow)) write(line);
2165
2321
  return;
2166
2322
  }
2167
2323
  for (const d of open) {
@@ -2173,6 +2329,7 @@ const decide = defineCommand({
2173
2329
  };
2174
2330
  if (d.recommended !== void 0) question.recommended = d.recommended;
2175
2331
  if (d.context !== void 0) question.context = d.context;
2332
+ if (d.document !== void 0) question.document = d.document;
2176
2333
  let note;
2177
2334
  question.onNote = (given) => {
2178
2335
  note = given;
@@ -2264,7 +2421,7 @@ const resume = defineCommand({
2264
2421
  },
2265
2422
  driver: {
2266
2423
  type: "string",
2267
- description: "sdk or cli (default: the one the run used)"
2424
+ description: "claude-code (default: the one the run used)"
2268
2425
  },
2269
2426
  "in-place": {
2270
2427
  type: "boolean",
@@ -2288,7 +2445,7 @@ const resume = defineCommand({
2288
2445
  },
2289
2446
  run: async ({ args, rawArgs }) => {
2290
2447
  if (args.driver !== void 0 && !isDriver(args.driver)) {
2291
- failed$1(`--driver must be sdk or cli, got \`${args.driver}\``);
2448
+ failed$1(badDriver(args.driver));
2292
2449
  return;
2293
2450
  }
2294
2451
  const store = openStore();
@@ -2339,6 +2496,36 @@ const resume = defineCommand({
2339
2496
  }
2340
2497
  }
2341
2498
  });
2499
+ // src/doctor/lease-checks.ts
2500
+ /**
2501
+ * @file What `ork doctor` says of Command Code leases (RFC-014): it removes the
2502
+ * `ork` MCP entries a crashed session left in its checkout's local scope, and
2503
+ * says where. Never prints a token.
2504
+ */
2505
+ /**
2506
+ * Removes the `ork` MCP entries a crashed Command Code session left in its
2507
+ * checkout's local scope (RFC-014), from the leases in the project's runs
2508
+ * folder. Says nothing when there were none. `bin` is the `cmd` program.
2509
+ */
2510
+ const leaseChecks = async (place, env = process.env, bin = "cmd") => {
2511
+ const { cleanStaleLeases, leaseLedger } = await import("./src-oaJYIddp.mjs");
2512
+ const { cleaned, failed } = await cleanStaleLeases({
2513
+ bin,
2514
+ ledger: leaseLedger(placeFiles(place).runs),
2515
+ env
2516
+ });
2517
+ const name = "command-code";
2518
+ return [...cleaned.length === 0 ? [] : [{
2519
+ name,
2520
+ status: "ok",
2521
+ detail: `removed the \`ork\` MCP entry a stopped session left in ${cleaned.join(", ")}`
2522
+ }], ...failed.length === 0 ? [] : [{
2523
+ name,
2524
+ status: "warn",
2525
+ detail: `could not remove the \`ork\` MCP entry a stopped session left in ${failed.join(", ")}`,
2526
+ hint: "Run `cmd mcp remove --scope local ork` in each of those folders."
2527
+ }]];
2528
+ };
2342
2529
  // src/project/ork-skills.ts
2343
2530
  /**
2344
2531
  * @file The Claude Code plugin's skills (`plugins/ork/skills/`) as text, by
@@ -2349,8 +2536,8 @@ const resume = defineCommand({
2349
2536
  const ORK_SKILLS = {
2350
2537
  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
2538
  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"
2539
+ 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",
2540
+ 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
2541
  };
2355
2542
  // src/project/init-skills.ts
2356
2543
  /**
@@ -2498,7 +2685,7 @@ const projectChecks = (root) => [...linkChecks(root), ...skillChecks(root, ORK_V
2498
2685
  * @file This installation of Ork: its name and version for the root command;
2499
2686
  * `ork doctor`, which prints Ork's version against the last update check and
2500
2687
  * 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
2688
+ * logins, the project's links) and fails when no agent driver can run; and
2502
2689
  * `ork upgrade`, which asks the npm registry for the latest version and, when
2503
2690
  * it is newer, upgrades Ork the way it was installed after the user agrees
2504
2691
  * (`--yes` skips the question; `--check` only reports). A clone or `npx` gets
@@ -2529,17 +2716,21 @@ const doctor = defineCommand({
2529
2716
  run: async () => {
2530
2717
  const { results, ok } = await runChecks(exec, process.env, process.versions.node);
2531
2718
  const place = projectAt(process.cwd());
2719
+ const experiments = experimentsCheck(place);
2532
2720
  for (const result of [
2533
2721
  orkCheck(),
2534
2722
  ...results,
2535
2723
  ...place.mode === "attached" ? [] : projectChecks(place.root),
2536
2724
  socketCheck(),
2537
- ...triggerChecks()
2725
+ ...triggerChecks(),
2726
+ ...await leaseChecks(place),
2727
+ experiments
2538
2728
  ]) for (const line of formatCheck(result)) write(line);
2539
2729
  if (!ok) {
2540
- write(`\n${style("red", "Not ready:")} Ork needs Node, git, and at least one Claude driver.`);
2730
+ write(`\n${style("red", "Not ready:")} Ork needs Node, git, and at least one agent driver.`);
2541
2731
  process.exitCode = 1;
2542
2732
  }
2733
+ if (experiments.status === "fail") process.exitCode = 1;
2543
2734
  }
2544
2735
  });
2545
2736
  /** The ids of runs still running anywhere on this machine. */
@@ -3034,7 +3225,7 @@ const driverLine = (checks, projectDefault) => {
3034
3225
  const why = ready.ready === 1 ? ready.detail : `more than one is ready, and ${ready.driver} comes first`;
3035
3226
  return `Runs use the driver that's ready when they start: ${ready.driver} here (${why}).`;
3036
3227
  }
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`."}`;
3228
+ 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
3229
  };
3039
3230
  /**
3040
3231
  * The line for the project at `root`: `ork doctor`'s driver checks through
@@ -3635,7 +3826,7 @@ const apply = defineCommand({
3635
3826
  item: itemArg$1,
3636
3827
  driver: {
3637
3828
  type: "string",
3638
- description: "sdk (API key or cloud provider) or cli (your claude login)"
3829
+ description: "claude-code (default: the driver ready here)"
3639
3830
  },
3640
3831
  "run-id": {
3641
3832
  type: "string",
@@ -3644,7 +3835,7 @@ const apply = defineCommand({
3644
3835
  },
3645
3836
  run: async ({ args }) => {
3646
3837
  if (args.driver !== void 0 && !isDriver(args.driver)) {
3647
- writeErr(`--driver must be sdk or cli, got \`${args.driver}\``);
3838
+ writeErr(badDriver(args.driver));
3648
3839
  process.exitCode = 1;
3649
3840
  return;
3650
3841
  }
@@ -4214,6 +4405,12 @@ const logs = defineCommand({
4214
4405
  process.exitCode = 1;
4215
4406
  return;
4216
4407
  }
4408
+ const problem = args.node === void 0 ? null : unknownNode(store.events(found.run.id), args.node, args.follow && found.run.status === "running");
4409
+ if (problem !== null) {
4410
+ writeErr(problem);
4411
+ process.exitCode = 1;
4412
+ return;
4413
+ }
4217
4414
  const abort = new AbortController();
4218
4415
  process.once("SIGINT", () => {
4219
4416
  abort.abort();
@@ -4247,8 +4444,9 @@ const tree = defineCommand({
4247
4444
  return;
4248
4445
  }
4249
4446
  const { run } = found;
4250
- const events = store.events(run.id).map((s) => s.event);
4251
- const roots = runTree(events);
4447
+ const stored = store.events(run.id);
4448
+ const events = stored.map((s) => s.event);
4449
+ const roots = nestItems(activeTree(stored));
4252
4450
  if (args.json) {
4253
4451
  write(JSON.stringify(roots, void 0, 2));
4254
4452
  return;
@@ -4377,6 +4575,8 @@ const init = defineCommand({
4377
4575
  skills: args.skills,
4378
4576
  link: args.link
4379
4577
  });
4578
+ const noCommit = await headWarning(exec);
4579
+ if (noCommit !== null) write(style("yellow", `\n! git: ${noCommit}`));
4380
4580
  write(`\nNext: edit ${WORKFLOW_DIR}/, then run ${style("bold", "ork validate")}.`);
4381
4581
  }
4382
4582
  });
@@ -4575,27 +4775,27 @@ const main = defineCommand({
4575
4775
  ps,
4576
4776
  logs,
4577
4777
  tree,
4578
- usage: () => import("./usage-EW7DI6Z7.mjs").then((module) => module.usage),
4579
- prune: () => import("./usage-EW7DI6Z7.mjs").then((module) => module.prune),
4778
+ usage: () => import("./usage-Cd8pEYyK.mjs").then((module) => module.usage),
4779
+ prune: () => import("./usage-Cd8pEYyK.mjs").then((module) => module.prune),
4580
4780
  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),
4781
+ relay: () => import("./relay-EceRIebO.mjs").then((module) => module.relay),
4782
+ pause: () => import("./pause-D7OVB5Ac.mjs").then((module) => module.pause),
4783
+ continue: () => import("./pause-D7OVB5Ac.mjs").then((module) => module.continueRun),
4584
4784
  stop,
4585
4785
  chat,
4586
- send: () => import("./send-nbkjf3sc.mjs").then((module) => module.send),
4786
+ send: () => import("./send-C0uwfUYi.mjs").then((module) => module.send),
4587
4787
  resume,
4588
4788
  feedback,
4589
4789
  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),
4790
+ worktrees: () => import("./worktrees-g1MQMGie.mjs").then((module) => module.worktrees),
4791
+ wt: () => import("./worktrees-g1MQMGie.mjs").then((module) => module.worktrees),
4792
+ watch: () => import("./watch-DW5R2OPO.mjs").then((module) => module.watch),
4793
+ mcp: () => import("./mcp-CslnG5wf.mjs").then((module) => module.mcp),
4794
+ ui: () => import("./ui-CBkHJdqk.mjs").then((module) => module.ui),
4595
4795
  doctor,
4596
4796
  upgrade,
4597
- completion: () => import("./completion-DBGfL-gN.mjs").then((module) => module.completion),
4598
- __complete: () => import("./completion-DBGfL-gN.mjs").then((module) => module.completeCommand(main))
4797
+ completion: () => import("./completion-Dv5kf598.mjs").then((module) => module.completion),
4798
+ __complete: () => import("./completion-Dv5kf598.mjs").then((module) => module.completeCommand(main))
4599
4799
  }
4600
4800
  });
4601
4801
  // src/bin.ts
@@ -4613,7 +4813,7 @@ const main = defineCommand({
4613
4813
  clearGitLocalEnv();
4614
4814
  const argv = process.argv.slice(2);
4615
4815
  if (argv[0] === "__complete") {
4616
- const { printCompletions } = await import("./completion-DBGfL-gN.mjs");
4816
+ const { printCompletions } = await import("./completion-Dv5kf598.mjs");
4617
4817
  await printCompletions(main, argv.slice(1));
4618
4818
  } else if (process.env["ORK_UPDATE_CHECK_CHILD"] === "1") await runUpdateChild();
4619
4819
  else {