@krapht/ork 1.0.0-alpha.20 → 1.0.0-alpha.21

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 (52) hide show
  1. package/dist/{__build_info__-D6mXC4cD.mjs → __build_info__-0wqQ-rtf.mjs} +3 -51
  2. package/dist/{attach-C1OnxyMh.mjs → attach-C0-YyNoY.mjs} +6 -6
  3. package/dist/{attach-CEGbRg1D.mjs → attach-CAKNqnUK.mjs} +64 -35
  4. package/dist/bin.mjs +1426 -72
  5. package/dist/{brief-DH66xmj8.mjs → brief-C2hew8cS.mjs} +7 -6
  6. package/dist/{builtin-files-Cf-NZDqM.mjs → builtin-files-YYAnbZGD.mjs} +1 -1
  7. package/dist/{chips-BqcT58qr.mjs → chips-CP_pgKlj.mjs} +7 -2
  8. package/dist/{completion-DS0gCZRm.mjs → completion-K4rAVsrf.mjs} +6 -6
  9. package/dist/{context-CGPh12HV.mjs → context-B-fjPJky.mjs} +4 -4
  10. package/dist/context-DMK0U1w0.mjs +160 -0
  11. package/dist/{detached-Dakw5RZj.mjs → detached-q0AYG0Bs.mjs} +6 -5
  12. package/dist/{find-BTikxc54.mjs → find-Ct5CNl1J.mjs} +1 -1
  13. package/dist/{flags-C8yCq0j5.mjs → flags-C90N4J1i.mjs} +3 -3
  14. package/dist/{follow-ups-CtlhHbwe.mjs → follow-ups-D3HPyNYM.mjs} +14 -9
  15. package/dist/{format-BB4E-T6x.mjs → format-CiPhQqbX.mjs} +24 -13
  16. package/dist/{help-groups-BpRgBSTh.mjs → help-groups-BQvcKQ9H.mjs} +8 -3
  17. package/dist/{in-a-run-CbnKjO7t.mjs → in-a-run-D_wg4iJi.mjs} +2 -2
  18. package/dist/{install-CWcpX5g4.mjs → install-Hji6LIei.mjs} +50 -2
  19. package/dist/{limits-VQqsBoQk.mjs → limits-BD6RZRCS.mjs} +1 -1
  20. package/dist/{mcp-wGfkMCVF.mjs → mcp-CpbDmBLG.mjs} +6 -6
  21. package/dist/{once-BHxSdZz_.mjs → once-J8Qn5fTG.mjs} +4 -4
  22. package/dist/{output-B1kwo0YU.mjs → output-DXZbnFt_.mjs} +61 -42
  23. package/dist/{pause-C_Wxh-KM.mjs → pause-Cl4ZYA35.mjs} +4 -4
  24. package/dist/{print-DNYzq4Rr.mjs → print-DzugGZQB.mjs} +2 -2
  25. package/dist/{project-records-XY3ku95x.mjs → project-records-CtkmlsHN.mjs} +3 -3
  26. package/dist/{relay-BA6tsnpc.mjs → relay-CiP77vga.mjs} +2 -2
  27. package/dist/{run-Bvvg-fiF.mjs → run-CtuNY3mE.mjs} +1 -1
  28. package/dist/{send-nrKtak5F.mjs → send-DRYGks8K.mjs} +3 -4
  29. package/dist/short-flags-DkJLWVqh.mjs +1 -0
  30. package/dist/short-flags-Qf21as0E.mjs +83 -0
  31. package/dist/{src-D4JSymhK.mjs → src-BMFQKHz4.mjs} +32 -14
  32. package/dist/{src-CmEvHdju.mjs → src-BrFfDKux.mjs} +8 -7
  33. package/dist/{src-8cOPOgrT.mjs → src-BuF9Lbow.mjs} +77 -18
  34. package/dist/{src-I_RyKu6_.mjs → src-k0A_QpST.mjs} +5 -3
  35. package/dist/store-rTgRu8xy.mjs +25 -0
  36. package/dist/ui/assets/app-BYxpKJRh.css +1 -0
  37. package/dist/ui/assets/{app-BowhjEAe.js → app-SEFEJLkF.js} +8 -8
  38. package/dist/ui/assets/{index-CrMa2oHB.js → index-Cb6Voq_W.js} +2 -2
  39. package/dist/ui/index.html +1 -1
  40. package/dist/{ui-DY3Dnpho.mjs → ui-C2f37TSb.mjs} +9 -6
  41. package/dist/{usage-1VwSg_Qt.mjs → usage-BuvGcQSk.mjs} +6 -7
  42. package/dist/validate-ObP5lt29.mjs +1 -0
  43. package/dist/{validate-BxayjjkV.mjs → validate-_HjACtk9.mjs} +6 -6
  44. package/dist/{verdict-CMvPCpWC.mjs → verdict-B592j1W3.mjs} +2 -2
  45. package/dist/{watch-CT0EtLPZ.mjs → watch-DV3ixCYj.mjs} +7 -7
  46. package/dist/{workflows-DHHi-APg.mjs → workflows-QLjBhGNS.mjs} +3 -3
  47. package/dist/{worktrees-CZvbHzbN.mjs → worktrees-D3ubG_Qw.mjs} +6 -6
  48. package/dist/yolo-BSH1E_kE.mjs +33 -0
  49. package/package.json +1 -1
  50. package/dist/run-DBUAq-Uu.mjs +0 -1452
  51. package/dist/store-CDpk8Wtz.mjs +0 -16
  52. package/dist/ui/assets/app--FijEXiJ.css +0 -1
