catherd-cli 1.4.0 → 2.0.0-beta.20261022113309

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 (320) hide show
  1. package/README.md +188 -169
  2. package/THIRD_PARTY_NOTICES.md +217 -35
  3. package/catalog/jev.json +27 -1
  4. package/catalog/models.json +56 -239
  5. package/package.json +6 -8
  6. package/plugin/.claude-plugin/plugin.json +1 -1
  7. package/plugin/.codex-plugin/plugin.json +1 -1
  8. package/plugin/bin/catherd-mcp +1 -1
  9. package/plugin/commands/catherd.md +2 -2
  10. package/plugin/skills/catherd/SKILL.md +97 -356
  11. package/plugin/skills/catherd-setup/SKILL.md +21 -20
  12. package/src/adapters/all.ts +0 -4
  13. package/src/adapters/backend.ts +19 -4
  14. package/src/adapters/claude-code/index.ts +126 -14
  15. package/src/adapters/codex/events.ts +10 -0
  16. package/src/adapters/codex/index.ts +17 -5
  17. package/src/adapters/cursor/home.ts +1 -1
  18. package/src/adapters/cursor/index.ts +10 -4
  19. package/src/adapters/opencode/index.ts +38 -1
  20. package/src/cli.ts +27 -27
  21. package/src/domain/agents.ts +3 -1
  22. package/src/domain/bars.ts +33 -107
  23. package/src/domain/blocked.ts +72 -0
  24. package/src/domain/branch.ts +51 -0
  25. package/src/domain/brief.ts +106 -0
  26. package/src/domain/catalog.ts +69 -313
  27. package/src/domain/changes.ts +23 -1
  28. package/src/domain/cost.ts +19 -29
  29. package/src/domain/errors.ts +6 -2
  30. package/src/domain/fail-lines.ts +26 -0
  31. package/src/domain/failover.ts +37 -75
  32. package/src/domain/findings.ts +47 -0
  33. package/src/domain/gate-brief.ts +53 -0
  34. package/src/domain/hints.ts +13 -2
  35. package/src/domain/ids.ts +13 -2
  36. package/src/domain/jev.ts +47 -2
  37. package/src/domain/judge.ts +124 -0
  38. package/src/domain/lane.ts +118 -3
  39. package/src/domain/milestone-memory.ts +53 -0
  40. package/src/domain/notice.ts +32 -8
  41. package/src/domain/notify.ts +32 -0
  42. package/src/domain/plan-doc.ts +198 -0
  43. package/src/domain/plan-lint.ts +141 -0
  44. package/src/domain/profile-rules.ts +175 -138
  45. package/src/domain/profile.ts +140 -63
  46. package/src/domain/record.ts +68 -3
  47. package/src/domain/reply.ts +29 -0
  48. package/src/domain/role-prompts.ts +182 -56
  49. package/src/domain/role-scope.ts +76 -0
  50. package/src/domain/role-tools.ts +34 -0
  51. package/src/domain/roles.ts +2 -0
  52. package/src/domain/route.ts +90 -0
  53. package/src/domain/rules.ts +33 -0
  54. package/src/domain/run-events.ts +298 -0
  55. package/src/domain/run-machine.ts +668 -0
  56. package/src/domain/run-state.ts +1310 -0
  57. package/src/domain/scorecard.ts +197 -0
  58. package/src/domain/select.ts +229 -72
  59. package/src/domain/slopalytics.ts +72 -0
  60. package/src/domain/state.ts +5 -0
  61. package/src/domain/util.ts +22 -0
  62. package/src/domain/workspace.ts +130 -0
  63. package/src/entry/answer-command.ts +39 -0
  64. package/src/entry/catalog-command.ts +24 -165
  65. package/src/entry/check-bin.ts +17 -0
  66. package/src/entry/cli-kit.ts +18 -2
  67. package/src/entry/daemon-bin.ts +22 -0
  68. package/src/entry/deps.ts +2 -0
  69. package/src/entry/doctor-command.ts +20 -2
  70. package/src/entry/glyphs.ts +15 -2
  71. package/src/entry/host-arg.ts +28 -0
  72. package/src/entry/init-command.ts +20 -69
  73. package/src/entry/knowledge-command.ts +121 -5
  74. package/src/entry/lock-command.ts +103 -8
  75. package/src/entry/mcp/handshake.ts +6 -2
  76. package/src/entry/mcp/role-bin.ts +20 -0
  77. package/src/entry/mcp/role-server.ts +134 -0
  78. package/src/entry/mcp/server.ts +85 -21
  79. package/src/entry/mcp/session-tools.ts +149 -0
  80. package/src/entry/panel/openapi.ts +189 -0
  81. package/src/entry/panel/server.ts +410 -0
  82. package/src/entry/panel-command.ts +68 -0
  83. package/src/entry/pause-command.ts +67 -0
  84. package/src/entry/profile-command.ts +33 -18
  85. package/src/entry/role-cli-command.ts +159 -0
  86. package/src/entry/runs-command.ts +81 -27
  87. package/src/entry/scorecard-command.ts +187 -0
  88. package/src/entry/supervise-bin.ts +1 -1
  89. package/src/entry/supervise-command.ts +30 -3
  90. package/src/entry/workspace-command.ts +44 -0
  91. package/src/infra/boot-lock.ts +25 -0
  92. package/src/infra/delivery.ts +3 -0
  93. package/src/infra/dispatch-dir.ts +2 -0
  94. package/src/infra/env.ts +11 -3
  95. package/src/infra/git.ts +70 -0
  96. package/src/infra/heavy-lock.ts +133 -6
  97. package/src/infra/host-probe.ts +16 -0
  98. package/src/infra/launch.ts +63 -7
  99. package/src/infra/lock-activity.ts +88 -0
  100. package/src/infra/login-env.ts +48 -0
  101. package/src/infra/memory.ts +45 -0
  102. package/src/infra/ntfy.ts +40 -0
  103. package/src/infra/open-url.ts +42 -0
  104. package/src/infra/paths.ts +37 -2
  105. package/src/infra/role-mcp.ts +69 -0
  106. package/src/infra/slopalytics.ts +30 -0
  107. package/src/infra/store.ts +6 -1
  108. package/src/infra/supervisor.ts +107 -16
  109. package/src/services/admission.ts +133 -31
  110. package/src/services/backends.ts +4 -6
  111. package/src/services/budget.ts +26 -6
  112. package/src/services/capture.ts +1 -22
  113. package/src/services/catalog-service.ts +155 -254
  114. package/src/services/credentials.ts +15 -26
  115. package/src/services/daemon.ts +181 -0
  116. package/src/services/dispatch-service.ts +209 -17
  117. package/src/services/dispatches.ts +30 -2
  118. package/src/services/doctor-access.ts +8 -3
  119. package/src/services/doctor-backends.ts +1 -1
  120. package/src/services/doctor-checks.ts +47 -0
  121. package/src/services/doctor-docker.ts +221 -0
  122. package/src/services/doctor-role-mcp.ts +65 -0
  123. package/src/services/doctor.ts +72 -15
  124. package/src/services/end-push.ts +157 -0
  125. package/src/services/failover-rungs.ts +109 -0
  126. package/src/services/finalize.ts +24 -2
  127. package/src/services/gate-env.ts +217 -0
  128. package/src/services/gate-service.ts +231 -45
  129. package/src/services/global-install.ts +46 -13
  130. package/src/services/jev-service.ts +2 -4
  131. package/src/services/lane-edit.ts +154 -0
  132. package/src/services/lane-service.ts +291 -55
  133. package/src/services/milestone-check.ts +433 -0
  134. package/src/services/milestone-tree.ts +170 -0
  135. package/src/services/milestones.ts +167 -29
  136. package/src/services/notifier.ts +124 -4
  137. package/src/services/notify-service.ts +26 -0
  138. package/src/services/orchestrator-wait.ts +82 -0
  139. package/src/services/panel-setup.ts +116 -0
  140. package/src/services/pause.ts +161 -0
  141. package/src/services/peek.ts +79 -17
  142. package/src/services/plan-input.ts +151 -0
  143. package/src/services/ports.ts +60 -5
  144. package/src/services/preflight.ts +232 -24
  145. package/src/services/profile-service.ts +31 -3
  146. package/src/services/profile-store.ts +18 -6
  147. package/src/services/protocol.ts +123 -25
  148. package/src/services/provenance.ts +34 -89
  149. package/src/services/questions.ts +24 -0
  150. package/src/services/reconcile.ts +33 -1
  151. package/src/services/reentry.ts +7 -6
  152. package/src/services/role-access.ts +27 -0
  153. package/src/services/routing-service.ts +161 -42
  154. package/src/services/run-briefs.ts +477 -0
  155. package/src/services/run-client.ts +217 -0
  156. package/src/services/run-effects.ts +837 -0
  157. package/src/services/run-events-store.ts +174 -0
  158. package/src/services/run-evidence.ts +1 -1
  159. package/src/services/run-pin.ts +125 -0
  160. package/src/services/run-read.ts +221 -0
  161. package/src/services/run-report.ts +176 -0
  162. package/src/services/run-service.ts +204 -23
  163. package/src/services/run-store.ts +159 -18
  164. package/src/services/scorecard-metrics.ts +86 -0
  165. package/src/services/scorecard.ts +382 -0
  166. package/src/services/scratch.ts +60 -0
  167. package/src/services/sessions.ts +3 -1
  168. package/src/services/state.ts +21 -0
  169. package/src/services/summary.ts +76 -12
  170. package/src/services/tree-path.ts +27 -0
  171. package/src/services/verifier-brief.ts +23 -0
  172. package/src/services/verifier-step.ts +48 -0
  173. package/src/services/workspace-admission.ts +259 -0
  174. package/src/services/workspace-service.ts +259 -0
  175. package/src/services/workspace-store.ts +95 -0
  176. package/web/dist/assets/Brand-PKlUX5TE.js +1 -0
  177. package/web/dist/assets/Brand-PKlUX5TE.js.br +0 -0
  178. package/web/dist/assets/Brand-PKlUX5TE.js.gz +0 -0
  179. package/web/dist/assets/Modal-Dxc5so53.js +1 -0
  180. package/web/dist/assets/Modal-Dxc5so53.js.br +0 -0
  181. package/web/dist/assets/Modal-Dxc5so53.js.gz +0 -0
  182. package/web/dist/assets/Pressable-C8-T6Ed_.js +1 -0
  183. package/web/dist/assets/StatusChip-B5xfI_Mn.js +153 -0
  184. package/web/dist/assets/StatusChip-B5xfI_Mn.js.br +0 -0
  185. package/web/dist/assets/StatusChip-B5xfI_Mn.js.gz +0 -0
  186. package/web/dist/assets/_app-Ea5cpyKe.js +1 -0
  187. package/web/dist/assets/_app-Ea5cpyKe.js.br +0 -0
  188. package/web/dist/assets/_app-Ea5cpyKe.js.gz +0 -0
  189. package/web/dist/assets/_runId-pTG0fXVo.js +1 -0
  190. package/web/dist/assets/_runId-pTG0fXVo.js.br +0 -0
  191. package/web/dist/assets/_runId-pTG0fXVo.js.gz +0 -0
  192. package/web/dist/assets/drawer-Du3cdlok.js +1 -0
  193. package/web/dist/assets/drawer-Du3cdlok.js.br +0 -0
  194. package/web/dist/assets/drawer-Du3cdlok.js.gz +0 -0
  195. package/web/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  196. package/web/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  197. package/web/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  198. package/web/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  199. package/web/dist/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
  200. package/web/dist/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
  201. package/web/dist/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
  202. package/web/dist/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
  203. package/web/dist/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
  204. package/web/dist/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
  205. package/web/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  206. package/web/dist/assets/index-B6GiwDkh.js +11 -0
  207. package/web/dist/assets/index-B6GiwDkh.js.br +0 -0
  208. package/web/dist/assets/index-B6GiwDkh.js.gz +0 -0
  209. package/web/dist/assets/index-C5IbylvM.css +2 -0
  210. package/web/dist/assets/index-C5IbylvM.css.br +0 -0
  211. package/web/dist/assets/index-C5IbylvM.css.gz +0 -0
  212. package/web/dist/assets/label-DFSYBc62.js +1 -0
  213. package/web/dist/assets/label-DFSYBc62.js.br +0 -0
  214. package/web/dist/assets/label-DFSYBc62.js.gz +0 -0
  215. package/web/dist/assets/link-D8EcKZUt.js +1 -0
  216. package/web/dist/assets/link-D8EcKZUt.js.br +0 -0
  217. package/web/dist/assets/link-D8EcKZUt.js.gz +0 -0
  218. package/web/dist/assets/login-DaN5HPPW.js +1 -0
  219. package/web/dist/assets/login-DaN5HPPW.js.br +0 -0
  220. package/web/dist/assets/login-DaN5HPPW.js.gz +0 -0
  221. package/web/dist/assets/query-Cu--ccT3.js +1 -0
  222. package/web/dist/assets/questions-shoO9OVj.js +1 -0
  223. package/web/dist/assets/questions-shoO9OVj.js.br +0 -0
  224. package/web/dist/assets/questions-shoO9OVj.js.gz +0 -0
  225. package/web/dist/assets/react-query.gen-Bu2yV-qJ.js +12 -0
  226. package/web/dist/assets/react-query.gen-Bu2yV-qJ.js.br +0 -0
  227. package/web/dist/assets/react-query.gen-Bu2yV-qJ.js.gz +0 -0
  228. package/web/dist/assets/rolldown-runtime-XKyaHtl9.js +1 -0
  229. package/web/dist/assets/run-status-C0DRuV7U.js +1 -0
  230. package/web/dist/assets/runs-C3rg2zZc.js +1 -0
  231. package/web/dist/assets/runs-C3rg2zZc.js.br +0 -0
  232. package/web/dist/assets/runs-C3rg2zZc.js.gz +0 -0
  233. package/web/dist/assets/setup-5I4Vl9fR.js +1 -0
  234. package/web/dist/assets/setup-5I4Vl9fR.js.br +0 -0
  235. package/web/dist/assets/setup-5I4Vl9fR.js.gz +0 -0
  236. package/web/dist/assets/table-BOxXhqS4.js +1 -0
  237. package/web/dist/assets/table-BOxXhqS4.js.br +0 -0
  238. package/web/dist/assets/table-BOxXhqS4.js.gz +0 -0
  239. package/web/dist/assets/textfield-BzRFkxBF.js +1 -0
  240. package/web/dist/assets/textfield-BzRFkxBF.js.br +0 -0
  241. package/web/dist/assets/textfield-BzRFkxBF.js.gz +0 -0
  242. package/web/dist/assets/time--9kSQbbp.js +1 -0
  243. package/web/dist/assets/use-css-variable-CpnN9WtA.js +5 -0
  244. package/web/dist/assets/use-css-variable-CpnN9WtA.js.br +0 -0
  245. package/web/dist/assets/use-css-variable-CpnN9WtA.js.gz +0 -0
  246. package/web/dist/assets/useMutation-CVPKlR34.js +1 -0
  247. package/web/dist/assets/useMutation-CVPKlR34.js.br +0 -0
  248. package/web/dist/assets/useMutation-CVPKlR34.js.gz +0 -0
  249. package/web/dist/index.html +15 -0
  250. package/catalog/ATTRIBUTION.md +0 -36
  251. package/catalog/scores.json +0 -3202
  252. package/catalog/sources.json +0 -97
  253. package/src/adapters/antigravity/events.ts +0 -131
  254. package/src/adapters/antigravity/home.ts +0 -46
  255. package/src/adapters/antigravity/index.ts +0 -320
  256. package/src/adapters/antigravity/models.ts +0 -37
  257. package/src/adapters/grok/events.ts +0 -112
  258. package/src/adapters/grok/home.ts +0 -132
  259. package/src/adapters/grok/index.ts +0 -339
  260. package/src/adapters/grok/models.ts +0 -48
  261. package/src/domain/calibration.ts +0 -125
  262. package/src/domain/sources.ts +0 -144
  263. package/src/entry/mcp/dispatch-tools.ts +0 -52
  264. package/src/entry/mcp/lane-tools.ts +0 -84
  265. package/src/entry/mcp/protocol-tools.ts +0 -57
  266. package/src/entry/mcp/run-tools.ts +0 -148
  267. package/src/entry/mcp/setup-tools.ts +0 -108
  268. package/src/entry/tui/commands.ts +0 -563
  269. package/src/entry/tui/effects.ts +0 -381
  270. package/src/entry/tui/fixtures.ts +0 -637
  271. package/src/entry/tui/profile-edits.ts +0 -146
  272. package/src/entry/tui/profile-tree.ts +0 -429
  273. package/src/entry/tui/providers/app.tsx +0 -162
  274. package/src/entry/tui/providers/boundary.tsx +0 -57
  275. package/src/entry/tui/providers/data.tsx +0 -160
  276. package/src/entry/tui/providers/keymap.tsx +0 -166
  277. package/src/entry/tui/providers/root.tsx +0 -45
  278. package/src/entry/tui/providers/theme.tsx +0 -17
  279. package/src/entry/tui/providers/toast.tsx +0 -77
  280. package/src/entry/tui/run.tsx +0 -114
  281. package/src/entry/tui/state.ts +0 -373
  282. package/src/entry/tui/stories.ts +0 -114
  283. package/src/entry/tui/text.ts +0 -130
  284. package/src/entry/tui/theme.ts +0 -140
  285. package/src/entry/tui/views/app.tsx +0 -208
  286. package/src/entry/tui/views/dialogs.tsx +0 -41
  287. package/src/entry/tui/views/profile-actions.ts +0 -331
  288. package/src/entry/tui/views/profiles.tsx +0 -520
  289. package/src/entry/tui/views/runs.tsx +0 -573
  290. package/src/entry/tui/views/save-dialog.tsx +0 -340
  291. package/src/entry/tui/views/status.tsx +0 -209
  292. package/src/entry/tui/widgets/chrome.tsx +0 -114
  293. package/src/entry/tui/widgets/dialog-confirm.tsx +0 -58
  294. package/src/entry/tui/widgets/dialog-prompt.tsx +0 -52
  295. package/src/entry/tui/widgets/dialog-select.tsx +0 -212
  296. package/src/entry/tui/widgets/dialog.tsx +0 -68
  297. package/src/entry/tui/widgets/line.tsx +0 -92
  298. package/src/entry/tui/widgets/list.tsx +0 -197
  299. package/src/entry/tui/widgets/spinner.tsx +0 -49
  300. package/src/entry/tui/widgets/toast.tsx +0 -62
  301. package/src/infra/sources/arena.ts +0 -87
  302. package/src/infra/sources/artificial-analysis.ts +0 -112
  303. package/src/infra/sources/cache.ts +0 -124
  304. package/src/infra/sources/csv.ts +0 -38
  305. package/src/infra/sources/epoch.ts +0 -82
  306. package/src/infra/sources/http.ts +0 -75
  307. package/src/infra/sources/litellm.ts +0 -58
  308. package/src/infra/sources/models-dev.ts +0 -59
  309. package/src/infra/sources/openrouter-endpoints.ts +0 -52
  310. package/src/infra/sources/openrouter-models.ts +0 -60
  311. package/src/infra/sources/rows.ts +0 -38
  312. package/src/infra/sources/vectara.ts +0 -69
  313. package/src/infra/sources/zip.ts +0 -48
  314. package/src/services/catalog-refresh.ts +0 -201
  315. package/src/services/doctor-sources.ts +0 -76
  316. package/src/services/runs-page.ts +0 -255
  317. package/src/services/source-derive.ts +0 -374
  318. package/src/services/source-sync.ts +0 -305
  319. package/src/services/standins.ts +0 -204
  320. package/src/services/treat-likes.ts +0 -97
