@krapht/ork 1.0.0-alpha.1 → 1.0.0-alpha.11

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 (43) hide show
  1. package/README.md +3 -4
  2. package/dist/{__build_info__-D9lSlMFy.mjs → __build_info__-CIchF6nr.mjs} +2 -2
  3. package/dist/bin.mjs +1479 -600
  4. package/dist/chips-BA7SrrH5.mjs +43 -0
  5. package/dist/{clean-CA_Oi2Wl.mjs → clean-Di-KT7Mf.mjs} +5 -2
  6. package/dist/{completion-DBGfL-gN.mjs → completion-C5D0sEs5.mjs} +23 -16
  7. package/dist/context-BaNshWr9.mjs +695 -0
  8. package/dist/{decide-CoIJh34e.mjs → decide-B6PnKeqo.mjs} +3 -3
  9. package/dist/{find-rBBk7Jui.mjs → find-BicBbGMg.mjs} +1 -1
  10. package/dist/format-lWhN-jYR.mjs +286 -0
  11. package/dist/{in-a-run-DdAakpRR.mjs → in-a-run-CgkVouyG.mjs} +1 -1
  12. package/dist/{limits-BPM9wUlV.mjs → limits-jZQp9ZJC.mjs} +1 -1
  13. package/dist/{list-Bv5Lxu7z.mjs → list-CEaJ07jd.mjs} +11 -4
  14. package/dist/{mcp-DM30MFBw.mjs → mcp-DYgVA9f5.mjs} +6 -6
  15. package/dist/output-CM95njYt.mjs +421 -0
  16. package/dist/{pause-C7lJkSma.mjs → pause-D6PuSlNl.mjs} +4 -4
  17. package/dist/{relay-BipJYszF.mjs → relay-DNx7be9k.mjs} +2 -2
  18. package/dist/{remove-BDs9isB6.mjs → remove-CpMb4NDl.mjs} +14 -7
  19. package/dist/{report-DK4LGqwF.mjs → report-CEN8VoUb.mjs} +31 -20
  20. package/dist/{send-nbkjf3sc.mjs → send-CLs_fyg7.mjs} +3 -3
  21. package/dist/{src-BxUJPGa5.mjs → src-9iGkjnsy.mjs} +487 -341
  22. package/dist/{src-B0TT7DBi.mjs → src-B6KvTm7B.mjs} +106 -90
  23. package/dist/src-BnZgZGrf.mjs +2697 -0
  24. package/dist/{src-hrj2h0IZ.mjs → src-D6_xjm-x.mjs} +6719 -3389
  25. package/dist/ui/THIRD_PARTY_LICENSES.txt +594 -0
  26. package/dist/ui/assets/app-CxURph7a.js +75 -0
  27. package/dist/ui/assets/app-QoHVi-mb.css +1 -0
  28. package/dist/ui/assets/{index-CPx74lFf.js → index-By_8KKG6.js} +3 -3
  29. package/dist/ui/assets/theme-CdXvqTQ6.js +1 -0
  30. package/dist/ui/index.html +2 -2
  31. package/dist/{ui-B98FSboa.mjs → ui-CIci4UBW.mjs} +4 -4
  32. package/dist/{usage-EW7DI6Z7.mjs → usage-GceBGMjU.mjs} +43 -32
  33. package/dist/{watch-BAkSGaOp.mjs → watch-BmoybMQg.mjs} +28 -16
  34. package/dist/{workflows-vsEohqa7.mjs → workflows-DZvW3i1e.mjs} +4 -3
  35. package/dist/{worktrees-Ahj9rdWK.mjs → worktrees-Be51E2rB.mjs} +84 -24
  36. package/package.json +2 -4
  37. package/dist/context-6x3kXREO.mjs +0 -274
  38. package/dist/format-zMmhqyLE.mjs +0 -427
  39. package/dist/output-TV_j7Ni0.mjs +0 -950
  40. package/dist/src-B9v0lDc6.mjs +0 -1204
  41. package/dist/ui/assets/app-BNT0SuIz.css +0 -1
  42. package/dist/ui/assets/app-CmkynZyW.js +0 -67
  43. package/dist/ui/assets/theme-3xH5jxKs.js +0 -1
