@krapht/ork 0.4.0 → 1.0.0-alpha.0

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__-DdTVd2p5.mjs} +36 -112
  3. package/dist/bin.mjs +663 -300
  4. package/dist/{clean-Dcr807be.mjs → clean-NnboOuz6.mjs} +1 -1
  5. package/dist/completion-snnYGjgK.mjs +405 -0
  6. package/dist/{context-C_bksb0C.mjs → context-DO3sBy9v.mjs} +11 -88
  7. package/dist/decide-B81wCZQg.mjs +113 -0
  8. package/dist/{find-DptmbhGd.mjs → find-BKW-uGey.mjs} +1 -1
  9. package/dist/{format-J-a21Yvn.mjs → format-CmTlTOjT.mjs} +71 -37
  10. package/dist/help-groups-BwYoAWsh.mjs +354 -0
  11. package/dist/{in-a-run-D1Qc5Mit.mjs → in-a-run-BPXX09Bs.mjs} +1 -1
  12. package/dist/{limits-BDZCmGEu.mjs → limits-JCve2QpI.mjs} +1 -1
  13. package/dist/{list-ZvY2OcLq.mjs → list-C7UEgmtd.mjs} +1 -1
  14. package/dist/{mcp-CHVKKg3B.mjs → mcp-CKPyNaKD.mjs} +7 -7
  15. package/dist/{output-CXSJO00X.mjs → output-B1V6ZJSx.mjs} +67 -17
  16. package/dist/{pause-BPSmg4yz.mjs → pause-B_SL_yDz.mjs} +4 -4
  17. package/dist/{relay-Do-aNbSD.mjs → relay-o-Luee1c.mjs} +2 -2
  18. package/dist/{remove-AwaMMaHf.mjs → remove-DAZ7ZCzt.mjs} +3 -3
  19. package/dist/{send-DhGBkowy.mjs → send-3B64fBNB.mjs} +3 -3
  20. package/dist/{src-CpDKIulI.mjs → src-BJTegEzY.mjs} +23 -13
  21. package/dist/{src-UWkk1sl9.mjs → src-C38BPzit.mjs} +251 -18
  22. package/dist/{src-CGqMati3.mjs → src-DHvWU4tX.mjs} +636 -217
  23. package/dist/{src-BHnxpJyH.mjs → src-Dyz_Gy1q.mjs} +35 -8
  24. package/dist/ui/THIRD_PARTY_LICENSES.txt +2 -2
  25. package/dist/ui/assets/app-C8nJ6NpT.js +67 -0
  26. package/dist/ui/assets/app-CH7YC3in.css +1 -0
  27. package/dist/ui/assets/{index-BiO54L90.css → index-BPIbqRRb.css} +1 -1
  28. package/dist/ui/assets/{index-MQNNgsyT.js → index-D83F2OKZ.js} +2 -2
  29. package/dist/ui/index.html +2 -3
  30. package/dist/{ui-DhutUMgQ.mjs → ui-CY4xxJUu.mjs} +5 -5
  31. package/dist/{usage-CT7Z-yjr.mjs → usage-CJnytoe4.mjs} +6 -6
  32. package/dist/{watch-0Qr2UfCQ.mjs → watch-BJB82ZJ7.mjs} +7 -7
  33. package/dist/workflows-CiM7S3l0.mjs +88 -0
  34. package/dist/{worktrees-DHl80k96.mjs → worktrees-86ySXxR6.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-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";
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-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";
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
  /**
@@ -581,6 +1031,12 @@ const resumeChat = async (options) => {
581
1031
  ...template === void 0 ? {} : { template }
582
1032
  });
583
1033
  if (!plan.ok) return plan.error;
1034
+ let driver = options.driver ?? session.driver ?? plan.driver;
1035
+ if (driver === void 0) {
1036
+ const ready = readyDriver(await (options.driverChecks ?? checksHere)());
1037
+ if (!ready.ok) return ["No Claude driver is ready here.", ...ready.hints].join(" ");
1038
+ driver = ready.driver;
1039
+ }
584
1040
  const record = appendRecorder(store, run.id, runDir);
585
1041
  record({
586
1042
  type: "user.message",
@@ -588,7 +1044,7 @@ const resumeChat = async (options) => {
588
1044
  body: options.body,
589
1045
  delivered: "resumed"
590
1046
  });
591
- const started = (await options.runtime(options.driver ?? session.driver ?? plan.driver)).start({
1047
+ const started = (await options.runtime(driver)).start({
592
1048
  ...plan.spec,
593
1049
  resume: session.sessionId,
594
1050
  prompt: options.body
@@ -705,6 +1161,15 @@ const runNode = async (options) => {
705
1161
  pluginDir: path.join(runDir, "plugins", options.nodeId)
706
1162
  });
707
1163
  if (!plan.ok) return failed([plan.error]);
1164
+ const named = sessionDriver({
1165
+ override: options.driver,
1166
+ named: plan.driver
1167
+ });
1168
+ const ready = named === void 0 ? chooseReadyDriver([options.nodeId], true, await (options.driverChecks ?? checksHere)()) : null;
1169
+ if (ready?.ok === false) return failed(ready.lines);
1170
+ if (ready?.line !== void 0) options.err(ready.line);
1171
+ const driver = named ?? ready?.driver;
1172
+ if (driver === void 0) return failed(["No Claude driver is ready here."]);
708
1173
  const planned = workflow.nodes[options.nodeId];
709
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.`);
710
1175
  const skipped = planned === void 0 ? [] : [...beforeOf(planned, workflow.defaults) === void 0 ? [] : ["before"], ..."verify" in planned && planned.verify !== void 0 ? ["verify"] : []];
@@ -766,7 +1231,7 @@ const runNode = async (options) => {
766
1231
  nodeType,
767
1232
  round: 1
768
1233
  });
769
- const session = (await options.runtime(options.driver ?? plan.driver)).start(plan.spec, {
1234
+ const session = (await options.runtime(driver)).start(plan.spec, {
770
1235
  human: control.human,
771
1236
  ...graph === null ? {} : { feedback: createFeedback({
772
1237
  graph,
@@ -813,7 +1278,9 @@ const runNode = async (options) => {
813
1278
  ...result.error === void 0 ? {} : { error: result.error }
814
1279
  }],
815
1280
  reasons: result.error === void 0 ? [] : [`${id}: ${result.error}`],
816
- worktrees: []
1281
+ worktrees: [],
1282
+ skipped: 0,
1283
+ failuresHandled: 0
817
1284
  }
818
1285
  });
819
1286
  await control.close();
@@ -1009,6 +1476,16 @@ const preflight = async (options, given) => {
1009
1476
  ok: false,
1010
1477
  lines: refused
1011
1478
  };
1479
+ const driver = await runReadyDriver(given.effective, {
1480
+ override: options.driver,
1481
+ recorded: options.resume?.settings.readyDriver,
1482
+ checks: options.driverChecks
1483
+ });
1484
+ if (!driver.ok) return {
1485
+ ok: false,
1486
+ lines: driver.lines
1487
+ };
1488
+ if (driver.line !== void 0) options.err(driver.line);
1012
1489
  const checkout = await prepareCheckout(workflow, {
1013
1490
  ...options,
1014
1491
  cwd: place.root
@@ -1019,7 +1496,8 @@ const preflight = async (options, given) => {
1019
1496
  };
1020
1497
  return {
1021
1498
  ...checkout,
1022
- limits: limits.now
1499
+ limits: limits.now,
1500
+ ...driver.driver === void 0 ? {} : { readyDriver: driver.driver }
1023
1501
  };
1024
1502
  };
1025
1503
  // src/run/workflow.ts
@@ -1059,6 +1537,7 @@ const runWorkflowFile = async (options) => {
1059
1537
  const projects = options.store.projects();
1060
1538
  const ready = await preflight(options, {
1061
1539
  workflow,
1540
+ effective: graph.workflow,
1062
1541
  place,
1063
1542
  projects,
1064
1543
  inputs: inputs.inputs
@@ -1119,6 +1598,7 @@ const runWorkflowFile = async (options) => {
1119
1598
  maskedNames
1120
1599
  };
1121
1600
  if (options.driver !== void 0) executorOptions.driver = options.driver;
1601
+ if (ready.readyDriver !== void 0) executorOptions.readyDriver = ready.readyDriver;
1122
1602
  if (options.tools !== void 0) executorOptions.tools = options.tools;
1123
1603
  const script = process.argv[1];
1124
1604
  if (script !== void 0 && onPath(script)) executorOptions.orkBin = script;
@@ -1149,6 +1629,7 @@ const runWorkflowFile = async (options) => {
1149
1629
  settings: {
1150
1630
  inPlace: options.inPlace,
1151
1631
  ...options.driver === void 0 ? {} : { driver: options.driver },
1632
+ ...ready.readyDriver === void 0 ? {} : { readyDriver: ready.readyDriver },
1152
1633
  ...ports.settings
1153
1634
  },
1154
1635
  ...ports.relay === void 0 ? {} : { relay: ports.relay },
@@ -1191,7 +1672,7 @@ const flagValues = (rawArgs, flag) => {
1191
1672
  };
1192
1673
  const isDriver = (value) => CLAUDE_DRIVERS.some((d) => d.id === value);
1193
1674
  /** 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));
1675
+ const loadRuntime = async (driver) => (await import("./src-Dyz_Gy1q.mjs")).createRuntime(driver, sessionEnv(process.env));
1195
1676
  /**
1196
1677
  * Ctrl-C asks the run to stop, and so does a terminal closing or a polite kill,
1197
1678
  * so that the run ends tidily and leaves no result unscrubbed.
@@ -1514,8 +1995,9 @@ const chat = defineCommand({
1514
1995
  /**
1515
1996
  * @file `ork decide`: answer a live run's open decisions from any terminal (it
1516
1997
  * 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).
1998
+ * `--say` gives the answer a note, which goes to the run with it. With `--id`
1999
+ * and `--choose`, `--note` and `--rating` instead note how the decision's
2000
+ * node went, as the run's feedback (specs/feedback.md).
1519
2001
  */
1520
2002
  const failed$2 = (message) => {
1521
2003
  writeErr(message);
@@ -1548,8 +2030,13 @@ const noteProblem = (note, rating) => {
1548
2030
  }, "--");
1549
2031
  return checked.ok ? void 0 : checked.error;
1550
2032
  };