@@ -1,1452 +0,0 @@
1
- import { B as appendRecorder, Dn as endLinksOf, Dt as startRun, Et as walkSummary, Fi as AGENT_DRIVERS, Ii as DRIVER_IDS, J as parseInputPairs, Jn as machineRulesOf, K as createReplay, Kn as gitRulesOf, Mi as readyDriver, Pn as liveStatus, R as openStore, Sn as deliverFollowUp, Sr as renderTemplate, Ti as retiredDriverHint, V as createRecorder, Wr as placeFiles, Xr as inside, Y as resolveInputs, Zn as findFollowUp, ct as createCalls, er as createSignals, ft as DETACHED_ENV, gn as createFollowUps, gr as createFeedback, h as resolveBase, ht as NOT_STARTED, kt as userMessageText, li as beforeOf, ln as sessionDriver, lt as adoptedCalls, mr as runGh, n as resumedOptions, nn as inspectProject, q as replayLoopAnswers, qn as loadProject, qr as runDirOf, rn as git, rt as createRelay, t as createWorkspace, tr as endOf, vn as checkFollowUpsDue, wr as startControl, xi as isClaudeNode, xr as parseTemplate, xt as planNodeSession, z as storePath, zn as templateOf } from "./src-8cOPOgrT.mjs";
2
- import { _ as clearOutputFiles, a as builtinExecutors, b as runScrubber, d as maskedEnv, f as maskedNamesOfRun, i as onPath, l as clearStateFiles, m as sessionEnv, n as missingProgramMessage, r as missingPrograms, t as envProblems, v as namedMcpEntries, y as resultOf } from "./src-I_RyKu6_.mjs";
3
- import { n as newRunId } from "./find-BTikxc54.mjs";
4
- import { h as nodeSession, n as ORK_VERSION } from "./__build_info__-D6mXC4cD.mjs";
5
- import { c as write, l as writeErr, s as style } from "./output-B1kwo0YU.mjs";
6
- import { d as formatEvent, f as formatRunEvent, m as createTerminalHuman, p as callsShown, t as attachRun } from "./attach-CEGbRg1D.mjs";
7
- import { a as refusedBuiltin, s as workflowArg, t as WORKFLOW_DIR } from "./workflows-DHHi-APg.mjs";
8
- import { a as formatReport, c as checksHere, d as runReadyDriver, f as unfitLines, i as walkLoaded, l as chooseReadyDriver, o as warningLines, s as budgetWarningLines, t as loadWorkflow, u as overrideRefusal } from "./validate-BxayjjkV.mjs";
9
- import { a as createView, o as styler } from "./verdict-CMvPCpWC.mjs";
10
- import { i as runLimits, r as limitsRefusal, t as chatNotice } from "./limits-VQqsBoQk.mjs";
11
- import path from "node:path";
12
- import { existsSync, linkSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
13
- import { createHash, randomUUID } from "node:crypto";
14
- import { homedir } from "node:os";
15
- import { setTimeout as setTimeout$1 } from "node:timers/promises";
16
- import { defineCommand } from "citty";
17
- // ../core/src/follow-ups/take.ts
18
- const put = (rows, f, run, at) => {
19
- const next = {
20
- ...f,
21
- takenBy: run,
22
- updatedAt: at
23
- };
24
- rows.putFollowUp(next);
25
- return next;
26
- };
27
- /**
28
- * Why `f` is held by the run that took it, as the end of `taken by run <id>,
29
- * <note>`; `undefined` when it is free. A run the store does not know counts as
30
- * live while the mark is recent, so a run that fails its preflight frees its
31
- * follow-ups when the window ends.
32
- */
33
- const heldNote = (f, isLive, now) => {
34
- if (f.takenBy === void 0) return void 0;
35
- const live = isLive(f.takenBy);
36
- if (live !== void 0) return live ? "still running" : void 0;
37
- return now - Date.parse(f.updatedAt) < 12e4 ? "not started yet; free in under 2 minutes" : void 0;
38
- };
39
- /** Whether another run holds `f`: it is not free to take again. */
40
- const heldByLive = (f, run, isLive, now) => f.takenBy !== run && heldNote(f, isLive, now) !== void 0;
41
- /**
42
- * The project's due `agent` follow-ups no live run has taken (`ids`), and those
43
- * a live run holds (`held`), so a second `--follow-ups due` doesn't take them
44
- * twice.
45
- */
46
- const availableDue = (rows, project, isLive, run = "", now = () => (/* @__PURE__ */ new Date()).toISOString()) => {
47
- const at = Date.parse(now());
48
- const due = rows.followUps(project).filter((f) => f.for === "agent" && f.status === "due");
49
- return {
50
- ids: due.filter((f) => !heldByLive(f, run, isLive, at)).map((f) => f.id),
51
- held: due.filter((f) => heldByLive(f, run, isLive, at))
52
- };
53
- };
54
- /**
55
- * `ork run start --follow-ups <ids>`: the open `agent` follow-ups among `ids`
56
- * become `run`'s brief; why each other id wasn't taken. One a live run took is
57
- * refused; one a finished, failed or crashed run took can be taken again.
58
- */
59
- const takeFollowUps = (rows, taking, now = () => (/* @__PURE__ */ new Date()).toISOString()) => {
60
- const taken = [];
61
- const refused = [];
62
- const at = Date.parse(now());
63
- for (const id of taking.ids) {
64
- const f = rows.followUp(taking.project, id);
65
- if (f === void 0) refused.push(`${id}: there is no such follow-up`);
66
- else if (f.for !== "agent") refused.push(`${id}: it is for ${f.for}`);
67
- else if (f.status === "done" || f.status === "dropped") refused.push(`${id}: it is ${f.status}`);
68
- else {
69
- const note = f.takenBy === taking.run ? void 0 : heldNote(f, taking.isLive, at);
70
- if (note === void 0) taken.push(put(rows, f, taking.run, now()));
71
- else refused.push(`${id}: taken by run ${f.takenBy}, ${note}`);
72
- }
73
- }
74
- return {
75
- taken,
76
- refused
77
- };
78
- };
79
- /** `ORK_HEAVY_SLOTS` as a count; anything but a positive integer is the default. */
80
- const heavySlots = (env = process.env) => {
81
- const count = Number(env["ORK_HEAVY_SLOTS"]);
82
- return Number.isInteger(count) && count > 0 ? count : 2;
83
- };
84
- const codeOf = (error) => error instanceof Error && "code" in error ? error.code : void 0;
85
- const alive = (pid) => {
86
- if (pid <= 0) return false;
87
- try {
88
- process.kill(pid, 0);
89
- return true;
90
- } catch (error) {
91
- return codeOf(error) === "EPERM";
92
- }
93
- };
94
- const read = (file) => {
95
- try {
96
- const value = JSON.parse(readFileSync(file, "utf8"));
97
- if (typeof value !== "object" || value === null || !("id" in value && "run" in value && "node" in value && "pid" in value)) return;
98
- const { id, run, node, pid } = value;
99
- return typeof id === "string" && typeof run === "string" && typeof node === "string" && typeof pid === "number" ? {
100
- id,
101
- run,
102
- node,
103
- pid
104
- } : void 0;
105
- } catch {
106
- return;
107
- }
108
- };
109
- const listed = (dir) => {
110
- try {
111
- return readdirSync(dir).filter((name) => name.endsWith(".json")).toSorted();
112
- } catch {
113
- return [];
114
- }
115
- };
116
- /** Written whole (a rename), so no reader sees half a record. */
117
- const writeWhole = (file, record) => {
118
- const draft = `${file}.${record.id}.tmp`;
119
- writeFileSync(draft, JSON.stringify(record), { mode: 384 });
120
- renameSync(draft, file);
121
- };
122
- /** Deletes a record if it is still `id`'s. */
123
- const dropIf = (file, id) => {
124
- if (read(file)?.id === id) rmSync(file, { force: true });
125
- };
126
- let tickets = 0;
127
- /** A ticket's name: sorted by when it was queued. */
128
- const ticketName = (id) => `${String(Date.now()).padStart(15, "0")}-${String(process.pid).padStart(8, "0")}-${String(tickets += 1).padStart(6, "0")}-${id}.json`;
129
- /** The ledger in `dir`, with `slots` slots. */
130
- const fileSlotLedger = (options) => {
131
- const { dir, slots } = options;
132
- const queue = path.join(dir, "queue");
133
- const slotFile = (index) => path.join(dir, `slot-${String(index)}.json`);
134
- const records = () => Array.from({ length: slots }, (_, index) => {
135
- const file = slotFile(index);
136
- return {
137
- file,
138
- held: read(file)
139
- };
140
- });
141
- const holders = () => records().flatMap(({ held }) => held === void 0 ? [] : [held]);
142
- const sweep = () => {
143
- const gone = [];
144
- for (const file of [...records().map((record) => record.file), ...listed(queue).map((name) => path.join(queue, name))]) {
145
- const held = read(file);
146
- if (held === void 0 || alive(held.pid)) continue;
147
- dropIf(file, held.id);
148
- gone.push(held);
149
- }
150
- return gone;
151
- };
152
- /** Links `draft` into a free slot; true when it got one. */
153
- const tryTake = (draft) => {
154
- for (const { file, held } of records()) {
155
- if (held !== void 0) continue;
156
- try {
157
- linkSync(draft, file);
158
- return true;
159
- } catch (error) {
160
- if (codeOf(error) !== "EEXIST") throw error;
161
- }
162
- }
163
- return false;
164
- };
165
- const take = async (holder, { signal, onWait }) => {
166
- mkdirSync(queue, {
167
- recursive: true,
168
- mode: 448
169
- });
170
- const record = {
171
- id: randomUUID(),
172
- ...holder
173
- };
174
- const ticket = path.join(queue, ticketName(record.id));
175
- writeWhole(ticket, record);
176
- let told = false;
177
- try {
178
- for (;;) {
179
- if (signal.aborted) return null;
180
- sweep();
181
- const free = records().filter(({ held }) => held === void 0).length;
182
- if (listed(queue).indexOf(path.basename(ticket)) < free && tryTake(ticket)) {
183
- const release = () => {
184
- for (const { file, held } of records()) if (held?.id === record.id) dropIf(file, record.id);
185
- };
186
- return release;
187
- }
188
- if (!told) {
189
- told = true;
190
- onWait?.(holders());
191
- }
192
- try {
193
- await setTimeout$1(options.pollMs ?? 500, void 0, { signal });
194
- } catch {
195
- return null;
196
- }
197
- }
198
- } finally {
199
- rmSync(ticket, { force: true });
200
- }
201
- };
202
- return {
203
- take,
204
- holders,
205
- sweep
206
- };
207
- };
208
- /** The machine's ledger: `~/.ork/slots`, `ORK_HEAVY_SLOTS` slots. */
209
- const machineSlotLedger = (options = {}) => fileSlotLedger({
210
- dir: path.join(options.home ?? homedir(), ".ork", "slots"),
211
- slots: heavySlots(options.env)
212
- });
213
- // ../core/src/machine/lock-ledger.ts
214
- /**
215
- * @file The machine-wide ledger of named locks (RFC-016): one run at a time
216
- * holds a lock, such as the branch it integrates into. Each lock is a slot
217
- * ledger with one slot (ADR-080's mechanism: hard-link records, tickets first
218
- * come, first served, a dead holder's record swept), in its own folder under
219
- * `~/.ork/locks`, named by a hash of its key. A `branch:<name>` lock is keyed
220
- * by the origin's URL (without its user part) and the branch, so clones of
221
- * one remote share it; any other name by the project's root and the name.
222
- */
223
- const BRANCH = "branch:";
224
- /**
225
- * A remote's URL as a key: no scheme, no user part (where a token may sit), no
226
- * `.git`; `git@host:path` and `https://host/path` read the same.
227
- */
228
- const remoteKey = (url) => {
229
- const trimmed = url.trim().replace(/\/+$/u, "").replace(/\.git$/u, "");
230
- const scheme = /^[a-z][\d+.a-z-]*:\/\//iu.exec(trimmed);
231
- if (scheme !== null) return trimmed.slice(scheme[0].length).replace(/^[^/@]*@/u, "");
232
- const scp = /^(?:[^/@]+@)?([^/:]+):(?!\/)(.*)$/u.exec(trimmed);
233
- return scp === null ? trimmed : `${scp[1] ?? ""}/${scp[2] ?? ""}`;
234
- };
235
- /** The origin's URL as a key, or `undefined` when the repo has no origin. */
236
- const originOf = async (repo) => {
237
- const result = await git(repo, [
238
- "remote",
239
- "get-url",
240
- "origin"
241
- ]);
242
- const url = result.stdout.trim();
243
- return result.code === 0 && url !== "" ? remoteKey(url) : void 0;
244
- };
245
- /**
246
- * A lock's key: `branch:<origin>#<branch>` for a branch lock (the repo's own
247
- * folder when it has no origin), else `<project>#<name>`.
248
- */
249
- const lockKey = async (name, place) => name.startsWith(BRANCH) ? `${BRANCH}${await originOf(place.repo) ?? path.resolve(place.repo)}#${name.slice(7)}` : `${path.resolve(place.project)}#${name}`;
250
- /**
251
- * Whether a lock's key falls back to the repo's own path: a `branch:` lock in a
252
- * repo with no origin, which no other clone shares.
253
- */
254
- const lockIsLocal = async (name, place) => name.startsWith(BRANCH) && await originOf(place.repo) === void 0;
255
- /** A key's folder name: a hash, so no URL or path is written as a name. */
256
- const folderOf = (key) => createHash("sha256").update(key).digest("hex").slice(0, 32);
257
- /** The locks in `dir`, for a run in `repo` of `project`. */
258
- const fileLockLedgers = (options) => ({ of: async (name) => fileSlotLedger({
259
- dir: path.join(options.dir, folderOf(await lockKey(name, options))),
260
- slots: 1,
261
- ...options.pollMs === void 0 ? {} : { pollMs: options.pollMs }
262
- }) });
263
- /** Where the machine's locks are: `~/.ork/locks`. */
264
- const machineLocksDir = (home = homedir()) => path.join(home, ".ork", "locks");
265
- /** The machine's locks (in `home`'s), for a run in `repo` of `project`. */
266
- const machineLockLedgers = (repo, project, home) => fileLockLedgers({
267
- repo,
268
- project,
269
- dir: machineLocksDir(home)
270
- });
271
- /** Drops every lock's records and tickets whose process is gone; returns them. */
272
- const sweepLocks = (dir) => {
273
- let folders;
274
- try {
275
- folders = readdirSync(dir);
276
- } catch {
277
- return [];
278
- }
279
- return folders.flatMap((folder) => fileSlotLedger({
280
- dir: path.join(dir, folder),
281
- slots: 1
282
- }).sweep());
283
- };
284
- // src/runs/chat.ts
285
- /**
286
- * @file `ork chat`'s two paths. A node of a live run gets the message through
287
- * the store and the run's socket, and the run's bus pushes it into the node's
288
- * session (specs/messaging.md#chat; `tellLiveRun` and `nodeSession` are in
289
- * core). A node of a finished run gets it by resuming its last Claude session
290
- * where it ran, as the run's sessions were set up (ADR-065), and the exchange
291
- * is added to the run's record.
292
- */
293
- /**
294
- * The declared node a spawned worker or an item of a fanned node is an instance
295
- * of.
296
- */
297
- const instanceOf = (nodes, id) => id in nodes ? void 0 : templateOf(id, new Set(Object.keys(nodes)));
298
- /** The run's sessions loaded the machine's Claude setup, so this one does. */
299
- const machineClaudeOf = (events) => events.some(({ event }) => event.type === "run.started" && event.settings?.machineClaude === true) ? { machineClaude: true } : {};
300
- /**
301
- * Resumes a finished node's last session with the user's message, prints the
302
- * session, and adds it to the run's record. Returns why it could not.
303
- */
304
- const resumeChat = async (options) => {
305
- const { store, run, node } = options;
306
- const events = store.events(run.id, 0);
307
- const { session, outputs } = nodeSession(events, node);
308
- if (!session.known) return `${node} is not a node of run ${run.id}.`;
309
- if (!session.chattable) return `${node} is not a leader or worker; only they have sessions to talk to.`;
310
- if (session.sessionId === void 0) return `${node} has no Claude session to resume in run ${run.id}.`;
311
- const cwd = session.cwd ?? session.checkout?.path;
312
- if (cwd === void 0 || !existsSync(cwd)) return `${node}'s session ran in ${cwd ?? "a folder Ork did not record"}, which is gone (a merged worktree is removed), so it cannot be resumed.`;
313
- const { workflow, place, workflowDir } = loadWorkflow(run.file, path.dirname(run.file), void 0, store.projects());
314
- if (workflow === null) return `The workflow ${run.file} is gone or no longer valid.`;
315
- const notice = chatNotice(store, place, options.home);
316
- if (notice !== null) options.out(notice);
317
- const runDir = runDirOf(place, run.id);
318
- const template = instanceOf(workflow.nodes, node);
319
- const plan = planNodeSession(workflow, node, {
320
- cwd,
321
- inputs: {},
322
- outputs,
323
- inPlace: session.checkout === void 0,
324
- workflowDir,
325
- projectRoot: run.project,
326
- orkDir: place.dir,
327
- pluginDir: path.join(runDir, "plugins", node),
328
- ...options.budgetLeftUsd === void 0 ? {} : { budgetLeftUsd: options.budgetLeftUsd },
329
- ...session.checkout === void 0 ? {} : { checkout: session.checkout },
330
- ...template === void 0 ? {} : { template }
331
- });
332
- if (!plan.ok) return plan.error;
333
- const declared = workflow.nodes[template ?? node];
334
- const itemModel = declared !== void 0 && isClaudeNode(declared) && declared.model?.includes("{{") === true ? session.model : void 0;
335
- const servers = namedMcpEntries(run.project, plan.mcp);
336
- if (!servers.ok) return servers.error;
337
- let driver = options.driver ?? session.driver ?? plan.driver;
338
- if (driver === void 0) {
339
- const ready = readyDriver(await (options.driverChecks ?? checksHere)());
340
- if (!ready.ok) return ["No agent driver is ready here.", ...ready.hints].join(" ");
341
- driver = ready.driver;
342
- }
343
- const record = appendRecorder(store, run.id, runDir);
344
- record({
345
- type: "user.message",
346
- node,
347
- body: options.body,
348
- delivered: "resumed"
349
- });
350
- const started = (await options.runtime(driver, placeFiles(place).runs)).start({
351
- ...plan.spec,
352
- ...itemModel === void 0 ? {} : { model: itemModel },
353
- ...servers.value,
354
- resume: session.sessionId,
355
- prompt: options.body,
356
- ...machineClaudeOf(events)
357
- }, { human: options.human });
358
- if (options.signal?.aborted === true) started.interrupt();
359
- options.signal?.addEventListener("abort", () => {
360
- started.interrupt();
361
- }, { once: true });
362
- const tools = /* @__PURE__ */ new Map();
363
- let failure = null;
364
- for await (const event of started.events) {
365
- record({
366
- type: "node.event",
367
- id: node,
368
- event
369
- });
370
- for (const line of formatEvent(node, event, tools)) options.out(line);
371
- if (event.type === "session.finished" && event.outcome.subtype === "error_driver") failure = event.outcome.errors.join("; ");
372
- }
373
- return failure;
374
- };
375
- // src/run/node-notes.ts
376
- /**
377
- * @file What a run of one node (`ork run --node`) leaves out, said before its
378
- * session starts: the messages, steps and `forEach` it does not run. A node
379
- * with `forEach` (RFC-013) runs once with no item when it has `items`, and is
380
- * refused when it has `from`, as the node that reports its list is not run.
381
- * And, after it, its run summary: how the node ended
382
- * (specs/runtime/session-ends.md).
383
- */
384
- /** A warning line, as the run prints it. */
385
- const noteLines = (lines) => lines.map((line) => `${style(["yellow", "bold"], "warning")} ${line}`);
386
- /** Why a node can't run alone, or what a run of it leaves out. */
387
- const oneNodeNotes = (workflow, id) => {
388
- const node = workflow.nodes[id];
389
- if (node === void 0) return { warnings: [] };
390
- const each = "forEach" in node ? node.forEach : void 0;
391
- if (each !== void 0 && "from" in each) return {
392
- error: `\`${id}\` runs once for each item of ${each.from}, a list another node reports, and a run of one node does not run that node; run the whole workflow, or give it \`items\` of its own`,
393
- warnings: []
394
- };
395
- const steps = [...beforeOf(node, workflow.defaults) === void 0 ? [] : ["before"], ..."verify" in node && node.verify !== void 0 ? ["verify"] : []];
396
- return { warnings: [
397
- ..."notify" in node && node.notify ? [`${id}'s notify: does nothing here: a run of one node has no message bus.`] : [],
398
- ...each === void 0 ? [] : [`${id}'s forEach: does not fan out in a run of one node: it runs once, with no item.`],
399
- ...steps.map((step) => `${id}'s ${step}: does not run in a run of one node.`)
400
- ] };
401
- };
402
- /** The `node.finished` fields that say how it ended: its error and its end. */
403
- const endedFields = (result) => ({
404
- ...result.error === void 0 ? {} : { error: result.error },
405
- end: endOf(result)
406
- });
407
- /** The one-node run's result, its reason the node's message. */
408
- const oneNodeResult = (runId, id, result, ok) => {
409
- const costUsd = result.usage?.costUsd ?? 0;
410
- return {
411
- runId,
412
- status: ok ? "succeeded" : "failed",
413
- costUsd,
414
- nodes: [{
415
- id,
416
- runs: 1,
417
- ok: result.ok,
418
- output: result.output,
419
- costUsd,
420
- ...endedFields(result)
421
- }],
422
- reasons: result.error === void 0 ? [] : [`${id}: ${result.error}`],
423
- worktrees: [],
424
- skipped: 0,
425
- failuresHandled: 0
426
- };
427
- };
428
- // src/follow-ups/context.ts
429
- /**
430
- * @file What `ork follow-ups` and a run share: the project's `followUps` rule,
431
- * the sink a run hands its sessions' `follow_up` tool, the due check that
432
- * runs before every call (a `gh` that fails only warns), and the follow-ups
433
- * `ork run --follow-ups` takes as a run's brief.
434
- */
435
- /** The project's `followUps` rule, when it sets one. */
436
- const followUpsRule = (place) => loadProject(place.root, placeFiles(place).rules).project?.followUps;
437
- /** What a run hands its sessions: each `follow_up` call, kept as the rule says. */
438
- const followUpSink = (options) => (input, at) => {
439
- const rule = followUpsRule(options.place);
440
- return deliverFollowUp(input, {
441
- store: options.store,
442
- project: options.place.root,
443
- worktree: at.cwd,
444
- ...rule === void 0 ? {} : { rule },
445
- ...options.home === void 0 ? {} : { home: options.home },
446
- ...options.gh === void 0 ? {} : { gh: options.gh }
447
- });
448
- };
449
- /** Gh's own words when it has no login. */
450
- const SIGNED_OUT = /not logged in|gh auth login/iu;
451
- /**
452
- * A gh error's first line, without a trailing period, so the notice is one
453
- * line.
454
- */
455
- const oneLine = (error) => (error.trim().split("\n")[0] ?? "").trim().replace(/\.$/u, "");
456
- /**
457
- * Moves the project's follow-ups whose time has come to due, as every `ork
458
- * follow-ups` call does first. `gh` failing only warns, through `warn`: one
459
- * line per call, however many refs failed.
460
- */
461
- const refreshDue = async (store, project, warn, gh = runGh) => {
462
- try {
463
- const checked = await checkFollowUpsDue({
464
- rows: store,
465
- project,
466
- gh
467
- });
468
- const [first] = checked.errors;
469
- if (first !== void 0) warn(checked.errors.some((error) => SIGNED_OUT.test(error)) ? "Can't check PRs: `gh` isn't signed in. After-merge follow-ups stay waiting; run `gh auth login` yourself." : `Can't check PRs: ${oneLine(first)}. After-merge follow-ups stay waiting.`);
470
- return checked.due;
471
- } catch (error) {
472
- warn(`couldn't check what is due: ${error instanceof Error ? error.message : String(error)}`);
473
- return [];
474
- }
475
- };
476
- /** The text a run starts from when it takes these follow-ups as its brief. */
477
- const briefOf = (followUps) => ["Do these follow-ups left by earlier changes:", ...followUps.flatMap((f) => [
478
- "",
479
- `## ${f.title} (${f.id})`,
480
- ...f.body === "" ? [] : [f.body],
481
- ...f.where.length === 0 ? [] : [`Files: ${f.where.join(", ")}`]
482
- ])].join("\n");
483
- /**
484
- * Whether a run is going, as `ork ps` decides: running with its process alive.
485
- * `undefined` for a run the store does not know yet: core holds its mark for a
486
- * short while, as the run may be about to begin.
487
- */
488
- const runIsLive = (store) => (runId) => {
489
- const [run] = store.lookup(runId);
490
- return run === void 0 ? void 0 : liveStatus(run) === "running";
491
- };
492
- const nothing = () => {};
493
- /**
494
- * The follow-ups `ids` (or prefixes) name, when every one can be taken as a
495
- * brief; nothing is written, so a run that fails to start leaves them alone.
496
- */
497
- const checkTakeable = (store, project, ids) => {
498
- const full = [];
499
- const lines = [];
500
- for (const id of ids) {
501
- const found = findFollowUp(store, project, id);
502
- if (found.ok) full.push(found.followUp.id);
503
- else lines.push(found.error);
504
- }
505
- const { taken, refused } = takeFollowUps({
506
- followUp: (p, id) => store.followUp(p, id),
507
- followUps: (p) => store.followUps(p),
508
- putFollowUp: nothing
509
- }, {
510
- project,
511
- run: "",
512
- ids: full,
513
- isLive: runIsLive(store)
514
- });
515
- const all = [...lines, ...refused];
516
- return all.length > 0 ? {
517
- ok: false,
518
- lines: all.map((line) => `--follow-ups: ${line}`)
519
- } : {
520
- ok: true,
521
- followUps: taken
522
- };
523
- };
524
- // src/run/origin.ts
525
- /**
526
- * @file Where a run's feedback comes from, beyond what its events say: Ork's
527
- * version (bound from the CLI's `package.json`) and the workflow file's
528
- * content hash; and the feedback and `follow_up` ports of the one node `ork
529
- * run --node` runs.
530
- */
531
- /** Ork's version and the workflow's hash, for the run's feedback records. */
532
- /** The Ork version a run records in `run.started` and `run.resumed`. */
533
- const ORK_RUN_VERSION = ORK_VERSION;
534
- const feedbackOrigin = (workflowFile) => {
535
- const origin = { version: ORK_VERSION };
536
- try {
537
- origin.workflowHash = createHash("sha256").update(readFileSync(workflowFile)).digest("hex").slice(0, 12);
538
- } catch {}
539
- return origin;
540
- };
541
- /**
542
- * Node `id`'s feedback port (with a graph) and `follow_up` port, kept as the
543
- * project's rule says; both mask the values of what its run masks.
544
- */
545
- const nodeReports = (options, { workflow, graph, record }) => {
546
- const { place, id } = options;
547
- const scrub = runScrubber(maskedNamesOfRun(workflow, place, options.store.projects()), process.env);
548
- const sink = followUpSink(options);
549
- return {
550
- ...graph === null ? {} : { feedback: createFeedback({
551
- graph,
552
- onEvent: record,
553
- scrub
554
- }).port(id) },
555
- followUps: createFollowUps({
556
- runId: options.runId,
557
- sink,
558
- scrub
559
- }).port(id, options.cwd)
560
- };
561
- };
562
- // src/run/node.ts
563
- /**
564
- * @file `ork run --node`: checks a workflow and its inputs, plans one Claude
565
- * node's session, streams it to the terminal, and records it as a run of one
566
- * node.
567
- */
568
- const succeeded = (outcome) => outcome?.report !== null && outcome?.report !== void 0 && outcome.report.status !== "fail";
569
- /**
570
- * Runs one Claude node of a workflow on its own: checks the workflow and the
571
- * inputs, plans the session, and streams it to the terminal.
572
- */
573
- const runNode = async (options) => {
574
- const failed = (lines) => {
575
- for (const line of lines) options.err(line);
576
- return {
577
- exitCode: 1,
578
- outcome: null
579
- };
580
- };
581
- const { report, workflow, graph, place, workflowDir } = loadWorkflow(options.file, options.cwd, options.home, options.store.projects());
582
- if (workflow === null) return failed(formatReport(report));
583
- for (const line of warningLines(report)) options.err(line);
584
- const pairs = parseInputPairs(options.inputs);
585
- if (!pairs.ok) return failed([`--input: ${pairs.error}`]);
586
- const inputs = resolveInputs(workflow, pairs.raw);
587
- if (!inputs.ok) return failed(inputs.errors);
588
- const notes = oneNodeNotes(workflow, options.nodeId);
589
- if (notes.error !== void 0) return failed([notes.error]);
590
- const limits = runLimits(options.store, place, options.home);
591
- const refused = limitsRefusal(limits, workflow);
592
- if (refused.length > 0) return failed(refused);
593
- const left = limits.now().leftUsd;
594
- const projectRoot = place.root;
595
- const runId = options.runId ?? newRunId(options.store, "node-");
596
- const runDir = runDirOf(place, runId);
597
- const cwd = inside(projectRoot, options.cwd) ? options.cwd : projectRoot;
598
- const plan = planNodeSession(workflow, options.nodeId, {
599
- ...left === void 0 ? {} : { budgetLeftUsd: left },
600
- cwd,
601
- inputs: inputs.inputs,
602
- inPlace: options.inPlace,
603
- workflowDir,
604
- projectRoot,
605
- orkDir: place.dir,
606
- pluginDir: path.join(runDir, "plugins", options.nodeId)
607
- });
608
- if (!plan.ok) return failed([plan.error]);
609
- const servers = namedMcpEntries(projectRoot, plan.mcp);
610
- if (!servers.ok) return failed([servers.error]);
611
- const named = sessionDriver({
612
- override: options.driver,
613
- named: plan.driver,
614
- pinned: plan.pinned
615
- });
616
- if (options.driver !== void 0 && named === options.driver) {
617
- const refusal = await overrideRefusal(options.driver, options.driverChecks);
618
- if (refusal.length > 0) return failed(refusal);
619
- }
620
- const ready = named === void 0 ? chooseReadyDriver([options.nodeId], true, await (options.driverChecks ?? checksHere)()) : null;
621
- if (ready?.ok === false) return failed(ready.lines);
622
- if (ready?.line !== void 0) options.err(ready.line);
623
- const driver = named ?? ready?.driver;
624
- if (driver === void 0) return failed(["No agent driver is ready here."]);
625
- const only = {
626
- override: driver,
627
- only: options.nodeId
628
- };
629
- const unfit = unfitLines(workflow, options.drivers, only);
630
- if (unfit.length > 0) return failed(unfit);
631
- const warnings = [
632
- ...budgetWarningLines(workflow, options.drivers, only),
633
- ...notes.warnings,
634
- ...plan.warnings
635
- ];
636
- for (const line of noteLines(warnings)) options.err(line);
637
- const id = options.nodeId;
638
- const recorder = createRecorder(options.store, {
639
- id: runId,
640
- workflow: workflow.name,
641
- file: path.resolve(options.cwd, options.file),
642
- project: projectRoot
643
- }, runDir);
644
- const started = Promise.withResolvers();
645
- const control = await startControl({
646
- store: options.store,
647
- runId,
648
- emit: (event) => {
649
- record(event);
650
- },
651
- onStop: () => {
652
- started.promise.then((session) => session.interrupt());
653
- },
654
- onMessage: (node, body) => {
655
- if (node !== id) return;
656
- (async () => {
657
- (await started.promise).send(userMessageText(body));
658
- record({
659
- type: "user.message",
660
- node,
661
- body,
662
- delivered: "now"
663
- });
664
- })();
665
- },
666
- ...options.human === void 0 ? {} : { local: options.human },
667
- ...options.home === void 0 ? {} : { home: options.home }
668
- });
669
- const signals = createSignals(feedbackOrigin(path.resolve(options.cwd, options.file)));
670
- const view = createView();
671
- const record = (raw) => {
672
- for (const event of signals(raw)) {
673
- control.publish(recorder(event));
674
- if (event.type === "feedback.recorded" && !options.json) for (const line of formatRunEvent(event, id.length, /* @__PURE__ */ new Map(), false, options.verbose, view)) options.out(line);
675
- }
676
- };
677
- const nodeType = workflow.nodes[id]?.type ?? "agent.worker";
678
- record({
679
- type: "run.started",
680
- runId,
681
- workflow: workflow.name,
682
- entries: [id],
683
- nodes: [{
684
- id,
685
- type: nodeType
686
- }],
687
- ...options.machineClaude === true ? { settings: { machineClaude: true } } : {}
688
- });
689
- record({
690
- type: "node.started",
691
- id,
692
- nodeType,
693
- round: 1
694
- });
695
- const runtime = await options.runtime(driver, placeFiles(place).runs);
696
- if (options.machineClaude === true) plan.spec.machineClaude = true;
697
- const session = runtime.start({
698
- ...plan.spec,
699
- ...servers.value
700
- }, {
701
- human: control.human,
702
- ...nodeReports({
703
- ...options,
704
- place,
705
- runId,
706
- id,
707
- cwd
708
- }, {
709
- workflow,
710
- graph,
711
- record
712
- })
713
- });
714
- started.resolve(session);
715
- options.onSession?.(session);
716
- const tools = /* @__PURE__ */ new Map();
717
- let outcome = null;
718
- for await (const event of session.events) {
719
- record({
720
- type: "node.event",
721
- id,
722
- event
723
- });
724
- if (event.type === "session.finished") outcome = event.outcome;
725
- if (options.json) options.out(JSON.stringify(event));
726
- else for (const line of formatEvent(options.nodeId, event, tools, options.verbose, view)) options.out(line);
727
- }
728
- const result = resultOf(outcome);
729
- record({
730
- type: "node.finished",
731
- id,
732
- round: 1,
733
- ok: result.ok,
734
- output: result.output,
735
- ...result.usage === void 0 ? {} : { usage: result.usage },
736
- ...endedFields(result)
737
- });
738
- record({
739
- type: "run.finished",
740
- result: oneNodeResult(runId, id, result, succeeded(outcome))
741
- });
742
- await control.close();
743
- return {
744
- exitCode: succeeded(outcome) ? 0 : 1,
745
- outcome
746
- };
747
- };
748
- /** Every value of a repeatable flag: `--input a=1 --input=b=2`. */
749
- // src/run/show-calls.ts
750
- /**
751
- * @file A foreground caller's terminal shows the runs it calls (ADR-081): each
752
- * `call.started` attaches to the called run under its call node, at any
753
- * depth, and asks its decisions when a person is at the terminal. Once the
754
- * caller has ended, following stops for a called run that still runs; one the
755
- * caller stopped gets a moment to end first.
756
- */
757
- /** How long a called run the caller stopped is followed while it stops. */
758
- const STOP_GRACE_MS = 5e3;
759
- const running = (store, id) => {
760
- const [record] = store.lookup(id);
761
- return record !== void 0 && liveStatus(record) === "running";
762
- };
763
- const showCalls = (options) => {
764
- const { store, showCalls: show } = options;
765
- const shown = [];
766
- return {
767
- /** Attaches to the run a `call.started` of the caller names. */
768
- onEvent: (event) => {
769
- if (event.type !== "call.started" || show === void 0 || options.json) return;
770
- const stop = new AbortController();
771
- const done = attachRun(store, event.run, options.out, {
772
- basePath: [event.node],
773
- signal: stop.signal,
774
- ...show.ask === void 0 ? { hintNoTerminal: true } : { human: show.ask },
775
- ...options.home === void 0 ? {} : { home: options.home }
776
- }).catch((error) => {
777
- options.err(`run ${event.run} could not be shown here: ${error instanceof Error ? error.message : String(error)}`);
778
- });
779
- shown.push({
780
- run: event.run,
781
- stop,
782
- done
783
- });
784
- },
785
- /**
786
- * The caller has ended `status`: stops following the called runs still
787
- * running, at once, or after a moment when the caller stopped them, and
788
- * names those that still run.
789
- */
790
- end: async (status) => {
791
- const stopped = status === "interrupted" || status === "timed_out";
792
- const timers = shown.filter(({ run }) => running(store, run)).map(({ stop }) => setTimeout(() => {
793
- stop.abort();
794
- }, stopped ? STOP_GRACE_MS : 0));
795
- await Promise.all(shown.map(({ done }) => done));
796
- for (const timer of timers) clearTimeout(timer);
797
- const say = styler(createView().term);
798
- for (const { run } of shown.filter(({ run: id }) => running(store, id))) options.out(say("dim", `↳ run ${run} still running · ork attach ${run}`));
799
- }
800
- };
801
- };
802
- // src/run/calls.ts
803
- /**
804
- * @file What a run needs to reach other runs: its port for `workflow.call`
805
- * nodes (RFC-004), its relay when links are on (RFC-003), and the budget a
806
- * caller has capped it to. A called run being resumed keeps its caller and
807
- * its cap, which the run recorded, and a run's called runs load the machine's
808
- * Claude setup when it does (ADR-065).
809
- */
810
- const callPorts = (options) => {
811
- const { graph, store, runId, home, human, runtime, out } = options;
812
- const linksOff = graph.workflow.rules?.links === "off";
813
- const cap = options.budgetCap ?? options.resume?.settings.budgetCap;
814
- const calledBy = options.calledBy ?? options.resume?.settings.calledBy;
815
- const baseRef = options.baseRef ?? options.resume?.settings.baseRef;
816
- const trigger = options.trigger ?? options.resume?.settings.trigger;
817
- const untrusted = options.untrusted ?? options.resume?.settings.untrusted;
818
- const machineClaude = options.machineClaude ?? options.resume?.settings.machineClaude ?? false;
819
- const shown = showCalls(options);
820
- const own = graph.workflow.limits?.budgetUsd;
821
- const budgetUsd = cap === void 0 ? own : Math.min(cap, own ?? Number.POSITIVE_INFINITY);
822
- return {
823
- graph: {
824
- ...graph,
825
- workflow: {
826
- ...graph.workflow,
827
- limits: {
828
- ...graph.workflow.limits,
829
- ...budgetUsd === void 0 ? {} : { budgetUsd }
830
- }
831
- }
832
- },
833
- calls: createCalls({
834
- store,
835
- runId,
836
- place: options.place,
837
- emit: (event) => {
838
- options.emit(event);
839
- shown.onEvent(event);
840
- },
841
- home,
842
- adopt: adoptedCalls(store, runId),
843
- newId: () => newRunId(store),
844
- machineClaude,
845
- detach: options.detach,
846
- env: maskedEnv(maskedNamesOfRun(graph.workflow, options.place, store.projects())),
847
- resumeSession: (run, node, body, signal) => {
848
- const { blocked, leftUsd } = runLimits(store, options.place, home).now();
849
- return blocked === void 0 ? resumeChat({
850
- store,
851
- run,
852
- node,
853
- body,
854
- runtime,
855
- human,
856
- out,
857
- signal,
858
- ...home === void 0 ? {} : { home },
859
- ...leftUsd === void 0 ? {} : { budgetLeftUsd: leftUsd }
860
- }) : Promise.resolve(`${NOT_STARTED}${blocked}.`);
861
- }
862
- }),
863
- shown,
864
- followUps: followUpSink({
865
- store,
866
- place: options.place,
867
- home,
868
- gh: options.gh
869
- }),
870
- ...linksOff ? {} : { relay: createRelay({
871
- store,
872
- runId,
873
- home,
874
- human
875
- }) },
876
- settings: {
877
- ...linksOff ? { links: "off" } : {},
878
- ...cap === void 0 ? {} : { budgetCap: cap },
879
- ...calledBy === void 0 ? {} : { calledBy },
880
- ...baseRef === void 0 ? {} : { baseRef },
881
- ...trigger === void 0 ? {} : { trigger },
882
- ...untrusted === void 0 ? {} : { untrusted },
883
- ...machineClaude ? { machineClaude } : {}
884
- }
885
- };
886
- };
887
- // src/run/live-handlers.ts
888
- /**
889
- * Takes what is on its way before the run ends: with no socket, a program's
890
- * message may not have been seen yet. Each command waits on `started`, so
891
- * waiting on it too lets them all land first.
892
- */
893
- const takeLastCommands = (control, started) => async () => {
894
- control.check();
895
- await started;
896
- };
897
- /** `ork pause`, `ork continue` and `ork stop --node`, on the run as it is. */
898
- const liveControl = (live) => (command, node) => {
899
- const { handle } = live;
900
- if (handle === void 0) return false;
901
- if (command === "pause") return handle.pause(node);
902
- if (command === "continue") return handle.proceed(node);
903
- return node !== void 0 && handle.stopNode(node);
904
- };
905
- const liveHandlers = (started) => {
906
- const when = (act) => {
907
- started.then(act);
908
- };
909
- return {
910
- onStop: () => {
911
- when((run) => {
912
- run.interrupt();
913
- });
914
- },
915
- onMessage: (node, body) => {
916
- when((run) => {
917
- run.tell(node, body);
918
- });
919
- },
920
- onRelay: (node, from, body) => {
921
- when((run) => {
922
- run.receive(node, from, body);
923
- });
924
- },
925
- onScript: (node, from, body) => {
926
- when((run) => {
927
- run.receive(node, from, body, "script");
928
- });
929
- },
930
- onNotice: (node, text) => {
931
- when((run) => {
932
- run.notice(node, text);
933
- });
934
- }
935
- };
936
- };
937
- // src/run/checkout.ts
938
- /**
939
- * @file Before a run with worktree nodes: the project must be a git repository
940
- * with a commit, its instructions, skills and agents must be committed
941
- * (worktrees get only committed files, unless `--allow-dirty`), and the
942
- * project's `git.base` must name a commit, which root worktrees start from.
943
- */
944
- /** Why a run refuses to start with uncommitted agent files. */
945
- const dirtyAgentMessage = (files) => [
946
- "These instructions, skills or agents have uncommitted changes, and the worktrees only get committed files:",
947
- ...files.slice(0, 20).map((f) => ` ${f}`),
948
- ...files.length > 20 ? [` … and ${String(files.length - 20)} more`] : [],
949
- "Commit or stash them, or pass --allow-dirty to run with the committed versions."
950
- ];
951
- /** Claude nodes that get their own worktree. */
952
- const worktreeNodes = (workflow) => Object.entries(workflow.nodes).filter(([, node]) => isClaudeNode(node) && node.type !== "agent.subagent" && (node.isolation ?? workflow.defaults?.isolation ?? "worktree") === "worktree").map(([id]) => id);
953
- /**
954
- * The project's folder for a message. Outside git, the place's root is the
955
- * folder of the workflow file, which is `<project>/.ork/workflows` for a
956
- * project's own workflow.
957
- */
958
- const projectFolder = (root) => {
959
- const tail = path.sep + WORKFLOW_DIR;
960
- return root.endsWith(tail) ? root.slice(0, -tail.length) : root;
961
- };
962
- /** Checks the project for a run's worktrees, and resolves `git.base`. */
963
- const prepareCheckout = async (workflow, options, inputs) => {
964
- const isolated = worktreeNodes(workflow);
965
- if (isolated.length === 0 || options.inPlace) return {
966
- ok: true,
967
- base: ""
968
- };
969
- const project = await inspectProject(options.cwd);
970
- if (project === null) return {
971
- ok: false,
972
- lines: [`${isolated.join(", ")} ${isolated.length === 1 ? "needs" : "need"} a git worktree, but ${projectFolder(options.cwd)} is not in a git repository with a commit; make a first commit (\`git commit --allow-empty -m "Start"\`) first, or pass --in-place to run them here`]
973
- };
974
- if (project.dirtyAgentFiles.length > 0 && options.allowDirty !== true) return {
975
- ok: false,
976
- lines: dirtyAgentMessage(project.dirtyAgentFiles)
977
- };
978
- if (project.dirtyOther > 0) options.err(`${style(["yellow", "bold"], "warning")} ${String(project.dirtyOther)} uncommitted change(s) in this checkout are not in the worktrees`);
979
- try {
980
- const base = await resolveBase(project.root, gitRulesOf(workflow), inputs);
981
- return {
982
- ok: true,
983
- root: project.root,
984
- base
985
- };
986
- } catch (error) {
987
- return {
988
- ok: false,
989
- lines: [error instanceof Error ? error.message : String(error)]
990
- };
991
- }
992
- };
993
- // src/run/lock-notes.ts
994
- /**
995
- * @file The note `ork run` prints at the start for a `branch:` lock that has no
996
- * origin to key by (RFC-016): the lock is local to the folder, so no other
997
- * clone shares it. Only a `branch:` lock gets one.
998
- */
999
- /**
1000
- * The notes, one per distinct `branch:` lock the run names that is local to
1001
- * `repo`.
1002
- */
1003
- const localLockNotes = async (workflow, inputs, repo) => {
1004
- const names = /* @__PURE__ */ new Set();
1005
- for (const node of Object.values(workflow.nodes)) {
1006
- const lock = "lock" in node ? node.lock : void 0;
1007
- if (lock === void 0) continue;
1008
- const parsed = parseTemplate(lock);
1009
- if (parsed.ok) names.add(renderTemplate(parsed.parts, {
1010
- inputs,
1011
- nodes: {}
1012
- }));
1013
- }
1014
- return (await Promise.all([...names].map(async (name) => await lockIsLocal(name, { repo }) ? name : void 0))).filter((name) => name !== void 0).map((name) => `${name} is local to ${path.resolve(repo)}: no origin, so no other clone shares it`);
1015
- };
1016
- // src/run/preflight.ts
1017
- const preflight = async (options, given) => {
1018
- const { workflow, place, projects } = given;
1019
- const installed = options.installed ?? (options.tools === void 0 ? (p) => onPath(p) : () => true);
1020
- const missing = missingPrograms(workflow, installed);
1021
- if (missing.length > 0) return {
1022
- ok: false,
1023
- lines: missingProgramMessage(missing)
1024
- };
1025
- const unset = envProblems(workflow, place, projects, options.resume?.history, options.err);
1026
- if (unset.length > 0) return {
1027
- ok: false,
1028
- lines: unset
1029
- };
1030
- const limits = runLimits(options.store, place, options.home);
1031
- const refused = limitsRefusal(limits, workflow, options.resume?.history);
1032
- if (refused.length > 0) return {
1033
- ok: false,
1034
- lines: refused
1035
- };
1036
- const driver = await runReadyDriver(given.effective, {
1037
- override: options.driver,
1038
- recorded: options.resume?.settings.readyDriver,
1039
- checks: options.driverChecks
1040
- });
1041
- if (!driver.ok) return {
1042
- ok: false,
1043
- lines: driver.lines
1044
- };
1045
- if (driver.line !== void 0) options.err(driver.line);
1046
- const unfit = unfitLines(given.effective, options.drivers, {
1047
- override: options.driver,
1048
- ready: driver.driver ?? options.resume?.settings.readyDriver
1049
- });
1050
- if (unfit.length > 0) return {
1051
- ok: false,
1052
- lines: unfit
1053
- };
1054
- const budget = budgetWarningLines(given.effective, options.drivers, {
1055
- override: options.driver,
1056
- ready: driver.driver ?? options.resume?.settings.readyDriver
1057
- });
1058
- for (const line of noteLines(budget)) options.err(line);
1059
- const checkout = await prepareCheckout(workflow, {
1060
- ...options,
1061
- cwd: place.root
1062
- }, given.inputs);
1063
- if (!checkout.ok) return {
1064
- ok: false,
1065
- lines: checkout.lines
1066
- };
1067
- const local = await localLockNotes(workflow, given.inputs, checkout.root ?? place.root);
1068
- for (const line of noteLines(local)) options.err(line);
1069
- return {
1070
- ...checkout,
1071
- limits: limits.now,
1072
- ...driver.driver === void 0 ? {} : { readyDriver: driver.driver }
1073
- };
1074
- };
1075
- // src/run/workflow.ts
1076
- /**
1077
- * @file `ork run` for a whole workflow: checks the workflow, its inputs and the
1078
- * project (git, uncommitted agent files), sets up the worktree workspace, and
1079
- * streams the scheduler's run. `ork resume` comes through here too, with the
1080
- * run's recorded state.
1081
- */
1082
- /**
1083
- * Runs a whole workflow: checks it and its inputs, then hands the compiled
1084
- * graph to the scheduler with the built-in node types, and streams the run.
1085
- */
1086
- const runWorkflowFile = async (options) => {
1087
- const failed = (lines) => {
1088
- for (const line of lines) options.err(line);
1089
- return {
1090
- exitCode: 1,
1091
- result: null
1092
- };
1093
- };
1094
- const loaded = loadWorkflow(options.file, options.cwd, options.home, options.store.projects());
1095
- const { workflow, graph, place, workflowDir, textHash } = loaded;
1096
- if (options.expectWorkflow !== void 0 && options.expectWorkflow !== textHash) return failed([`${options.file} isn't what was accepted: it changed after ork watch read it. Accept it again with \`ork watch --accept\`.`]);
1097
- if (workflow === null || graph === null) return failed(formatReport(loaded.report));
1098
- const { report } = options.resume === void 0 ? await walkLoaded(loaded) : loaded;
1099
- for (const line of warningLines(report)) options.err(line);
1100
- if (report.walk?.complete === false) options.err(walkSummary(report.walk));
1101
- const pairs = parseInputPairs(options.inputs);
1102
- if (!pairs.ok) return failed([`--input: ${pairs.error}`]);
1103
- const recorded = Object.fromEntries(Object.entries(options.resume?.inputs ?? {}).map(([k, v]) => [k, String(v)]));
1104
- const inputs = resolveInputs(workflow, {
1105
- ...recorded,
1106
- ...pairs.raw
1107
- });
1108
- if (!inputs.ok) return failed(inputs.errors);
1109
- const projects = options.store.projects();
1110
- const ready = await preflight(options, {
1111
- workflow,
1112
- effective: graph.workflow,
1113
- place,
1114
- projects,
1115
- inputs: inputs.inputs
1116
- });
1117
- if (!ready.ok) return failed(ready.lines);
1118
- const { root, base } = ready;
1119
- const cwd = root ?? (inside(place.root, options.cwd) ? options.cwd : place.root);
1120
- const runId = options.runId ?? newRunId(options.store);
1121
- const runDir = runDirOf(place, runId);
1122
- if (options.resume !== void 0) options.store.reopen(runId);
1123
- const record = options.resume === void 0 ? createRecorder(options.store, {
1124
- id: runId,
1125
- workflow: workflow.name,
1126
- file: path.resolve(options.cwd, options.file),
1127
- project: place.root,
1128
- ...options.calledBy === void 0 ? {} : { parent: options.calledBy.split("/")[0] ?? "" }
1129
- }, runDir) : appendRecorder(options.store, runId, runDir);
1130
- const width = Math.max(...Object.keys(workflow.nodes).map((id) => id.length));
1131
- const tools = /* @__PURE__ */ new Map();
1132
- const view = createView();
1133
- const started = Promise.withResolvers();
1134
- const live = {};
1135
- const control = await startControl({
1136
- store: options.store,
1137
- runId,
1138
- emit: (event) => {
1139
- onEvent(event);
1140
- },
1141
- ...liveHandlers(started.promise),
1142
- onControl: liveControl(live),
1143
- ...options.human === void 0 ? {} : { local: options.human },
1144
- ...options.home === void 0 ? {} : { home: options.home },
1145
- ...options.intervalMs === void 0 ? {} : { intervalMs: options.intervalMs }
1146
- });
1147
- const signals = createSignals(feedbackOrigin(path.resolve(options.cwd, options.file)));
1148
- const onEvent = (raw) => {
1149
- for (const event of signals(raw)) {
1150
- control.publish(record(event));
1151
- if (options.json) options.out(JSON.stringify(event));
1152
- else for (const line of formatRunEvent(event, width, tools, options.human !== void 0, options.verbose, view)) options.out(line);
1153
- }
1154
- };
1155
- const workspace = root === void 0 ? void 0 : createWorkspace({
1156
- graph,
1157
- runId,
1158
- root,
1159
- head: options.baseRef ?? options.resume?.settings.baseRef ?? base,
1160
- inputs: inputs.inputs,
1161
- onEvent,
1162
- ...resumedOptions(options.resume)
1163
- });
1164
- const maskedNames = maskedNamesOfRun(workflow, place, projects);
1165
- const executorOptions = {
1166
- runtimeFor: (driver) => options.runtime(driver, placeFiles(place).runs),
1167
- inPlace: options.inPlace,
1168
- maskedNames,
1169
- store: storePath(options.home)
1170
- };
1171
- if (options.driver !== void 0) executorOptions.driver = options.driver;
1172
- if (ready.readyDriver !== void 0) executorOptions.readyDriver = ready.readyDriver;
1173
- if (options.tools !== void 0) executorOptions.tools = options.tools;
1174
- const script = process.argv[1];
1175
- if (script !== void 0 && onPath(script)) executorOptions.orkBin = script;
1176
- const replay = options.resume === void 0 ? void 0 : createReplay(options.resume.history, options.resume.cutOff, options.resume.mail, options.resume.earlier, options.resume.fanOuts);
1177
- if (replay !== void 0) onEvent({
1178
- type: "run.resumed",
1179
- kept: replay.kept(),
1180
- ork: ORK_RUN_VERSION
1181
- });
1182
- const ports = callPorts({
1183
- ...options,
1184
- graph,
1185
- runId,
1186
- place,
1187
- human: control.human,
1188
- emit: onEvent
1189
- });
1190
- if (ports.settings.machineClaude === true) executorOptions.machineClaude = true;
1191
- clearOutputFiles(runDir, options.err);
1192
- const handle = startRun({
1193
- graph: ports.graph,
1194
- calls: ports.calls,
1195
- runId,
1196
- runDir,
1197
- inputs: inputs.inputs,
1198
- cwd,
1199
- executors: builtinExecutors(executorOptions),
1200
- human: options.resume === void 0 ? control.human : replayLoopAnswers(control.human, options.resume.loopAnswers),
1201
- onEvent,
1202
- orkVersion: ORK_RUN_VERSION,
1203
- settings: {
1204
- inPlace: options.inPlace,
1205
- ...options.driver === void 0 ? {} : { driver: options.driver },
1206
- ...ready.readyDriver === void 0 ? {} : { readyDriver: ready.readyDriver },
1207
- ...ports.settings
1208
- },
1209
- followUps: ports.followUps,
1210
- ...ports.relay === void 0 ? {} : { relay: ports.relay },
1211
- ...replay === void 0 ? {} : { replay },
1212
- workflowDir,
1213
- projectRoot: root ?? place.root,
1214
- orkDir: place.dir,
1215
- ...workspace === void 0 ? {} : { workspace },
1216
- scrub: runScrubber(maskedNames, process.env),
1217
- limits: ready.limits,
1218
- machine: {
1219
- rules: machineRulesOf(loadProject(place.root, placeFiles(place).rules).project),
1220
- ledger: machineSlotLedger(options.home === void 0 ? {} : { home: options.home })
1221
- },
1222
- locks: machineLockLedgers(root ?? place.root, place.root, options.home),
1223
- beforeFinish: takeLastCommands(control, started.promise)
1224
- });
1225
- live.handle = handle;
1226
- started.resolve(handle);
1227
- options.onRun?.(handle);
1228
- const result = await handle.done;
1229
- await ports.shown.end(result.status);
1230
- clearOutputFiles(runDir, options.err);
1231
- if (result.status === "succeeded") clearStateFiles(runDir, options.err);
1232
- await endLinksOf(options.store, runId, result.status, options.home);
1233
- await control.close();
1234
- return {
1235
- exitCode: result.status === "succeeded" ? 0 : 1,
1236
- result
1237
- };
1238
- };
1239
- // src/commands/run.ts
1240
- /**
1241
- * @file `ork run`: the command and its flags. It runs a whole workflow, or one
1242
- * Claude node with `--node`, with questions and permission prompts answered
1243
- * on the terminal.
1244
- */
1245
- const flagValues = (rawArgs, flag) => {
1246
- const values = [];
1247
- for (const [index, arg] of rawArgs.entries()) if (arg.startsWith(`${flag}=`)) values.push(arg.slice(flag.length + 1));
1248
- else if (arg === flag) {
1249
- const value = rawArgs[index + 1];
1250
- if (value !== void 0) values.push(value);
1251
- }
1252
- return values;
1253
- };
1254
- const isDriver = (value) => AGENT_DRIVERS.some((d) => d.id === value);
1255
- /** Why `--driver` refuses a value: a retired id names its replacement. */
1256
- const badDriver = (value) => `--${retiredDriverHint(value) ?? `driver must be one of ${AGENT_DRIVERS.map((d) => d.id).join(", ")}, got \`${value}\``}`;
1257
- /** Loads the drivers only when a run starts, so other commands stay fast. */
1258
- const loadRuntime = async (driver, runs) => (await import("./src-D4JSymhK.mjs")).createRuntime(driver, sessionEnv(process.env), runs);
1259
- /**
1260
- * Ctrl-C asks the run to stop, and so does a terminal closing or a polite kill,
1261
- * so that the run ends tidily and leaves no result unscrubbed.
1262
- */
1263
- const onInterrupt = (stop) => {
1264
- let asked = false;
1265
- const interrupt = () => {
1266
- if (asked) return;
1267
- asked = true;
1268
- writeErr(style("dim", "\ninterrupting…"));
1269
- stop();
1270
- };
1271
- process.once("SIGINT", interrupt);
1272
- process.once("SIGTERM", interrupt);
1273
- process.once("SIGHUP", interrupt);
1274
- };
1275
- /** The terminal that answers a run's questions; none in a detached run. */
1276
- const terminalHuman = () => {
1277
- const detached = process.env[DETACHED_ENV] === "1";
1278
- if (detached) writeErr(style("dim", "detached run: no terminal asks; answer with ork attach or ork decide"));
1279
- return detached ? void 0 : createTerminalHuman({
1280
- input: process.stdin,
1281
- output: process.stderr,
1282
- interactive: process.stdin.isTTY,
1283
- interrupt: () => {
1284
- process.kill(process.pid, "SIGINT");
1285
- }
1286
- });
1287
- };
1288
- const run = defineCommand({
1289
- meta: {
1290
- name: "run",
1291
- description: "Run a workflow, or one Claude node of it with --node"
1292
- },
1293
- args: {
1294
- workflow: {
1295
- type: "positional",
1296
- description: "A workflow file, the name of one in the project's workflows (.ork/workflows/), or one of Ork's own (ork:feedback-apply)",
1297
- required: true
1298
- },
1299
- node: {
1300
- type: "string",
1301
- description: "Run only this Claude node"
1302
- },
1303
- input: {
1304
- type: "string",
1305
- description: "A workflow input, name=value (repeatable)"
1306
- },
1307
- "follow-ups": {
1308
- type: "string",
1309
- description: "Follow-up ids an agent does, or `due` for every due one (repeatable or comma-separated): they become the run's brief, done when it publishes"
1310
- },
1311
- driver: {
1312
- type: "string",
1313
- description: `${DRIVER_IDS.join(", ")} (default: the driver ready here)`
1314
- },
1315
- "in-place": {
1316
- type: "boolean",
1317
- description: "Run worktree nodes in this checkout, without worktrees",
1318
- default: false
1319
- },
1320
- "allow-dirty": {
1321
- type: "boolean",
1322
- description: "Run although instructions or skills have uncommitted changes (worktrees get the committed versions)",
1323
- default: false
1324
- },
1325
- "machine-claude": {
1326
- type: "boolean",
1327
- description: "Sessions load this machine's Claude setup: its MCP servers and Claude's memory (default: clean sessions)",
1328
- default: false
1329
- },
1330
- json: {
1331
- type: "boolean",
1332
- description: "Print the events as JSON lines",
1333
- default: false
1334
- },
1335
- verbose: {
1336
- type: "boolean",
1337
- description: "Show each tool call whole, and why each failed node stopped, with its scrubbed details",
1338
- default: false
1339
- },
1340
- detach: {
1341
- type: "boolean",
1342
- description: "Run in the background; follow it with ork logs -f, answer with ork decide",
1343
- default: false
1344
- },
1345
- "run-id": {
1346
- type: "string",
1347
- description: "Internal: the id `--detach` picked for the run"
1348
- },
1349
- "called-by": {
1350
- type: "string",
1351
- description: "Internal: the workflow.call that started this run"
1352
- },
1353
- "budget-cap": {
1354
- type: "string",
1355
- description: "Internal: what the caller has left of its budget"
1356
- },
1357
- "base-ref": {
1358
- type: "string",
1359
- description: "Internal: what this run's worktrees start from"
1360
- },
1361
- trigger: {
1362
- type: "string",
1363
- description: "Internal: the trigger that started this run"
1364
- },
1365
- "expect-workflow": {
1366
- type: "string",
1367
- description: "Internal: the hash the workflow's text must have"
1368
- },
1369
- untrusted: {
1370
- type: "string",
1371
- description: "Internal: an input from what a trigger's check reported"
1372
- }
1373
- },
1374
- run: async ({ args, rawArgs }) => {
1375
- if (args.driver !== void 0 && !isDriver(args.driver)) {
1376
- writeErr(badDriver(args.driver));
1377
- process.exitCode = 1;
1378
- return;
1379
- }
1380
- const { prepareFollowUps: prepare, followUpsFlagFine: flagFine } = await import("./brief-DH66xmj8.mjs");
1381
- const cwd = process.cwd();
1382
- if (refusedBuiltin([args.workflow], cwd) || !flagFine(rawArgs)) return;
1383
- const target = workflowArg(args.workflow, cwd);
1384
- const store = openStore();
1385
- if (args.detach) {
1386
- try {
1387
- const { startDetached } = await import("./detached-Dakw5RZj.mjs");
1388
- await startDetached(store, {
1389
- rawArgs,
1390
- cwd,
1391
- target,
1392
- node: args.node
1393
- });
1394
- } finally {
1395
- store.close();
1396
- }
1397
- return;
1398
- }
1399
- const taken = await prepare(store, rawArgs, args["run-id"], target);
1400
- if (taken === void 0) {
1401
- store.close();
1402
- return;
1403
- }
1404
- const human = terminalHuman();
1405
- const common = {
1406
- file: target.file,
1407
- cwd,
1408
- ...taken,
1409
- inPlace: args["in-place"],
1410
- ...args["machine-claude"] ? { machineClaude: true } : {},
1411
- json: args.json,
1412
- verbose: args.verbose,
1413
- runtime: loadRuntime,
1414
- store,
1415
- ...human === void 0 ? {} : { human },
1416
- out: write,
1417
- err: writeErr,
1418
- ...args.driver === void 0 ? {} : { driver: args.driver }
1419
- };
1420
- try {
1421
- const { exitCode } = args.node === void 0 || args.node === "" ? await runWorkflowFile({
1422
- ...common,
1423
- allowDirty: args["allow-dirty"],
1424
- ...args["called-by"] === void 0 ? {} : { calledBy: args["called-by"] },
1425
- ...args["budget-cap"] === void 0 ? {} : { budgetCap: Number(args["budget-cap"]) },
1426
- ...args["base-ref"] === void 0 ? {} : { baseRef: args["base-ref"] },
1427
- ...args["expect-workflow"] === void 0 ? {} : { expectWorkflow: args["expect-workflow"] },
1428
- ...args.trigger === void 0 ? {} : {
1429
- trigger: args.trigger,
1430
- untrusted: flagValues(rawArgs, "--untrusted")
1431
- },
1432
- ...human === void 0 ? {} : { showCalls: callsShown(human, process.stdin.isTTY) },
1433
- onRun: (handle) => {
1434
- onInterrupt(handle.interrupt);
1435
- }
1436
- }) : await runNode({
1437
- ...common,
1438
- nodeId: args.node,
1439
- onSession: (session) => {
1440
- onInterrupt(() => {
1441
- session.interrupt();
1442
- });
1443
- }
1444
- });
1445
- process.exitCode = exitCode;
1446
- } finally {
1447
- human?.close();
1448
- store.close();
1449
- }
1450
- }
1451
- });
1452
- export { machineSlotLedger as _, onInterrupt as a, takeFollowUps as b, runWorkflowFile as c, followUpsRule as d, refreshDue as f, sweepLocks as g, machineLocksDir as h, loadRuntime as i, briefOf as l, resumeChat as m, flagValues as n, run as o, runIsLive as p, isDriver as r, terminalHuman as s, badDriver as t, checkTakeable as u, availableDue as v, heldNote as y };