@krapht/ork 1.0.0-alpha.13 → 1.0.0-alpha.15

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 (51) hide show
  1. package/dist/__build_info__-Bsb1kjgF.mjs +340 -0
  2. package/dist/{store-CM3cERGl.mjs → attach-BHO3X3cf.mjs} +36 -91
  3. package/dist/{attach-E6NNyA3T.mjs → attach-CPespxlQ.mjs} +6 -5
  4. package/dist/bin.mjs +148 -1444
  5. package/dist/brief-za0ozN_E.mjs +58 -0
  6. package/dist/{builtin-files-n6o6oWpB.mjs → builtin-files-2bHf76x-.mjs} +1 -1
  7. package/dist/{chips-CyeB_rhR.mjs → chips-BqcT58qr.mjs} +1 -1
  8. package/dist/{completion-SNNNxnv9.mjs → completion-CQ2UYKuJ.mjs} +52 -11
  9. package/dist/context-BNqDw9z1.mjs +106 -0
  10. package/dist/{find-CjsNYAAf.mjs → find-BYLlszqj.mjs} +1 -1
  11. package/dist/follow-ups-B4GJ3Vzw.mjs +461 -0
  12. package/dist/{format-C60TFpRe.mjs → format-C8MsFj8_.mjs} +30 -16
  13. package/dist/{help-groups-CyfPWlST.mjs → help-groups-CQ98mLjF.mjs} +20 -6
  14. package/dist/{in-a-run-BYjTXaST.mjs → in-a-run-Bp0k0J09.mjs} +1 -1
  15. package/dist/{__build_info__-Da73_nBs.mjs → install-DWtXTq4y.mjs} +3 -227
  16. package/dist/{limits-C5QKZNLT.mjs → limits-B1qzS4cX.mjs} +1 -1
  17. package/dist/{mcp-bcctR-wc.mjs → mcp-BOrVkyhN.mjs} +6 -6
  18. package/dist/{once-DPQu5Q3H.mjs → once-C1fL3GVo.mjs} +4 -4
  19. package/dist/orphan-guard-BFKg8VSd.mjs +58 -0
  20. package/dist/{output-CnRzrbUM.mjs → output-B1kwo0YU.mjs} +17 -1
  21. package/dist/{pause-BPvlDIvz.mjs → pause-DOMK-R8V.mjs} +4 -4
  22. package/dist/post-BzA6Qobq.mjs +44 -0
  23. package/dist/{relay-C8W0t_zc.mjs → relay-OWuu4fib.mjs} +2 -2
  24. package/dist/run--4-1xAF2.mjs +1451 -0
  25. package/dist/{run-CQIfq918.mjs → run-CiPsW9Kf.mjs} +1 -1
  26. package/dist/send-CnNNW-ot.mjs +105 -0
  27. package/dist/{src-CqRqsb2t.mjs → src-B9zWIVqo.mjs} +226 -44
  28. package/dist/{src-CGJGL1sZ.mjs → src-DQKz2eOH.mjs} +326 -81
  29. package/dist/{src-D93tHZ5B.mjs → src-DcrhgipG.mjs} +107 -47
  30. package/dist/{src-BeR6f70A.mjs → src-Dr_fTSu-.mjs} +5858 -3746
  31. package/dist/store-oKJ-cCf3.mjs +16 -0
  32. package/dist/ui/assets/app-Bdc6OBeZ.css +1 -0
  33. package/dist/ui/assets/app-Drp8Dm9y.js +111 -0
  34. package/dist/ui/assets/{index-CWwm8HTG.js → index-BtwkftKe.js} +2 -2
  35. package/dist/ui/index.html +1 -1
  36. package/dist/{ui-D2Ydofq0.mjs → ui-CzKlsCnm.mjs} +4 -4
  37. package/dist/{usage-6WIowblz.mjs → usage-DHGNk2JL.mjs} +13 -141
  38. package/dist/{context-DHudLA5e.mjs → validate-Bs_gg22g.mjs} +17 -107
  39. package/dist/{verdict-CIy8tAyX.mjs → verdict-CMvPCpWC.mjs} +2 -2
  40. package/dist/{watch-uOU_WeBI.mjs → watch-CCCfw3n7.mjs} +24 -8
  41. package/dist/{workflows-7tQdMnrA.mjs → workflows-CW28ZU-z.mjs} +3 -3
  42. package/dist/{worktrees-ZCII298F.mjs → worktrees-CyE8aF6j.mjs} +3 -5
  43. package/package.json +2 -2
  44. package/dist/clean-Bh4KZgzc.mjs +0 -91
  45. package/dist/decide-DuIHACvH.mjs +0 -50
  46. package/dist/list-BUb5Uc3H.mjs +0 -117
  47. package/dist/remove-BkFWGVYo.mjs +0 -201
  48. package/dist/report-CEN8VoUb.mjs +0 -140
  49. package/dist/send-CaZlGG5a.mjs +0 -163
  50. package/dist/ui/assets/app-DYRC0V4p.js +0 -111
  51. package/dist/ui/assets/app-QoHVi-mb.css +0 -1
