@krapht/ork 0.4.0 → 1.0.0-alpha.1

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 (37) hide show
  1. package/README.md +16 -12
  2. package/dist/{__build_info__-CtSVVuZE.mjs → __build_info__-D9lSlMFy.mjs} +36 -112
  3. package/dist/bin.mjs +688 -306
  4. package/dist/{clean-Dcr807be.mjs → clean-CA_Oi2Wl.mjs} +1 -1
  5. package/dist/completion-DBGfL-gN.mjs +405 -0
  6. package/dist/{context-C_bksb0C.mjs → context-6x3kXREO.mjs} +11 -88
  7. package/dist/decide-CoIJh34e.mjs +113 -0
  8. package/dist/{find-DptmbhGd.mjs → find-rBBk7Jui.mjs} +1 -1
  9. package/dist/{format-J-a21Yvn.mjs → format-zMmhqyLE.mjs} +71 -37
  10. package/dist/help-groups-BwYoAWsh.mjs +354 -0
  11. package/dist/{in-a-run-D1Qc5Mit.mjs → in-a-run-DdAakpRR.mjs} +1 -1
  12. package/dist/{limits-BDZCmGEu.mjs → limits-BPM9wUlV.mjs} +1 -1
  13. package/dist/{list-ZvY2OcLq.mjs → list-Bv5Lxu7z.mjs} +1 -1
  14. package/dist/{mcp-CHVKKg3B.mjs → mcp-DM30MFBw.mjs} +7 -7
  15. package/dist/{output-CXSJO00X.mjs → output-TV_j7Ni0.mjs} +67 -17
  16. package/dist/{pause-BPSmg4yz.mjs → pause-C7lJkSma.mjs} +4 -4
  17. package/dist/{relay-Do-aNbSD.mjs → relay-BipJYszF.mjs} +2 -2
  18. package/dist/{remove-AwaMMaHf.mjs → remove-BDs9isB6.mjs} +3 -3
  19. package/dist/{send-DhGBkowy.mjs → send-nbkjf3sc.mjs} +3 -3
  20. package/dist/{src-CpDKIulI.mjs → src-B0TT7DBi.mjs} +28 -15
  21. package/dist/{src-BHnxpJyH.mjs → src-B9v0lDc6.mjs} +58 -10
  22. package/dist/{src-UWkk1sl9.mjs → src-BxUJPGa5.mjs} +252 -18
  23. package/dist/{src-CGqMati3.mjs → src-hrj2h0IZ.mjs} +640 -220
  24. package/dist/ui/THIRD_PARTY_LICENSES.txt +2 -2
  25. package/dist/ui/assets/app-BNT0SuIz.css +1 -0
  26. package/dist/ui/assets/app-CmkynZyW.js +67 -0
  27. package/dist/ui/assets/{index-BiO54L90.css → index-BPIbqRRb.css} +1 -1
  28. package/dist/ui/assets/{index-MQNNgsyT.js → index-CPx74lFf.js} +2 -2
  29. package/dist/ui/index.html +2 -3
  30. package/dist/{ui-DhutUMgQ.mjs → ui-B98FSboa.mjs} +5 -5
  31. package/dist/{usage-CT7Z-yjr.mjs → usage-EW7DI6Z7.mjs} +6 -6
  32. package/dist/{watch-0Qr2UfCQ.mjs → watch-BAkSGaOp.mjs} +7 -7
  33. package/dist/workflows-vsEohqa7.mjs +88 -0
  34. package/dist/{worktrees-DHl80k96.mjs → worktrees-Ahj9rdWK.mjs} +5 -5
  35. package/package.json +3 -3
  36. package/dist/ui/assets/app-BgVDC_5l.js +0 -67
  37. package/dist/ui/assets/app-DVf68fj5.css +0 -1
package/dist/bin.mjs CHANGED
@@ -1,17 +1,18 @@
1
1
  #!/usr/bin/env node
2
- import { $ as resumable, $t as planMoveInto, A as parseInputPairs, At as FEEDBACK_KINDS, Bn as isClaudeNode, Bt as normalizeTitle, F as endLink, Fn as WorkflowSchema, G as tellLiveRun, Ht as maskReplacer, I as endLinksOf, It as sendPost, Jn as ORK_LAYOUT, Jt as serveRun, Ln as settingsOf, Mt as FEEDBACK_TARGETS, N as createCalls, P as addressOf, Pt as planPost, Q as detachResume, Qt as applySteps, R as peerOf, Rt as createFeedback, T as inspectProject, V as flattenTree, W as requestStop, Wn as INTEGRATIONS, Xn as firstLine, Xt as wakeRun, Yn as CLAUDE_DRIVERS, Yt as socketPath, Z as adoptedCalls, Zt as planAgentMigration, _n as inside, an as catalogAgent, cn as hasInRepoFiles, d as runningRunIds, dn as placeFiles, dt as runStatus, en as linkTools, et as createReplay, f as runningScript, fn as placeFor, ft as runTree, g as triggersReport, h as lateTriggers, hn as shownPath, i as NOT_STARTED, in as deleteAgent, it as followEvents, j as resolveInputs, jn as serializeWorkflow, jt as FEEDBACK_SEVERITIES, kt as gitRulesOf, l as planUpgrade, ln as inRepoPlace, lt as liveStatus, m as isNewer, mn as runDirOf, n as resolveBase, nn as toolsInUse, on as loadAgents, pn as projectsHome, qn as AGENT_TOOL_LAYOUTS, rn as createAgent, rt as detachRun, st as planNodeSession, t as createWorkspace, tn as toolLinks, tt as replayLoopAnswers, u as releaseNotes, un as newProjectId, vn as projectRootOf, vt as walkSummary, xt as userMessageText, yt as startRun, z as tellNode, zn as beforeOf, zt as fingerprint } from "./src-CGqMati3.mjs";
3
- import { C as checkNote, D as placeOf, O as feedbackActions, S as previewPost, T as acceptItem, _ as answerDecision, b as builtinWorkflowFile, c as checkDue, d as pendingNotice, f as readUpdateCache, g as nodeSession, h as writeUpdateCache, i as registryOf, k as openFixRun, 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, v as pendingDecisions, w as recordNote, y as builtinProblem } from "./__build_info__-CtSVVuZE.mjs";
4
- import { n as newRunId, t as findRun } from "./find-DptmbhGd.mjs";
5
- 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-CXSJO00X.mjs";
6
- import { n as listOrkWorktrees, t as createdWorktrees } from "./list-ZvY2OcLq.mjs";
7
- import { a as planClean, t as applyClean } from "./clean-Dcr807be.mjs";
8
- import { a as formatRunEvent, i as formatEvent, n as formatRuns, o as createTerminalHuman, r as formatTree, t as ago } from "./format-J-a21Yvn.mjs";
9
- import { a as validate, c as warningLines, d as refusedBuiltin, f as workflowArg, i as loadWorkflow, l as WORKFLOW_DIR, m as registeredProjects, o as validateFile, p as projectAt, r as formatReport, s as walkLoaded, t as context, u as WORKFLOW_SUFFIX } from "./context-C_bksb0C.mjs";
10
- import { i as runLimits, r as limitsRefusal, t as chatNotice } from "./limits-BDZCmGEu.mjs";
11
- 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-UWkk1sl9.mjs";
12
- import { i as decideRefusal, n as chatRefusal } from "./in-a-run-D1Qc5Mit.mjs";
13
- import { defineCommand, runMain } from "citty";
14
- import { createInterface } from "node:readline/promises";
2
+ import { Bn as serializeWorkflow, Bt as FEEDBACK_TARGETS, Cn as runDirOf, D as inspectProject, Dn as projectRootOf, Dt as userMessageText, En as inside, Ft as PROJECT_FILE, H as tellNode, Ht as planPost, I as createCalls, It as gitRulesOf, J as tellLiveRun, Jn as beforeOf, Jt as normalizeTitle, Kn as settingsOf, Kt as createFeedback, L as addressOf, Lt as loadProject, M as clearGitLocalEnv, N as parseInputPairs, P as resolveInputs, R as endLink, Rt as FEEDBACK_KINDS, Sn as projectsHome, Tt as startRun, Un as WorkflowSchema, V as peerOf, W as flattenTree, Wt as sendPost, Xt as maskReplacer, Yn as isClaudeNode, _ as lateTriggers, _n as hasInRepoFiles, _t as runStatus, an as wakeRun, ar as CLAUDE_DRIVERS, bn as placeFiles, cn as planMoveInto, d as runningRunIds, dn as toolsInUse, dt as namingNoDriver, er as INTEGRATIONS, et as adoptedCalls, f as runningScript, fn as createAgent, ft as sessionDriver, h as isNewer, hn as loadAgents, ht as liveStatus, i as NOT_STARTED, in as socketRoom, ir as ORK_LAYOUT, it as replayLoopAnswers, l as planUpgrade, ln as linkTools, mn as catalogAgent, n as resolveBase, nn as serveRun, nt as resumable, on as planAgentMigration, or as firstLine, ot as detachRun, pn as deleteAgent, q as requestStop, qt as fingerprint, rn as socketPath, rr as AGENT_TOOL_LAYOUTS, rt as createReplay, sn as applySteps, st as followEvents, t as createWorkspace, tt as detachResume, u as releaseNotes, un as toolLinks, ut as planNodeSession, v as triggersReport, vn as inRepoPlace, vt as runTree, wn as shownPath, wt as walkSummary, xn as placeFor, yn as newProjectId, z as endLinksOf, zt as FEEDBACK_SEVERITIES } from "./src-hrj2h0IZ.mjs";
3
+ import { C as feedbackActions, S as placeOf, _ as previewPost, b as acceptItem, c as checkDue, d as pendingNotice, f as readUpdateCache, g as nodeSession, h as writeUpdateCache, i as registryOf, l as fetchLatest, m as updateCachePath, n as ORK_VERSION, o as updateSwitchOff, p as refreshUpdateCache, r as isSwitchedOn, s as runInstall, t as ORK_NAME, u as newerVersion$1, v as checkNote, w as openFixRun, y as recordNote } from "./__build_info__-D9lSlMFy.mjs";
4
+ import { i as builtinWorkflowFile, n as pendingDecisions, r as builtinProblem, t as answerDecision } from "./decide-CoIJh34e.mjs";
5
+ import { n as newRunId, t as findRun } from "./find-rBBk7Jui.mjs";
6
+ import { a as write, c as openStore, i as style, l as storePath, n as cost, o as writeErr, s as nodeOf, t as confirm } from "./output-TV_j7Ni0.mjs";
7
+ import { n as listOrkWorktrees, t as createdWorktrees } from "./list-Bv5Lxu7z.mjs";
8
+ import { a as planClean, t as applyClean } from "./clean-CA_Oi2Wl.mjs";
9
+ import { c as subcommandsOf, i as argsOf, n as groupedEntries, o as publicArgs, s as resolved, t as groupOf } from "./help-groups-BwYoAWsh.mjs";
10
+ import { a as formatRunEvent, i as formatEvent, n as formatRuns, o as createTerminalHuman, r as formatTree, t as ago } from "./format-zMmhqyLE.mjs";
11
+ import { c as registeredProjects, i as refusedBuiltin, n as WORKFLOW_SUFFIX, o as workflowArg, s as projectAt, t as WORKFLOW_DIR } from "./workflows-vsEohqa7.mjs";
12
+ import { a as validate, c as warningLines, i as loadWorkflow, o as validateFile, r as formatReport, s as walkLoaded, t as context } from "./context-6x3kXREO.mjs";
13
+ import { i as runLimits, r as limitsRefusal, t as chatNotice } from "./limits-BPM9wUlV.mjs";
14
+ import { _ as resultOf, a as builtinExecutors, d as maskedEnv, f as maskedNamesOfRun, g as clearOutputFiles, h as sessionEnv, i as onPath, l as clearStateFiles, m as runScrubber, n as missingProgramMessage, r as missingPrograms, t as envProblems } from "./src-BxUJPGa5.mjs";
15
+ import { i as decideRefusal, n as chatRefusal } from "./in-a-run-DdAakpRR.mjs";
15
16
  import path from "node:path";
