@krapht/ork 0.2.0 → 0.4.0

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 (68) hide show
  1. package/THIRD_PARTY_NOTICES.md +7 -0
  2. package/dist/__build_info__-CtSVVuZE.mjs +691 -0
  3. package/dist/bin.mjs +1654 -3532
  4. package/dist/clean-Dcr807be.mjs +88 -0
  5. package/dist/context-C_bksb0C.mjs +351 -0
  6. package/dist/find-DptmbhGd.mjs +47 -0
  7. package/dist/format-J-a21Yvn.mjs +393 -0
  8. package/dist/in-a-run-D1Qc5Mit.mjs +22 -0
  9. package/dist/limits-BDZCmGEu.mjs +44 -0
  10. package/dist/list-ZvY2OcLq.mjs +110 -0
  11. package/dist/mcp-CHVKKg3B.mjs +91 -0
  12. package/dist/output-CXSJO00X.mjs +900 -0
  13. package/dist/pause-BPSmg4yz.mjs +65 -0
  14. package/dist/relay-Do-aNbSD.mjs +122 -0
  15. package/dist/remove-AwaMMaHf.mjs +194 -0
  16. package/dist/report-DK4LGqwF.mjs +129 -0
  17. package/dist/send-DhGBkowy.mjs +163 -0
  18. package/dist/{src-DAsOYtoR.mjs → src-BHnxpJyH.mjs} +496 -193
  19. package/dist/src-CGqMati3.mjs +12505 -0
  20. package/dist/src-CpDKIulI.mjs +3118 -0
  21. package/dist/src-UWkk1sl9.mjs +1899 -0
  22. package/dist/ui/THIRD_PARTY_LICENSES.txt +631 -0
  23. package/dist/ui/assets/app-BgVDC_5l.js +67 -0
  24. package/dist/ui/assets/app-DVf68fj5.css +1 -0
  25. package/dist/ui/assets/geist-mono-cyrillic-400-normal-C51Di1Mf.woff2 +0 -0
  26. package/dist/ui/assets/geist-mono-cyrillic-400-normal-CPJFfJgk.woff +0 -0
  27. package/dist/ui/assets/geist-mono-cyrillic-500-normal-D2cXvDHF.woff2 +0 -0
  28. package/dist/ui/assets/geist-mono-cyrillic-500-normal-DfuvdVgn.woff +0 -0
  29. package/dist/ui/assets/geist-mono-cyrillic-ext-400-normal-CkxHOdCb.woff +0 -0
  30. package/dist/ui/assets/geist-mono-cyrillic-ext-400-normal-XB2BPmDK.woff2 +0 -0
  31. package/dist/ui/assets/geist-mono-cyrillic-ext-500-normal-BY1KTvO5.woff +0 -0
  32. package/dist/ui/assets/geist-mono-cyrillic-ext-500-normal-C7aYOB1D.woff2 +0 -0
  33. package/dist/ui/assets/geist-mono-latin-400-normal-B2yvC1Cq.woff +0 -0
  34. package/dist/ui/assets/geist-mono-latin-400-normal-DTRLJnHl.woff2 +0 -0
  35. package/dist/ui/assets/geist-mono-latin-500-normal-D-GG86Jb.woff +0 -0
  36. package/dist/ui/assets/geist-mono-latin-500-normal-YINYabwD.woff2 +0 -0
  37. package/dist/ui/assets/geist-mono-latin-ext-400-normal-CfzLURNc.woff +0 -0
  38. package/dist/ui/assets/geist-mono-latin-ext-400-normal-DI-rJ0UV.woff2 +0 -0
  39. package/dist/ui/assets/geist-mono-latin-ext-500-normal-Buglb9-a.woff2 +0 -0
  40. package/dist/ui/assets/geist-mono-latin-ext-500-normal-CrWEgvU_.woff +0 -0
  41. package/dist/ui/assets/geist-mono-symbols2-400-normal-BS9BGVxz.woff2 +0 -0
  42. package/dist/ui/assets/geist-mono-symbols2-400-normal-DjbiTEEA.woff +0 -0
  43. package/dist/ui/assets/geist-mono-symbols2-500-normal-CmnCPGa0.woff +0 -0
  44. package/dist/ui/assets/geist-mono-symbols2-500-normal-Q_ksOahU.woff2 +0 -0
  45. package/dist/ui/assets/geist-mono-vietnamese-400-normal-CKEaXIlx.woff2 +0 -0
  46. package/dist/ui/assets/geist-mono-vietnamese-400-normal-CZEPLOgu.woff +0 -0
  47. package/dist/ui/assets/geist-mono-vietnamese-500-normal-Bs29RrUZ.woff2 +0 -0
  48. package/dist/ui/assets/geist-mono-vietnamese-500-normal-Dx9-epDo.woff +0 -0
  49. package/dist/ui/assets/geist-sans-latin-400-normal-BOaIZNA2.woff +0 -0
  50. package/dist/ui/assets/geist-sans-latin-400-normal-gapTbOY8.woff2 +0 -0
  51. package/dist/ui/assets/geist-sans-latin-500-normal-CN2lyvyL.woff +0 -0
  52. package/dist/ui/assets/geist-sans-latin-500-normal-uokXdC-Q.woff2 +0 -0
  53. package/dist/ui/assets/geist-sans-latin-600-normal-CA1yjETN.woff +0 -0
  54. package/dist/ui/assets/geist-sans-latin-600-normal-DFOURf8L.woff2 +0 -0
  55. package/dist/ui/assets/index-BiO54L90.css +1 -0
  56. package/dist/ui/assets/index-MQNNgsyT.js +10 -0
  57. package/dist/ui/assets/theme-3xH5jxKs.js +1 -0
  58. package/dist/ui/favicon.ico +0 -0
  59. package/dist/ui/favicon.svg +4 -0
  60. package/dist/ui/index.html +30 -0
  61. package/dist/ui/ork-wordmark-small-on-light.svg +4 -0
  62. package/dist/ui/ork-wordmark-small.svg +4 -0
  63. package/dist/ui-DhutUMgQ.mjs +472 -0
  64. package/dist/usage-CT7Z-yjr.mjs +419 -0
  65. package/dist/watch-0Qr2UfCQ.mjs +394 -0
  66. package/dist/worktrees-DHl80k96.mjs +212 -0
  67. package/package.json +6 -4
  68. package/dist/src-CJ_zF0Zj.mjs +0 -5433
