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

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 (71) hide show
  1. package/README.md +12 -7
  2. package/dist/__build_info__-D6mXC4cD.mjs +228 -0
  3. package/dist/apply-Mb0fNpCX.mjs +17 -0
  4. package/dist/argument-error-CihH46Le.mjs +27 -0
  5. package/dist/attach-C1OnxyMh.mjs +92 -0
  6. package/dist/attach-CEGbRg1D.mjs +1230 -0
  7. package/dist/bin.mjs +1312 -2390
  8. package/dist/brief-DH66xmj8.mjs +115 -0
  9. package/dist/{decide-CGyx9rv7.mjs → builtin-files-Cf-NZDqM.mjs} +3 -51
  10. package/dist/chips-BqcT58qr.mjs +43 -0
  11. package/dist/command-tree-CEkzWNy6.mjs +63 -0
  12. package/dist/{completion-CSU5vkEt.mjs → completion-DS0gCZRm.mjs} +200 -30
  13. package/dist/context-CGPh12HV.mjs +106 -0
  14. package/dist/detached-Dakw5RZj.mjs +38 -0
  15. package/dist/extra-arguments-BxQMXARO.mjs +13 -0
  16. package/dist/{find-D6Gj1sxg.mjs → find-BTikxc54.mjs} +2 -2
  17. package/dist/flags-C8yCq0j5.mjs +145 -0
  18. package/dist/follow-ups-CtlhHbwe.mjs +563 -0
  19. package/dist/format-BB4E-T6x.mjs +249 -0
  20. package/dist/{help-groups-BwYoAWsh.mjs → help-groups-BpRgBSTh.mjs} +42 -76
  21. package/dist/in-a-run-CbnKjO7t.mjs +27 -0
  22. package/dist/install-CWcpX5g4.mjs +211 -0
  23. package/dist/{limits-B1-DYc3d.mjs → limits-VQqsBoQk.mjs} +2 -2
  24. package/dist/{mcp-DE_EmyVm.mjs → mcp-wGfkMCVF.mjs} +6 -6
  25. package/dist/once-BHxSdZz_.mjs +112 -0
  26. package/dist/orphan-guard-BFKg8VSd.mjs +58 -0
  27. package/dist/output-B1kwo0YU.mjs +439 -0
  28. package/dist/outputs-text-CNTJ7Qq_.mjs +19 -0
  29. package/dist/{pause-CL6N_Xrj.mjs → pause-C_Wxh-KM.mjs} +7 -4
  30. package/dist/print-DNYzq4Rr.mjs +19 -0
  31. package/dist/project-records-XY3ku95x.mjs +233 -0
  32. package/dist/refusals-DQQX3AIb.mjs +116 -0
  33. package/dist/{relay-0SD0erwf.mjs → relay-BA6tsnpc.mjs} +2 -2
  34. package/dist/run-Bvvg-fiF.mjs +234 -0
  35. package/dist/run-DBUAq-Uu.mjs +1452 -0
  36. package/dist/send-nrKtak5F.mjs +105 -0
  37. package/dist/{src-Dujmvv4s.mjs → src-8cOPOgrT.mjs} +12523 -5507
  38. package/dist/{src-BbyEoMn4.mjs → src-CmEvHdju.mjs} +797 -233
  39. package/dist/src-D4JSymhK.mjs +3104 -0
  40. package/dist/{src-DOcK8Jfp.mjs → src-I_RyKu6_.mjs} +1022 -590
  41. package/dist/store-CDpk8Wtz.mjs +16 -0
  42. package/dist/ui/THIRD_PARTY_LICENSES.txt +612 -0
  43. package/dist/ui/assets/app--FijEXiJ.css +1 -0
  44. package/dist/ui/assets/app-BowhjEAe.js +112 -0
  45. package/dist/ui/assets/{index-9dFha9Qn.js → index-CrMa2oHB.js} +3 -3
  46. package/dist/ui/assets/theme-CdXvqTQ6.js +1 -0
  47. package/dist/ui/index.html +2 -2
  48. package/dist/{ui-CTmO2Xzu.mjs → ui-DY3Dnpho.mjs} +54 -25
  49. package/dist/usage-1VwSg_Qt.mjs +257 -0
  50. package/dist/validate-BxayjjkV.mjs +632 -0
  51. package/dist/verdict-CMvPCpWC.mjs +84 -0
  52. package/dist/{watch-CtbgDAcz.mjs → watch-CT0EtLPZ.mjs} +45 -17
  53. package/dist/{workflows-D1_WVukk.mjs → workflows-DHHi-APg.mjs} +30 -5
  54. package/dist/worktrees-CZvbHzbN.mjs +302 -0
  55. package/package.json +10 -8
  56. package/dist/__build_info__-fy6L1Mrd.mjs +0 -615
  57. package/dist/clean-DaHjs__2.mjs +0 -88
  58. package/dist/context-DT8MfEIR.mjs +0 -274
  59. package/dist/format-Dnfv8zMT.mjs +0 -427
  60. package/dist/in-a-run-pLAZeNys.mjs +0 -22
  61. package/dist/list-BrH8KRSJ.mjs +0 -110
  62. package/dist/output-BLQWD6MJ.mjs +0 -950
  63. package/dist/remove-BeGdqroy.mjs +0 -194
  64. package/dist/report-DK4LGqwF.mjs +0 -129
  65. package/dist/send-C8tunWhv.mjs +0 -163
  66. package/dist/src-CCUKRX51.mjs +0 -1204
  67. package/dist/ui/assets/app-BNT0SuIz.css +0 -1
  68. package/dist/ui/assets/app-T7wVAhgx.js +0 -67
  69. package/dist/ui/assets/theme-3xH5jxKs.js +0 -1
  70. package/dist/usage-DYUmP0i4.mjs +0 -419
  71. 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 { $t as planClean, An as requestContinue, Bn as processAlive, Br as loadAgents, C as detectInstaller, Cr as readCondition, Et as walkSummary, Gr as placeFor, Gt as feedbackListTable, I as activeTree, In as pausedOf, Jt as usageListTable, 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 followUpActions, Qr as LESSON_CAP_TOKENS, Qt as keyOf, R as openStore, S as runningScript, Tn as DEFAULT_FOLLOW_UPS_RULE, Tt as walkChecks, Un as nestItems, Ut as LIST_TABLES, Wr as placeFiles, Wt as decisionListTable, Xn as dueCause, Xr as inside, Xt as applyClean, Yn as changeFollowUp, Yr as workflowDirOf, Yt as worktreeListTable, Zn as findFollowUp, Zt as cleanStep, _ as usageReport, _n as postFollowUpMessage, _t as limitsReport, a as planRemove, ai as applyEdits, b as releaseNotes, bi as NodeSchema, cr as recordNote, d as mergeStateOf, di as renderJsonSchema, dt as resumable, en as stepActs, f as planLine, fi as parseWorkflow, fr as DISCUSSIONS, hi as EdgeInputSchema, hr as sendPost, i as lostWork, ii as WorkflowEditSchema, ir as END_KINDS, jn as requestPause, kn as unlinkNodes, m as stateParts, mr as runGh, mt as followEvents, o as projectHead, oi as newWorkflowText, or as previewPost, ot as answerDecision, p as removeRefusal, pr as planPost, pt as detachRun, qn as loadProject, qr as runDirOf, qt as runListTable, r as applyRemove, ri as skillsIn, s as readWorktrees, si as readDocument, st as pendingDecisions, tn as stepPrunes, tr as endOf, u as labelOf, un as nodeSays, ut as detachResume, v as capsHere, vi as expandEdge, vn as checkFollowUpsDue, vr as feedbackActions, wn as liveSessionOf, wt as checkWorkflowText, x as runningRunIds, xn as startFollowUpChecks, y as planUpgrade, yr as openFixRun, yt as explainWorkflowText, z as storePath } from "./src-8cOPOgrT.mjs";
