@krapht/ork 1.0.0-alpha.0 → 1.0.0-alpha.10

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 (41) hide show
  1. package/README.md +5 -6
  2. package/dist/{__build_info__-DdTVd2p5.mjs → __build_info__-DFF18K8y.mjs} +72 -3
  3. package/dist/bin.mjs +603 -384
  4. package/dist/{clean-NnboOuz6.mjs → clean-DVaEH9e7.mjs} +5 -2
  5. package/dist/{completion-snnYGjgK.mjs → completion-Dv5kf598.mjs} +6 -6
  6. package/dist/context-CEaKOVmG.mjs +683 -0
  7. package/dist/{decide-B81wCZQg.mjs → decide-3l9q8UtX.mjs} +3 -3
  8. package/dist/{find-BKW-uGey.mjs → find-DEjjOlTG.mjs} +1 -1
  9. package/dist/format-Cb5_m-Qd.mjs +191 -0
  10. package/dist/{in-a-run-BPXX09Bs.mjs → in-a-run-BS9Nc-JW.mjs} +1 -1
  11. package/dist/{limits-JCve2QpI.mjs → limits-BSwEjQST.mjs} +1 -1
  12. package/dist/{list-C7UEgmtd.mjs → list-Qxr6rwsY.mjs} +11 -4
  13. package/dist/{mcp-CKPyNaKD.mjs → mcp-CslnG5wf.mjs} +6 -6
  14. package/dist/output-Bqljtmp_.mjs +40 -0
  15. package/dist/{pause-B_SL_yDz.mjs → pause-D7OVB5Ac.mjs} +4 -4
  16. package/dist/{relay-o-Luee1c.mjs → relay-EceRIebO.mjs} +2 -2
  17. package/dist/{remove-DAZ7ZCzt.mjs → remove-0IQZVWmI.mjs} +14 -7
  18. package/dist/{send-3B64fBNB.mjs → send-C0uwfUYi.mjs} +3 -3
  19. package/dist/{src-DHvWU4tX.mjs → src-Df6KOIDC.mjs} +5682 -3248
  20. package/dist/{src-C38BPzit.mjs → src-cmmXKHn1.mjs} +266 -203
  21. package/dist/{src-BJTegEzY.mjs → src-i5k2Y-kf.mjs} +40 -88
  22. package/dist/src-oaJYIddp.mjs +2255 -0
  23. package/dist/ui/THIRD_PARTY_LICENSES.txt +594 -0
  24. package/dist/ui/assets/app-C8uhHVYp.css +1 -0
  25. package/dist/ui/assets/app-qs8Lofv6.js +75 -0
  26. package/dist/ui/assets/{index-D83F2OKZ.js → index-DV1ZaOk-.js} +3 -3
  27. package/dist/ui/assets/theme-CdXvqTQ6.js +1 -0
  28. package/dist/ui/index.html +2 -2
  29. package/dist/{ui-CY4xxJUu.mjs → ui-CBkHJdqk.mjs} +4 -4
  30. package/dist/{usage-CJnytoe4.mjs → usage-Cd8pEYyK.mjs} +5 -5
  31. package/dist/{watch-BJB82ZJ7.mjs → watch-DW5R2OPO.mjs} +6 -6
  32. package/dist/{workflows-CiM7S3l0.mjs → workflows-BT1gDRIV.mjs} +4 -3
  33. package/dist/{worktrees-86ySXxR6.mjs → worktrees-g1MQMGie.mjs} +78 -19
  34. package/package.json +2 -4
  35. package/dist/context-DO3sBy9v.mjs +0 -274
  36. package/dist/format-CmTlTOjT.mjs +0 -427
  37. package/dist/output-B1V6ZJSx.mjs +0 -950
  38. package/dist/src-Dyz_Gy1q.mjs +0 -1183
  39. package/dist/ui/assets/app-C8nJ6NpT.js +0 -67
  40. package/dist/ui/assets/app-CH7YC3in.css +0 -1
  41. package/dist/ui/assets/theme-3xH5jxKs.js +0 -1