2033
+ /** What the answer said, as stored (masked), after the answer's line. */
2034
+ const saidLine = (store, id) => {
2035
+ const note = store.decision(id)?.note;
2036
+ return note === void 0 ? "" : style("dim", ` · said: ${note}`);
2037
+ };
1551
2038
  /** 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(", ")}]`)}`);
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(", ")}]`)}`);
1553
2040
  const decide = defineCommand({
1554
2041
  meta: {
1555
2042
  name: "decide",
@@ -1584,6 +2071,10 @@ const decide = defineCommand({
1584
2071
  type: "string",
1585
2072
  description: "…with this option"
1586
2073
  },
2074
+ say: {
2075
+ type: "string",
2076
+ description: "…and say this with it: the answer's note, which goes to the run"
2077
+ },
1587
2078
  note: {
1588
2079
  type: "string",
1589
2080
  description: "…and note how its node went; the note joins the run's feedback"
@@ -1616,12 +2107,12 @@ const decide = defineCommand({
1616
2107
  failed$2(problem);
1617
2108
  return;
1618
2109
  }
1619
- const answered = await answerDecision(store, args.id, args.choose);
2110
+ const answered = await answerDecision(store, args.id, args.choose, void 0, "decide", args.say);
1620
2111
  if (!answered.ok) {
1621
2112
  failed$2(answered.error);
1622
2113
  return;
1623
2114
  }
1624
- write(`${style("green", "✓")} ${args.id}: ${args.choose}`);
2115
+ write(`${style("green", "✓")} ${args.id}: ${args.choose}${saidLine(store, args.id)}`);
1625
2116
  const noted = noteDecision(store, args.id, args.note, args.rating);
1626
2117
  if (noted === void 0) return;
1627
2118
  if (noted.ok) write(`${style("green", "✓")} noted in item ${noted.item}`);
@@ -1632,6 +2123,10 @@ const decide = defineCommand({
1632
2123
  failed$2("--note and --rating go with --id and --choose.");
1633
2124
  return;
1634
2125
  }
2126
+ if (args.say !== void 0) {
2127
+ failed$2("--say goes with --id and --choose; answering here asks for a note after each choice.");
2128
+ return;
2129
+ }
1635
2130
  const filter = {};
1636
2131
  if (args.run !== void 0 && args.run !== "") {
1637
2132
  const found = findRun(store, args.run, process.cwd());
@@ -1659,9 +2154,13 @@ const decide = defineCommand({
1659
2154
  };
1660
2155
  if (d.recommended !== void 0) question.recommended = d.recommended;
1661
2156
  if (d.context !== void 0) question.context = d.context;
2157
+ let note;
2158
+ question.onNote = (given) => {
2159
+ note = given;
2160
+ };
1662
2161
  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`);
2162
+ const answered = await answerDecision(store, d.id, choice, void 0, "decide", note);
2163
+ write(answered.ok ? `${style("green", "✓")} ${d.id}: ${choice}${saidLine(store, d.id)}\n` : `${style("yellow", "!")} ${answered.error}\n`);
1665
2164
  }
1666
2165
  } finally {
1667
2166
  human?.close();
@@ -1732,7 +2231,7 @@ const stop = defineCommand({
1732
2231
  const resume = defineCommand({
1733
2232
  meta: {
1734
2233
  name: "resume",
1735
- description: "Continue a stopped or crashed run: finished nodes keep their results"
2234
+ description: "Continue a run that didn't succeed (stopped, failed or crashed): finished nodes keep their results"
1736
2235
  },
1737
2236
  args: {
1738
2237
  run: {
@@ -1821,216 +2320,6 @@ const resume = defineCommand({
1821
2320
  }
1822
2321
  }
1823
2322
  });
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
2323
  // src/project/ork-skills.ts
2035
2324
  /**
2036
2325
  * @file The Claude Code plugin's skills (`plugins/ork/skills/`) as text, by
@@ -2041,6 +2330,7 @@ const triggerChecks = (home) => {
2041
2330
  const ORK_SKILLS = {
2042
2331
  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
2332
  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",
2044
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"
2045
2335
  };
2046
2336
  // src/project/init-skills.ts
@@ -2224,6 +2514,7 @@ const doctor = defineCommand({
2224
2514
  orkCheck(),
2225
2515
  ...results,
2226
2516
  ...place.mode === "attached" ? [] : projectChecks(place.root),
2517
+ socketCheck(),
2227
2518
  ...triggerChecks()
2228
2519
  ]) for (const line of formatCheck(result)) write(line);
2229
2520
  if (!ok) {
@@ -2287,7 +2578,8 @@ const upgrade = defineCommand({
2287
2578
  const registry = registryOf(process.env);
2288
2579
  const latest = await fetchLatest(ORK_NAME, {
2289
2580
  ...registry === void 0 ? {} : { registry },
2290
- timeoutMs: 15e3
2581
+ timeoutMs: 15e3,
2582
+ current: ORK_VERSION
2291
2583
  });
2292
2584
  const plan = planUpgrade({
2293
2585
  name: ORK_NAME,
@@ -2663,13 +2955,22 @@ const formatRegistered = (project, created) => {
2663
2955
  /**
2664
2956
  * @file Scaffolding a project: `.ork/workflows/` with a starter workflow and
2665
2957
  * the `.gitignore` entry for run state, never overwriting a file; then the
2666
- * tool folders the project already uses are linked to `.ork/`.
2958
+ * tool folders the project already uses are linked to `.ork/`. The starter
2959
+ * names no driver, as it is committed: a run takes the one ready on the
2960
+ * machine running it, and `ork init` says which that is here.
2667
2961
  */