2
+ import { i as placeOf$1, n as acceptItem, r as lessonPlaceOf, t as runInstall } from "./install-CWcpX5g4.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-Cf-NZDqM.mjs";
6
+ import { n as newRunId, t as findRun } from "./find-BTikxc54.mjs";
7
+ import { a as registryShown, c as fetchLatest, d as readUpdateCache, f as refreshUpdateCache, h as nodeSession, i as registryOf, l as newerVersion, m as writeUpdateCache, o as updateSwitchOff, p as updateCachePath, s as checkDue } from "./__build_info__-D6mXC4cD.mjs";
8
+ import { t as outputsText } from "./outputs-text-CNTJ7Qq_.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,13 +996,22 @@ 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
1003
  paused: pausedOf(events)
849
1004
  };
850
1005
  };
1006
+ /** The run's view plus its last finish's declared outputs, when it has any. */
1007
+ const viewWithOutputs = (context, run) => {
1008
+ const finished = context.store.controlEvents(run.id).map((stored) => stored.event).findLast((event) => event.type === "run.finished");
1009
+ const outputs = outputsText(finished?.type === "run.finished" ? finished.result.outputs : void 0);
1010
+ return {
1011
+ ...viewOf$2(context, run),
1012
+ ...outputs.length === 0 ? {} : { outputs }
1013
+ };
1014
+ };
851
1015
  // ../server/src/mcp-answers.ts
