@krapht/ork 1.0.0-alpha.14 → 1.0.0-alpha.16

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 (48) hide show
  1. package/README.md +10 -4
  2. package/dist/__build_info__-DcRdK0Du.mjs +359 -0
  3. package/dist/apply-Mb0fNpCX.mjs +17 -0
  4. package/dist/{attach-CdtGX9se.mjs → attach-7lB5_jpr.mjs} +5 -4
  5. package/dist/{store-CleF7KeS.mjs → attach-B-FeE1dJ.mjs} +60 -25
  6. package/dist/bin.mjs +328 -1734
  7. package/dist/brief-BvMtIFio.mjs +58 -0
  8. package/dist/{builtin-files-Bdq23IIi.mjs → builtin-files-BRSOObAv.mjs} +1 -1
  9. package/dist/command-tree-BkSYWmYJ.mjs +63 -0
  10. package/dist/{completion--ywde-56.mjs → completion-Cgb2qPPh.mjs} +45 -6
  11. package/dist/context-BN9A2Nnu.mjs +106 -0
  12. package/dist/{find-BVX-bL4u.mjs → find-mLYsR0uC.mjs} +2 -2
  13. package/dist/flags-CdgIH1L6.mjs +230 -0
  14. package/dist/follow-ups-DxCsOby1.mjs +496 -0
  15. package/dist/{format-CvTE5CZQ.mjs → format-hiDvtNGM.mjs} +35 -8
  16. package/dist/{help-groups-CpR2UzSn.mjs → help-groups-hB6kY34d.mjs} +5 -63
  17. package/dist/{in-a-run-cL5N9gpH.mjs → in-a-run-a447hxq8.mjs} +1 -1
  18. package/dist/{__build_info__-B82-dGJY.mjs → install-23g_1ngs.mjs} +3 -227
  19. package/dist/{limits-Dhe--wmX.mjs → limits-C_Xz_t3C.mjs} +1 -1
  20. package/dist/{mcp-CsmboyXv.mjs → mcp-CWng9I9n.mjs} +6 -6
  21. package/dist/{once-CrwI-ZdN.mjs → once-lvwcWfVf.mjs} +3 -3
  22. package/dist/orphan-guard-BFKg8VSd.mjs +58 -0
  23. package/dist/{pause-DssrYcda.mjs → pause-Cy11KrXK.mjs} +3 -3
  24. package/dist/post-C9soCx7i.mjs +46 -0
  25. package/dist/print-BaWiWf4v.mjs +19 -0
  26. package/dist/project-records-BRiyQzUo.mjs +230 -0
  27. package/dist/{relay-DASRgsTQ.mjs → relay-C0M1TWMB.mjs} +1 -1
  28. package/dist/run-B-j3Qxqc.mjs +1464 -0
  29. package/dist/{run-D8divGCs.mjs → run-D_u9V_Ve.mjs} +1 -1
  30. package/dist/{send-CXv8bx0i.mjs → send-qBxj0JbJ.mjs} +2 -2
  31. package/dist/{src-njdtejgZ.mjs → src-CXBmaGwS.mjs} +213 -75
  32. package/dist/{src-C7z_6fhn.mjs → src-DHDQ0itC.mjs} +106 -50
  33. package/dist/{src-tsuZRu0H.mjs → src-DN-RPsil.mjs} +5423 -4467
  34. package/dist/{src-8Mul0695.mjs → src-nXLIwDrt.mjs} +325 -81
  35. package/dist/store-Dyr1mP8K.mjs +16 -0
  36. package/dist/ui/assets/app-BPSCxb_l.css +1 -0
  37. package/dist/ui/assets/app-CnNRowFn.js +111 -0
  38. package/dist/ui/assets/{index-D-_uPI2v.js → index-CVzwYlz3.js} +2 -2
  39. package/dist/ui/index.html +1 -1
  40. package/dist/{ui-CNO1M0qT.mjs → ui-DLrSt5ny.mjs} +3 -3
  41. package/dist/{usage-BVXi0iFY.mjs → usage-CxmpYYO4.mjs} +61 -107
  42. package/dist/{context-Y_I4Mofq.mjs → validate-BVrT43GO.mjs} +35 -108
  43. package/dist/{watch-DVNIciHp.mjs → watch-mkLm4t9D.mjs} +19 -5
  44. package/dist/{workflows-B30QkwTk.mjs → workflows-Bvl2Fquu.mjs} +2 -2
  45. package/dist/{worktrees-DD4_TpkH.mjs → worktrees-DOOCoS2w.mjs} +49 -20
  46. package/package.json +8 -4
  47. package/dist/ui/assets/app-D_nAPnHM.css +0 -1
  48. package/dist/ui/assets/app-DqBAtXSZ.js +0 -111
