@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.
- package/dist/{__build_info__-D6mXC4cD.mjs → __build_info__-0wqQ-rtf.mjs} +3 -51
- package/dist/{attach-C1OnxyMh.mjs → attach-C0-YyNoY.mjs} +6 -6
- package/dist/{attach-CEGbRg1D.mjs → attach-CAKNqnUK.mjs} +64 -35
- package/dist/bin.mjs +1426 -72
- package/dist/{brief-DH66xmj8.mjs → brief-C2hew8cS.mjs} +7 -6
- package/dist/{builtin-files-Cf-NZDqM.mjs → builtin-files-YYAnbZGD.mjs} +1 -1
- package/dist/{chips-BqcT58qr.mjs → chips-CP_pgKlj.mjs} +7 -2
- package/dist/{completion-DS0gCZRm.mjs → completion-K4rAVsrf.mjs} +6 -6
- package/dist/{context-CGPh12HV.mjs → context-B-fjPJky.mjs} +4 -4
- package/dist/context-DMK0U1w0.mjs +160 -0
- package/dist/{detached-Dakw5RZj.mjs → detached-q0AYG0Bs.mjs} +6 -5
- package/dist/{find-BTikxc54.mjs → find-Ct5CNl1J.mjs} +1 -1
- package/dist/{flags-C8yCq0j5.mjs → flags-C90N4J1i.mjs} +3 -3
- package/dist/{follow-ups-CtlhHbwe.mjs → follow-ups-D3HPyNYM.mjs} +14 -9
- package/dist/{format-BB4E-T6x.mjs → format-CiPhQqbX.mjs} +24 -13
- package/dist/{help-groups-BpRgBSTh.mjs → help-groups-BQvcKQ9H.mjs} +8 -3
- package/dist/{in-a-run-CbnKjO7t.mjs → in-a-run-D_wg4iJi.mjs} +2 -2
- package/dist/{install-CWcpX5g4.mjs → install-Hji6LIei.mjs} +50 -2
- package/dist/{limits-VQqsBoQk.mjs → limits-BD6RZRCS.mjs} +1 -1
- package/dist/{mcp-wGfkMCVF.mjs → mcp-CpbDmBLG.mjs} +6 -6
- package/dist/{once-BHxSdZz_.mjs → once-J8Qn5fTG.mjs} +4 -4
- package/dist/{output-B1kwo0YU.mjs → output-DXZbnFt_.mjs} +61 -42
- package/dist/{pause-C_Wxh-KM.mjs → pause-Cl4ZYA35.mjs} +4 -4
- package/dist/{print-DNYzq4Rr.mjs → print-DzugGZQB.mjs} +2 -2
- package/dist/{project-records-XY3ku95x.mjs → project-records-CtkmlsHN.mjs} +3 -3
- package/dist/{relay-BA6tsnpc.mjs → relay-CiP77vga.mjs} +2 -2
- package/dist/{run-Bvvg-fiF.mjs → run-CtuNY3mE.mjs} +1 -1
- package/dist/{send-nrKtak5F.mjs → send-DRYGks8K.mjs} +3 -4
- package/dist/short-flags-DkJLWVqh.mjs +1 -0
- package/dist/short-flags-Qf21as0E.mjs +83 -0
- package/dist/{src-D4JSymhK.mjs → src-BMFQKHz4.mjs} +32 -14
- package/dist/{src-CmEvHdju.mjs → src-BrFfDKux.mjs} +8 -7
- package/dist/{src-8cOPOgrT.mjs → src-BuF9Lbow.mjs} +77 -18
- package/dist/{src-I_RyKu6_.mjs → src-k0A_QpST.mjs} +5 -3
- package/dist/store-rTgRu8xy.mjs +25 -0
- package/dist/ui/assets/app-BYxpKJRh.css +1 -0
- package/dist/ui/assets/{app-BowhjEAe.js → app-SEFEJLkF.js} +8 -8
- package/dist/ui/assets/{index-CrMa2oHB.js → index-Cb6Voq_W.js} +2 -2
- package/dist/ui/index.html +1 -1
- package/dist/{ui-DY3Dnpho.mjs → ui-C2f37TSb.mjs} +9 -6
- package/dist/{usage-1VwSg_Qt.mjs → usage-BuvGcQSk.mjs} +6 -7
- package/dist/validate-ObP5lt29.mjs +1 -0
- package/dist/{validate-BxayjjkV.mjs → validate-_HjACtk9.mjs} +6 -6
- package/dist/{verdict-CMvPCpWC.mjs → verdict-B592j1W3.mjs} +2 -2
- package/dist/{watch-CT0EtLPZ.mjs → watch-DV3ixCYj.mjs} +7 -7
- package/dist/{workflows-DHHi-APg.mjs → workflows-QLjBhGNS.mjs} +3 -3
- package/dist/{worktrees-CZvbHzbN.mjs → worktrees-D3ubG_Qw.mjs} +6 -6
- package/dist/yolo-BSH1E_kE.mjs +33 -0
- package/package.json +1 -1
- package/dist/run-DBUAq-Uu.mjs +0 -1452
- package/dist/store-CDpk8Wtz.mjs +0 -16
- package/dist/ui/assets/app--FijEXiJ.css +0 -1
package/dist/run-DBUAq-Uu.mjs
DELETED
|
@@ -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 };
|