@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.
- package/README.md +5 -6
- package/dist/{__build_info__-DdTVd2p5.mjs → __build_info__-DFF18K8y.mjs} +72 -3
- package/dist/bin.mjs +603 -384
- package/dist/{clean-NnboOuz6.mjs → clean-DVaEH9e7.mjs} +5 -2
- package/dist/{completion-snnYGjgK.mjs → completion-Dv5kf598.mjs} +6 -6
- package/dist/context-CEaKOVmG.mjs +683 -0
- package/dist/{decide-B81wCZQg.mjs → decide-3l9q8UtX.mjs} +3 -3
- package/dist/{find-BKW-uGey.mjs → find-DEjjOlTG.mjs} +1 -1
- package/dist/format-Cb5_m-Qd.mjs +191 -0
- package/dist/{in-a-run-BPXX09Bs.mjs → in-a-run-BS9Nc-JW.mjs} +1 -1
- package/dist/{limits-JCve2QpI.mjs → limits-BSwEjQST.mjs} +1 -1
- package/dist/{list-C7UEgmtd.mjs → list-Qxr6rwsY.mjs} +11 -4
- package/dist/{mcp-CKPyNaKD.mjs → mcp-CslnG5wf.mjs} +6 -6
- package/dist/output-Bqljtmp_.mjs +40 -0
- package/dist/{pause-B_SL_yDz.mjs → pause-D7OVB5Ac.mjs} +4 -4
- package/dist/{relay-o-Luee1c.mjs → relay-EceRIebO.mjs} +2 -2
- package/dist/{remove-DAZ7ZCzt.mjs → remove-0IQZVWmI.mjs} +14 -7
- package/dist/{send-3B64fBNB.mjs → send-C0uwfUYi.mjs} +3 -3
- package/dist/{src-DHvWU4tX.mjs → src-Df6KOIDC.mjs} +5682 -3248
- package/dist/{src-C38BPzit.mjs → src-cmmXKHn1.mjs} +266 -203
- package/dist/{src-BJTegEzY.mjs → src-i5k2Y-kf.mjs} +40 -88
- package/dist/src-oaJYIddp.mjs +2255 -0
- package/dist/ui/THIRD_PARTY_LICENSES.txt +594 -0
- package/dist/ui/assets/app-C8uhHVYp.css +1 -0
- package/dist/ui/assets/app-qs8Lofv6.js +75 -0
- package/dist/ui/assets/{index-D83F2OKZ.js → index-DV1ZaOk-.js} +3 -3
- package/dist/ui/assets/theme-CdXvqTQ6.js +1 -0
- package/dist/ui/index.html +2 -2
- package/dist/{ui-CY4xxJUu.mjs → ui-CBkHJdqk.mjs} +4 -4
- package/dist/{usage-CJnytoe4.mjs → usage-Cd8pEYyK.mjs} +5 -5
- package/dist/{watch-BJB82ZJ7.mjs → watch-DW5R2OPO.mjs} +6 -6
- package/dist/{workflows-CiM7S3l0.mjs → workflows-BT1gDRIV.mjs} +4 -3
- package/dist/{worktrees-86ySXxR6.mjs → worktrees-g1MQMGie.mjs} +78 -19
- package/package.json +2 -4
- package/dist/context-DO3sBy9v.mjs +0 -274
- package/dist/format-CmTlTOjT.mjs +0 -427
- package/dist/output-B1V6ZJSx.mjs +0 -950
- package/dist/src-Dyz_Gy1q.mjs +0 -1183
- package/dist/ui/assets/app-C8nJ6NpT.js +0 -67
- package/dist/ui/assets/app-CH7YC3in.css +0 -1
- 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 };
|