package/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # @krapht/ork
2
2
 
3
- The `ork` command: orchestrate Claude agents on your machine with hierarchical,
4
- serializable workflows.
3
+ The `ork` command: orchestrate coding agents (Claude Code, OpenCode, Command
4
+ Code) on your machine with hierarchical, serializable workflows.
5
5
 
6
- > **Alpha.** Workflows of Claude agents and tools, run from the terminal or the
6
+ > **Alpha.** Workflows of coding agents and tools, run from the terminal or the
7
7
  > browser (`ork ui`). Expect changes until 1.0.0. See the
8
8
  > [project README](https://github.com/mimshins/ork#readme), the
9
9
  > [guides](https://github.com/mimshins/ork/tree/main/docs#readme) and
@@ -28,7 +28,13 @@ ork upgrade # a newer version? upgrade the way you installed Ork
28
28
 
29
29
  A workflow that names no agent driver runs on the one that's ready. Today that
30
30
  is `claude-code`: your own `claude`, logged in or with `ANTHROPIC_API_KEY` or a
31
- cloud provider set. `--driver claude-code` names it.
31
+ cloud provider set. Your own `opencode` (`open-code`) and Command Code's `cmd`
32
+ (`command-code`) work too: `--driver <id>`, or a driver per node
33
+ ([Drivers](https://github.com/mimshins/ork/blob/main/docs/guides/drivers.md)).
34
+
35
+ Ork is built with Ork: its own workflows plan, build, review and document its
36
+ features, and each run is measured against a manual session in
37
+ [Benchmarks: Ork building Ork](https://github.com/mimshins/ork/blob/main/benchmarks/README.md).
32
38
 
33
39
  Ork never stores or asks for credentials: it uses the tools you already have,
34
40
  signed in with your own accounts. Once a day it asks the npm registry whether a
@@ -0,0 +1,359 @@
1
+ import { C as compareVersions, T as parseVersion, w as isNewer } from "./src-DN-RPsil.mjs";
2
+ import path from "node:path";
3
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ // ../core/src/follow-ups/change.ts
6
+ /** The project's follow-up whose id is, or starts with, `prefix`. */
7
+ const findFollowUp = (rows, project, prefix) => {
8
+ const exact = rows.followUp(project, prefix);
9
+ if (exact !== void 0) return {
10
+ ok: true,
11
+ followUp: exact
12
+ };
13
+ const matches = prefix === "" ? [] : rows.followUps(project).filter((f) => f.id.startsWith(prefix));
14
+ const [only, other] = matches;
15
+ if (only === void 0) return {
16
+ ok: false,
17
+ error: `No follow-up \`${prefix}\` in this project.`
18
+ };
19
+ if (other !== void 0) return {
20
+ ok: false,
21
+ error: `\`${prefix}\` matches ${matches.map((f) => f.id).join(", ")}; give more of the id.`
22
+ };
23
+ return {
24
+ ok: true,
25
+ followUp: only
26
+ };
27
+ };
28
+ /** Which statuses each action starts from, and where it goes. */
29
+ const ACTIONS = {
30
+ done: {
31
+ from: ["waiting", "due"],
32
+ to: () => "done",
33
+ note: "done"
34
+ },
35
+ drop: {
36
+ from: ["waiting", "due"],
37
+ to: () => "dropped",
38
+ note: "dropped"
39
+ },
40
+ reopen: {
41
+ from: ["done", "dropped"],
42
+ to: (f) => f.due === "now" ? "due" : "waiting",
43
+ note: "reopened"
44
+ },
45
+ due: {
46
+ from: ["waiting"],
47
+ to: () => "due",
48
+ note: "marked due"
49
+ }
50
+ };
51
+ /** Why `action` can't be done to `f` now, or `undefined`. */
52
+ const refusal = (f, action) => {
53
+ const rule = ACTIONS[action];
54
+ return rule.from.includes(f.status) ? void 0 : `${f.id} is ${f.status}, so it can't be ${rule.note}.`;
55
+ };
56
+ const check = (f, action) => {
57
+ const error = refusal(f, action);
58
+ return error === void 0 ? { ok: true } : {
59
+ ok: false,
60
+ kind: "status",
61
+ why: `it is ${f.status}`,
62
+ error
63
+ };
64
+ };
65
+ /** Whether `to` names one GitHub issue or pull request by its URL. */
66
+ const isGithubIssueUrl = (to) => /^https:\/\/github\.com\/[^/\s]+\/[^/\s]+\/(?:issues|pull)\/\d+$/u.test(to);
67
+ /**
68
+ * Why `f`'s draft can't be posted to GitHub whatever its status, or
69
+ * `undefined`.
70
+ */
71
+ const githubDraftRefusal = (f) => {
72
+ if (f.kind !== "message" || f.draft?.channel !== "github") return {
73
+ why: "not drafted for the github channel",
74
+ error: "only a message drafted for the github channel is posted; send the others yourself"
75
+ };
76
+ return isGithubIssueUrl(f.draft.to) ? void 0 : {
77
+ why: "its `to` isn't a GitHub issue or pull request URL",
78
+ error: `${f.id}'s \`to\` (${f.draft.to}) isn't a GitHub issue or pull request URL, so it is not posted; copy the text and send it yourself.`
79
+ };
80
+ };
81
+ const postCheck = (f) => {
82
+ const open = f.status === "waiting" || f.status === "due";
83
+ const refused = githubDraftRefusal(f);
84
+ if (refused !== void 0) return {
85
+ ok: false,
86
+ kind: "draft",
87
+ ...refused
88
+ };
89
+ return open ? { ok: true } : {
90
+ ok: false,
91
+ kind: "status",
92
+ why: `it is ${f.status}`,
93
+ error: `${f.id} is ${f.status}, so it is not posted.`
94
+ };
95
+ };
96
+ const followUpActions = (f) => ({
97
+ done: check(f, "done"),
98
+ drop: check(f, "drop"),
99
+ reopen: check(f, "reopen"),
100
+ due: check(f, "due"),
101
+ post: postCheck(f)
102
+ });
103
+ /** Applies `action` to `followUp`, and keeps it. */
104
+ const changeFollowUp = (rows, followUp, action, now = () => (/* @__PURE__ */ new Date()).toISOString()) => {
105
+ const rule = ACTIONS[action];
106
+ const refused = refusal(followUp, action);
107
+ if (refused !== void 0) return {
108
+ ok: false,
109
+ error: refused
110
+ };
111
+ const { note: _old, ...rest } = followUp;
112
+ const changed = {
113
+ ...rest,
114
+ status: rule.to(followUp),
115
+ note: rule.note,
116
+ updatedAt: now()
117
+ };
118
+ rows.putFollowUp(changed);
119
+ return {
120
+ ok: true,
121
+ followUp: changed
122
+ };
123
+ };
124
+ /**
125
+ * Why a due follow-up is due, read from the note `due.ts` left: its ref merged,
126
+ * its date came, its ref closed unmerged, or a person marked it. `undefined`
127
+ * for one that is not due, or was due from the start.
128
+ */
129
+ const dueCause = (f) => {
130
+ if (f.status !== "due" || f.note === void 0) return void 0;
131
+ if (f.note.includes("closed without merging")) return "closed-unmerged";
132
+ if (f.note.endsWith(" merged")) return "merged";
133
+ if (f.note.startsWith("due on ")) return "overdue";
134
+ return f.note === "marked due" ? "marked" : void 0;
135
+ };
136
+ // ../core/src/remote/session.ts
137
+ const chattable = (type) => type === "agent.leader" || type === "agent.worker";
138
+ /** Reads `node`'s session from a run's events, and every node's last output. */
139
+ const nodeSession = (events, node) => {
140
+ const session = {
141
+ known: false,
142
+ chattable: false,
143
+ running: false
144
+ };
145
+ const outputs = {};
146
+ for (const { event } of events) {
147
+ if (event.type === "run.started") {
148
+ const declared = event.nodes.find((n) => n.id === node);
149
+ if (declared !== void 0) {
150
+ session.known = true;
151
+ session.chattable = chattable(declared.type);
152
+ }
153
+ }
154
+ if (event.type === "node.finished") outputs[event.id] = event.output;
155
+ if (!("id" in event) || event.id !== node) continue;
156
+ if (event.type === "node.started") {
157
+ session.known = true;
158
+ session.chattable = chattable(event.nodeType);
159
+ session.running = true;
160
+ }
161
+ if (event.type === "node.finished") session.running = false;
162
+ if (event.type === "worktree" && event.action === "created") session.checkout = {
163
+ path: event.path,
164
+ ...event.branch === void 0 ? {} : { branch: event.branch },
165
+ own: true,
166
+ owner: node
167
+ };
168
+ if (event.type !== "node.event") continue;
169
+ const inner = event.event;
170
+ if (inner.fork === true) continue;
171
+ if (inner.type === "session.started") {
172
+ session.sessionId = inner.sessionId;
173
+ if (inner.cwd !== void 0) session.cwd = inner.cwd;
174
+ if (inner.driver !== void 0) session.driver = inner.driver;
175
+ if (inner.model !== "") session.model = inner.model;
176
+ }
177
+ if (inner.type === "session.finished" && inner.outcome.sessionId !== "") session.sessionId = inner.outcome.sessionId;
178
+ }
179
+ return {
180
+ session,
181
+ outputs
182
+ };
183
+ };
184
+ /** The dist-tags every install follows, toward and after 1.0.0. */
185
+ const CHANNEL_TAGS = [
186
+ "latest",
187
+ "alpha",
188
+ "beta",
189
+ "rc"
190
+ ];
191
+ /** The dist-tag of snapshots from `main`, followed only by a canary. */
192
+ const CANARY_TAG = "canary";
193
+ const updateCachePath = (home = homedir()) => path.join(home, ".ork", "update-check.json");
194
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
195
+ const text = (record, key) => {
196
+ const value = record[key];
197
+ return typeof value === "string" ? value : void 0;
198
+ };
199
+ const notifiedOf = (value) => {
200
+ if (!isRecord(value)) return void 0;
201
+ const version = text(value, "version");
202
+ const at = text(value, "at");
203
+ return version === void 0 || at === void 0 ? void 0 : {
204
+ version,
205
+ at
206
+ };
207
+ };
208
+ /** The cache, or `null` when there is none or it cannot be read. */
209
+ const readUpdateCache = (file) => {
210
+ let parsed;
211
+ try {
212
+ parsed = JSON.parse(readFileSync(file, "utf8"));
213
+ } catch {
214
+ return null;
215
+ }
216
+ if (!isRecord(parsed)) return null;
217
+ const checkedAt = text(parsed, "checkedAt");
218
+ if (checkedAt === void 0) return null;
219
+ const latest = text(parsed, "latest");
220
+ const notified = notifiedOf(parsed["notified"]);
221
+ return {
222
+ checkedAt,
223
+ ...latest === void 0 ? {} : { latest },
224
+ ...notified === void 0 ? {} : { notified }
225
+ };
226
+ };
227
+ /** Writes the cache whole (a temporary file, then a rename). */
228
+ const writeUpdateCache = (file, cache) => {
229
+ mkdirSync(path.dirname(file), { recursive: true });
230
+ const temporary = `${file}.${process.pid}.tmp`;
231
+ writeFileSync(temporary, `${JSON.stringify(cache, null, 2)}\n`);
232
+ renameSync(temporary, file);
233
+ };
234
+ const age = (iso, now) => {
235
+ const time = Date.parse(iso);
236
+ return Number.isNaN(time) ? Infinity : now.getTime() - time;
237
+ };
238
+ /** Whether it is time to ask the registry again. */
239
+ const checkDue = (cache, now) => cache === null || age(cache.checkedAt, now) >= 864e5;
240
+ /** Whether `version` is a canary snapshot: its pre-release starts `canary`. */
241
+ const isCanary = (version) => parseVersion(version)?.pre[0]?.startsWith(CANARY_TAG) === true;
242
+ /**
243
+ * The newest version, by semver precedence, among the dist-tags `current`
244
+ * follows: the channel tags, and `canary` too when `current` is a canary. A tag
245
+ * whose value doesn't parse is skipped; `null` when none does.
246
+ */
247
+ const newestTagged = (tags, current) => {
248
+ return (current !== void 0 && isCanary(current) ? [...CHANNEL_TAGS, "canary"] : CHANNEL_TAGS).flatMap((tag) => {
249
+ const value = tags[tag];
250
+ const version = typeof value === "string" ? parseVersion(value) : null;
251
+ return typeof value === "string" && version !== null ? [{
252
+ value,
253
+ version
254
+ }] : [];
255
+ }).reduce((best, candidate) => best === null || compareVersions(candidate.version, best.version) > 0 ? candidate : best, null)?.value ?? null;
256
+ };
257
+ /**
258
+ * The newest version on the registry that the running version follows ({@link
259
+ * newestTagged}), or `null` on any failure.
260
+ */
261
+ const fetchLatest = async (name, options = {}) => {
262
+ const registry = (options.registry ?? "https://registry.npmjs.org").replace(/\/+$/u, "");
263
+ const get = options.fetch ?? globalThis.fetch;
264
+ try {
265
+ const response = await get(`${registry}/-/package/${name}/dist-tags`, {
266
+ headers: { accept: "application/json" },
267
+ signal: AbortSignal.timeout(options.timeoutMs ?? 5e3)
268
+ });
269
+ if (!response.ok) return null;
270
+ const tags = await response.json();
271
+ return isRecord(tags) ? newestTagged(tags, options.current) : null;
272
+ } catch {
273
+ return null;
274
+ }
275
+ };
276
+ /**
277
+ * Asks the registry and records the answer. A failed check is recorded too, so
278
+ * an offline machine is not asked again until the interval passes; the last
279
+ * known version is kept. `options.current` is the running version.
280
+ */
281
+ const refreshUpdateCache = async (file, name, now, options = {}) => {
282
+ const before = readUpdateCache(file);
283
+ const latest = await fetchLatest(name, options) ?? before?.latest;
284
+ const cache = {
285
+ ...before,
286
+ checkedAt: now.toISOString(),
287
+ ...latest === void 0 ? {} : { latest }
288
+ };
289
+ writeUpdateCache(file, cache);
290
+ return cache;
291
+ };
292
+ /** The newer version the cache knows of, if any. */
293
+ const newerVersion = (cache, current) => cache?.latest !== void 0 && isNewer(cache.latest, current) ? cache.latest : null;
294
+ /**
295
+ * The version to tell the user about now: newer than theirs, and not already
296
+ * told today. `null` when there is nothing to say.
297
+ */
298
+ const pendingNotice = (cache, current, now) => {
299
+ const newer = newerVersion(cache, current);
300
+ if (newer === null) return null;
301
+ const told = cache?.notified;
302
+ return told?.version === newer && age(told.at, now) < 864e5 ? null : newer;
303
+ };
304
+ // ../core/src/update/switches.ts
305
+ /**
306
+ * @file The environment's say over the update check (ADR-038): the variables
307
+ * that turn it off (`ORK_NO_UPDATE_CHECK`, the common `NO_UPDATE_NOTIFIER`,
308
+ * and `CI`), and the registry to ask instead of npm's. The CLI adds its own
309
+ * rules about where a notice may go; the server behind `ork ui` uses only
310
+ * these.
311
+ */
312
+ /** Turns the automatic check and its notice off. */
313
+ const NO_UPDATE_ENV = "ORK_NO_UPDATE_CHECK";
314
+ /** A registry to ask instead of npm's, e.g. a company mirror. */
315
+ const UPDATE_REGISTRY_ENV = "ORK_UPDATE_REGISTRY";
316
+ /** The variables that turn the check off, in the order they are looked at. */
317
+ const UPDATE_SWITCHES = [
318
+ NO_UPDATE_ENV,
319
+ "NO_UPDATE_NOTIFIER",
320
+ "CI"
321
+ ];
322
+ /** Set to anything but empty, `0` or `false`. */
323
+ const isSwitchedOn = (value) => value !== void 0 && value !== "" && value !== "0" && value.toLowerCase() !== "false";
324
+ /** The variable that turns the check off in `env`, or `null` when none does. */
325
+ const updateSwitchOff = (env) => UPDATE_SWITCHES.find((name) => isSwitchedOn(env[name])) ?? null;
326
+ /** The registry `ORK_UPDATE_REGISTRY` names, if it names one. */
327
+ const registryOf = (env) => {
328
+ const registry = env[UPDATE_REGISTRY_ENV];
329
+ return registry === void 0 || registry === "" ? void 0 : registry;
330
+ };
331
+ /** The host of a registry that isn't an http(s) URL, past any `user:pass@`. */
332
+ const hostOf = (registry) => registry.replace(/^[a-z][\w+.-]*:\/\//iu, "").replace(/^[^/?#]*@/u, "").split(/[/?#]/u)[0] ?? "";
333
+ /**
334
+ * The registry as it may be shown (on a page, in a message): an http(s) URL
335
+ * without its user name and password, which a mirror's URL may carry; anything
336
+ * else only as its host.
337
+ */
338
+ const registryShown = (registry) => {
339
+ try {
340
+ const url = new URL(registry);
341
+ if (url.protocol === "http:" || url.protocol === "https:") {
342
+ if (url.username === "" && url.password === "") return registry;
343
+ url.username = "";
344
+ url.password = "";
345
+ return url.href;
346
+ }
347
+ } catch {}
348
+ return hostOf(registry);
349
+ };
350
+ // src/__build_info__.ts
351
+ /**
352
+ * @file The published package's name and version, bound to
353
+ * `packages/cli/package.json`. Do not edit by hand: `pnpm changesets:version`
354
+ * and `pnpm build` rewrite it (`tools/release/build-info.ts`), and a test
355
+ * fails when it is stale.
356
+ */
357
+ const ORK_NAME = "@krapht/ork";
358
+ const ORK_VERSION = "1.0.0-alpha.16";
359
+ export { dueCause as _, registryShown as a, githubDraftRefusal as b, fetchLatest as c, readUpdateCache as d, refreshUpdateCache as f, changeFollowUp as g, nodeSession as h, registryOf as i, newerVersion as l, writeUpdateCache as m, ORK_VERSION as n, updateSwitchOff as o, updateCachePath as p, isSwitchedOn as r, checkDue as s, ORK_NAME as t, pendingNotice as u, findFollowUp as v, isGithubIssueUrl as x, followUpActions as y };
@@ -0,0 +1,17 @@
1
+ // ../core/src/list-filters/apply.ts
2
+ /**
3
+ * @file Keeps the rows a list's filter matches (RFC-017). The list's own order
4
+ * and `--limit` stay the caller's: filter, then order, then limit.
5
+ */
6
+ const fieldMatches = (field, row, wanted) => {
7
+ if (wanted === void 0 || wanted === "all" || field.get === void 0) return true;
8
+ const got = field.get(row);
9
+ if (got === void 0) return false;
10
+ const values = typeof got === "string" ? [got] : got;
11
+ if (field.type === "since") return values.some((value) => Date.parse(value) >= Date.parse(wanted));
12
+ if (field.type === "until") return values.some((value) => Date.parse(value) < Date.parse(wanted));
13
+ return values.includes(wanted);
14
+ };
15
+ /** The rows of `rows` every field of `filter` matches, in their order. */
16
+ const applyFilters = (table, rows, filter) => rows.filter((row) => table.fields.every((field) => fieldMatches(field, row, filter[field.name])));
17
+ export { applyFilters as t };
@@ -1,8 +1,9 @@
1
- import "./src-tsuZRu0H.mjs";
2
- import { l as createTerminalHuman, n as attachRun, t as withStore } from "./store-CleF7KeS.mjs";
3
- import { t as findRun } from "./find-BVX-bL4u.mjs";
1
+ import "./src-DN-RPsil.mjs";
2
+ import { l as createTerminalHuman, t as attachRun } from "./attach-B-FeE1dJ.mjs";
3
+ import { t as findRun } from "./find-mLYsR0uC.mjs";
4
4
  import { c as write, l as writeErr, s as style } from "./output-B1kwo0YU.mjs";
5
- import { n as attachRefusal } from "./in-a-run-cL5N9gpH.mjs";
5
+ import { n as attachRefusal } from "./in-a-run-a447hxq8.mjs";
6
+ import { t as withStore } from "./store-Dyr1mP8K.mjs";
6
7
  import { defineCommand } from "citty";
7
8
  // src/commands/attach.ts
8
9
  /**
@@ -1,4 +1,5 @@
1
- import { $n as maskSecrets, An as END_KINDS, Dn as endOf, H as isNodeEvent, On as outcomeEnd, Tt as followEvents, U as openStore, cn as slotWaitLine, ct as followRunTree, lt as answerDecision, nn as lockWaitLine, ut as pendingDecisions } from "./src-tsuZRu0H.mjs";
1
+ import { Ft as slotWaitLine, Gn as endOf, I as isNodeEvent, Jn as END_KINDS, Kn as outcomeEnd, kt as lockWaitLine, nt as answerDecision, rt as pendingDecisions, tt as followRunTree, ut as followEvents, vr as maskSecrets } from "./src-DN-RPsil.mjs";
2
+ import { t as applyFilters } from "./apply-Mb0fNpCX.mjs";
2
3
  import { C as termOf, D as wrap, E as widthOf, O as readKeys, _ as interrupt, a as plain, b as canKey, g as createRegion, h as paint, m as bold, n as cost, p as apply, r as costOf, s as style, u as confirmOrStop, v as readLine, w as cut, x as plainEnv, y as withdrawn } from "./output-B1kwo0YU.mjs";
3
4
  import { n as chip, r as nodeChip, t as CHIP_LABELS } from "./chips-BqcT58qr.mjs";
4
5
  import { a as createView, i as formatBusEvent, n as nodeVerdict, o as styler, r as firstLine, t as hasConflict } from "./verdict-CMvPCpWC.mjs";
@@ -769,8 +770,9 @@ const createTerminalHuman = (io) => {
769
770
  /**
770
771
  * @file How a node's end reads on the terminal (specs/runtime/session-ends.md):
771
772
  * its kind's label and Ork's own message, a note for a kind that may pass,
772
- * the setup hint for an account or provider end, and the scrubbed raw detail
773
- * only when asked for. Never the provider's own words first.
773
+ * the setup hint for an account or provider end, the last line of a failed
774
+ * tool's own stderr, and the scrubbed raw detail only when asked for. Never
775
+ * the provider's own words first.
774
776
  */
775
777
  /** What a transient kind says in place of its hint on the log line. */
776
778
  const MAY_PASS = "(may pass; ork resume retries)";
@@ -779,11 +781,17 @@ const endHeadline = (end) => {
779
781
  const { label, transient } = END_KINDS[end.kind];
780
782
  return `${label}: ${end.message}${transient ? ` ${MAY_PASS}` : ""}`;
781
783
  };
784
+ /** The longest a script's last stderr line is shown on the log line. */
785
+ const LAST_LINE_LIMIT = 200;
786
+ /** The last non-empty line of a scrubbed detail, cut; "" when there is none. */
787
+ const lastLine = (detail) => (detail ?? "").split("\n").map((line) => line.trim()).findLast((line) => line !== "")?.slice(0, LAST_LINE_LIMIT) ?? "";
782
788
  /**
783
789
  * The lines under a headline: the hint for an account or provider end (what to
784
790
  * set up) or whenever `full` (`--verbose`, one node's log), and the detail only
785
791
  * when `full`. A transient kind's hint is its retry wording, which the headline
786
- * already says, so it is not printed twice.
792
+ * already says, so it is not printed twice. A failed tool (`command_failed`)
793
+ * also shows the last line of its detail without `full`: a script's stderr is
794
+ * the workflow's own message, not the provider's.
787
795
  */
788
796
  const endLines = (end, full, term = createView().term) => {
789
797
  const style = styler(term);
@@ -792,6 +800,10 @@ const endLines = (end, full, term = createView().term) => {
792
800
  const lines = [];
793
801
  if (hint !== "" && !transient && (full || setup)) lines.push(` ${style("dim", hint)}`);
794
802
  if (full && end.detail !== void 0) lines.push(style("dim", " details:"), ...end.detail.split("\n").map((line) => style("dim", ` ${line}`)));
803
+ else if (end.kind === "command_failed") {
804
+ const last = lastLine(end.detail);
805
+ if (last !== "") lines.push(` ${style("dim", last)}`);
806
+ }
795
807
  return lines;
796
808
  };
797
809
  /**
@@ -1027,7 +1039,8 @@ const formatRunEvent = (event, width, tools, terminal = false, verbose = false,
1027
1039
  if (event.type === "node.started") {
1028
1040
  const round = event.round > 1 ? ` (round ${String(event.round)})` : "";
1029
1041
  const owner = event.owner === void 0 ? "" : `, for ${event.owner}`;
1030
- return [`${tag(event.id)} ${style("dim", `${event.nodeType}${round}${owner}`)}`];
1042
+ const named = event.description === void 0 ? "" : ` · ${event.description}`;
1043
+ return [`${tag(event.id)} ${style("dim", `${event.nodeType}${round}${owner}${named}`)}`];
1031
1044
  }
1032
1045
  if (event.type === "node.event") {
1033
1046
  const inner = event.event;
@@ -1110,7 +1123,7 @@ const formatRunEvent = (event, width, tools, terminal = false, verbose = false,
1110
1123
  /** Turns stored events into lines, keeping the state a transcript needs. */
1111
1124
  const createLogPrinter = (options) => {
1112
1125
  const tools = /* @__PURE__ */ new Map();
1113
- const view = createView();
1126
+ const view = options.view ?? createView();
1114
1127
  let width = 8;
1115
1128
  return (stored) => {
1116
1129
  const { event } = stored;
@@ -1130,6 +1143,19 @@ const unknownNode = (stored, node, live) => {
1130
1143
  const declared = stored.some(({ event }) => event.type === "run.started" && event.nodes.some((n) => n.id === node));
1131
1144
  return named || declared || live ? null : `Run has no node ${node}.`;
1132
1145
  };
1146
+ /** `ork logs`'s filter flags (RFC-017): an event's time; no ids. */
1147
+ const logListTable = { fields: [{
1148
+ name: "since",
1149
+ description: "Only events since an age (24h, 30d, 12w), a day (2026-11-01), or all",
1150
+ type: "since",
1151
+ all: true,
1152
+ get: (stored) => stored.at
1153
+ }, {
1154
+ name: "until",
1155
+ description: "Only events on or before this day (2026-11-01)",
1156
+ type: "until",
1157
+ get: (stored) => stored.at
1158
+ }] };
1133
1159
  /**
1134
1160
  * Prints a run's events; with `follow`, then each new one as it is stored:
1135
1161
  * pushed over the run's socket, or polled from the store when there is none.
@@ -1138,8 +1164,13 @@ const unknownNode = (stored, node, live) => {
1138
1164
  */
1139
1165
  const followRun = async (store, run, out, options) => {
1140
1166
  const print = createLogPrinter(options);
1167
+ const matches = (stored) => applyFilters(logListTable, [stored], options.filter ?? {}).length > 0 && (options.node === void 0 || isNodeEvent(stored.event, options.node));
1168
+ const replayed = options.limit === void 0 ? [] : store.events(run.id, options.after ?? 0).filter((stored) => matches(stored)).slice(0, -options.limit);
1169
+ const dropped = new Set(replayed.map((stored) => stored.seq));
1141
1170
  if (await followEvents(store, run.id, (stored) => {
1142
- for (const line of print(stored)) out(line);
1171
+ const lines = print(stored);
1172
+ if (!matches(stored) || dropped.has(stored.seq)) return;
1173
+ for (const line of lines) out(line);
1143
1174
  }, {
1144
1175
  follow: options.follow,
1145
1176
  ...options.after === void 0 ? {} : { after: options.after },
@@ -1197,13 +1228,15 @@ const askStored = async (store, human, decision, { place, by, signal, home }) =>
1197
1228
  * asked through one `TerminalHuman`, withdrawn when it closes elsewhere.
1198
1229
  */
1199
1230
  const where = (path) => path.join(" › ");
1231
+ /** The gutter before a called run's lines: its call path, then ` › `. */
1232
+ const gutterOf = (path) => path.length === 0 ? "" : `${where(path)} › `;
1200
1233
  /**
1201
1234
  * Prints the events of `root` and of the runs it called until the root ends or
1202
1235
  * `signal` aborts, and asks each open decision with `options.human`.
1203
1236
  */
1204
1237
  const attachRun = async (store, root, out, options = {}) => {
1205
1238
  const { human, basePath = [] } = options;
1206
- const { term } = createView();
1239
+ const { term } = options.view ?? createView();
1207
1240
  const say = styler(term);
1208
1241
  const printers = /* @__PURE__ */ new Map();
1209
1242
  const prompts = /* @__PURE__ */ new Map();
@@ -1215,9 +1248,25 @@ const attachRun = async (store, root, out, options = {}) => {
1215
1248
  succeeded: false
1216
1249
  };
1217
1250
  const show = (path, lines) => {
1218
- const prefix = path.length === 0 ? "" : paint(term, "muted", `${where(path)} › `);
1251
+ const gutter = gutterOf(path);
1252
+ const prefix = gutter === "" ? "" : paint(term, "muted", gutter);
1219
1253
  for (const line of lines) out(line === "" ? line : `${prefix}${line}`);
1220
1254
  };
1255
+ /**
1256
+ * A run's printer, laid out in the columns its gutter leaves, so a line fits
1257
+ * the terminal with the gutter before it and a wrapped line goes on under its
1258
+ * own text, not at the terminal's first column.
1259
+ */
1260
+ const printerFor = (path) => createLogPrinter({
1261
+ json: false,
1262
+ view: {
1263
+ term: {
1264
+ ...term,
1265
+ width: Math.max(20, term.width - widthOf(gutterOf(path)))
1266
+ },
1267
+ roots: /* @__PURE__ */ new Map()
1268
+ }
1269
+ });
1221
1270
  const ask = (run, path, id) => {
1222
1271
  const decision = store.decision(id);
1223
1272
  if (human === void 0 || decision === void 0) return;
@@ -1277,7 +1326,7 @@ const attachRun = async (store, root, out, options = {}) => {
1277
1326
  }
1278
1327
  let print = printers.get(run);
1279
1328
  if (print === void 0) {
1280
- print = createLogPrinter({ json: false });
1329
+ print = printerFor(path);
1281
1330
  printers.set(run, print);
1282
1331
  }
1283
1332
  show(path, print(stored));
@@ -1296,18 +1345,4 @@ const attachRun = async (store, root, out, options = {}) => {
1296
1345
  exitCode: end === "aborted" || seen.succeeded ? 0 : 1
1297
1346
  };
1298
1347
  };
1299
- // src/store.ts
1300
- /**
1301
- * @file Opening the user's store (`~/.ork/state.db`) for the length of one
1302
- * command, and closing it after, whatever happens.
1303
- */
1304
- /** Runs `body` with the user's store open, and closes it after. */
1305
- const withStore = async (body) => {
1306
- const store = openStore();
1307
- try {
1308
- await body(store);
1309
- } finally {
1310
- store.close();
1311
- }
1312
- };
1313
- export { unknownNode as a, callsShown as c, createDecisionQueue as d, followRun as i, createTerminalHuman as l, attachRun as n, formatEvent as o, askStored as r, formatRunEvent as s, withStore as t, readText as u };
1348
+ export { unknownNode as a, callsShown as c, createDecisionQueue as d, logListTable as i, createTerminalHuman as l, askStored as n, formatEvent as o, followRun as r, formatRunEvent as s, attachRun as t, readText as u };