@@ -1,83 +1,15 @@
1
- import { $ as followEvents, C as isNodeEvent, Ct as processAlive, Et as nestItems, G as requestStop, Gn as runDirOf, Gt as nodeSays, Hn as placeFiles, J as detachResume, Jn as inside, K as tellLiveRun, Q as detachRun, R as linkNodes, Rn as loadAgents, S as activeTree, T as storePath, U as requestContinue, Un as placeFor, V as unlinkNodes, W as requestPause, Xn as LESSON_CAP_TOKENS, Y as resumable, _t as liveStatus, a as planUpgrade, ar as readDocument, at as explainWorkflowText, c as runningScript, dn as runGh, dt as walkChecks, fn as sendPost, ft as walkSummary, gr as expandEdge, i as capsHere, in as END_KINDS, ir as newWorkflowText, l as detectInstaller, ln as DISCUSSIONS, lr as renderJsonSchema, m as triggersReport, nr as WorkflowEditSchema, nt as limitsReport, o as releaseNotes, pr as EdgeInputSchema, qn as workflowDirOf, rr as applyEdits, s as runningRunIds, tn as endOf, tr as skillsIn, un as planPost, ur as parseWorkflow, ut as checkWorkflowText, vr as NodeSchema, vt as pausedOf, w as openStore, xn as readCondition, yt as runStatus } from "./src-BeR6f70A.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__-Da73_nBs.mjs";
3
- import { t as builtinProblem } from "./builtin-files-n6o6oWpB.mjs";
4
- import { n as pendingDecisions, t as answerDecision } from "./decide-DuIHACvH.mjs";
5
- import { n as newRunId, t as findRun } from "./find-CjsNYAAf.mjs";
6
- import { n as usageReport } from "./report-CEN8VoUb.mjs";
7
- import { a as planClean, i as keyOf, o as stepActs, r as cleanStep, s as stepPrunes, t as applyClean } from "./clean-Bh4KZgzc.mjs";
8
- import { a as readWorktrees, c as mergeStateOf, d as stateParts, i as projectHead, l as planLine, n as lostWork, r as planRemove, s as labelOf, t as applyRemove, u as removeRefusal } from "./remove-BkFWGVYo.mjs";
1
+ import { At as walkSummary, Br as WorkflowEditSchema, C as capsHere, Cn as liveStatus, Ct as explainWorkflowText, D as runningScript, Dr as placeFor, E as runningRunIds, En as runStatus, Er as placeFiles, H as isNodeEvent, Hn as END_KINDS, Hr as newWorkflowText, In as loadProject, Jn as planPost, Jr as parseWorkflow, Mn as nestItems, Mr as inside, N as triggersReport, O as detectInstaller, Ot as checkWorkflowText, Pr as LESSON_CAP_TOKENS, S as usageReport, Sn as tellLiveRun, Sr as loadAgents, T as releaseNotes, Tn as pausedOf, U as openStore, Ur as readDocument, V as activeTree, Vr as applyEdits, W as storePath, Xn as sendPost, Yn as runGh, Zr as EdgeInputSchema, _ as keyOf, _n as linkNodes, _t as followEvents, a as planRemove, b as stepPrunes, bn as requestPause, bt as limitsReport, d as planLine, ei as expandEdge, en as nodeSays, f as removeRefusal, g as cleanStep, gt as detachRun, h as applyClean, i as lostWork, jr as workflowDirOf, kn as processAlive, kr as runDirOf, kt as walkChecks, l as labelOf, ln as startFollowUpChecks, lt as answerDecision, mn as DEFAULT_FOLLOW_UPS_RULE, mt as resumable, ni as NodeSchema, o as projectHead, on as checkFollowUpsDue, or as readCondition, p as stateParts, pn as liveSessionOf, pt as detachResume, qn as DISCUSSIONS, qr as renderJsonSchema, r as applyRemove, s as readWorktrees, u as mergeStateOf, ut as pendingDecisions, v as planClean, vn as unlinkNodes, w as planUpgrade, xn as requestStop, y as stepActs, yn as requestContinue, zn as endOf, zr as skillsIn } from "./src-Dr_fTSu-.mjs";
2
+ import { a as acceptItem, c as feedbackActions, i as recordNote, l as openFixRun, n as previewPost, o as lessonPlaceOf, s as placeOf$1, t as runInstall } from "./install-DWtXTq4y.mjs";
3
+ import { _ as dueCause, a as registryShown, c as fetchLatest, d as readUpdateCache, f as refreshUpdateCache, g as changeFollowUp, h as nodeSession, i as registryOf, l as newerVersion, m as writeUpdateCache, o as updateSwitchOff, p as updateCachePath, s as checkDue, v as findFollowUp, y as followUpActions } from "./__build_info__-Bsb1kjgF.mjs";
4
+ import { t as postFollowUpMessage } from "./post-BzA6Qobq.mjs";
5
+ import { t as builtinProblem } from "./builtin-files-2bHf76x-.mjs";
6
+ import { n as newRunId, t as findRun } from "./find-BYLlszqj.mjs";
9
7
  import path from "node:path";