852
1016
  /**
853
1017
  * @file What `ork mcp`'s tools answer with (RFC-012): a route's reply as text,
@@ -1108,17 +1272,14 @@ const checkTool = (context) => tool("check", "Checks a workflow as `ork validate
1108
1272
  return answer({
1109
1273
  errors: count("error"),
1110
1274
  warnings: count("warning"),
1111
- diagnostics: diagnostics.map(({ severity, code, message, line, column }) => line === void 0 ? {
1275
+ diagnostics: diagnostics.map(({ severity, code, message, file, line, column }) => Object.assign({
1112
1276
  severity,
1113
1277
  code,
1114
1278
  message
1115
- } : {
1116
- severity,
1117
- code,
1118
- message,
1279
+ }, file === void 0 ? {} : { file }, line === void 0 ? {} : {
1119
1280
  line,
1120
1281
  column
1121
- }),
1282
+ })),
1122
1283
  walk: walk === void 0 ? null : walkSummary(walk),
1123
1284
  ...args.paths === true && walk !== void 0 ? { endings: walk.endings.slice(0, MOST_ENDINGS) } : {}
1124
1285
  });
@@ -1512,6 +1673,136 @@ const builderRoutes = (context) => {
1512
1673
  builderSave: ({ params, body }) => inProject(params.id, (project) => saveDraft(context, project, params.name, body.force === true))
1513
1674
  };
1514
1675
  };
1676
+ // ../server/src/end-view.ts
1677
+ /**
1678
+ * @file Resolves how nodes ended for the API: each `node.finished` event (read
1679
+ * through core's `endOf`, so a run stored before ends existed gets a kind
1680
+ * too) and each tree node get an `EndView` (contract-ends.ts).
1681
+ */
1682
+ /** Core's hints send the reader to Details; with no detail there is none. */
1683
+ const HINT_TO_DETAILS = /\bIts (?:last )?(?:output|detail) is under Details(?:; |\.\s*)/u;
1684
+ const hintFor = (hint, hasDetail) => {
1685
+ if (hasDetail) return hint;
1686
+ const rest = hint.replace(HINT_TO_DETAILS, "");
1687
+ return rest === "" ? rest : rest[0]?.toUpperCase() + rest.slice(1);
1688
+ };
1689
+ const endViewOf = (end) => {
1690
+ const info = END_KINDS[end.kind];
1691
+ return {
1692
+ kind: end.kind,
1693
+ message: end.message,
1694
+ ...end.detail === void 0 ? {} : { detail: end.detail },
1695
+ label: info.label,
1696
+ actor: info.actor,
1697
+ transient: info.transient,
1698
+ hint: hintFor(info.hint, end.detail !== void 0),
1699
+ docsAnchor: info.docsAnchor
1700
+ };
1701
+ };
1702
+ /** The event as served: a `node.finished` gets its `end`, old ones too. */
1703
+ const servedEvent = (stored) => {
1704
+ const { event } = stored;
1705
+ return event.type === "node.finished" ? {
1706
+ ...stored,
1707
+ event: {
1708
+ ...event,
1709
+ end: endViewOf(endOf(event))
1710
+ }
1711
+ } : {
1712
+ ...stored,
1713
+ event
1714
+ };
1715
+ };
1716
+ /**
1717
+ * The tree with each node's latest end, from the events it was folded from. A
1718
+ * node that started again has none until it finishes; a failed node's `now` is
1719
+ * the end's message, not the raw error.
1720
+ */
1721
+ const treeWithEnds = (roots, stored) => {
1722
+ const ends = /* @__PURE__ */ new Map();
1723
+ for (const { event } of stored) if (event.type === "node.started") ends.delete(event.id);
1724
+ else if (event.type === "node.finished") ends.set(event.id, endViewOf(endOf(event)));
1725
+ const dress = (node) => {
1726
+ const end = ends.get(node.id);
1727
+ const children = node.children.map(dress);
1728
+ if (end === void 0) return {
1729
+ ...node,
1730
+ children
1731
+ };
1732
+ const failed = node.state === "failed" && end.actor !== null;
1733
+ return {
1734
+ ...node,
1735
+ ...failed ? { now: end.message } : {},
1736
+ end,
1737
+ children
1738
+ };
1739
+ };
1740
+ return roots.map((node) => dress(node));
1741
+ };
1742
+ // ../server/src/list-query.ts
1743
+ /**
1744
+ * @file Reads a list route's filter fields the way the CLI reads its flags
1745
+ * (RFC-017, ADR-088): each value checked by core's `parseFilters` against the
1746
+ * list's table, a refused one answered as 400 in the CLI's words. A full ISO
1747
+ * time in `since` or `until` is taken as given (the form `usage` had before
1748
+ * the tables), the rest are an age, a day or `all`.
1749
+ */
1750
+ /** A `2026-11-01T10:00:00Z`-style time, as an ISO time; else `undefined`. */
1751
+ const isoTime = (value) => {
1752
+ if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}T/u.test(value)) return;
1753
+ const time = Date.parse(value);
1754
+ return Number.isNaN(time) ? void 0 : new Date(time).toISOString();
1755
+ };
1756
+ /**
1757
+ * The filter `query` asks for, checked against `table`; a value the table
1758
+ * refuses is a 400.
1759
+ */
1760
+ const filterOf = (table, query) => {
1761
+ const times = {};
1762
+ const rest = { ...query };
1763
+ for (const name of ["since", "until"]) {
1764
+ const iso = isoTime(query[name]);
1765
+ if (iso === void 0) continue;
1766
+ times[name] = iso;
1767
+ delete rest[name];
1768
+ }
1769
+ const parsed = parseFilters(table, rest, Date.now());
1770
+ return parsed.ok ? {
1771
+ ok: true,
1772
+ filter: {
1773
+ ...parsed.filter,
1774
+ ...times
1775
+ }
1776
+ } : {
1777
+ ok: false,
1778
+ refusal: fail(400, parsed.error)
1779
+ };
1780
+ };
1781
+ /**
1782
+ * The root a filter's `project` names (a registered project's id or root, else
1783
+ * the value as a root); `undefined` without one or for `all`.
1784
+ */
1785
+ const projectRootOf = (context, filter) => {
1786
+ const key = filter["project"];
1787
+ return key === void 0 || key === "all" ? void 0 : projectOf(context, key)?.root ?? key;
1788
+ };
1789
+ /** `now` is injectable so a test can move the clock. */
1790
+ const createSignInCodes = (now = Date.now) => {
1791
+ const expiries = /* @__PURE__ */ new Map();
1792
+ return {
1793
+ mint: () => {
1794
+ for (const [code, expiry] of expiries) if (expiry <= now()) expiries.delete(code);
1795
+ const code = randomBytes(32).toString("base64url");
1796
+ expiries.set(code, now() + 6e4);
1797
+ return code;
1798
+ },
1799
+ redeem: (code) => {
1800
+ const expiry = expiries.get(code);
1801
+ expiries.delete(code);
1802
+ return expiry !== void 0 && expiry > now();
1803
+ }
1804
+ };
1805
+ };
1515
1806
  // ../server/src/workflows.ts