@@ -0,0 +1,695 @@
1
+ import { Ar as AGENT_DRIVERS, Bn as placeFiles, C as openStore, Dr as readyDriver, Er as driverChecks, F as driverQueues, Fr as knownExperiments, Gn as workflowDirOf, Jn as LESSON_CAP_TOKENS, Mr as firstLine, Nr as EXPERIMENTS, P as driverRefusals, Pr as experimentsOn, Sn as socketRoom, Sr as INTEGRATIONS, Vn as placeFor, _ as readProjectTriggers, br as retiredDriverHint, ct as explainSession, f as lateTriggers, gt as walkSummary, ht as walkChecks, jr as DRIVER_IDS, mt as checkWorkflowText, p as triggersReport, qt as loadProject, ut as namingNoDriver, v as workflowTextHash, vr as DriverSchema, w as storePath } from "./src-D6_xjm-x.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__-CIchF6nr.mjs";
3
+ import { C as widthOf, a as write, b as termOf, f as paint, i as style, o as writeErr, r as plain, v as plainEnv, w as wrap, x as cut } from "./output-CM95njYt.mjs";
4
+ import { n as chip, t as CHIP_LABELS } from "./chips-BA7SrrH5.mjs";
5
+ import { c as registeredProjects, i as refusedBuiltin, o as workflowArg, r as findWorkflowFiles, s as projectAt } from "./workflows-DZvW3i1e.mjs";
6
+ import path from "node:path";
7
+ import { readFileSync } from "node:fs";
8
+ import { execFile, spawn } from "node:child_process";
9
+ import { defineCommand } from "citty";
10
+ import { styleText } from "node:util";
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.
308
+ */
309
+ /** The real checks: the tools on this machine and its environment. */
310
+ const checksHere = () => driverChecks(exec, process.env);
311
+ const subject = (names, all) => {
312
+ if (all) return "No driver is named, so the run uses";
313
+ const [only] = names;
314
+ return names.length === 1 && only !== void 0 ? `${only} names no driver, so it uses` : `${names.join(", ")} name no driver, so they use`;
315
+ };
316
+ /**
317
+ * The driver for `names`, the session nodes that name none, from `checks`.
318
+ * `all` says whether they are every session node of the run.
319
+ */
320
+ const chooseReadyDriver = (names, all, checks) => {
321
+ const ready = readyDriver(checks);
322
+ if (!ready.ok) return {
323
+ ok: false,
324
+ lines: [
325
+ `No agent driver is ready here, and ${names.join(", ")} ${names.length === 1 ? "names" : "name"} none.`,
326
+ ...ready.hints,
327
+ "See `ork doctor`."
328
+ ]
329
+ };
330
+ const why = ready.ready === 1 ? `the one ready here (${ready.detail})` : `more than one is ready, and a run takes ${ready.driver} first`;
331
+ return {
332
+ ok: true,
333
+ driver: ready.driver,
334
+ line: `${subject(names, all)} the ${ready.driver} driver: ${why}.`
335
+ };
336
+ };
337
+ /**
338
+ * What a run takes for its session nodes that name no driver: nothing when
339
+ * `--driver` overrides them all or each names one; the driver a resumed run
340
+ * recorded; otherwise the one ready here.
341
+ */
342
+ const runReadyDriver = async (workflow, options) => {
343
+ if (options.override !== void 0) return { ok: true };
344
+ const names = namingNoDriver(workflow);
345
+ if (names.length === 0) return { ok: true };
346
+ if (options.recorded !== void 0) return {
347
+ ok: true,
348
+ driver: options.recorded
349
+ };
350
+ const sessions = Object.values(workflow.nodes).filter((node) => node.type === "claude.leader" || node.type === "claude.worker").length;
351
+ return chooseReadyDriver(names, names.length === sessions, await (options.checks ?? checksHere)());
352
+ };
353
+ /**
354
+ * What stops a session node from running on its driver, one line each: the
355
+ * driver's declared `capabilities` and `proven` against what the node asks
356
+ * (`drivers` default: the real descriptors; tests pass their own).
357
+ */
358
+ const unfitLines = (workflow, drivers, choice) => driverRefusals(workflow, drivers ?? AGENT_DRIVERS, choice).map(({ message }) => message);
359
+ // src/run/driver-check.ts
360
+ /**
361
+ * @file `ork validate`'s driver check: each session node against what its
362
+ * driver declares (core's `driver-needs.ts`), for `--driver` if given, else
363
+ * the driver a run would take, the one ready here (`ready-driver.ts`).
364
+ */
365
+ const driverDiagnostics = async (workflow, options) => {
366
+ const ready = options.override === void 0 && namingNoDriver(workflow).length > 0 ? readyDriver(await (options.checks ?? checksHere)()) : void 0;
367
+ const drivers = options.drivers ?? AGENT_DRIVERS;
368
+ const choice = {
369
+ override: options.override,
370
+ ready: ready?.ok === true ? ready.driver : void 0
371
+ };
372
+ const at = (node, severity, code, message) => {
373
+ const path = ["nodes", node];
374
+ const found = {
375
+ severity,
376
+ code,
377
+ message,
378
+ path
379
+ };
380
+ const place = options.locate?.(path);
381
+ if (place !== null && place !== void 0) {
382
+ found.line = place.line;
383
+ found.column = place.column;
384
+ }
385
+ return found;
386
+ };
387
+ 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)))];
388
+ };
389
+ /** The `--driver` value, or why it is not a driver. */
390
+ const driverArg = (value) => {
391
+ if (value === void 0) return { ok: true };
392
+ const parsed = DriverSchema.safeParse(value);
393
+ return parsed.success ? {
394
+ ok: true,
395
+ driver: parsed.data
396
+ } : {
397
+ ok: false,
398
+ error: `--${retiredDriverHint(value) ?? `driver must be one of ${DRIVER_IDS.join(", ")}, got \`${value}\``}`
399
+ };
400
+ };
401
+ // src/commands/walk-report.ts
402
+ /**
403
+ * @file How `ork validate` shows a walk (RFC-009): one line on what it tried,
404
+ * and with `--paths`, each way a run can end. A gate's answer that ends the
405
+ * run shows here as an ending, not as a problem.
406
+ */
407
+ /** The walk's one line; unless `--paths` is on, it points there for each ending. */
408
+ const walkLine = (walk, showsPaths = false) => walk === void 0 ? [] : [style("dim", showsPaths ? walkSummary(walk) : `${walkSummary(walk)} See each with \`ork validate --paths\`.`)];
409
+ const endingLine = ({ status, notRun, paths }) => {
410
+ const ways = `${String(paths)} ${paths === 1 ? "way" : "ways"}`;
411
+ const word = status === "succeeded" ? style("green", status) : style("red", status);
412
+ const rest = notRun.length === 0 ? "every node ran" : `didn't run: ${notRun.map((id) => plain(id)).join(", ")}`;
413
+ return ` ${word} ${style("dim", `(${ways})`)} ${rest}`;
414
+ };
415
+ const endingLines = (ending) => [endingLine(ending), ...ending.choices.map((choice) => style("dim", ` ${plain(choice)}`))];
416
+ /** Each ending, the ones that succeeded first, with one way to get there. */
417
+ const pathLines = (walk) => {
418
+ const endings = walk?.endings ?? [];
419
+ return [...endings.filter(({ status }) => status === "succeeded"), ...endings.filter(({ status }) => status !== "succeeded")].flatMap((ending) => endingLines(ending));
420
+ };
421
+ // src/commands/validate-report.ts
422
+ const plural = (count, word) => `${count} ${word}${count === 1 ? "" : "s"}`;
423
+ const diagnosticLine = (file, d, term) => {
424
+ const where = d.line === void 0 ? file : `${file}:${d.line}:${d.column ?? 1}`;
425
+ if (term.rich) return [
426
+ `${d.severity === "error" ? chip(term, CHIP_LABELS.error, "danger") : chip(term, CHIP_LABELS.warn, "warn")} ${paint(term, "muted", cut(where, term.width - 9))}`,
427
+ ...wrap(d.message, term.width - 2).map((line) => ` ${line}`),
428
+ ` ${paint(term, "muted", `(${d.code})`)}`
429
+ ];
430
+ const severity = d.severity === "error" ? style(["red", "bold"], "error") : style(["yellow", "bold"], "warning");
431
+ return [`${style("dim", where)} ${severity} ${d.message} ${style("dim", `(${d.code})`)}`];
432
+ };
433
+ /** The warnings alone, for a run that goes ahead despite them. */
434
+ const warningLines = (report, term = termOf(process.stderr)) => report.diagnostics.filter((d) => d.severity === "warning").flatMap((d) => diagnosticLine(report.file, d, term));
435
+ /** The verdict line: `OK` or `FAIL` chip when rich, else `✓` or `✗`. */
436
+ const verdictLine = (term, ok, file, note) => {
437
+ const mark = term.rich ? chip(term, ok ? CHIP_LABELS.ok : CHIP_LABELS.fail, ok ? "ok" : "danger") : style(ok ? "green" : "red", ok ? "✓" : "✗");
438
+ const room = term.rich ? Math.max(8, term.width - widthOf(file) - 8) : Infinity;
439
+ return `${mark} ${file}${note === "" ? "" : ` ${style("dim", cut(note, room))}`}`;
440
+ };
441
+ /** Human-readable report, one line per diagnostic, compiler style. */
442
+ const formatReport = (report, showsPaths = false, term = termOf(process.stdout)) => {
443
+ const lines = report.diagnostics.flatMap((d) => diagnosticLine(report.file, d, term));
444
+ const errors = report.diagnostics.filter((d) => d.severity === "error").length;
445
+ const warnings = report.diagnostics.length - errors;
446
+ if (report.summary === void 0 && errors === 0) lines.push(verdictLine(term, true, report.file, warnings === 0 ? "" : `(${plural(warnings, "warning")})`));
447
+ else if (report.summary !== void 0 && errors === 0) {
448
+ const { nodes, edges, entries } = report.summary;
449
+ const extra = warnings > 0 ? `, ${plural(warnings, "warning")}` : "";
450
+ lines.push(verdictLine(term, true, report.file, `(${plural(nodes, "node")}, ${plural(edges, "edge")}, starts at ${entries.join(", ")}${extra})`));
451
+ } else lines.push(verdictLine(term, false, report.file, `(${plural(errors, "error")}, ${plural(warnings, "warning")})`));
452
+ return [...lines, ...walkLine(report.walk, showsPaths)];
453
+ };
454
+ /** A report's lines, and with `showsPaths` each path the walk found. */
455
+ const reportLines = (report, showsPaths) => [...formatReport(report, showsPaths), ...showsPaths ? pathLines(report.walk) : []];
456
+ // src/commands/validate.ts
457
+ /**
458
+ * @file `ork validate`: loads, parses and compiles workflow files with the
459
+ * project's rules (`.ork/project.yaml`) and the agents they name, checks
460
+ * their skill and context references, and reports diagnostics compiler-style,
461
+ * with file, line and column. `loadWorkflow` is shared with `ork run`.
462
+ */
463
+ const hasErrors = (diagnostics) => diagnostics.some((d) => d.severity === "error");
464
+ /**
465
+ * Parses and compiles one workflow file, with the agents the user and its
466
+ * project offer. `home` is the user's `~` (tests pass a folder); `registered`
467
+ * the projects a caller's store has, else those of the store in `home`.
468
+ */
469
+ const loadWorkflow = (file, cwd, home, registered = registeredProjects(home)) => {
470
+ const absolute = path.resolve(cwd, file);
471
+ const place = placeFor(path.dirname(absolute), registered);
472
+ const where = {
473
+ place,
474
+ workflowDir: workflowDirOf(place, absolute)
475
+ };
476
+ let text;
477
+ try {
478
+ text = readFileSync(path.resolve(cwd, file), "utf8");
479
+ } catch (error) {
480
+ return {
481
+ report: {
482
+ file,
483
+ diagnostics: [{
484
+ severity: "error",
485
+ code: "file.read",
486
+ message: error instanceof Error && "code" in error && error.code === "ENOENT" ? "file not found" : "cannot read file",
487
+ path: []
488
+ }]
489
+ },
490
+ workflow: null,
491
+ graph: null,
492
+ ...where
493
+ };
494
+ }
495
+ const { diagnostics, workflow, graph, summary, locate } = checkWorkflowText(text, {
496
+ ...where,
497
+ registered,
498
+ home
499
+ });
500
+ return {
501
+ report: {
502
+ file,
503
+ diagnostics,
504
+ ...summary === void 0 ? {} : { summary }
505
+ },
506
+ workflow,
507
+ graph,
508
+ textHash: workflowTextHash(text),
509
+ ...locate === void 0 ? {} : { locate },
510
+ ...where
511
+ };
512
+ };
513
+ const validateFile = (file, cwd) => loadWorkflow(file, cwd).report;
514
+ /**
515
+ * Walks what loaded, with its findings on its report. `ork validate` and `ork
516
+ * run` do this; the rest of what loads a workflow doesn't pay for a walk.
517
+ */
518
+ const walkLoaded = async (loaded) => {
519
+ const { diagnostics, walk } = await walkChecks({
520
+ diagnostics: loaded.report.diagnostics,
521
+ graph: loaded.graph,
522
+ ...loaded.locate === void 0 ? {} : { locate: loaded.locate }
523
+ });
524
+ return {
525
+ ...loaded,
526
+ report: {
527
+ ...loaded.report,
528
+ diagnostics,
529
+ ...walk === void 0 ? {} : { walk }
530
+ }
531
+ };
532
+ };
533
+ const validate = defineCommand({
534
+ meta: {
535
+ name: "validate",
536
+ description: "Check workflow files: schema, ownership tree, edges, loops and references, and walk every way through them"
537
+ },
538
+ args: {
539
+ files: {
540
+ type: "positional",
541
+ description: "Workflow files or names in .ork/workflows/ (default: every workflow there, and the project's triggers)",
542
+ required: false
543
+ },
544
+ json: {
545
+ type: "boolean",
546
+ description: "Print the reports as JSON",
547
+ default: false
548
+ },
549
+ paths: {
550
+ type: "boolean",
551
+ description: "List each way a run can end, and the choices that lead there",
552
+ default: false
553
+ },
554
+ driver: {
555
+ type: "string",
556
+ description: "Check the nodes against this driver (default: the one a run would take, the one ready here)"
557
+ }
558
+ },
559
+ run: async ({ args }) => {
560
+ const driver = driverArg(args.driver);
561
+ if (!driver.ok) {
562
+ writeErr(driver.error);
563
+ process.exitCode = 1;
564
+ return;
565
+ }
566
+ const cwd = process.cwd();
567
+ if (refusedBuiltin(args._, cwd)) return;
568
+ const files = findWorkflowFiles(args._, cwd, placeFiles(projectAt(cwd)).workflows);
569
+ if (files.length === 0) {
570
+ write("No workflow files found. Run `ork init`, or pass a path.");
571
+ process.exitCode = 1;
572
+ return;
573
+ }
574
+ const place = projectAt(cwd);
575
+ const triggers = args._.length === 0 ? readProjectTriggers(place, void 0, registeredProjects()) : void 0;
576
+ const walked = [];
577
+ for (const file of files) {
578
+ const loaded = await walkLoaded(loadWorkflow(file, cwd));
579
+ const { report } = loaded;
580
+ if (loaded.workflow !== null) report.diagnostics.push(...await driverDiagnostics(loaded.workflow, {
581
+ override: driver.driver,
582
+ locate: loaded.locate
583
+ }));
584
+ walked.push(report);
585
+ }
586
+ const reports = [...walked, ...triggers?.present === true ? [{
587
+ file: path.relative(cwd, triggers.file),
588
+ diagnostics: [...triggers.diagnostics, ...triggers.triggers.flatMap((item) => item.diagnostics)]
589
+ }] : []];
590
+ if (args.json) write(JSON.stringify(reports, void 0, 2));
591
+ else for (const report of reports) for (const line of reportLines(report, args.paths)) write(line);
592
+ if (reports.some((r) => hasErrors(r.diagnostics))) process.exitCode = 1;
593
+ }
594
+ });
595
+ // src/commands/context.ts
596
+ /**
597
+ * @file `ork context explain <wf> <node>`: what one node's session would load,
598
+ * layer by layer, as core's `explainSession` plans it without writing
599
+ * anything (one of Ork's own `ork:` workflows is written into the project's
600
+ * run folder first, as `ork run` does): its project instructions, every skill
601
+ * with the layer it came from and whether it is live, shadowed or switched
602
+ * off, its context files, its practices, and estimated tokens.
603
+ */
604
+ /** Loads a workflow file and plans one node's session in it, in place. */
605
+ const explainNode = (file, nodeId, cwd, home) => {
606
+ const { report, workflow, graph, place, workflowDir } = loadWorkflow(file, cwd, home);
607
+ if (workflow === null) return {
608
+ ok: false,
609
+ lines: formatReport(report)
610
+ };
611
+ const result = explainSession(workflow, graph, nodeId, {
612
+ root: place.root,
613
+ orkDir: place.dir,
614
+ workflowDir,
615
+ ...home === void 0 ? {} : { home }
616
+ });
617
+ return result.ok ? result : {
618
+ ok: false,
619
+ lines: [result.error]
620
+ };
621
+ };
622
+ const approx = (count) => style("dim", `≈${count.toLocaleString("en")} tokens`);
623
+ const formatExplanation = (e) => {
624
+ const width = Math.max(8, ...e.skills.map((s) => s.name.length));
625
+ const sourceWidth = Math.max(0, ...e.skills.map((s) => s.source.length));
626
+ const lines = [
627
+ `${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`)}`,
628
+ "",
629
+ ...e.agent === void 0 ? [] : [`${style("bold", "Agent")} ${e.agent.name} ${style("dim", `(${e.agent.where})`)} ${approx(e.agent.tokens)}`],
630
+ `${style("bold", "Instructions")} ${e.instructions}`,
631
+ "",
632
+ style("bold", "Skills")
633
+ ];
634
+ if (e.skills.length === 0) lines.push(style("dim", " none"));
635
+ for (const s of e.skills) {
636
+ const mark = s.state === "live" ? style("green", "✓") : style("dim", "−");
637
+ const state = s.why === void 0 ? approx(s.tokens).replace("tokens", "tokens when used") : style("yellow", `off: ${s.why}`);
638
+ lines.push(` ${mark} ${s.name.padEnd(width)} ${s.layer.padEnd(8)} ${style("dim", s.source.padEnd(sourceWidth))} ${state}`);
639
+ }
640
+ lines.push("", style("bold", "Context files"));
641
+ if (e.files.length === 0) lines.push(style("dim", " none"));
642
+ for (const f of e.files) {
643
+ 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)}`)}`;
644
+ lines.push(` ${f.layer.padEnd(8)} ${f.name} ${approx(f.tokens)}${cut}`);
645
+ }
646
+ lines.push("", style("bold", "Practices"));
647
+ for (const p of e.practices) lines.push(` ${p.name.padEnd(8)} ${approx(p.tokens)}`);
648
+ for (const name of e.skippedPractices) lines.push(` ${name.padEnd(8)} ${style("dim", "the project's own skill wins")}`);
649
+ if (e.practices.length === 0 && e.skippedPractices.length === 0) lines.push(style("dim", " off"));
650
+ lines.push("", `Appended to Claude Code's system prompt: ${approx(e.appendedTokens)}`);
651
+ return lines;
652
+ };
653
+ const explain = defineCommand({
654
+ meta: {
655
+ name: "explain",
656
+ description: "Show what one node's session loads, layer by layer"
657
+ },
658
+ args: {
659
+ workflow: {
660
+ type: "positional",
661
+ description: "A workflow file, the name of one in the project's workflows (.ork/workflows/), or one of Ork's own (ork:feedback-apply)",
662
+ required: true
663
+ },
664
+ node: {
665
+ type: "positional",
666
+ description: "Node id",
667
+ required: true
668
+ },
669
+ json: {
670
+ type: "boolean",
671
+ description: "Print JSON",
672
+ default: false
673
+ }
674
+ },
675
+ run: ({ args }) => {
676
+ const cwd = process.cwd();
677
+ if (refusedBuiltin([args.workflow], cwd)) return;
678
+ const result = explainNode(workflowArg(args.workflow, cwd).file, args.node, cwd);
679
+ if (!result.ok) {
680
+ for (const line of result.lines) process.stderr.write(`${line}\n`);
681
+ process.exitCode = 1;
682
+ return;
683
+ }
684
+ if (args.json) write(JSON.stringify(result.explanation, void 0, 2));
685
+ else for (const line of formatExplanation(result.explanation)) write(line);
686
+ }
687
+ });
688
+ const context = defineCommand({
689
+ meta: {
690
+ name: "context",
691
+ description: "Inspect the context Ork compiles for each node"
692
+ },
693
+ subCommands: { explain }
694
+ });
695
+ export { startUpdateCheck as C, showUpdateNotice as S, runChecks as _, validateFile as a, UPDATE_CHILD_ENV as b, warningLines as c, runReadyDriver as d, unfitLines as f, orkCheck as g, headWarning as h, validate as i, checksHere as l, experimentsCheck as m, explainNode as n, walkLoaded as o, exec as p, loadWorkflow as r, formatReport as s, context as t, chooseReadyDriver as u, socketCheck as v, updateCheckOff as w, runUpdateChild as x, triggerChecks as y };