10
8
  import { z } from "zod";
11
9
  import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
12
10
  import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
13
11
  import { homedir } from "node:os";
14
12
  import { createServer } from "node:http";
15
- // ../server/src/contract-builder.ts
16
- /** A new workflow's name, as its file and its `name:` take it. */
17
- const NewWorkflowSchema = z.strictObject({ name: z.string().min(1) });
18
- /** The server checks each edit; this file imports only types of the schema. */
19
- const WorkflowEditsSchema = z.strictObject({ edits: z.array(z.custom()).min(1) });
20
- /** Earlier text, and the file it was based on, to ask before replacing it. */
21
- const DraftTextSchema = z.strictObject({
22
- text: z.string(),
23
- baseHash: z.string().nullable()
24
- });
25
- const SaveWorkflowSchema = z.strictObject({
26
- /** Write over a file that changed since this draft began. */
27
- force: z.boolean().optional() });
28
- const BUILDER_API = {
29
- builderSchema: {
30
- method: "GET",
31
- path: "/api/workflow-schema"
32
- },
33
- builderCatalog: {
34
- method: "GET",
35
- path: "/api/projects/:id/catalog"
36
- },
37
- builderDrafts: {
38
- method: "GET",
39
- path: "/api/projects/:id/builder"
40
- },
41
- builderView: {
42
- method: "GET",
43
- path: "/api/projects/:id/builder/:name"
44
- },
45
- builderCreate: {
46
- method: "POST",
47
- path: "/api/projects/:id/builder",
48
- body: NewWorkflowSchema
49
- },
50
- builderEdit: {
51
- method: "POST",
52
- path: "/api/projects/:id/builder/:name/edits",
53
- body: WorkflowEditsSchema
54
- },
55
- builderRestore: {
56
- method: "POST",
57
- path: "/api/projects/:id/builder/:name/text",
58
- body: DraftTextSchema
59
- },
60
- builderDiscard: {
61
- method: "POST",
62
- path: "/api/projects/:id/builder/:name/discard"
63
- },
64
- builderSave: {
65
- method: "POST",
66
- path: "/api/projects/:id/builder/:name/save",
67
- body: SaveWorkflowSchema
68
- }
69
- };
70
- // ../server/src/contract-context.ts
71
- const CONTEXT_API = {
72
- runContext: {
73
- method: "GET",
74
- path: "/api/runs/:id/nodes/:node/context"
75
- },
76
- builderContext: {
77
- method: "GET",
78
- path: "/api/projects/:id/builder/:name/nodes/:node/context"
79
- }
80
- };
81
13
  // ../server/src/contract-feedback.ts