2668
2962
  /** Run state that never belongs in git. */
2669
2963
  const GITIGNORE_ENTRIES = [".ork/runs/"];
2670
- const STARTER = {
2964
+ const STARTER_NAME = "hello";
2965
+ /** Where `ork init` writes the starter, relative to the project root. */
2966
+ const STARTER_FILE = path.join(WORKFLOW_DIR, `${STARTER_NAME}${WORKFLOW_SUFFIX}`);
2967
+ /**
2968
+ * The starter workflow. It names no driver: it is committed, and one person's
2969
+ * login doesn't belong in a shared file.
2970
+ */
2971
+ const starterWorkflow = () => ({
2671
2972
  schemaVersion: 1,
2672
- name: "hello",
2973
+ name: STARTER_NAME,
2673
2974
  description: "A leader plans the task, a worker does it, and you approve the result.",
2674
2975
  inputs: { task: {
2675
2976
  type: "string",
@@ -2677,7 +2978,6 @@ const STARTER = {
2677
2978
  description: "What to do."
2678
2979
  } },
2679
2980
  defaults: {
2680
- driver: "sdk",
2681
2981
  model: "sonnet",
2682
2982
  isolation: "worktree"
2683
2983
  },
@@ -2702,7 +3002,34 @@ const STARTER = {
2702
3002
  }
2703
3003
  },
2704
3004
  edges: ["lead -> approve"]
3005
+ });
3006
+ /**
3007
+ * What `ork init` says of drivers, in one line: the project's `defaults.driver`
3008
+ * when it names one, else the driver a run would take here, the one ready
3009
+ * (`cli` when both are), or how to get one ready.
3010
+ */
3011
+ const driverLine = (checks, projectDefault) => {
3012
+ if (projectDefault !== void 0) return `Runs use the ${projectDefault} driver, ${PROJECT_FILE}'s default, unless a workflow names another.`;
3013
+ const ready = readyDriver(checks);
3014
+ if (ready.ok) {
3015
+ const why = ready.ready === 1 ? ready.detail : `more than one is ready, and ${ready.driver} comes first`;
3016
+ return `Runs use the driver that's ready when they start: ${ready.driver} here (${why}).`;
3017
+ }
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`."}`;
3019
+ };
3020
+ /**
3021
+ * The line for the project at `root`: `ork doctor`'s driver checks through
3022
+ * `run` and `env`, and the project's own default.
3023
+ */
3024
+ const initDriverLine = async (run, env, root) => {
3025
+ const projectDefault = loadProject(root).project?.defaults?.driver;
3026
+ return driverLine(projectDefault === void 0 ? await driverChecks(run, env) : [], projectDefault);
2705
3027
  };
3028
+ /**
3029
+ * The line for a starter about to be written in `root`, with the real tools and
3030
+ * environment; `null` when the starter is already there.
3031
+ */
3032
+ const initDriverLineHere = (root) => existsSync(path.join(root, STARTER_FILE)) ? Promise.resolve(null) : initDriverLine(exec, process.env, root);
2706
3033
  /** The run-state entries `root`'s `.gitignore` lacks. */
2707
3034
  const unignored = (root) => {
2708
3035
  const gitignore = path.join(root, ".gitignore");
@@ -2729,11 +3056,10 @@ const initProject = (cwd, options = {}) => {
2729
3056
  const created = [];
2730
3057
  const skipped = [];
2731
3058
  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);
3059
+ if (existsSync(path.join(cwd, STARTER_FILE))) skipped.push(STARTER_FILE);
2734
3060
  else {
2735
- writeFileSync(path.join(cwd, starter), serializeWorkflow(STARTER));
2736
- created.push(starter);
3061
+ writeFileSync(path.join(cwd, STARTER_FILE), serializeWorkflow(starterWorkflow()));
3062
+ created.push(STARTER_FILE);
2737
3063
  }
2738
3064
  const missing = ignoreRunState(cwd);
2739
3065
  if (missing.length > 0) created.push(`.gitignore (${missing.join(", ")})`);
@@ -3100,8 +3426,9 @@ const noteRun = (store, cwd, args) => {
3100
3426
  * @file `ork feedback apply` (ADR-059): runs Ork's own `ork:feedback-apply` in
3101
3427
  * the project an item came from, with the item as its untrusted input. Its
3102
3428
  * 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`.
3429
+ * Ork never merges. Kept, the item is `resolved`, unless its branch holds no
3430
+ * new commits: then there is nothing to keep, and, as when discarded, the
3431
+ * run's worktree and branch go and the item stays `accepted`.
3105
3432
  */
3106
3433
  /** The most of an item's records the worker reads, newest first. */
3107
3434
  const RECORDS = 10;
@@ -3386,8 +3713,9 @@ const split = defineCommand({
3386
3713
  /**
3387
3714
  * @file `ork feedback export` and `import` (specs/feedback.md): feedback
3388
3715
  * 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.
3716
+ * between machines without GitHub. A line is checked before it's added (one
3717
+ * from a newer Ork says so), and a record already here (its run and event) is
3718
+ * skipped.
3391
3719
  */
3392
3720
  const Text = z.string();
3393
3721
  const LineSchema = z.strictObject({
@@ -3425,6 +3753,11 @@ const exportLines = (records) => records.map(({ id: _id, ...record }) => JSON.st
3425
3753
  feedbackVersion: 1,
3426
3754
  ...record
3427
3755
  })).map((line) => `${line}\n`).join("");
3756
+ /** Why a line from a newer Ork is left out, or `null` when it isn't one. */
3757
+ const newerVersion = (value) => {
3758
+ const version = value !== null && typeof value === "object" ? Reflect.get(value, "feedbackVersion") : void 0;
3759
+ return typeof version === "number" && version > 1 ? `made by a newer Ork (\`feedbackVersion: ${String(version)}\`; this one reads ${String(1)}). Upgrade with \`ork upgrade\`.` : null;
3760
+ };
3428
3761
  /** The records in a JSONL file's text, and what was wrong with the rest. */
3429
3762
  const parseLines = (text) => {
3430
3763
  const records = [];
@@ -3438,6 +3771,11 @@ const parseLines = (text) => {
3438
3771
  problems.push(`line ${String(index + 1)}: not JSON`);
3439
3772
  continue;
3440
3773
  }
3774
+ const newer = newerVersion(value);
3775
+ if (newer !== null) {
3776
+ problems.push(`line ${String(index + 1)}: ${newer}`);
3777
+ continue;
3778
+ }
3441
3779
  const parsed = LineSchema.safeParse(value);
3442
3780
  if (!parsed.success) {
3443
3781
  const [issue] = parsed.error.issues;
@@ -3626,7 +3964,7 @@ const json$1 = {
3626
3964
  const feedback = defineCommand({
3627
3965
  meta: {
3628
3966
  name: "feedback",
3629
- description: "What got in the way in your runs, grouped: list, show, note, triage, accept, apply, publish, export, import"
3967
+ description: "What got in the way in your runs, grouped: list, show, note, dismiss, restore, accept, apply, triage, split, publish, export, import"
3630
3968
  },
3631
3969
  subCommands: {
3632
3970
  list: defineCommand({
@@ -3973,7 +4311,8 @@ const skillsStep = async (root, options) => {
3973
4311
  };
3974
4312
  // src/commands/setup.ts
3975
4313
  /**
3976
- * @file Setting up: `ork init` scaffolds `.ork/` in a project, records it, and
4314
+ * @file Setting up: `ork init` scaffolds `.ork/` in a project, with a starter
4315
+ * that names no driver (it says which a run takes here), records it, and
3977
4316
  * links the tool folders it uses; `ork link` points tools' agents and skills
3978
4317
  * folders at `.ork/` (never in an attached project); `ork agents` lists,
3979
4318
  * shows, adds and removes the agents nodes can take. `ork doctor` and `ork
@@ -3996,10 +4335,12 @@ const init = defineCommand({
3996
4335
  }
3997
4336
  },
3998
4337
  run: async ({ args }) => {
4338
+ const cwd = process.cwd();
4339
+ const driverLine = await initDriverLineHere(cwd);
3999
4340
  const store = openStore();
4000
4341
  let result;
4001
4342
  try {
4002
- result = initRecorded(store, process.cwd(), { link: args.link });
4343
+ result = initRecorded(store, cwd, { link: args.link });
4003
4344
  } finally {
4004
4345
  store.close();
4005
4346
  }
@@ -4011,8 +4352,9 @@ const init = defineCommand({
4011
4352
  const { created, skipped, links } = result;
4012
4353
  for (const item of created) write(`${style("green", "created")} ${item}`);
4013
4354
  for (const item of skipped) write(`${style("dim", "exists")} ${item}`);
4355
+ if (driverLine !== null && created.includes(STARTER_FILE)) write(driverLine);
4014
4356
  for (const line of formatLinks(links.filter((l) => l.state !== "linked"))) write(line);
4015
- await skillsStep(process.cwd(), {
4357
+ await skillsStep(cwd, {
4016
4358
  skills: args.skills,
4017
4359
  link: args.link
4018
4360
  });
@@ -4024,11 +4366,18 @@ const link = defineCommand({
4024
4366
  name: "link",
4025
4367
  description: "Point tools' agents and skills folders (.claude/agents, …) at .ork/"
4026
4368
  },
4027
- args: { check: {
4028
- type: "boolean",
4029
- description: "Only show each folder's state; change nothing",
4030
- default: false
4031
- } },
4369
+ args: {
4370
+ tools: {
4371
+ type: "positional",
4372
+ description: "The tools to link, e.g. claude opencode (default: the ones the project already uses)",
4373
+ required: false
4374
+ },
4375
+ check: {
4376
+ type: "boolean",
4377
+ description: "Only show each folder's state; change nothing",
4378
+ default: false
4379
+ }
4380
+ },
4032
4381
  run: ({ args }) => {
4033
4382
  const place = projectAt(process.cwd());
4034
4383
  const { root } = place;
@@ -4189,7 +4538,10 @@ const agents = defineCommand({
4189
4538
  }
4190
4539
  });
4191
4540
  // src/main.ts
4192
- /** @file The root `ork` command and its subcommands. */
4541
+ /**
4542
+ * @file The root `ork` command and its subcommands. `ork --help` lists them in
4543
+ * the groups of `help-groups.ts`.
4544
+ */
4193
4545
  const main = defineCommand({
4194
4546
  meta: ORK_META,
4195
4547
  subCommands: {
@@ -4204,25 +4556,27 @@ const main = defineCommand({
4204
4556
  ps,
4205
4557
  logs,
4206
4558
  tree,
4207
- usage: () => import("./usage-CT7Z-yjr.mjs").then((module) => module.usage),
4208
- prune: () => import("./usage-CT7Z-yjr.mjs").then((module) => module.prune),
4559
+ usage: () => import("./usage-CJnytoe4.mjs").then((module) => module.usage),
4560
+ prune: () => import("./usage-CJnytoe4.mjs").then((module) => module.prune),
4209
4561
  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),
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),
4213
4565
  stop,
4214
4566
  chat,
4215
- send: () => import("./send-DhGBkowy.mjs").then((module) => module.send),
4567
+ send: () => import("./send-3B64fBNB.mjs").then((module) => module.send),
4216
4568
  resume,
4217
4569
  feedback,
4218
4570
  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),
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),
4224
4576
  doctor,
4225
- upgrade
4577
+ upgrade,
4578
+ completion: () => import("./completion-snnYGjgK.mjs").then((module) => module.completion),
4579
+ __complete: () => import("./completion-snnYGjgK.mjs").then((module) => module.completeCommand(main))
4226
4580
  }
4227
4581
  });
4228
4582
  // src/bin.ts
@@ -4230,17 +4584,26 @@ const main = defineCommand({
4230
4584
  * @file The `ork` executable: runs the CLI's root command. The published
4231
4585
  * package's `bin` points at its bundle. When the new-version check is on, it
4232
4586
  * starts the daily background check and ends with the notice (ADR-038); the
4233
- * background copy it starts only runs the check.
4587
+ * background copy it starts only runs the check. It first clears git's local
4588
+ * variables (a hook's `GIT_DIR`, `GIT_INDEX_FILE`, …), so nothing Ork starts
4589
+ * acts on the hook's repository (specs/worktrees.md). `--help` leaves out the
4590
+ * flags only Ork passes to itself. `ork __complete`, which tab completion
4591
+ * runs on every Tab, goes first: no update check, and no citty, so a typed
4592
+ * `--help` is a word to complete.
4234
4593
  */
4235
- if (process.env["ORK_UPDATE_CHECK_CHILD"] === "1") await runUpdateChild();
4594
+ clearGitLocalEnv();
4595
+ const argv = process.argv.slice(2);
4596
+ if (argv[0] === "__complete") {
4597
+ const { printCompletions } = await import("./completion-snnYGjgK.mjs");
4598
+ await printCompletions(main, argv.slice(1));
4599
+ } else if (process.env["ORK_UPDATE_CHECK_CHILD"] === "1") await runUpdateChild();
4236
4600
  else {
4237
- const argv = process.argv.slice(2);
4238
4601
  if (updateCheckOff(process.env, argv, process.stderr.isTTY) === null) {
4239
4602
  startUpdateCheck();
4240
4603
  process.once("exit", () => {
4241
4604
  showUpdateNotice();
4242
4605
  });
4243
4606
  }
4244
- await runMain(main);
4607
+ await runMain(main, { showUsage: showHelp });
4245
4608
  }
4246
4609
  export {};