16
17
  import { stringify } from "yaml";
17
18
  import { z } from "zod";
@@ -20,7 +21,38 @@ import { createHash } from "node:crypto";
20
21
  import { homedir } from "node:os";
21
22
  import { execFile, spawn } from "node:child_process";
22
23
  import { setTimeout } from "node:timers/promises";
24
+ import { defineCommand, renderUsage, runMain } from "citty";
25
+ import { createInterface } from "node:readline/promises";
23
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
+ };
24
56
  // ../core/src/control/queue.ts
25
57
  const PERMISSION_OPTIONS = [{
26
58
  id: "allow",
@@ -30,6 +62,8 @@ const PERMISSION_OPTIONS = [{
30
62
  label: "Deny"
31
63
  }];
32
64
  const DENIED = "The user did not allow this tool.";
65
+ /** The deny message a session gets, with the person's words when given. */
66
+ const deniedMessage = (note) => note === void 0 ? DENIED : `${DENIED} They said: ${note}`;
33
67
  /** At most `max` characters of a tool's input, for the decision's context. */
34
68
  const inputPreview = (input, max = 400) => {
35
69
  const json = JSON.stringify(input);
@@ -56,22 +90,30 @@ const createDecisionQueue = (options) => {
56
90
  options: record.options
57
91
  };
58
92
  if (record.recommended !== void 0) event.recommended = record.recommended;
93
+ if (record.owner !== void 0) event.owner = record.owner;
59
94
  emit(event);
60
95
  const { promise, resolve, reject } = Promise.withResolvers();
61
96
  const settled = new AbortController();
62
- const finish = (answer, by) => {
97
+ /** `note` is the stored one: masked, and absent when blank. */
98
+ const finish = (answer, by, note) => {
63
99
  if (settled.signal.aborted) return;
64
100
  settled.abort();
65
- emit({
101
+ const answered = {
66
102
  type: "decision.answered",
67
103
  node: open.node,
68
104
  decision: id,
69
105
  answer,
70
106
  by
71
- });
72
- resolve({
107
+ };
108
+ if (note !== void 0) answered.note = note;
109
+ emit(answered);
110
+ resolve(note === void 0 ? {
73
111
  answer,
74
112
  by
113
+ } : {
114
+ answer,
115
+ by,
116
+ note
75
117
  });
76
118
  };
77
119
  const withdraw = () => {
@@ -90,13 +132,13 @@ const createDecisionQueue = (options) => {
90
132
  const fromStore = async () => {
91
133
  try {
92
134
  const closed = await options.waitFor(id, settled.signal);
93
- if (closed.status === "answered" && closed.answer !== void 0) finish(closed.answer, closed.answeredBy ?? "decide");
135
+ if (closed.status === "answered" && closed.answer !== void 0) finish(closed.answer, closed.answeredBy ?? "decide", closed.note);
94
136
  } catch {}
95
137
  };
96
138
  const fromTerminal = async (ask) => {
97
139
  try {
98
- const answer = await ask(settled.signal);
99
- if (store.answerDecision(id, answer, "terminal")) finish(answer, "terminal");
140
+ const { answer, note } = await ask(settled.signal);
141
+ if (store.answerDecision(id, answer, "terminal", note)) finish(answer, "terminal", store.decision(id)?.note);
100
142
  } catch {}
101
143
  };
102
144
  fromStore();
@@ -113,31 +155,92 @@ const createDecisionQueue = (options) => {
113
155
  };
114
156
  if (question.recommended !== void 0) open.recommended = question.recommended;
115
157
  if (question.context !== void 0) open.context = question.context;
158
+ if (question.owner !== void 0) open.owner = question.owner;
116
159
  const { local } = options;
117
- return decide(open, local === void 0 ? void 0 : (signal) => local.ask({
118
- ...question,
119
- signal
120
- }), question.signal).then(({ answer }) => answer);
160
+ return decide(open, local === void 0 ? void 0 : (signal) => {
161
+ let note;
162
+ return local.ask({
163
+ ...question,
164
+ signal,
165
+ onNote: (given) => {
166
+ note = given;
167
+ }
168
+ }).then((answer) => note === void 0 ? { answer } : {
169
+ answer,
170
+ note
171
+ });
172
+ }, question.signal).then(({ answer, note }) => {
173
+ if (note !== void 0) question.onNote?.(note);
174
+ return answer;
175
+ });
176
+ },
177
+ settled: (question) => {
178
+ count += 1;
179
+ const id = `${runId}-${String(count)}`;
180
+ const { owner } = question;
181
+ const record = store.openDecision({
182
+ id,
183
+ runId,
184
+ node: question.nodeId,
185
+ kind: "question",
186
+ question: question.question,
187
+ options: question.options,
188
+ owner,
189
+ ...question.recommended === void 0 ? {} : { recommended: question.recommended },
190
+ ...question.context === void 0 ? {} : { context: question.context }
191
+ });
192
+ emit({
193
+ type: "decision.opened",
194
+ node: record.node,
195
+ decision: id,
196
+ kind: "question",
197
+ question: record.question,
198
+ options: record.options,
199
+ owner,
200
+ ...record.recommended === void 0 ? {} : { recommended: record.recommended }
201
+ });
202
+ store.answerDecision(id, question.answer, "owner", question.note);
203
+ const note = store.decision(id)?.note;
204
+ emit({
205
+ type: "decision.answered",
206
+ node: record.node,
207
+ decision: id,
208
+ answer: question.answer,
209
+ by: "owner",
210
+ owner,
211
+ ...note === void 0 ? {} : { note }
212
+ });
121
213
  },
122
214
  permit: async (request) => {
123
215
  const { local } = options;
124
216
  let localMessage = DENIED;
125
- const { answer } = await decide({
217
+ const { answer, note } = await decide({
126
218
  node: request.nodeId,
127
219
  kind: "permission",
128
220
  question: `Allow ${request.toolName}?`,
129
221
  options: PERMISSION_OPTIONS,
130
222
  context: inputPreview(request.input)
131
- }, local === void 0 ? void 0 : (signal) => local.permit({
132
- ...request,
133
- signal
134
- }).then((result) => {
135
- if (!result.allow) localMessage = result.message;
136
- return result.allow ? "allow" : "deny";
137
- }));
223
+ }, local === void 0 ? void 0 : (signal) => {
224
+ let said;
225
+ return local.permit({
226
+ ...request,
227
+ signal,
228
+ onNote: (given) => {
229
+ said = given;
230
+ }
231
+ }).then((result) => {
232
+ if (!result.allow) localMessage = result.message;
233
+ const choice = result.allow ? "allow" : "deny";
234
+ return said === void 0 ? { answer: choice } : {
235
+ answer: choice,
236
+ note: said
237
+ };
238
+ });
239
+ });
240
+ if (note !== void 0) request.onNote?.(note);
138
241
  return answer === "allow" ? { allow: true } : {
139
242
  allow: false,
140
- message: localMessage
243
+ message: note === void 0 ? localMessage : deniedMessage(note)
141
244
  };
142
245
  }
143
246
  };
@@ -343,7 +446,7 @@ const createSignals = (options = {}) => {
343
446
  }
344
447
  if (event.type !== "node.event") return;
345
448
  const inner = event.event;
346
- if (inner.type === "session.started") sessions.set(event.id, {
449
+ if (inner.type === "session.started" && inner.fork !== true) sessions.set(event.id, {
347
450
  model: inner.model,
348
451
  ...inner.driver === void 0 ? {} : { driver: inner.driver }
349
452
  });
@@ -530,10 +633,357 @@ const appendRecorder = (store, runId, runDir) => {
530
633
  return stored;
531
634
  };
532
635
  };
533
- /** Begins `run` in the store; `runDir` gets the JSONL copy. */
534
- const createRecorder = (store, run, runDir) => {
535
- store.begin(run);
536
- return appendRecorder(store, run.id, runDir);
636
+ /** Begins `run` in the store; `runDir` gets the JSONL copy. */
637
+ const createRecorder = (store, run, runDir) => {
638
+ store.begin(run);
639
+ return appendRecorder(store, run.id, runDir);
640
+ };
641
+ // src/help-root.ts
642
+ /** Where the user docs start. */
643
+ const DOCS_URL = "https://github.com/mimshins/ork/blob/main/docs/README.md";
644
+ const rowOf = (entry) => {
645
+ const short = groupOf(entry.name)?.short ?? entry.description;
646
+ const alias = entry.aliases.length === 0 ? "" : ` (alias: ${entry.aliases.join(", ")})`;
647
+ return {
648
+ name: entry.name,
649
+ line: `${short}${alias}`
650
+ };
651
+ };
652
+ const groupedRows = (entries) => groupedEntries(entries).map((group) => ({
653
+ title: group.title,
654
+ rows: group.entries.map((entry) => rowOf(entry))
655
+ }));
656
+ /** A group's title, its rows with names `width` wide, and a blank line. */
657
+ const groupLines = (group, width) => [style("bold", `${group.title}:`)].concat(group.rows.map((row) => ` ${style("cyan", row.name.padEnd(width))} ${row.line}`), [""]);
658
+ /** The root command's help text. */
659
+ const renderRootHelp = async (cmd) => {
660
+ const meta = cmd.meta === void 0 ? {} : await resolved(cmd.meta);
661
+ const name = meta.name ?? "ork";
662
+ const groups = groupedRows(await subcommandsOf(cmd));
663
+ const width = Math.max(...groups.flatMap((g) => g.rows.map((r) => r.name.length)));
664
+ return [
665
+ `${meta.description ?? ""}.`,
666
+ `Docs: ${DOCS_URL}`,
667
+ "",
668
+ ...groups.flatMap((group) => groupLines(group, width)),
669
+ `Usage: ${name} <command> [flags]`,
670
+ "",
671
+ `Use "${name} <command> --help" for more about a command.`,
672
+ `${name} v${meta.version ?? ""}`
673
+ ].join("\n");
674
+ };
675
+ // src/help.ts
676
+ /**
677
+ * @file `ork <command> --help`: citty's usage, without the flags Ork passes to
678
+ * itself (the command tree's filter), and with `<command> [flags]` in place
679
+ * of citty's `a|b|c` list for a command with subcommands. The root command's
680
+ * help is grouped (`help-root.ts`).
681
+ */
682
+ /** Citty's `a|b|c` usage list of a command's shown subcommands. */
683
+ const choicesOf = async (cmd) => (await subcommandsOf(cmd)).flatMap((entry) => entry.hidden ? [] : [entry.name].concat(entry.aliases)).join("|");
684
+ /** The usage of `cmd` (under `parent`), as people see it. */
685
+ const renderHelp = async (cmd, parent) => {
686
+ const shown = {
687
+ args: publicArgs(await argsOf(cmd)),
688
+ ...cmd.meta === void 0 ? {} : { meta: cmd.meta },
689
+ ...cmd.subCommands === void 0 ? {} : { subCommands: cmd.subCommands }
690
+ };
691
+ if (cmd.subCommands !== void 0 && parent === void 0) return renderRootHelp(shown);
692
+ const above = parent?.meta === void 0 ? void 0 : { meta: parent.meta };
693
+ const usage = await renderUsage(shown, above);
694
+ if (cmd.subCommands === void 0) return usage;
695
+ const choices = await choicesOf(shown);
696
+ return usage.replace(` [OPTIONS] ${choices}`, ` <command> [flags]`).replace(` ${choices}`, ` <command> [flags]`);
697
+ };
698
+ /** Prints {@link renderHelp}'s usage, as citty prints its own. */
699
+ const showHelp = async (cmd, parent) => {
700
+ process.stdout.write(`${await renderHelp(cmd, parent)}\n\n`);
701
+ };
702
+ // src/update/notify.ts
703
+ /**
704
+ * @file The new-version notice (ADR-038). At most once a day a command starts a
705
+ * detached copy of `ork` that asks the npm registry for the latest version
706
+ * and records it in `~/.ork/update-check.json`, so no command waits on the
707
+ * network; a later command, when a newer version is known, ends with one line
708
+ * on stderr (once a day). Off in CI, without a terminal, with `--json`, and
709
+ * with `ORK_NO_UPDATE_CHECK=1` or `NO_UPDATE_NOTIFIER=1`.
710
+ */
711
+ /** Set in the background copy of `ork` that only runs the check. */
712
+ const UPDATE_CHILD_ENV = "ORK_UPDATE_CHECK_CHILD";
713
+ /**
714
+ * Why the automatic check is off for this command, or `null` when it is on: the
715
+ * environment's switches (core's, which `ork ui` reads too), then where a
716
+ * notice on stderr would go.
717
+ */
718
+ const updateCheckOff = (env, argv, terminal) => {
719
+ const switched = updateSwitchOff(env);
720
+ if (switched !== null) return switched === "CI" ? "in CI" : "turned off";
721
+ if (isSwitchedOn(env["ORK_DETACHED"]) || !terminal) return "no terminal";
722
+ if (argv.includes("--json")) return "JSON output";
723
+ if (argv[0] === "upgrade") return "ork upgrade checks for itself";
724
+ if (argv[0] === "ui" && argv.includes("--serve")) return "ork ui's server checks for itself";
725
+ return null;
726
+ };
727
+ /** The notice's text. */
728
+ const updateNotice = (latest, current) => `Ork ${latest} is out (you have ${current}). Upgrade with: ork upgrade`;
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 {}
752
+ };
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
+ });
760
+ };
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 {}
777
+ };
778
+ // src/update/upgrade.ts
779
+ /**
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`.
784
+ */
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."
793
+ };
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)}`})`
798
+ };
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.`
835
+ };
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")
882
+ };
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`
895
+ };
896
+ return {
897
+ name: "sockets",
898
+ status: "warn",
899
+ detail: `a run's socket path is ${String(bytes)} bytes, past the ${String(max)} this system allows`,
900
+ hint: "Runs still work, but answers, commands and `ork logs --follow` wait up to a second for each, as they read the store instead. A shorter home folder path fixes it."
901
+ };
902
+ };
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
+ }
935
+ };
936
+ // src/run/ready-driver.ts
937
+ /**
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.
943
+ */
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`;
950
+ };
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
+ };
971
+ };
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)());
537
987
  };
538
988
  // src/runs/chat.ts
539
989
  /**
@@ -541,7 +991,8 @@ const createRecorder = (store, run, runDir) => {
541
991
  * the store and the run's socket, and the run's bus pushes it into the node's
542
992
  * session (specs/messaging.md#chat; `tellLiveRun` and `nodeSession` are in
543
993
  * core). A node of a finished run gets it by resuming its last Claude session
544
- * where it ran, and the exchange is added to the run's record.
994
+ * where it ran, as the run's sessions were set up (ADR-065), and the exchange
995
+ * is added to the run's record.
545
996
  */
546
997
  /** The declared node a spawned worker (`<template>-<n>`) is an instance of. */
547
998
  const templateOf = (nodes, id) => {
@@ -549,13 +1000,16 @@ const templateOf = (nodes, id) => {
549
1000
  const template = id.replace(/-\d+$/u, "");
550
1001
  return template in nodes ? template : void 0;
551
1002
  };
1003
+ /** The run's sessions loaded the machine's Claude setup, so this one does. */
1004
+ const machineClaudeOf = (events) => events.some(({ event }) => event.type === "run.started" && event.settings?.machineClaude === true) ? { machineClaude: true } : {};
552
1005
  /**
553
1006
  * Resumes a finished node's last session with the user's message, prints the
554
1007
  * session, and adds it to the run's record. Returns why it could not.
555
1008
  */
556
1009
  const resumeChat = async (options) => {
557
1010
  const { store, run, node } = options;
558
- const { session, outputs } = nodeSession(store.events(run.id, 0), node);
1011
+ const events = store.events(run.id, 0);
1012
+ const { session, outputs } = nodeSession(events, node);
559
1013
  if (!session.known) return `${node} is not a node of run ${run.id}.`;
560
1014
  if (!session.chattable) return `${node} is not a leader or worker; only they have sessions to talk to.`;
561
1015
  if (session.sessionId === void 0) return `${node} has no Claude session to resume in run ${run.id}.`;
@@ -581,6 +1035,12 @@ const resumeChat = async (options) => {
581
1035
  ...template === void 0 ? {} : { template }
582
1036
  });
583
1037
  if (!plan.ok) return plan.error;
1038
+ let driver = options.driver ?? session.driver ?? plan.driver;
1039
+ if (driver === void 0) {
1040
+ const ready = readyDriver(await (options.driverChecks ?? checksHere)());
1041
+ if (!ready.ok) return ["No Claude driver is ready here.", ...ready.hints].join(" ");
1042
+ driver = ready.driver;
1043
+ }
584
1044
  const record = appendRecorder(store, run.id, runDir);
585
1045
  record({
586
1046
  type: "user.message",
@@ -588,10 +1048,11 @@ const resumeChat = async (options) => {
588
1048
  body: options.body,
589
1049
  delivered: "resumed"
590
1050
  });
591
- const started = (await options.runtime(options.driver ?? session.driver ?? plan.driver)).start({
1051
+ const started = (await options.runtime(driver)).start({
592
1052
  ...plan.spec,
593
1053
  resume: session.sessionId,
594
- prompt: options.body
1054
+ prompt: options.body,
1055
+ ...machineClaudeOf(events)
595
1056
  }, { human: options.human });
596
1057
  if (options.signal?.aborted === true) started.interrupt();
597
1058
  options.signal?.addEventListener("abort", () => {
@@ -705,6 +1166,15 @@ const runNode = async (options) => {
705
1166
  pluginDir: path.join(runDir, "plugins", options.nodeId)
706
1167
  });
707
1168
  if (!plan.ok) return failed([plan.error]);
1169
+ const named = sessionDriver({
1170
+ override: options.driver,
1171
+ named: plan.driver
1172
+ });
1173
+ const ready = named === void 0 ? chooseReadyDriver([options.nodeId], true, await (options.driverChecks ?? checksHere)()) : null;
1174
+ if (ready?.ok === false) return failed(ready.lines);
1175
+ if (ready?.line !== void 0) options.err(ready.line);
1176
+ const driver = named ?? ready?.driver;
1177
+ if (driver === void 0) return failed(["No Claude driver is ready here."]);
708
1178
  const planned = workflow.nodes[options.nodeId];
709
1179
  if (planned !== void 0 && "notify" in planned && planned.notify) options.err(`${style(["yellow", "bold"], "warning")} ${options.nodeId}'s notify: does nothing here: a run of one node has no message bus.`);