82
14
  const NoteSchema = z.strictObject({
83
15
  /** Its first line is the title. */
@@ -157,6 +89,133 @@ const FEEDBACK_API = {
157
89
  path: "/api/feedback/:id/split"
158
90
  }
159
91
  };
92
+ // ../server/src/contract-follow-ups.ts
93
+ /** `open` is due and waiting, the default; `all` adds done and dropped. */
94
+ const FollowUpsQuerySchema = z.strictObject({
95
+ status: z.enum([
96
+ "open",
97
+ "all",
98
+ "due",
99
+ "waiting",
100
+ "done",
101
+ "dropped"
102
+ ]).optional(),
103
+ /** Only those for this: `agent`, `me`, `project:<name>` or `person:<name>`. */
104
+ for: z.string().min(1).optional()
105
+ });
106
+ const FollowUpChangeSchema = z.strictObject({ action: z.enum([
107
+ "done",
108
+ "drop",
109
+ "reopen",
110
+ "due"
111
+ ]) });
112
+ /**
113
+ * Posts a drafted GitHub comment through the person's own `gh`. `confirm` says
114
+ * yes, and `digest` is the detail's `draftDigest`, which stands for the
115
+ * recipient and text they saw: the route refuses when the draft is no longer
116
+ * that, so nothing else is sent.
117
+ */
118
+ const FollowUpPostSchema = z.strictObject({
119
+ confirm: z.literal(true),
120
+ digest: z.string().min(1)
121
+ });
122
+ const FOLLOW_UPS_API = {
123
+ followUps: {
124
+ method: "GET",
125
+ path: "/api/projects/:id/follow-ups",
126
+ query: FollowUpsQuerySchema
127
+ },
128
+ followUp: {
129
+ method: "GET",
130
+ path: "/api/projects/:id/follow-ups/:followUp"
131
+ },
132
+ followUpChange: {
133
+ method: "POST",
134
+ path: "/api/projects/:id/follow-ups/:followUp",
135
+ body: FollowUpChangeSchema
136
+ },
137
+ followUpPost: {
138
+ method: "POST",
139
+ path: "/api/projects/:id/follow-ups/:followUp/post",
140
+ body: FollowUpPostSchema
141
+ }
142
+ };
143
+ // ../server/src/contract-after.ts
144
+ /**
145
+ * @file What the API offers after runs: the feedback loop's routes
146
+ * (contract-feedback.ts) and the follow-ups' (contract-follow-ups.ts), joined
147
+ * so the contract reads one part for both.
148
+ */
149
+ const AFTER_API = {
150
+ ...FEEDBACK_API,
151
+ ...FOLLOW_UPS_API
152
+ };
153
+ // ../server/src/contract-builder.ts
154
+ /** A new workflow's name, as its file and its `name:` take it. */
155
+ const NewWorkflowSchema = z.strictObject({ name: z.string().min(1) });
156
+ /** The server checks each edit; this file imports only types of the schema. */
157
+ const WorkflowEditsSchema = z.strictObject({ edits: z.array(z.custom()).min(1) });
158
+ /** Earlier text, and the file it was based on, to ask before replacing it. */
159
+ const DraftTextSchema = z.strictObject({
160
+ text: z.string(),
161
+ baseHash: z.string().nullable()
162
+ });
163
+ const SaveWorkflowSchema = z.strictObject({
164
+ /** Write over a file that changed since this draft began. */
165
+ force: z.boolean().optional() });
166
+ const BUILDER_API = {
167
+ builderSchema: {
168
+ method: "GET",
169
+ path: "/api/workflow-schema"
170
+ },
171
+ builderCatalog: {
172
+ method: "GET",
173
+ path: "/api/projects/:id/catalog"
174
+ },
175
+ builderDrafts: {
176
+ method: "GET",
177
+ path: "/api/projects/:id/builder"
178
+ },
179
+ builderView: {
180
+ method: "GET",
181
+ path: "/api/projects/:id/builder/:name"
182
+ },
183
+ builderCreate: {
184
+ method: "POST",
185
+ path: "/api/projects/:id/builder",
186
+ body: NewWorkflowSchema
187
+ },
188
+ builderEdit: {
189
+ method: "POST",
190
+ path: "/api/projects/:id/builder/:name/edits",
191
+ body: WorkflowEditsSchema
192
+ },
193
+ builderRestore: {
194
+ method: "POST",
195
+ path: "/api/projects/:id/builder/:name/text",
196
+ body: DraftTextSchema
197
+ },
198
+ builderDiscard: {
199
+ method: "POST",
200
+ path: "/api/projects/:id/builder/:name/discard"
201
+ },
202
+ builderSave: {
203
+ method: "POST",
204
+ path: "/api/projects/:id/builder/:name/save",
205
+ body: SaveWorkflowSchema
206
+ }
207
+ };
208
+ // ../server/src/contract-context.ts
209
+ const CONTEXT_API = {
210
+ runContext: {
211
+ method: "GET",
212
+ path: "/api/runs/:id/nodes/:node/context"
213
+ },
214
+ builderContext: {
215
+ method: "GET",
216
+ path: "/api/projects/:id/builder/:name/nodes/:node/context"
217
+ }
218
+ };
160
219
  // ../server/src/contract-links.ts
161
220
  /**
162
221
  * @file The routes that link two nodes of live runs (RFC-003): list, link and
@@ -324,7 +383,7 @@ const API = {
324
383
  },
325
384
  ...BUILDER_API,
326
385
  ...CONTEXT_API,
327
- ...FEEDBACK_API,
386
+ ...AFTER_API,
328
387
  ...WORKTREES_API,
329
388
  ...UPDATE_API,
330
389
  projects: {
@@ -448,7 +507,8 @@ const workflowGraph = (workflow) => ({
448
507
  maxRounds
449
508
  })) : [];
450
509
  }),
451
- channels: (workflow.messages ?? []).map(({ between: [a, b] }) => [a, b])
510
+ channels: (workflow.messages ?? []).map(({ between: [a, b] }) => [a, b]),
511
+ ...workflow.limits?.budgetTokens === void 0 ? {} : { budgetTokens: workflow.limits.budgetTokens }
452
512
  });
453
513
  /** The graph of a workflow file, or `null` if it is gone or does not parse. */
454
514
  const readWorkflowGraph = (file) => {
@@ -475,7 +535,8 @@ const runGraph = (events, file) => {
475
535
  })),