1516
1807
  /**
1517
1808
  * @file A project's workflows, for the UI's Run workflow dialog: each
@@ -1570,47 +1861,71 @@ const projectWorkflows = (project) => {
1570
1861
  * decisions, and usage. Each reads the store as the matching CLI command does
1571
1862
  * (`ork projects`, `ps`, `tree`, `logs`, `decide --list`, `usage`).
1572
1863
  */
1573
- /** A project filter's root: a registered project's, or the value as a root. */
1574
1864
  /** A registered project's place; caps are only known for one. */
1575
1865
  const placeOf = (context, key) => {
1576
1866
  const known = key === void 0 ? void 0 : projectOf(context, key);
1577
1867
  return known === void 0 ? void 0 : placeFor(known.root, context.store.projects());
1578
1868
  };
1579
- const projectRoot = (context, key) => projectOf(context, key)?.root ?? key;
1869
+ /** `ork ps`'s table, matching `--status` on the live status the API shows. */
1870
+ const liveRunTable = {
1871
+ ...runListTable,
1872
+ fields: runListTable.fields.map((field) => field.name === "status" ? Object.assign({}, field, { get: (run) => run.live }) : field)
1873
+ };
1580
1874
  const readRoutes = (context) => ({
1875
+ ...listFieldsRoutes(),
1581
1876
  health: () => ok({
1582
1877
  version: context.version,
1583
1878
  pid: context.pid,
1584
1879
  startedAt: context.startedAt
1585
1880
  }),
1881
+ signIn: () => ok({
1882
+ code: context.codes.mint(),
1883
+ expiresIn: 60
1884
+ }),
1586
1885
  projects: () => ok(context.store.projects()),
1587
1886
  triggers: () => ok(triggersReport(context.store, context.store.projects(), context.home)),
1588
1887
  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
- }),
1888
+ usage: ({ query }) => {
1889
+ const read = filterOf(usageListTable, query);
1890
+ if (!read.ok) return read.refusal;
1891
+ const { filter } = read;
1892
+ const { workflow, since, until } = filter;
1893
+ const root = projectRootOf(context, filter);
1894
+ return ok({
1895
+ ...usageReport(context.store.usage({
1896
+ ...root === void 0 ? {} : { project: root },
1897
+ ...workflow === void 0 ? {} : { workflow },
1898
+ ...since === void 0 || since === "all" ? {} : { since },
1899
+ ...until === void 0 ? {} : { until }
1900
+ })),
1901
+ limits: limitsReport(context.store, capsHere(context.home, placeOf(context, root)), /* @__PURE__ */ new Date())
1902
+ });
1903
+ },
1598
1904
  workflows: ({ params }) => {
1599
1905
  const project = projectOf(context, params.id);
1600
1906
  return project === void 0 ? fail(404, `No registered project \`${params.id}\`.`) : ok(projectWorkflows(project));
1601
1907
  },
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))),
1908
+ runs: ({ query }) => {
1909
+ const read = filterOf(liveRunTable, query);
1910
+ if (!read.ok) return read.refusal;
1911
+ const root = projectRootOf(context, read.filter);
1912
+ const limit = query.limit ?? 20;
1913
+ const narrowed = Object.keys(read.filter).some((name) => name !== "project");
1914
+ const views = context.store.runs({
1915
+ ...root === void 0 ? {} : { project: root },
1916
+ ...narrowed ? {} : { limit }
1917
+ }).map((run) => viewOf$2(context, run));
1918
+ return ok(applyFilters(liveRunTable, views, read.filter).slice(0, limit));
1919
+ },
1606
1920
  run: ({ params }) => {
1607
1921
  const found = runOf(context, params.id);
1608
- return found.ok ? ok(viewOf$2(context, found.run)) : found.refusal;
1922
+ return found.ok ? ok(viewWithOutputs(context, found.run)) : found.refusal;
1609
1923
  },
1610
1924
  tree: ({ params }) => {
1611
1925
  const found = runOf(context, params.id);
1612
1926
  if (!found.ok) return found.refusal;
1613
- return ok(activeTree(context.store.events(found.run.id)));
1927
+ const stored = context.store.events(found.run.id);
1928
+ return ok(treeWithEnds(nestItems(activeTree(stored)), stored));
1614
1929
  },
1615
1930
  graph: ({ params }) => {
1616
1931
  const found = runOf(context, params.id);
@@ -1620,15 +1935,28 @@ const readRoutes = (context) => ({
1620
1935
  events: ({ params, query }) => {
1621
1936
  const found = runOf(context, params.id);
1622
1937
  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);
1938
+ const events = context.store.events(found.run.id, query.after ?? 0).filter((s) => query.node === void 0 || isNodeEvent(s.event, query.node));
1624
1939
  const newest = query.last === void 0 ? events : events.slice(-query.last);
1625
- return ok(query.limit === void 0 ? newest : newest.slice(0, query.limit));
1940
+ return ok((query.limit === void 0 ? newest : newest.slice(0, query.limit)).map((stored) => servedEvent(stored)));
1626
1941
  },
1627
1942
  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);
1943
+ const read = filterOf(decisionListTable, query);
1944
+ if (!read.ok) return read.refusal;
1945
+ const { run, workflow: named } = read.filter;
1946
+ const workflow = named === void 0 ? {} : { workflow: named };
1947
+ if (run === void 0) {
1948
+ const root = projectRootOf(context, read.filter);
1949
+ return ok(pendingDecisions(context.store, {
1950
+ ...workflow,
1951
+ ...root === void 0 ? {} : { project: root }
1952
+ }));
1953
+ }
1954
+ const found = runOf(context, run);
1630
1955
  if (!found.ok) return found.refusal;
1631
- return ok(pendingDecisions(context.store, { runId: found.run.id }));
1956
+ return ok(pendingDecisions(context.store, {
1957
+ ...workflow,
1958
+ runId: found.run.id
1959
+ }));
1632
1960
  },
