@krapht/ork 1.0.0-alpha.0 → 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 +5 -6
  2. package/dist/{__build_info__-DdTVd2p5.mjs → __build_info__-DFF18K8y.mjs} +72 -3
  3. package/dist/bin.mjs +603 -384
  4. package/dist/{clean-NnboOuz6.mjs → clean-DVaEH9e7.mjs} +5 -2
  5. package/dist/{completion-snnYGjgK.mjs → completion-Dv5kf598.mjs} +6 -6
  6. package/dist/context-CEaKOVmG.mjs +683 -0
  7. package/dist/{decide-B81wCZQg.mjs → decide-3l9q8UtX.mjs} +3 -3
  8. package/dist/{find-BKW-uGey.mjs → find-DEjjOlTG.mjs} +1 -1
  9. package/dist/format-Cb5_m-Qd.mjs +191 -0
  10. package/dist/{in-a-run-BPXX09Bs.mjs → in-a-run-BS9Nc-JW.mjs} +1 -1
  11. package/dist/{limits-JCve2QpI.mjs → limits-BSwEjQST.mjs} +1 -1
  12. package/dist/{list-C7UEgmtd.mjs → list-Qxr6rwsY.mjs} +11 -4
  13. package/dist/{mcp-CKPyNaKD.mjs → mcp-CslnG5wf.mjs} +6 -6
  14. package/dist/output-Bqljtmp_.mjs +40 -0
  15. package/dist/{pause-B_SL_yDz.mjs → pause-D7OVB5Ac.mjs} +4 -4
  16. package/dist/{relay-o-Luee1c.mjs → relay-EceRIebO.mjs} +2 -2
  17. package/dist/{remove-DAZ7ZCzt.mjs → remove-0IQZVWmI.mjs} +14 -7
  18. package/dist/{send-3B64fBNB.mjs → send-C0uwfUYi.mjs} +3 -3
  19. package/dist/{src-DHvWU4tX.mjs → src-Df6KOIDC.mjs} +5682 -3248
  20. package/dist/{src-C38BPzit.mjs → src-cmmXKHn1.mjs} +266 -203
  21. package/dist/{src-BJTegEzY.mjs → src-i5k2Y-kf.mjs} +40 -88
  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-D83F2OKZ.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-CY4xxJUu.mjs → ui-CBkHJdqk.mjs} +4 -4
  30. package/dist/{usage-CJnytoe4.mjs → usage-Cd8pEYyK.mjs} +5 -5
  31. package/dist/{watch-BJB82ZJ7.mjs → watch-DW5R2OPO.mjs} +6 -6
  32. package/dist/{workflows-CiM7S3l0.mjs → workflows-BT1gDRIV.mjs} +4 -3
  33. package/dist/{worktrees-86ySXxR6.mjs → worktrees-g1MQMGie.mjs} +78 -19
  34. package/package.json +2 -4
  35. package/dist/context-DO3sBy9v.mjs +0 -274
  36. package/dist/format-CmTlTOjT.mjs +0 -427
  37. package/dist/output-B1V6ZJSx.mjs +0 -950
  38. package/dist/src-Dyz_Gy1q.mjs +0 -1183
  39. package/dist/ui/assets/app-C8nJ6NpT.js +0 -67
  40. package/dist/ui/assets/app-CH7YC3in.css +0 -1
  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-DHvWU4tX.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__-DdTVd2p5.mjs";
