@krapht/ork 1.0.0-alpha.2 → 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 (75) hide show
  1. package/README.md +12 -7
  2. package/dist/__build_info__-0wqQ-rtf.mjs +180 -0
  3. package/dist/apply-Mb0fNpCX.mjs +17 -0
  4. package/dist/argument-error-CihH46Le.mjs +27 -0
  5. package/dist/attach-C0-YyNoY.mjs +92 -0
  6. package/dist/attach-CAKNqnUK.mjs +1259 -0
  7. package/dist/bin.mjs +1885 -1609
  8. package/dist/brief-C2hew8cS.mjs +116 -0
  9. package/dist/{decide-CGyx9rv7.mjs → builtin-files-YYAnbZGD.mjs} +3 -51
  10. package/dist/chips-CP_pgKlj.mjs +48 -0
  11. package/dist/command-tree-CEkzWNy6.mjs +63 -0
  12. package/dist/{completion-CSU5vkEt.mjs → completion-K4rAVsrf.mjs} +200 -30
  13. package/dist/context-B-fjPJky.mjs +106 -0
  14. package/dist/context-DMK0U1w0.mjs +160 -0
  15. package/dist/detached-q0AYG0Bs.mjs +39 -0
  16. package/dist/extra-arguments-BxQMXARO.mjs +13 -0
  17. package/dist/{find-D6Gj1sxg.mjs → find-Ct5CNl1J.mjs} +2 -2
  18. package/dist/flags-C90N4J1i.mjs +145 -0
  19. package/dist/follow-ups-D3HPyNYM.mjs +568 -0
  20. package/dist/format-CiPhQqbX.mjs +260 -0
  21. package/dist/{help-groups-BwYoAWsh.mjs → help-groups-BQvcKQ9H.mjs} +49 -78
  22. package/dist/in-a-run-D_wg4iJi.mjs +27 -0
  23. package/dist/install-Hji6LIei.mjs +259 -0
  24. package/dist/{limits-B1-DYc3d.mjs → limits-BD6RZRCS.mjs} +2 -2
  25. package/dist/{mcp-DE_EmyVm.mjs → mcp-CpbDmBLG.mjs} +6 -6
  26. package/dist/once-J8Qn5fTG.mjs +112 -0
  27. package/dist/orphan-guard-BFKg8VSd.mjs +58 -0
  28. package/dist/output-DXZbnFt_.mjs +458 -0
  29. package/dist/outputs-text-CNTJ7Qq_.mjs +19 -0
  30. package/dist/{pause-CL6N_Xrj.mjs → pause-Cl4ZYA35.mjs} +7 -4
  31. package/dist/print-DzugGZQB.mjs +19 -0
  32. package/dist/project-records-CtkmlsHN.mjs +233 -0
  33. package/dist/refusals-DQQX3AIb.mjs +116 -0
  34. package/dist/{relay-0SD0erwf.mjs → relay-CiP77vga.mjs} +2 -2
  35. package/dist/run-CtuNY3mE.mjs +234 -0
  36. package/dist/send-DRYGks8K.mjs +104 -0
  37. package/dist/short-flags-DkJLWVqh.mjs +1 -0
  38. package/dist/short-flags-Qf21as0E.mjs +83 -0
  39. package/dist/src-BMFQKHz4.mjs +3122 -0
  40. package/dist/{src-BbyEoMn4.mjs → src-BrFfDKux.mjs} +800 -235
  41. package/dist/{src-Dujmvv4s.mjs → src-BuF9Lbow.mjs} +12580 -5505
  42. package/dist/{src-DOcK8Jfp.mjs → src-k0A_QpST.mjs} +1024 -590
  43. package/dist/store-rTgRu8xy.mjs +25 -0
  44. package/dist/ui/THIRD_PARTY_LICENSES.txt +612 -0
  45. package/dist/ui/assets/app-BYxpKJRh.css +1 -0
  46. package/dist/ui/assets/app-SEFEJLkF.js +112 -0
  47. package/dist/ui/assets/{index-9dFha9Qn.js → index-Cb6Voq_W.js} +3 -3
  48. package/dist/ui/assets/theme-CdXvqTQ6.js +1 -0
  49. package/dist/ui/index.html +2 -2
  50. package/dist/{ui-CTmO2Xzu.mjs → ui-C2f37TSb.mjs} +57 -25
  51. package/dist/usage-BuvGcQSk.mjs +256 -0
  52. package/dist/validate-ObP5lt29.mjs +1 -0
  53. package/dist/validate-_HjACtk9.mjs +632 -0
  54. package/dist/verdict-B592j1W3.mjs +84 -0
  55. package/dist/{watch-CtbgDAcz.mjs → watch-DV3ixCYj.mjs} +45 -17
  56. package/dist/{workflows-D1_WVukk.mjs → workflows-QLjBhGNS.mjs} +30 -5
  57. package/dist/worktrees-D3ubG_Qw.mjs +302 -0
  58. package/dist/yolo-BSH1E_kE.mjs +33 -0
  59. package/package.json +10 -8
  60. package/dist/__build_info__-fy6L1Mrd.mjs +0 -615
  61. package/dist/clean-DaHjs__2.mjs +0 -88
  62. package/dist/context-DT8MfEIR.mjs +0 -274
  63. package/dist/format-Dnfv8zMT.mjs +0 -427
  64. package/dist/in-a-run-pLAZeNys.mjs +0 -22
  65. package/dist/list-BrH8KRSJ.mjs +0 -110
  66. package/dist/output-BLQWD6MJ.mjs +0 -950
  67. package/dist/remove-BeGdqroy.mjs +0 -194
  68. package/dist/report-DK4LGqwF.mjs +0 -129
  69. package/dist/send-C8tunWhv.mjs +0 -163
  70. package/dist/src-CCUKRX51.mjs +0 -1204
  71. package/dist/ui/assets/app-BNT0SuIz.css +0 -1
  72. package/dist/ui/assets/app-T7wVAhgx.js +0 -67
  73. package/dist/ui/assets/theme-3xH5jxKs.js +0 -1
  74. package/dist/usage-DYUmP0i4.mjs +0 -419
  75. package/dist/worktrees-DdMYoNOC.mjs +0 -212