1633
1961
  decision: ({ params }) => {
1634
1962
  const found = context.store.decision(params.id);
@@ -1654,6 +1982,8 @@ const RECENT_EVENTS = 30;
1654
1982
  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
1983
  const project = z.string().min(1).describe("A registered project's id or root");
1656
1984
  const run = z.string().min(1).describe("A run's id, or a unique start of it");
1985
+ /** An error answer, in the CLI's words, when `project` names none registered. */
1986
+ 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
1987
  const mcpTools = (context) => {
1658
1988
  const read = readRoutes(context);
1659
1989
  const { builderCatalog } = builderRoutes(context);
@@ -1668,13 +1998,22 @@ const mcpTools = (context) => {
1668
1998
  ...NO_QUERY,
1669
1999
  params: { id: args.project }
1670
2000
  }))),
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
- }))),
2001
+ 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).", {
2002
+ ...RunsQuerySchema.shape,
2003
+ limit: z.union([z.number(), z.string()]).optional()
2004
+ }, ({ limit, ...args }) => {
2005
+ const bad = unknownProject(context, args.project);
2006
+ if (bad !== void 0) return bad;
2007
+ const count = limit === void 0 ? 10 : parseLimit(String(limit));
2008
+ if (count === null) return refused$1(limitRefusal(String(limit)));
2009
+ return fromReply(read.runs({
2010
+ ...NO_QUERY,
2011
+ query: {
2012
+ ...args,
2013
+ limit: count
2014
+ }
2015
+ }));
2016
+ }),
1678
2017
  tool("run", "One run: its status, what it waits on, its cost, and each node's state.", { run }, async (args) => {
1679
2018
  const view = await read.run({
1680
2019
  ...NO_QUERY,
@@ -1704,11 +2043,11 @@ const mcpTools = (context) => {
1704
2043
  }
1705
2044
  }));
1706
2045
  }),
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({
2046
+ 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
2047
  ...NO_QUERY,
1709
2048
  query: args
1710
2049
  }))),
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({
2050
+ 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
2051
  ...NO_QUERY,
1713
2052
  query: args
1714
2053
  }), (open) => ({
@@ -1820,7 +2159,8 @@ const openMcp = (options) => {
1820
2159
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
1821
2160
  ...options.home === void 0 ? {} : { home: options.home },
1822
2161
  ...options.script === void 0 ? {} : { script: options.script },
1823
- updates: createUpdates({ version: options.version })
2162
+ updates: createUpdates({ version: options.version }),
2163
+ codes: createSignInCodes()
1824
2164
  };
1825
2165
  return {
1826
2166
  tools: [...mcpTools(context), ...actingTools(context, { start: options.start })],
@@ -1838,6 +2178,17 @@ const openMcp = (options) => {
1838
2178
  }
1839
2179
  };
1840
2180
  };
2181
+ // ../server/src/access.ts
2182
+ /**
2183
+ * @file The secrets a server starts with (RFC-019): the API token (bearer), the
2184
+ * session secret (the browser's cookie) and the sign-in code store. Each is
2185
+ * the caller's or a new one.
2186
+ */
2187
+ const createAccess = (given) => ({
2188
+ token: given.token ?? newToken(),
2189
+ session: given.session ?? newToken(),
2190
+ codes: given.codes ?? createSignInCodes()
2191
+ });
1841
2192
  // ../server/src/assets.ts
1842
2193
  /**
1843
2194
  * @file The built UI's files (`packages/ui/dist`, shipped as the CLI's
@@ -2010,7 +2361,7 @@ const applyRoute = async (context, item, driver) => {
2010
2361
  * workflows would go and how big that file is, the project a fix would be
2011
2362
  * proposed in, and the run proposing one now, if any.
2012
2363
  */
2013
- const detailOf = (store, item) => {
2364
+ const detailOf$1 = (store, item) => {
2014
2365
  const registered = store.projects();
2015
2366
  const fixRun = openFixRun(store, item);
2016
2367
  const actions = feedbackActions(item, fixRun);
@@ -2106,14 +2457,18 @@ const feedbackRoutes = (context) => {
2106
2457
  note: ({ params, body }) => {
2107
2458
  const found = runOf(context, params.id);
2108
2459
  if (!found.ok) return found.refusal;
2109
- const noted = recordNote(store, found.run, body);
2460
+ const scrub = context.noteScrub?.(found.run, store.projects());
2461
+ const noted = recordNote(store, found.run, body, "", scrub);
2110
2462
  return noted.ok ? ok({ item: noted.item }) : fail(400, noted.error);
2111
2463
  },
2112
- feedbackItems: () => ok(store.feedbackItems({ all: true })),
2464
+ feedbackItems: ({ query }) => {
2465
+ const read = filterOf(feedbackListTable, query);
2466
+ return read.ok ? ok(applyFilters(feedbackListTable, store.feedbackItems({ all: true }), read.filter)) : read.refusal;
2467
+ },
2113
2468
  feedbackItem: ({ params }) => {
2114
2469
  const found = itemOf(store, params.id);
2115
2470
  if (!found.ok) return found.refusal;
2116
- return ok(detailOf(store, found.item));
2471
+ return ok(detailOf$1(store, found.item));
2117
2472
  },
2118
2473
  feedbackStatus: ({ params, body }) => {
2119
2474
  const found = itemOf(store, params.id);
@@ -2162,6 +2517,174 @@ const feedbackRoutes = (context) => {
2162
2517
  }
2163
2518
  };
2164
2519
  };
2520
+ // ../server/src/follow-up-routes.ts
2521
+ /**
2522
+ * @file The follow-ups' routes (specs/follow-ups.md), through the same core
2523
+ * functions as `ork follow-ups`: a project's list (after the due check, which
2524
+ * reads pull requests through the person's own `gh` and only reports a `gh`
2525
+ * that fails), one follow-up with what to copy and what can be done to it,
2526
+ * done, drop, reopen and due, and posting a drafted GitHub comment only with
2527
+ * the person's confirmation of the draft they saw (its digest). Ork never
2528
+ * runs a task's commands.
2529
+ */
2530
+ const OPEN = ["due", "waiting"];
2531
+ /** Stands for what a person saw of a draft: its recipient and text. */
2532
+ const digestOf$1 = (f) => f.draft === void 0 ? void 0 : createHash("sha256").update(JSON.stringify([f.draft.to, f.draft.text])).digest("hex");
2533
+ /** The place a project's rule keeps follow-ups in. */
2534
+ const ruleOf = (project) => (loadProject(project.root, placeFiles(project).rules).project?.followUps ?? DEFAULT_FOLLOW_UPS_RULE).store;
2535
+ /** Why a follow-up isn't where the project's rule says, when it isn't. */
2536
+ const fallbackOf = (placed, rule) => {
2537
+ if (placed === rule) return void 0;
2538
+ 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.`;
2539
+ };
2540
+ const deliveryOf = (store, f) => {
2541
+ if (!f.for.startsWith("project:")) return void 0;
2542
+ const name = f.for.slice(8).trim();
2543
+ const target = store.projects().find((p) => p.id === name);
2544
+ if (target === void 0) return {
2545
+ project: name,
2546
+ registered: false
2547
+ };
2548
+ const live = liveSessionOf(store, target.root);
2549
+ return {
2550
+ project: name,
2551
+ registered: true,
2552
+ root: target.root,
2553
+ keptIn: ruleOf(target),
2554
+ ...live === void 0 ? {} : { live }
2555
+ };
2556
+ };
2557
+ /** `f`, what can be done to it, where it is kept, and what to copy from it. */
2558
+ const detailOf = (context, project, f) => {
2559
+ const rule = ruleOf(project);
2560
+ const fallback = fallbackOf(f.place, rule);
2561
+ const cause = dueCause(f);
2562
+ const delivery = deliveryOf(context.store, f);
2563
+ const digest = digestOf$1(f);
2564
+ return {
2565
+ followUp: f,
2566
+ actions: followUpActions(f),
2567
+ ...cause === void 0 ? {} : { cause },
2568
+ keptIn: {
2569
+ place: f.place,
2570
+ rule,
2571
+ ...f.url === void 0 ? {} : { url: f.url },
2572
+ ...fallback === void 0 ? {} : { fallback }
2573
+ },
2574
+ ...delivery === void 0 ? {} : { delivery },
2575
+ ...digest === void 0 ? {} : { draftDigest: digest },
2576
+ copy: {
2577
+ text: f.draft?.text ?? "",
2578
+ commands: (f.steps ?? []).flatMap((step) => step.command === void 0 ? [] : [step.command])
2579
+ }
2580
+ };
2581
+ };
2582
+ const toItem = (f) => ({
2583
+ id: f.id,
2584
+ project: f.project,
2585
+ title: f.title,
2586
+ for: f.for,
2587
+ kind: f.kind,
2588
+ ...f.note === void 0 ? {} : { note: f.note }
2589
+ });
2590
+ /** The due follow-ups of `roots`, most recently changed first. */
2591
+ const dueOf = (store, roots) => {
2592
+ const due = roots.flatMap((root) => store.followUps(root)).filter((f) => f.status === "due").toSorted((a, b) => b.updatedAt.localeCompare(a.updatedAt));
2593
+ return {
2594
+ count: due.length,
2595
+ items: due.slice(0, 20).map((f) => toItem(f))
2596
+ };
2597
+ };
2598
+ const found = (context, params) => {
2599
+ const project = projectOf(context, params.id);
2600
+ if (project === void 0) return {
2601
+ ok: false,
2602
+ refusal: fail(404, `No registered project \`${params.id}\`.`)
2603
+ };
2604
+ if (params.followUp === void 0) return {
2605
+ ok: true,
2606
+ project
2607
+ };
2608
+ const one = findFollowUp(context.store, project.root, params.followUp);
2609
+ if (!one.ok) {
2610
+ const status = one.error.startsWith("No follow-up") ? 404 : 400;
2611
+ return {
2612
+ ok: false,
2613
+ refusal: fail(status, one.error)
2614
+ };
2615
+ }
2616
+ return {
2617
+ ok: true,
2618
+ project,
2619
+ followUp: one.followUp
2620
+ };
2621
+ };
2622
+ const followUpRoutes = (context) => {
2623
+ const { store } = context;
2624
+ const gh = context.gh ?? runGh;
2625
+ return {
2626
+ followUps: async ({ params, query }) => {
2627
+ const one = found(context, params);
2628
+ if (!one.ok) return one.refusal;
2629
+ const { root } = one.project;
2630
+ const errors = await checkFollowUpsDue({
2631
+ rows: store,
2632
+ project: root,
2633
+ gh
2634
+ }).then((checked) => checked.errors).catch((error) => [error instanceof Error ? error.message : String(error)]);
2635
+ const all = store.followUps(root);
2636
+ const open = (query.status ?? "open") === "open";
2637
+ const read = filterOf(followUpListTable, {
2638
+ ...query,
2639
+ ...open ? { status: "all" } : {}
2640
+ });
2641
+ if (!read.ok) return read.refusal;
2642
+ const other = projectRootOf(context, read.filter);
2643
+ const matched = other === void 0 || other === root ? applyFilters(followUpListTable, all, read.filter) : [];
2644
+ const shown = open ? matched.filter((f) => OPEN.some((s) => s === f.status)) : matched;
2645
+ return ok({
2646
+ project: root,
2647
+ followUps: shown,
2648
+ due: all.filter((f) => f.status === "due").length,
2649
+ checker: {
2650
+ available: errors.length === 0,
2651
+ errors
2652
+ }
2653
+ });
2654
+ },
2655
+ followUp: ({ params }) => {
2656
+ const one = found(context, params);
2657
+ if (!one.ok) return one.refusal;
2658
+ return one.followUp === void 0 ? fail(404, "No such follow-up.") : ok(detailOf(context, one.project, one.followUp));
2659
+ },
2660
+ followUpChange: ({ params, body }) => {
2661
+ const one = found(context, params);
2662
+ if (!one.ok) return one.refusal;
2663
+ if (one.followUp === void 0) return fail(404, "No such follow-up.");
2664
+ const changed = changeFollowUp(store, one.followUp, body.action);
2665
+ return changed.ok ? ok(detailOf(context, one.project, changed.followUp)) : fail(409, changed.error);
2666
+ },
2667
+ followUpPost: async ({ params, body }) => {
2668
+ const one = found(context, params);
2669
+ if (!one.ok) return one.refusal;
2670
+ const { followUp } = one;
2671
+ if (followUp === void 0) return fail(404, "No such follow-up.");
2672
+ const check = followUpActions(followUp).post;
2673
+ if (!check.ok) return fail(409, check.error);
2674
+ if (digestOf$1(followUp) !== body.digest) return fail(409, "The draft is no longer the one you confirmed; open the follow-up again.");
2675
+ const posted = await postFollowUpMessage(followUp, {
2676
+ rows: store,
2677
+ gh,
2678
+ confirmed: body.confirm,
2679
+ home: context.home ?? homedir()
2680
+ });
2681
+ return posted.ok ? ok({
2682
+ ...posted.url === void 0 ? {} : { url: posted.url },
2683
+ followUp: detailOf(context, one.project, posted.followUp)
2684
+ }) : fail(409, posted.error);
2685
+ }
2686
+ };
2687
+ };
2165
2688
  const ANSI = /\u001B\[[0-?]*[ -/]*[@-~]|\u001B\][^\u0007\u001B]*(?:\u0007|\u001B\\)|\u001B[@-Z\\-_]/gu;
2166
2689
  /** A line as a terminal would end up showing it. */
2167
2690
  const plainLine = (raw) => (raw.replaceAll(ANSI, "").split("\r").findLast((part) => part !== "") ?? "").replaceAll(/\s+$/gu, "");
@@ -2668,9 +3191,15 @@ const worktreesRoutes = (context) => {
2668
3191
  };
2669
3192
  return {
2670
3193
  worktrees: async ({ query }) => {
2671
- const all = await readAll(query.project);
3194
+ const filtered = filterOf(worktreeListTable, query);
3195
+ if (!filtered.ok) return filtered.refusal;
3196
+ const all = await readAll(projectRootOf(context, filtered.filter));
2672
3197
  if (!all.ok) return all.refusal;
2673
- return ok({ projects: all.reads.map((read) => listed(context, read)) });
3198
+ return ok({ projects: all.reads.map((read) => {
3199
+ const project = listed(context, read);
3200
+ project.items = applyFilters(worktreeListTable, project.items, filtered.filter);
3201
+ return project;
3202
+ }) });
2674
3203
  },
2675
3204
  worktreeRemovePlan: async ({ body }) => {
2676
3205
  const planned = await removePlanOf(context, body);
@@ -2716,6 +3245,7 @@ const apiRoutes = (context, port) => ({
2716
3245
  ...builderRoutes(context),
2717
3246
  ...contextRoutes(context),
2718
3247
  ...feedbackRoutes(context),
3248
+ ...followUpRoutes(context),
2719
3249
  ...worktreesRoutes(context),
2720
3250
  ...updateRoutes(context, port)
2721
3251
  });
@@ -2856,7 +3386,7 @@ const runStream = async (context, req, res, url, id, shutdown) => {
2856
3386
  }
2857
3387
  const stream = openEventStream(res);
2858
3388
  const end = await followEvents(context.store, found.run.id, (stored) => {
2859
- stream.send("run-event", stored, stored.seq);
3389
+ stream.send("run-event", servedEvent(stored), stored.seq);
2860
3390
  }, {
2861
3391
  follow: true,
2862
3392
  after,
@@ -2869,11 +3399,16 @@ const runStream = async (context, req, res, url, id, shutdown) => {
2869
3399
  const overviewStream = (context, res, url, shutdown) => {
2870
3400
  const key = url.searchParams.get("project");
2871
3401
  const root = key === null ? void 0 : projectOf(context, key)?.root ?? key;
3402
+ const workflow = url.searchParams.get("workflow");
2872
3403
  const stream = openEventStream(res);
2873
3404
  const last = {
2874
3405
  runs: "",
2875
3406
  decisions: "",
2876
- version: ""
3407
+ version: "",
3408
+ followUps: JSON.stringify({
3409
+ count: 0,
3410
+ items: []
3411
+ })
2877
3412
  };
2878
3413
  const tick = () => {
2879
3414
  const filter = root === void 0 ? {} : { project: root };
@@ -2881,11 +3416,15 @@ const overviewStream = (context, res, url, shutdown) => {
2881
3416
  ...filter,
2882
3417
  limit: OVERVIEW_RUNS
2883
3418
  }).map((run) => viewOf$2(context, run));
2884
- const decisions = pendingDecisions(context.store, filter);
3419
+ const decisions = pendingDecisions(context.store, {
3420
+ ...filter,
3421
+ ...workflow === null ? {} : { workflow }
3422
+ });
2885
3423
  for (const [name, value] of [
2886
3424
  ["runs", runs],
2887
3425
  ["decisions", decisions],
2888
- ["version", context.updates.info()]
3426
+ ["version", context.updates.info()],
3427
+ ["followUps", dueOf(context.store, root === void 0 ? context.store.projects().map((p) => p.root) : [root])]
2889
3428
  ]) {
2890
3429
  const text = JSON.stringify(value);
2891
3430
  if (text === last[name]) continue;
@@ -2959,7 +3498,9 @@ const createDispatch = (context, access) => {
2959
3498
  headers: req.headers
2960
3499
  }, {
2961
3500
  port: access.port(),
2962
- token: access.token
3501
+ token: access.token,
3502
+ session: access.session,
3503
+ codes: context.codes
2963
3504
  });
2964
3505
  if (verdict.kind === "refuse") {
2965
3506
  if (api) sendError(res, verdict.status, verdict.error);
@@ -2974,6 +3515,10 @@ const createDispatch = (context, access) => {
2974
3515
  }).end();
2975
3516
  return;
2976
3517
  }
3518
+ if (url.pathname === API.signIn.path && verdict.via !== "bearer") {
3519
+ sendError(res, 403, "Sign-in codes are minted with the API token only.");
3520
+ return;
3521
+ }
2977
3522
  const streamed = RUN_STREAM.pattern.exec(url.pathname);
2978
3523
  if (method === "GET" && streamed !== null) {
2979
3524
  const id = decodeURIComponent(streamed[1] ?? "");
@@ -3043,6 +3588,8 @@ const runningServer = (file) => {
3043
3588
  */
3044
3589
  /** The port `ork ui` serves on unless told otherwise. */
3045
3590
  const DEFAULT_PORT = 4417;
3591
+ /** The overview stream sends the due follow-ups itself; nothing more here. */
3592
+ const noticed = () => {};
3046
3593
  const listen = (server, port) => new Promise((resolve, reject) => {
3047
3594
  server.once("error", (error) => {
3048
3595
  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 +3607,13 @@ const startServer = async (options) => {
3060
3607
  ...options.home === void 0 ? {} : { home: options.home },
3061
3608
  ...options.update === void 0 ? {} : { options: options.update }
3062
3609
  });
3610
+ const { token, session, codes } = createAccess({
3611
+ ...options.token === void 0 ? {} : { token: options.token },
3612
+ ...options.sessionSecret === void 0 ? {} : { session: options.sessionSecret },
3613
+ ...options.codes === void 0 ? {} : { codes: options.codes }
3614
+ });
3063
3615
  const context = {
3616
+ codes,
3064
3617
  store,
3065
3618
  version: options.version,
3066
3619
  pid: process.pid,
@@ -3070,9 +3623,9 @@ const startServer = async (options) => {
3070
3623
  ...uiDir === void 0 ? {} : { uiDir },
3071
3624
  ...options.gh === void 0 ? {} : { gh: options.gh },
3072
3625
  updates,
3073
- ...options.restart === void 0 ? {} : { restart: options.restart }
3626
+ ...options.restart === void 0 ? {} : { restart: options.restart },
3627
+ ...options.noteScrub === void 0 ? {} : { noteScrub: options.noteScrub }
3074
3628
  };
3075
- const token = options.token ?? newToken();
3076
3629
  const shutdown = new AbortController();
3077
3630
  const log = options.log ?? ((line) => {
3078
3631
  process.stderr.write(`${line}\n`);
@@ -3081,6 +3634,7 @@ const startServer = async (options) => {
3081
3634
  const handle = createDispatch(context, {
3082
3635
  port: () => port,
3083
3636
  token,
3637
+ session,
3084
3638
  shutdown: shutdown.signal
3085
3639
  });
3086
3640
  const server = createServer((req, res) => {
@@ -3104,6 +3658,15 @@ const startServer = async (options) => {
3104
3658
  token,
3105
3659
  startedAt
3106
3660
  });
3661
+ const stopChecks = startFollowUpChecks({
3662
+ rows: store,
3663
+ gh: options.gh ?? runGh,
3664
+ projects: () => store.projects().map((project) => project.root),
3665
+ onDue: noticed,
3666
+ onError: (error) => {
3667
+ log(`ork ui: follow-ups: ${error}`);
3668
+ }
3669
+ });
3107
3670
  let closed;
3108
3671
  return {
3109
3672
  port,
@@ -3116,6 +3679,7 @@ const startServer = async (options) => {
3116
3679
  close: () => {
3117
3680
  closed ??= new Promise((resolve) => {
3118
3681
  shutdown.abort();
3682
+ stopChecks();
3119
3683
  updates.close();
3120
3684
  server.close(() => {
3121
3685
  store.close();
@@ -3128,4 +3692,4 @@ const startServer = async (options) => {
3128
3692
  }
3129
3693
  };
3130
3694
  };
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 };
3695
+ 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 };