@krapht/ork 1.0.0-alpha.2 → 1.0.0-alpha.21

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 (75) hide show
  1. package/README.md +12 -7
  2. package/dist/__build_info__-0wqQ-rtf.mjs +180 -0
  3. package/dist/apply-Mb0fNpCX.mjs +17 -0
  4. package/dist/argument-error-CihH46Le.mjs +27 -0
  5. package/dist/attach-C0-YyNoY.mjs +92 -0
  6. package/dist/attach-CAKNqnUK.mjs +1259 -0
  7. package/dist/bin.mjs +1885 -1609
  8. package/dist/brief-C2hew8cS.mjs +116 -0
  9. package/dist/{decide-CGyx9rv7.mjs → builtin-files-YYAnbZGD.mjs} +3 -51
  10. package/dist/chips-CP_pgKlj.mjs +48 -0
  11. package/dist/command-tree-CEkzWNy6.mjs +63 -0
  12. package/dist/{completion-CSU5vkEt.mjs → completion-K4rAVsrf.mjs} +200 -30
  13. package/dist/context-B-fjPJky.mjs +106 -0
  14. package/dist/context-DMK0U1w0.mjs +160 -0
  15. package/dist/detached-q0AYG0Bs.mjs +39 -0
  16. package/dist/extra-arguments-BxQMXARO.mjs +13 -0
  17. package/dist/{find-D6Gj1sxg.mjs → find-Ct5CNl1J.mjs} +2 -2
  18. package/dist/flags-C90N4J1i.mjs +145 -0
  19. package/dist/follow-ups-D3HPyNYM.mjs +568 -0
  20. package/dist/format-CiPhQqbX.mjs +260 -0
  21. package/dist/{help-groups-BwYoAWsh.mjs → help-groups-BQvcKQ9H.mjs} +49 -78
  22. package/dist/in-a-run-D_wg4iJi.mjs +27 -0
  23. package/dist/install-Hji6LIei.mjs +259 -0
  24. package/dist/{limits-B1-DYc3d.mjs → limits-BD6RZRCS.mjs} +2 -2
  25. package/dist/{mcp-DE_EmyVm.mjs → mcp-CpbDmBLG.mjs} +6 -6
  26. package/dist/once-J8Qn5fTG.mjs +112 -0
  27. package/dist/orphan-guard-BFKg8VSd.mjs +58 -0
  28. package/dist/output-DXZbnFt_.mjs +458 -0
  29. package/dist/outputs-text-CNTJ7Qq_.mjs +19 -0
  30. package/dist/{pause-CL6N_Xrj.mjs → pause-Cl4ZYA35.mjs} +7 -4
  31. package/dist/print-DzugGZQB.mjs +19 -0
  32. package/dist/project-records-CtkmlsHN.mjs +233 -0
  33. package/dist/refusals-DQQX3AIb.mjs +116 -0
  34. package/dist/{relay-0SD0erwf.mjs → relay-CiP77vga.mjs} +2 -2
  35. package/dist/run-CtuNY3mE.mjs +234 -0
  36. package/dist/send-DRYGks8K.mjs +104 -0
  37. package/dist/short-flags-DkJLWVqh.mjs +1 -0
  38. package/dist/short-flags-Qf21as0E.mjs +83 -0
  39. package/dist/src-BMFQKHz4.mjs +3122 -0
  40. package/dist/{src-BbyEoMn4.mjs → src-BrFfDKux.mjs} +800 -235
  41. package/dist/{src-Dujmvv4s.mjs → src-BuF9Lbow.mjs} +12580 -5505
  42. package/dist/{src-DOcK8Jfp.mjs → src-k0A_QpST.mjs} +1024 -590
  43. package/dist/store-rTgRu8xy.mjs +25 -0
  44. package/dist/ui/THIRD_PARTY_LICENSES.txt +612 -0
  45. package/dist/ui/assets/app-BYxpKJRh.css +1 -0
  46. package/dist/ui/assets/app-SEFEJLkF.js +112 -0
  47. package/dist/ui/assets/{index-9dFha9Qn.js → index-Cb6Voq_W.js} +3 -3
  48. package/dist/ui/assets/theme-CdXvqTQ6.js +1 -0
  49. package/dist/ui/index.html +2 -2
  50. package/dist/{ui-CTmO2Xzu.mjs → ui-C2f37TSb.mjs} +57 -25
  51. package/dist/usage-BuvGcQSk.mjs +256 -0
  52. package/dist/validate-ObP5lt29.mjs +1 -0
  53. package/dist/validate-_HjACtk9.mjs +632 -0
  54. package/dist/verdict-B592j1W3.mjs +84 -0
  55. package/dist/{watch-CtbgDAcz.mjs → watch-DV3ixCYj.mjs} +45 -17
  56. package/dist/{workflows-D1_WVukk.mjs → workflows-QLjBhGNS.mjs} +30 -5
  57. package/dist/worktrees-D3ubG_Qw.mjs +302 -0
  58. package/dist/yolo-BSH1E_kE.mjs +33 -0
  59. package/package.json +10 -8
  60. package/dist/__build_info__-fy6L1Mrd.mjs +0 -615
  61. package/dist/clean-DaHjs__2.mjs +0 -88
  62. package/dist/context-DT8MfEIR.mjs +0 -274
  63. package/dist/format-Dnfv8zMT.mjs +0 -427
  64. package/dist/in-a-run-pLAZeNys.mjs +0 -22
  65. package/dist/list-BrH8KRSJ.mjs +0 -110
  66. package/dist/output-BLQWD6MJ.mjs +0 -950
  67. package/dist/remove-BeGdqroy.mjs +0 -194
  68. package/dist/report-DK4LGqwF.mjs +0 -129
  69. package/dist/send-C8tunWhv.mjs +0 -163
  70. package/dist/src-CCUKRX51.mjs +0 -1204
  71. package/dist/ui/assets/app-BNT0SuIz.css +0 -1
  72. package/dist/ui/assets/app-T7wVAhgx.js +0 -67
  73. package/dist/ui/assets/theme-3xH5jxKs.js +0 -1
  74. package/dist/usage-DYUmP0i4.mjs +0 -419
  75. package/dist/worktrees-DdMYoNOC.mjs +0 -212
@@ -1,152 +1,87 @@
1
- import { B as linkNodes, Cn as runDirOf, Ct as walkChecks, En as inside, Fn as applyEdits, G as requestContinue, Gn as expandEdge, Hn as EdgeInputSchema, Ht as planPost, In as newWorkflowText, J as tellLiveRun, K as requestPause, Ln as readDocument, Nn as skillsIn, Nt as nodeSays, On as LESSON_CAP_TOKENS, Pn as WorkflowEditSchema, Rn as renderJsonSchema, St as checkWorkflowText, Tn as workflowDirOf, U as unlinkNodes, Ut as runGh, Vt as DISCUSSIONS, Wt as sendPost, _t as runStatus, bn as placeFiles, bt as processAlive, ct as explainWorkflowText, d as runningRunIds, f as runningScript, gt as pausedOf, hn as loadAgents, ht as liveStatus, l as planUpgrade, nt as resumable, o as limitsReport, ot as detachRun, p as detectInstaller, q as requestStop, qn as NodeSchema, r as capsHere, st as followEvents, tn as readCondition, tt as detachResume, u as releaseNotes, v as triggersReport, vt as runTree, wt as walkSummary, xn as placeFor, zn as parseWorkflow } from "./src-Dujmvv4s.mjs";
2
- import { C as feedbackActions, S as placeOf$1, _ as previewPost, a as registryShown, b as acceptItem, c as checkDue, f as readUpdateCache, g as nodeSession, h as writeUpdateCache, i as registryOf, l as fetchLatest, m as updateCachePath, o as updateSwitchOff, p as refreshUpdateCache, s as runInstall, u as newerVersion, w as openFixRun, x as lessonPlaceOf, y as recordNote } from "./__build_info__-fy6L1Mrd.mjs";
3
- import { n as pendingDecisions, r as builtinProblem, t as answerDecision } from "./decide-CGyx9rv7.mjs";
4
- import { n as newRunId, t as findRun } from "./find-D6Gj1sxg.mjs";
5
- import { c as openStore, l as storePath, s as nodeOf } from "./output-BLQWD6MJ.mjs";
6
- import { n as usageReport } from "./report-DK4LGqwF.mjs";
7
- import { a as planClean, i as keyOf, o as stepActs, r as cleanStep, s as stepPrunes, t as applyClean } from "./clean-DaHjs__2.mjs";
8
- import { a as readWorktrees, c as planLine, i as projectHead, l as removeRefusal, n as lostWork, o as labelOf, r as planRemove, s as mergeStateOf, t as applyRemove, u as stateParts } from "./remove-BeGdqroy.mjs";
1
+ import { $n as findFollowUp, $t as planClean, An as requestContinue, C as detectInstaller, Et as walkSummary, Gn as nestItems, Gt as feedbackListTable, Hn as processAlive, Hr as loadAgents, I as activeTree, In as pausedOf, Jt as usageListTable, Kr as placeFiles, Kt as followUpListTable, L as isNodeEvent, Ln as runStatus, Mn as requestStop, Nn as tellLiveRun, O as triggersReport, On as linkNodes, Pn as liveStatus, Qn as dueCause, Qr as inside, Qt as keyOf, R as openStore, S as runningScript, Si as NodeSchema, Tn as DEFAULT_FOLLOW_UPS_RULE, Tr as readCondition, Tt as walkChecks, Ut as LIST_TABLES, Vn as yoloOf, Wt as decisionListTable, Xt as applyClean, Yn as loadProject, Yr as runDirOf, Yt as worktreeListTable, Zn as changeFollowUp, Zr as workflowDirOf, Zt as cleanStep, _ as usageReport, _i as EdgeInputSchema, _n as postFollowUpMessage, _r as sendPost, _t as limitsReport, a as planRemove, ai as skillsIn, b as releaseNotes, bi as expandEdge, br as feedbackActions, ci as newWorkflowText, cr as previewPost, d as mergeStateOf, dt as resumable, ei as LESSON_CAP_TOKENS, en as stepActs, er as followUpActions, f as planLine, gr as runGh, hr as planPost, i as lostWork, jn as requestPause, kn as unlinkNodes, li as readDocument, m as stateParts, mi as parseWorkflow, mr as DISCUSSIONS, mt as followEvents, o as projectHead, oi as WorkflowEditSchema, or as END_KINDS, ot as answerDecision, p as removeRefusal, pi as renderJsonSchema, pt as detachRun, qr as placeFor, qt as runListTable, r as applyRemove, rr as endOf, s as readWorktrees, si as applyEdits, st as pendingDecisions, tn as stepPrunes, u as labelOf, un as nodeSays, ur as recordNote, ut as detachResume, v as capsHere, vn as checkFollowUpsDue, wn as liveSessionOf, wt as checkWorkflowText, x as runningRunIds, xn as startFollowUpChecks, xr as openFixRun, y as planUpgrade, yt as explainWorkflowText, z as storePath } from "./src-BuF9Lbow.mjs";
2
+ import { a as placeOf$1, i as lessonPlaceOf, n as nodeSession, r as acceptItem, t as runInstall } from "./install-Hji6LIei.mjs";
3
+ import { t as applyFilters } from "./apply-Mb0fNpCX.mjs";
4
+ import { i as parseLimit, n as unknownProjectRefusal, r as parseFilters, t as limitRefusal } from "./refusals-DQQX3AIb.mjs";
5
+ import { t as builtinProblem } from "./builtin-files-YYAnbZGD.mjs";
6
+ import { n as newRunId, t as findRun } from "./find-Ct5CNl1J.mjs";
7
+ import { t as outputsText } from "./outputs-text-CNTJ7Qq_.mjs";
8
+ import { a as registryShown, c as fetchLatest, d as readUpdateCache, f as refreshUpdateCache, i as registryOf, l as newerVersion, m as writeUpdateCache, o as updateSwitchOff, p as updateCachePath, s as checkDue } from "./__build_info__-0wqQ-rtf.mjs";
9
9
  import path from "node:path";