@@ -1,255 +0,0 @@
1
- import { sessionKey } from "../domain/host.ts";
2
- import { existsSync, readdirSync, readFileSync } from "node:fs";
3
- import { join } from "node:path";
4
- import { budgetStatus } from "../domain/budget.ts";
5
- import { ID_PATTERN } from "../domain/ids.ts";
6
- import type { RunRecord } from "../domain/record.ts";
7
- import { dispatchPaths } from "../infra/dispatch-dir.ts";
8
- import { nonBlankLines } from "../infra/store.ts";
9
- import { spendOf } from "./budget.ts";
10
- import { dispatchState, listDispatches, liveDispatches } from "./dispatches.ts";
11
- import { lastActivity } from "./finalize.ts";
12
- import type { Deps } from "./ports.ts";
13
- import { findRun, listRuns, readRecords, type Run, runPaths } from "./run-store.ts";
14
- import { groupRuns, type SessionGroup } from "./session-view.ts";
15
-
16
- /**
17
- * Spec §4: what the Runs tab shows. The top level is the sessions; a session's screen holds its runs, each with its
18
- * milestones and every role (live ones first); a role's screen holds its brief, reply and record.
19
- */
20
-
21
- export interface SessionRow {
22
- /** the session id; null for "earlier runs" */
23
- key: string | null;
24
- name: string;
25
- live: boolean;
26
- runs: number;
27
- liveRoles: number;
28
- landed: number;
29
- /** ISO time of its newest activity */
30
- lastActivity: string;
31
- }
32
-
33
- export interface RoleRow {
34
- run: string;
35
- dispatchId: string;
36
- name: string;
37
- role: string;
38
- rung: string;
39
- /** running | starting while live; once finished, the record's status (`finished` until it has one) */
40
- status: string;
41
- live: boolean;
42
- /** a live role's admission time: the page ticks its elapsed time from it */
43
- since: string;
44
- /** a finished role's seconds, from its record */
45
- secs: number | null;
46
- /** a live role's last command, file edit or message line */
47
- lastEvent: string | null;
48
- replyStatus: string | null;
49
- }
50
-
51
- export interface Milestone {
52
- name: string;
53
- landed: boolean;
54
- /** the ledger's "what" once landed */
55
- what: string;
56
- }
57
-
58
- export interface SessionRun {
59
- id: string;
60
- title: string;
61
- repo: string;
62
- createdAt: string;
63
- continued: "here" | "elsewhere" | null;
64
- continuedIn: string | null;
65
- /** the budget's spent fraction, null without a cap */
66
- budget: number | null;
67
- milestones: Milestone[];
68
- roles: RoleRow[];
69
- }
70
-
71
- export interface SessionDetail {
72
- session: SessionRow;
73
- runs: SessionRun[];
74
- /** the run folders to watch for changes */
75
- dirs: string[];
76
- }
77
-
78
- export interface RoleDetail {
79
- run: string;
80
- runTitle: string;
81
- dispatchId: string;
82
- name: string;
83
- role: string;
84
- rung: string;
85
- /** running | starting | finished */
86
- state: string;
87
- brief: string;
88
- reply: string;
89
- record: RunRecord | null;
90
- }
91
-
92
- export interface MilestoneDetail {
93
- run: string;
94
- runTitle: string;
95
- name: string;
96
- landed: boolean;
97
- what: string;
98
- /** R/digests/<name>.md; null when land has not written one (not landed, or landed by 1.0) or it is empty */
99
- digest: string | null;
100
- }
101
-
102
- const keyOf = (g: SessionGroup): string | null => (g.session ? sessionKey(g.session) : null);
103
-
104
- /** The runs that live on in a session now (it owns them): each run's live roles and landings count once. */
105
- const ownRuns = (g: SessionGroup): Run[] => g.runs.filter((x) => x.current).map((x) => x.run);
106
-
107
- function rowOf(deps: Deps, g: SessionGroup): SessionRow {
108
- const own = ownRuns(g);
109
- return {
110
- key: keyOf(g),
111
- name: g.session?.name ?? "earlier runs",
112
- live: g.session?.live ?? false,
113
- runs: g.runs.length,
114
- liveRoles: own.reduce((n, r) => n + liveDispatches(r, deps.now()).length, 0),
115
- landed: own.reduce((n, r) => n + Math.max(0, nonBlankLines(runPaths(r.dir).ledger).length - 1), 0),
116
- lastActivity: new Date(g.lastActivity).toISOString(),
117
- };
118
- }
119
-
120
- /** The Runs tab's top level: every session, newest activity first, "earlier runs" last. */
121
- export function sessionRows(deps: Deps): { rows: SessionRow[]; warnings: string[] } {
122
- const { runs, corrupt } = listRuns();
123
- return {
124
- rows: groupRuns(runs).map((g) => rowOf(deps, g)),
125
- warnings: corrupt.map((c) => `skipped run ${c.id}: ${c.reason}`),
126
- };
127
- }
128
-
129
- /** The ledger's landed milestones, then each milestone a lane file names that has not landed. */
130
- function milestonesOf(run: Run): Milestone[] {
131
- const landed = nonBlankLines(runPaths(run.dir).ledger)
132
- .slice(1)
133
- .map((l) => l.split(" | "))
134
- .map(([name, what]) => ({ name: (name ?? "").trim(), landed: true, what: (what ?? "").trim() }));
135
- const lanes = existsSync(runPaths(run.dir).lanes) ? readdirSync(runPaths(run.dir).lanes) : [];
136
- const open = [...new Set(lanes.map((f) => /^(M\d+)\./.exec(f)?.[1]).filter((m): m is string => !!m))]
137
- .filter((m) => !landed.some((l) => l.name === m))
138
- .map((name) => ({ name, landed: false, what: "" }));
139
- const n = (m: string) => Number(/\d+/.exec(m)?.[0] ?? 0);
140
- return [...landed, ...open.sort((a, b) => n(a.name) - n(b.name))];
141
- }
142
-
143
- /** Each role's latest dispatch, live ones first, then the newest finished first. */
144
- function rolesOf(deps: Deps, run: Run): RoleRow[] {
145
- const now = deps.now();
146
- const records = new Map(readRecords(run).records.map((r) => [r.dispatchId, r]));
147
- const latest = new Map<string, ReturnType<typeof listDispatches>[number]>();
148
- for (const d of listDispatches(run)) latest.set(d.admit.name, d);
149
- const rows = [...latest.values()].map((d): RoleRow => {
150
- const r = records.get(d.admit.dispatchId);
151
- const state = r ? "finished" : dispatchState(d, now);
152
- const live = state !== "finished";
153
- return {
154
- run: run.id,
155
- dispatchId: d.admit.dispatchId,
156
- name: d.admit.name,
157
- role: d.admit.role,
158
- rung: d.admit.rung,
159
- status: r?.status ?? state,
160
- live,
161
- since: d.admit.admittedAt,
162
- secs: r?.secs ?? null,
163
- lastEvent: live ? lastActivity(d) : null,
164
- replyStatus: r?.replyStatus ?? null,
165
- };
166
- });
167
- return rows.sort((a, b) =>
168
- a.live !== b.live ? (a.live ? -1 : 1) : b.dispatchId.localeCompare(a.dispatchId),
169
- );
170
- }
171
-
172
- function sessionRun(deps: Deps, x: SessionGroup["runs"][number]): SessionRun {
173
- const run = x.run;
174
- let budget: number | null = null;
175
- try {
176
- const b = budgetStatus(
177
- spendOf(run, readRecords(run).records, liveDispatches(run, deps.now()), deps.now()),
178
- deps.profiles.budgetFor?.(run.meta.repo) ?? deps.profiles.forRepo(run.meta.repo).budget,
179
- );
180
- budget = b?.fraction ?? null;
181
- } catch {
182
- // an unreadable profile: no bar, the rest of the page stands
183
- }
184
- return {
185
- id: run.id,
186
- title: run.meta.title,
187
- repo: run.meta.repo,
188
- createdAt: run.meta.createdAt,
189
- continued: x.continued,
190
- continuedIn: x.continuedIn,
191
- budget,
192
- milestones: milestonesOf(run),
193
- roles: rolesOf(deps, run),
194
- };
195
- }
196
-
197
- /** A session's screen: its runs, newest first, each with its milestones and roles. */
198
- export function sessionDetail(deps: Deps, key: string | null): SessionDetail {
199
- const g = groupRuns(listRuns().runs).find((x) => keyOf(x) === key);
200
- if (!g) throw new Error(key === null ? "no earlier runs" : `no session "${key}" in the runs`);
201
- return {
202
- session: rowOf(deps, g),
203
- runs: g.runs.map((x) => sessionRun(deps, x)),
204
- dirs: g.runs.map((x) => x.run.dir),
205
- };
206
- }
207
-
208
- const text = (file: string): string => {
209
- try {
210
- return readFileSync(file, "utf8");
211
- } catch {
212
- return "";
213
- }
214
- };
215
-
216
- /** A role's screen: its brief, its reply and its record. */
217
- export function roleDetail(deps: Deps, runId: string, dispatchId: string): RoleDetail {
218
- const run = findRun(runId);
219
- const d = listDispatches(run).find((x) => x.admit.dispatchId === dispatchId);
220
- if (!d) throw new Error(`no dispatch ${dispatchId} in run ${runId}`);
221
- const record = readRecords(run).records.find((r) => r.dispatchId === dispatchId) ?? null;
222
- const p = dispatchPaths(d.dir);
223
- return {
224
- run: run.id,
225
- runTitle: run.meta.title,
226
- dispatchId,
227
- name: d.admit.name,
228
- role: d.admit.role,
229
- rung: d.admit.rung,
230
- state: record ? "finished" : dispatchState(d, deps.now()),
231
- brief: text(p.brief),
232
- reply: text(p.reply),
233
- record,
234
- };
235
- }
236
-
237
- /** Spec 1.1 §10: a milestone's screen, its digest. */
238
- export function milestoneDetail(runId: string, name: string): MilestoneDetail {
239
- const run = findRun(runId);
240
- // the id rule land writes digests under (assertId): never a path out of the run folder
241
- const valid = ID_PATTERN.test(name) && !name.includes("..");
242
- const m = valid ? milestonesOf(run).find((x) => x.name === name) : undefined;
243
- if (!m) throw new Error(`no milestone "${name}" in run ${runId}`);
244
- const file = join(run.dir, "digests", `${name}.md`);
245
- const text = existsSync(file) ? readFileSync(file, "utf8") : "";
246
- return {
247
- run: run.id,
248
- runTitle: run.meta.title,
249
- name,
250
- landed: m.landed,
251
- what: m.what,
252
- // an empty file is no digest either: land writes the whole digest at once
253
- digest: text.trim() ? text : null,
254
- };
255
- }
@@ -1,374 +0,0 @@
1
- import { measuredOrBetter } from "../domain/bars.ts";
2
- import { calibrate, DIM_SOURCES, type FieldRef } from "../domain/calibration.ts";
3
- import {
4
- DIMS,
5
- type Dim,
6
- type Family,
7
- type FamilyFacts,
8
- type ModelsFile,
9
- outranks,
10
- type Score,
11
- type ScoresFile,
12
- } from "../domain/catalog.ts";
13
- import {
14
- defaultEffortOf,
15
- type Derived,
16
- familyEfforts,
17
- type FitRow,
18
- idMapper,
19
- type IdMapper,
20
- nearestEffort,
21
- type SourceId,
22
- type SourcesFile,
23
- splitSourceRung,
24
- } from "../domain/sources.ts";
25
- import { type AaAnswer, parseArtificialAnalysis } from "../infra/sources/artificial-analysis.ts";
26
- import { parseArena } from "../infra/sources/arena.ts";
27
- import { parseEpoch } from "../infra/sources/epoch.ts";
28
- import { LITELLM_EFFORTS, parseLiteLlm } from "../infra/sources/litellm.ts";
29
- import { modelsDevFacts } from "../infra/sources/models-dev.ts";
30
- import { parseOpenRouterEndpoints } from "../infra/sources/openrouter-endpoints.ts";
31
- import { type OpenRouterModel, parseOpenRouterModels } from "../infra/sources/openrouter-models.ts";
32
- import type { SourceRow } from "../infra/sources/rows.ts";
33
- import { parseVectara } from "../infra/sources/vectara.ts";
34
-
35
- /** Each source's cached answer (its `data`) with its fetch time; a source never fetched is absent. */
36
- export type RawAnswers = Partial<Record<SourceId, { fetchedAt: string; data: unknown }>>;
37
-
38
- export interface DeriveContext {
39
- models: ModelsFile;
40
- scores: ScoresFile;
41
- sources: SourcesFile;
42
- now: number;
43
- }
44
-
45
- /** Spec 1.2 §3.5: a price that differs by more than this is a sync warning. */
46
- const PRICE_TOLERANCE = 0.1;
47
- /** Spec 1.2 §3.5: the models.dev provider that serves a backend key its own catalog. */
48
- const OPENCODE_KEYS = ["opencode", "opencode-go"] as const;
49
-
50
- interface Keyed {
51
- key: string;
52
- family: Family | null;
53
- effort: string;
54
- assumed: boolean;
55
- row: SourceRow;
56
- }
57
-
58
- /** The score sources' rows, by source (spec 1.2 §3.3). */
59
- function scoreRows(raw: RawAnswers): [SourceId, SourceRow[]][] {
60
- const out: [SourceId, SourceRow[]][] = [];
61
- const { arena, vectara, epoch } = raw;
62
- const aa = raw["artificial-analysis"];
63
- if (arena) out.push(["arena", parseArena(arena.data as Record<string, unknown>, arena.fetchedAt)]);
64
- if (vectara) out.push(["vectara", parseVectara(String(vectara.data), vectara.fetchedAt)]);
65
- if (epoch) out.push(["epoch", parseEpoch(epoch.data as Record<string, string>, epoch.fetchedAt)]);
66
- if (aa) out.push(["artificial-analysis", parseArtificialAnalysis(aa.data as AaAnswer, aa.fetchedAt)]);
67
- return out;
68
- }
69
-
70
- const round = (v: number) => Math.round(v * 1e4) / 1e4;
71
-
72
- /**
73
- * Spec 1.2 §3.4–§4.3: what the cached answers say. Each score source's ids are mapped onto the catalog's
74
- * families (unmatched ones listed, never guessed), each dimension's anchor gives `measured` values, every
75
- * other source is fitted onto the anchor and gives `calibrated` values when the fit may be used, and an
76
- * effort no source covers takes the nearest covered effort's value as `adjacent`. models.dev gives the facts;
77
- * OpenRouter and LiteLLM only cross-check them.
78
- */
79
- export function derive(raw: RawAnswers, ctx: DeriveContext): Derived {
80
- const map = idMapper(ctx.models.families, ctx.sources.aliases);
81
- const unmatched: Record<string, Set<string>> = {};
82
- const table = new Map<string, Map<string, Keyed>>();
83
- for (const [source, rows] of scoreRows(raw))
84
- for (const row of rows) {
85
- const { id, effort } = splitSourceRung(row.rung);
86
- const family = map.family(id);
87
- if (!family) (unmatched[source] ??= new Set()).add(id);
88
- // spec 1.3 §7.1: a family with no effort (Composer) has one rung, #default, whatever effort a source names
89
- const effortless = family !== null && familyEfforts(family).length === 0;
90
- const e = effortless ? "default" : (effort ?? (family ? defaultEffortOf(ctx.sources, family) : ""));
91
- const key = `${map.key(family?.id ?? id)}#${e}`;
92
- const field = `${source}.${row.field}`;
93
- const at = table.get(field) ?? new Map<string, Keyed>();
94
- table.set(field, at);
95
- const had = at.get(key);
96
- if (!had || row.value > had.row.value)
97
- at.set(key, {
98
- key,
99
- family,
100
- effort: e,
101
- assumed: effort === null && family !== null && !effortless,
102
- row,
103
- });
104
- }
105
- const valuesOf = (f: FieldRef) =>
106
- new Map([...(table.get(`${f.source}.${f.field}`)?.values() ?? [])].map((k) => [k.key, k.row.value]));
107
-
108
- const scores: Score[] = [];
109
- const fits: FitRow[] = [];
110
- const push = (dim: Dim, f: FieldRef, k: Keyed, value: number, extra: Partial<Score>) => {
111
- if (!k.family) return;
112
- scores.push({
113
- rung: `${k.family.id}#${k.effort}`,
114
- dim,
115
- value: round(value),
116
- benchmark: f.benchmark,
117
- version: k.row.date,
118
- url: k.row.url,
119
- date: k.row.date,
120
- confidence: "measured",
121
- source: f.source,
122
- ...(k.assumed ? { effortAssumed: true } : {}),
123
- ...extra,
124
- });
125
- };
126
- for (const dim of DIMS) {
127
- const spec = DIM_SOURCES[dim];
128
- let anchor: Map<string, number>;
129
- if (spec.anchor === "shipped") {
130
- anchor = new Map();
131
- // the hand-typed values published for the rung; never the shipped keyless ones (they carry a source)
132
- for (const s of ctx.scores.scores)
133
- if (s.dim === dim && s.source === undefined && measuredOrBetter(s)) {
134
- const { id, effort } = splitSourceRung(s.rung);
135
- anchor.set(`${map.key(id)}#${effort}`, s.value);
136
- }
137
- } else {
138
- const ref = spec.anchor;
139
- anchor = valuesOf(ref);
140
- for (const k of table.get(`${ref.source}.${ref.field}`)?.values() ?? [])
141
- push(dim, ref, k, k.row.value, {});
142
- }
143
- for (const other of spec.others) {
144
- const values = valuesOf(other);
145
- if (values.size === 0) continue;
146
- const r = calibrate(anchor, values);
147
- fits.push({
148
- dim,
149
- source: other.source,
150
- field: other.field,
151
- n: r.n,
152
- a: r.fit?.a ?? null,
153
- b: r.fit?.b ?? null,
154
- r2: r.fit?.r2 ?? null,
155
- used: r.used,
156
- ...(r.why ? { why: r.why } : {}),
157
- });
158
- const fit = r.used ? r.fit : null;
159
- if (!fit) continue;
160
- for (const k of table.get(`${other.source}.${other.field}`)?.values() ?? [])
161
- push(dim, other, k, fit.a * k.row.value + fit.b, {
162
- confidence: "calibrated",
163
- fit: { source: other.source, field: other.field, a: fit.a, b: fit.b, r2: fit.r2, n: fit.n },
164
- });
165
- }
166
- }
167
- const shipped = new Set<string>();
168
- for (const s of ctx.scores.scores)
169
- if (s.confidence !== "inferred") {
170
- const { id, effort } = splitSourceRung(s.rung);
171
- const family = map.family(id);
172
- if (family && effort !== null) shipped.add(`${family.id}#${effort}|${s.dim}`);
173
- }
174
- scores.push(
175
- ...adjacent(ctx.models.families, scores, shipped, ctx.now, (f) => defaultEffortOf(ctx.sources, f)),
176
- );
177
-
178
- const { facts, warnings } = factsOf(raw, ctx, map);
179
- return {
180
- schema: 1,
181
- builtAt: new Date(ctx.now).toISOString(),
182
- scores,
183
- facts,
184
- fits,
185
- unmatched: Object.fromEntries(Object.entries(unmatched).map(([s, ids]) => [s, [...ids].sort()])),
186
- warnings,
187
- features: aaFeatures(table),
188
- };
189
- }
190
-
191
- /** Spec 1.2 §6.3: the Artificial Analysis numbers a stand-in is ranked on, as AA names them. */
192
- export const AA_FEATURES = [
193
- "artificial_analysis_intelligence_index",
194
- "hle",
195
- "scicode",
196
- "lcr",
197
- "cost_per_task",
198
- "median_output_tokens_per_second",
199
- ] as const;
200
-
201
- /** Each catalog rung's AA stand-in features, from the rows `derive` keyed (never shipped: AA is keyed). */
202
- function aaFeatures(table: Map<string, Map<string, Keyed>>): Derived["features"] {
203
- const out: Derived["features"] = {};
204
- for (const field of AA_FEATURES)
205
- for (const k of table.get(`artificial-analysis.${field}`)?.values() ?? []) {
206
- if (!k.family) continue;
207
- (out[`${k.family.id}#${k.effort}`] ??= {})[field] = k.row.value;
208
- }
209
- return out;
210
- }
211
-
212
- /**
213
- * Spec 1.2 §4.3 `adjacent`: for each family effort a dimension has no value at (neither one in `direct` nor
214
- * one `shipped` names, as `<family>#<effort>|<dim>`), the best value in `direct` at the nearest effort that
215
- * has one (the weaker on a tie). A sync spreads only its own values, and never onto a value the shipped file
216
- * carries; `rebuildShipped` spreads the shipped file's published values, with an empty `shipped`. A family that
217
- * one backend runs without an effort (Cursor's bare slug, spec 1.3 §7.1) also gets `#default`, carried from the
218
- * value nearest `defaultEffort(family)`.
219
- */
220
- export function adjacent(
221
- families: Family[],
222
- direct: Score[],
223
- shipped: ReadonlySet<string>,
224
- now: number,
225
- defaultEffort: (f: Family) => string = () => "high",
226
- ): Score[] {
227
- const out: Score[] = [];
228
- for (const f of families)
229
- for (const dim of DIMS) {
230
- const byEffort = new Map<string, Score>();
231
- for (const s of direct) {
232
- const { id, effort } = splitSourceRung(s.rung);
233
- if (id !== f.id || s.dim !== dim || effort === null) continue;
234
- const had = byEffort.get(effort);
235
- if (!had || outranks(s, had, now)) byEffort.set(effort, s);
236
- }
237
- if (byEffort.size === 0) continue;
238
- const efforts = familyEfforts(f);
239
- const bare = efforts.length > 0 && Object.values(f.on).some((o) => o && o.efforts.length === 0);
240
- for (const e of bare ? [...efforts, "default"] : efforts) {
241
- if (byEffort.has(e) || shipped.has(`${f.id}#${e}|${dim}`)) continue;
242
- const near = nearestEffort(e === "default" ? defaultEffort(f) : e, [...byEffort.keys()]);
243
- const from = near ? byEffort.get(near) : undefined;
244
- if (!near || !from) continue;
245
- out.push({
246
- ...from,
247
- rung: `${f.id}#${e}`,
248
- confidence: "adjacent",
249
- note: `${from.source ?? from.benchmark} has it at ${near}; carried to this effort`,
250
- });
251
- }
252
- }
253
- return out;
254
- }
255
-
256
- const differs = (a: number, b: number) => b !== 0 && Math.abs(a - b) / b > PRICE_TOLERANCE;
257
- const money = (n: number) => `$${n}/M`;
258
-
259
- /** Spec 1.2 §3.5: models.dev's facts per family, OpenRouter's speed facts, and the cross-check warnings. */
260
- function factsOf(
261
- raw: RawAnswers,
262
- ctx: DeriveContext,
263
- map: IdMapper,
264
- ): { facts: Record<string, FamilyFacts>; warnings: string[] } {
265
- const md = raw["models-dev"]?.data;
266
- const orModels = raw["openrouter-models"] ? parseOpenRouterModels(raw["openrouter-models"].data) : [];
267
- const liteLlm = raw.litellm ? parseLiteLlm(raw.litellm.data) : [];
268
- const endpoints = raw["openrouter-endpoints"];
269
- const speedRows = endpoints
270
- ? parseOpenRouterEndpoints(endpoints.data as Record<string, unknown>, endpoints.fetchedAt)
271
- : [];
272
- const aa = raw["artificial-analysis"];
273
- const aaRows = aa ? parseArtificialAnalysis(aa.data as AaAnswer, aa.fetchedAt) : [];
274
- const facts: Record<string, FamilyFacts> = {};
275
- const warnings: string[] = [];
276
- for (const f of ctx.models.families) {
277
- const x: FamilyFacts = { on: {}, speed: {} };
278
- const vendor = (f as { vendor?: unknown }).vendor;
279
- const own =
280
- md && typeof vendor === "string" ? modelsDevFacts(md, vendor, modelsDevId(md, vendor, f, map)) : null;
281
- if (own?.price) x.price = own.price;
282
- if (own && own.toolUse !== null && own.imageIn !== null && own.reasoning !== null)
283
- x.capabilities = { toolUse: own.toolUse, imageIn: own.imageIn, reasoning: own.reasoning };
284
- if (own?.releaseDate) x.releaseDate = own.releaseDate;
285
- for (const key of OPENCODE_KEYS) {
286
- const on = f.on[key];
287
- const got = md && on ? modelsDevFacts(md, key, on.id.slice(on.id.indexOf("/") + 1)) : null;
288
- if (got)
289
- x.on[key] = {
290
- ...(got.efforts ? { efforts: got.efforts } : {}),
291
- ...(got.context ? { context: got.context } : {}),
292
- };
293
- }
294
- for (const r of speedRows) if (map.family(r.rung) === f) x.speed[`openrouter.${r.field}`] = r.value;
295
- const defaultRung = `${map.key(f.id)}#${defaultEffortOf(ctx.sources, f)}`;
296
- for (const r of aaRows) {
297
- const { id, effort } = splitSourceRung(r.rung);
298
- if (map.family(id) !== f || `${map.key(f.id)}#${effort}` !== defaultRung) continue;
299
- if (/^median_|^cost_per_task$/.test(r.field)) x.speed[`artificial-analysis.${r.field}`] = r.value;
300
- }
301
- if (x.price || x.capabilities || x.releaseDate || Object.keys(x.on).length || Object.keys(x.speed).length)
302
- facts[f.id] = x;
303
- if (own?.price) warnings.push(...priceWarnings(f, own.price, orModels, liteLlm, map));
304
- if (own?.efforts) warnings.push(...effortWarnings(f, own.efforts, liteLlm, map));
305
- }
306
- return { facts, warnings };
307
- }
308
-
309
- /**
310
- * The id of `vendor`'s models.dev model that is `f`: its own id, else the first that maps to it (spec 1.3 §7.1:
311
- * `gemini-3-8-flash` is Google's `gemini-3.8-flash`, `gemini-3-1-pro` its aliased `gemini-3.1-pro-preview`).
312
- */
313
- function modelsDevId(md: unknown, vendor: string, f: Family, map: IdMapper): string {
314
- const ids = Object.keys((md as Record<string, { models?: object }>)[vendor]?.models ?? {});
315
- return ids.includes(f.id) ? f.id : (ids.find((id) => map.family(id) === f) ?? f.id);
316
- }
317
-
318
- /** The first entry of `list` whose id maps to `f` (OpenRouter's `:batch` variants never do). */
319
- const entryFor = <T extends { id: string }>(list: T[], f: Family, map: IdMapper): T | undefined =>
320
- list.find((m) => map.family(m.id) === f);
321
-
322
- function priceWarnings(
323
- f: Family,
324
- price: { input: number; output: number },
325
- orModels: OpenRouterModel[],
326
- liteLlm: ReturnType<typeof parseLiteLlm>,
327
- map: IdMapper,
328
- ): string[] {
329
- const out: string[] = [];
330
- for (const [name, other] of [
331
- ["OpenRouter", entryFor(orModels, f, map)?.price],
332
- ["LiteLLM", entryFor(liteLlm, f, map)?.price],
333
- ] as const) {
334
- if (!other) continue;
335
- for (const side of ["input", "output"] as const)
336
- if (differs(other[side], price[side]))
337
- out.push(
338
- `${f.id}: ${side} price ${money(price[side])} on models.dev, ${money(other[side])} on ${name} (more than 10 % apart)`,
339
- );
340
- }
341
- return out;
342
- }
343
-
344
- function effortWarnings(
345
- f: Family,
346
- efforts: string[],
347
- liteLlm: ReturnType<typeof parseLiteLlm>,
348
- map: IdMapper,
349
- ): string[] {
350
- const entry = entryFor(liteLlm, f, map);
351
- if (!entry) return [];
352
- const out: string[] = [];
353
- for (const e of LITELLM_EFFORTS) {
354
- const says = entry.efforts[e];
355
- if (says === undefined || says === efforts.includes(e)) continue;
356
- out.push(
357
- `${f.id}: LiteLLM says effort ${e} is ${says ? "" : "not "}supported; models.dev ${says ? "does not list" : "lists"} it`,
358
- );
359
- }
360
- return out;
361
- }
362
-
363
- /** Plan 13 R-H: the OpenRouter id of each catalog family OpenRouter lists, one each, for the endpoints fetch. */
364
- export function openRouterIds(
365
- orModelsData: unknown,
366
- ctx: Pick<DeriveContext, "models" | "sources">,
367
- ): string[] {
368
- const map = idMapper(ctx.models.families, ctx.sources.aliases);
369
- const models = parseOpenRouterModels(orModelsData);
370
- return ctx.models.families.flatMap((f) => {
371
- const m = entryFor(models, f, map);
372
- return m ? [m.id] : [];
373
- });
374
- }