@@ -0,0 +1,632 @@
1
+ import { $ as driverQueues, Bi as firstLine, D as lateTriggers, Di as retiredDriverHint, Dr as socketRoom, Et as walkSummary, Fi as whyReady, Hi as experimentsOn, Kr as placeFiles, M as readProjectTriggers, N as workflowTextHash, Ni as driverChecks, O as triggersReport, Pi as readyDriver, R as openStore, Ri as AGENT_DRIVERS, Ti as DriverSchema, Tt as walkChecks, Ui as knownExperiments, Vi as EXPERIMENTS, X as budgetWarnings, Yn as loadProject, Z as driverRefusals, Zr as workflowDirOf, cn as namingNoDriver, ki as INTEGRATIONS, qr as placeFor, wt as checkWorkflowText, z as storePath, zi as DRIVER_IDS } from "./src-BuF9Lbow.mjs";
2
+ import { d as readUpdateCache, f as refreshUpdateCache, i as registryOf, l as newerVersion, m as writeUpdateCache, n as ORK_VERSION, o as updateSwitchOff, p as updateCachePath, r as isSwitchedOn, s as checkDue, t as ORK_NAME, u as pendingNotice } from "./__build_info__-0wqQ-rtf.mjs";
3
+ import { S as wrap, a as plain, c as write, g as plainEnv, k as paint, l as writeErr, s as style, v as termOf, x as widthOf, y as cut } from "./output-DXZbnFt_.mjs";
4
+ import { r as chip, t as CHIP_LABELS } from "./chips-CP_pgKlj.mjs";
5
+ import { a as refusedBuiltin, c as projectAt, i as missingWorkflowText, l as registeredProjects, r as findWorkflowFiles } from "./workflows-QLjBhGNS.mjs";
6
+ import path from "node:path";
7
+ import { readFileSync } from "node:fs";
8
+ import { execFile, spawn } from "node:child_process";
9
+ import { styleText } from "node:util";
10
+ import { defineCommand } from "citty";
11
+ // src/update/notify.ts
12
+ /**
13
+ * @file The new-version notice (ADR-038). At most once a day a command starts a
14
+ * detached copy of `ork` that asks the npm registry for the latest version
15
+ * and records it in `~/.ork/update-check.json`, so no command waits on the
16
+ * network; a later command, when a newer version is known, ends with one line
17
+ * on stderr (once a day). Off in CI, without a terminal, with `--json`, and
18
+ * with `ORK_NO_UPDATE_CHECK=1` or `NO_UPDATE_NOTIFIER=1`.
19
+ */
20
+ /** Set in the background copy of `ork` that only runs the check. */
21
+ const UPDATE_CHILD_ENV = "ORK_UPDATE_CHECK_CHILD";
22
+ /**
23
+ * Why the automatic check is off for this command, or `null` when it is on: the
24
+ * environment's switches (core's, which `ork ui` reads too), then where a
25
+ * notice on stderr would go.
26
+ */
27
+ const updateCheckOff = (env, argv, terminal) => {
28
+ const switched = updateSwitchOff(env);
29
+ if (switched !== null) return switched === "CI" ? "in CI" : "turned off";
30
+ if (isSwitchedOn(env["ORK_DETACHED"]) || !terminal) return "no terminal";
31
+ if (argv.includes("--json")) return "JSON output";
32
+ if (argv[0] === "upgrade") return "ork upgrade checks for itself";
33
+ if (argv[0] === "ui" && argv.includes("--serve")) return "ork ui's server checks for itself";
34
+ return null;
35
+ };
36
+ /** The notice's text. */
37
+ const updateNotice = (latest, current) => `Ork ${latest} is out (you have ${current}). Upgrade with: ork upgrade`;
38
+ /**
39
+ * Starts the background check when one is due. Records the attempt first, so
40
+ * commands started in the next moments do not start one too.
41
+ */
42
+ const startUpdateCheck = (file = updateCachePath(), now = /* @__PURE__ */ new Date()) => {
43
+ try {
44
+ const cache = readUpdateCache(file);
45
+ if (!checkDue(cache, now)) return;
46
+ writeUpdateCache(file, {
47
+ ...cache,
48
+ checkedAt: now.toISOString()
49
+ });
50
+ const child = spawn(process.execPath, [...process.execArgv, process.argv[1] ?? ""], {
51
+ detached: true,
52
+ stdio: "ignore",
53
+ env: {
54
+ ...process.env,
55
+ [UPDATE_CHILD_ENV]: "1"
56
+ }
57
+ });
58
+ child.on("error", () => {});
59
+ child.unref();
60
+ } catch {}
61
+ };
62
+ /** The background copy's whole job: ask the registry, record the answer. */
63
+ const runUpdateChild = async () => {
64
+ const registry = registryOf(process.env);
65
+ await refreshUpdateCache(updateCachePath(), ORK_NAME, /* @__PURE__ */ new Date(), {
66
+ current: ORK_VERSION,
67
+ ...registry === void 0 ? {} : { registry }
68
+ });
69
+ };
70
+ /** Prints the notice when a newer version is known and not told today. */
71
+ const showUpdateNotice = (file = updateCachePath(), now = /* @__PURE__ */ new Date()) => {
72
+ try {
73
+ const cache = readUpdateCache(file);
74
+ const latest = pendingNotice(cache, ORK_VERSION, now);
75
+ if (cache === null || latest === null) return;
76
+ const text = updateNotice(latest, ORK_VERSION);
77
+ process.stderr.write(`\n${plainEnv() ? text : styleText("yellow", text, { stream: process.stderr })}\n`);
78
+ writeUpdateCache(file, {
79
+ ...cache,
80
+ notified: {
81
+ version: latest,
82
+ at: now.toISOString()
83
+ }
84
+ });
85
+ } catch {}
86
+ };
87
+ // src/update/upgrade.ts
88
+ /**
89
+ * @file `ork doctor`'s line for Ork itself (ADR-038): the running version
90
+ * against the last update check, read from the record without asking the
91
+ * network. What `ork upgrade` does is core's (`planUpgrade`, `runInstall`),
92
+ * shared with the server behind `ork ui`.
93
+ */
94
+ /** `ork doctor`'s line for Ork itself, from the last check. */
95
+ const versionCheck = (current, cache, checksOff) => {
96
+ const newer = newerVersion(cache, current);
97
+ if (newer !== null) return {
98
+ name: "ork",
99
+ status: "warn",
100
+ detail: `${current}; ${newer} is out`,
101
+ hint: "Run ork upgrade."
102
+ };
103
+ return {
104
+ name: "ork",
105
+ status: "ok",
106
+ detail: `${current} (${cache?.latest === void 0 ? checksOff === null ? "not checked for updates yet" : `the daily update check is off here: ${checksOff}` : `the latest as of ${cache.checkedAt.slice(0, 10)}`})`
107
+ };
108
+ };
109
+ // src/doctor/checks.ts
110
+ /**
111
+ * @file The checks behind `ork doctor`: Ork's own version against the last
112
+ * update check, each tool's presence and login status (the drivers' own
113
+ * checks, which `ork run` also reads to pick the one ready), and whether a
114
+ * run's socket path fits, and which experiments are on, through an injectable
115
+ * `exec` so tests need no real tools. Never prints a token or an account.
116
+ */
117
+ const TIMEOUT_MS = 15e3;
118
+ const exec = (program, args) => new Promise((resolve) => {
119
+ execFile(program, args, {
120
+ timeout: TIMEOUT_MS,
121
+ encoding: "utf8"
122
+ }, (error, stdout) => {
123
+ resolve({
124
+ ok: error === null,
125
+ stdout
126
+ });
127
+ });
128
+ });
129
+ /** Ork's own version, against the last update check (no network). */
130
+ const orkCheck = () => versionCheck(ORK_VERSION, readUpdateCache(updateCachePath()), updateCheckOff(process.env, [], process.stderr.isTTY));
131
+ const MIN_NODE = [24, 20];
132
+ const checkNode = (version) => {
133
+ const [major = 0, minor = 0] = version.split(".").map(Number);
134
+ const [minMajor, minMinor] = MIN_NODE;
135
+ return major > minMajor || major === minMajor && minor >= minMinor ? {
136
+ name: "node",
137
+ status: "ok",
138
+ detail: `v${version}`
139
+ } : {
140
+ name: "node",
141
+ status: "fail",
142
+ detail: `v${version}`,
143
+ hint: `Ork needs Node ${MIN_NODE.join(".")} or later.`
144
+ };
145
+ };
146
+ /** A work tree with no commit has no `HEAD` for a worktree to branch from. */
147
+ const checkHead = async (run) => {
148
+ return (await run("git", [
149
+ "rev-parse",
150
+ "--verify",
151
+ "HEAD"
152
+ ])).ok ? {
153
+ name: "git commits",
154
+ status: "ok",
155
+ detail: "HEAD exists"
156
+ } : {
157
+ name: "git commits",
158
+ status: "warn",
159
+ detail: "no commit yet",
160
+ hint: "Make a first commit: worktree runs branch from HEAD."
161
+ };
162
+ };
163
+ const checkGit = async (run) => {
164
+ const version = await run("git", ["--version"]);
165
+ if (!version.ok) return [{
166
+ name: "git",
167
+ status: "fail",
168
+ detail: "not found",
169
+ hint: "Install git; Ork gives each writing node its own worktree."
170
+ }];
171
+ const inside = await run("git", ["rev-parse", "--is-inside-work-tree"]);
172
+ const head = inside.ok ? await checkHead(run) : null;
173
+ return [
174
+ {
175
+ name: "git",
176
+ status: "ok",
177
+ detail: firstLine(version.stdout)
178
+ },
179
+ inside.ok ? {
180
+ name: "git repo",
181
+ status: "ok",
182
+ detail: "inside a work tree"
183
+ } : {
184
+ name: "git repo",
185
+ status: "warn",
186
+ detail: "not a git repository",
187
+ hint: "Run Ork inside a git repository so nodes can use worktrees."
188
+ },
189
+ ...head === null ? [] : [head]
190
+ ];
191
+ };
192
+ /** What `ork init` warns of: a work tree with no commit, else null. */
193
+ const headWarning = async (run) => {
194
+ const head = (await checkGit(run)).find((r) => r.name === "git commits");
195
+ return head?.status === "warn" ? `${head.detail}. ${head.hint ?? ""}`.trim() : null;
196
+ };
197
+ /**
198
+ * Runs every check: Node and git, then every registered agent driver and
199
+ * integration (ADR-022). Node, git, and at least one driver are required;
200
+ * integrations are optional.
201
+ */
202
+ const runChecks = async (run, env, nodeVersion) => {
203
+ const [git, checked, tools] = await Promise.all([
204
+ checkGit(run),
205
+ driverChecks(run, env),
206
+ Promise.all(INTEGRATIONS.flatMap((i) => i.doctor === void 0 ? [] : [i.doctor(run, env)]))
207
+ ]);
208
+ const drivers = checked.map(({ result }) => result);
209
+ const results = [
210
+ checkNode(nodeVersion),
211
+ ...git,
212
+ ...drivers,
213
+ ...tools
214
+ ];
215
+ return {
216
+ results,
217
+ ok: drivers.some((d) => d.status === "ok") && !results.some((r) => r.status === "fail")
218
+ };
219
+ };
220
+ /**
221
+ * Whether a run's socket fits under `~` (L38). Past the limit a run can't
222
+ * listen, and nothing else says so: answers, commands and followers fall back
223
+ * to the store, a second at a time. `home` is the user's `~`.
224
+ */
225
+ const socketCheck = (home, platform) => {
226
+ const { bytes, max } = socketRoom(home, platform);
227
+ if (max === void 0 || bytes <= max) return {
228
+ name: "sockets",
229
+ status: "ok",
230
+ detail: max === void 0 ? "not Unix sockets here" : `${String(bytes)} of ${String(max)} bytes`
231
+ };
232
+ return {
233
+ name: "sockets",
234
+ status: "warn",
235
+ detail: `a run's socket path is ${String(bytes)} bytes, past the ${String(max)} this system allows`,
236
+ hint: "Runs still work, but answers, commands and `ork logs --follow` wait up to a second for each, as they read the store instead. A shorter home folder path fixes it."
237
+ };
238
+ };
239
+ /**
240
+ * What it says of triggers (RFC-011): a long-lived process can die quietly, so
241
+ * a live trigger that hasn't looked when it should have is worth saying. `home`
242
+ * is the user's `~` (tests pass a folder).
243
+ */
244
+ const triggerChecks = (home) => {
245
+ const store = openStore(storePath(home));
246
+ try {
247
+ const report = triggersReport(store, store.projects(), home);
248
+ const broken = report.broken.map(({ file, message }) => ({
249
+ name: "triggers",
250
+ status: "warn",
251
+ detail: `${file}: ${message}`,
252
+ hint: "Its triggers are off until it can be read."
253
+ }));
254
+ const live = report.triggers.filter(({ standing }) => standing === "live");
255
+ if (live.length === 0) return broken;
256
+ const late = lateTriggers(report, /* @__PURE__ */ new Date());
257
+ if (late.length === 0) return [...broken, {
258
+ name: "ork watch",
259
+ status: "ok",
260
+ detail: `${String(live.length)} trigger(s) looking`
261
+ }];
262
+ return [...broken, {
263
+ name: "ork watch",
264
+ status: "warn",
265
+ detail: `not looking: ${late.map(({ trigger }) => trigger).join(", ")}`,
266
+ hint: report.pid === void 0 ? "Start it with `ork watch --detach`." : "See `ork watch --status`."
267
+ }];
268
+ } finally {
269
+ store.close();
270
+ }
271
+ };
272
+ const SOURCE_NAMES = {
273
+ project: "project file",
274
+ environment: "environment"
275
+ };
276
+ /**
277
+ * Which experiments are on and who turned them on; an unknown name fails this
278
+ * check alone. Prints names only. Outside a project, only the environment
279
+ * counts.
280
+ */
281
+ const experimentsCheck = (place, env = process.env, registry = EXPERIMENTS) => {
282
+ const { project, where } = loadProject(place.root, placeFiles(place).rules);
283
+ const state = experimentsOn({
284
+ env,
285
+ project,
286
+ where,
287
+ registry
288
+ });
289
+ if (!state.ok) return {
290
+ name: "experiments",
291
+ status: "fail",
292
+ detail: `unknown: ${state.unknown.map((u) => `\`${u.name}\` (${u.where})`).join(", ")}`,
293
+ hint: `Fix or remove the name. Known experiments: ${knownExperiments(registry)}.`
294
+ };
295
+ return {
296
+ name: "experiments",
297
+ status: "ok",
298
+ detail: state.on.length === 0 ? "none on" : state.on.map(({ name, sources }) => `${name} (${sources.map((s) => SOURCE_NAMES[s]).join(" and ")})`).join(", ")
299
+ };
300
+ };
301
+ // src/run/ready-driver.ts
302
+ /**
303
+ * @file The driver a run takes for the session nodes that name none (core's
304
+ * `driver.ts` has the whole rule): the one `ork doctor`'s own checks find
305
+ * ready on this machine, said in one line. With none ready, the run stops
306
+ * before any session starts, with the hints `ork doctor` gives. A resumed run
307
+ * keeps the driver it recorded. `--driver <id>` skips that look but checks
308
+ * that one driver's tool, and stops the run the same way when it isn't
309
+ * ready.
310
+ */
311
+ /** The real checks: the tools on this machine and its environment. */
312
+ const checksHere = (only) => only === void 0 ? driverChecks(exec, process.env) : Promise.all(AGENT_DRIVERS.filter(({ id }) => id === only).map(async (driver) => ({
313
+ id: driver.id,
314
+ result: await driver.doctor(exec, process.env)
315
+ })));
316
+ /**
317
+ * What stops a run on `--driver <id>`: its own doctor check isn't `ok`, so its
318
+ * tool isn't found or isn't signed in. Empty when it is ready. Drivers named on
319
+ * nodes aren't looked at here; they can sit on a branch that never runs.
320
+ */
321
+ const overrideRefusal = async (id, checks) => {
322
+ const found = (await (checks ?? checksHere)(id)).find((c) => c.id === id);
323
+ if (found === void 0 || found.result.status === "ok") return [];
324
+ const { detail, hint } = found.result;
325
+ return [
326
+ `--driver ${id}: ${id} isn't ready here (${detail}).`,
327
+ ...hint === void 0 ? [] : [hint],
328
+ "See `ork doctor`."
329
+ ];
330
+ };
331
+ const subject = (names, all) => {
332
+ if (all) return "No driver is named, so the run uses";
333
+ const [only] = names;
334
+ return names.length === 1 && only !== void 0 ? `${only} names no driver, so it uses` : `${names.join(", ")} name no driver, so they use`;
335
+ };
336
+ /**
337
+ * The driver for `names`, the session nodes that name none, from `checks`.
338
+ * `all` says whether they are every session node of the run.
339
+ */
340
+ const chooseReadyDriver = (names, all, checks) => {
341
+ const ready = readyDriver(checks);
342
+ if (!ready.ok) return {
343
+ ok: false,
344
+ lines: [
345
+ `No agent driver is ready here, and ${names.join(", ")} ${names.length === 1 ? "names" : "name"} none.`,
346
+ ...ready.hints,
347
+ "See `ork doctor`."
348
+ ]
349
+ };
350
+ const why = ready.ready === 1 ? `the one ready here (${ready.detail})` : whyReady(ready);
351
+ return {
352
+ ok: true,
353
+ driver: ready.driver,
354
+ line: `${subject(names, all)} the ${ready.driver} driver: ${why}.`
355
+ };
356
+ };
357
+ /**
358
+ * What a run takes for its session nodes that name no driver: nothing when
359
+ * `--driver` overrides them all or each names one; the driver a resumed run
360
+ * recorded; otherwise the one ready here.
361
+ */
362
+ const runReadyDriver = async (workflow, options) => {
363
+ if (options.override !== void 0) {
364
+ const lines = await overrideRefusal(options.override, options.checks);
365
+ return lines.length === 0 ? { ok: true } : {
366
+ ok: false,
367
+ lines
368
+ };
369
+ }
370
+ const names = namingNoDriver(workflow);
371
+ if (names.length === 0) return { ok: true };
372
+ if (options.recorded !== void 0) return {
373
+ ok: true,
374
+ driver: options.recorded
375
+ };
376
+ const sessions = Object.values(workflow.nodes).filter((node) => node.type === "agent.leader" || node.type === "agent.worker").length;
377
+ return chooseReadyDriver(names, names.length === sessions, await (options.checks ?? checksHere)());
378
+ };
379
+ /**
380
+ * What stops a session node from running on its driver, one line each: the
381
+ * driver's declared `capabilities` and `proven` against what the node asks
382
+ * (`drivers` default: the real descriptors; tests pass their own).
383
+ */
384
+ const unfitLines = (workflow, drivers, choice) => driverRefusals(workflow, drivers ?? AGENT_DRIVERS, choice).map(({ message }) => message);
385
+ /**
386
+ * What a dollar budget can't see on a session node's driver, one line each
387
+ * (RFC-015): warnings, never a reason to refuse. Same arguments as
388
+ * `unfitLines`.
389
+ */
390
+ const budgetWarningLines = (workflow, drivers, choice) => budgetWarnings(workflow, drivers ?? AGENT_DRIVERS, choice).map(({ message }) => message);
391
+ // src/run/driver-check.ts
392
+ /**
393
+ * @file `ork validate`'s driver check: each session node against what its
394
+ * driver declares (core's `driver-needs.ts`), for `--driver` if given, else
395
+ * the driver a run would take, the one ready here (`ready-driver.ts`).
396
+ */
397
+ const driverDiagnostics = async (workflow, options) => {
398
+ const ready = options.override === void 0 && namingNoDriver(workflow).length > 0 ? readyDriver(await (options.checks ?? checksHere)()) : void 0;
399
+ const drivers = options.drivers ?? AGENT_DRIVERS;
400
+ const choice = {
401
+ override: options.override,
402
+ ready: ready?.ok === true ? ready.driver : void 0
403
+ };
404
+ const at = (node, severity, code, message) => {
405
+ const path = ["nodes", node];
406
+ const found = {
407
+ severity,
408
+ code,
409
+ message,
410
+ path
411
+ };
412
+ const place = options.locate?.(path);
413
+ if (place !== null && place !== void 0) {
414
+ found.line = place.line;
415
+ found.column = place.column;
416
+ }
417
+ return found;
418
+ };
419
+ return [
420
+ ...driverRefusals(workflow, drivers, choice).map(({ node, message }) => at(node, "error", "driver.unfit", message)),
421
+ ...budgetWarnings(workflow, drivers, choice).map(({ node, message }) => at(node, "warning", "driver.budget", message)),
422
+ ...driverQueues(workflow, drivers, choice).filter((found) => found.owner === void 0).flatMap(({ nodes, message }) => nodes.slice(0, 1).map((node) => at(node, "warning", "driver.queues", message)))
423
+ ];
424
+ };
425
+ /** The `--driver` value, or why it is not a driver. */
426
+ const driverArg = (value) => {
427
+ if (value === void 0) return { ok: true };
428
+ const parsed = DriverSchema.safeParse(value);
429
+ return parsed.success ? {
430
+ ok: true,
431
+ driver: parsed.data
432
+ } : {
433
+ ok: false,
434
+ error: `--${retiredDriverHint(value) ?? `driver must be one of ${DRIVER_IDS.join(", ")}, got \`${value}\``}`
435
+ };
436
+ };
437
+ // src/commands/walk-report.ts
438
+ /**
439
+ * @file How `ork validate` shows a walk (RFC-009): one line on what it tried,
440
+ * and with `--paths`, each way a run can end. A gate's answer that ends the
441
+ * run shows here as an ending, not as a problem.
442
+ */
443
+ /** The walk's one line; unless `--paths` is on, it points there for each ending. */
444
+ const walkLine = (walk, showsPaths = false) => walk === void 0 ? [] : [style("dim", showsPaths ? walkSummary(walk) : `${walkSummary(walk)} See each with \`ork validate --paths\`.`)];
445
+ const endingLine = ({ status, notRun, paths }) => {
446
+ const ways = `${String(paths)} ${paths === 1 ? "way" : "ways"}`;
447
+ const word = status === "succeeded" ? style("green", status) : style("red", status);
448
+ const rest = notRun.length === 0 ? "every node ran" : `didn't run: ${notRun.map((id) => plain(id)).join(", ")}`;
449
+ return ` ${word} ${style("dim", `(${ways})`)} ${rest}`;
450
+ };
451
+ const endingLines = (ending) => [endingLine(ending), ...ending.choices.map((choice) => style("dim", ` ${plain(choice)}`))];
452
+ /** Each ending, the ones that succeeded first, with one way to get there. */
453
+ const pathLines = (walk) => {
454
+ const endings = walk?.endings ?? [];
455
+ return [...endings.filter(({ status }) => status === "succeeded"), ...endings.filter(({ status }) => status !== "succeeded")].flatMap((ending) => endingLines(ending));
456
+ };
457
+ // src/commands/validate-report.ts
458
+ const plural = (count, word) => `${count} ${word}${count === 1 ? "" : "s"}`;
459
+ const diagnosticLine = (file, d, term) => {
460
+ const at = d.file ?? file;
461
+ const where = d.line === void 0 ? at : `${at}:${d.line}:${d.column ?? 1}`;
462
+ if (term.rich) return [
463
+ `${d.severity === "error" ? chip(term, CHIP_LABELS.error, "danger") : chip(term, CHIP_LABELS.warn, "warn")} ${paint(term, "muted", cut(where, term.width - 9))}`,
464
+ ...wrap(d.message, term.width - 2).map((line) => ` ${line}`),
465
+ ` ${paint(term, "muted", `(${d.code})`)}`
466
+ ];
467
+ const severity = d.severity === "error" ? style(["red", "bold"], "error") : style(["yellow", "bold"], "warning");
468
+ return [`${style("dim", where)} ${severity} ${d.message} ${style("dim", `(${d.code})`)}`];
469
+ };
470
+ /** The warnings alone, for a run that goes ahead despite them. */
471
+ const warningLines = (report, term = termOf(process.stderr)) => report.diagnostics.filter((d) => d.severity === "warning").flatMap((d) => diagnosticLine(report.file, d, term));
472
+ /** The verdict line: `OK` or `FAIL` chip when rich, else `✓` or `✗`. */
473
+ const verdictLine = (term, ok, file, note) => {
474
+ const mark = term.rich ? chip(term, ok ? CHIP_LABELS.ok : CHIP_LABELS.fail, ok ? "ok" : "danger") : style(ok ? "green" : "red", ok ? "✓" : "✗");
475
+ const room = term.rich ? Math.max(8, term.width - widthOf(file) - 8) : Infinity;
476
+ return `${mark} ${file}${note === "" ? "" : ` ${style("dim", cut(note, room))}`}`;
477
+ };
478
+ /** Human-readable report, one line per diagnostic, compiler style. */
479
+ const formatReport = (report, showsPaths = false, term = termOf(process.stdout)) => {
480
+ const lines = report.diagnostics.flatMap((d) => diagnosticLine(report.file, d, term));
481
+ const errors = report.diagnostics.filter((d) => d.severity === "error").length;
482
+ const warnings = report.diagnostics.length - errors;
483
+ if (report.summary === void 0 && errors === 0) lines.push(verdictLine(term, true, report.file, warnings === 0 ? "" : `(${plural(warnings, "warning")})`));
484
+ else if (report.summary !== void 0 && errors === 0) {
485
+ const { nodes, edges, entries } = report.summary;
486
+ const extra = warnings > 0 ? `, ${plural(warnings, "warning")}` : "";
487
+ lines.push(verdictLine(term, true, report.file, `(${plural(nodes, "node")}, ${plural(edges, "edge")}, starts at ${entries.join(", ")}${extra})`));
488
+ } else lines.push(verdictLine(term, false, report.file, `(${plural(errors, "error")}, ${plural(warnings, "warning")})`));
489
+ return [...lines, ...walkLine(report.walk, showsPaths)];
490
+ };
491
+ /** A report's lines, and with `showsPaths` each path the walk found. */
492
+ const reportLines = (report, showsPaths) => [...formatReport(report, showsPaths), ...showsPaths ? pathLines(report.walk) : []];
493
+ // src/commands/validate.ts
494
+ /**
495
+ * @file `ork validate`: loads, parses and compiles workflow files with the
496
+ * project's rules (`.ork/project.yaml`) and the agents they name, checks
497
+ * their skill and context references, and reports diagnostics compiler-style,
498
+ * with file, line and column. `loadWorkflow` is shared with `ork run`.
499
+ */
500
+ const hasErrors = (diagnostics) => diagnostics.some((d) => d.severity === "error");
501
+ /**
502
+ * Parses and compiles one workflow file, with the agents the user and its
503
+ * project offer. `home` is the user's `~` (tests pass a folder); `registered`
504
+ * the projects a caller's store has, else those of the store in `home`.
505
+ */
506
+ const loadWorkflow = (file, cwd, home, registered = registeredProjects(home)) => {
507
+ const absolute = path.resolve(cwd, file);
508
+ const place = placeFor(path.dirname(absolute), registered);
509
+ const where = {
510
+ place,
511
+ workflowDir: workflowDirOf(place, absolute)
512
+ };
513
+ let text;
514
+ try {
515
+ text = readFileSync(path.resolve(cwd, file), "utf8");
516
+ } catch (error) {
517
+ return {
518
+ report: {
519
+ file,
520
+ diagnostics: [{
521
+ severity: "error",
522
+ code: "file.read",
523
+ message: error instanceof Error && "code" in error && error.code === "ENOENT" ? missingWorkflowText(file, placeFiles(place).workflows) : "cannot read file",
524
+ path: []
525
+ }]
526
+ },
527
+ workflow: null,
528
+ graph: null,
529
+ ...where
530
+ };
531
+ }
532
+ const { diagnostics, workflow, graph, summary, locate } = checkWorkflowText(text, {
533
+ ...where,
534
+ registered,
535
+ home
536
+ });
537
+ return {
538
+ report: {
539
+ file,
540
+ diagnostics,
541
+ ...summary === void 0 ? {} : { summary }
542
+ },
543
+ workflow,
544
+ graph,
545
+ textHash: workflowTextHash(text),
546
+ ...locate === void 0 ? {} : { locate },
547
+ ...where
548
+ };
549
+ };
550
+ const validateFile = (file, cwd) => loadWorkflow(file, cwd).report;
551
+ /**
552
+ * Walks what loaded, with its findings on its report. `ork validate` and `ork
553
+ * run` do this; the rest of what loads a workflow doesn't pay for a walk.
554
+ */
555
+ const walkLoaded = async (loaded) => {
556
+ const { diagnostics, walk } = await walkChecks({
557
+ diagnostics: loaded.report.diagnostics,
558
+ graph: loaded.graph,
559
+ ...loaded.locate === void 0 ? {} : { locate: loaded.locate }
560
+ });
561
+ return {
562
+ ...loaded,
563
+ report: {
564
+ ...loaded.report,
565
+ diagnostics,
566
+ ...walk === void 0 ? {} : { walk }
567
+ }
568
+ };
569
+ };
570
+ const validate = defineCommand({
571
+ meta: {
572
+ name: "validate",
573
+ description: "Check workflow files: schema, ownership tree, edges, loops and references, and walk every way through them"
574
+ },
575
+ args: {
576
+ files: {
577
+ type: "positional",
578
+ description: "Workflow files or names in .ork/workflows/ (default: every workflow there, and the project's triggers)",
579
+ required: false
580
+ },
581
+ json: {
582
+ type: "boolean",
583
+ description: "Print the reports as JSON",
584
+ default: false
585
+ },
586
+ paths: {
587
+ type: "boolean",
588
+ description: "List each way a run can end, and the choices that lead there",
589
+ default: false
590
+ },
591
+ driver: {
592
+ type: "string",
593
+ description: "Check the nodes against this driver (default: the one a run would take, the one ready here)"
594
+ }
595
+ },
596
+ run: async ({ args }) => {
597
+ const driver = driverArg(args.driver);
598
+ if (!driver.ok) {
599
+ writeErr(driver.error);
600
+ process.exitCode = 1;
601
+ return;
602
+ }
603
+ const cwd = process.cwd();
604
+ if (refusedBuiltin(args._, cwd)) return;
605
+ const files = findWorkflowFiles(args._, cwd, placeFiles(projectAt(cwd)).workflows);
606
+ if (files.length === 0) {
607
+ write("No workflow files found. Run `ork init`, or pass a path.");
608
+ process.exitCode = 1;
609
+ return;
610
+ }
611
+ const place = projectAt(cwd);
612
+ const triggers = args._.length === 0 ? readProjectTriggers(place, void 0, registeredProjects()) : void 0;
613
+ const walked = [];
614
+ for (const file of files) {
615
+ const loaded = await walkLoaded(loadWorkflow(file, cwd));
616
+ const { report } = loaded;
617
+ if (loaded.workflow !== null) report.diagnostics.push(...await driverDiagnostics(loaded.workflow, {
618
+ override: driver.driver,
619
+ locate: loaded.locate
620
+ }));
621
+ walked.push(report);
622
+ }
623
+ const reports = [...walked, ...triggers?.present === true ? [{
624
+ file: path.relative(cwd, triggers.file),
625
+ diagnostics: [...triggers.diagnostics, ...triggers.triggers.flatMap((item) => item.diagnostics)]
626
+ }] : []];
627
+ if (args.json) write(JSON.stringify(reports, void 0, 2));
628
+ else for (const report of reports) for (const line of reportLines(report, args.paths)) write(line);
629
+ if (reports.some((r) => hasErrors(r.diagnostics))) process.exitCode = 1;
630
+ }
631
+ });
632
+ export { startUpdateCheck as C, showUpdateNotice as S, runChecks as _, formatReport as a, UPDATE_CHILD_ENV as b, checksHere as c, runReadyDriver as d, unfitLines as f, orkCheck as g, headWarning as h, walkLoaded as i, chooseReadyDriver as l, experimentsCheck as m, validate as n, warningLines as o, exec as p, validateFile as r, budgetWarningLines as s, loadWorkflow as t, overrideRefusal as u, socketCheck as v, updateCheckOff as w, runUpdateChild as x, triggerChecks as y };