10
10
  import { z } from "zod";
11
11
  import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
12
12
  import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
13
13
  import { homedir } from "node:os";
14
14
  import { createServer } from "node:http";
15
- // ../core/src/store/activity.ts
16
- const LONGEST_LINE = 120;
15
+ // ../server/src/contract-lists.ts
16
+ /** A filter's value: any text; the handler checks it with core. */
17
+ const text = () => z.string().min(1).optional();
17
18
  /**
18
- * The fields of a tool's input that say what it's working on, best first. Only
19
- * these are shown, so what else a tool was given stays in the logs.
19
+ * A `since` or `until`: `all`, an age (`24h`), a day, or an ISO time. Read with
20
+ * core by the handler, so a bad one is refused in the CLI's words.
20
21
  */
21
- const TARGETS = [
22
- "file_path",
23
- "notebook_path",
24
- "path",
25
- "url",
26
- "pattern",
27
- "query",
28
- "command"
29
- ];
30
- const oneLine = (text) => {
31
- const line = text.replaceAll(/\s+/gu, " ").trim();
32
- return line.length > LONGEST_LINE ? `${line.slice(0, 119)}…` : line;
33
- };
34
- const toolLine = ({ name, input }) => {
35
- const target = TARGETS.map((field) => input[field]).find((value) => typeof value === "string" && value.trim() !== "");
36
- if (typeof target !== "string") return name;
37
- return oneLine(target.startsWith(`${name} `) ? target : `${name} ${target}`);
38
- };
39
- /** What a finished node said, or why it failed. */
40
- const finishedNow = (ok, error, summary) => {
41
- if (!ok) return error === void 0 ? void 0 : oneLine(error);
42
- return typeof summary === "string" && summary.trim() !== "" ? oneLine(summary) : void 0;
43
- };
44
- /** Each node's activity, by id; a node that never started has none. */
45
- const runActivity = (stored) => {
46
- const nodes = /* @__PURE__ */ new Map();
47
- const of = (id) => {
48
- const found = nodes.get(id);
49
- if (found !== void 0) return found;
50
- const fresh = {};
51
- nodes.set(id, fresh);
52
- return fresh;
53
- };
54
- for (const { at, event } of stored) if (event.type === "node.started") {
55
- const node = of(event.id);
56
- node.startedAt ??= at;
57
- delete node.finishedAt;
58
- delete node.now;
59
- } else if (event.type === "node.event") {
60
- if (event.event.type === "tool.called") of(event.id).now = toolLine(event.event);
61
- } else if (event.type === "decision.opened") of(event.node).now = oneLine(`Asked: ${event.question}`);
62
- else if (event.type === "decision.answered") of(event.node).now = oneLine(`Answered: ${event.answer}`);
63
- else if (event.type === "decision.withdrawn") delete of(event.node).now;
64
- else if (event.type === "node.finished") {
65
- const node = of(event.id);
66
- node.finishedAt = at;
67
- const now = finishedNow(event.ok, event.error, event.output["summary"]);
68
- if (now === void 0) delete node.now;
69
- else node.now = now;
70
- }
71
- return nodes;
72
- };
73
- /** The ownership tree, with what each node is doing and when it ran. */
74
- const activeTree = (stored) => {
75
- const activity = runActivity(stored);
76
- const dress = (node) => {
77
- Object.assign(node, activity.get(node.id));
78
- for (const child of node.children) dress(child);
79
- };
80
- const roots = runTree(stored.map(({ event }) => event));
81
- for (const root of roots) dress(root);
82
- return roots;
83
- };
84
- // ../server/src/contract-builder.ts
85
- /** A new workflow's name, as its file and its `name:` take it. */
86
- const NewWorkflowSchema = z.strictObject({ name: z.string().min(1) });
87
- /** The server checks each edit; this file imports only types of the schema. */
88
- const WorkflowEditsSchema = z.strictObject({ edits: z.array(z.custom()).min(1) });
89
- /** Earlier text, and the file it was based on, to ask before replacing it. */
90
- const DraftTextSchema = z.strictObject({
91
- text: z.string(),
92
- baseHash: z.string().nullable()
93
- });
94
- const SaveWorkflowSchema = z.strictObject({
95
- /** Write over a file that changed since this draft began. */
96
- force: z.boolean().optional() });
97
- const BUILDER_API = {
98
- builderSchema: {
99
- method: "GET",
100
- path: "/api/workflow-schema"
101
- },
102
- builderCatalog: {
103
- method: "GET",
104
- path: "/api/projects/:id/catalog"
105
- },
106
- builderDrafts: {
107
- method: "GET",
108
- path: "/api/projects/:id/builder"
109
- },
110
- builderView: {
111
- method: "GET",
112
- path: "/api/projects/:id/builder/:name"
113
- },
114
- builderCreate: {
115
- method: "POST",
116
- path: "/api/projects/:id/builder",
117
- body: NewWorkflowSchema
22
+ const time = text;
23
+ /** Each list's filter fields, by flag name, in its core table's order. */
24
+ const LIST_SHAPES = {
25
+ runs: {
26
+ status: text(),
27
+ workflow: text(),
28
+ project: text(),
29
+ since: time(),
30
+ until: time()
118
31
  },
119
- builderEdit: {
120
- method: "POST",
121
- path: "/api/projects/:id/builder/:name/edits",
122
- body: WorkflowEditsSchema
32
+ feedback: {
33
+ status: text(),
34
+ kind: text(),
35
+ severity: text(),
36
+ workflow: text(),
37
+ run: text()
123
38
  },
124
- builderRestore: {
125
- method: "POST",
126
- path: "/api/projects/:id/builder/:name/text",
127
- body: DraftTextSchema
39
+ "follow-ups": {
40
+ status: text(),
41
+ for: text(),
42
+ kind: text(),
43
+ due: text(),
44
+ project: text()
128
45
  },
129
- builderDiscard: {
130
- method: "POST",
131
- path: "/api/projects/:id/builder/:name/discard"
46
+ decisions: {
47
+ workflow: text(),
48
+ run: text(),
49
+ project: text()
132
50
  },
133
- builderSave: {
134
- method: "POST",
135
- path: "/api/projects/:id/builder/:name/save",
136
- body: SaveWorkflowSchema
137
- }
138
- };
139
- // ../server/src/contract-context.ts
140
- const CONTEXT_API = {
141
- runContext: {
142
- method: "GET",
143
- path: "/api/runs/:id/nodes/:node/context"
51
+ worktrees: {
52
+ run: text(),
53
+ project: text()
144
54
  },
145
- builderContext: {
146
- method: "GET",
147
- path: "/api/projects/:id/builder/:name/nodes/:node/context"
55
+ usage: {
56
+ workflow: text(),
57
+ project: text(),
58
+ since: time(),
59
+ until: time()
148
60
  }
149
61
  };
62
+ /** A list's filter fields as query params: its core table's, each optional. */
63
+ const listShape = (list) => LIST_SHAPES[list];
64
+ Object.keys(LIST_SHAPES.runs), Object.keys(LIST_SHAPES.feedback), Object.keys(LIST_SHAPES["follow-ups"]), Object.keys(LIST_SHAPES.decisions), Object.keys(LIST_SHAPES.worktrees), Object.keys(LIST_SHAPES.usage);
65
+ const Count$1 = z.coerce.number().int().positive();
66
+ /** `GET /api/runs`: `runListTable`'s fields, and the newest `limit`. */
67
+ const RunsQuerySchema = z.strictObject({
68
+ limit: Count$1.optional(),
69
+ ...LIST_SHAPES.runs
70
+ });
71
+ /**
72
+ * `GET /api/usage`: `usageListTable`'s fields; `since` and `until` also take
73
+ * ISO times (`since` included, `until` not), as before.
74
+ */
75
+ const UsageQuerySchema = z.strictObject(LIST_SHAPES.usage);
76
+ /**
77
+ * `GET /api/decisions`: `decisionListTable`'s fields; `workflow` is ANDed with
78
+ * `run` or `project`.
79
+ */
80
+ const DecisionsQuerySchema = z.strictObject(LIST_SHAPES.decisions);
81
+ const LISTS_API = { listFields: {
82
+ method: "GET",
83
+ path: "/api/list-fields"
84
+ } };
150
85
  // ../server/src/contract-feedback.ts
151
86
  const NoteSchema = z.strictObject({
152
87
  /** Its first line is the title. */
@@ -167,7 +102,13 @@ const AcceptSchema = z.strictObject({
167
102
  /** Which workflow's lessons, when it was seen in more than one. */
168
103
  workflow: z.string().min(1).optional()
169
104
  });
170
- const ApplySchema = z.strictObject({ driver: z.enum(["sdk", "cli"]).optional() });
105
+ const DriverIdSchema = z.enum({
106
+ "claude-code": "claude-code",
107
+ "open-code": "open-code",
108
+ "command-code": "command-code"
109
+ });
110
+ DriverIdSchema.options;
111
+ const ApplySchema = z.strictObject({ driver: DriverIdSchema.optional() });
171
112
  const PublishPreviewSchema = z.strictObject({
172
113
  /** Also hide the repositories' and workflows' names. */
173
114
  hideNames: z.boolean() });
@@ -176,6 +117,11 @@ const PublishSchema = z.strictObject({
176
117
  /** The preview's digest: what the person saw, so nothing else is sent. */
177
118
  digest: z.string().min(1)
178
119
  });
120
+ /**
121
+ * The filters are core's `feedbackListTable` fields; with none, every item
122
+ * comes, dismissed too (the panel hides them itself).
123
+ */
124
+ const FeedbackItemsQuerySchema = z.strictObject(listShape("feedback"));
179
125
  const FEEDBACK_API = {
180
126
  note: {
181
127
  method: "POST",
@@ -184,7 +130,8 @@ const FEEDBACK_API = {
184
130
  },
185
131
  feedbackItems: {
186
132
  method: "GET",
187
- path: "/api/feedback"
133
+ path: "/api/feedback",
134
+ query: FeedbackItemsQuerySchema
188
135
  },
189
136
  feedbackItem: {
190
137
  method: "GET",
@@ -220,6 +167,125 @@ const FEEDBACK_API = {
220
167
  path: "/api/feedback/:id/split"
221
168
  }
222
169
  };
170
+ // ../server/src/contract-follow-ups.ts
171
+ /**
172
+ * The filters are core's `followUpListTable` fields. `status` is `open` (due
173
+ * and waiting, the default), `all` (adds done and dropped), or the table's.
174
+ */
175
+ const FollowUpsQuerySchema = z.strictObject(listShape("follow-ups"));
176
+ const FollowUpChangeSchema = z.strictObject({ action: z.enum([
177
+ "done",
178
+ "drop",
179
+ "reopen",
180
+ "due"
181
+ ]) });
182
+ /**
183
+ * Posts a drafted GitHub comment through the person's own `gh`. `confirm` says
184
+ * yes, and `digest` is the detail's `draftDigest`, which stands for the
185
+ * recipient and text they saw: the route refuses when the draft is no longer
186
+ * that, so nothing else is sent.
187
+ */
188
+ const FollowUpPostSchema = z.strictObject({
189
+ confirm: z.literal(true),
190
+ digest: z.string().min(1)
191
+ });
192
+ const FOLLOW_UPS_API = {
193
+ followUps: {
194
+ method: "GET",
195
+ path: "/api/projects/:id/follow-ups",
196
+ query: FollowUpsQuerySchema
197
+ },
198
+ followUp: {
199
+ method: "GET",
200
+ path: "/api/projects/:id/follow-ups/:followUp"
201
+ },
202
+ followUpChange: {
203
+ method: "POST",
204
+ path: "/api/projects/:id/follow-ups/:followUp",
205
+ body: FollowUpChangeSchema
206
+ },
207
+ followUpPost: {
208
+ method: "POST",
209
+ path: "/api/projects/:id/follow-ups/:followUp/post",
210
+ body: FollowUpPostSchema
211
+ }
212
+ };
213
+ // ../server/src/contract-after.ts
214
+ /**
215
+ * @file What the API offers after runs: the feedback loop's routes
216
+ * (contract-feedback.ts) and the follow-ups' (contract-follow-ups.ts), joined
217
+ * so the contract reads one part for both.
218
+ */
219
+ const AFTER_API = {
220
+ ...FEEDBACK_API,
221
+ ...FOLLOW_UPS_API
222
+ };
223
+ // ../server/src/contract-builder.ts
224
+ /** A new workflow's name, as its file and its `name:` take it. */
225
+ const NewWorkflowSchema = z.strictObject({ name: z.string().min(1) });
226
+ /** The server checks each edit; this file imports only types of the schema. */
227
+ const WorkflowEditsSchema = z.strictObject({ edits: z.array(z.custom()).min(1) });
228
+ /** Earlier text, and the file it was based on, to ask before replacing it. */
229
+ const DraftTextSchema = z.strictObject({
230
+ text: z.string(),
231
+ baseHash: z.string().nullable()
232
+ });
233
+ const SaveWorkflowSchema = z.strictObject({
234
+ /** Write over a file that changed since this draft began. */
235
+ force: z.boolean().optional() });
236
+ const BUILDER_API = {
237
+ builderSchema: {
238
+ method: "GET",
239
+ path: "/api/workflow-schema"
240
+ },
241
+ builderCatalog: {
242
+ method: "GET",
243
+ path: "/api/projects/:id/catalog"
244
+ },
245
+ builderDrafts: {
246
+ method: "GET",
247
+ path: "/api/projects/:id/builder"
248
+ },
249
+ builderView: {
250
+ method: "GET",
251
+ path: "/api/projects/:id/builder/:name"
252
+ },
253
+ builderCreate: {
254
+ method: "POST",
255
+ path: "/api/projects/:id/builder",
256
+ body: NewWorkflowSchema
257
+ },
258
+ builderEdit: {
259
+ method: "POST",
260
+ path: "/api/projects/:id/builder/:name/edits",
261
+ body: WorkflowEditsSchema
262
+ },
263
+ builderRestore: {
264
+ method: "POST",
265
+ path: "/api/projects/:id/builder/:name/text",
266
+ body: DraftTextSchema
267
+ },
268
+ builderDiscard: {
269
+ method: "POST",
270
+ path: "/api/projects/:id/builder/:name/discard"
271
+ },
272
+ builderSave: {
273
+ method: "POST",
274
+ path: "/api/projects/:id/builder/:name/save",
275
+ body: SaveWorkflowSchema
276
+ }
277
+ };
278
+ // ../server/src/contract-context.ts
279
+ const CONTEXT_API = {
280
+ runContext: {
281
+ method: "GET",
282
+ path: "/api/runs/:id/nodes/:node/context"
283
+ },
284
+ builderContext: {
285
+ method: "GET",
286
+ path: "/api/projects/:id/builder/:name/nodes/:node/context"
287
+ }
288
+ };
223
289
  // ../server/src/contract-links.ts
224
290
  /**
225
291
  * @file The routes that link two nodes of live runs (RFC-003): list, link and
@@ -275,9 +341,11 @@ planId: z.string().min(1) })
275
341
  };
276
342
  // ../server/src/contract-worktrees.ts
277
343
  const Root = z.string().min(1);
278
- const WorktreesQuerySchema = z.strictObject({
279
- /** A project's root; every project Ork knows without one. */
280
- project: Root.optional() });
344
+ /**
345
+ * The filters are core's `worktreeListTable` fields (`project`: a root; every
346
+ * project without one).
347
+ */
348
+ const WorktreesQuerySchema = z.strictObject(listShape("worktrees"));
281
349
  const RemovePlanSchema = z.strictObject({
282
350
  project: Root,
283
351
  /** The item's branch, or a detached worktree's path. */
@@ -327,18 +395,6 @@ const WORKTREES_API = {
327
395
  // ../server/src/contract.ts
328
396
  const Count = z.coerce.number().int().positive();
329
397
  const Seq = z.coerce.number().int().nonnegative();
330
- const Driver = z.enum(["sdk", "cli"]);
331
- const RunsQuerySchema = z.strictObject({
332
- project: z.string().min(1).optional(),
333
- limit: Count.optional()
334
- });
335
- const UsageQuerySchema = z.strictObject({
336
- project: z.string().min(1).optional(),
337
- workflow: z.string().min(1).optional(),
338
- /** ISO times: `since` included, `until` not. */
339
- since: z.iso.datetime().optional(),
340
- until: z.iso.datetime().optional()
341
- });
342
398
  const EventsQuerySchema = z.strictObject({
343
399
  after: Seq.optional(),
344
400
  node: z.string().min(1).optional(),
@@ -347,10 +403,6 @@ const EventsQuerySchema = z.strictObject({
347
403
  /** Only the newest this many, still oldest first. */
348
404
  last: Count.optional()
349
405
  });
350
- const DecisionsQuerySchema = z.strictObject({
351
- run: z.string().min(1).optional(),
352
- project: z.string().min(1).optional()
353
- });
354
406
  /** An answer: the option, and what the person said with it, if anything. */
355
407
  const AnswerSchema = z.strictObject({
356
408
  choose: z.string().min(1),
@@ -358,7 +410,7 @@ const AnswerSchema = z.strictObject({
358
410
  });
359
411
  const RunFlags = {
360
412
  inputs: z.record(z.string(), z.string()).optional(),
361
- driver: Driver.optional(),
413
+ driver: DriverIdSchema.optional(),
362
414
  inPlace: z.boolean().optional(),
363
415
  allowDirty: z.boolean().optional()
364
416
  };
@@ -384,10 +436,16 @@ const API = {
384
436
  method: "GET",
385
437
  path: "/api/health"
386
438
  },
439
+ /** Bearer token only (RFC-019): mints a single-use code for `/?code=`. */
440
+ signIn: {
441
+ method: "POST",
442
+ path: "/api/sign-in"
443
+ },
387
444
  ...BUILDER_API,
388
445
  ...CONTEXT_API,
389
- ...FEEDBACK_API,
446
+ ...AFTER_API,
390
447
  ...WORKTREES_API,
448
+ ...LISTS_API,
391
449
  ...UPDATE_API,
392
450
  projects: {
393
451
  method: "GET",
@@ -483,11 +541,70 @@ const STREAMS = {
483
541
  run: "/api/runs/:id/stream",
484
542
  /**
485
543
  * `runs`, `decisions`, `version`, each when it changes; `?project=` keeps
486
- * one. `upgrade`: the upgrade's output, then `end` (contract-update.ts).
544
+ * one, `?workflow=` narrows `decisions` to that workflow's. `upgrade`: the
545
+ * upgrade's output, then `end` (contract-update.ts).
487
546
  */
488
547
  overview: "/api/stream",
489
548
  upgrade: "/api/upgrade/stream"
490
549
  };
550
+ /** A route's path with its `:params` filled in, each encoded. */
551
+ const fillPath = (path, params) => path.replaceAll(/:(\w+)/gu, (_, name) => encodeURIComponent(params[name] ?? ""));
552
+ // ../server/src/client.ts
553
+ /**
554
+ * @file The typed client of the API (ADR-039), built from the contract: a call
555
+ * names a route and passes its params, query and body, and gets the route's
556
+ * response type back, or the server's error. The UI uses it in the browser
557
+ * (the session cookie goes with each request on its own); scripts pass the
558
+ * token. It uses only `fetch`, so it runs in a browser and in Node.
559
+ */
560
+ /** An object's string and number fields, as text. */
561
+ const entries = (value) => typeof value === "object" && value !== null ? Object.entries(value).flatMap(([key, field]) => typeof field === "string" || typeof field === "number" ? [[key, String(field)]] : []) : [];
562
+ const queryString = (query) => {
563
+ const search = new URLSearchParams();
564
+ for (const [key, value] of entries(query)) search.set(key, value);
565
+ const text = search.toString();
566
+ return text === "" ? "" : `?${text}`;
567
+ };
568
+ const errorOf = (body, status) => typeof body === "object" && body !== null && "error" in body && typeof body.error === "string" ? body.error : `The server answered ${String(status)}.`;
569
+ const createClient = (options = {}) => {
570
+ const send = (url, init) => (options.fetch ?? globalThis.fetch)(url, init);
571
+ return { call: async (name, ...[input]) => {
572
+ const route = API[name];
573
+ const parts = input ?? {};
574
+ const params = Object.fromEntries(entries(parts.params));
575
+ const url = `${options.base ?? ""}${fillPath(route.path, params)}${queryString(parts.query)}`;
576
+ const headers = { accept: "application/json" };
577
+ if (options.token !== void 0) headers["authorization"] = `Bearer ${options.token}`;
578
+ const init = {
579
+ method: route.method,
580
+ headers
581
+ };
582
+ if (route.method === "POST") {
583
+ headers["content-type"] = "application/json";
584
+ init.body = JSON.stringify(parts.body ?? {});
585
+ }
586
+ let response;
587
+ try {
588
+ response = await send(url, init);
589
+ } catch (error) {
590
+ return {
591
+ ok: false,
592
+ status: 0,
593
+ error: `No answer: ${error instanceof Error ? error.message : String(error)}`
594
+ };
595
+ }
596
+ const body = await response.json().catch(() => null);
597
+ return response.ok ? {
598
+ ok: true,
599
+ status: response.status,
600
+ data: body
601
+ } : {
602
+ ok: false,
603
+ status: response.status,
604
+ error: errorOf(body, response.status)
605
+ };
606
+ } };
607
+ };
491
608
  // ../server/src/graph.ts
492
609
  /**
493
610
  * @file A workflow or a run as the graph the UI draws: its nodes, its edges
@@ -509,7 +626,8 @@ const workflowGraph = (workflow) => ({
509
626
  maxRounds
510
627
  })) : [];
511
628
  }),
512
- channels: (workflow.messages ?? []).map(({ between: [a, b] }) => [a, b])
629
+ channels: (workflow.messages ?? []).map(({ between: [a, b] }) => [a, b]),
630
+ ...workflow.limits?.budgetTokens === void 0 ? {} : { budgetTokens: workflow.limits.budgetTokens }
513
631
  });
514
632
  /** The graph of a workflow file, or `null` if it is gone or does not parse. */
515
633
  const readWorkflowGraph = (file) => {
@@ -536,18 +654,22 @@ const runGraph = (events, file) => {
536
654
  })),
537
655
  edges: edges ?? onFile?.edges ?? [],
538
656
  channels: channels ?? onFile?.channels ?? [],
539
- ...started?.type === "run.started" && started.budgetUsd !== void 0 ? { budgetUsd: started.budgetUsd } : {}
657
+ ...started?.type === "run.started" && started.budgetUsd !== void 0 ? { budgetUsd: started.budgetUsd } : {},
658
+ ...onFile?.budgetTokens === void 0 ? {} : { budgetTokens: onFile.budgetTokens }
540
659
  };
541
660
  };
542
661
  // ../server/src/guards.ts
543
662
  /**
544
- * @file What the server lets in (ADR-039,
663
+ * @file What the server lets in (ADR-039, RFC-019,
545
664
  * specs/server/streams-contract-and-security.md#security): only its own
546
- * loopback `Host` (against DNS rebinding); only with its token, from the
547
- * session cookie or an `Authorization: Bearer` header; and, for a write sent
548
- * with the cookie, only from its own `Origin` (against CSRF). The token in
549
- * `?token=` is swapped once for the cookie, with a redirect that takes it out
550
- * of the address bar. Pure: it reads the request's parts and decides.
665
+ * loopback `Host` (against DNS rebinding); only with the token in an
666
+ * `Authorization: Bearer` header (scripts, `ork mcp`) or the session secret
667
+ * in the cookie (the browser); and, for a write sent with the cookie, only
668
+ * from its own `Origin` (against CSRF). A single-use code in `?code=` is
669
+ * swapped once for the cookie, with a redirect that takes it out of the
670
+ * address bar; the token itself never reaches a browser. It reads the
671
+ * request's parts and decides; its only state is the code store it spends
672
+ * from.
551
673
  */
552
674
  const WRITES = /* @__PURE__ */ new Set([
553
675
  "POST",
@@ -555,11 +677,13 @@ const WRITES = /* @__PURE__ */ new Set([
555
677
  "PATCH",
556
678
  "DELETE"
557
679
  ]);
680
+ /** What a used, expired or wrong code all get, so none tells them apart. */
681
+ const BAD_CODE = "That sign-in link is not valid any more; open it again with `ork ui`.";
558
682
  /** A new token: 32 random bytes. */
559
683
  const newToken = () => randomBytes(32).toString("base64url");
560
684
  /** The session cookie's name; one per port, as cookies ignore ports. */
561
685
  const cookieName = (port) => `ork_ui_${String(port)}`;
562
- /** Whether `given` is the token, compared in constant time. */
686
+ /** Whether `given` is the token (or session secret), compared in constant time. */
563
687
  const sameToken = (given, token) => {
564
688
  const a = Buffer.from(given);
565
689
  const b = Buffer.from(token);
@@ -575,22 +699,22 @@ const cookieValue = (request, name) => (header(request, "cookie") ?? "").split("
575
699
  const bearer = (request) => {
576
700
  return /^Bearer\s+(\S+)$/iu.exec(header(request, "authorization") ?? "")?.[1];
577
701
  };
578
- /** The session cookie that holds `token`. */
579
- const sessionCookie = (port, token) => `${cookieName(port)}=${token}; HttpOnly; SameSite=Strict; Path=/`;
702
+ /** The session cookie that holds the session secret. */
703
+ const sessionCookie = (port, session) => `${cookieName(port)}=${session}; HttpOnly; SameSite=Strict; Path=/`;
580
704
  const exchange = (request, context) => {
581
- const url = new URL(request.url, "http://127.0.0.1");
582
- const given = url.searchParams.get("token");
705
+ const url = new URL(request.url.replace(/^[/\\]+/u, "/"), "http://127.0.0.1");
706
+ const given = url.searchParams.get("code");
583
707
  if (given === null || request.method !== "GET") return null;
584
- if (!sameToken(given, context.token)) return {
708
+ if (!context.codes.redeem(given)) return {
585
709
  kind: "refuse",
586
710
  status: 401,
587
- error: "That link's token is not this server's; open it again with `ork ui`."
711
+ error: BAD_CODE
588
712
  };
589
- url.searchParams.delete("token");
713
+ url.searchParams.delete("code");
590
714
  return {
591
715
  kind: "exchange",
592
- cookie: sessionCookie(context.port, context.token),
593
- location: `${url.pathname}${url.search}`
716
+ cookie: sessionCookie(context.port, context.session),
717
+ location: `${url.pathname.replace(/^\/+/u, "/")}${url.search}`
594
718
  };
595
719
  };
596
720
  /** Decides whether a request may go on. */
@@ -605,15 +729,20 @@ const guard = (request, context) => {
605
729
  if (exchanged !== null) return exchanged;
606
730
  const fromBearer = bearer(request);
607
731
  const fromCookie = cookieValue(request, cookieName(context.port));
732
+ const via = fromBearer === void 0 ? "cookie" : "bearer";
608
733
  const given = fromBearer ?? fromCookie;
609
- if (given === void 0 || !sameToken(given, context.token)) return {
734
+ const secret = via === "bearer" ? context.token : context.session;
735
+ if (given === void 0 || !sameToken(given, secret)) return {
610
736
  kind: "refuse",
611
737
  status: 401,
612
738
  error: "Not signed in; open the UI with `ork ui`."
613
739
  };
614
- if (!WRITES.has(request.method)) return { kind: "pass" };
740
+ if (!WRITES.has(request.method)) return {
741
+ kind: "pass",
742
+ via
743
+ };
615
744
  const origin = header(request, "origin");
616
- if ((fromBearer === void 0 || origin !== void 0) && (origin === void 0 || !ownOrigins(context.port).includes(origin))) return {
745
+ if ((via === "cookie" || origin !== void 0) && (origin === void 0 || !ownOrigins(context.port).includes(origin))) return {
617
746
  kind: "refuse",
618
747
  status: 403,
619
748
  error: `Origin ${origin ?? "(none)"} may not write here.`
@@ -624,7 +753,10 @@ const guard = (request, context) => {
624
753
  status: 415,
625
754
  error: "Send JSON (Content-Type: application/json)."
626
755
  };
627
- return { kind: "pass" };
756
+ return {
757
+ kind: "pass",
758
+ via
759
+ };
628
760
  };
629
761
  /** How often an open event stream gets a keep-alive comment. */
630
762
  const KEEP_ALIVE_MS = 15e3;
@@ -821,6 +953,29 @@ const route = async (handlers, req, res, url) => {
821
953
  else sendJson(res, reply.status, reply.body);
822
954
  return true;
823
955
  };
956
+ // ../server/src/list-fields-routes.ts
957
+ /**
958
+ * @file `GET /api/list-fields`: the filter fields of each list, from core's
959
+ * tables (RFC-017), as text without their functions, so the UI builds its
960
+ * filter bar without importing core.
961
+ */
962
+ const infoOf = (field) => ({
963
+ name: field.name,
964
+ description: field.description,
965
+ type: field.type,
966
+ ...field.values === void 0 ? {} : { values: [...field.values] },
967
+ ...field.all === true ? { all: true } : {}
968
+ });
969
+ /** The answer, built once: the tables do not change while the server runs. */
970
+ const listFields = () => ({
971
+ runs: LIST_TABLES.ps.fields.map(infoOf),
972
+ feedback: LIST_TABLES["feedback list"].fields.map(infoOf),
973
+ "follow-ups": LIST_TABLES["follow-ups list"].fields.map(infoOf),
974
+ decisions: LIST_TABLES["decide --list"].fields.map(infoOf),
975
+ worktrees: LIST_TABLES["worktrees list"].fields.map(infoOf),
976
+ usage: LIST_TABLES.usage.fields.map(infoOf)
977
+ });
978
+ const listFieldsRoutes = () => ({ listFields: () => ok(listFields()) });
824
979
  // ../server/src/context.ts
825
980
  /**
826
981
  * @file What the server's handlers share: the store it reads and writes, where
@@ -841,11 +996,21 @@ const runOf = (context, id) => {
841
996
  const projectOf = (context, key) => context.store.projects().find((p) => p.id === key || p.root === key);
842
997
  /** A run as the API shows it: with its live status and what is paused. */
843
998
  const viewOf$2 = (context, run) => {
844
- const events = run.status === "running" ? context.store.events(run.id).map((stored) => stored.event) : [];
999
+ const events = run.status === "running" ? context.store.controlEvents(run.id).map((stored) => stored.event) : [];
845
1000
  return {
846
1001
  ...run,
847
1002
  live: runStatus(run, events),
848
- paused: pausedOf(events)
1003
+ paused: pausedOf(events),
1004
+ ...yoloOf(events) ? { yolo: true } : {}
1005
+ };
1006
+ };
1007
+ /** The run's view plus its last finish's declared outputs, when it has any. */
1008
+ const viewWithOutputs = (context, run) => {
1009
+ const finished = context.store.controlEvents(run.id).map((stored) => stored.event).findLast((event) => event.type === "run.finished");
1010
+ const outputs = outputsText(finished?.type === "run.finished" ? finished.result.outputs : void 0);
1011
+ return {
1012
+ ...viewOf$2(context, run),
1013
+ ...outputs.length === 0 ? {} : { outputs }
849
1014
  };
850
1015
  };
851
1016
  // ../server/src/mcp-answers.ts
@@ -1108,17 +1273,14 @@ const checkTool = (context) => tool("check", "Checks a workflow as `ork validate
1108
1273
  return answer({
1109
1274
  errors: count("error"),
1110
1275
  warnings: count("warning"),
1111
- diagnostics: diagnostics.map(({ severity, code, message, line, column }) => line === void 0 ? {
1276
+ diagnostics: diagnostics.map(({ severity, code, message, file, line, column }) => Object.assign({
1112
1277
  severity,
1113
1278
  code,
1114
1279
  message
1115
- } : {
1116
- severity,
1117
- code,
1118
- message,
1280
+ }, file === void 0 ? {} : { file }, line === void 0 ? {} : {
1119
1281
  line,
1120
1282
  column
1121
- }),
1283
+ })),
1122
1284
  walk: walk === void 0 ? null : walkSummary(walk),
1123
1285
  ...args.paths === true && walk !== void 0 ? { endings: walk.endings.slice(0, MOST_ENDINGS) } : {}
1124
1286
  });
@@ -1512,6 +1674,136 @@ const builderRoutes = (context) => {
1512
1674
  builderSave: ({ params, body }) => inProject(params.id, (project) => saveDraft(context, project, params.name, body.force === true))
1513
1675
  };
1514
1676
  };
1677
+ // ../server/src/end-view.ts
1678
+ /**
1679
+ * @file Resolves how nodes ended for the API: each `node.finished` event (read
1680
+ * through core's `endOf`, so a run stored before ends existed gets a kind
1681
+ * too) and each tree node get an `EndView` (contract-ends.ts).
1682
+ */
1683
+ /** Core's hints send the reader to Details; with no detail there is none. */
1684
+ const HINT_TO_DETAILS = /\bIts (?:last )?(?:output|detail) is under Details(?:; |\.\s*)/u;
1685
+ const hintFor = (hint, hasDetail) => {
1686
+ if (hasDetail) return hint;
1687
+ const rest = hint.replace(HINT_TO_DETAILS, "");
1688
+ return rest === "" ? rest : rest[0]?.toUpperCase() + rest.slice(1);
1689
+ };
1690
+ const endViewOf = (end) => {
1691
+ const info = END_KINDS[end.kind];
1692
+ return {
1693
+ kind: end.kind,
1694
+ message: end.message,
1695
+ ...end.detail === void 0 ? {} : { detail: end.detail },
1696
+ label: info.label,
1697
+ actor: info.actor,
1698
+ transient: info.transient,
1699
+ hint: hintFor(info.hint, end.detail !== void 0),
1700
+ docsAnchor: info.docsAnchor
1701
+ };
1702
+ };
1703
+ /** The event as served: a `node.finished` gets its `end`, old ones too. */
1704
+ const servedEvent = (stored) => {
1705
+ const { event } = stored;
1706
+ return event.type === "node.finished" ? {
1707
+ ...stored,
1708
+ event: {
1709
+ ...event,
1710
+ end: endViewOf(endOf(event))
1711
+ }
1712
+ } : {
1713
+ ...stored,
1714
+ event
1715
+ };
1716
+ };
1717
+ /**
1718
+ * The tree with each node's latest end, from the events it was folded from. A
1719
+ * node that started again has none until it finishes; a failed node's `now` is
1720
+ * the end's message, not the raw error.
1721
+ */
1722
+ const treeWithEnds = (roots, stored) => {
1723
+ const ends = /* @__PURE__ */ new Map();
1724
+ for (const { event } of stored) if (event.type === "node.started") ends.delete(event.id);
1725
+ else if (event.type === "node.finished") ends.set(event.id, endViewOf(endOf(event)));
1726
+ const dress = (node) => {
1727
+ const end = ends.get(node.id);
1728
+ const children = node.children.map(dress);
1729
+ if (end === void 0) return {
1730
+ ...node,
1731
+ children
1732
+ };
1733
+ const failed = node.state === "failed" && end.actor !== null;
1734
+ return {
1735
+ ...node,
1736
+ ...failed ? { now: end.message } : {},
1737
+ end,
1738
+ children
1739
+ };
1740
+ };
1741
+ return roots.map((node) => dress(node));
1742
+ };
1743
+ // ../server/src/list-query.ts
1744
+ /**
1745
+ * @file Reads a list route's filter fields the way the CLI reads its flags
1746
+ * (RFC-017, ADR-088): each value checked by core's `parseFilters` against the
1747
+ * list's table, a refused one answered as 400 in the CLI's words. A full ISO
1748
+ * time in `since` or `until` is taken as given (the form `usage` had before
1749
+ * the tables), the rest are an age, a day or `all`.
1750
+ */
1751
+ /** A `2026-11-01T10:00:00Z`-style time, as an ISO time; else `undefined`. */
1752
+ const isoTime = (value) => {
1753
+ if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}T/u.test(value)) return;
1754
+ const time = Date.parse(value);
1755
+ return Number.isNaN(time) ? void 0 : new Date(time).toISOString();
1756
+ };
1757
+ /**
1758
+ * The filter `query` asks for, checked against `table`; a value the table
1759
+ * refuses is a 400.
1760
+ */
1761
+ const filterOf = (table, query) => {
1762
+ const times = {};
1763
+ const rest = { ...query };
1764
+ for (const name of ["since", "until"]) {
1765
+ const iso = isoTime(query[name]);
1766
+ if (iso === void 0) continue;
1767
+ times[name] = iso;
1768
+ delete rest[name];
1769
+ }
1770
+ const parsed = parseFilters(table, rest, Date.now());
1771
+ return parsed.ok ? {
1772
+ ok: true,
1773
+ filter: {
1774
+ ...parsed.filter,
1775
+ ...times
1776
+ }
1777
+ } : {
1778
+ ok: false,
1779
+ refusal: fail(400, parsed.error)
1780
+ };
1781
+ };
1782
+ /**
1783
+ * The root a filter's `project` names (a registered project's id or root, else
1784
+ * the value as a root); `undefined` without one or for `all`.
1785
+ */
1786
+ const projectRootOf = (context, filter) => {
1787
+ const key = filter["project"];
1788
+ return key === void 0 || key === "all" ? void 0 : projectOf(context, key)?.root ?? key;
1789
+ };
1790
+ /** `now` is injectable so a test can move the clock. */
1791
+ const createSignInCodes = (now = Date.now) => {
1792
+ const expiries = /* @__PURE__ */ new Map();
1793
+ return {
1794
+ mint: () => {
1795
+ for (const [code, expiry] of expiries) if (expiry <= now()) expiries.delete(code);
1796
+ const code = randomBytes(32).toString("base64url");
1797
+ expiries.set(code, now() + 6e4);
1798
+ return code;
1799
+ },
1800
+ redeem: (code) => {
1801
+ const expiry = expiries.get(code);
1802
+ expiries.delete(code);
1803
+ return expiry !== void 0 && expiry > now();
1804
+ }
1805
+ };
1806
+ };
1515
1807
  // ../server/src/workflows.ts
1516
1808
  /**
1517
1809
  * @file A project's workflows, for the UI's Run workflow dialog: each
@@ -1570,47 +1862,71 @@ const projectWorkflows = (project) => {
1570
1862
  * decisions, and usage. Each reads the store as the matching CLI command does
1571
1863
  * (`ork projects`, `ps`, `tree`, `logs`, `decide --list`, `usage`).
1572
1864
  */
1573
- /** A project filter's root: a registered project's, or the value as a root. */
1574
1865
  /** A registered project's place; caps are only known for one. */
1575
1866
  const placeOf = (context, key) => {
1576
1867
  const known = key === void 0 ? void 0 : projectOf(context, key);
1577
1868
  return known === void 0 ? void 0 : placeFor(known.root, context.store.projects());
1578
1869
  };
1579
- const projectRoot = (context, key) => projectOf(context, key)?.root ?? key;
1870
+ /** `ork ps`'s table, matching `--status` on the live status the API shows. */
1871
+ const liveRunTable = {
1872
+ ...runListTable,
1873
+ fields: runListTable.fields.map((field) => field.name === "status" ? Object.assign({}, field, { get: (run) => run.live }) : field)
1874
+ };
1580
1875
  const readRoutes = (context) => ({
1876
+ ...listFieldsRoutes(),
1581
1877
  health: () => ok({
1582
1878
  version: context.version,
1583
1879
  pid: context.pid,
1584
1880
  startedAt: context.startedAt
1585
1881
  }),
1882
+ signIn: () => ok({
1883
+ code: context.codes.mint(),
1884
+ expiresIn: 60
1885
+ }),
1586
1886
  projects: () => ok(context.store.projects()),
1587
1887
  triggers: () => ok(triggersReport(context.store, context.store.projects(), context.home)),
1588
1888
  links: ({ query }) => ok(context.store.links(query.run === void 0 ? {} : { run: query.run })),
1589
- usage: ({ query }) => ok({
1590
- ...usageReport(context.store.usage({
1591
- ...query.project === void 0 ? {} : { project: projectRoot(context, query.project) },
1592
- ...query.workflow === void 0 ? {} : { workflow: query.workflow },
1593
- ...query.since === void 0 ? {} : { since: query.since },
1594
- ...query.until === void 0 ? {} : { until: query.until }
1595
- })),
1596
- limits: limitsReport(context.store, capsHere(context.home, placeOf(context, query.project)), /* @__PURE__ */ new Date())
1597
- }),
1889
+ usage: ({ query }) => {
1890
+ const read = filterOf(usageListTable, query);
1891
+ if (!read.ok) return read.refusal;
1892
+ const { filter } = read;
1893
+ const { workflow, since, until } = filter;
1894
+ const root = projectRootOf(context, filter);
1895
+ return ok({
1896
+ ...usageReport(context.store.usage({
1897
+ ...root === void 0 ? {} : { project: root },
1898
+ ...workflow === void 0 ? {} : { workflow },
1899
+ ...since === void 0 || since === "all" ? {} : { since },
1900
+ ...until === void 0 ? {} : { until }
1901
+ })),
1902
+ limits: limitsReport(context.store, capsHere(context.home, placeOf(context, root)), /* @__PURE__ */ new Date())
1903
+ });
1904
+ },
1598
1905
  workflows: ({ params }) => {
1599
1906
  const project = projectOf(context, params.id);
1600
1907
  return project === void 0 ? fail(404, `No registered project \`${params.id}\`.`) : ok(projectWorkflows(project));
1601
1908
  },
1602
- runs: ({ query }) => ok(context.store.runs({
1603
- limit: query.limit ?? 20,
1604
- ...query.project === void 0 ? {} : { project: projectRoot(context, query.project) }
1605
- }).map((run) => viewOf$2(context, run))),
1909
+ runs: ({ query }) => {
1910
+ const read = filterOf(liveRunTable, query);
1911
+ if (!read.ok) return read.refusal;
1912
+ const root = projectRootOf(context, read.filter);
1913
+ const limit = query.limit ?? 20;
1914
+ const narrowed = Object.keys(read.filter).some((name) => name !== "project");
1915
+ const views = context.store.runs({
1916
+ ...root === void 0 ? {} : { project: root },
1917
+ ...narrowed ? {} : { limit }
1918
+ }).map((run) => viewOf$2(context, run));
1919
+ return ok(applyFilters(liveRunTable, views, read.filter).slice(0, limit));
1920
+ },
1606
1921
  run: ({ params }) => {
1607
1922
  const found = runOf(context, params.id);
1608
- return found.ok ? ok(viewOf$2(context, found.run)) : found.refusal;
1923
+ return found.ok ? ok(viewWithOutputs(context, found.run)) : found.refusal;
1609
1924
  },
1610
1925
  tree: ({ params }) => {
1611
1926
  const found = runOf(context, params.id);
1612
1927
  if (!found.ok) return found.refusal;
1613
- return ok(activeTree(context.store.events(found.run.id)));
1928
+ const stored = context.store.events(found.run.id);
1929
+ return ok(treeWithEnds(nestItems(activeTree(stored)), stored));
1614
1930
  },
1615
1931
  graph: ({ params }) => {
1616
1932
  const found = runOf(context, params.id);
@@ -1620,15 +1936,28 @@ const readRoutes = (context) => ({
1620
1936
  events: ({ params, query }) => {
1621
1937
  const found = runOf(context, params.id);
1622
1938
  if (!found.ok) return found.refusal;
1623
- const events = context.store.events(found.run.id, query.after ?? 0).filter((s) => query.node === void 0 || nodeOf(s.event) === query.node);
1939
+ const events = context.store.events(found.run.id, query.after ?? 0).filter((s) => query.node === void 0 || isNodeEvent(s.event, query.node));
1624
1940
  const newest = query.last === void 0 ? events : events.slice(-query.last);
1625
- return ok(query.limit === void 0 ? newest : newest.slice(0, query.limit));
1941
+ return ok((query.limit === void 0 ? newest : newest.slice(0, query.limit)).map((stored) => servedEvent(stored)));
1626
1942
  },
1627
1943
  decisions: ({ query }) => {
1628
- if (query.run === void 0) return ok(pendingDecisions(context.store, query.project === void 0 ? {} : { project: projectRoot(context, query.project) }));
1629
- const found = runOf(context, query.run);
1630
- if (!found.ok) return found.refusal;
1631
- return ok(pendingDecisions(context.store, { runId: found.run.id }));
1944
+ const read = filterOf(decisionListTable, query);
1945
+ if (!read.ok) return read.refusal;
1946
+ const { run, workflow: named } = read.filter;
1947
+ const workflow = named === void 0 ? {} : { workflow: named };
1948
+ if (run === void 0) {
1949
+ const root = projectRootOf(context, read.filter);
1950
+ return ok(pendingDecisions(context.store, {
1951
+ ...workflow,
1952
+ ...root === void 0 ? {} : { project: root }
1953
+ }));
1954
+ }
1955
+ const found = runOf(context, run);
1956
+ if (!found.ok) return fail(400, `--run finds no run: ${found.refusal.error}`);
1957
+ return ok(pendingDecisions(context.store, {
1958
+ ...workflow,
1959
+ runId: found.run.id
1960
+ }));
1632
1961
  },
1633
1962
  decision: ({ params }) => {
1634
1963
  const found = context.store.decision(params.id);
@@ -1654,6 +1983,8 @@ const RECENT_EVENTS = 30;
1654
1983
  const ANSWERED_BY_A_PERSON = "Only a person answers these: in `ork ui`, or with `ork decide` in their own terminal. Tell them what waits, and where.";
1655
1984
  const project = z.string().min(1).describe("A registered project's id or root");
1656
1985
  const run = z.string().min(1).describe("A run's id, or a unique start of it");
1986
+ /** An error answer, in the CLI's words, when `project` names none registered. */
1987
+ const unknownProject = (context, key) => key === void 0 || key === "all" || projectOf(context, key) !== void 0 ? void 0 : refused$1(unknownProjectRefusal(key, context.store.projects().map((p) => p.id)));
1657
1988
  const mcpTools = (context) => {
1658
1989
  const read = readRoutes(context);
1659
1990
  const { builderCatalog } = builderRoutes(context);
@@ -1668,13 +1999,22 @@ const mcpTools = (context) => {
1668
1999
  ...NO_QUERY,
1669
2000
  params: { id: args.project }
1670
2001
  }))),
1671
- tool("runs", "Runs, newest first, with their live status and cost. 10 unless a limit is given.", RunsQuerySchema.shape, (args) => fromReply(read.runs({
1672
- ...NO_QUERY,
1673
- query: {
1674
- limit: 10,
1675
- ...args
1676
- }
1677
- }))),
2002
+ tool("runs", "Runs, newest first, with their live status and cost. 10 unless a limit is given. Narrow them with `status` (running, paused, crashed, succeeded, failed, … or all), `workflow`, `project` (or all), `since` (24h, 30d, 2026-11-01, all) and `until` (a day).", {
2003
+ ...RunsQuerySchema.shape,
2004
+ limit: z.union([z.number(), z.string()]).optional()
2005
+ }, ({ limit, ...args }) => {
2006
+ const bad = unknownProject(context, args.project);
2007
+ if (bad !== void 0) return bad;
2008
+ const count = limit === void 0 ? 10 : parseLimit(String(limit));
2009
+ if (count === null) return refused$1(limitRefusal(String(limit)));
2010
+ return fromReply(read.runs({
2011
+ ...NO_QUERY,
2012
+ query: {
2013
+ ...args,
2014
+ limit: count
2015
+ }
2016
+ }));
2017
+ }),
1678
2018
  tool("run", "One run: its status, what it waits on, its cost, and each node's state.", { run }, async (args) => {
1679
2019
  const view = await read.run({
1680
2020
  ...NO_QUERY,
@@ -1704,11 +2044,11 @@ const mcpTools = (context) => {
1704
2044
  }
1705
2045
  }));
1706
2046
  }),
1707
- tool("usage", "What runs spent, by workflow and model, and how close the limits are. `since` and `until` are ISO times.", UsageQuerySchema.shape, (args) => fromReply(read.usage({
2047
+ tool("usage", "What runs spent, by workflow and model, and how close the limits are. Narrow it with `workflow`, `project` (or all), `since` and `until`: an age (24h, 30d), a day (2026-11-01) or an ISO time; `since` also takes all.", UsageQuerySchema.shape, (args) => unknownProject(context, args.project) ?? fromReply(read.usage({
1708
2048
  ...NO_QUERY,
1709
2049
  query: args
1710
2050
  }))),
1711
- tool("decisions", "The decisions open in live runs, waiting on a person. This server can't answer them.", DecisionsQuerySchema.shape, (args) => fromReply(read.decisions({
2051
+ tool("decisions", "The decisions open in live runs, waiting on a person. This server can't answer them. Narrow them with `workflow`, `run` (an id or a start of one) and `project` (or all).", DecisionsQuerySchema.shape, (args) => unknownProject(context, args.project) ?? fromReply(read.decisions({
1712
2052
  ...NO_QUERY,
1713
2053
  query: args
1714
2054
  }), (open) => ({
@@ -1820,7 +2160,8 @@ const openMcp = (options) => {
1820
2160
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
1821
2161
  ...options.home === void 0 ? {} : { home: options.home },
1822
2162
  ...options.script === void 0 ? {} : { script: options.script },
1823
- updates: createUpdates({ version: options.version })
2163
+ updates: createUpdates({ version: options.version }),
2164
+ codes: createSignInCodes()
1824
2165
  };
1825
2166
  return {
1826
2167
  tools: [...mcpTools(context), ...actingTools(context, { start: options.start })],
@@ -1838,6 +2179,17 @@ const openMcp = (options) => {
1838
2179
  }
1839
2180
  };
1840
2181
  };
2182
+ // ../server/src/access.ts
2183
+ /**
2184
+ * @file The secrets a server starts with (RFC-019): the API token (bearer), the
2185
+ * session secret (the browser's cookie) and the sign-in code store. Each is
2186
+ * the caller's or a new one.
2187
+ */
2188
+ const createAccess = (given) => ({
2189
+ token: given.token ?? newToken(),
2190
+ session: given.session ?? newToken(),
2191
+ codes: given.codes ?? createSignInCodes()
2192
+ });
1841
2193
  // ../server/src/assets.ts
1842
2194
  /**
1843
2195
  * @file The built UI's files (`packages/ui/dist`, shipped as the CLI's
@@ -2010,7 +2362,7 @@ const applyRoute = async (context, item, driver) => {
2010
2362
  * workflows would go and how big that file is, the project a fix would be
2011
2363
  * proposed in, and the run proposing one now, if any.
2012
2364
  */
2013
- const detailOf = (store, item) => {
2365
+ const detailOf$1 = (store, item) => {
2014
2366
  const registered = store.projects();
2015
2367
  const fixRun = openFixRun(store, item);
2016
2368
  const actions = feedbackActions(item, fixRun);
@@ -2106,14 +2458,18 @@ const feedbackRoutes = (context) => {
2106
2458
  note: ({ params, body }) => {
2107
2459
  const found = runOf(context, params.id);
2108
2460
  if (!found.ok) return found.refusal;
2109
- const noted = recordNote(store, found.run, body);
2461
+ const scrub = context.noteScrub?.(found.run, store.projects());
2462
+ const noted = recordNote(store, found.run, body, "", scrub);
2110
2463
  return noted.ok ? ok({ item: noted.item }) : fail(400, noted.error);
2111
2464
  },
2112
- feedbackItems: () => ok(store.feedbackItems({ all: true })),
2465
+ feedbackItems: ({ query }) => {
2466
+ const read = filterOf(feedbackListTable, query);
2467
+ return read.ok ? ok(applyFilters(feedbackListTable, store.feedbackItems({ all: true }), read.filter)) : read.refusal;
2468
+ },
2113
2469
  feedbackItem: ({ params }) => {
2114
2470
  const found = itemOf(store, params.id);
2115
2471
  if (!found.ok) return found.refusal;
2116
- return ok(detailOf(store, found.item));
2472
+ return ok(detailOf$1(store, found.item));
2117
2473
  },
2118
2474
  feedbackStatus: ({ params, body }) => {
2119
2475
  const found = itemOf(store, params.id);
@@ -2162,6 +2518,174 @@ const feedbackRoutes = (context) => {
2162
2518
  }
2163
2519
  };
2164
2520
  };
2521
+ // ../server/src/follow-up-routes.ts
2522
+ /**
2523
+ * @file The follow-ups' routes (specs/follow-ups.md), through the same core
2524
+ * functions as `ork follow-ups`: a project's list (after the due check, which
2525
+ * reads pull requests through the person's own `gh` and only reports a `gh`
2526
+ * that fails), one follow-up with what to copy and what can be done to it,
2527
+ * done, drop, reopen and due, and posting a drafted GitHub comment only with
2528
+ * the person's confirmation of the draft they saw (its digest). Ork never
2529
+ * runs a task's commands.
2530
+ */
2531
+ const OPEN = ["due", "waiting"];
2532
+ /** Stands for what a person saw of a draft: its recipient and text. */
2533
+ const digestOf$1 = (f) => f.draft === void 0 ? void 0 : createHash("sha256").update(JSON.stringify([f.draft.to, f.draft.text])).digest("hex");
2534
+ /** The place a project's rule keeps follow-ups in. */
2535
+ const ruleOf = (project) => (loadProject(project.root, placeFiles(project).rules).project?.followUps ?? DEFAULT_FOLLOW_UPS_RULE).store;
2536
+ /** Why a follow-up isn't where the project's rule says, when it isn't. */
2537
+ const fallbackOf = (placed, rule) => {
2538
+ if (placed === rule) return void 0;
2539
+ return rule === "github" ? "GitHub couldn't be used when it was added (gh not signed in, or the issue failed), so Ork's store keeps it." : `It couldn't be written to ${rule} when it was added, so Ork's store keeps it.`;
2540
+ };
2541
+ const deliveryOf = (store, f) => {
2542
+ if (!f.for.startsWith("project:")) return void 0;
2543
+ const name = f.for.slice(8).trim();
2544
+ const target = store.projects().find((p) => p.id === name);
2545
+ if (target === void 0) return {
2546
+ project: name,
2547
+ registered: false
2548
+ };
2549
+ const live = liveSessionOf(store, target.root);
2550
+ return {
2551
+ project: name,
2552
+ registered: true,
2553
+ root: target.root,
2554
+ keptIn: ruleOf(target),
2555
+ ...live === void 0 ? {} : { live }
2556
+ };
2557
+ };
2558
+ /** `f`, what can be done to it, where it is kept, and what to copy from it. */
2559
+ const detailOf = (context, project, f) => {
2560
+ const rule = ruleOf(project);
2561
+ const fallback = fallbackOf(f.place, rule);
2562
+ const cause = dueCause(f);
2563
+ const delivery = deliveryOf(context.store, f);
2564
+ const digest = digestOf$1(f);
2565
+ return {
2566
+ followUp: f,
2567
+ actions: followUpActions(f),
2568
+ ...cause === void 0 ? {} : { cause },
2569
+ keptIn: {
2570
+ place: f.place,
2571
+ rule,
2572
+ ...f.url === void 0 ? {} : { url: f.url },
2573
+ ...fallback === void 0 ? {} : { fallback }
2574
+ },
2575
+ ...delivery === void 0 ? {} : { delivery },
2576
+ ...digest === void 0 ? {} : { draftDigest: digest },
2577
+ copy: {
2578
+ text: f.draft?.text ?? "",
2579
+ commands: (f.steps ?? []).flatMap((step) => step.command === void 0 ? [] : [step.command])
2580
+ }
2581
+ };
2582
+ };
2583
+ const toItem = (f) => ({
2584
+ id: f.id,
2585
+ project: f.project,
2586
+ title: f.title,
2587
+ for: f.for,
2588
+ kind: f.kind,
2589
+ ...f.note === void 0 ? {} : { note: f.note }
2590
+ });
2591
+ /** The due follow-ups of `roots`, most recently changed first. */
2592
+ const dueOf = (store, roots) => {
2593
+ const due = roots.flatMap((root) => store.followUps(root)).filter((f) => f.status === "due").toSorted((a, b) => b.updatedAt.localeCompare(a.updatedAt));
2594
+ return {
2595
+ count: due.length,
2596
+ items: due.slice(0, 20).map((f) => toItem(f))
2597
+ };
2598
+ };
2599
+ const found = (context, params) => {
2600
+ const project = projectOf(context, params.id);
2601
+ if (project === void 0) return {
2602
+ ok: false,
2603
+ refusal: fail(404, `No registered project \`${params.id}\`.`)
2604
+ };
2605
+ if (params.followUp === void 0) return {
2606
+ ok: true,
2607
+ project
2608
+ };
2609
+ const one = findFollowUp(context.store, project.root, params.followUp);
2610
+ if (!one.ok) {
2611
+ const status = one.error.startsWith("No follow-up") ? 404 : 400;
2612
+ return {
2613
+ ok: false,
2614
+ refusal: fail(status, one.error)
2615
+ };
2616
+ }
2617
+ return {
2618
+ ok: true,
2619
+ project,
2620
+ followUp: one.followUp
2621
+ };
2622
+ };
2623
+ const followUpRoutes = (context) => {
2624
+ const { store } = context;
2625
+ const gh = context.gh ?? runGh;
2626
+ return {
2627
+ followUps: async ({ params, query }) => {
2628
+ const one = found(context, params);
2629
+ if (!one.ok) return one.refusal;
2630
+ const { root } = one.project;
2631
+ const errors = await checkFollowUpsDue({
2632
+ rows: store,
2633
+ project: root,
2634
+ gh
2635
+ }).then((checked) => checked.errors).catch((error) => [error instanceof Error ? error.message : String(error)]);
2636
+ const all = store.followUps(root);
2637
+ const open = (query.status ?? "open") === "open";
2638
+ const read = filterOf(followUpListTable, {
2639
+ ...query,
2640
+ ...open ? { status: "all" } : {}
2641
+ });
2642
+ if (!read.ok) return read.refusal;
2643
+ const other = projectRootOf(context, read.filter);
2644
+ const matched = other === void 0 || other === root ? applyFilters(followUpListTable, all, read.filter) : [];
2645
+ const shown = open ? matched.filter((f) => OPEN.some((s) => s === f.status)) : matched;
2646
+ return ok({
2647
+ project: root,
2648
+ followUps: shown,
2649
+ due: all.filter((f) => f.status === "due").length,
2650
+ checker: {
2651
+ available: errors.length === 0,
2652
+ errors
2653
+ }
2654
+ });
2655
+ },
2656
+ followUp: ({ params }) => {
2657
+ const one = found(context, params);
2658
+ if (!one.ok) return one.refusal;
2659
+ return one.followUp === void 0 ? fail(404, "No such follow-up.") : ok(detailOf(context, one.project, one.followUp));
2660
+ },
2661
+ followUpChange: ({ params, body }) => {
2662
+ const one = found(context, params);
2663
+ if (!one.ok) return one.refusal;
2664
+ if (one.followUp === void 0) return fail(404, "No such follow-up.");
2665
+ const changed = changeFollowUp(store, one.followUp, body.action);
2666
+ return changed.ok ? ok(detailOf(context, one.project, changed.followUp)) : fail(409, changed.error);
2667
+ },
2668
+ followUpPost: async ({ params, body }) => {
2669
+ const one = found(context, params);
2670
+ if (!one.ok) return one.refusal;
2671
+ const { followUp } = one;
2672
+ if (followUp === void 0) return fail(404, "No such follow-up.");
2673
+ const check = followUpActions(followUp).post;
2674
+ if (!check.ok) return fail(409, check.error);
2675
+ if (digestOf$1(followUp) !== body.digest) return fail(409, "The draft is no longer the one you confirmed; open the follow-up again.");
2676
+ const posted = await postFollowUpMessage(followUp, {
2677
+ rows: store,
2678
+ gh,
2679
+ confirmed: body.confirm,
2680
+ home: context.home ?? homedir()
2681
+ });
2682
+ return posted.ok ? ok({
2683
+ ...posted.url === void 0 ? {} : { url: posted.url },
2684
+ followUp: detailOf(context, one.project, posted.followUp)
2685
+ }) : fail(409, posted.error);
2686
+ }
2687
+ };
2688
+ };
2165
2689
  const ANSI = /\u001B\[[0-?]*[ -/]*[@-~]|\u001B\][^\u0007\u001B]*(?:\u0007|\u001B\\)|\u001B[@-Z\\-_]/gu;
2166
2690
  /** A line as a terminal would end up showing it. */
2167
2691
  const plainLine = (raw) => (raw.replaceAll(ANSI, "").split("\r").findLast((part) => part !== "") ?? "").replaceAll(/\s+$/gu, "");
@@ -2668,9 +3192,15 @@ const worktreesRoutes = (context) => {
2668
3192
  };
2669
3193
  return {
2670
3194
  worktrees: async ({ query }) => {
2671
- const all = await readAll(query.project);
3195
+ const filtered = filterOf(worktreeListTable, query);
3196
+ if (!filtered.ok) return filtered.refusal;
3197
+ const all = await readAll(projectRootOf(context, filtered.filter));
2672
3198
  if (!all.ok) return all.refusal;
2673
- return ok({ projects: all.reads.map((read) => listed(context, read)) });
3199
+ return ok({ projects: all.reads.map((read) => {
3200
+ const project = listed(context, read);
3201
+ project.items = applyFilters(worktreeListTable, project.items, filtered.filter);
3202
+ return project;
3203
+ }) });
2674
3204
  },
2675
3205
  worktreeRemovePlan: async ({ body }) => {
2676
3206
  const planned = await removePlanOf(context, body);
@@ -2716,6 +3246,7 @@ const apiRoutes = (context, port) => ({
2716
3246
  ...builderRoutes(context),
2717
3247
  ...contextRoutes(context),
2718
3248
  ...feedbackRoutes(context),
3249
+ ...followUpRoutes(context),
2719
3250
  ...worktreesRoutes(context),
2720
3251
  ...updateRoutes(context, port)
2721
3252
  });
@@ -2856,7 +3387,7 @@ const runStream = async (context, req, res, url, id, shutdown) => {
2856
3387
  }
2857
3388
  const stream = openEventStream(res);
2858
3389
  const end = await followEvents(context.store, found.run.id, (stored) => {
2859
- stream.send("run-event", stored, stored.seq);
3390
+ stream.send("run-event", servedEvent(stored), stored.seq);
2860
3391
  }, {
2861
3392
  follow: true,
2862
3393
  after,
@@ -2869,11 +3400,16 @@ const runStream = async (context, req, res, url, id, shutdown) => {
2869
3400
  const overviewStream = (context, res, url, shutdown) => {
2870
3401
  const key = url.searchParams.get("project");
2871
3402
  const root = key === null ? void 0 : projectOf(context, key)?.root ?? key;
3403
+ const workflow = url.searchParams.get("workflow");
2872
3404
  const stream = openEventStream(res);
2873
3405
  const last = {
2874
3406
  runs: "",
2875
3407
  decisions: "",
2876
- version: ""
3408
+ version: "",
3409
+ followUps: JSON.stringify({
3410
+ count: 0,
3411
+ items: []
3412
+ })
2877
3413
  };
2878
3414
  const tick = () => {
2879
3415
  const filter = root === void 0 ? {} : { project: root };
@@ -2881,11 +3417,15 @@ const overviewStream = (context, res, url, shutdown) => {
2881
3417
  ...filter,
2882
3418
  limit: OVERVIEW_RUNS
2883
3419
  }).map((run) => viewOf$2(context, run));
2884
- const decisions = pendingDecisions(context.store, filter);
3420
+ const decisions = pendingDecisions(context.store, {
3421
+ ...filter,
3422
+ ...workflow === null ? {} : { workflow }
3423
+ });
2885
3424
  for (const [name, value] of [
2886
3425
  ["runs", runs],
2887
3426
  ["decisions", decisions],
2888
- ["version", context.updates.info()]
3427
+ ["version", context.updates.info()],
3428
+ ["followUps", dueOf(context.store, root === void 0 ? context.store.projects().map((p) => p.root) : [root])]
2889
3429
  ]) {
2890
3430
  const text = JSON.stringify(value);
2891
3431
  if (text === last[name]) continue;
@@ -2959,7 +3499,9 @@ const createDispatch = (context, access) => {
2959
3499
  headers: req.headers
2960
3500
  }, {
2961
3501
  port: access.port(),
2962
- token: access.token
3502
+ token: access.token,
3503
+ session: access.session,
3504
+ codes: context.codes
2963
3505
  });
2964
3506
  if (verdict.kind === "refuse") {
2965
3507
  if (api) sendError(res, verdict.status, verdict.error);
@@ -2974,6 +3516,10 @@ const createDispatch = (context, access) => {
2974
3516
  }).end();
2975
3517
  return;
2976
3518
  }
3519
+ if (url.pathname === API.signIn.path && verdict.via !== "bearer") {
3520
+ sendError(res, 403, "Sign-in codes are minted with the API token only.");
3521
+ return;
3522
+ }
2977
3523
  const streamed = RUN_STREAM.pattern.exec(url.pathname);
2978
3524
  if (method === "GET" && streamed !== null) {
2979
3525
  const id = decodeURIComponent(streamed[1] ?? "");
@@ -3043,6 +3589,8 @@ const runningServer = (file) => {
3043
3589
  */
3044
3590
  /** The port `ork ui` serves on unless told otherwise. */
3045
3591
  const DEFAULT_PORT = 4417;
3592
+ /** The overview stream sends the due follow-ups itself; nothing more here. */
3593
+ const noticed = () => {};
3046
3594
  const listen = (server, port) => new Promise((resolve, reject) => {
3047
3595
  server.once("error", (error) => {
3048
3596
  reject(error.code === "EADDRINUSE" ? /* @__PURE__ */ new Error(`Port ${String(port)} is in use; start Ork's UI on another with \`--port <n>\`.`) : error);
@@ -3060,7 +3608,13 @@ const startServer = async (options) => {
3060
3608
  ...options.home === void 0 ? {} : { home: options.home },
3061
3609
  ...options.update === void 0 ? {} : { options: options.update }
3062
3610
  });
3611
+ const { token, session, codes } = createAccess({
3612
+ ...options.token === void 0 ? {} : { token: options.token },
3613
+ ...options.sessionSecret === void 0 ? {} : { session: options.sessionSecret },
3614
+ ...options.codes === void 0 ? {} : { codes: options.codes }
3615
+ });
3063
3616
  const context = {
3617
+ codes,
3064
3618
  store,
3065
3619
  version: options.version,
3066
3620
  pid: process.pid,
@@ -3070,9 +3624,9 @@ const startServer = async (options) => {
3070
3624
  ...uiDir === void 0 ? {} : { uiDir },
3071
3625
  ...options.gh === void 0 ? {} : { gh: options.gh },
3072
3626
  updates,
3073
- ...options.restart === void 0 ? {} : { restart: options.restart }
3627
+ ...options.restart === void 0 ? {} : { restart: options.restart },
3628
+ ...options.noteScrub === void 0 ? {} : { noteScrub: options.noteScrub }
3074
3629
  };
3075
- const token = options.token ?? newToken();
3076
3630
  const shutdown = new AbortController();
3077
3631
  const log = options.log ?? ((line) => {
3078
3632
  process.stderr.write(`${line}\n`);
@@ -3081,6 +3635,7 @@ const startServer = async (options) => {
3081
3635
  const handle = createDispatch(context, {
3082
3636
  port: () => port,
3083
3637
  token,
3638
+ session,
3084
3639
  shutdown: shutdown.signal
3085
3640
  });
3086
3641
  const server = createServer((req, res) => {
@@ -3104,6 +3659,15 @@ const startServer = async (options) => {
3104
3659
  token,
3105
3660
  startedAt
3106
3661
  });
3662
+ const stopChecks = startFollowUpChecks({
3663
+ rows: store,
3664
+ gh: options.gh ?? runGh,
3665
+ projects: () => store.projects().map((project) => project.root),
3666
+ onDue: noticed,
3667
+ onError: (error) => {
3668
+ log(`ork ui: follow-ups: ${error}`);
3669
+ }
3670
+ });
3107
3671
  let closed;
3108
3672
  return {
3109
3673
  port,
@@ -3116,6 +3680,7 @@ const startServer = async (options) => {
3116
3680
  close: () => {
3117
3681
  closed ??= new Promise((resolve) => {
3118
3682
  shutdown.abort();
3683
+ stopChecks();
3119
3684
  updates.close();
3120
3685
  server.close(() => {
3121
3686
  store.close();
@@ -3128,4 +3693,4 @@ const startServer = async (options) => {
3128
3693
  }
3129
3694
  };
3130
3695
  };
3131
- export { openMcp as a, refused$1 as c, uiFilePath as i, newToken as l, startServer as n, tool as o, runningServer as r, answer as s, DEFAULT_PORT as t };
3696
+ export { openMcp as a, refused$1 as c, uiFilePath as i, newToken as l, startServer as n, tool as o, runningServer as r, answer as s, DEFAULT_PORT as t, createClient as u };