476
536
  edges: edges ?? onFile?.edges ?? [],
477
537
  channels: channels ?? onFile?.channels ?? [],
478
- ...started?.type === "run.started" && started.budgetUsd !== void 0 ? { budgetUsd: started.budgetUsd } : {}
538
+ ...started?.type === "run.started" && started.budgetUsd !== void 0 ? { budgetUsd: started.budgetUsd } : {},
539
+ ...onFile?.budgetTokens === void 0 ? {} : { budgetTokens: onFile.budgetTokens }
479
540
  };
480
541
  };
481
542
  // ../server/src/guards.ts
@@ -2023,7 +2084,7 @@ const applyRoute = async (context, item, driver) => {
2023
2084
  * workflows would go and how big that file is, the project a fix would be
2024
2085
  * proposed in, and the run proposing one now, if any.
2025
2086
  */
2026
- const detailOf = (store, item) => {
2087
+ const detailOf$1 = (store, item) => {
2027
2088
  const registered = store.projects();
2028
2089
  const fixRun = openFixRun(store, item);
2029
2090
  const actions = feedbackActions(item, fixRun);
@@ -2126,7 +2187,7 @@ const feedbackRoutes = (context) => {
2126
2187
  feedbackItem: ({ params }) => {
2127
2188
  const found = itemOf(store, params.id);
2128
2189
  if (!found.ok) return found.refusal;
2129
- return ok(detailOf(store, found.item));
2190
+ return ok(detailOf$1(store, found.item));
2130
2191
  },
2131
2192
  feedbackStatus: ({ params, body }) => {
2132
2193
  const found = itemOf(store, params.id);
@@ -2175,6 +2236,172 @@ const feedbackRoutes = (context) => {
2175
2236
  }
2176
2237
  };
2177
2238
  };
2239
+ // ../server/src/follow-up-routes.ts
2240
+ /**
2241
+ * @file The follow-ups' routes (specs/follow-ups.md), through the same core
2242
+ * functions as `ork follow-ups`: a project's list (after the due check, which
2243
+ * reads pull requests through the person's own `gh` and only reports a `gh`
2244
+ * that fails), one follow-up with what to copy and what can be done to it,
2245
+ * done, drop, reopen and due, and posting a drafted GitHub comment only with
2246
+ * the person's confirmation of the draft they saw (its digest). Ork never
2247
+ * runs a task's commands.
2248
+ */
2249
+ const OPEN = ["due", "waiting"];
2250
+ /** Stands for what a person saw of a draft: its recipient and text. */
2251
+ const digestOf$1 = (f) => f.draft === void 0 ? void 0 : createHash("sha256").update(JSON.stringify([f.draft.to, f.draft.text])).digest("hex");
2252
+ /** The place a project's rule keeps follow-ups in. */
2253
+ const ruleOf = (project) => (loadProject(project.root, placeFiles(project).rules).project?.followUps ?? DEFAULT_FOLLOW_UPS_RULE).store;
2254
+ /** Why a follow-up isn't where the project's rule says, when it isn't. */
2255
+ const fallbackOf = (placed, rule) => {
2256
+ if (placed === rule) return void 0;
2257
+ 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.`;
2258
+ };
2259
+ const deliveryOf = (store, f) => {
2260
+ if (!f.for.startsWith("project:")) return void 0;
2261
+ const name = f.for.slice(8).trim();
2262
+ const target = store.projects().find((p) => p.id === name);
2263
+ if (target === void 0) return {
2264
+ project: name,
2265
+ registered: false
2266
+ };
2267
+ const live = liveSessionOf(store, target.root);
2268
+ return {
2269
+ project: name,
2270
+ registered: true,
2271
+ root: target.root,
2272
+ keptIn: ruleOf(target),
2273
+ ...live === void 0 ? {} : { live }
2274
+ };
2275
+ };
2276
+ /** `f`, what can be done to it, where it is kept, and what to copy from it. */
2277
+ const detailOf = (context, project, f) => {
2278
+ const rule = ruleOf(project);
2279
+ const fallback = fallbackOf(f.place, rule);
2280
+ const cause = dueCause(f);
2281
+ const delivery = deliveryOf(context.store, f);
2282
+ const digest = digestOf$1(f);
2283
+ return {
2284
+ followUp: f,
2285
+ actions: followUpActions(f),
2286
+ ...cause === void 0 ? {} : { cause },
2287
+ keptIn: {
2288
+ place: f.place,
2289
+ rule,
2290
+ ...f.url === void 0 ? {} : { url: f.url },
2291
+ ...fallback === void 0 ? {} : { fallback }
2292
+ },
2293
+ ...delivery === void 0 ? {} : { delivery },
2294
+ ...digest === void 0 ? {} : { draftDigest: digest },
2295
+ copy: {
2296
+ text: f.draft?.text ?? "",
2297
+ commands: (f.steps ?? []).flatMap((step) => step.command === void 0 ? [] : [step.command])
2298
+ }
2299
+ };
2300
+ };
2301
+ const toItem = (f) => ({
2302
+ id: f.id,
2303
+ project: f.project,
2304
+ title: f.title,
2305
+ for: f.for,
2306
+ kind: f.kind,
2307
+ ...f.note === void 0 ? {} : { note: f.note }
2308
+ });
2309
+ /** The due follow-ups of `roots`, most recently changed first. */
2310
+ const dueOf = (store, roots) => {
2311
+ const due = roots.flatMap((root) => store.followUps(root)).filter((f) => f.status === "due").toSorted((a, b) => b.updatedAt.localeCompare(a.updatedAt));
2312
+ return {
2313
+ count: due.length,
2314
+ items: due.slice(0, 20).map((f) => toItem(f))
2315
+ };
2316
+ };
2317
+ const found = (context, params) => {
2318
+ const project = projectOf(context, params.id);
2319
+ if (project === void 0) return {
2320
+ ok: false,
2321
+ refusal: fail(404, `No registered project \`${params.id}\`.`)
2322
+ };
2323
+ if (params.followUp === void 0) return {
2324
+ ok: true,
2325
+ project
2326
+ };
2327
+ const one = findFollowUp(context.store, project.root, params.followUp);
2328
+ if (!one.ok) {
2329
+ const status = one.error.startsWith("No follow-up") ? 404 : 400;
2330
+ return {
2331
+ ok: false,
2332
+ refusal: fail(status, one.error)
2333
+ };
2334
+ }
2335
+ return {
2336
+ ok: true,
2337
+ project,
2338
+ followUp: one.followUp
2339
+ };
2340
+ };
2341
+ const followUpRoutes = (context) => {
2342
+ const { store } = context;
2343
+ const gh = context.gh ?? runGh;
2344
+ return {
2345
+ followUps: async ({ params, query }) => {
2346
+ const one = found(context, params);
2347
+ if (!one.ok) return one.refusal;
2348
+ const { root } = one.project;
2349
+ const errors = await checkFollowUpsDue({
2350
+ rows: store,
2351
+ project: root,
2352
+ gh
2353
+ }).then((checked) => checked.errors).catch((error) => [error instanceof Error ? error.message : String(error)]);
2354
+ const all = store.followUps(root);
2355
+ const status = query.status ?? "open";
2356
+ const shown = all.filter((f) => {
2357
+ if (query.for !== void 0 && f.for !== query.for) return false;
2358
+ if (status === "all") return true;
2359
+ if (status === "open") return OPEN.some((s) => s === f.status);
2360
+ return f.status === status;
2361
+ });
2362
+ return ok({
2363
+ project: root,
2364
+ followUps: shown,
2365
+ due: all.filter((f) => f.status === "due").length,
2366
+ checker: {
2367
+ available: errors.length === 0,
2368
+ errors
2369
+ }
2370
+ });
2371
+ },
2372
+ followUp: ({ params }) => {
2373
+ const one = found(context, params);
2374
+ if (!one.ok) return one.refusal;
2375
+ return one.followUp === void 0 ? fail(404, "No such follow-up.") : ok(detailOf(context, one.project, one.followUp));
2376
+ },
2377
+ followUpChange: ({ params, body }) => {
2378
+ const one = found(context, params);
2379
+ if (!one.ok) return one.refusal;
2380
+ if (one.followUp === void 0) return fail(404, "No such follow-up.");
2381
+ const changed = changeFollowUp(store, one.followUp, body.action);
2382
+ return changed.ok ? ok(detailOf(context, one.project, changed.followUp)) : fail(409, changed.error);
2383
+ },
2384
+ followUpPost: async ({ params, body }) => {
2385
+ const one = found(context, params);
2386
+ if (!one.ok) return one.refusal;
2387
+ const { followUp } = one;
2388
+ if (followUp === void 0) return fail(404, "No such follow-up.");
2389
+ const check = followUpActions(followUp).post;
2390
+ if (!check.ok) return fail(409, check.error);
2391
+ if (digestOf$1(followUp) !== body.digest) return fail(409, "The draft is no longer the one you confirmed; open the follow-up again.");
2392
+ const posted = await postFollowUpMessage(followUp, {
2393
+ rows: store,
2394
+ gh,
2395
+ confirmed: body.confirm,
2396
+ home: context.home ?? homedir()
2397
+ });
2398
+ return posted.ok ? ok({
2399
+ ...posted.url === void 0 ? {} : { url: posted.url },
2400
+ followUp: detailOf(context, one.project, posted.followUp)
2401
+ }) : fail(409, posted.error);
2402
+ }
2403
+ };
2404
+ };
2178
2405
  const ANSI = /\u001B\[[0-?]*[ -/]*[@-~]|\u001B\][^\u0007\u001B]*(?:\u0007|\u001B\\)|\u001B[@-Z\\-_]/gu;
2179
2406
  /** A line as a terminal would end up showing it. */
2180
2407
  const plainLine = (raw) => (raw.replaceAll(ANSI, "").split("\r").findLast((part) => part !== "") ?? "").replaceAll(/\s+$/gu, "");
@@ -2729,6 +2956,7 @@ const apiRoutes = (context, port) => ({
2729
2956
  ...builderRoutes(context),
2730
2957
  ...contextRoutes(context),
2731
2958
  ...feedbackRoutes(context),
2959
+ ...followUpRoutes(context),
2732
2960
  ...worktreesRoutes(context),
2733
2961
  ...updateRoutes(context, port)
2734
2962
  });
@@ -2887,7 +3115,11 @@ const overviewStream = (context, res, url, shutdown) => {
2887
3115
  const last = {
2888
3116
  runs: "",
2889
3117
  decisions: "",
2890
- version: ""
3118
+ version: "",
3119
+ followUps: JSON.stringify({
3120
+ count: 0,
3121
+ items: []
3122
+ })
2891
3123
  };
2892
3124
  const tick = () => {
2893
3125
  const filter = root === void 0 ? {} : { project: root };
@@ -2902,7 +3134,8 @@ const overviewStream = (context, res, url, shutdown) => {
2902
3134
  for (const [name, value] of [
2903
3135
  ["runs", runs],
2904
3136
  ["decisions", decisions],
2905
- ["version", context.updates.info()]
3137
+ ["version", context.updates.info()],
3138
+ ["followUps", dueOf(context.store, root === void 0 ? context.store.projects().map((p) => p.root) : [root])]
2906
3139
  ]) {
2907
3140
  const text = JSON.stringify(value);
2908
3141
  if (text === last[name]) continue;
@@ -3060,6 +3293,8 @@ const runningServer = (file) => {
3060
3293
  */
3061
3294
  /** The port `ork ui` serves on unless told otherwise. */
3062
3295
  const DEFAULT_PORT = 4417;
3296
+ /** The overview stream sends the due follow-ups itself; nothing more here. */
3297
+ const noticed = () => {};
3063
3298
  const listen = (server, port) => new Promise((resolve, reject) => {
3064
3299
  server.once("error", (error) => {
3065
3300
  reject(error.code === "EADDRINUSE" ? /* @__PURE__ */ new Error(`Port ${String(port)} is in use; start Ork's UI on another with \`--port <n>\`.`) : error);
@@ -3121,6 +3356,15 @@ const startServer = async (options) => {
3121
3356
  token,
3122
3357
  startedAt
3123
3358
  });
3359
+ const stopChecks = startFollowUpChecks({
3360
+ rows: store,
3361
+ gh: options.gh ?? runGh,
3362
+ projects: () => store.projects().map((project) => project.root),
3363
+ onDue: noticed,
3364
+ onError: (error) => {
3365
+ log(`ork ui: follow-ups: ${error}`);
3366
+ }
3367
+ });
3124
3368
  let closed;
3125
3369
  return {
3126
3370
  port,
@@ -3133,6 +3377,7 @@ const startServer = async (options) => {
3133
3377
  close: () => {
3134
3378
  closed ??= new Promise((resolve) => {
3135
3379
  shutdown.abort();
3380
+ stopChecks();
3136
3381
  updates.close();
3137
3382
  server.close(() => {
3138
3383
  store.close();