@@ -0,0 +1,683 @@
1
+ import { C as workflowTextHash, D as openStore, Mn as LESSON_CAP_TOKENS, O as storePath, R as driverRefusals, Rt as loadProject, S as readProjectTriggers, Tn as placeFor, _ as lateTriggers, _r as EXPERIMENTS, dr as readyDriver, gr as firstLine, hr as DRIVER_IDS, ht as walkSummary, ir as retiredDriverHint, kn as workflowDirOf, lt as namingNoDriver, mr as AGENT_DRIVERS, mt as walkChecks, nr as DriverSchema, on as socketRoom, or as INTEGRATIONS, pt as checkWorkflowText, st as explainSession, ur as driverChecks, v as triggersReport, vr as experimentsOn, wn as placeFiles, yr as knownExperiments, z as driverQueues } from "./src-Df6KOIDC.mjs";
2
+ import { c as checkDue, d as pendingNotice, f as readUpdateCache, h as writeUpdateCache, i as registryOf, m as updateCachePath, n as ORK_VERSION, o as updateSwitchOff, p as refreshUpdateCache, r as isSwitchedOn, t as ORK_NAME, u as newerVersion } from "./__build_info__-DFF18K8y.mjs";
3
+ import { a as write, i as style, o as writeErr, r as plain } from "./output-Bqljtmp_.mjs";
4
+ import { c as registeredProjects, i as refusedBuiltin, o as workflowArg, r as findWorkflowFiles, s as projectAt } from "./workflows-BT1gDRIV.mjs";
5
+ import path from "node:path";
6
+ import { readFileSync } from "node:fs";
7
+ import { execFile, spawn } from "node:child_process";
8
+ import { defineCommand } from "citty";
9
+ import { styleText } from "node:util";
10
+ // src/update/notify.ts
11
+ /**
12
+ * @file The new-version notice (ADR-038). At most once a day a command starts a
13
+ * detached copy of `ork` that asks the npm registry for the latest version
14
+ * and records it in `~/.ork/update-check.json`, so no command waits on the
15
+ * network; a later command, when a newer version is known, ends with one line
16
+ * on stderr (once a day). Off in CI, without a terminal, with `--json`, and
17
+ * with `ORK_NO_UPDATE_CHECK=1` or `NO_UPDATE_NOTIFIER=1`.
18
+ */
19
+ /** Set in the background copy of `ork` that only runs the check. */
20
+ const UPDATE_CHILD_ENV = "ORK_UPDATE_CHECK_CHILD";
21
+ /**
22
+ * Why the automatic check is off for this command, or `null` when it is on: the
23
+ * environment's switches (core's, which `ork ui` reads too), then where a
24
+ * notice on stderr would go.
25
+ */
26
+ const updateCheckOff = (env, argv, terminal) => {
27
+ const switched = updateSwitchOff(env);
28
+ if (switched !== null) return switched === "CI" ? "in CI" : "turned off";
29
+ if (isSwitchedOn(env["ORK_DETACHED"]) || !terminal) return "no terminal";
30
+ if (argv.includes("--json")) return "JSON output";
31
+ if (argv[0] === "upgrade") return "ork upgrade checks for itself";
32
+ if (argv[0] === "ui" && argv.includes("--serve")) return "ork ui's server checks for itself";
33
+ return null;
34
+ };
35
+ /** The notice's text. */
36
+ const updateNotice = (latest, current) => `Ork ${latest} is out (you have ${current}). Upgrade with: ork upgrade`;
37
+ /**
38
+ * Starts the background check when one is due. Records the attempt first, so
39
+ * commands started in the next moments do not start one too.
40
+ */
41
+ const startUpdateCheck = (file = updateCachePath(), now = /* @__PURE__ */ new Date()) => {
42
+ try {
43
+ const cache = readUpdateCache(file);
44
+ if (!checkDue(cache, now)) return;
45
+ writeUpdateCache(file, {
46
+ ...cache,
47
+ checkedAt: now.toISOString()
48
+ });
49
+ const child = spawn(process.execPath, [...process.execArgv, process.argv[1] ?? ""], {
50
+ detached: true,
51
+ stdio: "ignore",
52
+ env: {
53
+ ...process.env,
54
+ [UPDATE_CHILD_ENV]: "1"
55
+ }
56
+ });
57
+ child.on("error", () => {});
58
+ child.unref();
59
+ } catch {}
60
+ };
61
+ /** The background copy's whole job: ask the registry, record the answer. */
62
+ const runUpdateChild = async () => {
63
+ const registry = registryOf(process.env);
64
+ await refreshUpdateCache(updateCachePath(), ORK_NAME, /* @__PURE__ */ new Date(), {
65
+ current: ORK_VERSION,
66
+ ...registry === void 0 ? {} : { registry }
67
+ });
68
+ };
69
+ /** Prints the notice when a newer version is known and not told today. */
70
+ const showUpdateNotice = (file = updateCachePath(), now = /* @__PURE__ */ new Date()) => {
71
+ try {
72
+ const cache = readUpdateCache(file);
73
+ const latest = pendingNotice(cache, ORK_VERSION, now);
74
+ if (cache === null || latest === null) return;
75
+ const text = updateNotice(latest, ORK_VERSION);
76
+ process.stderr.write(`\n${styleText("yellow", text, { stream: process.stderr })}\n`);
77
+ writeUpdateCache(file, {
78
+ ...cache,
79
+ notified: {
80
+ version: latest,
81
+ at: now.toISOString()
82
+ }
83
+ });
84
+ } catch {}
85
+ };
86
+ // src/update/upgrade.ts
87
+ /**
88
+ * @file `ork doctor`'s line for Ork itself (ADR-038): the running version
89
+ * against the last update check, read from the record without asking the
90
+ * network. What `ork upgrade` does is core's (`planUpgrade`, `runInstall`),
91
+ * shared with the server behind `ork ui`.
92
+ */
93
+ /** `ork doctor`'s line for Ork itself, from the last check. */
94
+ const versionCheck = (current, cache, checksOff) => {
95
+ const newer = newerVersion(cache, current);
96
+ if (newer !== null) return {
97
+ name: "ork",
98
+ status: "warn",
99
+ detail: `${current}; ${newer} is out`,
100
+ hint: "Run ork upgrade."
101
+ };
102
+ return {
103
+ name: "ork",
104
+ status: "ok",
105
+ 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)}`})`
106
+ };
107
+ };
108
+ // src/doctor/checks.ts
109
+ /**
110
+ * @file The checks behind `ork doctor`: Ork's own version against the last
111
+ * update check, each tool's presence and login status (the drivers' own
112
+ * checks, which `ork run` also reads to pick the one ready), and whether a
113
+ * run's socket path fits, and which experiments are on, through an injectable
114
+ * `exec` so tests need no real tools. Never prints a token or an account.
115
+ */
116
+ const TIMEOUT_MS = 15e3;
117
+ const exec = (program, args) => new Promise((resolve) => {
118
+ execFile(program, args, {
119
+ timeout: TIMEOUT_MS,
120
+ encoding: "utf8"
121
+ }, (error, stdout) => {
122
+ resolve({
123
+ ok: error === null,
124
+ stdout
125
+ });
126
+ });
127
+ });
128
+ /** Ork's own version, against the last update check (no network). */
129
+ const orkCheck = () => versionCheck(ORK_VERSION, readUpdateCache(updateCachePath()), updateCheckOff(process.env, [], process.stderr.isTTY));
130
+ const MIN_NODE = [24, 20];
131
+ const checkNode = (version) => {
132
+ const [major = 0, minor = 0] = version.split(".").map(Number);
133
+ const [minMajor, minMinor] = MIN_NODE;
134
+ return major > minMajor || major === minMajor && minor >= minMinor ? {
135
+ name: "node",
136
+ status: "ok",
137
+ detail: `v${version}`
138
+ } : {
139
+ name: "node",
140
+ status: "fail",
141
+ detail: `v${version}`,
142
+ hint: `Ork needs Node ${MIN_NODE.join(".")} or later.`
143
+ };
144
+ };
145
+ /** A work tree with no commit has no `HEAD` for a worktree to branch from. */
146
+ const checkHead = async (run) => {
147
+ return (await run("git", [
148
+ "rev-parse",
149
+ "--verify",
150
+ "HEAD"
151
+ ])).ok ? {
152
+ name: "git commits",
153
+ status: "ok",
154
+ detail: "HEAD exists"
155
+ } : {
156
+ name: "git commits",
157
+ status: "warn",
158
+ detail: "no commit yet",
159
+ hint: "Make a first commit: worktree runs branch from HEAD."
160
+ };
161
+ };
162
+ const checkGit = async (run) => {
163
+ const version = await run("git", ["--version"]);
164
+ if (!version.ok) return [{
165
+ name: "git",
166
+ status: "fail",
167
+ detail: "not found",
168
+ hint: "Install git; Ork gives each writing node its own worktree."
169
+ }];
170
+ const inside = await run("git", ["rev-parse", "--is-inside-work-tree"]);
171
+ const head = inside.ok ? await checkHead(run) : null;
172
+ return [
173
+ {
174
+ name: "git",
175
+ status: "ok",
176
+ detail: firstLine(version.stdout)
177
+ },
178
+ inside.ok ? {
179
+ name: "git repo",
180
+ status: "ok",
181
+ detail: "inside a work tree"
182
+ } : {
183
+ name: "git repo",
184
+ status: "warn",
185
+ detail: "not a git repository",
186
+ hint: "Run Ork inside a git repository so nodes can use worktrees."
187
+ },
188
+ ...head === null ? [] : [head]
189
+ ];
190
+ };
191
+ /** What `ork init` warns of: a work tree with no commit, else null. */
192
+ const headWarning = async (run) => {
193
+ const head = (await checkGit(run)).find((r) => r.name === "git commits");
194
+ return head?.status === "warn" ? `${head.detail}. ${head.hint ?? ""}`.trim() : null;
195
+ };
196
+ /**
197
+ * Runs every check: Node and git, then every registered agent driver and
198
+ * integration (ADR-022). Node, git, and at least one driver are required;
199
+ * integrations are optional.
200
+ */
201
+ const runChecks = async (run, env, nodeVersion) => {
202
+ const [git, checked, tools] = await Promise.all([
203
+ checkGit(run),
204
+ driverChecks(run, env),
205
+ Promise.all(INTEGRATIONS.flatMap((i) => i.doctor === void 0 ? [] : [i.doctor(run, env)]))
206
+ ]);
207
+ const drivers = checked.map(({ result }) => result);
208
+ const results = [
209
+ checkNode(nodeVersion),
210
+ ...git,
211
+ ...drivers,
212
+ ...tools
213
+ ];
214
+ return {
215
+ results,
216
+ ok: drivers.some((d) => d.status === "ok") && !results.some((r) => r.status === "fail")
217
+ };
218
+ };
219
+ /**
220
+ * Whether a run's socket fits under `~` (L38). Past the limit a run can't
221
+ * listen, and nothing else says so: answers, commands and followers fall back
222
+ * to the store, a second at a time. `home` is the user's `~`.
223
+ */
224
+ const socketCheck = (home, platform) => {
225
+ const { bytes, max } = socketRoom(home, platform);
226
+ if (max === void 0 || bytes <= max) return {
227
+ name: "sockets",
228
+ status: "ok",
229
+ detail: max === void 0 ? "not Unix sockets here" : `${String(bytes)} of ${String(max)} bytes`
230
+ };
231
+ return {
232
+ name: "sockets",
233
+ status: "warn",
234
+ detail: `a run's socket path is ${String(bytes)} bytes, past the ${String(max)} this system allows`,
235
+ 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."
236
+ };
237
+ };
238
+ /**
239
+ * What it says of triggers (RFC-011): a long-lived process can die quietly, so
240
+ * a live trigger that hasn't looked when it should have is worth saying. `home`
241
+ * is the user's `~` (tests pass a folder).
242
+ */
243
+ const triggerChecks = (home) => {
244
+ const store = openStore(storePath(home));
245
+ try {
246
+ const report = triggersReport(store, store.projects(), home);
247
+ const broken = report.broken.map(({ file, message }) => ({
248
+ name: "triggers",
249
+ status: "warn",
250
+ detail: `${file}: ${message}`,
251
+ hint: "Its triggers are off until it can be read."
252
+ }));
253
+ const live = report.triggers.filter(({ standing }) => standing === "live");
254
+ if (live.length === 0) return broken;
255
+ const late = lateTriggers(report, /* @__PURE__ */ new Date());
256
+ if (late.length === 0) return [...broken, {
257
+ name: "ork watch",
258
+ status: "ok",
259
+ detail: `${String(live.length)} trigger(s) looking`
260
+ }];
261
+ return [...broken, {
262
+ name: "ork watch",
263
+ status: "warn",
264
+ detail: `not looking: ${late.map(({ trigger }) => trigger).join(", ")}`,
265
+ hint: report.pid === void 0 ? "Start it with `ork watch --detach`." : "See `ork watch --status`."
266
+ }];
267
+ } finally {
268
+ store.close();
269
+ }
270
+ };
271
+ const SOURCE_NAMES = {
272
+ project: "project file",
273
+ environment: "environment"
274
+ };
275
+ /**
276
+ * Which experiments are on and who turned them on; an unknown name fails this
277
+ * check alone. Prints names only. Outside a project, only the environment
278
+ * counts.
279
+ */
280
+ const experimentsCheck = (place, env = process.env, registry = EXPERIMENTS) => {
281
+ const { project, where } = loadProject(place.root, placeFiles(place).rules);
282
+ const state = experimentsOn({
283
+ env,
284
+ project,
285
+ where,
286
+ registry
287
+ });
288
+ if (!state.ok) return {
289
+ name: "experiments",
290
+ status: "fail",
291
+ detail: `unknown: ${state.unknown.map((u) => `\`${u.name}\` (${u.where})`).join(", ")}`,
292
+ hint: `Fix or remove the name. Known experiments: ${knownExperiments(registry)}.`
293
+ };
294
+ return {
295
+ name: "experiments",
296
+ status: "ok",
297
+ detail: state.on.length === 0 ? "none on" : state.on.map(({ name, sources }) => `${name} (${sources.map((s) => SOURCE_NAMES[s]).join(" and ")})`).join(", ")
298
+ };
299
+ };
300
+ // src/run/ready-driver.ts
301
+ /**
302
+ * @file The driver a run takes for the session nodes that name none (core's
303
+ * `driver.ts` has the whole rule): the one `ork doctor`'s own checks find
304
+ * ready on this machine, said in one line. With none ready, the run stops
305
+ * before any session starts, with the hints `ork doctor` gives. A resumed run
306
+ * keeps the driver it recorded.
307
+ */
308
+ /** The real checks: the tools on this machine and its environment. */
309
+ const checksHere = () => driverChecks(exec, process.env);
310
+ const subject = (names, all) => {
311
+ if (all) return "No driver is named, so the run uses";
312
+ const [only] = names;
313
+ return names.length === 1 && only !== void 0 ? `${only} names no driver, so it uses` : `${names.join(", ")} name no driver, so they use`;
314
+ };
315
+ /**
316
+ * The driver for `names`, the session nodes that name none, from `checks`.
317
+ * `all` says whether they are every session node of the run.
318
+ */
319
+ const chooseReadyDriver = (names, all, checks) => {
320
+ const ready = readyDriver(checks);
321
+ if (!ready.ok) return {
322
+ ok: false,
323
+ lines: [
324
+ `No agent driver is ready here, and ${names.join(", ")} ${names.length === 1 ? "names" : "name"} none.`,
325
+ ...ready.hints,
326
+ "See `ork doctor`."
327
+ ]
328
+ };
329
+ const why = ready.ready === 1 ? `the one ready here (${ready.detail})` : `more than one is ready, and a run takes ${ready.driver} first`;
330
+ return {
331
+ ok: true,
332
+ driver: ready.driver,
333
+ line: `${subject(names, all)} the ${ready.driver} driver: ${why}.`
334
+ };
335
+ };
336
+ /**
337
+ * What a run takes for its session nodes that name no driver: nothing when
338
+ * `--driver` overrides them all or each names one; the driver a resumed run
339
+ * recorded; otherwise the one ready here.
340
+ */
341
+ const runReadyDriver = async (workflow, options) => {
342
+ if (options.override !== void 0) return { ok: true };
343
+ const names = namingNoDriver(workflow);
344
+ if (names.length === 0) return { ok: true };
345
+ if (options.recorded !== void 0) return {
346
+ ok: true,
347
+ driver: options.recorded
348
+ };
349
+ const sessions = Object.values(workflow.nodes).filter((node) => node.type === "claude.leader" || node.type === "claude.worker").length;
350
+ return chooseReadyDriver(names, names.length === sessions, await (options.checks ?? checksHere)());
351
+ };
352
+ /**
353
+ * What stops a session node from running on its driver, one line each: the
354
+ * driver's declared `capabilities` and `proven` against what the node asks
355
+ * (`drivers` default: the real descriptors; tests pass their own).
356
+ */
357
+ const unfitLines = (workflow, drivers, choice) => driverRefusals(workflow, drivers ?? AGENT_DRIVERS, choice).map(({ message }) => message);
358
+ // src/run/driver-check.ts
359
+ /**
360
+ * @file `ork validate`'s driver check: each session node against what its
361
+ * driver declares (core's `driver-needs.ts`), for `--driver` if given, else
362
+ * the driver a run would take, the one ready here (`ready-driver.ts`).
363
+ */
364
+ const driverDiagnostics = async (workflow, options) => {
365
+ const ready = options.override === void 0 && namingNoDriver(workflow).length > 0 ? readyDriver(await (options.checks ?? checksHere)()) : void 0;
366
+ const drivers = options.drivers ?? AGENT_DRIVERS;
367
+ const choice = {
368
+ override: options.override,
369
+ ready: ready?.ok === true ? ready.driver : void 0
370
+ };
371
+ const at = (node, severity, code, message) => {
372
+ const path = ["nodes", node];
373
+ const found = {
374
+ severity,
375
+ code,
376
+ message,
377
+ path
378
+ };
379
+ const place = options.locate?.(path);
380
+ if (place !== null && place !== void 0) {
381
+ found.line = place.line;
382
+ found.column = place.column;
383
+ }
384
+ return found;
385
+ };
386
+ return [...driverRefusals(workflow, drivers, choice).map(({ node, message }) => at(node, "error", "driver.unfit", message)), ...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)))];
387
+ };
388
+ /** The `--driver` value, or why it is not a driver. */
389
+ const driverArg = (value) => {
390
+ if (value === void 0) return { ok: true };
391
+ const parsed = DriverSchema.safeParse(value);
392
+ return parsed.success ? {
393
+ ok: true,
394
+ driver: parsed.data
395
+ } : {
396
+ ok: false,
397
+ error: `--${retiredDriverHint(value) ?? `driver must be one of ${DRIVER_IDS.join(", ")}, got \`${value}\``}`
398
+ };
399
+ };
400
+ // src/commands/walk-report.ts
401
+ /**
402
+ * @file How `ork validate` shows a walk (RFC-009): one line on what it tried,
403
+ * and with `--paths`, each way a run can end. A gate's answer that ends the
404
+ * run shows here as an ending, not as a problem.
405
+ */
406
+ /** The walk's one line; unless `--paths` is on, it points there for each ending. */
407
+ const walkLine = (walk, showsPaths = false) => walk === void 0 ? [] : [style("dim", showsPaths ? walkSummary(walk) : `${walkSummary(walk)} See each with \`ork validate --paths\`.`)];
408
+ const endingLine = ({ status, notRun, paths }) => {
409
+ const ways = `${String(paths)} ${paths === 1 ? "way" : "ways"}`;
410
+ const word = status === "succeeded" ? style("green", status) : style("red", status);
411
+ const rest = notRun.length === 0 ? "every node ran" : `didn't run: ${notRun.map((id) => plain(id)).join(", ")}`;
412
+ return ` ${word} ${style("dim", `(${ways})`)} ${rest}`;
413
+ };
414
+ const endingLines = (ending) => [endingLine(ending), ...ending.choices.map((choice) => style("dim", ` ${plain(choice)}`))];
415
+ /** Each ending, the ones that succeeded first, with one way to get there. */
416
+ const pathLines = (walk) => {
417
+ const endings = walk?.endings ?? [];
418
+ return [...endings.filter(({ status }) => status === "succeeded"), ...endings.filter(({ status }) => status !== "succeeded")].flatMap((ending) => endingLines(ending));
419
+ };
420
+ // src/commands/validate.ts
421
+ /**
422
+ * @file `ork validate`: loads, parses and compiles workflow files with the
423
+ * project's rules (`.ork/project.yaml`) and the agents they name, checks
424
+ * their skill and context references, and reports diagnostics compiler-style,
425
+ * with file, line and column. `loadWorkflow` is shared with `ork run`.
426
+ */
427
+ const hasErrors = (diagnostics) => diagnostics.some((d) => d.severity === "error");
428
+ /**
429
+ * Parses and compiles one workflow file, with the agents the user and its
430
+ * project offer. `home` is the user's `~` (tests pass a folder); `registered`
431
+ * the projects a caller's store has, else those of the store in `home`.
432
+ */
433
+ const loadWorkflow = (file, cwd, home, registered = registeredProjects(home)) => {
434
+ const absolute = path.resolve(cwd, file);
435
+ const place = placeFor(path.dirname(absolute), registered);
436
+ const where = {
437
+ place,
438
+ workflowDir: workflowDirOf(place, absolute)
439
+ };
440
+ let text;
441
+ try {
442
+ text = readFileSync(path.resolve(cwd, file), "utf8");
443
+ } catch (error) {
444
+ return {
445
+ report: {
446
+ file,
447
+ diagnostics: [{
448
+ severity: "error",
449
+ code: "file.read",
450
+ message: error instanceof Error && "code" in error && error.code === "ENOENT" ? "file not found" : "cannot read file",
451
+ path: []
452
+ }]
453
+ },
454
+ workflow: null,
455
+ graph: null,
456
+ ...where
457
+ };
458
+ }
459
+ const { diagnostics, workflow, graph, summary, locate } = checkWorkflowText(text, {
460
+ ...where,
461
+ registered,
462
+ home
463
+ });
464
+ return {
465
+ report: {
466
+ file,
467
+ diagnostics,
468
+ ...summary === void 0 ? {} : { summary }
469
+ },
470
+ workflow,
471
+ graph,
472
+ textHash: workflowTextHash(text),
473
+ ...locate === void 0 ? {} : { locate },
474
+ ...where
475
+ };
476
+ };
477
+ const validateFile = (file, cwd) => loadWorkflow(file, cwd).report;
478
+ /**
479
+ * Walks what loaded, with its findings on its report. `ork validate` and `ork
480
+ * run` do this; the rest of what loads a workflow doesn't pay for a walk.
481
+ */
482
+ const walkLoaded = async (loaded) => {
483
+ const { diagnostics, walk } = await walkChecks({
484
+ diagnostics: loaded.report.diagnostics,
485
+ graph: loaded.graph,
486
+ ...loaded.locate === void 0 ? {} : { locate: loaded.locate }
487
+ });
488
+ return {
489
+ ...loaded,
490
+ report: {
491
+ ...loaded.report,
492
+ diagnostics,
493
+ ...walk === void 0 ? {} : { walk }
494
+ }
495
+ };
496
+ };
497
+ const plural = (count, word) => `${count} ${word}${count === 1 ? "" : "s"}`;
498
+ const diagnosticLine = (file, d) => {
499
+ const where = d.line === void 0 ? file : `${file}:${d.line}:${d.column ?? 1}`;
500
+ const severity = d.severity === "error" ? style(["red", "bold"], "error") : style(["yellow", "bold"], "warning");
501
+ return `${style("dim", where)} ${severity} ${d.message} ${style("dim", `(${d.code})`)}`;
502
+ };
503
+ /** The warnings alone, for a run that goes ahead despite them. */
504
+ const warningLines = (report) => report.diagnostics.filter((d) => d.severity === "warning").map((d) => diagnosticLine(report.file, d));
505
+ /** Human-readable report, one line per diagnostic, compiler style. */
506
+ const formatReport = (report, showsPaths = false) => {
507
+ const lines = report.diagnostics.map((d) => diagnosticLine(report.file, d));
508
+ const errors = report.diagnostics.filter((d) => d.severity === "error").length;
509
+ const warnings = report.diagnostics.length - errors;
510
+ if (report.summary === void 0 && errors === 0) lines.push(`${style("green", "✓")} ${report.file}${warnings === 0 ? "" : style("dim", ` (${plural(warnings, "warning")})`)}`);
511
+ else if (report.summary !== void 0 && errors === 0) {
512
+ const { nodes, edges, entries } = report.summary;
513
+ const extra = warnings > 0 ? `, ${plural(warnings, "warning")}` : "";
514
+ lines.push(`${style("green", "✓")} ${report.file} ${style("dim", `(${plural(nodes, "node")}, ${plural(edges, "edge")}, starts at ${entries.join(", ")}${extra})`)}`);
515
+ } else lines.push(`${style("red", "✗")} ${report.file} ${style("dim", `(${plural(errors, "error")}, ${plural(warnings, "warning")})`)}`);
516
+ return [...lines, ...walkLine(report.walk, showsPaths)];
517
+ };
518
+ const validate = defineCommand({
519
+ meta: {
520
+ name: "validate",
521
+ description: "Check workflow files: schema, ownership tree, edges, loops and references, and walk every way through them"
522
+ },
523
+ args: {
524
+ files: {
525
+ type: "positional",
526
+ description: "Workflow files or names in .ork/workflows/ (default: every workflow there, and the project's triggers)",
527
+ required: false
528
+ },
529
+ json: {
530
+ type: "boolean",
531
+ description: "Print the reports as JSON",
532
+ default: false
533
+ },
534
+ paths: {
535
+ type: "boolean",
536
+ description: "List each way a run can end, and the choices that lead there",
537
+ default: false
538
+ },
539
+ driver: {
540
+ type: "string",
541
+ description: "Check the nodes against this driver (default: the one a run would take, the one ready here)"
542
+ }
543
+ },
544
+ run: async ({ args }) => {
545
+ const driver = driverArg(args.driver);
546
+ if (!driver.ok) {
547
+ writeErr(driver.error);
548
+ process.exitCode = 1;
549
+ return;
550
+ }
551
+ const cwd = process.cwd();
552
+ if (refusedBuiltin(args._, cwd)) return;
553
+ const files = findWorkflowFiles(args._, cwd, placeFiles(projectAt(cwd)).workflows);
554
+ if (files.length === 0) {
555
+ write("No workflow files found. Run `ork init`, or pass a path.");
556
+ process.exitCode = 1;
557
+ return;
558
+ }
559
+ const place = projectAt(cwd);
560
+ const triggers = args._.length === 0 ? readProjectTriggers(place, void 0, registeredProjects()) : void 0;
561
+ const walked = [];
562
+ for (const file of files) {
563
+ const loaded = await walkLoaded(loadWorkflow(file, cwd));
564
+ const { report } = loaded;
565
+ if (loaded.workflow !== null) report.diagnostics.push(...await driverDiagnostics(loaded.workflow, {
566
+ override: driver.driver,
567
+ locate: loaded.locate
568
+ }));
569
+ walked.push(report);
570
+ }
571
+ const reports = [...walked, ...triggers?.present === true ? [{
572
+ file: path.relative(cwd, triggers.file),
573
+ diagnostics: [...triggers.diagnostics, ...triggers.triggers.flatMap((item) => item.diagnostics)]
574
+ }] : []];
575
+ if (args.json) write(JSON.stringify(reports, void 0, 2));
576
+ else for (const report of reports) {
577
+ for (const line of formatReport(report, args.paths)) write(line);
578
+ if (args.paths) for (const line of pathLines(report.walk)) write(line);
579
+ }
580
+ if (reports.some((r) => hasErrors(r.diagnostics))) process.exitCode = 1;
581
+ }
582
+ });
583
+ // src/commands/context.ts
584
+ /**
585
+ * @file `ork context explain <wf> <node>`: what one node's session would load,
586
+ * layer by layer, as core's `explainSession` plans it without writing
587
+ * anything (one of Ork's own `ork:` workflows is written into the project's
588
+ * run folder first, as `ork run` does): its project instructions, every skill
589
+ * with the layer it came from and whether it is live, shadowed or switched
590
+ * off, its context files, its practices, and estimated tokens.
591
+ */
592
+ /** Loads a workflow file and plans one node's session in it, in place. */
593
+ const explainNode = (file, nodeId, cwd, home) => {
594
+ const { report, workflow, graph, place, workflowDir } = loadWorkflow(file, cwd, home);
595
+ if (workflow === null) return {
596
+ ok: false,
597
+ lines: formatReport(report)
598
+ };
599
+ const result = explainSession(workflow, graph, nodeId, {
600
+ root: place.root,
601
+ orkDir: place.dir,
602
+ workflowDir,
603
+ ...home === void 0 ? {} : { home }
604
+ });
605
+ return result.ok ? result : {
606
+ ok: false,
607
+ lines: [result.error]
608
+ };
609
+ };
610
+ const approx = (count) => style("dim", `≈${count.toLocaleString("en")} tokens`);
611
+ const formatExplanation = (e) => {
612
+ const width = Math.max(8, ...e.skills.map((s) => s.name.length));
613
+ const sourceWidth = Math.max(0, ...e.skills.map((s) => s.source.length));
614
+ const lines = [
615
+ `${style("bold", e.node)} ${style("dim", `(${e.type}) in ${e.workflow}`)}${e.template === void 0 ? "" : style("dim", `, planned as ${e.template}, the node it was spawned from`)}`,
616
+ "",
617
+ ...e.agent === void 0 ? [] : [`${style("bold", "Agent")} ${e.agent.name} ${style("dim", `(${e.agent.where})`)} ${approx(e.agent.tokens)}`],
618
+ `${style("bold", "Instructions")} ${e.instructions}`,
619
+ "",
620
+ style("bold", "Skills")
621
+ ];
622
+ if (e.skills.length === 0) lines.push(style("dim", " none"));
623
+ for (const s of e.skills) {
624
+ const mark = s.state === "live" ? style("green", "✓") : style("dim", "−");
625
+ const state = s.why === void 0 ? approx(s.tokens).replace("tokens", "tokens when used") : style("yellow", `off: ${s.why}`);
626
+ lines.push(` ${mark} ${s.name.padEnd(width)} ${s.layer.padEnd(8)} ${style("dim", s.source.padEnd(sourceWidth))} ${state}`);
627
+ }
628
+ lines.push("", style("bold", "Context files"));
629
+ if (e.files.length === 0) lines.push(style("dim", " none"));
630
+ for (const f of e.files) {
631
+ const cut = f.cutFrom === void 0 ? "" : ` ${style("yellow", `cut at the ${LESSON_CAP_TOKENS.toLocaleString("en")}-token cap; the whole file is ${approx(f.cutFrom)}`)}`;
632
+ lines.push(` ${f.layer.padEnd(8)} ${f.name} ${approx(f.tokens)}${cut}`);
633
+ }
634
+ lines.push("", style("bold", "Practices"));
635
+ for (const p of e.practices) lines.push(` ${p.name.padEnd(8)} ${approx(p.tokens)}`);
636
+ for (const name of e.skippedPractices) lines.push(` ${name.padEnd(8)} ${style("dim", "the project's own skill wins")}`);
637
+ if (e.practices.length === 0 && e.skippedPractices.length === 0) lines.push(style("dim", " off"));
638
+ lines.push("", `Appended to Claude Code's system prompt: ${approx(e.appendedTokens)}`);
639
+ return lines;
640
+ };
641
+ const explain = defineCommand({
642
+ meta: {
643
+ name: "explain",
644
+ description: "Show what one node's session loads, layer by layer"
645
+ },
646
+ args: {
647
+ workflow: {
648
+ type: "positional",
649
+ description: "A workflow file, the name of one in the project's workflows (.ork/workflows/), or one of Ork's own (ork:feedback-apply)",
650
+ required: true
651
+ },
652
+ node: {
653
+ type: "positional",
654
+ description: "Node id",
655
+ required: true
656
+ },
657
+ json: {
658
+ type: "boolean",
659
+ description: "Print JSON",
660
+ default: false
661
+ }
662
+ },
663
+ run: ({ args }) => {
664
+ const cwd = process.cwd();
665
+ if (refusedBuiltin([args.workflow], cwd)) return;
666
+ const result = explainNode(workflowArg(args.workflow, cwd).file, args.node, cwd);
667
+ if (!result.ok) {
668
+ for (const line of result.lines) process.stderr.write(`${line}\n`);
669
+ process.exitCode = 1;
670
+ return;
671
+ }
672
+ if (args.json) write(JSON.stringify(result.explanation, void 0, 2));
673
+ else for (const line of formatExplanation(result.explanation)) write(line);
674
+ }
675
+ });
676
+ const context = defineCommand({
677
+ meta: {
678
+ name: "context",
679
+ description: "Inspect the context Ork compiles for each node"
680
+ },
681
+ subCommands: { explain }
682
+ });
683
+ export { startUpdateCheck as C, showUpdateNotice as S, runChecks as _, validate as a, UPDATE_CHILD_ENV as b, warningLines as c, runReadyDriver as d, unfitLines as f, orkCheck as g, headWarning as h, loadWorkflow as i, checksHere as l, experimentsCheck as m, explainNode as n, validateFile as o, exec as p, formatReport as r, walkLoaded as s, context as t, chooseReadyDriver as u, socketCheck as v, updateCheckOff as w, runUpdateChild as x, triggerChecks as y };