710
1180
  const skipped = planned === void 0 ? [] : [...beforeOf(planned, workflow.defaults) === void 0 ? [] : ["before"], ..."verify" in planned && planned.verify !== void 0 ? ["verify"] : []];
@@ -758,7 +1228,8 @@ const runNode = async (options) => {
758
1228
  nodes: [{
759
1229
  id,
760
1230
  type: nodeType
761
- }]
1231
+ }],
1232
+ ...options.machineClaude === true ? { settings: { machineClaude: true } } : {}
762
1233
  });
763
1234
  record({
764
1235
  type: "node.started",
@@ -766,7 +1237,9 @@ const runNode = async (options) => {
766
1237
  nodeType,
767
1238
  round: 1
768
1239
  });
769
- const session = (await options.runtime(options.driver ?? plan.driver)).start(plan.spec, {
1240
+ const runtime = await options.runtime(driver);
1241
+ if (options.machineClaude === true) plan.spec.machineClaude = true;
1242
+ const session = runtime.start(plan.spec, {
770
1243
  human: control.human,
771
1244
  ...graph === null ? {} : { feedback: createFeedback({
772
1245
  graph,
@@ -813,7 +1286,9 @@ const runNode = async (options) => {
813
1286
  ...result.error === void 0 ? {} : { error: result.error }
814
1287
  }],
815
1288
  reasons: result.error === void 0 ? [] : [`${id}: ${result.error}`],
816
- worktrees: []
1289
+ worktrees: [],
1290
+ skipped: 0,
1291
+ failuresHandled: 0
817
1292
  }
818
1293
  });
819
1294
  await control.close();
@@ -828,7 +1303,8 @@ const runNode = async (options) => {
828
1303
  * @file What a run needs to reach other runs: its port for `workflow.call`
829
1304
  * nodes (RFC-004), its relay when links are on (RFC-003), and the budget a
830
1305
  * caller has capped it to. A called run being resumed keeps its caller and
831
- * its cap, which the run recorded.
1306
+ * its cap, which the run recorded, and a run's called runs load the machine's
1307
+ * Claude setup when it does (ADR-065).
832
1308
  */
833
1309
  const callPorts = (options) => {
834
1310
  const { graph, store, runId, home, human, runtime, out } = options;
@@ -838,6 +1314,7 @@ const callPorts = (options) => {
838
1314
  const baseRef = options.baseRef ?? options.resume?.settings.baseRef;
839
1315
  const trigger = options.trigger ?? options.resume?.settings.trigger;
840
1316
  const untrusted = options.untrusted ?? options.resume?.settings.untrusted;
1317
+ const machineClaude = options.machineClaude ?? options.resume?.settings.machineClaude ?? false;
841
1318
  const own = graph.workflow.limits?.budgetUsd;
842
1319
  const budgetUsd = cap === void 0 ? own : Math.min(cap, own ?? Number.POSITIVE_INFINITY);
843
1320
  return {
@@ -859,6 +1336,7 @@ const callPorts = (options) => {
859
1336
  home,
860
1337
  adopt: adoptedCalls(store, runId),
861
1338
  newId: () => newRunId(store),
1339
+ machineClaude,
862
1340
  env: maskedEnv(maskedNamesOfRun(graph.workflow, options.place, store.projects())),
863
1341
  resumeSession: (run, node, body, signal) => {
864
1342
  const { blocked, leftUsd } = runLimits(store, options.place, home).now();
@@ -888,7 +1366,8 @@ const callPorts = (options) => {
888
1366
  ...calledBy === void 0 ? {} : { calledBy },
889
1367
  ...baseRef === void 0 ? {} : { baseRef },
890
1368
  ...trigger === void 0 ? {} : { trigger },
891
- ...untrusted === void 0 ? {} : { untrusted }
1369
+ ...untrusted === void 0 ? {} : { untrusted },
1370
+ ...machineClaude ? { machineClaude } : {}
892
1371
  }
893
1372
  };
894
1373
  };
@@ -1009,6 +1488,16 @@ const preflight = async (options, given) => {
1009
1488
  ok: false,
1010
1489
  lines: refused
1011
1490
  };
1491
+ const driver = await runReadyDriver(given.effective, {
1492
+ override: options.driver,
1493
+ recorded: options.resume?.settings.readyDriver,
1494
+ checks: options.driverChecks
1495
+ });
1496
+ if (!driver.ok) return {
1497
+ ok: false,
1498
+ lines: driver.lines
1499
+ };
1500
+ if (driver.line !== void 0) options.err(driver.line);
1012
1501
  const checkout = await prepareCheckout(workflow, {
1013
1502
  ...options,
1014
1503
  cwd: place.root
@@ -1019,7 +1508,8 @@ const preflight = async (options, given) => {
1019
1508
  };
1020
1509
  return {
1021
1510
  ...checkout,
1022
- limits: limits.now
1511
+ limits: limits.now,
1512
+ ...driver.driver === void 0 ? {} : { readyDriver: driver.driver }
1023
1513
  };
1024
1514
  };
1025
1515
  // src/run/workflow.ts
@@ -1059,6 +1549,7 @@ const runWorkflowFile = async (options) => {
1059
1549
  const projects = options.store.projects();
1060
1550
  const ready = await preflight(options, {
1061
1551
  workflow,
1552
+ effective: graph.workflow,
1062
1553
  place,
1063
1554
  projects,
1064
1555
  inputs: inputs.inputs
@@ -1119,6 +1610,7 @@ const runWorkflowFile = async (options) => {
1119
1610
  maskedNames
1120
1611
  };
1121
1612
  if (options.driver !== void 0) executorOptions.driver = options.driver;
1613
+ if (ready.readyDriver !== void 0) executorOptions.readyDriver = ready.readyDriver;
1122
1614
  if (options.tools !== void 0) executorOptions.tools = options.tools;
1123
1615
  const script = process.argv[1];
1124
1616
  if (script !== void 0 && onPath(script)) executorOptions.orkBin = script;
@@ -1135,6 +1627,7 @@ const runWorkflowFile = async (options) => {
1135
1627
  human: control.human,
1136
1628
  emit: onEvent
1137
1629
  });
1630
+ if (ports.settings.machineClaude === true) executorOptions.machineClaude = true;
1138
1631
  clearOutputFiles(runDir, options.err);
1139
1632
  const handle = startRun({
1140
1633
  graph: ports.graph,
@@ -1149,6 +1642,7 @@ const runWorkflowFile = async (options) => {
1149
1642
  settings: {
1150
1643
  inPlace: options.inPlace,
1151
1644
  ...options.driver === void 0 ? {} : { driver: options.driver },
1645
+ ...ready.readyDriver === void 0 ? {} : { readyDriver: ready.readyDriver },
1152
1646
  ...ports.settings
1153
1647
  },
1154
1648
  ...ports.relay === void 0 ? {} : { relay: ports.relay },
@@ -1191,7 +1685,7 @@ const flagValues = (rawArgs, flag) => {
1191
1685
  };
1192
1686
  const isDriver = (value) => CLAUDE_DRIVERS.some((d) => d.id === value);
1193
1687
  /** Loads the drivers only when a run starts, so other commands stay fast. */
1194
- const loadRuntime = async (driver) => (await import("./src-BHnxpJyH.mjs")).createRuntime(driver, sessionEnv(process.env));
1688
+ const loadRuntime = async (driver) => (await import("./src-B9v0lDc6.mjs")).createRuntime(driver, sessionEnv(process.env));
1195
1689
  /**
1196
1690
  * Ctrl-C asks the run to stop, and so does a terminal closing or a polite kill,
1197
1691
  * so that the run ends tidily and leaves no result unscrubbed.
@@ -1247,6 +1741,11 @@ const run = defineCommand({
1247
1741
  description: "Run although instructions or skills have uncommitted changes (worktrees get the committed versions)",
1248
1742
  default: false
1249
1743
  },
1744
+ "machine-claude": {
1745
+ type: "boolean",
1746
+ description: "Sessions load this machine's Claude setup: its MCP servers and Claude's memory (default: clean sessions)",
1747
+ default: false
1748
+ },
1250
1749
  json: {
1251
1750
  type: "boolean",
1252
1751
  description: "Print the events as JSON lines",
@@ -1331,6 +1830,7 @@ const run = defineCommand({
1331
1830
  cwd,
1332
1831
  inputs: flagValues(rawArgs, "--input"),
1333
1832
  inPlace: args["in-place"],
1833
+ ...args["machine-claude"] ? { machineClaude: true } : {},
1334
1834
  json: args.json,
1335
1835
  runtime: loadRuntime,
1336
1836
  store,
@@ -1514,8 +2014,9 @@ const chat = defineCommand({
1514
2014
  /**
1515
2015
  * @file `ork decide`: answer a live run's open decisions from any terminal (it
1516
2016
  * writes to the store, then wakes the run on its socket; specs/messaging.md).
1517
- * With `--id` and `--choose`, `--note` and `--rating` also note how the
1518
- * decision's node went, as the run's feedback (specs/feedback.md).
2017
+ * `--say` gives the answer a note, which goes to the run with it. With `--id`
2018
+ * and `--choose`, `--note` and `--rating` instead note how the decision's
2019
+ * node went, as the run's feedback (specs/feedback.md).
1519
2020
  */
1520
2021
  const failed$2 = (message) => {
1521
2022
  writeErr(message);
@@ -1548,8 +2049,13 @@ const noteProblem = (note, rating) => {
1548
2049
  }, "--");
1549
2050
  return checked.ok ? void 0 : checked.error;
1550
2051
  };
2052
+ /** What the answer said, as stored (masked), after the answer's line. */
2053
+ const saidLine = (store, id) => {
2054
+ const note = store.decision(id)?.note;
2055
+ return note === void 0 ? "" : style("dim", ` · said: ${note}`);
2056
+ };
1551
2057
  /** One line per open decision, for `--list`. */
1552
- const formatDecisions = (decisions) => decisions.length === 0 ? ["No open decisions."] : decisions.map((d) => `${style("bold", d.id)} ${style("cyan", d.node)} ${d.question} ${style("dim", `[${d.options.map((o) => o.id === d.recommended ? `${o.id}*` : o.id).join(", ")}]`)}`);
2058
+ const formatDecisions = (decisions) => decisions.length === 0 ? ["No open decisions."] : decisions.map((d) => `${style("bold", d.id)} ${style("cyan", d.node)}${d.owner === void 0 ? "" : style("dim", ` via ${d.owner}`)} ${d.question} ${style("dim", `[${d.options.map((o) => o.id === d.recommended ? `${o.id}*` : o.id).join(", ")}]`)}`);
1553
2059
  const decide = defineCommand({
1554
2060
  meta: {
1555
2061
  name: "decide",
@@ -1584,6 +2090,10 @@ const decide = defineCommand({
1584
2090
  type: "string",
1585
2091
  description: "…with this option"
1586
2092
  },
2093
+ say: {
2094
+ type: "string",
2095
+ description: "…and say this with it: the answer's note, which goes to the run"
2096
+ },
1587
2097
  note: {
1588
2098
  type: "string",
1589
2099
  description: "…and note how its node went; the note joins the run's feedback"
@@ -1616,12 +2126,12 @@ const decide = defineCommand({
1616
2126
  failed$2(problem);
1617
2127
  return;
1618
2128
  }
1619
- const answered = await answerDecision(store, args.id, args.choose);
2129
+ const answered = await answerDecision(store, args.id, args.choose, void 0, "decide", args.say);
1620
2130
  if (!answered.ok) {
1621
2131
  failed$2(answered.error);
1622
2132
  return;
1623
2133
  }
1624
- write(`${style("green", "✓")} ${args.id}: ${args.choose}`);
2134
+ write(`${style("green", "✓")} ${args.id}: ${args.choose}${saidLine(store, args.id)}`);
1625
2135
  const noted = noteDecision(store, args.id, args.note, args.rating);
1626
2136
  if (noted === void 0) return;
1627
2137
  if (noted.ok) write(`${style("green", "✓")} noted in item ${noted.item}`);
@@ -1632,6 +2142,10 @@ const decide = defineCommand({
1632
2142
  failed$2("--note and --rating go with --id and --choose.");
1633
2143
  return;
1634
2144
  }
2145
+ if (args.say !== void 0) {
2146
+ failed$2("--say goes with --id and --choose; answering here asks for a note after each choice.");
2147
+ return;
2148
+ }
1635
2149
  const filter = {};
1636
2150
  if (args.run !== void 0 && args.run !== "") {
1637
2151
  const found = findRun(store, args.run, process.cwd());
@@ -1659,9 +2173,13 @@ const decide = defineCommand({
1659
2173
  };
1660
2174
  if (d.recommended !== void 0) question.recommended = d.recommended;
1661
2175
  if (d.context !== void 0) question.context = d.context;
2176
+ let note;
2177
+ question.onNote = (given) => {
2178
+ note = given;
2179
+ };
1662
2180
  const choice = await human.ask(question);
1663
- const answered = await answerDecision(store, d.id, choice);
1664
- write(answered.ok ? `${style("green", "✓")} ${d.id}: ${choice}\n` : `${style("yellow", "!")} ${answered.error}\n`);
2181
+ const answered = await answerDecision(store, d.id, choice, void 0, "decide", note);
2182
+ write(answered.ok ? `${style("green", "✓")} ${d.id}: ${choice}${saidLine(store, d.id)}\n` : `${style("yellow", "!")} ${answered.error}\n`);
1665
2183
  }
1666
2184
  } finally {
1667
2185
  human?.close();
@@ -1732,7 +2250,7 @@ const stop = defineCommand({
1732
2250
  const resume = defineCommand({
1733
2251
  meta: {
1734
2252
  name: "resume",
1735
- description: "Continue a stopped or crashed run: finished nodes keep their results"
2253
+ description: "Continue a run that didn't succeed (stopped, failed or crashed): finished nodes keep their results"
1736
2254
  },
1737
2255
  args: {
1738
2256
  run: {
@@ -1821,216 +2339,6 @@ const resume = defineCommand({
1821
2339
  }
1822
2340
  }
1823
2341
  });
1824
- // src/update/notify.ts
1825
- /**
1826
- * @file The new-version notice (ADR-038). At most once a day a command starts a
1827
- * detached copy of `ork` that asks the npm registry for the latest version
1828
- * and records it in `~/.ork/update-check.json`, so no command waits on the
1829
- * network; a later command, when a newer version is known, ends with one line
1830
- * on stderr (once a day). Off in CI, without a terminal, with `--json`, and
1831
- * with `ORK_NO_UPDATE_CHECK=1` or `NO_UPDATE_NOTIFIER=1`.
1832
- */
1833
- /** Set in the background copy of `ork` that only runs the check. */
1834
- const UPDATE_CHILD_ENV = "ORK_UPDATE_CHECK_CHILD";
1835
- /**
1836
- * Why the automatic check is off for this command, or `null` when it is on: the
1837
- * environment's switches (core's, which `ork ui` reads too), then where a
1838
- * notice on stderr would go.
1839
- */
1840
- const updateCheckOff = (env, argv, terminal) => {
1841
- const switched = updateSwitchOff(env);
1842
- if (switched !== null) return switched === "CI" ? "in CI" : "turned off";
1843
- if (isSwitchedOn(env["ORK_DETACHED"]) || !terminal) return "no terminal";
1844
- if (argv.includes("--json")) return "JSON output";
1845
- if (argv[0] === "upgrade") return "ork upgrade checks for itself";
1846
- if (argv[0] === "ui" && argv.includes("--serve")) return "ork ui's server checks for itself";
1847
- return null;
1848
- };
1849
- /** The notice's text. */
1850
- const updateNotice = (latest, current) => `Ork ${latest} is out (you have ${current}). Upgrade with: ork upgrade`;
1851
- /**
1852
- * Starts the background check when one is due. Records the attempt first, so
1853
- * commands started in the next moments do not start one too.
1854
- */
1855
- const startUpdateCheck = (file = updateCachePath(), now = /* @__PURE__ */ new Date()) => {
1856
- try {
1857
- const cache = readUpdateCache(file);
1858
- if (!checkDue(cache, now)) return;
1859
- writeUpdateCache(file, {
1860
- ...cache,
1861
- checkedAt: now.toISOString()
1862
- });
1863
- const child = spawn(process.execPath, [...process.execArgv, process.argv[1] ?? ""], {
1864
- detached: true,
1865
- stdio: "ignore",
1866
- env: {
1867
- ...process.env,
1868
- [UPDATE_CHILD_ENV]: "1"
1869
- }
1870
- });
1871
- child.on("error", () => {});
1872
- child.unref();
1873
- } catch {}
1874
- };
1875
- /** The background copy's whole job: ask the registry, record the answer. */
1876
- const runUpdateChild = async () => {
1877
- const registry = registryOf(process.env);
1878
- await refreshUpdateCache(updateCachePath(), ORK_NAME, /* @__PURE__ */ new Date(), registry === void 0 ? {} : { registry });
1879
- };
1880
- /** Prints the notice when a newer version is known and not told today. */
1881
- const showUpdateNotice = (file = updateCachePath(), now = /* @__PURE__ */ new Date()) => {
1882
- try {
1883
- const cache = readUpdateCache(file);
1884
- const latest = pendingNotice(cache, ORK_VERSION, now);
1885
- if (cache === null || latest === null) return;
1886
- const text = updateNotice(latest, ORK_VERSION);
1887
- process.stderr.write(`\n${styleText("yellow", text, { stream: process.stderr })}\n`);
1888
- writeUpdateCache(file, {
1889
- ...cache,
1890
- notified: {
1891
- version: latest,
1892
- at: now.toISOString()
1893
- }
1894
- });
1895
- } catch {}
1896
- };
1897
- // src/update/upgrade.ts
1898
- /**
1899
- * @file `ork doctor`'s line for Ork itself (ADR-038): the running version
1900
- * against the last update check, read from the record without asking the
1901
- * network. What `ork upgrade` does is core's (`planUpgrade`, `runInstall`),
1902
- * shared with the server behind `ork ui`.
1903
- */
1904
- /** `ork doctor`'s line for Ork itself, from the last check. */
1905
- const versionCheck = (current, cache, checksOff) => {
1906
- const newer = newerVersion(cache, current);
1907
- if (newer !== null) return {
1908
- name: "ork",
1909
- status: "warn",
1910
- detail: `${current}; ${newer} is out`,
1911
- hint: "Run ork upgrade."
1912
- };
1913
- return {
1914
- name: "ork",
1915
- status: "ok",
1916
- detail: `${current} (${cache?.latest === void 0 ? checksOff ? "update check off" : "not checked for updates yet" : `the latest as of ${cache.checkedAt.slice(0, 10)}`})`
1917
- };
1918
- };
1919
- // src/doctor/checks.ts
1920
- /**
1921
- * @file The checks behind `ork doctor`: Ork's own version against the last
1922
- * update check, and each tool's presence and login status, through an
1923
- * injectable `exec` so tests need no real tools. Never prints a token or an
1924
- * account.
1925
- */
1926
- const TIMEOUT_MS = 15e3;
1927
- const exec = (program, args) => new Promise((resolve) => {
1928
- execFile(program, args, {
1929
- timeout: TIMEOUT_MS,
1930
- encoding: "utf8"
1931
- }, (error, stdout) => {
1932
- resolve({
1933
- ok: error === null,
1934
- stdout
1935
- });
1936
- });
1937
- });
1938
- /** Ork's own version, against the last update check (no network). */
1939
- const orkCheck = () => versionCheck(ORK_VERSION, readUpdateCache(updateCachePath()), updateCheckOff(process.env, [], true) !== null);
1940
- const MIN_NODE = [24, 20];
1941
- const checkNode = (version) => {
1942
- const [major = 0, minor = 0] = version.split(".").map(Number);
1943
- const [minMajor, minMinor] = MIN_NODE;
1944
- return major > minMajor || major === minMajor && minor >= minMinor ? {
1945
- name: "node",
1946
- status: "ok",
1947
- detail: `v${version}`
1948
- } : {
1949
- name: "node",
1950
- status: "fail",
1951
- detail: `v${version}`,
1952
- hint: `Ork needs Node ${MIN_NODE.join(".")} or later.`
1953
- };
1954
- };
1955
- const checkGit = async (run) => {
1956
- const version = await run("git", ["--version"]);
1957
- if (!version.ok) return [{
1958
- name: "git",
1959
- status: "fail",
1960
- detail: "not found",
1961
- hint: "Install git; Ork gives each writing node its own worktree."
1962
- }];
1963
- const inside = await run("git", ["rev-parse", "--is-inside-work-tree"]);
1964
- return [{
1965
- name: "git",
1966
- status: "ok",
1967
- detail: firstLine(version.stdout)
1968
- }, inside.ok ? {
1969
- name: "git repo",
1970
- status: "ok",
1971
- detail: "inside a work tree"
1972
- } : {
1973
- name: "git repo",
1974
- status: "warn",
1975
- detail: "not a git repository",
1976
- hint: "Run Ork inside a git repository so nodes can use worktrees."
1977
- }];
1978
- };
1979
- /**
1980
- * Runs every check: Node and git, then every registered Claude driver and
1981
- * integration (ADR-022). Node, git, and at least one driver are required;
1982
- * integrations are optional.
1983
- */
1984
- const runChecks = async (run, env, nodeVersion) => {
1985
- const [git, drivers, tools] = await Promise.all([
1986
- checkGit(run),
1987
- Promise.all(CLAUDE_DRIVERS.map((d) => d.doctor(run, env))),
1988
- Promise.all(INTEGRATIONS.flatMap((i) => i.doctor === void 0 ? [] : [i.doctor(run, env)]))
1989
- ]);
1990
- const results = [
1991
- checkNode(nodeVersion),
1992
- ...git,
1993
- ...drivers,
1994
- ...tools
1995
- ];
1996
- return {
1997
- results,
1998
- ok: drivers.some((d) => d.status === "ok") && !results.some((r) => r.status === "fail")
1999
- };
2000
- };
2001
- /**
2002
- * What it says of triggers (RFC-011): a long-lived process can die quietly, so
2003
- * a live trigger that hasn't looked when it should have is worth saying. `home`
2004
- * is the user's `~` (tests pass a folder).
2005
- */
2006
- const triggerChecks = (home) => {
2007
- const store = openStore(storePath(home));
2008
- try {
2009
- const report = triggersReport(store, store.projects(), home);
2010
- const broken = report.broken.map(({ file, message }) => ({
2011
- name: "triggers",
2012
- status: "warn",
2013
- detail: `${file}: ${message}`,
2014
- hint: "Its triggers are off until it can be read."
2015
- }));
2016
- const live = report.triggers.filter(({ standing }) => standing === "live");
2017
- if (live.length === 0) return broken;
2018
- const late = lateTriggers(report, /* @__PURE__ */ new Date());
2019
- if (late.length === 0) return [...broken, {
2020
- name: "ork watch",
2021
- status: "ok",
2022
- detail: `${String(live.length)} trigger(s) looking`
2023
- }];
2024
- return [...broken, {
2025
- name: "ork watch",
2026
- status: "warn",
2027
- detail: `not looking: ${late.map(({ trigger }) => trigger).join(", ")}`,
2028
- hint: report.pid === void 0 ? "Start it with `ork watch --detach`." : "See `ork watch --status`."
2029
- }];
2030
- } finally {
2031
- store.close();
2032
- }
2033
- };
2034
2342
  // src/project/ork-skills.ts
2035
2343
  /**
2036
2344
  * @file The Claude Code plugin's skills (`plugins/ork/skills/`) as text, by
@@ -2041,6 +2349,7 @@ const triggerChecks = (home) => {
2041
2349
  const ORK_SKILLS = {
2042
2350
  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",
2043
2351
  run: "---\nname: run\ndescription: Use when starting, following or explaining an Ork run - what it's doing, why it failed, what it cost, and what waits on the person - and when the person asks about their Ork runs.\n---\n\n# Running and following Ork workflows\n\nOrk runs workflows on the person's machine with their accounts, so a run spends\ntheir money and can push, merge or deploy. Start one only when they ask, and\nnever answer a decision for them.\n\n## Starting\n\n1. Find it: Ork's `workflows` tool gives each workflow's name and inputs.\n2. Check it first with `check` (`workflow: <name>`). Don't start one with an\n error.\n3. Start it with `start_run`, giving every required input. Claude Code asks the\n person first, unless they've allowed it. It answers with the run's id; the run goes on in the\n background, whatever happens to this conversation.\n\nWithout Ork's tools: `ork run <workflow> --detach --input name=value`.\n\n## Following\n\n- `run` gives a run's status, what it waits on, its cost so far, and each\n node's state. `runs` lists the newest runs.\n- `run_events` gives its newest events; `node` narrows them, and `last` or\n `after` widens them. A `node.finished` event with `ok: false` says why a node\n failed.\n- `explain_context` says what a node's session was given: its agent,\n instructions, skills and files. Use it when a node didn't know something.\n- `usage` says what runs spent, by workflow and model, and how close the limits\n are.\n\nSay what you found plainly: which node, what it said, and what it cost. Quote a\nnode's own words rather than retelling them.\n\n## What only the person does\n\n- **Decisions.** `decisions` lists the questions waiting on them: a gate, a loop\n that ran out of rounds, a permission. Tell them what each asks, the options,\n and the recommended one, and that they answer in `ork ui` or with\n `ork decide` in their own terminal. Don't answer one: never run\n `ork decide --id … --choose …`; with Ork's plugin, a hook stops it.\n- **Stopping, pausing, resuming.** Tell them the command: `ork stop <run>`,\n `ork pause <run>`, `ork continue <run>`, `ork resume <run>`. A node can be\n stopped alone with `--node <id>`.\n\n## Without Ork's tools\n\n| Want | Command |\n| ---------------------- | ------------------------------ |\n| Runs, newest first | `ork ps` |\n| A run's transcript | `ork logs <run>` (`--follow`) |\n| Its nodes and cost | `ork tree <run>` |\n| What runs spent | `ork usage` |\n| What waits on a person | `ork decide --list` |\n| A node's context | `ork context explain <wf> <n>` |\n",
2352
+ setup: "---\nname: setup\ndescription: Use when the person asks you to set up Ork, install it, or get started with it in a repository - checking it's installed, setting up the project, making Ork's tools available to you, and a first workflow.\n---\n\n# Setting up Ork\n\nOrk runs workflows on the person's machine with their own accounts. Setting it\nup changes their machine and their repository, so say what each step does and\nask before it. Work through these in order, and stop where they say no.\n\n## 1. Check\n\n```sh\nork --version\nork doctor\n```\n\n- **No `ork`:** it needs Node 24.20 or later (`node --version`). The install is\n `npm install --global @krapht/ork`; show it and run it only when they say\n yes. The guide:\n https://github.com/mimshins/ork/blob/main/docs/getting-started/installation.md\n- **Read `ork doctor` to them.** It needs git, a git repository with a commit,\n and one Claude driver. Don't run `git init` or a commit without asking.\n- **A driver not ready:** they log in themselves, with `claude` (the CLI\n driver), or by setting `ANTHROPIC_API_KEY` or a cloud provider in their own\n shell (the SDK driver). Never ask for a key, never take one pasted to you, and\n never write one into a file or a command. `gh` and `aws` are optional, and\n their logins are their own too (`gh auth login`, `aws sso login`).\n\n## 2. The project\n\n- **`ork init`** keeps Ork's files in the repository, in `.ork/`, committed with\n the code. It adds `.ork/runs/` to `.gitignore`, links `.claude/` and similar\n folders to `.ork/`, and writes a starter workflow, `hello`.\n- **`ork register`** keeps them beside it, in `~/.ork/projects/<id>/`, and\n writes nothing into the repository. For a repository whose process isn't\n theirs to change, or to try Ork first.\n\nExplain both and ask which. Skip the question when the repository decides it: a\n`.ork/` folder means `ork init` has run (`ork projects` lists what's\nregistered). Neither overwrites a file.\n\n## 3. Make Ork available to you\n\nIf Ork's tools (`workflows`, `check`) already answer you, skip this. Otherwise,\nby where you run, and asking before each change to a config:\n\n- **Claude Code:** Ork's plugin brings the skills and the tools. The person\n types these themselves:\n\n ```\n /plugin marketplace add mimshins/ork\n /plugin install ork@ork\n ```\n\n Or, in a project set up with `ork init`, the skills in the repository for\n everyone who clones it, then the tools:\n\n ```sh\n ork init --skills\n claude mcp add ork -- ork mcp\n ```\n\n New tools start when they restart Claude Code or reconnect `ork` in `/mcp`;\n so does the plugin's, once `ork` is installed.\n\n- **Another MCP client:** add a server named `ork` with command `ork` and args\n `[\"mcp\"]` to its MCP config, where its own docs say. Its tools find the\n project by its id, from `ork projects`.\n- **A shell only:** use the CLI. `ork --help` and each command's `--help` say\n what it takes.\n\n## 4. A first workflow\n\nOffer to write one for a task they name. Follow `/ork:write`, check each draft\n(the `check` tool, or `ork validate <file>`) until it has no error, then save\nit. Don't start it: a run spends their money. Start it only when they say so,\nin the background with its inputs (`start_run`, or\n`ork run <name> --detach --input name=value`), and follow it with `/ork:run`.\n\n## 5. Show them `ork ui`\n\nTell them to run `ork ui` in their own terminal: their runs, graphs and logs in\nthe browser, and where they answer what a run asks.\n\n## Never\n\n- Ask for, store or pass on a password, key or token.\n- Answer a run's decision: they answer in `ork ui` or with `ork decide` in\n their own terminal.\n- Install, edit a config, or start a run without a yes.\n",
2044
2353
  write: "---\nname: write\ndescription: Use when writing or changing an Ork workflow (a `.ork/workflows/*.ork.yaml` file) - its nodes, edges, conditions and loops - and checking each draft until Ork accepts it.\n---\n\n# Writing an Ork workflow\n\nAn Ork workflow is one YAML file in the project, `.ork/workflows/<name>.ork.yaml`.\nIt names the nodes (Claude sessions, programs, a person's decisions) and the\nedges that carry work between them. Ork runs it on the person's machine with\ntheir own accounts.\n\n## How to work\n\n1. **Look first.** Ork's `workflows` tool lists the project's workflows; read\n one that's close. `agents` lists the agents a node can use. Before you split\n the work, read `/ork:principles`: a program where a program will do, a\n session only for judgment.\n2. **Don't guess a field.** `workflow_format` gives the format; ask it for one\n node type (`nodeType: \"human.gate\"`) to see that type's fields.\n3. **Check every draft** with Ork's `check` tool (`text`, or `workflow` for a\n file), and fix what it says until it has no error. Read its warnings: they\n mean something. Without Ork's tools, run `ork validate <file>`.\n4. **Then save the file**, beginning with the schema line below, so editors can\n check it too.\n\n## The shape of a file\n\n```yaml\n# yaml-language-server: $schema=https://raw.githubusercontent.com/mimshins/ork/main/packages/schema/workflow.schema.json\nschemaVersion: 1\nname: fix-and-ship\ndescription: Fix a failing test, check it with the test suite, and ask before opening a pull request.\ninputs:\n issue:\n type: string\n required: true\n description: What fails.\ndefaults:\n model: sonnet\n budgetUsd: 2\nlimits:\n budgetUsd: 10\nnodes:\n fix:\n type: claude.worker\n prompt: \"Fix this, and keep the change small: {{inputs.issue}}\"\n tests:\n type: tool.shell\n command: pnpm test\n allow: [pnpm test]\n ask:\n type: human.gate\n question: \"Open a pull request for {{nodes.fix.output.branch}}?\"\n options: [open, hold]\n recommended: hold\n pr:\n type: tool.gh\n command: pr create --fill --head {{nodes.fix.output.branch}}\n allow: [pr create]\nedges:\n - fix -> tests\n - from: tests\n to: fix\n when: \"!ok\"\n maxRounds: 2\n - from: tests\n to: ask\n when: ok\n - from: ask\n to: pr\n when: \"choice == 'open'\"\n```\n\nNode ids are lowercase: letters, digits, `-` and `_`. Secrets never go in the\nfile: a tool reads one with `env:` and `{{env.NAME}}`.\n\n## Nodes\n\n| Type | Does |\n| --------------------------------- | ------------------------------------------------------------------- |\n| `claude.worker` | A Claude session that does one task and reports |\n| `claude.leader` | A session that plans and delegates to the nodes it owns (`parent:`) |\n| `claude.subagent` | A subagent inside its parent's session; it takes no edges |\n| `tool.shell` | Any program, run without a shell; `allow:` lists command prefixes |\n| `tool.git`, `tool.gh`, `tool.aws` | The person's own `git`, `gh`, `aws`, with an allow list |\n| `mcp.call` | One tool call on an MCP server, with no session |\n| `human.gate` | A question with options and a recommended one |\n| `control.join` | Waits for its inputs: `all` (default), `any`, or `n` of them |\n| `control.router`, `control.split` | Pass their input on; a router takes its first matching edge |\n| `workflow.call` | Starts another workflow as its own run |\n\n- **A Claude node reports** `status` (`pass`, `changes_requested`, `done` or\n `fail`), `summary`, `artifacts`, `branch`, and any field its `output.schema`\n declares, such as `verdict: [approve, escalate]`. For another verdict, declare\n a field: `status` can't take other values.\n- **A tool node reports** `exitCode`, `stdout`, `stderr`, and with\n `output: { format: json, schema: { state: [OPEN, CLOSED] } }` its JSON as\n `json`. Declare the fields you read: a run then checks them, and a typo can't\n pass.\n- **A gate reports** `choice` and `answeredBy`. A `timeout` needs a `default`.\n\n## Edges and conditions\n\n- `a -> b`, `a -> b, c`, `b, c -> j` are plain edges, taken when the source\n succeeded. A failed node takes no plain edge: handle a failure with\n `when: \"!ok\"`, or `status == 'fail'` on a Claude node.\n- `{ from: a, to: b, when: \"...\" }` is taken when the condition holds. A bare\n name reads the source's output: `when: \"verdict == 'approve'\"`. Quote any\n condition that starts with `!`, or YAML reads it as a tag.\n- Conditions have `==`, `!=`, `<`, `>`, `&&`, `||`, `!`, and `any`, `all`,\n `count`, `len`, `contains`. A join's `statuses` suits `any(statuses, 'fail')`.\n- A node runs when any edge into it is taken; only a join waits. A node with no\n way in taken is skipped, and so is what follows it.\n- Templates, `{{inputs.x}}`, `{{nodes.a.output.field}}`, `{{run.id}}`, go in\n `prompt`, `instructions`, `command`, `cwd` and `question`. Each value stays\n one argument of a command, so it can't inject one.\n\n## Loops\n\nEvery cycle needs an edge with `maxRounds`. Give the way back a condition and\nthe way out its opposite, as `!ok` and `ok` above; a plain edge beside a loop\nfires every round. When a loop runs out, Ork asks a person what to do.\n\n## What `check` refuses, and why\n\n- **A value the source can't have** (`when.value`): `status == 'failed'` (it's\n `fail`), a gate's `choice == 'aprove'` for `approve`, a value outside a\n declared list. The message names the values it can have.\n- **A loop no condition ends**, a plain edge beside a loop, a loop of sessions\n with no `limits.budgetUsd` or `limits.timeout`: warnings, each worth fixing.\n- **`no way the walk tried reaches x`**: Ork tried every way through, and none\n got to `x`. A join waiting on a branch a first-match router never takes is\n the usual cause.\n"
2045
2354
  };
2046
2355
  // src/project/init-skills.ts
@@ -2224,6 +2533,7 @@ const doctor = defineCommand({
2224
2533
  orkCheck(),
2225
2534
  ...results,
2226
2535
  ...place.mode === "attached" ? [] : projectChecks(place.root),
2536
+ socketCheck(),
2227
2537
  ...triggerChecks()
2228
2538
  ]) for (const line of formatCheck(result)) write(line);
2229
2539
  if (!ok) {
@@ -2287,7 +2597,8 @@ const upgrade = defineCommand({
2287
2597
  const registry = registryOf(process.env);
2288
2598
  const latest = await fetchLatest(ORK_NAME, {
2289
2599
  ...registry === void 0 ? {} : { registry },
2290
- timeoutMs: 15e3
2600
+ timeoutMs: 15e3,
2601
+ current: ORK_VERSION
2291
2602
  });
2292
2603
  const plan = planUpgrade({
2293
2604
  name: ORK_NAME,
@@ -2663,13 +2974,22 @@ const formatRegistered = (project, created) => {
2663
2974
  /**
2664
2975
  * @file Scaffolding a project: `.ork/workflows/` with a starter workflow and
2665
2976
  * the `.gitignore` entry for run state, never overwriting a file; then the
2666
- * tool folders the project already uses are linked to `.ork/`.
2977
+ * tool folders the project already uses are linked to `.ork/`. The starter
2978
+ * names no driver, as it is committed: a run takes the one ready on the
2979
+ * machine running it, and `ork init` says which that is here.
2667
2980
  */
2668
2981
  /** Run state that never belongs in git. */
2669
2982
  const GITIGNORE_ENTRIES = [".ork/runs/"];
2670
- const STARTER = {
2983
+ const STARTER_NAME = "hello";
2984
+ /** Where `ork init` writes the starter, relative to the project root. */
2985
+ const STARTER_FILE = path.join(WORKFLOW_DIR, `${STARTER_NAME}${WORKFLOW_SUFFIX}`);
2986
+ /**
2987
+ * The starter workflow. It names no driver: it is committed, and one person's
2988
+ * login doesn't belong in a shared file.
2989
+ */
2990
+ const starterWorkflow = () => ({
2671
2991
  schemaVersion: 1,
2672
- name: "hello",
2992
+ name: STARTER_NAME,
2673
2993
  description: "A leader plans the task, a worker does it, and you approve the result.",
2674
2994
  inputs: { task: {
2675
2995
  type: "string",
@@ -2677,7 +2997,6 @@ const STARTER = {
2677
2997
  description: "What to do."
2678
2998
  } },
2679
2999
  defaults: {
2680
- driver: "sdk",
2681
3000
  model: "sonnet",
2682
3001
  isolation: "worktree"
2683
3002
  },
@@ -2702,7 +3021,34 @@ const STARTER = {
2702
3021
  }
2703
3022
  },
2704
3023
  edges: ["lead -> approve"]
3024
+ });
3025
+ /**
3026
+ * What `ork init` says of drivers, in one line: the project's `defaults.driver`
3027
+ * when it names one, else the driver a run would take here, the one ready
3028
+ * (`cli` when both are), or how to get one ready.
3029
+ */
3030
+ const driverLine = (checks, projectDefault) => {
3031
+ if (projectDefault !== void 0) return `Runs use the ${projectDefault} driver, ${PROJECT_FILE}'s default, unless a workflow names another.`;
3032
+ const ready = readyDriver(checks);
3033
+ if (ready.ok) {
3034
+ const why = ready.ready === 1 ? ready.detail : `more than one is ready, and ${ready.driver} comes first`;
3035
+ return `Runs use the driver that's ready when they start: ${ready.driver} here (${why}).`;
3036
+ }
3037
+ return `Runs use the driver that's ready when they start, and none is ready here yet: ${checks.find(({ id }) => id === "cli")?.result.hint ?? "see `ork doctor`."}`;
3038
+ };
3039
+ /**
3040
+ * The line for the project at `root`: `ork doctor`'s driver checks through
3041
+ * `run` and `env`, and the project's own default.
3042
+ */
3043
+ const initDriverLine = async (run, env, root) => {
3044
+ const projectDefault = loadProject(root).project?.defaults?.driver;
3045
+ return driverLine(projectDefault === void 0 ? await driverChecks(run, env) : [], projectDefault);
2705
3046
  };
3047
+ /**
3048
+ * The line for a starter about to be written in `root`, with the real tools and
3049
+ * environment; `null` when the starter is already there.
3050
+ */
3051
+ const initDriverLineHere = (root) => existsSync(path.join(root, STARTER_FILE)) ? Promise.resolve(null) : initDriverLine(exec, process.env, root);
2706
3052
  /** The run-state entries `root`'s `.gitignore` lacks. */
2707
3053
  const unignored = (root) => {
2708
3054
  const gitignore = path.join(root, ".gitignore");
@@ -2729,11 +3075,10 @@ const initProject = (cwd, options = {}) => {
2729
3075
  const created = [];
2730
3076
  const skipped = [];
2731
3077
  mkdirSync(path.join(cwd, WORKFLOW_DIR), { recursive: true });
2732
- const starter = path.join(WORKFLOW_DIR, `${STARTER.name}${WORKFLOW_SUFFIX}`);
2733
- if (existsSync(path.join(cwd, starter))) skipped.push(starter);
3078
+ if (existsSync(path.join(cwd, STARTER_FILE))) skipped.push(STARTER_FILE);
2734
3079
  else {
2735
- writeFileSync(path.join(cwd, starter), serializeWorkflow(STARTER));
2736
- created.push(starter);
3080
+ writeFileSync(path.join(cwd, STARTER_FILE), serializeWorkflow(starterWorkflow()));
3081
+ created.push(STARTER_FILE);
2737
3082
  }
2738
3083
  const missing = ignoreRunState(cwd);
2739
3084
  if (missing.length > 0) created.push(`.gitignore (${missing.join(", ")})`);
@@ -3100,8 +3445,9 @@ const noteRun = (store, cwd, args) => {
3100
3445
  * @file `ork feedback apply` (ADR-059): runs Ork's own `ork:feedback-apply` in
3101
3446
  * the project an item came from, with the item as its untrusted input. Its
3102
3447
  * worker proposes a fix on a branch and its gate asks to keep or discard it;
3103
- * Ork never merges. Kept, the item is `resolved`; discarded, the run's
3104
- * worktree and branch go and the item stays `accepted`.
3448
+ * Ork never merges. Kept, the item is `resolved`, unless its branch holds no
3449
+ * new commits: then there is nothing to keep, and, as when discarded, the
3450
+ * run's worktree and branch go and the item stays `accepted`.
3105
3451
  */
3106
3452
  /** The most of an item's records the worker reads, newest first. */
3107
3453
  const RECORDS = 10;
@@ -3386,8 +3732,9 @@ const split = defineCommand({
3386
3732
  /**
3387
3733
  * @file `ork feedback export` and `import` (specs/feedback.md): feedback
3388
3734
  * records as JSON lines, one record a line, so a team can move feedback
3389
- * between machines without GitHub. A line is checked before it's added, and a
3390
- * record already here (its run and event) is skipped.
3735
+ * between machines without GitHub. A line is checked before it's added (one
3736
+ * from a newer Ork says so), and a record already here (its run and event) is
3737
+ * skipped.
3391
3738
  */
3392
3739
  const Text = z.string();
3393
3740
  const LineSchema = z.strictObject({
@@ -3425,6 +3772,11 @@ const exportLines = (records) => records.map(({ id: _id, ...record }) => JSON.st
3425
3772
  feedbackVersion: 1,
3426
3773
  ...record
3427
3774
  })).map((line) => `${line}\n`).join("");
3775
+ /** Why a line from a newer Ork is left out, or `null` when it isn't one. */
3776
+ const newerVersion = (value) => {
3777
+ const version = value !== null && typeof value === "object" ? Reflect.get(value, "feedbackVersion") : void 0;
3778
+ return typeof version === "number" && version > 1 ? `made by a newer Ork (\`feedbackVersion: ${String(version)}\`; this one reads ${String(1)}). Upgrade with \`ork upgrade\`.` : null;
3779
+ };
3428
3780
  /** The records in a JSONL file's text, and what was wrong with the rest. */
3429
3781
  const parseLines = (text) => {
3430
3782
  const records = [];
@@ -3438,6 +3790,11 @@ const parseLines = (text) => {
3438
3790
  problems.push(`line ${String(index + 1)}: not JSON`);
3439
3791
  continue;
3440
3792
  }
3793
+ const newer = newerVersion(value);
3794
+ if (newer !== null) {
3795
+ problems.push(`line ${String(index + 1)}: ${newer}`);
3796
+ continue;
3797
+ }
3441
3798
  const parsed = LineSchema.safeParse(value);
3442
3799
  if (!parsed.success) {
3443
3800
  const [issue] = parsed.error.issues;
@@ -3626,7 +3983,7 @@ const json$1 = {
3626
3983
  const feedback = defineCommand({
3627
3984
  meta: {
3628
3985
  name: "feedback",
3629
- description: "What got in the way in your runs, grouped: list, show, note, triage, accept, apply, publish, export, import"
3986
+ description: "What got in the way in your runs, grouped: list, show, note, dismiss, restore, accept, apply, triage, split, publish, export, import"
3630
3987
  },
3631
3988
  subCommands: {
3632
3989
  list: defineCommand({
@@ -3973,7 +4330,8 @@ const skillsStep = async (root, options) => {
3973
4330
  };
3974
4331
  // src/commands/setup.ts
3975
4332
  /**
3976
- * @file Setting up: `ork init` scaffolds `.ork/` in a project, records it, and
4333
+ * @file Setting up: `ork init` scaffolds `.ork/` in a project, with a starter
4334
+ * that names no driver (it says which a run takes here), records it, and
3977
4335
  * links the tool folders it uses; `ork link` points tools' agents and skills
3978
4336
  * folders at `.ork/` (never in an attached project); `ork agents` lists,
3979
4337
  * shows, adds and removes the agents nodes can take. `ork doctor` and `ork
@@ -3996,10 +4354,12 @@ const init = defineCommand({
3996
4354
  }
3997
4355
  },
3998
4356
  run: async ({ args }) => {
4357
+ const cwd = process.cwd();
4358
+ const driverLine = await initDriverLineHere(cwd);
3999
4359
  const store = openStore();
4000
4360
  let result;
4001
4361
  try {
4002
- result = initRecorded(store, process.cwd(), { link: args.link });
4362
+ result = initRecorded(store, cwd, { link: args.link });
4003
4363
  } finally {
4004
4364
  store.close();
4005
4365
  }
@@ -4011,8 +4371,9 @@ const init = defineCommand({
4011
4371
  const { created, skipped, links } = result;
4012
4372
  for (const item of created) write(`${style("green", "created")} ${item}`);
4013
4373
  for (const item of skipped) write(`${style("dim", "exists")} ${item}`);
4374
+ if (driverLine !== null && created.includes(STARTER_FILE)) write(driverLine);
4014
4375
  for (const line of formatLinks(links.filter((l) => l.state !== "linked"))) write(line);
4015
- await skillsStep(process.cwd(), {
4376
+ await skillsStep(cwd, {
4016
4377
  skills: args.skills,
4017
4378
  link: args.link
4018
4379
  });
@@ -4024,11 +4385,18 @@ const link = defineCommand({
4024
4385
  name: "link",
4025
4386
  description: "Point tools' agents and skills folders (.claude/agents, …) at .ork/"
4026
4387
  },
4027
- args: { check: {
4028
- type: "boolean",
4029
- description: "Only show each folder's state; change nothing",
4030
- default: false
4031
- } },
4388
+ args: {
4389
+ tools: {
4390
+ type: "positional",
4391
+ description: "The tools to link, e.g. claude opencode (default: the ones the project already uses)",
4392
+ required: false
4393
+ },
4394
+ check: {
4395
+ type: "boolean",
4396
+ description: "Only show each folder's state; change nothing",
4397
+ default: false
4398
+ }
4399
+ },
4032
4400
  run: ({ args }) => {
4033
4401
  const place = projectAt(process.cwd());
4034
4402
  const { root } = place;
@@ -4189,7 +4557,10 @@ const agents = defineCommand({
4189
4557
  }
4190
4558
  });
4191
4559
  // src/main.ts
4192
- /** @file The root `ork` command and its subcommands. */
4560
+ /**
4561
+ * @file The root `ork` command and its subcommands. `ork --help` lists them in
4562
+ * the groups of `help-groups.ts`.
4563
+ */
4193
4564
  const main = defineCommand({
4194
4565
  meta: ORK_META,
4195
4566
  subCommands: {
@@ -4204,25 +4575,27 @@ const main = defineCommand({
4204
4575
  ps,
4205
4576
  logs,
4206
4577
  tree,
4207
- usage: () => import("./usage-CT7Z-yjr.mjs").then((module) => module.usage),
4208
- prune: () => import("./usage-CT7Z-yjr.mjs").then((module) => module.prune),
4578
+ usage: () => import("./usage-EW7DI6Z7.mjs").then((module) => module.usage),
4579
+ prune: () => import("./usage-EW7DI6Z7.mjs").then((module) => module.prune),
4209
4580
  decide,
4210
- relay: () => import("./relay-Do-aNbSD.mjs").then((module) => module.relay),
4211
- pause: () => import("./pause-BPSmg4yz.mjs").then((module) => module.pause),
4212
- continue: () => import("./pause-BPSmg4yz.mjs").then((module) => module.continueRun),
4581
+ relay: () => import("./relay-BipJYszF.mjs").then((module) => module.relay),
4582
+ pause: () => import("./pause-C7lJkSma.mjs").then((module) => module.pause),
4583
+ continue: () => import("./pause-C7lJkSma.mjs").then((module) => module.continueRun),
4213
4584
  stop,
4214
4585
  chat,
4215
- send: () => import("./send-DhGBkowy.mjs").then((module) => module.send),
4586
+ send: () => import("./send-nbkjf3sc.mjs").then((module) => module.send),
4216
4587
  resume,
4217
4588
  feedback,
4218
4589
  context,
4219
- worktrees: () => import("./worktrees-DHl80k96.mjs").then((module) => module.worktrees),
4220
- wt: () => import("./worktrees-DHl80k96.mjs").then((module) => module.worktrees),
4221
- watch: () => import("./watch-0Qr2UfCQ.mjs").then((module) => module.watch),
4222
- mcp: () => import("./mcp-CHVKKg3B.mjs").then((module) => module.mcp),
4223
- ui: () => import("./ui-DhutUMgQ.mjs").then((module) => module.ui),
4590
+ worktrees: () => import("./worktrees-Ahj9rdWK.mjs").then((module) => module.worktrees),
4591
+ wt: () => import("./worktrees-Ahj9rdWK.mjs").then((module) => module.worktrees),
4592
+ watch: () => import("./watch-BAkSGaOp.mjs").then((module) => module.watch),
4593
+ mcp: () => import("./mcp-DM30MFBw.mjs").then((module) => module.mcp),
4594
+ ui: () => import("./ui-B98FSboa.mjs").then((module) => module.ui),
4224
4595
  doctor,
4225
- upgrade
4596
+ upgrade,
4597
+ completion: () => import("./completion-DBGfL-gN.mjs").then((module) => module.completion),
4598
+ __complete: () => import("./completion-DBGfL-gN.mjs").then((module) => module.completeCommand(main))
4226
4599
  }
4227
4600
  });
4228
4601
  // src/bin.ts
@@ -4230,17 +4603,26 @@ const main = defineCommand({
4230
4603
  * @file The `ork` executable: runs the CLI's root command. The published
4231
4604
  * package's `bin` points at its bundle. When the new-version check is on, it
4232
4605
  * starts the daily background check and ends with the notice (ADR-038); the
4233
- * background copy it starts only runs the check.
4606
+ * background copy it starts only runs the check. It first clears git's local
4607
+ * variables (a hook's `GIT_DIR`, `GIT_INDEX_FILE`, …), so nothing Ork starts
4608
+ * acts on the hook's repository (specs/worktrees.md). `--help` leaves out the
4609
+ * flags only Ork passes to itself. `ork __complete`, which tab completion
4610
+ * runs on every Tab, goes first: no update check, and no citty, so a typed
4611
+ * `--help` is a word to complete.
4234
4612
  */
4235
- if (process.env["ORK_UPDATE_CHECK_CHILD"] === "1") await runUpdateChild();
4613
+ clearGitLocalEnv();
4614
+ const argv = process.argv.slice(2);
4615
+ if (argv[0] === "__complete") {
4616
+ const { printCompletions } = await import("./completion-DBGfL-gN.mjs");
4617
+ await printCompletions(main, argv.slice(1));
4618
+ } else if (process.env["ORK_UPDATE_CHECK_CHILD"] === "1") await runUpdateChild();
4236
4619
  else {
4237
- const argv = process.argv.slice(2);
4238
4620
  if (updateCheckOff(process.env, argv, process.stderr.isTTY) === null) {
4239
4621
  startUpdateCheck();
4240
4622
  process.once("exit", () => {
4241
4623
  showUpdateNotice();
4242
4624
  });
4243
4625
  }
4244
- await runMain(main);
4626
+ await runMain(main, { showUsage: showHelp });
4245
4627
  }
4246
4628
  export {};