@@ -0,0 +1,88 @@
1
+ import { E as removeWorktree, O as gitOk } from "./src-CGqMati3.mjs";
2
+ // ../core/src/worktree/clean.ts
3
+ /**
4
+ * @file What `ork worktrees clean` and `ork worktrees remove` do with what
5
+ * list.ts reads: the plan (what goes, what's kept and why), by one rule for
6
+ * every caller, and carrying it out. The CLI and the Worktrees panel both
7
+ * plan here, so neither removes what the other would keep.
8
+ */
9
+ /** What names an item for `remove`: its branch, or its worktree's path. */
10
+ const keyOf = (item) => item.branch ?? item.path ?? `${item.runId}/${item.nodeId}`;
11
+ /** The reasons no `force` overrides: removing would break something. */
12
+ const MAIN_CHECKOUT = "checked out in the main checkout";
13
+ const RUN_GOING = "its run is still going";
14
+ const kept = (item, skip) => ({
15
+ item,
16
+ removeWorktree: false,
17
+ deleteBranch: false,
18
+ skip
19
+ });
20
+ /** What clean does with one item. */
21
+ const cleanStep = (item, options) => {
22
+ if (item.inMainCheckout) return kept(item, MAIN_CHECKOUT);
23
+ if (item.heldBy !== void 0) return kept(item, `checked out in ${item.heldBy}`);
24
+ if (options.live?.has(item.runId) === true) return kept(item, RUN_GOING);
25
+ if (item.dirty && !options.force) return kept(item, "uncommitted changes");
26
+ if (item.branch === void 0 && item.checkedOut === void 0 && item.ahead > 0 && !options.force) return kept(item, "commits on no branch");
27
+ const removable = item.path !== null && !item.missing;
28
+ if (options.branches && item.branch !== void 0 && !item.merged && !options.force) return {
29
+ item,
30
+ removeWorktree: removable,
31
+ deleteBranch: false,
32
+ skip: "branch not merged"
33
+ };
34
+ return {
35
+ item,
36
+ removeWorktree: removable,
37
+ deleteBranch: options.branches && item.branch !== void 0
38
+ };
39
+ };
40
+ /** Whether a step drops a worktree whose folder is gone, by its own path. */
41
+ const stepPrunes = (step) => step.item.missing && step.item.path !== null && step.skip === void 0;
42
+ /** Whether a step does anything. */
43
+ const stepActs = (step) => step.removeWorktree || step.deleteBranch || stepPrunes(step);
44
+ /**
45
+ * What `ork worktrees clean` would do. A worktree with uncommitted changes, an
46
+ * unmerged branch, or a detached worktree holding commits no branch has, is
47
+ * kept unless `force`; one checked out in the main checkout or in a worktree
48
+ * Ork didn't create, or whose run is still going, is kept even then.
49
+ */
50
+ const planClean = (items, filter, options) => items.filter((item) => filter.runId === void 0 || item.runId === filter.runId).filter((item) => filter.key === void 0 || keyOf(item) === filter.key).filter((item) => !filter.merged || item.merged).map((item) => cleanStep(item, options));
51
+ /**
52
+ * Carries out one step; what it did goes into `done` as it goes. It touches
53
+ * only its own item: a missing worktree is dropped by its own path (`git
54
+ * worktree remove`), never by `git worktree prune`, which would drop every
55
+ * missing worktree in the repository, the user's own included.
56
+ */
57
+ const applyStep = async (root, step, force, done) => {
58
+ const { item } = step;
59
+ if (stepPrunes(step) && !step.removeWorktree && item.path !== null) {
60
+ await gitOk(root, [
61
+ "worktree",
62
+ "remove",
63
+ item.path
64
+ ]);
65
+ done.push(`pruned ${item.path}`);
66
+ }
67
+ if (step.removeWorktree && item.path !== null) {
68
+ await removeWorktree(root, item.path, force);
69
+ done.push(`removed ${item.path}`);
70
+ }
71
+ if (step.deleteBranch && item.branch !== void 0) {
72
+ await gitOk(root, [
73
+ "branch",
74
+ force ? "-D" : "-d",
75
+ item.branch
76
+ ]);
77
+ done.push(`deleted ${item.branch}`);
78
+ }
79
+ };
80
+ /**
81
+ * Carries out a clean plan, step by step. What it did goes into `done` as it
82
+ * goes, so a caller still has it when git fails halfway.
83
+ */
84
+ const applyClean = async (root, steps, force, done = []) => {
85
+ for (const step of steps) await applyStep(root, step, force, done);
86
+ return done;
87
+ };
88
+ export { planClean as a, keyOf as i, applyStep as n, stepActs as o, cleanStep as r, stepPrunes as s, applyClean as t };
@@ -0,0 +1,351 @@
1
+ import { _n as inside, _t as walkChecks, b as readProjectTriggers, dn as placeFiles, fn as placeFor, gn as workflowDirOf, gt as checkWorkflowText, mn as runDirOf, ot as explainSession, vt as walkSummary, x as workflowTextHash, yn as LESSON_CAP_TOKENS } from "./src-CGqMati3.mjs";
2
+ import { b as builtinWorkflowFile, x as isBuiltinArg, y as builtinProblem } from "./__build_info__-CtSVVuZE.mjs";
3
+ import { a as write, c as openStore, i as style, l as storePath, o as writeErr, r as plain } from "./output-CXSJO00X.mjs";
4
+ import { defineCommand } from "citty";
5
+ import path from "node:path";
6
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
7
+ // src/project/place.ts
8
+ /**
9
+ * @file Which project a folder belongs to, from the projects the store has
10
+ * registered (ADR-035): an attached project's files are in
11
+ * `~/.ork/projects/<id>/`, an in-repo one's in its `.ork/`.
12
+ */
13
+ /** The registered projects; none before Ork has a store (never creates one). */
14
+ const registeredProjects = (home) => {
15
+ const file = storePath(home);
16
+ if (!existsSync(file)) return [];
17
+ const store = openStore(file);
18
+ try {
19
+ return store.projects();
20
+ } finally {
21
+ store.close();
22
+ }
23
+ };
24
+ /** The project `dir` is in: registered, or the in-repo one at its git root. */
25
+ const projectAt = (dir, home) => placeFor(dir, registeredProjects(home));
26
+ /** A path to show: relative to `cwd` when inside it, else as it is. */
27
+ const fromCwd = (target, cwd) => inside(cwd, target) ? path.relative(cwd, target) || "." : target;
28
+ // src/workflows.ts
29
+ /**
30
+ * @file Finding workflow files: the project's workflows folder by default
31
+ * (`.ork/workflows/`, or an attached project's), directories expanded, a
32
+ * workflow given by name, and one of Ork's own by its `ork:` name, written
33
+ * into the project's run folder.
34
+ */
35
+ const WORKFLOW_DIR = path.join(".ork", "workflows");
36
+ const WORKFLOW_SUFFIX = ".ork.yaml";
37
+ /**
38
+ * Expands the given paths to workflow files: a directory contributes its
39
+ * `*.ork.yaml` files, and a name one in the project's workflows folder
40
+ * (`workflows`, default `.ork/workflows/` under `cwd`). With no paths, uses
41
+ * that folder.
42
+ */
43
+ const findWorkflowFiles = (paths, cwd, workflows = path.join(cwd, WORKFLOW_DIR)) => {
44
+ return (paths.length > 0 ? paths : [fromCwd(workflows, cwd)]).flatMap((target) => {
45
+ const absolute = path.resolve(cwd, target);
46
+ if (existsSync(absolute) && statSync(absolute).isDirectory()) return readdirSync(absolute).filter((name) => name.endsWith(WORKFLOW_SUFFIX)).toSorted().map((name) => path.join(target, name));
47
+ return [resolveWorkflowArg(target, cwd, workflows)];
48
+ });
49
+ };
50
+ /**
51
+ * A workflow given by path, by name for one in the project's workflows folder
52
+ * (`hello` is `.ork/workflows/hello.ork.yaml` when no file `hello` exists), or
53
+ * by its `ork:` name for one of Ork's own. An unknown `ork:` name comes back as
54
+ * it is: commands refuse it first (`unknownBuiltin`).
55
+ */
56
+ const resolveWorkflowArg = (arg, cwd, workflows = path.join(cwd, WORKFLOW_DIR)) => {
57
+ if (isBuiltinArg(arg)) {
58
+ const file = builtinWorkflowFile(projectAt(cwd), arg);
59
+ return file === void 0 ? arg : fromCwd(file, cwd);
60
+ }
61
+ if (existsSync(path.resolve(cwd, arg)) || arg.endsWith(".ork.yaml")) return arg;
62
+ const named = path.join(workflows, `${arg}${WORKFLOW_SUFFIX}`);
63
+ return existsSync(named) ? fromCwd(named, cwd) : arg;
64
+ };
65
+ /**
66
+ * Refuses an `ork:` name that isn't one of Ork's workflows, saying which are,
67
+ * or whose file's place in the project is a link; true when it did, so the
68
+ * command stops.
69
+ */
70
+ const refusedBuiltin = (args, cwd) => {
71
+ const problems = args.filter((arg) => isBuiltinArg(arg)).flatMap((arg) => builtinProblem(projectAt(cwd), arg) ?? []);
72
+ for (const problem of problems) writeErr(problem);
73
+ if (problems.length > 0) process.exitCode = 1;
74
+ return problems.length > 0;
75
+ };
76
+ /**
77
+ * A command's workflow argument, by path or by name in the project `cwd` is in,
78
+ * with the project the file belongs to and a run's folder there.
79
+ */
80
+ const workflowArg = (arg, cwd) => {
81
+ const file = resolveWorkflowArg(arg, cwd, placeFiles(projectAt(cwd)).workflows);
82
+ const place = projectAt(path.dirname(path.resolve(cwd, file)));
83
+ return {
84
+ file,
85
+ place,
86
+ runDir: (runId) => runDirOf(place, runId)
87
+ };
88
+ };
89
+ // src/commands/walk-report.ts
90
+ /**
91
+ * @file How `ork validate` shows a walk (RFC-009): one line on what it tried,
92
+ * and with `--paths`, each way a run can end. A gate's answer that ends the
93
+ * run shows here as an ending, not as a problem.
94
+ */
95
+ const walkLine = (walk) => walk === void 0 ? [] : [style("dim", walkSummary(walk))];
96
+ const endingLine = ({ status, notRun, paths }) => {
97
+ const ways = `${String(paths)} ${paths === 1 ? "way" : "ways"}`;
98
+ const word = status === "succeeded" ? style("green", status) : style("red", status);
99
+ const rest = notRun.length === 0 ? "every node ran" : `didn't run: ${notRun.map((id) => plain(id)).join(", ")}`;
100
+ return ` ${word} ${style("dim", `(${ways})`)} ${rest}`;
101
+ };
102
+ const endingLines = (ending) => [endingLine(ending), ...ending.choices.map((choice) => style("dim", ` ${plain(choice)}`))];
103
+ /** Each ending, the ones that succeeded first, with one way to get there. */
104
+ const pathLines = (walk) => {
105
+ const endings = walk?.endings ?? [];
106
+ return [...endings.filter(({ status }) => status === "succeeded"), ...endings.filter(({ status }) => status !== "succeeded")].flatMap((ending) => endingLines(ending));
107
+ };
108
+ // src/commands/validate.ts
109
+ /**
110
+ * @file `ork validate`: loads, parses and compiles workflow files with the
111
+ * project's rules (`.ork/project.yaml`) and the agents they name, checks
112
+ * their skill and context references, and reports diagnostics compiler-style,
113
+ * with file, line and column. `loadWorkflow` is shared with `ork run`.
114
+ */
115
+ const hasErrors = (diagnostics) => diagnostics.some((d) => d.severity === "error");
116
+ /**
117
+ * Parses and compiles one workflow file, with the agents the user and its
118
+ * project offer. `home` is the user's `~` (tests pass a folder); `registered`
119
+ * the projects a caller's store has, else those of the store in `home`.
120
+ */
121
+ const loadWorkflow = (file, cwd, home, registered = registeredProjects(home)) => {
122
+ const absolute = path.resolve(cwd, file);
123
+ const place = placeFor(path.dirname(absolute), registered);
124
+ const where = {
125
+ place,
126
+ workflowDir: workflowDirOf(place, absolute)
127
+ };
128
+ let text;
129
+ try {
130
+ text = readFileSync(path.resolve(cwd, file), "utf8");
131
+ } catch (error) {
132
+ return {
133
+ report: {
134
+ file,
135
+ diagnostics: [{
136
+ severity: "error",
137
+ code: "file.read",
138
+ message: error instanceof Error && "code" in error && error.code === "ENOENT" ? "file not found" : "cannot read file",
139
+ path: []
140
+ }]
141
+ },
142
+ workflow: null,
143
+ graph: null,
144
+ ...where
145
+ };
146
+ }
147
+ const { diagnostics, workflow, graph, summary, locate } = checkWorkflowText(text, {
148
+ ...where,
149
+ registered,
150
+ home
151
+ });
152
+ return {
153
+ report: {
154
+ file,
155
+ diagnostics,
156
+ ...summary === void 0 ? {} : { summary }
157
+ },
158
+ workflow,
159
+ graph,
160
+ textHash: workflowTextHash(text),
161
+ ...locate === void 0 ? {} : { locate },
162
+ ...where
163
+ };
164
+ };
165
+ const validateFile = (file, cwd) => loadWorkflow(file, cwd).report;
166
+ /**
167
+ * Walks what loaded, with its findings on its report. `ork validate` and `ork
168
+ * run` do this; the rest of what loads a workflow doesn't pay for a walk.
169
+ */
170
+ const walkLoaded = async (loaded) => {
171
+ const { diagnostics, walk } = await walkChecks({
172
+ diagnostics: loaded.report.diagnostics,
173
+ graph: loaded.graph,
174
+ ...loaded.locate === void 0 ? {} : { locate: loaded.locate }
175
+ });
176
+ return {
177
+ ...loaded,
178
+ report: {
179
+ ...loaded.report,
180
+ diagnostics,
181
+ ...walk === void 0 ? {} : { walk }
182
+ }
183
+ };
184
+ };
185
+ const plural = (count, word) => `${count} ${word}${count === 1 ? "" : "s"}`;
186
+ const diagnosticLine = (file, d) => {
187
+ const where = d.line === void 0 ? file : `${file}:${d.line}:${d.column ?? 1}`;
188
+ const severity = d.severity === "error" ? style(["red", "bold"], "error") : style(["yellow", "bold"], "warning");
189
+ return `${style("dim", where)} ${severity} ${d.message} ${style("dim", `(${d.code})`)}`;
190
+ };
191
+ /** The warnings alone, for a run that goes ahead despite them. */
192
+ const warningLines = (report) => report.diagnostics.filter((d) => d.severity === "warning").map((d) => diagnosticLine(report.file, d));
193
+ /** Human-readable report, one line per diagnostic, compiler style. */
194
+ const formatReport = (report) => {
195
+ const lines = report.diagnostics.map((d) => diagnosticLine(report.file, d));
196
+ const errors = report.diagnostics.filter((d) => d.severity === "error").length;
197
+ const warnings = report.diagnostics.length - errors;
198
+ if (report.summary === void 0 && errors === 0) lines.push(`${style("green", "✓")} ${report.file}${warnings === 0 ? "" : style("dim", ` (${plural(warnings, "warning")})`)}`);
199
+ else if (report.summary !== void 0 && errors === 0) {
200
+ const { nodes, edges, entries } = report.summary;
201
+ const extra = warnings > 0 ? `, ${plural(warnings, "warning")}` : "";
202
+ lines.push(`${style("green", "✓")} ${report.file} ${style("dim", `(${plural(nodes, "node")}, ${plural(edges, "edge")}, starts at ${entries.join(", ")}${extra})`)}`);
203
+ } else lines.push(`${style("red", "✗")} ${report.file} ${style("dim", `(${plural(errors, "error")}, ${plural(warnings, "warning")})`)}`);
204
+ return [...lines, ...walkLine(report.walk)];
205
+ };
206
+ const validate = defineCommand({
207
+ meta: {
208
+ name: "validate",
209
+ description: "Check workflow files: schema, ownership tree, edges, loops and references, and walk every way through them"
210
+ },
211
+ args: {
212
+ json: {
213
+ type: "boolean",
214
+ description: "Print the reports as JSON",
215
+ default: false
216
+ },
217
+ paths: {
218
+ type: "boolean",
219
+ description: "List each way a run can end, and the choices that lead there",
220
+ default: false
221
+ }
222
+ },
223
+ run: async ({ args }) => {
224
+ const cwd = process.cwd();
225
+ if (refusedBuiltin(args._, cwd)) return;
226
+ const files = findWorkflowFiles(args._, cwd, placeFiles(projectAt(cwd)).workflows);
227
+ if (files.length === 0) {
228
+ write("No workflow files found. Run `ork init`, or pass a path.");
229
+ process.exitCode = 1;
230
+ return;
231
+ }
232
+ const place = projectAt(cwd);
233
+ const triggers = args._.length === 0 ? readProjectTriggers(place, void 0, registeredProjects()) : void 0;
234
+ const walked = [];
235
+ for (const file of files) {
236
+ const { report } = await walkLoaded(loadWorkflow(file, cwd));
237
+ walked.push(report);
238
+ }
239
+ const reports = [...walked, ...triggers?.present === true ? [{
240
+ file: path.relative(cwd, triggers.file),
241
+ diagnostics: [...triggers.diagnostics, ...triggers.triggers.flatMap((item) => item.diagnostics)]
242
+ }] : []];
243
+ if (args.json) write(JSON.stringify(reports, void 0, 2));
244
+ else for (const report of reports) {
245
+ for (const line of formatReport(report)) write(line);
246
+ if (args.paths) for (const line of pathLines(report.walk)) write(line);
247
+ }
248
+ if (reports.some((r) => hasErrors(r.diagnostics))) process.exitCode = 1;
249
+ }
250
+ });
251
+ // src/commands/context.ts
252
+ /**
253
+ * @file `ork context explain <wf> <node>`: what one node's session would load,
254
+ * layer by layer, as core's `explainSession` plans it without writing
255
+ * anything (one of Ork's own `ork:` workflows is written into the project's
256
+ * run folder first, as `ork run` does): its project instructions, every skill
257
+ * with the layer it came from and whether it is live, shadowed or switched
258
+ * off, its context files, its practices, and estimated tokens.
259
+ */
260
+ /** Loads a workflow file and plans one node's session in it, in place. */
261
+ const explainNode = (file, nodeId, cwd, home) => {
262
+ const { report, workflow, graph, place, workflowDir } = loadWorkflow(file, cwd, home);
263
+ if (workflow === null) return {
264
+ ok: false,
265
+ lines: formatReport(report)
266
+ };
267
+ const result = explainSession(workflow, graph, nodeId, {
268
+ root: place.root,
269
+ orkDir: place.dir,
270
+ workflowDir,
271
+ ...home === void 0 ? {} : { home }
272
+ });
273
+ return result.ok ? result : {
274
+ ok: false,
275
+ lines: [result.error]
276
+ };
277
+ };
278
+ const approx = (count) => style("dim", `≈${count.toLocaleString("en")} tokens`);
279
+ const formatExplanation = (e) => {
280
+ const width = Math.max(8, ...e.skills.map((s) => s.name.length));
281
+ const sourceWidth = Math.max(0, ...e.skills.map((s) => s.source.length));
282
+ const lines = [
283
+ `${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`)}`,
284
+ "",
285
+ ...e.agent === void 0 ? [] : [`${style("bold", "Agent")} ${e.agent.name} ${style("dim", `(${e.agent.where})`)} ${approx(e.agent.tokens)}`],
286
+ `${style("bold", "Instructions")} ${e.instructions}`,
287
+ "",
288
+ style("bold", "Skills")
289
+ ];
290
+ if (e.skills.length === 0) lines.push(style("dim", " none"));
291
+ for (const s of e.skills) {
292
+ const mark = s.state === "live" ? style("green", "✓") : style("dim", "−");
293
+ const state = s.why === void 0 ? approx(s.tokens).replace("tokens", "tokens when used") : style("yellow", `off: ${s.why}`);
294
+ lines.push(` ${mark} ${s.name.padEnd(width)} ${s.layer.padEnd(8)} ${style("dim", s.source.padEnd(sourceWidth))} ${state}`);
295
+ }
296
+ lines.push("", style("bold", "Context files"));
297
+ if (e.files.length === 0) lines.push(style("dim", " none"));
298
+ for (const f of e.files) {
299
+ 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)}`)}`;
300
+ lines.push(` ${f.layer.padEnd(8)} ${f.name} ${approx(f.tokens)}${cut}`);
301
+ }
302
+ lines.push("", style("bold", "Practices"));
303
+ for (const p of e.practices) lines.push(` ${p.name.padEnd(8)} ${approx(p.tokens)}`);
304
+ for (const name of e.skippedPractices) lines.push(` ${name.padEnd(8)} ${style("dim", "the project's own skill wins")}`);
305
+ if (e.practices.length === 0 && e.skippedPractices.length === 0) lines.push(style("dim", " off"));
306
+ lines.push("", `Appended to Claude Code's system prompt: ${approx(e.appendedTokens)}`);
307
+ return lines;
308
+ };
309
+ const explain = defineCommand({
310
+ meta: {
311
+ name: "explain",
312
+ description: "Show what one node's session loads, layer by layer"
313
+ },
314
+ args: {
315
+ workflow: {
316
+ type: "positional",
317
+ description: "A workflow file, the name of one in the project's workflows (.ork/workflows/), or one of Ork's own (ork:feedback-apply)",
318
+ required: true
319
+ },
320
+ node: {
321
+ type: "positional",
322
+ description: "Node id",
323
+ required: true
324
+ },
325
+ json: {
326
+ type: "boolean",
327
+ description: "Print JSON",
328
+ default: false
329
+ }
330
+ },
331
+ run: ({ args }) => {
332
+ const cwd = process.cwd();
333
+ if (refusedBuiltin([args.workflow], cwd)) return;
334
+ const result = explainNode(workflowArg(args.workflow, cwd).file, args.node, cwd);
335
+ if (!result.ok) {
336
+ for (const line of result.lines) process.stderr.write(`${line}\n`);
337
+ process.exitCode = 1;
338
+ return;
339
+ }
340
+ if (args.json) write(JSON.stringify(result.explanation, void 0, 2));
341
+ else for (const line of formatExplanation(result.explanation)) write(line);
342
+ }
343
+ });
344
+ const context = defineCommand({
345
+ meta: {
346
+ name: "context",
347
+ description: "Inspect the context Ork compiles for each node"
348
+ },
349
+ subCommands: { explain }
350
+ });
351
+ export { validate as a, warningLines as c, refusedBuiltin as d, workflowArg as f, loadWorkflow as i, WORKFLOW_DIR as l, registeredProjects as m, explainNode as n, validateFile as o, projectAt as p, formatReport as r, walkLoaded as s, context as t, WORKFLOW_SUFFIX as u };
@@ -0,0 +1,47 @@
1
+ import { fn as placeFor } from "./src-CGqMati3.mjs";
2
+ import { randomUUID } from "node:crypto";
3
+ // ../core/src/remote/find.ts
4
+ /**
5
+ * @file Finding a stored run from another process: by a run id or a unique
6
+ * prefix of one, or, with none, the newest run of the project a folder is in;
7
+ * and picking a new run's id. Shared by the CLI's commands and the local
8
+ * server (ADR-039).
9
+ */
10
+ /** The run `arg` names, or the newest run of the project `cwd` is in. */
11
+ const findRun = (store, arg, cwd) => {
12
+ if (arg === void 0 || arg === "") {
13
+ const [latest] = store.runs({
14
+ project: placeFor(cwd, store.projects()).root,
15
+ limit: 1
16
+ });
17
+ return latest === void 0 ? {
18
+ ok: false,
19
+ error: "No runs in this project yet; see `ork ps --all`."
20
+ } : {
21
+ ok: true,
22
+ run: latest
23
+ };
24
+ }
25
+ const found = store.lookup(arg);
26
+ const [only] = found;
27
+ if (only === void 0) return {
28
+ ok: false,
29
+ error: `No run \`${arg}\`.`
30
+ };
31
+ if (found.length > 1) return {
32
+ ok: false,
33
+ error: `\`${arg}\` matches ${String(found.length)} runs: ${found.map((r) => r.id).join(", ")}`
34
+ };
35
+ return {
36
+ ok: true,
37
+ run: only
38
+ };
39
+ };
40
+ /** A run id no stored run has yet; `node-…` for a run of one node. */
41
+ const newRunId = (store, prefix = "") => {
42
+ for (;;) {
43
+ const id = `${prefix}${randomUUID().slice(0, 8)}`;
44
+ if (store.lookup(id).length === 0) return id;
45
+ }
46
+ };
47
+ export { newRunId as n, findRun as t };