4
- import { i as builtinWorkflowFile, n as pendingDecisions, r as builtinProblem, t as answerDecision } from "./decide-B81wCZQg.mjs";
5
- import { n as newRunId, t as findRun } from "./find-BKW-uGey.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-B1V6ZJSx.mjs";
7
- import { n as listOrkWorktrees, t as createdWorktrees } from "./list-C7UEgmtd.mjs";
8
- import { a as planClean, t as applyClean } from "./clean-NnboOuz6.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-CmTlTOjT.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-CiM7S3l0.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-DO3sBy9v.mjs";
13
- import { i as runLimits, r as limitsRefusal, t as chatNotice } from "./limits-JCve2QpI.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-C38BPzit.mjs";
15
- import { i as decideRefusal, n as chatRefusal } from "./in-a-run-BPXX09Bs.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
777
+ // src/human.ts
703
778
  /**
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`.
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`.
710
784
  */
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`;
729
- /**
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.
732
- */
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
  /**
@@ -991,21 +1060,24 @@ const runReadyDriver = async (workflow, options) => {
991
1060
  * the store and the run's socket, and the run's bus pushes it into the node's
992
1061
  * session (specs/messaging.md#chat; `tellLiveRun` and `nodeSession` are in
993
1062
  * core). A node of a finished run gets it by resuming its last Claude session
994
- * where it ran, and the exchange is added to the run's record.
1063
+ * where it ran, as the run's sessions were set up (ADR-065), and the exchange
1064
+ * is added to the run's record.
995
1065
  */
996
- /** The declared node a spawned worker (`<template>-<n>`) is an instance of. */
997
- const templateOf = (nodes, id) => {
998
- if (id in nodes) return void 0;
999
- const template = id.replace(/-\d+$/u, "");
1000
- return template in nodes ? template : void 0;
1001
- };
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)));
1071
+ /** The run's sessions loaded the machine's Claude setup, so this one does. */
1072
+ const machineClaudeOf = (events) => events.some(({ event }) => event.type === "run.started" && event.settings?.machineClaude === true) ? { machineClaude: true } : {};
1002
1073
  /**
1003
1074
  * Resumes a finished node's last session with the user's message, prints the
1004
1075
  * session, and adds it to the run's record. Returns why it could not.
1005
1076
  */
1006
1077
  const resumeChat = async (options) => {
1007
1078
  const { store, run, node } = options;
1008
- const { session, outputs } = nodeSession(store.events(run.id, 0), node);
1079
+ const events = store.events(run.id, 0);
1080
+ const { session, outputs } = nodeSession(events, node);
1009
1081
  if (!session.known) return `${node} is not a node of run ${run.id}.`;
1010
1082
  if (!session.chattable) return `${node} is not a leader or worker; only they have sessions to talk to.`;
1011
1083
  if (session.sessionId === void 0) return `${node} has no Claude session to resume in run ${run.id}.`;
@@ -1016,7 +1088,7 @@ const resumeChat = async (options) => {
1016
1088
  const notice = chatNotice(store, place, options.home);
1017
1089
  if (notice !== null) options.out(notice);
1018
1090
  const runDir = runDirOf(place, run.id);
1019
- const template = templateOf(workflow.nodes, node);
1091
+ const template = instanceOf(workflow.nodes, node);
1020
1092
  const plan = planNodeSession(workflow, node, {
1021
1093
  cwd,
1022
1094
  inputs: {},
@@ -1031,10 +1103,12 @@ const resumeChat = async (options) => {
1031
1103
  ...template === void 0 ? {} : { template }
1032
1104
  });
1033
1105
  if (!plan.ok) return plan.error;
1106
+ const servers = namedMcpEntries(run.project, plan.mcp);
1107
+ if (!servers.ok) return servers.error;
1034
1108
  let driver = options.driver ?? session.driver ?? plan.driver;
1035
1109
  if (driver === void 0) {
1036
1110
  const ready = readyDriver(await (options.driverChecks ?? checksHere)());
1037
- 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(" ");
1038
1112
  driver = ready.driver;
1039
1113
  }
1040
1114
  const record = appendRecorder(store, run.id, runDir);
@@ -1044,10 +1118,12 @@ const resumeChat = async (options) => {
1044
1118
  body: options.body,
1045
1119
  delivered: "resumed"
1046
1120
  });
1047
- const started = (await options.runtime(driver)).start({
1121
+ const started = (await options.runtime(driver, placeFiles(place).runs)).start({
1048
1122
  ...plan.spec,
1123
+ ...servers.value,
1049
1124
  resume: session.sessionId,
1050
- prompt: options.body
1125
+ prompt: options.body,
1126
+ ...machineClaudeOf(events)
1051
1127
  }, { human: options.human });
1052
1128
  if (options.signal?.aborted === true) started.interrupt();
1053
1129
  options.signal?.addEventListener("abort", () => {
@@ -1079,12 +1155,22 @@ const createLogPrinter = (options) => {
1079
1155
  return (stored) => {
1080
1156
  const { event } = stored;
1081
1157
  if (event.type === "run.started") width = Math.max(...event.nodes.map((n) => n.id.length), 1);
1082
- if (options.node !== void 0 && nodeOf(event) !== options.node) return [];
1158
+ if (options.node !== void 0 && !isNodeEvent(event, options.node)) return [];
1083
1159
  if (options.json) return [JSON.stringify(stored)];
1084
1160
  return formatRunEvent(event, width, tools);
1085
1161
  };
1086
1162
  };
1087
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
+ /**
1088
1174
  * Prints a run's events; with `follow`, then each new one as it is stored:
1089
1175
  * pushed over the run's socket, or polled from the store when there is none.
1090
1176
  * Returns when `run.finished` is stored, the run's process is gone, or `signal`
@@ -1102,6 +1188,31 @@ const followRun = async (store, run, out, options) => {
1102
1188
  ...options.signal === void 0 ? {} : { signal: options.signal }
1103
1189
  }) === "crashed") out(style("red", `run ${run.id} stopped without finishing (its process is gone)`));
1104
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
+ };
1105
1216
  // src/run/origin.ts
1106
1217
  /**
1107
1218
  * @file Where a run's feedback comes from, beyond what its events say: Ork's
@@ -1109,6 +1220,8 @@ const followRun = async (store, run, out, options) => {
1109
1220
  * content hash.
1110
1221
  */
1111
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;
1112
1225
  const feedbackOrigin = (workflowFile) => {
1113
1226
  const origin = { version: ORK_VERSION };
1114
1227
  try {
@@ -1142,6 +1255,8 @@ const runNode = async (options) => {
1142
1255
  if (!pairs.ok) return failed([`--input: ${pairs.error}`]);
1143
1256
  const inputs = resolveInputs(workflow, pairs.raw);
1144
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]);
1145
1260
  const limits = runLimits(options.store, place, options.home);
1146
1261
  const refused = limitsRefusal(limits, workflow);
1147
1262
  if (refused.length > 0) return failed(refused);
@@ -1161,6 +1276,8 @@ const runNode = async (options) => {
1161
1276
  pluginDir: path.join(runDir, "plugins", options.nodeId)
1162
1277
  });
1163
1278
  if (!plan.ok) return failed([plan.error]);
1279
+ const servers = namedMcpEntries(projectRoot, plan.mcp);
1280
+ if (!servers.ok) return failed([servers.error]);
1164
1281
  const named = sessionDriver({
1165
1282
  override: options.driver,
1166
1283
  named: plan.driver
@@ -1169,12 +1286,13 @@ const runNode = async (options) => {
1169
1286
  if (ready?.ok === false) return failed(ready.lines);
1170
1287
  if (ready?.line !== void 0) options.err(ready.line);
1171
1288
  const driver = named ?? ready?.driver;
1172
- if (driver === void 0) return failed(["No Claude driver is ready here."]);
1173
- const planned = workflow.nodes[options.nodeId];
1174
- 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.`);
1175
- const skipped = planned === void 0 ? [] : [...beforeOf(planned, workflow.defaults) === void 0 ? [] : ["before"], ..."verify" in planned && planned.verify !== void 0 ? ["verify"] : []];
1176
- for (const step of skipped) options.err(`${style(["yellow", "bold"], "warning")} ${options.nodeId}'s ${step}: does not run in a run of one node.`);
1177
- 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);
1178
1296
  const id = options.nodeId;
