@krapht/ork 0.1.0 → 0.3.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.
- package/README.md +9 -5
- package/THIRD_PARTY_NOTICES.md +7 -0
- package/dist/__build_info__-CXXMLvjt.mjs +104 -0
- package/dist/bin.mjs +1579 -3343
- package/dist/context-BFGC_C02.mjs +435 -0
- package/dist/find-BcvhT6UN.mjs +47 -0
- package/dist/format-DqpObyMl.mjs +553 -0
- package/dist/in-a-run-BbLhZOFW.mjs +22 -0
- package/dist/limits-BKYAmqCR.mjs +44 -0
- package/dist/mcp-BWsdQyri.mjs +91 -0
- package/dist/output-Q2EdeVUt.mjs +856 -0
- package/dist/pause-DKACDfV1.mjs +65 -0
- package/dist/relay-CbK-Wme9.mjs +122 -0
- package/dist/report-DK4LGqwF.mjs +129 -0
- package/dist/send-C6yr04Fe.mjs +163 -0
- package/dist/{src-CDkj_-3F.mjs → src-CZOVF2yU.mjs} +496 -193
- package/dist/src-Cz1yOUTg.mjs +11841 -0
- package/dist/src-D5ruZFgD.mjs +1973 -0
- package/dist/src-DtN1k-B3.mjs +1899 -0
- package/dist/ui/THIRD_PARTY_LICENSES.txt +631 -0
- package/dist/ui/assets/app-BlDdP396.js +61 -0
- package/dist/ui/assets/app-C722xNZ3.css +1 -0
- package/dist/ui/assets/geist-mono-cyrillic-400-normal-C51Di1Mf.woff2 +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-400-normal-CPJFfJgk.woff +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-500-normal-D2cXvDHF.woff2 +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-500-normal-DfuvdVgn.woff +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-ext-400-normal-CkxHOdCb.woff +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-ext-400-normal-XB2BPmDK.woff2 +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-ext-500-normal-BY1KTvO5.woff +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-ext-500-normal-C7aYOB1D.woff2 +0 -0
- package/dist/ui/assets/geist-mono-latin-400-normal-B2yvC1Cq.woff +0 -0
- package/dist/ui/assets/geist-mono-latin-400-normal-DTRLJnHl.woff2 +0 -0
- package/dist/ui/assets/geist-mono-latin-500-normal-D-GG86Jb.woff +0 -0
- package/dist/ui/assets/geist-mono-latin-500-normal-YINYabwD.woff2 +0 -0
- package/dist/ui/assets/geist-mono-latin-ext-400-normal-CfzLURNc.woff +0 -0
- package/dist/ui/assets/geist-mono-latin-ext-400-normal-DI-rJ0UV.woff2 +0 -0
- package/dist/ui/assets/geist-mono-latin-ext-500-normal-Buglb9-a.woff2 +0 -0
- package/dist/ui/assets/geist-mono-latin-ext-500-normal-CrWEgvU_.woff +0 -0
- package/dist/ui/assets/geist-mono-symbols2-400-normal-BS9BGVxz.woff2 +0 -0
- package/dist/ui/assets/geist-mono-symbols2-400-normal-DjbiTEEA.woff +0 -0
- package/dist/ui/assets/geist-mono-symbols2-500-normal-CmnCPGa0.woff +0 -0
- package/dist/ui/assets/geist-mono-symbols2-500-normal-Q_ksOahU.woff2 +0 -0
- package/dist/ui/assets/geist-mono-vietnamese-400-normal-CKEaXIlx.woff2 +0 -0
- package/dist/ui/assets/geist-mono-vietnamese-400-normal-CZEPLOgu.woff +0 -0
- package/dist/ui/assets/geist-mono-vietnamese-500-normal-Bs29RrUZ.woff2 +0 -0
- package/dist/ui/assets/geist-mono-vietnamese-500-normal-Dx9-epDo.woff +0 -0
- package/dist/ui/assets/geist-sans-latin-400-normal-BOaIZNA2.woff +0 -0
- package/dist/ui/assets/geist-sans-latin-400-normal-gapTbOY8.woff2 +0 -0
- package/dist/ui/assets/geist-sans-latin-500-normal-CN2lyvyL.woff +0 -0
- package/dist/ui/assets/geist-sans-latin-500-normal-uokXdC-Q.woff2 +0 -0
- package/dist/ui/assets/geist-sans-latin-600-normal-CA1yjETN.woff +0 -0
- package/dist/ui/assets/geist-sans-latin-600-normal-DFOURf8L.woff2 +0 -0
- package/dist/ui/assets/index-BiO54L90.css +1 -0
- package/dist/ui/assets/index-BoG6O-Lp.js +10 -0
- package/dist/ui/assets/theme-3xH5jxKs.js +1 -0
- package/dist/ui/favicon.ico +0 -0
- package/dist/ui/favicon.svg +4 -0
- package/dist/ui/index.html +30 -0
- package/dist/ui/ork-wordmark-small-on-light.svg +4 -0
- package/dist/ui/ork-wordmark-small.svg +4 -0
- package/dist/ui-CZS1XKla.mjs +160 -0
- package/dist/usage-BIjf9Xdt.mjs +418 -0
- package/dist/watch-B8o4Szd_.mjs +394 -0
- package/package.json +4 -2
- package/dist/src-CqNlvqV_.mjs +0 -5357
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import { E as planNodeSession, Qt as placeFor, Zt as placeFiles, an as practiceInstructions, ct as checkWorkflowText, en as runDirOf, g as workflowTextHash, h as readProjectTriggers, lt as walkChecks, nn as workflowDirOf, on as resolvePractices, rn as inside, ut as walkSummary } from "./src-Cz1yOUTg.mjs";
|
|
2
|
+
import { a as write, c as openStore, i as style, l as storePath, r as plain } from "./output-Q2EdeVUt.mjs";
|
|
3
|
+
import { defineCommand } from "citty";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
6
|
+
// ../core/src/update/version.ts
|
|
7
|
+
const VERSION = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/u;
|
|
8
|
+
/** `1.2.3`, `v1.2.3-beta.1` and the like; `null` for anything else. */
|
|
9
|
+
const parseVersion = (text) => {
|
|
10
|
+
const match = VERSION.exec(text.trim());
|
|
11
|
+
if (match === null) return null;
|
|
12
|
+
const [, major = "0", minor = "0", patch = "0", pre] = match;
|
|
13
|
+
return {
|
|
14
|
+
core: [
|
|
15
|
+
Number(major),
|
|
16
|
+
Number(minor),
|
|
17
|
+
Number(patch)
|
|
18
|
+
],
|
|
19
|
+
pre: pre === void 0 ? [] : pre.split(".")
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
const NUMERIC = /^\d+$/u;
|
|
23
|
+
/** One pre-release identifier against another: numbers sort below words. */
|
|
24
|
+
const compareIdentifier = (a, b) => {
|
|
25
|
+
const an = NUMERIC.test(a);
|
|
26
|
+
const bn = NUMERIC.test(b);
|
|
27
|
+
if (an && bn) return Number(a) - Number(b);
|
|
28
|
+
if (an !== bn) return an ? -1 : 1;
|
|
29
|
+
return a < b ? -1 : Number(a > b);
|
|
30
|
+
};
|
|
31
|
+
const comparePre = (a, b) => {
|
|
32
|
+
if (a.length === 0 || b.length === 0) return b.length - a.length;
|
|
33
|
+
for (let i = 0; i < Math.min(a.length, b.length); i += 1) {
|
|
34
|
+
const order = compareIdentifier(a[i] ?? "", b[i] ?? "");
|
|
35
|
+
if (order !== 0) return order;
|
|
36
|
+
}
|
|
37
|
+
return a.length - b.length;
|
|
38
|
+
};
|
|
39
|
+
/** Semver precedence: negative when `a` is older, 0 when equal. */
|
|
40
|
+
const compareVersions = (a, b) => {
|
|
41
|
+
for (let i = 0; i < 3; i += 1) {
|
|
42
|
+
const order = (a.core[i] ?? 0) - (b.core[i] ?? 0);
|
|
43
|
+
if (order !== 0) return order;
|
|
44
|
+
}
|
|
45
|
+
return comparePre(a.pre, b.pre);
|
|
46
|
+
};
|
|
47
|
+
/** Whether `candidate` is newer than `current`; false when either is invalid. */
|
|
48
|
+
const isNewer = (candidate, current) => {
|
|
49
|
+
const a = parseVersion(candidate);
|
|
50
|
+
const b = parseVersion(current);
|
|
51
|
+
return a !== null && b !== null && compareVersions(a, b) > 0;
|
|
52
|
+
};
|
|
53
|
+
// src/project/place.ts
|
|
54
|
+
/**
|
|
55
|
+
* @file Which project a folder belongs to, from the projects the store has
|
|
56
|
+
* registered (ADR-035): an attached project's files are in
|
|
57
|
+
* `~/.ork/projects/<id>/`, an in-repo one's in its `.ork/`.
|
|
58
|
+
*/
|
|
59
|
+
/** The registered projects; none before Ork has a store (never creates one). */
|
|
60
|
+
const registeredProjects = (home) => {
|
|
61
|
+
const file = storePath(home);
|
|
62
|
+
if (!existsSync(file)) return [];
|
|
63
|
+
const store = openStore(file);
|
|
64
|
+
try {
|
|
65
|
+
return store.projects();
|
|
66
|
+
} finally {
|
|
67
|
+
store.close();
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
/** The project `dir` is in: registered, or the in-repo one at its git root. */
|
|
71
|
+
const projectAt = (dir, home) => placeFor(dir, registeredProjects(home));
|
|
72
|
+
/** A path to show: relative to `cwd` when inside it, else as it is. */
|
|
73
|
+
const fromCwd = (target, cwd) => inside(cwd, target) ? path.relative(cwd, target) || "." : target;
|
|
74
|
+
// src/workflows.ts
|
|
75
|
+
/**
|
|
76
|
+
* @file Finding workflow files: the project's workflows folder by default
|
|
77
|
+
* (`.ork/workflows/`, or an attached project's), directories expanded, and a
|
|
78
|
+
* workflow given by name.
|
|
79
|
+
*/
|
|
80
|
+
const WORKFLOW_DIR = path.join(".ork", "workflows");
|
|
81
|
+
const WORKFLOW_SUFFIX = ".ork.yaml";
|
|
82
|
+
/**
|
|
83
|
+
* Expands the given paths to workflow files: a directory contributes its
|
|
84
|
+
* `*.ork.yaml` files, and a name one in the project's workflows folder
|
|
85
|
+
* (`workflows`, default `.ork/workflows/` under `cwd`). With no paths, uses
|
|
86
|
+
* that folder.
|
|
87
|
+
*/
|
|
88
|
+
const findWorkflowFiles = (paths, cwd, workflows = path.join(cwd, WORKFLOW_DIR)) => {
|
|
89
|
+
return (paths.length > 0 ? paths : [fromCwd(workflows, cwd)]).flatMap((target) => {
|
|
90
|
+
const absolute = path.resolve(cwd, target);
|
|
91
|
+
if (existsSync(absolute) && statSync(absolute).isDirectory()) return readdirSync(absolute).filter((name) => name.endsWith(WORKFLOW_SUFFIX)).toSorted().map((name) => path.join(target, name));
|
|
92
|
+
return [resolveWorkflowArg(target, cwd, workflows)];
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* A workflow given by path, or by name for one in the project's workflows
|
|
97
|
+
* folder: `hello` is `.ork/workflows/hello.ork.yaml` when no file `hello`
|
|
98
|
+
* exists.
|
|
99
|
+
*/
|
|
100
|
+
const resolveWorkflowArg = (arg, cwd, workflows = path.join(cwd, WORKFLOW_DIR)) => {
|
|
101
|
+
if (existsSync(path.resolve(cwd, arg)) || arg.endsWith(".ork.yaml")) return arg;
|
|
102
|
+
const named = path.join(workflows, `${arg}${WORKFLOW_SUFFIX}`);
|
|
103
|
+
return existsSync(named) ? fromCwd(named, cwd) : arg;
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* A command's workflow argument, by path or by name in the project `cwd` is in,
|
|
107
|
+
* with the project the file belongs to and a run's folder there.
|
|
108
|
+
*/
|
|
109
|
+
const workflowArg = (arg, cwd) => {
|
|
110
|
+
const file = resolveWorkflowArg(arg, cwd, placeFiles(projectAt(cwd)).workflows);
|
|
111
|
+
const place = projectAt(path.dirname(path.resolve(cwd, file)));
|
|
112
|
+
return {
|
|
113
|
+
file,
|
|
114
|
+
place,
|
|
115
|
+
runDir: (runId) => runDirOf(place, runId)
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
// src/commands/walk-report.ts
|
|
119
|
+
/**
|
|
120
|
+
* @file How `ork validate` shows a walk (RFC-009): one line on what it tried,
|
|
121
|
+
* and with `--paths`, each way a run can end. A gate's answer that ends the
|
|
122
|
+
* run shows here as an ending, not as a problem.
|
|
123
|
+
*/
|
|
124
|
+
const walkLine = (walk) => walk === void 0 ? [] : [style("dim", walkSummary(walk))];
|
|
125
|
+
const endingLine = ({ status, notRun, paths }) => {
|
|
126
|
+
const ways = `${String(paths)} ${paths === 1 ? "way" : "ways"}`;
|
|
127
|
+
const word = status === "succeeded" ? style("green", status) : style("red", status);
|
|
128
|
+
const rest = notRun.length === 0 ? "every node ran" : `didn't run: ${notRun.map((id) => plain(id)).join(", ")}`;
|
|
129
|
+
return ` ${word} ${style("dim", `(${ways})`)} ${rest}`;
|
|
130
|
+
};
|
|
131
|
+
const endingLines = (ending) => [endingLine(ending), ...ending.choices.map((choice) => style("dim", ` ${plain(choice)}`))];
|
|
132
|
+
/** Each ending, the ones that succeeded first, with one way to get there. */
|
|
133
|
+
const pathLines = (walk) => {
|
|
134
|
+
const endings = walk?.endings ?? [];
|
|
135
|
+
return [...endings.filter(({ status }) => status === "succeeded"), ...endings.filter(({ status }) => status !== "succeeded")].flatMap((ending) => endingLines(ending));
|
|
136
|
+
};
|
|
137
|
+
// src/commands/validate.ts
|
|
138
|
+
/**
|
|
139
|
+
* @file `ork validate`: loads, parses and compiles workflow files with the
|
|
140
|
+
* project's rules (`.ork/project.yaml`) and the agents they name, checks
|
|
141
|
+
* their skill and context references, and reports diagnostics compiler-style,
|
|
142
|
+
* with file, line and column. `loadWorkflow` is shared with `ork run`.
|
|
143
|
+
*/
|
|
144
|
+
const hasErrors = (diagnostics) => diagnostics.some((d) => d.severity === "error");
|
|
145
|
+
/**
|
|
146
|
+
* Parses and compiles one workflow file, with the agents the user and its
|
|
147
|
+
* project offer. `home` is the user's `~` (tests pass a folder); `registered`
|
|
148
|
+
* the projects a caller's store has, else those of the store in `home`.
|
|
149
|
+
*/
|
|
150
|
+
const loadWorkflow = (file, cwd, home, registered = registeredProjects(home)) => {
|
|
151
|
+
const absolute = path.resolve(cwd, file);
|
|
152
|
+
const place = placeFor(path.dirname(absolute), registered);
|
|
153
|
+
const where = {
|
|
154
|
+
place,
|
|
155
|
+
workflowDir: workflowDirOf(place, absolute)
|
|
156
|
+
};
|
|
157
|
+
let text;
|
|
158
|
+
try {
|
|
159
|
+
text = readFileSync(path.resolve(cwd, file), "utf8");
|
|
160
|
+
} catch (error) {
|
|
161
|
+
return {
|
|
162
|
+
report: {
|
|
163
|
+
file,
|
|
164
|
+
diagnostics: [{
|
|
165
|
+
severity: "error",
|
|
166
|
+
code: "file.read",
|
|
167
|
+
message: error instanceof Error && "code" in error && error.code === "ENOENT" ? "file not found" : "cannot read file",
|
|
168
|
+
path: []
|
|
169
|
+
}]
|
|
170
|
+
},
|
|
171
|
+
workflow: null,
|
|
172
|
+
graph: null,
|
|
173
|
+
...where
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
const { diagnostics, workflow, graph, summary, locate } = checkWorkflowText(text, {
|
|
177
|
+
...where,
|
|
178
|
+
registered,
|
|
179
|
+
home
|
|
180
|
+
});
|
|
181
|
+
return {
|
|
182
|
+
report: {
|
|
183
|
+
file,
|
|
184
|
+
diagnostics,
|
|
185
|
+
...summary === void 0 ? {} : { summary }
|
|
186
|
+
},
|
|
187
|
+
workflow,
|
|
188
|
+
graph,
|
|
189
|
+
textHash: workflowTextHash(text),
|
|
190
|
+
...locate === void 0 ? {} : { locate },
|
|
191
|
+
...where
|
|
192
|
+
};
|
|
193
|
+
};
|
|
194
|
+
const validateFile = (file, cwd) => loadWorkflow(file, cwd).report;
|
|
195
|
+
/**
|
|
196
|
+
* Walks what loaded, with its findings on its report. `ork validate` and `ork
|
|
197
|
+
* run` do this; the rest of what loads a workflow doesn't pay for a walk.
|
|
198
|
+
*/
|
|
199
|
+
const walkLoaded = async (loaded) => {
|
|
200
|
+
const { diagnostics, walk } = await walkChecks({
|
|
201
|
+
diagnostics: loaded.report.diagnostics,
|
|
202
|
+
graph: loaded.graph,
|
|
203
|
+
...loaded.locate === void 0 ? {} : { locate: loaded.locate }
|
|
204
|
+
});
|
|
205
|
+
return {
|
|
206
|
+
...loaded,
|
|
207
|
+
report: {
|
|
208
|
+
...loaded.report,
|
|
209
|
+
diagnostics,
|
|
210
|
+
...walk === void 0 ? {} : { walk }
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
};
|
|
214
|
+
const plural = (count, word) => `${count} ${word}${count === 1 ? "" : "s"}`;
|
|
215
|
+
const diagnosticLine = (file, d) => {
|
|
216
|
+
const where = d.line === void 0 ? file : `${file}:${d.line}:${d.column ?? 1}`;
|
|
217
|
+
const severity = d.severity === "error" ? style(["red", "bold"], "error") : style(["yellow", "bold"], "warning");
|
|
218
|
+
return `${style("dim", where)} ${severity} ${d.message} ${style("dim", `(${d.code})`)}`;
|
|
219
|
+
};
|
|
220
|
+
/** The warnings alone, for a run that goes ahead despite them. */
|
|
221
|
+
const warningLines = (report) => report.diagnostics.filter((d) => d.severity === "warning").map((d) => diagnosticLine(report.file, d));
|
|
222
|
+
/** Human-readable report, one line per diagnostic, compiler style. */
|
|
223
|
+
const formatReport = (report) => {
|
|
224
|
+
const lines = report.diagnostics.map((d) => diagnosticLine(report.file, d));
|
|
225
|
+
const errors = report.diagnostics.filter((d) => d.severity === "error").length;
|
|
226
|
+
const warnings = report.diagnostics.length - errors;
|
|
227
|
+
if (report.summary === void 0 && errors === 0) lines.push(`${style("green", "✓")} ${report.file}${warnings === 0 ? "" : style("dim", ` (${plural(warnings, "warning")})`)}`);
|
|
228
|
+
else if (report.summary !== void 0 && errors === 0) {
|
|
229
|
+
const { nodes, edges, entries } = report.summary;
|
|
230
|
+
const extra = warnings > 0 ? `, ${plural(warnings, "warning")}` : "";
|
|
231
|
+
lines.push(`${style("green", "✓")} ${report.file} ${style("dim", `(${plural(nodes, "node")}, ${plural(edges, "edge")}, starts at ${entries.join(", ")}${extra})`)}`);
|
|
232
|
+
} else lines.push(`${style("red", "✗")} ${report.file} ${style("dim", `(${plural(errors, "error")}, ${plural(warnings, "warning")})`)}`);
|
|
233
|
+
return [...lines, ...walkLine(report.walk)];
|
|
234
|
+
};
|
|
235
|
+
const validate = defineCommand({
|
|
236
|
+
meta: {
|
|
237
|
+
name: "validate",
|
|
238
|
+
description: "Check workflow files: schema, ownership tree, edges, loops and references, and walk every way through them"
|
|
239
|
+
},
|
|
240
|
+
args: {
|
|
241
|
+
json: {
|
|
242
|
+
type: "boolean",
|
|
243
|
+
description: "Print the reports as JSON",
|
|
244
|
+
default: false
|
|
245
|
+
},
|
|
246
|
+
paths: {
|
|
247
|
+
type: "boolean",
|
|
248
|
+
description: "List each way a run can end, and the choices that lead there",
|
|
249
|
+
default: false
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
run: async ({ args }) => {
|
|
253
|
+
const cwd = process.cwd();
|
|
254
|
+
const files = findWorkflowFiles(args._, cwd, placeFiles(projectAt(cwd)).workflows);
|
|
255
|
+
if (files.length === 0) {
|
|
256
|
+
write("No workflow files found. Run `ork init`, or pass a path.");
|
|
257
|
+
process.exitCode = 1;
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const place = projectAt(cwd);
|
|
261
|
+
const triggers = args._.length === 0 ? readProjectTriggers(place, void 0, registeredProjects()) : void 0;
|
|
262
|
+
const walked = [];
|
|
263
|
+
for (const file of files) {
|
|
264
|
+
const { report } = await walkLoaded(loadWorkflow(file, cwd));
|
|
265
|
+
walked.push(report);
|
|
266
|
+
}
|
|
267
|
+
const reports = [...walked, ...triggers?.present === true ? [{
|
|
268
|
+
file: path.relative(cwd, triggers.file),
|
|
269
|
+
diagnostics: [...triggers.diagnostics, ...triggers.triggers.flatMap((item) => item.diagnostics)]
|
|
270
|
+
}] : []];
|
|
271
|
+
if (args.json) write(JSON.stringify(reports, void 0, 2));
|
|
272
|
+
else for (const report of reports) {
|
|
273
|
+
for (const line of formatReport(report)) write(line);
|
|
274
|
+
if (args.paths) for (const line of pathLines(report.walk)) write(line);
|
|
275
|
+
}
|
|
276
|
+
if (reports.some((r) => hasErrors(r.diagnostics))) process.exitCode = 1;
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
// src/commands/context.ts
|
|
280
|
+
/**
|
|
281
|
+
* @file `ork context explain <wf> <node>`: what one node's session would load,
|
|
282
|
+
* layer by layer. Its project instructions, every skill with the layer it
|
|
283
|
+
* came from and whether it is live, shadowed or switched off, its context
|
|
284
|
+
* files, its practices, and estimated tokens. Plans the session without
|
|
285
|
+
* writing anything.
|
|
286
|
+
*/
|
|
287
|
+
/** A rough count, for comparing sizes: about four characters a token. */
|
|
288
|
+
const tokens = (text) => Math.ceil(text.length / 4);
|
|
289
|
+
/** Which project instruction file Claude Code reads from the checkout. */
|
|
290
|
+
const instructionsOf = (stack, checkout) => {
|
|
291
|
+
if (stack.instructions === "ignore") return "left out (project.instructions: ignore)";
|
|
292
|
+
const has = (file) => existsSync(path.join(checkout, file));
|
|
293
|
+
if ([
|
|
294
|
+
"CLAUDE.md",
|
|
295
|
+
".claude/CLAUDE.md",
|
|
296
|
+
"CLAUDE.local.md"
|
|
297
|
+
].some((file) => has(file))) return "CLAUDE.md, loaded by Claude Code";
|
|
298
|
+
if (has("AGENTS.md")) return "AGENTS.md, loaded by Claude Code";
|
|
299
|
+
return "none in the project";
|
|
300
|
+
};
|
|
301
|
+
const skillLines = (stack, root) => stack.skills.map((skill) => {
|
|
302
|
+
const source = skill.text === void 0 ? path.relative(root, skill.dir) || "." : "built-in";
|
|
303
|
+
const line = {
|
|
304
|
+
name: skill.layer !== "project" ? `ork:${skill.name}` : skill.name,
|
|
305
|
+
layer: skill.layer,
|
|
306
|
+
source: source.startsWith("..") ? skill.dir : source,
|
|
307
|
+
state: skill.shadowedBy === void 0 && skill.ignored === void 0 ? "live" : "off",
|
|
308
|
+
tokens: tokens(skill.text ?? readFileSync(path.join(skill.dir, "SKILL.md"), "utf8"))
|
|
309
|
+
};
|
|
310
|
+
if (skill.shadowedBy !== void 0) line.why = `shadowed by the ${skill.shadowedBy} layer`;
|
|
311
|
+
else if (skill.ignored !== void 0) line.why = skill.ignored === "ignore" ? "project.skills: ignore" : "not in project.skills.allow";
|
|
312
|
+
return line;
|
|
313
|
+
});
|
|
314
|
+
const explainNode = (file, nodeId, cwd, home) => {
|
|
315
|
+
const { report, workflow, graph, place, workflowDir } = loadWorkflow(file, cwd, home);
|
|
316
|
+
if (workflow === null) return {
|
|
317
|
+
ok: false,
|
|
318
|
+
lines: formatReport(report)
|
|
319
|
+
};
|
|
320
|
+
const { root } = place;
|
|
321
|
+
const plan = planNodeSession(workflow, nodeId, {
|
|
322
|
+
cwd: root,
|
|
323
|
+
inputs: {},
|
|
324
|
+
inPlace: true,
|
|
325
|
+
workflowDir,
|
|
326
|
+
projectRoot: root,
|
|
327
|
+
orkDir: place.dir,
|
|
328
|
+
...home === void 0 ? {} : { home }
|
|
329
|
+
});
|
|
330
|
+
if (!plan.ok) return {
|
|
331
|
+
ok: false,
|
|
332
|
+
lines: [plan.error]
|
|
333
|
+
};
|
|
334
|
+
const { context } = plan;
|
|
335
|
+
const practices = practiceInstructions(resolvePractices(workflow, nodeId), root);
|
|
336
|
+
const names = ["caveman", "ponytail"].filter((n) => !practices.skipped.includes(n));
|
|
337
|
+
const ref = graph?.agents.get(nodeId);
|
|
338
|
+
const role = ref === void 0 ? void 0 : workflow.agents?.[ref.name]?.instructions;
|
|
339
|
+
return {
|
|
340
|
+
ok: true,
|
|
341
|
+
explanation: {
|
|
342
|
+
node: nodeId,
|
|
343
|
+
type: workflow.nodes[nodeId]?.type ?? "",
|
|
344
|
+
workflow: workflow.name,
|
|
345
|
+
...ref === void 0 ? {} : { agent: {
|
|
346
|
+
name: ref.name,
|
|
347
|
+
where: ref.where,
|
|
348
|
+
tokens: tokens(role ?? "")
|
|
349
|
+
} },
|
|
350
|
+
instructions: instructionsOf(context, root),
|
|
351
|
+
skills: skillLines(context, root),
|
|
352
|
+
files: context.files.map((f) => ({
|
|
353
|
+
layer: f.layer,
|
|
354
|
+
name: f.name,
|
|
355
|
+
tokens: tokens(f.text)
|
|
356
|
+
})),
|
|
357
|
+
practices: practices.text.map((text, index) => ({
|
|
358
|
+
name: names[index] ?? "practice",
|
|
359
|
+
tokens: tokens(text)
|
|
360
|
+
})),
|
|
361
|
+
skippedPractices: practices.skipped,
|
|
362
|
+
appendedTokens: tokens(plan.spec.systemAppend)
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
};
|
|
366
|
+
const approx = (count) => style("dim", `≈${count.toLocaleString("en")} tokens`);
|
|
367
|
+
const formatExplanation = (e) => {
|
|
368
|
+
const width = Math.max(8, ...e.skills.map((s) => s.name.length));
|
|
369
|
+
const sourceWidth = Math.max(0, ...e.skills.map((s) => s.source.length));
|
|
370
|
+
const lines = [
|
|
371
|
+
`${style("bold", e.node)} ${style("dim", `(${e.type}) in ${e.workflow}`)}`,
|
|
372
|
+
"",
|
|
373
|
+
...e.agent === void 0 ? [] : [`${style("bold", "Agent")} ${e.agent.name} ${style("dim", `(${e.agent.where})`)} ${approx(e.agent.tokens)}`],
|
|
374
|
+
`${style("bold", "Instructions")} ${e.instructions}`,
|
|
375
|
+
"",
|
|
376
|
+
style("bold", "Skills")
|
|
377
|
+
];
|
|
378
|
+
if (e.skills.length === 0) lines.push(style("dim", " none"));
|
|
379
|
+
for (const s of e.skills) {
|
|
380
|
+
const mark = s.state === "live" ? style("green", "✓") : style("dim", "−");
|
|
381
|
+
const state = s.why === void 0 ? approx(s.tokens).replace("tokens", "tokens when used") : style("yellow", `off: ${s.why}`);
|
|
382
|
+
lines.push(` ${mark} ${s.name.padEnd(width)} ${s.layer.padEnd(8)} ${style("dim", s.source.padEnd(sourceWidth))} ${state}`);
|
|
383
|
+
}
|
|
384
|
+
lines.push("", style("bold", "Context files"));
|
|
385
|
+
if (e.files.length === 0) lines.push(style("dim", " none"));
|
|
386
|
+
for (const f of e.files) lines.push(` ${f.layer.padEnd(8)} ${f.name} ${approx(f.tokens)}`);
|
|
387
|
+
lines.push("", style("bold", "Practices"));
|
|
388
|
+
for (const p of e.practices) lines.push(` ${p.name.padEnd(8)} ${approx(p.tokens)}`);
|
|
389
|
+
for (const name of e.skippedPractices) lines.push(` ${name.padEnd(8)} ${style("dim", "the project's own skill wins")}`);
|
|
390
|
+
if (e.practices.length === 0 && e.skippedPractices.length === 0) lines.push(style("dim", " off"));
|
|
391
|
+
lines.push("", `Appended to Claude Code's system prompt: ${approx(e.appendedTokens)}`);
|
|
392
|
+
return lines;
|
|
393
|
+
};
|
|
394
|
+
const explain = defineCommand({
|
|
395
|
+
meta: {
|
|
396
|
+
name: "explain",
|
|
397
|
+
description: "Show what one node's session loads, layer by layer"
|
|
398
|
+
},
|
|
399
|
+
args: {
|
|
400
|
+
workflow: {
|
|
401
|
+
type: "positional",
|
|
402
|
+
description: "A workflow file, or the name of one in the project's workflows (.ork/workflows/)",
|
|
403
|
+
required: true
|
|
404
|
+
},
|
|
405
|
+
node: {
|
|
406
|
+
type: "positional",
|
|
407
|
+
description: "Node id",
|
|
408
|
+
required: true
|
|
409
|
+
},
|
|
410
|
+
json: {
|
|
411
|
+
type: "boolean",
|
|
412
|
+
description: "Print JSON",
|
|
413
|
+
default: false
|
|
414
|
+
}
|
|
415
|
+
},
|
|
416
|
+
run: ({ args }) => {
|
|
417
|
+
const cwd = process.cwd();
|
|
418
|
+
const result = explainNode(workflowArg(args.workflow, cwd).file, args.node, cwd);
|
|
419
|
+
if (!result.ok) {
|
|
420
|
+
for (const line of result.lines) process.stderr.write(`${line}\n`);
|
|
421
|
+
process.exitCode = 1;
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
if (args.json) write(JSON.stringify(result.explanation, void 0, 2));
|
|
425
|
+
else for (const line of formatExplanation(result.explanation)) write(line);
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
const context = defineCommand({
|
|
429
|
+
meta: {
|
|
430
|
+
name: "context",
|
|
431
|
+
description: "Inspect the context Ork compiles for each node"
|
|
432
|
+
},
|
|
433
|
+
subCommands: { explain }
|
|
434
|
+
});
|
|
435
|
+
export { validate as a, warningLines as c, workflowArg as d, projectAt as f, loadWorkflow as i, WORKFLOW_DIR as l, explainNode as n, validateFile as o, isNewer as p, formatReport as r, walkLoaded as s, context as t, WORKFLOW_SUFFIX as u };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Qt as placeFor } from "./src-Cz1yOUTg.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 };
|