1179
1297
  const recorder = createRecorder(options.store, {
1180
1298
  id: runId,
@@ -1223,7 +1341,8 @@ const runNode = async (options) => {
1223
1341
  nodes: [{
1224
1342
  id,
1225
1343
  type: nodeType
1226
- }]
1344
+ }],
1345
+ ...options.machineClaude === true ? { settings: { machineClaude: true } } : {}
1227
1346
  });
1228
1347
  record({
1229
1348
  type: "node.started",
@@ -1231,7 +1350,12 @@ const runNode = async (options) => {
1231
1350
  nodeType,
1232
1351
  round: 1
1233
1352
  });
1234
- const session = (await options.runtime(driver)).start(plan.spec, {
1353
+ const runtime = await options.runtime(driver, placeFiles(place).runs);
1354
+ if (options.machineClaude === true) plan.spec.machineClaude = true;
1355
+ const session = runtime.start({
1356
+ ...plan.spec,
1357
+ ...servers.value
1358
+ }, {
1235
1359
  human: control.human,
1236
1360
  ...graph === null ? {} : { feedback: createFeedback({
1237
1361
  graph,
@@ -1295,7 +1419,8 @@ const runNode = async (options) => {
1295
1419
  * @file What a run needs to reach other runs: its port for `workflow.call`
1296
1420
  * nodes (RFC-004), its relay when links are on (RFC-003), and the budget a
1297
1421
  * caller has capped it to. A called run being resumed keeps its caller and
1298
- * its cap, which the run recorded.
1422
+ * its cap, which the run recorded, and a run's called runs load the machine's
1423
+ * Claude setup when it does (ADR-065).
1299
1424
  */
1300
1425
  const callPorts = (options) => {
1301
1426
  const { graph, store, runId, home, human, runtime, out } = options;
@@ -1305,6 +1430,7 @@ const callPorts = (options) => {
1305
1430
  const baseRef = options.baseRef ?? options.resume?.settings.baseRef;
1306
1431
  const trigger = options.trigger ?? options.resume?.settings.trigger;
1307
1432
  const untrusted = options.untrusted ?? options.resume?.settings.untrusted;
1433
+ const machineClaude = options.machineClaude ?? options.resume?.settings.machineClaude ?? false;
1308
1434
  const own = graph.workflow.limits?.budgetUsd;
1309
1435
  const budgetUsd = cap === void 0 ? own : Math.min(cap, own ?? Number.POSITIVE_INFINITY);
1310
1436
  return {
@@ -1326,6 +1452,7 @@ const callPorts = (options) => {
1326
1452
  home,
1327
1453
  adopt: adoptedCalls(store, runId),
1328
1454
  newId: () => newRunId(store),
1455
+ machineClaude,
1329
1456
  env: maskedEnv(maskedNamesOfRun(graph.workflow, options.place, store.projects())),
1330
1457
  resumeSession: (run, node, body, signal) => {
1331
1458
  const { blocked, leftUsd } = runLimits(store, options.place, home).now();
@@ -1355,7 +1482,8 @@ const callPorts = (options) => {
1355
1482
  ...calledBy === void 0 ? {} : { calledBy },
1356
1483
  ...baseRef === void 0 ? {} : { baseRef },
1357
1484
  ...trigger === void 0 ? {} : { trigger },
1358
- ...untrusted === void 0 ? {} : { untrusted }
1485
+ ...untrusted === void 0 ? {} : { untrusted },
1486
+ ...machineClaude ? { machineClaude } : {}
1359
1487
  }
1360
1488
  };
1361
1489
  };
@@ -1486,6 +1614,14 @@ const preflight = async (options, given) => {
1486
1614
  lines: driver.lines
1487
1615
  };
1488
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
+ };
1489
1625
  const checkout = await prepareCheckout(workflow, {
1490
1626
  ...options,
1491
1627
  cwd: place.root
@@ -1576,7 +1712,7 @@ const runWorkflowFile = async (options) => {
1576
1712
  for (const event of signals(raw)) {
1577
1713
  control.publish(record(event));
1578
1714
  if (options.json) options.out(JSON.stringify(event));
1579
- 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);
1580
1716
  }
1581
1717
  };
1582
1718
  const workspace = root === void 0 ? void 0 : createWorkspace({
@@ -1593,7 +1729,7 @@ const runWorkflowFile = async (options) => {
1593
1729
  });
1594
1730
  const maskedNames = maskedNamesOfRun(workflow, place, projects);
1595
1731
  const executorOptions = {
1596
- runtimeFor: options.runtime,
1732
+ runtimeFor: (driver) => options.runtime(driver, placeFiles(place).runs),
1597
1733
  inPlace: options.inPlace,
1598
1734
  maskedNames
1599
1735
  };
@@ -1602,10 +1738,11 @@ const runWorkflowFile = async (options) => {
1602
1738
  if (options.tools !== void 0) executorOptions.tools = options.tools;
1603
1739
  const script = process.argv[1];
1604
1740
  if (script !== void 0 && onPath(script)) executorOptions.orkBin = script;
1605
- 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);
1606
1742
  if (replay !== void 0) onEvent({
1607
1743
  type: "run.resumed",
1608
- kept: replay.kept()
1744
+ kept: replay.kept(),
1745
+ ork: ORK_RUN_VERSION
1609
1746
  });
1610
1747
  const ports = callPorts({
1611
1748
  ...options,
@@ -1615,6 +1752,7 @@ const runWorkflowFile = async (options) => {
1615
1752
  human: control.human,
1616
1753
  emit: onEvent
1617
1754
  });
1755
+ if (ports.settings.machineClaude === true) executorOptions.machineClaude = true;
1618
1756
  clearOutputFiles(runDir, options.err);
1619
1757
  const handle = startRun({
1620
1758
  graph: ports.graph,
@@ -1626,6 +1764,7 @@ const runWorkflowFile = async (options) => {
1626
1764
  executors: builtinExecutors(executorOptions),
1627
1765
  human: options.resume === void 0 ? control.human : replayLoopAnswers(control.human, options.resume.loopAnswers),
1628
1766
  onEvent,
1767
+ orkVersion: ORK_RUN_VERSION,
1629
1768
  settings: {
1630
1769
  inPlace: options.inPlace,
1631
1770
  ...options.driver === void 0 ? {} : { driver: options.driver },
@@ -1670,9 +1809,11 @@ const flagValues = (rawArgs, flag) => {
1670
1809
  }
1671
1810
  return values;
1672
1811
  };
1673
- 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}\``}`;
1674
1815
  /** Loads the drivers only when a run starts, so other commands stay fast. */
1675
- const loadRuntime = async (driver) => (await import("./src-Dyz_Gy1q.mjs")).createRuntime(driver, sessionEnv(process.env));
1816
+ const loadRuntime = async (driver, runs) => (await import("./src-oaJYIddp.mjs")).createRuntime(driver, sessionEnv(process.env), runs);
1676
1817
  /**
1677
1818
  * Ctrl-C asks the run to stop, and so does a terminal closing or a polite kill,
1678
1819
  * so that the run ends tidily and leaves no result unscrubbed.
@@ -1690,11 +1831,18 @@ const onInterrupt = (stop) => {
1690
1831
  process.once("SIGHUP", interrupt);
1691
1832
  };
1692
1833
  /** The terminal that answers a run's questions; none in a detached run. */
1693
- const terminalHuman = () => process.env["ORK_DETACHED"] === "1" ? void 0 : createTerminalHuman({
1694
- input: process.stdin,
1695
- output: process.stderr,
1696
- interactive: process.stdin.isTTY
1697
- });
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
+ };
1698
1846
  const run = defineCommand({
1699
1847
  meta: {
1700
1848
  name: "run",
@@ -1716,7 +1864,7 @@ const run = defineCommand({
1716
1864
  },
1717
1865
  driver: {
1718
1866
  type: "string",
1719
- description: "sdk (API key or cloud provider) or cli (your claude login)"
1867
+ description: "claude-code (default: the driver ready here)"
1720
1868
  },
1721
1869
  "in-place": {
1722
1870
  type: "boolean",
@@ -1728,6 +1876,11 @@ const run = defineCommand({
1728
1876
  description: "Run although instructions or skills have uncommitted changes (worktrees get the committed versions)",
1729
1877
  default: false
1730
1878
  },
1879
+ "machine-claude": {
1880
+ type: "boolean",
1881
+ description: "Sessions load this machine's Claude setup: its MCP servers and Claude's memory (default: clean sessions)",
1882
+ default: false
1883
+ },
1731
1884
  json: {
1732
1885
  type: "boolean",
1733
1886
  description: "Print the events as JSON lines",
@@ -1769,7 +1922,7 @@ const run = defineCommand({
1769
1922
  },
1770
1923
  run: async ({ args, rawArgs }) => {
1771
1924
  if (args.driver !== void 0 && !isDriver(args.driver)) {
1772
- writeErr(`--driver must be ${CLAUDE_DRIVERS.map((d) => d.id).join(" or ")}, got \`${args.driver}\``);
1925
+ writeErr(badDriver(args.driver));
1773
1926
  process.exitCode = 1;
1774
1927
  return;
1775
1928
  }
@@ -1812,6 +1965,7 @@ const run = defineCommand({
1812
1965
  cwd,
1813
1966
  inputs: flagValues(rawArgs, "--input"),
1814
1967
  inPlace: args["in-place"],
1968
+ ...args["machine-claude"] ? { machineClaude: true } : {},
1815
1969
  json: args.json,
1816
1970
  runtime: loadRuntime,
1817
1971
  store,
@@ -1926,7 +2080,7 @@ const chat = defineCommand({
1926
2080
  },
1927
2081
  driver: {
1928
2082
  type: "string",
1929
- 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)"
1930
2084
  }
1931
2085
  },
1932
2086
  run: async ({ args }) => {
@@ -1952,7 +2106,7 @@ const chat = defineCommand({
1952
2106
  return;
1953
2107
  }
1954
2108
  if (args.driver !== void 0 && !isDriver(args.driver)) {
1955
- writeErr(`--driver must be sdk or cli, not \`${args.driver}\`.`);
2109
+ writeErr(`${badDriver(args.driver)}.`);
1956
2110
  process.exitCode = 1;
1957
2111
  return;
1958
2112
  }
@@ -1997,7 +2151,8 @@ const chat = defineCommand({
1997
2151
  * writes to the store, then wakes the run on its socket; specs/messaging.md).
1998
2152
  * `--say` gives the answer a note, which goes to the run with it. With `--id`
1999
2153
  * and `--choose`, `--note` and `--rating` instead note how the decision's
2000
- * 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.
2001
2156
  */
2002
2157
  const failed$2 = (message) => {
2003
2158
  writeErr(message);
@@ -2036,7 +2191,7 @@ const saidLine = (store, id) => {
2036
2191
  return note === void 0 ? "" : style("dim", ` · said: ${note}`);
2037
2192
  };
2038
2193
  /** One line per open decision, for `--list`. */
2039
- 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(", ")}]`)}`);
2040
2195
  const decide = defineCommand({
2041
2196
  meta: {
2042
2197
  name: "decide",
@@ -2063,6 +2218,15 @@ const decide = defineCommand({
2063
2218
  description: "List them as JSON",
2064
2219
  default: false
2065
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
+ },
2066
2230
  id: {
2067
2231
  type: "string",
2068
2232
  description: "Answer this decision…"
@@ -2085,7 +2249,7 @@ const decide = defineCommand({
2085
2249
  }
2086
2250
  },
2087
2251
  run: async ({ args }) => {
2088
- 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;
2089
2253
  if (refusal !== null) {
2090
2254
  failed$2(refusal);
2091
2255
  return;
@@ -2097,6 +2261,16 @@ const decide = defineCommand({
2097
2261
  interactive: process.stdin.isTTY
2098
2262
  });
2099
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
+ }
2100
2274
  if (args.id !== void 0 || args.choose !== void 0) {
2101
2275
  if (args.id === void 0 || args.choose === void 0) {
2102
2276
  failed$2("--id and --choose go together.");
@@ -2128,6 +2302,7 @@ const decide = defineCommand({
2128
2302
  return;
2129
2303
  }
2130
2304
  const filter = {};
2305
+ if (args.workflow !== void 0) filter.workflow = args.workflow;
2131
2306
  if (args.run !== void 0 && args.run !== "") {
2132
2307
  const found = findRun(store, args.run, process.cwd());
2133
2308
  if (!found.ok) {
@@ -2142,7 +2317,7 @@ const decide = defineCommand({
2142
2317
  return;
2143
2318
  }
2144
2319
  if (args.list || !process.stdin.isTTY || open.length === 0) {
2145
- for (const line of formatDecisions(open)) write(line);
2320
+ for (const line of formatDecisions(open, args.workflow)) write(line);
2146
2321
  return;
2147
2322
  }
2148
2323
  for (const d of open) {
@@ -2154,6 +2329,7 @@ const decide = defineCommand({
2154
2329
  };
2155
2330
  if (d.recommended !== void 0) question.recommended = d.recommended;
2156
2331
  if (d.context !== void 0) question.context = d.context;
2332
+ if (d.document !== void 0) question.document = d.document;
2157
2333
  let note;
2158
2334
  question.onNote = (given) => {
2159
2335
  note = given;
@@ -2245,7 +2421,7 @@ const resume = defineCommand({
2245
2421
  },
2246
2422
  driver: {
2247
2423
  type: "string",
2248
- description: "sdk or cli (default: the one the run used)"
2424
+ description: "claude-code (default: the one the run used)"
2249
2425
  },
2250
2426
  "in-place": {
2251
2427
  type: "boolean",
@@ -2269,7 +2445,7 @@ const resume = defineCommand({
2269
2445
  },
2270
2446
  run: async ({ args, rawArgs }) => {
2271
2447
  if (args.driver !== void 0 && !isDriver(args.driver)) {
2272
- failed$1(`--driver must be sdk or cli, got \`${args.driver}\``);
2448
+ failed$1(badDriver(args.driver));
2273
2449
  return;
2274
2450
  }
2275
2451
  const store = openStore();
@@ -2320,6 +2496,36 @@ const resume = defineCommand({
2320
2496
  }
2321
2497
  }
2322
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
+ };
2323
2529
  // src/project/ork-skills.ts
2324
2530
  /**
2325
2531
  * @file The Claude Code plugin's skills (`plugins/ork/skills/`) as text, by
@@ -2330,8 +2536,8 @@ const resume = defineCommand({
2330
2536
  const ORK_SKILLS = {
2331
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",
2332
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",
2333
- 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",
2334
- 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"
2335
2541
  };
2336
2542
  // src/project/init-skills.ts
2337
2543
  /**
@@ -2479,7 +2685,7 @@ const projectChecks = (root) => [...linkChecks(root), ...skillChecks(root, ORK_V
2479
2685
  * @file This installation of Ork: its name and version for the root command;
2480
2686
  * `ork doctor`, which prints Ork's version against the last update check and
2481
2687
  * the environment checks (Node, git, the user's own claude, gh and aws
2482
- * 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
2483
2689
  * `ork upgrade`, which asks the npm registry for the latest version and, when
2484
2690
  * it is newer, upgrades Ork the way it was installed after the user agrees
2485
2691
  * (`--yes` skips the question; `--check` only reports). A clone or `npx` gets
@@ -2510,17 +2716,21 @@ const doctor = defineCommand({
2510
2716
  run: async () => {
2511
2717
  const { results, ok } = await runChecks(exec, process.env, process.versions.node);
2512
2718
  const place = projectAt(process.cwd());
2719
+ const experiments = experimentsCheck(place);
2513
2720
  for (const result of [
2514
2721
  orkCheck(),
2515
2722
  ...results,
2516
2723
  ...place.mode === "attached" ? [] : projectChecks(place.root),
2517
2724
  socketCheck(),
2518
- ...triggerChecks()
2725
+ ...triggerChecks(),
2726
+ ...await leaseChecks(place),
2727
+ experiments
2519
2728
  ]) for (const line of formatCheck(result)) write(line);
2520
2729
  if (!ok) {
2521
- 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.`);
2522
2731
  process.exitCode = 1;
2523
2732
  }
2733
+ if (experiments.status === "fail") process.exitCode = 1;
2524
2734
  }
2525
2735
  });
2526
2736
  /** The ids of runs still running anywhere on this machine. */
@@ -3015,7 +3225,7 @@ const driverLine = (checks, projectDefault) => {
3015
3225
  const why = ready.ready === 1 ? ready.detail : `more than one is ready, and ${ready.driver} comes first`;
3016
3226
  return `Runs use the driver that's ready when they start: ${ready.driver} here (${why}).`;
3017
3227
  }
3018
- 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`."}`;
3019
3229
  };
3020
3230
  /**
3021
3231
  * The line for the project at `root`: `ork doctor`'s driver checks through
@@ -3616,7 +3826,7 @@ const apply = defineCommand({
3616
3826
  item: itemArg$1,
3617
3827
  driver: {
3618
3828
  type: "string",
3619
- description: "sdk (API key or cloud provider) or cli (your claude login)"
3829
+ description: "claude-code (default: the driver ready here)"
3620
3830
  },
3621
3831
  "run-id": {
3622
3832
  type: "string",
@@ -3625,7 +3835,7 @@ const apply = defineCommand({
3625
3835
  },
3626
3836
  run: async ({ args }) => {
3627
3837
  if (args.driver !== void 0 && !isDriver(args.driver)) {
3628
- writeErr(`--driver must be sdk or cli, got \`${args.driver}\``);
3838
+ writeErr(badDriver(args.driver));
3629
3839
  process.exitCode = 1;
3630
3840
  return;
3631
3841
  }
@@ -4195,6 +4405,12 @@ const logs = defineCommand({
4195
4405
  process.exitCode = 1;
4196
4406
  return;
4197
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
+ }
4198
4414
  const abort = new AbortController();
4199
4415
  process.once("SIGINT", () => {
4200
4416
  abort.abort();
@@ -4228,8 +4444,9 @@ const tree = defineCommand({
4228
4444
  return;
4229
4445
  }
4230
4446
  const { run } = found;
4231
- const events = store.events(run.id).map((s) => s.event);
4232
- 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));
4233
4450
  if (args.json) {
4234
4451
  write(JSON.stringify(roots, void 0, 2));
4235
4452
  return;
@@ -4358,6 +4575,8 @@ const init = defineCommand({
4358
4575
  skills: args.skills,
4359
4576
  link: args.link
4360
4577
  });
4578
+ const noCommit = await headWarning(exec);
4579
+ if (noCommit !== null) write(style("yellow", `\n! git: ${noCommit}`));
4361
4580
  write(`\nNext: edit ${WORKFLOW_DIR}/, then run ${style("bold", "ork validate")}.`);
4362
4581
  }
4363
4582
  });
@@ -4556,27 +4775,27 @@ const main = defineCommand({
4556
4775
  ps,
4557
4776
  logs,
4558
4777
  tree,
4559
- usage: () => import("./usage-CJnytoe4.mjs").then((module) => module.usage),
4560
- prune: () => import("./usage-CJnytoe4.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),
4561
4780
  decide,
4562
- relay: () => import("./relay-o-Luee1c.mjs").then((module) => module.relay),
4563
- pause: () => import("./pause-B_SL_yDz.mjs").then((module) => module.pause),
4564
- continue: () => import("./pause-B_SL_yDz.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),
4565
4784
  stop,
4566
4785
  chat,
4567
- send: () => import("./send-3B64fBNB.mjs").then((module) => module.send),
4786
+ send: () => import("./send-C0uwfUYi.mjs").then((module) => module.send),
4568
4787
  resume,
4569
4788
  feedback,
4570
4789
  context,
4571
- worktrees: () => import("./worktrees-86ySXxR6.mjs").then((module) => module.worktrees),
4572
- wt: () => import("./worktrees-86ySXxR6.mjs").then((module) => module.worktrees),
4573
- watch: () => import("./watch-BJB82ZJ7.mjs").then((module) => module.watch),
4574
- mcp: () => import("./mcp-CKPyNaKD.mjs").then((module) => module.mcp),
4575
- ui: () => import("./ui-CY4xxJUu.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),
4576
4795
  doctor,
4577
4796
  upgrade,
4578
- completion: () => import("./completion-snnYGjgK.mjs").then((module) => module.completion),
4579
- __complete: () => import("./completion-snnYGjgK.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))
4580
4799
  }
4581
4800
  });
4582
4801
  // src/bin.ts
@@ -4594,7 +4813,7 @@ const main = defineCommand({
4594
4813
  clearGitLocalEnv();
4595
4814
  const argv = process.argv.slice(2);
4596
4815
  if (argv[0] === "__complete") {
4597
- const { printCompletions } = await import("./completion-snnYGjgK.mjs");
4816
+ const { printCompletions } = await import("./completion-Dv5kf598.mjs");
4598
4817
  await printCompletions(main, argv.slice(1));
4599
4818
  } else if (process.env["ORK_UPDATE_CHECK_CHILD"] === "1") await runUpdateChild();
4600
4819
  else {